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"> <component name="ProjectDictionaryState">
<dictionary name="denny"> <dictionary name="denny">
<words> <words>
<w>bahroo</w>
<w>broadcastify</w>
<w>esnid</w> <w>esnid</w>
<w>fleetsync</w> <w>fleetsync</w>
<w>icecast</w>
<w>ident</w> <w>ident</w>
<w>polyphase</w> <w>polyphase</w>
<w>samplerate</w>
<w>sdrtrunk</w>
<w>sheirer</w>
<w>shoutcast</w>
<w>streamable</w>
<w>talkgroup</w> <w>talkgroup</w>
<w>tgid</w> <w>tgid</w>
<w>ultravox</w>
<w>xtea</w>
</words> </words>
</dictionary> </dictionary>
</component> </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/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/libusb4java-1.2.0-linux-x86.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/JLargeArrays-1.2.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-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-windows-x86.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libusb4java-1.2.0-osx-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/usb4java-javax-1.2.0.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/jsr80-1.0.1.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/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/libusb4java-1.2.0-osx-x86_64.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/JTransforms-3.0.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> </CLASSES>
<JAVADOC /> <JAVADOC>
<SOURCES /> <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> </library>
</component> </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" /> <property name="caretWidth" class="java.lang.Integer" />
</properties> </properties>
</component> </component>
<component name="EntryPointsManager"> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<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">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
</component> </component>
</project> </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) 0.3.0 2017xxxx (work in progress)
- Requires Java 8 for compatibility with eventual changeover to JavaFX - 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. - Extensive GUI updates for tuner, channel and alias management.
- Updated alias list management of alias id values and wildcards. Weighting is now - 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 applied to each wildcard so that they can be ordered from most specific to least
@@ -14,10 +18,10 @@
decoders and filters decoders and filters
- New audio playback interface with global mute and support for one or two - New audio playback interface with global mute and support for one or two
audio channels 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 with selected channels having the highest priority and any audio without
an alias identifier having the lowest priority. an name identifier having the lowest priority.
- Updated Audio recording per call with new 'Non-Recordable' alias action - Updated Audio recording per call with new 'Non-Recordable' name action
- Updated Baseband (I/Q) recording at 48 kHz - Updated Baseband (I/Q) recording at 48 kHz
- Added Airspy tuner support - Added Airspy tuner support
- New traffic channel pool manager for trunked call following - New traffic channel pool manager for trunked call following

View File

@@ -27,4 +27,7 @@
<appender-ref ref="STDOUT" /> <appender-ref ref="STDOUT" />
</root> </root>
<!-- Apache Mina TCP/HTTP library -->
<logger name="org.apache.mina" level="INFO" />
</configuration> </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 /> <SOURCES />
</library> </library>
</orderEntry> </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> </component>
</module> </module>

View File

