diff --git a/.idea/misc.xml b/.idea/misc.xml index 6746d627..08f2781a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index a6d25f94..74800ded 100644 --- a/build.gradle +++ b/build.gradle @@ -70,8 +70,8 @@ dependencies { implementation 'net.coderazzi:tablefilter-swing:5.4.0' implementation 'org.apache.commons:commons-lang3:3.8.1' implementation 'org.apache.commons:commons-io:1.3.2' - implementation 'org.apache.mina:mina-core:2.0.19' - implementation 'org.apache.mina:mina-http:2.0.19' + implementation 'org.apache.mina:mina-core:2.1.3' + implementation 'org.apache.mina:mina-http:2.1.3' implementation 'org.controlsfx:controlsfx:11.0.1' implementation 'org.slf4j:slf4j-api:1.7.25' implementation 'org.usb4java:libusb4java:1.3.0' diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AbstractAudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/AbstractAudioBroadcaster.java new file mode 100644 index 00000000..c6aa11ee --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AbstractAudioBroadcaster.java @@ -0,0 +1,177 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2020 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 io.github.dsheirer.audio.broadcast; + +import io.github.dsheirer.sample.Listener; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base Audio Broadcaster class + */ +public abstract class AbstractAudioBroadcaster implements Listener +{ + private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioBroadcaster.class); + private Listener mBroadcastEventListener; + private T mBroadcastConfiguration; + protected ObjectProperty mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY); + protected int mStreamedAudioCount = 0; + protected int mErrorAudioCount = 0; + protected int mAgedOffAudioCount = 0; + + /** + * Constructs an instance + * @param broadcastConfiguration to use for this broadcaster + */ + public AbstractAudioBroadcaster(T broadcastConfiguration) + { + mBroadcastConfiguration = broadcastConfiguration; + } + + /** + * Observable broadcast state property + */ + public ObjectProperty broadcastStateProperty() + { + return mBroadcastState; + } + + /** + * Current state of the broadcastAudio connection + */ + public BroadcastState getBroadcastState() + { + return mBroadcastState.get(); + } + + /** + * Sets or changes the broadcast state + */ + public void setBroadcastState(BroadcastState broadcastState) + { + mBroadcastState.setValue(broadcastState); + broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE)); + } + + /** + * Starts the broadcaster + */ + public abstract void start(); + + /** + * Stops the broadcaster + */ + public abstract void stop(); + + /** + * Perform any shutdown related tasks. + */ + public abstract void dispose(); + + /** + * Access the configuration for the broadcaster + */ + public T getBroadcastConfiguration() + { + return mBroadcastConfiguration; + } + + /** + * Registers the listener to receive broadcast events/state changes + */ + public void setListener(Listener listener) + { + mBroadcastEventListener = listener; + } + + /** + * Removes the listener from receiving broadcast events/state changes + */ + public void removeListener() + { + mBroadcastEventListener = null; + } + + /** + * Broadcasts the event to any registered listener + */ + protected void broadcast(BroadcastEvent event) + { + if(mBroadcastEventListener != null) + { + mBroadcastEventListener.receive(event); + } + } + + /** + * Number of audio recordings streamed to remote server + */ + public int getStreamedAudioCount() + { + return mStreamedAudioCount; + } + + /** + * Increments the streamed or uploaded audio count by one + */ + public void incrementStreamedAudioCount() + { + mStreamedAudioCount++; + } + + /** + * Number of audio recordings that were removed for exceeding age limit + */ + public int getAgedOffAudioCount() + { + return mAgedOffAudioCount; + } + + /** + * Increments the aged-off audio count by one + */ + public void incrementAgedOffAudioCount() + { + mAgedOffAudioCount++; + } + + /** + * Number of audio recordings awaiting streaming or upload + */ + public abstract int getAudioQueueSize(); + + /** + * Total audio upload/stream error count + */ + public int getAudioErrorCount() + { + return mErrorAudioCount; + } + + /** + * Increments the error audio count by one + */ + public void incrementErrorAudioCount() + { + mErrorAudioCount++; + } +} diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingBroadcaster.java similarity index 83% rename from src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java rename to src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingBroadcaster.java index 341871a8..05641152 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioBroadcaster.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingBroadcaster.java @@ -23,10 +23,7 @@ package io.github.dsheirer.audio.broadcast; import io.github.dsheirer.audio.convert.ISilenceGenerator; import io.github.dsheirer.identifier.IdentifierCollection; -import io.github.dsheirer.sample.Listener; import io.github.dsheirer.util.ThreadPool; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import org.apache.commons.math3.util.FastMath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,25 +37,17 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -public abstract class AudioBroadcaster implements Listener +public abstract class AudioStreamingBroadcaster extends AbstractAudioBroadcaster { - private final static Logger mLog = LoggerFactory.getLogger(AudioBroadcaster.class); + private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingBroadcaster.class); public static final int PROCESSOR_RUN_INTERVAL_MS = 1000; - private ScheduledFuture mRecordingQueueProcessorFuture; private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor(); private Queue mAudioRecordingQueue = new LinkedTransferQueue<>(); - private ISilenceGenerator mSilenceGenerator; - private Listener mBroadcastEventListener; - private ObjectProperty mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY); - - private int mStreamedAudioCount = 0; - private int mAgedOffAudioCount = 0; - private BroadcastConfiguration mBroadcastConfiguration; private long mDelay; private long mMaximumRecordingAge; private AtomicBoolean mStreaming = new AtomicBoolean(); @@ -84,22 +73,14 @@ public abstract class AudioBroadcaster implements Listener * The last audio packet's metadata is automatically attached to the closed audio recording when it is enqueued for * broadcast. That metadata will be updated on the remote server once the audio recording is opened for streaming. */ - public AudioBroadcaster(BroadcastConfiguration broadcastConfiguration) + public AudioStreamingBroadcaster(T broadcastConfiguration) { - mBroadcastConfiguration = broadcastConfiguration; - mDelay = mBroadcastConfiguration.getDelay(); - mMaximumRecordingAge = mBroadcastConfiguration.getMaximumRecordingAge(); + super(broadcastConfiguration); + mDelay = getBroadcastConfiguration().getDelay(); + mMaximumRecordingAge = getBroadcastConfiguration().getMaximumRecordingAge(); mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat()); } - /** - * Observable broadcast state property - */ - public ObjectProperty broadcastStateProperty() - { - return mBroadcastState; - } - public void dispose() { } @@ -186,27 +167,11 @@ public abstract class AudioBroadcaster implements Listener /** * Size of recording queue for recordings awaiting streaming */ - public int getQueueSize() + public int getAudioQueueSize() { return mAudioRecordingQueue.size(); } - /** - * Number of audio recordings streamed to remote server - */ - public int getStreamedAudioCount() - { - return mStreamedAudioCount; - } - - /** - * Number of audio recordings that were removed for exceeding age limit - */ - public int getAgedOffAudioCount() - { - return mAgedOffAudioCount; - } - /** * Primary insert method for the stream manager to nominate completed audio recordings for broadcast. * @@ -225,45 +190,10 @@ public abstract class AudioBroadcaster implements Listener } } - /** - * Broadcast configuration used by this broadcaster - */ - public BroadcastConfiguration getBroadcastConfiguration() - { - return mBroadcastConfiguration; - } - - /** - * Registers the listener to receive broadcast events/state changes - */ - public void setListener(Listener listener) - { - mBroadcastEventListener = listener; - } - - /** - * Removes the listener from receiving broadcast events/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) + public void setBroadcastState(BroadcastState state) { if(mBroadcastState.get() != state) { @@ -272,11 +202,9 @@ public abstract class AudioBroadcaster implements Listener mLog.info("[" + getStreamName() + "] status: " + state); } - mBroadcastState.setValue(state); + super.setBroadcastState(state); - broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE)); - - if(mBroadcastState.get().isErrorState()) + if(mBroadcastState.get() instanceof BroadcastState && ((BroadcastState)mBroadcastState.get()).isErrorState()) { stop(); } @@ -300,14 +228,6 @@ public abstract class AudioBroadcaster implements Listener } } - /** - * Current state of the broadcastAudio connection - */ - public BroadcastState getBroadcastState() - { - return mBroadcastState.get(); - } - /** * Indicates if the broadcaster is currently connected to the remote server */ @@ -417,7 +337,7 @@ public abstract class AudioBroadcaster implements Listener if(mInputStream != null) { mStreamedAudioCount++; - broadcast(new BroadcastEvent(AudioBroadcaster.this, + broadcast(new BroadcastEvent(AudioStreamingBroadcaster.this, BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE)); metadataUpdateRequired = true; } @@ -435,7 +355,7 @@ public abstract class AudioBroadcaster implements Listener nextRecording = mAudioRecordingQueue.remove(); nextRecording.removePendingReplay(); mAgedOffAudioCount++; - broadcast(new BroadcastEvent(AudioBroadcaster.this, + broadcast(new BroadcastEvent(AudioStreamingBroadcaster.this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE)); nextRecording = mAudioRecordingQueue.peek(); } @@ -482,7 +402,7 @@ public abstract class AudioBroadcaster implements Listener nextRecording.removePendingReplay(); - broadcast(new BroadcastEvent(AudioBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE)); + broadcast(new BroadcastEvent(AudioStreamingBroadcaster.this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE)); } //If we closed out a recording and don't have a new/next recording, send an empty metadata update diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java index 33e463a6..e5669926 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastConfiguration.java @@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration; import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration; import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration; import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration; @@ -46,6 +47,7 @@ import java.net.SocketAddress; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ + @JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"), @JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"), @JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"), @JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"), @@ -64,11 +66,12 @@ public abstract class BroadcastConfiguration private LongProperty mDelay = new SimpleLongProperty(); private LongProperty mMaximumRecordingAge = new SimpleLongProperty(10 * 60 * 1000); //10 minutes default private BooleanProperty mEnabled = new SimpleBooleanProperty(false); - private BooleanProperty mValid; + protected BooleanProperty mValid = new SimpleBooleanProperty(); private int mId = ++UNIQUE_ID; public BroadcastConfiguration() { + mValid.bind(Bindings.and(Bindings.isNotNull(mHost), Bindings.greaterThan(mPort, 0))); } /** @@ -155,12 +158,6 @@ public abstract class BroadcastConfiguration */ public BooleanProperty validProperty() { - if(mValid == null) - { - mValid = new SimpleBooleanProperty(); - mValid.bind(Bindings.and(Bindings.isNotNull(mHost), Bindings.greaterThan(mPort, 0))); - } - return mValid; } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastEvent.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastEvent.java index 802c9814..16131e87 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastEvent.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastEvent.java @@ -21,7 +21,7 @@ package io.github.dsheirer.audio.broadcast; public class BroadcastEvent { private BroadcastConfiguration mBroadcastConfiguration; - private AudioBroadcaster mAudioBroadcaster; + private AbstractAudioBroadcaster mAudioBroadcaster; private Event mEvent; /** @@ -36,7 +36,7 @@ public class BroadcastEvent mEvent = event; } - public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event) + public BroadcastEvent(AbstractAudioBroadcaster audioBroadcaster, Event event) { mAudioBroadcaster = audioBroadcaster; mEvent = event; @@ -47,7 +47,7 @@ public class BroadcastEvent return mBroadcastConfiguration; } - public AudioBroadcaster getAudioBroadcaster() + public AbstractAudioBroadcaster getAudioBroadcaster() { return mAudioBroadcaster; } @@ -83,6 +83,7 @@ public class BroadcastEvent BROADCASTER_STATE_CHANGE, BROADCASTER_STREAMED_COUNT_CHANGE, BROADCASTER_AGED_OFF_COUNT_CHANGE, + BROADCASTER_ERROR_COUNT_CHANGE, BROADCASTER_DELETE, CONFIGURATION_ADD, diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java index 85a55e90..ad663354 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java @@ -22,14 +22,16 @@ package io.github.dsheirer.audio.broadcast; import io.github.dsheirer.alias.AliasModel; -import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyConfiguration; +import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster; +import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration; +import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration; import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster; import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration; import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster; import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration; import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster; import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration; -import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster; +import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2AudioStreamingBroadcaster; import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration; import io.github.dsheirer.audio.convert.IAudioConverter; import io.github.dsheirer.audio.convert.ISilenceGenerator; @@ -51,7 +53,7 @@ public class BroadcastFactory * @param configuration describing the server and audio types * @return configured broadcaster or null */ - public static AudioBroadcaster getBroadcaster(BroadcastConfiguration configuration, AliasModel aliasModel) + public static AbstractAudioBroadcaster getBroadcaster(BroadcastConfiguration configuration, AliasModel aliasModel) { if(configuration != null) { @@ -61,8 +63,10 @@ public class BroadcastFactory { switch(configuration.getBroadcastServerType()) { + case BROADCASTIFY_CALL: + return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration, aliasModel); case BROADCASTIFY: - return new IcecastTCPAudioBroadcaster((BroadcastifyConfiguration) configuration, aliasModel); + return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration, aliasModel); case ICECAST_TCP: return new IcecastTCPAudioBroadcaster((IcecastTCPConfiguration) configuration, aliasModel); case ICECAST_HTTP: @@ -70,7 +74,7 @@ public class BroadcastFactory case SHOUTCAST_V1: return new ShoutcastV1AudioBroadcaster((ShoutcastV1Configuration) configuration, aliasModel); case SHOUTCAST_V2: - return new ShoutcastV2AudioBroadcaster((ShoutcastV2Configuration) configuration, aliasModel); + return new ShoutcastV2AudioStreamingBroadcaster((ShoutcastV2Configuration) configuration, aliasModel); case UNKNOWN: default: mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name()); @@ -112,8 +116,10 @@ public class BroadcastFactory { switch(serverType) { + case BROADCASTIFY_CALL: + return new BroadcastifyCallConfiguration(format); case BROADCASTIFY: - return new BroadcastifyConfiguration(format); + return new BroadcastifyFeedConfiguration(format); case ICECAST_HTTP: return new IcecastHTTPConfiguration(format); case ICECAST_TCP: diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java index 50be596b..a5940160 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java @@ -30,7 +30,6 @@ import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import io.github.dsheirer.util.ThreadPool; import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +41,7 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -67,19 +67,19 @@ public class BroadcastModel extends AbstractTableModel implements Listener