#30 - adds streaming status color indicators to the streaming panel table.

Resolves issue with temporary streaming file name collisions by adding a 1-up number to each generated temporary recording file.
This commit is contained in:
Denny
2017-01-03 06:41:22 -05:00
parent 42c18341b6
commit 4578e45f60
37 changed files with 1236 additions and 692 deletions

View File

@@ -223,7 +223,8 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
}
catch(IOException ioe)
{
mLog.error("Error deleting temporary internet recording file: " + recording.getPath().toString());
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] - error deleting temporary internet " +
"recording file: " + recording.getPath().toString() + " - " + ioe.getMessage());
}
}
@@ -354,44 +355,51 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{
if(mProcessing.compareAndSet(false, true))
{
if(mInputStream == null || mInputStream.available() <= 0)
try
{
if(mFinalSilencePadding > 0)
if(mInputStream == null || mInputStream.available() <= 0)
{
broadcastAudio(mSilenceGenerator.generate(mFinalSilencePadding));
mFinalSilencePadding = 0;
if(mFinalSilencePadding > 0)
{
broadcastAudio(mSilenceGenerator.generate(mFinalSilencePadding));
mFinalSilencePadding = 0;
}
nextRecording();
}
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
if(mInputStream != null)
{
mBytesStreamedActual += mInputStream.read(audio);
//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;
broadcastAudio(audio);
//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);
}
}
catch(IOException ioe)
else
{
mLog.error("Error reading from in-memory audio recording input stream", ioe);
broadcastAudio(mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS));
}
}
else
catch(Exception e)
{
broadcastAudio(mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS));
mLog.error("Error while processing audio streaming queue", e);
}
mProcessing.set(false);
@@ -447,17 +455,18 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
metadataUpdateRequired = false;
}
removeRecording(recording);
}
catch(IOException ioe)
{
mLog.error("Error reading temporary audio stream recording [" + recording.getPath().toString() +
"] - skipping recording");
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] error reading temporary audio " +
"stream recording [" + recording.getPath().toString() + "] - skipping recording - ", ioe);
mInputStream = null;
metadataUpdateRequired = false;
}
removeRecording(recording);
broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}

View File

@@ -53,11 +53,11 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
private static final int COLUMN_SERVER_ICON = 0;
private static final int COLUMN_STREAM_NAME = 1;
private static final int COLUMN_BROADCASTER_STATUS = 2;
private static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
private static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
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<>();
@@ -513,15 +513,15 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
if(audioBroadcasterA != null)
{
return audioBroadcasterA.getBroadcastState().toString();
return audioBroadcasterA.getBroadcastState();
}
else if(!configuration.isEnabled())
{
return "Disabled";
return BroadcastState.DISABLED;
}
else if(!configuration.isValid())
{
return "Invalid Settings";
return BroadcastState.INVALID_SETTINGS;
}
case COLUMN_BROADCASTER_QUEUE_SIZE:
AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());

View File

@@ -33,6 +33,9 @@ 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;
@@ -77,8 +80,10 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setPreferredWidth(1);
mBroadcastConfigurationTable.getColumnModel().getColumn(1).setPreferredWidth(25);
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);
@@ -261,4 +266,67 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
}
}
/**
* 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

@@ -35,6 +35,11 @@ public enum BroadcastState
*/
CONNECTING("Connecting", false),
/**
* Indicates the configuration is disabled
*/
DISABLED("Disabled", true),
/**
* Disconnected from the broadcastAudio server
*/
@@ -50,6 +55,11 @@ public enum BroadcastState
*/
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
/**
* Invalid configuration settings
*/
INVALID_SETTINGS("Invalid Settings", true),
/**
* Remote server max sources has been exceeded
*/

View File

@@ -30,18 +30,20 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
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 AudioBroadcaster mAudioBroadcaster;
private Path mTempDirectory;
@@ -197,6 +199,7 @@ public class StreamManager implements Listener<AudioPacket>
{
StringBuilder sb = new StringBuilder();
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
sb.append(sNextRecordingNumber.incrementAndGet()).append("_");
sb.append( TimeStamp.getLongTimeStamp( "_" ) );
sb.append( mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat().getFileExtension() );
@@ -221,6 +224,9 @@ public class StreamManager implements Listener<AudioPacket>
.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

@@ -1,47 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v1;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.shoutcast.ShoutcastMetadataUpdater;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
public abstract class ShoutcastAudioBroadcaster extends AudioBroadcaster
{
private ShoutcastMetadataUpdater mMetadataUpdater;
public ShoutcastAudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
{
super(threadPoolManager, broadcastConfiguration);
}
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new ShoutcastMetadataUpdater(getThreadPoolManager(),
(ShoutcastV1Configuration)getBroadcastConfiguration());
}
mMetadataUpdater.update(metadata);
}
}

View File

