From f44892c62b3f3e8891228f011701cc37c30cd25d Mon Sep 17 00:00:00 2001 From: Denny Date: Sun, 4 Dec 2016 07:28:03 -0500 Subject: [PATCH] #30 WIP - broadcastify and icecast TCP streams working and editors are complete. Adds icons to editor and table views. --- images/icecast.png | Bin 0 -> 33318 bytes images/shoutcast.png | Bin 0 -> 28226 bytes src/alias/Alias.java | 54 +- src/alias/AliasController.java | 2 +- src/alias/AliasModel.java | 36 + src/alias/MultipleAliasEditor.java | 740 ++++++++++-------- src/audio/broadcast/AudioBroadcaster.java | 242 +++--- .../BroadcastConfigurationEditor.java | 128 +++ src/audio/broadcast/BroadcastFactory.java | 8 +- src/audio/broadcast/BroadcastModel.java | 51 +- src/audio/broadcast/BroadcastPanel.java | 36 +- src/audio/broadcast/BroadcastServerType.java | 8 +- src/audio/broadcast/BroadcastState.java | 5 - src/audio/broadcast/StreamManager.java | 83 +- .../broadcast/StreamableAudioRecording.java | 31 +- .../BroadcastifyConfigurationEditor.java | 122 +-- .../BroadcastifySelectionDialog.java | 93 --- .../icecast/IcecastTCPAudioBroadcaster.java | 160 ++-- .../icecast/IcecastTCPConfiguration.java | 2 - .../IcecastTCPConfigurationEditor.java | 269 +++++++ .../v1/ShoutcastV1AudioBroadcaster.java | 6 + src/controller/ControllerPanel.java | 2 +- 22 files changed, 1189 insertions(+), 889 deletions(-) create mode 100644 images/icecast.png create mode 100644 images/shoutcast.png create mode 100644 src/audio/broadcast/BroadcastConfigurationEditor.java delete mode 100644 src/audio/broadcast/broadcastify/BroadcastifySelectionDialog.java create mode 100644 src/audio/broadcast/icecast/IcecastTCPConfigurationEditor.java diff --git a/images/icecast.png b/images/icecast.png new file mode 100644 index 0000000000000000000000000000000000000000..e57643f2bbaccb790d47fccd78eafe92e928207f GIT binary patch literal 33318 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 literal 0 HcmV?d00001 diff --git a/images/shoutcast.png b/images/shoutcast.png new file mode 100644 index 0000000000000000000000000000000000000000..083f2cafb0879e90b890de7a5232c15e210e33ec GIT binary patch literal 28226 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

