mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
#30 WIP - broadcastify and icecast TCP streams working and editors are complete. Adds icons to editor and table views.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user