@@ -19,7 +19,10 @@ package alias;
import java.awt.Color; import java.awt.Color;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
@@ -30,6 +33,7 @@ import javax.xml.bind.annotation.XmlTransient;
import alias.action.AliasAction; import alias.action.AliasAction;
import alias.id.AliasID; import alias.id.AliasID;
import alias.id.AliasIDType; import alias.id.AliasIDType;
import alias.id.broadcast.BroadcastChannel;
import alias.id.nonrecordable.NonRecordable; import alias.id.nonrecordable.NonRecordable;
import alias.id.priority.Priority; import alias.id.priority.Priority;
@@ -175,7 +179,34 @@ public class Alias
{ {
return !mAliasActions.isEmpty(); 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 * Returns the priority level of this alias, if defined, or the default priority
*/ */
@@ -184,7 +215,7 @@ public class Alias
{ {
for( AliasID id: mAliasIDs ) for( AliasID id: mAliasIDs )
{ {
if( id.getType() == AliasIDType.Priority ) if( id.getType() == AliasIDType.PRIORITY)
{ {
return ((Priority)id).getPriority(); return ((Priority)id).getPriority();
} }
@@ -197,7 +228,7 @@ public class Alias
{ {
for( AliasID id: mAliasIDs ) for( AliasID id: mAliasIDs )
{ {
if( id.getType() == AliasIDType.Priority ) if( id.getType() == AliasIDType.PRIORITY)
{ {
return true; return true;
} }
@@ -216,7 +247,7 @@ public class Alias
{ {
for( AliasID id: mAliasIDs ) for( AliasID id: mAliasIDs )
{ {
if( id.getType() == AliasIDType.Priority ) if( id.getType() == AliasIDType.PRIORITY)
{ {
((Priority)id).setPriority( priority ); ((Priority)id).setPriority( priority );
return; return;
@@ -229,7 +260,7 @@ public class Alias
addAliasID( p ); addAliasID( p );
} }
} }
/** /**
* Inspects the alias for a non-recordable alias id. Default is true; * Inspects the alias for a non-recordable alias id. Default is true;
*/ */
@@ -238,7 +269,7 @@ public class Alias
{ {
for( AliasID id: getId() ) for( AliasID id: getId() )
{ {
if( id.getType() == AliasIDType.NonRecordable ) if( id.getType() == AliasIDType.NON_RECORDABLE)
{ {
return false; return false;
} }
@@ -258,7 +289,7 @@ public class Alias
for( AliasID id: getId() ) for( AliasID id: getId() )
{ {
if( id.getType() == AliasIDType.NonRecordable ) if( id.getType() == AliasIDType.NON_RECORDABLE)
{ {
toRemove = id; toRemove = id;
break; break;
@@ -274,7 +305,7 @@ public class Alias
{ {
for( AliasID id: getId() ) for( AliasID id: getId() )
{ {
if( id.getType() == AliasIDType.NonRecordable ) if( id.getType() == AliasIDType.NON_RECORDABLE)
{ {
return; return;
} }
@@ -283,4 +314,62 @@ public class Alias
addAliasID( new NonRecordable() ); 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; package alias;
import audio.broadcast.BroadcastModel;
import gui.editor.Editor; import gui.editor.Editor;
import java.awt.Color; import java.awt.Color;
@@ -52,12 +53,12 @@ public class AliasController extends JPanel
private IconCellRenderer mIconCellRenderer; private IconCellRenderer mIconCellRenderer;
public AliasController( AliasModel aliasModel, SettingsManager settingsManager ) public AliasController(AliasModel aliasModel, BroadcastModel broadcastModel, SettingsManager settingsManager )
{ {
mAliasModel = aliasModel; mAliasModel = aliasModel;
mAliasEditor = new AliasEditor( mAliasModel, settingsManager ); mAliasEditor = new AliasEditor( mAliasModel, broadcastModel, settingsManager );
mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, settingsManager ); mMultipleAliasEditor = new MultipleAliasEditor( mAliasModel, broadcastModel, settingsManager );
mIconCellRenderer = new IconCellRenderer( settingsManager ); mIconCellRenderer = new IconCellRenderer( settingsManager );

View File

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

View File

@@ -17,8 +17,6 @@
******************************************************************************/ ******************************************************************************/
package alias; package alias;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
import alias.action.AliasAction; import alias.action.AliasAction;
import alias.action.AliasActionType; import alias.action.AliasActionType;
import alias.action.beep.BeepAction; import alias.action.beep.BeepAction;
@@ -29,6 +27,8 @@ import alias.action.script.ScriptAction;
import alias.action.script.ScriptActionEditor; import alias.action.script.ScriptActionEditor;
import alias.id.AliasID; import alias.id.AliasID;
import alias.id.AliasIDType; import alias.id.AliasIDType;
import alias.id.broadcast.BroadcastChannel;
import alias.id.broadcast.BroadcastChannelEditor;
import alias.id.esn.ESNEditor; import alias.id.esn.ESNEditor;
import alias.id.esn.Esn; import alias.id.esn.Esn;
import alias.id.fleetsync.FleetsyncID; import alias.id.fleetsync.FleetsyncID;
@@ -53,6 +53,9 @@ import alias.id.talkgroup.TalkgroupID;
import alias.id.talkgroup.TalkgroupIDEditor; import alias.id.talkgroup.TalkgroupIDEditor;
import alias.id.uniqueID.UniqueID; import alias.id.uniqueID.UniqueID;
import alias.id.uniqueID.UniqueIDEditor; import alias.id.uniqueID.UniqueIDEditor;
import audio.broadcast.BroadcastModel;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
public class AliasFactory public class AliasFactory
{ {
@@ -60,22 +63,27 @@ public class AliasFactory
{ {
switch( id.getType() ) switch( id.getType() )
{ {
case BROADCAST_CHANNEL:
BroadcastChannel originalBroadcast = (BroadcastChannel)id;
BroadcastChannel copyBroadcast = new BroadcastChannel();
copyBroadcast.setChannelName(originalBroadcast.getChannelName());
return copyBroadcast;
case ESN: case ESN:
Esn originalESN = (Esn)id; Esn originalESN = (Esn)id;
Esn copyESN = new Esn(); Esn copyESN = new Esn();
copyESN.setEsn( originalESN.getEsn() ); copyESN.setEsn( originalESN.getEsn() );
return copyESN; return copyESN;
case Fleetsync: case FLEETSYNC:
FleetsyncID originalFleetsyncID = (FleetsyncID)id; FleetsyncID originalFleetsyncID = (FleetsyncID)id;
FleetsyncID copyFleetsyncID = new FleetsyncID(); FleetsyncID copyFleetsyncID = new FleetsyncID();
copyFleetsyncID.setIdent( originalFleetsyncID.getIdent() ); copyFleetsyncID.setIdent( originalFleetsyncID.getIdent() );
return copyFleetsyncID; return copyFleetsyncID;
case LTRNetUID: case LTR_NET_UID:
UniqueID originalUniqueID = (UniqueID)id; UniqueID originalUniqueID = (UniqueID)id;
UniqueID copyUniqueID = new UniqueID(); UniqueID copyUniqueID = new UniqueID();
copyUniqueID.setUid( originalUniqueID.getUid() ); copyUniqueID.setUid( originalUniqueID.getUid() );
return copyUniqueID; return copyUniqueID;
case LoJack: case LOJACK:
LoJackFunctionAndID originalLoJackFunctionAndID = (LoJackFunctionAndID)id; LoJackFunctionAndID originalLoJackFunctionAndID = (LoJackFunctionAndID)id;
LoJackFunctionAndID copyLoJackFunctionAndID = new LoJackFunctionAndID(); LoJackFunctionAndID copyLoJackFunctionAndID = new LoJackFunctionAndID();
copyLoJackFunctionAndID.setFunction( originalLoJackFunctionAndID.getFunction() ); copyLoJackFunctionAndID.setFunction( originalLoJackFunctionAndID.getFunction() );
@@ -96,24 +104,24 @@ public class AliasFactory
MPT1327ID copyMPT1327ID = new MPT1327ID(); MPT1327ID copyMPT1327ID = new MPT1327ID();
copyMPT1327ID.setIdent( originalMPT1327ID.getIdent() ); copyMPT1327ID.setIdent( originalMPT1327ID.getIdent() );
return copyMPT1327ID; return copyMPT1327ID;
case NonRecordable: case NON_RECORDABLE:
return new NonRecordable(); return new NonRecordable();
case Priority: case PRIORITY:
Priority originalPriority = (Priority)id; Priority originalPriority = (Priority)id;
Priority copyPriority = new Priority(); Priority copyPriority = new Priority();
copyPriority.setPriority( originalPriority.getPriority() ); copyPriority.setPriority( originalPriority.getPriority() );
return copyPriority; return copyPriority;
case Site: case SITE:
SiteID originalSiteID = (SiteID)id; SiteID originalSiteID = (SiteID)id;
SiteID copySiteID = new SiteID(); SiteID copySiteID = new SiteID();
copySiteID.setSite( originalSiteID.getSite() ); copySiteID.setSite( originalSiteID.getSite() );
return copySiteID; return copySiteID;
case Talkgroup: case TALKGROUP:
TalkgroupID originalTalkgroupID = (TalkgroupID)id; TalkgroupID originalTalkgroupID = (TalkgroupID)id;
TalkgroupID copyTalkgroupID = new TalkgroupID(); TalkgroupID copyTalkgroupID = new TalkgroupID();
copyTalkgroupID.setTalkgroup( originalTalkgroupID.getTalkgroup() ); copyTalkgroupID.setTalkgroup( originalTalkgroupID.getTalkgroup() );
return copyTalkgroupID; return copyTalkgroupID;
case Status: case STATUS:
StatusID originalStatusID = (StatusID)id; StatusID originalStatusID = (StatusID)id;
StatusID copyStatusID = new StatusID(); StatusID copyStatusID = new StatusID();
copyStatusID.setStatus( originalStatusID.getStatus() ); copyStatusID.setStatus( originalStatusID.getStatus() );
@@ -183,19 +191,21 @@ public class AliasFactory
return copy; return copy;
} }
public static Editor<AliasID> getEditor( AliasID aliasID ) public static Editor<AliasID> getEditor( AliasID aliasID, BroadcastModel broadcastModel )
{ {
if( aliasID != null ) if( aliasID != null )
{ {
switch( aliasID.getType() ) switch( aliasID.getType() )
{ {
case BROADCAST_CHANNEL:
return new BroadcastChannelEditor(aliasID, broadcastModel);
case ESN: case ESN:
return new ESNEditor( aliasID ); return new ESNEditor( aliasID );
case Fleetsync: case FLEETSYNC:
return new FleetsyncIDEditor( aliasID ); return new FleetsyncIDEditor( aliasID );
case LTRNetUID: case LTR_NET_UID:
return new UniqueIDEditor( aliasID ); return new UniqueIDEditor( aliasID );
case LoJack: case LOJACK:
return new LoJackIDEditor( aliasID ); return new LoJackIDEditor( aliasID );
case MDC1200: case MDC1200:
return new MDC1200IDEditor( aliasID ); return new MDC1200IDEditor( aliasID );
@@ -203,15 +213,15 @@ public class AliasFactory
return new MINEditor( aliasID ); return new MINEditor( aliasID );
case MPT1327: case MPT1327:
return new MPT1327IDEditor( aliasID ); return new MPT1327IDEditor( aliasID );
case NonRecordable: case NON_RECORDABLE:
return new NonRecordableEditor( aliasID ); return new NonRecordableEditor( aliasID );
case Priority: case PRIORITY:
return new PriorityEditor( aliasID ); return new PriorityEditor( aliasID );
case Site: case SITE:
return new SiteIDEditor( aliasID ); return new SiteIDEditor( aliasID );
case Status: case STATUS:
return new StatusIDEditor( aliasID ); return new StatusIDEditor( aliasID );
case Talkgroup: case TALKGROUP:
return new TalkgroupIDEditor( aliasID ); return new TalkgroupIDEditor( aliasID );
default: default:
break; break;
@@ -245,13 +255,15 @@ public class AliasFactory
{ {
switch( type ) switch( type )
{ {
case BROADCAST_CHANNEL:
return new BroadcastChannel();
case ESN: case ESN:
return new Esn(); return new Esn();
case Fleetsync: case FLEETSYNC:
return new FleetsyncID(); return new FleetsyncID();
case LTRNetUID: case LTR_NET_UID:
return new UniqueID(); return new UniqueID();
case LoJack: case LOJACK:
return new LoJackFunctionAndID(); return new LoJackFunctionAndID();
case MDC1200: case MDC1200:
return new MDC1200ID(); return new MDC1200ID();
@@ -259,15 +271,15 @@ public class AliasFactory
return new Min(); return new Min();
case MPT1327: case MPT1327:
return new MPT1327ID(); return new MPT1327ID();
case NonRecordable: case NON_RECORDABLE:
return new NonRecordable(); return new NonRecordable();
case Priority: case PRIORITY:
return new Priority(); return new Priority();
case Site: case SITE:
return new SiteID(); return new SiteID();
case Status: case STATUS:
return new StatusID(); return new StatusID();
case Talkgroup: case TALKGROUP:
return new TalkgroupID(); return new TalkgroupID();
default: default:
throw new IllegalArgumentException( "Unrecognized Alias ID type: " + type ); 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.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern;
public class AliasList implements Listener<AliasEvent> public class AliasList implements Listener<AliasEvent>
{ {
@@ -120,7 +119,7 @@ public class AliasList implements Listener<AliasEvent>
mESN.put(esn, alias); mESN.put(esn, alias);
} }
break; break;
case Fleetsync: case FLEETSYNC:
String fleetsync = ((FleetsyncID)id).getIdent(); String fleetsync = ((FleetsyncID)id).getIdent();
if(fleetsync != null) if(fleetsync != null)
@@ -134,7 +133,7 @@ public class AliasList implements Listener<AliasEvent>
mFleetsync.put(fleetsync, alias); mFleetsync.put(fleetsync, alias);
} }
break; break;
case LoJack: case LOJACK:
mLoJack.put((LoJackFunctionAndID) id, alias); mLoJack.put((LoJackFunctionAndID) id, alias);
break; break;
case MDC1200: case MDC1200:
@@ -179,10 +178,10 @@ public class AliasList implements Listener<AliasEvent>
mMobileID.put(min, alias); mMobileID.put(min, alias);
} }
break; break;
case LTRNetUID: case LTR_NET_UID:
mUniqueID.put(((UniqueID)id).getUid(), alias); mUniqueID.put(((UniqueID)id).getUid(), alias);
break; break;
case Site: case SITE:
String siteID = ((SiteID)id).getSite(); String siteID = ((SiteID)id).getSite();
if(siteID != null) if(siteID != null)
@@ -196,10 +195,10 @@ public class AliasList implements Listener<AliasEvent>
mSiteID.put(siteID, alias); mSiteID.put(siteID, alias);
} }
break; break;
case Status: case STATUS:
mStatus.put(((StatusID) id).getStatus(), alias); mStatus.put(((StatusID) id).getStatus(), alias);
break; break;
case Talkgroup: case TALKGROUP:
String tgid = ((TalkgroupID)id).getTalkgroup(); String tgid = ((TalkgroupID)id).getTalkgroup();
if(tgid != null) if(tgid != null)
@@ -213,8 +212,9 @@ public class AliasList implements Listener<AliasEvent>
mTalkgroup.put(tgid, alias); mTalkgroup.put(tgid, alias);
} }
break; break;
case NonRecordable: case BROADCAST_CHANNEL:
case Priority: case NON_RECORDABLE:
case PRIORITY:
//We don't maintain lookups for these items //We don't maintain lookups for these items
break; break;
default: default:
@@ -279,7 +279,7 @@ public class AliasList implements Listener<AliasEvent>
mESN.remove(esn); mESN.remove(esn);
break; break;
case Fleetsync: case FLEETSYNC:
String fleetsync = ((FleetsyncID)id).getIdent(); String fleetsync = ((FleetsyncID)id).getIdent();
if(fleetsync != null) if(fleetsync != null)
@@ -292,7 +292,7 @@ public class AliasList implements Listener<AliasEvent>
mFleetsync.remove(fleetsync); mFleetsync.remove(fleetsync);
} }
break; break;
case LoJack: case LOJACK:
mLoJack.remove((LoJackFunctionAndID) id); mLoJack.remove((LoJackFunctionAndID) id);
break; break;
case MDC1200: case MDC1200:
@@ -334,16 +334,16 @@ public class AliasList implements Listener<AliasEvent>
mMobileID.remove(min); mMobileID.remove(min);
} }
break; break;
case LTRNetUID: case LTR_NET_UID:
mUniqueID.remove(((UniqueID) id).getUid()); mUniqueID.remove(((UniqueID) id).getUid());
break; break;
case Site: case SITE:
mSiteID.remove(((SiteID) id).getSite()); mSiteID.remove(((SiteID) id).getSite());
break; break;
case Status: case STATUS:
mStatus.remove(((StatusID) id).getStatus()); mStatus.remove(((StatusID) id).getStatus());
break; break;
case Talkgroup: case TALKGROUP:
String tgid = ((TalkgroupID)id).getTalkgroup(); String tgid = ((TalkgroupID)id).getTalkgroup();
if(tgid != null) if(tgid != null)
@@ -356,8 +356,8 @@ public class AliasList implements Listener<AliasEvent>
mTalkgroup.remove(tgid); mTalkgroup.remove(tgid);
} }
break; break;
case NonRecordable: case NON_RECORDABLE:
case Priority: case PRIORITY:
//We don't maintain lookups for these items //We don't maintain lookups for these items
break; break;
default: default:

View File

@@ -26,6 +26,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.table.AbstractTableModel; import javax.swing.table.AbstractTableModel;
import alias.id.broadcast.BroadcastChannel;
import sample.Broadcaster; import sample.Broadcaster;
import sample.Listener; import sample.Listener;
import alias.AliasEvent.Event; 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 @Override
public int getRowCount() public int getRowCount()
{ {
@@ -289,6 +317,14 @@ public class AliasModel extends AbstractTableModel
public void broadcast( AliasEvent event ) 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 ) if( event.getEvent() == Event.CHANGE )
{ {
int index = mAliases.indexOf( event.getAlias() ); int index = mAliases.indexOf( event.getAlias() );

View File

@@ -1,5 +1,8 @@
package alias; package alias;
import alias.id.AliasIDType;
import alias.id.broadcast.BroadcastChannel;
import audio.broadcast.BroadcastModel;
import gui.editor.Editor; import gui.editor.Editor;
import java.awt.Color; import java.awt.Color;
@@ -7,6 +10,7 @@ import java.awt.Dimension;
import java.awt.EventQueue; import java.awt.EventQueue;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultComboBoxModel;
@@ -33,385 +37,429 @@ import settings.SettingsManager;
import alias.AliasEvent.Event; import alias.AliasEvent.Event;
import alias.id.priority.Priority; import alias.id.priority.Priority;
public class MultipleAliasEditor extends Editor<List<Alias>> public class MultipleAliasEditor extends Editor<List<Alias>>
implements Listener<AliasEvent> implements Listener<AliasEvent>
{ {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final static Logger mLog = LoggerFactory.getLogger( MultipleAliasEditor.class ); 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 JLabel mAliasCountLabel; private static String HELP_TEXT =
private JLabel mAliasCount; "<html>Select attributes below to change for all selected aliases</html>";
private JCheckBox mListCheckBox = new JCheckBox( "List" ); private JLabel mAliasCountLabel;
private JComboBox<String> mListCombo = new JComboBox<>(); private JLabel mAliasCount;
private JCheckBox mGroupCheckBox = new JCheckBox( "Group" );
private JComboBox<String> mGroupCombo = new JComboBox<>(); private JCheckBox mListCheckBox = new JCheckBox("List");
private JCheckBox mIconCheckBox = new JCheckBox( "Icon" ); 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 JComboBox<MapIcon> mMapIconCombo;
private JCheckBox mColorCheckBox = new JCheckBox( "Color" ); private JCheckBox mColorCheckBox = new JCheckBox("Color");
private JButton mButtonColor; private JButton mButtonColor;
private JButton mBtnIconManager; private JButton mBtnIconManager;
private JCheckBox mRecordCheckBox = new JCheckBox( "Record" ); private JCheckBox mRecordCheckBox = new JCheckBox("Record");
private JComboBox<Record> mRecordActionCombo = new JComboBox<>( Record.values() ); private JComboBox<Record> mRecordActionCombo = new JComboBox<>(Record.values());
private JCheckBox mPriorityCheckBox = new JCheckBox( "Priority" ); private JCheckBox mPriorityCheckBox = new JCheckBox("Priority");
private JSlider mPrioritySlider; private JSlider mPrioritySlider;
private JLabel mPrioritySliderLabel; private JLabel mPrioritySliderLabel;
private JCheckBox mStreamCheckBox = new JCheckBox("Stream");
private AliasModel mAliasModel; private JComboBox<String> mStreamCombo;
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 );
mListCombo.setEditable( true ); private AliasModel mAliasModel;
add( mListCombo, "wrap" ); 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 ); mAliasModel.addListener(this);
add( mGroupCombo, "wrap" );
add( mColorCheckBox ); init();
}
mButtonColor = new JButton( "Select ..." ); public List<Alias> getAliases()
mButtonColor.addActionListener( new ActionListener() {
{ if (hasItem())
@Override {
public void actionPerformed( ActionEvent e ) 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( Color newColor = JColorChooser.showDialog(
MultipleAliasEditor.this, MultipleAliasEditor.this,
"Choose color for this alias", null ); "Choose color for this alias", null);
if( newColor != null ) if (newColor != null)
{ {
mButtonColor.setForeground( newColor ); mButtonColor.setForeground(newColor);
mButtonColor.setBackground( newColor ); mButtonColor.setBackground(newColor);
} }
} }
} ); });
add( mButtonColor, "wrap" ); add(mButtonColor, "wrap");
add( mIconCheckBox );
mMapIconCombo = new JComboBox<MapIcon>( mSettingsManager.getMapIcons() );
MapIconListCellRenderer renderer = new MapIconListCellRenderer(); add(mIconCheckBox);
renderer.setPreferredSize( new Dimension( 200, 30 ) );
mMapIconCombo.setRenderer( renderer );
add( mMapIconCombo, "wrap" );
//Dummy place holder mMapIconCombo = new JComboBox<MapIcon>(mSettingsManager.getMapIcons());
add( new JLabel() );
MapIconListCellRenderer renderer = new MapIconListCellRenderer();
mBtnIconManager = new JButton( "Icon Manager" ); renderer.setPreferredSize(new Dimension(200, 30));
mBtnIconManager.addActionListener( new ActionListener() mMapIconCombo.setRenderer(renderer);
{ add(mMapIconCombo, "wrap");
@Override
public void actionPerformed( ActionEvent arg0 ) //Dummy place holder
add(new JLabel());
mBtnIconManager = new JButton("Icon Manager");
mBtnIconManager.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent arg0)
{ {
final IconManager iconManager = final IconManager iconManager =
new IconManager( mSettingsManager, MultipleAliasEditor.this ); new IconManager(mSettingsManager, MultipleAliasEditor.this);
EventQueue.invokeLater( new Runnable() EventQueue.invokeLater(new Runnable()
{ {
@Override @Override
public void run() public void run()
{ {
iconManager.setVisible( true ); iconManager.setVisible(true);
} }
} ); });
} }
} ); });
add( mBtnIconManager, "span,wrap" );
add( mRecordCheckBox );
add( mRecordActionCombo );
//Placeholder add(mBtnIconManager, "span,wrap");
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 );
mPrioritySlider.setMajorTickSpacing( 20 ); add(mRecordCheckBox);
mPrioritySlider.setMinorTickSpacing( 5 ); add(mRecordActionCombo);
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();
if( priority > Priority.MAX_PRIORITY ) //Placeholder
{ add(new JLabel(" "));
mPrioritySliderLabel.setText( "Priority: Do Not Monitor" ); mPrioritySliderLabel = new JLabel("Audio Priority: " + Priority.MIN_PRIORITY);
} add(mPrioritySliderLabel);
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 );
JButton resetButton = new JButton( "Reset" ); add(mPriorityCheckBox);
resetButton.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
reset();
}
} );
add( resetButton );
setModified( false ); mPrioritySlider = new JSlider(JSlider.HORIZONTAL,
} Priority.MIN_PRIORITY,
Priority.MAX_PRIORITY + 1,
@Override Priority.MIN_PRIORITY);
public void setItem( List<Alias> item )
{
super.setItem( item );
StringBuilder sb = new StringBuilder(); mPrioritySlider.setMajorTickSpacing(20);
sb.append( "Multiple Selected [ " ); mPrioritySlider.setMinorTickSpacing(5);
mPrioritySlider.setPaintTicks(true);
if( hasItem() ) mPrioritySlider.setLabelTable(mPrioritySlider.createStandardLabels(25, 25));
{ mPrioritySlider.setPaintLabels(true);
sb.append( String.valueOf( getAliases().size() ) ); mPrioritySlider.addChangeListener(new ChangeListener()
} {
else @Override
{ public void stateChanged(ChangeEvent e)
sb.append( "0" ); {
} int priority = mPrioritySlider.getValue();
sb.append( " ]" ); if (priority > Priority.MAX_PRIORITY)
{
mAliasCount.setText( sb.toString() ); mPrioritySliderLabel.setText("Audio Priority: Do Not Monitor");
}
else
{
mPrioritySliderLabel.setText("Audio Priority: " + mPrioritySlider.getValue());
}
}
});
mPrioritySlider.setToolTipText(HELP_TEXT);
List<String> listNames = mAliasModel.getListNames(); add(mPrioritySlider);
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() ] ) ) );
setCheckBoxesUnselected(); add(mStreamCheckBox);
setModified( false );
}
private void setCheckBoxesUnselected()
{
mListCheckBox.setSelected( false );
mGroupCheckBox.setSelected( false );
mColorCheckBox.setSelected( false );
mIconCheckBox.setSelected( false );
mRecordCheckBox.setSelected( false );
mPriorityCheckBox.setSelected( false );
}
@Override mStreamCombo = new JComboBox<String>();
public void reset() mStreamCombo.setEditable(true);
{ add(mStreamCombo, "wrap");
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();
}
@Override JButton saveButton = new JButton("Save");
public void save() saveButton.addActionListener(new ActionListener()
{ {
if( hasRequestedChanges() && hasItem() ) @Override
{ public void actionPerformed(ActionEvent e)
List<Alias> aliases = getAliases(); {
save();
for( Alias alias: aliases ) }
{ });
if( mListCheckBox.isSelected() ) add(saveButton);
{
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 ); JButton resetButton = new JButton("Reset");
} resetButton.addActionListener(new ActionListener()
{
if( mPriorityCheckBox.isSelected() ) @Override
{ public void actionPerformed(ActionEvent e)
int priority = mPrioritySlider.getValue(); {
reset();
}
});
add(resetButton);
//This is a work-around -- we use max priority + 1 in the setModified(false);
//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 );
}
}
for( Alias alias: aliases ) @Override
{ public void setItem(List<Alias> item)
//Broadcast an alias change event to save the updates {
mAliasModel.broadcast( new AliasEvent( alias, Event.CHANGE ) ); super.setItem(item);
}
}
}
@Override StringBuilder sb = new StringBuilder();
public void receive( AliasEvent event ) sb.append("Multiple Selected [ ");
{
if( event.getEvent() == Event.DELETE && if (hasItem())
hasItem() && {
getAliases().contains( event.getAlias() ) ) sb.append(String.valueOf(getAliases().size()));
{ }
getAliases().remove( event.getAlias() ); else
setItem( getAliases() ); {
} sb.append("0");
} }
public enum Record sb.append(" ]");
{
NON_RECORDABLE( "Non-Recordable" ), mAliasCount.setText(sb.toString());
RECORDABLE( "Recordable" );
List<String> listNames = mAliasModel.getListNames();
private String mLabel; listNames.add(0, "");
mListCombo.setModel(new DefaultComboBoxModel<String>(
private Record( String label ) listNames.toArray(new String[listNames.size()])));
{
mLabel = label; List<String> groupNames = mAliasModel.getGroupNames();
} groupNames.add(0, "");
mGroupCombo.setModel(new DefaultComboBoxModel<String>(
public String toString() groupNames.toArray(new String[groupNames.size()])));
{
return mLabel; 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.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlTransient;
import alias.id.broadcast.BroadcastChannel;
import playlist.version1.Group; import playlist.version1.Group;
import alias.id.esn.Esn; import alias.id.esn.Esn;
import alias.id.fleetsync.FleetsyncID; import alias.id.fleetsync.FleetsyncID;
@@ -36,7 +37,7 @@ import alias.id.talkgroup.TalkgroupID;
import alias.id.uniqueID.UniqueID; import alias.id.uniqueID.UniqueID;
@SuppressWarnings( "deprecation" ) @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, MDC1200ID.class, Min.class, MPT1327ID.class, NonRecordable.class,
Priority.class, SiteID.class, StatusID.class, TalkgroupID.class, Priority.class, SiteID.class, StatusID.class, TalkgroupID.class,
UniqueID.class } ) UniqueID.class } )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -70,6 +70,6 @@ public class UniqueID extends AliasID
@Override @Override
public AliasIDType getType() 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