#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

@@ -9,6 +9,7 @@
<w>ident</w> <w>ident</w>
<w>polyphase</w> <w>polyphase</w>
<w>samplerate</w> <w>samplerate</w>
<w>sdrtrunk</w>
<w>shoutcast</w> <w>shoutcast</w>
<w>streamable</w> <w>streamable</w>
<w>talkgroup</w> <w>talkgroup</w>

View File

@@ -212,6 +212,7 @@ public class AliasList implements Listener<AliasEvent>
mTalkgroup.put(tgid, alias); mTalkgroup.put(tgid, alias);
} }
break; break;
case BROADCAST_CHANNEL:
case NON_RECORDABLE: case NON_RECORDABLE:
case PRIORITY: case PRIORITY:
//We don't maintain lookups for these items //We don't maintain lookups for these items

View File

@@ -23,16 +23,42 @@ import alias.id.AliasIDType;
import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlAttribute;
public class BroadcastChannel extends AliasID public class BroadcastChannel extends AliasID implements Comparable<BroadcastChannel>
{ {
private String mChannelName; private String mChannelName;
public BroadcastChannel() public BroadcastChannel()
{ {
//JAXB Constructor
}
@Override
public int compareTo(BroadcastChannel other)
{
if(mChannelName != null && other.getChannelName() != null)
{
return mChannelName.compareTo(other.getChannelName());
}
else if(mChannelName != null)
{
return -1;
}
else
{
return 1;
}
} }
/** /**
* Name of the broadcast channel configuration * Creates a named broadcast channel
*/
public BroadcastChannel(String channelName)
{
mChannelName = channelName;
}
/**
* Name of the broadcastAudio channel configuration
*/ */
@XmlAttribute(name="channel") @XmlAttribute(name="channel")
public String getChannelName() public String getChannelName()
@@ -41,7 +67,7 @@ public class BroadcastChannel extends AliasID
} }
/** /**
* Sets the name of the broadcast channel configuration * Sets the name of the broadcastAudio channel configuration
*/ */
public void setChannelName(String channel) public void setChannelName(String channel)
{ {

View File

@@ -19,12 +19,15 @@
package alias.id.broadcast; package alias.id.broadcast;
import alias.id.AliasID; import alias.id.AliasID;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastModel; import audio.broadcast.BroadcastModel;
import gui.editor.DocumentListenerEditor; import gui.editor.DocumentListenerEditor;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.util.List; import java.util.List;
@@ -40,16 +43,14 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
+ "streaming server"; + "streaming server";
private JComboBox<String> mBroadcastConfigurations; private JComboBox<String> mBroadcastConfigurations;
private BroadcastModel mBroadcastModel;
public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel) public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel)
{ {
List<String> channelNames = broadcastModel.getBroadcastConfigurationNames(); mBroadcastModel = broadcastModel;
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
setItem( aliasID );
initGUI(); initGUI();
setItem( aliasID );
} }
private void initGUI() private void initGUI()
@@ -58,15 +59,20 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
add( new JLabel( "Broadcast Channel" ), "grow" ); add( new JLabel( "Broadcast Channel" ), "grow" );
List<String> channelNames = mBroadcastModel.getBroadcastConfigurationNames();
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
mBroadcastConfigurations.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mBroadcastConfigurations, "grow"); add(mBroadcastConfigurations, "grow");
BroadcastChannel channel = (BroadcastChannel)getItem();
if(channel.isValid())
{
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
}
JLabel help = new JLabel( "Help ..." ); JLabel help = new JLabel( "Help ..." );
help.setForeground( Color.BLUE.brighter() ); help.setForeground( Color.BLUE.brighter() );
help.setCursor( new Cursor( Cursor.HAND_CURSOR ) ); help.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
@@ -83,9 +89,41 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
add( help, "align left" ); add( help, "align left" );
} }
@Override
public void setItem(AliasID item)
{
super.setItem(item);
if(item instanceof BroadcastChannel)
{
BroadcastChannel channel = (BroadcastChannel)item;
if(channel.isValid())
{
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
}
}
setModified(false);
}
@Override @Override
public void save() public void save()
{ {
((BroadcastChannel)getItem()).setChannelName((String)mBroadcastConfigurations.getSelectedItem()); String channel = (String)mBroadcastConfigurations.getSelectedItem();
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getBroadcastConfiguration(channel);
if(selectedConfiguration != null)
{
((BroadcastChannel)getItem()).setChannelName(selectedConfiguration.getName());
}
else
{
((BroadcastChannel)getItem()).setChannelName(null);
}
} }
} }

View File

@@ -1,39 +0,0 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast;
import audio.AudioPacket;
import controller.ThreadPoolManager;
import sample.Listener;
public class AudioBroadcastManager implements Listener<AudioPacket>
{
private ThreadPoolManager mThreadPoolManager;
public AudioBroadcastManager(ThreadPoolManager threadPoolManager)
{
mThreadPoolManager = threadPoolManager;
}
@Override
public void receive(AudioPacket audioPacket)
{
}
}

View File

@@ -0,0 +1,425 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
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.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AudioBroadcaster implements IAudioPacketListener, Listener<AudioPacket>
{
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 byte[] mSilenceFrame;
private int mStreamedAudioCount = 0;
private Listener<BroadcastEvent> mBroadcastEventListener;
private BroadcastState mBroadcastState = BroadcastState.READY;
private BroadcastConfiguration mBroadcastConfiguration;
private boolean mEnabled = true;
/**
* AudioBroadcaster for sending audio to a remote broadcastAudio server.
*/
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);
//Create a 1 second silence frame - from 1200 millis of silence
mSilenceFrame = BroadcastFactory.getSilenceFrame(getBroadcastConfiguration().getBroadcastFormat(), 1200);
mStreamManager = new StreamManager(threadPoolManager, this, broadcastConfiguration.getDelay(),
SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY));
mStreamManager.start();
startProcessor();
}
/**
* Recording backlog awaiting streaming
*/
public int getQueueSize()
{
return mAudioRecordingQueue.size();
}
/**
* Number of audio recordings streamed to remote server
*/
public int getStreamedAudioCount()
{
return mStreamedAudioCount;
}
/**
* 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.
*
* @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));
}
}
/**
* Removes the temporary recording file from disk.
*
* @param recording to remove
*/
private void removeRecording(StreamableAudioRecording recording)
{
try
{
Files.delete(recording.getPath());
}
catch(IOException ioe)
{
mLog.error("Error deleting temporary internet recording file: " + recording.getPath().toString());
}
}
/**
* Broadcast configuration used by this broadcaster
*/
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
/**
* IAudioPacketListener interface method
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
}
/**
* Broadcasts the audio binary data.
*/
protected abstract void broadcastAudio(byte[] audio);
/**
* Broadcasts the next song's audio metadata prior to streaming the next song.
* @param metadata
*/
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
*/
public void setListener(Listener<BroadcastEvent> listener)
{
mBroadcastEventListener = listener;
}
/**
* Removes the listener from receiving broadcastAudio state changes
*/
public void removeListener()
{
mBroadcastEventListener = null;
}
/**
* Broadcasts the event to any registered listener
*/
public void broadcast(BroadcastEvent event)
{
if(mBroadcastEventListener != null)
{
mBroadcastEventListener.receive(event);
}
}
/**
* Sets the state of the broadcastAudio connection
*/
protected void setBroadcastState(BroadcastState state)
{
if(mBroadcastState != state)
{
mLog.debug("Changing State to: " + state);
mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
}
}
/**
* Current state of the broadcastAudio connection
*/
public BroadcastState getBroadcastState()
{
return mBroadcastState;
}
public boolean connected()
{
return getBroadcastState() == BroadcastState.CONNECTED;
}
/**
* Indicates if this broadcaster can connect and is not currently in an error state or a connected state.
*/
public boolean canConnect()
{
BroadcastState state = getBroadcastState();
return state != BroadcastState.CONNECTED && !state.isErrorState();
}
/**
* Audio recording queue processor. Fetches recordings from the queue and chunks the recording byte content
* to subclass implementations for broadcast in the appropriate manner.
*/
public class RecordingQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private ByteArrayInputStream mInputStream;
private int mChunkSize;
@Override
public void run()
{
if(mProcessing.compareAndSet(false, true))
{
if(mInputStream == null || mInputStream.available() <= 0)
{
nextRecording();
}
if(mInputStream != null)
{
int length = Math.min(mChunkSize, mInputStream.available());
byte[] audio = new byte[length];
try
{
mInputStream.read(audio);
broadcastAudio(audio);
}
catch(IOException ioe)
{
mLog.error("Error reading from in-memory audio recording input stream", ioe);
}
}
else
{
broadcastAudio(mSilenceFrame);
}
mProcessing.set(false);
}
}
/**
* Loads the next recording for broadcast
*/
private void nextRecording()
{
if(mInputStream != null)
{
mStreamedAudioCount++;
broadcast(new BroadcastEvent(AudioBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
}
mInputStream = null;
StreamableAudioRecording recording = mAudioRecordingQueue.poll();
if(recording != null)
{
try
{
byte[] audio = Files.readAllBytes(recording.getPath());
if(audio != null && audio.length > 0)
{
mInputStream = new ByteArrayInputStream(audio);
int wholeIntervalChunks = (int)(recording.getRecordingLength() / PROCESSOR_RUN_INTERVAL_MS);
//Check for divide by zero situation
if(wholeIntervalChunks == 0)
{
wholeIntervalChunks = 1;
}
mChunkSize = (int)(mInputStream.available() / wholeIntervalChunks) + 1;
broadcastMetadata(recording.getAudioMetadata());
}
else
{
broadcastMetadata(null);
}
}
catch(IOException ioe)
{
mLog.error("Error reading temporary audio stream recording [" + recording.getPath().toString() +
"] - skipping recording");
mInputStream = null;
}
removeRecording(recording);
broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
else
{
broadcastMetadata(null);
}
}
}
}

View File

@@ -24,6 +24,7 @@ import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
@@ -31,17 +32,24 @@ import java.net.SocketAddress;
@XmlRootElement(name = "stream") @XmlRootElement(name = "stream")
public abstract class BroadcastConfiguration public abstract class BroadcastConfiguration
{ {
private BroadcastFormat mBroadcastFormat; private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
private String mName; private String mName;
private String mHost; private String mHost;
private int mPort; private int mPort;
private String mPassword; private String mPassword;
private long mDelay;
private boolean mEnabled = true;
public BroadcastConfiguration() public BroadcastConfiguration()
{ {
//No-arg constructor required for JAXB //No-arg constructor required for JAXB
} }
/**
* Creates a copy/clone of the configuration
*/
public abstract BroadcastConfiguration copyOf();
/** /**
* Broadcast audio streaming configuration. Describes the streaming server and configuration details. * Broadcast audio streaming configuration. Describes the streaming server and configuration details.
*/ */
@@ -57,7 +65,7 @@ public abstract class BroadcastConfiguration
public abstract BroadcastServerType getBroadcastServerType(); public abstract BroadcastServerType getBroadcastServerType();
/** /**
* Name identifying this broadcast configuration. * Name identifying this broadcastAudio configuration.
*/ */
@XmlAttribute(name = "name") @XmlAttribute(name = "name")
public String getName() public String getName()
@@ -120,7 +128,7 @@ public abstract class BroadcastConfiguration
} }
/** /**
* Port number of the streaming server to where the broadcast will be directed. * Port number of the streaming server to where the broadcastAudio will be directed.
* *
* @param port * @param port
*/ */
@@ -152,7 +160,7 @@ public abstract class BroadcastConfiguration
} }
/** /**
* Sets the password used to authenticate the broadcast broadcast user to the streaming server * Sets the password used to authenticate the broadcastAudio broadcastAudio user to the streaming server
* *
* @param password * @param password
*/ */
@@ -170,11 +178,95 @@ public abstract class BroadcastConfiguration
} }
/** /**
* Audio broadcast content type (e.g. mp3) * Audio broadcastAudio content type (e.g. mp3)
*/ */
@XmlElement(name = "format") @XmlAttribute(name = "format")
public BroadcastFormat getBroadcastFormat() public BroadcastFormat getBroadcastFormat()
{ {
return mBroadcastFormat; return mBroadcastFormat;
} }
public void setBroadcastFormat(BroadcastFormat format)
{
mBroadcastFormat = format;
}
/**
* Audio broadcastAudio delay from recording start until the audio file is broadcastAudio to the server.
*/
@XmlAttribute(name = "delay")
public long getDelay()
{
return mDelay;
}
/**
* User specified delay in milli-seconds from audio call start until when the audio is broadcastAudio to the server.
* @param delay in milliseconds
*/
public void setDelay(long delay)
{
mDelay = delay;
}
/**
* Indicates if this broadcaster is enable, meaning that it will automatically connect on startup.
*/
@XmlAttribute(name = "enabled")
public boolean isEnabled()
{
return mEnabled;
}
public void setEnabled(boolean enabled)
{
mEnabled = enabled;
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof BroadcastConfiguration))
{
return false;
}
BroadcastConfiguration that = (BroadcastConfiguration) o;
if (getBroadcastFormat() != that.getBroadcastFormat())
{
return false;
}
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null)
{
return false;
}
return getBroadcastServerType() == that.getBroadcastServerType();
}
@Override
public int hashCode()
{
int result = getBroadcastFormat().hashCode();
result = 31 * result + (getName() != null ? getName().hashCode() : 0);
result = 31 * result + getBroadcastServerType().hashCode();
return result;
}
/**
* Indicates if this configuration is valid. A minimal check will ensure that it contains at least a hostname and
* port number.
*/
@XmlTransient
public boolean isValid()
{
return mHost != null && mPort > 0;
}
} }

