#30 WIP - broadcastify and icecast TCP streams working and editors are complete. Adds icons to editor and table views.

This commit is contained in:
Denny
2016-12-04 07:28:03 -05:00
parent b71415b82a
commit f44892c62b
22 changed files with 1189 additions and 889 deletions

BIN
images/icecast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
images/shoutcast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -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;
}
}

View File

@@ -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 );

View File

@@ -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() );

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View 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;
}
}

View File

@@ -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>();

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
});
}
}

View File

@@ -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;

View File

@@ -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.
*/

View File

@@ -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
{

View File

@@ -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());
}
}

View File

@@ -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.
*/

View File

@@ -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();
}
}
}

View File

@@ -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() + "]");
}
}
}
}

View File

@@ -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;

View 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;
}
}

View File

@@ -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)
{

View File

@@ -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);