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