View File

@@ -18,9 +18,10 @@
******************************************************************************/ ******************************************************************************/
package audio.broadcast; package audio.broadcast;
public class BroadcastConfigurationEvent public class BroadcastEvent
{ {
private BroadcastConfiguration mBroadcastConfiguration; private BroadcastConfiguration mBroadcastConfiguration;
private AudioBroadcaster mAudioBroadcaster;
private Event mEvent; private Event mEvent;
/** /**
@@ -28,17 +29,44 @@ public class BroadcastConfigurationEvent
* @param configuration - alias that changed * @param configuration - alias that changed
* @param event - change event * @param event - change event
*/ */
public BroadcastConfigurationEvent(BroadcastConfiguration configuration, Event event ) public BroadcastEvent(BroadcastConfiguration configuration, Event event )
{ {
mBroadcastConfiguration = configuration; mBroadcastConfiguration = configuration;
mEvent = event; mEvent = event;
} }
public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event )
{
mAudioBroadcaster = audioBroadcaster;
mEvent = event;
}
public BroadcastConfiguration getBroadcastConfiguration() public BroadcastConfiguration getBroadcastConfiguration()
{ {
return mBroadcastConfiguration; return mBroadcastConfiguration;
} }
public AudioBroadcaster getAudioBroadcaster()
{
return mAudioBroadcaster;
}
/**
* Indicates if this event pertains to an audio broadcaster
*/
public boolean isAudioBroadcasterEvent()
{
return mAudioBroadcaster != null;
}
/**
* Indicates if this event pertains to a broadcast configuration
*/
public boolean isBroadcastConfigurationEvent()
{
return mBroadcastConfiguration != null;
}
public Event getEvent() public Event getEvent()
{ {
return mEvent; return mEvent;
@@ -49,8 +77,14 @@ public class BroadcastConfigurationEvent
*/ */
public enum Event public enum Event
{ {
ADD, BROADCASTER_ADD,
CHANGE, BROADCASTER_QUEUE_CHANGE,
DELETE; BROADCASTER_STATE_CHANGE,
BROADCASTER_STREAMED_COUNT_CHANGE,
BROADCASTER_DELETE,
CONFIGURATION_ADD,
CONFIGURATION_CHANGE,
CONFIGURATION_DELETE;
} }
} }

View File

@@ -18,27 +18,32 @@
******************************************************************************/ ******************************************************************************/
package audio.broadcast; package audio.broadcast;
import audio.AudioPacket;
import audio.broadcast.broadcastify.BroadcastifyConfiguration; import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor; import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
import audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import audio.broadcast.icecast.IcecastHTTPConfiguration; import audio.broadcast.icecast.IcecastHTTPConfiguration;
import audio.broadcast.icecast.IcecastHTTPBroadcaster; import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
import audio.broadcast.icecast.IcecastTCPBroadcaster;
import audio.broadcast.icecast.IcecastTCPConfiguration; import audio.broadcast.icecast.IcecastTCPConfiguration;
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration; import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v1.ShoutcastV1Broadcaster; import audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration; import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
import audio.broadcast.shoutcast.v2.ShoutcastV2Broadcaster; import audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
import audio.convert.IAudioConverter; import audio.convert.IAudioConverter;
import audio.convert.MP3AudioConverter; import audio.convert.MP3AudioConverter;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import gui.editor.Editor; import gui.editor.Editor;
import gui.editor.EmptyEditor; import gui.editor.EmptyEditor;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import record.AudioRecorder; import record.AudioRecorder;
import record.mp3.MP3Recorder; import record.mp3.MP3Recorder;
import settings.SettingsManager;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public class BroadcastFactory public class BroadcastFactory
{ {
@@ -54,34 +59,35 @@ public class BroadcastFactory
* @param configuration describing the server and audio types * @param configuration describing the server and audio types
* @return configured broadcaster or null * @return configured broadcaster or null
*/ */
public static Broadcaster getBroadcaster(ThreadPoolManager threadPoolManager, public static AudioBroadcaster getBroadcaster(DefaultAsyncHttpClient defaultAsyncHttpClient,
BroadcastConfiguration configuration) ThreadPoolManager threadPoolManager,
BroadcastConfiguration configuration)
{ {
IAudioConverter converter = getAudioConverter(configuration); if(configuration != null)
if(converter != null)
{ {
switch(configuration.getBroadcastServerType()) IAudioConverter converter = getAudioConverter(configuration);
if(converter != null)
{ {
case BROADCASTIFY: switch(configuration.getBroadcastServerType())
return new IcecastTCPBroadcaster(threadPoolManager, {
(BroadcastifyConfiguration)configuration, converter); case BROADCASTIFY:
case ICECAST_HTTP: return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
return new IcecastHTTPBroadcaster(threadPoolManager, (BroadcastifyConfiguration)configuration);
(IcecastHTTPConfiguration)configuration, converter); case ICECAST_TCP:
case ICECAST_TCP: return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
return new IcecastTCPBroadcaster(threadPoolManager, (IcecastTCPConfiguration)configuration);
(IcecastTCPConfiguration)configuration, converter); case ICECAST_HTTP:
case SHOUTCAST_V1: return new IcecastHTTPAudioBroadcaster(threadPoolManager, (IcecastHTTPConfiguration)configuration);
return new ShoutcastV1Broadcaster(threadPoolManager, case SHOUTCAST_V1:
(ShoutcastV1Configuration)configuration, converter); return new ShoutcastV1AudioBroadcaster(threadPoolManager, (ShoutcastV1Configuration)configuration);
case SHOUTCAST_V2: case SHOUTCAST_V2:
return new ShoutcastV2Broadcaster(threadPoolManager, return new ShoutcastV2AudioBroadcaster(threadPoolManager, (ShoutcastV2Configuration)configuration);
(ShoutcastV2Configuration)configuration, converter); case UNKNOWN:
case UNKNOWN: default:
default: mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
mLog.info("Unrecognized broadcast configuration: " + configuration.getBroadcastFormat().name()); break;
break; }
} }
} }
@@ -100,14 +106,14 @@ public class BroadcastFactory
case MP3: case MP3:
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE); return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
default: default:
mLog.info("Unrecognized broadcast format: " + configuration.getBroadcastFormat().name()); mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
} }
return null; return null;
} }
/** /**
* Creates a broadcast configuration for the specified server type and format * Creates a broadcastAudio configuration for the specified server type and format
* *
* @param serverType for the configuration * @param serverType for the configuration
* @param format for the output audio format * @param format for the output audio format
@@ -129,7 +135,7 @@ public class BroadcastFactory
return new ShoutcastV2Configuration(format); return new ShoutcastV2Configuration(format);
case UNKNOWN: case UNKNOWN:
default: default:
mLog.info("Unrecognized broadcast server type: " + serverType.name()); mLog.info("Unrecognized broadcastAudio server type: " + serverType.name());
break; break;
} }
@@ -137,20 +143,21 @@ public class BroadcastFactory
} }
/** /**
* Constructs an editor for the specified broadcast configuration * Constructs an editor for the specified broadcastAudio configuration
* @param configuration to modify or view * @param configuration to modify or view
* @param broadcastModel model for broadcast configurations * @param broadcastModel model for broadcastAudio configurations
* @return an editor for the specified broadcast configuration * @return an editor for the specified broadcastAudio configuration
*/ */
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration, public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
BroadcastModel broadcastModel) BroadcastModel broadcastModel,
SettingsManager settingsManager)
{ {
Editor<BroadcastConfiguration> editor; Editor<BroadcastConfiguration> editor;
switch (configuration.getBroadcastServerType()) switch (configuration.getBroadcastServerType())
{ {
case BROADCASTIFY: case BROADCASTIFY:
editor = new BroadcastifyConfigurationEditor(broadcastModel); editor = new BroadcastifyConfigurationEditor(broadcastModel, settingsManager);
break; break;
default: default:
editor = new EmptyEditor<BroadcastConfiguration>(); editor = new EmptyEditor<BroadcastConfiguration>();
@@ -163,7 +170,7 @@ public class BroadcastFactory
} }
/** /**
* Creates an audio recorder for the specified broadcast format using the specified path output file name * Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
*/ */
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat) public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
{ {
@@ -172,8 +179,28 @@ public class BroadcastFactory
case MP3: case MP3:
return new MP3Recorder(path); return new MP3Recorder(path);
default: default:
mLog.debug("Unrecognized broadcast format [" + broadcastFormat + "] cannot create audio recorder"); mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
return null; return null;
} }
} }
public static byte[] getSilenceFrame(BroadcastFormat format, long duration)
{
int length = (int)(duration * 8); //8000 Hz sample rate
float[] silence = new float[length];
AudioPacket silencePacket = new AudioPacket(silence, null);
List<AudioPacket> silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
switch(format)
{
case MP3:
MP3AudioConverter converter = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE,
MP3Recorder.CONSTANT_BIT_RATE);
return converter.convert(silencePackets);
default:
throw new IllegalArgumentException("Unrecognized broadcast format: " + format.name());
}
}
} }

View File

@@ -28,6 +28,7 @@ public enum BroadcastFormat
private BroadcastFormat(String value, String fileExtension) private BroadcastFormat(String value, String fileExtension)
{ {
mValue = value; mValue = value;
mFileExtension = fileExtension;
} }
public String getValue() public String getValue()

View File