@@ -18,7 +18,9 @@
******************************************************************************/
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;
@@ -37,13 +39,14 @@ import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1AudioBroadcaster extends ShoutcastAudioBroadcaster
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();
@@ -71,6 +74,16 @@ public class ShoutcastV1AudioBroadcaster extends ShoutcastAudioBroadcaster
return (ShoutcastV1Configuration)getBroadcastConfiguration();
}
@Override
protected IBroadcastMetadataUpdater getMetadataUpdater()
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getThreadPoolManager(), getConfiguration());
}
return mMetadataUpdater;
}
/**
* Broadcasts the audio frame or sequence

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
* 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
@@ -16,12 +16,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast;
package audio.broadcast.shoutcast.v1;
import audio.broadcast.icecast.IcecastConfiguration;
import audio.broadcast.icecast.IcecastHeader;
import audio.broadcast.shoutcast.v1.ShoutcastMetadata;
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.IBroadcastMetadataUpdater;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
@@ -29,9 +26,6 @@ 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.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.http.HttpClientCodec;
import org.apache.mina.http.HttpRequestImpl;
import org.apache.mina.http.api.HttpMethod;
@@ -40,7 +34,6 @@ 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;
@@ -53,9 +46,9 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastMetadataUpdater
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastMetadataUpdater.class);
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
private final static String UTF8 = "UTF-8";
private ThreadPoolManager mThreadPoolManager;
@@ -73,7 +66,7 @@ public class ShoutcastMetadataUpdater
* 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 ShoutcastMetadataUpdater(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration shoutcastV1Configuration)
public ShoutcastV1BroadcastMetadataUpdater(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration shoutcastV1Configuration)
{
mThreadPoolManager = threadPoolManager;
mShoutcastV1Configuration = shoutcastV1Configuration;
@@ -89,7 +82,7 @@ public class ShoutcastMetadataUpdater
mSocketConnector = new NioSocketConnector();
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(ShoutcastMetadataUpdater.class));
// new LoggingFilter(ShoutcastV1BroadcastMetadataUpdater.class));
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
@@ -257,7 +250,6 @@ public class ShoutcastMetadataUpdater
sb.append("&song=").append(URLEncoder.encode(song, UTF8));
Map<String,String> headers = new HashMap<>();
// headers.put("User-Agent", SystemProperties.getInstance().getApplicationName() + " (Mozilla compatible)");
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_0, HttpMethod.GET, "/admin.cgi",
sb.toString(), headers);

View File

@@ -239,7 +239,6 @@ public class ShoutcastV1ConfigurationEditor extends BroadcastConfigurationEditor
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setDescription(mDescription.getText());
config.setPassword(mPassword.getText());
config.setDescription(mDescription.getText());
config.setGenre(mGenre.getText());

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
* 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
@@ -18,120 +18,199 @@
******************************************************************************/
package audio.broadcast.shoutcast.v2;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFactory;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast;
import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre;
import audio.broadcast.shoutcast.v2.message.ConfigureIcyName;
import audio.broadcast.shoutcast.v2.message.ConfigureIcyPublic;
import audio.broadcast.shoutcast.v2.message.ConfigureIcyURL;
import audio.broadcast.shoutcast.v2.message.MP3Audio;
import audio.broadcast.shoutcast.v2.message.NegotiateMaxPayloadSize;
import audio.broadcast.shoutcast.v2.message.RequestCipher;
import audio.broadcast.shoutcast.v2.message.SetupBroadcast;
import audio.broadcast.shoutcast.v2.message.Standby;
import audio.broadcast.shoutcast.v2.message.UltravoxMessage;
import audio.broadcast.shoutcast.v2.message.UltravoxMessageFactory;
import audio.broadcast.shoutcast.v2.message.UltravoxMessageType;
import audio.broadcast.shoutcast.v2.message.UltravoxMetadata;
import audio.broadcast.shoutcast.v2.message.XMLMetadata;
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 record.wave.AudioPacketMonoWaveReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
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
public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2AudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private static final boolean GET_RESPONSE = true;
private static final boolean NO_RESPONSE_EXPECTED = false;
private long mLastConnectionAttempt = 0;
private Socket mSocket;
private DataOutputStream mOutputStream;
private DataInputStream mInputStream;
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private int mMaxPayloadSize = 16377;
private List<AudioPacket> mPacketsToBroadcast = new ArrayList<>();
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
private LinkedTransferQueue<UltravoxMessage> mMetadataMessageQueue = new LinkedTransferQueue<>();
/**
* Creates a Shoutcast Version 2 broadcaster.
* @param configuration details for shoutcast version 2
* 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);
}
/**
* Shoutcast V2 Configuration information
*/
private ShoutcastV2Configuration getShoutcastConfiguration()
public ShoutcastV2Configuration getConfiguration()
{
return (ShoutcastV2Configuration)getBroadcastConfiguration();
}
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
mLog.debug("Request to broadcast audio metadata to shoutcast v2 server - needs coding");
}
/**
* Broadcast audio
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
{
//If we're connected, send the audio, otherwise discard it
if(connect())
{
List<UltravoxMessage> audioMessages = getAudioMessages(audio);
//Dispatch any queued metadata messages
UltravoxMessage metadataMessage = mMetadataMessageQueue.poll();
try
{
for(UltravoxMessage audioMessage: audioMessages)
{
send(audioMessage, NO_RESPONSE_EXPECTED);
}
}
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
return;
}
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;
}
/**
* Indicates if the audio handler is currently connected to the remote server and capable of streaming audio.
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
* is currently connected to the remote server following any connection attempts.
*
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
*
* This method will attempt to establish a connection or reconnect to the streaming server.
* @return true if the audio handler can stream audio
*/
private boolean connect()
{
if(!connected())
if(!connected() && canConnect() &&
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
mConnecting.compareAndSet(false, true))
{
createConnection();
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();
@@ -139,112 +218,85 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
/**
* Disconnect from the remote server
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect()
{
// TerminateBroadcast terminate = (TerminateBroadcast)UltravoxMessageFactory
// .getMessage(UltravoxMessageType.TERMINATE_BROADCAST);
// response = send(terminate, out, in);
// mLog.debug("Terminate Response:" + (response.getPayload() != null ? response.getPayload() : ""));
}
/**
* Sends the message. When get response is true, reads from the input stream and converts the response to a
* message that is returned.
*
* @param message to send to the shoutcast/ultravox server
* @param getResponse true if the sent message will generate a response
* @return response message when getResponse is set to true
* @throws IOException if there is an issue while communicating with the server
*/
private UltravoxMessage send(UltravoxMessage message, boolean getResponse) throws IOException
{
byte[] bytes = message.getMessage();
mOutputStream.write(bytes);
if(getResponse)
if(connected())
{
try
if(mStreamingSession != null)
{
Thread.sleep(50);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
int available = mInputStream.available();
if(available > 0)
{
byte[] buffer = new byte[available];
int read = mInputStream.read(buffer);
return UltravoxMessageFactory.getMessage(buffer);
mStreamingSession.write(new TerminateBroadcast());
mStreamingSession.closeNow();
}
}
return null;
}
/**
* Loads the encoded audio byte array into a list of ultravox messages ready for transmission
* @param audio to embed as message payload
* @return list of messages containing the audio payload
*/
private List<UltravoxMessage> getAudioMessages(byte[] audio)
{
List<UltravoxMessage> messages = new ArrayList<>();
int pointer = 0;
while(pointer < audio.length)
{
int payloadSize = Math.min(mMaxPayloadSize, audio.length - pointer);
byte[] payload = new byte[payloadSize];
System.arraycopy(audio, pointer, payload, 0, payloadSize);
MP3Audio mp3Audio = (MP3Audio)UltravoxMessageFactory.getMessage(UltravoxMessageType.MP3_DATA);
mp3Audio.setPayload(payload);
messages.add(mp3Audio);
pointer += payloadSize;
}
return messages;
}
/**
* Encodes the list of metadata in one or more Ultravox XMLMetadata messages according to the maximum negotiated
* 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 xmlMetadata list of xml encoded values
* @return a sequence of XMLMetadata messages sufficient to carry the complete set of metadata
* @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(List<String> xmlMetadata)
private List<UltravoxMessage> getMetadataMessages(AudioMetadata metadata)
{
List<UltravoxMessage> messages = new ArrayList<>();
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<metadata>\n");
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?><metadata>");
for(String metadata: xmlMetadata)
sb.append(UltravoxMetadata.ALBUM_TITLE.asXML(getConfiguration().getName()));
sb.append(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION.asXML(SystemProperties.getInstance().getApplicationName()));
if(getConfiguration().hasGenre())
{
sb.append(metadata);
sb.append(UltravoxMetadata.GENRE.asXML(getConfiguration().getGenre()));
}
sb.append("\n</metadata>");
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>");
mLog.debug("Metadata:" + sb.toString());
byte[] xml = sb.toString().getBytes();
int pointer = 0;
@@ -257,6 +309,8 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
}
List<UltravoxMessage> messages = new ArrayList<>();
while(pointer < xml.length && messageCounter <= messageCount)
{
int payloadSize = Math.min(mMaxPayloadSize - 6, xml.length - pointer);
@@ -269,7 +323,7 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
System.arraycopy(xml, pointer, payload, 6, payloadSize);
XMLMetadata message = (XMLMetadata)UltravoxMessageFactory.getMessage(UltravoxMessageType.XML_METADATA);
CacheableXMLMetadata message = new CacheableXMLMetadata();
message.setPayload(payload);
messages.add(message);
@@ -281,235 +335,194 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
/**
* Creates a connnection to the remote server using the shoutcast configuration information. Once disconnected
* following a successful connection, attempts to reestablish a connection on a set interval
* IO Handler for managing Shoutcast V2 connection and credentials
*/
private void createConnection()
public class ShoutcastV2IOHandler extends IoHandlerAdapter
{
if(!connected() && System.currentTimeMillis() - mLastConnectionAttempt >= RECONNECT_INTERVAL_MILLISECONDS)
/**
* Sends stream configuration and user credentials upon connecting to remote server
*/
@Override
public void sessionOpened(IoSession session) throws Exception
{
mLastConnectionAttempt = System.currentTimeMillis();
session.write(UltravoxMessageFactory.getMessage(UltravoxMessageType.REQUEST_CIPHER));
}
createSocket();
if (mSocket.isConnected())
@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())
{
RequestCipher requestCipher = (RequestCipher) UltravoxMessageFactory
.getMessage(UltravoxMessageType.REQUEST_CIPHER);
setBroadcastState(BroadcastState.DISCONNECTED);
}
try
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())
{
UltravoxMessage response = send(requestCipher, GET_RESPONSE);
case REQUEST_CIPHER:
String cipherKey = ((RequestCipher)ultravoxMessage).getCipher();
if (response instanceof RequestCipher)
{
RequestCipher cipherResponse = (RequestCipher) response;
AuthenticateBroadcast authenticateBroadcast = new AuthenticateBroadcast();
AuthenticateBroadcast authenticateBroadcast = (AuthenticateBroadcast) UltravoxMessageFactory
.getMessage(UltravoxMessageType.AUTHENTICATE_BROADCAST);
authenticateBroadcast.setCredentials(cipherResponse.getCipher(),
getShoutcastConfiguration().getStreamID(), getShoutcastConfiguration().getUserID(),
getShoutcastConfiguration().getPassword());
response = send(authenticateBroadcast, GET_RESPONSE);
authenticateBroadcast.setCredentials(cipherKey, getConfiguration().getStreamID(),
getConfiguration().getUserID(), getConfiguration().getPassword());
if (response.isErrorResponse())
session.write(authenticateBroadcast);
break;
case AUTHENTICATE_BROADCAST:
if(ultravoxMessage.isErrorResponse())
{
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
return;
}
String errorMessage = ultravoxMessage.getErrorMessage();
UltravoxMessage streamMimeType = UltravoxMessageFactory
.getMessage(UltravoxMessageType.STREAM_MIME_TYPE);
streamMimeType.setPayload(getShoutcastConfiguration().getBroadcastFormat().getValue());
response = send(streamMimeType, GET_RESPONSE);
if (response.isErrorResponse())
{
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
mLog.error("Audio format [" + getShoutcastConfiguration().getBroadcastFormat() + "] not supported");
return;
}
SetupBroadcast setupBroadcast = (SetupBroadcast) UltravoxMessageFactory
.getMessage(UltravoxMessageType.SETUP_BROADCAST);
setupBroadcast.setBitRate(getShoutcastConfiguration().getBitRate(),
getShoutcastConfiguration().getBitRate());
response = send(setupBroadcast, GET_RESPONSE);
if (response.isErrorResponse())
{
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
mLog.error("Audio bit rate [" + getShoutcastConfiguration().getBitRate() + "] not supported");
return;
}
NegotiateMaxPayloadSize negotiateMaxPayloadSize = (NegotiateMaxPayloadSize) UltravoxMessageFactory
.getMessage(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE);
negotiateMaxPayloadSize.setMaximumPayloadSize(mMaxPayloadSize, 14000);
response = send(negotiateMaxPayloadSize, GET_RESPONSE);
mMaxPayloadSize = ((NegotiateMaxPayloadSize) response).getMaximumPayloadSize();
List<String> metadata = new ArrayList<>();
if (getShoutcastConfiguration().getStreamName() != null)
{
metadata.add(UltravoxMetadata.TITLE_1.asXML(getShoutcastConfiguration().getStreamName()));
ConfigureIcyName icyName = (ConfigureIcyName) UltravoxMessageFactory
.getMessage(UltravoxMessageType.CONFIGURE_ICY_NAME);
icyName.setName(getShoutcastConfiguration().getStreamName());
response = send(icyName, NO_RESPONSE_EXPECTED);
}
if (getShoutcastConfiguration().getGenre() != null)
{
metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getGenre()));
ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory
.getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE);
icyGenre.setGenre(getShoutcastConfiguration().getGenre());
response = send(icyGenre, NO_RESPONSE_EXPECTED);
}
if (getShoutcastConfiguration().getURL() != null)
{
ConfigureIcyURL icyURL = (ConfigureIcyURL) UltravoxMessageFactory
.getMessage(UltravoxMessageType.CONFIGURE_ICY_URL);
icyURL.setURL(getShoutcastConfiguration().getURL());
response = send(icyURL, NO_RESPONSE_EXPECTED);
}
ConfigureIcyPublic icyPublic = (ConfigureIcyPublic) UltravoxMessageFactory
.getMessage(UltravoxMessageType.CONFIGURE_ICY_PUBLIC);
icyPublic.setPublic(getShoutcastConfiguration().isPublic());
response = send(icyPublic, NO_RESPONSE_EXPECTED);
Standby standby = (Standby) UltravoxMessageFactory.getMessage(UltravoxMessageType.STANDBY);
response = send(standby, GET_RESPONSE);
if (response.isValidResponse())
{
setBroadcastState(BroadcastState.CONNECTED);
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
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLog.error("Error message after configuration and sending standby:" +
(response.getError() != null ? response.getError() : "no error message"));
StreamMimeType streamMimeType = new StreamMimeType();
streamMimeType.setFormat(getConfiguration().getBroadcastFormat());
session.write(streamMimeType);
}
metadata.add(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION
.asXML(SystemProperties.getInstance().getApplicationName()));
mLog.debug("Fetching metadata messages from metadata...");
List<UltravoxMessage> messages = getMetadataMessages(metadata);
for (UltravoxMessage message : messages)
break;
case STREAM_MIME_TYPE:
if(ultravoxMessage.isErrorResponse())
{
mLog.debug("Sending metadata message");
send(message, GET_RESPONSE);
mLog.error("Unsupported Audio Format:" + getConfiguration().getBroadcastFormat().toString() +
" - " + ultravoxMessage.getErrorMessage());
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
}
else
{
SetupBroadcast setupBroadcast = new SetupBroadcast();
mLog.debug("We're connected!!!");
}
}
catch (IOException e)
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Error while creating connection to server", e);
//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());
/**
* Creates a socket connection to the remote server and sets the state to CONNECTING.
*/
private void createSocket()
{
if(mSocket == null)
{
try
{
mSocket = new Socket(getShoutcastConfiguration().getHost(),
getShoutcastConfiguration().getPort());
mOutputStream = new DataOutputStream(mSocket.getOutputStream());
mInputStream = new DataInputStream(mSocket.getInputStream());
}
catch(UnknownHostException uhe)
{
setBroadcastState(BroadcastState.NO_SERVER);
mLog.error("Unknown host or port. Unable to create connection to streaming server host[" +
getShoutcastConfiguration().getHost() + "] and port[" +
getShoutcastConfiguration().getPort() + "] - will reattempt connection periodically");
return;
}
catch(IOException ioe)
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Error connecting to streaming server host[" +
getShoutcastConfiguration().getHost() + "] and port[" +
getShoutcastConfiguration().getPort() + "]", ioe);
return;
}
}
if(mSocket.isConnected())
{
setBroadcastState(BroadcastState.CONNECTING);
}
else
{
try
{
SocketAddress address = getShoutcastConfiguration().getAddress();
mSocket.connect(address);
setBroadcastState(BroadcastState.CONNECTING);
}
catch(UnknownHostException uhe)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
catch(IOException e)
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Error connecting to streaming server host[" +
getShoutcastConfiguration().getHost() + "] and port[" +
getShoutcastConfiguration().getPort() + "]", e);
}
}
}
public static UltravoxMessage send(UltravoxMessage message,
DataOutputStream out,
DataInputStream in,
int maxPayloadSize) throws IOException
{
byte[] bytes = message.getMessage();
out.write(bytes);
try
{
Thread.sleep(50);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
int available = in.available();
if(available > 0)
{
byte[] buffer = new byte[available];
int read = in.read(buffer);
return UltravoxMessageFactory.getMessage(buffer);
}
return null;
}
}

