#30 WIP - broadcastify multi-channel streaming is functional.

This commit is contained in:
Denny
2016-11-27 11:58:34 -05:00
parent ecfac83eb4
commit b71415b82a
37 changed files with 2049 additions and 715 deletions

View File

@@ -19,11 +19,11 @@
package audio.broadcast.icecast;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcastFactory;
import audio.convert.IAudioConverter;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.BoundRequestBuilder;
@@ -53,9 +53,9 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<ByteBuffer>
public class IcecastHTTPAudioBroadcaster extends AudioBroadcaster implements Publisher<ByteBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPBroadcaster.class );
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPAudioBroadcaster.class );
private static final String HTTP_100_CONTINUE = "100-continue";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
@@ -69,15 +69,13 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
/**
* Creates an Icecast 2.4.x (or newer) compatible broadcaster using HTTP 1.1 protocol
*
* Note: use @see IcecastTCPBroadcaster for Icecast version 2.3.x and older.
* Note: use @see IcecastTCPAudioBroadcaster for Icecast version 2.3.x and older.
* @param configuration
* @param audioConverter
*/
public IcecastHTTPBroadcaster(ThreadPoolManager threadPoolManager,
IcecastHTTPConfiguration configuration,
IAudioConverter audioConverter)
public IcecastHTTPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastHTTPConfiguration configuration)
{
super(threadPoolManager, configuration, audioConverter);
super(threadPoolManager, configuration);
}
/**
@@ -88,31 +86,24 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
}
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
mLog.debug("Request to send metadata to Icecast HTTP server - needs code!");
}
/**
* Broadcast any queued audio packets using Icecast V2 Protocol
*/
@Override
public void broadcast()
protected void broadcastAudio(byte[] audio)
{
if(connect())
{
if (mSubscription != null && mSubscription.hasDemand())
{
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
if (!mPacketsToBroadcast.isEmpty())
{
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
mLog.debug("We have [" + mPacketsToBroadcast.size() +
"] packets - sending:" + convertedAudio.length + " bytes");
ByteBuffer buffer = ByteBuffer.wrap(convertedAudio);
mSubscription.broadcast(buffer);
mPacketsToBroadcast.clear();
}
mSubscription.broadcast(ByteBuffer.wrap(audio));
}
}
}
@@ -418,8 +409,9 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
else
{
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
@@ -432,7 +424,7 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(broadcaster);
reader.setListener(audioBroadcaster);
reader.read();
}
catch (IOException e)

View File

