diff --git a/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml b/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml
index 7ea6f207..89a40528 100644
--- a/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml
+++ b/.idea/libraries/org_asynchttpclient_async_http_client_2_0_18.xml
@@ -26,6 +26,7 @@
+
diff --git a/imports/mina-core-2.0.16.jar b/imports/mina-core-2.0.16.jar
new file mode 100644
index 00000000..0e6d9883
Binary files /dev/null and b/imports/mina-core-2.0.16.jar differ
diff --git a/imports/mina-http-2.0.16.jar b/imports/mina-http-2.0.16.jar
new file mode 100644
index 00000000..62a5efcc
Binary files /dev/null and b/imports/mina-http-2.0.16.jar differ
diff --git a/src/audio/broadcast/AudioBroadcaster.java b/src/audio/broadcast/AudioBroadcaster.java
index 58904a33..e4d4e9e9 100644
--- a/src/audio/broadcast/AudioBroadcaster.java
+++ b/src/audio/broadcast/AudioBroadcaster.java
@@ -151,6 +151,22 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
}
}
+ /**
+ * 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
*/
@@ -246,7 +262,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{
if(mBroadcastState != state)
{
- mLog.debug("Changing State to: " + state);
+ mLog.debug("[" + getStreamName() + "] changing state to: " + state);
mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
diff --git a/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
index 1905c48c..fa9e9e92 100644
--- a/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
+++ b/src/audio/broadcast/icecast/IcecastTCPAudioBroadcaster.java
@@ -19,7 +19,6 @@
package audio.broadcast.icecast;
import audio.broadcast.AudioBroadcaster;
-import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
@@ -27,35 +26,32 @@ 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.future.IoFuture;
-import org.apache.mina.core.future.IoFutureListener;
-import org.apache.mina.core.service.IoHandler;
import org.apache.mina.core.service.IoHandlerAdapter;
-import org.apache.mina.core.session.IdleStatus;
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;
+import org.apache.mina.http.api.HttpRequest;
+import org.apache.mina.http.api.HttpVersion;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
-import org.asynchttpclient.BoundRequestBuilder;
import org.asynchttpclient.DefaultAsyncHttpClient;
-import org.asynchttpclient.Realm;
-import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
import java.io.IOException;
+import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.SocketAddress;
-import java.net.SocketException;
import java.net.URLEncoder;
-import java.net.UnknownHostException;
import java.nio.charset.Charset;
+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;
@@ -65,10 +61,11 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
private final static String UTF8 = "UTF-8";
private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":";
- private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
+ private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
- NioSocketConnector mSocketConnector;
- IoSession mSession = null;
+ private NioSocketConnector mSocketConnector;
+ private IoSession mSession = null;
+ private IcecastMetadataUpdater mIcecastMetadataUpdater;
private long mLastConnectionAttempt = 0;
private byte[] mSilenceFrame;
@@ -103,32 +100,15 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
@Override
protected void broadcastAudio(byte[] audio)
{
- boolean completed = false;
-
- while(!completed)
+ if(connect())
{
- if(connect())
+ try
{
- try
- {
- send(audio);
- completed = true;
- }
- catch(SocketException se)
- {
- //The remote server likely disconnected - setup to reconnect
- mLog.error("Resetting Icecast TCP Audio Broadcaster - socket error: " + se.getMessage());
- disconnect();
- }
- catch(Exception e)
- {
- completed = true;
- mLog.error("Error sending audio", e);
- }
+ send(audio);
}
- else
+ catch(IOException ioe)
{
- completed = true;
+ mLog.error("Error sending audio", ioe);
}
}
}
@@ -139,74 +119,62 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
- if(mSession != null && mSession.isConnected())
+ StringBuilder sb = new StringBuilder();
+
+ if(metadata != null)
{
- StringBuilder sb = new StringBuilder();
+ Metadata to = metadata.getMetadata(MetadataType.TO);
- if(metadata != null)
+ sb.append("TO:");
+
+ if(to != null)
{
- Metadata to = metadata.getMetadata(MetadataType.TO);
-
- sb.append("TO:");
-
- if(to != null)
+ if(to.hasAlias())
{
- if(to.hasAlias())
- {
- sb.append(to.getAlias().getName());
- }
- else
- {
- sb.append(to.getValue());
- }
+ sb.append(to.getAlias().getName());
}
else
{
- sb.append("UNKNOWN");
- }
-
- Metadata from = metadata.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");
+ sb.append(to.getValue());
}
}
else
{
- sb.append("Scanning ....");
+ sb.append("UNKNOWN");
}
- try
- {
- String songEncoded = URLEncoder.encode(sb.toString(), UTF8);
- StringBuilder query = new StringBuilder();
- query.append("GET /admin/metadata?mode=updinfo");
- query.append("&mount=").append(getConfiguration().getMountPoint());
- query.append("&charset=UTF%2d8");
- query.append("&song=").append(songEncoded);
+ Metadata from = metadata.getMetadata(MetadataType.FROM);
- send(query.toString());
- }
- catch(Exception e)
+ sb.append(" FROM:");
+
+ if(from != null)
{
- mLog.debug("Error while updating metadata", e);
+
+ if(from.hasAlias())
+ {
+ sb.append(from.getAlias().getName());
+ }
+ else
+ {
+ sb.append(from.getValue());
+ }
+ }
+ else
+ {
+ sb.append("UNKNOWN");
}
}
+ else
+ {
+ sb.append("Scanning ....");
+ }
+
+ if(mIcecastMetadataUpdater == null)
+ {
+ mIcecastMetadataUpdater = new IcecastMetadataUpdater();
+ }
+
+ mIcecastMetadataUpdater.update(sb.toString());
}
/**
@@ -219,8 +187,12 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
*/
private boolean connect()
{
- if(!connected() && canConnect() && mConnecting.compareAndSet(false, true))
+ if(!connected() && canConnect() &&
+ (mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
+ mConnecting.compareAndSet(false, true))
{
+ mLastConnectionAttempt = System.currentTimeMillis();
+
if(mSocketConnector == null)
{
mLog.debug("NIO socket connector - creating");
@@ -241,7 +213,8 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
@Override
public void run()
{
- mLog.debug("Runnable - connecting to server");
+ mLog.debug("Attempting connection to Icecast TCP Server");
+
setBroadcastState(BroadcastState.CONNECTING);
try
@@ -249,11 +222,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
-
- mLog.debug("Runnable - blocking for connection");
future.awaitUninterruptibly();
-
- mLog.debug("Runnable - assigning session");
mSession = future.getSession();
}
catch(RuntimeIoException rie)
@@ -267,26 +236,15 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.BROADCAST_ERROR);
- mLog.debug("Cause Class:" + throwableCause.getClass());
+ mLog.debug("Failed to connect", rie);
+ }
+ else
+ {
+ mLog.debug("Failed to connect - no exception is available");
}
- mLog.debug("Failed to connect", rie);
+ mConnecting.set(false);
}
-
- if(mSession != null)
- {
- mLog.debug("We have a session -- blocking thread awaiting the closing future");
- mSession.getCloseFuture().awaitUninterruptibly();
- }
-
- mLog.debug("Disposing the connector");
- mSocketConnector.dispose();
- mSession = null;
- mSocketConnector = null;
-
- mConnecting.set(false);
-
- mLog.debug("Session is closed & finished");
}
};
@@ -311,11 +269,6 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
{
mSession.closeNow();
}
-
- if(!isErrorState())
- {
- setBroadcastState(BroadcastState.READY);
- }
}
}
@@ -395,21 +348,12 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
*/
public class IcecastTCPIOHandler extends IoHandlerAdapter
{
- @Override
- public void sessionCreated(IoSession session) throws Exception
- {
- mLog.debug("Session Created");
- super.sessionCreated(session);
- }
-
/**
* Sends stream configuration and user credentials upon connecting to remote server
*/
@Override
public void sessionOpened(IoSession session) throws Exception
{
- mLog.debug("Session Opened");
-
StringBuilder sb = new StringBuilder();
sb.append("SOURCE ").append(getConfiguration().getMountPoint());
sb.append(" HTTP/1.0").append(TERMINATOR);
@@ -452,14 +396,13 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLog.debug("Session Closed - State = Disconnected");
- super.sessionClosed(session);
- }
- @Override
- public void sessionIdle(IoSession session, IdleStatus status) throws Exception
- {
- mLog.debug("Session Idle");
- super.sessionIdle(session, status);
+ mSocketConnector.dispose();
+ mSession = null;
+ mSocketConnector = null;
+
+ mConnecting.set(false);
+ super.sessionClosed(session);
}
@Override
@@ -509,13 +452,6 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
}
- @Override
- public void messageSent(IoSession session, Object message) throws Exception
- {
- mLog.debug("Message Sent");
- super.messageSent(session, message);
- }
-
@Override
public void inputClosed(IoSession session) throws Exception
{
@@ -524,155 +460,161 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
}
- public static void main(String[] args)
+ public class IcecastMetadataUpdater
{
- NioSocketConnector connector = new NioSocketConnector();
- connector.setConnectTimeoutCheckInterval(10000);
- connector.getFilterChain().addLast("logger", new LoggingFilter(IcecastTCPAudioBroadcaster.class));
+ private NioSocketConnector mSocketConnector;
+ private AtomicBoolean mUpdating = new AtomicBoolean();
+ private Queue mMetadataQueue = new LinkedTransferQueue<>();
+ private Map mHTTPHeaders;
+ private boolean mStackTraceLoggingSuppressed;
- connector.getFilterChain().addLast("codec",
- new ProtocolCodecFilter(new TextLineCodecFactory((Charset.forName("UTF-8")))));
-
- connector.setHandler(new IoHandler()
+ /**
+ * 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 IcecastMetadataUpdater()
{
- @Override
- public void sessionCreated(IoSession ioSession) throws Exception
+ }
+
+ /**
+ * Initializes the network socket and HTTP headers map
+ */
+ private void init()
+ {
+ if(mSocketConnector == null)
{
+ mSocketConnector = new NioSocketConnector();
+// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter());
+ mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
- }
-
- @Override
- public void sessionOpened(IoSession ioSession) throws Exception
- {
- mLog.debug("Session Opened");
-
- IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3);
- config.setHost("localhost");
- config.setPort(8000);
- config.setMountPoint("/stream");
- config.setUserName("source");
- config.setPassword("denny");
- config.setPublic(true);
-
- StringBuilder sb = new StringBuilder();
- sb.append("SOURCE ").append(config.getMountPoint());
- sb.append(" HTTP/1.0").append(TERMINATOR);
-
- sb.append("Authorization: ").append(config.getEncodedCredentials()).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(config.getBroadcastFormat().getValue()).append(TERMINATOR);
- sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
- .append(config.isPublic() ? "1" : "0").append(TERMINATOR);
-
- if(config.hasGenre())
+ //Each metadata update session is single-use, so we'll shut it down upon success or failure
+ mSocketConnector.setHandler(new IoHandlerAdapter()
{
- sb.append(IcecastHeader.GENRE.getValue()).append(SEPARATOR)
- .append(config.getGenre()).append(TERMINATOR);
- }
+ @Override
+ public void exceptionCaught(IoSession session, Throwable cause) throws Exception
+ {
+// mLog.debug("Session " + session.getId() + " metadata update complete - ERROR received");
+ session.closeNow();
+ }
- if(config.hasDescription())
+ @Override
+ public void messageReceived(IoSession session, Object message) throws Exception
+ {
+// mLog.debug("Session " + session.getId() + " metadata update complete - response received");
+ session.closeNow();
+ }
+ });
+
+ mHTTPHeaders = new HashMap<>();
+ mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
+ mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getEncodedCredentials());
+ }
+ }
+
+ /**
+ * 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(String metadata)
+ {
+ if(metadata != null)
+ {
+ mMetadataQueue.offer(metadata);
+
+ if(mUpdating.compareAndSet(false, true))
{
- sb.append(IcecastHeader.DESCRIPTION.getValue()).append(SEPARATOR)
- .append(config.getDescription()).append(TERMINATOR);
- }
+ String metadataUpdate = mMetadataQueue.poll();
-// sb.append(getAudioInfoMetadata());
+ while(metadataUpdate != null)
+ {
+ //Ensure we're setup for network communications (only happens once)
+ init();
- sb.append(TERMINATOR).append(TERMINATOR);
+ HttpRequest updateRequest = createUpdateRequest(metadataUpdate);
- mLog.debug("Sending connection string");
- ioSession.write(sb.toString());
- }
+ if(updateRequest != null)
+ {
+ getThreadPoolManager().scheduleOnce(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ try
+ {
+ ConnectFuture connectFuture = mSocketConnector
+ .connect(new InetSocketAddress(getConfiguration().getHost(),
+ getConfiguration().getPort()));
+ connectFuture.awaitUninterruptibly();
+ IoSession session = connectFuture.getSession();
- @Override
- public void sessionClosed(IoSession ioSession) throws Exception
- {
+ 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);
- mLog.debug("Session Closed");
- }
+ mStackTraceLoggingSuppressed = true;
+ }
+ }
+ }
+ }
+ }, 0l, TimeUnit.SECONDS);
+ }
- @Override
- public void sessionIdle(IoSession ioSession, IdleStatus idleStatus) throws Exception
- {
- mLog.debug("Session Idle");
- }
+ //Fetch next metadata update to send
+ metadataUpdate = mMetadataQueue.poll();
+ }
- @Override
- public void exceptionCaught(IoSession ioSession, Throwable throwable) throws Exception
- {
- mLog.debug("Session Error", throwable);
- }
-
- @Override
- public void messageReceived(IoSession ioSession, Object o) throws Exception
- {
- if(o instanceof String)
- {
- mLog.debug("Message Received: " + o.toString());
- }
- else
- {
- mLog.debug("Message Received: " + o.getClass());
+ mUpdating.set(false);
}
}
+ }
- @Override
- public void messageSent(IoSession ioSession, Object o) throws Exception
- {
- mLog.debug("Message Sent");
- }
-
- @Override
- public void inputClosed(IoSession ioSession) throws Exception
- {
- //This is invoked when the remote server disconnects us for whatever reason
- mLog.debug("Input Closed");
-
- ioSession.closeNow();
- }
- });
-
- IoSession session = null;
-
- for(;;)
+ /**
+ * Creates an HTTP GET request to update the metadata on the remote server
+ */
+ private HttpRequest createUpdateRequest(String metadata)
{
try
{
- ConnectFuture future = connector.connect(new InetSocketAddress("localhost", 8000));
- future.awaitUninterruptibly();
- session = future.getSession();
- break;
+ String songEncoded = URLEncoder.encode(metadata, UTF8);
+ StringBuilder sb = new StringBuilder();
+ sb.append("mode=updinfo");
+ sb.append("&mount=").append(getConfiguration().getMountPoint());
+ sb.append("&charset=UTF%2d8");
+ sb.append("&song=").append(songEncoded);
+
+ return new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
+ sb.toString(), mHTTPHeaders);
}
- catch(RuntimeIoException rie)
+ catch(UnsupportedEncodingException e)
{
- Throwable throwableCause = rie.getCause();
-
- if(throwableCause instanceof ConnectException)
- {
- //Indicate that the remote server is not available setState(NoServer)
- }
- else if(throwableCause != null)
- {
- mLog.debug("Cause Class:" + throwableCause.getClass());
- }
-
- mLog.debug("Failed to connect", rie);
- break;
+ //This should never happen
+ mLog.error("UTF-8 encoding is not supported - can't update song metadata");
}
- }
- if(session != null)
- {
- mLog.debug("Closing the session ... blocking on the closing future");
- session.getCloseFuture().awaitUninterruptibly();
+ return null;
}
-
- mLog.debug("Disposing the connector");
- connector.dispose();
- //sessionClosed() is now invoked
- mLog.debug("Finished");
}
}
diff --git a/src/controller/ThreadPoolManager.java b/src/controller/ThreadPoolManager.java
index 89e7efa6..d96cf9c6 100644
--- a/src/controller/ThreadPoolManager.java
+++ b/src/controller/ThreadPoolManager.java
@@ -59,14 +59,10 @@ public class ThreadPoolManager
TimeUnit unit )
throws RejectedExecutionException
{
- if( mExecutor == null )
- {
- mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
- new NamingThreadFactory( "sdrtrunk" ) );
- }
+ ScheduledExecutorService executorService = getScheduledExecutorService();
ScheduledFuture> task =
- mExecutor.scheduleAtFixedRate( command, 0, period, unit );
+ executorService.scheduleAtFixedRate( command, 0, period, unit );
mTasks.put( task, type );
@@ -76,13 +72,9 @@ public class ThreadPoolManager
public void scheduleOnce( Runnable command, long delay, TimeUnit unit )
throws RejectedExecutionException
{
- if( mExecutor == null )
- {
- mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
- new NamingThreadFactory( "sdrtrunk" ) );
- }
+ ScheduledExecutorService executorService = getScheduledExecutorService();
- mExecutor.schedule( command, delay, unit );
+ executorService.schedule( command, delay, unit );
}
public boolean cancel( ScheduledFuture> task )