Merge pull request #85 from DSheirer/30-audio_streaming_support

30 audio streaming support
This commit is contained in:
Denny Sheirer
2017-01-07 08:25:19 -05:00
committed by GitHub
139 changed files with 13368 additions and 1272 deletions

View File

@@ -1,12 +1,22 @@
<component name="ProjectDictionaryState">
<dictionary name="denny">
<words>
<w>bahroo</w>
<w>broadcastify</w>
<w>esnid</w>
<w>fleetsync</w>
<w>icecast</w>
<w>ident</w>
<w>polyphase</w>
<w>samplerate</w>
<w>sdrtrunk</w>
<w>sheirer</w>
<w>shoutcast</w>
<w>streamable</w>
<w>talkgroup</w>
<w>tgid</w>
<w>ultravox</w>
<w>xtea</w>
</words>
</dictionary>
</component>

View File

@@ -8,7 +8,6 @@
<root url="jar://$PROJECT_DIR$/imports/tablefilter-swing-5.2.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-linux-x86.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/JLargeArrays-1.2.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/slf4j-api-1.7.7.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-linux-ppc.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-windows-x86.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-osx-x86.jar!/" />
@@ -21,11 +20,46 @@
<root url="jar://$PROJECT_DIR$/imports/usb4java-javax-1.2.0.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jsr80-1.0.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-linux-arm.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.2.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-osx-x86_64.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/JTransforms-3.0.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.3.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/java-lame-3.98.4.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/commons-codec-1.9.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/commons-logging-1.2.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/slf4j-api-1.7.21.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
<JAVADOC>
<root url="https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/index.html" />
</JAVADOC>
<SOURCES>
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/examples" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/fluent-hc/src/examples" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/fluent-hc/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/fluent-hc/src/test/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-cache/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-cache/src/main/java-deprecated" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-cache/src/test/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-osgi/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-osgi/src/test/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-win/src/examples" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-win/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient-win/src/test/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/examples" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/main/java-deprecated" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/test/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpmime/src/examples" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpmime/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpmime/src/main/java-deprecated" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpmime/src/test/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-asyncclient-4.1.2/httpasyncclient/src/examples" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-asyncclient-4.1.2/httpasyncclient/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-asyncclient-4.1.2/httpasyncclient/src/main/java-deprecated" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-asyncclient-4.1.2/httpasyncclient/src/test/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/examples" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/main/java" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/main/java-deprecated" />
<root url="file://$USER_HOME$/Downloads/httpcomponents-client-4.5.2/httpclient/src/test/java" />
</SOURCES>
</library>
</component>

View File

@@ -1,9 +0,0 @@
<component name="libraryTable">
<library name="jmbe-interface-0.3.3">
<CLASSES>
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

View File

@@ -0,0 +1,20 @@
<component name="libraryTable">
<library name="org.asynchttpclient:async-http-client:2.0.18" type="repository">
<properties maven-id="org.asynchttpclient:async-http-client:2.0.18" />
<CLASSES>
<root url="jar://$PROJECT_DIR$/imports/slf4j-api-1.7.21.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/javassist-3.20.0-GA.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/axis.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jaxrpc.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/commons-discovery-0.2.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/activation-1.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/java-mail-1.4.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/wsdl4j-1.5.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/radioreference-14.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/mina-core-2.0.16.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/mina-http-2.0.16.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

15
.idea/misc.xml generated
View File

@@ -38,20 +38,7 @@
<property name="caretWidth" class="java.lang.Integer" />
</properties>
</component>
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

View File

@@ -1,6 +1,10 @@
0.3.0 2016xxxx (work in progress)
- Requires Java 8 for compatibility with eventual changeover to JavaFX
0.3.0 2017xxxx (work in progress)
- FEATURE: audio streaming - adds capability to stream to multiple remote broadcast
servers (Broadcastify, Icecast v2.3 and v2.4+, and Shoutcast V1 and V2). Each
stream can be independently configured with a delay. Audio calls are directed
to one or more streams by attaching one or more stream identifiers to each alias.
- Requires Java 8 (or newer) for compatibility with eventual changeover to JavaFX
- Extensive GUI updates for tuner, channel and alias management.
- Updated alias list management of alias id values and wildcards. Weighting is now
applied to each wildcard so that they can be ordered from most specific to least
@@ -14,10 +18,10 @@
decoders and filters
- New audio playback interface with global mute and support for one or two
audio channels
- Audio playback is prioritized using a new Call Priority alias action
- Audio playback is prioritized using a new Call Priority name action
with selected channels having the highest priority and any audio without
an alias identifier having the lowest priority.
- Updated Audio recording per call with new 'Non-Recordable' alias action
an name identifier having the lowest priority.
- Updated Audio recording per call with new 'Non-Recordable' name action
- Updated Baseband (I/Q) recording at 48 kHz
- Added Airspy tuner support
- New traffic channel pool manager for trunked call following

View File

@@ -27,4 +27,7 @@
<appender-ref ref="STDOUT" />
</root>
<!-- Apache Mina TCP/HTTP library -->
<logger name="org.apache.mina" level="INFO" />
</configuration>

BIN
images/broadcastify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

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

BIN
imports/activation-1.1.jar Normal file

Binary file not shown.

BIN
imports/axis.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
imports/java-mail-1.4.jar Normal file

Binary file not shown.

Binary file not shown.

BIN
imports/jaxrpc.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
imports/wsdl4j-1.5.1.jar Normal file

Binary file not shown.

View File

