mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
#30 WIP - broadcastify and icecast TCP streams working and editors are complete. Adds icons to editor and table views.
This commit is contained in:
BIN
images/icecast.png
Normal file
BIN
images/icecast.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
images/shoutcast.png
Normal file
BIN
images/shoutcast.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -19,7 +19,10 @@ package alias;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
@@ -176,7 +179,34 @@ public class Alias
|
||||
{
|
||||
return !mAliasActions.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Perform any validation/cleanup actions on this alias.
|
||||
*/
|
||||
public void validate()
|
||||
{
|
||||
//Check for duplicate broadcast channels
|
||||
List<BroadcastChannel> broadcastChannels = getBroadcastChannels();
|
||||
|
||||
if(broadcastChannels.size() > 0)
|
||||
{
|
||||
Set<String> channelNames = new TreeSet<>();
|
||||
|
||||
for(BroadcastChannel broadcastChannel: broadcastChannels)
|
||||
{
|
||||
String channelName = broadcastChannel.getChannelName();
|
||||
|
||||
if(channelName == null || channelName.isEmpty() || channelNames.contains(channelName))
|
||||
{
|
||||
removeAliasID(broadcastChannel);
|
||||
}
|
||||
else
|
||||
{
|
||||
channelNames.add(channelName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the priority level of this alias, if defined, or the default priority
|
||||
*/
|
||||
@@ -320,4 +350,26 @@ public class Alias
|
||||
|
||||
return broadcastChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this alias contains a broadcast channel alias id with the channel name.
|
||||
*/
|
||||
public boolean hasBroadcastChannel(String channel)
|
||||
{
|
||||
if(channel == null || channel.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(AliasID id: getId())
|
||||
{
|
||||
if(id.getType() == AliasIDType.BROADCAST_CHANNEL &&
|
||||
((BroadcastChannel)id).getChannelName().contentEquals(channel))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class AliasController extends JPanel
|
||||
mAliasModel = aliasModel;
|
||||
|
||||
mAliasEditor = new AliasEditor( mAliasModel, broadcastModel, settingsManager );
|
||||
mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, settingsManager );
|
||||
mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, broadcastModel, settingsManager );
|
||||
|
||||
mIconCellRenderer = new IconCellRenderer( settingsManager );
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
|
||||
import alias.id.broadcast.BroadcastChannel;
|
||||
import sample.Broadcaster;
|
||||
import sample.Listener;
|
||||
import alias.AliasEvent.Event;
|
||||
@@ -210,6 +211,33 @@ public class AliasModel extends AbstractTableModel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames any broadcast channels that have the previous name.
|
||||
* @param previousName to rename
|
||||
* @param newName to assign to the broadcast channel
|
||||
*/
|
||||
public void renameBroadcastChannel(String previousName, String newName)
|
||||
{
|
||||
if(previousName == null || previousName.isEmpty() || newName == null || newName.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for(Alias alias: mAliases)
|
||||
{
|
||||
if(alias.hasBroadcastChannel(previousName))
|
||||
{
|
||||
for(BroadcastChannel broadcastChannel: alias.getBroadcastChannels())
|
||||
{
|
||||
if(broadcastChannel.getChannelName().contentEquals(previousName))
|
||||
{
|
||||
broadcastChannel.setChannelName(newName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount()
|
||||
{
|
||||
@@ -289,6 +317,14 @@ public class AliasModel extends AbstractTableModel
|
||||
|
||||
public void broadcast( AliasEvent event )
|
||||
{
|
||||
Alias alias = event.getAlias();
|
||||
|
||||
//Validate the alias following a user action that changed the alias or any alias IDs
|
||||
if(alias != null)
|
||||
{
|
||||
alias.validate();
|
||||
}
|
||||
|
||||
if( event.getEvent() == Event.CHANGE )
|
||||
{
|
||||
int index = mAliases.indexOf( event.getAlias() );
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package alias;
|
||||
|
||||
import alias.id.AliasIDType;
|
||||
import alias.id.broadcast.BroadcastChannel;
|
||||
import audio.broadcast.BroadcastModel;
|
||||
import gui.editor.Editor;
|
||||
|
||||
import java.awt.Color;
|
||||
@@ -7,6 +10,7 @@ import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.DefaultComboBoxModel;
|
||||
@@ -33,385 +37,429 @@ import settings.SettingsManager;
|
||||
import alias.AliasEvent.Event;
|
||||
import alias.id.priority.Priority;
|
||||
|
||||
public class MultipleAliasEditor extends Editor<List<Alias>>
|
||||
implements Listener<AliasEvent>
|
||||
public class MultipleAliasEditor extends Editor<List<Alias>>
|
||||
implements Listener<AliasEvent>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final static Logger mLog = LoggerFactory.getLogger( MultipleAliasEditor.class );
|
||||
|
||||
private static String HELP_TEXT =
|
||||
"<html>Select attributes below to change for all selected aliases</html>";
|
||||
private static final long serialVersionUID = 1L;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MultipleAliasEditor.class);
|
||||
|
||||
private JLabel mAliasCountLabel;
|
||||
private JLabel mAliasCount;
|
||||
private static String HELP_TEXT =
|
||||
"<html>Select attributes below to change for all selected aliases</html>";
|
||||
|
||||
private JCheckBox mListCheckBox = new JCheckBox( "List" );
|
||||
private JComboBox<String> mListCombo = new JComboBox<>();
|
||||
private JCheckBox mGroupCheckBox = new JCheckBox( "Group" );
|
||||
private JComboBox<String> mGroupCombo = new JComboBox<>();
|
||||
private JCheckBox mIconCheckBox = new JCheckBox( "Icon" );
|
||||
private JLabel mAliasCountLabel;
|
||||
private JLabel mAliasCount;
|
||||
|
||||
private JCheckBox mListCheckBox = new JCheckBox("List");
|
||||
private JComboBox<String> mListCombo = new JComboBox<>();
|
||||
private JCheckBox mGroupCheckBox = new JCheckBox("Group");
|
||||
private JComboBox<String> mGroupCombo = new JComboBox<>();
|
||||
private JCheckBox mIconCheckBox = new JCheckBox("Icon");
|
||||
private JComboBox<MapIcon> mMapIconCombo;
|
||||
private JCheckBox mColorCheckBox = new JCheckBox( "Color" );
|
||||
private JCheckBox mColorCheckBox = new JCheckBox("Color");
|
||||
private JButton mButtonColor;
|
||||
private JButton mBtnIconManager;
|
||||
private JCheckBox mRecordCheckBox = new JCheckBox( "Record" );
|
||||
private JComboBox<Record> mRecordActionCombo = new JComboBox<>( Record.values() );
|
||||
private JCheckBox mPriorityCheckBox = new JCheckBox( "Priority" );
|
||||
private JCheckBox mRecordCheckBox = new JCheckBox("Record");
|
||||
private JComboBox<Record> mRecordActionCombo = new JComboBox<>(Record.values());
|
||||
private JCheckBox mPriorityCheckBox = new JCheckBox("Priority");
|
||||
private JSlider mPrioritySlider;
|
||||
private JLabel mPrioritySliderLabel;
|
||||
|
||||
private AliasModel mAliasModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
|
||||
public MultipleAliasEditor( AliasModel aliasModel, SettingsManager settingsManager )
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
mAliasModel.addListener( this );
|
||||
|
||||
mSettingsManager = settingsManager;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
if( hasItem() )
|
||||
{
|
||||
return (List<Alias>)getItem();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void init()
|
||||
{
|
||||
setLayout( new MigLayout( "fill,wrap 2", "[grow,fill][grow,fill]",
|
||||
"[][][][][][][][][][][][][grow,fill]" ) );
|
||||
|
||||
mAliasCountLabel = new JLabel( "Alias:" );
|
||||
add( mAliasCountLabel );
|
||||
|
||||
mAliasCount = new JLabel( "Multiple" );
|
||||
add( mAliasCount, "span" );
|
||||
|
||||
add( new JSeparator(), "span" );
|
||||
|
||||
add( new JLabel( HELP_TEXT ), "span" );
|
||||
|
||||
add( mListCheckBox );
|
||||
private JCheckBox mStreamCheckBox = new JCheckBox("Stream");
|
||||
private JComboBox<String> mStreamCombo;
|
||||
|
||||
mListCombo.setEditable( true );
|
||||
add( mListCombo, "wrap" );
|
||||
private AliasModel mAliasModel;
|
||||
private BroadcastModel mBroadcastModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
|
||||
add( mGroupCheckBox );
|
||||
public MultipleAliasEditor(AliasModel aliasModel, BroadcastModel broadcastModel, SettingsManager settingsManager)
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
mBroadcastModel = broadcastModel;
|
||||
mSettingsManager = settingsManager;
|
||||
|
||||
mGroupCombo.setEditable( true );
|
||||
add( mGroupCombo, "wrap" );
|
||||
mAliasModel.addListener(this);
|
||||
|
||||
add( mColorCheckBox );
|
||||
init();
|
||||
}
|
||||
|
||||
mButtonColor = new JButton( "Select ..." );
|
||||
mButtonColor.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
public List<Alias> getAliases()
|
||||
{
|
||||
if (hasItem())
|
||||
{
|
||||
return (List<Alias>) getItem();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void init()
|
||||
{
|
||||
setLayout(new MigLayout("fill,wrap 2", "[grow,fill][grow,fill]",
|
||||
"[][][][][][][][][][][][][][grow,fill]"));
|
||||
|
||||
mAliasCountLabel = new JLabel("Alias:");
|
||||
add(mAliasCountLabel);
|
||||
|
||||
mAliasCount = new JLabel("Multiple");
|
||||
add(mAliasCount, "span");
|
||||
|
||||
add(new JSeparator(), "span");
|
||||
|
||||
add(new JLabel(HELP_TEXT), "span");
|
||||
|
||||
add(mListCheckBox);
|
||||
|
||||
mListCombo.setEditable(true);
|
||||
add(mListCombo, "wrap");
|
||||
|
||||
add(mGroupCheckBox);
|
||||
|
||||
mGroupCombo.setEditable(true);
|
||||
add(mGroupCombo, "wrap");
|
||||
|
||||
add(mColorCheckBox);
|
||||
|
||||
mButtonColor = new JButton("Select ...");
|
||||
mButtonColor.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
Color newColor = JColorChooser.showDialog(
|
||||
MultipleAliasEditor.this,
|
||||
"Choose color for this alias", null );
|
||||
Color newColor = JColorChooser.showDialog(
|
||||
MultipleAliasEditor.this,
|
||||
"Choose color for this alias", null);
|
||||
|
||||
if( newColor != null )
|
||||
{
|
||||
mButtonColor.setForeground( newColor );
|
||||
mButtonColor.setBackground( newColor );
|
||||
}
|
||||
if (newColor != null)
|
||||
{
|
||||
mButtonColor.setForeground(newColor);
|
||||
mButtonColor.setBackground(newColor);
|
||||
}
|
||||
}
|
||||
} );
|
||||
add( mButtonColor, "wrap" );
|
||||
|
||||
add( mIconCheckBox );
|
||||
|
||||
mMapIconCombo = new JComboBox<MapIcon>( mSettingsManager.getMapIcons() );
|
||||
});
|
||||
add(mButtonColor, "wrap");
|
||||
|
||||
MapIconListCellRenderer renderer = new MapIconListCellRenderer();
|
||||
renderer.setPreferredSize( new Dimension( 200, 30 ) );
|
||||
mMapIconCombo.setRenderer( renderer );
|
||||
add( mMapIconCombo, "wrap" );
|
||||
add(mIconCheckBox);
|
||||
|
||||
//Dummy place holder
|
||||
add( new JLabel() );
|
||||
|
||||
mBtnIconManager = new JButton( "Icon Manager" );
|
||||
mBtnIconManager.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent arg0 )
|
||||
mMapIconCombo = new JComboBox<MapIcon>(mSettingsManager.getMapIcons());
|
||||
|
||||
MapIconListCellRenderer renderer = new MapIconListCellRenderer();
|
||||
renderer.setPreferredSize(new Dimension(200, 30));
|
||||
mMapIconCombo.setRenderer(renderer);
|
||||
add(mMapIconCombo, "wrap");
|
||||
|
||||
//Dummy place holder
|
||||
add(new JLabel());
|
||||
|
||||
mBtnIconManager = new JButton("Icon Manager");
|
||||
mBtnIconManager.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent arg0)
|
||||
{
|
||||
final IconManager iconManager =
|
||||
new IconManager( mSettingsManager, MultipleAliasEditor.this );
|
||||
|
||||
EventQueue.invokeLater( new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
iconManager.setVisible( true );
|
||||
}
|
||||
} );
|
||||
final IconManager iconManager =
|
||||
new IconManager(mSettingsManager, MultipleAliasEditor.this);
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
iconManager.setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
} );
|
||||
|
||||
add( mBtnIconManager, "span,wrap" );
|
||||
|
||||
add( mRecordCheckBox );
|
||||
add( mRecordActionCombo );
|
||||
});
|
||||
|
||||
//Placeholder
|
||||
add( new JLabel( " " ) );
|
||||
mPrioritySliderLabel = new JLabel( "Priority: " + Priority.MIN_PRIORITY );
|
||||
add( mPrioritySliderLabel );
|
||||
|
||||
add( mPriorityCheckBox );
|
||||
|
||||
mPrioritySlider = new JSlider( JSlider.HORIZONTAL,
|
||||
Priority.MIN_PRIORITY,
|
||||
Priority.MAX_PRIORITY + 1,
|
||||
Priority.MIN_PRIORITY );
|
||||
add(mBtnIconManager, "span,wrap");
|
||||
|
||||
mPrioritySlider.setMajorTickSpacing( 20 );
|
||||
mPrioritySlider.setMinorTickSpacing( 5 );
|
||||
mPrioritySlider.setPaintTicks( true );
|
||||
mPrioritySlider.setLabelTable( mPrioritySlider.createStandardLabels( 25, 25 ) );
|
||||
mPrioritySlider.setPaintLabels( true );
|
||||
mPrioritySlider.addChangeListener( new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged( ChangeEvent e )
|
||||
{
|
||||
int priority = mPrioritySlider.getValue();
|
||||
add(mRecordCheckBox);
|
||||
add(mRecordActionCombo);
|
||||
|
||||
if( priority > Priority.MAX_PRIORITY )
|
||||
{
|
||||
mPrioritySliderLabel.setText( "Priority: Do Not Monitor" );
|
||||
}
|
||||
else
|
||||
{
|
||||
mPrioritySliderLabel.setText( "Priority: " + mPrioritySlider.getValue() );
|
||||
}
|
||||
}
|
||||
} );
|
||||
mPrioritySlider.setToolTipText( HELP_TEXT );
|
||||
|
||||
add( mPrioritySlider );
|
||||
|
||||
JButton saveButton = new JButton( "Save" );
|
||||
saveButton.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
save();
|
||||
}
|
||||
} );
|
||||
add( saveButton );
|
||||
//Placeholder
|
||||
add(new JLabel(" "));
|
||||
mPrioritySliderLabel = new JLabel("Audio Priority: " + Priority.MIN_PRIORITY);
|
||||
add(mPrioritySliderLabel);
|
||||
|
||||
JButton resetButton = new JButton( "Reset" );
|
||||
resetButton.addActionListener( new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed( ActionEvent e )
|
||||
{
|
||||
reset();
|
||||
}
|
||||
} );
|
||||
add( resetButton );
|
||||
add(mPriorityCheckBox);
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem( List<Alias> item )
|
||||
{
|
||||
super.setItem( item );
|
||||
mPrioritySlider = new JSlider(JSlider.HORIZONTAL,
|
||||
Priority.MIN_PRIORITY,
|
||||
Priority.MAX_PRIORITY + 1,
|
||||
Priority.MIN_PRIORITY);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append( "Multiple Selected [ " );
|
||||
|
||||
if( hasItem() )
|
||||
{
|
||||
sb.append( String.valueOf( getAliases().size() ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append( "0" );
|
||||
}
|
||||
mPrioritySlider.setMajorTickSpacing(20);
|
||||
mPrioritySlider.setMinorTickSpacing(5);
|
||||
mPrioritySlider.setPaintTicks(true);
|
||||
mPrioritySlider.setLabelTable(mPrioritySlider.createStandardLabels(25, 25));
|
||||
mPrioritySlider.setPaintLabels(true);
|
||||
mPrioritySlider.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
int priority = mPrioritySlider.getValue();
|
||||
|
||||
sb.append( " ]" );
|
||||
|
||||
mAliasCount.setText( sb.toString() );
|
||||
if (priority > Priority.MAX_PRIORITY)
|
||||
{
|
||||
mPrioritySliderLabel.setText("Audio Priority: Do Not Monitor");
|
||||
}
|
||||
else
|
||||
{
|
||||
mPrioritySliderLabel.setText("Audio Priority: " + mPrioritySlider.getValue());
|
||||
}
|
||||
}
|
||||
});
|
||||
mPrioritySlider.setToolTipText(HELP_TEXT);
|
||||
|
||||
List<String> listNames = mAliasModel.getListNames();
|
||||
listNames.add( 0, "" );
|
||||
mListCombo.setModel( new DefaultComboBoxModel<String>(
|
||||
listNames.toArray( new String[ listNames.size() ] ) ) );
|
||||
|
||||
List<String> groupNames = mAliasModel.getGroupNames();
|
||||
groupNames.add( 0, "" );
|
||||
mGroupCombo.setModel( new DefaultComboBoxModel<String>(
|
||||
groupNames.toArray( new String[ groupNames.size() ] ) ) );
|
||||
add(mPrioritySlider);
|
||||
|
||||
setCheckBoxesUnselected();
|
||||
|
||||
setModified( false );
|
||||
}
|
||||
|
||||
private void setCheckBoxesUnselected()
|
||||
{
|
||||
mListCheckBox.setSelected( false );
|
||||
mGroupCheckBox.setSelected( false );
|
||||
mColorCheckBox.setSelected( false );
|
||||
mIconCheckBox.setSelected( false );
|
||||
mRecordCheckBox.setSelected( false );
|
||||
mPriorityCheckBox.setSelected( false );
|
||||
}
|
||||
add(mStreamCheckBox);
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
List<String> listNames = mAliasModel.getListNames();
|
||||
listNames.add( 0, "" );
|
||||
mListCombo.setModel( new DefaultComboBoxModel<String>(
|
||||
listNames.toArray( new String[ listNames.size() ] ) ) );
|
||||
|
||||
List<String> groupNames = mAliasModel.getGroupNames();
|
||||
groupNames.add( 0, "" );
|
||||
mGroupCombo.setModel( new DefaultComboBoxModel<String>(
|
||||
groupNames.toArray( new String[ groupNames.size() ] ) ) );
|
||||
|
||||
mButtonColor.setForeground( getForeground() );
|
||||
mButtonColor.setBackground( getBackground() );
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
private boolean hasRequestedChanges()
|
||||
{
|
||||
return mListCheckBox.isSelected() ||
|
||||
mGroupCheckBox.isSelected() ||
|
||||
mColorCheckBox.isSelected() ||
|
||||
mIconCheckBox.isSelected() ||
|
||||
mRecordCheckBox.isSelected() ||
|
||||
mPriorityCheckBox.isSelected();
|
||||
}
|
||||
mStreamCombo = new JComboBox<String>();
|
||||
mStreamCombo.setEditable(true);
|
||||
add(mStreamCombo, "wrap");
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if( hasRequestedChanges() && hasItem() )
|
||||
{
|
||||
List<Alias> aliases = getAliases();
|
||||
|
||||
for( Alias alias: aliases )
|
||||
{
|
||||
if( mListCheckBox.isSelected() )
|
||||
{
|
||||
String list = null;
|
||||
|
||||
if( mListCombo.getSelectedItem() != null )
|
||||
{
|
||||
list = (String)mListCombo.getSelectedItem();
|
||||
}
|
||||
|
||||
alias.setList( list );
|
||||
}
|
||||
|
||||
if( mGroupCheckBox.isSelected() )
|
||||
{
|
||||
String group = null;
|
||||
|
||||
if( mGroupCombo.getSelectedItem() != null )
|
||||
{
|
||||
group = (String)mGroupCombo.getSelectedItem();
|
||||
}
|
||||
|
||||
alias.setGroup( group );
|
||||
}
|
||||
|
||||
if( mColorCheckBox.isSelected() )
|
||||
{
|
||||
alias.setColor( mButtonColor.getForeground().getRGB() );
|
||||
}
|
||||
|
||||
if( mIconCheckBox.isSelected() )
|
||||
{
|
||||
if( mMapIconCombo.getSelectedItem() != null )
|
||||
{
|
||||
alias.setIconName( ((MapIcon)mMapIconCombo.getSelectedItem()).getName() );
|
||||
}
|
||||
else
|
||||
{
|
||||
alias.setIconName( null );
|
||||
}
|
||||
}
|
||||
|
||||
if( mRecordCheckBox.isSelected() )
|
||||
{
|
||||
Record action =
|
||||
(Record)mRecordActionCombo.getSelectedItem();
|
||||
JButton saveButton = new JButton("Save");
|
||||
saveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
save();
|
||||
}
|
||||
});
|
||||
add(saveButton);
|
||||
|
||||
alias.setRecordable( action == Record.RECORDABLE );
|
||||
}
|
||||
|
||||
if( mPriorityCheckBox.isSelected() )
|
||||
{
|
||||
int priority = mPrioritySlider.getValue();
|
||||
JButton resetButton = new JButton("Reset");
|
||||
resetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
});
|
||||
add(resetButton);
|
||||
|
||||
//This is a work-around -- we use max priority + 1 in the
|
||||
//gui to indicate do not monitor, and change the value here
|
||||
if( priority == Priority.MAX_PRIORITY + 1 )
|
||||
{
|
||||
priority = Priority.DO_NOT_MONITOR;
|
||||
}
|
||||
|
||||
alias.setCallPriority( priority );
|
||||
}
|
||||
}
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
for( Alias alias: aliases )
|
||||
{
|
||||
//Broadcast an alias change event to save the updates
|
||||
mAliasModel.broadcast( new AliasEvent( alias, Event.CHANGE ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void setItem(List<Alias> item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
@Override
|
||||
public void receive( AliasEvent event )
|
||||
{
|
||||
if( event.getEvent() == Event.DELETE &&
|
||||
hasItem() &&
|
||||
getAliases().contains( event.getAlias() ) )
|
||||
{
|
||||
getAliases().remove( event.getAlias() );
|
||||
setItem( getAliases() );
|
||||
}
|
||||
}
|
||||
|
||||
public enum Record
|
||||
{
|
||||
NON_RECORDABLE( "Non-Recordable" ),
|
||||
RECORDABLE( "Recordable" );
|
||||
|
||||
private String mLabel;
|
||||
|
||||
private Record( String label )
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Multiple Selected [ ");
|
||||
|
||||
if (hasItem())
|
||||
{
|
||||
sb.append(String.valueOf(getAliases().size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("0");
|
||||
}
|
||||
|
||||
sb.append(" ]");
|
||||
|
||||
mAliasCount.setText(sb.toString());
|
||||
|
||||
List<String> listNames = mAliasModel.getListNames();
|
||||
listNames.add(0, "");
|
||||
mListCombo.setModel(new DefaultComboBoxModel<String>(
|
||||
listNames.toArray(new String[listNames.size()])));
|
||||
|
||||
List<String> groupNames = mAliasModel.getGroupNames();
|
||||
groupNames.add(0, "");
|
||||
mGroupCombo.setModel(new DefaultComboBoxModel<String>(
|
||||
groupNames.toArray(new String[groupNames.size()])));
|
||||
|
||||
List<String> streamNames = mBroadcastModel.getBroadcastConfigurationNames();
|
||||
mStreamCombo.setModel(new DefaultComboBoxModel<String>(streamNames.toArray(new String[streamNames.size()])));
|
||||
|
||||
setCheckBoxesUnselected();
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
private void setCheckBoxesUnselected()
|
||||
{
|
||||
mListCheckBox.setSelected(false);
|
||||
mGroupCheckBox.setSelected(false);
|
||||
mColorCheckBox.setSelected(false);
|
||||
mIconCheckBox.setSelected(false);
|
||||
mRecordCheckBox.setSelected(false);
|
||||
mPriorityCheckBox.setSelected(false);
|
||||
mStreamCheckBox.setSelected(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
List<String> listNames = mAliasModel.getListNames();
|
||||
listNames.add(0, "");
|
||||
mListCombo.setModel(new DefaultComboBoxModel<String>(
|
||||
listNames.toArray(new String[listNames.size()])));
|
||||
|
||||
List<String> groupNames = mAliasModel.getGroupNames();
|
||||
groupNames.add(0, "");
|
||||
mGroupCombo.setModel(new DefaultComboBoxModel<String>(
|
||||
groupNames.toArray(new String[groupNames.size()])));
|
||||
|
||||
mButtonColor.setForeground(getForeground());
|
||||
mButtonColor.setBackground(getBackground());
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
private boolean hasRequestedChanges()
|
||||
{
|
||||
return mListCheckBox.isSelected() ||
|
||||
mGroupCheckBox.isSelected() ||
|
||||
mColorCheckBox.isSelected() ||
|
||||
mIconCheckBox.isSelected() ||
|
||||
mRecordCheckBox.isSelected() ||
|
||||
mPriorityCheckBox.isSelected() ||
|
||||
mStreamCheckBox.isSelected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if (hasRequestedChanges() && hasItem())
|
||||
{
|
||||
List<Alias> aliases = getAliases();
|
||||
|
||||
for (Alias alias : aliases)
|
||||
{
|
||||
if (mListCheckBox.isSelected())
|
||||
{
|
||||
String list = null;
|
||||
|
||||
if (mListCombo.getSelectedItem() != null)
|
||||
{
|
||||
list = (String) mListCombo.getSelectedItem();
|
||||
}
|
||||
|
||||
alias.setList(list);
|
||||
}
|
||||
|
||||
if (mGroupCheckBox.isSelected())
|
||||
{
|
||||
String group = null;
|
||||
|
||||
if (mGroupCombo.getSelectedItem() != null)
|
||||
{
|
||||
group = (String) mGroupCombo.getSelectedItem();
|
||||
}
|
||||
|
||||
alias.setGroup(group);
|
||||
}
|
||||
|
||||
if (mColorCheckBox.isSelected())
|
||||
{
|
||||
alias.setColor(mButtonColor.getForeground().getRGB());
|
||||
}
|
||||
|
||||
if (mIconCheckBox.isSelected())
|
||||
{
|
||||
if (mMapIconCombo.getSelectedItem() != null)
|
||||
{
|
||||
alias.setIconName(((MapIcon) mMapIconCombo.getSelectedItem()).getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
alias.setIconName(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (mRecordCheckBox.isSelected())
|
||||
{
|
||||
Record action =
|
||||
(Record) mRecordActionCombo.getSelectedItem();
|
||||
|
||||
alias.setRecordable(action == Record.RECORDABLE);
|
||||
}
|
||||
|
||||
if (mPriorityCheckBox.isSelected())
|
||||
{
|
||||
int priority = mPrioritySlider.getValue();
|
||||
|
||||
//This is a work-around -- we use max priority + 1 in the
|
||||
//gui to indicate do not monitor, and change the value here
|
||||
if (priority == Priority.MAX_PRIORITY + 1)
|
||||
{
|
||||
priority = Priority.DO_NOT_MONITOR;
|
||||
}
|
||||
|
||||
alias.setCallPriority(priority);
|
||||
}
|
||||
|
||||
if(mStreamCheckBox.isSelected())
|
||||
{
|
||||
if(mStreamCombo.getSelectedItem() != null)
|
||||
{
|
||||
String streamName = (String)mStreamCombo.getSelectedItem();
|
||||
|
||||
boolean hasChannel = false;
|
||||
|
||||
//Ensure the alias doesn't already have the channel
|
||||
for(BroadcastChannel channel: alias.getBroadcastChannels())
|
||||
{
|
||||
if(channel.getChannelName().equals(streamName))
|
||||
{
|
||||
hasChannel = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasChannel)
|
||||
{
|
||||
BroadcastChannel channel = (BroadcastChannel)AliasFactory
|
||||
.getAliasID(AliasIDType.BROADCAST_CHANNEL);
|
||||
|
||||
channel.setChannelName(streamName);
|
||||
alias.addAliasID(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Alias alias : aliases)
|
||||
{
|
||||
//Broadcast an alias change event to save the updates
|
||||
mAliasModel.broadcast(new AliasEvent(alias, Event.CHANGE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AliasEvent event)
|
||||
{
|
||||
if (event.getEvent() == Event.DELETE &&
|
||||
hasItem() &&
|
||||
getAliases().contains(event.getAlias()))
|
||||
{
|
||||
getAliases().remove(event.getAlias());
|
||||
setItem(getAliases());
|
||||
}
|
||||
}
|
||||
|
||||
public enum Record
|
||||
{
|
||||
NON_RECORDABLE("Non-Recordable"),
|
||||
RECORDABLE("Recordable");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
private Record(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,56 +30,113 @@ import sample.Listener;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract class AudioBroadcaster implements IAudioPacketListener, Listener<AudioPacket>
|
||||
public abstract class AudioBroadcaster implements IAudioPacketListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( AudioBroadcaster.class );
|
||||
|
||||
public static final int PROCESSOR_RUN_INTERVAL_MS = 1000;
|
||||
|
||||
private ThreadPoolManager mThreadPoolManager;
|
||||
private ScheduledFuture mScheduledTask;
|
||||
|
||||
private StreamManager mStreamManager;
|
||||
private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
|
||||
private ArrayBlockingQueue<StreamableAudioRecording> mAudioRecordingQueue;
|
||||
private LinkedTransferQueue<StreamableAudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||
|
||||
private byte[] mSilenceFrame;
|
||||
private int mStreamedAudioCount = 0;
|
||||
|
||||
private Listener<BroadcastEvent> mBroadcastEventListener;
|
||||
private BroadcastState mBroadcastState = BroadcastState.READY;
|
||||
|
||||
private StreamManager mStreamManager;
|
||||
private int mStreamedAudioCount = 0;
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
|
||||
private boolean mEnabled = true;
|
||||
private long mDelay;
|
||||
private AtomicBoolean mStreaming = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* AudioBroadcaster for sending audio to a remote broadcastAudio server.
|
||||
* AudioBroadcaster for streaming audio recordings to a remote streaming audio server. Audio recordings are
|
||||
* generated by an internal StreamManager that converts an inbound stream of AudioPackets into a recording of the
|
||||
* desired audio format (e.g. MP3) and nominates the recording to an internal recording queue for streaming. The
|
||||
* broadcaster supports receiving audio packets from multiple audio sources. Each audio packet's internal audio
|
||||
* metadata source string is used to reassemble each packet stream. Recordings are capped at 30 seconds length.
|
||||
* If a source audio packet stream exceeds 30 seconds in length, it will be chunked into 30 second recordings.
|
||||
*
|
||||
* This broadcaster supports a time delay setting for delaying broadcast of audio recordings. The delay setting is
|
||||
* defined in the broadcast configuration. When this delay is greater than zero, the recording will remain in the
|
||||
* audio broadcaster queue until the recording start time + delay elapses. Audio recordings are processed in a FIFO
|
||||
* manner.
|
||||
*
|
||||
* Use the start() and stop() methods to connect to/disconnect from the remote server. Audio recordings will be
|
||||
* streamed to the remote server when available. One second silence frames will be broadcast to the server when
|
||||
* there are no recordings available, in order to maintain a connection with the remote server. Any audio packet
|
||||
* streams received while the broadcaster is stopped will be ignored.
|
||||
*
|
||||
* The last audio packet's metadata is automatically attached to the closed audio recording when it is enqueued for
|
||||
* broadcast. That metadata will be updated on the remote server once the audio recording is opened for streaming.
|
||||
*/
|
||||
public AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
mThreadPoolManager = threadPoolManager;
|
||||
mBroadcastConfiguration = broadcastConfiguration;
|
||||
|
||||
//Recording queue - bounded at 10 queued recordings maximum - new stream recordings will be deleted once full
|
||||
mAudioRecordingQueue = new ArrayBlockingQueue<>(10);
|
||||
mDelay = mBroadcastConfiguration.getDelay();
|
||||
|
||||
//Create a 1 second silence frame - from 1200 millis of silence
|
||||
mSilenceFrame = BroadcastFactory.getSilenceFrame(getBroadcastConfiguration().getBroadcastFormat(), 1200);
|
||||
|
||||
mStreamManager = new StreamManager(threadPoolManager, this, broadcastConfiguration.getDelay(),
|
||||
mStreamManager = new StreamManager(threadPoolManager, this,
|
||||
SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY));
|
||||
|
||||
mStreamManager.start();
|
||||
|
||||
startProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording backlog awaiting streaming
|
||||
* Connects to the remote server specified by the broadcast configuration and starts audio streaming.
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mStreaming.compareAndSet(false, true))
|
||||
{
|
||||
mStreamManager.start();
|
||||
|
||||
if(mScheduledTask == null)
|
||||
{
|
||||
if(mThreadPoolManager != null)
|
||||
{
|
||||
mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
|
||||
mRecordingQueueProcessor, PROCESSOR_RUN_INTERVAL_MS, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the remote server.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mStreaming.compareAndSet(true, false))
|
||||
{
|
||||
mStreamManager.stop();
|
||||
|
||||
if(mThreadPoolManager != null && mScheduledTask != null)
|
||||
{
|
||||
mThreadPoolManager.cancel(mScheduledTask);
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the broadcaster from the remote server for a reset or final stop.
|
||||
*/
|
||||
protected abstract void disconnect();
|
||||
|
||||
/**
|
||||
* Size of recording queue for recordings awaiting streaming
|
||||
*/
|
||||
public int getQueueSize()
|
||||
{
|
||||
@@ -95,51 +152,18 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues the streamable audio recording for broadcastAudio. The bounded queue is managed by removing older recordings
|
||||
* at the head of the queue, once full, to make room for new recordings. This ensures that once the broadcaster
|
||||
* (re)starts broadcasting or catches up with the queue, that it is broadcasting the most current audio recordings.
|
||||
* Primary insert method for the stream manager to nominate completed audio recordings for broadcast.
|
||||
*
|
||||
* @param recording to queue for broadcasting
|
||||
*/
|
||||
public void receive(StreamableAudioRecording recording)
|
||||
{
|
||||
boolean success = false;
|
||||
|
||||
try
|
||||
{
|
||||
success = mAudioRecordingQueue.add(recording);
|
||||
}
|
||||
catch(IllegalStateException ise)
|
||||
{
|
||||
//The queue is full - delete the oldest recording at the head of the queue to make room for a new one
|
||||
removeRecording(mAudioRecordingQueue.poll());
|
||||
}
|
||||
|
||||
if(!success)
|
||||
{
|
||||
try
|
||||
{
|
||||
success = mAudioRecordingQueue.add(recording);
|
||||
}
|
||||
catch(IllegalStateException ise)
|
||||
{
|
||||
//Do nothing on second attempt
|
||||
}
|
||||
}
|
||||
|
||||
//If we can't queue the recording after two attempts, remove the recording
|
||||
if(!success)
|
||||
{
|
||||
removeRecording(recording);
|
||||
}
|
||||
else
|
||||
{
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
mAudioRecordingQueue.add(recording);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the temporary recording file from disk.
|
||||
* Cleanup method to remove a temporary recording file from disk.
|
||||
*
|
||||
* @param recording to remove
|
||||
*/
|
||||
@@ -164,16 +188,16 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
}
|
||||
|
||||
/**
|
||||
* IAudioPacketListener interface method
|
||||
* IAudioPacketListener interface method to gain access to the internal stream manager for enqueuing audio packets.
|
||||
*/
|
||||
@Override
|
||||
public Listener<AudioPacket> getAudioPacketListener()
|
||||
{
|
||||
return this;
|
||||
return mStreamManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the audio binary data.
|
||||
* Broadcast binary audio data frames or sequences.
|
||||
*/
|
||||
protected abstract void broadcastAudio(byte[] audio);
|
||||
|
||||
@@ -183,73 +207,6 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
*/
|
||||
protected abstract void broadcastMetadata(AudioMetadata metadata);
|
||||
|
||||
/**
|
||||
* Disconnects the broadcaster and halts all broadcasting.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
mEnabled = false;
|
||||
mStreamManager.stop();
|
||||
stopProcessor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary audio packet input method. Audio packets are queued and then processed by the underlying broadcastAudio
|
||||
* handler for broadcastAudio to the remote server.
|
||||
*/
|
||||
public void receive(AudioPacket packet)
|
||||
{
|
||||
if(mEnabled)
|
||||
{
|
||||
mStreamManager.receive(packet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the audio queue processor. The audio packet queue is serviced every 100 milliseconds. Subsequent calls
|
||||
* to this method once running are ignored.
|
||||
*/
|
||||
private void startProcessor()
|
||||
{
|
||||
if(mEnabled && mScheduledTask == null)
|
||||
{
|
||||
if(mThreadPoolManager != null)
|
||||
{
|
||||
mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
|
||||
mRecordingQueueProcessor, PROCESSOR_RUN_INTERVAL_MS, TimeUnit.MILLISECONDS );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the audio queue processor
|
||||
*/
|
||||
private void stopProcessor()
|
||||
{
|
||||
if(mThreadPoolManager != null && mScheduledTask != null)
|
||||
{
|
||||
mThreadPoolManager.cancel(mScheduledTask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses or unpauses the broadcastAudio
|
||||
*/
|
||||
public void setPaused(boolean paused)
|
||||
{
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
if(paused)
|
||||
{
|
||||
setBroadcastState(BroadcastState.PAUSED);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.READY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive broadcastAudio state changes
|
||||
*/
|
||||
@@ -288,6 +245,11 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
mBroadcastState = state;
|
||||
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
||||
|
||||
if(mBroadcastState.isErrorState())
|
||||
{
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +261,10 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
public boolean connected()
|
||||
/**
|
||||
* Indicates if the broadcaster is currently connected to the remote server
|
||||
*/
|
||||
protected boolean connected()
|
||||
{
|
||||
return getBroadcastState() == BroadcastState.CONNECTED;
|
||||
}
|
||||
@@ -314,6 +279,15 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
return state != BroadcastState.CONNECTED && !state.isErrorState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current broadcast state is an error state, meaning that it cannot recover or connect using the
|
||||
* current configuration.
|
||||
*/
|
||||
protected boolean isErrorState()
|
||||
{
|
||||
return getBroadcastState().isErrorState();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Audio recording queue processor. Fetches recordings from the queue and chunks the recording byte content
|
||||
@@ -366,19 +340,24 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
*/
|
||||
private void nextRecording()
|
||||
{
|
||||
boolean metadataUpdateRequired = false;
|
||||
|
||||
if(mInputStream != null)
|
||||
{
|
||||
mStreamedAudioCount++;
|
||||
broadcast(new BroadcastEvent(AudioBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||
metadataUpdateRequired = true;
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
|
||||
StreamableAudioRecording recording = mAudioRecordingQueue.poll();
|
||||
StreamableAudioRecording nextRecording = mAudioRecordingQueue.peek();
|
||||
|
||||
if(recording != null)
|
||||
if(nextRecording != null && nextRecording.getStartTime() + mDelay <= System.currentTimeMillis())
|
||||
{
|
||||
StreamableAudioRecording recording = mAudioRecordingQueue.poll();
|
||||
|
||||
try
|
||||
{
|
||||
byte[] audio = Files.readAllBytes(recording.getPath());
|
||||
@@ -398,10 +377,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
mChunkSize = (int)(mInputStream.available() / wholeIntervalChunks) + 1;
|
||||
|
||||
broadcastMetadata(recording.getAudioMetadata());
|
||||
}
|
||||
else
|
||||
{
|
||||
broadcastMetadata(null);
|
||||
metadataUpdateRequired = false;
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
@@ -416,7 +392,9 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
|
||||
|
||||
broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
else
|
||||
|
||||
//If we closed out a recording and don't have anything new, send an empty metadata update
|
||||
if(metadataUpdateRequired)
|
||||
{
|
||||
broadcastMetadata(null);
|
||||
}
|
||||
|
||||
128
src/audio/broadcast/BroadcastConfigurationEditor.java
Normal file
128
src/audio/broadcast/BroadcastConfigurationEditor.java
Normal file
@@ -0,0 +1,128 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package audio.broadcast;
|
||||
|
||||
import alias.AliasModel;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
|
||||
import gui.editor.DocumentListenerEditor;
|
||||
import settings.SettingsManager;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public abstract class BroadcastConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
|
||||
{
|
||||
protected SettingsManager mSettingsManager;
|
||||
protected BroadcastModel mBroadcastModel;
|
||||
protected AliasModel mAliasModel;
|
||||
|
||||
public BroadcastConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, SettingsManager settingsManager)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
mAliasModel = aliasModel;
|
||||
mSettingsManager = settingsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the configuration with the new name and prompts the user to update any aliases that have a broadcast
|
||||
* channel alias id with the old name to update to the new name.
|
||||
*
|
||||
* @param broadcastConfiguration to assign a new channel name
|
||||
* @param newName to assign to the broadcast configuration. Note: assumes that validateConfiguration() has been
|
||||
* invoked to verify that newName is non-null and non-empty
|
||||
*/
|
||||
protected void updateConfigurationName(BroadcastConfiguration broadcastConfiguration, String newName)
|
||||
{
|
||||
String previousName = broadcastConfiguration.getName();
|
||||
|
||||
if(previousName == null || previousName.isEmpty())
|
||||
{
|
||||
broadcastConfiguration.setName(newName);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!previousName.equals(newName))
|
||||
{
|
||||
mAliasModel.renameBroadcastChannel(previousName, newName);
|
||||
broadcastConfiguration.setName(newName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a text field control for a non-null, non-empty value
|
||||
* @param field to validate
|
||||
* @param title to use for error dialog
|
||||
* @param message to use for error dialog
|
||||
* @return true if field contains a non-null, non-empty value
|
||||
*/
|
||||
protected boolean validateTextField(JTextField field, String title, String message)
|
||||
{
|
||||
String text = field.getText();
|
||||
|
||||
if(text == null || text.isEmpty())
|
||||
{
|
||||
JOptionPane.showMessageDialog(BroadcastConfigurationEditor.this, message, title,
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
field.requestFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the text field control that contains an integer value for non-null, non-empty and within the
|
||||
* specified min/max valid range.
|
||||
* @param field to validate
|
||||
* @param title to use for error dialog
|
||||
* @param message to use for error dialog
|
||||
* @param minValid value
|
||||
* @param maxValid value
|
||||
* @return true if field contains a non-null, non-empty value within the valid min/max range
|
||||
*/
|
||||
protected boolean validateIntegerTextField(JTextField field, String title, String message, int minValid, int maxValid)
|
||||
{
|
||||
if(validateTextField(field, title, message))
|
||||
{
|
||||
String text = field.getText();
|
||||
|
||||
try
|
||||
{
|
||||
int value = Integer.parseInt(text);
|
||||
|
||||
if(minValid <= value && value <= maxValid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(BroadcastConfigurationEditor.this, message, title,
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
field.requestFocus();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
******************************************************************************/
|
||||
package audio.broadcast;
|
||||
|
||||
import alias.AliasModel;
|
||||
import audio.AudioPacket;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
|
||||
@@ -25,6 +26,7 @@ import audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||
import audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
import audio.broadcast.icecast.IcecastTCPConfigurationEditor;
|
||||
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||
import audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
|
||||
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||
@@ -150,6 +152,7 @@ public class BroadcastFactory
|
||||
*/
|
||||
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
|
||||
BroadcastModel broadcastModel,
|
||||
AliasModel aliasModel,
|
||||
SettingsManager settingsManager)
|
||||
{
|
||||
Editor<BroadcastConfiguration> editor;
|
||||
@@ -157,7 +160,10 @@ public class BroadcastFactory
|
||||
switch (configuration.getBroadcastServerType())
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
editor = new BroadcastifyConfigurationEditor(broadcastModel, settingsManager);
|
||||
editor = new BroadcastifyConfigurationEditor(broadcastModel, aliasModel, settingsManager);
|
||||
break;
|
||||
case ICECAST_TCP:
|
||||
editor = new IcecastTCPConfigurationEditor(broadcastModel, aliasModel, settingsManager);
|
||||
break;
|
||||
default:
|
||||
editor = new EmptyEditor<BroadcastConfiguration>();
|
||||
|
||||
@@ -40,6 +40,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -228,7 +229,9 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
|
||||
for(BroadcastConfiguration configurationToCompare: mBroadcastConfigurations)
|
||||
{
|
||||
if(configurationToCompare != configuration && configurationToCompare.getName().equals(name))
|
||||
if(configurationToCompare != configuration &&
|
||||
configurationToCompare.getName() != null &&
|
||||
configurationToCompare.getName().equals(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -276,7 +279,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
|
||||
if (audioBroadcaster != null)
|
||||
{
|
||||
audioBroadcaster.receive(audioPacket);
|
||||
audioBroadcaster.getAudioPacketListener().receive(audioPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,7 +301,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
|
||||
if (audioBroadcaster != null)
|
||||
{
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
|
||||
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
|
||||
{
|
||||
@Override
|
||||
@@ -308,6 +310,10 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
});
|
||||
|
||||
audioBroadcaster.start();
|
||||
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
|
||||
|
||||
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||
|
||||
if(index >= 0)
|
||||
@@ -409,10 +415,14 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
createBroadcaster(broadcastEvent.getBroadcastConfiguration());
|
||||
break;
|
||||
case CONFIGURATION_CHANGE:
|
||||
//Delete and recreate the broadcaster for any broadcast configuration changes
|
||||
BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
|
||||
cleanupMapAssociations(broadcastConfiguration);
|
||||
createBroadcaster(broadcastConfiguration);
|
||||
|
||||
//Delete and recreate the broadcaster for any broadcast configuration changes
|
||||
String previousChannelName = cleanupMapAssociations(broadcastConfiguration);
|
||||
deleteBroadcaster(previousChannelName);
|
||||
|
||||
//Delay restarting the broadcaster to allow remote server time to cleanup
|
||||
mThreadPoolManager.scheduleOnce(new DelayedBroadcasterStartup(broadcastConfiguration), 1, TimeUnit.SECONDS);
|
||||
break;
|
||||
case CONFIGURATION_DELETE:
|
||||
deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getName());
|
||||
@@ -455,8 +465,10 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
* with the new map associations. Remove any broadcaster that is associated with the old configuration name.
|
||||
*
|
||||
* @param broadcastConfiguration
|
||||
* @return previous or current channel name that has been replaced with the current configuration channel name so
|
||||
* that anything else associated with the previous name can be cleaned up.
|
||||
*/
|
||||
private void cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
|
||||
private String cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
String oldName = null;
|
||||
|
||||
@@ -471,11 +483,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
|
||||
if(oldName != null)
|
||||
{
|
||||
deleteBroadcaster(oldName);
|
||||
mBroadcastConfigurationMap.remove(oldName);
|
||||
}
|
||||
|
||||
mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
|
||||
|
||||
return oldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -620,8 +633,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
@Override
|
||||
public void accept(Path path)
|
||||
{
|
||||
mLog.info("Deleting orphaned temporary stream recording: " + path.toString());
|
||||
|
||||
try
|
||||
{
|
||||
Files.delete(path);
|
||||
@@ -639,4 +650,24 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides delayed startup of a broadcaster to allow for the remote server to complete a disconnection.
|
||||
*/
|
||||
public class DelayedBroadcasterStartup implements Runnable
|
||||
{
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
|
||||
public DelayedBroadcasterStartup(BroadcastConfiguration configuration)
|
||||
{
|
||||
mBroadcastConfiguration = configuration;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
createBroadcaster(mBroadcastConfiguration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
******************************************************************************/
|
||||
package audio.broadcast;
|
||||
|
||||
import alias.AliasModel;
|
||||
import com.jidesoft.swing.JideSplitPane;
|
||||
import gui.editor.Editor;
|
||||
import gui.editor.EmptyEditor;
|
||||
@@ -27,8 +28,11 @@ import org.slf4j.LoggerFactory;
|
||||
import settings.SettingsManager;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.swing.event.TableColumnModelEvent;
|
||||
import javax.swing.event.TableColumnModelListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseAdapter;
|
||||
@@ -43,6 +47,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
|
||||
|
||||
private BroadcastModel mBroadcastModel;
|
||||
private AliasModel mAliasModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
|
||||
private JTable mBroadcastConfigurationTable;
|
||||
@@ -53,9 +58,10 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
|
||||
private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
|
||||
|
||||
public BroadcastPanel(BroadcastModel broadcastModel, SettingsManager settingManager)
|
||||
public BroadcastPanel(BroadcastModel broadcastModel, AliasModel aliasModel, SettingsManager settingManager)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
mAliasModel = aliasModel;
|
||||
mSettingsManager = settingManager;
|
||||
mEditor = mEmptyEditor;
|
||||
|
||||
@@ -68,10 +74,11 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
|
||||
mBroadcastConfigurationTable = new JTable(mBroadcastModel);
|
||||
mBroadcastConfigurationTable.setAutoCreateRowSorter(true);
|
||||
mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
|
||||
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
|
||||
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setWidth(15);
|
||||
|
||||
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setPreferredWidth(1);
|
||||
mBroadcastConfigurationTable.getColumnModel().getColumn(1).setPreferredWidth(25);
|
||||
|
||||
JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
|
||||
|
||||
@@ -79,7 +86,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
|
||||
|
||||
mNewButton = new JButton("New ...");
|
||||
mNewButton.setToolTipText("Create a new broadcastAudio configuration");
|
||||
mNewButton.setToolTipText("Create a new broadcast audio configuration");
|
||||
mNewButton.addMouseListener(new MouseAdapter()
|
||||
{
|
||||
@Override
|
||||
@@ -88,6 +95,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
|
||||
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.BROADCASTIFY));
|
||||
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_TCP));
|
||||
|
||||
menu.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
@@ -201,7 +209,8 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
|
||||
if (selectedConfiguration != null)
|
||||
{
|
||||
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mSettingsManager);
|
||||
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mAliasModel,
|
||||
mSettingsManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -222,10 +231,12 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
|
||||
public AddBroadcastConfigurationItem(BroadcastServerType type)
|
||||
{
|
||||
super(type.toString());
|
||||
super(type.toString(), mSettingsManager.getImageIcon(type.getIconName(), 14));
|
||||
|
||||
mBroadcastServerType = type;
|
||||
|
||||
setToolTipText("Add a new " + type.toString() + " audio stream configuration");
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
@@ -234,12 +245,15 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
final BroadcastConfiguration configuration =
|
||||
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
|
||||
|
||||
if(configuration != null)
|
||||
{
|
||||
mBroadcastModel.addBroadcastConfiguration(configuration);
|
||||
|
||||
mBroadcastModel.addBroadcastConfiguration(configuration);
|
||||
|
||||
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
|
||||
int tableRow = mBroadcastConfigurationTable.convertRowIndexToView(modelRow);
|
||||
mBroadcastConfigurationTable.changeSelection(tableRow, 0, false, false);}
|
||||
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
|
||||
int tableRow = mBroadcastConfigurationTable.convertRowIndexToView(modelRow);
|
||||
mBroadcastConfigurationTable.changeSelection(tableRow, 0, false, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ package audio.broadcast;
|
||||
public enum BroadcastServerType
|
||||
{
|
||||
BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
|
||||
ICECAST_HTTP("Icecast 2.4+", null),
|
||||
ICECAST_TCP("Icecast 2.3", null),
|
||||
SHOUTCAST_V1("Shoutcast 1.x", null),
|
||||
SHOUTCAST_V2("Shoutcast 2.x", null),
|
||||
ICECAST_HTTP("Icecast 2.4+", "images/icecast.png"),
|
||||
ICECAST_TCP("Icecast 2.3", "images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast 1.x", "images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast 2.x", "images/shoutcast.png"),
|
||||
UNKNOWN("Unknown", null);
|
||||
|
||||
private String mLabel;
|
||||
|
||||
@@ -60,11 +60,6 @@ public enum BroadcastState
|
||||
*/
|
||||
NO_SERVER("No Server", false),
|
||||
|
||||
/**
|
||||
* Connected but paused. No audio is streaming.
|
||||
*/
|
||||
PAUSED("Paused", false),
|
||||
|
||||
/**
|
||||
* Initial state with no connection attempted.
|
||||
*/
|
||||
|
||||
@@ -28,10 +28,14 @@ import util.TimeStamp;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class StreamManager implements Listener<AudioPacket>
|
||||
{
|
||||
@@ -40,11 +44,11 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
|
||||
private ThreadPoolManager mThreadPoolManager;
|
||||
private AudioBroadcaster mAudioBroadcaster;
|
||||
private long mDelay;
|
||||
private Path mTempDirectory;
|
||||
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
|
||||
private Runnable mRecorderMonitor;
|
||||
private ScheduledFuture<?> mRecorderMonitorFuture;
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Stream manager processes all incoming audio packets and reassembles individual audio streams, converts audio
|
||||
@@ -52,22 +56,19 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
* ensure that recordings don't run too long before they are streamed out and to ensure that inactive recordings
|
||||
* are closed in a timely fashion.
|
||||
*
|
||||
* Completed streamable audio recordings are nominated to the output listener for streaming once a specified delay
|
||||
* has elapsed.
|
||||
* Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion
|
||||
*
|
||||
* @param threadPoolManager for scheduling runnables
|
||||
* @param audioBroadcaster to receive completed streamable audio recordings
|
||||
* @param delay from recording start until audio recording is nominated to output listener
|
||||
* @param tempDirectory where to store temporary audio recordings
|
||||
*/
|
||||
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, long delay, Path tempDirectory)
|
||||
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, Path tempDirectory)
|
||||
{
|
||||
assert(tempDirectory != null && Files.isDirectory(tempDirectory));
|
||||
assert(mThreadPoolManager != null);
|
||||
|
||||
mThreadPoolManager = threadPoolManager;
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
mDelay = delay;
|
||||
mTempDirectory = tempDirectory;
|
||||
}
|
||||
|
||||
@@ -77,16 +78,15 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mRecorderMonitorFuture == null)
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
if(mRecorderMonitor == null)
|
||||
{
|
||||
mRecorderMonitor = new RecorderMonitor();
|
||||
}
|
||||
|
||||
mRecorderMonitorFuture = mThreadPoolManager
|
||||
.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING, mRecorderMonitor, 2,
|
||||
TimeUnit.SECONDS);
|
||||
mRecorderMonitorFuture = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
|
||||
mRecorderMonitor, 2, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,24 +95,29 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mRecorderMonitorFuture != null)
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
mRecorderMonitorFuture.cancel(true);
|
||||
}
|
||||
|
||||
synchronized (mStreamRecorders)
|
||||
{
|
||||
mStreamRecorders.entrySet().stream().forEach(entry ->
|
||||
if(mRecorderMonitorFuture != null)
|
||||
{
|
||||
removeRecorder(entry.getKey());
|
||||
});
|
||||
mRecorderMonitorFuture.cancel(true);
|
||||
}
|
||||
|
||||
synchronized (mStreamRecorders)
|
||||
{
|
||||
List<Integer> streamKeys = new ArrayList<>(mStreamRecorders.keySet());
|
||||
|
||||
for(Integer streamKey: streamKeys)
|
||||
{
|
||||
removeRecorder(streamKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioPacket audioPacket)
|
||||
{
|
||||
if(audioPacket.hasAudioMetadata())
|
||||
if(mRunning.get() && audioPacket.hasAudioMetadata())
|
||||
{
|
||||
synchronized (mStreamRecorders)
|
||||
{
|
||||
@@ -168,19 +173,9 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
recorder.stop();
|
||||
|
||||
StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(),
|
||||
recorder.getMetadata(), recorder.getRecordingLength());
|
||||
recorder.getMetadata(), recorder.getTimeRecordingStart(), recorder.getRecordingLength());
|
||||
|
||||
if(mDelay <= 0)
|
||||
{
|
||||
mAudioBroadcaster.receive(streamableAudioRecording);
|
||||
}
|
||||
else
|
||||
{
|
||||
long waitTimeMillis = mDelay - (System.currentTimeMillis() - recorder.getTimeRecordingStart());
|
||||
|
||||
mThreadPoolManager.scheduleOnce(new RecordingDispatcher(streamableAudioRecording),
|
||||
waitTimeMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
mAudioBroadcaster.receive(streamableAudioRecording);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,29 +195,7 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the streamable audio recording to the output listener.
|
||||
*/
|
||||
public class RecordingDispatcher implements Runnable
|
||||
{
|
||||
StreamableAudioRecording mStreamableAudioRecording;
|
||||
|
||||
public RecordingDispatcher(StreamableAudioRecording recording)
|
||||
{
|
||||
mStreamableAudioRecording = recording;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(mAudioBroadcaster != null)
|
||||
{
|
||||
mAudioBroadcaster.receive(mStreamableAudioRecording);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors recorders to ensure they don't exceed the maximum life-span allowed for the recording.
|
||||
* Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording.
|
||||
*/
|
||||
public class RecorderMonitor implements Runnable
|
||||
{
|
||||
|
||||
@@ -22,23 +22,26 @@ import audio.metadata.AudioMetadata;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class StreamableAudioRecording
|
||||
public class StreamableAudioRecording implements Comparable<StreamableAudioRecording>
|
||||
{
|
||||
private Path mPath;
|
||||
private AudioMetadata mAudioMetadata;
|
||||
private long mRecordingLengthMilliSeconds;
|
||||
private long mStartTime;
|
||||
private long mRecordingLength;
|
||||
|
||||
/**
|
||||
* Audio recording that is ready to be streamed
|
||||
* @param path to the audio recording file
|
||||
* @param audioMetadata associated with the recording
|
||||
* @param start time of recording in milliseconds since epoch
|
||||
* @param recordingLength in milliseconds
|
||||
*/
|
||||
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long recordingLength)
|
||||
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long start, long recordingLength)
|
||||
{
|
||||
mPath = path;
|
||||
mAudioMetadata = audioMetadata;
|
||||
mRecordingLengthMilliSeconds = recordingLength;
|
||||
mStartTime = start;
|
||||
mRecordingLength = recordingLength;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,11 +60,29 @@ public class StreamableAudioRecording
|
||||
return mAudioMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording start time in milliseconds since epoch
|
||||
*/
|
||||
public long getStartTime()
|
||||
{
|
||||
return mStartTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording length in milliseconds
|
||||
*/
|
||||
public long getRecordingLength()
|
||||
{
|
||||
return mRecordingLengthMilliSeconds;
|
||||
return mRecordingLength;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements comparable for sorting recordings based on start time in ascending order
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(StreamableAudioRecording otherRecording)
|
||||
{
|
||||
return Long.compare(getStartTime(), otherRecording.getStartTime());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
******************************************************************************/
|
||||
package audio.broadcast.broadcastify;
|
||||
|
||||
import alias.AliasModel;
|
||||
import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastConfigurationEditor;
|
||||
import audio.broadcast.BroadcastEvent;
|
||||
import audio.broadcast.BroadcastModel;
|
||||
import audio.broadcast.BroadcastServerType;
|
||||
@@ -33,13 +35,10 @@ import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
|
||||
public class BroadcastifyConfigurationEditor extends BroadcastConfigurationEditor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
|
||||
|
||||
private SettingsManager mSettingsManager;
|
||||
private BroadcastModel mBroadcastModel;
|
||||
private JTextField mFeedID;
|
||||
private JTextField mName;
|
||||
private JTextField mServer;
|
||||
private JTextField mPort;
|
||||
@@ -49,10 +48,11 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
private JButton mResetButton;
|
||||
private JCheckBox mEnabled;
|
||||
|
||||
public BroadcastifyConfigurationEditor(BroadcastModel model, SettingsManager settingsManager)
|
||||
public BroadcastifyConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
|
||||
SettingsManager settingsManager)
|
||||
{
|
||||
mBroadcastModel = model;
|
||||
mSettingsManager = settingsManager;
|
||||
super(broadcastModel, aliasModel, settingsManager);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -69,37 +69,38 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
|
||||
add(channelLabel, "span, align center");
|
||||
|
||||
add(new JLabel("Feed ID:"));
|
||||
mFeedID = new JTextField();
|
||||
mFeedID.getDocument().addDocumentListener(this);
|
||||
add(mFeedID);
|
||||
|
||||
add(new JLabel("Name:"));
|
||||
mName = new JTextField();
|
||||
mName.getDocument().addDocumentListener(this);
|
||||
mName.setToolTipText("Unique name for this stream");
|
||||
add(mName);
|
||||
|
||||
add(new JLabel("Server:"));
|
||||
mServer = new JTextField();
|
||||
mServer.getDocument().addDocumentListener(this);
|
||||
mServer.setToolTipText("Server address (e.g. audio3.broadcastify.com). See MyBCFY feed details, Technicals tab");
|
||||
add(mServer);
|
||||
|
||||
add(new JLabel("Port:"));
|
||||
mPort = new JTextField();
|
||||
mPort.getDocument().addDocumentListener(this);
|
||||
mPort.setToolTipText("Server port number (e.g. 80). See MyBCFY feed details, Technicals tab");
|
||||
add(mPort);
|
||||
|
||||
add(new JLabel("Mount:"));
|
||||
mMountPoint = new JTextField();
|
||||
mMountPoint.getDocument().addDocumentListener(this);
|
||||
mMountPoint.setToolTipText("Mount point (e.g. \\awlkejrlkjsd). See MyBCFY feed details, Technicals tab");
|
||||
add(mMountPoint);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
mPassword = new JTextField();
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
mPassword.setToolTipText("Password. See MyBCFY feed details, Technicals tab");
|
||||
add(mPassword);
|
||||
|
||||
mEnabled = new JCheckBox("Enabled");
|
||||
mEnabled.setToolTipText("Enable (checked) or disable this stream");
|
||||
mEnabled.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
@@ -112,6 +113,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
|
||||
mSaveButton = new JButton("Save");
|
||||
mSaveButton.setEnabled(false);
|
||||
mSaveButton.setToolTipText("Click to save configuration information");
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
@@ -124,6 +126,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
|
||||
mResetButton = new JButton("Reset");
|
||||
mResetButton.setEnabled(false);
|
||||
mResetButton.setToolTipText("Click to reset any changes you've made\n since the last save");
|
||||
mResetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
@@ -154,7 +157,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
{
|
||||
BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
|
||||
|
||||
mFeedID.setText(config.getFeedID() != 0 ? String.valueOf(config.getFeedID()) : null);
|
||||
mName.setText(config.getName());
|
||||
mServer.setText(config.getHost());
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
@@ -164,7 +166,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
}
|
||||
else
|
||||
{
|
||||
mFeedID.setText(null);
|
||||
mName.setText(null);
|
||||
mServer.setText(null);
|
||||
mPort.setText(null);
|
||||
@@ -183,8 +184,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
|
||||
if(validateConfiguration())
|
||||
{
|
||||
config.setName(mName.getText());
|
||||
config.setFeedID(getFeedID());
|
||||
updateConfigurationName(config, mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
config.setPort(getPort());
|
||||
config.setMountPoint(mMountPoint.getText());
|
||||
@@ -219,12 +219,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
return false;
|
||||
}
|
||||
|
||||
//Feed ID is optional, but required
|
||||
if(!validateIntegerTextField(mFeedID, "Invalid Feed ID", "Please specify a feed ID", 1, Integer.MAX_VALUE))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Server address is optional, but required
|
||||
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||
{
|
||||
@@ -252,90 +246,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a text field control for a non-null, non-empty value
|
||||
* @param field to validate
|
||||
* @param title to use for error dialog
|
||||
* @param message to use for error dialog
|
||||
* @return true if field contains a non-null, non-empty value
|
||||
*/
|
||||
private boolean validateTextField(JTextField field, String title, String message)
|
||||
{
|
||||
String text = field.getText();
|
||||
|
||||
if(text == null || text.isEmpty())
|
||||
{
|
||||
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
field.requestFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the text field control that contains an integer value for non-null, non-empty and within the
|
||||
* specified min/max valid range.
|
||||
* @param field to validate
|
||||
* @param title to use for error dialog
|
||||
* @param message to use for error dialog
|
||||
* @param minValid value
|
||||
* @param maxValid value
|
||||
* @return true if field contains a non-null, non-empty value within the valid min/max range
|
||||
*/
|
||||
private boolean validateIntegerTextField(JTextField field, String title, String message, int minValid, int maxValid)
|
||||
{
|
||||
if(validateTextField(field, title, message))
|
||||
{
|
||||
String text = field.getText();
|
||||
|
||||
try
|
||||
{
|
||||
int value = Integer.parseInt(text);
|
||||
|
||||
if(minValid <= value && value <= maxValid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
field.requestFocus();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the feed ID text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getFeedID()
|
||||
{
|
||||
String feedID = mFeedID.getText();
|
||||
|
||||
if(feedID != null && !feedID.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(feedID);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package audio.broadcast.broadcastify;
|
||||
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class BroadcastifySelectionDialog extends JFrame
|
||||
{
|
||||
|
||||
public BroadcastifySelectionDialog()
|
||||
{
|
||||
setTitle("Broadcastify Channels");
|
||||
setSize(new Dimension(400, 400));
|
||||
add(new SelectionPanel());
|
||||
}
|
||||
|
||||
public class SelectionPanel extends JPanel
|
||||
{
|
||||
private JTextField mUserName;
|
||||
private JTextField mPassword;
|
||||
private JButton mConnectButton;
|
||||
|
||||
public SelectionPanel()
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][grow,fill]" ) );
|
||||
|
||||
add(new JLabel("Enter your credentials and click 'Connect'"), "span, align center");
|
||||
|
||||
add(new JLabel("User Name:"));
|
||||
|
||||
mUserName = new JTextField();
|
||||
add(mUserName);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
|
||||
mPassword = new JTextField();
|
||||
add(mPassword);
|
||||
|
||||
mConnectButton = new JButton("Connect");
|
||||
add(mConnectButton,"span, align center");
|
||||
|
||||
add(new JSeparator(JSeparator.HORIZONTAL), "span,growx");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
BroadcastifySelectionDialog dialog = new BroadcastifySelectionDialog();
|
||||
|
||||
try
|
||||
{
|
||||
EventQueue.invokeAndWait(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
|
||||
dialog.setVisible(true);
|
||||
dialog.setLocationRelativeTo(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,29 +18,19 @@
|
||||
******************************************************************************/
|
||||
package audio.broadcast.icecast;
|
||||
|
||||
import alias.Alias;
|
||||
import alias.id.broadcast.BroadcastChannel;
|
||||
import audio.AudioPacket;
|
||||
import audio.broadcast.AudioBroadcaster;
|
||||
import audio.broadcast.BroadcastFactory;
|
||||
import audio.broadcast.BroadcastFormat;
|
||||
import audio.broadcast.BroadcastState;
|
||||
import audio.metadata.AudioMetadata;
|
||||
import audio.metadata.Metadata;
|
||||
import audio.metadata.MetadataType;
|
||||
import controller.ThreadPoolManager;
|
||||
import org.asynchttpclient.AsyncCompletionHandler;
|
||||
import org.asynchttpclient.AsyncHandler;
|
||||
import org.asynchttpclient.BoundRequestBuilder;
|
||||
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||
import org.asynchttpclient.ListenableFuture;
|
||||
import org.asynchttpclient.Realm;
|
||||
import org.asynchttpclient.Response;
|
||||
import org.asynchttpclient.uri.Uri;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import properties.SystemProperties;
|
||||
import record.wave.AudioPacketMonoWaveReader;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
@@ -51,8 +41,6 @@ import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
@@ -90,7 +78,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration information
|
||||
* Icecast broadcast configuration
|
||||
*/
|
||||
private IcecastTCPConfiguration getConfiguration()
|
||||
{
|
||||
@@ -98,7 +86,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast any queued audio packets using Icecast V2 Protocol
|
||||
* Broadcasts the audio frame or sequence
|
||||
*/
|
||||
@Override
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
@@ -117,10 +105,10 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
catch(SocketException se)
|
||||
{
|
||||
//The remote server likely disconnected - setup to reconnect
|
||||
mLog.error("Socket Exception on Icecast TCP Audio Broadcaster -- disconnecting and resetting", se);
|
||||
disconnect(false);
|
||||
mLog.error("Resetting Icecast TCP Audio Broadcaster - socket error: " + se.getMessage());
|
||||
disconnect();
|
||||
}
|
||||
catch(IOException e)
|
||||
catch(Exception e)
|
||||
{
|
||||
completed = true;
|
||||
mLog.error("Error sending audio", e);
|
||||
@@ -133,6 +121,9 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an audio metadata update
|
||||
*/
|
||||
@Override
|
||||
protected void broadcastMetadata(AudioMetadata metadata)
|
||||
{
|
||||
@@ -214,16 +205,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
builder.setRealm(realm);
|
||||
builder.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||
|
||||
AsyncHandler handler = new AsyncCompletionHandler()
|
||||
{
|
||||
@Override
|
||||
public Object onCompleted(Response response) throws Exception
|
||||
{
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
mDefaultAsyncHttpClient.executeRequest(builder.build(), handler);
|
||||
mDefaultAsyncHttpClient.executeRequest(builder.build());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -237,9 +219,11 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio handler is currently connected to the remote server and capable of streaming audio.
|
||||
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
|
||||
* is currently connected to the remote server following any connection attempts.
|
||||
*
|
||||
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
|
||||
*
|
||||
* This method will attempt to establish a connection or reconnect to the streaming server.
|
||||
* @return true if the audio handler can stream audio
|
||||
*/
|
||||
private boolean connect()
|
||||
@@ -254,10 +238,12 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect from the remote server
|
||||
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
|
||||
*/
|
||||
public void disconnect(boolean shutdown)
|
||||
public void disconnect()
|
||||
{
|
||||
mLog.info("Disconnecting Icecast TCP audio broadcaster");
|
||||
|
||||
if(mOutputStream != null)
|
||||
{
|
||||
try
|
||||
@@ -301,19 +287,16 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
mSocket = null;
|
||||
}
|
||||
|
||||
if(shutdown)
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
else
|
||||
if(!isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.READY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connnection to the remote server using the shoutcast configuration information. Once disconnected
|
||||
* following a successful connection, attempts to reestablish a connection on a set interval
|
||||
* Creates a connection to the remote server using the icecast configuration information. Once disconnected
|
||||
* following a successful connection, successive calls to this method will attempt to reestablish a connection on a
|
||||
* minimum reconnection attempt interval
|
||||
*/
|
||||
private void createConnection()
|
||||
{
|
||||
@@ -340,12 +323,6 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
|
||||
|
||||
if(getConfiguration().hasName())
|
||||
{
|
||||
sb.append(IcecastHeader.NAME.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().getName()).append(TERMINATOR);
|
||||
}
|
||||
|
||||
if(getConfiguration().hasGenre())
|
||||
{
|
||||
sb.append(IcecastHeader.GENRE.getValue()).append(SEPARATOR)
|
||||
@@ -374,13 +351,17 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
}
|
||||
|
||||
mLog.debug("Checking response ...");
|
||||
checkResponse();
|
||||
checkConnectionAttemptResponse();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio metadata description string that can optionally be included when connecting to the remote
|
||||
* broadcast server.
|
||||
*/
|
||||
private String getAudioInfoMetadata()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -422,13 +403,13 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a socket connection to the remote server and sets the state to CONNECTING.
|
||||
* Creates a socket connection to the remote server and sets the state to CONNECTING or indicates an error state
|
||||
* if the socket is unable to connect to the remote server with the current configuration information.
|
||||
*/
|
||||
private void createSocket()
|
||||
{
|
||||
if(mSocket == null)
|
||||
{
|
||||
mLog.debug("Creating socket");
|
||||
try
|
||||
{
|
||||
mSocket = new Socket(getConfiguration().getHost(),
|
||||
@@ -492,6 +473,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
|
||||
/**
|
||||
* Sends the string data to the remote server
|
||||
*
|
||||
* @param data to send
|
||||
* @throws IOException if there is an error communicating with the remote server
|
||||
*/
|
||||
@@ -525,6 +507,11 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains any pending response from the remote server.
|
||||
* @return string response
|
||||
* @throws IOException if there is an error
|
||||
*/
|
||||
private String getResponse() throws IOException
|
||||
{
|
||||
if(mInputStream != null)
|
||||
@@ -549,7 +536,11 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
return null;
|
||||
}
|
||||
|
||||
private void checkResponse()
|
||||
/**
|
||||
* Checks for a remote server response following a connection attempt and evaluates the response for indication of a
|
||||
* successful connection or an error state
|
||||
*/
|
||||
private void checkConnectionAttemptResponse()
|
||||
{
|
||||
String response = null;
|
||||
|
||||
@@ -575,86 +566,23 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if(response.startsWith("HTTP/1.0 403 Mountpoint in use"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
|
||||
}
|
||||
else if(response.contains("Invalid Password") || response.contains("Authentication Required"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.INVALID_PASSWORD);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug("Unrecognized server response:" + response);
|
||||
mLog.error("Unrecognized server response:" + response);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mLog.debug("Response was empty");
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
boolean test = false;
|
||||
|
||||
IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3);
|
||||
|
||||
String streamName = "Broadcastify SDRTrunk #2";
|
||||
|
||||
config.setName(streamName);
|
||||
config.setHost("audio3.broadcastify.com");
|
||||
config.setPort(80);
|
||||
config.setMountPoint("/k0yrdpx7zn4h");
|
||||
config.setUserName("source");
|
||||
config.setPassword("k8j9405n");
|
||||
config.setDescription("SDRTrunk Test Feed #2");
|
||||
config.setGenre("Scanner");
|
||||
config.setPublic(false);
|
||||
config.setURL("http://www.radioreference.com");
|
||||
config.setBitRate(16);
|
||||
|
||||
AudioMetadata metadata = new AudioMetadata(0, false);
|
||||
Alias toAlias = new Alias("ToAlias");
|
||||
toAlias.addAliasID(new BroadcastChannel(streamName));
|
||||
metadata.receive(new Metadata(MetadataType.TO, "1234", toAlias ));
|
||||
|
||||
if(test)
|
||||
{
|
||||
mLog.debug("Auth:" + config.getEncodedCredentials());
|
||||
}
|
||||
else
|
||||
{
|
||||
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
||||
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
|
||||
|
||||
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
|
||||
|
||||
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
||||
mLog.debug("Opening: " + path.toString());
|
||||
|
||||
mLog.debug("Registering and starting audio playback");
|
||||
|
||||
int playCount = 0;
|
||||
|
||||
while(true)
|
||||
{
|
||||
mLog.debug("Playback started [" + path.toString() + "]");
|
||||
|
||||
toAlias.setName("Playback #" + playCount++);
|
||||
|
||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||
{
|
||||
reader.setMetadata(metadata);
|
||||
reader.setListener(audioBroadcaster);
|
||||
reader.read();
|
||||
audioBroadcaster.receive(new AudioPacket(AudioPacket.Type.END, metadata));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
mLog.error("Error", e);
|
||||
}
|
||||
|
||||
mLog.debug("Playback ended [" + path.toString() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,6 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
sb.append(getPassword());
|
||||
}
|
||||
|
||||
mLog.debug("Connection String: " + sb.toString());
|
||||
|
||||
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
|
||||
|
||||
return "Basic " + base64;
|
||||
|
||||
269
src/audio/broadcast/icecast/IcecastTCPConfigurationEditor.java
Normal file
269
src/audio/broadcast/icecast/IcecastTCPConfigurationEditor.java
Normal file
@@ -0,0 +1,269 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package audio.broadcast.icecast;
|
||||
|
||||
import alias.AliasModel;
|
||||
import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastConfigurationEditor;
|
||||
import audio.broadcast.BroadcastEvent;
|
||||
import audio.broadcast.BroadcastModel;
|
||||
import audio.broadcast.BroadcastServerType;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import gui.editor.DocumentListenerEditor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import settings.SettingsManager;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class IcecastTCPConfigurationEditor extends BroadcastConfigurationEditor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfigurationEditor.class );
|
||||
|
||||
private JTextField mName;
|
||||
private JTextField mServer;
|
||||
private JTextField mPort;
|
||||
private JTextField mMountPoint;
|
||||
private JTextField mPassword;
|
||||
private JButton mSaveButton;
|
||||
private JButton mResetButton;
|
||||
private JCheckBox mEnabled;
|
||||
|
||||
public IcecastTCPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
|
||||
SettingsManager settingsManager)
|
||||
{
|
||||
super(broadcastModel, aliasModel, settingsManager);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][grow,fill]" ) );
|
||||
setPreferredSize(new Dimension(150,400));
|
||||
|
||||
JLabel channelLabel = new JLabel("Icecast 2.3 Stream");
|
||||
|
||||
ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.ICECAST_TCP.getIconName(), 25);
|
||||
channelLabel.setIcon(icon);
|
||||
|
||||
add(channelLabel, "span, align center");
|
||||
|
||||
add(new JLabel("Name:"));
|
||||
mName = new JTextField();
|
||||
mName.getDocument().addDocumentListener(this);
|
||||
mName.setToolTipText("Unique name for this stream");
|
||||
add(mName);
|
||||
|
||||
add(new JLabel("Server:"));
|
||||
mServer = new JTextField();
|
||||
mServer.getDocument().addDocumentListener(this);
|
||||
mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
|
||||
add(mServer);
|
||||
|
||||
add(new JLabel("Port:"));
|
||||
mPort = new JTextField();
|
||||
mPort.getDocument().addDocumentListener(this);
|
||||
mPort.setToolTipText("Server port number (e.g. 80)");
|
||||
add(mPort);
|
||||
|
||||
add(new JLabel("Mount:"));
|
||||
mMountPoint = new JTextField();
|
||||
mMountPoint.getDocument().addDocumentListener(this);
|
||||
mMountPoint.setToolTipText("Mount point (e.g. \\12345)");
|
||||
add(mMountPoint);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
mPassword = new JTextField();
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
mPassword.setToolTipText("Password for the stream");
|
||||
add(mPassword);
|
||||
|
||||
mEnabled = new JCheckBox("Enabled");
|
||||
mEnabled.setToolTipText("Enable (checked) or disable this stream");
|
||||
mEnabled.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
add(mEnabled, "span");
|
||||
|
||||
mSaveButton = new JButton("Save");
|
||||
mSaveButton.setEnabled(false);
|
||||
mSaveButton.setToolTipText("Click to save configuration information");
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
save();
|
||||
}
|
||||
});
|
||||
add(mSaveButton);
|
||||
|
||||
mResetButton = new JButton("Reset");
|
||||
mResetButton.setEnabled(false);
|
||||
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
|
||||
mResetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(false);
|
||||
setItem(getItem());
|
||||
}
|
||||
});
|
||||
add(mResetButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModified(boolean modified)
|
||||
{
|
||||
super.setModified(modified);
|
||||
|
||||
mSaveButton.setEnabled(modified);
|
||||
mResetButton.setEnabled(modified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
|
||||
|
||||
mName.setText(config.getName());
|
||||
mServer.setText(config.getHost());
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
mMountPoint.setText(config.getMountPoint());
|
||||
mPassword.setText(config.getPassword());
|
||||
mEnabled.setSelected(config.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
mName.setText(null);
|
||||
mServer.setText(null);
|
||||
mPort.setText(null);
|
||||
mMountPoint.setText(null);
|
||||
mPassword.setText(null);
|
||||
mEnabled.setSelected(false);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
|
||||
|
||||
if(validateConfiguration())
|
||||
{
|
||||
updateConfigurationName(config, mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
config.setPort(getPort());
|
||||
config.setMountPoint(mMountPoint.getText());
|
||||
config.setPassword(mPassword.getText());
|
||||
config.setEnabled(mEnabled.isSelected());
|
||||
|
||||
setModified(false);
|
||||
|
||||
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user specified values in the controls prior to saving them to the configuration
|
||||
*/
|
||||
private boolean validateConfiguration()
|
||||
{
|
||||
String name = mName.getText();
|
||||
|
||||
//A unique stream name is required
|
||||
if(!mBroadcastModel.isUniqueName(name, getItem()))
|
||||
{
|
||||
String message = (name == null || name.isEmpty()) ?
|
||||
"Please specify a stream name." : "Stream name " + name +
|
||||
"is already in use.\nPlease choose another name.";
|
||||
|
||||
JOptionPane.showMessageDialog(IcecastTCPConfigurationEditor.this,
|
||||
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mName.requestFocus();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Server address is optional, but required
|
||||
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Port is optional, but required
|
||||
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Mount Point is optional, but required
|
||||
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Password is optional, but required
|
||||
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getPort()
|
||||
{
|
||||
String port = mPort.getText();
|
||||
|
||||
if(port != null && !port.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(port);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,12 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
|
||||
return (ShoutcastV1Configuration)getBroadcastConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void disconnect()
|
||||
{
|
||||
//TODO: implement a disconnect
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void broadcastMetadata(AudioMetadata metadata)
|
||||
{
|
||||
|
||||
@@ -91,7 +91,7 @@ public class ControllerPanel extends JPanel
|
||||
|
||||
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
|
||||
|
||||
mBroadcastPanel = new BroadcastPanel(broadcastModel, settingsManager);
|
||||
mBroadcastPanel = new BroadcastPanel(broadcastModel, aliasModel, settingsManager);
|
||||
|
||||
mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user