#30 WIP on using the new apache mina library

This commit is contained in:
Denny
2016-12-18 09:09:47 -05:00
parent d085ecc0c9
commit 1d7354b4a8
6 changed files with 228 additions and 277 deletions

View File

@@ -26,6 +26,7 @@
<root url="jar://$PROJECT_DIR$/imports/async-http-client-2.0.24.jar!/" /> <root url="jar://$PROJECT_DIR$/imports/async-http-client-2.0.24.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/async-http-client-netty-utils-2.0.24.jar!/" /> <root url="jar://$PROJECT_DIR$/imports/async-http-client-netty-utils-2.0.24.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/mina-core-2.0.16.jar!/" /> <root url="jar://$PROJECT_DIR$/imports/mina-core-2.0.16.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/mina-http-2.0.16.jar!/" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES /> <SOURCES />

Binary file not shown.

Binary file not shown.

View File

@@ -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 * Size of recording queue for recordings awaiting streaming
*/ */
@@ -246,7 +262,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{ {
if(mBroadcastState != state) if(mBroadcastState != state)
{ {
mLog.debug("Changing State to: " + state); mLog.debug("[" + getStreamName() + "] changing state to: " + state);
mBroadcastState = state; mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE)); broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));

View File

@@ -19,7 +19,6 @@
package audio.broadcast.icecast; package audio.broadcast.icecast;
import audio.broadcast.AudioBroadcaster; import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState; import audio.broadcast.BroadcastState;
import audio.metadata.AudioMetadata; import audio.metadata.AudioMetadata;
import audio.metadata.Metadata; import audio.metadata.Metadata;
@@ -27,35 +26,32 @@ import audio.metadata.MetadataType;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import org.apache.mina.core.RuntimeIoException; import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture; 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.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession; import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter; import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory; import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter; 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.apache.mina.transport.socket.nio.NioSocketConnector;
import org.asynchttpclient.BoundRequestBuilder;
import org.asynchttpclient.DefaultAsyncHttpClient; import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.Realm;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import properties.SystemProperties; import properties.SystemProperties;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.Charset; 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.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; 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 UTF8 = "UTF-8";
private final static String TERMINATOR = "\r\n"; private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":"; 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; private NioSocketConnector mSocketConnector;
IoSession mSession = null; private IoSession mSession = null;
private IcecastMetadataUpdater mIcecastMetadataUpdater;
private long mLastConnectionAttempt = 0; private long mLastConnectionAttempt = 0;
private byte[] mSilenceFrame; private byte[] mSilenceFrame;
@@ -103,32 +100,15 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
@Override @Override
protected void broadcastAudio(byte[] audio) protected void broadcastAudio(byte[] audio)
{ {
boolean completed = false; if(connect())
while(!completed)
{ {
if(connect()) try
{ {
try send(audio);
{
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);
}
} }
else catch(IOException ioe)
{ {
completed = true; mLog.error("Error sending audio", ioe);
} }
} }
} }
@@ -139,74 +119,62 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
@Override @Override
protected void broadcastMetadata(AudioMetadata metadata) 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); if(to.hasAlias())
sb.append("TO:");
if(to != null)
{ {
if(to.hasAlias()) sb.append(to.getAlias().getName());
{
sb.append(to.getAlias().getName());
}
else
{
sb.append(to.getValue());
}
} }
else else
{ {
sb.append("UNKNOWN"); sb.append(to.getValue());
}
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");
} }
} }
else else
{ {
sb.append("Scanning ...."); sb.append("UNKNOWN");
} }
try Metadata from = metadata.getMetadata(MetadataType.FROM);
{
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);
send(query.toString()); sb.append(" FROM:");
}
catch(Exception e) 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() 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) if(mSocketConnector == null)
{ {
mLog.debug("NIO socket connector - creating"); mLog.debug("NIO socket connector - creating");
@@ -241,7 +213,8 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
@Override @Override
public void run() public void run()
{ {
mLog.debug("Runnable - connecting to server"); mLog.debug("Attempting connection to Icecast TCP Server");
setBroadcastState(BroadcastState.CONNECTING); setBroadcastState(BroadcastState.CONNECTING);
try try
@@ -249,11 +222,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
ConnectFuture future = mSocketConnector ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(), .connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort())); getBroadcastConfiguration().getPort()));
mLog.debug("Runnable - blocking for connection");
future.awaitUninterruptibly(); future.awaitUninterruptibly();
mLog.debug("Runnable - assigning session");
mSession = future.getSession(); mSession = future.getSession();
} }
catch(RuntimeIoException rie) catch(RuntimeIoException rie)
@@ -267,26 +236,15 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
else if(throwableCause != null) else if(throwableCause != null)
{ {
setBroadcastState(BroadcastState.BROADCAST_ERROR); 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(); mSession.closeNow();
} }
if(!isErrorState())
{
setBroadcastState(BroadcastState.READY);
}
} }
} }
@@ -395,21 +348,12 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
*/ */
public class IcecastTCPIOHandler extends IoHandlerAdapter 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 * Sends stream configuration and user credentials upon connecting to remote server
*/ */
@Override @Override
public void sessionOpened(IoSession session) throws Exception public void sessionOpened(IoSession session) throws Exception
{ {
mLog.debug("Session Opened");
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("SOURCE ").append(getConfiguration().getMountPoint()); sb.append("SOURCE ").append(getConfiguration().getMountPoint());
sb.append(" HTTP/1.0").append(TERMINATOR); sb.append(" HTTP/1.0").append(TERMINATOR);
@@ -452,14 +396,13 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
{ {
setBroadcastState(BroadcastState.DISCONNECTED); setBroadcastState(BroadcastState.DISCONNECTED);
mLog.debug("Session Closed - State = Disconnected"); mLog.debug("Session Closed - State = Disconnected");
super.sessionClosed(session);
}
@Override mSocketConnector.dispose();
public void sessionIdle(IoSession session, IdleStatus status) throws Exception mSession = null;
{ mSocketConnector = null;
mLog.debug("Session Idle");
super.sessionIdle(session, status); mConnecting.set(false);
super.sessionClosed(session);
} }
@Override @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 @Override
public void inputClosed(IoSession session) throws Exception 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(); private NioSocketConnector mSocketConnector;
connector.setConnectTimeoutCheckInterval(10000); private AtomicBoolean mUpdating = new AtomicBoolean();
connector.getFilterChain().addLast("logger", new LoggingFilter(IcecastTCPAudioBroadcaster.class)); private Queue<String> mMetadataQueue = new LinkedTransferQueue<>();
private Map<String,String> mHTTPHeaders;
private boolean mStackTraceLoggingSuppressed;
connector.getFilterChain().addLast("codec", /**
new ProtocolCodecFilter(new TextLineCodecFactory((Charset.forName("UTF-8"))))); * 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
connector.setHandler(new IoHandler() * 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());
} //Each metadata update session is single-use, so we'll shut it down upon success or failure
mSocketConnector.setHandler(new IoHandlerAdapter()
@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())
{ {
sb.append(IcecastHeader.GENRE.getValue()).append(SEPARATOR) @Override
.append(config.getGenre()).append(TERMINATOR); 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) String metadataUpdate = mMetadataQueue.poll();
.append(config.getDescription()).append(TERMINATOR);
}
// 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"); if(updateRequest != null)
ioSession.write(sb.toString()); {
} 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 if(session != null)
public void sessionClosed(IoSession ioSession) throws Exception {
{ 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 //Fetch next metadata update to send
public void sessionIdle(IoSession ioSession, IdleStatus idleStatus) throws Exception metadataUpdate = mMetadataQueue.poll();
{ }
mLog.debug("Session Idle");
}
@Override mUpdating.set(false);
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());
} }
} }
}
@Override /**
public void messageSent(IoSession ioSession, Object o) throws Exception * Creates an HTTP GET request to update the metadata on the remote server
{ */
mLog.debug("Message Sent"); private HttpRequest createUpdateRequest(String metadata)
}
@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(;;)
{ {
try try
{ {
ConnectFuture future = connector.connect(new InetSocketAddress("localhost", 8000)); String songEncoded = URLEncoder.encode(metadata, UTF8);
future.awaitUninterruptibly(); StringBuilder sb = new StringBuilder();
session = future.getSession(); sb.append("mode=updinfo");
break; 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(); //This should never happen
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
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;
} }
}
if(session != null) return null;
{
mLog.debug("Closing the session ... blocking on the closing future");
session.getCloseFuture().awaitUninterruptibly();
} }
mLog.debug("Disposing the connector");
connector.dispose();
//sessionClosed() is now invoked
mLog.debug("Finished");
} }
} }

View File

@@ -59,14 +59,10 @@ public class ThreadPoolManager
TimeUnit unit ) TimeUnit unit )
throws RejectedExecutionException throws RejectedExecutionException
{ {
if( mExecutor == null ) ScheduledExecutorService executorService = getScheduledExecutorService();
{
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
}
ScheduledFuture<?> task = ScheduledFuture<?> task =
mExecutor.scheduleAtFixedRate( command, 0, period, unit ); executorService.scheduleAtFixedRate( command, 0, period, unit );
mTasks.put( task, type ); mTasks.put( task, type );
@@ -76,13 +72,9 @@ public class ThreadPoolManager
public void scheduleOnce( Runnable command, long delay, TimeUnit unit ) public void scheduleOnce( Runnable command, long delay, TimeUnit unit )
throws RejectedExecutionException throws RejectedExecutionException
{ {
if( mExecutor == null ) ScheduledExecutorService executorService = getScheduledExecutorService();
{
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
}
mExecutor.schedule( command, delay, unit ); executorService.schedule( command, delay, unit );
} }
public boolean cancel( ScheduledFuture<?> task ) public boolean cancel( ScheduledFuture<?> task )