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

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

View File

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

View File

@@ -0,0 +1,128 @@
/*******************************************************************************
* 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 alias.AliasModel;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
import gui.editor.DocumentListenerEditor;
import settings.SettingsManager;
import javax.swing.*;
public abstract class BroadcastConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
{
protected SettingsManager mSettingsManager;
protected BroadcastModel mBroadcastModel;
protected AliasModel mAliasModel;
public BroadcastConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, SettingsManager settingsManager)
{
mBroadcastModel = broadcastModel;
mAliasModel = aliasModel;
mSettingsManager = settingsManager;
}
/**
* Updates the configuration with the new name and prompts the user to update any aliases that have a broadcast
* channel alias id with the old name to update to the new name.
*
* @param broadcastConfiguration to assign a new channel name
* @param newName to assign to the broadcast configuration. Note: assumes that validateConfiguration() has been
* invoked to verify that newName is non-null and non-empty
*/
protected void updateConfigurationName(BroadcastConfiguration broadcastConfiguration, String newName)
{
String previousName = broadcastConfiguration.getName();
if(previousName == null || previousName.isEmpty())
{
broadcastConfiguration.setName(newName);
return;
}
if(!previousName.equals(newName))
{
mAliasModel.renameBroadcastChannel(previousName, newName);
broadcastConfiguration.setName(newName);
}
}
/**
* 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
*/
protected boolean validateTextField(JTextField field, String title, String message)
{
String text = field.getText();
if(text == null || text.isEmpty())
{
JOptionPane.showMessageDialog(BroadcastConfigurationEditor.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
*/
protected 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(BroadcastConfigurationEditor.this, message, title,
JOptionPane.ERROR_MESSAGE);
field.requestFocus();
}
return false;
}
}

View File

@@ -18,6 +18,7 @@
******************************************************************************/
package audio.broadcast;
import alias.AliasModel;
import audio.AudioPacket;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
@@ -25,6 +26,7 @@ import audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import audio.broadcast.icecast.IcecastHTTPConfiguration;
import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
import audio.broadcast.icecast.IcecastTCPConfiguration;
import audio.broadcast.icecast.IcecastTCPConfigurationEditor;
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
@@ -150,6 +152,7 @@ public class BroadcastFactory
*/
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
BroadcastModel broadcastModel,
AliasModel aliasModel,
SettingsManager settingsManager)
{
Editor<BroadcastConfiguration> editor;
@@ -157,7 +160,10 @@ public class BroadcastFactory
switch (configuration.getBroadcastServerType())
{
case BROADCASTIFY:
editor = new BroadcastifyConfigurationEditor(broadcastModel, settingsManager);
editor = new BroadcastifyConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
case ICECAST_TCP:
editor = new IcecastTCPConfigurationEditor(broadcastModel, aliasModel, settingsManager);
break;
default:
editor = new EmptyEditor<BroadcastConfiguration>();

View File

@@ -40,6 +40,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -228,7 +229,9 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
for(BroadcastConfiguration configurationToCompare: mBroadcastConfigurations)
{
if(configurationToCompare != configuration && configurationToCompare.getName().equals(name))
if(configurationToCompare != configuration &&
configurationToCompare.getName() != null &&
configurationToCompare.getName().equals(name))
{
return false;
}
@@ -276,7 +279,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
if (audioBroadcaster != null)
{
audioBroadcaster.receive(audioPacket);
audioBroadcaster.getAudioPacketListener().receive(audioPacket);
}
}
}
@@ -298,7 +301,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
if (audioBroadcaster != null)
{
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
{
@Override
@@ -308,6 +310,10 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
}
});
audioBroadcaster.start();
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
@@ -409,10 +415,14 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
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);
//Delete and recreate the broadcaster for any broadcast configuration changes
String previousChannelName = cleanupMapAssociations(broadcastConfiguration);
deleteBroadcaster(previousChannelName);
//Delay restarting the broadcaster to allow remote server time to cleanup
mThreadPoolManager.scheduleOnce(new DelayedBroadcasterStartup(broadcastConfiguration), 1, TimeUnit.SECONDS);
break;
case CONFIGURATION_DELETE:
deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getName());
@@ -455,8 +465,10 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
* with the new map associations. Remove any broadcaster that is associated with the old configuration name.
*
* @param broadcastConfiguration
* @return previous or current channel name that has been replaced with the current configuration channel name so
* that anything else associated with the previous name can be cleaned up.
*/
private void cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
private String cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
{
String oldName = null;
@@ -471,11 +483,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
if(oldName != null)
{
deleteBroadcaster(oldName);
mBroadcastConfigurationMap.remove(oldName);
}
mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
return oldName;
}
@Override
@@ -620,8 +633,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
@Override
public void accept(Path path)
{
mLog.info("Deleting orphaned temporary stream recording: " + path.toString());
try
{
Files.delete(path);
@@ -639,4 +650,24 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
}
}
}
/**
* Provides delayed startup of a broadcaster to allow for the remote server to complete a disconnection.
*/
public class DelayedBroadcasterStartup implements Runnable
{
private BroadcastConfiguration mBroadcastConfiguration;
public DelayedBroadcasterStartup(BroadcastConfiguration configuration)
{
mBroadcastConfiguration = configuration;
}
@Override
public void run()
{
createBroadcaster(mBroadcastConfiguration);
}
}
}

