mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-14 17:47:32 -05:00
#30 WIP - broadcastify multi-channel streaming is functional.
This commit is contained in:
1
.idea/dictionaries/denny.xml
generated
1
.idea/dictionaries/denny.xml
generated
@@ -9,6 +9,7 @@
|
|||||||
<w>ident</w>
|
<w>ident</w>
|
||||||
<w>polyphase</w>
|
<w>polyphase</w>
|
||||||
<w>samplerate</w>
|
<w>samplerate</w>
|
||||||
|
<w>sdrtrunk</w>
|
||||||
<w>shoutcast</w>
|
<w>shoutcast</w>
|
||||||
<w>streamable</w>
|
<w>streamable</w>
|
||||||
<w>talkgroup</w>
|
<w>talkgroup</w>
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ public class AliasList implements Listener<AliasEvent>
|
|||||||
mTalkgroup.put(tgid, alias);
|
mTalkgroup.put(tgid, alias);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case BROADCAST_CHANNEL:
|
||||||
case NON_RECORDABLE:
|
case NON_RECORDABLE:
|
||||||
case PRIORITY:
|
case PRIORITY:
|
||||||
//We don't maintain lookups for these items
|
//We don't maintain lookups for these items
|
||||||
|
|||||||
@@ -23,16 +23,42 @@ import alias.id.AliasIDType;
|
|||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAttribute;
|
import javax.xml.bind.annotation.XmlAttribute;
|
||||||
|
|
||||||
public class BroadcastChannel extends AliasID
|
public class BroadcastChannel extends AliasID implements Comparable<BroadcastChannel>
|
||||||
{
|
{
|
||||||
private String mChannelName;
|
private String mChannelName;
|
||||||
|
|
||||||
public BroadcastChannel()
|
public BroadcastChannel()
|
||||||
{
|
{
|
||||||
|
//JAXB Constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(BroadcastChannel other)
|
||||||
|
{
|
||||||
|
if(mChannelName != null && other.getChannelName() != null)
|
||||||
|
{
|
||||||
|
return mChannelName.compareTo(other.getChannelName());
|
||||||
|
}
|
||||||
|
else if(mChannelName != null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the broadcast channel configuration
|
* Creates a named broadcast channel
|
||||||
|
*/
|
||||||
|
public BroadcastChannel(String channelName)
|
||||||
|
{
|
||||||
|
mChannelName = channelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the broadcastAudio channel configuration
|
||||||
*/
|
*/
|
||||||
@XmlAttribute(name="channel")
|
@XmlAttribute(name="channel")
|
||||||
public String getChannelName()
|
public String getChannelName()
|
||||||
@@ -41,7 +67,7 @@ public class BroadcastChannel extends AliasID
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the name of the broadcast channel configuration
|
* Sets the name of the broadcastAudio channel configuration
|
||||||
*/
|
*/
|
||||||
public void setChannelName(String channel)
|
public void setChannelName(String channel)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,12 +19,15 @@
|
|||||||
package alias.id.broadcast;
|
package alias.id.broadcast;
|
||||||
|
|
||||||
import alias.id.AliasID;
|
import alias.id.AliasID;
|
||||||
|
import audio.broadcast.BroadcastConfiguration;
|
||||||
import audio.broadcast.BroadcastModel;
|
import audio.broadcast.BroadcastModel;
|
||||||
import gui.editor.DocumentListenerEditor;
|
import gui.editor.DocumentListenerEditor;
|
||||||
import net.miginfocom.swing.MigLayout;
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
import java.awt.event.MouseAdapter;
|
import java.awt.event.MouseAdapter;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -40,16 +43,14 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
|
|||||||
+ "streaming server";
|
+ "streaming server";
|
||||||
|
|
||||||
private JComboBox<String> mBroadcastConfigurations;
|
private JComboBox<String> mBroadcastConfigurations;
|
||||||
|
private BroadcastModel mBroadcastModel;
|
||||||
|
|
||||||
public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel)
|
public BroadcastChannelEditor(AliasID aliasID, BroadcastModel broadcastModel)
|
||||||
{
|
{
|
||||||
List<String> channelNames = broadcastModel.getBroadcastConfigurationNames();
|
mBroadcastModel = broadcastModel;
|
||||||
|
|
||||||
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
|
|
||||||
|
|
||||||
setItem( aliasID );
|
|
||||||
|
|
||||||
initGUI();
|
initGUI();
|
||||||
|
setItem( aliasID );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initGUI()
|
private void initGUI()
|
||||||
@@ -58,15 +59,20 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
|
|||||||
|
|
||||||
add( new JLabel( "Broadcast Channel" ), "grow" );
|
add( new JLabel( "Broadcast Channel" ), "grow" );
|
||||||
|
|
||||||
|
List<String> channelNames = mBroadcastModel.getBroadcastConfigurationNames();
|
||||||
|
|
||||||
|
mBroadcastConfigurations = new JComboBox<String>(channelNames.toArray(new String[channelNames.size()]));
|
||||||
|
mBroadcastConfigurations.addActionListener(new ActionListener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e)
|
||||||
|
{
|
||||||
|
setModified(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
add(mBroadcastConfigurations, "grow");
|
add(mBroadcastConfigurations, "grow");
|
||||||
|
|
||||||
BroadcastChannel channel = (BroadcastChannel)getItem();
|
|
||||||
|
|
||||||
if(channel.isValid())
|
|
||||||
{
|
|
||||||
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
|
|
||||||
}
|
|
||||||
|
|
||||||
JLabel help = new JLabel( "Help ..." );
|
JLabel help = new JLabel( "Help ..." );
|
||||||
help.setForeground( Color.BLUE.brighter() );
|
help.setForeground( Color.BLUE.brighter() );
|
||||||
help.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
|
help.setCursor( new Cursor( Cursor.HAND_CURSOR ) );
|
||||||
@@ -82,10 +88,42 @@ public class BroadcastChannelEditor extends DocumentListenerEditor<AliasID>
|
|||||||
|
|
||||||
add( help, "align left" );
|
add( help, "align left" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setItem(AliasID item)
|
||||||
|
{
|
||||||
|
super.setItem(item);
|
||||||
|
|
||||||
|
if(item instanceof BroadcastChannel)
|
||||||
|
{
|
||||||
|
BroadcastChannel channel = (BroadcastChannel)item;
|
||||||
|
|
||||||
|
if(channel.isValid())
|
||||||
|
{
|
||||||
|
mBroadcastConfigurations.setSelectedItem(channel.getChannelName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setModified(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save()
|
public void save()
|
||||||
{
|
{
|
||||||
((BroadcastChannel)getItem()).setChannelName((String)mBroadcastConfigurations.getSelectedItem());
|
String channel = (String)mBroadcastConfigurations.getSelectedItem();
|
||||||
|
|
||||||
|
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getBroadcastConfiguration(channel);
|
||||||
|
|
||||||
|
|
||||||
|
if(selectedConfiguration != null)
|
||||||
|
{
|
||||||
|
((BroadcastChannel)getItem()).setChannelName(selectedConfiguration.getName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
((BroadcastChannel)getItem()).setChannelName(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.XmlElement;
|
||||||
import javax.xml.bind.annotation.XmlRootElement;
|
import javax.xml.bind.annotation.XmlRootElement;
|
||||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||||
|
import javax.xml.bind.annotation.XmlTransient;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
|
|
||||||
@@ -31,17 +32,24 @@ import java.net.SocketAddress;
|
|||||||
@XmlRootElement(name = "stream")
|
@XmlRootElement(name = "stream")
|
||||||
public abstract class BroadcastConfiguration
|
public abstract class BroadcastConfiguration
|
||||||
{
|
{
|
||||||
private BroadcastFormat mBroadcastFormat;
|
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
|
||||||
private String mName;
|
private String mName;
|
||||||
private String mHost;
|
private String mHost;
|
||||||
private int mPort;
|
private int mPort;
|
||||||
private String mPassword;
|
private String mPassword;
|
||||||
|
private long mDelay;
|
||||||
|
private boolean mEnabled = true;
|
||||||
|
|
||||||
public BroadcastConfiguration()
|
public BroadcastConfiguration()
|
||||||
{
|
{
|
||||||
//No-arg constructor required for JAXB
|
//No-arg constructor required for JAXB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a copy/clone of the configuration
|
||||||
|
*/
|
||||||
|
public abstract BroadcastConfiguration copyOf();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast audio streaming configuration. Describes the streaming server and configuration details.
|
* Broadcast audio streaming configuration. Describes the streaming server and configuration details.
|
||||||
*/
|
*/
|
||||||
@@ -57,7 +65,7 @@ public abstract class BroadcastConfiguration
|
|||||||
public abstract BroadcastServerType getBroadcastServerType();
|
public abstract BroadcastServerType getBroadcastServerType();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name identifying this broadcast configuration.
|
* Name identifying this broadcastAudio configuration.
|
||||||
*/
|
*/
|
||||||
@XmlAttribute(name = "name")
|
@XmlAttribute(name = "name")
|
||||||
public String getName()
|
public String getName()
|
||||||
@@ -120,7 +128,7 @@ public abstract class BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Port number of the streaming server to where the broadcast will be directed.
|
* Port number of the streaming server to where the broadcastAudio will be directed.
|
||||||
*
|
*
|
||||||
* @param port
|
* @param port
|
||||||
*/
|
*/
|
||||||
@@ -152,7 +160,7 @@ public abstract class BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the password used to authenticate the broadcast broadcast user to the streaming server
|
* Sets the password used to authenticate the broadcastAudio broadcastAudio user to the streaming server
|
||||||
*
|
*
|
||||||
* @param password
|
* @param password
|
||||||
*/
|
*/
|
||||||
@@ -170,11 +178,95 @@ public abstract class BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio broadcast content type (e.g. mp3)
|
* Audio broadcastAudio content type (e.g. mp3)
|
||||||
*/
|
*/
|
||||||
@XmlElement(name = "format")
|
@XmlAttribute(name = "format")
|
||||||
public BroadcastFormat getBroadcastFormat()
|
public BroadcastFormat getBroadcastFormat()
|
||||||
{
|
{
|
||||||
return mBroadcastFormat;
|
return mBroadcastFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBroadcastFormat(BroadcastFormat format)
|
||||||
|
{
|
||||||
|
mBroadcastFormat = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audio broadcastAudio delay from recording start until the audio file is broadcastAudio to the server.
|
||||||
|
*/
|
||||||
|
@XmlAttribute(name = "delay")
|
||||||
|
public long getDelay()
|
||||||
|
{
|
||||||
|
return mDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User specified delay in milli-seconds from audio call start until when the audio is broadcastAudio to the server.
|
||||||
|
* @param delay in milliseconds
|
||||||
|
*/
|
||||||
|
public void setDelay(long delay)
|
||||||
|
{
|
||||||
|
mDelay = delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this broadcaster is enable, meaning that it will automatically connect on startup.
|
||||||
|
*/
|
||||||
|
@XmlAttribute(name = "enabled")
|
||||||
|
public boolean isEnabled()
|
||||||
|
{
|
||||||
|
return mEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled)
|
||||||
|
{
|
||||||
|
mEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (this == o)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(o instanceof BroadcastConfiguration))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BroadcastConfiguration that = (BroadcastConfiguration) o;
|
||||||
|
|
||||||
|
if (getBroadcastFormat() != that.getBroadcastFormat())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBroadcastServerType() == that.getBroadcastServerType();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
int result = getBroadcastFormat().hashCode();
|
||||||
|
result = 31 * result + (getName() != null ? getName().hashCode() : 0);
|
||||||
|
result = 31 * result + getBroadcastServerType().hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this configuration is valid. A minimal check will ensure that it contains at least a hostname and
|
||||||
|
* port number.
|
||||||
|
*/
|
||||||
|
@XmlTransient
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
return mHost != null && mPort > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package audio.broadcast;
|
package audio.broadcast;
|
||||||
|
|
||||||
public class BroadcastConfigurationEvent
|
public class BroadcastEvent
|
||||||
{
|
{
|
||||||
private BroadcastConfiguration mBroadcastConfiguration;
|
private BroadcastConfiguration mBroadcastConfiguration;
|
||||||
|
private AudioBroadcaster mAudioBroadcaster;
|
||||||
private Event mEvent;
|
private Event mEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,17 +29,44 @@ public class BroadcastConfigurationEvent
|
|||||||
* @param configuration - alias that changed
|
* @param configuration - alias that changed
|
||||||
* @param event - change event
|
* @param event - change event
|
||||||
*/
|
*/
|
||||||
public BroadcastConfigurationEvent(BroadcastConfiguration configuration, Event event )
|
public BroadcastEvent(BroadcastConfiguration configuration, Event event )
|
||||||
{
|
{
|
||||||
mBroadcastConfiguration = configuration;
|
mBroadcastConfiguration = configuration;
|
||||||
mEvent = event;
|
mEvent = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event )
|
||||||
|
{
|
||||||
|
mAudioBroadcaster = audioBroadcaster;
|
||||||
|
mEvent = event;
|
||||||
|
}
|
||||||
|
|
||||||
public BroadcastConfiguration getBroadcastConfiguration()
|
public BroadcastConfiguration getBroadcastConfiguration()
|
||||||
{
|
{
|
||||||
return mBroadcastConfiguration;
|
return mBroadcastConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AudioBroadcaster getAudioBroadcaster()
|
||||||
|
{
|
||||||
|
return mAudioBroadcaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this event pertains to an audio broadcaster
|
||||||
|
*/
|
||||||
|
public boolean isAudioBroadcasterEvent()
|
||||||
|
{
|
||||||
|
return mAudioBroadcaster != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this event pertains to a broadcast configuration
|
||||||
|
*/
|
||||||
|
public boolean isBroadcastConfigurationEvent()
|
||||||
|
{
|
||||||
|
return mBroadcastConfiguration != null;
|
||||||
|
}
|
||||||
|
|
||||||
public Event getEvent()
|
public Event getEvent()
|
||||||
{
|
{
|
||||||
return mEvent;
|
return mEvent;
|
||||||
@@ -49,8 +77,14 @@ public class BroadcastConfigurationEvent
|
|||||||
*/
|
*/
|
||||||
public enum Event
|
public enum Event
|
||||||
{
|
{
|
||||||
ADD,
|
BROADCASTER_ADD,
|
||||||
CHANGE,
|
BROADCASTER_QUEUE_CHANGE,
|
||||||
DELETE;
|
BROADCASTER_STATE_CHANGE,
|
||||||
|
BROADCASTER_STREAMED_COUNT_CHANGE,
|
||||||
|
BROADCASTER_DELETE,
|
||||||
|
|
||||||
|
CONFIGURATION_ADD,
|
||||||
|
CONFIGURATION_CHANGE,
|
||||||
|
CONFIGURATION_DELETE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,27 +18,32 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package audio.broadcast;
|
package audio.broadcast;
|
||||||
|
|
||||||
|
import audio.AudioPacket;
|
||||||
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||||
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
|
import audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
|
||||||
|
import audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||||
import audio.broadcast.icecast.IcecastHTTPConfiguration;
|
import audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||||
import audio.broadcast.icecast.IcecastHTTPBroadcaster;
|
import audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||||
import audio.broadcast.icecast.IcecastTCPBroadcaster;
|
|
||||||
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||||
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
import audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||||
import audio.broadcast.shoutcast.v1.ShoutcastV1Broadcaster;
|
import audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
|
||||||
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
import audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||||
import audio.broadcast.shoutcast.v2.ShoutcastV2Broadcaster;
|
import audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
|
||||||
import audio.convert.IAudioConverter;
|
import audio.convert.IAudioConverter;
|
||||||
import audio.convert.MP3AudioConverter;
|
import audio.convert.MP3AudioConverter;
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
import gui.editor.Editor;
|
import gui.editor.Editor;
|
||||||
import gui.editor.EmptyEditor;
|
import gui.editor.EmptyEditor;
|
||||||
|
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import record.AudioRecorder;
|
import record.AudioRecorder;
|
||||||
import record.mp3.MP3Recorder;
|
import record.mp3.MP3Recorder;
|
||||||
|
import settings.SettingsManager;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class BroadcastFactory
|
public class BroadcastFactory
|
||||||
{
|
{
|
||||||
@@ -54,34 +59,35 @@ public class BroadcastFactory
|
|||||||
* @param configuration describing the server and audio types
|
* @param configuration describing the server and audio types
|
||||||
* @return configured broadcaster or null
|
* @return configured broadcaster or null
|
||||||
*/
|
*/
|
||||||
public static Broadcaster getBroadcaster(ThreadPoolManager threadPoolManager,
|
public static AudioBroadcaster getBroadcaster(DefaultAsyncHttpClient defaultAsyncHttpClient,
|
||||||
BroadcastConfiguration configuration)
|
ThreadPoolManager threadPoolManager,
|
||||||
|
BroadcastConfiguration configuration)
|
||||||
{
|
{
|
||||||
IAudioConverter converter = getAudioConverter(configuration);
|
if(configuration != null)
|
||||||
|
|
||||||
if(converter != null)
|
|
||||||
{
|
{
|
||||||
switch(configuration.getBroadcastServerType())
|
IAudioConverter converter = getAudioConverter(configuration);
|
||||||
|
|
||||||
|
if(converter != null)
|
||||||
{
|
{
|
||||||
case BROADCASTIFY:
|
switch(configuration.getBroadcastServerType())
|
||||||
return new IcecastTCPBroadcaster(threadPoolManager,
|
{
|
||||||
(BroadcastifyConfiguration)configuration, converter);
|
case BROADCASTIFY:
|
||||||
case ICECAST_HTTP:
|
return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
|
||||||
return new IcecastHTTPBroadcaster(threadPoolManager,
|
(BroadcastifyConfiguration)configuration);
|
||||||
(IcecastHTTPConfiguration)configuration, converter);
|
case ICECAST_TCP:
|
||||||
case ICECAST_TCP:
|
return new IcecastTCPAudioBroadcaster(defaultAsyncHttpClient, threadPoolManager,
|
||||||
return new IcecastTCPBroadcaster(threadPoolManager,
|
(IcecastTCPConfiguration)configuration);
|
||||||
(IcecastTCPConfiguration)configuration, converter);
|
case ICECAST_HTTP:
|
||||||
case SHOUTCAST_V1:
|
return new IcecastHTTPAudioBroadcaster(threadPoolManager, (IcecastHTTPConfiguration)configuration);
|
||||||
return new ShoutcastV1Broadcaster(threadPoolManager,
|
case SHOUTCAST_V1:
|
||||||
(ShoutcastV1Configuration)configuration, converter);
|
return new ShoutcastV1AudioBroadcaster(threadPoolManager, (ShoutcastV1Configuration)configuration);
|
||||||
case SHOUTCAST_V2:
|
case SHOUTCAST_V2:
|
||||||
return new ShoutcastV2Broadcaster(threadPoolManager,
|
return new ShoutcastV2AudioBroadcaster(threadPoolManager, (ShoutcastV2Configuration)configuration);
|
||||||
(ShoutcastV2Configuration)configuration, converter);
|
case UNKNOWN:
|
||||||
case UNKNOWN:
|
default:
|
||||||
default:
|
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
|
||||||
mLog.info("Unrecognized broadcast configuration: " + configuration.getBroadcastFormat().name());
|
break;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,14 +106,14 @@ public class BroadcastFactory
|
|||||||
case MP3:
|
case MP3:
|
||||||
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
|
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
|
||||||
default:
|
default:
|
||||||
mLog.info("Unrecognized broadcast format: " + configuration.getBroadcastFormat().name());
|
mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a broadcast configuration for the specified server type and format
|
* Creates a broadcastAudio configuration for the specified server type and format
|
||||||
*
|
*
|
||||||
* @param serverType for the configuration
|
* @param serverType for the configuration
|
||||||
* @param format for the output audio format
|
* @param format for the output audio format
|
||||||
@@ -129,7 +135,7 @@ public class BroadcastFactory
|
|||||||
return new ShoutcastV2Configuration(format);
|
return new ShoutcastV2Configuration(format);
|
||||||
case UNKNOWN:
|
case UNKNOWN:
|
||||||
default:
|
default:
|
||||||
mLog.info("Unrecognized broadcast server type: " + serverType.name());
|
mLog.info("Unrecognized broadcastAudio server type: " + serverType.name());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,20 +143,21 @@ public class BroadcastFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an editor for the specified broadcast configuration
|
* Constructs an editor for the specified broadcastAudio configuration
|
||||||
* @param configuration to modify or view
|
* @param configuration to modify or view
|
||||||
* @param broadcastModel model for broadcast configurations
|
* @param broadcastModel model for broadcastAudio configurations
|
||||||
* @return an editor for the specified broadcast configuration
|
* @return an editor for the specified broadcastAudio configuration
|
||||||
*/
|
*/
|
||||||
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
|
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
|
||||||
BroadcastModel broadcastModel)
|
BroadcastModel broadcastModel,
|
||||||
|
SettingsManager settingsManager)
|
||||||
{
|
{
|
||||||
Editor<BroadcastConfiguration> editor;
|
Editor<BroadcastConfiguration> editor;
|
||||||
|
|
||||||
switch (configuration.getBroadcastServerType())
|
switch (configuration.getBroadcastServerType())
|
||||||
{
|
{
|
||||||
case BROADCASTIFY:
|
case BROADCASTIFY:
|
||||||
editor = new BroadcastifyConfigurationEditor(broadcastModel);
|
editor = new BroadcastifyConfigurationEditor(broadcastModel, settingsManager);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
editor = new EmptyEditor<BroadcastConfiguration>();
|
editor = new EmptyEditor<BroadcastConfiguration>();
|
||||||
@@ -163,7 +170,7 @@ public class BroadcastFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an audio recorder for the specified broadcast format using the specified path output file name
|
* Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
|
||||||
*/
|
*/
|
||||||
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
|
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
|
||||||
{
|
{
|
||||||
@@ -172,8 +179,28 @@ public class BroadcastFactory
|
|||||||
case MP3:
|
case MP3:
|
||||||
return new MP3Recorder(path);
|
return new MP3Recorder(path);
|
||||||
default:
|
default:
|
||||||
mLog.debug("Unrecognized broadcast format [" + broadcastFormat + "] cannot create audio recorder");
|
mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] getSilenceFrame(BroadcastFormat format, long duration)
|
||||||
|
{
|
||||||
|
int length = (int)(duration * 8); //8000 Hz sample rate
|
||||||
|
float[] silence = new float[length];
|
||||||
|
AudioPacket silencePacket = new AudioPacket(silence, null);
|
||||||
|
List<AudioPacket> silencePackets = new ArrayList<>();
|
||||||
|
silencePackets.add(silencePacket);
|
||||||
|
|
||||||
|
switch(format)
|
||||||
|
{
|
||||||
|
case MP3:
|
||||||
|
MP3AudioConverter converter = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE,
|
||||||
|
MP3Recorder.CONSTANT_BIT_RATE);
|
||||||
|
|
||||||
|
return converter.convert(silencePackets);
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unrecognized broadcast format: " + format.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public enum BroadcastFormat
|
|||||||
private BroadcastFormat(String value, String fileExtension)
|
private BroadcastFormat(String value, String fileExtension)
|
||||||
{
|
{
|
||||||
mValue = value;
|
mValue = value;
|
||||||
|
mFileExtension = fileExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getValue()
|
public String getValue()
|
||||||
|
|||||||
@@ -18,28 +18,70 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package audio.broadcast;
|
package audio.broadcast;
|
||||||
|
|
||||||
|
import alias.id.broadcast.BroadcastChannel;
|
||||||
|
import audio.AudioPacket;
|
||||||
|
import controller.ThreadPoolManager;
|
||||||
|
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import properties.SystemProperties;
|
||||||
|
import sample.Broadcaster;
|
||||||
import sample.Listener;
|
import sample.Listener;
|
||||||
|
import settings.SettingsManager;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
import javax.swing.table.AbstractTableModel;
|
import javax.swing.table.AbstractTableModel;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class BroadcastModel extends AbstractTableModel
|
public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
|
||||||
{
|
{
|
||||||
private static final int COLUMN_CHANNEL_NAME = 0;
|
private final static Logger mLog = LoggerFactory.getLogger( BroadcastModel.class );
|
||||||
private static final int COLUMN_SERVER_TYPE = 1;
|
|
||||||
private static final int COLUMN_SERVER_ADDRESS = 2;
|
public static final String TEMPORARY_STREAM_DIRECTORY = "streaming";
|
||||||
|
public static final String TEMPORARY_STREAM_FILE_SUFFIX = "temporary_streaming_file_";
|
||||||
|
|
||||||
|
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
|
||||||
|
|
||||||
|
private static final int COLUMN_SERVER_ICON = 0;
|
||||||
|
private static final int COLUMN_STREAM_NAME = 1;
|
||||||
|
private static final int COLUMN_BROADCASTER_STATUS = 2;
|
||||||
|
private static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
|
||||||
|
private static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
|
||||||
|
|
||||||
private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>();
|
private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>();
|
||||||
private sample.Broadcaster<BroadcastConfigurationEvent> mBroadcastEventBroadcaster = new sample.Broadcaster<>();
|
|
||||||
|
|
||||||
public BroadcastModel()
|
private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
|
||||||
|
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
||||||
|
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
|
||||||
|
private ThreadPoolManager mThreadPoolManager;
|
||||||
|
private SettingsManager mSettingsManager;
|
||||||
|
|
||||||
|
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model for managing Broadcast configurations and any associated broadcaster instances.
|
||||||
|
*/
|
||||||
|
public BroadcastModel(ThreadPoolManager threadPoolManager, SettingsManager settingsManager)
|
||||||
{
|
{
|
||||||
|
mThreadPoolManager = threadPoolManager;
|
||||||
|
mSettingsManager = settingsManager;
|
||||||
|
|
||||||
|
removeOrphanedTemporaryRecordings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of broadcast configuration names
|
* List of broadcastAudio configuration names
|
||||||
*/
|
*/
|
||||||
public List<String> getBroadcastConfigurationNames()
|
public List<String> getBroadcastConfigurationNames()
|
||||||
{
|
{
|
||||||
@@ -54,7 +96,7 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current list of broadcast configurations
|
* Current list of broadcastAudio configurations
|
||||||
*/
|
*/
|
||||||
public List<BroadcastConfiguration> getBroadcastConfigurations()
|
public List<BroadcastConfiguration> getBroadcastConfigurations()
|
||||||
{
|
{
|
||||||
@@ -62,7 +104,7 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the list of broadcast configurations to this model
|
* Adds the list of broadcastAudio configurations to this model
|
||||||
*/
|
*/
|
||||||
public void addBroadcastConfigurations(List<BroadcastConfiguration> configurations)
|
public void addBroadcastConfigurations(List<BroadcastConfiguration> configurations)
|
||||||
{
|
{
|
||||||
@@ -73,58 +115,278 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the broadcast configuration to this model
|
* Adds the broadcastAudio configuration to this model
|
||||||
*/
|
*/
|
||||||
public void addBroadcastConfiguration(BroadcastConfiguration configuration)
|
public void addBroadcastConfiguration(BroadcastConfiguration configuration)
|
||||||
{
|
{
|
||||||
if(configuration != null)
|
if(configuration != null)
|
||||||
{
|
{
|
||||||
mBroadcastConfigurations.add(configuration);
|
ensureUniqueName(configuration);
|
||||||
|
|
||||||
int index = mBroadcastConfigurations.size() - 1;
|
if(!mBroadcastConfigurations.contains(configuration))
|
||||||
|
{
|
||||||
|
mBroadcastConfigurations.add(configuration);
|
||||||
|
|
||||||
fireTableRowsInserted( index, index );
|
int index = mBroadcastConfigurations.size() - 1;
|
||||||
|
|
||||||
broadcast( new BroadcastConfigurationEvent( configuration, BroadcastConfigurationEvent.Event.ADD ) );
|
fireTableRowsInserted( index, index );
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeBroadcastConfiguration(BroadcastConfiguration configuration)
|
mBroadcastConfigurationMap.put(configuration.getName(), configuration);
|
||||||
{
|
|
||||||
if(configuration != null && mBroadcastConfigurations.contains(configuration))
|
|
||||||
{
|
|
||||||
int index = mBroadcastConfigurations.indexOf(configuration);
|
|
||||||
|
|
||||||
mBroadcastConfigurations.remove(configuration);
|
process( new BroadcastEvent( configuration, BroadcastEvent.Event.CONFIGURATION_ADD) );
|
||||||
|
}
|
||||||
fireTableRowsDeleted( index, index );
|
|
||||||
|
|
||||||
broadcast( new BroadcastConfigurationEvent( configuration, BroadcastConfigurationEvent.Event.DELETE ) );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers the listener to receive broadcast configuration events
|
* Clones the configuration and adds it this model with a unique configuration name
|
||||||
*/
|
*/
|
||||||
public void addListener(Listener<BroadcastConfigurationEvent> listener)
|
public BroadcastConfiguration cloneBroadcastConfiguration(BroadcastConfiguration configuration)
|
||||||
|
{
|
||||||
|
if(configuration != null)
|
||||||
|
{
|
||||||
|
BroadcastConfiguration clone = configuration.copyOf();
|
||||||
|
addBroadcastConfiguration(clone);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the configuration's name so that it is unique among all other broadcast configurations
|
||||||
|
* @param configuration
|
||||||
|
*/
|
||||||
|
private void ensureUniqueName(BroadcastConfiguration configuration)
|
||||||
|
{
|
||||||
|
if(configuration.getName() == null || configuration.getName().isEmpty())
|
||||||
|
{
|
||||||
|
configuration.setName("New Configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
while(!isUniqueName(configuration.getName(), configuration))
|
||||||
|
{
|
||||||
|
String currentName = configuration.getName();
|
||||||
|
|
||||||
|
if(currentName.matches(UNIQUE_NAME_REGEX))
|
||||||
|
{
|
||||||
|
int currentVersion = 1;
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Matcher m = Pattern.compile(UNIQUE_NAME_REGEX).matcher(currentName);
|
||||||
|
|
||||||
|
if(m.find())
|
||||||
|
{
|
||||||
|
String version = m.group(2);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
currentVersion = Integer.parseInt(version);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
//Couldn't parse the version number -- keep incrementing until we find a winner
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVersion++;
|
||||||
|
|
||||||
|
sb.append(m.group(1)).append("(").append(currentVersion).append(")");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append(configuration.getName()).append("(").append(currentVersion).append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
configuration.setName(sb.toString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(configuration.getName()).append("(2)");
|
||||||
|
configuration.setName(sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the name is unique among all of the broadcast configurations. Checks each of the configurations
|
||||||
|
* in this model for a name collision. Assumes that the name argument will be assigned to the configuration
|
||||||
|
* argument if the name is unique, therefore, the name will not be checked against the configuration argument for
|
||||||
|
* a collision.
|
||||||
|
*
|
||||||
|
* @param name to check for uniqueness
|
||||||
|
* @param configuration to ignore when checking the name for uniqueness
|
||||||
|
* @return true if the name is not null and unique among all configurations managed by this model
|
||||||
|
*/
|
||||||
|
public boolean isUniqueName(String name, BroadcastConfiguration configuration)
|
||||||
|
{
|
||||||
|
if(name == null || name.isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(BroadcastConfiguration configurationToCompare: mBroadcastConfigurations)
|
||||||
|
{
|
||||||
|
if(configurationToCompare != configuration && configurationToCompare.getName().equals(name))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeBroadcastConfiguration(BroadcastConfiguration broadcastConfiguration)
|
||||||
|
{
|
||||||
|
if(broadcastConfiguration != null && mBroadcastConfigurations.contains(broadcastConfiguration))
|
||||||
|
{
|
||||||
|
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
|
||||||
|
|
||||||
|
mBroadcastConfigurations.remove(broadcastConfiguration);
|
||||||
|
|
||||||
|
mBroadcastConfigurationMap.remove(broadcastConfiguration.getName());
|
||||||
|
|
||||||
|
broadcast( new BroadcastEvent( broadcastConfiguration, BroadcastEvent.Event.CONFIGURATION_DELETE) );
|
||||||
|
|
||||||
|
fireTableRowsDeleted( index, index );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the broadcaster associated with the stream name or null if there is no broadcaster setup for the name.
|
||||||
|
*/
|
||||||
|
public AudioBroadcaster getBroadcaster(String streamName)
|
||||||
|
{
|
||||||
|
return mBroadcasterMap.get(streamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receive(AudioPacket audioPacket)
|
||||||
|
{
|
||||||
|
if (audioPacket.hasAudioMetadata() && audioPacket.getAudioMetadata().isStreamable())
|
||||||
|
{
|
||||||
|
for (BroadcastChannel broadcastChannel : audioPacket.getAudioMetadata().getBroadcastChannels())
|
||||||
|
{
|
||||||
|
String channelName = broadcastChannel.getChannelName();
|
||||||
|
|
||||||
|
if (channelName != null)
|
||||||
|
{
|
||||||
|
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
|
||||||
|
|
||||||
|
if (audioBroadcaster != null)
|
||||||
|
{
|
||||||
|
audioBroadcaster.receive(audioPacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new broadcaster for the broadcast configuration and adds it to the model
|
||||||
|
*/
|
||||||
|
private void createBroadcaster(BroadcastConfiguration broadcastConfiguration)
|
||||||
|
{
|
||||||
|
if (broadcastConfiguration != null &&
|
||||||
|
broadcastConfiguration.isEnabled() &&
|
||||||
|
broadcastConfiguration.isValid() &&
|
||||||
|
!mBroadcasterMap.containsKey(broadcastConfiguration.getName()))
|
||||||
|
{
|
||||||
|
AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(getDefaultAsyncHttpClient(),
|
||||||
|
mThreadPoolManager, broadcastConfiguration);
|
||||||
|
|
||||||
|
if (audioBroadcaster != null)
|
||||||
|
{
|
||||||
|
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
|
||||||
|
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void receive(BroadcastEvent broadcastEvent)
|
||||||
|
{
|
||||||
|
process(broadcastEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||||
|
|
||||||
|
if(index >= 0)
|
||||||
|
{
|
||||||
|
fireTableRowsUpdated(index, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shut down a broadcaster created from the configuration and remove it from this model
|
||||||
|
*/
|
||||||
|
private void deleteBroadcaster(String name)
|
||||||
|
{
|
||||||
|
if(name != null && mBroadcasterMap.containsKey(name))
|
||||||
|
{
|
||||||
|
AudioBroadcaster audioBroadcaster = mBroadcasterMap.remove(name);
|
||||||
|
|
||||||
|
if(audioBroadcaster != null)
|
||||||
|
{
|
||||||
|
audioBroadcaster.stop();
|
||||||
|
audioBroadcaster.removeListener();
|
||||||
|
|
||||||
|
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||||
|
|
||||||
|
if(index >= 0)
|
||||||
|
{
|
||||||
|
fireTableRowsUpdated(index, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DefaultAsyncHttpClient getDefaultAsyncHttpClient()
|
||||||
|
{
|
||||||
|
if (mDefaultAsyncHttpClient == null)
|
||||||
|
{
|
||||||
|
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mDefaultAsyncHttpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the broadcast configuration identified by the stream name
|
||||||
|
*/
|
||||||
|
public BroadcastConfiguration getBroadcastConfiguration(String streamName)
|
||||||
|
{
|
||||||
|
return mBroadcastConfigurationMap.get(streamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the listener to receive broadcastAudio configuration events
|
||||||
|
*/
|
||||||
|
public void addListener(Listener<BroadcastEvent> listener)
|
||||||
{
|
{
|
||||||
mBroadcastEventBroadcaster.addListener(listener);
|
mBroadcastEventBroadcaster.addListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the listener from receiving broadcast configuration events
|
* Removes the listener from receiving broadcastAudio configuration events
|
||||||
*/
|
*/
|
||||||
public void removeListener(Listener<BroadcastConfigurationEvent> listener)
|
public void removeListener(Listener<BroadcastEvent> listener)
|
||||||
{
|
{
|
||||||
mBroadcastEventBroadcaster.removeListener(listener);
|
mBroadcastEventBroadcaster.removeListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts the broadcast configuration change event
|
* Broadcasts the broadcastAudio configuration change event
|
||||||
*/
|
*/
|
||||||
public void broadcast( BroadcastConfigurationEvent event )
|
public void broadcast( BroadcastEvent event )
|
||||||
{
|
{
|
||||||
if( event.getEvent() == BroadcastConfigurationEvent.Event.CHANGE )
|
if( event.getEvent() == BroadcastEvent.Event.CONFIGURATION_CHANGE)
|
||||||
{
|
{
|
||||||
int index = mBroadcastConfigurations.indexOf( event.getBroadcastConfiguration() );
|
int index = mBroadcastConfigurations.indexOf( event.getBroadcastConfiguration() );
|
||||||
|
|
||||||
@@ -134,6 +396,88 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
mBroadcastEventBroadcaster.broadcast( event );
|
mBroadcastEventBroadcaster.broadcast( event );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a broadcast event from one of the broadcasters managed by this model
|
||||||
|
*/
|
||||||
|
public void process(BroadcastEvent broadcastEvent)
|
||||||
|
{
|
||||||
|
if(broadcastEvent.isBroadcastConfigurationEvent())
|
||||||
|
{
|
||||||
|
switch(broadcastEvent.getEvent())
|
||||||
|
{
|
||||||
|
case CONFIGURATION_ADD:
|
||||||
|
createBroadcaster(broadcastEvent.getBroadcastConfiguration());
|
||||||
|
break;
|
||||||
|
case CONFIGURATION_CHANGE:
|
||||||
|
//Delete and recreate the broadcaster for any broadcast configuration changes
|
||||||
|
BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
|
||||||
|
cleanupMapAssociations(broadcastConfiguration);
|
||||||
|
createBroadcaster(broadcastConfiguration);
|
||||||
|
break;
|
||||||
|
case CONFIGURATION_DELETE:
|
||||||
|
deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(broadcastEvent.isAudioBroadcasterEvent())
|
||||||
|
{
|
||||||
|
int row = mBroadcastConfigurations.indexOf(broadcastEvent.getAudioBroadcaster().getBroadcastConfiguration());
|
||||||
|
|
||||||
|
switch(broadcastEvent.getEvent())
|
||||||
|
{
|
||||||
|
case BROADCASTER_QUEUE_CHANGE:
|
||||||
|
if(row >= 0)
|
||||||
|
{
|
||||||
|
fireTableCellUpdated(row, COLUMN_BROADCASTER_QUEUE_SIZE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BROADCASTER_STATE_CHANGE:
|
||||||
|
if(row >= 0)
|
||||||
|
{
|
||||||
|
fireTableCellUpdated(row, COLUMN_BROADCASTER_STATUS);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BROADCASTER_STREAMED_COUNT_CHANGE:
|
||||||
|
if(row >= 0)
|
||||||
|
{
|
||||||
|
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Rebroadcast the event to any listeners of this model
|
||||||
|
broadcast(broadcastEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the name of a broadcast configuration changes, remove the old map associations so that they can be replaced
|
||||||
|
* with the new map associations. Remove any broadcaster that is associated with the old configuration name.
|
||||||
|
*
|
||||||
|
* @param broadcastConfiguration
|
||||||
|
*/
|
||||||
|
private void cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
|
||||||
|
{
|
||||||
|
String oldName = null;
|
||||||
|
|
||||||
|
for(Map.Entry<String,BroadcastConfiguration> entry: mBroadcastConfigurationMap.entrySet())
|
||||||
|
{
|
||||||
|
if(entry.getValue() == broadcastConfiguration)
|
||||||
|
{
|
||||||
|
oldName = entry.getKey();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(oldName != null)
|
||||||
|
{
|
||||||
|
deleteBroadcaster(oldName);
|
||||||
|
mBroadcastConfigurationMap.remove(oldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getRowCount()
|
public int getRowCount()
|
||||||
{
|
{
|
||||||
@@ -143,7 +487,7 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
@Override
|
@Override
|
||||||
public int getColumnCount()
|
public int getColumnCount()
|
||||||
{
|
{
|
||||||
return 3;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -157,12 +501,47 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
{
|
{
|
||||||
switch(columnIndex)
|
switch(columnIndex)
|
||||||
{
|
{
|
||||||
case COLUMN_CHANNEL_NAME:
|
case COLUMN_SERVER_ICON:
|
||||||
|
String iconName = configuration.getBroadcastServerType().getIconName();
|
||||||
|
|
||||||
|
if(iconName != null && mSettingsManager != null)
|
||||||
|
{
|
||||||
|
return mSettingsManager.getImageIcon(iconName, 14);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case COLUMN_STREAM_NAME:
|
||||||
return configuration.getName();
|
return configuration.getName();
|
||||||
case COLUMN_SERVER_TYPE:
|
case COLUMN_BROADCASTER_STATUS:
|
||||||
return configuration.getBroadcastServerType().toString();
|
AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
|
||||||
case COLUMN_SERVER_ADDRESS:
|
|
||||||
return configuration.getHost();
|
if(audioBroadcasterA != null)
|
||||||
|
{
|
||||||
|
return audioBroadcasterA.getBroadcastState().toString();
|
||||||
|
}
|
||||||
|
else if(!configuration.isEnabled())
|
||||||
|
{
|
||||||
|
return "Disabled";
|
||||||
|
}
|
||||||
|
else if(!configuration.isValid())
|
||||||
|
{
|
||||||
|
return "Invalid Settings";
|
||||||
|
}
|
||||||
|
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||||
|
AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());
|
||||||
|
|
||||||
|
if(audioBroadcasterB != null)
|
||||||
|
{
|
||||||
|
return audioBroadcasterB.getQueueSize();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||||
|
AudioBroadcaster audioBroadcasterC = mBroadcasterMap.get(configuration.getName());
|
||||||
|
|
||||||
|
if(audioBroadcasterC != null)
|
||||||
|
{
|
||||||
|
return audioBroadcasterC.getStreamedAudioCount();
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -172,6 +551,17 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getColumnClass(int columnIndex)
|
||||||
|
{
|
||||||
|
if(columnIndex == COLUMN_SERVER_ICON)
|
||||||
|
{
|
||||||
|
return ImageIcon.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String.class;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast configuration at the specified model row
|
* Broadcast configuration at the specified model row
|
||||||
*/
|
*/
|
||||||
@@ -193,14 +583,60 @@ public class BroadcastModel extends AbstractTableModel
|
|||||||
{
|
{
|
||||||
switch(column)
|
switch(column)
|
||||||
{
|
{
|
||||||
case COLUMN_CHANNEL_NAME:
|
case COLUMN_SERVER_ICON:
|
||||||
return "Channel";
|
return null;
|
||||||
case COLUMN_SERVER_TYPE:
|
case COLUMN_STREAM_NAME:
|
||||||
return "Server";
|
return "Name";
|
||||||
case COLUMN_SERVER_ADDRESS:
|
case COLUMN_BROADCASTER_STATUS:
|
||||||
return "Address";
|
return "Status";
|
||||||
|
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||||
|
return "Queued";
|
||||||
|
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||||
|
return "Streamed";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any temporary stream recordings left-over from the previous application run.
|
||||||
|
*
|
||||||
|
* This should only be invoked on startup.
|
||||||
|
*/
|
||||||
|
private void removeOrphanedTemporaryRecordings()
|
||||||
|
{
|
||||||
|
Path path = SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY);
|
||||||
|
|
||||||
|
if(path != null && Files.isDirectory(path))
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(TEMPORARY_STREAM_FILE_SUFFIX);
|
||||||
|
sb.append("*.*");
|
||||||
|
|
||||||
|
try(DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path, sb.toString()))
|
||||||
|
{
|
||||||
|
directoryStream.forEach(new Consumer<Path>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void accept(Path path)
|
||||||
|
{
|
||||||
|
mLog.info("Deleting orphaned temporary stream recording: " + path.toString());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Files.delete(path);
|
||||||
|
}
|
||||||
|
catch(IOException ioe)
|
||||||
|
{
|
||||||
|
mLog.error("Couldn't delete orphaned temporary recording: " + path.toString(), ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(IOException ioe)
|
||||||
|
{
|
||||||
|
mLog.error("Error discovering orphaned temporary stream recording files", ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,13 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package audio.broadcast;
|
package audio.broadcast;
|
||||||
|
|
||||||
import alias.Alias;
|
|
||||||
import alias.AliasFactory;
|
|
||||||
import alias.id.AliasID;
|
|
||||||
import alias.id.AliasIDType;
|
|
||||||
import alias.id.AliasIdentifierEditor;
|
|
||||||
import com.jidesoft.swing.JideSplitPane;
|
import com.jidesoft.swing.JideSplitPane;
|
||||||
import gui.editor.Editor;
|
import gui.editor.Editor;
|
||||||
import gui.editor.EmptyEditor;
|
import gui.editor.EmptyEditor;
|
||||||
import net.miginfocom.swing.MigLayout;
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import settings.SettingsManager;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.event.ListSelectionEvent;
|
import javax.swing.event.ListSelectionEvent;
|
||||||
@@ -38,22 +36,27 @@ import java.awt.event.MouseEvent;
|
|||||||
|
|
||||||
public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
|
public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
|
||||||
{
|
{
|
||||||
private static final String NEW_ALIAS = "New";
|
private final static Logger mLog = LoggerFactory.getLogger( BroadcastPanel.class );
|
||||||
private static final String COPY_ALIAS = "Copy";
|
|
||||||
private static final String DELETE_ALIAS = "Delete";
|
private static final String NEW_BROADCAST_CONFIGURATION = "New";
|
||||||
|
private static final String COPY_BROADCAST_CONFIGURATION = "Copy";
|
||||||
|
private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
|
||||||
|
|
||||||
private BroadcastModel mBroadcastModel;
|
private BroadcastModel mBroadcastModel;
|
||||||
|
private SettingsManager mSettingsManager;
|
||||||
|
|
||||||
private JTable mBroadcastConfigurationTable;
|
private JTable mBroadcastConfigurationTable;
|
||||||
private JideSplitPane mSplitPane;
|
private JideSplitPane mSplitPane;
|
||||||
private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:");
|
private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:");
|
||||||
private Editor<BroadcastConfiguration> mEditor;
|
private Editor<BroadcastConfiguration> mEditor;
|
||||||
private JButton mNewButton = new JButton(NEW_ALIAS);
|
private JButton mNewButton = new JButton(NEW_BROADCAST_CONFIGURATION);
|
||||||
private JButton mCopyButton = new JButton(COPY_ALIAS);
|
private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
|
||||||
private JButton mDeleteButton = new JButton(DELETE_ALIAS);
|
private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
|
||||||
|
|
||||||
public BroadcastPanel(BroadcastModel broadcastModel)
|
public BroadcastPanel(BroadcastModel broadcastModel, SettingsManager settingManager)
|
||||||
{
|
{
|
||||||
mBroadcastModel = broadcastModel;
|
mBroadcastModel = broadcastModel;
|
||||||
|
mSettingsManager = settingManager;
|
||||||
mEditor = mEmptyEditor;
|
mEditor = mEmptyEditor;
|
||||||
|
|
||||||
init();
|
init();
|
||||||
@@ -68,6 +71,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
|||||||
mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
|
mBroadcastConfigurationTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
|
||||||
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
mBroadcastConfigurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
|
mBroadcastConfigurationTable.getSelectionModel().addListSelectionListener(BroadcastPanel.this);
|
||||||
|
mBroadcastConfigurationTable.getColumnModel().getColumn(0).setWidth(15);
|
||||||
|
|
||||||
JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
|
JScrollPane scroller = new JScrollPane(mBroadcastConfigurationTable);
|
||||||
|
|
||||||
@@ -75,7 +79,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
|||||||
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
|
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
|
||||||
|
|
||||||
mNewButton = new JButton("New ...");
|
mNewButton = new JButton("New ...");
|
||||||
mNewButton.setToolTipText("Create a new broadcast configuration");
|
mNewButton.setToolTipText("Create a new broadcastAudio configuration");
|
||||||
mNewButton.addMouseListener(new MouseAdapter()
|
mNewButton.addMouseListener(new MouseAdapter()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
@@ -121,18 +125,62 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
|||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e)
|
public void actionPerformed(ActionEvent e)
|
||||||
{
|
{
|
||||||
|
switch(e.getActionCommand())
|
||||||
|
{
|
||||||
|
case COPY_BROADCAST_CONFIGURATION:
|
||||||
|
BroadcastConfiguration copySelected = getSelectedBroadcastConfiguration();
|
||||||
|
mBroadcastModel.cloneBroadcastConfiguration(copySelected);
|
||||||
|
break;
|
||||||
|
case DELETE_BROADCAST_CONFIGURATION:
|
||||||
|
BroadcastConfiguration deleteSelected = getSelectedBroadcastConfiguration();
|
||||||
|
|
||||||
|
if(deleteSelected != null)
|
||||||
|
{
|
||||||
|
int choice = JOptionPane.showConfirmDialog(BroadcastPanel.this, "Do you want to delete broadcast\n" +
|
||||||
|
" configuration " + deleteSelected.getName() + "?", "Delete Broadcast Configuration?",
|
||||||
|
JOptionPane.YES_NO_OPTION);
|
||||||
|
|
||||||
|
if(choice == JOptionPane.YES_OPTION)
|
||||||
|
{
|
||||||
|
mBroadcastModel.removeBroadcastConfiguration(deleteSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the selected configuration
|
||||||
|
*/
|
||||||
|
private BroadcastConfiguration getSelectedBroadcastConfiguration()
|
||||||
|
{
|
||||||
|
int viewRow = mBroadcastConfigurationTable.getSelectedRow();
|
||||||
|
|
||||||
|
if(viewRow >= 0)
|
||||||
|
{
|
||||||
|
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(viewRow);
|
||||||
|
|
||||||
|
if(modelRow >= 0)
|
||||||
|
{
|
||||||
|
return mBroadcastModel.getConfigurationAt(modelRow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void valueChanged(ListSelectionEvent e)
|
public void valueChanged(ListSelectionEvent e)
|
||||||
{
|
{
|
||||||
if (!e.getValueIsAdjusting())
|
if (!e.getValueIsAdjusting())
|
||||||
{
|
{
|
||||||
int row = mBroadcastConfigurationTable.getSelectedRow();
|
BroadcastConfiguration selectedConfiguration = getSelectedBroadcastConfiguration();
|
||||||
int modelRow = mBroadcastConfigurationTable.convertRowIndexToModel(row);
|
|
||||||
|
|
||||||
BroadcastConfiguration selectedConfiguration = mBroadcastModel.getConfigurationAt(modelRow);
|
//Enable buttons accordingly
|
||||||
|
mCopyButton.setEnabled(selectedConfiguration != null);
|
||||||
|
mDeleteButton.setEnabled(selectedConfiguration != null);
|
||||||
|
|
||||||
if (mEditor.isModified())
|
if (mEditor.isModified())
|
||||||
{
|
{
|
||||||
@@ -153,7 +201,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
|||||||
|
|
||||||
if (selectedConfiguration != null)
|
if (selectedConfiguration != null)
|
||||||
{
|
{
|
||||||
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel);
|
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mSettingsManager);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -186,6 +234,7 @@ public class BroadcastPanel extends JPanel implements ActionListener, ListSelect
|
|||||||
final BroadcastConfiguration configuration =
|
final BroadcastConfiguration configuration =
|
||||||
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
|
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
|
||||||
|
|
||||||
|
|
||||||
mBroadcastModel.addBroadcastConfiguration(configuration);
|
mBroadcastModel.addBroadcastConfiguration(configuration);
|
||||||
|
|
||||||
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
|
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
|
||||||
|
|||||||
@@ -20,36 +20,29 @@ package audio.broadcast;
|
|||||||
|
|
||||||
public enum BroadcastServerType
|
public enum BroadcastServerType
|
||||||
{
|
{
|
||||||
/**
|
BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
|
||||||
* Broadcastify is currently using Icecast Server 2.3.2
|
ICECAST_HTTP("Icecast 2.4+", null),
|
||||||
*/
|
ICECAST_TCP("Icecast 2.3", null),
|
||||||
BROADCASTIFY("Broadcastify"),
|
SHOUTCAST_V1("Shoutcast 1.x", null),
|
||||||
|
SHOUTCAST_V2("Shoutcast 2.x", null),
|
||||||
/**
|
UNKNOWN("Unknown", null);
|
||||||
* Icecast Server 2.4.x and later
|
|
||||||
*/
|
|
||||||
ICECAST_HTTP("Icecast 2.4+"),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Icecast Server 2.3.x
|
|
||||||
*/
|
|
||||||
ICECAST_TCP("Icecast 2.3"),
|
|
||||||
|
|
||||||
SHOUTCAST_V1("Shoutcast 1.x"),
|
|
||||||
|
|
||||||
SHOUTCAST_V2("Shoutcast 2.x"),
|
|
||||||
|
|
||||||
UNKNOWN("Unknown");
|
|
||||||
|
|
||||||
private String mLabel;
|
private String mLabel;
|
||||||
|
private String mIconName;
|
||||||
|
|
||||||
BroadcastServerType(String label)
|
BroadcastServerType(String label, String iconName)
|
||||||
{
|
{
|
||||||
mLabel = label;
|
mLabel = label;
|
||||||
|
mIconName = iconName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
return mLabel;
|
return mLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getIconName()
|
||||||
|
{
|
||||||
|
return mIconName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public enum BroadcastState
|
|||||||
BROADCAST_ERROR("Broadcast Error", false),
|
BROADCAST_ERROR("Broadcast Error", false),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connected to the broadcast server and capable of streaming
|
* Connected to the broadcastAudio server and capable of streaming
|
||||||
*/
|
*/
|
||||||
CONNECTED("Connected", false),
|
CONNECTED("Connected", false),
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ public enum BroadcastState
|
|||||||
CONNECTING("Connecting", false),
|
CONNECTING("Connecting", false),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnected from the broadcast server
|
* Disconnected from the broadcastAudio server
|
||||||
*/
|
*/
|
||||||
DISCONNECTED("Disconnected", true),
|
DISCONNECTED("Disconnected", true),
|
||||||
|
|
||||||
|
|||||||
@@ -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 static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
|
||||||
|
|
||||||
private ThreadPoolManager mThreadPoolManager;
|
private ThreadPoolManager mThreadPoolManager;
|
||||||
private BroadcastFormat mBroadcastFormat;
|
private AudioBroadcaster mAudioBroadcaster;
|
||||||
private Listener<StreamableAudioRecording> mOutputListener;
|
|
||||||
private long mDelay;
|
private long mDelay;
|
||||||
private Path mTempDirectory;
|
private Path mTempDirectory;
|
||||||
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
|
private Map<Integer, AudioRecorder> mStreamRecorders = new HashMap<>();
|
||||||
@@ -57,24 +56,17 @@ public class StreamManager implements Listener<AudioPacket>
|
|||||||
* has elapsed.
|
* has elapsed.
|
||||||
*
|
*
|
||||||
* @param threadPoolManager for scheduling runnables
|
* @param threadPoolManager for scheduling runnables
|
||||||
* @param broadcastFormat defines the output recording format
|
* @param audioBroadcaster to receive completed streamable audio recordings
|
||||||
* @param outputListener to receive completed streamable audio recordings
|
|
||||||
* @param delay from recording start until audio recording is nominated to output listener
|
* @param delay from recording start until audio recording is nominated to output listener
|
||||||
* @param tempDirectory where to store temporary audio recordings
|
* @param tempDirectory where to store temporary audio recordings
|
||||||
*/
|
*/
|
||||||
public StreamManager(ThreadPoolManager threadPoolManager,
|
public StreamManager(ThreadPoolManager threadPoolManager, AudioBroadcaster audioBroadcaster, long delay, Path tempDirectory)
|
||||||
BroadcastFormat broadcastFormat,
|
|
||||||
Listener<StreamableAudioRecording> outputListener,
|
|
||||||
long delay,
|
|
||||||
Path tempDirectory)
|
|
||||||
{
|
{
|
||||||
assert(tempDirectory != null && Files.isDirectory(tempDirectory));
|
assert(tempDirectory != null && Files.isDirectory(tempDirectory));
|
||||||
assert(mThreadPoolManager != null);
|
assert(mThreadPoolManager != null);
|
||||||
assert(mBroadcastFormat != null);
|
|
||||||
|
|
||||||
mThreadPoolManager = threadPoolManager;
|
mThreadPoolManager = threadPoolManager;
|
||||||
mBroadcastFormat = broadcastFormat;
|
mAudioBroadcaster = audioBroadcaster;
|
||||||
mOutputListener = outputListener;
|
|
||||||
mDelay = delay;
|
mDelay = delay;
|
||||||
mTempDirectory = tempDirectory;
|
mTempDirectory = tempDirectory;
|
||||||
}
|
}
|
||||||
@@ -142,7 +134,7 @@ public class StreamManager implements Listener<AudioPacket>
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
|
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
|
||||||
mBroadcastFormat);
|
mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat());
|
||||||
recorder.start(mThreadPoolManager.getScheduledExecutorService());
|
recorder.start(mThreadPoolManager.getScheduledExecutorService());
|
||||||
recorder.receive(audioPacket);
|
recorder.receive(audioPacket);
|
||||||
mStreamRecorders.put(sourceChannelID, recorder);
|
mStreamRecorders.put(sourceChannelID, recorder);
|
||||||
@@ -176,11 +168,11 @@ public class StreamManager implements Listener<AudioPacket>
|
|||||||
recorder.stop();
|
recorder.stop();
|
||||||
|
|
||||||
StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(),
|
StreamableAudioRecording streamableAudioRecording = new StreamableAudioRecording(recorder.getPath(),
|
||||||
recorder.getMetadata());
|
recorder.getMetadata(), recorder.getRecordingLength());
|
||||||
|
|
||||||
if(mDelay <= 0)
|
if(mDelay <= 0)
|
||||||
{
|
{
|
||||||
mOutputListener.receive(streamableAudioRecording);
|
mAudioBroadcaster.receive(streamableAudioRecording);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -198,9 +190,9 @@ public class StreamManager implements Listener<AudioPacket>
|
|||||||
private Path getTemporaryRecordingPath()
|
private Path getTemporaryRecordingPath()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("temporary_streaming_file_");
|
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
|
||||||
sb.append( TimeStamp.getLongTimeStamp( "_" ) );
|
sb.append( TimeStamp.getLongTimeStamp( "_" ) );
|
||||||
sb.append( mBroadcastFormat.getFileExtension() );
|
sb.append( mAudioBroadcaster.getBroadcastConfiguration().getBroadcastFormat().getFileExtension() );
|
||||||
|
|
||||||
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
|
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
|
||||||
|
|
||||||
@@ -222,9 +214,9 @@ public class StreamManager implements Listener<AudioPacket>
|
|||||||
@Override
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
if(mOutputListener != null)
|
if(mAudioBroadcaster != null)
|
||||||
{
|
{
|
||||||
mOutputListener.receive(mStreamableAudioRecording);
|
mAudioBroadcaster.receive(mStreamableAudioRecording);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,19 @@ public class StreamableAudioRecording
|
|||||||
{
|
{
|
||||||
private Path mPath;
|
private Path mPath;
|
||||||
private AudioMetadata mAudioMetadata;
|
private AudioMetadata mAudioMetadata;
|
||||||
|
private long mRecordingLengthMilliSeconds;
|
||||||
|
|
||||||
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata)
|
/**
|
||||||
|
* Audio recording that is ready to be streamed
|
||||||
|
* @param path to the audio recording file
|
||||||
|
* @param audioMetadata associated with the recording
|
||||||
|
* @param recordingLength in milliseconds
|
||||||
|
*/
|
||||||
|
public StreamableAudioRecording(Path path, AudioMetadata audioMetadata, long recordingLength)
|
||||||
{
|
{
|
||||||
mPath = path;
|
mPath = path;
|
||||||
mAudioMetadata = audioMetadata;
|
mAudioMetadata = audioMetadata;
|
||||||
|
mRecordingLengthMilliSeconds = recordingLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,4 +56,12 @@ public class StreamableAudioRecording
|
|||||||
{
|
{
|
||||||
return mAudioMetadata;
|
return mAudioMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recording length in milliseconds
|
||||||
|
*/
|
||||||
|
public long getRecordingLength()
|
||||||
|
{
|
||||||
|
return mRecordingLengthMilliSeconds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package audio.broadcast.broadcastify;
|
package audio.broadcast.broadcastify;
|
||||||
|
|
||||||
|
import audio.broadcast.BroadcastConfiguration;
|
||||||
import audio.broadcast.BroadcastFormat;
|
import audio.broadcast.BroadcastFormat;
|
||||||
import audio.broadcast.BroadcastServerType;
|
import audio.broadcast.BroadcastServerType;
|
||||||
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||||
@@ -26,11 +27,12 @@ import javax.xml.bind.annotation.XmlAttribute;
|
|||||||
|
|
||||||
public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||||
{
|
{
|
||||||
public int mFeedID;
|
private int mFeedID;
|
||||||
|
|
||||||
public BroadcastifyConfiguration()
|
public BroadcastifyConfiguration()
|
||||||
{
|
{
|
||||||
//No-arg constructor for JAXB
|
//No-arg constructor for JAXB
|
||||||
|
this(BroadcastFormat.MP3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +49,34 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
|||||||
setSampleRate(8000);
|
setSampleRate(8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BroadcastConfiguration copyOf()
|
||||||
|
{
|
||||||
|
BroadcastifyConfiguration copy = new BroadcastifyConfiguration(getBroadcastFormat());
|
||||||
|
|
||||||
|
//Broadcast Configuration Parameters
|
||||||
|
copy.setName(getName());
|
||||||
|
copy.setHost(getHost());
|
||||||
|
copy.setPort(getPort());
|
||||||
|
copy.setPassword(getPassword());
|
||||||
|
copy.setDelay(getDelay());
|
||||||
|
|
||||||
|
copy.setEnabled(false);
|
||||||
|
|
||||||
|
//Icecast Configuration Parameters
|
||||||
|
copy.setUserName(getUserName());
|
||||||
|
copy.setMountPoint(getMountPoint());
|
||||||
|
copy.setDescription(getDescription());
|
||||||
|
copy.setGenre(getGenre());
|
||||||
|
copy.setPublic(isPublic());
|
||||||
|
copy.setURL(getURL());
|
||||||
|
|
||||||
|
//Broadcastify Configuration Parameters
|
||||||
|
copy.setFeedID(getFeedID());
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BroadcastServerType getBroadcastServerType()
|
public BroadcastServerType getBroadcastServerType()
|
||||||
{
|
{
|
||||||
@@ -63,4 +93,10 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
|||||||
{
|
{
|
||||||
mFeedID = feedID;
|
mFeedID = feedID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
return super.isValid() && (mFeedID > 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,14 @@
|
|||||||
package audio.broadcast.broadcastify;
|
package audio.broadcast.broadcastify;
|
||||||
|
|
||||||
import audio.broadcast.BroadcastConfiguration;
|
import audio.broadcast.BroadcastConfiguration;
|
||||||
import audio.broadcast.BroadcastConfigurationEvent;
|
import audio.broadcast.BroadcastEvent;
|
||||||
import audio.broadcast.BroadcastModel;
|
import audio.broadcast.BroadcastModel;
|
||||||
|
import audio.broadcast.BroadcastServerType;
|
||||||
import gui.editor.DocumentListenerEditor;
|
import gui.editor.DocumentListenerEditor;
|
||||||
import net.miginfocom.swing.MigLayout;
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import settings.SettingsManager;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
@@ -31,6 +35,9 @@ import java.awt.event.ActionListener;
|
|||||||
|
|
||||||
public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
|
public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
|
||||||
{
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger( BroadcastifyConfigurationEditor.class );
|
||||||
|
|
||||||
|
private SettingsManager mSettingsManager;
|
||||||
private BroadcastModel mBroadcastModel;
|
private BroadcastModel mBroadcastModel;
|
||||||
private JTextField mFeedID;
|
private JTextField mFeedID;
|
||||||
private JTextField mName;
|
private JTextField mName;
|
||||||
@@ -40,24 +47,27 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
|||||||
private JTextField mPassword;
|
private JTextField mPassword;
|
||||||
private JButton mSaveButton;
|
private JButton mSaveButton;
|
||||||
private JButton mResetButton;
|
private JButton mResetButton;
|
||||||
|
private JCheckBox mEnabled;
|
||||||
|
|
||||||
public BroadcastifyConfigurationEditor(BroadcastModel model)
|
public BroadcastifyConfigurationEditor(BroadcastModel model, SettingsManager settingsManager)
|
||||||
{
|
{
|
||||||
mBroadcastModel = model;
|
mBroadcastModel = model;
|
||||||
|
mSettingsManager = settingsManager;
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init()
|
private void init()
|
||||||
{
|
{
|
||||||
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][][grow,fill]" ) );
|
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]", "[][][][][][][][][][grow,fill]" ) );
|
||||||
setPreferredSize(new Dimension(150,400));
|
setPreferredSize(new Dimension(150,400));
|
||||||
|
|
||||||
add(new JLabel("Broadcastify Channel"), "span, align center");
|
|
||||||
|
|
||||||
JButton selectButton = new JButton("Select ...");
|
JLabel channelLabel = new JLabel("Broadcastify Stream");
|
||||||
add(selectButton, "wrap");
|
|
||||||
|
|
||||||
add(new JSeparator(JSeparator.HORIZONTAL), "span,growx");
|
ImageIcon broadcastifyIcon = mSettingsManager.getImageIcon(BroadcastServerType.BROADCASTIFY.getIconName(), 25);
|
||||||
|
channelLabel.setIcon(broadcastifyIcon);
|
||||||
|
|
||||||
|
add(channelLabel, "span, align center");
|
||||||
|
|
||||||
add(new JLabel("Feed ID:"));
|
add(new JLabel("Feed ID:"));
|
||||||
mFeedID = new JTextField();
|
mFeedID = new JTextField();
|
||||||
@@ -89,6 +99,17 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
|||||||
mPassword.getDocument().addDocumentListener(this);
|
mPassword.getDocument().addDocumentListener(this);
|
||||||
add(mPassword);
|
add(mPassword);
|
||||||
|
|
||||||
|
mEnabled = new JCheckBox("Enabled");
|
||||||
|
mEnabled.addActionListener(new ActionListener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e)
|
||||||
|
{
|
||||||
|
setModified(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(mEnabled, "span");
|
||||||
|
|
||||||
mSaveButton = new JButton("Save");
|
mSaveButton = new JButton("Save");
|
||||||
mSaveButton.setEnabled(false);
|
mSaveButton.setEnabled(false);
|
||||||
mSaveButton.addActionListener(new ActionListener()
|
mSaveButton.addActionListener(new ActionListener()
|
||||||
@@ -139,6 +160,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
|||||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||||
mMountPoint.setText(config.getMountPoint());
|
mMountPoint.setText(config.getMountPoint());
|
||||||
mPassword.setText(config.getPassword());
|
mPassword.setText(config.getPassword());
|
||||||
|
mEnabled.setSelected(config.isEnabled());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -148,6 +170,7 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
|||||||
mPort.setText(null);
|
mPort.setText(null);
|
||||||
mMountPoint.setText(null);
|
mMountPoint.setText(null);
|
||||||
mPassword.setText(null);
|
mPassword.setText(null);
|
||||||
|
mEnabled.setSelected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setModified(false);
|
setModified(false);
|
||||||
@@ -158,14 +181,151 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
|||||||
{
|
{
|
||||||
BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
|
BroadcastifyConfiguration config = (BroadcastifyConfiguration)getItem();
|
||||||
|
|
||||||
|
if(validateConfiguration())
|
||||||
|
{
|
||||||
|
config.setName(mName.getText());
|
||||||
|
config.setFeedID(getFeedID());
|
||||||
|
config.setHost(mServer.getText());
|
||||||
|
config.setPort(getPort());
|
||||||
|
config.setMountPoint(mMountPoint.getText());
|
||||||
|
config.setPassword(mPassword.getText());
|
||||||
|
config.setEnabled(mEnabled.isSelected());
|
||||||
|
|
||||||
|
setModified(false);
|
||||||
|
|
||||||
|
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the user specified values in the controls prior to saving them to the configuration
|
||||||
|
*/
|
||||||
|
private boolean validateConfiguration()
|
||||||
|
{
|
||||||
|
String name = mName.getText();
|
||||||
|
|
||||||
|
//A unique stream name is required
|
||||||
|
if(!mBroadcastModel.isUniqueName(name, getItem()))
|
||||||
|
{
|
||||||
|
String message = (name == null || name.isEmpty()) ?
|
||||||
|
"Please specify a stream name." : "Stream name " + name +
|
||||||
|
"is already in use.\nPlease choose another name.";
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this,
|
||||||
|
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
|
||||||
|
|
||||||
|
mName.requestFocus();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Feed ID is optional, but required
|
||||||
|
if(!validateIntegerTextField(mFeedID, "Invalid Feed ID", "Please specify a feed ID", 1, Integer.MAX_VALUE))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Server address is optional, but required
|
||||||
|
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Port is optional, but required
|
||||||
|
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Mount Point is optional, but required
|
||||||
|
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Password is optional, but required
|
||||||
|
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a text field control for a non-null, non-empty value
|
||||||
|
* @param field to validate
|
||||||
|
* @param title to use for error dialog
|
||||||
|
* @param message to use for error dialog
|
||||||
|
* @return true if field contains a non-null, non-empty value
|
||||||
|
*/
|
||||||
|
private boolean validateTextField(JTextField field, String title, String message)
|
||||||
|
{
|
||||||
|
String text = field.getText();
|
||||||
|
|
||||||
|
if(text == null || text.isEmpty())
|
||||||
|
{
|
||||||
|
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
|
||||||
|
JOptionPane.ERROR_MESSAGE);
|
||||||
|
|
||||||
|
field.requestFocus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the text field control that contains an integer value for non-null, non-empty and within the
|
||||||
|
* specified min/max valid range.
|
||||||
|
* @param field to validate
|
||||||
|
* @param title to use for error dialog
|
||||||
|
* @param message to use for error dialog
|
||||||
|
* @param minValid value
|
||||||
|
* @param maxValid value
|
||||||
|
* @return true if field contains a non-null, non-empty value within the valid min/max range
|
||||||
|
*/
|
||||||
|
private boolean validateIntegerTextField(JTextField field, String title, String message, int minValid, int maxValid)
|
||||||
|
{
|
||||||
|
if(validateTextField(field, title, message))
|
||||||
|
{
|
||||||
|
String text = field.getText();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int value = Integer.parseInt(text);
|
||||||
|
|
||||||
|
if(minValid <= value && value <= maxValid)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
//Do nothing, we couldn't parse the number value
|
||||||
|
}
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this, message, title,
|
||||||
|
JOptionPane.ERROR_MESSAGE);
|
||||||
|
|
||||||
|
field.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an Integer from the feed ID text control or returns 0 if the control doesn't contain an integer value.
|
||||||
|
*/
|
||||||
|
private int getFeedID()
|
||||||
|
{
|
||||||
String feedID = mFeedID.getText();
|
String feedID = mFeedID.getText();
|
||||||
|
|
||||||
if(feedID != null && !feedID.isEmpty())
|
if(feedID != null && !feedID.isEmpty())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int id = Integer.parseInt(feedID);
|
return Integer.parseInt(feedID);
|
||||||
config.setFeedID(id);
|
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
@@ -173,17 +333,21 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setName(mName.getText());
|
return 0;
|
||||||
config.setHost(mServer.getText());
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||||
|
*/
|
||||||
|
private int getPort()
|
||||||
|
{
|
||||||
String port = mPort.getText();
|
String port = mPort.getText();
|
||||||
|
|
||||||
if(port != null && !port.isEmpty())
|
if(port != null && !port.isEmpty())
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
int number = Integer.parseInt(port);
|
return Integer.parseInt(port);
|
||||||
config.setPort(number);
|
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
@@ -191,11 +355,6 @@ public class BroadcastifyConfigurationEditor extends DocumentListenerEditor<Broa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.setMountPoint(mMountPoint.getText());
|
return 0;
|
||||||
config.setPassword(mPassword.getText());
|
|
||||||
|
|
||||||
setModified(false);
|
|
||||||
|
|
||||||
mBroadcastModel.broadcast(new BroadcastConfigurationEvent(getItem(), BroadcastConfigurationEvent.Event.CHANGE));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
package audio.broadcast.icecast;
|
package audio.broadcast.icecast;
|
||||||
|
|
||||||
import audio.AudioPacket;
|
import audio.AudioPacket;
|
||||||
|
import audio.broadcast.AudioBroadcaster;
|
||||||
import audio.broadcast.BroadcastFormat;
|
import audio.broadcast.BroadcastFormat;
|
||||||
import audio.broadcast.BroadcastState;
|
import audio.broadcast.BroadcastState;
|
||||||
import audio.broadcast.Broadcaster;
|
|
||||||
import audio.broadcast.BroadcastFactory;
|
import audio.broadcast.BroadcastFactory;
|
||||||
import audio.convert.IAudioConverter;
|
import audio.metadata.AudioMetadata;
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
import org.asynchttpclient.AsyncHandler;
|
import org.asynchttpclient.AsyncHandler;
|
||||||
import org.asynchttpclient.BoundRequestBuilder;
|
import org.asynchttpclient.BoundRequestBuilder;
|
||||||
@@ -53,9 +53,9 @@ import java.util.List;
|
|||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<ByteBuffer>
|
public class IcecastHTTPAudioBroadcaster extends AudioBroadcaster implements Publisher<ByteBuffer>
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPBroadcaster.class );
|
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPAudioBroadcaster.class );
|
||||||
|
|
||||||
private static final String HTTP_100_CONTINUE = "100-continue";
|
private static final String HTTP_100_CONTINUE = "100-continue";
|
||||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
||||||
@@ -69,15 +69,13 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
|
|||||||
/**
|
/**
|
||||||
* Creates an Icecast 2.4.x (or newer) compatible broadcaster using HTTP 1.1 protocol
|
* Creates an Icecast 2.4.x (or newer) compatible broadcaster using HTTP 1.1 protocol
|
||||||
*
|
*
|
||||||
* Note: use @see IcecastTCPBroadcaster for Icecast version 2.3.x and older.
|
* Note: use @see IcecastTCPAudioBroadcaster for Icecast version 2.3.x and older.
|
||||||
* @param configuration
|
* @param configuration
|
||||||
* @param audioConverter
|
* @param audioConverter
|
||||||
*/
|
*/
|
||||||
public IcecastHTTPBroadcaster(ThreadPoolManager threadPoolManager,
|
public IcecastHTTPAudioBroadcaster(ThreadPoolManager threadPoolManager, IcecastHTTPConfiguration configuration)
|
||||||
IcecastHTTPConfiguration configuration,
|
|
||||||
IAudioConverter audioConverter)
|
|
||||||
{
|
{
|
||||||
super(threadPoolManager, configuration, audioConverter);
|
super(threadPoolManager, configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,31 +86,24 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
|
|||||||
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
|
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void broadcastMetadata(AudioMetadata metadata)
|
||||||
|
{
|
||||||
|
mLog.debug("Request to send metadata to Icecast HTTP server - needs code!");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast any queued audio packets using Icecast V2 Protocol
|
* Broadcast any queued audio packets using Icecast V2 Protocol
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void broadcast()
|
protected void broadcastAudio(byte[] audio)
|
||||||
{
|
{
|
||||||
if(connect())
|
if(connect())
|
||||||
{
|
{
|
||||||
if (mSubscription != null && mSubscription.hasDemand())
|
if (mSubscription != null && mSubscription.hasDemand())
|
||||||
{
|
{
|
||||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
mSubscription.broadcast(ByteBuffer.wrap(audio));
|
||||||
|
|
||||||
if (!mPacketsToBroadcast.isEmpty())
|
|
||||||
{
|
|
||||||
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
|
|
||||||
|
|
||||||
mLog.debug("We have [" + mPacketsToBroadcast.size() +
|
|
||||||
"] packets - sending:" + convertedAudio.length + " bytes");
|
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.wrap(convertedAudio);
|
|
||||||
|
|
||||||
mSubscription.broadcast(buffer);
|
|
||||||
|
|
||||||
mPacketsToBroadcast.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,8 +409,9 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
||||||
|
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
|
||||||
|
|
||||||
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
|
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
|
||||||
|
|
||||||
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
||||||
mLog.debug("Opening: " + path.toString());
|
mLog.debug("Opening: " + path.toString());
|
||||||
@@ -432,7 +424,7 @@ public class IcecastHTTPBroadcaster extends Broadcaster implements Publisher<Byt
|
|||||||
|
|
||||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||||
{
|
{
|
||||||
reader.setListener(broadcaster);
|
reader.setListener(audioBroadcaster);
|
||||||
reader.read();
|
reader.read();
|
||||||
}
|
}
|
||||||
catch (IOException e)
|
catch (IOException e)
|
||||||
@@ -36,6 +36,11 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
private int mSampleRate = 8000;
|
private int mSampleRate = 8000;
|
||||||
private String mURL;
|
private String mURL;
|
||||||
|
|
||||||
|
public IcecastHTTPConfiguration()
|
||||||
|
{
|
||||||
|
//no-arg JAXB constructor
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Icecast 2.3.x and 2.4.x compatible configuration
|
* Icecast 2.3.x and 2.4.x compatible configuration
|
||||||
* @param format
|
* @param format
|
||||||
@@ -45,6 +50,33 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
super(format);
|
super(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BroadcastConfiguration copyOf()
|
||||||
|
{
|
||||||
|
IcecastHTTPConfiguration copy = new IcecastHTTPConfiguration(getBroadcastFormat());
|
||||||
|
|
||||||
|
//Broadcast Configuration Parameters
|
||||||
|
copy.setName(getName());
|
||||||
|
copy.setHost(getHost());
|
||||||
|
copy.setPort(getPort());
|
||||||
|
copy.setPassword(getPassword());
|
||||||
|
copy.setDelay(getDelay());
|
||||||
|
copy.setEnabled(false);
|
||||||
|
|
||||||
|
//Icecast Configuration Parameters
|
||||||
|
copy.setUserName(getUserName());
|
||||||
|
copy.setMountPoint(getMountPoint());
|
||||||
|
copy.setDescription(getDescription());
|
||||||
|
copy.setGenre(getGenre());
|
||||||
|
copy.setPublic(isPublic());
|
||||||
|
copy.setBitRate(getBitRate());
|
||||||
|
copy.setChannels(getChannels());
|
||||||
|
copy.setSampleRate(getSampleRate());
|
||||||
|
copy.setURL(getURL());
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BroadcastServerType getBroadcastServerType()
|
public BroadcastServerType getBroadcastServerType()
|
||||||
{
|
{
|
||||||
@@ -149,7 +181,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public visibility of the broadcast
|
* Public visibility of the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public boolean isPublic()
|
public boolean isPublic()
|
||||||
{
|
{
|
||||||
@@ -157,8 +189,8 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets public visibility of the broadcast
|
* Sets public visibility of the broadcastAudio
|
||||||
* @param isPublic indicates if the broadcast should be visible to the public
|
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||||
*/
|
*/
|
||||||
public void setPublic(boolean isPublic)
|
public void setPublic(boolean isPublic)
|
||||||
{
|
{
|
||||||
@@ -166,7 +198,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of audio channels in the broadcast
|
* Number of audio channels in the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public int getChannels()
|
public int getChannels()
|
||||||
{
|
{
|
||||||
@@ -174,7 +206,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the number of audio channels in the broadcast
|
* Sets the number of audio channels in the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public void setChannels(int channels)
|
public void setChannels(int channels)
|
||||||
{
|
{
|
||||||
@@ -224,7 +256,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
*/
|
*/
|
||||||
public String getURL()
|
public String getURL()
|
||||||
{
|
{
|
||||||
@@ -232,7 +264,7 @@ public class IcecastHTTPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
public void setURL(String url)
|
public void setURL(String url)
|
||||||
|
|||||||
@@ -18,16 +18,24 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package audio.broadcast.icecast;
|
package audio.broadcast.icecast;
|
||||||
|
|
||||||
|
import alias.Alias;
|
||||||
|
import alias.id.broadcast.BroadcastChannel;
|
||||||
import audio.AudioPacket;
|
import audio.AudioPacket;
|
||||||
|
import audio.broadcast.AudioBroadcaster;
|
||||||
|
import audio.broadcast.BroadcastFactory;
|
||||||
import audio.broadcast.BroadcastFormat;
|
import audio.broadcast.BroadcastFormat;
|
||||||
import audio.broadcast.BroadcastState;
|
import audio.broadcast.BroadcastState;
|
||||||
import audio.broadcast.Broadcaster;
|
import audio.metadata.AudioMetadata;
|
||||||
import audio.broadcast.BroadcastFactory;
|
import audio.metadata.Metadata;
|
||||||
import audio.convert.IAudioConverter;
|
import audio.metadata.MetadataType;
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
|
import org.asynchttpclient.AsyncCompletionHandler;
|
||||||
|
import org.asynchttpclient.AsyncHandler;
|
||||||
import org.asynchttpclient.BoundRequestBuilder;
|
import org.asynchttpclient.BoundRequestBuilder;
|
||||||
import org.asynchttpclient.DefaultAsyncHttpClient;
|
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||||
|
import org.asynchttpclient.ListenableFuture;
|
||||||
import org.asynchttpclient.Realm;
|
import org.asynchttpclient.Realm;
|
||||||
|
import org.asynchttpclient.Response;
|
||||||
import org.asynchttpclient.uri.Uri;
|
import org.asynchttpclient.uri.Uri;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -37,6 +45,7 @@ import record.wave.AudioPacketMonoWaveReader;
|
|||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.ConnectException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
@@ -44,91 +53,186 @@ import java.net.URLEncoder;
|
|||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
public class IcecastTCPBroadcaster extends Broadcaster
|
public class IcecastTCPAudioBroadcaster extends AudioBroadcaster
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPBroadcaster.class );
|
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPAudioBroadcaster.class );
|
||||||
private final static String UTF8 = "UTF-8";
|
private final static String UTF8 = "UTF-8";
|
||||||
private final static String TERMINATOR = "\r\n";
|
private final static String TERMINATOR = "\r\n";
|
||||||
private final static String SEPARATOR = ":";
|
private final static String SEPARATOR = ":";
|
||||||
|
|
||||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
||||||
private long mLastConnectionAttempt = 0;
|
private long mLastConnectionAttempt = 0;
|
||||||
private List<AudioPacket> mPacketsToBroadcast = new ArrayList<>();
|
|
||||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||||
private Socket mSocket;
|
private Socket mSocket;
|
||||||
private DataOutputStream mOutputStream;
|
private DataOutputStream mOutputStream;
|
||||||
private DataInputStream mInputStream;
|
private DataInputStream mInputStream;
|
||||||
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
|
private DefaultAsyncHttpClient mDefaultAsyncHttpClient;
|
||||||
|
private boolean mFirstMetadataUpdateSuppressed = false;
|
||||||
private int mCounter;
|
private byte[] mSilenceFrame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
|
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
|
||||||
* compatible with Icecast version 2.3.2 and older versions of the server software.
|
* compatible with Icecast version 2.3.2 and older versions of the server software.
|
||||||
*
|
*
|
||||||
* Note: use @see IcecastHTTPBroadcaster for Icecast version 2.4.x and newer.
|
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
|
||||||
*
|
*
|
||||||
* @param configuration
|
* @param configuration
|
||||||
* @param audioConverter
|
|
||||||
*/
|
*/
|
||||||
public IcecastTCPBroadcaster(ThreadPoolManager threadPoolManager,
|
public IcecastTCPAudioBroadcaster(DefaultAsyncHttpClient httpClient,
|
||||||
IcecastTCPConfiguration configuration,
|
ThreadPoolManager threadPoolManager,
|
||||||
IAudioConverter audioConverter)
|
IcecastTCPConfiguration configuration)
|
||||||
{
|
{
|
||||||
super(threadPoolManager, configuration, audioConverter);
|
super(threadPoolManager, configuration);
|
||||||
|
|
||||||
|
mDefaultAsyncHttpClient = httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration information
|
* Configuration information
|
||||||
*/
|
*/
|
||||||
private IcecastHTTPConfiguration getConfiguration()
|
private IcecastTCPConfiguration getConfiguration()
|
||||||
{
|
{
|
||||||
return (IcecastHTTPConfiguration)getBroadcastConfiguration();
|
return (IcecastTCPConfiguration)getBroadcastConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast any queued audio packets using Icecast V2 Protocol
|
* Broadcast any queued audio packets using Icecast V2 Protocol
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void broadcast()
|
protected void broadcastAudio(byte[] audio)
|
||||||
{
|
{
|
||||||
if(connect())
|
boolean completed = false;
|
||||||
|
|
||||||
|
while(!completed)
|
||||||
{
|
{
|
||||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
if(connect())
|
||||||
|
|
||||||
if (!mPacketsToBroadcast.isEmpty())
|
|
||||||
{
|
{
|
||||||
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
|
|
||||||
|
|
||||||
mLog.debug("We have [" + mPacketsToBroadcast.size() +
|
|
||||||
"] packets - sending:" + convertedAudio.length + " bytes");
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
send(convertedAudio);
|
send(audio);
|
||||||
|
completed = true;
|
||||||
}
|
}
|
||||||
catch(SocketException se)
|
catch(SocketException se)
|
||||||
{
|
{
|
||||||
disconnect();
|
//The remote server likely disconnected - setup to reconnect
|
||||||
|
mLog.error("Socket Exception on Icecast TCP Audio Broadcaster -- disconnecting and resetting", se);
|
||||||
|
disconnect(false);
|
||||||
}
|
}
|
||||||
catch(IOException e)
|
catch(IOException e)
|
||||||
{
|
{
|
||||||
|
completed = true;
|
||||||
mLog.error("Error sending audio", e);
|
mLog.error("Error sending audio", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mPacketsToBroadcast.clear();
|
@Override
|
||||||
|
protected void broadcastMetadata(AudioMetadata metadata)
|
||||||
|
{
|
||||||
|
if(connected() && mFirstMetadataUpdateSuppressed)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
mCounter++;
|
if(metadata != null)
|
||||||
|
{
|
||||||
|
Metadata to = metadata.getMetadata(MetadataType.TO);
|
||||||
|
|
||||||
if(mCounter % 10 == 0)
|
sb.append("TO:");
|
||||||
|
|
||||||
|
if(to != null)
|
||||||
{
|
{
|
||||||
updateMetadata("Song Count " + mCounter);
|
if(to.hasAlias())
|
||||||
|
{
|
||||||
|
sb.append(to.getAlias().getName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append(to.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append("UNKNOWN");
|
||||||
|
}
|
||||||
|
|
||||||
|
Metadata from = metadata.getMetadata(MetadataType.FROM);
|
||||||
|
|
||||||
|
sb.append(" FROM:");
|
||||||
|
|
||||||
|
if(from != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(from.hasAlias())
|
||||||
|
{
|
||||||
|
sb.append(from.getAlias().getName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append(from.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append("UNKNOWN");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append("Scanning ....");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if(mDefaultAsyncHttpClient == null)
|
||||||
|
{
|
||||||
|
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
String songEncoded = URLEncoder.encode(sb.toString(), UTF8);
|
||||||
|
|
||||||
|
StringBuilder query = new StringBuilder();
|
||||||
|
query.append("/admin/metadata?mode=updinfo");
|
||||||
|
query.append("&mount=").append(getConfiguration().getMountPoint());
|
||||||
|
query.append("&charset=UTF%2d8");
|
||||||
|
query.append("&song=").append(songEncoded);
|
||||||
|
|
||||||
|
Uri uri = new Uri(Uri.HTTP, null, getConfiguration().getHost(), getConfiguration().getPort(),
|
||||||
|
query.toString(), null);
|
||||||
|
|
||||||
|
BoundRequestBuilder builder = mDefaultAsyncHttpClient.prepareGet(uri.toUrl());
|
||||||
|
|
||||||
|
//Use Basic (base64) authentication
|
||||||
|
Realm realm = new Realm.Builder(getConfiguration().getUserName(), getConfiguration().getPassword())
|
||||||
|
.setScheme(Realm.AuthScheme.BASIC).setUsePreemptiveAuth(true).build();
|
||||||
|
builder.setRealm(realm);
|
||||||
|
builder.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||||
|
|
||||||
|
AsyncHandler handler = new AsyncCompletionHandler()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Object onCompleted(Response response) throws Exception
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mDefaultAsyncHttpClient.executeRequest(builder.build(), handler);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.debug("Error while updating metadata", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(connected())
|
||||||
|
{
|
||||||
|
mFirstMetadataUpdateSuppressed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +256,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
/**
|
/**
|
||||||
* Disconnect from the remote server
|
* Disconnect from the remote server
|
||||||
*/
|
*/
|
||||||
public void disconnect()
|
public void disconnect(boolean shutdown)
|
||||||
{
|
{
|
||||||
if(mOutputStream != null)
|
if(mOutputStream != null)
|
||||||
{
|
{
|
||||||
@@ -197,7 +301,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
mSocket = null;
|
mSocket = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mLog.debug("Icecast stream is now disconnected");
|
if(shutdown)
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.READY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,7 +337,6 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
.append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
|
.append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
|
||||||
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
|
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
|
||||||
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
|
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
|
||||||
|
|
||||||
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
|
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
|
||||||
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
|
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
|
||||||
|
|
||||||
@@ -334,6 +444,14 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
getConfiguration().getPort() + "] - will reattempt connection periodically");
|
getConfiguration().getPort() + "] - will reattempt connection periodically");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
catch(ConnectException ce)
|
||||||
|
{
|
||||||
|
setBroadcastState(BroadcastState.NO_SERVER);
|
||||||
|
mLog.error("Connection refused. Unable to create connection to streaming server host[" +
|
||||||
|
getConfiguration().getHost() + "] and port[" +
|
||||||
|
getConfiguration().getPort() + "]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
catch(IOException ioe)
|
catch(IOException ioe)
|
||||||
{
|
{
|
||||||
setBroadcastState(BroadcastState.ERROR);
|
setBroadcastState(BroadcastState.ERROR);
|
||||||
@@ -457,7 +575,7 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
{
|
{
|
||||||
setBroadcastState(BroadcastState.CONNECTED);
|
setBroadcastState(BroadcastState.CONNECTED);
|
||||||
}
|
}
|
||||||
else if(response.startsWith("Invalid Password"))
|
else if(response.contains("Invalid Password") || response.contains("Authentication Required"))
|
||||||
{
|
{
|
||||||
setBroadcastState(BroadcastState.INVALID_PASSWORD);
|
setBroadcastState(BroadcastState.INVALID_PASSWORD);
|
||||||
}
|
}
|
||||||
@@ -469,56 +587,20 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
setBroadcastState(BroadcastState.CONNECTED);
|
||||||
mLog.debug("Response was empty");
|
mLog.debug("Response was empty");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMetadata(String songName)
|
|
||||||
{
|
|
||||||
if(songName != null && !songName.isEmpty())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if(mDefaultAsyncHttpClient == null)
|
|
||||||
{
|
|
||||||
mDefaultAsyncHttpClient = new DefaultAsyncHttpClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
String songEncoded = URLEncoder.encode(songName, UTF8);
|
|
||||||
|
|
||||||
StringBuilder query = new StringBuilder();
|
|
||||||
query.append("/admin/metadata?mode=updinfo");
|
|
||||||
query.append("&mount=").append(getConfiguration().getMountPoint());
|
|
||||||
query.append("&charset=UTF%2d8");
|
|
||||||
query.append("&song=").append(songEncoded);
|
|
||||||
|
|
||||||
Uri uri = new Uri(Uri.HTTP, null, getConfiguration().getHost(), getConfiguration().getPort(),
|
|
||||||
query.toString(), null);
|
|
||||||
|
|
||||||
BoundRequestBuilder builder = mDefaultAsyncHttpClient.prepareGet(uri.toUrl());
|
|
||||||
|
|
||||||
//Use Basic (base64) authentication
|
|
||||||
Realm realm = new Realm.Builder(getConfiguration().getUserName(), getConfiguration().getPassword())
|
|
||||||
.setScheme(Realm.AuthScheme.BASIC).setUsePreemptiveAuth(true).build();
|
|
||||||
builder.setRealm(realm);
|
|
||||||
builder.addHeader(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
|
||||||
|
|
||||||
mDefaultAsyncHttpClient.executeRequest(builder.build());
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
mLog.debug("Error while updating metadata", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args)
|
public static void main(String[] args)
|
||||||
{
|
{
|
||||||
boolean test = false;
|
boolean test = false;
|
||||||
|
|
||||||
IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3);
|
IcecastTCPConfiguration config = new IcecastTCPConfiguration(BroadcastFormat.MP3);
|
||||||
|
|
||||||
config.setName("Broadcastify SDRTrunk #2");
|
String streamName = "Broadcastify SDRTrunk #2";
|
||||||
|
|
||||||
|
config.setName(streamName);
|
||||||
config.setHost("audio3.broadcastify.com");
|
config.setHost("audio3.broadcastify.com");
|
||||||
config.setPort(80);
|
config.setPort(80);
|
||||||
config.setMountPoint("/k0yrdpx7zn4h");
|
config.setMountPoint("/k0yrdpx7zn4h");
|
||||||
@@ -530,6 +612,11 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
config.setURL("http://www.radioreference.com");
|
config.setURL("http://www.radioreference.com");
|
||||||
config.setBitRate(16);
|
config.setBitRate(16);
|
||||||
|
|
||||||
|
AudioMetadata metadata = new AudioMetadata(0, false);
|
||||||
|
Alias toAlias = new Alias("ToAlias");
|
||||||
|
toAlias.addAliasID(new BroadcastChannel(streamName));
|
||||||
|
metadata.receive(new Metadata(MetadataType.TO, "1234", toAlias ));
|
||||||
|
|
||||||
if(test)
|
if(test)
|
||||||
{
|
{
|
||||||
mLog.debug("Auth:" + config.getEncodedCredentials());
|
mLog.debug("Auth:" + config.getEncodedCredentials());
|
||||||
@@ -537,22 +624,29 @@ public class IcecastTCPBroadcaster extends Broadcaster
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
||||||
|
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
|
||||||
|
|
||||||
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
|
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
|
||||||
|
|
||||||
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
||||||
mLog.debug("Opening: " + path.toString());
|
mLog.debug("Opening: " + path.toString());
|
||||||
|
|
||||||
mLog.debug("Registering and starting audio playback");
|
mLog.debug("Registering and starting audio playback");
|
||||||
|
|
||||||
|
int playCount = 0;
|
||||||
|
|
||||||
while(true)
|
while(true)
|
||||||
{
|
{
|
||||||
mLog.debug("Playback started [" + path.toString() + "]");
|
mLog.debug("Playback started [" + path.toString() + "]");
|
||||||
|
|
||||||
|
toAlias.setName("Playback #" + playCount++);
|
||||||
|
|
||||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||||
{
|
{
|
||||||
reader.setListener(broadcaster);
|
reader.setMetadata(metadata);
|
||||||
|
reader.setListener(audioBroadcaster);
|
||||||
reader.read();
|
reader.read();
|
||||||
|
audioBroadcaster.receive(new AudioPacket(AudioPacket.Type.END, metadata));
|
||||||
}
|
}
|
||||||
catch (IOException e)
|
catch (IOException e)
|
||||||
{
|
{
|
||||||
@@ -22,9 +22,10 @@ import audio.broadcast.BroadcastConfiguration;
|
|||||||
import audio.broadcast.BroadcastFormat;
|
import audio.broadcast.BroadcastFormat;
|
||||||
import audio.broadcast.BroadcastServerType;
|
import audio.broadcast.BroadcastServerType;
|
||||||
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
import audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAttribute;
|
import javax.xml.bind.annotation.XmlAttribute;
|
||||||
import javax.xml.bind.annotation.XmlElement;
|
|
||||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||||
import javax.xml.bind.annotation.XmlTransient;
|
import javax.xml.bind.annotation.XmlTransient;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@@ -32,6 +33,8 @@ import java.util.Base64;
|
|||||||
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
|
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
|
||||||
public class IcecastTCPConfiguration extends BroadcastConfiguration
|
public class IcecastTCPConfiguration extends BroadcastConfiguration
|
||||||
{
|
{
|
||||||
|
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
|
||||||
|
|
||||||
private String mUserName;
|
private String mUserName;
|
||||||
private String mMountPoint;
|
private String mMountPoint;
|
||||||
private String mDescription;
|
private String mDescription;
|
||||||
@@ -44,6 +47,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
|
|
||||||
public IcecastTCPConfiguration()
|
public IcecastTCPConfiguration()
|
||||||
{
|
{
|
||||||
|
setUserName("source");
|
||||||
//No-arg constructor for JAXB
|
//No-arg constructor for JAXB
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +58,31 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
public IcecastTCPConfiguration(BroadcastFormat format)
|
public IcecastTCPConfiguration(BroadcastFormat format)
|
||||||
{
|
{
|
||||||
super(format);
|
super(format);
|
||||||
|
setUserName("source");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BroadcastConfiguration copyOf()
|
||||||
|
{
|
||||||
|
IcecastTCPConfiguration copy = new IcecastTCPConfiguration(getBroadcastFormat());
|
||||||
|
|
||||||
|
//Broadcast Configuration Parameters
|
||||||
|
copy.setName(getName());
|
||||||
|
copy.setHost(getHost());
|
||||||
|
copy.setPort(getPort());
|
||||||
|
copy.setPassword(getPassword());
|
||||||
|
copy.setDelay(getDelay());
|
||||||
|
copy.setEnabled(false);
|
||||||
|
|
||||||
|
//Icecast Configuration Parameters
|
||||||
|
copy.setUserName(getUserName());
|
||||||
|
copy.setMountPoint(getMountPoint());
|
||||||
|
copy.setDescription(getDescription());
|
||||||
|
copy.setGenre(getGenre());
|
||||||
|
copy.setPublic(isPublic());
|
||||||
|
copy.setURL(getURL());
|
||||||
|
|
||||||
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -79,6 +108,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
sb.append(getPassword());
|
sb.append(getPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mLog.debug("Connection String: " + sb.toString());
|
||||||
|
|
||||||
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
|
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
|
||||||
|
|
||||||
return "Basic " + base64;
|
return "Basic " + base64;
|
||||||
@@ -175,7 +206,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public visibility of the broadcast
|
* Public visibility of the broadcastAudio
|
||||||
*/
|
*/
|
||||||
@XmlAttribute( name="public" )
|
@XmlAttribute( name="public" )
|
||||||
public boolean isPublic()
|
public boolean isPublic()
|
||||||
@@ -184,8 +215,8 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets public visibility of the broadcast
|
* Sets public visibility of the broadcastAudio
|
||||||
* @param isPublic indicates if the broadcast should be visible to the public
|
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||||
*/
|
*/
|
||||||
public void setPublic(boolean isPublic)
|
public void setPublic(boolean isPublic)
|
||||||
{
|
{
|
||||||
@@ -193,7 +224,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of audio channels in the broadcast
|
* Number of audio channels in the broadcastAudio
|
||||||
*/
|
*/
|
||||||
@XmlAttribute( name="channels" )
|
@XmlAttribute( name="channels" )
|
||||||
public int getChannels()
|
public int getChannels()
|
||||||
@@ -202,7 +233,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the number of audio channels in the broadcast
|
* Sets the number of audio channels in the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public void setChannels(int channels)
|
public void setChannels(int channels)
|
||||||
{
|
{
|
||||||
@@ -254,7 +285,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
*/
|
*/
|
||||||
@XmlAttribute( name="url" )
|
@XmlAttribute( name="url" )
|
||||||
public String getURL()
|
public String getURL()
|
||||||
@@ -263,7 +294,7 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
public void setURL(String url)
|
public void setURL(String url)
|
||||||
@@ -275,4 +306,35 @@ public class IcecastTCPConfiguration extends BroadcastConfiguration
|
|||||||
{
|
{
|
||||||
return mURL != null;
|
return mURL != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid()
|
||||||
|
{
|
||||||
|
if(!super.isValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if(mUserName == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if(mMountPoint == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if(mChannels != 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if(mSampleRate <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if(mBitRate <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,14 @@
|
|||||||
package audio.broadcast.shoutcast.v1;
|
package audio.broadcast.shoutcast.v1;
|
||||||
|
|
||||||
import audio.AudioPacket;
|
import audio.AudioPacket;
|
||||||
import audio.broadcast.Broadcaster;
|
import audio.broadcast.AudioBroadcaster;
|
||||||
import audio.broadcast.BroadcastFactory;
|
import audio.broadcast.BroadcastFactory;
|
||||||
import audio.broadcast.BroadcastFormat;
|
import audio.broadcast.BroadcastFormat;
|
||||||
import audio.broadcast.BroadcastMetadata;
|
import audio.broadcast.BroadcastMetadata;
|
||||||
import audio.broadcast.BroadcastState;
|
import audio.broadcast.BroadcastState;
|
||||||
import audio.convert.IAudioConverter;
|
import audio.metadata.AudioMetadata;
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
|
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import record.wave.AudioPacketMonoWaveReader;
|
import record.wave.AudioPacketMonoWaveReader;
|
||||||
@@ -41,9 +42,9 @@ import java.nio.file.Paths;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ShoutcastV1Broadcaster extends Broadcaster
|
public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1Broadcaster.class );
|
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV1AudioBroadcaster.class );
|
||||||
|
|
||||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
||||||
private long mLastConnectionAttempt = 0;
|
private long mLastConnectionAttempt = 0;
|
||||||
@@ -58,13 +59,10 @@ public class ShoutcastV1Broadcaster extends Broadcaster
|
|||||||
/**
|
/**
|
||||||
* Creates a Shoutcast Version 1 broadcaster.
|
* Creates a Shoutcast Version 1 broadcaster.
|
||||||
* @param configuration
|
* @param configuration
|
||||||
* @param audioConverter
|
|
||||||
*/
|
*/
|
||||||
public ShoutcastV1Broadcaster(ThreadPoolManager threadPoolManager,
|
public ShoutcastV1AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV1Configuration configuration)
|
||||||
ShoutcastV1Configuration configuration,
|
|
||||||
IAudioConverter audioConverter)
|
|
||||||
{
|
{
|
||||||
super(threadPoolManager, configuration, audioConverter);
|
super(threadPoolManager, configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,34 +73,29 @@ public class ShoutcastV1Broadcaster extends Broadcaster
|
|||||||
return (ShoutcastV1Configuration)getBroadcastConfiguration();
|
return (ShoutcastV1Configuration)getBroadcastConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void broadcastMetadata(AudioMetadata metadata)
|
||||||
|
{
|
||||||
|
mLog.debug("Request to send audio metadata to shoutcast v1 server - needs code");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process audio packets using Shoutcast V1 Protocol
|
* Process audio packets using Shoutcast V1 Protocol
|
||||||
*/
|
*/
|
||||||
public void broadcast()
|
@Override
|
||||||
|
protected void broadcastAudio(byte[] audio)
|
||||||
{
|
{
|
||||||
//If we're connected, send the audio, otherwise discard it
|
//If we're connected, send the audio, otherwise discard it
|
||||||
if(connect())
|
if(connect())
|
||||||
{
|
{
|
||||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
try
|
||||||
|
|
||||||
if(!mPacketsToBroadcast.isEmpty())
|
|
||||||
{
|
{
|
||||||
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
|
send(audio);
|
||||||
|
}
|
||||||
mPacketsToBroadcast.clear();
|
catch (IOException e)
|
||||||
|
{
|
||||||
if(convertedAudio != null)
|
mLog.error("Error while dispatching audio", e);
|
||||||
{
|
setBroadcastState(BroadcastState.BROADCAST_ERROR);
|
||||||
try
|
|
||||||
{
|
|
||||||
send(convertedAudio);
|
|
||||||
}
|
|
||||||
catch(IOException e)
|
|
||||||
{
|
|
||||||
mLog.error("Error while dispatching audio", e);
|
|
||||||
setBroadcastState(BroadcastState.BROADCAST_ERROR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,8 +342,9 @@ public class ShoutcastV1Broadcaster extends Broadcaster
|
|||||||
config.setBitRate(16);
|
config.setBitRate(16);
|
||||||
|
|
||||||
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
||||||
|
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
|
||||||
|
|
||||||
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
|
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
|
||||||
|
|
||||||
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
||||||
mLog.debug("Opening: " + path.toString());
|
mLog.debug("Opening: " + path.toString());
|
||||||
@@ -363,7 +357,7 @@ public class ShoutcastV1Broadcaster extends Broadcaster
|
|||||||
|
|
||||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||||
{
|
{
|
||||||
reader.setListener(broadcaster);
|
reader.setListener(audioBroadcaster);
|
||||||
reader.read();
|
reader.read();
|
||||||
}
|
}
|
||||||
catch (IOException e)
|
catch (IOException e)
|
||||||
@@ -21,6 +21,7 @@ package audio.broadcast.shoutcast.v1;
|
|||||||
import audio.broadcast.BroadcastConfiguration;
|
import audio.broadcast.BroadcastConfiguration;
|
||||||
import audio.broadcast.BroadcastFormat;
|
import audio.broadcast.BroadcastFormat;
|
||||||
import audio.broadcast.BroadcastServerType;
|
import audio.broadcast.BroadcastServerType;
|
||||||
|
import audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||||
|
|
||||||
public class ShoutcastV1Configuration extends BroadcastConfiguration
|
public class ShoutcastV1Configuration extends BroadcastConfiguration
|
||||||
{
|
{
|
||||||
@@ -31,11 +32,40 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
|||||||
private int mBitRate;
|
private int mBitRate;
|
||||||
private String mURL;
|
private String mURL;
|
||||||
|
|
||||||
|
public ShoutcastV1Configuration()
|
||||||
|
{
|
||||||
|
//No-arg JAXB constructor
|
||||||
|
}
|
||||||
|
|
||||||
public ShoutcastV1Configuration(BroadcastFormat format)
|
public ShoutcastV1Configuration(BroadcastFormat format)
|
||||||
{
|
{
|
||||||
super(format);
|
super(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BroadcastConfiguration copyOf()
|
||||||
|
{
|
||||||
|
ShoutcastV1Configuration copy = new ShoutcastV1Configuration(getBroadcastFormat());
|
||||||
|
|
||||||
|
//Broadcast Configuration Parameters
|
||||||
|
copy.setName(getName());
|
||||||
|
copy.setHost(getHost());
|
||||||
|
copy.setPort(getPort());
|
||||||
|
copy.setPassword(getPassword());
|
||||||
|
copy.setDelay(getDelay());
|
||||||
|
copy.setEnabled(false);
|
||||||
|
|
||||||
|
//Icecast Configuration Parameters
|
||||||
|
copy.setStreamName(getStreamName());
|
||||||
|
copy.setGenre(getGenre());
|
||||||
|
copy.setPublic(isPublic());
|
||||||
|
copy.setChannels(getChannels());
|
||||||
|
copy.setBitRate(getBitRate());
|
||||||
|
copy.setURL(getURL());
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BroadcastServerType getBroadcastServerType()
|
public BroadcastServerType getBroadcastServerType()
|
||||||
{
|
{
|
||||||
@@ -77,7 +107,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public visibility of the broadcast
|
* Public visibility of the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public boolean isPublic()
|
public boolean isPublic()
|
||||||
{
|
{
|
||||||
@@ -85,8 +115,8 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets public visibility of the broadcast
|
* Sets public visibility of the broadcastAudio
|
||||||
* @param isPublic indicates if the broadcast should be visible to the public
|
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||||
*/
|
*/
|
||||||
public void setPublic(boolean isPublic)
|
public void setPublic(boolean isPublic)
|
||||||
{
|
{
|
||||||
@@ -94,7 +124,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of audio channels in the broadcast
|
* Number of audio channels in the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public int getChannels()
|
public int getChannels()
|
||||||
{
|
{
|
||||||
@@ -102,7 +132,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the number of audio channels in the broadcast
|
* Sets the number of audio channels in the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public void setChannels(int channels)
|
public void setChannels(int channels)
|
||||||
{
|
{
|
||||||
@@ -127,7 +157,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
*/
|
*/
|
||||||
public String getURL()
|
public String getURL()
|
||||||
{
|
{
|
||||||
@@ -135,7 +165,7 @@ public class ShoutcastV1Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
public void setURL(String url)
|
public void setURL(String url)
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
package audio.broadcast.shoutcast.v2;
|
package audio.broadcast.shoutcast.v2;
|
||||||
|
|
||||||
import audio.AudioPacket;
|
import audio.AudioPacket;
|
||||||
|
import audio.broadcast.AudioBroadcaster;
|
||||||
import audio.broadcast.BroadcastConfiguration;
|
import audio.broadcast.BroadcastConfiguration;
|
||||||
import audio.broadcast.BroadcastFormat;
|
import audio.broadcast.BroadcastFormat;
|
||||||
import audio.broadcast.BroadcastState;
|
import audio.broadcast.BroadcastState;
|
||||||
import audio.broadcast.Broadcaster;
|
|
||||||
import audio.broadcast.BroadcastFactory;
|
import audio.broadcast.BroadcastFactory;
|
||||||
import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast;
|
import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast;
|
||||||
import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre;
|
import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre;
|
||||||
@@ -39,8 +39,9 @@ import audio.broadcast.shoutcast.v2.message.UltravoxMessageFactory;
|
|||||||
import audio.broadcast.shoutcast.v2.message.UltravoxMessageType;
|
import audio.broadcast.shoutcast.v2.message.UltravoxMessageType;
|
||||||
import audio.broadcast.shoutcast.v2.message.UltravoxMetadata;
|
import audio.broadcast.shoutcast.v2.message.UltravoxMetadata;
|
||||||
import audio.broadcast.shoutcast.v2.message.XMLMetadata;
|
import audio.broadcast.shoutcast.v2.message.XMLMetadata;
|
||||||
import audio.convert.IAudioConverter;
|
import audio.metadata.AudioMetadata;
|
||||||
import controller.ThreadPoolManager;
|
import controller.ThreadPoolManager;
|
||||||
|
import org.asynchttpclient.DefaultAsyncHttpClient;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import properties.SystemProperties;
|
import properties.SystemProperties;
|
||||||
@@ -57,9 +58,9 @@ import java.nio.file.Paths;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ShoutcastV2Broadcaster extends Broadcaster
|
public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2Broadcaster.class );
|
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2AudioBroadcaster.class );
|
||||||
|
|
||||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
||||||
private static final boolean GET_RESPONSE = true;
|
private static final boolean GET_RESPONSE = true;
|
||||||
@@ -75,13 +76,10 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
|||||||
/**
|
/**
|
||||||
* Creates a Shoutcast Version 2 broadcaster.
|
* Creates a Shoutcast Version 2 broadcaster.
|
||||||
* @param configuration details for shoutcast version 2
|
* @param configuration details for shoutcast version 2
|
||||||
* @param audioConverter to convert PCM audio
|
|
||||||
*/
|
*/
|
||||||
public ShoutcastV2Broadcaster(ThreadPoolManager threadPoolManager,
|
public ShoutcastV2AudioBroadcaster(ThreadPoolManager threadPoolManager, BroadcastConfiguration configuration)
|
||||||
BroadcastConfiguration configuration,
|
|
||||||
IAudioConverter audioConverter)
|
|
||||||
{
|
{
|
||||||
super(threadPoolManager, configuration, audioConverter);
|
super(threadPoolManager, configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,41 +90,36 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
|||||||
return (ShoutcastV2Configuration)getBroadcastConfiguration();
|
return (ShoutcastV2Configuration)getBroadcastConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void broadcastMetadata(AudioMetadata metadata)
|
||||||
|
{
|
||||||
|
mLog.debug("Request to broadcast audio metadata to shoutcast v2 server - needs coding");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcast audio packets
|
* Broadcast audio
|
||||||
*/
|
*/
|
||||||
public void broadcast()
|
@Override
|
||||||
|
protected void broadcastAudio(byte[] audio)
|
||||||
{
|
{
|
||||||
//If we're connected, send the audio, otherwise discard it
|
//If we're connected, send the audio, otherwise discard it
|
||||||
if(connect())
|
if(connect())
|
||||||
{
|
{
|
||||||
mAudioQueue.drainTo(mPacketsToBroadcast, 5);
|
List<UltravoxMessage> audioMessages = getAudioMessages(audio);
|
||||||
|
|
||||||
if(!mPacketsToBroadcast.isEmpty())
|
try
|
||||||
{
|
{
|
||||||
byte[] convertedAudio = getAudioConverter().convert(mPacketsToBroadcast);
|
for(UltravoxMessage audioMessage: audioMessages)
|
||||||
|
|
||||||
mPacketsToBroadcast.clear();
|
|
||||||
|
|
||||||
if(convertedAudio != null)
|
|
||||||
{
|
{
|
||||||
List<UltravoxMessage> audioMessages = getAudioMessages(convertedAudio);
|
send(audioMessage, NO_RESPONSE_EXPECTED);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for(UltravoxMessage audioMessage: audioMessages)
|
|
||||||
{
|
|
||||||
send(audioMessage, NO_RESPONSE_EXPECTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch(IOException e)
|
|
||||||
{
|
|
||||||
mLog.error("Error while dispatching audio", e);
|
|
||||||
setBroadcastState(BroadcastState.BROADCAST_ERROR);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch(IOException e)
|
||||||
|
{
|
||||||
|
mLog.error("Error while dispatching audio", e);
|
||||||
|
setBroadcastState(BroadcastState.BROADCAST_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,13 +363,13 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
|||||||
response = send(icyName, NO_RESPONSE_EXPECTED);
|
response = send(icyName, NO_RESPONSE_EXPECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getShoutcastConfiguration().getStreamGenre() != null)
|
if (getShoutcastConfiguration().getGenre() != null)
|
||||||
{
|
{
|
||||||
metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getStreamGenre()));
|
metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getGenre()));
|
||||||
|
|
||||||
ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory
|
ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory
|
||||||
.getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE);
|
.getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE);
|
||||||
icyGenre.setGenre(getShoutcastConfiguration().getStreamGenre());
|
icyGenre.setGenre(getShoutcastConfiguration().getGenre());
|
||||||
response = send(icyGenre, NO_RESPONSE_EXPECTED);
|
response = send(icyGenre, NO_RESPONSE_EXPECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +527,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
|||||||
config.setPort(8000);
|
config.setPort(8000);
|
||||||
config.setPassword("denny3");
|
config.setPassword("denny3");
|
||||||
config.setStreamName("Denny's Audio Broadcast Test");
|
config.setStreamName("Denny's Audio Broadcast Test");
|
||||||
config.setStreamGenre("Public Safety");
|
config.setGenre("Public Safety");
|
||||||
config.setPublic(true);
|
config.setPublic(true);
|
||||||
config.setURL("http://localhost:8000");
|
config.setURL("http://localhost:8000");
|
||||||
config.setBitRate(16);
|
config.setBitRate(16);
|
||||||
@@ -545,8 +538,9 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
ThreadPoolManager threadPoolManager = new ThreadPoolManager();
|
||||||
|
DefaultAsyncHttpClient httpClient = new DefaultAsyncHttpClient();
|
||||||
|
|
||||||
final Broadcaster broadcaster = BroadcastFactory.getBroadcaster(threadPoolManager,config);
|
final AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(httpClient, threadPoolManager,config);
|
||||||
|
|
||||||
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
Path path = Paths.get("/home/denny/Music/PCM.wav");
|
||||||
mLog.debug("Opening: " + path.toString());
|
mLog.debug("Opening: " + path.toString());
|
||||||
@@ -559,7 +553,7 @@ public class ShoutcastV2Broadcaster extends Broadcaster
|
|||||||
|
|
||||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(path, true))
|
||||||
{
|
{
|
||||||
reader.setListener(broadcaster);
|
reader.setListener(audioBroadcaster);
|
||||||
reader.read();
|
reader.read();
|
||||||
}
|
}
|
||||||
catch (IOException e)
|
catch (IOException e)
|
||||||
@@ -28,16 +28,46 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
|||||||
private String mUserID;
|
private String mUserID;
|
||||||
private int mBitRate;
|
private int mBitRate;
|
||||||
private String mStreamName;
|
private String mStreamName;
|
||||||
private String mStreamGenre;
|
private String mGenre;
|
||||||
private String mURL;
|
private String mURL;
|
||||||
private boolean mPublic = true;
|
private boolean mPublic = true;
|
||||||
|
|
||||||
|
public ShoutcastV2Configuration()
|
||||||
|
{
|
||||||
|
//No-arg JAXB constructor
|
||||||
|
}
|
||||||
|
|
||||||
public ShoutcastV2Configuration(BroadcastFormat format)
|
public ShoutcastV2Configuration(BroadcastFormat format)
|
||||||
{
|
{
|
||||||
super(format);
|
super(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BroadcastConfiguration copyOf()
|
||||||
|
{
|
||||||
|
ShoutcastV2Configuration copy = new ShoutcastV2Configuration(getBroadcastFormat());
|
||||||
|
|
||||||
|
//Broadcast Configuration Parameters
|
||||||
|
copy.setName(getName());
|
||||||
|
copy.setHost(getHost());
|
||||||
|
copy.setPort(getPort());
|
||||||
|
copy.setPassword(getPassword());
|
||||||
|
copy.setDelay(getDelay());
|
||||||
|
copy.setEnabled(false);
|
||||||
|
|
||||||
|
//Shoutcast V2 Configuration Parameters
|
||||||
|
copy.setStreamID(getStreamID());
|
||||||
|
copy.setUserID(getUserID());
|
||||||
|
copy.setBitRate(getBitRate());
|
||||||
|
copy.setStreamName(getStreamName());
|
||||||
|
copy.setGenre(getGenre());
|
||||||
|
copy.setURL(getURL());
|
||||||
|
copy.setPublic(isPublic());
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BroadcastServerType getBroadcastServerType()
|
public BroadcastServerType getBroadcastServerType()
|
||||||
{
|
{
|
||||||
@@ -117,21 +147,21 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
|||||||
/**
|
/**
|
||||||
* Stream genre
|
* Stream genre
|
||||||
*/
|
*/
|
||||||
public String getStreamGenre()
|
public String getGenre()
|
||||||
{
|
{
|
||||||
return mStreamGenre;
|
return mGenre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the genre tag for this stream
|
* Sets the genre tag for this stream
|
||||||
*/
|
*/
|
||||||
public void setStreamGenre(String genre)
|
public void setGenre(String genre)
|
||||||
{
|
{
|
||||||
mStreamGenre = genre;
|
mGenre = genre;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public visibility of the broadcast
|
* Public visibility of the broadcastAudio
|
||||||
*/
|
*/
|
||||||
public boolean isPublic()
|
public boolean isPublic()
|
||||||
{
|
{
|
||||||
@@ -139,8 +169,8 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets public visibility of the broadcast
|
* Sets public visibility of the broadcastAudio
|
||||||
* @param isPublic indicates if the broadcast should be visible to the public
|
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||||
*/
|
*/
|
||||||
public void setPublic(boolean isPublic)
|
public void setPublic(boolean isPublic)
|
||||||
{
|
{
|
||||||
@@ -148,7 +178,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
*/
|
*/
|
||||||
public String getURL()
|
public String getURL()
|
||||||
{
|
{
|
||||||
@@ -156,7 +186,7 @@ public class ShoutcastV2Configuration extends BroadcastConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL associated with the broadcast where users can find additional details.
|
* URL associated with the broadcastAudio where users can find additional details.
|
||||||
* @param url
|
* @param url
|
||||||
*/
|
*/
|
||||||
public void setURL(String url)
|
public void setURL(String url)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class StreamMimeType extends UltravoxMessage
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the broadcast format or MIME type
|
* Sets the broadcastAudio format or MIME type
|
||||||
* @param format of the stream
|
* @param format of the stream
|
||||||
*/
|
*/
|
||||||
public void setFormat(BroadcastFormat format)
|
public void setFormat(BroadcastFormat format)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ package audio.broadcast.shoutcast.v2.message;
|
|||||||
public class TerminateBroadcast extends UltravoxMessage
|
public class TerminateBroadcast extends UltravoxMessage
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Client request to server to terminate the broadcast
|
* Client request to server to terminate the broadcastAudio
|
||||||
*
|
*
|
||||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,11 +18,15 @@
|
|||||||
package audio.metadata;
|
package audio.metadata;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import alias.id.broadcast.BroadcastChannel;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -312,6 +316,23 @@ public class AudioMetadata implements Listener<Metadata>
|
|||||||
return mStreamable;
|
return mStreamable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Collection<BroadcastChannel> getBroadcastChannels()
|
||||||
|
{
|
||||||
|
Set<BroadcastChannel> broadcastChannels = new TreeSet<>();
|
||||||
|
|
||||||
|
for(Metadata metadata: mMetadata.values())
|
||||||
|
{
|
||||||
|
if(metadata.hasAlias() && metadata.getAlias().isStreamable())
|
||||||
|
{
|
||||||
|
Alias alias = metadata.getAlias();
|
||||||
|
|
||||||
|
broadcastChannels.addAll(alias.getBroadcastChannels());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return broadcastChannels;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public class ControllerPanel extends JPanel
|
|||||||
|
|
||||||
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
|
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
|
||||||
|
|
||||||
mBroadcastPanel = new BroadcastPanel(broadcastModel);
|
mBroadcastPanel = new BroadcastPanel(broadcastModel, settingsManager);
|
||||||
|
|
||||||
mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager);
|
mCallEventPanel = new CallEventPanel(settingsManager, channelProcessingManager);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ package gui;
|
|||||||
import alias.AliasModel;
|
import alias.AliasModel;
|
||||||
import alias.action.AliasActionManager;
|
import alias.action.AliasActionManager;
|
||||||
import audio.AudioManager;
|
import audio.AudioManager;
|
||||||
import audio.broadcast.AudioBroadcastManager;
|
|
||||||
import audio.broadcast.BroadcastModel;
|
import audio.broadcast.BroadcastModel;
|
||||||
import com.jidesoft.swing.JideSplitPane;
|
import com.jidesoft.swing.JideSplitPane;
|
||||||
import controller.ControllerPanel;
|
import controller.ControllerPanel;
|
||||||
@@ -137,10 +136,9 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
AudioManager audioManager = new AudioManager(threadPoolManager, sourceManager.getMixerManager());
|
AudioManager audioManager = new AudioManager(threadPoolManager, sourceManager.getMixerManager());
|
||||||
channelProcessingManager.addAudioPacketListener(audioManager);
|
channelProcessingManager.addAudioPacketListener(audioManager);
|
||||||
|
|
||||||
BroadcastModel broadcastModel = new BroadcastModel();
|
BroadcastModel broadcastModel = new BroadcastModel(threadPoolManager, mSettingsManager);
|
||||||
|
|
||||||
AudioBroadcastManager audioBroadcastManager = new AudioBroadcastManager(threadPoolManager);
|
channelProcessingManager.addAudioPacketListener(broadcastModel);
|
||||||
channelProcessingManager.addAudioPacketListener(audioBroadcastManager);
|
|
||||||
|
|
||||||
MapService mapService = new MapService(mSettingsManager);
|
MapService mapService = new MapService(mSettingsManager);
|
||||||
channelProcessingManager.addMessageListener(mapService);
|
channelProcessingManager.addMessageListener(mapService);
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ public class DecoderSelectionFrame extends JInternalFrame
|
|||||||
public DecoderSelectionFrame( JDesktopPane desktop,
|
public DecoderSelectionFrame( JDesktopPane desktop,
|
||||||
IControllableFileSource source )
|
IControllableFileSource source )
|
||||||
{
|
{
|
||||||
|
ThreadPoolManager tpm = new ThreadPoolManager();
|
||||||
|
|
||||||
AliasModel aliasModel = new AliasModel();
|
AliasModel aliasModel = new AliasModel();
|
||||||
BroadcastModel broadcastModel = new BroadcastModel();
|
BroadcastModel broadcastModel = new BroadcastModel(tpm, null);
|
||||||
ChannelModel channelModel = new ChannelModel();
|
ChannelModel channelModel = new ChannelModel();
|
||||||
ChannelMapModel channelMapModel = new ChannelMapModel();
|
ChannelMapModel channelMapModel = new ChannelMapModel();
|
||||||
|
|
||||||
ThreadPoolManager tpm = new ThreadPoolManager();
|
|
||||||
|
|
||||||
mPlaylistManager = new PlaylistManager( tpm, aliasModel, broadcastModel, channelModel, channelMapModel );
|
mPlaylistManager = new PlaylistManager( tpm, aliasModel, broadcastModel, channelModel, channelMapModel );
|
||||||
|
|
||||||
mDesktop = desktop;
|
mDesktop = desktop;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import javax.xml.bind.JAXBException;
|
|||||||
import javax.xml.bind.Marshaller;
|
import javax.xml.bind.Marshaller;
|
||||||
import javax.xml.bind.Unmarshaller;
|
import javax.xml.bind.Unmarshaller;
|
||||||
|
|
||||||
import audio.broadcast.BroadcastConfigurationEvent;
|
import audio.broadcast.BroadcastEvent;
|
||||||
import audio.broadcast.BroadcastModel;
|
import audio.broadcast.BroadcastModel;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -109,10 +109,10 @@ public class PlaylistManager implements ChannelEventListener
|
|||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
mBroadcastModel.addListener(new Listener<BroadcastConfigurationEvent>()
|
mBroadcastModel.addListener(new Listener<BroadcastEvent>()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void receive(BroadcastConfigurationEvent broadcastConfigurationEvent)
|
public void receive(BroadcastEvent broadcastEvent)
|
||||||
{
|
{
|
||||||
schedulePlaylistSave();
|
schedulePlaylistSave();
|
||||||
}
|
}
|
||||||
@@ -346,7 +346,7 @@ public class PlaylistManager implements ChannelEventListener
|
|||||||
}
|
}
|
||||||
catch ( Exception e )
|
catch ( Exception e )
|
||||||
{
|
{
|
||||||
mLog.error( "coulcn't open inputstream to load playlist [" + playlistPath.toString() + "]", e );
|
mLog.error( "couldn't open input stream to load playlist [" + playlistPath.toString() + "]", e );
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
|||||||
private BufferProcessor mBufferProcessor;
|
private BufferProcessor mBufferProcessor;
|
||||||
private ScheduledFuture<?> mProcessorHandle;
|
private ScheduledFuture<?> mProcessorHandle;
|
||||||
|
|
||||||
|
private long mSampleCount;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file
|
* Abstract audio recorder that implements audio packet queueing and threaded audio conversion/writing to a file
|
||||||
*
|
*
|
||||||
@@ -95,6 +97,15 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
|||||||
return mTimeRecordingStart;
|
return mTimeRecordingStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recording length in milliseconds
|
||||||
|
*/
|
||||||
|
public long getRecordingLength()
|
||||||
|
{
|
||||||
|
//Assumes audio sample rate of 8000 samples/second or 8 samples/milli-second
|
||||||
|
return mSampleCount / 8;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the IAudioPacketListener interface and simply redirects to the Listener<AudioPacket> interface.
|
* Implements the IAudioPacketListener interface and simply redirects to the Listener<AudioPacket> interface.
|
||||||
* This is necessary since you can't have multiple methods with the same erasure (ie Listener<xxx>) in the
|
* This is necessary since you can't have multiple methods with the same erasure (ie Listener<xxx>) in the
|
||||||
@@ -137,7 +148,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
|||||||
*/
|
*/
|
||||||
protected OutputStream getOutputStream()
|
protected OutputStream getOutputStream()
|
||||||
{
|
{
|
||||||
return null;
|
return mFileOutputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,6 +180,7 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
mFileOutputStream.flush();
|
||||||
mFileOutputStream.close();
|
mFileOutputStream.close();
|
||||||
}
|
}
|
||||||
catch (IOException e)
|
catch (IOException e)
|
||||||
@@ -204,8 +216,6 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
|||||||
{
|
{
|
||||||
mFileOutputStream = new FileOutputStream(mPath.toFile());
|
mFileOutputStream = new FileOutputStream(mPath.toFile());
|
||||||
|
|
||||||
mLog.info("Created Audio Recording [" + getPath().toString() + "]");
|
|
||||||
|
|
||||||
/* Schedule the handler to run every half second */
|
/* Schedule the handler to run every half second */
|
||||||
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
mProcessorHandle = executor.scheduleAtFixedRate(mBufferProcessor, 0, 500, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
@@ -230,6 +240,14 @@ public abstract class AudioRecorder extends Module implements Listener<AudioPack
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
record(mPacketsToProcess);
|
record(mPacketsToProcess);
|
||||||
|
|
||||||
|
for(AudioPacket packet: mPacketsToProcess)
|
||||||
|
{
|
||||||
|
if(packet.getType() == AudioPacket.Type.AUDIO)
|
||||||
|
{
|
||||||
|
mSampleCount += packet.getAudioBuffer().getSamples().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(IOException ioe)
|
catch(IOException ioe)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,26 +95,50 @@ public class MP3Recorder extends AudioRecorder
|
|||||||
|
|
||||||
public static void main(String[] args)
|
public static void main(String[] args)
|
||||||
{
|
{
|
||||||
Path inputPath = Paths.get("/home/denny/Music/PCM.wav");
|
mLog.debug("Starting ...");
|
||||||
Path outputPath = Paths.get("/home/denny/Music/denny_test/PCM.mp3");
|
|
||||||
|
// Path inputPath = Paths.get("/home/denny/Music/PCM.wav");
|
||||||
|
// Path outputPath = Paths.get("/home/denny/Music/denny_test/PCM.mp3");
|
||||||
|
//
|
||||||
|
// mLog.debug("Reading: " + inputPath.toString());
|
||||||
|
// mLog.debug("Writing: " + outputPath.toString());
|
||||||
|
//
|
||||||
|
// final MP3Recorder recorder = new MP3Recorder(outputPath);
|
||||||
|
// recorder.start(Executors.newSingleThreadScheduledExecutor());
|
||||||
|
//
|
||||||
|
// try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(inputPath, true))
|
||||||
|
// {
|
||||||
|
// reader.setListener(recorder);
|
||||||
|
// reader.read();
|
||||||
|
// recorder.stop();
|
||||||
|
// }
|
||||||
|
// catch (IOException e)
|
||||||
|
// {
|
||||||
|
// mLog.error("Error", e);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
Path outputPath = Paths.get("/home/denny/Music/Silence_test_10_seconds.mp3");
|
||||||
|
|
||||||
mLog.debug("Reading: " + inputPath.toString());
|
|
||||||
mLog.debug("Writing: " + outputPath.toString());
|
mLog.debug("Writing: " + outputPath.toString());
|
||||||
|
|
||||||
final MP3Recorder recorder = new MP3Recorder(outputPath);
|
final MP3Recorder recorder = new MP3Recorder(outputPath);
|
||||||
recorder.start(Executors.newSingleThreadScheduledExecutor());
|
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
recorder.start(executor);
|
||||||
|
|
||||||
try (AudioPacketMonoWaveReader reader = new AudioPacketMonoWaveReader(inputPath, true))
|
long duration = 1165;
|
||||||
|
int length = (int)(duration * 8); //8000 Hz sample rate
|
||||||
|
float[] silence = new float[length];
|
||||||
|
AudioPacket silencePacket = new AudioPacket(silence, null);
|
||||||
|
|
||||||
|
for(int x = 0; x < 10; x++)
|
||||||
{
|
{
|
||||||
reader.setListener(recorder);
|
recorder.receive(silencePacket);
|
||||||
reader.read();
|
|
||||||
recorder.stop();
|
|
||||||
}
|
|
||||||
catch (IOException e)
|
|
||||||
{
|
|
||||||
mLog.error("Error", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recorder.stop();
|
||||||
|
|
||||||
|
executor.shutdown();
|
||||||
|
|
||||||
mLog.debug("Finished");
|
mLog.debug("Finished");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts a received float sample to multiple listeners
|
* Broadcasts an item to multiple listeners
|
||||||
*/
|
*/
|
||||||
public class Broadcaster<T> implements Listener<T>
|
public class Broadcaster<T> implements Listener<T>
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user