#30 - changes internal MP3 temporary recording stream management so that the broadcast model manages the recordings and sends the completed recordings to the broadcasters to avoid 2 or more streams recording MP3 files for the same audio call.

Note: there is a bug where temporary recordings are not being consistently deleted that I'm still working on.
This commit is contained in:
Denny
2017-01-03 09:27:17 -05:00
parent 4578e45f60
commit bc430b255f
5 changed files with 184 additions and 90 deletions

View File

@@ -36,7 +36,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AudioBroadcaster implements IAudioPacketListener public abstract class AudioBroadcaster implements Listener<AudioRecording>
{ {
private final static Logger mLog = LoggerFactory.getLogger( AudioBroadcaster.class ); private final static Logger mLog = LoggerFactory.getLogger( AudioBroadcaster.class );
@@ -46,14 +46,13 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
private ScheduledFuture mScheduledTask; private ScheduledFuture mScheduledTask;
private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor(); private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
private LinkedTransferQueue<StreamableAudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>(); private LinkedTransferQueue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private ISilenceGenerator mSilenceGenerator; private ISilenceGenerator mSilenceGenerator;
private Listener<BroadcastEvent> mBroadcastEventListener; private Listener<BroadcastEvent> mBroadcastEventListener;
private BroadcastState mBroadcastState = BroadcastState.READY; private BroadcastState mBroadcastState = BroadcastState.READY;
private StreamManager mStreamManager;
private int mStreamedAudioCount = 0; private int mStreamedAudioCount = 0;
private BroadcastConfiguration mBroadcastConfiguration; private BroadcastConfiguration mBroadcastConfiguration;
private long mDelay; private long mDelay;
@@ -85,11 +84,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
mThreadPoolManager = threadPoolManager; mThreadPoolManager = threadPoolManager;
mBroadcastConfiguration = broadcastConfiguration; mBroadcastConfiguration = broadcastConfiguration;
mDelay = mBroadcastConfiguration.getDelay(); mDelay = mBroadcastConfiguration.getDelay();
mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat()); mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat());
mStreamManager = new StreamManager(threadPoolManager, this,
SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY));
} }
protected ThreadPoolManager getThreadPoolManager() protected ThreadPoolManager getThreadPoolManager()
@@ -133,8 +128,6 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{ {
if(mStreaming.compareAndSet(false, true)) if(mStreaming.compareAndSet(false, true))
{ {
mStreamManager.start();
if(mScheduledTask == null) if(mScheduledTask == null)
{ {
if(mThreadPoolManager != null) if(mThreadPoolManager != null)
@@ -153,8 +146,6 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
{ {
if(mStreaming.compareAndSet(true, false)) if(mStreaming.compareAndSet(true, false))
{ {
mStreamManager.stop();
if(mThreadPoolManager != null && mScheduledTask != null) if(mThreadPoolManager != null && mScheduledTask != null)
{ {
mThreadPoolManager.cancel(mScheduledTask); mThreadPoolManager.cancel(mScheduledTask);
@@ -201,7 +192,7 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
* *
* @param recording to queue for broadcasting * @param recording to queue for broadcasting
*/ */
public void receive(StreamableAudioRecording recording) public void receive(AudioRecording recording)
{ {
if(connected()) if(connected())
{ {
@@ -210,24 +201,6 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
} }
} }
/**
* Cleanup method to remove a temporary recording file from disk.
*
* @param recording to remove
*/
private void removeRecording(StreamableAudioRecording recording)
{
try
{
Files.delete(recording.getPath());
}
catch(IOException ioe)
{
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] - error deleting temporary internet " +
"recording file: " + recording.getPath().toString() + " - " + ioe.getMessage());
}
}
/** /**
* Broadcast configuration used by this broadcaster * Broadcast configuration used by this broadcaster
*/ */
@@ -236,15 +209,6 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
return mBroadcastConfiguration; return mBroadcastConfiguration;
} }
/**
* IAudioPacketListener interface method to gain access to the internal stream manager for enqueuing audio packets.
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return mStreamManager;
}
/** /**
* Registers the listener to receive broadcast events/state changes * Registers the listener to receive broadcast events/state changes
*/ */
@@ -291,11 +255,11 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
if(!connected()) if(!connected())
{ {
StreamableAudioRecording recording = mAudioRecordingQueue.poll(); //Remove all pending audio recordings
AudioRecording recording = mAudioRecordingQueue.poll();
while(recording != null) while(recording != null)
{ {
removeRecording(recording);
recording = mAudioRecordingQueue.poll(); recording = mAudioRecordingQueue.poll();
} }
} }
@@ -426,37 +390,38 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
mInputStream = null; mInputStream = null;
StreamableAudioRecording nextRecording = mAudioRecordingQueue.peek(); AudioRecording nextRecording = mAudioRecordingQueue.peek();
if(nextRecording != null && nextRecording.getStartTime() + mDelay <= System.currentTimeMillis()) if(nextRecording != null && nextRecording.getStartTime() + mDelay <= System.currentTimeMillis())
{ {
StreamableAudioRecording recording = mAudioRecordingQueue.poll(); AudioRecording recording = mAudioRecordingQueue.poll();
try try
{ {
byte[] audio = Files.readAllBytes(recording.getPath()); if(Files.exists(recording.getPath()))
if(audio != null && audio.length > 0)
{ {
mInputStream = new ByteArrayInputStream(audio); byte[] audio = Files.readAllBytes(recording.getPath());
mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS - if(audio != null && audio.length > 0)
(recording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);
while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
{ {
mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS; mInputStream = new ByteArrayInputStream(audio);
}
if(connected()) mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS -
{ (recording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);
broadcastMetadata(recording.getAudioMetadata());
}
metadataUpdateRequired = false; while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
{
mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS;
}
if(connected())
{
broadcastMetadata(recording.getAudioMetadata());
}
metadataUpdateRequired = false;
}
} }
removeRecording(recording);
} }
catch(IOException ioe) catch(IOException ioe)
{ {
@@ -467,6 +432,8 @@ public abstract class AudioBroadcaster implements IAudioPacketListener
metadataUpdateRequired = false; metadataUpdateRequired = false;
} }
recording.removePendingReplay();
broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE)); broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
} }

View File

@@ -21,13 +21,15 @@ package audio.broadcast;
import audio.metadata.AudioMetadata; import audio.metadata.AudioMetadata;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
public class StreamableAudioRecording implements Comparable<StreamableAudioRecording> public class AudioRecording implements Comparable<AudioRecording>
{ {
private Path mPath; private Path mPath;
private AudioMetadata mAudioMetadata; private AudioMetadata mAudioMetadata;
private long mStartTime; private long mStartTime;
private long mRecordingLength; private long mRecordingLength;
private AtomicInteger mPendingReplayCount = new AtomicInteger();
/** /**
* Audio recording that is ready to be streamed * Audio recording that is ready to be streamed
@@ -36,7 +38,7 @@ public class StreamableAudioRecording implements Comparable<StreamableAudioRecor
* @param start time of recording in milliseconds since epoch * @param start time of recording in milliseconds since epoch
* @param recordingLength in milliseconds * @param recordingLength in milliseconds
*/ */
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long start, long recordingLength) public AudioRecording(Path path, AudioMetadata audioMetadata, long start, long recordingLength)
{ {
mPath = path; mPath = path;
mAudioMetadata = audioMetadata; mAudioMetadata = audioMetadata;
@@ -81,8 +83,33 @@ public class StreamableAudioRecording implements Comparable<StreamableAudioRecor
* Implements comparable for sorting recordings based on start time in ascending order * Implements comparable for sorting recordings based on start time in ascending order
*/ */
@Override @Override
public int compareTo(StreamableAudioRecording otherRecording) public int compareTo(AudioRecording otherRecording)
{ {
return Long.compare(getStartTime(), otherRecording.getStartTime()); return Long.compare(getStartTime(), otherRecording.getStartTime());
} }
/**
* Increments the count of pending replays.
*/
public void addPendingReplay()
{
mPendingReplayCount.incrementAndGet();
}
/**
* Decrements the count of pending replays.
*/
public void removePendingReplay()
{
mPendingReplayCount.decrementAndGet();
}
/**
* Indicates if there are any remaining pending replays. Once the pending replay count is less than or equal to
* zero, the recording can be deleted.
*/
public boolean hasPendingReplays()
{
return mPendingReplayCount.get() <= 0;
}
} }

View File

@@ -20,6 +20,7 @@ package audio.broadcast;
import alias.id.broadcast.BroadcastChannel; import alias.id.broadcast.BroadcastChannel;
import audio.AudioPacket; import audio.AudioPacket;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -36,6 +37,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@@ -60,12 +62,13 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4; public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>(); private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>();
private List<AudioRecording> mRecordingQueue = new CopyOnWriteArrayList<>();
private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>(); private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>(); private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
private ThreadPoolManager mThreadPoolManager; private ThreadPoolManager mThreadPoolManager;
private SettingsManager mSettingsManager; private SettingsManager mSettingsManager;
private StreamManager mStreamManager;
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>(); private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
/** /**
@@ -75,6 +78,13 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
{ {
mThreadPoolManager = threadPoolManager; mThreadPoolManager = threadPoolManager;
mSettingsManager = settingsManager; mSettingsManager = settingsManager;
mStreamManager = new StreamManager(threadPoolManager, new CompletedRecordingListener(), BroadcastFormat.MP3,
SystemProperties.getInstance().getApplicationFolder(TEMPORARY_STREAM_DIRECTORY));
mStreamManager.start();
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_RECORDING,
new RecordingDeletionMonitor(), 15l, TimeUnit.SECONDS );
removeOrphanedTemporaryRecordings(); removeOrphanedTemporaryRecordings();
} }
@@ -265,22 +275,10 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
@Override @Override
public void receive(AudioPacket audioPacket) public void receive(AudioPacket audioPacket)
{ {
if (audioPacket.hasAudioMetadata() && audioPacket.getAudioMetadata().isStreamable()) if (audioPacket.hasAudioMetadata() &&
audioPacket.getAudioMetadata().isStreamable())
{ {
for (BroadcastChannel broadcastChannel : audioPacket.getAudioMetadata().getBroadcastChannels()) mStreamManager.receive(audioPacket);
{
String channelName = broadcastChannel.getChannelName();
if (channelName != null)
{
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
if (audioBroadcaster != null)
{
audioBroadcaster.getAudioPacketListener().receive(audioPacket);
}
}
}
} }
} }
@@ -600,6 +598,25 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
return null; return null;
} }
/**
* Cleanup method to remove a temporary recording file from disk.
*
* @param recording to remove
*/
private void removeRecording(AudioRecording recording)
{
try
{
Files.delete(recording.getPath());
}
catch(IOException ioe)
{
mLog.error("Error deleting temporary internet recording file: " + recording.getPath().toString() + " - " +
ioe.getMessage());
}
}
/** /**
* Removes any temporary stream recordings left-over from the previous application run. * Removes any temporary stream recordings left-over from the previous application run.
* *
@@ -659,4 +676,80 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
createBroadcaster(mBroadcastConfiguration); createBroadcaster(mBroadcastConfiguration);
} }
} }
/**
* Processes completed audio recordings and distributes them to the audio broadcasters. Adds the recording to
* the audio recording queue to be monitored for deletion.
*/
public class CompletedRecordingListener implements Listener<AudioRecording>
{
@Override
public void receive(AudioRecording audioRecording)
{
AudioMetadata metadata = audioRecording.getAudioMetadata();
if(metadata != null && metadata.isStreamable())
{
for (BroadcastChannel broadcastChannel : metadata.getBroadcastChannels())
{
String channelName = broadcastChannel.getChannelName();
if (channelName != null)
{
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
if (audioBroadcaster != null)
{
audioRecording.addPendingReplay();
audioBroadcaster.receive(audioRecording);
}
}
}
}
mRecordingQueue.add(audioRecording);
}
}
/**
* Monitors the recording queue and removes any recordings that have no pending replays by audio broadcasters
*/
public class RecordingDeletionMonitor implements Runnable
{
@Override
public void run()
{
try
{
List<AudioRecording> recordingsToDelete = new ArrayList<>();
Iterator<AudioRecording> it = mRecordingQueue.iterator();
AudioRecording recording;
while(it.hasNext())
{
recording = it.next();
if(!recording.hasPendingReplays())
{
recordingsToDelete.add(recording);
}
}
if(!recordingsToDelete.isEmpty())
{
for(AudioRecording recordingToDelete: recordingsToDelete)
{
mRecordingQueue.remove(recordingToDelete);
removeRecording(recordingToDelete);
}
}
}
catch(Exception e)
{
mLog.error("Error while checking audio recording queue for recordings to delete", e);
}
}
}
} }

View File

@@ -45,7 +45,8 @@ public class StreamManager implements Listener<AudioPacket>
private static AtomicInteger sNextRecordingNumber = new AtomicInteger(); private static AtomicInteger sNextRecordingNumber = new AtomicInteger();
private ThreadPoolManager mThreadPoolManager; private ThreadPoolManager mThreadPoolManager;
private AudioBroadcaster mAudioBroadcaster; private Listener<AudioRecording> mAudioRecordingListener;
private BroadcastFormat mBroadcastFormat;
private Path mTempDirectory; private Path mTempDirectory;
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>(); private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
private Runnable mRecorderMonitor; private Runnable mRecorderMonitor;
@@ -61,16 +62,18 @@ public class StreamManager implements Listener<AudioPacket>
* Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion * Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion
* *
* @param threadPoolManager for scheduling runnables * @param threadPoolManager for scheduling runnables
* @param audioBroadcaster to receive completed streamable audio recordings * @param listener to receive completed audio recordings
* @param tempDirectory where to store temporary audio recordings * @param tempDirectory where to store temporary audio recordings
*/ */
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, Path tempDirectory) public StreamManager(ThreadPoolManager threadPoolManager, Listener<AudioRecording> listener,
BroadcastFormat broadcastFormat, Path tempDirectory)
{ {
assert(tempDirectory != null && Files.isDirectory(tempDirectory)); assert(tempDirectory != null && Files.isDirectory(tempDirectory));
assert(mThreadPoolManager != null); assert(mThreadPoolManager != null);
mThreadPoolManager = threadPoolManager; mThreadPoolManager = threadPoolManager;
mAudioBroadcaster = audioBroadcaster; mAudioRecordingListener = listener;
mBroadcastFormat = broadcastFormat;
mTempDirectory = tempDirectory; mTempDirectory = tempDirectory;
} }
@@ -141,7 +144,7 @@ public class StreamManager implements Listener<AudioPacket>
else else
{ {
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(), AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat()); mBroadcastFormat);
recorder.start(mThreadPoolManager.getScheduledExecutorService()); recorder.start(mThreadPoolManager.getScheduledExecutorService());
recorder.receive(audioPacket); recorder.receive(audioPacket);
mStreamRecorders.put(sourceChannelID, recorder); mStreamRecorders.put(sourceChannelID, recorder);
@@ -178,13 +181,13 @@ public class StreamManager implements Listener<AudioPacket>
@Override @Override
public void receive(AudioRecorder audioRecorder) public void receive(AudioRecorder audioRecorder)
{ {
StreamableAudioRecording streamableAudioRecording = AudioRecording audioRecording =
new StreamableAudioRecording(audioRecorder.getPath(), audioRecorder.getMetadata(), new AudioRecording(audioRecorder.getPath(), audioRecorder.getMetadata(),
audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength()); audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength());
if(mAudioBroadcaster != null) if(mAudioRecordingListener != null)
{ {
mAudioBroadcaster.receive(streamableAudioRecording); mAudioRecordingListener.receive(audioRecording);
} }
} }
}); });
@@ -201,7 +204,7 @@ public class StreamManager implements Listener<AudioPacket>
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX); sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
sb.append(sNextRecordingNumber.incrementAndGet()).append("_"); sb.append(sNextRecordingNumber.incrementAndGet()).append("_");
sb.append( TimeStamp.getLongTimeStamp( "_" ) ); sb.append( TimeStamp.getLongTimeStamp( "_" ) );
sb.append( mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat().getFileExtension() ); sb.append( mBroadcastFormat.getFileExtension() );
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString()); Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());

View File

@@ -169,7 +169,11 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
{ {
mRecordingClosedListener = listener; mRecordingClosedListener = listener;
mRunning.set(false); if(!mRunning.compareAndSet(true, false))
{
//If this recorder has already been stopped/closed, immediately inform the listener
mRecordingClosedListener.receive(this);
}
} }