View File

@@ -22,12 +22,13 @@ 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 mStreamName;
private String mGenre;
private String mURL;
private boolean mPublic = true;
@@ -59,7 +60,6 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
copy.setStreamID(getStreamID());
copy.setUserID(getUserID());
copy.setBitRate(getBitRate());
copy.setStreamName(getStreamName());
copy.setGenre(getGenre());
copy.setURL(getURL());
copy.setPublic(isPublic());
@@ -77,6 +77,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
/**
* Stream ID
*/
@XmlAttribute(name = "stream_id")
public int getStreamID()
{
return mStreamID;
@@ -96,6 +97,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
/**
* User ID
*/
@XmlAttribute(name = "user_id")
public String getUserID()
{
return mUserID;
@@ -113,6 +115,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
/**
* Bit rate in kilobits per second
*/
@XmlAttribute(name = "bitrate")
public int getBitRate()
{
return mBitRate;
@@ -127,26 +130,10 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
mBitRate = bitRate;
}
/**
* Stream name
*/
public String getStreamName()
{
return mStreamName;
}
/**
* Sets the stream name
* @param name
*/
public void setStreamName(String name)
{
mStreamName = name;
}
/**
* Stream genre
*/
@XmlAttribute(name = "genre")
public String getGenre()
{
return mGenre;
@@ -160,9 +147,16 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
mGenre = genre;
}
public boolean hasGenre()
{
return mGenre != null && !mGenre.isEmpty();
}
/**
* Public visibility of the broadcastAudio
*/
@XmlAttribute(name = "public")
public boolean isPublic()
{
return mPublic;
@@ -180,6 +174,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
/**
* URL associated with the broadcastAudio where users can find additional details.
*/
@XmlAttribute(name = "url")
public String getURL()
{
return mURL;
@@ -193,4 +188,18 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
{
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

@@ -1,106 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
public class XMLMetadata extends UltravoxMessage
{
/**
* Client request to server to send cacheable XML metadata
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
XMLMetadata()
{
super(UltravoxMessageType.XML_METADATA);
}
/**
* Server response to client request
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*
* @param data bytes received from the server
*/
XMLMetadata(byte[] data)
{
super(data);
}
// /**
// * Adds custom metadata (TXXX) tagged value to the metadata set. The metadata value is identified by the
// * id parameter.
// *
// * @param id tag to identify the type of metadata
// * @param value for the id tag
// */
// public void addCustomMetadata(String id, String value)
// {
// StringBuilder sb = new StringBuilder();
// sb.append("<").append(UltravoxMetadata.CUSTOM_TEXT_FIELD.getXMLTag());
// sb.append(" id=\"").append(id).append("\">");
// sb.append(value);
// sb.append("</").append(UltravoxMetadata.CUSTOM_TEXT_FIELD.getXMLTag()).append(">");
//
// mMetadata.add(sb.toString());
// }
//
// /**
// * Adds the binary object data to the metadata set.
// * @param id tag to identify the binary object
// * @param mime type
// * @param filename associated with the data
// * @param base64Data base-64 encoded binary data to embed in the metadata tag
// */
// public void addBinaryObject(String id, String mime, String filename, String base64Data)
// {
// StringBuilder sb = new StringBuilder();
// sb.append("<").append(UltravoxMetadata.GENERAL_BINARY_OBJECT.getXMLTag());
// sb.append(" mime=\"").append(mime).append("\"");
// sb.append(" filename=\"").append(filename).append("\"");
// sb.append(" id=\"").append(id).append("\"");
// sb.append(">");
// sb.append(base64Data);
// sb.append("</").append(UltravoxMetadata.GENERAL_BINARY_OBJECT.getXMLTag()).append(">");
//
// mMetadata.add(sb.toString());
// }
//
// /**
// * Adds a recording time (TDRC) tagged metadata timestamp
// *
// * @param timestamp for the recording time
// */
// public void addRecordingTime(long timestamp)
// {
// Calendar calendar = new GregorianCalendar();
// calendar.setTimeInMillis(timestamp);
//
// StringBuilder sb = new StringBuilder();
// sb.append("<TDRC>\n");
// sb.append("<YEAR>").append(calendar.get(Calendar.YEAR)).append("</YEAR>\n");
// sb.append("<MONTH>").append(calendar.get(Calendar.MONTH)).append("</MONTH>\n");
// sb.append("<DAY>").append(calendar.get(Calendar.DAY_OF_MONTH)).append("</DAY>\n");
// sb.append("<HOUR>").append(calendar.get(Calendar.HOUR_OF_DAY)).append("</HOUR>\n");
// sb.append("<MINUTE>").append(calendar.get(Calendar.MINUTE)).append("</MINUTE>\n");
// sb.append("<ZONE>").append(calendar.get(Calendar.ZONE_OFFSET)).append("</ZONE>\n");
// sb.append("</TDRC>\n");
//
// mMetadata.add(sb.toString());
// }
}

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
import util.XTEA;
@@ -24,12 +24,12 @@ 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
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
AuthenticateBroadcast()
public AuthenticateBroadcast()
{
super(UltravoxMessageType.AUTHENTICATE_BROADCAST);
}
@@ -60,7 +60,7 @@ public class AuthenticateBroadcast extends UltravoxMessage
StringBuilder sb = new StringBuilder();
sb.append(SHOUTCAST_VERSION);
sb.append(ULTRAVOX_VERSION);
sb.append(":");
sb.append(streamID);
sb.append(":");

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

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class ConfigureIcyGenre extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-GENRE metadata
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
ConfigureIcyGenre()
public ConfigureIcyGenre()
{
super(UltravoxMessageType.CONFIGURE_ICY_GENRE);
}

View File

@@ -16,20 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
import util.XTEA;
import java.util.Arrays;
package audio.broadcast.shoutcast.v2.ultravox;
public class ConfigureIcyName extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-NAME metadata
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
ConfigureIcyName()
public ConfigureIcyName()
{
super(UltravoxMessageType.CONFIGURE_ICY_NAME);
}

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class ConfigureIcyPublic extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-PUBLIC metadata
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
ConfigureIcyPublic()
public ConfigureIcyPublic()
{
super(UltravoxMessageType.CONFIGURE_ICY_PUBLIC);
}

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class ConfigureIcyURL extends UltravoxMessage
{
/**
* Client request to server to configure the ICY-URL metadata
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
ConfigureIcyURL()
public ConfigureIcyURL()
{
super(UltravoxMessageType.CONFIGURE_ICY_URL);
}

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class FlushCachedMetadata extends UltravoxMessage
{
/**
* Client request to server to flush any currently cached stream metadata
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
FlushCachedMetadata()
public FlushCachedMetadata()
{
super(UltravoxMessageType.FLUSH_CACHED_METADATA);
}

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class MP3Audio extends UltravoxMessage
{
@@ -25,7 +25,7 @@ public class MP3Audio extends UltravoxMessage
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
MP3Audio()
public MP3Audio()
{
super(UltravoxMessageType.MP3_DATA);
}

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class NegotiateBufferSize extends UltravoxMessage
{
/**
* Client request to server to negotiate the server's buffer size
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
NegotiateBufferSize()
public NegotiateBufferSize()
{
super(UltravoxMessageType.NEGOTIATE_BUFFER_SIZE);
}
@@ -55,10 +53,10 @@ public class NegotiateBufferSize extends UltravoxMessage
}
/**
* Negotiated buffer size from a server response message.
* Negotiated buffer size from a server response ultravox.
*
* @return server negotiated buffer size, or zero if this is not a server response message, or this is an error
* server response message, or the server response message buffer size cannot be determined from the message payload.
* @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()
{

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class NegotiateMaxPayloadSize extends UltravoxMessage
{
/**
* Client request to server to negotiate maximum payload size
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
NegotiateMaxPayloadSize()
public NegotiateMaxPayloadSize()
{
super(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE);
}
@@ -42,7 +40,7 @@ public class NegotiateMaxPayloadSize extends UltravoxMessage
}
/**
* Sets the maximum payload size.
* Sets the maximum payload size in bytes
*
* @param desiredMaximum payload size (no larger than 16377)
* @param minimum acceptable payload size (no larger than 16377)
@@ -59,11 +57,11 @@ public class NegotiateMaxPayloadSize extends UltravoxMessage
}
/**
* Negotiated maximum payload size from a server response message.
* Negotiated maximum payload size from a server response ultravox.
*
* @return server negotiated maximum payload size, or zero if this is not a server response message, or this is an
* error server response message, or the server response message maximum payload size cannot be determined from the
* message payload.
* @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()
{

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

@@ -16,19 +16,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class RequestCipher extends UltravoxMessage
{
/**
* Client request to server for encryption key
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
RequestCipher()
public RequestCipher()
{
super(UltravoxMessageType.REQUEST_CIPHER);
setPayload(SHOUTCAST_VERSION);
setPayload(ULTRAVOX_VERSION);
}
/**

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class SetupBroadcast extends UltravoxMessage
{
/**
* Client request to server to specify the stream bit rate
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
SetupBroadcast()
public SetupBroadcast()
{
super(UltravoxMessageType.SETUP_BROADCAST);
}

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class Standby extends UltravoxMessage
{
/**
* Client request to server to notify that configuration is complete, prepare for streaming
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
Standby()
public Standby()
{
super(UltravoxMessageType.STANDBY);
}

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
import audio.broadcast.BroadcastFormat;
@@ -24,10 +24,8 @@ public class StreamMimeType extends UltravoxMessage
{
/**
* Client request to server to set stream MIME type
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
StreamMimeType()
public StreamMimeType()
{
super(UltravoxMessageType.STREAM_MIME_TYPE);
}

View File

@@ -16,16 +16,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class TerminateBroadcast extends UltravoxMessage
{
/**
* Client request to server to terminate the broadcastAudio
*
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/
TerminateBroadcast()
public TerminateBroadcast()
{
super(UltravoxMessageType.TERMINATE_BROADCAST);
}

View File

@@ -0,0 +1,50 @@
/*******************************************************************************
* 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;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderAdapter;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UltravoxDecoder extends ProtocolDecoderAdapter
{
private final static Logger mLog = LoggerFactory.getLogger(UltravoxDecoder.class);
@Override
public void decode(IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput out) throws Exception
{
byte[] message = new byte[ioBuffer.limit()];
ioBuffer.get(message);
UltravoxMessage ultravoxMessage = UltravoxMessageFactory.getMessage(message);
if(ultravoxMessage != null)
{
out.write(ultravoxMessage);
}
else
{
out.write(message);
}
}
}

View File

@@ -0,0 +1,41 @@
/*******************************************************************************
* 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;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
public class UltravoxEncoder extends ProtocolEncoderAdapter
{
@Override
public void encode(IoSession ioSession, Object message, ProtocolEncoderOutput out) throws Exception
{
if(message instanceof UltravoxMessage)
{
byte[] data = ((UltravoxMessage)message).getMessage();
IoBuffer buf = IoBuffer.allocate(data.length).setAutoExpand(false);
buf.put(data);
buf.flip();
out.write(buf);
}
}
}

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
import bits.BinaryMessage;
@@ -24,7 +24,7 @@ import java.util.Arrays;
public abstract class UltravoxMessage
{
public static final String SHOUTCAST_VERSION = "2.1";
public static final String ULTRAVOX_VERSION = "2.1";
public static final String VALID_RESPONSE_PREFIX = "ACK";
public static final String VALID_RESPONSE_PAYLOAD_PREFIX = "ACK:";
public static final String ERROR_RESPONSE_PREFIX = "NAK:";
@@ -40,7 +40,7 @@ public abstract class UltravoxMessage
private BinaryMessage mMessage;
/**
* Constructs a message from the byte array
* Constructs a ultravox from the byte array
*/
public UltravoxMessage(byte[] data)
{
@@ -56,9 +56,9 @@ public abstract class UltravoxMessage
}
/**
* Constructs an empty message of the specified length
* Constructs an empty ultravox of the specified length
*
* @param ultravoxMessageType type of message
* @param ultravoxMessageType type of ultravox
*/
public UltravoxMessage(UltravoxMessageType ultravoxMessageType)
{
@@ -71,7 +71,7 @@ public abstract class UltravoxMessage
/**
* Message byte array
*
* @return message bytes
* @return ultravox bytes
*/
public byte[] getMessage()
{
@@ -80,11 +80,11 @@ public abstract class UltravoxMessage
public String toString()
{
return mMessage.toString();
return getMessageType().name();
}
/**
* Sets the message sync bits to the predefined sync pattern: 01011010 (0x5A)
* Sets the ultravox sync bits to the predefined sync pattern: 01011010 (0x5A)
*/
private void setSync()
{
@@ -92,7 +92,7 @@ public abstract class UltravoxMessage
}
/**
* Ultravox message class
* Ultravox ultravox class
*/
public UltravoxMessageClass getMessageClass()
{
@@ -100,7 +100,7 @@ public abstract class UltravoxMessage
}
/**
* Ultravox message type
* Ultravox ultravox type
* @return
*/
public UltravoxMessageType getMessageType()
@@ -109,8 +109,8 @@ public abstract class UltravoxMessage
}
/**
* Sets the message bits according to the message class and type
* @param messageType of ultravox message
* Sets the ultravox bits according to the ultravox class and type
* @param messageType of ultravox ultravox
*/
private void setMessageType(UltravoxMessageType messageType)
{
@@ -118,7 +118,7 @@ public abstract class UltravoxMessage
}
/**
* Indicates if this message has the required delivery flag set
* Indicates if this ultravox has the required delivery flag set
*/
public boolean isRequiredDelivery()
{
@@ -141,7 +141,7 @@ public abstract class UltravoxMessage
}
/**
* Send queue priority (0 - 7) for this message
* Send queue priority (0 - 7) for this ultravox
*/
public int getSendQueuePriority()
{
@@ -149,7 +149,7 @@ public abstract class UltravoxMessage
}
/**
* Sets send queue priority for this message
* Sets send queue priority for this ultravox
* @param priority 0 - 7
*/
public void setSendQueuePriority(int priority)
@@ -186,7 +186,7 @@ public abstract class UltravoxMessage
}
/**
* Sets the message payload, updates the message length field, and appends a trailing 0x00 byte after the payload.
* Sets the ultravox payload, updates the ultravox length field, and appends a trailing 0x00 byte after the payload.
*/
public void setPayload(String payload)
{
@@ -211,6 +211,9 @@ public abstract class UltravoxMessage
mMessage.setByte(payloadPointer, (byte)0x00);
}
/**
* Indicates if the response is a valid (ie non-error) response
*/
public boolean isValidResponse()
{
String payload = getPayload();
@@ -218,6 +221,9 @@ public abstract class UltravoxMessage
return payload != null && payload.startsWith(VALID_RESPONSE_PREFIX);
}
/**
* Indicates if the response message contains an error message.
*/
public boolean isErrorResponse()
{
String payload = getPayload();
@@ -225,7 +231,10 @@ public abstract class UltravoxMessage
return payload != null && payload.startsWith(ERROR_RESPONSE_PREFIX);
}
public String getError()
/**
* Textual message available when the error response flag is set.
*/
public String getErrorMessage()
{
String payload = getPayload();

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public enum UltravoxMessageClass
{

View File

@@ -16,12 +16,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public class UltravoxMessageFactory
{
/**
* Creates an empty message of the specified message type for sending to the server
* Creates an empty ultravox of the specified ultravox type for sending to the server
*/
public static UltravoxMessage getMessage(UltravoxMessageType type)
{
@@ -29,6 +29,8 @@ public class UltravoxMessageFactory
{
case AUTHENTICATE_BROADCAST:
return new AuthenticateBroadcast();
case CACHEABLE_XML_METADATA:
return new CacheableXMLMetadata();
case CONFIGURE_ICY_GENRE:
return new ConfigureIcyGenre();
case CONFIGURE_ICY_NAME:
@@ -45,6 +47,8 @@ public class UltravoxMessageFactory
return new NegotiateBufferSize();
case NEGOTIATE_MAX_PAYLOAD_SIZE:
return new NegotiateMaxPayloadSize();
case PASS_THROUGH_XML_METADATA:
return new PassThroughXMLMetadata();
case REQUEST_CIPHER:
return new RequestCipher();
case SETUP_BROADCAST:
@@ -55,19 +59,17 @@ public class UltravoxMessageFactory
return new StreamMimeType();
case TERMINATE_BROADCAST:
return new TerminateBroadcast();
case XML_METADATA:
return new XMLMetadata();
default:
return null;
}
}
/**
* Creates an ultravox message from the server response bytes.
* Creates an ultravox ultravox from the server response bytes.
*
* @param bytes in network order (big-endian).
*
* @return constructed message or null if the message type cannot be determined or is unrecognized
* @return constructed ultravox or null if the ultravox type cannot be determined or is unrecognized
*/
public static UltravoxMessage getMessage(byte[] bytes)
{
@@ -77,6 +79,8 @@ public class UltravoxMessageFactory
{
case AUTHENTICATE_BROADCAST:
return new AuthenticateBroadcast(bytes);
case CACHEABLE_XML_METADATA:
return new CacheableXMLMetadata(bytes);
case CONFIGURE_ICY_GENRE:
return new ConfigureIcyGenre(bytes);
case CONFIGURE_ICY_NAME:
@@ -93,6 +97,8 @@ public class UltravoxMessageFactory
return new NegotiateBufferSize(bytes);
case NEGOTIATE_MAX_PAYLOAD_SIZE:
return new NegotiateMaxPayloadSize(bytes);
case PASS_THROUGH_XML_METADATA:
return new PassThroughXMLMetadata(bytes);
case REQUEST_CIPHER:
return new RequestCipher(bytes);
case SETUP_BROADCAST:
@@ -103,8 +109,6 @@ public class UltravoxMessageFactory
return new StreamMimeType(bytes);
case TERMINATE_BROADCAST:
return new TerminateBroadcast(bytes);
case XML_METADATA:
return new XMLMetadata(bytes);
case UNKNOWN:
default:
return null;
@@ -112,10 +116,10 @@ public class UltravoxMessageFactory
}
/**
* Identifies the message type from the server response byte array
* Identifies the ultravox type from the server response byte array
*
* @param data in little-endian format
* @return message type or UNKNOWN if the message type cannot be determined.
* @return ultravox type or UNKNOWN if the ultravox type cannot be determined.
*/
private static UltravoxMessageType getMessageType(byte[] data)
{

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public enum UltravoxMessageType
{
@@ -33,7 +33,8 @@ public enum UltravoxMessageType
CONFIGURE_ICY_GENRE(0x1101),
CONFIGURE_ICY_URL(0x1102),
CONFIGURE_ICY_PUBLIC(0x1103),
XML_METADATA(0x3902),
CACHEABLE_XML_METADATA(0x3902),
PASS_THROUGH_XML_METADATA(0x5902),
MP3_DATA(0x7000),
UNKNOWN(0x0000);
@@ -80,7 +81,9 @@ public enum UltravoxMessageType
case 0x1103:
return CONFIGURE_ICY_PUBLIC;
case 0x3902:
return XML_METADATA;
return CACHEABLE_XML_METADATA;
case 0x5902:
return PASS_THROUGH_XML_METADATA;
case 0x7000:
return MP3_DATA;
default:

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast.shoutcast.v2.message;
package audio.broadcast.shoutcast.v2.ultravox;
public enum UltravoxMetadata
{

View File

@@ -0,0 +1,52 @@
/*******************************************************************************
* 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;
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;
public class UltravoxProtocolFactory implements ProtocolCodecFactory
{
private ProtocolEncoder mEncoder;
private ProtocolDecoder mDecoder;
@Override
public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception
{
if(mEncoder == null)
{
mEncoder = new UltravoxEncoder();
}
return mEncoder;
}
@Override
public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception
{
if(mDecoder == null)
{
mDecoder = new UltravoxDecoder();
}
return mDecoder;
}
}