@@ -18,28 +18,70 @@
******************************************************************************/ ******************************************************************************/
package audio.broadcast; package audio.broadcast;
import alias.id.broadcast.BroadcastChannel;
import audio.AudioPacket;
import controller.ThreadPoolManager;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Broadcaster;
import sample.Listener; import sample.Listener;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.table.AbstractTableModel; import javax.swing.table.AbstractTableModel;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BroadcastModel extends AbstractTableModel public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
{ {
private static final int COLUMN_CHANNEL_NAME = 0; private final static Logger mLog = LoggerFactory.getLogger( BroadcastModel.class );
private static final int COLUMN_SERVER_TYPE = 1;
private static final int COLUMN_SERVER_ADDRESS = 2; public static final String TEMPORARY_STREAM_DIRECTORY = "streaming";
public static final String TEMPORARY_STREAM_FILE_SUFFIX = "temporary_streaming_file_";
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
private static final int COLUMN_SERVER_ICON = 0;
private static final int COLUMN_STREAM_NAME = 1;
private static final int COLUMN_BROADCASTER_STATUS = 2;
private static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
private static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>(); private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>();
private sample.Broadcaster<BroadcastConfigurationEvent> mBroadcastEventBroadcaster = new sample.Broadcaster<>();
public BroadcastModel() private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
private ThreadPoolManager mThreadPoolManager;
private SettingsManager mSettingsManager;
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
/**
* Model for managing Broadcast configurations and any associated broadcaster instances.
*/
public BroadcastModel(ThreadPoolManager threadPoolManager, SettingsManager settingsManager)
{ {
mThreadPoolManager = threadPoolManager;
mSettingsManager = settingsManager;
removeOrphanedTemporaryRecordings();
} }
/** /**
* List of broadcast configuration names * List of broadcastAudio configuration names
*/ */
public List<String> getBroadcastConfigurationNames() public List<String> getBroadcastConfigurationNames()
{ {
@@ -54,7 +96,7 @@ public class BroadcastModel extends AbstractTableModel
} }
/** /**
* Current list of broadcast configurations * Current list of broadcastAudio configurations
*/ */
public List<BroadcastConfiguration> getBroadcastConfigurations() public List<BroadcastConfiguration> getBroadcastConfigurations()
{ {
@@ -62,7 +104,7 @@ public class BroadcastModel extends AbstractTableModel
} }
/** /**
* Adds the list of broadcast configurations to this model * Adds the list of broadcastAudio configurations to this model
*/ */
public void addBroadcastConfigurations(List<BroadcastConfiguration> configurations) public void addBroadcastConfigurations(List<BroadcastConfiguration> configurations)
{ {
@@ -73,58 +115,278 @@ public class BroadcastModel extends AbstractTableModel
} }
/** /**
* Adds the broadcast configuration to this model * Adds the broadcastAudio configuration to this model
*/ */
public void addBroadcastConfiguration(BroadcastConfiguration configuration) public void addBroadcastConfiguration(BroadcastConfiguration configuration)
{ {
if(configuration != null) if(configuration != null)
{ {
mBroadcastConfigurations.add(configuration); ensureUniqueName(configuration);
int index = mBroadcastConfigurations.size() - 1; if(!mBroadcastConfigurations.contains(configuration))
{
mBroadcastConfigurations.add(configuration);
fireTableRowsInserted( index, index ); int index = mBroadcastConfigurations.size() - 1;
broadcast( new BroadcastConfigurationEvent( configuration, BroadcastConfigurationEvent.Event.ADD ) ); fireTableRowsInserted( index, index );
}
}
public void removeBroadcastConfiguration(BroadcastConfiguration configuration) mBroadcastConfigurationMap.put(configuration.getName(), configuration);
{
if(configuration != null && mBroadcastConfigurations.contains(configuration))
{
int index = mBroadcastConfigurations.indexOf(configuration);
mBroadcastConfigurations.remove(configuration); process( new BroadcastEvent( configuration, BroadcastEvent.Event.CONFIGURATION_ADD) );
}
fireTableRowsDeleted( index, index );
broadcast( new BroadcastConfigurationEvent( configuration, BroadcastConfigurationEvent.Event.DELETE ) );
} }
} }
/** /**
* Registers the listener to receive broadcast configuration events * Clones the configuration and adds it this model with a unique configuration name
*/ */
public void addListener(Listener<BroadcastConfigurationEvent> listener) public BroadcastConfiguration cloneBroadcastConfiguration(BroadcastConfiguration configuration)
{
if(configuration != null)
{
BroadcastConfiguration clone = configuration.copyOf();
addBroadcastConfiguration(clone);
return clone;
}
return null;
}
/**
* Updates the configuration's name so that it is unique among all other broadcast configurations
* @param configuration
*/
private void ensureUniqueName(BroadcastConfiguration configuration)
{
if(configuration.getName() == null || configuration.getName().isEmpty())
{
configuration.setName("New Configuration");
}
while(!isUniqueName(configuration.getName(), configuration))
{
String currentName = configuration.getName();
if(currentName.matches(UNIQUE_NAME_REGEX))
{
int currentVersion = 1;
StringBuilder sb = new StringBuilder();
Matcher m = Pattern.compile(UNIQUE_NAME_REGEX).matcher(currentName);
if(m.find())
{
String version = m.group(2);
try
{
currentVersion = Integer.parseInt(version);
}
catch(Exception e)
{
//Couldn't parse the version number -- keep incrementing until we find a winner
}
currentVersion++;
sb.append(m.group(1)).append("(").append(currentVersion).append(")");
}
else
{
sb.append(configuration.getName()).append("(").append(currentVersion).append(")");
}
configuration.setName(sb.toString());
}
else
{
StringBuilder sb = new StringBuilder();
sb.append(configuration.getName()).append("(2)");
configuration.setName(sb.toString());
}
}
}
/**
* Indicates if the name is unique among all of the broadcast configurations. Checks each of the configurations
* in this model for a name collision. Assumes that the name argument will be assigned to the configuration
* argument if the name is unique, therefore, the name will not be checked against the configuration argument for
* a collision.
*
* @param name to check for uniqueness
* @param configuration to ignore when checking the name for uniqueness
* @return true if the name is not null and unique among all configurations managed by this model
*/
public boolean isUniqueName(String name, BroadcastConfiguration configuration)
{
if(name == null || name.isEmpty())
{
return false;
}
for(BroadcastConfiguration configurationToCompare: mBroadcastConfigurations)
{
if(configurationToCompare != configuration && configurationToCompare.getName().equals(name))
{
return false;
}
}
return true;
}
public void removeBroadcastConfiguration(BroadcastConfiguration broadcastConfiguration)
{
if(broadcastConfiguration != null && mBroadcastConfigurations.contains(broadcastConfiguration))
{
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
mBroadcastConfigurations.remove(broadcastConfiguration);
mBroadcastConfigurationMap.remove(broadcastConfiguration.getName());
broadcast( new BroadcastEvent( broadcastConfiguration, BroadcastEvent.Event.CONFIGURATION_DELETE) );
fireTableRowsDeleted( index, index );
}
}
/**
* Returns the broadcaster associated with the stream name or null if there is no broadcaster setup for the name.
*/
public AudioBroadcaster getBroadcaster(String streamName)
{
return mBroadcasterMap.get(streamName);
}
@Override
public void receive(AudioPacket audioPacket)
{
if (audioPacket.hasAudioMetadata() && audioPacket.getAudioMetadata().isStreamable())
{
for (BroadcastChannel broadcastChannel : audioPacket.getAudioMetadata().getBroadcastChannels())
{
String channelName = broadcastChannel.getChannelName();
if (channelName != null)
{
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
if (audioBroadcaster != null)
{
audioBroadcaster.receive(audioPacket);
}
}
}
}
}
/**
* Creates a new broadcaster for the broadcast configuration and adds it to the model
*/
private void createBroadcaster(BroadcastConfiguration broadcastConfiguration)
{
if (broadcastConfiguration != null &&
broadcastConfiguration.isEnabled() &&
broadcastConfiguration.isValid() &&
!mBroadcasterMap.containsKey(broadcastConfiguration.getName()))
{
AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(getDefaultAsyncHttpClient(),
mThreadPoolManager, broadcastConfiguration);
if (audioBroadcaster != null)
{
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
{
process(broadcastEvent);
}
});
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
{
fireTableRowsUpdated(index, index);
}
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
}
}
}
/**
* Shut down a broadcaster created from the configuration and remove it from this model
*/
private void deleteBroadcaster(String name)
{
if(name != null && mBroadcasterMap.containsKey(name))
{
AudioBroadcaster audioBroadcaster = mBroadcasterMap.remove(name);
if(audioBroadcaster != null)
{
audioBroadcaster.stop();
audioBroadcaster.removeListener();
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
{
fireTableRowsUpdated(index, index);
}
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
}
}
}
private DefaultAsyncHttpClient getDefaultAsyncHttpClient()
{
if (mDefaultAsyncHttpClient == null)
{
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
}
return mDefaultAsyncHttpClient;
}
/**
* Returns the broadcast configuration identified by the stream name
*/
public BroadcastConfiguration getBroadcastConfiguration(String streamName)
{
return mBroadcastConfigurationMap.get(streamName);
}
/**
* Registers the listener to receive broadcastAudio configuration events
*/
public void addListener(Listener<BroadcastEvent> listener)
{ {
mBroadcastEventBroadcaster.addListener(listener); mBroadcastEventBroadcaster.addListener(listener);
} }
/** /**
* Removes the listener from receiving broadcast configuration events * Removes the listener from receiving broadcastAudio configuration events
*/ */
public void removeListener(Listener<BroadcastConfigurationEvent> listener) public void removeListener(Listener<BroadcastEvent> listener)
{ {
mBroadcastEventBroadcaster.removeListener(listener); mBroadcastEventBroadcaster.removeListener(listener);
} }
/** /**
* Broadcasts the broadcast configuration change event * Broadcasts the broadcastAudio configuration change event
*/ */
public void broadcast( BroadcastConfigurationEvent event ) public void broadcast( BroadcastEvent event )
{ {
if( event.getEvent() == BroadcastConfigurationEvent.Event.CHANGE ) if( event.getEvent() == BroadcastEvent.Event.CONFIGURATION_CHANGE)
{ {
int index = mBroadcastConfigurations.indexOf( event.getBroadcastConfiguration() ); int index = mBroadcastConfigurations.indexOf( event.getBroadcastConfiguration() );
@@ -134,6 +396,88 @@ public class BroadcastModel extends AbstractTableModel
mBroadcastEventBroadcaster.broadcast( event ); mBroadcastEventBroadcaster.broadcast( event );
} }
/**
* Process a broadcast event from one of the broadcasters managed by this model
*/
public void process(BroadcastEvent broadcastEvent)
{
if(broadcastEvent.isBroadcastConfigurationEvent())
{
switch(broadcastEvent.getEvent())
{
case CONFIGURATION_ADD:
createBroadcaster(broadcastEvent.getBroadcastConfiguration());
break;
case CONFIGURATION_CHANGE:
//Delete and recreate the broadcaster for any broadcast configuration changes
BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
cleanupMapAssociations(broadcastConfiguration);
createBroadcaster(broadcastConfiguration);
break;
case CONFIGURATION_DELETE:
deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getName());
break;
}
}
else if(broadcastEvent.isAudioBroadcasterEvent())
{
int row = mBroadcastConfigurations.indexOf(broadcastEvent.getAudioBroadcaster().getBroadcastConfiguration());
switch(broadcastEvent.getEvent())
{
case BROADCASTER_QUEUE_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_QUEUE_SIZE);
}
break;
case BROADCASTER_STATE_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_STATUS);
}
break;
case BROADCASTER_STREAMED_COUNT_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
}
break;
}
}
//Rebroadcast the event to any listeners of this model
broadcast(broadcastEvent);
}
/**
* When the name of a broadcast configuration changes, remove the old map associations so that they can be replaced
* with the new map associations. Remove any broadcaster that is associated with the old configuration name.
*
* @param broadcastConfiguration
*/
private void cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
{
String oldName = null;
for(Map.Entry<String,BroadcastConfiguration> entry: mBroadcastConfigurationMap.entrySet())
{
if(entry.getValue() == broadcastConfiguration)
{
oldName = entry.getKey();
continue;
}
}
if(oldName != null)
{
deleteBroadcaster(oldName);
mBroadcastConfigurationMap.remove(oldName);
}
mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
}
@Override @Override
public int getRowCount() public int getRowCount()
{ {
@@ -143,7 +487,7 @@ public class BroadcastModel extends AbstractTableModel
@Override @Override
public int getColumnCount() public int getColumnCount()
{ {
return 3; return 5;
} }
@Override @Override
@@ -157,12 +501,47 @@ public class BroadcastModel extends AbstractTableModel
{ {
switch(columnIndex) switch(columnIndex)
{ {
case COLUMN_CHANNEL_NAME: case COLUMN_SERVER_ICON:
String iconName = configuration.getBroadcastServerType().getIconName();
if(iconName != null && mSettingsManager != null)
{
return mSettingsManager.getImageIcon(iconName, 14);
}
break;
case COLUMN_STREAM_NAME:
return configuration.getName(); return configuration.getName();
case COLUMN_SERVER_TYPE: case COLUMN_BROADCASTER_STATUS:
return configuration.getBroadcastServerType().toString(); AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
case COLUMN_SERVER_ADDRESS:
return configuration.getHost(); if(audioBroadcasterA != null)
{
return audioBroadcasterA.getBroadcastState().toString();
}
else if(!configuration.isEnabled())
{
return "Disabled";
}
else if(!configuration.isValid())
{
return "Invalid Settings";
}
case COLUMN_BROADCASTER_QUEUE_SIZE:
AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterB != null)
{
return audioBroadcasterB.getQueueSize();
}
break;
case COLUMN_BROADCASTER_STREAMED_COUNT:
AudioBroadcaster audioBroadcasterC = mBroadcasterMap.get(configuration.getName());
if(audioBroadcasterC != null)
{
return audioBroadcasterC.getStreamedAudioCount();
}
break;
default: default:
break; break;
} }
@@ -172,6 +551,17 @@ public class BroadcastModel extends AbstractTableModel
return null; return null;
} }
@Override
public Class<?> getColumnClass(int columnIndex)
{
if(columnIndex == COLUMN_SERVER_ICON)
{
return ImageIcon.class;
}
return String.class;
}
/** /**
* Broadcast configuration at the specified model row * Broadcast configuration at the specified model row
*/ */
@@ -193,14 +583,60 @@ public class BroadcastModel extends AbstractTableModel
{ {
switch(column) switch(column)
{ {
case COLUMN_CHANNEL_NAME: case COLUMN_SERVER_ICON:
return "Channel"; return null;
case COLUMN_SERVER_TYPE: case COLUMN_STREAM_NAME:
return "Server"; return "Name";
case COLUMN_SERVER_ADDRESS: case COLUMN_BROADCASTER_STATUS:
return "Address"; return "Status";
case COLUMN_BROADCASTER_QUEUE_SIZE:
return "Queued";
case COLUMN_BROADCASTER_STREAMED_COUNT:
return "Streamed";
} }
return null; return null;
} }
/**
* Removes any temporary stream recordings left-over from the previous application run.
*
* This should only be invoked on startup.
*/
private void removeOrphanedTemporaryRecordings()
{
Path path = SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY);
if(path != null && Files.isDirectory(path))
{
StringBuilder sb = new StringBuilder();
sb.append(TEMPORARY_STREAM_FILE_SUFFIX);
sb.append("*.*");
try(DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path, sb.toString()))
{
directoryStream.forEach(new Consumer<Path>()
{
@Override
public void accept(Path path)
{
mLog.info("Deleting orphaned temporary stream recording: " + path.toString());
try
{
Files.delete(path);
}
catch(IOException ioe)
{
mLog.error("Couldn't delete orphaned temporary recording: " + path.toString(), ioe);
}
}
});
}
catch(IOException ioe)
{
mLog.error("Error discovering orphaned temporary stream recording files", ioe);
}
}
}
} }