@@ -20,6 +20,6 @@
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" name="jmbe-interface-0.3.3" level="project" />
<orderEntry type="library" name="org.asynchttpclient:async-http-client:2.0.18" level="project" />
</component>
</module>

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;
@@ -30,6 +33,7 @@ import javax.xml.bind.annotation.XmlTransient;
import alias.action.AliasAction;
import alias.id.AliasID;
import alias.id.AliasIDType;
import alias.id.broadcast.BroadcastChannel;
import alias.id.nonrecordable.NonRecordable;
import alias.id.priority.Priority;
@@ -175,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
*/
@@ -184,7 +215,7 @@ public class Alias
{
for( AliasID id: mAliasIDs )
{
if( id.getType() == AliasIDType.Priority )
if( id.getType() == AliasIDType.PRIORITY)
{
return ((Priority)id).getPriority();
}
@@ -197,7 +228,7 @@ public class Alias
{
for( AliasID id: mAliasIDs )
{
if( id.getType() == AliasIDType.Priority )
if( id.getType() == AliasIDType.PRIORITY)
{
return true;
}
@@ -216,7 +247,7 @@ public class Alias
{
for( AliasID id: mAliasIDs )
{
if( id.getType() == AliasIDType.Priority )
if( id.getType() == AliasIDType.PRIORITY)
{
((Priority)id).setPriority( priority );
return;
@@ -229,7 +260,7 @@ public class Alias
addAliasID( p );
}
}
/**
* Inspects the alias for a non-recordable alias id. Default is true;
*/
@@ -238,7 +269,7 @@ public class Alias
{
for( AliasID id: getId() )
{
if( id.getType() == AliasIDType.NonRecordable )
if( id.getType() == AliasIDType.NON_RECORDABLE)
{
return false;
}
@@ -258,7 +289,7 @@ public class Alias
for( AliasID id: getId() )
{
if( id.getType() == AliasIDType.NonRecordable )
if( id.getType() == AliasIDType.NON_RECORDABLE)
{
toRemove = id;
break;
@@ -274,7 +305,7 @@ public class Alias
{
for( AliasID id: getId() )
{
if( id.getType() == AliasIDType.NonRecordable )
if( id.getType() == AliasIDType.NON_RECORDABLE)
{
return;
}
@@ -283,4 +314,62 @@ public class Alias
addAliasID( new NonRecordable() );
}
}
/**
* Inspects the alias for broadcast channel/streamable alias ids. Default is false;
*/
@XmlTransient
public boolean isStreamable()
{
for( AliasID id: getId() )
{
if( id.getType() == AliasIDType.BROADCAST_CHANNEL)
{
return true;
}
}
return false;
}
/**
* List of broadcast channels specified for this alias.
*/
@XmlTransient
public List<BroadcastChannel> getBroadcastChannels()
{
List<BroadcastChannel> broadcastChannels = new ArrayList<>();
for(AliasID id: getId())
{
if(id.getType() == AliasIDType.BROADCAST_CHANNEL)
{
broadcastChannels.add((BroadcastChannel)id);
}
}
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

@@ -1,5 +1,6 @@
package alias;
import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import java.awt.Color;
@@ -52,12 +53,12 @@ public class AliasController extends JPanel
private IconCellRenderer mIconCellRenderer;
public AliasController( AliasModel aliasModel, SettingsManager settingsManager )
public AliasController(AliasModel aliasModel, BroadcastModel broadcastModel, SettingsManager settingsManager )
{
mAliasModel = aliasModel;
mAliasEditor = new AliasEditor( mAliasModel, settingsManager );
mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, settingsManager );
mAliasEditor = new AliasEditor( mAliasModel, broadcastModel, settingsManager );
mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, broadcastModel, settingsManager );
mIconCellRenderer = new IconCellRenderer( settingsManager );

View File

@@ -17,6 +17,7 @@
******************************************************************************/
package alias;
import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import java.awt.Color;
@@ -49,11 +50,11 @@ public class AliasEditor extends Editor<Alias>
private AliasIdentifierEditor mAliasIdentifierEditor;
private AliasActionEditor mAliasActionEditor;
public AliasEditor( AliasModel aliasModel, SettingsManager settingsManager )
public AliasEditor(AliasModel aliasModel, BroadcastModel broadcastModel, SettingsManager settingsManager )
{
mAliasModel = aliasModel;
mAliasNameEditor = new AliasNameEditor( mAliasModel, settingsManager );
mAliasIdentifierEditor = new AliasIdentifierEditor( aliasModel );
mAliasIdentifierEditor = new AliasIdentifierEditor( aliasModel, broadcastModel );
mAliasActionEditor = new AliasActionEditor( aliasModel );
mAliasModel.addListener( this );

View File

@@ -17,8 +17,6 @@
******************************************************************************/
package alias;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
import alias.action.AliasAction;
import alias.action.AliasActionType;
import alias.action.beep.BeepAction;
@@ -29,6 +27,8 @@ import alias.action.script.ScriptAction;
import alias.action.script.ScriptActionEditor;
import alias.id.AliasID;
import alias.id.AliasIDType;
import alias.id.broadcast.BroadcastChannel;
import alias.id.broadcast.BroadcastChannelEditor;
import alias.id.esn.ESNEditor;
import alias.id.esn.Esn;
import alias.id.fleetsync.FleetsyncID;
@@ -53,6 +53,9 @@ import alias.id.talkgroup.TalkgroupID;
import alias.id.talkgroup.TalkgroupIDEditor;
import alias.id.uniqueID.UniqueID;
import alias.id.uniqueID.UniqueIDEditor;
import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
public class AliasFactory
{
@@ -60,22 +63,27 @@ public class AliasFactory
{
switch( id.getType() )
{
case BROADCAST_CHANNEL:
BroadcastChannel originalBroadcast = (BroadcastChannel)id;
BroadcastChannel copyBroadcast = new BroadcastChannel();
copyBroadcast.setChannelName(originalBroadcast.getChannelName());
return copyBroadcast;
case ESN:
Esn originalESN = (Esn)id;
Esn copyESN = new Esn();
copyESN.setEsn( originalESN.getEsn() );
return copyESN;
case Fleetsync:
case FLEETSYNC:
FleetsyncID originalFleetsyncID = (FleetsyncID)id;
FleetsyncID copyFleetsyncID = new FleetsyncID();
copyFleetsyncID.setIdent( originalFleetsyncID.getIdent() );
return copyFleetsyncID;
case LTRNetUID:
case LTR_NET_UID:
UniqueID originalUniqueID = (UniqueID)id;
UniqueID copyUniqueID = new UniqueID();
copyUniqueID.setUid( originalUniqueID.getUid() );
return copyUniqueID;
case LoJack:
case LOJACK:
LoJackFunctionAndID originalLoJackFunctionAndID = (LoJackFunctionAndID)id;
LoJackFunctionAndID copyLoJackFunctionAndID = new LoJackFunctionAndID();
copyLoJackFunctionAndID.setFunction( originalLoJackFunctionAndID.getFunction() );
@@ -96,24 +104,24 @@ public class AliasFactory
MPT1327ID copyMPT1327ID = new MPT1327ID();
copyMPT1327ID.setIdent( originalMPT1327ID.getIdent() );
return copyMPT1327ID;
case NonRecordable:
case NON_RECORDABLE:
return new NonRecordable();
case Priority:
case PRIORITY:
Priority originalPriority = (Priority)id;
Priority copyPriority = new Priority();
copyPriority.setPriority( originalPriority.getPriority() );
return copyPriority;
case Site:
case SITE:
SiteID originalSiteID = (SiteID)id;
SiteID copySiteID = new SiteID();
copySiteID.setSite( originalSiteID.getSite() );
return copySiteID;
case Talkgroup:
case TALKGROUP:
TalkgroupID originalTalkgroupID = (TalkgroupID)id;
TalkgroupID copyTalkgroupID = new TalkgroupID();
copyTalkgroupID.setTalkgroup( originalTalkgroupID.getTalkgroup() );
return copyTalkgroupID;
case Status:
case STATUS:
StatusID originalStatusID = (StatusID)id;
StatusID copyStatusID = new StatusID();
copyStatusID.setStatus( originalStatusID.getStatus() );
@@ -183,19 +191,21 @@ public class AliasFactory
return copy;
}
public static Editor<AliasID> getEditor( AliasID aliasID )
public static Editor<AliasID> getEditor( AliasID aliasID, BroadcastModel broadcastModel )
{
if( aliasID != null )
{
switch( aliasID.getType() )
{
case BROADCAST_CHANNEL:
return new BroadcastChannelEditor(aliasID, broadcastModel);
case ESN:
return new ESNEditor( aliasID );
case Fleetsync:
case FLEETSYNC:
return new FleetsyncIDEditor( aliasID );
case LTRNetUID:
case LTR_NET_UID:
return new UniqueIDEditor( aliasID );
case LoJack:
case LOJACK:
return new LoJackIDEditor( aliasID );
case MDC1200:
return new MDC1200IDEditor( aliasID );
@@ -203,15 +213,15 @@ public class AliasFactory
return new MINEditor( aliasID );
case MPT1327:
return new MPT1327IDEditor( aliasID );
case NonRecordable:
case NON_RECORDABLE:
return new NonRecordableEditor( aliasID );
case Priority:
case PRIORITY:
return new PriorityEditor( aliasID );
case Site:
case SITE:
return new SiteIDEditor( aliasID );
case Status:
case STATUS:
return new StatusIDEditor( aliasID );
case Talkgroup:
case TALKGROUP:
return new TalkgroupIDEditor( aliasID );
default:
break;
@@ -245,13 +255,15 @@ public class AliasFactory
{
switch( type )
{
case BROADCAST_CHANNEL:
return new BroadcastChannel();
case ESN:
return new Esn();
case Fleetsync:
case FLEETSYNC:
return new FleetsyncID();
case LTRNetUID:
case LTR_NET_UID:
return new UniqueID();
case LoJack:
case LOJACK:
return new LoJackFunctionAndID();
case MDC1200:
return new MDC1200ID();
@@ -259,15 +271,15 @@ public class AliasFactory
return new Min();
case MPT1327:
return new MPT1327ID();
case NonRecordable:
case NON_RECORDABLE:
return new NonRecordable();
case Priority:
case PRIORITY:
return new Priority();
case Site:
case SITE:
return new SiteID();
case Status:
case STATUS:
return new StatusID();
case Talkgroup:
case TALKGROUP:
return new TalkgroupID();
default:
throw new IllegalArgumentException( "Unrecognized Alias ID type: " + type );

View File

@@ -41,7 +41,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class AliasList implements Listener<AliasEvent>
{
@@ -120,7 +119,7 @@ public class AliasList implements Listener<AliasEvent>
mESN.put(esn, alias);
}
break;
case Fleetsync:
case FLEETSYNC:
String fleetsync = ((FleetsyncID)id).getIdent();
if(fleetsync != null)
@@ -134,7 +133,7 @@ public class AliasList implements Listener<AliasEvent>
mFleetsync.put(fleetsync, alias);
}
break;
case LoJack:
case LOJACK:
mLoJack.put((LoJackFunctionAndID) id, alias);
break;
case MDC1200:
@@ -179,10 +178,10 @@ public class AliasList implements Listener<AliasEvent>
mMobileID.put(min, alias);
}
break;
case LTRNetUID:
case LTR_NET_UID:
mUniqueID.put(((UniqueID)id).getUid(), alias);
break;
case Site:
case SITE:
String siteID = ((SiteID)id).getSite();
if(siteID != null)
@@ -196,10 +195,10 @@ public class AliasList implements Listener<AliasEvent>
mSiteID.put(siteID, alias);
}
break;
case Status:
case STATUS:
mStatus.put(((StatusID) id).getStatus(), alias);
break;
case Talkgroup:
case TALKGROUP:
String tgid = ((TalkgroupID)id).getTalkgroup();
if(tgid != null)
@@ -213,8 +212,9 @@ public class AliasList implements Listener<AliasEvent>
mTalkgroup.put(tgid, alias);
}
break;
case NonRecordable:
case Priority:
case BROADCAST_CHANNEL:
case NON_RECORDABLE:
case PRIORITY:
//We don't maintain lookups for these items
break;
default:
@@ -279,7 +279,7 @@ public class AliasList implements Listener<AliasEvent>
mESN.remove(esn);
break;
case Fleetsync:
case FLEETSYNC:
String fleetsync = ((FleetsyncID)id).getIdent();
if(fleetsync != null)
@@ -292,7 +292,7 @@ public class AliasList implements Listener<AliasEvent>
mFleetsync.remove(fleetsync);
}
break;
case LoJack:
case LOJACK:
mLoJack.remove((LoJackFunctionAndID) id);
break;
case MDC1200:
@@ -334,16 +334,16 @@ public class AliasList implements Listener<AliasEvent>
mMobileID.remove(min);
}
break;
case LTRNetUID:
case LTR_NET_UID:
mUniqueID.remove(((UniqueID) id).getUid());
break;
case Site:
case SITE:
mSiteID.remove(((SiteID) id).getSite());
break;
case Status:
case STATUS:
mStatus.remove(((StatusID) id).getStatus());
break;
case Talkgroup:
case TALKGROUP:
String tgid = ((TalkgroupID)id).getTalkgroup();
if(tgid != null)
@@ -356,8 +356,8 @@ public class AliasList implements Listener<AliasEvent>
mTalkgroup.remove(tgid);
}
break;
case NonRecordable:
case Priority:
case NON_RECORDABLE:
case PRIORITY:
//We don't maintain lookups for these items
break;
default:

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

@@ -21,6 +21,7 @@ import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import alias.id.broadcast.BroadcastChannel;
import playlist.version1.Group;
import alias.id.esn.Esn;
import alias.id.fleetsync.FleetsyncID;
@@ -36,7 +37,7 @@ import alias.id.talkgroup.TalkgroupID;
import alias.id.uniqueID.UniqueID;
@SuppressWarnings( "deprecation" )
@XmlSeeAlso( { Esn.class, FleetsyncID.class, Group.class, LoJackFunctionAndID.class,
@XmlSeeAlso( {BroadcastChannel.class, Esn.class, FleetsyncID.class, Group.class, LoJackFunctionAndID.class,
MDC1200ID.class, Min.class, MPT1327ID.class, NonRecordable.class,
Priority.class, SiteID.class, StatusID.class, TalkgroupID.class,
UniqueID.class } )

View File

@@ -20,20 +20,20 @@ package alias.id;
public enum AliasIDType
{
ESN( "ESN" ),
Fleetsync( "Fleetsync" ),
LoJack( "LoJack" ),
LTRNetUID( "LTR-Net UID" ),
FLEETSYNC( "Fleetsync" ),
LOJACK( "LoJack" ),
LTR_NET_UID( "LTR-Net UID" ),
MDC1200( "MDC-1200" ),
MIN( "Passport MIN" ),
MPT1327( "MPT-1327" ),
Site( "Site" ),
Status( "Status" ),
Talkgroup( "Talkgroup" ),
SITE( "Site" ),
STATUS( "Status" ),
TALKGROUP( "Talkgroup" ),
Invert( "Audio Inversion" ),
Priority( "Audio Priority" ),
NonRecordable( "Audio Non-Recordable" ),
Streaming( "Audio Streaming" );
INVERT( "Audio Inversion" ),
PRIORITY( "Audio Priority" ),
NON_RECORDABLE( "Audio Non-Recordable" ),
BROADCAST_CHANNEL( "Audio Broadcast Channel" );
private String mLabel;

View File

@@ -17,6 +17,7 @@
******************************************************************************/
package alias.id;
import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
@@ -61,10 +62,13 @@ public class AliasIdentifierEditor extends Editor<Alias>
private EditorContainer mEditorContainer = new EditorContainer();
private AliasModel mAliasModel;
private BroadcastModel mBroadcastModel;
public AliasIdentifierEditor( AliasModel model )
public AliasIdentifierEditor( AliasModel aliasModel, BroadcastModel broadcastModel )
{
mAliasModel = model;
mAliasModel = aliasModel;
mBroadcastModel = broadcastModel;
init();
}
@@ -120,45 +124,35 @@ public class AliasIdentifierEditor extends Editor<Alias>
JPopupMenu menu = new JPopupMenu();
menu.add( new AddAliasIdentifierItem( AliasIDType.ESN ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.Fleetsync ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.LoJack ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.LTRNetUID ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.FLEETSYNC) );
menu.add( new AddAliasIdentifierItem( AliasIDType.LOJACK) );
menu.add( new AddAliasIdentifierItem( AliasIDType.LTR_NET_UID) );
menu.add( new AddAliasIdentifierItem( AliasIDType.MDC1200 ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.MPT1327 ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.MIN ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.Site ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.Status ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.Talkgroup ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.SITE) );
menu.add( new AddAliasIdentifierItem( AliasIDType.STATUS) );
menu.add( new AddAliasIdentifierItem( AliasIDType.TALKGROUP) );
menu.addSeparator();
menu.add( new AddAliasIdentifierItem( AliasIDType.BROADCAST_CHANNEL) );
boolean separatorAdded = false;
if( hasItem() )
{
Alias alias = getItem();
if( !alias.hasCallPriority() )
{
if( !separatorAdded )
{
menu.addSeparator();
separatorAdded = true;
}
menu.add( new AddAliasIdentifierItem( AliasIDType.Priority ) );
}
if( alias.isRecordable() )
{
if( !separatorAdded )
{
menu.addSeparator();
separatorAdded = true;
}
menu.add( new AddAliasIdentifierItem( AliasIDType.NonRecordable ) );
menu.add( new AddAliasIdentifierItem( AliasIDType.NON_RECORDABLE) );
}
if( !alias.hasCallPriority() )
{
menu.add( new AddAliasIdentifierItem( AliasIDType.PRIORITY) );
}
}
menu.show( e.getComponent(), e.getX(), e.getY() );
}
} );
@@ -327,7 +321,7 @@ public class AliasIdentifierEditor extends Editor<Alias>
removeAll();
//This will always give us an editor
mEditor = AliasFactory.getEditor( aliasID );
mEditor = AliasFactory.getEditor( aliasID, mBroadcastModel );
add( mEditor );

View File

@@ -0,0 +1,107 @@
/*******************************************************************************
* 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 alias.id.broadcast;
import alias.id.AliasID;
import alias.id.AliasIDType;
import javax.xml.bind.annotation.XmlAttribute;
public class BroadcastChannel extends AliasID implements Comparable<BroadcastChannel>
{
private String mChannelName;
public BroadcastChannel()
{
//JAXB Constructor
}
@Override
public int compareTo(BroadcastChannel other)
{
if(mChannelName != null && other.getChannelName() != null)
{
return mChannelName.compareTo(other.getChannelName());
}
else if(mChannelName != null)
{
return -1;
}
else
{
return 1;
}
}
/**
* Creates a named broadcast channel
*/
public BroadcastChannel(String channelName)
{
mChannelName = channelName;
}
/**
* Name of the broadcastAudio channel configuration
*/
@XmlAttribute(name="channel")
public String getChannelName()
{
return mChannelName;
}
/**
* Sets the name of the broadcastAudio channel configuration
*/
public void setChannelName(String channel)
{
mChannelName = channel;
}
@Override
public AliasIDType getType()
{
return AliasIDType.BROADCAST_CHANNEL;
}
@Override
public boolean isValid()
{
return mChannelName != null;
}
@Override
public boolean matches( AliasID id )
{
return false;
}
@Override
public String toString()
{
if(isValid())
{
return "Broadcast Channel: " + mChannelName;
}
else
{
return "Broadcast Channel: None Selected";
}
}
}

View File

@@ -0,0 +1,129 @@
/*******************************************************************************
* 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 alias.id.broadcast;
import alias.id.AliasID;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastModel;
import gui.editor.DocumentListenerEditor;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
{
private static final long serialVersionUID = 1L;
private static final String HELP_TEXT = "<html>"
+ "<h3>Audio Broadcast Channel</h3>"
+ "Name of a audio streaming broadcast configuration<br>"
+ "to be used for streaming audio to an external<br>"
+ "streaming server";
private JComboBox<String> mBroadcastConfigurations;
private BroadcastModel mBroadcastModel;
public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel)
{
mBroadcastModel = broadcastModel;
initGUI();
setItem( aliasID );
}
private void initGUI()
{
setLayout( new MigLayout( "fill,wrap 1", "[]", "[][]" ) );
add( new JLabel( "Broadcast Channel" ), "grow" );
List<String> channelNames = mBroadcastModel.getBroadcastConfigurationNames();
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
mBroadcastConfigurations.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mBroadcastConfigurations, "grow");
JLabel help = new JLabel( "Help ..." );
help.setForeground( Color.BLUE.brighter() );
help.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
help.addMouseListener( new MouseAdapter()
{
@Override
public void mouseClicked( MouseEvent e )
{
JOptionPane.showMessageDialog( BroadcastChannelEditor.this,
HELP_TEXT, "Help", JOptionPane.INFORMATION_MESSAGE );
}
} );
add( help, "align left" );
}
@Override
public void setItem(AliasID item)
{
super.setItem(item);
if(item instanceof BroadcastChannel)
{
BroadcastChannel channel = (BroadcastChannel)item;
if(channel.isValid())
{
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
}
}
setModified(false);
}
@Override
public void save()
{
String channel = (String)mBroadcastConfigurations.getSelectedItem();
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getBroadcastConfiguration(channel);
if(selectedConfiguration != null)
{
((BroadcastChannel)getItem()).setChannelName(selectedConfiguration.getName());
}
else
{
((BroadcastChannel)getItem()).setChannelName(null);
}
}
}

View File

@@ -74,6 +74,6 @@ public class FleetsyncID extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.Fleetsync;
return AliasIDType.FLEETSYNC;
}
}

View File

@@ -101,7 +101,7 @@ public class LoJackFunctionAndID extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.LoJack;
return AliasIDType.LOJACK;
}
@Override

View File

@@ -8,7 +8,7 @@ public class NonRecordable extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.NonRecordable;
return AliasIDType.NON_RECORDABLE;
}
@Override

View File

@@ -78,6 +78,6 @@ public class Priority extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.Priority;
return AliasIDType.PRIORITY;
}
}

View File

@@ -63,6 +63,6 @@ public class SiteID extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.Site;
return AliasIDType.SITE;
}
}

View File

@@ -70,6 +70,6 @@ public class StatusID extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.Status;
return AliasIDType.STATUS;
}
}

View File

@@ -78,6 +78,6 @@ public class TalkgroupID extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.Talkgroup;
return AliasIDType.TALKGROUP;
}
}

View File

@@ -70,6 +70,6 @@ public class UniqueID extends AliasID
@Override
public AliasIDType getType()
{
return AliasIDType.LTRNetUID;
return AliasIDType.LTR_NET_UID;
}
}

58
src/audio/AudioUtils.java Normal file
View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.wave.RealBufferWaveRecorder;
import sample.ConversionUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
public class AudioUtils
{
private final static Logger mLog = LoggerFactory.getLogger(AudioUtils.class);
/**
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
*/
public static byte[] convertTo16BitSamples(List<AudioPacket> audioPackets)
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try
{
for(AudioPacket packet: audioPackets)
{
//Converting from 32-bit floats to signed 16-bit samples
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
stream.write(buffer.array());
}
}
catch(IOException e)
{
mLog.error("Error writing converted PCM bytes to output stream");
}
return stream.toByteArray();
}
}

View File

@@ -0,0 +1,462 @@
/*******************************************************************************
* 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 audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.convert.ISilenceGenerator;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Listener;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
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 Listener<AudioRecording>
{
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 RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
private List<AudioRecording> mAudioRecordingQueue = new CopyOnWriteArrayList<>();
private ISilenceGenerator mSilenceGenerator;
private Listener<BroadcastEvent> mBroadcastEventListener;
private BroadcastState mBroadcastState = BroadcastState.READY;
private int mStreamedAudioCount = 0;
private BroadcastConfiguration mBroadcastConfiguration;
private long mDelay;
private AtomicBoolean mStreaming = new AtomicBoolean();
/**
* 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;
mDelay = mBroadcastConfiguration.getDelay();
mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat());
}
protected ThreadPoolManager getThreadPoolManager()
{
return mThreadPoolManager;
}
/**
* Broadcast binary audio data frames or sequences.
*/
protected abstract void broadcastAudio(byte[] audio);
/**
* Protocol-specific metadata updater
*/
protected abstract IBroadcastMetadataUpdater getMetadataUpdater();
/**
* Broadcasts the next song's audio metadata prior to streaming the next song.
* @param metadata for the next recording that will be streamed
*/
protected void broadcastMetadata(AudioMetadata metadata)
{
IBroadcastMetadataUpdater metadataUpdater = getMetadataUpdater();
if(metadataUpdater != null)
{
metadataUpdater.update(metadata);
}
}
/**
* Disconnects the broadcaster from the remote server for a reset or final stop.
*/
protected abstract void disconnect();
/**
* Connects to the remote server specified by the broadcast configuration and starts audio streaming.
*/
public void start()
{
if(mStreaming.compareAndSet(false, true))
{
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))
{
if(mThreadPoolManager != null && mScheduledTask != null)
{
mThreadPoolManager.cancel(mScheduledTask);
}
disconnect();
}
}
/**
* Stream name for the broadcast configuration for this broadcaster
* @return stream name or null
*/
public String getStreamName()
{
BroadcastConfiguration config = getBroadcastConfiguration();
if(config != null)
{
return config.getName();
}
return null;
}
/**
* Size of recording queue for recordings awaiting streaming
*/
public int getQueueSize()
{
return mAudioRecordingQueue.size();
}
/**
* Number of audio recordings streamed to remote server
*/
public int getStreamedAudioCount()
{
return mStreamedAudioCount;
}
/**
* Primary insert method for the stream manager to nominate completed audio recordings for broadcast.
*
* @param recording to queue for broadcasting
*/
public void receive(AudioRecording recording)
{
if(connected())
{
mAudioRecordingQueue.add(recording);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
}
/**
* Broadcast configuration used by this broadcaster
*/
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
/**
* Registers the listener to receive broadcast events/state changes
*/
public void setListener(Listener<BroadcastEvent> listener)
{
mBroadcastEventListener = listener;
}
/**
* Removes the listener from receiving broadcast events/state changes
*/
public void removeListener()
{
mBroadcastEventListener = null;
}
/**
* Broadcasts the event to any registered listener
*/
public void broadcast(BroadcastEvent event)
{
if(mBroadcastEventListener != null)
{
mBroadcastEventListener.receive(event);
}
}
/**
* Sets the state of the broadcastAudio connection
*/
protected void setBroadcastState(BroadcastState state)
{
if(mBroadcastState != state)
{
mLog.info("[" + getStreamName() + "] status: " + state);
mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
if(mBroadcastState.isErrorState())
{
stop();
}
if(!connected())
{
//Remove all pending audio recordings
while(!mAudioRecordingQueue.isEmpty())
{
AudioRecording recording = mAudioRecordingQueue.get(0);
recording.removePendingReplay();
}
}
}
}
/**
* Current state of the broadcastAudio connection
*/
public BroadcastState getBroadcastState()
{
return mBroadcastState;
}
/**
* Indicates if the broadcaster is currently connected to the remote server
*/
protected boolean connected()
{
return getBroadcastState() == BroadcastState.CONNECTED;
}
/**
* Indicates if this broadcaster can connect and is not currently in an error state or a connected state.
*/
public boolean canConnect()
{
BroadcastState state = getBroadcastState();
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
* to subclass implementations for broadcast in the appropriate manner.
*/
public class RecordingQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private ByteArrayInputStream mInputStream;
private long mFinalSilencePadding = 0;
private int mBytesStreamedActual = 0;
private int mBytesStreamedRequired = 0;
@Override
public void run()
{
if(mProcessing.compareAndSet(false, true))
{
try
{
if(mInputStream == null || mInputStream.available() <= 0)
{
if(mFinalSilencePadding > 0)
{
broadcastAudio(mSilenceGenerator.generate(mFinalSilencePadding));
mFinalSilencePadding = 0;
}
nextRecording();
}
if(mInputStream != null)
{
//We need to stream at 13.888 fps (144 byte frame) to achieve 2000 Bps or 16 kbps
mBytesStreamedRequired += 2000; //2000 bytes per second for 16 kbps data rate
int bytesToStream = mBytesStreamedRequired - mBytesStreamedActual;
//Trim length to whole-frame intervals (144 byte frame)
bytesToStream -= (bytesToStream % 144);
int length = Math.min(bytesToStream, mInputStream.available());
byte[] audio = new byte[length];
try
{
mBytesStreamedActual += mInputStream.read(audio);
broadcastAudio(audio);
}
catch(IOException ioe)
{
mLog.error("Error reading from in-memory audio recording input stream", ioe);
}
}
else
{
broadcastAudio(mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS));
}
}
catch(Exception e)
{
mLog.error("Error while processing audio streaming queue", e);
}
mProcessing.set(false);
}
}
/**
* Loads the next recording for broadcast
*/
private void nextRecording()
{
mBytesStreamedActual = 0;
mBytesStreamedRequired = 0;
boolean metadataUpdateRequired = false;
if(mInputStream != null)
{
mStreamedAudioCount++;
broadcast(new BroadcastEvent(AudioBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
metadataUpdateRequired = true;
}
mInputStream = null;
AudioRecording nextRecording = null;
//Find the recording with the earliest start time
Iterator<AudioRecording> it = mAudioRecordingQueue.iterator();
while(it.hasNext())
{
AudioRecording recording = it.next();
if(nextRecording == null || (recording.getStartTime() < nextRecording.getStartTime()))
{
nextRecording = recording;
}
}
if(nextRecording != null && nextRecording.getStartTime() + mDelay <= System.currentTimeMillis())
{
mAudioRecordingQueue.remove(nextRecording);
try
{
if(Files.exists(nextRecording.getPath()))
{
byte[] audio = Files.readAllBytes(nextRecording.getPath());
if(audio != null && audio.length > 0)
{
mInputStream = new ByteArrayInputStream(audio);
mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS -
(nextRecording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);
while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
{
mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS;
}
if(connected())
{
broadcastMetadata(nextRecording.getAudioMetadata());
}
metadataUpdateRequired = false;
}
}
}
catch(IOException ioe)
{
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] error reading temporary audio " +
"stream recording [" + nextRecording.getPath().toString() + "] - skipping recording - ", ioe);
mInputStream = null;
metadataUpdateRequired = false;
}
nextRecording.removePendingReplay();
broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
//If we closed out a recording and don't have a new/next recording, send an empty metadata update
if(metadataUpdateRequired && connected())
{
broadcastMetadata(null);
}
}
}
}

View File

@@ -0,0 +1,119 @@
/*******************************************************************************
* 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 audio.metadata.AudioMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
public class AudioRecording implements Comparable<AudioRecording>
{
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
private Path mPath;
private AudioMetadata mAudioMetadata;
private long mStartTime;
private long mRecordingLength;
private AtomicInteger mPendingReplayCount = new AtomicInteger();
/**
* 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 AudioRecording(Path path, AudioMetadata audioMetadata, long start, long recordingLength)
{
mPath = path;
mAudioMetadata = audioMetadata;
mStartTime = start;
mRecordingLength = recordingLength;
}
/**
* Path to the completed audio recording
*/
public Path getPath()
{
return mPath;
}
/**
* Optional audio metadata for the recording.
*/
public AudioMetadata getAudioMetadata()
{
return mAudioMetadata;
}
/**
* Recording start time in milliseconds since epoch
*/
public long getStartTime()
{
return mStartTime;
}
/**
* Recording length in milliseconds
*/
public long getRecordingLength()
{
return mRecordingLength;
}
/**
* Implements comparable for sorting recordings based on start time in ascending order
*/
@Override
public int compareTo(AudioRecording otherRecording)
{
return Long.compare(getStartTime(), otherRecording.getStartTime());
}
/**
* Increments the count of pending replays.
*/
public void addPendingReplay()
{
mPendingReplayCount.incrementAndGet();
}
/**
* Decrements the count of pending replays.
*/
public void removePendingReplay()
{
mPendingReplayCount.decrementAndGet();
}
/**
* Indicates if there are any remaining pending replays. Once the pending replay count is less than or equal to
* zero, the recording can be deleted.
*/
public boolean hasPendingReplays()
{
return mPendingReplayCount.get() > 0;
}
}

View File

@@ -0,0 +1,279 @@
/*******************************************************************************
* 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 audio.broadcast.broadcastify.BroadcastifyConfiguration;
import audio.broadcast.icecast.IcecastHTTPConfiguration;
import audio.broadcast.icecast.IcecastTCPConfiguration;
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@XmlSeeAlso({BroadcastifyConfiguration.class,
IcecastTCPConfiguration.class,
IcecastHTTPConfiguration.class,
ShoutcastV1Configuration.class,
ShoutcastV2Configuration.class})
@XmlRootElement(name = "stream")
public abstract class BroadcastConfiguration
{
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
private String mName;
private String mHost;
private int mPort;
private String mPassword;
private long mDelay;
private boolean mEnabled = true;
public BroadcastConfiguration()
{
//No-arg constructor required for JAXB
}
/**
* Creates a copy/clone of the configuration
*/
public abstract BroadcastConfiguration copyOf();
/**
* Broadcast audio streaming configuration. Describes the streaming server and configuration details.
*/
public BroadcastConfiguration(BroadcastFormat format)
{
mBroadcastFormat = format;
}
/**
* Broadcast server type
*/
@XmlAttribute(name = "type")
public abstract BroadcastServerType getBroadcastServerType();
/**
* Name identifying this broadcastAudio configuration.
*/
@XmlAttribute(name = "name")
public String getName()
{
return mName;
}
/**
* Sets the name name that identifies/describes this configuration
*
* @param name
*/
public void setName(String name)
{
mName = name;
}
/**
* Indicates if this configuration has a name value
*/
public boolean hasName()
{
return mName != null;
}
/**
* BROADCAST server host name.
*/
@XmlAttribute(name = "host")
public String getHost()
{
return mHost;
}
/**
* Host name or IP address of the streaming server
*
* @param host
*/
public void setHost(String host)
{
mHost = host;
}
/**
* Indicates if this configuration has a host value
*/
public boolean hasHost()
{
return mHost != null;
}
/**
* BROADCAST server port number;
*/
@XmlAttribute(name = "port")
public int getPort()
{
return mPort;
}
/**
* Port number of the streaming server to where the broadcastAudio will be directed.
*
* @param port
*/
public void setPort(int port)
{
mPort = port;
}
/**
* Indicates if this configuration contains a port value
*/
public boolean hasPort()
{
return mPort > 0;
}
public SocketAddress getAddress()
{
return new InetSocketAddress(getHost(), getPort());
}
/**
* Password to authenticate with the streaming server.
*/
@XmlAttribute(name = "password")
public String getPassword()
{
return mPassword;
}
/**
* Sets the password used to authenticate the broadcastAudio broadcastAudio user to the streaming server
*
* @param password
*/
public void setPassword(String password)
{
mPassword = password;
}
/**
* Indicates if this configuration has a password value
*/
public boolean hasPassword()
{
return mPassword != null;
}
/**
* Audio broadcastAudio content type (e.g. mp3)
*/
@XmlAttribute(name = "format")
public BroadcastFormat getBroadcastFormat()
{
return mBroadcastFormat;
}
public void setBroadcastFormat(BroadcastFormat format)
{
mBroadcastFormat = format;
}
/**
* Audio broadcastAudio delay from recording start until the audio file is broadcastAudio to the server.
*/
@XmlAttribute(name = "delay")
public long getDelay()
{
return mDelay;
}
/**
* User specified delay in milli-seconds from audio call start until when the audio is broadcastAudio to the server.
* @param delay in milliseconds
*/
public void setDelay(long delay)
{
mDelay = delay;
}
/**
* Indicates if this broadcaster is enable, meaning that it will automatically connect on startup.
*/
@XmlAttribute(name = "enabled")
public boolean isEnabled()
{
return mEnabled;
}
public void setEnabled(boolean enabled)
{
mEnabled = enabled;
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof BroadcastConfiguration))
{
return false;
}
BroadcastConfiguration that = (BroadcastConfiguration) o;
if (getBroadcastFormat() != that.getBroadcastFormat())
{
return false;
}
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null)
{
return false;
}
return getBroadcastServerType() == that.getBroadcastServerType();
}
@Override
public int hashCode()
{
int result = getBroadcastFormat().hashCode();
result = 31 * result + (getName() != null ? getName().hashCode() : 0);
result = 31 * result + getBroadcastServerType().hashCode();
return result;
}
/**
* Indicates if this configuration is valid. A minimal check will ensure that it contains at least a hostname and
* port number.
*/
@XmlTransient
public boolean isValid()
{
return mHost != null && mPort > 0;
}
}

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

@@ -0,0 +1,90 @@
/*******************************************************************************
* 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;
public class BroadcastEvent
{
private BroadcastConfiguration mBroadcastConfiguration;
private AudioBroadcaster mAudioBroadcaster;
private Event mEvent;
/**
* AliasEvent - event describing any changes to an alias
* @param configuration - alias that changed
* @param event - change event
*/
public BroadcastEvent(BroadcastConfiguration configuration, Event event )
{
mBroadcastConfiguration = configuration;
mEvent = event;
}
public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event )
{
mAudioBroadcaster = audioBroadcaster;
mEvent = event;
}
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
public AudioBroadcaster getAudioBroadcaster()
{
return mAudioBroadcaster;
}
/**
* Indicates if this event pertains to an audio broadcaster
*/
public boolean isAudioBroadcasterEvent()
{
return mAudioBroadcaster != null;
}
/**
* Indicates if this event pertains to a broadcast configuration
*/
public boolean isBroadcastConfigurationEvent()
{
return mBroadcastConfiguration != null;
}
public Event getEvent()
{
return mEvent;
}
/**
* Channel events to describe the specific event
*/
public enum Event
{
BROADCASTER_ADD,
BROADCASTER_QUEUE_CHANGE,
BROADCASTER_STATE_CHANGE,
BROADCASTER_STREAMED_COUNT_CHANGE,
BROADCASTER_DELETE,
CONFIGURATION_ADD,
CONFIGURATION_CHANGE,
CONFIGURATION_DELETE;
}
}