View File

@@ -18,6 +18,7 @@
******************************************************************************/
package audio.broadcast;
import alias.AliasModel;
import com.jidesoft.swing.JideSplitPane;
import gui.editor.Editor;
import gui.editor.EmptyEditor;
@@ -27,8 +28,11 @@ import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
@@ -43,6 +47,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
private BroadcastModel mBroadcastModel;
private AliasModel mAliasModel;
private SettingsManager mSettingsManager;
private JTable mBroadcastConfigurationTable;
@@ -53,9 +58,10 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
public BroadcastPanel(BroadcastModel broadcastModel, SettingsManager settingManager)
public BroadcastPanel(BroadcastModel broadcastModel, AliasModel aliasModel, SettingsManager settingManager)
{
mBroadcastModel = broadcastModel;
mAliasModel = aliasModel;
mSettingsManager = settingManager;
mEditor = mEmptyEditor;
@@ -68,10 +74,11 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
mBroadcastConfigurationTable = new JTable(mBroadcastModel);
mBroadcastConfigurationTable.setAutoCreateRowSorter(true);
mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setWidth(15);
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setPreferredWidth(1);
mBroadcastConfigurationTable.getColumnModel().getColumn(1).setPreferredWidth(25);
JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
@@ -79,7 +86,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
mNewButton = new JButton("New ...");
mNewButton.setToolTipText("Create a new broadcastAudio configuration");
mNewButton.setToolTipText("Create a new broadcast audio configuration");
mNewButton.addMouseListener(new MouseAdapter()
{
@Override
@@ -88,6 +95,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
JPopupMenu menu = new JPopupMenu();
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.BROADCASTIFY));
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_TCP));
menu.show(e.getComponent(), e.getX(), e.getY());
}
@@ -201,7 +209,8 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
if (selectedConfiguration != null)
{
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mSettingsManager);
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mAliasModel,
mSettingsManager);
}
else
{
@@ -222,10 +231,12 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
public AddBroadcastConfigurationItem(BroadcastServerType type)
{
super(type.toString());
super(type.toString(), mSettingsManager.getImageIcon(type.getIconName(), 14));
mBroadcastServerType = type;
setToolTipText("Add a new " + type.toString() + " audio stream configuration");
addActionListener(new ActionListener()
{
@Override
@@ -234,12 +245,15 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
final BroadcastConfiguration configuration =
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
if(configuration != null)
{
mBroadcastModel.addBroadcastConfiguration(configuration);
mBroadcastModel.addBroadcastConfiguration(configuration);
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
int tableRow = mBroadcastConfigurationTable.convertRowIndexToView(modelRow);
mBroadcastConfigurationTable.changeSelection(tableRow, 0, false, false);}
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
int tableRow = mBroadcastConfigurationTable.convertRowIndexToView(modelRow);
mBroadcastConfigurationTable.changeSelection(tableRow, 0, false, false);
}
}
});
}
}