View File

@@ -18,15 +18,13 @@
******************************************************************************/ ******************************************************************************/
package audio.broadcast; package audio.broadcast;
import alias.Alias;
import alias.AliasFactory;
import alias.id.AliasID;
import alias.id.AliasIDType;
import alias.id.AliasIdentifierEditor;
import com.jidesoft.swing.JideSplitPane; import com.jidesoft.swing.JideSplitPane;
import gui.editor.Editor; import gui.editor.Editor;
import gui.editor.EmptyEditor; import gui.editor.EmptyEditor;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionEvent;
@@ -38,22 +36,27 @@ import java.awt.event.MouseEvent;
public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
{ {
private static final String NEW_ALIAS = "New"; private final static Logger mLog = LoggerFactory.getLogger( BroadcastPanel.class );
private static final String COPY_ALIAS = "Copy";
private static final String DELETE_ALIAS = "Delete"; private static final String NEW_BROADCAST_CONFIGURATION = "New";
private static final String COPY_BROADCAST_CONFIGURATION = "Copy";
private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
private BroadcastModel mBroadcastModel; private BroadcastModel mBroadcastModel;
private SettingsManager mSettingsManager;
private JTable mBroadcastConfigurationTable; private JTable mBroadcastConfigurationTable;
private JideSplitPane mSplitPane; private JideSplitPane mSplitPane;
private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:"); private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:");
private Editor<BroadcastConfiguration> mEditor; private Editor<BroadcastConfiguration> mEditor;
private JButton mNewButton = new JButton(NEW_ALIAS); private JButton mNewButton = new JButton(NEW_BROADCAST_CONFIGURATION);
private JButton mCopyButton = new JButton(COPY_ALIAS); private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
private JButton mDeleteButton = new JButton(DELETE_ALIAS); private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
public BroadcastPanel(BroadcastModel broadcastModel) public BroadcastPanel(BroadcastModel broadcastModel, SettingsManager settingManager)
{ {
mBroadcastModel = broadcastModel; mBroadcastModel = broadcastModel;
mSettingsManager = settingManager;
mEditor = mEmptyEditor; mEditor = mEmptyEditor;
init(); init();
@@ -68,6 +71,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this); mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setWidth(15);
JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable); JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
@@ -75,7 +79,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]")); buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
mNewButton = new JButton("New ..."); mNewButton = new JButton("New ...");
mNewButton.setToolTipText("Create a new broadcast configuration"); mNewButton.setToolTipText("Create a new broadcastAudio configuration");
mNewButton.addMouseListener(new MouseAdapter() mNewButton.addMouseListener(new MouseAdapter()
{ {
@Override @Override
@@ -121,18 +125,62 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
@Override @Override
public void actionPerformed(ActionEvent e) public void actionPerformed(ActionEvent e)
{ {
switch(e.getActionCommand())
{
case COPY_BROADCAST_CONFIGURATION:
BroadcastConfiguration copySelected = getSelectedBroadcastConfiguration();
mBroadcastModel.cloneBroadcastConfiguration(copySelected);
break;
case DELETE_BROADCAST_CONFIGURATION:
BroadcastConfiguration deleteSelected = getSelectedBroadcastConfiguration();
if(deleteSelected != null)
{
int choice = JOptionPane.showConfirmDialog(BroadcastPanel.this, "Do you want to delete broadcast\n" +
" configuration " + deleteSelected.getName() + "?", "Delete Broadcast Configuration?",
JOptionPane.YES_NO_OPTION);
if(choice == JOptionPane.YES_OPTION)
{
mBroadcastModel.removeBroadcastConfiguration(deleteSelected);
}
}
break;
}
} }
/**
* Returns the selected configuration
*/
private BroadcastConfiguration getSelectedBroadcastConfiguration()
{
int viewRow = mBroadcastConfigurationTable.getSelectedRow();
if(viewRow >= 0)
{
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(viewRow);
if(modelRow >= 0)
{
return mBroadcastModel.getConfigurationAt(modelRow);
}
}
return null;
}
@Override @Override
public void valueChanged(ListSelectionEvent e) public void valueChanged(ListSelectionEvent e)
{ {
if (!e.getValueIsAdjusting()) if (!e.getValueIsAdjusting())
{ {
int row = mBroadcastConfigurationTable.getSelectedRow(); BroadcastConfiguration selectedConfiguration = getSelectedBroadcastConfiguration();
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(row);
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getConfigurationAt(modelRow); //Enable buttons accordingly
mCopyButton.setEnabled(selectedConfiguration != null);
mDeleteButton.setEnabled(selectedConfiguration != null);
if (mEditor.isModified()) if (mEditor.isModified())
{ {
@@ -153,7 +201,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
if (selectedConfiguration != null) if (selectedConfiguration != null)
{ {
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel); mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mSettingsManager);
} }
else else
{ {
@@ -186,6 +234,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
final BroadcastConfiguration configuration = final BroadcastConfiguration configuration =
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3); BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
mBroadcastModel.addBroadcastConfiguration(configuration); mBroadcastModel.addBroadcastConfiguration(configuration);
int modelRow = mBroadcastModel.getRowForConfiguration(configuration); int modelRow = mBroadcastModel.getRowForConfiguration(configuration);

View File

@@ -20,36 +20,29 @@ package audio.broadcast;
public enum BroadcastServerType public enum BroadcastServerType
{ {
/** BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
* Broadcastify is currently using Icecast Server 2.3.2 ICECAST_HTTP("Icecast 2.4+", null),
*/ ICECAST_TCP("Icecast 2.3", null),
BROADCASTIFY("Broadcastify"), SHOUTCAST_V1("Shoutcast 1.x", null),
SHOUTCAST_V2("Shoutcast 2.x", null),
/** UNKNOWN("Unknown", null);
* Icecast Server 2.4.x and later
*/
ICECAST_HTTP("Icecast 2.4+"),
/**
* Icecast Server 2.3.x
*/
ICECAST_TCP("Icecast 2.3"),
SHOUTCAST_V1("Shoutcast 1.x"),
SHOUTCAST_V2("Shoutcast 2.x"),
UNKNOWN("Unknown");
private String mLabel; private String mLabel;
private String mIconName;
BroadcastServerType(String label) BroadcastServerType(String label, String iconName)
{ {
mLabel = label; mLabel = label;
mIconName = iconName;
} }
public String toString() public String toString()
{ {
return mLabel; return mLabel;
} }
public String getIconName()
{
return mIconName;
}
} }

View File

@@ -26,7 +26,7 @@ public enum BroadcastState
BROADCAST_ERROR("Broadcast Error", false), BROADCAST_ERROR("Broadcast Error", false),
/** /**
* Connected to the broadcast server and capable of streaming * Connected to the broadcastAudio server and capable of streaming
*/ */
CONNECTED("Connected", false), CONNECTED("Connected", false),
@@ -36,7 +36,7 @@ public enum BroadcastState
CONNECTING("Connecting", false), CONNECTING("Connecting", false),
/** /**
* Disconnected from the broadcast server * Disconnected from the broadcastAudio server
*/ */
DISCONNECTED("Disconnected", true), DISCONNECTED("Disconnected", true),

View File

@@ -1,242 +0,0 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
package audio.broadcast;
import audio.AudioPacket;
import audio.IAudioPacketListener;
import audio.convert.IAudioConverter;
import controller.ThreadPoolManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class Broadcaster implements IAudioPacketListener, Listener<AudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger( Broadcaster.class );
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture mScheduledTask;
private AudioQueueProcessor mAudioQueueProcessor;
protected LinkedTransferQueue<AudioPacket> mAudioQueue = new LinkedTransferQueue<>();
private List<Listener<BroadcastState>> mBroadcastStateListeners = new CopyOnWriteArrayList<>();
private BroadcastState mBroadcastState = BroadcastState.READY;
private BroadcastConfiguration mBroadcastConfiguration;
private IAudioConverter mAudioConverter;
private boolean mEnabled = true;
/**
* Broadcaster for sending audio to a remote broadcast server.
*/
public Broadcaster(ThreadPoolManager threadPoolManager,
BroadcastConfiguration broadcastConfiguration,
IAudioConverter audioConverter)
{
mThreadPoolManager = threadPoolManager;
mBroadcastConfiguration = broadcastConfiguration;
mAudioConverter = audioConverter;
mAudioQueueProcessor = new AudioQueueProcessor();
}
/**
* Broadcast configuration used by this broadcaster
*/
public BroadcastConfiguration getBroadcastConfiguration()
{
return mBroadcastConfiguration;
}
/**
* IAudioPacketListener interface method
*/
@Override
public Listener<AudioPacket> getAudioPacketListener()
{
return this;
}
/**
* Audio converter used by this broadcaster to convert PCM audio packets to the desired output format.
*/
protected IAudioConverter getAudioConverter()
{
return mAudioConverter;
}
/**
* Commands the broadcaster to broadcast any queued audio packets. This method is invoked by a scheduled thread
* pool task to periodically command the sub-class to broadcast queued audio packets.
*/
protected abstract void broadcast();
/**
* Disconnects the broadcaster and halts all broadcasting.
*/
public void stop()
{
mEnabled = false;
stopProcessor();
}
/**
* Primary audio packet input method. Audio packets are queued and then processed by the underlying broadcast
* handler for broadcast to the remote server.
*/
public void receive(AudioPacket packet)
{
if(mEnabled)
{
startProcessor();
mAudioQueue.add(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,
mAudioQueueProcessor, 250, TimeUnit.MILLISECONDS );
}
}
}
/**
* Stops the audio queue processor
*/
private void stopProcessor()
{
if(mThreadPoolManager != null && mScheduledTask != null)
{
mThreadPoolManager.cancel(mScheduledTask);
}
}
/**
* Pauses or unpauses the broadcast
*/
public void setPaused(boolean paused)
{
if(!getBroadcastState().isErrorState())
{
if(paused)
{
setBroadcastState(BroadcastState.PAUSED);
}
else
{
setBroadcastState(BroadcastState.READY);
}
}
}
/**
* Registers the listener to receive broadcast state changes
*/
public void addListener(Listener<BroadcastState> listener)
{
mBroadcastStateListeners.add(listener);
}
/**
* Removes the listener from receiving broadcast state changes
*/
public void removeListener(Listener<BroadcastState> listener)
{
mBroadcastStateListeners.remove(listener);
}
/**
* Sets the state of the broadcast connection
*/
protected void setBroadcastState(BroadcastState state)
{
if(mBroadcastState != state)
{
mLog.debug("Changing State to: " + state);
mBroadcastState = state;
for(Listener<BroadcastState> listener: mBroadcastStateListeners)
{
listener.receive(state);
}
}
}
/**
* Current state of the broadcast connection
*/
public BroadcastState getBroadcastState()
{
return mBroadcastState;
}
public boolean connected()
{
return getBroadcastState() == BroadcastState.CONNECTED;
}
/**
* Indicates if this broadcaster can connect and is not currently in an error state or a connected state.
*/
public boolean canConnect()
{
BroadcastState state = getBroadcastState();
return state != BroadcastState.CONNECTED && !state.isErrorState();
}
/**
* Audio packet queue processor. Runs via a scheduled thread pool task as a timing processor to instruct
* sub-classes to broadcast queued audio packets. Internal state monitor ensures that subsequent invocations of
* the broadcast method do not start until the previous invocation completes.
*/
public class AudioQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
if(mProcessing.compareAndSet(false, true))
{
broadcast();
mProcessing.set(false);
}
}
}
}

View File

@@ -39,8 +39,7 @@ public class StreamManager implements Listener<AudioPacket>
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
private ThreadPoolManager mThreadPoolManager; private ThreadPoolManager mThreadPoolManager;
private BroadcastFormat mBroadcastFormat; private AudioBroadcaster mAudioBroadcaster;
private Listener<StreamableAudioRecording> mOutputListener;
private long mDelay; private long mDelay;
private Path mTempDirectory; private Path mTempDirectory;
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>(); private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
@@ -57,24 +56,17 @@ public class StreamManager implements Listener<AudioPacket>
* has elapsed. * has elapsed.
* *
* @param threadPoolManager for scheduling runnables * @param threadPoolManager for scheduling runnables
* @param broadcastFormat defines the output recording format * @param audioBroadcaster to receive completed streamable audio recordings
* @param outputListener to receive completed streamable audio recordings
* @param delay from recording start until audio recording is nominated to output listener * @param delay from recording start until audio recording is nominated to output listener
* @param tempDirectory where to store temporary audio recordings * @param tempDirectory where to store temporary audio recordings
*/ */
public StreamManager(ThreadPoolManager threadPoolManager, public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, long delay, Path tempDirectory)
BroadcastFormat broadcastFormat,
Listener<StreamableAudioRecording> outputListener,
long delay,
Path tempDirectory)
{ {
assert(tempDirectory != null && Files.isDirectory(tempDirectory)); assert(tempDirectory != null && Files.isDirectory(tempDirectory));
assert(mThreadPoolManager != null); assert(mThreadPoolManager != null);
assert(mBroadcastFormat != null);
mThreadPoolManager = threadPoolManager; mThreadPoolManager = threadPoolManager;
mBroadcastFormat = broadcastFormat; mAudioBroadcaster = audioBroadcaster;
mOutputListener = outputListener;
mDelay = delay; mDelay = delay;
mTempDirectory = tempDirectory; mTempDirectory = tempDirectory;
} }
@@ -142,7 +134,7 @@ public class StreamManager implements Listener<AudioPacket>
else else
{ {
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(), AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
mBroadcastFormat); mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat());
recorder.start(mThreadPoolManager.getScheduledExecutorService()); recorder.start(mThreadPoolManager.getScheduledExecutorService());
recorder.receive(audioPacket); recorder.receive(audioPacket);
mStreamRecorders.put(sourceChannelID, recorder); mStreamRecorders.put(sourceChannelID, recorder);
@@ -176,11 +168,11 @@ public class StreamManager implements Listener<AudioPacket>
recorder.stop(); recorder.stop();
StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(), StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(),
recorder.getMetadata()); recorder.getMetadata(), recorder.getRecordingLength());
if(mDelay <= 0) if(mDelay <= 0)
{ {
mOutputListener.receive(streamableAudioRecording); mAudioBroadcaster.receive(streamableAudioRecording);
} }
else else
{ {
@@ -198,9 +190,9 @@ public class StreamManager implements Listener<AudioPacket>
private Path getTemporaryRecordingPath() private Path getTemporaryRecordingPath()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("temporary_streaming_file_"); sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
sb.append( TimeStamp.getLongTimeStamp( "_" ) ); sb.append( TimeStamp.getLongTimeStamp( "_" ) );
sb.append( mBroadcastFormat.getFileExtension() ); sb.append( mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat().getFileExtension() );
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString()); Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
@@ -222,9 +214,9 @@ public class StreamManager implements Listener<AudioPacket>
@Override @Override
public void run() public void run()
{ {
if(mOutputListener != null) if(mAudioBroadcaster != null)
{ {
mOutputListener.receive(mStreamableAudioRecording); mAudioBroadcaster.receive(mStreamableAudioRecording);
} }
} }
} }

View File

@@ -26,11 +26,19 @@ public class StreamableAudioRecording
{ {
private Path mPath; private Path mPath;
private AudioMetadata mAudioMetadata; private AudioMetadata mAudioMetadata;
private long mRecordingLengthMilliSeconds;
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata) /**
* Audio recording that is ready to be streamed
* @param path to the audio recording file
* @param audioMetadata associated with the recording
* @param recordingLength in milliseconds
*/
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long recordingLength)
{ {
mPath = path; mPath = path;
mAudioMetadata = audioMetadata; mAudioMetadata = audioMetadata;
mRecordingLengthMilliSeconds = recordingLength;
} }
/** /**
@@ -48,4 +56,12 @@ public class StreamableAudioRecording
{ {
return mAudioMetadata; return mAudioMetadata;
} }
/**
* Recording length in milliseconds
*/
public long getRecordingLength()
{
return mRecordingLengthMilliSeconds;
}
} }