View File

@@ -0,0 +1,215 @@
/*******************************************************************************
* 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 audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import audio.broadcast.icecast.IcecastHTTPConfiguration;
import audio.broadcast.icecast.IcecastHTTPConfigurationEditor;
import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
import audio.broadcast.icecast.IcecastTCPConfiguration;
import audio.broadcast.icecast.IcecastTCPConfigurationEditor;
import audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v1.ShoutcastV1ConfigurationEditor;
import audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
import audio.broadcast.shoutcast.v2.ShoutcastV2ConfigurationEditor;
import audio.convert.IAudioConverter;
import audio.convert.ISilenceGenerator;
import audio.convert.MP3AudioConverter;
import audio.convert.MP3SilenceGenerator;
import controller.ThreadPoolManager;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.AudioRecorder;
import record.mp3.MP3Recorder;
import settings.SettingsManager;
import java.nio.file.Path;
public class BroadcastFactory
{
private final static Logger mLog = LoggerFactory.getLogger( BroadcastFactory.class );
public static final int MP3_MONO_16_KHZ_BITRATE = 16;
public static final boolean MP3_CONSTANT_BITRATE = false;
/**
* Creates an audio streaming broadcaster for the configuration
* @param threadPoolManager for creating an audio broadcasting connection thread
* @param configuration describing the server and audio types
* @return configured broadcaster or null
*/
public static AudioBroadcaster getBroadcaster(ThreadPoolManager threadPoolManager,
BroadcastConfiguration configuration)
{
if(configuration != null)
{
IAudioConverter converter = getAudioConverter(configuration);
if(converter != null)
{
switch(configuration.getBroadcastServerType())
{
case BROADCASTIFY:
return new IcecastTCPAudioBroadcaster(threadPoolManager,
(BroadcastifyConfiguration)configuration);
case ICECAST_TCP:
return new IcecastTCPAudioBroadcaster(threadPoolManager,
(IcecastTCPConfiguration)configuration);
case ICECAST_HTTP:
return new IcecastHTTPAudioBroadcaster(threadPoolManager,
(IcecastHTTPConfiguration)configuration);
case SHOUTCAST_V1:
return new ShoutcastV1AudioBroadcaster(threadPoolManager,
(ShoutcastV1Configuration)configuration);
case SHOUTCAST_V2:
return new ShoutcastV2AudioBroadcaster(threadPoolManager,
(ShoutcastV2Configuration)configuration);
case UNKNOWN:
default:
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
break;
}
}
}
return null;
}
/**
* Creates an audio convert to convert from 8 kHz PCM audio to the specified format
* @param configuration containing the requested output audio format
* @return audio convert or null
*/
public static IAudioConverter getAudioConverter(BroadcastConfiguration configuration)
{
switch(configuration.getBroadcastFormat())
{
case MP3:
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
default:
mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
}
return null;
}
/**
* Creates a broadcastAudio configuration for the specified server type and format
*
* @param serverType for the configuration
* @param format for the output audio format
* @return constructed (empty) configuration
*/
public static BroadcastConfiguration getConfiguration(BroadcastServerType serverType, BroadcastFormat format)
{
switch(serverType)
{
case BROADCASTIFY:
return new BroadcastifyConfiguration(format);
case ICECAST_HTTP:
return new IcecastHTTPConfiguration(format);
case ICECAST_TCP:
return new IcecastTCPConfiguration(format);
case SHOUTCAST_V1:
return new ShoutcastV1Configuration(format);
case SHOUTCAST_V2:
return new ShoutcastV2Configuration(format);
case UNKNOWN:
default:
mLog.info("Unrecognized broadcastAudio server type: " + serverType.name());
break;
}
return null;
}
/**
* Constructs an editor for the specified broadcastAudio configuration
* @param configuration to modify or view
* @param broadcastModel model for broadcastAudio configurations
* @return an editor for the specified broadcastAudio configuration
*/
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
BroadcastModel broadcastModel,
AliasModel aliasModel,
SettingsManager settingsManager)
{
Editor<BroadcastConfiguration> editor;
switch (configuration.getBroadcastServerType())
{
case BROADCASTIFY:
editor = new BroadcastifyConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
case ICECAST_TCP:
editor = new IcecastTCPConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
case ICECAST_HTTP:
editor = new IcecastHTTPConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
case SHOUTCAST_V1:
editor = new ShoutcastV1ConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
case SHOUTCAST_V2:
editor = new ShoutcastV2ConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
default:
editor = new EmptyEditor<BroadcastConfiguration>();
break;
}
editor.setItem(configuration);
return editor;
}
/**
* Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
*/
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
{
switch (broadcastFormat)
{
case MP3:
return new MP3Recorder(path);
default:
mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
return null;
}
}
public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format)
{
switch (format)
{
case MP3:
return new MP3SilenceGenerator();
default:
throw new IllegalArgumentException("Unrecognized broadcast format [" + format +
"] can't create silence generator");
}
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* 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;
public enum BroadcastFormat
{
MP3("audio/mpeg", ".mp3");
private String mValue;
private String mFileExtension;
private BroadcastFormat(String value, String fileExtension)
{
mValue = value;
mFileExtension = fileExtension;
}
public String getValue()
{
return mValue;
}
public String getFileExtension()
{
return mFileExtension;
}
}

View File

@@ -0,0 +1,755 @@
/*******************************************************************************
* 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.id.broadcast.BroadcastChannel;
import audio.AudioPacket;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Broadcaster;
import sample.Listener;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import java.io.IOException;
import java.nio.file.DirectoryStream;
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.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger( BroadcastModel.class );
public static final String TEMPORARY_STREAM_DIRECTORY = "streaming";
public static final String TEMPORARY_STREAM_FILE_SUFFIX = "temporary_streaming_file_";
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
public static final int COLUMN_SERVER_ICON = 0;
public static final int COLUMN_STREAM_NAME = 1;
public static final int COLUMN_BROADCASTER_STATUS = 2;
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>();
private List<AudioRecording> mRecordingQueue = new CopyOnWriteArrayList<>();
private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
private ThreadPoolManager mThreadPoolManager;
private SettingsManager mSettingsManager;
private StreamManager mStreamManager;
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
/**
* Model for managing Broadcast configurations and any associated broadcaster instances.
*/
public BroadcastModel(ThreadPoolManager threadPoolManager, SettingsManager settingsManager)
{
mThreadPoolManager = threadPoolManager;
mSettingsManager = settingsManager;
mStreamManager = new StreamManager(threadPoolManager, new CompletedRecordingListener(), BroadcastFormat.MP3,
SystemProperties.getInstance().getApplicationFolder(TEMPORARY_STREAM_DIRECTORY));
mStreamManager.start();
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_RECORDING,
new RecordingDeletionMonitor(), 15l, TimeUnit.SECONDS );
removeOrphanedTemporaryRecordings();
}
/**
* List of broadcastAudio configuration names
*/
public List<String> getBroadcastConfigurationNames()
{
List<String> names = new ArrayList<>();
for(BroadcastConfiguration configuration: mBroadcastConfigurations)
{
names.add(configuration.getName());
}
return names;
}
/**
* Current list of broadcastAudio configurations
*/
public List<BroadcastConfiguration> getBroadcastConfigurations()
{
return mBroadcastConfigurations;
}
/**
* Adds the list of broadcastAudio configurations to this model
*/
public void addBroadcastConfigurations(List<BroadcastConfiguration> configurations)
{
for(BroadcastConfiguration configuration: configurations)
{
addBroadcastConfiguration(configuration);
}
}
/**
* Adds the broadcastAudio configuration to this model
*/
public void addBroadcastConfiguration(BroadcastConfiguration configuration)
{
if(configuration != null)
{
ensureUniqueName(configuration);
if(!mBroadcastConfigurations.contains(configuration))
{
mBroadcastConfigurations.add(configuration);
int index = mBroadcastConfigurations.size() - 1;
fireTableRowsInserted( index, index );
mBroadcastConfigurationMap.put(configuration.getName(), configuration);
process( new BroadcastEvent( configuration, BroadcastEvent.Event.CONFIGURATION_ADD) );
}
}
}
/**
* Clones the configuration and adds it this model with a unique configuration name
*/
public BroadcastConfiguration cloneBroadcastConfiguration(BroadcastConfiguration configuration)
{
if(configuration != null)
{
BroadcastConfiguration clone = configuration.copyOf();
addBroadcastConfiguration(clone);
return clone;
}
return null;
}
/**
* Updates the configuration's name so that it is unique among all other broadcast configurations
* @param configuration
*/
private void ensureUniqueName(BroadcastConfiguration configuration)
{
if(configuration.getName() == null || configuration.getName().isEmpty())
{
configuration.setName("New Configuration");
}
while(!isUniqueName(configuration.getName(), configuration))
{
String currentName = configuration.getName();
if(currentName.matches(UNIQUE_NAME_REGEX))
{
int currentVersion = 1;
StringBuilder sb = new StringBuilder();
Matcher m = Pattern.compile(UNIQUE_NAME_REGEX).matcher(currentName);
if(m.find())
{
String version = m.group(2);
try
{
currentVersion = Integer.parseInt(version);
}
catch(Exception e)
{
//Couldn't parse the version number -- keep incrementing until we find a winner
}
currentVersion++;
sb.append(m.group(1)).append("(").append(currentVersion).append(")");
}
else
{
sb.append(configuration.getName()).append("(").append(currentVersion).append(")");
}
configuration.setName(sb.toString());
}
else
{
StringBuilder sb = new StringBuilder();
sb.append(configuration.getName()).append("(2)");
configuration.setName(sb.toString());
}
}
}
/**
* Indicates if the name is unique among all of the broadcast configurations. Checks each of the configurations
* in this model for a name collision. Assumes that the name argument will be assigned to the configuration
* argument if the name is unique, therefore, the name will not be checked against the configuration argument for
* a collision.
*
* @param name to check for uniqueness
* @param configuration to ignore when checking the name for uniqueness
* @return true if the name is not null and unique among all configurations managed by this model
*/
public boolean isUniqueName(String name, BroadcastConfiguration configuration)
{
if(name == null || name.isEmpty())
{
return false;
}
for(BroadcastConfiguration configurationToCompare: mBroadcastConfigurations)
{
if(configurationToCompare != configuration &&
configurationToCompare.getName() != null &&
configurationToCompare.getName().equals(name))
{
return false;
}
}
return true;
}
public void removeBroadcastConfiguration(BroadcastConfiguration broadcastConfiguration)
{
if(broadcastConfiguration != null && mBroadcastConfigurations.contains(broadcastConfiguration))
{
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
mBroadcastConfigurations.remove(broadcastConfiguration);
mBroadcastConfigurationMap.remove(broadcastConfiguration.getName());
process(new BroadcastEvent( broadcastConfiguration, BroadcastEvent.Event.CONFIGURATION_DELETE));
fireTableRowsDeleted( index, index );
}
}
/**
* Returns the broadcaster associated with the stream name or null if there is no broadcaster setup for the name.
*/
public AudioBroadcaster getBroadcaster(String streamName)
{
return mBroadcasterMap.get(streamName);
}
@Override
public void receive(AudioPacket audioPacket)
{
if (audioPacket.hasAudioMetadata() &&
audioPacket.getAudioMetadata().isStreamable())
{
mStreamManager.receive(audioPacket);
}
}
/**
* Creates a new broadcaster for the broadcast configuration and adds it to the model
*/
private void createBroadcaster(BroadcastConfiguration broadcastConfiguration)
{
if (broadcastConfiguration != null &&
broadcastConfiguration.isEnabled() &&
broadcastConfiguration.isValid() &&
!mBroadcasterMap.containsKey(broadcastConfiguration.getName()))
{
AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(mThreadPoolManager,
broadcastConfiguration);
if (audioBroadcaster != null)
{
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
{
process(broadcastEvent);
}
});
audioBroadcaster.start();
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
{
fireTableRowsUpdated(index, index);
}
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
}
}
}
/**
* Shut down a broadcaster created from the configuration and remove it from this model
*/
private void deleteBroadcaster(String name)
{
if(name != null && mBroadcasterMap.containsKey(name))
{
AudioBroadcaster audioBroadcaster = mBroadcasterMap.remove(name);
if(audioBroadcaster != null)
{
audioBroadcaster.stop();
audioBroadcaster.removeListener();
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
{
fireTableRowsUpdated(index, index);
}
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
}
}
}
/**
* Returns the broadcast configuration identified by the stream name
*/
public BroadcastConfiguration getBroadcastConfiguration(String streamName)
{
return mBroadcastConfigurationMap.get(streamName);
}
/**
* Registers the listener to receive broadcastAudio configuration events
*/
public void addListener(Listener<BroadcastEvent> listener)
{
mBroadcastEventBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving broadcastAudio configuration events
*/
public void removeListener(Listener<BroadcastEvent> listener)
{
mBroadcastEventBroadcaster.removeListener(listener);
}
/**
* Broadcasts the broadcastAudio configuration change event
*/
private void broadcast(BroadcastEvent event)
{
mBroadcastEventBroadcaster.broadcast( event );
}
/**
* Process a broadcast event from one of the broadcasters managed by this model
*/
public void process(BroadcastEvent broadcastEvent)
{
if(broadcastEvent.isBroadcastConfigurationEvent())
{
switch(broadcastEvent.getEvent())
{
case CONFIGURATION_ADD:
createBroadcaster(broadcastEvent.getBroadcastConfiguration());
break;
case CONFIGURATION_CHANGE:
BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
fireTableRowsUpdated( index, index );
//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());
break;
}
}
else if(broadcastEvent.isAudioBroadcasterEvent())
{
int row = mBroadcastConfigurations.indexOf(broadcastEvent.getAudioBroadcaster().getBroadcastConfiguration());
switch(broadcastEvent.getEvent())
{
case BROADCASTER_QUEUE_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_QUEUE_SIZE);
}
break;
case BROADCASTER_STATE_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_STATUS);
}
break;
case BROADCASTER_STREAMED_COUNT_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
}
break;
}
}
//Rebroadcast the event to any listeners of this model
broadcast(broadcastEvent);
}
/**
* When the name of a broadcast configuration changes, remove the old map associations so that they can be replaced
* 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 String cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
{
String oldName = null;
for(Map.Entry<String,BroadcastConfiguration> entry: mBroadcastConfigurationMap.entrySet())
{
if(entry.getValue() == broadcastConfiguration)
{
oldName = entry.getKey();
continue;
}
}
if(oldName != null)
{
mBroadcastConfigurationMap.remove(oldName);
}
mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
return oldName;
}
@Override
public int getRowCount()
{
return mBroadcastConfigurations.size();
}
@Override
public int getColumnCount()
{
return 5;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex)
{
try
{
if( rowIndex <= mBroadcastConfigurations.size())
{
BroadcastConfiguration configuration = mBroadcastConfigurations.get(rowIndex);
if(configuration != null)
{
switch(columnIndex)
{
case COLUMN_SERVER_ICON:
String iconName = configuration.getBroadcastServerType().getIconName();
if(iconName != null && mSettingsManager != null)
{
return mSettingsManager.getImageIcon(iconName, 14);
}
break;
case COLUMN_STREAM_NAME:
return configuration.getName();
case COLUMN_BROADCASTER_STATUS:
AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterA != null)
{
return audioBroadcasterA.getBroadcastState();
}
else if(!configuration.isEnabled())
{
return BroadcastState.DISABLED;
}
else if(!configuration.isValid())
{
return BroadcastState.INVALID_SETTINGS;
}
case COLUMN_BROADCASTER_QUEUE_SIZE:
AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterB != null)
{
return audioBroadcasterB.getQueueSize();
}
break;
case COLUMN_BROADCASTER_STREAMED_COUNT:
AudioBroadcaster audioBroadcasterC = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterC != null)
{
return audioBroadcasterC.getStreamedAudioCount();
}
break;
default:
break;
}
}
}
}
catch(Exception e)
{
mLog.error("Error accessing data in broadcast model", e);
}
return null;
}
@Override
public Class<?> getColumnClass(int columnIndex)
{
if(columnIndex == COLUMN_SERVER_ICON)
{
return ImageIcon.class;
}
return String.class;
}
/**
* Broadcast configuration at the specified model row
*/
public BroadcastConfiguration getConfigurationAt(int rowIndex)
{
return mBroadcastConfigurations.get(rowIndex);
}
/**
* Model row number for the specified configuration
*/
public int getRowForConfiguration(BroadcastConfiguration configuration)
{
return mBroadcastConfigurations.indexOf(configuration);
}
@Override
public String getColumnName(int column)
{
switch(column)
{
case COLUMN_SERVER_ICON:
return null;
case COLUMN_STREAM_NAME:
return "Name";
case COLUMN_BROADCASTER_STATUS:
return "Status";
case COLUMN_BROADCASTER_QUEUE_SIZE:
return "Queued";
case COLUMN_BROADCASTER_STREAMED_COUNT:
return "Streamed";
}
return null;
}
/**
* Cleanup method to remove a temporary recording file from disk.
*
* @param recording to remove
*/
private void removeRecording(AudioRecording recording)
{
try
{
Files.delete(recording.getPath());
}
catch(IOException ioe)
{
mLog.error("Error deleting temporary internet recording file: " + recording.getPath().toString() + " - " +
ioe.getMessage());
}
}
/**
* Removes any temporary stream recordings left-over from the previous application run.
*
* This should only be invoked on startup.
*/
private void removeOrphanedTemporaryRecordings()
{
Path path = SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY);
if(path != null && Files.isDirectory(path))
{
StringBuilder sb = new StringBuilder();
sb.append(TEMPORARY_STREAM_FILE_SUFFIX);
sb.append("*.*");
try(DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path, sb.toString()))
{
directoryStream.forEach(new Consumer<Path>()
{
@Override
public void accept(Path path)
{
try
{
Files.delete(path);
}
catch(IOException ioe)
{
mLog.error("Couldn't delete orphaned temporary recording: " + path.toString(), ioe);
}
}
});
}
catch(IOException ioe)
{
mLog.error("Error discovering orphaned temporary stream recording files", ioe);
}
}
}
/**
* 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);
}
}
/**
* Processes completed audio recordings and distributes them to the audio broadcasters. Adds the recording to
* the audio recording queue to be monitored for deletion.
*/
public class CompletedRecordingListener implements Listener<AudioRecording>
{
@Override
public void receive(AudioRecording audioRecording)
{
AudioMetadata metadata = audioRecording.getAudioMetadata();
if(metadata != null && metadata.isStreamable())
{
for (BroadcastChannel broadcastChannel : metadata.getBroadcastChannels())
{
String channelName = broadcastChannel.getChannelName();
if (channelName != null)
{
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
if (audioBroadcaster != null)
{
audioRecording.addPendingReplay();
audioBroadcaster.receive(audioRecording);
}
}
}
}
mRecordingQueue.add(audioRecording);
}
}
/**
* Monitors the recording queue and removes any recordings that have no pending replays by audio broadcasters
*/
public class RecordingDeletionMonitor implements Runnable
{
@Override
public void run()
{
try
{
List<AudioRecording> recordingsToDelete = new ArrayList<>();
Iterator<AudioRecording> it = mRecordingQueue.iterator();
AudioRecording recording;
while(it.hasNext())
{
recording = it.next();
if(!recording.hasPendingReplays())
{
recordingsToDelete.add(recording);
}
}
if(!recordingsToDelete.isEmpty())
{
for(AudioRecording recordingToDelete: recordingsToDelete)
{
mRecordingQueue.remove(recordingToDelete);
removeRecording(recordingToDelete);
}
}
}
catch(Exception e)
{
mLog.error("Error while checking audio recording queue for recordings to delete", e);
}
}
}
}

