mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
#30 WIP - broadcastify multi-channel streaming is functional.
This commit is contained in:
@@ -212,6 +212,7 @@ public class AliasList implements Listener<AliasEvent>
|
||||
mTalkgroup.put(tgid, alias);
|
||||
}
|
||||
break;
|
||||
case BROADCAST_CHANNEL:
|
||||
case NON_RECORDABLE:
|
||||
case PRIORITY:
|
||||
//We don't maintain lookups for these items
|
||||
|
||||
@@ -23,16 +23,42 @@ import alias.id.AliasIDType;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
public class BroadcastChannel extends AliasID
|
||||
public class BroadcastChannel extends AliasID implements Comparable<BroadcastChannel>
|
||||
{
|
||||
private String mChannelName;
|
||||
|
||||
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")
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -19,12 +19,15 @@
|
||||
package alias.id.broadcast;
|
||||
|
||||
import alias.id.AliasID;
|
||||
import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastModel;
|
||||
import gui.editor.DocumentListenerEditor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.List;
|
||||
@@ -40,16 +43,14 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
|
||||
+ "streaming server";
|
||||
|
||||
private JComboBox<String> mBroadcastConfigurations;
|
||||
private BroadcastModel mBroadcastModel;
|
||||
|
||||
public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel)
|
||||
{
|
||||
List<String> channelNames = broadcastModel.getBroadcastConfigurationNames();
|
||||
|
||||
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
|
||||
|
||||
setItem( aliasID );
|
||||
mBroadcastModel = broadcastModel;
|
||||
|
||||
initGUI();
|
||||
setItem( aliasID );
|
||||
}
|
||||
|
||||
private void initGUI()
|
||||
@@ -58,15 +59,20 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
|
||||
|
||||
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");
|
||||
|
||||
BroadcastChannel channel = (BroadcastChannel)getItem();
|
||||
|
||||
if(channel.isValid())
|
||||
{
|
||||
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
|
||||
}
|
||||
|
||||
JLabel help = new JLabel( "Help ..." );
|
||||
help.setForeground( Color.BLUE.brighter() );
|
||||
help.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
|
||||
@@ -82,10 +88,42 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
|
||||
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
425
src/audio/broadcast/AudioBroadcaster.java
Normal file
425
src/audio/broadcast/AudioBroadcaster.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
@@ -31,17 +32,24 @@ import java.net.SocketAddress;
|
||||
@XmlRootElement(name = "stream")
|
||||
public abstract class BroadcastConfiguration
|
||||
{
|
||||
private BroadcastFormat mBroadcastFormat;
|
||||
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
|
||||
private String mName;
|
||||
private String mHost;
|
||||
private int mPort;
|
||||
private String mPassword;
|
||||
private long mDelay;
|
||||
private boolean mEnabled = true;
|
||||
|
||||
public BroadcastConfiguration()
|
||||
{
|
||||
//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.
|
||||
*/
|
||||
@@ -57,7 +65,7 @@ public abstract class BroadcastConfiguration
|
||||
public abstract BroadcastServerType getBroadcastServerType();
|
||||
|
||||
/**
|
||||
* Name identifying this broadcast configuration.
|
||||
* Name identifying this broadcastAudio configuration.
|
||||
*/
|
||||
@XmlAttribute(name = "name")
|
||||
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
|
||||
*/
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
******************************************************************************/
|
||||
package audio.broadcast;
|
||||
|
||||
public class BroadcastConfigurationEvent
|
||||
public class BroadcastEvent
|
||||
{
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
private AudioBroadcaster mAudioBroadcaster;
|
||||
private Event mEvent;
|
||||
|
||||
/**
|
||||
@@ -28,17 +29,44 @@ public class BroadcastConfigurationEvent
|
||||
* @param configuration - alias that changed
|
||||
* @param event - change event
|
||||
*/
|
||||
public BroadcastConfigurationEvent(BroadcastConfiguration configuration, Event event )
|
||||
public BroadcastEvent(BroadcastConfiguration configuration, Event event )
|
||||
{
|
||||
mBroadcastConfiguration = configuration;
|
||||
mEvent = event;
|
||||
}
|
||||
|
||||
|
||||
public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event )
|
||||
{
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
mEvent = event;
|
||||
}
|
||||
|
||||
public BroadcastConfiguration getBroadcastConfiguration()
|
||||
{
|
||||
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()
|
||||
{
|
||||
return mEvent;
|
||||
@@ -49,8 +77,14 @@ public class BroadcastConfigurationEvent
|
||||
*/
|
||||
public enum Event
|
||||
{
|
||||
ADD,
|
||||
CHANGE,
|
||||
DELETE;
|
||||
BROADCASTER_ADD,
|
||||
BROADCASTER_QUEUE_CHANGE,
|
||||
BROADCASTER_STATE_CHANGE,
|
||||
BROADCASTER_STREAMED_COUNT_CHANGE,
|
||||
BROADCASTER_DELETE,
|
||||
|
||||
CONFIGURATION_ADD,
|
||||
CONFIGURATION_CHANGE,
|
||||
CONFIGURATION_DELETE;
|
||||
}
|
||||
}
|
||||
@@ -18,27 +18,32 @@
|
||||
******************************************************************************/
|
||||
package audio.broadcast;
|
||||
|
||||
import audio.AudioPacket;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
|
||||
import audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||
import audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import audio.broadcast.icecast.IcecastHTTPBroadcaster;
|
||||
import audio.broadcast.icecast.IcecastTCPBroadcaster;
|
||||
import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
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.ShoutcastV2Broadcaster;
|
||||
import audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
|
||||
import audio.convert.IAudioConverter;
|
||||
import audio.convert.MP3AudioConverter;
|
||||
import controller.ThreadPoolManager;
|
||||
import gui.editor.Editor;
|
||||
import gui.editor.EmptyEditor;
|
||||
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import record.AudioRecorder;
|
||||
import record.mp3.MP3Recorder;
|
||||
import settings.SettingsManager;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BroadcastFactory
|
||||
{
|
||||
@@ -54,34 +59,35 @@ public class BroadcastFactory
|
||||
* @param configuration describing the server and audio types
|
||||
* @return configured broadcaster or null
|
||||
*/
|
||||
public static Broadcaster getBroadcaster(ThreadPoolManager threadPoolManager,
|
||||
BroadcastConfiguration configuration)
|
||||
public static AudioBroadcaster getBroadcaster(DefaultAsyncHttpClient defaultAsyncHttpClient,
|
||||
ThreadPoolManager threadPoolManager,
|
||||
BroadcastConfiguration configuration)
|
||||
{
|
||||
IAudioConverter converter = getAudioConverter(configuration);
|
||||
|
||||
if(converter != null)
|
||||
if(configuration != null)
|
||||
{
|
||||
switch(configuration.getBroadcastServerType())
|
||||
IAudioConverter converter = getAudioConverter(configuration);
|
||||
|
||||
if(converter != null)
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPBroadcaster(threadPoolManager,
|
||||
(BroadcastifyConfiguration)configuration, converter);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPBroadcaster(threadPoolManager,
|
||||
(IcecastHTTPConfiguration)configuration, converter);
|
||||
case ICECAST_TCP:
|
||||
return new IcecastTCPBroadcaster(threadPoolManager,
|
||||
(IcecastTCPConfiguration)configuration, converter);
|
||||
case SHOUTCAST_V1:
|
||||
return new ShoutcastV1Broadcaster(threadPoolManager,
|
||||
(ShoutcastV1Configuration)configuration, converter);
|
||||
case SHOUTCAST_V2:
|
||||
return new ShoutcastV2Broadcaster(threadPoolManager,
|
||||
(ShoutcastV2Configuration)configuration, converter);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
mLog.info("Unrecognized broadcast configuration: " + configuration.getBroadcastFormat().name());
|
||||
break;
|
||||
switch(configuration.getBroadcastServerType())
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
|
||||
(BroadcastifyConfiguration)configuration);
|
||||
case ICECAST_TCP:
|
||||
return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
|
||||
(IcecastTCPConfiguration)configuration);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPAudioBroadcaster(threadPoolManager, (IcecastHTTPConfiguration)configuration);
|
||||
case SHOUTCAST_V1:
|
||||
return new ShoutcastV1AudioBroadcaster(threadPoolManager, (ShoutcastV1Configuration)configuration);
|
||||
case SHOUTCAST_V2:
|
||||
return new ShoutcastV2AudioBroadcaster(threadPoolManager, (ShoutcastV2Configuration)configuration);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,14 +106,14 @@ public class BroadcastFactory
|
||||
case MP3:
|
||||
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
|
||||
default:
|
||||
mLog.info("Unrecognized broadcast format: " + configuration.getBroadcastFormat().name());
|
||||
mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
|
||||
}
|
||||
|
||||
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 format for the output audio format
|
||||
@@ -129,7 +135,7 @@ public class BroadcastFactory
|
||||
return new ShoutcastV2Configuration(format);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
mLog.info("Unrecognized broadcast server type: " + serverType.name());
|
||||
mLog.info("Unrecognized broadcastAudio server type: " + serverType.name());
|
||||
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 broadcastModel model for broadcast configurations
|
||||
* @return an editor for the specified broadcast configuration
|
||||
* @param broadcastModel model for broadcastAudio configurations
|
||||
* @return an editor for the specified broadcastAudio configuration
|
||||
*/
|
||||
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
|
||||
BroadcastModel broadcastModel)
|
||||
BroadcastModel broadcastModel,
|
||||
SettingsManager settingsManager)
|
||||
{
|
||||
Editor<BroadcastConfiguration> editor;
|
||||
|
||||
switch (configuration.getBroadcastServerType())
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
editor = new BroadcastifyConfigurationEditor(broadcastModel);
|
||||
editor = new BroadcastifyConfigurationEditor(broadcastModel, settingsManager);
|
||||
break;
|
||||
default:
|
||||
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)
|
||||
{
|
||||
@@ -172,8 +179,28 @@ public class BroadcastFactory
|
||||
case MP3:
|
||||
return new MP3Recorder(path);
|
||||
default:
|
||||
mLog.debug("Unrecognized broadcast format [" + broadcastFormat + "] cannot create audio recorder");
|
||||
mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public enum BroadcastFormat
|
||||
private BroadcastFormat(String value, String fileExtension)
|
||||
{
|
||||
mValue = value;
|
||||
mFileExtension = fileExtension;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
|
||||
@@ -18,28 +18,70 @@
|
||||
******************************************************************************/
|
||||
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 settings.SettingsManager;
|
||||
|
||||
import javax.swing.*;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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 static final int COLUMN_SERVER_TYPE = 1;
|
||||
private static final int COLUMN_SERVER_ADDRESS = 2;
|
||||
private final static Logger mLog = LoggerFactory.getLogger( BroadcastModel.class );
|
||||
|
||||
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 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()
|
||||
{
|
||||
@@ -54,7 +96,7 @@ public class BroadcastModel extends AbstractTableModel
|
||||
}
|
||||
|
||||
/**
|
||||
* Current list of broadcast configurations
|
||||
* Current list of broadcastAudio configurations
|
||||
*/
|
||||
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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if(configuration != null && mBroadcastConfigurations.contains(configuration))
|
||||
{
|
||||
int index = mBroadcastConfigurations.indexOf(configuration);
|
||||
mBroadcastConfigurationMap.put(configuration.getName(), configuration);
|
||||
|
||||
mBroadcastConfigurations.remove(configuration);
|
||||
|
||||
fireTableRowsDeleted( index, index );
|
||||
|
||||
broadcast( new BroadcastConfigurationEvent( configuration, BroadcastConfigurationEvent.Event.DELETE ) );
|
||||
process( new BroadcastEvent( configuration, BroadcastEvent.Event.CONFIGURATION_ADD) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() );
|
||||
|
||||
@@ -134,6 +396,88 @@ public class BroadcastModel extends AbstractTableModel
|
||||
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
|
||||
public int getRowCount()
|
||||
{
|
||||
@@ -143,7 +487,7 @@ public class BroadcastModel extends AbstractTableModel
|
||||
@Override
|
||||
public int getColumnCount()
|
||||
{
|
||||
return 3;
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -157,12 +501,47 @@ public class BroadcastModel extends AbstractTableModel
|
||||
{
|
||||
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();
|
||||
case COLUMN_SERVER_TYPE:
|
||||
return configuration.getBroadcastServerType().toString();
|
||||
case COLUMN_SERVER_ADDRESS:
|
||||
return configuration.getHost();
|
||||
case COLUMN_BROADCASTER_STATUS:
|
||||
AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
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:
|
||||
break;
|
||||
}
|
||||
@@ -172,6 +551,17 @@ public class BroadcastModel extends AbstractTableModel
|
||||
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
|
||||
*/
|
||||
@@ -193,14 +583,60 @@ public class BroadcastModel extends AbstractTableModel
|
||||
{
|
||||
switch(column)
|
||||
{
|
||||
case COLUMN_CHANNEL_NAME:
|
||||
return "Channel";
|
||||
case COLUMN_SERVER_TYPE:
|
||||
return "Server";
|
||||
case COLUMN_SERVER_ADDRESS:
|
||||
return "Address";
|
||||
case COLUMN_SERVER_ICON:
|
||||
return null;
|
||||
case COLUMN_STREAM_NAME:
|
||||
return "Name";
|
||||
case COLUMN_BROADCASTER_STATUS:
|
||||
return "Status";
|
||||
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||
return "Queued";
|
||||
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||
return "Streamed";
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,15 +18,13 @@
|
||||
******************************************************************************/
|
||||
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 gui.editor.Editor;
|
||||
import gui.editor.EmptyEditor;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import settings.SettingsManager;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
@@ -38,22 +36,27 @@ import java.awt.event.MouseEvent;
|
||||
|
||||
public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
|
||||
{
|
||||
private static final String NEW_ALIAS = "New";
|
||||
private static final String COPY_ALIAS = "Copy";
|
||||
private static final String DELETE_ALIAS = "Delete";
|
||||
private final static Logger mLog = LoggerFactory.getLogger( BroadcastPanel.class );
|
||||
|
||||
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 SettingsManager mSettingsManager;
|
||||
|
||||
private JTable mBroadcastConfigurationTable;
|
||||
private JideSplitPane mSplitPane;
|
||||
private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:");
|
||||
private Editor<BroadcastConfiguration> mEditor;
|
||||
private JButton mNewButton = new JButton(NEW_ALIAS);
|
||||
private JButton mCopyButton = new JButton(COPY_ALIAS);
|
||||
private JButton mDeleteButton = new JButton(DELETE_ALIAS);
|
||||
private JButton mNewButton = new JButton(NEW_BROADCAST_CONFIGURATION);
|
||||
private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
|
||||
private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
|
||||
|
||||
public BroadcastPanel(BroadcastModel broadcastModel)
|
||||
public BroadcastPanel(BroadcastModel broadcastModel, SettingsManager settingManager)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
mSettingsManager = settingManager;
|
||||
mEditor = mEmptyEditor;
|
||||
|
||||
init();
|
||||
@@ -68,6 +71,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
|
||||
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
|
||||
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setWidth(15);
|
||||
|
||||
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]", "[]"));
|
||||
|
||||
mNewButton = new JButton("New ...");
|
||||
mNewButton.setToolTipText("Create a new broadcast configuration");
|
||||
mNewButton.setToolTipText("Create a new broadcastAudio configuration");
|
||||
mNewButton.addMouseListener(new MouseAdapter()
|
||||
{
|
||||
@Override
|
||||
@@ -121,18 +125,62 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
@Override
|
||||
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
|
||||
public void valueChanged(ListSelectionEvent e)
|
||||
{
|
||||
if (!e.getValueIsAdjusting())
|
||||
{
|
||||
int row = mBroadcastConfigurationTable.getSelectedRow();
|
||||
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(row);
|
||||
BroadcastConfiguration selectedConfiguration = getSelectedBroadcastConfiguration();
|
||||
|
||||
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getConfigurationAt(modelRow);
|
||||
//Enable buttons accordingly
|
||||
mCopyButton.setEnabled(selectedConfiguration != null);
|
||||
mDeleteButton.setEnabled(selectedConfiguration != null);
|
||||
|
||||
if (mEditor.isModified())
|
||||
{
|
||||
@@ -153,7 +201,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
|
||||
if (selectedConfiguration != null)
|
||||
{
|
||||
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel);
|
||||
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mSettingsManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -186,6 +234,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
||||
final BroadcastConfiguration configuration =
|
||||
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
|
||||
|
||||
|
||||
mBroadcastModel.addBroadcastConfiguration(configuration);
|
||||
|
||||
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
|
||||
|
||||
@@ -20,36 +20,29 @@ package audio.broadcast;
|
||||
|
||||
public enum BroadcastServerType
|
||||
{
|
||||
/**
|
||||
* Broadcastify is currently using Icecast Server 2.3.2
|
||||
*/
|
||||
BROADCASTIFY("Broadcastify"),
|
||||
|
||||
/**
|
||||
* 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");
|
||||
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),
|
||||
UNKNOWN("Unknown", null);
|
||||
|
||||
private String mLabel;
|
||||
private String mIconName;
|
||||
|
||||
BroadcastServerType(String label)
|
||||
BroadcastServerType(String label, String iconName)
|
||||
{
|
||||
mLabel = label;
|
||||
mIconName = iconName;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public String getIconName()
|
||||
{
|
||||
return mIconName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public enum BroadcastState
|
||||
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),
|
||||
|
||||
@@ -36,7 +36,7 @@ public enum BroadcastState
|
||||
CONNECTING("Connecting", false),
|
||||
|
||||
/**
|
||||
* Disconnected from the broadcast server
|
||||
* Disconnected from the broadcastAudio server
|
||||
*/
|
||||
DISCONNECTED("Disconnected", true),
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,7 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
|
||||
|
||||
private ThreadPoolManager mThreadPoolManager;
|
||||
private BroadcastFormat mBroadcastFormat;
|
||||
private Listener<StreamableAudioRecording> mOutputListener;
|
||||
private AudioBroadcaster mAudioBroadcaster;
|
||||
private long mDelay;
|
||||
private Path mTempDirectory;
|
||||
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
|
||||
@@ -57,24 +56,17 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
* has elapsed.
|
||||
*
|
||||
* @param threadPoolManager for scheduling runnables
|
||||
* @param broadcastFormat defines the output recording format
|
||||
* @param outputListener to receive completed streamable audio recordings
|
||||
* @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,
|
||||
BroadcastFormat broadcastFormat,
|
||||
Listener<StreamableAudioRecording> outputListener,
|
||||
long delay,
|
||||
Path tempDirectory)
|
||||
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, long delay, Path tempDirectory)
|
||||
{
|
||||
assert(tempDirectory != null && Files.isDirectory(tempDirectory));
|
||||
assert(mThreadPoolManager != null);
|
||||
assert(mBroadcastFormat != null);
|
||||
|
||||
mThreadPoolManager = threadPoolManager;
|
||||
mBroadcastFormat = broadcastFormat;
|
||||
mOutputListener = outputListener;
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
mDelay = delay;
|
||||
mTempDirectory = tempDirectory;
|
||||
}
|
||||
@@ -142,7 +134,7 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
else
|
||||
{
|
||||
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
|
||||
mBroadcastFormat);
|
||||
mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat());
|
||||
recorder.start(mThreadPoolManager.getScheduledExecutorService());
|
||||
recorder.receive(audioPacket);
|
||||
mStreamRecorders.put(sourceChannelID, recorder);
|
||||
@@ -176,11 +168,11 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
recorder.stop();
|
||||
|
||||
StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(),
|
||||
recorder.getMetadata());
|
||||
recorder.getMetadata(), recorder.getRecordingLength());
|
||||
|
||||
if(mDelay <= 0)
|
||||
{
|
||||
mOutputListener.receive(streamableAudioRecording);
|
||||
mAudioBroadcaster.receive(streamableAudioRecording);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -198,9 +190,9 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
private Path getTemporaryRecordingPath()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("temporary_streaming_file_");
|
||||
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
|
||||
sb.append( TimeStamp.getLongTimeStamp( "_" ) );
|
||||
sb.append( mBroadcastFormat.getFileExtension() );
|
||||
sb.append( mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat().getFileExtension() );
|
||||
|
||||
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
|
||||
|
||||
@@ -222,9 +214,9 @@ public class StreamManager implements Listener<AudioPacket>
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(mOutputListener != null)
|
||||
if(mAudioBroadcaster != null)
|
||||
{
|
||||
mOutputListener.receive(mStreamableAudioRecording);
|
||||
mAudioBroadcaster.receive(mStreamableAudioRecording);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,19 @@ public class StreamableAudioRecording
|
||||
{
|
||||
private Path mPath;
|
||||
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;
|
||||
mAudioMetadata = audioMetadata;
|
||||
mRecordingLengthMilliSeconds = recordingLength;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,4 +56,12 @@ public class StreamableAudioRecording
|
||||
{
|
||||
return mAudioMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording length in milliseconds
|
||||
*/
|
||||
public long getRecordingLength()
|
||||
{
|
||||
return mRecordingLengthMilliSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
******************************************************************************/
|
||||
package audio.broadcast.broadcastify;
|
||||
|
||||
import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastFormat;
|
||||
import audio.broadcast.BroadcastServerType;
|
||||
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
@@ -26,11 +27,12 @@ import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
public int mFeedID;
|
||||
private int mFeedID;
|
||||
|
||||
public BroadcastifyConfiguration()
|
||||
{
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +49,34 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
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
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
@@ -63,4 +93,10 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return super.isValid() && (mFeedID > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
package audio.broadcast.broadcastify;
|
||||
|
||||
import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastConfigurationEvent;
|
||||
import audio.broadcast.BroadcastEvent;
|
||||
import audio.broadcast.BroadcastModel;
|
||||
import audio.broadcast.BroadcastServerType;
|
||||
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.*;
|
||||
@@ -31,6 +35,9 @@ import java.awt.event.ActionListener;
|
||||
|
||||
public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
|
||||
|
||||
private SettingsManager mSettingsManager;
|
||||
private BroadcastModel mBroadcastModel;
|
||||
private JTextField mFeedID;
|
||||
private JTextField mName;
|
||||
@@ -40,24 +47,27 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
private JTextField mPassword;
|
||||
private JButton mSaveButton;
|
||||
private JButton mResetButton;
|
||||
private JCheckBox mEnabled;
|
||||
|
||||
public BroadcastifyConfigurationEditor(BroadcastModel model)
|
||||
public BroadcastifyConfigurationEditor(BroadcastModel model, SettingsManager settingsManager)
|
||||
{
|
||||
mBroadcastModel = model;
|
||||
mSettingsManager = settingsManager;
|
||||
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));
|
||||
|
||||
add(new JLabel("Broadcastify Channel"), "span, align center");
|
||||
|
||||
JButton selectButton = new JButton("Select ...");
|
||||
add(selectButton, "wrap");
|
||||
JLabel channelLabel = new JLabel("Broadcastify Stream");
|
||||
|
||||
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:"));
|
||||
mFeedID = new JTextField();
|
||||
@@ -89,6 +99,17 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
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.setEnabled(false);
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
@@ -139,6 +160,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
mMountPoint.setText(config.getMountPoint());
|
||||
mPassword.setText(config.getPassword());
|
||||
mEnabled.setSelected(config.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -148,6 +170,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
mPort.setText(null);
|
||||
mMountPoint.setText(null);
|
||||
mPassword.setText(null);
|
||||
mEnabled.setSelected(false);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
@@ -158,14 +181,151 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
{
|
||||
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();
|
||||
|
||||
if(feedID != null && !feedID.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
int id = Integer.parseInt(feedID);
|
||||
config.setFeedID(id);
|
||||
return Integer.parseInt(feedID);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -173,17 +333,21 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
}
|
||||
}
|
||||
|
||||
config.setName(mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
int number = Integer.parseInt(port);
|
||||
config.setPort(number);
|
||||
return Integer.parseInt(port);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -191,11 +355,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
||||
}
|
||||
}
|
||||
|
||||
config.setMountPoint(mMountPoint.getText());
|
||||
config.setPassword(mPassword.getText());
|
||||
|
||||
setModified(false);
|
||||
|
||||
mBroadcastModel.broadcast(new BroadcastConfigurationEvent(getItem(), BroadcastConfigurationEvent.Event.CHANGE));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
package audio.broadcast.icecast;
|
||||
|
||||
import audio.AudioPacket;
|
||||
import audio.broadcast.AudioBroadcaster;
|
||||
import audio.broadcast.BroadcastFormat;
|
||||
import audio.broadcast.BroadcastState;
|
||||
import audio.broadcast.Broadcaster;
|
||||
import audio.broadcast.BroadcastFactory;
|
||||
import audio.convert.IAudioConverter;
|
||||
import audio.metadata.AudioMetadata;
|
||||
import controller.ThreadPoolManager;
|
||||
import org.asynchttpclient.AsyncHandler;
|
||||
import org.asynchttpclient.BoundRequestBuilder;
|
||||
@@ -53,9 +53,9 @@ import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
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 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
|
||||
*
|
||||
* 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 audioConverter
|
||||
*/
|
||||
public IcecastHTTPBroadcaster(ThreadPoolManager threadPoolManager,
|
||||
IcecastHTTPConfiguration configuration,
|
||||
IAudioConverter audioConverter)
|
||||
public IcecastHTTPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastHTTPConfiguration configuration)
|
||||
{
|
||||
super(threadPoolManager, configuration, audioConverter);
|
||||
super(threadPoolManager, configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,31 +86,24 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
|
||||
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
|
||||
*/
|
||||
@Override
|
||||
public void broadcast()
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
{
|
||||
if(connect())
|
||||
{
|
||||
if (mSubscription != null && mSubscription.hasDemand())
|
||||
{
|
||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
||||
|
||||
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();
|
||||
}
|
||||
mSubscription.broadcast(ByteBuffer.wrap(audio));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,8 +409,9 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
|
||||
else
|
||||
{
|
||||
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");
|
||||
mLog.debug("Opening: " + path.toString());
|
||||
@@ -432,7 +424,7 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
|
||||
|
||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||
{
|
||||
reader.setListener(broadcaster);
|
||||
reader.setListener(audioBroadcaster);
|
||||
reader.read();
|
||||
}
|
||||
catch (IOException e)
|
||||
@@ -36,6 +36,11 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
||||
private int mSampleRate = 8000;
|
||||
private String mURL;
|
||||
|
||||
public IcecastHTTPConfiguration()
|
||||
{
|
||||
//no-arg JAXB constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Icecast 2.3.x and 2.4.x compatible configuration
|
||||
* @param format
|
||||
@@ -45,6 +50,33 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
||||
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
|
||||
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()
|
||||
{
|
||||
@@ -157,8 +189,8 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets public visibility of the broadcast
|
||||
* @param isPublic indicates if the broadcast should be visible to the public
|
||||
* Sets public visibility of the broadcastAudio
|
||||
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||
*/
|
||||
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()
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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
|
||||
*/
|
||||
public void setURL(String url)
|
||||
|
||||
@@ -18,16 +18,24 @@
|
||||
******************************************************************************/
|
||||
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.broadcast.Broadcaster;
|
||||
import audio.broadcast.BroadcastFactory;
|
||||
import audio.convert.IAudioConverter;
|
||||
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;
|
||||
@@ -37,6 +45,7 @@ import record.wave.AudioPacketMonoWaveReader;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
@@ -44,91 +53,186 @@ import java.net.URLEncoder;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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 TERMINATOR = "\r\n";
|
||||
private final static String SEPARATOR = ":";
|
||||
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private List<AudioPacket> mPacketsToBroadcast = new ArrayList<>();
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
private Socket mSocket;
|
||||
private DataOutputStream mOutputStream;
|
||||
private DataInputStream mInputStream;
|
||||
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
|
||||
|
||||
private int mCounter;
|
||||
private boolean mFirstMetadataUpdateSuppressed = false;
|
||||
private byte[] mSilenceFrame;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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 audioConverter
|
||||
*/
|
||||
public IcecastTCPBroadcaster(ThreadPoolManager threadPoolManager,
|
||||
IcecastTCPConfiguration configuration,
|
||||
IAudioConverter audioConverter)
|
||||
public IcecastTCPAudioBroadcaster(DefaultAsyncHttpClient httpClient,
|
||||
ThreadPoolManager threadPoolManager,
|
||||
IcecastTCPConfiguration configuration)
|
||||
{
|
||||
super(threadPoolManager, configuration, audioConverter);
|
||||
super(threadPoolManager, configuration);
|
||||
|
||||
mDefaultAsyncHttpClient = httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration information
|
||||
*/
|
||||
private IcecastHTTPConfiguration getConfiguration()
|
||||
private IcecastTCPConfiguration getConfiguration()
|
||||
{
|
||||
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
|
||||
return (IcecastTCPConfiguration)getBroadcastConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast any queued audio packets using Icecast V2 Protocol
|
||||
*/
|
||||
@Override
|
||||
public void broadcast()
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
{
|
||||
if(connect())
|
||||
boolean completed = false;
|
||||
|
||||
while(!completed)
|
||||
{
|
||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
||||
|
||||
if (!mPacketsToBroadcast.isEmpty())
|
||||
if(connect())
|
||||
{
|
||||
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
|
||||
|
||||
mLog.debug("We have [" + mPacketsToBroadcast.size() +
|
||||
"] packets - sending:" + convertedAudio.length + " bytes");
|
||||
|
||||
try
|
||||
{
|
||||
send(convertedAudio);
|
||||
send(audio);
|
||||
completed = true;
|
||||
}
|
||||
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)
|
||||
{
|
||||
completed = true;
|
||||
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
|
||||
*/
|
||||
public void disconnect()
|
||||
public void disconnect(boolean shutdown)
|
||||
{
|
||||
if(mOutputStream != null)
|
||||
{
|
||||
@@ -197,7 +301,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
||||
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);
|
||||
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
|
||||
|
||||
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
|
||||
|
||||
@@ -334,6 +444,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
||||
getConfiguration().getPort() + "] - will reattempt connection periodically");
|
||||
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)
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
@@ -457,7 +575,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if(response.startsWith("Invalid Password"))
|
||||
else if(response.contains("Invalid Password") || response.contains("Authentication Required"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.INVALID_PASSWORD);
|
||||
}
|
||||
@@ -469,56 +587,20 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
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)
|
||||
{
|
||||
boolean test = false;
|
||||
|
||||
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.setPort(80);
|
||||
config.setMountPoint("/k0yrdpx7zn4h");
|
||||
@@ -530,6 +612,11 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
||||
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());
|
||||
@@ -537,22 +624,29 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
||||
else
|
||||
{
|
||||
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");
|
||||
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.setListener(broadcaster);
|
||||
reader.setMetadata(metadata);
|
||||
reader.setListener(audioBroadcaster);
|
||||
reader.read();
|
||||
audioBroadcaster.receive(new AudioPacket(AudioPacket.Type.END, metadata));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
@@ -22,9 +22,10 @@ import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastFormat;
|
||||
import audio.broadcast.BroadcastServerType;
|
||||
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.util.Base64;
|
||||
@@ -32,6 +33,8 @@ import java.util.Base64;
|
||||
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
|
||||
public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
|
||||
|
||||
private String mUserName;
|
||||
private String mMountPoint;
|
||||
private String mDescription;
|
||||
@@ -44,6 +47,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
|
||||
public IcecastTCPConfiguration()
|
||||
{
|
||||
setUserName("source");
|
||||
//No-arg constructor for JAXB
|
||||
}
|
||||
|
||||
@@ -54,6 +58,31 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
public IcecastTCPConfiguration(BroadcastFormat 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
|
||||
@@ -79,6 +108,8 @@ 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;
|
||||
@@ -175,7 +206,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* Public visibility of the broadcast
|
||||
* Public visibility of the broadcastAudio
|
||||
*/
|
||||
@XmlAttribute( name="public" )
|
||||
public boolean isPublic()
|
||||
@@ -184,8 +215,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets public visibility of the broadcast
|
||||
* @param isPublic indicates if the broadcast should be visible to the public
|
||||
* Sets public visibility of the broadcastAudio
|
||||
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||
*/
|
||||
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" )
|
||||
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)
|
||||
{
|
||||
@@ -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" )
|
||||
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
|
||||
*/
|
||||
public void setURL(String url)
|
||||
@@ -275,4 +306,35 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
package audio.broadcast.shoutcast.v1;
|
||||
|
||||
import audio.AudioPacket;
|
||||
import audio.broadcast.Broadcaster;
|
||||
import audio.broadcast.AudioBroadcaster;
|
||||
import audio.broadcast.BroadcastFactory;
|
||||
import audio.broadcast.BroadcastFormat;
|
||||
import audio.broadcast.BroadcastMetadata;
|
||||
import audio.broadcast.BroadcastState;
|
||||
import audio.convert.IAudioConverter;
|
||||
import audio.metadata.AudioMetadata;
|
||||
import controller.ThreadPoolManager;
|
||||
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import record.wave.AudioPacketMonoWaveReader;
|
||||
@@ -41,9 +42,9 @@ import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
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 long mLastConnectionAttempt = 0;
|
||||
@@ -58,13 +59,10 @@ public class ShoutcastV1Broadcaster extends Broadcaster
|
||||
/**
|
||||
* Creates a Shoutcast Version 1 broadcaster.
|
||||
* @param configuration
|
||||
* @param audioConverter
|
||||
*/
|
||||
public ShoutcastV1Broadcaster(ThreadPoolManager threadPoolManager,
|
||||
ShoutcastV1Configuration configuration,
|
||||
IAudioConverter audioConverter)
|
||||
public ShoutcastV1AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration configuration)
|
||||
{
|
||||
super(threadPoolManager, configuration, audioConverter);
|
||||
super(threadPoolManager, configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,34 +73,29 @@ public class ShoutcastV1Broadcaster extends Broadcaster
|
||||
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
|
||||
*/
|
||||
public void broadcast()
|
||||
@Override
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
{
|
||||
//If we're connected, send the audio, otherwise discard it
|
||||
if(connect())
|
||||
{
|
||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
||||
|
||||
if(!mPacketsToBroadcast.isEmpty())
|
||||
try
|
||||
{
|
||||
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
|
||||
|
||||
mPacketsToBroadcast.clear();
|
||||
|
||||
if(convertedAudio != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
send(convertedAudio);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Error while dispatching audio", e);
|
||||
setBroadcastState(BroadcastState.BROADCAST_ERROR);
|
||||
}
|
||||
}
|
||||
send(audio);
|
||||
}
|
||||
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);
|
||||
|
||||
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");
|
||||
mLog.debug("Opening: " + path.toString());
|
||||
@@ -363,7 +357,7 @@ public class ShoutcastV1Broadcaster extends Broadcaster
|
||||
|
||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||
{
|
||||
reader.setListener(broadcaster);
|
||||
reader.setListener(audioBroadcaster);
|
||||
reader.read();
|
||||
}
|
||||
catch (IOException e)
|
||||
@@ -21,6 +21,7 @@ package audio.broadcast.shoutcast.v1;
|
||||
import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastFormat;
|
||||
import audio.broadcast.BroadcastServerType;
|
||||
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
|
||||
public class ShoutcastV1Configuration extends BroadcastConfiguration
|
||||
{
|
||||
@@ -31,11 +32,40 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
||||
private int mBitRate;
|
||||
private String mURL;
|
||||
|
||||
public ShoutcastV1Configuration()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
public ShoutcastV1Configuration(BroadcastFormat 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
|
||||
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()
|
||||
{
|
||||
@@ -85,8 +115,8 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets public visibility of the broadcast
|
||||
* @param isPublic indicates if the broadcast should be visible to the public
|
||||
* Sets public visibility of the broadcastAudio
|
||||
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||
*/
|
||||
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()
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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
|
||||
*/
|
||||
public void setURL(String url)
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
package audio.broadcast.shoutcast.v2;
|
||||
|
||||
import audio.AudioPacket;
|
||||
import audio.broadcast.AudioBroadcaster;
|
||||
import audio.broadcast.BroadcastConfiguration;
|
||||
import audio.broadcast.BroadcastFormat;
|
||||
import audio.broadcast.BroadcastState;
|
||||
import audio.broadcast.Broadcaster;
|
||||
import audio.broadcast.BroadcastFactory;
|
||||
import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast;
|
||||
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.UltravoxMetadata;
|
||||
import audio.broadcast.shoutcast.v2.message.XMLMetadata;
|
||||
import audio.convert.IAudioConverter;
|
||||
import audio.metadata.AudioMetadata;
|
||||
import controller.ThreadPoolManager;
|
||||
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import properties.SystemProperties;
|
||||
@@ -57,9 +58,9 @@ import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
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 boolean GET_RESPONSE = true;
|
||||
@@ -75,13 +76,10 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
||||
/**
|
||||
* Creates a Shoutcast Version 2 broadcaster.
|
||||
* @param configuration details for shoutcast version 2
|
||||
* @param audioConverter to convert PCM audio
|
||||
*/
|
||||
public ShoutcastV2Broadcaster(ThreadPoolManager threadPoolManager,
|
||||
BroadcastConfiguration configuration,
|
||||
IAudioConverter audioConverter)
|
||||
public ShoutcastV2AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration configuration)
|
||||
{
|
||||
super(threadPoolManager, configuration, audioConverter);
|
||||
super(threadPoolManager, configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,41 +90,36 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
||||
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(connect())
|
||||
{
|
||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
||||
List<UltravoxMessage> audioMessages = getAudioMessages(audio);
|
||||
|
||||
if(!mPacketsToBroadcast.isEmpty())
|
||||
try
|
||||
{
|
||||
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
|
||||
|
||||
mPacketsToBroadcast.clear();
|
||||
|
||||
if(convertedAudio != null)
|
||||
for(UltravoxMessage audioMessage: audioMessages)
|
||||
{
|
||||
List<UltravoxMessage> audioMessages = getAudioMessages(convertedAudio);
|
||||
|
||||
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;
|
||||
}
|
||||
send(audioMessage, NO_RESPONSE_EXPECTED);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
.getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE);
|
||||
icyGenre.setGenre(getShoutcastConfiguration().getStreamGenre());
|
||||
icyGenre.setGenre(getShoutcastConfiguration().getGenre());
|
||||
response = send(icyGenre, NO_RESPONSE_EXPECTED);
|
||||
}
|
||||
|
||||
@@ -534,7 +527,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
||||
config.setPort(8000);
|
||||
config.setPassword("denny3");
|
||||
config.setStreamName("Denny's Audio Broadcast Test");
|
||||
config.setStreamGenre("Public Safety");
|
||||
config.setGenre("Public Safety");
|
||||
config.setPublic(true);
|
||||
config.setURL("http://localhost:8000");
|
||||
config.setBitRate(16);
|
||||
@@ -545,8 +538,9 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
||||
else
|
||||
{
|
||||
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");
|
||||
mLog.debug("Opening: " + path.toString());
|
||||
@@ -559,7 +553,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
||||
|
||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||
{
|
||||
reader.setListener(broadcaster);
|
||||
reader.setListener(audioBroadcaster);
|
||||
reader.read();
|
||||
}
|
||||
catch (IOException e)
|
||||
@@ -28,16 +28,46 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
||||
private String mUserID;
|
||||
private int mBitRate;
|
||||
private String mStreamName;
|
||||
private String mStreamGenre;
|
||||
private String mGenre;
|
||||
private String mURL;
|
||||
private boolean mPublic = true;
|
||||
|
||||
public ShoutcastV2Configuration()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
public ShoutcastV2Configuration(BroadcastFormat 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
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
@@ -117,21 +147,21 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
||||
/**
|
||||
* Stream genre
|
||||
*/
|
||||
public String getStreamGenre()
|
||||
public String getGenre()
|
||||
{
|
||||
return mStreamGenre;
|
||||
return mGenre;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
@@ -139,8 +169,8 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets public visibility of the broadcast
|
||||
* @param isPublic indicates if the broadcast should be visible to the public
|
||||
* Sets public visibility of the broadcastAudio
|
||||
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||
*/
|
||||
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()
|
||||
{
|
||||
@@ -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
|
||||
*/
|
||||
public void setURL(String url)
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
public void setFormat(BroadcastFormat format)
|
||||
|
||||
@@ -21,7 +21,7 @@ package audio.broadcast.shoutcast.v2.message;
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -18,11 +18,15 @@
|
||||
package audio.metadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.LoggerFactory;
|
||||
|
||||
@@ -312,6 +316,23 @@ public class AudioMetadata implements Listener<Metadata>
|
||||
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()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -91,7 +91,7 @@ public class ControllerPanel extends JPanel
|
||||
|
||||
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
|
||||
|
||||
mBroadcastPanel = new BroadcastPanel(broadcastModel);
|
||||
mBroadcastPanel = new BroadcastPanel(broadcastModel, settingsManager);
|
||||
|
||||
mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager);
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ package gui;
|
||||
import alias.AliasModel;
|
||||
import alias.action.AliasActionManager;
|
||||
import audio.AudioManager;
|
||||
import audio.broadcast.AudioBroadcastManager;
|
||||
import audio.broadcast.BroadcastModel;
|
||||
import com.jidesoft.swing.JideSplitPane;
|
||||
import controller.ControllerPanel;
|
||||
@@ -137,10 +136,9 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
AudioManager audioManager = new AudioManager(threadPoolManager, sourceManager.getMixerManager());
|
||||
channelProcessingManager.addAudioPacketListener(audioManager);
|
||||
|
||||
BroadcastModel broadcastModel = new BroadcastModel();
|
||||
BroadcastModel broadcastModel = new BroadcastModel(threadPoolManager, mSettingsManager);
|
||||
|
||||
AudioBroadcastManager audioBroadcastManager = new AudioBroadcastManager(threadPoolManager);
|
||||
channelProcessingManager.addAudioPacketListener(audioBroadcastManager);
|
||||
channelProcessingManager.addAudioPacketListener(broadcastModel);
|
||||
|
||||
MapService mapService = new MapService(mSettingsManager);
|
||||
channelProcessingManager.addMessageListener(mapService);
|
||||
|
||||
@@ -31,13 +31,13 @@ public class DecoderSelectionFrame extends JInternalFrame
|
||||
public DecoderSelectionFrame( JDesktopPane desktop,
|
||||
IControllableFileSource source )
|
||||
{
|
||||
ThreadPoolManager tpm = new ThreadPoolManager();
|
||||
|
||||
AliasModel aliasModel = new AliasModel();
|
||||
BroadcastModel broadcastModel = new BroadcastModel();
|
||||
BroadcastModel broadcastModel = new BroadcastModel(tpm, null);
|
||||
ChannelModel channelModel = new ChannelModel();
|
||||
ChannelMapModel channelMapModel = new ChannelMapModel();
|
||||
|
||||
ThreadPoolManager tpm = new ThreadPoolManager();
|
||||
|
||||
|
||||
mPlaylistManager = new PlaylistManager( tpm, aliasModel, broadcastModel, channelModel, channelMapModel );
|
||||
|
||||
mDesktop = desktop;
|
||||
|
||||
@@ -33,7 +33,7 @@ import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.Marshaller;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
|
||||
import audio.broadcast.BroadcastConfigurationEvent;
|
||||
import audio.broadcast.BroadcastEvent;
|
||||
import audio.broadcast.BroadcastModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -109,10 +109,10 @@ public class PlaylistManager implements ChannelEventListener
|
||||
}
|
||||
} );
|
||||
|
||||
mBroadcastModel.addListener(new Listener<BroadcastConfigurationEvent>()
|
||||
mBroadcastModel.addListener(new Listener<BroadcastEvent>()
|
||||
{
|
||||
@Override
|
||||
public void receive(BroadcastConfigurationEvent broadcastConfigurationEvent)
|
||||
public void receive(BroadcastEvent broadcastEvent)
|
||||
{
|
||||
schedulePlaylistSave();
|
||||
}
|
||||
@@ -346,7 +346,7 @@ public class PlaylistManager implements ChannelEventListener
|
||||
}
|
||||
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
|
||||
{
|
||||
|
||||
@@ -55,6 +55,8 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
||||
private BufferProcessor mBufferProcessor;
|
||||
private ScheduledFuture<?> mProcessorHandle;
|
||||
|
||||
private long mSampleCount;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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()
|
||||
{
|
||||
return null;
|
||||
return mFileOutputStream;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,6 +180,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
||||
{
|
||||
try
|
||||
{
|
||||
mFileOutputStream.flush();
|
||||
mFileOutputStream.close();
|
||||
}
|
||||
catch (IOException e)
|
||||
@@ -204,8 +216,6 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
||||
{
|
||||
mFileOutputStream = new FileOutputStream(mPath.toFile());
|
||||
|
||||
mLog.info("Created Audio Recording [" + getPath().toString() + "]");
|
||||
|
||||
/* Schedule the handler to run every half second */
|
||||
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
@@ -230,6 +240,14 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
||||
try
|
||||
{
|
||||
record(mPacketsToProcess);
|
||||
|
||||
for(AudioPacket packet: mPacketsToProcess)
|
||||
{
|
||||
if(packet.getType() == AudioPacket.Type.AUDIO)
|
||||
{
|
||||
mSampleCount += packet.getAudioBuffer().getSamples().length;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
|
||||
@@ -95,26 +95,50 @@ public class MP3Recorder extends AudioRecorder
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
Path inputPath = Paths.get("/home/denny/Music/PCM.wav");
|
||||
Path outputPath = Paths.get("/home/denny/Music/denny_test/PCM.mp3");
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
// 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());
|
||||
|
||||
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);
|
||||
reader.read();
|
||||
recorder.stop();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
mLog.error("Error", e);
|
||||
recorder.receive(silencePacket);
|
||||
}
|
||||
|
||||
recorder.stop();
|
||||
|
||||
executor.shutdown();
|
||||
|
||||
mLog.debug("Finished");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.util.List;
|
||||
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>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user