diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml
index d0051597..b254f1ea 100644
--- a/.idea/dictionaries/denny.xml
+++ b/.idea/dictionaries/denny.xml
@@ -4,6 +4,7 @@
bahroo
esnid
fleetsync
+ icecast
ident
polyphase
shoutcast
diff --git a/.idea/libraries/imports.xml b/.idea/libraries/imports.xml
index 7e05f72d..cacca7ef 100644
--- a/.idea/libraries/imports.xml
+++ b/.idea/libraries/imports.xml
@@ -25,8 +25,45 @@
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/imports/commons-codec-1.9.jar b/imports/commons-codec-1.9.jar
new file mode 100644
index 00000000..ef35f1c5
Binary files /dev/null and b/imports/commons-codec-1.9.jar differ
diff --git a/imports/commons-logging-1.2.jar b/imports/commons-logging-1.2.jar
new file mode 100644
index 00000000..93a3b9f6
Binary files /dev/null and b/imports/commons-logging-1.2.jar differ
diff --git a/imports/httpclient-4.5.2.jar b/imports/httpclient-4.5.2.jar
new file mode 100644
index 00000000..701609fc
Binary files /dev/null and b/imports/httpclient-4.5.2.jar differ
diff --git a/imports/httpcore-4.4.5.jar b/imports/httpcore-4.4.5.jar
new file mode 100644
index 00000000..6bbf0ac8
Binary files /dev/null and b/imports/httpcore-4.4.5.jar differ
diff --git a/src/audio/broadcast/BroadcastState.java b/src/audio/broadcast/BroadcastState.java
index 7c04ae6e..47a6bd82 100644
--- a/src/audio/broadcast/BroadcastState.java
+++ b/src/audio/broadcast/BroadcastState.java
@@ -45,6 +45,16 @@ public enum BroadcastState
*/
INVALID_PASSWORD("Invalid Password", true),
+ /**
+ * Remote server max sources has been exceeded
+ */
+ MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
+
+ /**
+ * Specified mount point is already in use
+ */
+ MOUNT_POINT_IN_USE("Mount Point In Use", true),
+
/**
* Server is not known or reachable
*/
@@ -60,6 +70,11 @@ public enum BroadcastState
*/
READY("Ready", false),
+ /**
+ * Error on the remote server
+ */
+ REMOTE_SERVER_ERROR("Remote Server Error", true),
+
/**
* Server host name or port is invalid
*/
diff --git a/src/audio/broadcast/BroadcasterFactory.java b/src/audio/broadcast/BroadcasterFactory.java
index cc416e1a..45f4ab73 100644
--- a/src/audio/broadcast/BroadcasterFactory.java
+++ b/src/audio/broadcast/BroadcasterFactory.java
@@ -18,6 +18,8 @@
******************************************************************************/
package audio.broadcast;
+import audio.broadcast.icecast.IcecastConfiguration;
+import audio.broadcast.icecast.IcecastHandler;
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v1.ShoutcastV1Handler;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
@@ -29,7 +31,8 @@ import controller.ThreadPoolManager;
public class BroadcasterFactory
{
public static final int MP3_MONO_16_KHZ_BITRATE = 16;
- public static final boolean MP3_NO_VARIABLE_BITRATE = false;
+ public static final boolean MP3_CONSTANT_BITRATE = false;
+ public static final boolean MP3_VARIABLE_BITRATE = true;
/**
* Creates an audio streaming broadcaster for the configuration
@@ -49,7 +52,8 @@ public class BroadcasterFactory
case BROADCASTIFY:
break;
case ICECAST:
- break;
+ return new Broadcaster(threadPoolManager,
+ new IcecastHandler((IcecastConfiguration)configuration, converter));
case SHOUTCAST_V1:
return new Broadcaster(threadPoolManager,
new ShoutcastV1Handler((ShoutcastV1Configuration)configuration, converter));
@@ -74,7 +78,7 @@ public class BroadcasterFactory
switch(format)
{
case MP3:
- return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_NO_VARIABLE_BITRATE);
+ return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
}
return null;
diff --git a/src/audio/broadcast/HttpAsyncAudioStreamer.java b/src/audio/broadcast/HttpAsyncAudioStreamer.java
new file mode 100644
index 00000000..3c0fdcc6
--- /dev/null
+++ b/src/audio/broadcast/HttpAsyncAudioStreamer.java
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
+package audio.broadcast;
+
+import audio.AudioPacket;
+import audio.IAudioPacketListener;
+import audio.convert.IAudioConverter;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpVersion;
+import org.apache.http.entity.AbstractHttpEntity;
+import org.apache.http.entity.BasicHttpEntity;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.nio.ContentEncoder;
+import org.apache.http.nio.IOControl;
+import org.apache.http.nio.entity.HttpAsyncContentProducer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import sample.Listener;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class HttpAsyncAudioStreamer extends BasicHttpEntityEnclosingRequest
+ implements IAudioPacketListener, Listener, HttpAsyncContentProducer
+{
+ private final static Logger mLog = LoggerFactory.getLogger( HttpAsyncAudioStreamer.class );
+
+ private LinkedBlockingQueue mAudioQueue;
+ private IAudioConverter mIAudioConverter;
+ private IOControl mIOControl;
+ private AtomicBoolean mSuspended = new AtomicBoolean();
+
+ public HttpAsyncAudioStreamer(String method, String uri, IAudioConverter audioConverter, int queueSize)
+ {
+ super(method, uri, HttpVersion.HTTP_1_1);
+ mIAudioConverter = audioConverter;
+ mAudioQueue = new LinkedBlockingQueue<>(queueSize);
+
+ setEntity(new MyHttpEntity());
+ }
+
+ /**
+ * Queues the audio packet for conversion and streaming. Audio packets are assumed to arrive in sequence.
+ */
+ @Override
+ public void receive(AudioPacket audioPacket)
+ {
+ //Insert the packet in the queue. If the queue is currently full, remove the head element and attempt
+ //to reinsert the packet.
+ if(!mAudioQueue.offer(audioPacket))
+ {
+ mLog.debug("Streamer - making room to add an audio packet");
+ mAudioQueue.poll();
+ mAudioQueue.offer(audioPacket);
+ }
+
+ if(mIOControl != null && mSuspended.compareAndSet(true, false))
+ {
+ mLog.debug("Streamer - content available, resuming streaming");
+ mIOControl.requestOutput();
+ }
+ }
+
+ @Override
+ public Listener getAudioPacketListener()
+ {
+ return this;
+ }
+
+ @Override
+ public void produceContent(ContentEncoder contentEncoder, IOControl ioControl) throws IOException
+ {
+ //IOControl is thread safe, so we'll keep a reference to it so that we can do flow control
+ if(mIOControl == null)
+ {
+ mLog.debug("IOControl class is: " + ioControl.getClass());
+ mIOControl = ioControl;
+ }
+
+ AudioPacket packet = mAudioQueue.poll();
+
+ List packets = new ArrayList<>();
+ packets.add(packet);
+ byte[] converted = mIAudioConverter.convert(packets);
+ mLog.debug("Produce Content - writing converted audio: " + converted.length + " to:" + contentEncoder.getClass());
+ int wrote = contentEncoder.write(ByteBuffer.wrap(converted));
+
+ mLog.debug("We wrote [" + wrote + "] bytes to the content encoder");
+
+ if(mAudioQueue.isEmpty() && mSuspended.compareAndSet(false, true))
+ {
+ mLog.debug("Produce Content - suspending output");
+ mIOControl.suspendOutput();
+ }
+
+// List packets = new ArrayList<>();
+// mAudioQueue.drainTo(packets);
+//
+// if(packets.isEmpty() && mSuspended.compareAndSet(false, true))
+// {
+// mLog.debug("Produce Content - no audio - suspending output");
+// mIOControl.suspendOutput();
+// }
+// else
+// {
+// byte[] converted = mIAudioConverter.convert(packets);
+// mLog.debug("Produce Content - writing converted audio: " + converted.length);
+// contentEncoder.write(ByteBuffer.wrap(converted));
+// }
+ }
+
+ @Override
+ public boolean isRepeatable()
+ {
+ return false;
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ mLog.debug("We were closed!");
+ }
+
+ public class MyHttpEntity extends AbstractHttpEntity
+ {
+ public MyHttpEntity()
+ {
+ }
+
+ @Override
+ public boolean isRepeatable()
+ {
+ return false;
+ }
+
+ @Override
+ public long getContentLength()
+ {
+ return -1;
+ }
+
+ @Override
+ public InputStream getContent() throws IOException, UnsupportedOperationException
+ {
+ return null;
+ }
+
+ @Override
+ public void writeTo(OutputStream outputStream) throws IOException
+ {
+ }
+
+ @Override
+ public boolean isStreaming()
+ {
+ return true;
+ }
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastConfiguration.java b/src/audio/broadcast/icecast/IcecastConfiguration.java
new file mode 100644
index 00000000..46070dd0
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastConfiguration.java
@@ -0,0 +1,202 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
+package audio.broadcast.icecast;
+
+import audio.broadcast.BroadcastConfiguration;
+import audio.broadcast.BroadcastFormat;
+import audio.broadcast.BroadcastServerType;
+
+import java.util.Base64;
+
+public class IcecastConfiguration extends BroadcastConfiguration
+{
+ private String mUserName;
+ private String mMountPoint;
+ private String mStreamName;
+ private String mDescription;
+ private String mGenre;
+ private boolean mPublic;
+ private int mChannels;
+ private int mBitRate;
+ private String mURL;
+
+ public IcecastConfiguration(BroadcastFormat format)
+ {
+ super(format);
+ }
+
+ @Override
+ public BroadcastServerType getBroadcastServerType()
+ {
+ return BroadcastServerType.ICECAST;
+ }
+
+ /**
+ * Base64 encoded version of the username and password with prepended 'Basic ' tag.
+ */
+ public String getAuthorization()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(getUserName()).append(":").append(getPassword());
+
+ String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
+
+ return "Basic " + base64;
+ }
+
+ public String getUserName()
+ {
+ return mUserName;
+ }
+
+ public void setUserName(String userName)
+ {
+ mUserName = userName;
+ }
+
+ /**
+ * Mount point or path to the stream
+ * @return mount point
+ */
+ public String getMountPoint()
+ {
+ return mMountPoint;
+ }
+
+ /**
+ * Sets the mount point (path) for the stream
+ * @param mountPoint
+ */
+ public void setMountPoint(String mountPoint)
+ {
+ mMountPoint = mountPoint;
+ }
+
+ /**
+ * Stream name
+ */
+ public String getStreamName()
+ {
+ return mStreamName;
+ }
+
+ /**
+ * Sets the stream name
+ * @param name
+ */
+ public void setStreamName(String name)
+ {
+ mStreamName = name;
+ }
+
+
+ public String getDescription()
+ {
+ return mDescription;
+ }
+
+ public void setDescription(String description)
+ {
+ mDescription = description;
+ }
+
+ /**
+ * Stream genre
+ */
+ public String getGenre()
+ {
+ return mGenre;
+ }
+
+ /**
+ * Stream genre
+ * @param genre
+ */
+ public void setGenre(String genre)
+ {
+ mGenre = genre;
+ }
+
+ /**
+ * Public visibility of the broadcast
+ */
+ public boolean isPublic()
+ {
+ return mPublic;
+ }
+
+ /**
+ * Sets public visibility of the broadcast
+ * @param isPublic indicates if the broadcast should be visible to the public
+ */
+ public void setPublic(boolean isPublic)
+ {
+ mPublic = isPublic;
+ }
+
+ /**
+ * Number of audio channels in the broadcast
+ */
+ public int getChannels()
+ {
+ return mChannels;
+ }
+
+ /**
+ * Sets the number of audio channels in the broadcast
+ */
+ public void setChannels(int channels)
+ {
+ mChannels = channels;
+ }
+
+ /**
+ * Bit rate in bits per second
+ */
+ public int getBitRate()
+ {
+ return mBitRate;
+ }
+
+ /**
+ * Bit rate
+ * @param bitRate in samples per second
+ */
+ public void setBitRate(int bitRate)
+ {
+ mBitRate = bitRate;
+ }
+
+ /**
+ * URL associated with the broadcast where users can find additional details.
+ */
+ public String getURL()
+ {
+ return mURL;
+ }
+
+ /**
+ * URL associated with the broadcast where users can find additional details.
+ * @param url
+ */
+ public void setURL(String url)
+ {
+ mURL = url;
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastHandler.java b/src/audio/broadcast/icecast/IcecastHandler.java
new file mode 100644
index 00000000..1161089f
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastHandler.java
@@ -0,0 +1,381 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
+package audio.broadcast.icecast;
+
+import audio.AudioPacket;
+import audio.broadcast.BroadcastFormat;
+import audio.broadcast.BroadcastHandler;
+import audio.broadcast.BroadcastState;
+import audio.broadcast.Broadcaster;
+import audio.broadcast.BroadcasterFactory;
+import audio.broadcast.HttpAsyncAudioStreamer;
+import audio.convert.IAudioConverter;
+import controller.ThreadPoolManager;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.concurrent.FutureCallback;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
+import org.apache.http.impl.nio.client.HttpAsyncClients;
+import org.apache.http.nio.IOControl;
+import org.apache.http.nio.client.methods.AsyncCharConsumer;
+import org.apache.http.nio.client.methods.HttpAsyncMethods;
+import org.apache.http.nio.protocol.BasicAsyncRequestProducer;
+import org.apache.http.nio.protocol.HttpAsyncRequestProducer;
+import org.apache.http.protocol.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
+import record.wave.AudioPacketMonoWaveReader;
+
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.CharBuffer;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.concurrent.Future;
+
+public class IcecastHandler extends BroadcastHandler
+{
+ private final static Logger mLog = LoggerFactory.getLogger( IcecastHandler.class );
+
+ private static final String HTTP_100_CONTINUE = "100-continue";
+ private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
+ private long mLastConnectionAttempt = 0;
+ private CloseableHttpAsyncClient mHttpClient;
+ private HttpAsyncAudioStreamer mAudioStreamer;
+ private URI mURI;
+ private int mCounter = 0;
+
+ /**
+ * Creates an Icecast broadcast handler.
+ * @param configuration
+ * @param audioConverter
+ */
+ public IcecastHandler(IcecastConfiguration configuration, IAudioConverter audioConverter)
+ {
+ super(configuration, audioConverter);
+
+ mAudioStreamer = new HttpAsyncAudioStreamer(HttpPut.METHOD_NAME, getConfiguration().getMountPoint(),
+ getAudioConverter(), 20);
+ }
+
+ /**
+ * Configuration information
+ */
+ private IcecastConfiguration getConfiguration()
+ {
+ return (IcecastConfiguration)getBroadcastConfiguration();
+ }
+
+ private URI getURI() throws URISyntaxException
+ {
+ if(mURI == null)
+ {
+ mURI = new URIBuilder()
+ .setScheme("http")
+ .setHost(getConfiguration().getHost())
+ .setPort(getConfiguration().getPort())
+ .setPath(getConfiguration().getMountPoint())
+ .build();
+ }
+
+ return mURI;
+ }
+
+ /**
+ * Process audio packets using Shoutcast V1 Protocol
+ * @param audioPackets
+ */
+ @Override
+ public void broadcast(List audioPackets)
+ {
+ if(mCounter > 10)
+ {
+ mLog.debug("Connecting ...");
+ connect();
+ }
+
+ for(AudioPacket audioPacket: audioPackets)
+ {
+ mAudioStreamer.receive(audioPacket);
+
+ mCounter++;
+ }
+ }
+
+ /**
+ * Indicates if the audio handler is currently connected to the remote server and capable of streaming audio.
+ *
+ * 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())
+ {
+ createConnection();
+ }
+
+ return connected();
+ }
+
+
+ /**
+ * Disconnect from the remote server
+ */
+ @Override
+ public void disconnect()
+ {
+
+ }
+
+ /**
+ * 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
+ */
+ private void createConnection()
+ {
+ if(!connected() && System.currentTimeMillis() - mLastConnectionAttempt >= RECONNECT_INTERVAL_MILLISECONDS)
+ {
+ mLog.debug("Creating a connection to icecast server");
+ mLastConnectionAttempt = System.currentTimeMillis();
+
+ if(mHttpClient == null)
+ {
+ mHttpClient = HttpAsyncClients.createDefault();
+ mHttpClient.start();
+ }
+
+// try
+// {
+ HttpHost httpHost = new HttpHost(getConfiguration().getHost(), getConfiguration().getPort());
+// mAudioStreamer.addHeader(IcecastHeader.EXPECT.getValue(), HTTP_100_CONTINUE);
+
+ mAudioStreamer.addHeader(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getAuthorization());
+ mAudioStreamer.addHeader(HttpHeaders.ACCEPT, "*/*");
+ mAudioStreamer.addHeader(HttpHeaders.CONTENT_TYPE, getConfiguration().getBroadcastFormat().getValue());
+ mAudioStreamer.addHeader(IcecastHeader.PUBLIC.getValue(), getConfiguration().isPublic() ? "1" : "0");
+ mAudioStreamer.addHeader(IcecastHeader.NAME.getValue(), getConfiguration().getStreamName());
+ mAudioStreamer.addHeader(IcecastHeader.DESCRIPTION.getValue(), getConfiguration().getHost());
+ mAudioStreamer.addHeader(IcecastHeader.URL.getValue(), getConfiguration().getURL());
+ mAudioStreamer.addHeader(IcecastHeader.GENRE.getValue(), getConfiguration().getGenre());
+ mAudioStreamer.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
+
+// BasicAsyncRequestProducer producer = new BasicAsyncRequestProducer(httpHost, mAudioStreamer);
+ BasicAsyncRequestProducer producer = new MyProducer(httpHost, mAudioStreamer, mAudioStreamer);
+ AsyncCharConsumer consumer = new AsyncCharConsumer()
+ {
+ @Override
+ protected void onCharReceived(CharBuffer charBuffer, IOControl ioControl) throws IOException
+ {
+ mLog.debug("Received consumer characters!");
+ }
+
+ @Override
+ protected void onResponseReceived(HttpResponse response) throws HttpException, IOException
+ {
+ mLog.debug("Received consumer response:\n" + response.toString());
+
+ switch(response.getStatusLine().getStatusCode())
+ {
+ case HttpStatus.SC_OK: //200
+ case HttpStatus.SC_CONTINUE: //100
+ mLog.debug("Connected to icecast server");
+ setBroadcastState(BroadcastState.CONNECTED);
+ break;
+ case HttpStatus.SC_UNAUTHORIZED: //401
+ setBroadcastState(BroadcastState.INVALID_PASSWORD);
+ mLog.error("Couldn't connect to Icecast2 server using login credentials");
+ break;
+ case HttpStatus.SC_FORBIDDEN: //403
+ String reason = response.getStatusLine().getReasonPhrase();
+
+ if(reason.contains("Content-Type"))
+ {
+ setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
+ mLog.error("Icecast2 server does not support audio format");
+ }
+ else if(reason.startsWith("internal format"))
+ {
+ setBroadcastState(BroadcastState.REMOTE_SERVER_ERROR);
+ mLog.error("Icecast2 server has an error");
+ }
+ else if(reason.startsWith("too many sources"))
+ {
+ setBroadcastState(BroadcastState.MAX_SOURCES_EXCEEDED);
+ mLog.error("Icecast2 server maximum number of sources exceeded");
+ }
+ else if(reason.startsWith("Mountpoint"))
+ {
+ setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
+ mLog.error("Icecast2 server mount point is already in use");
+ }
+ case HttpStatus.SC_INTERNAL_SERVER_ERROR: //500
+ setBroadcastState(BroadcastState.INVALID_PASSWORD);
+ mLog.error("Couldn't connect to Icecast2 server using login credentials");
+ break;
+ default:
+ setBroadcastState(BroadcastState.ERROR);
+ mLog.error("Error connecting to remote server. HTTP Status Code:" +
+ response.getStatusLine().getStatusCode() + " Reason:" +
+ response.getStatusLine().getReasonPhrase());
+ }
+ }
+
+ @Override
+ protected HttpResponse buildResult(HttpContext httpContext) throws Exception
+ {
+ mLog.debug("Received consumer request for response!!!!!!");
+ return null;
+ }
+ };
+
+ mLog.debug("Sending connection request");
+ Future response = mHttpClient.execute(producer, consumer, new FutureCallback()
+ {
+ @Override
+ public void completed(HttpResponse httpResponse)
+ {
+ mLog.debug("Received final response:\n" + httpResponse.toString());
+ }
+
+ @Override
+ public void failed(Exception e)
+ {
+ mLog.debug("Received final response error", e);
+ }
+
+ @Override
+ public void cancelled()
+ {
+ mLog.debug("Received final response - cancelled");
+ }
+ });
+
+ mLog.debug("Create connection finished ... processing");
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ boolean test = false;
+
+ IcecastConfiguration config = new IcecastConfiguration(BroadcastFormat.MP3);
+ config.setAlias("Test Configuration");
+ config.setHost("localhost");
+ config.setPort(8000);
+ config.setMountPoint("/dennyTestStream");
+ config.setUserName("source");
+ config.setPassword("denny");
+ config.setStreamName("Denny's Audio Broadcast Test");
+ config.setDescription("Description");
+ config.setGenre("Scanner Audio");
+ config.setPublic(true);
+ config.setURL("http://localhost:8000");
+ config.setBitRate(16);
+
+ if(test)
+ {
+ CloseableHttpClient client = HttpClients.createDefault();
+
+ URI uri;
+
+ try
+ {
+ uri = new URIBuilder()
+ .setScheme("http")
+ .setHost(config.getHost())
+ .setPort(config.getPort())
+ .setPath(config.getMountPoint())
+ .build();
+
+ HttpPut httpPut = new HttpPut(uri);
+ httpPut.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ httpPut.addHeader(IcecastHeader.AUTHORIZATION.getValue(), config.getAuthorization());
+ httpPut.addHeader(IcecastHeader.HOST.getValue(), config.getHost());
+ httpPut.addHeader(IcecastHeader.ACCEPT.getValue(), "*/*");
+ httpPut.addHeader(IcecastHeader.CONTENT_TYPE.getValue(), config.getBroadcastFormat().getValue());
+ httpPut.addHeader(IcecastHeader.PUBLIC.getValue(), config.isPublic() ? "1" : "0");
+ httpPut.addHeader(IcecastHeader.NAME.getValue(), config.getStreamName());
+ httpPut.addHeader(IcecastHeader.DESCRIPTION.getValue(), config.getHost());
+ httpPut.addHeader(IcecastHeader.URL.getValue(), config.getURL());
+ httpPut.addHeader(IcecastHeader.GENRE.getValue(), config.getGenre());
+ httpPut.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
+ httpPut.addHeader(IcecastHeader.EXPECT.getValue(), HTTP_100_CONTINUE);
+
+ CloseableHttpResponse response = client.execute(httpPut);
+
+ mLog.debug("Response:" + response.toString());
+
+ }
+ catch(URISyntaxException e)
+ {
+ mLog.error("URI syntax exception", e);
+ }
+ catch(IOException ioe)
+ {
+ mLog.error("IO Exception", ioe);
+ }
+ }
+ else
+ {
+ ThreadPoolManager threadPoolManager = new ThreadPoolManager();
+
+ final Broadcaster broadcaster = BroadcasterFactory.getBroadcaster(threadPoolManager,config);
+
+ Path path = Paths.get("/home/denny/Music/PCM.wav");
+ mLog.debug("Opening: " + path.toString());
+
+ mLog.debug("Registering and starting audio playback");
+
+ while(true)
+ {
+ mLog.debug("Playback started [" + path.toString() + "]");
+
+ try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
+ {
+ reader.setListener(broadcaster);
+ reader.read();
+ }
+ catch (IOException e)
+ {
+ mLog.error("Error", e);
+ }
+
+ mLog.debug("Playback ended [" + path.toString() + "]");
+ }
+ }
+ }
+}
diff --git a/src/audio/broadcast/icecast/IcecastHeader.java b/src/audio/broadcast/icecast/IcecastHeader.java
new file mode 100644
index 00000000..5792316b
--- /dev/null
+++ b/src/audio/broadcast/icecast/IcecastHeader.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2016 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ *
+ ******************************************************************************/
+package audio.broadcast.icecast;
+
+public enum IcecastHeader
+{
+ ACCEPT("Accept"),
+ AUTHORIZATION("Authorization"),
+ CONTENT_TYPE("Content-Type"),
+ DESCRIPTION("ice-description"),
+ EXPECT("Expect"),
+ GENRE("ice-genre"),
+ HOST("Host"),
+ NAME("ice-name"),
+ PUBLIC("ice-public"),
+ URL("ice-url"),
+ USER_AGENT("User-Agent"),
+
+ UNKNOWN("unknown");
+
+ private String mValue;
+
+ private IcecastHeader(String value)
+ {
+ mValue = value;
+ }
+
+ public String getValue()
+ {
+ return mValue;
+ }
+}
diff --git a/src/audio/broadcast/icecast/MyProducer.java b/src/audio/broadcast/icecast/MyProducer.java
new file mode 100644
index 00000000..7ae6d454
--- /dev/null
+++ b/src/audio/broadcast/icecast/MyProducer.java
@@ -0,0 +1,103 @@
+/*******************************************************************************
+ * 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
+ *
+ ******************************************************************************/
+package audio.broadcast.icecast;
+
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.nio.ContentEncoder;
+import org.apache.http.nio.IOControl;
+import org.apache.http.nio.entity.HttpAsyncContentProducer;
+import org.apache.http.nio.protocol.BasicAsyncRequestProducer;
+import org.apache.http.protocol.HttpContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+public class MyProducer extends BasicAsyncRequestProducer
+{
+ private final static Logger mLog = LoggerFactory.getLogger( MyProducer.class );
+
+ public MyProducer(HttpHost target, HttpEntityEnclosingRequest request, HttpAsyncContentProducer producer)
+ {
+ super(target, request, producer);
+ }
+
+ @Override
+ public void produceContent(ContentEncoder encoder, IOControl ioctrl) throws IOException
+ {
+// mLog.debug("############### Producing Content");
+ super.produceContent(encoder, ioctrl);
+ }
+
+ @Override
+ public HttpRequest generateRequest()
+ {
+ mLog.debug("############### Generating a request");
+
+ HttpRequest request = super.generateRequest();
+
+ mLog.debug("############### Our request class is:" + request.getClass() );
+
+ return request;
+ }
+
+ @Override
+ public HttpHost getTarget()
+ {
+ mLog.debug("############### Getting our target");
+ return super.getTarget();
+ }
+
+ @Override
+ public void requestCompleted(HttpContext context)
+ {
+ mLog.debug("############### requesting completed");
+ super.requestCompleted(context);
+ }
+
+ @Override
+ public void failed(Exception ex)
+ {
+ mLog.debug("############### Fail!");
+ super.failed(ex);
+ }
+
+ @Override
+ public boolean isRepeatable()
+ {
+ mLog.debug("############### Indicating if we're repeatable");
+ return super.isRepeatable();
+ }
+
+ @Override
+ public void resetRequest() throws IOException
+ {
+ mLog.debug("############### resetting request");
+ super.resetRequest();
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ mLog.debug("############### Closing");
+ super.close();
+ }
+}
diff --git a/src/audio/broadcast/shoutcast/v2/ShoutcastV2Handler.java b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Handler.java
index 9ec4c0f6..2c586174 100644
--- a/src/audio/broadcast/shoutcast/v2/ShoutcastV2Handler.java
+++ b/src/audio/broadcast/shoutcast/v2/ShoutcastV2Handler.java
@@ -44,6 +44,7 @@ import audio.convert.IAudioConverter;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import properties.SystemProperties;
import record.wave.AudioPacketMonoWaveReader;
import java.io.DataInputStream;
@@ -402,7 +403,8 @@ public class ShoutcastV2Handler extends BroadcastHandler
(response.getError() != null ? response.getError() : "no error message"));
}
- metadata.add(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION.asXML("sdrtrunk"));
+ metadata.add(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION
+ .asXML(SystemProperties.getInstance().getApplicationName()));
mLog.debug("Fetching metadata messages from metadata...");
List messages = getMetadataMessages(metadata);
diff --git a/src/gui/SDRTrunk.java b/src/gui/SDRTrunk.java
index c3e6fa40..0e21c27b 100644
--- a/src/gui/SDRTrunk.java
+++ b/src/gui/SDRTrunk.java
@@ -229,7 +229,7 @@ public class SDRTrunk implements Listener
/**
* Setup main JFrame window
*/
- mTitle = getTitle();
+ mTitle = SystemProperties.getInstance().getApplicationName();
mMainGui.setTitle( mTitle );
mMainGui.setBounds( 100, 100, 1280, 800 );
mMainGui.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
@@ -448,32 +448,6 @@ public class SDRTrunk implements Listener
return homePath;
}
- private String getTitle()
- {
- StringBuilder sb = new StringBuilder();
-
- sb.append( "sdrtrunk" );
-
- try( BufferedReader reader = new BufferedReader(
- new InputStreamReader( this.getClass()
- .getResourceAsStream( "/sdrtrunk-version" ) ) ) )
- {
- String version = reader.readLine();
-
- if( version != null )
- {
- sb.append( " V" );
- sb.append( version );
- }
- }
- catch( Exception e )
- {
- mLog.error( "Couldn't read sdrtrunk version from application jar file" );
- }
-
- return sb.toString();
- }
-
@Override
public void receive( TunerEvent event )
{
diff --git a/src/properties/SystemProperties.java b/src/properties/SystemProperties.java
index 38837ed5..de25de05 100644
--- a/src/properties/SystemProperties.java
+++ b/src/properties/SystemProperties.java
@@ -17,11 +17,13 @@
******************************************************************************/
package properties;
+import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -47,7 +49,7 @@ public class SystemProperties
private static SystemProperties mInstance;
private static Properties mProperties;
private Path mPropertiesPath;
-
+ private String mApplicationName;
private SystemProperties()
{
@@ -67,7 +69,7 @@ public class SystemProperties
return mInstance;
}
-
+
/**
* Saves any currently changed settings to the application properties file
*/
@@ -86,9 +88,6 @@ public class SystemProperties
"SDRTrunk - SDR Trunking Decoder Application Settings";
mProperties.store( out, comments );
-
-// mLog.info( "SystemProperties - saved current properties [" +
-// propsPath.toString() + "]" );
}
catch( Exception e )
{
@@ -216,6 +215,37 @@ public class SystemProperties
propertiesPath.toString() + "]" );
}
+ public String getApplicationName()
+ {
+ if(mApplicationName == null)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append( "sdrtrunk" );
+
+ try(BufferedReader reader = new BufferedReader(
+ new InputStreamReader( this.getClass()
+ .getResourceAsStream( "/sdrtrunk-version" ) ) ) )
+ {
+ String version = reader.readLine();
+
+ if( version != null )
+ {
+ sb.append( " V" );
+ sb.append( version );
+ }
+ }
+ catch( Exception e )
+ {
+ mLog.error( "Couldn't read sdrtrunk version from application jar file" );
+ }
+
+ mApplicationName = sb.toString();
+ }
+
+ return mApplicationName;
+ }
+
/**
* Returns the value of the property, or null if the property doesn't exist
*/