View File

@@ -0,0 +1,332 @@
/*******************************************************************************
* 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 com.jidesoft.swing.JideSplitPane;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
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 javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
{
private final static Logger mLog = LoggerFactory.getLogger( BroadcastPanel.class );
private static final String NEW_BROADCAST_CONFIGURATION = "New";
private static final String COPY_BROADCAST_CONFIGURATION = "Copy";
private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
private BroadcastModel mBroadcastModel;
private AliasModel mAliasModel;
private SettingsManager mSettingsManager;
private JTable mBroadcastConfigurationTable;
private JideSplitPane mSplitPane;
private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:");
private Editor<BroadcastConfiguration> mEditor;
private JButton mNewButton = new JButton(NEW_BROADCAST_CONFIGURATION);
private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
public BroadcastPanel(BroadcastModel broadcastModel, AliasModel aliasModel, SettingsManager settingManager)
{
mBroadcastModel = broadcastModel;
mAliasModel = aliasModel;
mSettingsManager = settingManager;
mEditor = mEmptyEditor;
init();
}
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
mBroadcastConfigurationTable = new JTable(mBroadcastModel);
mBroadcastConfigurationTable.setAutoCreateRowSorter(true);
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
mBroadcastConfigurationTable.getColumnModel().getColumn(BroadcastModel.COLUMN_SERVER_ICON).setPreferredWidth(1);
mBroadcastConfigurationTable.getColumnModel().getColumn(BroadcastModel.COLUMN_STREAM_NAME).setPreferredWidth(25);
mBroadcastConfigurationTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS)
.setCellRenderer(new StatusCellRenderer());
JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
JPanel buttonsPanel = new JPanel();
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
mNewButton = new JButton("New ...");
mNewButton.setToolTipText("Create a new broadcast audio configuration");
mNewButton.addMouseListener(new MouseAdapter()
{
@Override
public void mouseClicked(MouseEvent e)
{
JPopupMenu menu = new JPopupMenu();
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.BROADCASTIFY));
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_TCP));
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_HTTP));
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.SHOUTCAST_V1));
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.SHOUTCAST_V2));
menu.show(e.getComponent(), e.getX(), e.getY());
}
});
buttonsPanel.add(mNewButton);
mCopyButton.addActionListener(this);
mCopyButton.setEnabled(false);
mCopyButton.setToolTipText("Creates a copy of the currently selected streaming configuration and adds it");
buttonsPanel.add(mCopyButton);
mDeleteButton.addActionListener(this);
mDeleteButton.setEnabled(false);
mDeleteButton.setToolTipText("Deletes the currently selected streaming configuration");
buttonsPanel.add(mDeleteButton);
JPanel listAndButtonsPanel = new JPanel();
listAndButtonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill][]"));
listAndButtonsPanel.add(scroller, "wrap");
listAndButtonsPanel.add(buttonsPanel);
mSplitPane = new JideSplitPane(JideSplitPane.HORIZONTAL_SPLIT);
mSplitPane.setDividerSize(5);
mSplitPane.setShowGripper(true);
mSplitPane.add(listAndButtonsPanel);
mSplitPane.add(mEditor);
add(mSplitPane);
}
@Override
public void actionPerformed(ActionEvent e)
{
switch(e.getActionCommand())
{
case COPY_BROADCAST_CONFIGURATION:
BroadcastConfiguration copySelected = getSelectedBroadcastConfiguration();
mBroadcastModel.cloneBroadcastConfiguration(copySelected);
break;
case DELETE_BROADCAST_CONFIGURATION:
BroadcastConfiguration deleteSelected = getSelectedBroadcastConfiguration();
if(deleteSelected != null)
{
int choice = JOptionPane.showConfirmDialog(BroadcastPanel.this, "Do you want to delete broadcast\n" +
" configuration " + deleteSelected.getName() + "?", "Delete Broadcast Configuration?",
JOptionPane.YES_NO_OPTION);
if(choice == JOptionPane.YES_OPTION)
{
mBroadcastModel.removeBroadcastConfiguration(deleteSelected);
}
}
break;
}
}
/**
* Returns the selected configuration
*/
private BroadcastConfiguration getSelectedBroadcastConfiguration()
{
int viewRow = mBroadcastConfigurationTable.getSelectedRow();
if(viewRow >= 0)
{
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(viewRow);
if(modelRow >= 0)
{
return mBroadcastModel.getConfigurationAt(modelRow);
}
}
return null;
}
@Override
public void valueChanged(ListSelectionEvent e)
{
if (!e.getValueIsAdjusting())
{
BroadcastConfiguration selectedConfiguration = getSelectedBroadcastConfiguration();
//Enable buttons accordingly
mCopyButton.setEnabled(selectedConfiguration != null);
mDeleteButton.setEnabled(selectedConfiguration != null);
if (mEditor.isModified())
{
int option = JOptionPane.showConfirmDialog(
BroadcastPanel.this,
"Configuration settings have changed. Do you want to save these changes?",
"Save Changes?",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (option == JOptionPane.YES_OPTION)
{
mEditor.save();
}
}
mSplitPane.removePane(mEditor);
if (selectedConfiguration != null)
{
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mAliasModel,
mSettingsManager);
}
else
{
mEditor = mEmptyEditor;
}
mSplitPane.add(mEditor);
revalidate();
}
}
public class AddBroadcastConfigurationItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private BroadcastServerType mBroadcastServerType;
public AddBroadcastConfigurationItem(BroadcastServerType type)
{
super(type.toString(), mSettingsManager.getImageIcon(type.getIconName(), 14));
mBroadcastServerType = type;
setToolTipText("Add a new " + type.toString() + " audio stream configuration");
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
final BroadcastConfiguration configuration =
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
if(configuration != null)
{
mBroadcastModel.addBroadcastConfiguration(configuration);
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
int tableRow = mBroadcastConfigurationTable.convertRowIndexToView(modelRow);
mBroadcastConfigurationTable.changeSelection(tableRow, 0, false, false);
}
}
});
}
}
/**
* Custom cell renderer for the broadcast state column.
*/
public class StatusCellRenderer extends DefaultTableCellRenderer
{
public StatusCellRenderer()
{
setOpaque(true);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column)
{
JLabel component = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if(isSelected)
{
setBackground(table.getSelectionBackground());
setForeground(table.getSelectionForeground());
}
else
{
if(value instanceof BroadcastState)
{
BroadcastState state = (BroadcastState)value;
if(state == BroadcastState.CONNECTED)
{
setBackground(Color.GREEN);
setForeground(table.getForeground());
}
else if(state == BroadcastState.DISABLED)
{
setBackground(table.getBackground());
setForeground(Color.LIGHT_GRAY);
}
else if(state == BroadcastState.INVALID_SETTINGS)
{
setBackground(Color.YELLOW);
setForeground(table.getForeground());
}
else if(state.isErrorState())
{
setBackground(Color.RED);
setForeground(table.getForeground());
}
else
{
setBackground(table.getBackground());
setForeground(table.getForeground());
}
}
else
{
setForeground(table.getForeground());
setBackground(table.getBackground());
}
}
return this;
}
}
}

View File

@@ -0,0 +1,48 @@
/*******************************************************************************
* 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;
public enum BroadcastServerType
{
BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
UNKNOWN("Unknown", null);
private String mLabel;
private String mIconName;
BroadcastServerType(String label, String iconName)
{
mLabel = label;
mIconName = iconName;
}
public String toString()
{
return mLabel;
}
public String getIconName()
{
return mIconName;
}
}

View File

@@ -0,0 +1,116 @@
/*******************************************************************************
* 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;
public enum BroadcastState
{
/**
* General error in configuration that causes the remote server to reject the connection
*/
CONFIGURATION_ERROR("Configuration Error", true),
/**
* Connected to the broadcastAudio server and capable of streaming
*/
CONNECTED("Connected", false),
/**
* Connection interrupted, attempting to reconnect.
*/
CONNECTING("Connecting", false),
/**
* Indicates the configuration is disabled
*/
DISABLED("Disabled", true),
/**
* Disconnected from the broadcastAudio server
*/
DISCONNECTED("Disconnected", false),
/**
* Invalid credentials - user name or password
*/
INVALID_CREDENTIALS("Invalid User Name/Password", true),
/**
* Invalid mount point
*/
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
/**
* Invalid configuration settings
*/
INVALID_SETTINGS("Invalid Settings", true),
/**
* Remote server max sources has been exceeded
*/
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
/**
* Specified mount point is already in use
*/
MOUNT_POINT_IN_USE("Mount Point In Use", true),
/**
* Server is not known or reachable
*/
NO_SERVER("No Server", false),
/**
* Initial state with no connection attempted.
*/
READY("Ready", false),
/**
* Error while broadcasting stream data. Temporary error state to allow connection to be reset.
*/
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false),
/**
* Unsupported audio format
*/
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true),
/**
* Unspecified error
*/
ERROR("Error", true);
private String mLabel;
private boolean mErrorState;
private BroadcastState(String label, boolean connected)
{
mLabel = label;
mErrorState = connected;
}
public String toString()
{
return mLabel;
}
public boolean isErrorState()
{
return mErrorState;
}
}

View File

@@ -0,0 +1,31 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 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 audio.metadata.AudioMetadata;
public interface IBroadcastMetadataUpdater
{
/**
* Updates broadcast audio metadata
*
* @param metadata containing metadata attributes
*/
void update(AudioMetadata metadata);
}

View File

@@ -0,0 +1,238 @@
/*******************************************************************************
* 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 audio.AudioPacket;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.AudioRecorder;
import sample.Listener;
import util.TimeStamp;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class StreamManager implements Listener<AudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
private static AtomicInteger sNextRecordingNumber = new AtomicInteger();
private ThreadPoolManager mThreadPoolManager;
private Listener<AudioRecording> mAudioRecordingListener;
private BroadcastFormat mBroadcastFormat;
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
* to desired output format and persists each stream to disc. Each recording is capped at a maximum length to
* 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 broadcast) upon completion
*
* @param threadPoolManager for scheduling runnables
* @param listener to receive completed audio recordings
* @param tempDirectory where to store temporary audio recordings
*/
public StreamManager(ThreadPoolManager threadPoolManager, Listener<AudioRecording> listener,
BroadcastFormat broadcastFormat, Path tempDirectory)
{
assert(tempDirectory != null && Files.isDirectory(tempDirectory));
assert(mThreadPoolManager != null);
mThreadPoolManager = threadPoolManager;
mAudioRecordingListener = listener;
mBroadcastFormat = broadcastFormat;
mTempDirectory = tempDirectory;
}
/**
* Starts the stream manager. Schedules a processor to monitor for inactive temporary stream recorders
* every 2 seconds.
*/
public void start()
{
if(mRunning.compareAndSet(false, true))
{
if(mRecorderMonitor == null)
{
mRecorderMonitor = new RecorderMonitor();
}
mRecorderMonitorFuture = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
mRecorderMonitor, 2, TimeUnit.SECONDS);
}
}
/**
* Stops the stream manager. Stops the inactive temporary stream recorder monitoring thread.
*/
public void stop()
{
if(mRunning.compareAndSet(true, false))
{
if(mRecorderMonitorFuture != null)
{
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(mRunning.get() && audioPacket.hasAudioMetadata())
{
synchronized (mStreamRecorders)
{
int sourceChannelID = audioPacket.getAudioMetadata().getSource();
AudioPacket.Type type = audioPacket.getType();
if(type == AudioPacket.Type.AUDIO)
{
if(mStreamRecorders.containsKey(sourceChannelID))
{
AudioRecorder recorder = mStreamRecorders.get(sourceChannelID);
if(recorder != null)
{
recorder.receive(audioPacket);
}
}
else
{
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
mBroadcastFormat);
recorder.start(mThreadPoolManager.getScheduledExecutorService());
recorder.receive(audioPacket);
mStreamRecorders.put(sourceChannelID, recorder);
}
}
else if(type == AudioPacket.Type.END)
{
removeRecorder(sourceChannelID);
}
else
{
mLog.info("Unrecognized Audio Packet Type: " + type);
}
}
}
}
/**
* Removes the recorder associated with the source channel ID.
*
* Note: this method invocation is not thread safe and must be invoked by a thread safe mechanism that protects the
* mStreamRecorders map.
*
* @param sourceChannelID identifying the recorder
*/
private void removeRecorder(Integer sourceChannelID)
{
if(mStreamRecorders.containsKey(sourceChannelID))
{
AudioRecorder recorder = mStreamRecorders.remove(sourceChannelID);
recorder.close(new Listener<AudioRecorder>()
{
@Override
public void receive(AudioRecorder audioRecorder)
{
AudioRecording audioRecording =
new AudioRecording(audioRecorder.getPath(), audioRecorder.getMetadata(),
audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength());
if(mAudioRecordingListener != null)
{
mAudioRecordingListener.receive(audioRecording);
}
}
});
}
}
/**
* Creates a temporary streaming recording file path
*/
private Path getTemporaryRecordingPath()
{
StringBuilder sb = new StringBuilder();
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
sb.append(sNextRecordingNumber.incrementAndGet()).append("_");
sb.append( TimeStamp.getLongTimeStamp( "_" ) );
sb.append( mBroadcastFormat.getFileExtension() );
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
return temporaryRecordingPath;
}
/**
* Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording.
*/
public class RecorderMonitor implements Runnable
{
@Override
public void run()
{
synchronized (mStreamRecorders)
{
long now = System.currentTimeMillis();
mStreamRecorders.entrySet().stream()
.filter(entry -> entry.getValue().getTimeRecordingStart() + MAXIMUM_RECORDER_LIFESPAN_MILLIS < now)
.forEach(entry ->
{
mLog.debug("Maximum recording time limit reached - removing recorder: " +
entry.getValue().getPath().toString());
removeRecorder(entry.getKey());
});
}
}
}
}

View File

@@ -0,0 +1,96 @@
/*******************************************************************************
* 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 audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.icecast.IcecastTCPConfiguration;
import javax.xml.bind.annotation.XmlAttribute;
public class BroadcastifyConfiguration extends IcecastTCPConfiguration
{
private int mFeedID;
public BroadcastifyConfiguration()
{
//No-arg constructor for JAXB
this(BroadcastFormat.MP3);
}
/**
* Broadcastify configuration for Icecast 2.3.2 compatible servers
*
* @param format of output audio (MP3)
*/
public BroadcastifyConfiguration(BroadcastFormat format)
{
super(format);
setBitRate(16);
setChannels(1);
setSampleRate(8000);
}
@Override
public BroadcastConfiguration copyOf()
{
BroadcastifyConfiguration copy = new BroadcastifyConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
//Broadcastify Configuration Parameters
copy.setFeedID(getFeedID());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.BROADCASTIFY;
}
@XmlAttribute(name="feed_id")
public int getFeedID()
{
return mFeedID;
}
public void setFeedID(int feedID)
{
mFeedID = feedID;
}
}

View File