gg9RHl*+SaOwqO3Kshv-o|y>Wh=It&Hgzc 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 literal 0 HcmV?d00001 diff --git a/src/alias/Alias.java b/src/alias/Alias.java index 5447cb2c..3770b014 100644 --- a/src/alias/Alias.java +++ b/src/alias/Alias.java @@ -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 broadcastChannels = getBroadcastChannels(); + + if(broadcastChannels.size() > 0) + { + Set 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; + } } diff --git a/src/alias/AliasController.java b/src/alias/AliasController.java index ad8ec80d..0619672c 100644 --- a/src/alias/AliasController.java +++ b/src/alias/AliasController.java @@ -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 ); diff --git a/src/alias/AliasModel.java b/src/alias/AliasModel.java index a051a0d0..a46bd532 100644 --- a/src/alias/AliasModel.java +++ b/src/alias/AliasModel.java @@ -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() ); diff --git a/src/alias/MultipleAliasEditor.java b/src/alias/MultipleAliasEditor.java index e22e0b1b..ba6861d1 100644 --- a/src/alias/MultipleAliasEditor.java +++ b/src/alias/MultipleAliasEditor.java @@ -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> - implements Listener +public class MultipleAliasEditor extends Editor> + implements Listener { - private static final long serialVersionUID = 1L; - private final static Logger mLog = LoggerFactory.getLogger( MultipleAliasEditor.class ); - - private static String HELP_TEXT = - "Select attributes below to change for all selected aliases"; + 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 = + "Select attributes below to change for all selected aliases"; - private JCheckBox mListCheckBox = new JCheckBox( "List" ); - private JComboBox mListCombo = new JComboBox<>(); - private JCheckBox mGroupCheckBox = new JCheckBox( "Group" ); - private JComboBox mGroupCombo = new JComboBox<>(); - private JCheckBox mIconCheckBox = new JCheckBox( "Icon" ); + private JLabel mAliasCountLabel; + private JLabel mAliasCount; + + private JCheckBox mListCheckBox = new JCheckBox("List"); + private JComboBox mListCombo = new JComboBox<>(); + private JCheckBox mGroupCheckBox = new JCheckBox("Group"); + private JComboBox mGroupCombo = new JComboBox<>(); + private JCheckBox mIconCheckBox = new JCheckBox("Icon"); private JComboBox 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 mRecordActionCombo = new JComboBox<>( Record.values() ); - private JCheckBox mPriorityCheckBox = new JCheckBox( "Priority" ); + private JCheckBox mRecordCheckBox = new JCheckBox("Record"); + private JComboBox 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 getAliases() - { - if( hasItem() ) - { - return (List)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 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 getAliases() + { + if (hasItem()) + { + return (List) 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( 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(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 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 listNames = mAliasModel.getListNames(); - listNames.add( 0, "" ); - mListCombo.setModel( new DefaultComboBoxModel( - listNames.toArray( new String[ listNames.size() ] ) ) ); - - List groupNames = mAliasModel.getGroupNames(); - groupNames.add( 0, "" ); - mGroupCombo.setModel( new DefaultComboBoxModel( - 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 listNames = mAliasModel.getListNames(); - listNames.add( 0, "" ); - mListCombo.setModel( new DefaultComboBoxModel( - listNames.toArray( new String[ listNames.size() ] ) ) ); - - List groupNames = mAliasModel.getGroupNames(); - groupNames.add( 0, "" ); - mGroupCombo.setModel( new DefaultComboBoxModel( - 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(); + mStreamCombo.setEditable(true); + add(mStreamCombo, "wrap"); - @Override - public void save() - { - if( hasRequestedChanges() && hasItem() ) - { - List 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 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 listNames = mAliasModel.getListNames(); + listNames.add(0, ""); + mListCombo.setModel(new DefaultComboBoxModel( + listNames.toArray(new String[listNames.size()]))); + + List groupNames = mAliasModel.getGroupNames(); + groupNames.add(0, ""); + mGroupCombo.setModel(new DefaultComboBoxModel( + groupNames.toArray(new String[groupNames.size()]))); + + List streamNames = mBroadcastModel.getBroadcastConfigurationNames(); + mStreamCombo.setModel(new DefaultComboBoxModel(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 listNames = mAliasModel.getListNames(); + listNames.add(0, ""); + mListCombo.setModel(new DefaultComboBoxModel( + listNames.toArray(new String[listNames.size()]))); + + List groupNames = mAliasModel.getGroupNames(); + groupNames.add(0, ""); + mGroupCombo.setModel(new DefaultComboBoxModel( + 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 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; + } + } } diff --git a/src/audio/broadcast/AudioBroadcaster.java b/src/audio/broadcast/AudioBroadcaster.java index 676f8c4e..aaafdcb9 100644 --- a/src/audio/broadcast/AudioBroadcaster.java +++ b/src/audio/broadcast/AudioBroadcaster.java @@ -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 +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 mAudioRecordingQueue; + private LinkedTransferQueue mAudioRecordingQueue = new LinkedTransferQueue<>(); + private byte[] mSilenceFrame; - private int mStreamedAudioCount = 0; private Listener 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 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); } diff --git a/src/audio/broadcast/BroadcastConfigurationEditor.java b/src/audio/broadcast/BroadcastConfigurationEditor.java new file mode 100644 index 00000000..678ba383 --- /dev/null +++ b/src/audio/broadcast/BroadcastConfigurationEditor.java @@ -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 + * + ******************************************************************************/ +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 +{ + 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; + } +} diff --git a/src/audio/broadcast/BroadcastFactory.java b/src/audio/broadcast/BroadcastFactory.java index 740c4574..8feca393 100644 --- a/src/audio/broadcast/BroadcastFactory.java +++ b/src/audio/broadcast/BroadcastFactory.java @@ -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 getEditor(BroadcastConfiguration configuration, BroadcastModel broadcastModel, + AliasModel aliasModel, SettingsManager settingsManager) { Editor 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(); diff --git a/src/audio/broadcast/BroadcastModel.java b/src/audio/broadcast/BroadcastModel.java index 5f65d208..165cf503 100644 --- a/src/audio/broadcast/BroadcastModel.java +++ b/src/audio/broadcast/BroadcastModel.java @@ -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