@@ -36,6 +36,11 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
private int mSampleRate = 8000;
private String mURL;
public IcecastHTTPConfiguration()
{
//no-arg JAXB constructor
}
/**
* Icecast 2.3.x and 2.4.x compatible configuration
* @param format
@@ -45,6 +50,33 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
super(format);
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastHTTPConfiguration copy = new IcecastHTTPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setBitRate(getBitRate());
copy.setChannels(getChannels());
copy.setSampleRate(getSampleRate());
copy.setURL(getURL());
return copy;
}
@Override
public BroadcastServerType getBroadcastServerType()
{
@@ -149,7 +181,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Public visibility of the broadcast
* Public visibility of the broadcastAudio
*/
public boolean isPublic()
{
@@ -157,8 +189,8 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
@@ -166,7 +198,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Number of audio channels in the broadcast
* Number of audio channels in the broadcastAudio
*/
public int getChannels()
{
@@ -174,7 +206,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* Sets the number of audio channels in the broadcast
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
@@ -224,7 +256,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
*/
public String getURL()
{
@@ -232,7 +264,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)

View File

@@ -18,16 +18,24 @@
******************************************************************************/
package audio.broadcast.icecast;
import alias.Alias;
import alias.id.broadcast.BroadcastChannel;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFactory;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcastFactory;
import audio.convert.IAudioConverter;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ThreadPoolManager;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.BoundRequestBuilder;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Realm;
import org.asynchttpclient.Response;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,6 +45,7 @@ import record.wave.AudioPacketMonoWaveReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
@@ -44,91 +53,186 @@ import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastTCPBroadcaster extends Broadcaster
public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPBroadcaster.class );
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPAudioBroadcaster.class );
private final static String UTF8 = "UTF-8";
private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private long mLastConnectionAttempt = 0;
private List<AudioPacket> mPacketsToBroadcast = new ArrayList<>();
private AtomicBoolean mConnecting = new AtomicBoolean();
private Socket mSocket;
private DataOutputStream mOutputStream;
private DataInputStream mInputStream;
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
private int mCounter;
private boolean mFirstMetadataUpdateSuppressed = false;
private byte[] mSilenceFrame;
/**
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
* compatible with Icecast version 2.3.2 and older versions of the server software.
*
* Note: use @see IcecastHTTPBroadcaster for Icecast version 2.4.x and newer.
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
*
* @param configuration
* @param audioConverter
*/
public IcecastTCPBroadcaster(ThreadPoolManager threadPoolManager,
IcecastTCPConfiguration configuration,
IAudioConverter audioConverter)
public IcecastTCPAudioBroadcaster(DefaultAsyncHttpClient httpClient,
ThreadPoolManager threadPoolManager,
IcecastTCPConfiguration configuration)
{
super(threadPoolManager, configuration, audioConverter);
super(threadPoolManager, configuration);
mDefaultAsyncHttpClient = httpClient;
}
/**
* Configuration information
*/
private IcecastHTTPConfiguration getConfiguration()
private IcecastTCPConfiguration getConfiguration()
{
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
return (IcecastTCPConfiguration)getBroadcastConfiguration();
}
/**
* Broadcast any queued audio packets using Icecast V2 Protocol
*/
@Override
public void broadcast()
protected void broadcastAudio(byte[] audio)
{
if(connect())
boolean completed = false;
while(!completed)
{
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
if (!mPacketsToBroadcast.isEmpty())
if(connect())
{
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
mLog.debug("We have [" + mPacketsToBroadcast.size() +
"] packets - sending:" + convertedAudio.length + " bytes");
try
{
send(convertedAudio);
send(audio);
completed = true;
}
catch(SocketException se)
{
disconnect();
//The remote server likely disconnected - setup to reconnect
mLog.error("Socket Exception on Icecast TCP Audio Broadcaster -- disconnecting and resetting", se);
disconnect(false);
}
catch(IOException e)
{
completed = true;
mLog.error("Error sending audio", e);
}
}
else
{
completed = true;
}
}
}
mPacketsToBroadcast.clear();
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
if(connected() && mFirstMetadataUpdateSuppressed)
{
StringBuilder sb = new StringBuilder();
mCounter++;
if(metadata != null)
{
Metadata to = metadata.getMetadata(MetadataType.TO);
if(mCounter % 10 == 0)
sb.append("TO:");
if(to != null)
{
updateMetadata("Song Count " + mCounter);
if(to.hasAlias())
{
sb.append(to.getAlias().getName());
}
else
{
sb.append(to.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
Metadata from = metadata.getMetadata(MetadataType.FROM);
sb.append(" FROM:");
if(from != null)
{
if(from.hasAlias())
{
sb.append(from.getAlias().getName());
}
else
{
sb.append(from.getValue());
}
}
else
{
sb.append("UNKNOWN");
}
}
else
{
sb.append("Scanning ....");
}
try
{
if(mDefaultAsyncHttpClient == null)
{
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
}
String songEncoded = URLEncoder.encode(sb.toString(), UTF8);
StringBuilder query = new StringBuilder();
query.append("/admin/metadata?mode=updinfo");
query.append("&mount=").append(getConfiguration().getMountPoint());
query.append("&charset=UTF%2d8");
query.append("&song=").append(songEncoded);
Uri uri = new Uri(Uri.HTTP, null, getConfiguration().getHost(), getConfiguration().getPort(),
query.toString(), null);
BoundRequestBuilder builder = mDefaultAsyncHttpClient.prepareGet(uri.toUrl());
//Use Basic (base64) authentication
Realm realm = new Realm.Builder(getConfiguration().getUserName(), getConfiguration().getPassword())
.setScheme(Realm.AuthScheme.BASIC).setUsePreemptiveAuth(true).build();
builder.setRealm(realm);
builder.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
AsyncHandler handler = new AsyncCompletionHandler()
{
@Override
public Object onCompleted(Response response) throws Exception
{
return null;
}
};
mDefaultAsyncHttpClient.executeRequest(builder.build(), handler);
}
catch(Exception e)
{
mLog.debug("Error while updating metadata", e);
}
}
else if(connected())
{
mFirstMetadataUpdateSuppressed = true;
}
}
@@ -152,7 +256,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
/**
* Disconnect from the remote server
*/
public void disconnect()
public void disconnect(boolean shutdown)
{
if(mOutputStream != null)
{
@@ -197,7 +301,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
mSocket = null;
}
mLog.debug("Icecast stream is now disconnected");
if(shutdown)
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
else
{
setBroadcastState(BroadcastState.READY);
}
}
/**
@@ -226,7 +337,6 @@ public class IcecastTCPBroadcaster extends Broadcaster
.append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
@@ -334,6 +444,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
getConfiguration().getPort() + "] - will reattempt connection periodically");
return;
}
catch(ConnectException ce)
{
setBroadcastState(BroadcastState.NO_SERVER);
mLog.error("Connection refused. Unable to create connection to streaming server host[" +
getConfiguration().getHost() + "] and port[" +
getConfiguration().getPort() + "]");
return;
}
catch(IOException ioe)
{
setBroadcastState(BroadcastState.ERROR);
@@ -457,7 +575,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if(response.startsWith("Invalid Password"))
else if(response.contains("Invalid Password") || response.contains("Authentication Required"))
{
setBroadcastState(BroadcastState.INVALID_PASSWORD);
}
@@ -469,56 +587,20 @@ public class IcecastTCPBroadcaster extends Broadcaster
}
else
{
setBroadcastState(BroadcastState.CONNECTED);
mLog.debug("Response was empty");
}
}
private void updateMetadata(String songName)
{
if(songName != null && !songName.isEmpty())
{
try
{
if(mDefaultAsyncHttpClient == null)
{
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
}
String songEncoded = URLEncoder.encode(songName, UTF8);
StringBuilder query = new StringBuilder();
query.append("/admin/metadata?mode=updinfo");
query.append("&mount=").append(getConfiguration().getMountPoint());
query.append("&charset=UTF%2d8");
query.append("&song=").append(songEncoded);
Uri uri = new Uri(Uri.HTTP, null, getConfiguration().getHost(), getConfiguration().getPort(),
query.toString(), null);
BoundRequestBuilder builder = mDefaultAsyncHttpClient.prepareGet(uri.toUrl());
//Use Basic (base64) authentication
Realm realm = new Realm.Builder(getConfiguration().getUserName(), getConfiguration().getPassword())
.setScheme(Realm.AuthScheme.BASIC).setUsePreemptiveAuth(true).build();
builder.setRealm(realm);
builder.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
mDefaultAsyncHttpClient.executeRequest(builder.build());
}
catch(Exception e)
{
mLog.debug("Error while updating metadata", e);
}
}
}
public static void main(String[] args)
{
boolean test = false;
IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3);
config.setName("Broadcastify SDRTrunk #2");
String streamName = "Broadcastify SDRTrunk #2";
config.setName(streamName);
config.setHost("audio3.broadcastify.com");
config.setPort(80);
config.setMountPoint("/k0yrdpx7zn4h");
@@ -530,6 +612,11 @@ public class IcecastTCPBroadcaster extends Broadcaster
config.setURL("http://www.radioreference.com");
config.setBitRate(16);
AudioMetadata metadata = new AudioMetadata(0, false);
Alias toAlias = new Alias("ToAlias");
toAlias.addAliasID(new BroadcastChannel(streamName));
metadata.receive(new Metadata(MetadataType.TO, "1234", toAlias ));
if(test)
{
mLog.debug("Auth:" + config.getEncodedCredentials());
@@ -537,22 +624,29 @@ public class IcecastTCPBroadcaster extends Broadcaster
else
{
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
mLog.debug("Registering and starting audio playback");
int playCount = 0;
while(true)
{
mLog.debug("Playback started [" + path.toString() + "]");
toAlias.setName("Playback #" + playCount++);
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setListener(broadcaster);
reader.setMetadata(metadata);
reader.setListener(audioBroadcaster);
reader.read();
audioBroadcaster.receive(new AudioPacket(AudioPacket.Type.END, metadata));
}
catch (IOException e)
{

View File

@@ -22,9 +22,10 @@ import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import java.util.Base64;
@@ -32,6 +33,8 @@ import java.util.Base64;
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
public class IcecastTCPConfiguration extends BroadcastConfiguration
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
private String mUserName;
private String mMountPoint;
private String mDescription;
@@ -44,6 +47,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
public IcecastTCPConfiguration()
{
setUserName("source");
//No-arg constructor for JAXB
}
@@ -54,6 +58,31 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
public IcecastTCPConfiguration(BroadcastFormat format)
{
super(format);
setUserName("source");
}
@Override
public BroadcastConfiguration copyOf()
{
IcecastTCPConfiguration copy = new IcecastTCPConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
return copy;
}
@Override
@@ -79,6 +108,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
sb.append(getPassword());
}
mLog.debug("Connection String: " + sb.toString());
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
return "Basic " + base64;
@@ -175,7 +206,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Public visibility of the broadcast
* Public visibility of the broadcastAudio
*/
@XmlAttribute( name="public" )
public boolean isPublic()
@@ -184,8 +215,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Sets public visibility of the broadcast
* @param isPublic indicates if the broadcast should be visible to the public
* Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcastAudio should be visible to the public
*/
public void setPublic(boolean isPublic)
{
@@ -193,7 +224,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Number of audio channels in the broadcast
* Number of audio channels in the broadcastAudio
*/
@XmlAttribute( name="channels" )
public int getChannels()
@@ -202,7 +233,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* Sets the number of audio channels in the broadcast
* Sets the number of audio channels in the broadcastAudio
*/
public void setChannels(int channels)
{
@@ -254,7 +285,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
*/
@XmlAttribute( name="url" )
public String getURL()
@@ -263,7 +294,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
}
/**
* URL associated with the broadcast where users can find additional details.
* URL associated with the broadcastAudio where users can find additional details.
* @param url
*/
public void setURL(String url)
@@ -275,4 +306,35 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
{
return mURL != null;
}
@Override
public boolean isValid()
{
if(!super.isValid())
{
return false;
}
else if(mUserName == null)
{
return false;
}
else if(mMountPoint == null)
{
return false;
}
else if(mChannels != 1)
{
return false;
}
else if(mSampleRate <= 0)
{
return false;
}
else if(mBitRate <= 0)
{
return false;
}
return true;
}
}