#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:
Denny Sheirer
2020-05-02 10:29:45 +00:00
committed by GitHub
parent cc52ae5bd7
commit 379461e649
30 changed files with 1883 additions and 522 deletions

2
.idea/misc.xml generated
View File

@@ -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>

View File

@@ -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'

View File

@@ -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++;
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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:

View File

@@ -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);
}
}
}

View File

@@ -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"),

View File

@@ -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.
*/

View File

@@ -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!");
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:

View File

@@ -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);
});
}
}