@@ -0,0 +1,369 @@
/*******************************************************************************
* 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 alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import com.radioreference.api.soap2.UserFeedBroadcast;
import gui.editor.DocumentListenerEditor;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class BroadcastifyConfigurationEditor extends BroadcastConfigurationEditor implements Listener<UserFeedBroadcast>
{
private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
private static final int ONE_MINUTE_MS = 60000;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mMountPoint;
private JTextField mPassword;
private JTextField mFeedID;
private JSlider mDelay;
private JLabel mDelayValue;
private JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
public BroadcastifyConfigurationEditor(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("Broadcastify Stream");
ImageIcon broadcastifyIcon = mSettingsManager.getImageIcon(BroadcastServerType.BROADCASTIFY.getIconName(), 25);
channelLabel.setIcon(broadcastifyIcon);
add(channelLabel, "span, align center");
JButton lookupButton = new JButton("Lookup ...");
lookupButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
final UserFeedSelectionDialog dialog = new UserFeedSelectionDialog(BroadcastifyConfigurationEditor.this);
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
dialog.setLocationRelativeTo(BroadcastifyConfigurationEditor.this);
dialog.setVisible(true);
}
});
}
});
add(new JLabel(""));
add(lookupButton, "grow,wrap");
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);
add(new JLabel("Feed ID:"));
mFeedID = new JTextField();
mFeedID.getDocument().addDocumentListener(this);
mFeedID.setToolTipText("Feed ID (optional). See MyBCFY feed details");
add(mFeedID);
add(new JLabel());
mDelayValue = new JLabel("0 Minutes");
add(mDelayValue);
add(new JLabel("Delay:"));
mDelay = new JSlider(0,60,0);
mDelay.setMajorTickSpacing(10);
mDelay.setMinorTickSpacing(5);
mDelay.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mDelayValue.setText(mDelay.getValue() + " Minutes");
setModified(true);
}
});
mDelay.setToolTipText("Audio broadcast delay in minutes");
add(mDelay);
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(new JLabel());
add(mEnabled);
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\n 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())
{
BroadcastifyConfiguration config = (BroadcastifyConfiguration)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());
mFeedID.setText(String.valueOf(config.getFeedID()));
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mMountPoint.setText(null);
mPassword.setText(null);
mFeedID.setText(null);
mDelay.setValue(0);
mEnabled.setSelected(false);
}
setModified(false);
}
@Override
public void save()
{
BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
if(validateConfiguration())
{
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setMountPoint(mMountPoint.getText());
config.setPassword(mPassword.getText());
config.setFeedID(getFeedID());
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
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(BroadcastifyConfigurationEditor.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;
}
/**
* Parses an Integer from the port 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;
}
@Override
public void receive(UserFeedBroadcast userFeedBroadcast)
{
mName.setText(userFeedBroadcast.getDescr());
mServer.setText(userFeedBroadcast.getHostname());
mPort.setText(userFeedBroadcast.getPort());
mMountPoint.setText(userFeedBroadcast.getMount());
mPassword.setText(userFeedBroadcast.getPassword());
mFeedID.setText(String.valueOf(userFeedBroadcast.getFeedId()));
}
}

View File

@@ -0,0 +1,247 @@
/*******************************************************************************
* 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 com.radioreference.api.soap2.UserFeedBroadcast;
import external.radioreference.RadioReferenceService;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.xml.rpc.ServiceException;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
public class UserFeedSelectionDialog extends JDialog implements DocumentListener
{
private final static Logger mLog = LoggerFactory.getLogger( UserFeedSelectionDialog.class );
private Listener<UserFeedBroadcast> mListener;
private JTextField mUserNameText;
private JTextField mPasswordText;
private JButton mLookupButton;
private JList mUserFeedBroadcastList;
private JButton mOKButton;
public UserFeedSelectionDialog(Listener<UserFeedBroadcast> listener)
{
mListener = listener;
init();
}
private void init()
{
JPanel panel = new JPanel();
panel.setLayout(new MigLayout("","[align right,grow,fill][align left,grow,fill][]","[][][][][grow][]"));
panel.add(new JLabel("Please enter your credentials"), "align left,span");
panel.add(new JLabel("User Name:"));
mUserNameText = new JTextField();
mUserNameText.getDocument().addDocumentListener(this);
panel.add(mUserNameText, "wrap");
panel.add(new JLabel("Password:"));
mPasswordText = new JTextField();
mPasswordText.getDocument().addDocumentListener(this);
panel.add(mPasswordText, "wrap");
mLookupButton = new JButton("Get Feeds");
mLookupButton.setEnabled(false);
mLookupButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mLookupButton.setEnabled(false);
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
try
{
RadioReferenceService service =
new RadioReferenceService(mUserNameText.getText(), mPasswordText.getText());
UserFeedBroadcast[] feeds = service.getUserFeedBroadcasts();
mUserFeedBroadcastList.setListData(feeds);
}
catch(ServiceException se)
{
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
"Unable to create radio reference web service client. See log file.",
"Error", JOptionPane.ERROR_MESSAGE);
mLog.error("Error creating the radio reference web service client", se);
}
catch(RemoteException re)
{
Throwable cause = re.getCause();
if(cause == null)
{
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
re.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
else if(cause instanceof UnknownHostException)
{
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
"Broadcastify server is unavailable", "No Server",
JOptionPane.ERROR_MESSAGE);
}
else
{
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
"An unknown error has occurred. Please see log file.", "" +
"Unknown Error", JOptionPane.ERROR_MESSAGE);
mLog.error("Error retrieving user broadcast feed configurations from radio reference " +
"service: ", re);
}
}
mLookupButton.setEnabled(true);
}
});
}
});
panel.add(mLookupButton,"grow,span");
mUserFeedBroadcastList = new JList();
mUserFeedBroadcastList.setVisibleRowCount(3);
mUserFeedBroadcastList.setLayoutOrientation(JList.VERTICAL);
mUserFeedBroadcastList.setCellRenderer(new CellRenderer());
mUserFeedBroadcastList.addListSelectionListener(new ListSelectionListener()
{
@Override
public void valueChanged(ListSelectionEvent e)
{
mOKButton.setEnabled(mUserFeedBroadcastList.getSelectedValue() != null);
}
});
JScrollPane listScrollPane = new JScrollPane(mUserFeedBroadcastList);
panel.add(listScrollPane, "grow,span");
mOKButton = new JButton("Ok");
mOKButton.setEnabled(false);
mOKButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
if(mListener != null)
{
mListener.receive((UserFeedBroadcast)mUserFeedBroadcastList.getSelectedValue());
}
dispose();
}
});
panel.add(mOKButton,"grow");
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
dispose();
}
});
panel.add(cancelButton, "wrap");
setTitle("Broadcastify Feed Lookup");
setLayout(new BorderLayout());
add(panel, BorderLayout.CENTER);
setSize(300,250);
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setModalityType(ModalityType.APPLICATION_MODAL);
}
private void updateButtons()
{
String username = mUserNameText.getText();
String password = mPasswordText.getText();
mLookupButton.setEnabled(username != null && !username.isEmpty() && password != null && !password.isEmpty());
}
@Override
public void insertUpdate(DocumentEvent e)
{
updateButtons();
}
@Override
public void removeUpdate(DocumentEvent e)
{
updateButtons();
}
@Override
public void changedUpdate(DocumentEvent e)
{
updateButtons();
}
private class CellRenderer extends JLabel implements ListCellRenderer<UserFeedBroadcast>
{
public CellRenderer()
{
setOpaque(true);
setHorizontalAlignment(LEFT);
setVerticalAlignment(CENTER);
}
@Override
public Component getListCellRendererComponent(JList<? extends UserFeedBroadcast> list,
UserFeedBroadcast value,
int index,
boolean isSelected,
boolean cellHasFocus)
{
if (isSelected)
{
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
}
else
{
setBackground(list.getBackground());
setForeground(list.getForeground());
}
setText(value.getDescr());
return this; }
}
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* 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 audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.IBroadcastMetadataUpdater;
import controller.ThreadPoolManager;
public abstract class IcecastAudioBroadcaster extends AudioBroadcaster
{
private IBroadcastMetadataUpdater mMetadataUpdater;
public IcecastAudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
{
super(threadPoolManager, broadcastConfiguration);
}
/**
* Icecast broadcast configuration
*/
protected IcecastConfiguration getConfiguration()
{
return (IcecastConfiguration)getBroadcastConfiguration();
}
@Override
protected IBroadcastMetadataUpdater getMetadataUpdater()
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getThreadPoolManager(), getConfiguration());
}
return mMetadataUpdater;
}
}

View File

