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 */