mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
#757 Implements Broadcastify Calls API audio broadcaster and resolves audio streaming error introduced with #5 (#773)
Co-authored-by: Denny <denny@denny-desktop>
This commit is contained in:
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_13" default="false" project-jdk-name="14" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_13" default="false" project-jdk-name="13.0.2" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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'
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
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<T extends BroadcastConfiguration> implements Listener<AudioRecording>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioBroadcaster.class);
|
||||
private Listener<BroadcastEvent> mBroadcastEventListener;
|
||||
private T mBroadcastConfiguration;
|
||||
protected ObjectProperty<BroadcastState> 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<BroadcastState> 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<BroadcastEvent> 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++;
|
||||
}
|
||||
}
|
||||
@@ -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<AudioRecording>
|
||||
public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration> extends AbstractAudioBroadcaster<T>
|
||||
{
|
||||
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<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||
|
||||
private ISilenceGenerator mSilenceGenerator;
|
||||
|
||||
private Listener<BroadcastEvent> mBroadcastEventListener;
|
||||
private ObjectProperty<BroadcastState> 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<AudioRecording>
|
||||
* 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<BroadcastState> broadcastStateProperty()
|
||||
{
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
@@ -186,27 +167,11 @@ public abstract class AudioBroadcaster implements Listener<AudioRecording>
|
||||
/**
|
||||
* 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<AudioRecording>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast configuration used by this broadcaster
|
||||
*/
|
||||
public BroadcastConfiguration getBroadcastConfiguration()
|
||||
{
|
||||
return mBroadcastConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive broadcast events/state changes
|
||||
*/
|
||||
public void setListener(Listener<BroadcastEvent> 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<AudioRecording>
|
||||
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<AudioRecording>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<AudioRecording>
|
||||
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<AudioRecording>
|
||||
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<AudioRecording>
|
||||
|
||||
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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<Audio
|
||||
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
|
||||
public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
|
||||
public static final int COLUMN_BROADCASTER_AGED_OFF_COUNT = 5;
|
||||
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 6;
|
||||
|
||||
public static final String[] COLUMN_NAMES = new String[]
|
||||
{"Streaming", "Name", "Status", "Queued", "Streamed", "Aged Off"};
|
||||
{"Streaming", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
|
||||
|
||||
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
|
||||
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
|
||||
private ObservableList<BroadcastConfiguration> mBroadcastConfigurations =
|
||||
FXCollections.observableArrayList(BroadcastConfiguration.extractor());
|
||||
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
|
||||
private List<AudioRecording> mRecordingQueue = new CopyOnWriteArrayList<>();
|
||||
private Map<Integer,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
||||
private Map<Integer,AbstractAudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
||||
private IconManager mIconManager;
|
||||
private AliasModel mAliasModel;
|
||||
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
|
||||
private BroadcastEventListener mBroadcastEventListener = new BroadcastEventListener();
|
||||
|
||||
/**
|
||||
* Model for managing Broadcast configurations and any associated broadcaster instances.
|
||||
@@ -93,10 +93,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS);
|
||||
|
||||
removeOrphanedTemporaryRecordings();
|
||||
mBroadcastConfigurations.addListener(new ConfigurationChangeListener());
|
||||
|
||||
//TODO: remove broadcast of change events and simply have the playlist manager register as a listener on the configs
|
||||
//TODO: list to detect changes and execute playlist saves.
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,11 +108,11 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
*/
|
||||
public void clear()
|
||||
{
|
||||
List<BroadcastConfiguration> configsToRemove = new ArrayList<>(mBroadcastConfigurations);
|
||||
List<ConfiguredBroadcast> configuredBroadcasts = new ArrayList<>(mConfiguredBroadcasts);
|
||||
|
||||
for(BroadcastConfiguration configToRemove: configsToRemove)
|
||||
for(ConfiguredBroadcast configuredBroadcast: configuredBroadcasts)
|
||||
{
|
||||
removeBroadcastConfiguration(configToRemove);
|
||||
removeBroadcastConfiguration(configuredBroadcast.getBroadcastConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,9 +123,9 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
{
|
||||
List<String> names = new ArrayList<>();
|
||||
|
||||
for(BroadcastConfiguration configuration : mBroadcastConfigurations)
|
||||
for(ConfiguredBroadcast configuredBroadcast: mConfiguredBroadcasts)
|
||||
{
|
||||
names.add(configuration.getName());
|
||||
names.add(configuredBroadcast.getBroadcastConfiguration().getName());
|
||||
}
|
||||
|
||||
return names;
|
||||
@@ -138,9 +134,16 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
/**
|
||||
* Current list of broadcastAudio configurations
|
||||
*/
|
||||
public ObservableList<BroadcastConfiguration> getBroadcastConfigurations()
|
||||
public List<BroadcastConfiguration> getBroadcastConfigurations()
|
||||
{
|
||||
return mBroadcastConfigurations;
|
||||
List<BroadcastConfiguration> configs = new ArrayList<>();
|
||||
|
||||
for(ConfiguredBroadcast configuredBroadcast: mConfiguredBroadcasts)
|
||||
{
|
||||
configs.add(configuredBroadcast.getBroadcastConfiguration());
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,18 +166,15 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
{
|
||||
ensureUniqueName(configuration);
|
||||
|
||||
if(!mBroadcastConfigurations.contains(configuration))
|
||||
ConfiguredBroadcast configuredBroadcast = getConfiguredBroadcast(configuration);
|
||||
|
||||
if(configuredBroadcast == null)
|
||||
{
|
||||
mBroadcastConfigurations.add(configuration);
|
||||
ConfiguredBroadcast configuredBroadcast = new ConfiguredBroadcast(configuration);
|
||||
configuredBroadcast = new ConfiguredBroadcast(configuration);
|
||||
mConfiguredBroadcasts.add(configuredBroadcast);
|
||||
|
||||
int index = mBroadcastConfigurations.size() - 1;
|
||||
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
fireTableRowsInserted(index, index);
|
||||
|
||||
process(new BroadcastEvent(configuration, BroadcastEvent.Event.CONFIGURATION_ADD));
|
||||
|
||||
return configuredBroadcast;
|
||||
}
|
||||
}
|
||||
@@ -182,21 +182,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones the configuration and adds it this model with a unique configuration name
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -271,11 +256,11 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
return false;
|
||||
}
|
||||
|
||||
for(BroadcastConfiguration configurationToCompare : mBroadcastConfigurations)
|
||||
for(ConfiguredBroadcast configuredBroadcast: mConfiguredBroadcasts)
|
||||
{
|
||||
if(configurationToCompare != configuration &&
|
||||
configurationToCompare.getName() != null &&
|
||||
configurationToCompare.getName().equals(name))
|
||||
BroadcastConfiguration toCompare = configuredBroadcast.getBroadcastConfiguration();
|
||||
|
||||
if(toCompare != configuration && toCompare.getName() != null && toCompare.getName().equals(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -286,20 +271,19 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
|
||||
public void removeBroadcastConfiguration(BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
if(broadcastConfiguration != null && mBroadcastConfigurations.contains(broadcastConfiguration))
|
||||
ConfiguredBroadcast configuredBroadcast = getConfiguredBroadcast(broadcastConfiguration);
|
||||
|
||||
if(configuredBroadcast != null)
|
||||
{
|
||||
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
|
||||
mBroadcastConfigurations.remove(broadcastConfiguration);
|
||||
mConfiguredBroadcasts.remove(configuredBroadcast);
|
||||
|
||||
Iterator<ConfiguredBroadcast> it = mConfiguredBroadcasts.iterator();
|
||||
while(it.hasNext())
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
if(it.next().getBroadcastConfiguration() == broadcastConfiguration)
|
||||
{
|
||||
it.remove();
|
||||
break;
|
||||
}
|
||||
mBroadcasterMap.remove(broadcastConfiguration.getId());
|
||||
configuredBroadcast.getAudioBroadcaster().stop();;
|
||||
configuredBroadcast.setAudioBroadcaster(null);
|
||||
}
|
||||
|
||||
process(new BroadcastEvent(broadcastConfiguration, BroadcastEvent.Event.CONFIGURATION_DELETE));
|
||||
@@ -311,9 +295,16 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
/**
|
||||
* Returns the broadcaster associated with the stream name or null if there is no broadcaster setup for the name.
|
||||
*/
|
||||
public AudioBroadcaster getBroadcaster(String streamName)
|
||||
public AbstractAudioBroadcaster getBroadcaster(String streamName)
|
||||
{
|
||||
return mBroadcasterMap.get(streamName);
|
||||
BroadcastConfiguration broadcastConfiguration = getBroadcastConfiguration(streamName);
|
||||
|
||||
if(broadcastConfiguration != null)
|
||||
{
|
||||
return mBroadcasterMap.get(broadcastConfiguration.getId());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -327,7 +318,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
|
||||
if(channelName != null)
|
||||
{
|
||||
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
|
||||
AbstractAudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
@@ -346,33 +337,25 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
*/
|
||||
private void createBroadcaster(BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
if(broadcastConfiguration != null && broadcastConfiguration.isEnabled() && broadcastConfiguration.isValid())
|
||||
{
|
||||
//Remove current broadcaster if one exists
|
||||
deleteBroadcaster(broadcastConfiguration.getId());
|
||||
ConfiguredBroadcast configuredBroadcast = getConfiguredBroadcast(broadcastConfiguration);
|
||||
|
||||
AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration, mAliasModel);
|
||||
if(configuredBroadcast != null && broadcastConfiguration.isEnabled())
|
||||
{
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
deleteBroadcaster(configuredBroadcast);
|
||||
}
|
||||
|
||||
AbstractAudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration, mAliasModel);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
audioBroadcaster.setListener(broadcastEvent -> process(broadcastEvent));
|
||||
configuredBroadcast.setAudioBroadcaster(audioBroadcaster);
|
||||
audioBroadcaster.setListener(mBroadcastEventListener);
|
||||
audioBroadcaster.start();
|
||||
|
||||
Iterator<ConfiguredBroadcast> it = mConfiguredBroadcasts.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
ConfiguredBroadcast configuredBroadcast = it.next();
|
||||
|
||||
if(configuredBroadcast.getBroadcastConfiguration() == broadcastConfiguration)
|
||||
{
|
||||
configuredBroadcast.setAudioBroadcaster(audioBroadcaster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getId(), audioBroadcaster);
|
||||
|
||||
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
@@ -387,39 +370,27 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
/**
|
||||
* Shut down a broadcaster created from the configuration and remove it from this model
|
||||
*/
|
||||
private void deleteBroadcaster(int id)
|
||||
private void deleteBroadcaster(ConfiguredBroadcast configuredBroadcast)
|
||||
{
|
||||
if(mBroadcasterMap.containsKey(id))
|
||||
if(configuredBroadcast != null && configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
AudioBroadcaster audioBroadcaster = mBroadcasterMap.remove(id);
|
||||
mBroadcasterMap.remove(configuredBroadcast.getBroadcastConfiguration().getId());
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
AbstractAudioBroadcaster broadcaster = configuredBroadcast.getAudioBroadcaster();
|
||||
configuredBroadcast.setAudioBroadcaster(null);
|
||||
|
||||
broadcaster.stop();
|
||||
broadcaster.removeListener();
|
||||
broadcaster.dispose();
|
||||
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
Iterator<ConfiguredBroadcast> it = mConfiguredBroadcasts.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
ConfiguredBroadcast configuredBroadcast = it.next();
|
||||
|
||||
if(configuredBroadcast.getBroadcastConfiguration().getId() == id)
|
||||
{
|
||||
configuredBroadcast.setAudioBroadcaster(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
audioBroadcaster.stop();
|
||||
audioBroadcaster.removeListener();
|
||||
audioBroadcaster.dispose();
|
||||
|
||||
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
fireTableRowsUpdated(index, index);
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
|
||||
fireTableRowsUpdated(index, index);
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(broadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,14 +399,15 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
*/
|
||||
public BroadcastConfiguration getBroadcastConfiguration(String streamName)
|
||||
{
|
||||
for(BroadcastConfiguration config: mBroadcastConfigurations)
|
||||
for(ConfiguredBroadcast configuredBroadcast: mConfiguredBroadcasts)
|
||||
{
|
||||
if(config.getName() != null && config.getName().contentEquals(streamName))
|
||||
if(configuredBroadcast.getBroadcastConfiguration().getName() != null &&
|
||||
configuredBroadcast.getBroadcastConfiguration().getName().equals(streamName))
|
||||
{
|
||||
return config;
|
||||
return configuredBroadcast.getBroadcastConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -455,6 +427,43 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
mBroadcastEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configured broadcast that contains the specified configuration
|
||||
* @param configuration to search for
|
||||
* @return configured broadcast that matches, or null
|
||||
*/
|
||||
private ConfiguredBroadcast getConfiguredBroadcast(BroadcastConfiguration configuration)
|
||||
{
|
||||
for(ConfiguredBroadcast configuredBroadcast: mConfiguredBroadcasts)
|
||||
{
|
||||
if(configuredBroadcast.getBroadcastConfiguration().equals(configuration))
|
||||
{
|
||||
return configuredBroadcast;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the configured broadcast that contains the specified broadcaster
|
||||
* @param broadcaster to search for
|
||||
* @return configured broadcast that matches, or null
|
||||
*/
|
||||
private ConfiguredBroadcast getConfiguredBroadcast(AbstractAudioBroadcaster broadcaster)
|
||||
{
|
||||
for(ConfiguredBroadcast configuredBroadcast: mConfiguredBroadcasts)
|
||||
{
|
||||
if(configuredBroadcast.getAudioBroadcaster() != null &&
|
||||
configuredBroadcast.getAudioBroadcaster().equals(broadcaster))
|
||||
{
|
||||
return configuredBroadcast;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the broadcastAudio configuration change event
|
||||
*/
|
||||
@@ -477,9 +486,10 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
break;
|
||||
case CONFIGURATION_CHANGE:
|
||||
BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
|
||||
ConfiguredBroadcast configuredBroadcast = getConfiguredBroadcast(broadcastConfiguration);
|
||||
|
||||
//Delete the broadcaster if it exists
|
||||
deleteBroadcaster(broadcastConfiguration.getId());
|
||||
deleteBroadcaster(configuredBroadcast);
|
||||
|
||||
//If the configuration is enabled, create a new broadcaster after a brief delay
|
||||
if(broadcastConfiguration.isEnabled())
|
||||
@@ -489,17 +499,18 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
fireTableRowsUpdated(index, index);
|
||||
break;
|
||||
case CONFIGURATION_DELETE:
|
||||
deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getId());
|
||||
deleteBroadcaster(getConfiguredBroadcast(broadcastEvent.getBroadcastConfiguration()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if(broadcastEvent.isAudioBroadcasterEvent())
|
||||
{
|
||||
int row = mBroadcastConfigurations.indexOf(broadcastEvent.getAudioBroadcaster().getBroadcastConfiguration());
|
||||
ConfiguredBroadcast configuredBroadcast = getConfiguredBroadcast(broadcastEvent.getAudioBroadcaster());
|
||||
int row = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
|
||||
switch(broadcastEvent.getEvent())
|
||||
{
|
||||
@@ -526,6 +537,11 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_AGED_OFF_COUNT);
|
||||
}
|
||||
case BROADCASTER_ERROR_COUNT_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_ERROR_COUNT);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -537,7 +553,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
@Override
|
||||
public int getRowCount()
|
||||
{
|
||||
return mBroadcastConfigurations.size();
|
||||
return mConfiguredBroadcasts.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -551,16 +567,17 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
{
|
||||
try
|
||||
{
|
||||
if(rowIndex <= mBroadcastConfigurations.size())
|
||||
if(rowIndex <= mConfiguredBroadcasts.size())
|
||||
{
|
||||
BroadcastConfiguration configuration = mBroadcastConfigurations.get(rowIndex);
|
||||
ConfiguredBroadcast configuredBroadcast = mConfiguredBroadcasts.get(rowIndex);
|
||||
|
||||
if(configuration != null)
|
||||
if(configuredBroadcast != null)
|
||||
{
|
||||
switch(columnIndex)
|
||||
{
|
||||
case COLUMN_SERVER_ICON:
|
||||
String iconPath = configuration.getBroadcastServerType().getIconPath();
|
||||
String iconPath = configuredBroadcast.getBroadcastConfiguration()
|
||||
.getBroadcastServerType().getIconPath();
|
||||
|
||||
if(iconPath != null && mIconManager != null)
|
||||
{
|
||||
@@ -568,45 +585,46 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
break;
|
||||
case COLUMN_STREAM_NAME:
|
||||
return configuration.getName();
|
||||
return configuredBroadcast.getBroadcastConfiguration().getName();
|
||||
case COLUMN_BROADCASTER_STATUS:
|
||||
AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterA != null)
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
return audioBroadcasterA.getBroadcastState();
|
||||
return configuredBroadcast.getAudioBroadcaster().getBroadcastState();
|
||||
}
|
||||
else if(!configuration.isEnabled())
|
||||
else if(!configuredBroadcast.getBroadcastConfiguration().isEnabled())
|
||||
{
|
||||
return BroadcastState.DISABLED;
|
||||
}
|
||||
else if(!configuration.isValid())
|
||||
else if(!configuredBroadcast.getBroadcastConfiguration().isValid())
|
||||
{
|
||||
return BroadcastState.INVALID_SETTINGS;
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||
AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterB != null)
|
||||
else
|
||||
{
|
||||
return audioBroadcasterB.getQueueSize();
|
||||
return BroadcastState.ERROR;
|
||||
}
|
||||
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
return configuredBroadcast.getAudioBroadcaster().getAudioQueueSize();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||
AudioBroadcaster audioBroadcasterC = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterC != null)
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
return audioBroadcasterC.getStreamedAudioCount();
|
||||
return configuredBroadcast.getAudioBroadcaster().getStreamedAudioCount();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
|
||||
AudioBroadcaster audioBroadcasterD = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterD != null)
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
return audioBroadcasterD.getAgedOffAudioCount();
|
||||
return configuredBroadcast.getAudioBroadcaster().getAgedOffAudioCount();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_ERROR_COUNT:
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
return configuredBroadcast.getAudioBroadcaster().getAudioErrorCount();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -633,6 +651,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
|
||||
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||
case COLUMN_BROADCASTER_ERROR_COUNT:
|
||||
return Integer.class;
|
||||
case COLUMN_SERVER_ICON:
|
||||
return ImageIcon.class;
|
||||
@@ -642,22 +661,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast configuration at the specified model row
|
||||
*/
|
||||
public BroadcastConfiguration getConfigurationAt(int rowIndex)
|
||||
{
|
||||
return mBroadcastConfigurations.get(rowIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model row number for the specified configuration
|
||||
*/
|
||||
public int getRowForConfiguration(BroadcastConfiguration configuration)
|
||||
{
|
||||
return mBroadcastConfigurations.indexOf(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column)
|
||||
{
|
||||
@@ -805,27 +808,14 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
|
||||
/**
|
||||
* List change listener to detect broadcast configuration changes and broadcast a change event
|
||||
* Adapter to receive and process broadcast events from constructed audio broadcasters
|
||||
*/
|
||||
public class ConfigurationChangeListener implements ListChangeListener<BroadcastConfiguration>
|
||||
public class BroadcastEventListener implements Listener<BroadcastEvent>
|
||||
{
|
||||
@Override
|
||||
public void onChanged(Change<? extends BroadcastConfiguration> change)
|
||||
public void receive(BroadcastEvent broadcastEvent)
|
||||
{
|
||||
while(change.next())
|
||||
{
|
||||
if(change.wasUpdated())
|
||||
{
|
||||
for(int x = change.getFrom(); x < change.getTo(); x++)
|
||||
{
|
||||
BroadcastConfiguration config = change.getList().get(x);
|
||||
BroadcastEvent event = new BroadcastEvent(config, BroadcastEvent.Event.CONFIGURATION_CHANGE);
|
||||
process(event);
|
||||
broadcast(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
process(broadcastEvent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,16 @@ package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
public enum BroadcastServerType
|
||||
{
|
||||
BROADCASTIFY("Broadcastify", "/images/broadcastify.png"), //Icecast Server 2.3.2
|
||||
/**
|
||||
* Broadcastify feeds (ie streaming) service
|
||||
*/
|
||||
BROADCASTIFY("Broadcastify Feed", "/images/broadcastify.png"), //Icecast Server 2.3.2
|
||||
|
||||
/**
|
||||
* Broadcastify calls - completed audio recording push service
|
||||
*/
|
||||
BROADCASTIFY_CALL("Broadcastify Call", "/images/broadcastify.png"),
|
||||
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "/images/icecast.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "/images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "/images/shoutcast.png"),
|
||||
|
||||
@@ -35,7 +35,7 @@ import javafx.util.Callback;
|
||||
public class ConfiguredBroadcast
|
||||
{
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
private AudioBroadcaster mAudioBroadcaster;
|
||||
private AbstractAudioBroadcaster mAudioBroadcaster;
|
||||
private ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
@@ -93,7 +93,7 @@ public class ConfiguredBroadcast
|
||||
* Sets the audio broadcaster
|
||||
* @param audioBroadcaster to use for this configuration
|
||||
*/
|
||||
public void setAudioBroadcaster(AudioBroadcaster audioBroadcaster)
|
||||
public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster)
|
||||
{
|
||||
mBroadcastState.unbind();
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
@@ -126,11 +126,19 @@ public class ConfiguredBroadcast
|
||||
/**
|
||||
* Optional audio broadcaster created from the configuration
|
||||
*/
|
||||
public AudioBroadcaster getAudioBroadcaster()
|
||||
public AbstractAudioBroadcaster getAudioBroadcaster()
|
||||
{
|
||||
return mAudioBroadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configured broadcast has a non-null audio broadcaster assigned
|
||||
*/
|
||||
public boolean hasAudioBroadcaster()
|
||||
{
|
||||
return mAudioBroadcaster != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable property extractor for use with observable lists to detect changes internal to this object.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
|
||||
*
|
||||
* Note: this is not the same as the Broadcastify Feeds (ie streaming) service
|
||||
*/
|
||||
public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<BroadcastifyCallConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyCallBroadcaster.class);
|
||||
|
||||
private static final String ENCODING_TYPE_MP3 = "mp3";
|
||||
private static final String MULTIPART_TYPE = "multipart";
|
||||
private static final String DEFAULT_SUBTYPE = "form-data";
|
||||
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
|
||||
private static final String API_ENDPOINT = BroadcastifyCallConfiguration.DEV_ENDPOINT;
|
||||
// private static final String API_ENDPOINT = BroadcastifyCallConfiguration.PRODUCTION_ENDPOINT;
|
||||
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
|
||||
private HttpClient mHttpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_2)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
* @param config to use
|
||||
* @param aliasModel for access to aliases
|
||||
*/
|
||||
public BroadcastifyCallBroadcaster(BroadcastifyCallConfiguration config, AliasModel aliasModel)
|
||||
{
|
||||
super(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(response != null)
|
||||
{
|
||||
mLog.error("Error connecting to Broadcastify calls server on startup [" + response + "]");
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
|
||||
if(mAudioRecordingProcessorFuture == null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
|
||||
0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
if(mAudioRecordingProcessorFuture != null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture.cancel(true);
|
||||
mAudioRecordingProcessorFuture = null;
|
||||
dispose();
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares for disposal
|
||||
*/
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
audioRecording.removePendingReplay();
|
||||
audioRecording = mAudioRecordingQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
|
||||
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
|
||||
* the audio processor thread will persistently invoke this method to attempt a reconnect.
|
||||
*/
|
||||
private boolean connected()
|
||||
{
|
||||
if(getBroadcastState() != BroadcastState.CONNECTED &&
|
||||
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(response == null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
return getBroadcastState() == BroadcastState.CONNECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioQueueSize()
|
||||
{
|
||||
return mAudioRecordingQueue.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioRecording audioRecording)
|
||||
{
|
||||
mAudioRecordingQueue.offer(audioRecording);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
|
||||
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
|
||||
* deleted to ensure that the in-memory queue size doesn't blow up.
|
||||
* @param audioRecording to test
|
||||
* @return true if the recording is valid
|
||||
*/
|
||||
private boolean isValid(AudioRecording audioRecording)
|
||||
{
|
||||
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
|
||||
getBroadcastConfiguration().getMaximumRecordingAge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes any enqueued audio recordings. The broadcastify calls API uses a two-step process that includes
|
||||
* requesting an upload URL and then uploading the audio recording to that URL. This method employs asynchronous
|
||||
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
|
||||
*/
|
||||
private void processRecordingQueue()
|
||||
{
|
||||
while(connected() && !mAudioRecordingQueue.isEmpty())
|
||||
{
|
||||
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
|
||||
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
|
||||
{
|
||||
float durationSeconds = (float)(audioRecording.getRecordingLength() / 1E3f);
|
||||
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||
String talkgroup = getTo(audioRecording);
|
||||
String radioId = getFrom(audioRecording);
|
||||
float frequency = getFrequency(audioRecording);
|
||||
|
||||
BroadcastifyCallBuilder bodyBuilder = new BroadcastifyCallBuilder();
|
||||
bodyBuilder.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||
.addPart(FormField.SYSTEM_ID, getBroadcastConfiguration().getSystemID())
|
||||
.addPart(FormField.CALL_DURATION, durationSeconds)
|
||||
.addPart(FormField.TIMESTAMP, timestampSeconds)
|
||||
.addPart(FormField.TALKGROUP_ID, talkgroup)
|
||||
.addPart(FormField.RADIO_ID, radioId)
|
||||
.addPart(FormField.FREQUENCY, frequency)
|
||||
.addPart(FormField.ENCODING, ENCODING_TYPE_MP3);
|
||||
|
||||
try
|
||||
{
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.ACCEPT, "*/*")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
mHttpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.whenComplete((stringHttpResponse, throwable) -> {
|
||||
if(throwable != null || stringHttpResponse.statusCode() != 200)
|
||||
{
|
||||
if(throwable instanceof IOException)
|
||||
{
|
||||
//We get socket reset exceptions occasionally when the remote server doesn't
|
||||
//fully read our request and immediately responds.
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Error while sending upload URL request", throwable);
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
}
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
}
|
||||
else
|
||||
{
|
||||
String urlResponse = stringHttpResponse.body();
|
||||
|
||||
if(urlResponse.startsWith("0 "))
|
||||
{
|
||||
HttpRequest.BodyPublisher filePublisher = null;
|
||||
|
||||
try
|
||||
{
|
||||
filePublisher = HttpRequest.BodyPublishers.ofFile(audioRecording.getPath());
|
||||
}
|
||||
catch(FileNotFoundException fnfe)
|
||||
{
|
||||
mLog.error("Broadcastify calls API - audio recording file not found - ignoring upload");
|
||||
}
|
||||
|
||||
if(filePublisher != null)
|
||||
{
|
||||
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(urlResponse.substring(2)))
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
|
||||
.PUT(filePublisher)
|
||||
.build();
|
||||
|
||||
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
|
||||
.whenComplete((fileResponse, throwable1) -> {
|
||||
if(throwable1 != null || fileResponse.statusCode() != 200)
|
||||
{
|
||||
if(throwable1 instanceof IOException)
|
||||
{
|
||||
//We get socket reset exceptions occasionally when the remote server doesn't
|
||||
//fully read our request and immediately responds.
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("Broadcastify calls API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
}
|
||||
else
|
||||
{
|
||||
incrementStreamedAudioCount();
|
||||
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||
}
|
||||
|
||||
audioRecording.removePendingReplay();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//Register an error for the file not found exception
|
||||
mLog.error("Broadcastify calls API - upload file not found [" +
|
||||
audioRecording.getPath().toString() + "]");
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Broadcastify calls API upload URL request failed [" + urlResponse + "]");
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unknown Error", e);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
|
||||
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.peek();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
if(isValid(audioRecording))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
|
||||
mAudioRecordingQueue.poll();
|
||||
audioRecording.removePendingReplay();
|
||||
incrementAgedOffAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
|
||||
audioRecording = mAudioRecordingQueue.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a frequency value from the audio recording identifier collection.
|
||||
*/
|
||||
private static float getFrequency(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
|
||||
Form.CHANNEL_FREQUENCY, Role.ANY);
|
||||
|
||||
if(identifier instanceof ConfigurationLongIdentifier)
|
||||
{
|
||||
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
return value / 1E6f;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
|
||||
*/
|
||||
private static String getFrom(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof RadioIdentifier)
|
||||
{
|
||||
return ((RadioIdentifier)identifier).getValue().toString();
|
||||
}
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
List<Identifier> toIdentifiers = audioRecording.getIdentifierCollection().getIdentifiers(Role.TO);
|
||||
|
||||
if(toIdentifiers.size() >= 1)
|
||||
{
|
||||
Identifier to = toIdentifiers.get(0);
|
||||
|
||||
if(to instanceof TalkgroupIdentifier)
|
||||
{
|
||||
TalkgroupIdentifier talkgroupIdentifier = (TalkgroupIdentifier)to;
|
||||
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
|
||||
talkgroupIdentifier.getProtocol()));
|
||||
}
|
||||
else if(to instanceof PatchGroupIdentifier)
|
||||
{
|
||||
PatchGroup patchGroup = ((PatchGroupIdentifier)to).getValue();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
else if(to instanceof RadioIdentifier)
|
||||
{
|
||||
return ((RadioIdentifier)to).getValue().toString();
|
||||
}
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests both the connection and configuration against the Broadcastify Call API service
|
||||
* @param configuration containing API key and system id
|
||||
* @return error string or null if test is successful
|
||||
*/
|
||||
public static String testConnection(BroadcastifyCallConfiguration configuration)
|
||||
{
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
|
||||
BroadcastifyCallBuilder bodyBuilder = new BroadcastifyCallBuilder();
|
||||
bodyBuilder.addPart(FormField.API_KEY, configuration.getApiKey())
|
||||
.addPart(FormField.SYSTEM_ID, configuration.getSystemID())
|
||||
.addPart(FormField.CALL_DURATION, 0)
|
||||
.addPart(FormField.TIMESTAMP, (int)(System.currentTimeMillis() / 1E3))
|
||||
.addPart(FormField.TALKGROUP_ID, 999)
|
||||
.addPart(FormField.RADIO_ID, 999999)
|
||||
.addPart(FormField.FREQUENCY, 450.00000)
|
||||
.addPart(FormField.ENCODING, ENCODING_TYPE_MP3);
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(API_ENDPOINT))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.ACCEPT, "*/*")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||
|
||||
String responseBody = response.body();
|
||||
if(response.statusCode() == 200 && responseBody != null && responseBody.startsWith("0 "))
|
||||
{
|
||||
//success!
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return responseBody;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return e.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public class AudioRecordingProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
processRecordingQueue();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
|
||||
int ts = (int)(System.currentTimeMillis() / 1E3);
|
||||
|
||||
BroadcastifyCallBuilder builder = new BroadcastifyCallBuilder();
|
||||
builder.addPart(FormField.API_KEY, BroadcastifyCallConfiguration.SDRTRUNK_DEV_API_KEY)
|
||||
.addPart(FormField.SYSTEM_ID, 11)
|
||||
.addPart(FormField.CALL_DURATION, 3.0)
|
||||
.addPart(FormField.TIMESTAMP, 1234568)
|
||||
.addPart(FormField.TALKGROUP_ID, 1234)
|
||||
.addPart(FormField.RADIO_ID, 123456)
|
||||
.addPart(FormField.FREQUENCY, 450.12345)
|
||||
.addPart(FormField.ENCODING, "mp3");
|
||||
|
||||
HttpRequest.BodyPublisher body = builder.build();
|
||||
|
||||
if(body != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(BroadcastifyCallConfiguration.DEV_ENDPOINT))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + builder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.ACCEPT, "*/*")
|
||||
.POST(body).build();
|
||||
|
||||
HttpResponse<String> response = null;
|
||||
|
||||
mLog.info("Submitting request for URL");
|
||||
try
|
||||
{
|
||||
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
mLog.error("Error", ioe);
|
||||
}
|
||||
|
||||
if(response != null)
|
||||
{
|
||||
String url = response.body().toString();
|
||||
|
||||
mLog.info("URL Requested - Response: " + url);
|
||||
if(response.statusCode() == 200)
|
||||
{
|
||||
if(url != null && url.startsWith("0 "))
|
||||
{
|
||||
String file = "/home/denny/SDRTrunk/recordings/20200426_160130CNYICC_Onondaga_TRAFFIC__TO_1_FROM_911005.mp3";
|
||||
Path path = Path.of(file);
|
||||
|
||||
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url.substring(2)))
|
||||
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
|
||||
.POST(HttpRequest.BodyPublishers.ofFile(path))
|
||||
.build();
|
||||
|
||||
mLog.info("Submitting file upload");
|
||||
httpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
|
||||
.whenComplete((stringHttpResponse, throwable) -> {
|
||||
mLog.debug("Upload complete");
|
||||
if(throwable != null)
|
||||
{
|
||||
mLog.info("Upload Complete:" + stringHttpResponse.body());
|
||||
}
|
||||
else
|
||||
{
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Got HTTP 200 OK, but invalid response: " + url);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("Error response:" + url);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.debug("Error", e);
|
||||
}
|
||||
}
|
||||
|
||||
while(true);
|
||||
|
||||
// mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builder for an HTTP body publisher to produce a broadcastify call event
|
||||
*/
|
||||
public class BroadcastifyCallBuilder
|
||||
{
|
||||
private static final String DASH_DASH = "--";
|
||||
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||
private List<Part> mParts = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public BroadcastifyCallBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the static multi-part boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
return BOUNDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a string part to the call
|
||||
*/
|
||||
public BroadcastifyCallBuilder addPart(FormField key, String value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a number part to the call
|
||||
*/
|
||||
public BroadcastifyCallBuilder addPart(FormField key, Number value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value.toString()));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form-data item
|
||||
*/
|
||||
private static String formatPart(Part part, String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
|
||||
sb.append(part.getValue()).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the boundary closing item
|
||||
*/
|
||||
private static String getClosingBoundary(String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BodyPublisher for accessing the call form data
|
||||
*/
|
||||
public HttpRequest.BodyPublisher build()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
return HttpRequest.BodyPublishers.ofByteArray(sb.toString().getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key:Value pair holder
|
||||
*/
|
||||
public class Part
|
||||
{
|
||||
private String mKey;
|
||||
private String mValue;
|
||||
|
||||
/**
|
||||
* Constructs a new part
|
||||
* @param key value
|
||||
* @param value item
|
||||
*/
|
||||
public Part(String key, String value)
|
||||
{
|
||||
mKey = key;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getKey()
|
||||
{
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
/**
|
||||
* Streaming configuration for Broadcastify Calls API.
|
||||
*
|
||||
* Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
|
||||
* with the structure of the audio streaming subsystem in sdrtrunk
|
||||
*/
|
||||
public class BroadcastifyCallConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
public static final String SDRTRUNK_DEV_API_KEY = "c33aae37-8572-11ea-bd8b-0ecc8ab9ccec";
|
||||
public static final String SDRTRUNK_PRODUCTION_API_KEY = ""; //TODO: get production api key
|
||||
public static final String DEV_ENDPOINT = "https://api.broadcastify.com/call-upload-dev";
|
||||
public static final String PRODUCTION_ENDPOINT = "https://api.broadcastify.com/call-upload";
|
||||
public static final String CURRENT_ENDPOINT = DEV_ENDPOINT;
|
||||
|
||||
private IntegerProperty mSystemID = new SimpleIntegerProperty();
|
||||
|
||||
/**
|
||||
* Constructor for faster jackson
|
||||
*/
|
||||
public BroadcastifyCallConfiguration()
|
||||
{
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
* @param format to use for audio recording (MP3)
|
||||
*/
|
||||
public BroadcastifyCallConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
setHost(CURRENT_ENDPOINT);
|
||||
|
||||
//The parent class binds this property, so we unbind it and rebind it here
|
||||
mValid.unbind();
|
||||
mValid.bind(Bindings.greaterThan(mSystemID, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* System ID as a property
|
||||
*/
|
||||
public IntegerProperty systemIDProperty()
|
||||
{
|
||||
return mSystemID;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getApiKey()
|
||||
{
|
||||
return SDRTRUNK_DEV_API_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* System ID as provided by broadcastify.com
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system_id")
|
||||
public int getSystemID()
|
||||
{
|
||||
return mSystemID.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system ID provided by broadcastify.com
|
||||
*/
|
||||
public void setSystemID(int systemID)
|
||||
{
|
||||
mSystemID.set(systemID);
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.BROADCASTIFY_CALL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
BroadcastifyCallConfiguration copy = new BroadcastifyCallConfiguration();
|
||||
copy.setSystemID(getSystemID());
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,13 @@ import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyConfiguration.class);
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyFeedConfiguration.class);
|
||||
|
||||
private int mFeedID;
|
||||
|
||||
public BroadcastifyConfiguration()
|
||||
public BroadcastifyFeedConfiguration()
|
||||
{
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
@@ -47,7 +47,7 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
*
|
||||
* @param format of output audio (MP3)
|
||||
*/
|
||||
public BroadcastifyConfiguration(BroadcastFormat format)
|
||||
public BroadcastifyFeedConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
@@ -56,9 +56,9 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
setSampleRate(8000);
|
||||
}
|
||||
|
||||
public static BroadcastifyConfiguration from(UserFeedBroadcast userFeedBroadcast)
|
||||
public static BroadcastifyFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
|
||||
{
|
||||
BroadcastifyConfiguration config = new BroadcastifyConfiguration(BroadcastFormat.MP3);
|
||||
BroadcastifyFeedConfiguration config = new BroadcastifyFeedConfiguration(BroadcastFormat.MP3);
|
||||
config.setName(userFeedBroadcast.getDescription());
|
||||
config.setHost(userFeedBroadcast.getHostname());
|
||||
config.setMountPoint(userFeedBroadcast.getMount());
|
||||
@@ -80,7 +80,7 @@ public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
BroadcastifyConfiguration copy = new BroadcastifyConfiguration(getBroadcastFormat());
|
||||
BroadcastifyFeedConfiguration copy = new BroadcastifyFeedConfiguration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
/**
|
||||
* HTTP headers used for posting to Broadcastify calls api
|
||||
*/
|
||||
public enum FormField
|
||||
{
|
||||
API_KEY("apiKey"),
|
||||
SYSTEM_ID("systemId"),
|
||||
CALL_DURATION("callDuration"),
|
||||
TIMESTAMP("ts"),
|
||||
TALKGROUP_ID("tg"),
|
||||
RADIO_ID("src"),
|
||||
FREQUENCY("freq"),
|
||||
ENCODING("enc");
|
||||
|
||||
private String mHeader;
|
||||
|
||||
FormField(String header)
|
||||
{
|
||||
mHeader = header;
|
||||
}
|
||||
|
||||
public String getHeader()
|
||||
{
|
||||
return mHeader;
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,11 @@
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
|
||||
public abstract class IcecastAudioBroadcaster extends AudioBroadcaster
|
||||
public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
|
||||
{
|
||||
private IBroadcastMetadataUpdater mMetadataUpdater;
|
||||
private AliasModel mAliasModel;
|
||||
|
||||
@@ -24,13 +24,13 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = BroadcastifyConfiguration.class, name="broadcastifyConfiguration"),
|
||||
@JsonSubTypes.Type(value = BroadcastifyFeedConfiguration.class, name="broadcastifyConfiguration"),
|
||||
})
|
||||
public class IcecastTCPConfiguration extends IcecastConfiguration
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
@@ -40,7 +40,7 @@ import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
|
||||
public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1AudioBroadcaster.class);
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
|
||||
@@ -25,7 +25,7 @@ import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.AuthenticateBroadcast;
|
||||
@@ -70,9 +70,9 @@ import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBroadcastMetadataUpdater
|
||||
public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioBroadcaster.class);
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class);
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
private int mMaxPayloadSize = 16377;
|
||||
|
||||
@@ -95,7 +95,7 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
||||
*
|
||||
* @param configuration for the Shoutcast V2 stream
|
||||
*/
|
||||
public ShoutcastV2AudioBroadcaster(ShoutcastV2Configuration configuration, AliasModel aliasModel)
|
||||
public ShoutcastV2AudioStreamingBroadcaster(ShoutcastV2Configuration configuration, AliasModel aliasModel)
|
||||
{
|
||||
super(configuration);
|
||||
mAliasModel = aliasModel;
|
||||
@@ -47,7 +47,7 @@ import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.ConfiguredBroadcast;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.gui.playlist.alias.action.ActionEditor;
|
||||
@@ -167,8 +167,8 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
//Listen for changes to the stream configurations and refresh the stream lists
|
||||
mPlaylistManager.getBroadcastModel().getBroadcastConfigurations()
|
||||
.addListener((ListChangeListener<BroadcastConfiguration>)c -> updateStreamViews());
|
||||
mPlaylistManager.getBroadcastModel().getConfiguredBroadcasts()
|
||||
.addListener((ListChangeListener<ConfiguredBroadcast>)c -> updateStreamViews());
|
||||
|
||||
MyEventBus.getEventBus().register(this);
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import io.github.dsheirer.module.decode.mpt1327.identifier.MPT1327Talkgroup;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
|
||||
import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.rrapi.type.Flavor;
|
||||
import io.github.dsheirer.rrapi.type.Site;
|
||||
@@ -102,6 +104,30 @@ public class RadioReferenceDecoder
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the talkgroup value to the format used by radio reference
|
||||
* @param value of the talkgroup
|
||||
* @param protocol for the talkgroup
|
||||
* @return radio reference formatted talkgroup
|
||||
*/
|
||||
public static int convertToRadioReferenceTalkgroup(int value, Protocol protocol)
|
||||
{
|
||||
switch(protocol)
|
||||
{
|
||||
case LTR:
|
||||
int area = LTRTalkgroupFormatter.getArea(value);
|
||||
int home = LTRTalkgroupFormatter.getLcn(value);
|
||||
int group = LTRTalkgroupFormatter.getTalkgroup(value);
|
||||
return (area * 100000) + (home * 1000) + group;
|
||||
case MPT1327:
|
||||
int prefix = MPT1327TalkgroupFormatter.getPrefix(value);
|
||||
int ident = MPT1327TalkgroupFormatter.getIdent(value);
|
||||
return (prefix * 10000) + ident;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a talkgroup identifier for the talkgroup using the protocol for the system
|
||||
* @param talkgroup with value
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
/**
|
||||
* Broadcast configuration editor base class
|
||||
* @param <T> type of broadcast configuration
|
||||
*/
|
||||
public abstract class AbstractBroadcastEditor<T extends BroadcastConfiguration> extends Editor<T>
|
||||
{
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private Button mSaveButton;
|
||||
private Button mResetButton;
|
||||
private TextField mFormatField;
|
||||
private TextField mNameTextField;
|
||||
private ToggleSwitch mEnabledSwitch;
|
||||
protected EditorModificationListener mEditorModificationListener = new EditorModificationListener();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for access the stream manager
|
||||
*/
|
||||
public AbstractBroadcastEditor(PlaylistManager playlistManager)
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
getFormatField().setText(getBroadcastServerType().toString());
|
||||
|
||||
VBox buttonBox = new VBox();
|
||||
buttonBox.setPadding(new Insets(10,10,10,10));
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(getSaveButton(), getResetButton());
|
||||
|
||||
HBox editorBox = new HBox();
|
||||
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
|
||||
editorBox.getChildren().addAll(getEditorPane(), buttonBox);
|
||||
getChildren().addAll(editorBox);
|
||||
}
|
||||
|
||||
protected PlaylistManager getPlaylistManager()
|
||||
{
|
||||
return mPlaylistManager;
|
||||
}
|
||||
|
||||
protected abstract GridPane getEditorPane();
|
||||
|
||||
public abstract BroadcastServerType getBroadcastServerType();
|
||||
|
||||
@Override
|
||||
public void setItem(T item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
getNameTextField().setDisable(item == null);
|
||||
getEnabledSwitch().setDisable(item == null);
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
getNameTextField().setText(item.getName());
|
||||
getEnabledSwitch().selectedProperty().set(item.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
getNameTextField().setText(null);
|
||||
getEnabledSwitch().selectedProperty().set(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void save()
|
||||
{
|
||||
BroadcastConfiguration configuration = getItem();
|
||||
|
||||
if(configuration != null)
|
||||
{
|
||||
configuration.setEnabled(getEnabledSwitch().isSelected());
|
||||
|
||||
//Detect stream name change so that we can update any aliases that might be using the previous name
|
||||
String previousName = configuration.getName();
|
||||
String updatedName = getNameTextField().getText();
|
||||
configuration.setName(getNameTextField().getText());
|
||||
|
||||
if(previousName != null && !previousName.isEmpty() && !updatedName.contentEquals(previousName))
|
||||
{
|
||||
if(getPlaylistManager().getAliasModel().hasAliasesWithBroadcastChannel(previousName))
|
||||
{
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.getButtonTypes().clear();
|
||||
alert.getButtonTypes().addAll(ButtonType.NO, ButtonType.YES);
|
||||
alert.setTitle("Update Aliases");
|
||||
alert.setHeaderText("Rename requires updating aliases for this stream");
|
||||
alert.setContentText("Do you want to update aliases to new stream name?");
|
||||
alert.initOwner(((Node)getSaveButton()).getScene().getWindow());
|
||||
|
||||
//Workaround for JavaFX KDE on Linux bug in FX 10/11: https://bugs.openjdk.java.net/browse/JDK-8179073
|
||||
alert.setResizable(true);
|
||||
alert.onShownProperty().addListener(e -> {
|
||||
Platform.runLater(() -> alert.setResizable(false));
|
||||
});
|
||||
|
||||
alert.showAndWait().ifPresent(buttonType -> {
|
||||
if(buttonType == ButtonType.YES)
|
||||
{
|
||||
getPlaylistManager().getAliasModel().updateBroadcastChannel(previousName, updatedName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: remove this after we get rid of Swing tables so that we don't have to announce these changes.
|
||||
mPlaylistManager.getBroadcastModel().process(new BroadcastEvent(configuration,
|
||||
BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
protected Button getSaveButton()
|
||||
{
|
||||
if(mSaveButton == null)
|
||||
{
|
||||
mSaveButton = new Button("Save");
|
||||
mSaveButton.setDisable(true);
|
||||
mSaveButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mSaveButton.setOnAction(event -> save());
|
||||
mSaveButton.disableProperty().bind(modifiedProperty().not());
|
||||
}
|
||||
|
||||
return mSaveButton;
|
||||
}
|
||||
|
||||
protected Button getResetButton()
|
||||
{
|
||||
if(mResetButton == null)
|
||||
{
|
||||
mResetButton = new Button("Reset");
|
||||
mResetButton.setDisable(true);
|
||||
mResetButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mResetButton.setOnAction(event -> setItem(getItem()));
|
||||
mResetButton.disableProperty().bind(modifiedProperty().not());
|
||||
}
|
||||
|
||||
return mResetButton;
|
||||
}
|
||||
|
||||
protected TextField getNameTextField()
|
||||
{
|
||||
if(mNameTextField == null)
|
||||
{
|
||||
mNameTextField = new TextField();
|
||||
mNameTextField.setDisable(true);
|
||||
mNameTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mNameTextField;
|
||||
}
|
||||
|
||||
protected TextField getFormatField()
|
||||
{
|
||||
if(mFormatField == null)
|
||||
{
|
||||
mFormatField = new TextField();
|
||||
mFormatField.setDisable(true);
|
||||
}
|
||||
|
||||
return mFormatField;
|
||||
}
|
||||
|
||||
protected ToggleSwitch getEnabledSwitch()
|
||||
{
|
||||
if(mEnabledSwitch == null)
|
||||
{
|
||||
mEnabledSwitch = new ToggleSwitch();
|
||||
mEnabledSwitch.setDisable(true);
|
||||
mEnabledSwitch.selectedProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mEnabledSwitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple string change listener that sets the editor modified flag to true any time text fields are edited.
|
||||
*/
|
||||
public class EditorModificationListener implements ChangeListener<String>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue)
|
||||
{
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,39 +23,18 @@
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Base class for broadcast configuration editors.
|
||||
*/
|
||||
public abstract class AbstractStreamEditor<T extends BroadcastConfiguration> extends Editor<T>
|
||||
public abstract class AbstractStreamEditor<T extends BroadcastConfiguration> extends AbstractBroadcastEditor<T>
|
||||
{
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private Button mSaveButton;
|
||||
private Button mResetButton;
|
||||
private TextField mFormatField;
|
||||
private TextField mNameTextField;
|
||||
private TextField mHostTextField;
|
||||
private TextField mUnMaskedPasswordTextField;
|
||||
private PasswordField mMaskedPasswordTextField;
|
||||
@@ -63,25 +42,13 @@ public abstract class AbstractStreamEditor<T extends BroadcastConfiguration> ext
|
||||
private IntegerTextField mPortTextField;
|
||||
private IntegerTextField mDelayTextField;
|
||||
private IntegerTextField mMaxAgeTextField;
|
||||
private ToggleSwitch mEnabledSwitch;
|
||||
protected EditorModificationListener mEditorModificationListener = new EditorModificationListener();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public AbstractStreamEditor(PlaylistManager playlistManager)
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
|
||||
VBox buttonBox = new VBox();
|
||||
buttonBox.setPadding(new Insets(10,10,10,10));
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(getSaveButton(), getResetButton());
|
||||
|
||||
HBox editorBox = new HBox();
|
||||
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
|
||||
editorBox.getChildren().addAll(getEditorPane(), buttonBox);
|
||||
getChildren().addAll(editorBox);
|
||||
super(playlistManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,32 +56,26 @@ public abstract class AbstractStreamEditor<T extends BroadcastConfiguration> ext
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
getNameTextField().setDisable(item == null);
|
||||
getHostTextField().setDisable(item == null);
|
||||
getPortTextField().setDisable(item == null);
|
||||
getMaskedPasswordTextField().setDisable(item == null);
|
||||
getUnMaskedPasswordTextField().setDisable(item == null);
|
||||
getEnabledSwitch().setDisable(item == null);
|
||||
getMaxAgeTextField().setDisable(item == null);
|
||||
getDelayTextField().setDisable(item == null);
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
getNameTextField().setText(item.getName());
|
||||
getHostTextField().setText(item.getHost());
|
||||
getPortTextField().set(item.getPort());
|
||||
getMaskedPasswordTextField().setText(item.getPassword());
|
||||
getEnabledSwitch().setSelected(item.isEnabled());
|
||||
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000)); //Convert millis to seconds
|
||||
getDelayTextField().set((int)(item.getDelay() / 1000)); //Convert millis to seconds
|
||||
}
|
||||
else
|
||||
{
|
||||
getNameTextField().setText(null);
|
||||
getHostTextField().setText(null);
|
||||
getPortTextField().set(0);
|
||||
getMaskedPasswordTextField().setText(null);
|
||||
getEnabledSwitch().setSelected(false);
|
||||
getMaxAgeTextField().set(0);
|
||||
getDelayTextField().set(0);
|
||||
}
|
||||
@@ -130,80 +91,24 @@ public abstract class AbstractStreamEditor<T extends BroadcastConfiguration> ext
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
super.save();
|
||||
|
||||
BroadcastConfiguration configuration = getItem();
|
||||
|
||||
if(configuration != null)
|
||||
{
|
||||
configuration.setEnabled(getEnabledSwitch().isSelected());
|
||||
configuration.setHost(getHostTextField().getText());
|
||||
configuration.setPort(getPortTextField().get());
|
||||
configuration.setPassword(getMaskedPasswordTextField().getText());
|
||||
configuration.setDelay(getDelayTextField().get() * 1000); //Convert seconds to millis
|
||||
configuration.setMaximumRecordingAge(getMaxAgeTextField().get() * 1000); //Convert seconds to millis
|
||||
|
||||
//Detect stream name change so that we can update any aliases that might be using the previous name
|
||||
String previousName = configuration.getName();
|
||||
String updatedName = getNameTextField().getText();
|
||||
configuration.setName(getNameTextField().getText());
|
||||
|
||||
if(previousName != null && !previousName.isEmpty() && !updatedName.contentEquals(previousName))
|
||||
{
|
||||
if(mPlaylistManager.getAliasModel().hasAliasesWithBroadcastChannel(previousName))
|
||||
{
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.getButtonTypes().clear();
|
||||
alert.getButtonTypes().addAll(ButtonType.NO, ButtonType.YES);
|
||||
alert.setTitle("Update Aliases");
|
||||
alert.setHeaderText("Rename requires updating aliases for this stream");
|
||||
alert.setContentText("Do you want to update aliases to new stream name?");
|
||||
alert.initOwner(((Node)getSaveButton()).getScene().getWindow());
|
||||
|
||||
//Workaround for JavaFX KDE on Linux bug in FX 10/11: https://bugs.openjdk.java.net/browse/JDK-8179073
|
||||
alert.setResizable(true);
|
||||
alert.onShownProperty().addListener(e -> {
|
||||
Platform.runLater(() -> alert.setResizable(false));
|
||||
});
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
|
||||
if(result.get() == ButtonType.YES)
|
||||
{
|
||||
mPlaylistManager.getAliasModel().updateBroadcastChannel(previousName, updatedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
public abstract BroadcastServerType getBroadcastServerType();
|
||||
|
||||
protected abstract GridPane getEditorPane();
|
||||
|
||||
protected TextField getFormatField()
|
||||
{
|
||||
if(mFormatField == null)
|
||||
{
|
||||
mFormatField = new TextField();
|
||||
mFormatField.setDisable(true);
|
||||
}
|
||||
|
||||
return mFormatField;
|
||||
}
|
||||
|
||||
protected TextField getNameTextField()
|
||||
{
|
||||
if(mNameTextField == null)
|
||||
{
|
||||
mNameTextField = new TextField();
|
||||
mNameTextField.setDisable(true);
|
||||
mNameTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mNameTextField;
|
||||
}
|
||||
|
||||
protected TextField getHostTextField()
|
||||
{
|
||||
if(mHostTextField == null)
|
||||
@@ -287,57 +192,4 @@ public abstract class AbstractStreamEditor<T extends BroadcastConfiguration> ext
|
||||
|
||||
return mMaxAgeTextField;
|
||||
}
|
||||
|
||||
protected ToggleSwitch getEnabledSwitch()
|
||||
{
|
||||
if(mEnabledSwitch == null)
|
||||
{
|
||||
mEnabledSwitch = new ToggleSwitch();
|
||||
mEnabledSwitch.setDisable(true);
|
||||
mEnabledSwitch.selectedProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mEnabledSwitch;
|
||||
}
|
||||
|
||||
private Button getSaveButton()
|
||||
{
|
||||
if(mSaveButton == null)
|
||||
{
|
||||
mSaveButton = new Button("Save");
|
||||
mSaveButton.setDisable(true);
|
||||
mSaveButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mSaveButton.setOnAction(event -> save());
|
||||
mSaveButton.disableProperty().bind(modifiedProperty().not());
|
||||
}
|
||||
|
||||
return mSaveButton;
|
||||
}
|
||||
|
||||
private Button getResetButton()
|
||||
{
|
||||
if(mResetButton == null)
|
||||
{
|
||||
mResetButton = new Button("Reset");
|
||||
mResetButton.setDisable(true);
|
||||
mResetButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mResetButton.setOnAction(event -> setItem(getItem()));
|
||||
mResetButton.disableProperty().bind(modifiedProperty().not());
|
||||
}
|
||||
|
||||
return mResetButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple string change listener that sets the editor modified flag to true any time text fields are edited.
|
||||
*/
|
||||
public class EditorModificationListener implements ChangeListener<String>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue)
|
||||
{
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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 <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Broadcastify calls API configuration editor
|
||||
*/
|
||||
public class BroadcastifyCallEditor extends AbstractBroadcastEditor<BroadcastifyCallConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyCallEditor.class);
|
||||
private IntegerTextField mSystemIdTextField;
|
||||
private Button mTestButton;
|
||||
private IntegerTextField mMaxAgeTextField;
|
||||
private GridPane mEditorPane;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for accessing the broadcast model
|
||||
*/
|
||||
public BroadcastifyCallEditor(PlaylistManager playlistManager)
|
||||
{
|
||||
super(playlistManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastifyCallConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
getSystemIdTextField().setDisable(item == null);
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
getSystemIdTextField().set(item.getSystemID());
|
||||
}
|
||||
else
|
||||
{
|
||||
getSystemIdTextField().set(0);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
int systemID = getSystemIdTextField().get() != null ? getSystemIdTextField().get() : 0;
|
||||
getItem().setSystemID(systemID);
|
||||
}
|
||||
|
||||
super.save();
|
||||
}
|
||||
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.BROADCASTIFY_CALL;
|
||||
}
|
||||
|
||||
protected GridPane getEditorPane()
|
||||
{
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setPadding(new Insets(10, 5, 10,10));
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(5);
|
||||
|
||||
int row = 0;
|
||||
|
||||
Label formatLabel = new Label("Format");
|
||||
GridPane.setHalignment(formatLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(formatLabel, 0, row);
|
||||
mEditorPane.getChildren().add(formatLabel);
|
||||
|
||||
GridPane.setConstraints(getFormatField(), 1, row);
|
||||
mEditorPane.getChildren().add(getFormatField());
|
||||
|
||||
Label enabledLabel = new Label("Enabled");
|
||||
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(enabledLabel, 2, row);
|
||||
mEditorPane.getChildren().add(enabledLabel);
|
||||
|
||||
GridPane.setConstraints(getEnabledSwitch(), 3, row);
|
||||
mEditorPane.getChildren().add(getEnabledSwitch());
|
||||
|
||||
Label systemLabel = new Label("Name");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getNameTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getNameTextField());
|
||||
|
||||
Label systemIdLabel = new Label("System ID");
|
||||
GridPane.setHalignment(systemIdLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemIdLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemIdLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemIdTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getSystemIdTextField());
|
||||
|
||||
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
|
||||
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(maxAgeLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(maxAgeLabel);
|
||||
|
||||
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getMaxAgeTextField());
|
||||
|
||||
GridPane.setConstraints(getTestButton(), 1, ++row);
|
||||
mEditorPane.getChildren().add(getTestButton());
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
protected IntegerTextField getMaxAgeTextField()
|
||||
{
|
||||
if(mMaxAgeTextField == null)
|
||||
{
|
||||
mMaxAgeTextField = new IntegerTextField();
|
||||
mMaxAgeTextField.setDisable(true);
|
||||
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mMaxAgeTextField;
|
||||
}
|
||||
|
||||
private IntegerTextField getSystemIdTextField()
|
||||
{
|
||||
if(mSystemIdTextField == null)
|
||||
{
|
||||
mSystemIdTextField = new IntegerTextField();
|
||||
mSystemIdTextField.setDisable(true);
|
||||
mSystemIdTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mSystemIdTextField;
|
||||
}
|
||||
|
||||
private Button getTestButton()
|
||||
{
|
||||
if(mTestButton == null)
|
||||
{
|
||||
mTestButton = new Button("Test Connection");
|
||||
mTestButton.setOnAction(event -> {
|
||||
int systemID = getSystemIdTextField().get();
|
||||
|
||||
if(systemID < 1)
|
||||
{
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, "Please enter a non-zero System ID",
|
||||
ButtonType.OK);
|
||||
alert.setTitle("Test Connection");
|
||||
alert.setHeaderText("A valid System ID is required");
|
||||
alert.initOwner(getTestButton().getScene().getWindow());
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
|
||||
BroadcastifyCallConfiguration configToTest = new BroadcastifyCallConfiguration();
|
||||
configToTest.setSystemID(systemID);
|
||||
String result = BroadcastifyCallBroadcaster.testConnection(configToTest);
|
||||
|
||||
if(result == null)
|
||||
{
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Test successful.",
|
||||
ButtonType.OK);
|
||||
alert.setTitle("Test Connection");
|
||||
alert.setHeaderText("Success!");
|
||||
alert.initOwner(getTestButton().getScene().getWindow());
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, "Error: " + result,
|
||||
ButtonType.OK);
|
||||
alert.setTitle("Test Connection");
|
||||
alert.setHeaderText("Test Failed.");
|
||||
alert.initOwner(getTestButton().getScene().getWindow());
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mTestButton;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ package io.github.dsheirer.gui.playlist.streaming;
|
||||
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.geometry.HPos;
|
||||
@@ -36,7 +36,7 @@ import javafx.scene.layout.GridPane;
|
||||
/**
|
||||
* Broadcastify streaming configuration editor
|
||||
*/
|
||||
public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyConfiguration>
|
||||
public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyFeedConfiguration>
|
||||
{
|
||||
private GridPane mEditorPane;
|
||||
private TextField mMountPointTextField;
|
||||
@@ -48,7 +48,7 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyC
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastifyConfiguration item)
|
||||
public void setItem(BroadcastifyFeedConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
@@ -72,13 +72,13 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyC
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
super.save();
|
||||
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setFeedID(getFeedIdTextField().get());
|
||||
getItem().setMountPoint(getMountPointTextField().getText());
|
||||
}
|
||||
|
||||
super.save();
|
||||
}
|
||||
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
@@ -100,7 +100,6 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyC
|
||||
GridPane.setConstraints(formatLabel, 0, 0);
|
||||
mEditorPane.getChildren().add(formatLabel);
|
||||
|
||||
getFormatField().setText(getBroadcastServerType().toString());
|
||||
GridPane.setConstraints(getFormatField(), 1, 0);
|
||||
mEditorPane.getChildren().add(getFormatField());
|
||||
|
||||
|
||||
@@ -82,8 +82,6 @@ public abstract class IcecastStreamEditor extends AbstractStreamEditor<IcecastCo
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
super.save();
|
||||
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setMountPoint(getMountPointTextField().getText());
|
||||
@@ -93,7 +91,7 @@ public abstract class IcecastStreamEditor extends AbstractStreamEditor<IcecastCo
|
||||
getItem().setURL(getURLTextField().getText());
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
super.save();
|
||||
}
|
||||
|
||||
protected GridPane getEditorPane()
|
||||
|
||||
@@ -82,8 +82,6 @@ public class ShoutcastV1StreamEditor extends AbstractStreamEditor<ShoutcastV1Con
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
super.save();
|
||||
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setDescription(getDescriptionTextField().getText());
|
||||
@@ -91,7 +89,7 @@ public class ShoutcastV1StreamEditor extends AbstractStreamEditor<ShoutcastV1Con
|
||||
getItem().setPublic(getPublicToggleSwitch().isSelected());
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
super.save();
|
||||
}
|
||||
|
||||
protected GridPane getEditorPane()
|
||||
|
||||
@@ -91,8 +91,6 @@ public class ShoutcastV2StreamEditor extends AbstractStreamEditor<ShoutcastV2Con
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
super.save();
|
||||
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setURL(getUrlTextField().getText());
|
||||
@@ -102,7 +100,7 @@ public class ShoutcastV2StreamEditor extends AbstractStreamEditor<ShoutcastV2Con
|
||||
getItem().setStreamID(getStreamIdTextField().get());
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
super.save();
|
||||
}
|
||||
|
||||
protected GridPane getEditorPane()
|
||||
|
||||
@@ -35,12 +35,14 @@ public class StreamEditorFactory
|
||||
* @param broadcastServerType to edit
|
||||
* @return editor or the default unknown editor
|
||||
*/
|
||||
public static AbstractStreamEditor getEditor(BroadcastServerType broadcastServerType, PlaylistManager playlistManager)
|
||||
public static AbstractBroadcastEditor getEditor(BroadcastServerType broadcastServerType, PlaylistManager playlistManager)
|
||||
{
|
||||
switch(broadcastServerType)
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
return new BroadcastifyStreamEditor(playlistManager);
|
||||
case BROADCASTIFY_CALL:
|
||||
return new BroadcastifyCallEditor(playlistManager);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPStreamEditor(playlistManager);
|
||||
case ICECAST_TCP:
|
||||
|
||||
@@ -27,15 +27,13 @@ import io.github.dsheirer.audio.broadcast.BroadcastFactory;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.ConfiguredBroadcast;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -64,7 +62,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -76,7 +74,7 @@ public class StreamingEditor extends SplitPane
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(StreamingEditor.class);
|
||||
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private final PlaylistManager mPlaylistManager;
|
||||
private TableView<ConfiguredBroadcast> mConfiguredBroadcastTableView;
|
||||
private MenuButton mNewButton;
|
||||
private Button mDeleteButton;
|
||||
@@ -84,13 +82,13 @@ public class StreamingEditor extends SplitPane
|
||||
private Tab mConfigurationTab;
|
||||
private Tab mAliasTab;
|
||||
private Label mRadioReferenceLoginLabel;
|
||||
private AbstractStreamEditor mCurrentEditor;
|
||||
private UnknownStreamEditor mUnknownEditor;
|
||||
private Map<BroadcastServerType, AbstractStreamEditor> mEditorMap = new HashMap();
|
||||
private List<UserFeedBroadcast> mBroadcastifyFeeds = new ArrayList<>();
|
||||
private AbstractBroadcastEditor<?> mCurrentEditor;
|
||||
private final UnknownStreamEditor mUnknownEditor;
|
||||
private Map<BroadcastServerType, AbstractBroadcastEditor<?>> mEditorMap = new EnumMap<>(BroadcastServerType.class);
|
||||
private final List<UserFeedBroadcast> mBroadcastifyFeeds = new ArrayList<>();
|
||||
private ScrollPane mEditorScrollPane;
|
||||
private StreamAliasSelectionEditor mStreamAliasSelectionEditor;
|
||||
private StreamConfigurationEditorModificationListener mStreamConfigurationEditorModificationListener =
|
||||
private final StreamConfigurationEditorModificationListener mStreamConfigurationEditorModificationListener =
|
||||
new StreamConfigurationEditorModificationListener();
|
||||
|
||||
/**
|
||||
@@ -124,7 +122,7 @@ public class StreamingEditor extends SplitPane
|
||||
getItems().addAll(editorBox, getTabPane());
|
||||
}
|
||||
|
||||
private void setEditor(AbstractStreamEditor<?> editor)
|
||||
private void setEditor(AbstractBroadcastEditor<?> editor)
|
||||
{
|
||||
if(editor != getCurrentEditor())
|
||||
{
|
||||
@@ -167,20 +165,17 @@ public class StreamingEditor extends SplitPane
|
||||
alert.setTitle("Save Changes");
|
||||
alert.setHeaderText("Streaming configuration has been modified");
|
||||
alert.setContentText("Do you want to save these changes?");
|
||||
alert.initOwner(((Node)getNewButton()).getScene().getWindow());
|
||||
alert.initOwner((getNewButton()).getScene().getWindow());
|
||||
|
||||
//Workaround for JavaFX KDE on Linux bug in FX 10/11: https://bugs.openjdk.java.net/browse/JDK-8179073
|
||||
alert.setResizable(true);
|
||||
alert.onShownProperty().addListener(e -> {
|
||||
Platform.runLater(() -> alert.setResizable(false));
|
||||
alert.onShownProperty().addListener(e -> Platform.runLater(() -> alert.setResizable(false)));
|
||||
alert.showAndWait().ifPresent(buttonType -> {
|
||||
if(buttonType == ButtonType.YES)
|
||||
{
|
||||
getCurrentEditor().save();
|
||||
}
|
||||
});
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
|
||||
if(result.get() == ButtonType.YES)
|
||||
{
|
||||
getCurrentEditor().save();
|
||||
}
|
||||
}
|
||||
|
||||
getDeleteButton().setDisable(configuredBroadcast == null);
|
||||
@@ -203,7 +198,7 @@ public class StreamingEditor extends SplitPane
|
||||
|
||||
if(editorType == null || editorType != configType)
|
||||
{
|
||||
AbstractStreamEditor editor = mEditorMap.get(configType);
|
||||
AbstractBroadcastEditor editor = mEditorMap.get(configType);
|
||||
|
||||
if(editor == null)
|
||||
{
|
||||
@@ -280,7 +275,7 @@ public class StreamingEditor extends SplitPane
|
||||
return mRadioReferenceLoginLabel;
|
||||
}
|
||||
|
||||
private AbstractStreamEditor getCurrentEditor()
|
||||
private AbstractBroadcastEditor getCurrentEditor()
|
||||
{
|
||||
if(mCurrentEditor == null)
|
||||
{
|
||||
@@ -455,20 +450,15 @@ public class StreamingEditor extends SplitPane
|
||||
public CreateBroadcastifyMenuItem(UserFeedBroadcast userFeedBroadcast)
|
||||
{
|
||||
mUserFeedBroadcast = userFeedBroadcast;
|
||||
setText("Broadcastify: " + mUserFeedBroadcast.getDescription());
|
||||
setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
BroadcastConfiguration configuration = BroadcastifyConfiguration.from(mUserFeedBroadcast);
|
||||
setText("Broadcastify Feed: " + mUserFeedBroadcast.getDescription());
|
||||
setOnAction(event -> {
|
||||
BroadcastConfiguration configuration = BroadcastifyFeedConfiguration.from(mUserFeedBroadcast);
|
||||
|
||||
if(configuration != null)
|
||||
{
|
||||
ConfiguredBroadcast configuredBroadcast = mPlaylistManager.getBroadcastModel()
|
||||
.addBroadcastConfiguration(configuration);
|
||||
getConfiguredBroadcastTableView().getSelectionModel().select(configuredBroadcast);
|
||||
}
|
||||
if(configuration != null)
|
||||
{
|
||||
ConfiguredBroadcast configuredBroadcast = mPlaylistManager.getBroadcastModel()
|
||||
.addBroadcastConfiguration(configuration);
|
||||
getConfiguredBroadcastTableView().getSelectionModel().select(configuredBroadcast);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -485,16 +475,15 @@ public class StreamingEditor extends SplitPane
|
||||
{
|
||||
setText(type.toString());
|
||||
mBroadcastServerType = type;
|
||||
setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
BroadcastConfiguration config = BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
|
||||
ConfiguredBroadcast configuredBroadcast = mPlaylistManager.getBroadcastModel()
|
||||
.addBroadcastConfiguration(config);
|
||||
getConfiguredBroadcastTableView().getSelectionModel().select(configuredBroadcast);
|
||||
}
|
||||
|
||||
//Temporary disable until we receive a production API key for broadcastify calls api
|
||||
setDisable(mBroadcastServerType == BroadcastServerType.BROADCASTIFY_CALL);
|
||||
|
||||
setOnAction(event -> {
|
||||
BroadcastConfiguration config = BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
|
||||
ConfiguredBroadcast configuredBroadcast = mPlaylistManager.getBroadcastModel()
|
||||
.addBroadcastConfiguration(config);
|
||||
getConfiguredBroadcastTableView().getSelectionModel().select(configuredBroadcast);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user