@@ -0,0 +1,281 @@
/*******************************************************************************
* 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 audio.broadcast.IBroadcastMetadataUpdater;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ThreadPoolManager;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.http.HttpClientCodec;
import org.apache.mina.http.HttpRequestImpl;
import org.apache.mina.http.api.HttpMethod;
import org.apache.mina.http.api.HttpRequest;
import org.apache.mina.http.api.HttpVersion;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class);
private final static String UTF8 = "UTF-8";
private ThreadPoolManager mThreadPoolManager;
private IcecastConfiguration mIcecastConfiguration;
private NioSocketConnector mSocketConnector;
private AtomicBoolean mUpdating = new AtomicBoolean();
private Queue<String> mMetadataQueue = new LinkedTransferQueue<>();
private Map<String, String> mHTTPHeaders;
private boolean mStackTraceLoggingSuppressed;
/**
* Icecast song metadata updater. Each metadata update is processed in the order received and the dispatch of
* the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
* sequence, those updates will be queued and processed in the order received.
*/
public IcecastBroadcastMetadataUpdater(ThreadPoolManager threadPoolManager, IcecastConfiguration icecastConfiguration)
{
mThreadPoolManager = threadPoolManager;
mIcecastConfiguration = icecastConfiguration;
}
/**
* Socket connector - lazy constructor.
*/
private NioSocketConnector getSocketConnector()
{
if (mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastBroadcastMetadataUpdater.class));
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
//Each metadata update session is single-use, so we'll shut it down upon success or failure
mSocketConnector.setHandler(new IoHandlerAdapter()
{
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
//Single-use session - close it after we receive an error
session.closeNow();
mLog.error("Metadata update failed");
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception
{
//Single-use session - close it after we receive a response
session.closeNow();
}
});
}
return mSocketConnector;
}
/**
* Sends a song metadata update to the remote server. Additional metadata updates received while the updater
* is in the process of sending an existing metadata update will be queued and processed in received order.
*
* Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
* and will remain in the update queue until the next metadata update is requested. However, this is a design
* trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
*/
public void update(AudioMetadata audioMetadata)
{
mMetadataQueue.offer(getSong(audioMetadata));
if (mUpdating.compareAndSet(false, true))
{
String song = mMetadataQueue.poll();
while (song != null)
{
HttpRequest updateRequest = createUpdateRequest(song);
if (updateRequest != null)
{
mThreadPoolManager.scheduleOnce(new Runnable()
{
@Override
public void run()
{
try
{
ConnectFuture connectFuture = getSocketConnector()
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
mIcecastConfiguration.getPort()));
connectFuture.awaitUninterruptibly();
IoSession session = connectFuture.getSession();
if (session != null)
{
session.write(updateRequest);
}
}
catch (Exception e)
{
Throwable throwableCause = e.getCause();
if (throwableCause instanceof ConnectException)
{
//Do nothing, the server is unavailable
}
else
{
if (!mStackTraceLoggingSuppressed)
{
mLog.error("Error sending metadata update. Future errors will " +
"be suppressed", e);
mStackTraceLoggingSuppressed = true;
}
}
}
}
}, 0l, TimeUnit.SECONDS);
}
//Fetch next metadata update to send
song = mMetadataQueue.poll();
}
mUpdating.set(false);
}
}
/**
* Creates the song information for a metadata update
*/
private static String getSong(AudioMetadata audioMetadata)
{
StringBuilder sb = new StringBuilder();
if(audioMetadata != null)
{
Metadata to = audioMetadata.getMetadata(MetadataType.TO);
sb.append("TO:");
if(to != null)
{
if(to.hasAlias())
{
sb.append(to.getAlias().getName());
}
else
{
sb.append(to.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
Metadata from = audioMetadata.getMetadata(MetadataType.FROM);
sb.append(" FROM:");
if(from != null)
{
if(from.hasAlias())
{
sb.append(from.getAlias().getName());
}
else
{
sb.append(from.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
}
else
{
sb.append("Scanning ....");
}
return sb.toString();
}
/**
* Creates an HTTP GET request to update the metadata on the remote server
*/
private HttpRequest createUpdateRequest(String song)
{
try
{
StringBuilder sb = new StringBuilder();
sb.append("mode=updinfo");
sb.append("&mount=").append(mIcecastConfiguration.getMountPoint());
sb.append("&charset=UTF%2d8");
sb.append("&song=").append(URLEncoder.encode(song, UTF8));
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
sb.toString(), getHTTPHeaders());
return request;
}
catch (UnsupportedEncodingException e)
{
//This should never happen
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
}
return null;
}
/**
* HTTP headers to use for a metadata update request.
*/
private Map<String,String> getHTTPHeaders()
{
if(mHTTPHeaders == null)
{
mHTTPHeaders = new HashMap<>();
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials());
}
return mHTTPHeaders;
}
}

View File

@@ -0,0 +1,292 @@
/*******************************************************************************
* 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 audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import java.util.Base64;
public abstract class IcecastConfiguration extends BroadcastConfiguration
{
protected String mUserName = "source";
private String mMountPoint = "/stream";
private String mDescription;
private String mGenre;
private boolean mPublic;
private int mBitRate = 16;
private int mChannels = 1;
private int mSampleRate = 8000;
private String mURL;
public IcecastConfiguration(BroadcastFormat format)
{
super(format);
}
public IcecastConfiguration()
{
}
/**
* Base64 encoded version of the username and password with prepended 'Basic ' tag.
*/
@XmlTransient
public String getBase64EncodedCredentials()
{
StringBuilder sb = new StringBuilder();
if(hasUserName())
{
sb.append(getUserName()).append(":").append(getPassword());
}
else
{
sb.append(getPassword());
}
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
return "Basic " + base64;
}
@XmlAttribute( name="user_name" )
public String getUserName()
{
return mUserName;
}
public void setUserName(String userName)
{
mUserName = userName;
}
public boolean hasUserName()
{
return mUserName != null;
}
/**
* Mount point or path to the stream
* @return mount point
*/
@XmlAttribute( name="mount_point" )
public String getMountPoint()
{
return mMountPoint;
}
/**
* Sets the mount point (path) for the stream
* @param mountPoint
*/
public void setMountPoint(String mountPoint)
{
if(mountPoint != null && !mountPoint.isEmpty())
{
if(mountPoint.startsWith("/"))
{
mMountPoint = mountPoint;
}
else
{
mMountPoint = "/" + mountPoint;
}
}
}
public boolean hasMountPoint()
{
return mMountPoint != null;
}
@XmlAttribute( name="description" )
public String getDescription()
{
return mDescription;
}
public void setDescription(String description)
{
mDescription = description;
}
public boolean hasDescription()
{
return mDescription != null;
}
/**
* Stream genre
*/
@XmlAttribute( name="genre" )
public String getGenre()
{
return mGenre;
}
/**
* Stream genre
* @param genre
*/
public void setGenre(String genre)
{
mGenre = genre;
}
public boolean hasGenre()
{
return mGenre != null;
}
/**
* Public visibility of the broadcastAudio
*/
@XmlAttribute( name="public" )
public boolean isPublic()
{
return mPublic;
}
/**
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
mPublic = isPublic;
}
/**
* Number of audio channels in the broadcastAudio
*/
@XmlAttribute( name="channels" )
public int getChannels()
{
return mChannels;
}
/**
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
mChannels = channels;
}
public boolean hasChannels()
{
return mChannels > 0;
}
/**
* Bit rate in bits per second
*/
@XmlAttribute( name="bitrate" )
public int getBitRate()
{
return mBitRate;
}
/**
* Bit rate
* @param bitRate in samples per second
*/
public void setBitRate(int bitRate)
{
mBitRate = bitRate;
}
public boolean hasBitRate()
{
return mBitRate > 0;
}
@XmlAttribute( name="sample_rate" )
public int getSampleRate()
{
return mSampleRate;
}
public void setSampleRate(int sampleRate)
{
mSampleRate = sampleRate;
}
public boolean hasSampleRate()
{
return mSampleRate > 0;
}
/**
* URL associated with the broadcastAudio where users can find additional details.
*/
@XmlAttribute( name="url" )
public String getURL()
{
return mURL;
}
/**
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)
{
mURL = url;
}
public boolean hasURL()
{
return mURL != null;
}
@Override
public boolean isValid()
{
if(!super.isValid())
{
return false;
}
else if(getUserName() == null)
{
return false;
}
else if(getMountPoint() == null)
{
return false;
}
else if(getChannels() != 1)
{
return false;
}
else if(getSampleRate() <= 0)
{
return false;
}
else if(getBitRate() <= 0)
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,329 @@
/*******************************************************************************
* 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 audio.broadcast.BroadcastState;
import audio.convert.MP3AudioConverter;
import controller.ThreadPoolManager;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderException;
import org.apache.mina.http.HttpClientCodec;
import org.apache.mina.http.HttpException;
import org.apache.mina.http.HttpRequestImpl;
import org.apache.mina.http.api.DefaultHttpResponse;
import org.apache.mina.http.api.HttpMethod;
import org.apache.mina.http.api.HttpStatus;
import org.apache.mina.http.api.HttpVersion;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
private static final Logger mLog = LoggerFactory.getLogger( IcecastHTTPAudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private Map<String, String> mHTTPHeaders;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
* compatible with Icecast version 2.3.2 and older versions of the server software.
*
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
*
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
* IcecastHTTPIOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
*
* @param configuration for the Icecast stream
*/
public IcecastHTTPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastHTTPConfiguration configuration)
{
super(threadPoolManager, configuration);
}
/**
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
IoBuffer buffer = IoBuffer.allocate(audio.length);
buffer.put(audio);
buffer.flip();
mStreamingSession.write(buffer);
}
}
/**
* (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.
*
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected() && canConnect() &&
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
mConnecting.compareAndSet(false, true))
{
mLastConnectionAttempt = System.currentTimeMillis();
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(IcecastTCPAudioBroadcaster.class));
mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec());
mSocketConnector.setHandler(new IcecastHTTPIOHandler());
}
mStreamingSession = null;
Runnable runnable = new Runnable()
{
@Override
public void run()
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
mStreamingSession = future.getSession();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect - no exception is available");
}
disconnect();
}
}
};
getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
}
return connected();
}
/**
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect()
{
if(mStreamingSession != null)
{
mStreamingSession.closeNow();
}
mConnecting.set(false);
}
/**
* IO Handler for managing Icecast HTTP connection and credentials
*/
public class IcecastHTTPIOHandler extends IoHandlerAdapter
{
@Override
public void sessionOpened(IoSession session) throws Exception
{
//Send stream configuration and user credentials upon connecting to remote server
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.PUT,
getConfiguration().getMountPoint(), "", getHTTPHeaders());
session.write(request);
}
private Map<String,String> getHTTPHeaders()
{
if(mHTTPHeaders == null)
{
mHTTPHeaders = new HashMap<>();
mHTTPHeaders.put(IcecastHeader.ACCEPT.getValue(), "*/*");
mHTTPHeaders.put(IcecastHeader.CONTENT_TYPE.getValue(), getConfiguration().getBroadcastFormat().getValue());
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getBase64EncodedCredentials());
mHTTPHeaders.put(IcecastHeader.PUBLIC.getValue(), getConfiguration().isPublic() ? "1" : "0");
StringBuilder sb = new StringBuilder();
sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
sb.append("channels=").append(getConfiguration().getChannels());
mHTTPHeaders.put(IcecastHeader.AUDIO_INFO.getValue(), sb.toString());
if(getConfiguration().hasName())
{
mHTTPHeaders.put(IcecastHeader.NAME.getValue(), getConfiguration().getName());
}
if(getConfiguration().hasDescription())
{
mHTTPHeaders.put(IcecastHeader.DESCRIPTION.getValue(), getConfiguration().getDescription());
}
if(getConfiguration().hasURL())
{
mHTTPHeaders.put(IcecastHeader.URL.getValue(), getConfiguration().getURL());
}
if(getConfiguration().hasGenre())
{
mHTTPHeaders.put(IcecastHeader.GENRE.getValue(), getConfiguration().getGenre());
}
if(getConfiguration().hasBitRate())
{
mHTTPHeaders.put(IcecastHeader.BITRATE.getValue(), String.valueOf(getConfiguration().getBitRate()));
}
}
return mHTTPHeaders;
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
mSocketConnector.dispose();
mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false);
super.sessionClosed(session);
}
@Override
public void exceptionCaught(IoSession session, Throwable throwable) throws Exception
{
if(throwable instanceof ProtocolDecoderException)
{
Throwable cause = ((ProtocolDecoderException)throwable).getCause();
if(cause instanceof HttpException &&
((HttpException)cause).getStatusCode() == HttpStatus.CLIENT_ERROR_LENGTH_REQUIRED.code())
{
//Ignore - Icecast 2.4 sometimes doesn't send any headers with their HTTP responses. Mina expects a
//content-length for HTTP responses.
}
else
{
mLog.error("HTTP protocol decoder error", throwable);
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
disconnect();
}
}
else
{
mLog.error("Broadcast error", throwable);
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
disconnect();
}
mConnecting.set(false);
}
@Override
public void messageReceived(IoSession session, Object object) throws Exception
{
if(object instanceof DefaultHttpResponse)
{
DefaultHttpResponse response = (DefaultHttpResponse)object;
switch(response.getStatus())
{
case INFORMATIONAL_CONTINUE:
break;
case SUCCESS_OK:
setBroadcastState(BroadcastState.CONNECTED);
mConnecting.set(false);
break;
case CLIENT_ERROR_UNAUTHORIZED:
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
disconnect();
break;
case CLIENT_ERROR_FORBIDDEN:
setBroadcastState(BroadcastState.CONFIGURATION_ERROR);
disconnect();
break;
default:
setBroadcastState(BroadcastState.ERROR);
disconnect();
mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
break;
}
}
else
{
mLog.error("Icecast HTTP broadcaster - unrecognized message [ " + object.getClass() +
"] received:" + object.toString());
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* 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 audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import java.util.Base64;
public class IcecastHTTPConfiguration extends IcecastConfiguration
{
public IcecastHTTPConfiguration()
{
//no-arg JAXB constructor
this(BroadcastFormat.MP3);
}
/**
* Icecast 2.4.x+ compatible configuration
*/
public IcecastHTTPConfiguration(BroadcastFormat format)
{
super(format);
setPassword("change me!");
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastHTTPConfiguration copy = new IcecastHTTPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setBitRate(getBitRate());
copy.setChannels(getChannels());
copy.setSampleRate(getSampleRate());
copy.setURL(getURL());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.ICECAST_HTTP;
}
}

View File

@@ -0,0 +1,351 @@
/*******************************************************************************
* 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 net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class IcecastHTTPConfigurationEditor extends BroadcastConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPConfigurationEditor.class );
private static final int ONE_MINUTE_MS = 60000;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mMountPoint;
private JTextField mUsername;
private JTextField mPassword;
private JTextField mDescription;
private JTextField mGenre;
private JCheckBox mPublic;
private JSlider mDelay;
private JLabel mDelayValue;
private JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
public IcecastHTTPConfigurationEditor(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 (v2.4+) Stream");
ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.ICECAST_HTTP.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("User Name:"));
mUsername = new JTextField();
mUsername.getDocument().addDocumentListener(this);
mUsername.setToolTipText("User name for the stream. Default: source");
add(mUsername);
add(new JLabel("Password:"));
mPassword = new JTextField();
mPassword.getDocument().addDocumentListener(this);
mPassword.setToolTipText("Password for the stream");
add(mPassword);
add(new JLabel("Description:"));
mDescription = new JTextField();
mDescription.getDocument().addDocumentListener(this);
mDescription.setToolTipText("Description of the stream contents");
add(mDescription);
add(new JLabel("Genre:"));
mGenre = new JTextField();
mGenre.getDocument().addDocumentListener(this);
mGenre.setToolTipText("Genre (e.g. public safety)");
add(mGenre);
add(new JLabel("Public:"));
mPublic = new JCheckBox();
mPublic.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
mPublic.setToolTipText("Designate stream as public (checked) or private");
add(mPublic);
add(new JLabel());
mDelayValue = new JLabel("0 Minutes");
add(mDelayValue);
add(new JLabel("Delay:"));
mDelay = new JSlider(0,60,0);
mDelay.setMajorTickSpacing(10);
mDelay.setMinorTickSpacing(5);
mDelay.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mDelayValue.setText(mDelay.getValue() + " Minutes");
setModified(true);
}
});
mDelay.setToolTipText("Audio broadcast delay in minutes");
add(mDelay);
add(new JLabel("Enabled:"));
mEnabled = new JCheckBox();
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled);
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())
{
IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
mName.setText(config.getName());
mServer.setText(config.getHost());
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mMountPoint.setText(config.getMountPoint());
mUsername.setText(config.getUserName());
mPassword.setText(config.getPassword());
mDescription.setText(config.getDescription());
mGenre.setText(config.getGenre());
mPublic.setSelected(config.isPublic());
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mMountPoint.setText(null);
mUsername.setText(null);
mPassword.setText(null);
mDescription.setText(null);
mGenre.setText(null);
mPublic.setSelected(true);
mDelay.setValue(0);
mEnabled.setSelected(false);
}
setModified(false);
}
@Override
public void save()
{
IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
if(validateConfiguration())
{
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setMountPoint(mMountPoint.getText());
config.setUserName(mUsername.getText());
config.setPassword(mPassword.getText());
config.setDescription(mDescription.getText());
config.setGenre(mGenre.getText());
config.setPublic(mPublic.isSelected());
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
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(IcecastHTTPConfigurationEditor.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 required
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
{
return false;
}
//User name is required
if(!validateTextField(mUsername, "Invalid User Name", "Please specify a user name."))
{
return false;
}
//Password is required
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
{
return false;
}
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

@@ -0,0 +1,50 @@
/*******************************************************************************
* 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;
public enum IcecastHeader
{
ACCEPT("Accept"),
AUDIO_INFO("ice-audio-info"),
AUTHORIZATION("Authorization"),
BITRATE("ice-bitrate"),
CONTENT_TYPE("Content-Type"),
DESCRIPTION("ice-description"),
EXPECT("Expect"),
GENRE("ice-genre"),
HOST("Host"),
NAME("ice-name"),
PUBLIC("ice-public"),
URL("ice-url"),
USER_AGENT("User-Agent"),
UNKNOWN("unknown");
private String mValue;
private IcecastHeader(String value)
{
mValue = value;
}
public String getValue()
{
return mValue;
}
}

View File

@@ -0,0 +1,296 @@
/*******************************************************************************
* 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 audio.broadcast.BroadcastState;
import audio.broadcast.icecast.codec.IcecastCodecFactory;
import audio.convert.MP3AudioConverter;
import controller.ThreadPoolManager;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPAudioBroadcaster.class );
private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
* compatible with Icecast version 2.3.2 and older versions of the server software.
*
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
*
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
* ShoutcastV2IOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
*
* @param configuration for the Icecast stream
*/
public IcecastTCPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastTCPConfiguration configuration)
{
super(threadPoolManager, configuration);
}
/**
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
mStreamingSession.write(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.
*
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected() && canConnect() &&
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
mConnecting.compareAndSet(false, true))
{
mLastConnectionAttempt = System.currentTimeMillis();
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(IcecastTCPAudioBroadcaster.class));
mSocketConnector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new IcecastCodecFactory()));
mSocketConnector.setHandler(new IcecastTCPIOHandler());
}
mStreamingSession = null;
Runnable runnable = new Runnable()
{
@Override
public void run()
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
mStreamingSession = future.getSession();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect - no exception is available");
}
disconnect();
}
}
};
getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
}
return connected();
}
/**
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect()
{
if(connected())
{
if(mStreamingSession != null)
{
mStreamingSession.closeNow();
}
}
}
/**
* IO Handler for managing Icecast TCP connection and credentials
*/
public class IcecastTCPIOHandler extends IoHandlerAdapter
{
/**
* Sends stream configuration and user credentials upon connecting to remote server
*/
@Override
public void sessionOpened(IoSession session) throws Exception
{
StringBuilder sb = new StringBuilder();
sb.append("SOURCE ").append(getConfiguration().getMountPoint());
sb.append(" HTTP/1.0").append(TERMINATOR);
sb.append("Authorization: ").append(getConfiguration().getBase64EncodedCredentials()).append(TERMINATOR);
sb.append(IcecastHeader.USER_AGENT.getValue()).append(SEPARATOR)
.append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
sb.append(IcecastHeader.AUDIO_INFO.getValue()).append(SEPARATOR);
sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
sb.append("channels=").append(getConfiguration().getChannels()).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)
.append(getConfiguration().getGenre()).append(TERMINATOR);
}
if(getConfiguration().hasDescription())
{
sb.append(IcecastHeader.DESCRIPTION.getValue()).append(SEPARATOR)
.append(getConfiguration().getDescription()).append(TERMINATOR);
}
sb.append(TERMINATOR).append(TERMINATOR);
session.write(sb.toString());
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
mSocketConnector.dispose();
mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false);
super.sessionClosed(session);
}
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
if(cause instanceof IOException && ((IOException)cause).getMessage().startsWith("Connection reset"))
{
mLog.info("Streaming connection reset by remote server - reestablishing connection");
disconnect();
connect();
}
else
{
mLog.error("Broadcast error", cause);
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
mConnecting.set(false);
}
@Override
public void messageReceived(IoSession session, Object object) throws Exception
{
if(object instanceof String)
{
String message = (String)object;
if(message != null && !message.trim().isEmpty())
{
if(message.startsWith("HTTP/1.0 200 OK"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if(message.startsWith("HTTP/1.0 403 Mountpoint in use"))
{
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
}
else if(message.contains("Invalid Password") ||
message.contains("Authentication Required"))
{
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
}
else
{
mLog.error("Unrecognized server response:" + message);
setBroadcastState(BroadcastState.ERROR);
}
}
}
else
{
mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
"] received:" + object.toString());
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*******************************************************************************
* 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 audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.annotation.XmlSeeAlso;
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
public class IcecastTCPConfiguration extends IcecastConfiguration
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
public IcecastTCPConfiguration()
{
this(BroadcastFormat.MP3);
}
/**
* Icecast 2.3.x and 2.4.x compatible configuration
* @param format of audio
*/
public IcecastTCPConfiguration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastTCPConfiguration copy = new IcecastTCPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.ICECAST_TCP;
}
}

View File

@@ -0,0 +1,337 @@
/*******************************************************************************
* 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 javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
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 static final int ONE_MINUTE_MS = 60000;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mMountPoint;
private JTextField mPassword;
private JTextField mDescription;
private JTextField mGenre;
private JCheckBox mPublic;
private JSlider mDelay;
private JLabel mDelayValue;
private JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
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 (v2.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);
add(new JLabel("Description:"));
mDescription = new JTextField();
mDescription.getDocument().addDocumentListener(this);
mDescription.setToolTipText("Description of the stream contents");
add(mDescription);
add(new JLabel("Genre:"));
mGenre = new JTextField();
mGenre.getDocument().addDocumentListener(this);
mGenre.setToolTipText("Genre (e.g. public safety)");
add(mGenre);
add(new JLabel("Public:"));
mPublic = new JCheckBox();
mPublic.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
mPublic.setToolTipText("Designate stream as public (checked) or private");
add(mPublic);
add(new JLabel());
mDelayValue = new JLabel("0 Minutes");
add(mDelayValue);
add(new JLabel("Delay:"));
mDelay = new JSlider(0,60,0);
mDelay.setMajorTickSpacing(10);
mDelay.setMinorTickSpacing(5);
mDelay.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mDelayValue.setText(mDelay.getValue() + " Minutes");
setModified(true);
}
});
mDelay.setToolTipText("Audio broadcast delay in minutes");
add(mDelay);
add(new JLabel("Enabled:")); //Empty
mEnabled = new JCheckBox();
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled);
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());
mDescription.setText(config.getDescription());
mGenre.setText(config.getGenre());
mPublic.setSelected(config.isPublic());
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mMountPoint.setText(null);
mPassword.setText(null);
mDescription.setText(null);
mGenre.setText(null);
mPublic.setSelected(true);
mDelay.setValue(0);
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.setDescription(mDescription.getText());
config.setGenre(mGenre.getText());
config.setPublic(mPublic.isSelected());
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
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

@@ -0,0 +1,63 @@
/*******************************************************************************
* 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.codec;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineDecoder;
import java.nio.charset.StandardCharsets;
public class IcecastCodecFactory implements ProtocolCodecFactory
{
private ProtocolEncoder mProtocolEncoder;
private ProtocolDecoder mProtocolDecoder;
/**
* Factory for codecs to process Icecast string protocol and byte array audio frames for use with Apache Mina
*/
public IcecastCodecFactory()
{
}
@Override
public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception
{
if(mProtocolEncoder == null)
{
mProtocolEncoder = new IcecastEncoder();
}
return mProtocolEncoder;
}
@Override
public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception
{
if(mProtocolDecoder == null)
{
mProtocolDecoder = new TextLineDecoder(StandardCharsets.UTF_8, LineDelimiter.AUTO);
}
return mProtocolDecoder;
}
}

View File

@@ -0,0 +1,94 @@
/*******************************************************************************
* 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.codec;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineEncoder;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
public class IcecastEncoder extends ProtocolEncoderAdapter
{
private static final AttributeKey ENCODER = new AttributeKey(TextLineEncoder.class, "encoder");
private static final Charset mCharset = StandardCharsets.UTF_8;
private static final int MAX_LINE_LENGTH = 2147483647;
/**
* Icecast Protocol Encoder is an Apache Mina compliant protocol encoder that supports Icecast string-based
* connection negotiation as well as byte array audio frame data.
*/
public IcecastEncoder()
{
}
@Override
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception
{
//Audio frame data
if(message instanceof byte[])
{
byte[] data = (byte[])message;
IoBuffer buf = IoBuffer.allocate(data.length).setAutoExpand(false);
buf.put(data);
buf.flip();
out.write(buf);
}
//Icecast connection messages
else if(message instanceof String)
{
CharsetEncoder encoder = (CharsetEncoder)session.getAttribute(ENCODER);
if(encoder == null)
{
encoder = mCharset.newEncoder();
session.setAttribute(ENCODER, encoder);
}
String value = (String)message;
IoBuffer buf = IoBuffer.allocate(value.length()).setAutoExpand(true);
buf.putString(value, encoder);
if(buf.position() > MAX_LINE_LENGTH)
{
throw new IllegalArgumentException("Line length: " + buf.position());
}
else
{
buf.putString(LineDelimiter.UNIX.getValue(), encoder);
buf.flip();
out.write(buf);
}
}
else
{
throw new IllegalArgumentException("Can't encode - unrecognized object type: " + message.getClass());
}
}
}

View File

@@ -0,0 +1,95 @@
/*******************************************************************************
* 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.shoutcast.v1;
import audio.broadcast.BroadcastFormat;
public enum ShoutcastMetadata
{
AIM("icy-aim:"),
AUDIO_BIT_RATE("icy-br:"),
CHANNELS("icy-channels:"),
CONTENT_TYPE("Content-Type:"),
DESCRIPTION("icy-description:"),
GENRE("icy-genre:"),
ICE_AUDIO_INFO("ice-audio-info:"),
ICQ("icy-icq:"),
INTERNET_RELAY_CHAT("icy-irc:"),
METADATA_INTERVAL("icy-metaint:"),
METADATA_REQUEST("icy-metadata:"),
PRE_BUFFER("icy-prebuffer:"),
PUBLIC("icy-pub:"),
RESET("icy-reset:"),
SERVER("server:"),
STREAM_NAME("icy-name:"),
SONG_TITLE("icy-title:"),
URL("icy-url:"),
USER_AGENT("User-Agent:"),
UNKNOWN("Unknown");
private String mTag;
private ShoutcastMetadata(String tag)
{
mTag = tag;
}
public static final String COMMAND_TERMINATOR = "\n";
public String toString()
{
return mTag;
}
/**
* Encodes the metadata tag and value with an end-of-line character, appropriate for sending over the network.
*/
public String encode(String value)
{
if(value != null && !value.isEmpty())
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(value).append(COMMAND_TERMINATOR);
return sb.toString();
}
return null;
}
public String encode(boolean value)
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(value ? "1" : "0").append(COMMAND_TERMINATOR);
return sb.toString();
}
public String encode(BroadcastFormat broadcastFormat)
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(broadcastFormat.getValue()).append(COMMAND_TERMINATOR);
return sb.toString();
}
public String encode(int value)
{
StringBuilder sb = new StringBuilder();
sb.append(mTag).append(String.valueOf(value)).append(COMMAND_TERMINATOR);
return sb.toString();
}
}

View File

@@ -0,0 +1,292 @@
/*******************************************************************************
* 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.shoutcast.v1;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastState;
import audio.broadcast.IBroadcastMetadataUpdater;
import controller.ThreadPoolManager;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1AudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private IBroadcastMetadataUpdater mMetadataUpdater;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
* Creates a Shoutcast v1 compatible broadcaster using TCP protocol.
*
* Note: use @see ShoutcastV2AudioBroadcaster for Shoutcast version 2.x and newer.
*
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
* ShoutcastV1IOHandler manages all interaction with the Shoutcast server and manages the overall broadcast state.
*
* @param configuration for the Shoutcast stream
*/
public ShoutcastV1AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration configuration)
{
super(threadPoolManager, configuration);
}
/**
* Shoutcast V1 broadcast configuration
*/
private ShoutcastV1Configuration getConfiguration()
{
return (ShoutcastV1Configuration)getBroadcastConfiguration();
}
@Override
protected IBroadcastMetadataUpdater getMetadataUpdater()
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getThreadPoolManager(), getConfiguration());
}
return mMetadataUpdater;
}
/**
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
IoBuffer buf = IoBuffer.allocate(audio.length).setAutoExpand(false);
buf.put(audio);
buf.flip();
mStreamingSession.write(buf);
}
}
/**
* (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.
*
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected() && canConnect() &&
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
mConnecting.compareAndSet(false, true))
{
mLastConnectionAttempt = System.currentTimeMillis();
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(ShoutcastV1AudioBroadcaster.class));
mSocketConnector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new TextLineCodecFactory()));
mSocketConnector.setHandler(new ShoutcastIOHandler());
}
mStreamingSession = null;
Runnable runnable = new Runnable()
{
@Override
public void run()
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
mStreamingSession = future.getSession();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("Failed to connect - no exception is available");
}
disconnect();
}
}
};
getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
}
return connected();
}
/**
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect()
{
if(connected())
{
if(mStreamingSession != null)
{
mStreamingSession.closeNow();
}
}
}
/**
* IO Handler for managing Icecast TCP connection and credentials
*/
public class ShoutcastIOHandler extends IoHandlerAdapter
{
/**
* Sends stream configuration and user credentials upon connecting to remote server
*/
@Override
public void sessionOpened(IoSession session) throws Exception
{
StringBuilder sb = new StringBuilder();
//Password
sb.append(getConfiguration().getPassword()).append(ShoutcastMetadata.COMMAND_TERMINATOR);
//Metadata
sb.append(ShoutcastMetadata.STREAM_NAME.encode(getConfiguration().getName()));
sb.append(ShoutcastMetadata.PUBLIC.encode(getConfiguration().isPublic()));
sb.append(ShoutcastMetadata.GENRE.encode(getConfiguration().getGenre()));
sb.append(ShoutcastMetadata.DESCRIPTION.encode(getConfiguration().getDescription()));
sb.append(ShoutcastMetadata.AUDIO_BIT_RATE.encode(getConfiguration().getBitRate()));
//End of connection string
sb.append(ShoutcastMetadata.COMMAND_TERMINATOR);
session.write(sb.toString());
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
mSocketConnector.dispose();
mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false);
super.sessionClosed(session);
}
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
if(cause instanceof IOException && ((IOException)cause).getMessage().startsWith("Connection reset"))
{
mLog.info("Streaming connection reset by remote server - reestablishing connection");
disconnect();
connect();
}
else
{
mLog.error("Broadcast error", cause);
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
mConnecting.set(false);
}
@Override
public void messageReceived(IoSession session, Object object) throws Exception
{
if(object instanceof String)
{
String message = (String)object;
if(message != null && !message.trim().isEmpty())
{
if(message.startsWith("OK"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if(message.startsWith("icy-caps:"))
{
//TODO: what does icy-caps:11 tell us?
}
else
{
mLog.error("Unrecognized server response:" + message);
setBroadcastState(BroadcastState.ERROR);
}
}
}
else
{
mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
"] received:" + object.toString());
}
}
}
}