View File

@@ -18,6 +18,7 @@
******************************************************************************/ ******************************************************************************/
package audio.broadcast.broadcastify; package audio.broadcast.broadcastify;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat; import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType; import audio.broadcast.BroadcastServerType;
import audio.broadcast.icecast.IcecastTCPConfiguration; import audio.broadcast.icecast.IcecastTCPConfiguration;
@@ -26,11 +27,12 @@ import javax.xml.bind.annotation.XmlAttribute;
public class BroadcastifyConfiguration extends IcecastTCPConfiguration public class BroadcastifyConfiguration extends IcecastTCPConfiguration
{ {
public int mFeedID; private int mFeedID;
public BroadcastifyConfiguration() public BroadcastifyConfiguration()
{ {
//No-arg constructor for JAXB //No-arg constructor for JAXB
this(BroadcastFormat.MP3);
} }
/** /**
@@ -47,6 +49,34 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
setSampleRate(8000); setSampleRate(8000);
} }
@Override
public BroadcastConfiguration copyOf()
{
BroadcastifyConfiguration copy = new BroadcastifyConfiguration(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());
//Broadcastify Configuration Parameters
copy.setFeedID(getFeedID());
return copy;
}
@Override @Override
public BroadcastServerType getBroadcastServerType() public BroadcastServerType getBroadcastServerType()
{ {
@@ -63,4 +93,10 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
{ {
mFeedID = feedID; mFeedID = feedID;
} }
@Override
public boolean isValid()
{
return super.isValid() && (mFeedID > 0);
}
} }

View File

@@ -19,10 +19,14 @@
package audio.broadcast.broadcastify; package audio.broadcast.broadcastify;
import audio.broadcast.BroadcastConfiguration; import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEvent; import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel; import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import gui.editor.DocumentListenerEditor; import gui.editor.DocumentListenerEditor;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
@@ -31,6 +35,9 @@ import java.awt.event.ActionListener;
public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration> public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
{ {
private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
private SettingsManager mSettingsManager;
private BroadcastModel mBroadcastModel; private BroadcastModel mBroadcastModel;
private JTextField mFeedID; private JTextField mFeedID;
private JTextField mName; private JTextField mName;
@@ -40,24 +47,27 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
private JTextField mPassword; private JTextField mPassword;
private JButton mSaveButton; private JButton mSaveButton;
private JButton mResetButton; private JButton mResetButton;
private JCheckBox mEnabled;
public BroadcastifyConfigurationEditor(BroadcastModel model) public BroadcastifyConfigurationEditor(BroadcastModel model, SettingsManager settingsManager)
{ {
mBroadcastModel = model; mBroadcastModel = model;
mSettingsManager = settingsManager;
init(); init();
} }
private void init() private void init()
{ {
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][][grow,fill]" ) ); setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][grow,fill]" ) );
setPreferredSize(new Dimension(150,400)); setPreferredSize(new Dimension(150,400));
add(new JLabel("Broadcastify Channel"), "span, align center");
JButton selectButton = new JButton("Select ..."); JLabel channelLabel = new JLabel("Broadcastify Stream");
add(selectButton, "wrap");
add(new JSeparator(JSeparator.HORIZONTAL), "span,growx"); ImageIcon broadcastifyIcon = mSettingsManager.getImageIcon(BroadcastServerType.BROADCASTIFY.getIconName(), 25);
channelLabel.setIcon(broadcastifyIcon);
add(channelLabel, "span, align center");
add(new JLabel("Feed ID:")); add(new JLabel("Feed ID:"));
mFeedID = new JTextField(); mFeedID = new JTextField();
@@ -89,6 +99,17 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mPassword.getDocument().addDocumentListener(this); mPassword.getDocument().addDocumentListener(this);
add(mPassword); add(mPassword);
mEnabled = new JCheckBox("Enabled");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled, "span");
mSaveButton = new JButton("Save"); mSaveButton = new JButton("Save");
mSaveButton.setEnabled(false); mSaveButton.setEnabled(false);
mSaveButton.addActionListener(new ActionListener() mSaveButton.addActionListener(new ActionListener()
@@ -139,6 +160,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null); mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mMountPoint.setText(config.getMountPoint()); mMountPoint.setText(config.getMountPoint());
mPassword.setText(config.getPassword()); mPassword.setText(config.getPassword());
mEnabled.setSelected(config.isEnabled());
} }
else else
{ {
@@ -148,6 +170,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mPort.setText(null); mPort.setText(null);
mMountPoint.setText(null); mMountPoint.setText(null);
mPassword.setText(null); mPassword.setText(null);
mEnabled.setSelected(false);
} }
setModified(false); setModified(false);
@@ -158,14 +181,151 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
{ {
BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem(); BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
if(validateConfiguration())
{
config.setName(mName.getText());
config.setFeedID(getFeedID());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setMountPoint(mMountPoint.getText());
config.setPassword(mPassword.getText());
config.setEnabled(mEnabled.isSelected());
setModified(false);
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
}
}
/**
* Validates the user specified values in the controls prior to saving them to the configuration
*/
private boolean validateConfiguration()
{
String name = mName.getText();
//A unique stream name is required
if(!mBroadcastModel.isUniqueName(name, getItem()))
{
String message = (name == null || name.isEmpty()) ?
"Please specify a stream name." : "Stream name " + name +
"is already in use.\nPlease choose another name.";
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this,
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
mName.requestFocus();
return false;
}
//Feed ID is optional, but required
if(!validateIntegerTextField(mFeedID, "Invalid Feed ID", "Please specify a feed ID", 1, Integer.MAX_VALUE))
{
return true;
}
//Server address is optional, but required
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
{
return true;
}
//Port is optional, but required
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
{
return true;
}
//Mount Point is optional, but required
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
{
return true;
}
//Password is optional, but required
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
{
return true;
}
return true;
}
/**
* Validates a text field control for a non-null, non-empty value
* @param field to validate
* @param title to use for error dialog
* @param message to use for error dialog
* @return true if field contains a non-null, non-empty value
*/
private boolean validateTextField(JTextField field, String title, String message)
{
String text = field.getText();
if(text == null || text.isEmpty())
{
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
JOptionPane.ERROR_MESSAGE);
field.requestFocus();
return false;
}
return true;
}
/**
* Validates the text field control that contains an integer value for non-null, non-empty and within the
* specified min/max valid range.
* @param field to validate
* @param title to use for error dialog
* @param message to use for error dialog
* @param minValid value
* @param maxValid value
* @return true if field contains a non-null, non-empty value within the valid min/max range
*/
private boolean validateIntegerTextField(JTextField field, String title, String message, int minValid, int maxValid)
{
if(validateTextField(field, title, message))
{
String text = field.getText();
try
{
int value = Integer.parseInt(text);
if(minValid <= value && value <= maxValid)
{
return true;
}
}
catch(Exception e)
{
//Do nothing, we couldn't parse the number value
}
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
JOptionPane.ERROR_MESSAGE);
field.requestFocus();
}
return false;
}
/**
* Parses an Integer from the feed ID text control or returns 0 if the control doesn't contain an integer value.
*/
private int getFeedID()
{
String feedID = mFeedID.getText(); String feedID = mFeedID.getText();
if(feedID != null && !feedID.isEmpty()) if(feedID != null && !feedID.isEmpty())
{ {
try try
{ {
int id = Integer.parseInt(feedID); return Integer.parseInt(feedID);
config.setFeedID(id);
} }
catch(Exception e) catch(Exception e)
{ {
@@ -173,17 +333,21 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
} }
} }
config.setName(mName.getText()); return 0;
config.setHost(mServer.getText()); }
/**
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
*/
private int getPort()
{
String port = mPort.getText(); String port = mPort.getText();
if(port != null && !port.isEmpty()) if(port != null && !port.isEmpty())
{ {
try try
{ {
int number = Integer.parseInt(port); return Integer.parseInt(port);
config.setPort(number);
} }
catch(Exception e) catch(Exception e)
{ {
@@ -191,11 +355,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
} }
} }
config.setMountPoint(mMountPoint.getText()); return 0;
config.setPassword(mPassword.getText());
setModified(false);
mBroadcastModel.broadcast(new BroadcastConfigurationEvent(getItem(), BroadcastConfigurationEvent.Event.CHANGE));
} }
} }