View File

@@ -21,10 +21,10 @@ package audio.broadcast;
public enum BroadcastServerType
{
BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
ICECAST_HTTP("Icecast 2.4+", null),
ICECAST_TCP("Icecast 2.3", null),
SHOUTCAST_V1("Shoutcast 1.x", null),
SHOUTCAST_V2("Shoutcast 2.x", null),
ICECAST_HTTP("Icecast 2.4+", "images/icecast.png"),
ICECAST_TCP("Icecast 2.3", "images/icecast.png"),
SHOUTCAST_V1("Shoutcast 1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast 2.x", "images/shoutcast.png"),
UNKNOWN("Unknown", null);
private String mLabel;

View File

@@ -60,11 +60,6 @@ public enum BroadcastState
*/
NO_SERVER("No Server", false),
/**
* Connected but paused. No audio is streaming.
*/
PAUSED("Paused", false),
/**
* Initial state with no connection attempted.
*/

View File

@@ -28,10 +28,14 @@ import util.TimeStamp;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class StreamManager implements Listener<AudioPacket>
{
@@ -40,11 +44,11 @@ public class StreamManager implements Listener<AudioPacket>
private ThreadPoolManager mThreadPoolManager;
private AudioBroadcaster mAudioBroadcaster;
private long mDelay;
private Path mTempDirectory;
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
private Runnable mRecorderMonitor;
private ScheduledFuture<?> mRecorderMonitorFuture;
private AtomicBoolean mRunning = new AtomicBoolean();
/**
* Stream manager processes all incoming audio packets and reassembles individual audio streams, converts audio
@@ -52,22 +56,19 @@ public class StreamManager implements Listener<AudioPacket>
* ensure that recordings don't run too long before they are streamed out and to ensure that inactive recordings
* are closed in a timely fashion.
*
* Completed streamable audio recordings are nominated to the output listener for streaming once a specified delay
* has elapsed.
* Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion
*
* @param threadPoolManager for scheduling runnables
* @param audioBroadcaster to receive completed streamable audio recordings
* @param delay from recording start until audio recording is nominated to output listener
* @param tempDirectory where to store temporary audio recordings
*/
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, long delay, Path tempDirectory)
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, Path tempDirectory)
{
assert(tempDirectory != null && Files.isDirectory(tempDirectory));
assert(mThreadPoolManager != null);
mThreadPoolManager = threadPoolManager;
mAudioBroadcaster = audioBroadcaster;
mDelay = delay;
mTempDirectory = tempDirectory;
}
@@ -77,16 +78,15 @@ public class StreamManager implements Listener<AudioPacket>
*/
public void start()
{
if(mRecorderMonitorFuture == null)
if(mRunning.compareAndSet(false, true))
{
if(mRecorderMonitor == null)
{
mRecorderMonitor = new RecorderMonitor();
}
mRecorderMonitorFuture = mThreadPoolManager
.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING, mRecorderMonitor, 2,
TimeUnit.SECONDS);
mRecorderMonitorFuture = mThreadPoolManager.scheduleFixedRate(ThreadPoolManager.ThreadType.AUDIO_PROCESSING,
mRecorderMonitor, 2, TimeUnit.SECONDS);
}
}
@@ -95,24 +95,29 @@ public class StreamManager implements Listener<AudioPacket>
*/
public void stop()
{
if(mRecorderMonitorFuture != null)
if(mRunning.compareAndSet(true, false))
{
mRecorderMonitorFuture.cancel(true);
}
synchronized (mStreamRecorders)
{
mStreamRecorders.entrySet().stream().forEach(entry ->
if(mRecorderMonitorFuture != null)
{
removeRecorder(entry.getKey());
});
mRecorderMonitorFuture.cancel(true);
}
synchronized (mStreamRecorders)
{
List<Integer> streamKeys = new ArrayList<>(mStreamRecorders.keySet());
for(Integer streamKey: streamKeys)
{
removeRecorder(streamKey);
}
}
}
}
@Override
public void receive(AudioPacket audioPacket)
{
if(audioPacket.hasAudioMetadata())
if(mRunning.get() && audioPacket.hasAudioMetadata())
{
synchronized (mStreamRecorders)
{
@@ -168,19 +173,9 @@ public class StreamManager implements Listener<AudioPacket>
recorder.stop();
StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(),
recorder.getMetadata(), recorder.getRecordingLength());
recorder.getMetadata(), recorder.getTimeRecordingStart(), recorder.getRecordingLength());
if(mDelay <= 0)
{
mAudioBroadcaster.receive(streamableAudioRecording);
}
else
{
long waitTimeMillis = mDelay - (System.currentTimeMillis() - recorder.getTimeRecordingStart());
mThreadPoolManager.scheduleOnce(new RecordingDispatcher(streamableAudioRecording),
waitTimeMillis, TimeUnit.MILLISECONDS);
}
mAudioBroadcaster.receive(streamableAudioRecording);
}
}
@@ -200,29 +195,7 @@ public class StreamManager implements Listener<AudioPacket>
}
/**
* Dispatches the streamable audio recording to the output listener.
*/
public class RecordingDispatcher implements Runnable
{
StreamableAudioRecording mStreamableAudioRecording;
public RecordingDispatcher(StreamableAudioRecording recording)
{
mStreamableAudioRecording = recording;
}
@Override
public void run()
{
if(mAudioBroadcaster != null)
{
mAudioBroadcaster.receive(mStreamableAudioRecording);
}
}
}
/**
* Monitors recorders to ensure they don't exceed the maximum life-span allowed for the recording.
* Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording.
*/
public class RecorderMonitor implements Runnable
{

View File

@@ -22,23 +22,26 @@ import audio.metadata.AudioMetadata;
import java.nio.file.Path;
public class StreamableAudioRecording
public class StreamableAudioRecording implements Comparable<StreamableAudioRecording>
{
private Path mPath;
private AudioMetadata mAudioMetadata;
private long mRecordingLengthMilliSeconds;
private long mStartTime;
private long mRecordingLength;
/**
* Audio recording that is ready to be streamed
* @param path to the audio recording file
* @param audioMetadata associated with the recording
* @param start time of recording in milliseconds since epoch
* @param recordingLength in milliseconds
*/
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long recordingLength)
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long start, long recordingLength)
{
mPath = path;
mAudioMetadata = audioMetadata;
mRecordingLengthMilliSeconds = recordingLength;
mStartTime = start;
mRecordingLength = recordingLength;
}
/**
@@ -57,11 +60,29 @@ public class StreamableAudioRecording
return mAudioMetadata;
}
/**
* Recording start time in milliseconds since epoch
*/
public long getStartTime()
{
return mStartTime;
}
/**
* Recording length in milliseconds
*/
public long getRecordingLength()
{
return mRecordingLengthMilliSeconds;
return mRecordingLength;
}
/**
* Implements comparable for sorting recordings based on start time in ascending order
*/
@Override
public int compareTo(StreamableAudioRecording otherRecording)
{
return Long.compare(getStartTime(), otherRecording.getStartTime());
}
}