View File

@@ -0,0 +1,267 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 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.shoutcast.v1;
import audio.broadcast.IBroadcastMetadataUpdater;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ThreadPoolManager;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.http.HttpClientCodec;
import org.apache.mina.http.HttpRequestImpl;
import org.apache.mina.http.api.HttpMethod;
import org.apache.mina.http.api.HttpRequest;
import org.apache.mina.http.api.HttpVersion;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
private final static String UTF8 = "UTF-8";
private ThreadPoolManager mThreadPoolManager;
private ShoutcastV1Configuration mShoutcastV1Configuration;
private NioSocketConnector mSocketConnector;
private AtomicBoolean mUpdating = new AtomicBoolean();
private Queue<String> mMetadataQueue = new LinkedTransferQueue<>();
private Map<String, String> mHTTPHeaders;
private boolean mStackTraceLoggingSuppressed;
/**
* Shoutcast song metadata updater. Each metadata update is processed in the order received and the dispatch of
* the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
* sequence, those updates will be queued and processed in the order received.
*/
public ShoutcastV1BroadcastMetadataUpdater(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration shoutcastV1Configuration)
{
mThreadPoolManager = threadPoolManager;
mShoutcastV1Configuration = shoutcastV1Configuration;
}
/**
* Socket connector - lazy constructor.
*/
private NioSocketConnector getSocketConnector()
{
if (mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(ShoutcastV1BroadcastMetadataUpdater.class));
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
//Each metadata update session is single-use, so we'll shut it down upon success or failure
mSocketConnector.setHandler(new IoHandlerAdapter()
{
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
//Single-use session - close it after we receive an error
session.closeNow();
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception
{
//Single-use session - close it after we receive a response
session.closeNow();
}
});
}
return mSocketConnector;
}
/**
* Sends a song metadata update to the remote server. Additional metadata updates received while the updater
* is in the process of sending an existing metadata update will be queued and processed in received order.
*
* Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
* and will remain in the update queue until the next metadata update is requested. However, this is a design
* trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
*/
public void update(AudioMetadata audioMetadata)
{
mMetadataQueue.offer(getSong(audioMetadata));
if (mUpdating.compareAndSet(false, true))
{
String song = mMetadataQueue.poll();
while (song != null)
{
HttpRequest updateRequest = createUpdateRequest(song);
if (updateRequest != null)
{
mThreadPoolManager.scheduleOnce(new Runnable()
{
@Override
public void run()
{
try
{
ConnectFuture connectFuture = getSocketConnector()
.connect(new InetSocketAddress(mShoutcastV1Configuration.getHost(),
mShoutcastV1Configuration.getPort()));
connectFuture.awaitUninterruptibly();
IoSession session = connectFuture.getSession();
if (session != null)
{
session.write(updateRequest);
}
}
catch (Exception e)
{
Throwable throwableCause = e.getCause();
if (throwableCause instanceof ConnectException)
{
//Do nothing, the server is unavailable
}
else
{
if (!mStackTraceLoggingSuppressed)
{
mLog.error("Error sending metadata update. Future errors will " +
"be suppressed", e);
mStackTraceLoggingSuppressed = true;
}
}
}
}
}, 0l, TimeUnit.SECONDS);
}
//Fetch next metadata update to send
song = mMetadataQueue.poll();
}
mUpdating.set(false);
}
}
/**
* Creates the song information for a metadata update
*/
private static String getSong(AudioMetadata audioMetadata)
{
StringBuilder sb = new StringBuilder();
if(audioMetadata != null)
{
Metadata to = audioMetadata.getMetadata(MetadataType.TO);
sb.append("TO:");
if(to != null)
{
if(to.hasAlias())
{
sb.append(to.getAlias().getName());
}
else
{
sb.append(to.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
Metadata from = audioMetadata.getMetadata(MetadataType.FROM);
sb.append(" FROM:");
if(from != null)
{
if(from.hasAlias())
{
sb.append(from.getAlias().getName());
}
else
{
sb.append(from.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
}
else
{
sb.append("Scanning ....");
}
return sb.toString();
}
/**
* Creates an HTTP GET request to update the metadata on the remote server
*/
private HttpRequest createUpdateRequest(String song)
{
try
{
StringBuilder sb = new StringBuilder();
sb.append("pass=").append(mShoutcastV1Configuration.getPassword());
sb.append("&mode=updinfo");
sb.append("&song=").append(URLEncoder.encode(song, UTF8));
Map<String,String> headers = new HashMap<>();
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_0, HttpMethod.GET, "/admin.cgi",
sb.toString(), headers);
return request;
}
catch (UnsupportedEncodingException e)
{
//This should never happen
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
}
return null;
}
}

View File

@@ -0,0 +1,170 @@
/*******************************************************************************
* 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.shoutcast.v1;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import javax.xml.bind.annotation.XmlAttribute;
public class ShoutcastV1Configuration extends BroadcastConfiguration
{
private String mGenre;
private String mDescription;
private boolean mPublic;
private int mChannels = 1;
private int mBitRate = 16;
public ShoutcastV1Configuration()
{
//No-arg JAXB constructor
}
public ShoutcastV1Configuration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
ShoutcastV1Configuration copy = new ShoutcastV1Configuration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setChannels(getChannels());
copy.setBitRate(getBitRate());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.SHOUTCAST_V1;
}
/**
* Stream genre
*/
@XmlAttribute(name="genre")
public String getGenre()
{
return mGenre;
}
/**
* Stream genre
* @param genre
*/
public void setGenre(String genre)
{
mGenre = genre;
}
/**
* Stream description
*/
@XmlAttribute(name="description")
public String getDescription()
{
return mDescription;
}
/**
* Stream description
* @param description
*/
public void setDescription(String description)
{
mDescription = description;
}
/**
* Public visibility of the broadcastAudio
*/
@XmlAttribute(name="public")
public boolean isPublic()
{
return mPublic;
}
/**
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
mPublic = isPublic;
}
/**
* Number of audio channels in the broadcastAudio
*/
@XmlAttribute(name="channels")
public int getChannels()
{
return mChannels;
}
/**
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
mChannels = channels;
}
/**
* Bit rate in bits per second
*/
@XmlAttribute(name="bitrate")
public int getBitRate()
{
return mBitRate;
}
/**
* Bit rate
* @param bitRate in samples per second
*/
public void setBitRate(int bitRate)
{
mBitRate = bitRate;
}
@Override
public boolean isValid()
{
return getName() != null && !getName().isEmpty() &&
getHost() != null && !getHost().isEmpty() &&
getPort() > 0 && getPort() < 65535 &&
getPassword() != null && !getPassword().isEmpty();
}
}

View File

@@ -0,0 +1,319 @@
/*******************************************************************************
* 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.shoutcast.v1;
import alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ShoutcastV1ConfigurationEditor extends BroadcastConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1ConfigurationEditor.class );
private static final int ONE_MINUTE_MS = 60000;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mPassword;
private JTextField mGenre;
private JTextField mDescription;
private JCheckBox mPublic;
private JSlider mDelay;
private JLabel mDelayValue;
private JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
public ShoutcastV1ConfigurationEditor(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("Shoutcast (v1.x) Stream");
ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.SHOUTCAST_V1.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.shoutcast.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("Password:"));
mPassword = new JTextField();
mPassword.getDocument().addDocumentListener(this);
mPassword.setToolTipText("Password for the stream");
add(mPassword);
add(new JLabel("Genre:"));
mGenre = new JTextField();
mGenre.getDocument().addDocumentListener(this);
mGenre.setToolTipText("Genre (e.g. public safety)");
add(mGenre);
add(new JLabel("Description:"));
mDescription = new JTextField();
mDescription.getDocument().addDocumentListener(this);
mDescription.setToolTipText("Description of the stream");
add(mDescription);
add(new JLabel("Public:"));
mPublic = new JCheckBox();
mPublic.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
mPublic.setToolTipText("Designate stream as public (checked) or private");
add(mPublic);
add(new JLabel());
mDelayValue = new JLabel("0 Minutes");
add(mDelayValue);
add(new JLabel("Delay:"));
mDelay = new JSlider(0,60,0);
mDelay.setMajorTickSpacing(10);
mDelay.setMinorTickSpacing(5);
mDelay.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mDelayValue.setText(mDelay.getValue() + " Minutes");
setModified(true);
}
});
mDelay.setToolTipText("Audio broadcast delay in minutes");
add(mDelay);
add(new JLabel("Enabled:")); //Empty
mEnabled = new JCheckBox();
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled);
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())
{
ShoutcastV1Configuration config = (ShoutcastV1Configuration)getItem();
mName.setText(config.getName());
mServer.setText(config.getHost());
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mPassword.setText(config.getPassword());
mGenre.setText(config.getGenre());
mDescription.setText(config.getDescription());
mPublic.setSelected(config.isPublic());
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mPassword.setText(null);
mGenre.setText(null);
mDescription.setText(null);
mPublic.setSelected(true);
mDelay.setValue(0);
mEnabled.setSelected(false);
}
setModified(false);
}
@Override
public void save()
{
ShoutcastV1Configuration config = (ShoutcastV1Configuration)getItem();
if(validateConfiguration())
{
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setPassword(mPassword.getText());
config.setDescription(mDescription.getText());
config.setGenre(mGenre.getText());
config.setPublic(mPublic.isSelected());
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
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(ShoutcastV1ConfigurationEditor.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;
}
//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

@@ -0,0 +1,528 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 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.shoutcast.v2;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastState;
import audio.broadcast.IBroadcastMetadataUpdater;
import audio.broadcast.shoutcast.v2.ultravox.AuthenticateBroadcast;
import audio.broadcast.shoutcast.v2.ultravox.CacheableXMLMetadata;
import audio.broadcast.shoutcast.v2.ultravox.ConfigureIcyName;
import audio.broadcast.shoutcast.v2.ultravox.ConfigureIcyPublic;
import audio.broadcast.shoutcast.v2.ultravox.MP3Audio;
import audio.broadcast.shoutcast.v2.ultravox.NegotiateMaxPayloadSize;
import audio.broadcast.shoutcast.v2.ultravox.RequestCipher;
import audio.broadcast.shoutcast.v2.ultravox.SetupBroadcast;
import audio.broadcast.shoutcast.v2.ultravox.Standby;
import audio.broadcast.shoutcast.v2.ultravox.StreamMimeType;
import audio.broadcast.shoutcast.v2.ultravox.TerminateBroadcast;
import audio.broadcast.shoutcast.v2.ultravox.UltravoxMessage;
import audio.broadcast.shoutcast.v2.ultravox.UltravoxMessageFactory;
import audio.broadcast.shoutcast.v2.ultravox.UltravoxMessageType;
import audio.broadcast.shoutcast.v2.ultravox.UltravoxMetadata;
import audio.broadcast.shoutcast.v2.ultravox.UltravoxProtocolFactory;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ThreadPoolManager;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2AudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private int mMaxPayloadSize = 16377;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
private LinkedTransferQueue<UltravoxMessage> mMetadataMessageQueue = new LinkedTransferQueue<>();
/**
* Shoutcast 2.x (Ultravox 2.1) broadcaster. This broadcaster is compatible with Shoutcast 2.x and newer versions
* of the server software.
*
* Note: use @see ShoutcastV1AudioBroadcaster for Shoutcast version 1.x and older.
*
* This broadcaster uses the Apache Mina library for the streaming socket connection. The
* ShoutcastV2IOHandler manages all interaction with the Shoutcast server and manages the overall broadcast state.
*
* @param threadPoolManager for thread pool access
* @param configuration for the Shoutcast V2 stream
*/
public ShoutcastV2AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV2Configuration configuration)
{
super(threadPoolManager, configuration);
}
public ShoutcastV2Configuration getConfiguration()
{
return (ShoutcastV2Configuration)getBroadcastConfiguration();
}
/**
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
{
//Dispatch any queued metadata messages
UltravoxMessage metadataMessage = mMetadataMessageQueue.poll();
while(metadataMessage != null)
{
mStreamingSession.write(metadataMessage);
metadataMessage = mMetadataMessageQueue.poll();
}
//Dispatch audio message
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
MP3Audio mp3Audio = new MP3Audio();
mp3Audio.setPayload(audio);
mStreamingSession.write(mp3Audio);
}
}
@Override
public void update(AudioMetadata metadata)
{
List<UltravoxMessage> metadataMessages = getMetadataMessages(metadata);
if(!metadataMessages.isEmpty())
{
mMetadataMessageQueue.addAll(metadataMessages);
}
}
@Override
protected IBroadcastMetadataUpdater getMetadataUpdater()
{
return this;
}
/**
* (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.
*
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected() && canConnect() &&
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
mConnecting.compareAndSet(false, true))
{
mLastConnectionAttempt = System.currentTimeMillis();
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(ShoutcastV2AudioBroadcaster.class));
mSocketConnector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new UltravoxProtocolFactory()));
mSocketConnector.setHandler(new ShoutcastV2IOHandler());
}
mStreamingSession = null;
Runnable runnable = new Runnable()
{
@Override
public void run()
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
mStreamingSession = future.getSession();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Failed to connect - no exception is available");
}
disconnect();
}
}
};
getThreadPoolManager().scheduleOnce(runnable, 0l, TimeUnit.SECONDS);
}
return connected();
}
/**
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect()
{
if(connected())
{
if(mStreamingSession != null)
{
mStreamingSession.write(new TerminateBroadcast());
mStreamingSession.closeNow();
}
}
}
/**
* Encodes the list of metadata in one or more Ultravox CacheableXMLMetadata messages according to the maximum negotiated
* payload size for the current connection. Each entry in the list of metadata strings should be an xml encoded
* value: <tag>value</tag>
*
* See UltravoxMetadata.TAG.asXML(String value)
*
* @param metadata containing audio metadata tags and attributes
* @return a sequence of CacheableXMLMetadata messages sufficient to carry the complete set of metadata
*/
private List<UltravoxMessage> getMetadataMessages(AudioMetadata metadata)
{
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?><metadata>");
sb.append(UltravoxMetadata.ALBUM_TITLE.asXML(getConfiguration().getName()));
sb.append(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION.asXML(SystemProperties.getInstance().getApplicationName()));
if(getConfiguration().hasGenre())
{
sb.append(UltravoxMetadata.GENRE.asXML(getConfiguration().getGenre()));
}
if(getConfiguration().hasURL())
{
sb.append(UltravoxMetadata.URL.asXML(getConfiguration().getURL()));
}
if(metadata != null)
{
Metadata to = metadata.getMetadata(MetadataType.TO);
if(to != null)
{
if(to.hasAlias())
{
sb.append(UltravoxMetadata.TITLE_2.asXML(to.getAlias().toString()));
}
else
{
sb.append(UltravoxMetadata.TITLE_2.asXML(to.getValue()));
}
}
Metadata from = metadata.getMetadata(MetadataType.FROM);
if(from != null)
{
if(from.hasAlias())
{
sb.append(UltravoxMetadata.TITLE_3.asXML("From: " + from.getAlias().toString()));
}
else
{
sb.append(UltravoxMetadata.TITLE_3.asXML("From: " + from.getValue()));
}
}
}
else
{
sb.append(UltravoxMetadata.TITLE_2.asXML("Scanning ..."));
}
sb.append("</metadata>");
byte[] xml = sb.toString().getBytes();
int pointer = 0;
int messageCounter = 1;
int messageCount = (int)Math.ceil((double)xml.length / (double)(mMaxPayloadSize - 6));
if(messageCount > 32)
{
messageCount = 32; //Max number of metadata messages in a sequence
}
List<UltravoxMessage> messages = new ArrayList<>();
while(pointer < xml.length && messageCounter <= messageCount)
{
int payloadSize = Math.min(mMaxPayloadSize - 6, xml.length - pointer);
byte[] payload = new byte[payloadSize + 6];
payload[1] = (byte)(0x01);
payload[3] = (byte)(messageCount & 0xFF);
payload[5] = (byte)(messageCounter & 0xFF);
System.arraycopy(xml, pointer, payload, 6, payloadSize);
CacheableXMLMetadata message = new CacheableXMLMetadata();
message.setPayload(payload);
messages.add(message);
pointer += payloadSize;
}
return messages;
}
/**
* IO Handler for managing Shoutcast V2 connection and credentials
*/
public class ShoutcastV2IOHandler extends IoHandlerAdapter
{
/**
* Sends stream configuration and user credentials upon connecting to remote server
*/
@Override
public void sessionOpened(IoSession session) throws Exception
{
session.write(UltravoxMessageFactory.getMessage(UltravoxMessageType.REQUEST_CIPHER));
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
mSocketConnector.dispose();
mStreamingSession = null;
mSocketConnector = null;
mConnecting.set(false);
super.sessionClosed(session);
}
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
if(cause instanceof IOException && ((IOException)cause).getMessage().startsWith("Connection reset"))
{
mLog.info("Streaming connection reset by remote server - reestablishing connection");
disconnect();
connect();
}
else
{
mLog.error("Broadcast error", cause);
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
mConnecting.set(false);
}
@Override
public void messageReceived(IoSession session, Object object) throws Exception
{
if(object instanceof UltravoxMessage)
{
UltravoxMessage ultravoxMessage = (UltravoxMessage)object;
switch (ultravoxMessage.getMessageType())
{
case REQUEST_CIPHER:
String cipherKey = ((RequestCipher)ultravoxMessage).getCipher();
AuthenticateBroadcast authenticateBroadcast = new AuthenticateBroadcast();
authenticateBroadcast.setCredentials(cipherKey, getConfiguration().getStreamID(),
getConfiguration().getUserID(), getConfiguration().getPassword());
session.write(authenticateBroadcast);
break;
case AUTHENTICATE_BROADCAST:
if(ultravoxMessage.isErrorResponse())
{
String errorMessage = ultravoxMessage.getErrorMessage();
if(errorMessage.startsWith(AuthenticateBroadcast.STREAM_ID_ERROR))
{
setBroadcastState(BroadcastState.INVALID_MOUNT_POINT);
}
else
{
mLog.error("Invalid Credentials - response: " + ultravoxMessage.getPayload());
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
}
}
else
{
StreamMimeType streamMimeType = new StreamMimeType();
streamMimeType.setFormat(getConfiguration().getBroadcastFormat());
session.write(streamMimeType);
}
break;
case STREAM_MIME_TYPE:
if(ultravoxMessage.isErrorResponse())
{
mLog.error("Unsupported Audio Format:" + getConfiguration().getBroadcastFormat().toString() +
" - " + ultravoxMessage.getErrorMessage());
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
}
else
{
SetupBroadcast setupBroadcast = new SetupBroadcast();
//Use the same value for average and minimum bit rates
setupBroadcast.setBitRate(getConfiguration().getBitRate(), getConfiguration().getBitRate());
session.write(setupBroadcast);
}
break;
case SETUP_BROADCAST:
if(ultravoxMessage.isErrorResponse())
{
mLog.error("Unsupported Audio Bit Rate:" + getConfiguration().getBitRate() +
" - " + ultravoxMessage.getErrorMessage());
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
}
else
{
NegotiateMaxPayloadSize negotiateMaxPayloadSize = new NegotiateMaxPayloadSize();
negotiateMaxPayloadSize.setMaximumPayloadSize(16377, 4192);
session.write(negotiateMaxPayloadSize);
}
break;
case NEGOTIATE_MAX_PAYLOAD_SIZE:
if(ultravoxMessage.isErrorResponse())
{
mLog.error("Unsupported maximum payload size (18 min - 36 kbps max) - " +
ultravoxMessage.getErrorMessage());
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
}
else
{
mMaxPayloadSize = ((NegotiateMaxPayloadSize)ultravoxMessage).getMaximumPayloadSize();
ConfigureIcyPublic configureIcyPublic = new ConfigureIcyPublic();
configureIcyPublic.setPublic(getConfiguration().isPublic());
session.write(configureIcyPublic);
}
break;
case CONFIGURE_ICY_PUBLIC:
if(ultravoxMessage.isErrorResponse())
{
mLog.error("Error setting shoutcast stream as public - " + ultravoxMessage.getErrorMessage());
setBroadcastState(BroadcastState.ERROR);
}
else
{
ConfigureIcyName configureIcyName = new ConfigureIcyName();
configureIcyName.setName(getConfiguration().getName());
session.write(configureIcyName);
}
break;
case CONFIGURE_ICY_NAME:
if(ultravoxMessage.isErrorResponse())
{
mLog.error("Error setting shoutcast stream name - " + ultravoxMessage.getErrorMessage());
setBroadcastState(BroadcastState.CONFIGURATION_ERROR);
}
else
{
session.write(new Standby());
}
break;
case STANDBY:
if(ultravoxMessage.isErrorResponse())
{
mLog.error("Error following stream configuration and standby - " +
ultravoxMessage.getErrorMessage());
setBroadcastState(BroadcastState.ERROR);
}
else
{
setBroadcastState(BroadcastState.CONNECTED);
}
break;
default:
mLog.error("Unrecognized ultravox message:" + ultravoxMessage.getMessageType() +
" payload:" + ultravoxMessage.getPayload());
setBroadcastState(BroadcastState.ERROR);
}
}
else
{
mLog.debug("Unrecognized message received from shoutcast v2 server:" + object.toString());
setBroadcastState(BroadcastState.ERROR);
}
}
}
}

View File

@@ -0,0 +1,205 @@
/*******************************************************************************
* 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.shoutcast.v2;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import javax.xml.bind.annotation.XmlAttribute;
public class ShoutcastV2Configuration extends BroadcastConfiguration
{
private int mStreamID;
private String mUserID;
private int mBitRate = 16;
private String mGenre;
private String mURL;
private boolean mPublic = true;
public ShoutcastV2Configuration()
{
//No-arg JAXB constructor
}
public ShoutcastV2Configuration(BroadcastFormat format)
{
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
ShoutcastV2Configuration copy = new ShoutcastV2Configuration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Shoutcast V2 Configuration Parameters
copy.setStreamID(getStreamID());
copy.setUserID(getUserID());
copy.setBitRate(getBitRate());
copy.setGenre(getGenre());
copy.setURL(getURL());
copy.setPublic(isPublic());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.SHOUTCAST_V2;
}
/**
* Stream ID
*/
@XmlAttribute(name = "stream_id")
public int getStreamID()
{
return mStreamID;
}
/**
* Sets the stream ID
*
* @param streamID numeric stream identifier between 1 - 2147483647
*/
public void setStreamID(int streamID)
{
assert(1 <= streamID && streamID <= Integer.MAX_VALUE);
mStreamID = streamID;
}
/**
* User ID
*/
@XmlAttribute(name = "user_id")
public String getUserID()
{
return mUserID;
}
/**
* Sets the user ID
* @param userID
*/
public void setUserID(String userID)
{
mUserID = userID;
}
/**
* Bit rate in kilobits per second
*/
@XmlAttribute(name = "bitrate")
public int getBitRate()
{
return mBitRate;
}
/**
* Bit rate
* @param bitRate in kilobits per second
*/
public void setBitRate(int bitRate)
{
mBitRate = bitRate;
}
/**
* Stream genre
*/
@XmlAttribute(name = "genre")
public String getGenre()
{
return mGenre;
}
/**
* Sets the genre tag for this stream
*/
public void setGenre(String genre)
{
mGenre = genre;
}
public boolean hasGenre()
{
return mGenre != null && !mGenre.isEmpty();
}
/**
* Public visibility of the broadcastAudio
*/
@XmlAttribute(name = "public")
public boolean isPublic()
{
return mPublic;
}
/**
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
mPublic = isPublic;
}
/**
* URL associated with the broadcastAudio where users can find additional details.
*/
@XmlAttribute(name = "url")
public String getURL()
{
return mURL;
}
/**
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)
{
mURL = url;
}
public boolean hasURL()
{
return mURL != null && !mURL.isEmpty();
}
@Override
public boolean isValid()
{
return super.isValid() && hasName() &&
getUserID() != null && !getUserID().isEmpty() &&
1 <= getStreamID() &&
getPassword() != null && !getPassword().isEmpty();
}
}

View File

@@ -0,0 +1,363 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 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.shoutcast.v2;
import alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ShoutcastV2ConfigurationEditor extends BroadcastConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2ConfigurationEditor.class );
private static final int ONE_MINUTE_MS = 60000;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mStreamID;
private JTextField mUserID;
private JTextField mPassword;
private JTextField mGenre;
private JCheckBox mPublic;
private JSlider mDelay;
private JLabel mDelayValue;
private JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
public ShoutcastV2ConfigurationEditor(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("Shoutcast (v2) Stream");
ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.SHOUTCAST_V2.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.shoutcast.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("Stream ID:"));
mStreamID = new JTextField();
mStreamID.getDocument().addDocumentListener(this);
mStreamID.setToolTipText("Stream Identifier)");
add(mStreamID);
add(new JLabel("User ID:"));
mUserID = new JTextField();
mUserID.getDocument().addDocumentListener(this);
mUserID.setToolTipText("User ID/Name");
add(mUserID);
add(new JLabel("Password:"));
mPassword = new JTextField();
mPassword.getDocument().addDocumentListener(this);
mPassword.setToolTipText("Password for the stream");
add(mPassword);
add(new JLabel("Genre:"));
mGenre = new JTextField();
mGenre.getDocument().addDocumentListener(this);
mGenre.setToolTipText("Genre (e.g. public safety)");
add(mGenre);
add(new JLabel("Public:"));
mPublic = new JCheckBox();
mPublic.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
mPublic.setToolTipText("Designate stream as public (checked) or private");
add(mPublic);
add(new JLabel());
mDelayValue = new JLabel("0 Minutes");
add(mDelayValue);
add(new JLabel("Delay:"));
mDelay = new JSlider(0,60,0);
mDelay.setMajorTickSpacing(10);
mDelay.setMinorTickSpacing(5);
mDelay.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mDelayValue.setText(mDelay.getValue() + " Minutes");
setModified(true);
}
});
mDelay.setToolTipText("Audio broadcast delay in minutes");
add(mDelay);
add(new JLabel("Enabled:")); //Empty
mEnabled = new JCheckBox();
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled);
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())
{
ShoutcastV2Configuration config = (ShoutcastV2Configuration)getItem();
mName.setText(config.getName());
mServer.setText(config.getHost());
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mStreamID.setText(String.valueOf(config.getStreamID()));
mUserID.setText(config.getUserID());
mPassword.setText(config.getPassword());
mGenre.setText(config.getGenre());
mPublic.setSelected(config.isPublic());
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mStreamID.setText(null);
mUserID.setText(null);
mPassword.setText(null);
mGenre.setText(null);
mPublic.setSelected(true);
mDelay.setValue(0);
mEnabled.setSelected(false);
}
setModified(false);
}
@Override
public void save()
{
ShoutcastV2Configuration config = (ShoutcastV2Configuration)getItem();
if(validateConfiguration())
{
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setUserID(mUserID.getText());
config.setPassword(mPassword.getText());
config.setStreamID(getStreamID());
config.setGenre(mGenre.getText());
config.setPublic(mPublic.isSelected());
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
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(ShoutcastV2ConfigurationEditor.this,
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
mName.requestFocus();
return false;
}
//Server address is required
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
{
return true;
}
//Port is required
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
{
return true;
}
//Stream ID is required
if(!validateIntegerTextField(mStreamID, "Invalid Stream ID", "Please specify a stream ID of 1 or higher", 1, Integer.MAX_VALUE))
{
return true;
}
//User ID is required
if(!validateTextField(mUserID, "Invalid User ID", "Please specify a user ID/name."))
{
return true;
}
//Password is 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;
}
/**
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
*/
private int getStreamID()
{
String streamID = mStreamID.getText();
if(streamID != null && !streamID.isEmpty())
{
try
{
return Integer.parseInt(streamID);
}
catch(Exception e)
{
//Do nothing, we couldn't parse the number value
}
}
return 0;
}
}