View File

@@ -19,11 +19,11 @@
package audio.broadcast.icecast; package audio.broadcast.icecast;
import audio.AudioPacket; import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFormat; import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState; import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcastFactory; import audio.broadcast.BroadcastFactory;
import audio.convert.IAudioConverter; import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.BoundRequestBuilder; import org.asynchttpclient.BoundRequestBuilder;
@@ -53,9 +53,9 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; 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 String HTTP_100_CONTINUE = "100-continue";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds 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 * 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 configuration
* @param audioConverter * @param audioConverter
*/ */
public IcecastHTTPBroadcaster(ThreadPoolManager threadPoolManager, public IcecastHTTPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastHTTPConfiguration configuration)
IcecastHTTPConfiguration configuration,
IAudioConverter audioConverter)
{ {
super(threadPoolManager, configuration, audioConverter); super(threadPoolManager, configuration);
} }
/** /**
@@ -88,31 +86,24 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
return (IcecastHTTPConfiguration)getBroadcastConfiguration(); 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 * Broadcast any queued audio packets using Icecast V2 Protocol
*/ */
@Override @Override
public void broadcast() protected void broadcastAudio(byte[] audio)
{ {
if(connect()) if(connect())
{ {
if (mSubscription != null && mSubscription.hasDemand()) if (mSubscription != null && mSubscription.hasDemand())
{ {
mAudioQueue.drainTo(mPacketsToBroadcast, 5); mSubscription.broadcast(ByteBuffer.wrap(audio));
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();
}
} }
} }
} }
@@ -418,8 +409,9 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
else else
{ {
ThreadPoolManager threadPoolManager = new ThreadPoolManager(); 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"); Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString()); mLog.debug("Opening: " + path.toString());
@@ -432,7 +424,7 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true)) try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{ {
reader.setListener(broadcaster); reader.setListener(audioBroadcaster);
reader.read(); reader.read();
} }
catch (IOException e) catch (IOException e)

View File

@@ -36,6 +36,11 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
private int mSampleRate = 8000; private int mSampleRate = 8000;
private String mURL; private String mURL;
public IcecastHTTPConfiguration()
{
//no-arg JAXB constructor
}
/** /**
* Icecast 2.3.x and 2.4.x compatible configuration * Icecast 2.3.x and 2.4.x compatible configuration
* @param format * @param format
@@ -45,6 +50,33 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
super(format); 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 @Override
public BroadcastServerType getBroadcastServerType() 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() public boolean isPublic()
{ {
@@ -157,8 +189,8 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
} }
/** /**
* Sets public visibility of the broadcast * Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcast should be visible to the public * @param isPublic indicates if the broadcastAudio should be visible to the public
*/ */
public void setPublic(boolean isPublic) 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() 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) 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() 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 * @param url
*/ */
public void setURL(String url) public void setURL(String url)

View File

