#30 WIP - broadcastify and icecast TCP streams working and editors are complete. Adds icons to editor and table views.

This commit is contained in:
Denny
2016-12-04 07:28:03 -05:00
parent b71415b82a
commit f44892c62b
22 changed files with 1189 additions and 889 deletions

View File

@@ -30,56 +30,113 @@ import sample.Listener;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AudioBroadcaster implements IAudioPacketListener, Listener<AudioPacket>
public abstract class AudioBroadcaster implements IAudioPacketListener
{
private final static Logger mLog = LoggerFactory.getLogger( AudioBroadcaster.class );
public static final int PROCESSOR_RUN_INTERVAL_MS = 1000;
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture mScheduledTask;
private StreamManager mStreamManager;
private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
private ArrayBlockingQueue<StreamableAudioRecording> mAudioRecordingQueue;
private LinkedTransferQueue<StreamableAudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private byte[] mSilenceFrame;
private int mStreamedAudioCount = 0;
private Listener<BroadcastEvent> mBroadcastEventListener;
private BroadcastState mBroadcastState = BroadcastState.READY;
private StreamManager mStreamManager;
private int mStreamedAudioCount = 0;
private BroadcastConfiguration mBroadcastConfiguration;
private boolean mEnabled = true;
private long mDelay;
private AtomicBoolean mStreaming = new AtomicBoolean();
/**
* AudioBroadcaster for sending audio to a remote broadcastAudio server.
* AudioBroadcaster for streaming audio recordings to a remote streaming audio server. Audio recordings are
* generated by an internal StreamManager that converts an inbound stream of AudioPackets into a recording of the
* desired audio format (e.g. MP3) and nominates the recording to an internal recording queue for streaming. The
* broadcaster supports receiving audio packets from multiple audio sources. Each audio packet's internal audio
* metadata source string is used to reassemble each packet stream. Recordings are capped at 30 seconds length.
* If a source audio packet stream exceeds 30 seconds in length, it will be chunked into 30 second recordings.
*
* This broadcaster supports a time delay setting for delaying broadcast of audio recordings. The delay setting is
* defined in the broadcast configuration. When this delay is greater than zero, the recording will remain in the
* audio broadcaster queue until the recording start time + delay elapses. Audio recordings are processed in a FIFO
* manner.
*
* Use the start() and stop() methods to connect to/disconnect from the remote server. Audio recordings will be
* streamed to the remote server when available. One second silence frames will be broadcast to the server when
* there are no recordings available, in order to maintain a connection with the remote server. Any audio packet
* streams received while the broadcaster is stopped will be ignored.
*
* The last audio packet's metadata is automatically attached to the closed audio recording when it is enqueued for
* broadcast. That metadata will be updated on the remote server once the audio recording is opened for streaming.
*/
public AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration broadcastConfiguration)
{
mThreadPoolManager = threadPoolManager;
mBroadcastConfiguration = broadcastConfiguration;
//Recording queue - bounded at 10 queued recordings maximum - new stream recordings will be deleted once full
mAudioRecordingQueue = new ArrayBlockingQueue<>(10);
mDelay = mBroadcastConfiguration.getDelay();
//Create a 1 second silence frame - from 1200 millis of silence
mSilenceFrame = BroadcastFactory.getSilenceFrame(getBroadcastConfiguration().getBroadcastFormat(), 1200);
mStreamManager = new StreamManager(threadPoolManager, this, broadcastConfiguration.getDelay(),
mStreamManager = new StreamManager(threadPoolManager, this,
SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY));
mStreamManager.start();
startProcessor();
}
/**
* Recording backlog awaiting streaming
* Connects to the remote server specified by the broadcast configuration and starts audio streaming.
*/
public void start()
{
if(mStreaming.compareAndSet(false, true))
{
mStreamManager.start();
if(mScheduledTask == null)
{
if(mThreadPoolManager != null)
{
mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
mRecordingQueueProcessor, PROCESSOR_RUN_INTERVAL_MS, TimeUnit.MILLISECONDS );
}
}
}
}
/**
* Disconnects from the remote server.
*/
public void stop()
{
if(mStreaming.compareAndSet(true, false))
{
mStreamManager.stop();
if(mThreadPoolManager != null && mScheduledTask != null)
{
mThreadPoolManager.cancel(mScheduledTask);
}
disconnect();
}
}
/**
* Disconnects the broadcaster from the remote server for a reset or final stop.
*/
protected abstract void disconnect();
/**
* Size of recording queue for recordings awaiting streaming
*/
public int getQueueSize()
{
@@ -95,51 +152,18 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
}
/**
* Queues the streamable audio recording for broadcastAudio. The bounded queue is managed by removing older recordings
* at the head of the queue, once full, to make room for new recordings. This ensures that once the broadcaster
* (re)starts broadcasting or catches up with the queue, that it is broadcasting the most current audio recordings.
* Primary insert method for the stream manager to nominate completed audio recordings for broadcast.
*
* @param recording to queue for broadcasting
*/
public void receive(StreamableAudioRecording recording)
{
boolean success = false;
try
{
success = mAudioRecordingQueue.add(recording);
}
catch(IllegalStateException ise)
{
//The queue is full - delete the oldest recording at the head of the queue to make room for a new one
removeRecording(mAudioRecordingQueue.poll());
}
if(!success)
{
try
{
success = mAudioRecordingQueue.add(recording);
}
catch(IllegalStateException ise)
{
//Do nothing on second attempt
}
}
//If we can't queue the recording after two attempts, remove the recording
if(!success)
{
removeRecording(recording);
}
else
{
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
mAudioRecordingQueue.add(recording);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
/**
* Removes the temporary recording file from disk.
* Cleanup method to remove a temporary recording file from disk.
*
* @param recording to remove
*/
@@ -164,16 +188,16 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
}
/**
* IAudioPacketListener interface method
* IAudioPacketListener interface method to gain access to the internal stream manager for enqueuing audio packets.
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
return mStreamManager;
}
/**
* Broadcasts the audio binary data.
* Broadcast binary audio data frames or sequences.
*/
protected abstract void broadcastAudio(byte[] audio);
@@ -183,73 +207,6 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
*/
protected abstract void broadcastMetadata(AudioMetadata metadata);
/**
* Disconnects the broadcaster and halts all broadcasting.
*/
public void stop()
{
mEnabled = false;
mStreamManager.stop();
stopProcessor();
}
/**
* Primary audio packet input method. Audio packets are queued and then processed by the underlying broadcastAudio
* handler for broadcastAudio to the remote server.
*/
public void receive(AudioPacket packet)
{
if(mEnabled)
{
mStreamManager.receive(packet);
}
}
/**
* Starts the audio queue processor. The audio packet queue is serviced every 100 milliseconds. Subsequent calls
* to this method once running are ignored.
*/
private void startProcessor()
{
if(mEnabled && mScheduledTask == null)
{
if(mThreadPoolManager != null)
{
mScheduledTask = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
mRecordingQueueProcessor, PROCESSOR_RUN_INTERVAL_MS, TimeUnit.MILLISECONDS );
}
}
}
/**
* Stops the audio queue processor
*/
private void stopProcessor()
{
if(mThreadPoolManager != null && mScheduledTask != null)
{
mThreadPoolManager.cancel(mScheduledTask);
}
}
/**
* Pauses or unpauses the broadcastAudio
*/
public void setPaused(boolean paused)
{
if(!getBroadcastState().isErrorState())
{
if(paused)
{
setBroadcastState(BroadcastState.PAUSED);
}
else
{
setBroadcastState(BroadcastState.READY);
}
}
}
/**
* Registers the listener to receive broadcastAudio state changes
*/
@@ -288,6 +245,11 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
if(mBroadcastState.isErrorState())
{
stop();
}
}
}
@@ -299,7 +261,10 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
return mBroadcastState;
}
public boolean connected()
/**
* Indicates if the broadcaster is currently connected to the remote server
*/
protected boolean connected()
{
return getBroadcastState() == BroadcastState.CONNECTED;
}
@@ -314,6 +279,15 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
return state != BroadcastState.CONNECTED && !state.isErrorState();
}
/**
* Indicates if the current broadcast state is an error state, meaning that it cannot recover or connect using the
* current configuration.
*/
protected boolean isErrorState()
{
return getBroadcastState().isErrorState();
}
/**
* Audio recording queue processor. Fetches recordings from the queue and chunks the recording byte content
@@ -366,19 +340,24 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
*/
private void nextRecording()
{
boolean metadataUpdateRequired = false;
if(mInputStream != null)
{
mStreamedAudioCount++;
broadcast(new BroadcastEvent(AudioBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
metadataUpdateRequired = true;
}
mInputStream = null;
StreamableAudioRecording recording = mAudioRecordingQueue.poll();
StreamableAudioRecording nextRecording = mAudioRecordingQueue.peek();
if(recording != null)
if(nextRecording != null && nextRecording.getStartTime() + mDelay <= System.currentTimeMillis())
{
StreamableAudioRecording recording = mAudioRecordingQueue.poll();
try
{
byte[] audio = Files.readAllBytes(recording.getPath());
@@ -398,10 +377,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
mChunkSize = (int)(mInputStream.available() / wholeIntervalChunks) + 1;
broadcastMetadata(recording.getAudioMetadata());
}
else
{
broadcastMetadata(null);
metadataUpdateRequired = false;
}
}
catch(IOException ioe)
@@ -416,7 +392,9 @@ public abstract class AudioBroadcaster implements IAudioPacketListener, Listener
broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
else
//If we closed out a recording and don't have anything new, send an empty metadata update
if(metadataUpdateRequired)
{
broadcastMetadata(null);
}