View File

@@ -0,0 +1,102 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
import util.XTEA;
import java.util.Arrays;
public class AuthenticateBroadcast extends UltravoxMessage
{
public static final String STREAM_ID_ERROR = "Stream ID Error";
/**
* Client request to server to send authentication credentials
*/
public AuthenticateBroadcast()
{
super(UltravoxMessageType.AUTHENTICATE_BROADCAST);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
AuthenticateBroadcast(byte[] data)
{
super(data);
}
/**
* Set credential to authenticate the stream with the server
*
* @param encryptionKey received from the server in the RequestCipher message
* @param streamID that we're going to stream to
* @param userID for the stream. Note: this is optional and can be null. The server currently ignores this value.
* @param password for the stream.
*/
public void setCredentials(String encryptionKey, int streamID, String userID, String password)
{
XTEA xtea = new XTEA(encryptionKey);
StringBuilder sb = new StringBuilder();
sb.append(ULTRAVOX_VERSION);
sb.append(":");
sb.append(streamID);
sb.append(":");
sb.append(userID != null ? encrypt(xtea, userID) : "");
sb.append(":");
sb.append(password != null ? encrypt(xtea, password) : "");
setPayload(sb.toString());
}
/**
* Encrypts the value using the XTEA block cipher algorithm and returns the encrypted value as a 16 hex value
*
* @param xtea instance of the XTEA algorithm with a loaded encryption key
* @param value to encode
* @return encrypted password as 64-bit, 16 character hexadecimal value
*/
private static String encrypt(XTEA xtea, String value)
{
byte[] valueBytes = value.getBytes();
if(valueBytes.length % 8 != 0 || valueBytes.length == 0)
{
int newLength = ((valueBytes.length / 8) + 1) * 8;
valueBytes = Arrays.copyOf(valueBytes, newLength);
}
byte[] encrypted = xtea.encrypt(valueBytes);
StringBuilder sb = new StringBuilder();
for(byte b: encrypted)
{
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class CacheableXMLMetadata extends UltravoxMessage
{
/**
* Client request to server to send cacheable XML metadata
*/
public CacheableXMLMetadata()
{
super(UltravoxMessageType.CACHEABLE_XML_METADATA);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
CacheableXMLMetadata(byte[] data)
{
super(data);
}
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class ConfigureIcyGenre extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-GENRE metadata
*/
public ConfigureIcyGenre()
{
super(UltravoxMessageType.CONFIGURE_ICY_GENRE);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
ConfigureIcyGenre(byte[] data)
{
super(data);
}
public void setGenre(String genre)
{
setPayload(genre);
}
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class ConfigureIcyName extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-NAME metadata
*/
public ConfigureIcyName()
{
super(UltravoxMessageType.CONFIGURE_ICY_NAME);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
ConfigureIcyName(byte[] data)
{
super(data);
}
public void setName(String name)
{
setPayload(name);
}
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class ConfigureIcyPublic extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-PUBLIC metadata
*/
public ConfigureIcyPublic()
{
super(UltravoxMessageType.CONFIGURE_ICY_PUBLIC);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
ConfigureIcyPublic(byte[] data)
{
super(data);
}
public void setPublic(boolean publicStream)
{
setPayload(publicStream ? "1" : "0");
}
}

View File

@@ -0,0 +1,46 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class ConfigureIcyURL extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-URL metadata
*/
public ConfigureIcyURL()
{
super(UltravoxMessageType.CONFIGURE_ICY_URL);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
ConfigureIcyURL(byte[] data)
{
super(data);
}
public void setURL(String url)
{
setPayload(url);
}
}

View File

@@ -0,0 +1,41 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class FlushCachedMetadata extends UltravoxMessage
{
/**
* Client request to server to flush any currently cached stream metadata
*/
public FlushCachedMetadata()
{
super(UltravoxMessageType.FLUSH_CACHED_METADATA);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
FlushCachedMetadata(byte[] data)
{
super(data);
}
}

View File

@@ -0,0 +1,44 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class MP3Audio extends UltravoxMessage
{
/**
* Client request to server to send authentication credentials
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
public MP3Audio()
{
super(UltravoxMessageType.MP3_DATA);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
MP3Audio(byte[] data)
{
super(data);
}
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class NegotiateBufferSize extends UltravoxMessage
{
/**
* Client request to server to negotiate the server's buffer size
*/
public NegotiateBufferSize()
{
super(UltravoxMessageType.NEGOTIATE_BUFFER_SIZE);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
NegotiateBufferSize(byte[] data)
{
super(data);
}
/**
* Sets the desired and minimum requested buffer sizes to send to the server
* @param desired buffer size in kilobytes
* @param minimum buffer size in kilobytes
*/
public void setBufferSize(int desired, int minimum)
{
StringBuilder sb = new StringBuilder();
sb.append(desired).append(":").append(minimum);
setPayload(sb.toString());
}
/**
* Negotiated buffer size from a server response ultravox.
*
* @return server negotiated buffer size, or zero if this is not a server response ultravox, or this is an error
* server response ultravox, or the server response ultravox buffer size cannot be determined from the ultravox payload.
*/
public int getBufferSize()
{
String payload = getPayload();
if(payload != null && payload.startsWith("ACK:"))
{
try
{
int bufferSize = Integer.parseInt(payload.substring(4));
return bufferSize;
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
}
return 0;
}
}

View File

@@ -0,0 +1,85 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class NegotiateMaxPayloadSize extends UltravoxMessage
{
/**
* Client request to server to negotiate maximum payload size
*/
public NegotiateMaxPayloadSize()
{
super(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
NegotiateMaxPayloadSize(byte[] data)
{
super(data);
}
/**
* Sets the maximum payload size in bytes
*
* @param desiredMaximum payload size (no larger than 16377)
* @param minimum acceptable payload size (no larger than 16377)
*/
public void setMaximumPayloadSize(int desiredMaximum, int minimum)
{
assert(desiredMaximum <= 16377);
assert(minimum <= 16377);
StringBuilder sb = new StringBuilder();
sb.append(desiredMaximum).append(":").append(minimum);
setPayload(sb.toString());
}
/**
* Negotiated maximum payload size from a server response ultravox.
*
* @return server negotiated maximum payload size, or zero if this is not a server response ultravox, or this is an
* error server response ultravox, or the server response ultravox maximum payload size cannot be determined from the
* ultravox payload.
*/
public int getMaximumPayloadSize()
{
String payload = getPayload();
if(payload != null && payload.startsWith("ACK:"))
{
try
{
int maximumPayloadSize = Integer.parseInt(payload.substring(4));
return maximumPayloadSize;
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
}
return 0;
}
}

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 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.shoutcast.v2.ultravox;
public class PassThroughXMLMetadata extends UltravoxMessage
{
/**
* Client request to server to send pass-through XML metadata
*/
public PassThroughXMLMetadata()
{
super(UltravoxMessageType.PASS_THROUGH_XML_METADATA);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
PassThroughXMLMetadata(byte[] data)
{
super(data);
}
}

View File

@@ -0,0 +1,57 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class RequestCipher extends UltravoxMessage
{
/**
* Client request to server for encryption key
*/
public RequestCipher()
{
super(UltravoxMessageType.REQUEST_CIPHER);
setPayload(ULTRAVOX_VERSION);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
RequestCipher(byte[] data)
{
super(data);
}
/**
* Cipher key returned from the server to use for encrypting user id and password credentials
*
* @return cipher key or null
*/
public String getCipher()
{
if(isValidResponse())
{
return getPayload().replace(VALID_RESPONSE_PAYLOAD_PREFIX, "");
}
return null;
}
}

View File

@@ -0,0 +1,55 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class SetupBroadcast extends UltravoxMessage
{
/**
* Client request to server to specify the stream bit rate
*/
public SetupBroadcast()
{
super(UltravoxMessageType.SETUP_BROADCAST);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
SetupBroadcast(byte[] data)
{
super(data);
}
/**
* Sets the average and maximum bit rates. Set both to the same value for a Constant Bit Rate (CBR) stream.
*
* @param average bit rate of the stream in kbps
* @param maximum bit rate of the stream in kbps
*/
public void setBitRate(int average, int maximum)
{
StringBuilder sb = new StringBuilder();
sb.append(average).append(":").append(maximum);
setPayload(sb.toString());
}
}

View File

@@ -0,0 +1,41 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class Standby extends UltravoxMessage
{
/**
* Client request to server to notify that configuration is complete, prepare for streaming
*/
public Standby()
{
super(UltravoxMessageType.STANDBY);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
Standby(byte[] data)
{
super(data);
}
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
import audio.broadcast.BroadcastFormat;
public class StreamMimeType extends UltravoxMessage
{
/**
* Client request to server to set stream MIME type
*/
public StreamMimeType()
{
super(UltravoxMessageType.STREAM_MIME_TYPE);
}
/**
* Server response to client request
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
StreamMimeType(byte[] data)
{
super(data);
}
/**
* Sets the broadcastAudio format or MIME type
* @param format of the stream
*/
public void setFormat(BroadcastFormat format)
{
setPayload(format.getValue());
}
}

View File

@@ -0,0 +1,41 @@
/*******************************************************************************
* 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.shoutcast.v2.ultravox;
public class TerminateBroadcast extends UltravoxMessage
{
/**
* Client request to server to terminate the broadcastAudio
*/
public TerminateBroadcast()
{
super(UltravoxMessageType.TERMINATE_BROADCAST);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
TerminateBroadcast(byte[] data)
{
super(data);
}
}

Some files were not shown because too many files have changed in this diff Show More