@@ -18,16 +18,24 @@
******************************************************************************/ ******************************************************************************/
package audio.broadcast.icecast; package audio.broadcast.icecast;
import alias.Alias;
import alias.id.broadcast.BroadcastChannel;
import audio.AudioPacket; import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFactory;
import audio.broadcast.BroadcastFormat; import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState; import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster; import audio.metadata.AudioMetadata;
import audio.broadcast.BroadcastFactory; import audio.metadata.Metadata;
import audio.convert.IAudioConverter; import audio.metadata.MetadataType;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.BoundRequestBuilder; import org.asynchttpclient.BoundRequestBuilder;
import org.asynchttpclient.DefaultAsyncHttpClient; import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Realm; import org.asynchttpclient.Realm;
import org.asynchttpclient.Response;
import org.asynchttpclient.uri.Uri; import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -37,6 +45,7 @@ import record.wave.AudioPacketMonoWaveReader;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.net.SocketException; import java.net.SocketException;
@@ -44,91 +53,186 @@ import java.net.URLEncoder;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; 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 UTF8 = "UTF-8";
private final static String TERMINATOR = "\r\n"; private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":"; private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private long mLastConnectionAttempt = 0; private long mLastConnectionAttempt = 0;
private List<AudioPacket> mPacketsToBroadcast = new ArrayList<>();
private AtomicBoolean mConnecting = new AtomicBoolean(); private AtomicBoolean mConnecting = new AtomicBoolean();
private Socket mSocket; private Socket mSocket;
private DataOutputStream mOutputStream; private DataOutputStream mOutputStream;
private DataInputStream mInputStream; private DataInputStream mInputStream;
private DefaultAsyncHttpClient mDefaultAsyncHttpClient; private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
private boolean mFirstMetadataUpdateSuppressed = false;
private int mCounter; private byte[] mSilenceFrame;
/** /**
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is * 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. * 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 configuration
* @param audioConverter
*/ */
public IcecastTCPBroadcaster(ThreadPoolManager threadPoolManager, public IcecastTCPAudioBroadcaster(DefaultAsyncHttpClient httpClient,
IcecastTCPConfiguration configuration, ThreadPoolManager threadPoolManager,
IAudioConverter audioConverter) IcecastTCPConfiguration configuration)
{ {
super(threadPoolManager, configuration, audioConverter); super(threadPoolManager, configuration);
mDefaultAsyncHttpClient = httpClient;
} }
/** /**
* Configuration information * Configuration information
*/ */
private IcecastHTTPConfiguration getConfiguration() private IcecastTCPConfiguration getConfiguration()
{ {
return (IcecastHTTPConfiguration)getBroadcastConfiguration(); return (IcecastTCPConfiguration)getBroadcastConfiguration();
} }
/** /**
* Broadcast any queued audio packets using Icecast V2 Protocol * Broadcast any queued audio packets using Icecast V2 Protocol
*/ */
@Override @Override
public void broadcast() protected void broadcastAudio(byte[] audio)
{ {
if(connect()) boolean completed = false;
while(!completed)
{ {
mAudioQueue.drainTo(mPacketsToBroadcast, 5); if(connect())
if (!mPacketsToBroadcast.isEmpty())
{ {
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
mLog.debug("We have [" + mPacketsToBroadcast.size() +
"] packets - sending:" + convertedAudio.length + " bytes");
try try
{ {
send(convertedAudio); send(audio);
completed = true;
} }
catch(SocketException se) 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) catch(IOException e)
{ {
completed = true;
mLog.error("Error sending audio", e); 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 * Disconnect from the remote server
*/ */
public void disconnect() public void disconnect(boolean shutdown)
{ {
if(mOutputStream != null) if(mOutputStream != null)
{ {
@@ -197,7 +301,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
mSocket = null; 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); .append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR) sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR); .append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR) sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR); .append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
@@ -334,6 +444,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
getConfiguration().getPort() + "] - will reattempt connection periodically"); getConfiguration().getPort() + "] - will reattempt connection periodically");
return; 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) catch(IOException ioe)
{ {
setBroadcastState(BroadcastState.ERROR); setBroadcastState(BroadcastState.ERROR);
@@ -457,7 +575,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
{ {
setBroadcastState(BroadcastState.CONNECTED); setBroadcastState(BroadcastState.CONNECTED);
} }
else if(response.startsWith("Invalid Password")) else if(response.contains("Invalid Password") || response.contains("Authentication Required"))
{ {
setBroadcastState(BroadcastState.INVALID_PASSWORD); setBroadcastState(BroadcastState.INVALID_PASSWORD);
} }
@@ -469,56 +587,20 @@ public class IcecastTCPBroadcaster extends Broadcaster
} }
else else
{ {
setBroadcastState(BroadcastState.CONNECTED);
mLog.debug("Response was empty"); 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) public static void main(String[] args)
{ {
boolean test = false; boolean test = false;
IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3); 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.setHost("audio3.broadcastify.com");
config.setPort(80); config.setPort(80);
config.setMountPoint("/k0yrdpx7zn4h"); config.setMountPoint("/k0yrdpx7zn4h");
@@ -530,6 +612,11 @@ public class IcecastTCPBroadcaster extends Broadcaster
config.setURL("http://www.radioreference.com"); config.setURL("http://www.radioreference.com");
config.setBitRate(16); 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) if(test)
{ {
mLog.debug("Auth:" + config.getEncodedCredentials()); mLog.debug("Auth:" + config.getEncodedCredentials());
@@ -537,22 +624,29 @@ public class IcecastTCPBroadcaster extends Broadcaster
else else
{ {
ThreadPoolManager threadPoolManager = new ThreadPoolManager(); 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"); Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString()); mLog.debug("Opening: " + path.toString());
mLog.debug("Registering and starting audio playback"); mLog.debug("Registering and starting audio playback");
int playCount = 0;
while(true) while(true)
{ {
mLog.debug("Playback started [" + path.toString() + "]"); mLog.debug("Playback started [" + path.toString() + "]");
toAlias.setName("Playback #" + playCount++);
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true)) try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{ {
reader.setListener(broadcaster); reader.setMetadata(metadata);
reader.setListener(audioBroadcaster);
reader.read(); reader.read();
audioBroadcaster.receive(new AudioPacket(AudioPacket.Type.END, metadata));
} }
catch (IOException e) catch (IOException e)
{ {

View File

@@ -22,9 +22,10 @@ import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat; import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType; import audio.broadcast.BroadcastServerType;
import audio.broadcast.broadcastify.BroadcastifyConfiguration; import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlTransient;
import java.util.Base64; import java.util.Base64;
@@ -32,6 +33,8 @@ import java.util.Base64;
@XmlSeeAlso( {BroadcastifyConfiguration.class } ) @XmlSeeAlso( {BroadcastifyConfiguration.class } )
public class IcecastTCPConfiguration extends BroadcastConfiguration public class IcecastTCPConfiguration extends BroadcastConfiguration
{ {
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
private String mUserName; private String mUserName;
private String mMountPoint; private String mMountPoint;
private String mDescription; private String mDescription;
@@ -44,6 +47,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
public IcecastTCPConfiguration() public IcecastTCPConfiguration()
{ {
setUserName("source");
//No-arg constructor for JAXB //No-arg constructor for JAXB
} }
@@ -54,6 +58,31 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
public IcecastTCPConfiguration(BroadcastFormat format) public IcecastTCPConfiguration(BroadcastFormat format)
{ {
super(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 @Override
@@ -79,6 +108,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
sb.append(getPassword()); sb.append(getPassword());
} }
mLog.debug("Connection String: " + sb.toString());
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes()); String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
return "Basic " + base64; 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" ) @XmlAttribute( name="public" )
public boolean isPublic() public boolean isPublic()
@@ -184,8 +215,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
} }
/** /**
* Sets public visibility of the broadcast * Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcast should be visible to the public * @param isPublic indicates if the broadcastAudio should be visible to the public
*/ */
public void setPublic(boolean isPublic) 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" ) @XmlAttribute( name="channels" )
public int getChannels() 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) 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" ) @XmlAttribute( name="url" )
public String getURL() 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 * @param url
*/ */
public void setURL(String url) public void setURL(String url)
@@ -275,4 +306,35 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
{ {
return mURL != null; 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;
}
} }

View File

@@ -19,13 +19,14 @@
package audio.broadcast.shoutcast.v1; package audio.broadcast.shoutcast.v1;
import audio.AudioPacket; import audio.AudioPacket;
import audio.broadcast.Broadcaster; import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFactory; import audio.broadcast.BroadcastFactory;
import audio.broadcast.BroadcastFormat; import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastMetadata; import audio.broadcast.BroadcastMetadata;
import audio.broadcast.BroadcastState; import audio.broadcast.BroadcastState;
import audio.convert.IAudioConverter; import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import record.wave.AudioPacketMonoWaveReader; import record.wave.AudioPacketMonoWaveReader;
@@ -41,9 +42,9 @@ import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class ShoutcastV1Broadcaster extends Broadcaster public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
{ {
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1Broadcaster.class ); private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1AudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private long mLastConnectionAttempt = 0; private long mLastConnectionAttempt = 0;
@@ -58,13 +59,10 @@ public class ShoutcastV1Broadcaster extends Broadcaster
/** /**
* Creates a Shoutcast Version 1 broadcaster. * Creates a Shoutcast Version 1 broadcaster.
* @param configuration * @param configuration
* @param audioConverter
*/ */
public ShoutcastV1Broadcaster(ThreadPoolManager threadPoolManager, public ShoutcastV1AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration configuration)
ShoutcastV1Configuration configuration,
IAudioConverter audioConverter)
{ {
super(threadPoolManager, configuration, audioConverter); super(threadPoolManager, configuration);
} }
/** /**
@@ -75,34 +73,29 @@ public class ShoutcastV1Broadcaster extends Broadcaster
return (ShoutcastV1Configuration)getBroadcastConfiguration(); return (ShoutcastV1Configuration)getBroadcastConfiguration();
} }
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
mLog.debug("Request to send audio metadata to shoutcast v1 server - needs code");
}
/** /**
* Process audio packets using Shoutcast V1 Protocol * Process audio packets using Shoutcast V1 Protocol
*/ */
public void broadcast() @Override
protected void broadcastAudio(byte[] audio)
{ {
//If we're connected, send the audio, otherwise discard it //If we're connected, send the audio, otherwise discard it
if(connect()) if(connect())
{ {
mAudioQueue.drainTo(mPacketsToBroadcast, 5); try
if(!mPacketsToBroadcast.isEmpty())
{ {
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast); send(audio);
}
mPacketsToBroadcast.clear(); catch (IOException e)
{
if(convertedAudio != null) mLog.error("Error while dispatching audio", e);
{ setBroadcastState(BroadcastState.BROADCAST_ERROR);
try
{
send(convertedAudio);
}
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
}
}
} }
} }
} }
@@ -349,8 +342,9 @@ public class ShoutcastV1Broadcaster extends Broadcaster
config.setBitRate(16); config.setBitRate(16);
ThreadPoolManager threadPoolManager = new ThreadPoolManager(); 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"); Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString()); mLog.debug("Opening: " + path.toString());
@@ -363,7 +357,7 @@ public class ShoutcastV1Broadcaster extends Broadcaster
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true)) try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{ {
reader.setListener(broadcaster); reader.setListener(audioBroadcaster);
reader.read(); reader.read();
} }
catch (IOException e) catch (IOException e)

View File

@@ -21,6 +21,7 @@ package audio.broadcast.shoutcast.v1;
import audio.broadcast.BroadcastConfiguration; import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat; import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastServerType; import audio.broadcast.BroadcastServerType;
import audio.broadcast.icecast.IcecastTCPConfiguration;
public class ShoutcastV1Configuration extends BroadcastConfiguration public class ShoutcastV1Configuration extends BroadcastConfiguration
{ {
@@ -31,11 +32,40 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
private int mBitRate; private int mBitRate;
private String mURL; private String mURL;
public ShoutcastV1Configuration()
{
//No-arg JAXB constructor
}
public ShoutcastV1Configuration(BroadcastFormat format) public ShoutcastV1Configuration(BroadcastFormat format)
{ {
super(format); super(format);
} }
@Override
public BroadcastConfiguration copyOf()
{
ShoutcastV1Configuration copy = new ShoutcastV1Configuration(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.setStreamName(getStreamName());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setChannels(getChannels());
copy.setBitRate(getBitRate());
copy.setURL(getURL());
return copy;
}
@Override @Override
public BroadcastServerType getBroadcastServerType() public BroadcastServerType getBroadcastServerType()
{ {
@@ -77,7 +107,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
} }
/** /**
* Public visibility of the broadcast * Public visibility of the broadcastAudio
*/ */
public boolean isPublic() public boolean isPublic()
{ {
@@ -85,8 +115,8 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
} }
/** /**
* Sets public visibility of the broadcast * Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcast should be visible to the public * @param isPublic indicates if the broadcastAudio should be visible to the public
*/ */
public void setPublic(boolean isPublic) public void setPublic(boolean isPublic)
{ {
@@ -94,7 +124,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
} }
/** /**
* Number of audio channels in the broadcast * Number of audio channels in the broadcastAudio
*/ */
public int getChannels() public int getChannels()
{ {
@@ -102,7 +132,7 @@ public class ShoutcastV1Configuration 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) public void setChannels(int channels)
{ {
@@ -127,7 +157,7 @@ public class ShoutcastV1Configuration 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() public String getURL()
{ {
@@ -135,7 +165,7 @@ public class ShoutcastV1Configuration 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 * @param url
*/ */
public void setURL(String url) public void setURL(String url)

View File

@@ -19,10 +19,10 @@
package audio.broadcast.shoutcast.v2; package audio.broadcast.shoutcast.v2;
import audio.AudioPacket; import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastConfiguration; import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastFormat; import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState; import audio.broadcast.BroadcastState;
import audio.broadcast.Broadcaster;
import audio.broadcast.BroadcastFactory; import audio.broadcast.BroadcastFactory;
import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast; import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast;
import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre; import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre;
@@ -39,8 +39,9 @@ import audio.broadcast.shoutcast.v2.message.UltravoxMessageFactory;
import audio.broadcast.shoutcast.v2.message.UltravoxMessageType; import audio.broadcast.shoutcast.v2.message.UltravoxMessageType;
import audio.broadcast.shoutcast.v2.message.UltravoxMetadata; import audio.broadcast.shoutcast.v2.message.UltravoxMetadata;
import audio.broadcast.shoutcast.v2.message.XMLMetadata; import audio.broadcast.shoutcast.v2.message.XMLMetadata;
import audio.convert.IAudioConverter; import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager; import controller.ThreadPoolManager;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import properties.SystemProperties; import properties.SystemProperties;
@@ -57,9 +58,9 @@ import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class ShoutcastV2Broadcaster extends Broadcaster public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
{ {
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2Broadcaster.class ); private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2AudioBroadcaster.class );
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
private static final boolean GET_RESPONSE = true; private static final boolean GET_RESPONSE = true;
@@ -75,13 +76,10 @@ public class ShoutcastV2Broadcaster extends Broadcaster
/** /**
* Creates a Shoutcast Version 2 broadcaster. * Creates a Shoutcast Version 2 broadcaster.
* @param configuration details for shoutcast version 2 * @param configuration details for shoutcast version 2
* @param audioConverter to convert PCM audio
*/ */
public ShoutcastV2Broadcaster(ThreadPoolManager threadPoolManager, public ShoutcastV2AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration configuration)
BroadcastConfiguration configuration,
IAudioConverter audioConverter)
{ {
super(threadPoolManager, configuration, audioConverter); super(threadPoolManager, configuration);
} }
/** /**
@@ -92,41 +90,36 @@ public class ShoutcastV2Broadcaster extends Broadcaster
return (ShoutcastV2Configuration)getBroadcastConfiguration(); return (ShoutcastV2Configuration)getBroadcastConfiguration();
} }
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
mLog.debug("Request to broadcast audio metadata to shoutcast v2 server - needs coding");
}
/** /**
* Broadcast audio packets * Broadcast audio
*/ */
public void broadcast() @Override
protected void broadcastAudio(byte[] audio)
{ {
//If we're connected, send the audio, otherwise discard it //If we're connected, send the audio, otherwise discard it
if(connect()) if(connect())
{ {
mAudioQueue.drainTo(mPacketsToBroadcast, 5); List<UltravoxMessage> audioMessages = getAudioMessages(audio);
if(!mPacketsToBroadcast.isEmpty()) try
{ {
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast); for(UltravoxMessage audioMessage: audioMessages)
mPacketsToBroadcast.clear();
if(convertedAudio != null)
{ {
List<UltravoxMessage> audioMessages = getAudioMessages(convertedAudio); send(audioMessage, NO_RESPONSE_EXPECTED);
try
{
for(UltravoxMessage audioMessage: audioMessages)
{
send(audioMessage, NO_RESPONSE_EXPECTED);
}
}
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
return;
}
} }
} }
catch(IOException e)
{
mLog.error("Error while dispatching audio", e);
setBroadcastState(BroadcastState.BROADCAST_ERROR);
return;
}
} }
} }
@@ -370,13 +363,13 @@ public class ShoutcastV2Broadcaster extends Broadcaster
response = send(icyName, NO_RESPONSE_EXPECTED); response = send(icyName, NO_RESPONSE_EXPECTED);
} }
if (getShoutcastConfiguration().getStreamGenre() != null) if (getShoutcastConfiguration().getGenre() != null)
{ {
metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getStreamGenre())); metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getGenre()));
ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory
.getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE); .getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE);
icyGenre.setGenre(getShoutcastConfiguration().getStreamGenre()); icyGenre.setGenre(getShoutcastConfiguration().getGenre());
response = send(icyGenre, NO_RESPONSE_EXPECTED); response = send(icyGenre, NO_RESPONSE_EXPECTED);
} }
@@ -534,7 +527,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
config.setPort(8000); config.setPort(8000);
config.setPassword("denny3"); config.setPassword("denny3");
config.setStreamName("Denny's Audio Broadcast Test"); config.setStreamName("Denny's Audio Broadcast Test");
config.setStreamGenre("Public Safety"); config.setGenre("Public Safety");
config.setPublic(true); config.setPublic(true);
config.setURL("http://localhost:8000"); config.setURL("http://localhost:8000");
config.setBitRate(16); config.setBitRate(16);
@@ -545,8 +538,9 @@ public class ShoutcastV2Broadcaster extends Broadcaster
else else
{ {
ThreadPoolManager threadPoolManager = new ThreadPoolManager(); 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"); Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString()); mLog.debug("Opening: " + path.toString());
@@ -559,7 +553,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true)) try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{ {
reader.setListener(broadcaster); reader.setListener(audioBroadcaster);
reader.read(); reader.read();
} }
catch (IOException e) catch (IOException e)

View File

@@ -28,16 +28,46 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
private String mUserID; private String mUserID;
private int mBitRate; private int mBitRate;
private String mStreamName; private String mStreamName;
private String mStreamGenre; private String mGenre;
private String mURL; private String mURL;
private boolean mPublic = true; private boolean mPublic = true;
public ShoutcastV2Configuration()
{
//No-arg JAXB constructor
}
public ShoutcastV2Configuration(BroadcastFormat format) public ShoutcastV2Configuration(BroadcastFormat format)
{ {
super(format); super(format);
} }
@Override
public BroadcastConfiguration copyOf()
{
ShoutcastV2Configuration copy = new ShoutcastV2Configuration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Shoutcast V2 Configuration Parameters
copy.setStreamID(getStreamID());
copy.setUserID(getUserID());
copy.setBitRate(getBitRate());
copy.setStreamName(getStreamName());
copy.setGenre(getGenre());
copy.setURL(getURL());
copy.setPublic(isPublic());
return copy;
}
@Override @Override
public BroadcastServerType getBroadcastServerType() public BroadcastServerType getBroadcastServerType()
{ {
@@ -117,21 +147,21 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
/** /**
* Stream genre * Stream genre
*/ */
public String getStreamGenre() public String getGenre()
{ {
return mStreamGenre; return mGenre;
} }
/** /**
* Sets the genre tag for this stream * Sets the genre tag for this stream
*/ */
public void setStreamGenre(String genre) public void setGenre(String genre)
{ {
mStreamGenre = genre; mGenre = genre;
} }
/** /**
* Public visibility of the broadcast * Public visibility of the broadcastAudio
*/ */
public boolean isPublic() public boolean isPublic()
{ {
@@ -139,8 +169,8 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
} }
/** /**
* Sets public visibility of the broadcast * Sets public visibility of the broadcastAudio
* @param isPublic indicates if the broadcast should be visible to the public * @param isPublic indicates if the broadcastAudio should be visible to the public
*/ */
public void setPublic(boolean isPublic) public void setPublic(boolean isPublic)
{ {
@@ -148,7 +178,7 @@ public class ShoutcastV2Configuration 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() public String getURL()
{ {
@@ -156,7 +186,7 @@ public class ShoutcastV2Configuration 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 * @param url
*/ */
public void setURL(String url) public void setURL(String url)

View File

@@ -45,7 +45,7 @@ public class StreamMimeType extends UltravoxMessage
} }
/** /**
* Sets the broadcast format or MIME type * Sets the broadcastAudio format or MIME type
* @param format of the stream * @param format of the stream
*/ */
public void setFormat(BroadcastFormat format) public void setFormat(BroadcastFormat format)

View File

@@ -21,7 +21,7 @@ package audio.broadcast.shoutcast.v2.message;
public class TerminateBroadcast extends UltravoxMessage public class TerminateBroadcast extends UltravoxMessage
{ {
/** /**
* Client request to server to terminate the broadcast * Client request to server to terminate the broadcastAudio
* *
* Package private constructor. Use the UltravoxMessageFactory for this constructor. * Package private constructor. Use the UltravoxMessageFactory for this constructor.
*/ */

View File

@@ -18,11 +18,15 @@
package audio.metadata; package audio.metadata;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import alias.id.broadcast.BroadcastChannel;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -312,6 +316,23 @@ public class AudioMetadata implements Listener<Metadata>
return mStreamable; return mStreamable;
} }
public Collection<BroadcastChannel> getBroadcastChannels()
{
Set<BroadcastChannel> broadcastChannels = new TreeSet<>();
for(Metadata metadata: mMetadata.values())
{
if(metadata.hasAlias() && metadata.getAlias().isStreamable())
{
Alias alias = metadata.getAlias();
broadcastChannels.addAll(alias.getBroadcastChannels());
}
}
return broadcastChannels;
}
public String toString() public String toString()
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View File

@@ -91,7 +91,7 @@ public class ControllerPanel extends JPanel
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager); mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
mBroadcastPanel = new BroadcastPanel(broadcastModel); mBroadcastPanel = new BroadcastPanel(broadcastModel, settingsManager);
mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager); mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager);

View File

@@ -20,7 +20,6 @@ package gui;
import alias.AliasModel; import alias.AliasModel;
import alias.action.AliasActionManager; import alias.action.AliasActionManager;
import audio.AudioManager; import audio.AudioManager;
import audio.broadcast.AudioBroadcastManager;
import audio.broadcast.BroadcastModel; import audio.broadcast.BroadcastModel;
import com.jidesoft.swing.JideSplitPane; import com.jidesoft.swing.JideSplitPane;
import controller.ControllerPanel; import controller.ControllerPanel;
@@ -137,10 +136,9 @@ public class SDRTrunk implements Listener<TunerEvent>
AudioManager audioManager = new AudioManager(threadPoolManager, sourceManager.getMixerManager()); AudioManager audioManager = new AudioManager(threadPoolManager, sourceManager.getMixerManager());
channelProcessingManager.addAudioPacketListener(audioManager); channelProcessingManager.addAudioPacketListener(audioManager);
BroadcastModel broadcastModel = new BroadcastModel(); BroadcastModel broadcastModel = new BroadcastModel(threadPoolManager, mSettingsManager);
AudioBroadcastManager audioBroadcastManager = new AudioBroadcastManager(threadPoolManager); channelProcessingManager.addAudioPacketListener(broadcastModel);
channelProcessingManager.addAudioPacketListener(audioBroadcastManager);
MapService mapService = new MapService(mSettingsManager); MapService mapService = new MapService(mSettingsManager);
channelProcessingManager.addMessageListener(mapService); channelProcessingManager.addMessageListener(mapService);

View File

@@ -31,13 +31,13 @@ public class DecoderSelectionFrame extends JInternalFrame
public DecoderSelectionFrame( JDesktopPane desktop, public DecoderSelectionFrame( JDesktopPane desktop,
IControllableFileSource source ) IControllableFileSource source )
{ {
ThreadPoolManager tpm = new ThreadPoolManager();
AliasModel aliasModel = new AliasModel(); AliasModel aliasModel = new AliasModel();
BroadcastModel broadcastModel = new BroadcastModel(); BroadcastModel broadcastModel = new BroadcastModel(tpm, null);
ChannelModel channelModel = new ChannelModel(); ChannelModel channelModel = new ChannelModel();
ChannelMapModel channelMapModel = new ChannelMapModel(); ChannelMapModel channelMapModel = new ChannelMapModel();
ThreadPoolManager tpm = new ThreadPoolManager();
mPlaylistManager = new PlaylistManager( tpm, aliasModel, broadcastModel, channelModel, channelMapModel ); mPlaylistManager = new PlaylistManager( tpm, aliasModel, broadcastModel, channelModel, channelMapModel );
mDesktop = desktop; mDesktop = desktop;

View File

@@ -33,7 +33,7 @@ import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller; import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller; import javax.xml.bind.Unmarshaller;
import audio.broadcast.BroadcastConfigurationEvent; import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel; import audio.broadcast.BroadcastModel;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -109,10 +109,10 @@ public class PlaylistManager implements ChannelEventListener
} }
} ); } );
mBroadcastModel.addListener(new Listener<BroadcastConfigurationEvent>() mBroadcastModel.addListener(new Listener<BroadcastEvent>()
{ {
@Override @Override
public void receive(BroadcastConfigurationEvent broadcastConfigurationEvent) public void receive(BroadcastEvent broadcastEvent)
{ {
schedulePlaylistSave(); schedulePlaylistSave();
} }
@@ -346,7 +346,7 @@ public class PlaylistManager implements ChannelEventListener
} }
catch ( Exception e ) catch ( Exception e )
{ {
mLog.error( "coulcn't open inputstream to load playlist [" + playlistPath.toString() + "]", e ); mLog.error( "couldn't open input stream to load playlist [" + playlistPath.toString() + "]", e );
} }
finally finally
{ {

View File

@@ -55,6 +55,8 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
private BufferProcessor mBufferProcessor; private BufferProcessor mBufferProcessor;
private ScheduledFuture<?> mProcessorHandle; private ScheduledFuture<?> mProcessorHandle;
private long mSampleCount;
/** /**
* Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file * Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file
* *
@@ -95,6 +97,15 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
return mTimeRecordingStart; return mTimeRecordingStart;
} }
/**
* Recording length in milliseconds
*/
public long getRecordingLength()
{
//Assumes audio sample rate of 8000 samples/second or 8 samples/milli-second
return mSampleCount / 8;
}
/** /**
* Implements the IAudioPacketListener interface and simply redirects to the Listener<AudioPacket> interface. * Implements the IAudioPacketListener interface and simply redirects to the Listener<AudioPacket> interface.
* This is necessary since you can't have multiple methods with the same erasure (ie Listener<xxx>) in the * This is necessary since you can't have multiple methods with the same erasure (ie Listener<xxx>) in the
@@ -137,7 +148,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
*/ */
protected OutputStream getOutputStream() protected OutputStream getOutputStream()
{ {
return null; return mFileOutputStream;
} }
/** /**
@@ -169,6 +180,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
{ {
try try
{ {
mFileOutputStream.flush();
mFileOutputStream.close(); mFileOutputStream.close();
} }
catch (IOException e) catch (IOException e)
@@ -204,8 +216,6 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
{ {
mFileOutputStream = new FileOutputStream(mPath.toFile()); mFileOutputStream = new FileOutputStream(mPath.toFile());
mLog.info("Created Audio Recording [" + getPath().toString() + "]");
/* Schedule the handler to run every half second */ /* Schedule the handler to run every half second */
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS); mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
} }
@@ -230,6 +240,14 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
try try
{ {
record(mPacketsToProcess); record(mPacketsToProcess);
for(AudioPacket packet: mPacketsToProcess)
{
if(packet.getType() == AudioPacket.Type.AUDIO)
{
mSampleCount += packet.getAudioBuffer().getSamples().length;
}
}
} }
catch(IOException ioe) catch(IOException ioe)
{ {

View File

@@ -95,26 +95,50 @@ public class MP3Recorder extends AudioRecorder
public static void main(String[] args) public static void main(String[] args)
{ {
Path inputPath = Paths.get("/home/denny/Music/PCM.wav"); mLog.debug("Starting ...");
Path outputPath = Paths.get("/home/denny/Music/denny_test/PCM.mp3");
// Path inputPath = Paths.get("/home/denny/Music/PCM.wav");
// Path outputPath = Paths.get("/home/denny/Music/denny_test/PCM.mp3");
//
// mLog.debug("Reading: " + inputPath.toString());
// mLog.debug("Writing: " + outputPath.toString());
//
// final MP3Recorder recorder = new MP3Recorder(outputPath);
// recorder.start(Executors.newSingleThreadScheduledExecutor());
//
// try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(inputPath, true))
// {
// reader.setListener(recorder);
// reader.read();
// recorder.stop();
// }
// catch (IOException e)
// {
// mLog.error("Error", e);
// }
//
Path outputPath = Paths.get("/home/denny/Music/Silence_test_10_seconds.mp3");
mLog.debug("Reading: " + inputPath.toString());
mLog.debug("Writing: " + outputPath.toString()); mLog.debug("Writing: " + outputPath.toString());
final MP3Recorder recorder = new MP3Recorder(outputPath); final MP3Recorder recorder = new MP3Recorder(outputPath);
recorder.start(Executors.newSingleThreadScheduledExecutor()); ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
recorder.start(executor);
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(inputPath, true)) long duration = 1165;
int length = (int)(duration * 8); //8000 Hz sample rate
float[] silence = new float[length];
AudioPacket silencePacket = new AudioPacket(silence, null);
for(int x = 0; x < 10; x++)
{ {
reader.setListener(recorder); recorder.receive(silencePacket);
reader.read();
recorder.stop();
}
catch (IOException e)
{
mLog.error("Error", e);
} }
recorder.stop();
executor.shutdown();
mLog.debug("Finished"); mLog.debug("Finished");
} }
} }

View File

@@ -21,7 +21,7 @@ import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* Broadcasts a received float sample to multiple listeners * Broadcasts an item to multiple listeners
*/ */
public class Broadcaster<T> implements Listener<T> public class Broadcaster<T> implements Listener<T>
{ {