View File

@@ -18,7 +18,9 @@
******************************************************************************/
package audio.broadcast.broadcastify;
import alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
@@ -33,13 +35,10 @@ import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
public class BroadcastifyConfigurationEditor extends BroadcastConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
private SettingsManager mSettingsManager;
private BroadcastModel mBroadcastModel;
private JTextField mFeedID;
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
@@ -49,10 +48,11 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
private JButton mResetButton;
private JCheckBox mEnabled;
public BroadcastifyConfigurationEditor(BroadcastModel model, SettingsManager settingsManager)
public BroadcastifyConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
SettingsManager settingsManager)
{
mBroadcastModel = model;
mSettingsManager = settingsManager;
super(broadcastModel, aliasModel, settingsManager);
init();
}
@@ -69,37 +69,38 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
add(channelLabel, "span, align center");
add(new JLabel("Feed ID:"));
mFeedID = new JTextField();
mFeedID.getDocument().addDocumentListener(this);
add(mFeedID);
add(new JLabel("Name:"));
mName = new JTextField();
mName.getDocument().addDocumentListener(this);
mName.setToolTipText("Unique name for this stream");
add(mName);
add(new JLabel("Server:"));
mServer = new JTextField();
mServer.getDocument().addDocumentListener(this);
mServer.setToolTipText("Server address (e.g. audio3.broadcastify.com). See MyBCFY feed details, Technicals tab");
add(mServer);
add(new JLabel("Port:"));
mPort = new JTextField();
mPort.getDocument().addDocumentListener(this);
mPort.setToolTipText("Server port number (e.g. 80). See MyBCFY feed details, Technicals tab");
add(mPort);
add(new JLabel("Mount:"));
mMountPoint = new JTextField();
mMountPoint.getDocument().addDocumentListener(this);
mMountPoint.setToolTipText("Mount point (e.g. \\awlkejrlkjsd). See MyBCFY feed details, Technicals tab");
add(mMountPoint);
add(new JLabel("Password:"));
mPassword = new JTextField();
mPassword.getDocument().addDocumentListener(this);
mPassword.setToolTipText("Password. See MyBCFY feed details, Technicals tab");
add(mPassword);
mEnabled = new JCheckBox("Enabled");
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
@@ -112,6 +113,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mSaveButton = new JButton("Save");
mSaveButton.setEnabled(false);
mSaveButton.setToolTipText("Click to save configuration information");
mSaveButton.addActionListener(new ActionListener()
{
@Override
@@ -124,6 +126,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
mResetButton = new JButton("Reset");
mResetButton.setEnabled(false);
mResetButton.setToolTipText("Click to reset any changes you've made\n since the last save");
mResetButton.addActionListener(new ActionListener()
{
@Override
@@ -154,7 +157,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
{
BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
mFeedID.setText(config.getFeedID() != 0 ? String.valueOf(config.getFeedID()) : null);
mName.setText(config.getName());
mServer.setText(config.getHost());
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
@@ -164,7 +166,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
}
else
{
mFeedID.setText(null);
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
@@ -183,8 +184,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
if(validateConfiguration())
{
config.setName(mName.getText());
config.setFeedID(getFeedID());
updateConfigurationName(config, mName.getText());
config.setHost(mServer.getText());
config.setPort(getPort());
config.setMountPoint(mMountPoint.getText());
@@ -219,12 +219,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
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."))
{
@@ -252,90 +246,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
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();
if(feedID != null && !feedID.isEmpty())
{
try
{
return Integer.parseInt(feedID);
}
catch(Exception e)
{
//Do nothing, we couldn't parse the number value
}
}
return 0;
}
/**
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
*/

View File

@@ -1,93 +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.broadcastify;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import java.awt.*;
public class BroadcastifySelectionDialog extends JFrame
{
public BroadcastifySelectionDialog()
{
setTitle("Broadcastify Channels");
setSize(new Dimension(400, 400));
add(new SelectionPanel());
}
public class SelectionPanel extends JPanel
{
private JTextField mUserName;
private JTextField mPassword;
private JButton mConnectButton;
public SelectionPanel()
{
init();
}
private void init()
{
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][grow,fill]" ) );
add(new JLabel("Enter your credentials and click 'Connect'"), "span, align center");
add(new JLabel("User Name:"));
mUserName = new JTextField();
add(mUserName);
add(new JLabel("Password:"));
mPassword = new JTextField();
add(mPassword);
mConnectButton = new JButton("Connect");
add(mConnectButton,"span, align center");
add(new JSeparator(JSeparator.HORIZONTAL), "span,growx");
}
}
public static void main(String[] args)
{
BroadcastifySelectionDialog dialog = new BroadcastifySelectionDialog();
try
{
EventQueue.invokeAndWait(new Runnable()
{
@Override
public void run()
{
dialog.setVisible(true);
dialog.setLocationRelativeTo(null);
}
});
}
catch(Exception e)
{
e.printStackTrace();
}
}
}

View File

@@ -18,29 +18,19 @@
******************************************************************************/
package audio.broadcast.icecast;
import alias.Alias;
import alias.id.broadcast.BroadcastChannel;
import audio.AudioPacket;
import audio.broadcast.AudioBroadcaster;
import audio.broadcast.BroadcastFactory;
import audio.broadcast.BroadcastFormat;
import audio.broadcast.BroadcastState;
import audio.metadata.AudioMetadata;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ThreadPoolManager;
import org.asynchttpclient.AsyncCompletionHandler;
import org.asynchttpclient.AsyncHandler;
import org.asynchttpclient.BoundRequestBuilder;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.ListenableFuture;
import org.asynchttpclient.Realm;
import org.asynchttpclient.Response;
import org.asynchttpclient.uri.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import record.wave.AudioPacketMonoWaveReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -51,8 +41,6 @@ import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
@@ -90,7 +78,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
/**
* Configuration information
* Icecast broadcast configuration
*/
private IcecastTCPConfiguration getConfiguration()
{
@@ -98,7 +86,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
/**
* Broadcast any queued audio packets using Icecast V2 Protocol
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
@@ -117,10 +105,10 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
catch(SocketException se)
{
//The remote server likely disconnected - setup to reconnect
mLog.error("Socket Exception on Icecast TCP Audio Broadcaster -- disconnecting and resetting", se);
disconnect(false);
mLog.error("Resetting Icecast TCP Audio Broadcaster - socket error: " + se.getMessage());
disconnect();
}
catch(IOException e)
catch(Exception e)
{
completed = true;
mLog.error("Error sending audio", e);
@@ -133,6 +121,9 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
}
/**
* Broadcasts an audio metadata update
*/
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{
@@ -214,16 +205,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
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);
mDefaultAsyncHttpClient.executeRequest(builder.build());
}
catch(Exception e)
{
@@ -237,9 +219,11 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
/**
* Indicates if the audio handler is currently connected to the remote server and capable of streaming audio.
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
* is currently connected to the remote server following any connection attempts.
*
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
*
* This method will attempt to establish a connection or reconnect to the streaming server.
* @return true if the audio handler can stream audio
*/
private boolean connect()
@@ -254,10 +238,12 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
/**
* Disconnect from the remote server
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
*/
public void disconnect(boolean shutdown)
public void disconnect()
{
mLog.info("Disconnecting Icecast TCP audio broadcaster");
if(mOutputStream != null)
{
try
@@ -301,19 +287,16 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
mSocket = null;
}
if(shutdown)
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
else
if(!isErrorState())
{
setBroadcastState(BroadcastState.READY);
}
}
/**
* Creates a connnection to the remote server using the shoutcast configuration information. Once disconnected
* following a successful connection, attempts to reestablish a connection on a set interval
* Creates a connection to the remote server using the icecast configuration information. Once disconnected
* following a successful connection, successive calls to this method will attempt to reestablish a connection on a
* minimum reconnection attempt interval
*/
private void createConnection()
{
@@ -340,12 +323,6 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
if(getConfiguration().hasName())
{
sb.append(IcecastHeader.NAME.getValue()).append(SEPARATOR)
.append(getConfiguration().getName()).append(TERMINATOR);
}
if(getConfiguration().hasGenre())
{
sb.append(IcecastHeader.GENRE.getValue()).append(SEPARATOR)
@@ -374,13 +351,17 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
mLog.debug("Checking response ...");
checkResponse();
checkConnectionAttemptResponse();
}
mConnecting.set(false);
}
}
/**
* Creates an audio metadata description string that can optionally be included when connecting to the remote
* broadcast server.
*/
private String getAudioInfoMetadata()
{
StringBuilder sb = new StringBuilder();
@@ -422,13 +403,13 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
/**
* Creates a socket connection to the remote server and sets the state to CONNECTING.
* Creates a socket connection to the remote server and sets the state to CONNECTING or indicates an error state
* if the socket is unable to connect to the remote server with the current configuration information.
*/
private void createSocket()
{
if(mSocket == null)
{
mLog.debug("Creating socket");
try
{
mSocket = new Socket(getConfiguration().getHost(),
@@ -492,6 +473,7 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
/**
* Sends the string data to the remote server
*
* @param data to send
* @throws IOException if there is an error communicating with the remote server
*/
@@ -525,6 +507,11 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
}
}
/**
* Obtains any pending response from the remote server.
* @return string response
* @throws IOException if there is an error
*/
private String getResponse() throws IOException
{
if(mInputStream != null)
@@ -549,7 +536,11 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
return null;
}
private void checkResponse()
/**
* Checks for a remote server response following a connection attempt and evaluates the response for indication of a
* successful connection or an error state
*/
private void checkConnectionAttemptResponse()
{
String response = null;
@@ -575,86 +566,23 @@ public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if(response.startsWith("HTTP/1.0 403 Mountpoint in use"))
{
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
}
else if(response.contains("Invalid Password") || response.contains("Authentication Required"))
{
setBroadcastState(BroadcastState.INVALID_PASSWORD);
}
else
{
mLog.debug("Unrecognized server response:" + response);
mLog.error("Unrecognized server response:" + response);
setBroadcastState(BroadcastState.ERROR);
}
}
else
{
setBroadcastState(BroadcastState.CONNECTED);
mLog.debug("Response was empty");
}
}
public static void main(String[] args)
{
boolean test = false;
IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3);
String streamName = "Broadcastify SDRTrunk #2";
config.setName(streamName);
config.setHost("audio3.broadcastify.com");
config.setPort(80);
config.setMountPoint("/k0yrdpx7zn4h");
config.setUserName("source");
config.setPassword("k8j9405n");
config.setDescription("SDRTrunk Test Feed #2");
config.setGenre("Scanner");
config.setPublic(false);
config.setURL("http://www.radioreference.com");
config.setBitRate(16);
AudioMetadata metadata = new AudioMetadata(0, false);
Alias toAlias = new Alias("ToAlias");
toAlias.addAliasID(new BroadcastChannel(streamName));
metadata.receive(new Metadata(MetadataType.TO, "1234", toAlias ));
if(test)
{
mLog.debug("Auth:" + config.getEncodedCredentials());
}
else
{
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
Path path = Paths.get("/home/denny/Music/PCM.wav");
mLog.debug("Opening: " + path.toString());
mLog.debug("Registering and starting audio playback");
int playCount = 0;
while(true)
{
mLog.debug("Playback started [" + path.toString() + "]");
toAlias.setName("Playback #" + playCount++);
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
{
reader.setMetadata(metadata);
reader.setListener(audioBroadcaster);
reader.read();
audioBroadcaster.receive(new AudioPacket(AudioPacket.Type.END, metadata));
}
catch (IOException e)
{
mLog.error("Error", e);
}
mLog.debug("Playback ended [" + path.toString() + "]");
}
}
}
}

View File

@@ -108,8 +108,6 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
sb.append(getPassword());
}
mLog.debug("Connection String: " + sb.toString());
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
return "Basic " + base64;

View File

@@ -0,0 +1,269 @@
/*******************************************************************************
* 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.icecast;
import alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
import gui.editor.DocumentListenerEditor;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class IcecastTCPConfigurationEditor extends BroadcastConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfigurationEditor.class );
private JTextField mName;
private JTextField mServer;
private JTextField mPort;
private JTextField mMountPoint;
private JTextField mPassword;
private JButton mSaveButton;
private JButton mResetButton;
private JCheckBox mEnabled;
public IcecastTCPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
SettingsManager settingsManager)
{
super(broadcastModel, aliasModel, settingsManager);
init();
}
private void init()
{
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][grow,fill]" ) );
setPreferredSize(new Dimension(150,400));
JLabel channelLabel = new JLabel("Icecast 2.3 Stream");
ImageIcon icon = mSettingsManager.getImageIcon(BroadcastServerType.ICECAST_TCP.getIconName(), 25);
channelLabel.setIcon(icon);
add(channelLabel, "span, align center");
add(new JLabel("Name:"));
mName = new JTextField();
mName.getDocument().addDocumentListener(this);
mName.setToolTipText("Unique name for this stream");
add(mName);
add(new JLabel("Server:"));
mServer = new JTextField();
mServer.getDocument().addDocumentListener(this);
mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
add(mServer);
add(new JLabel("Port:"));
mPort = new JTextField();
mPort.getDocument().addDocumentListener(this);
mPort.setToolTipText("Server port number (e.g. 80)");
add(mPort);
add(new JLabel("Mount:"));
mMountPoint = new JTextField();
mMountPoint.getDocument().addDocumentListener(this);
mMountPoint.setToolTipText("Mount point (e.g. \\12345)");
add(mMountPoint);
add(new JLabel("Password:"));
mPassword = new JTextField();
mPassword.getDocument().addDocumentListener(this);
mPassword.setToolTipText("Password for the stream");
add(mPassword);
mEnabled = new JCheckBox("Enabled");
mEnabled.setToolTipText("Enable (checked) or disable this stream");
mEnabled.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(mEnabled, "span");
mSaveButton = new JButton("Save");
mSaveButton.setEnabled(false);
mSaveButton.setToolTipText("Click to save configuration information");
mSaveButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
save();
}
});
add(mSaveButton);
mResetButton = new JButton("Reset");
mResetButton.setEnabled(false);
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
mResetButton.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(false);
setItem(getItem());
}
});
add(mResetButton);
}
@Override
public void setModified(boolean modified)
{
super.setModified(modified);
mSaveButton.setEnabled(modified);
mResetButton.setEnabled(modified);
}
@Override
public void setItem(BroadcastConfiguration item)
{
super.setItem(item);
if(hasItem())
{
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
mName.setText(config.getName());
mServer.setText(config.getHost());
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
mMountPoint.setText(config.getMountPoint());
mPassword.setText(config.getPassword());
mEnabled.setSelected(config.isEnabled());
}
else
{
mName.setText(null);
mServer.setText(null);
mPort.setText(null);
mMountPoint.setText(null);
mPassword.setText(null);
mEnabled.setSelected(false);
}
setModified(false);
}
@Override
public void save()
{
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
if(validateConfiguration())
{
updateConfigurationName(config, mName.getText());
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(IcecastTCPConfigurationEditor.this,
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
mName.requestFocus();
return false;
}
//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;
}
/**
* 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();
if(port != null && !port.isEmpty())
{
try
{
return Integer.parseInt(port);
}
catch(Exception e)
{
//Do nothing, we couldn't parse the number value
}
}
return 0;
}
}

View File

@@ -73,6 +73,12 @@ public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
return (ShoutcastV1Configuration)getBroadcastConfiguration();
}
@Override
protected void disconnect()
{
//TODO: implement a disconnect
}
@Override
protected void broadcastMetadata(AudioMetadata metadata)
{