mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 17:17:29 -05:00
Updates pull request from Roman Tsypuk to convert the build system from
ant to gradle. Removes linux and windows application launch scripts and produces an executable jar with all resources included.
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.audio.convert.ISilenceGenerator;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract class AudioBroadcaster implements Listener<AudioRecording>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioBroadcaster.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 BroadcastState mBroadcastState = BroadcastState.READY;
|
||||
|
||||
private int mStreamedAudioCount = 0;
|
||||
private int mAgedOffAudioCount = 0;
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
private long mDelay;
|
||||
private long mMaximumRecordingAge;
|
||||
private AtomicBoolean mStreaming = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* AudioBroadcaster for streaming audio recordings to a remote streaming audio server. Audio recordings are
|
||||
* generated by an internal StreamManager that converts an inbound stream of AudioPackets into a recording of the
|
||||
* desired audio format (e.g. MP3) and nominates the recording to an internal recording queue for streaming. The
|
||||
* broadcaster supports receiving audio packets from multiple audio sources. Each audio packet's internal audio
|
||||
* metadata source string is used to reassemble each packet stream. Recordings are capped at 30 seconds length.
|
||||
* If a source audio packet stream exceeds 30 seconds in length, it will be chunked into 30 second recordings.
|
||||
*
|
||||
* This broadcaster supports a time delay setting for delaying broadcast of audio recordings. The delay setting is
|
||||
* defined in the broadcast configuration. When this delay is greater than zero, the recording will remain in the
|
||||
* audio broadcaster queue until the recording start time + delay elapses. Audio recordings are processed in a FIFO
|
||||
* manner.
|
||||
*
|
||||
* Use the start() and stop() methods to connect to/disconnect from the remote server. Audio recordings will be
|
||||
* streamed to the remote server when available. One second silence frames will be broadcast to the server when
|
||||
* there are no recordings available, in order to maintain a connection with the remote server. Any audio packet
|
||||
* streams received while the broadcaster is stopped will be ignored.
|
||||
*
|
||||
* 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)
|
||||
{
|
||||
mBroadcastConfiguration = broadcastConfiguration;
|
||||
mDelay = mBroadcastConfiguration.getDelay();
|
||||
mMaximumRecordingAge = mBroadcastConfiguration.getMaximumRecordingAge();
|
||||
mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast binary audio data frames or sequences.
|
||||
*/
|
||||
protected abstract void broadcastAudio(byte[] audio);
|
||||
|
||||
/**
|
||||
* Protocol-specific metadata updater
|
||||
*/
|
||||
protected abstract IBroadcastMetadataUpdater getMetadataUpdater();
|
||||
|
||||
/**
|
||||
* Broadcasts the next song's audio metadata prior to streaming the next song.
|
||||
*
|
||||
* @param metadata for the next recording that will be streamed
|
||||
*/
|
||||
protected void broadcastMetadata(Metadata metadata)
|
||||
{
|
||||
IBroadcastMetadataUpdater metadataUpdater = getMetadataUpdater();
|
||||
|
||||
if(metadataUpdater != null)
|
||||
{
|
||||
metadataUpdater.update(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the broadcaster from the remote server for a reset or final stop.
|
||||
*/
|
||||
protected abstract void disconnect();
|
||||
|
||||
/**
|
||||
* Connects to the remote server specified by the broadcast configuration and starts audio streaming.
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mStreaming.compareAndSet(false, true))
|
||||
{
|
||||
if(mRecordingQueueProcessorFuture == null)
|
||||
{
|
||||
mRecordingQueueProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mRecordingQueueProcessor,
|
||||
0, PROCESSOR_RUN_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the remote server.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mStreaming.compareAndSet(true, false))
|
||||
{
|
||||
if(mRecordingQueueProcessorFuture != null)
|
||||
{
|
||||
mRecordingQueueProcessorFuture.cancel(true);
|
||||
mRecordingQueueProcessorFuture = null;
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream name for the broadcast configuration for this broadcaster
|
||||
*
|
||||
* @return stream name or null
|
||||
*/
|
||||
public String getStreamName()
|
||||
{
|
||||
BroadcastConfiguration config = getBroadcastConfiguration();
|
||||
|
||||
if(config != null)
|
||||
{
|
||||
return config.getName();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Size of recording queue for recordings awaiting streaming
|
||||
*/
|
||||
public int getQueueSize()
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param recording to queue for broadcasting
|
||||
*/
|
||||
public void receive(AudioRecording recording)
|
||||
{
|
||||
if(connected())
|
||||
{
|
||||
mAudioRecordingQueue.offer(recording);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if(mBroadcastState != state)
|
||||
{
|
||||
mLog.info("[" + getStreamName() + "] status: " + state);
|
||||
mBroadcastState = state;
|
||||
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
||||
|
||||
if(mBroadcastState.isErrorState())
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
if(!connected())
|
||||
{
|
||||
//Remove all pending audio recordings
|
||||
while(!mAudioRecordingQueue.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
AudioRecording recording = mAudioRecordingQueue.remove();
|
||||
recording.removePendingReplay();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current state of the broadcastAudio connection
|
||||
*/
|
||||
public BroadcastState getBroadcastState()
|
||||
{
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the broadcaster is currently connected to the remote server
|
||||
*/
|
||||
protected boolean connected()
|
||||
{
|
||||
return getBroadcastState() == BroadcastState.CONNECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this broadcaster can connect and is not currently in an error state or a connected state.
|
||||
*/
|
||||
public boolean canConnect()
|
||||
{
|
||||
BroadcastState state = getBroadcastState();
|
||||
|
||||
return state != BroadcastState.CONNECTED && !state.isErrorState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current broadcast state is an error state, meaning that it cannot recover or connect using the
|
||||
* current configuration.
|
||||
*/
|
||||
protected boolean isErrorState()
|
||||
{
|
||||
return getBroadcastState().isErrorState();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Audio recording queue processor. Fetches recordings from the queue and chunks the recording byte content
|
||||
* to subclass implementations for broadcast in the appropriate manner.
|
||||
*/
|
||||
public class RecordingQueueProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
private ByteArrayInputStream mInputStream;
|
||||
private long mFinalSilencePadding = 0;
|
||||
private int mBytesStreamedActual = 0;
|
||||
private int mBytesStreamedRequired = 0;
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(mProcessing.compareAndSet(false, true))
|
||||
{
|
||||
try
|
||||
{
|
||||
if(mInputStream == null || mInputStream.available() <= 0)
|
||||
{
|
||||
if(mFinalSilencePadding > 0)
|
||||
{
|
||||
broadcastAudio(mSilenceGenerator.generate(mFinalSilencePadding));
|
||||
mFinalSilencePadding = 0;
|
||||
}
|
||||
|
||||
nextRecording();
|
||||
}
|
||||
|
||||
if(mInputStream != null)
|
||||
{
|
||||
//We need to stream at 13.888 fps (144 byte frame) to achieve 2000 Bps or 16 kbps
|
||||
mBytesStreamedRequired += 2000; //2000 bytes per second for 16 kbps data rate
|
||||
int bytesToStream = mBytesStreamedRequired - mBytesStreamedActual;
|
||||
|
||||
//Trim length to whole-frame intervals (144 byte frame)
|
||||
bytesToStream -= (bytesToStream % 144);
|
||||
|
||||
int length = Math.min(bytesToStream, mInputStream.available());
|
||||
|
||||
byte[] audio = new byte[length];
|
||||
|
||||
try
|
||||
{
|
||||
mBytesStreamedActual += mInputStream.read(audio);
|
||||
|
||||
broadcastAudio(audio);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error reading from in-memory audio recording input stream", ioe);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
broadcastAudio(mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error while processing audio streaming queue", e);
|
||||
}
|
||||
|
||||
mProcessing.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the next recording for broadcast
|
||||
*/
|
||||
private void nextRecording()
|
||||
{
|
||||
mBytesStreamedActual = 0;
|
||||
mBytesStreamedRequired = 0;
|
||||
|
||||
boolean metadataUpdateRequired = false;
|
||||
|
||||
if(mInputStream != null)
|
||||
{
|
||||
mStreamedAudioCount++;
|
||||
broadcast(new BroadcastEvent(AudioBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||
metadataUpdateRequired = true;
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
|
||||
//Peek at the next recording but don't remove it from the queue yet, so we can inspect the start time for
|
||||
//age limits and/or delay elapsed
|
||||
AudioRecording nextRecording = mAudioRecordingQueue.peek();
|
||||
|
||||
//Purge any recordings that have exceeded maximum recording age limit
|
||||
while(nextRecording != null &&
|
||||
(nextRecording.getStartTime() + mDelay + mMaximumRecordingAge) < java.lang.System.currentTimeMillis())
|
||||
{
|
||||
nextRecording = mAudioRecordingQueue.remove();
|
||||
nextRecording.removePendingReplay();
|
||||
mAgedOffAudioCount++;
|
||||
broadcast(new BroadcastEvent(AudioBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
|
||||
nextRecording = mAudioRecordingQueue.peek();
|
||||
}
|
||||
|
||||
if(nextRecording != null && nextRecording.getStartTime() + mDelay <= System.currentTimeMillis())
|
||||
{
|
||||
nextRecording = mAudioRecordingQueue.remove();
|
||||
|
||||
try
|
||||
{
|
||||
if(Files.exists(nextRecording.getPath()))
|
||||
{
|
||||
byte[] audio = Files.readAllBytes(nextRecording.getPath());
|
||||
|
||||
if(audio != null && audio.length > 0)
|
||||
{
|
||||
mInputStream = new ByteArrayInputStream(audio);
|
||||
|
||||
mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS -
|
||||
(nextRecording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);
|
||||
|
||||
while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
|
||||
{
|
||||
mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS;
|
||||
}
|
||||
|
||||
if(connected())
|
||||
{
|
||||
broadcastMetadata(nextRecording.getMetadata());
|
||||
}
|
||||
|
||||
metadataUpdateRequired = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] error reading temporary audio " +
|
||||
"stream recording [" + nextRecording.getPath().toString() + "] - skipping recording - ", ioe);
|
||||
|
||||
mInputStream = null;
|
||||
metadataUpdateRequired = false;
|
||||
}
|
||||
|
||||
nextRecording.removePendingReplay();
|
||||
|
||||
broadcast(new BroadcastEvent(AudioBroadcaster.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
|
||||
if(metadataUpdateRequired && connected())
|
||||
{
|
||||
broadcastMetadata(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.channel.metadata.Metadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AudioRecording implements Comparable<AudioRecording>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
|
||||
|
||||
private Path mPath;
|
||||
private long mStartTime;
|
||||
private long mRecordingLength;
|
||||
private AtomicInteger mPendingReplayCount = new AtomicInteger();
|
||||
private Metadata mMetadata;
|
||||
|
||||
/**
|
||||
* Audio recording that is ready to be streamed
|
||||
*
|
||||
* @param path to the audio recording file
|
||||
* @param metadata associated with the recording
|
||||
* @param start time of recording in milliseconds since epoch
|
||||
* @param recordingLength in milliseconds
|
||||
*/
|
||||
public AudioRecording(Path path, Metadata metadata, long start, long recordingLength)
|
||||
{
|
||||
mPath = path;
|
||||
mMetadata = metadata;
|
||||
mStartTime = start;
|
||||
mRecordingLength = recordingLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to the completed audio recording
|
||||
*/
|
||||
public Path getPath()
|
||||
{
|
||||
return mPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional audio metadata for the recording.
|
||||
*/
|
||||
public Metadata getMetadata()
|
||||
{
|
||||
return mMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording start time in milliseconds since epoch
|
||||
*/
|
||||
public long getStartTime()
|
||||
{
|
||||
return mStartTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recording length in milliseconds
|
||||
*/
|
||||
public long getRecordingLength()
|
||||
{
|
||||
return mRecordingLength;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements comparable for sorting recordings based on start time in ascending order
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(AudioRecording otherRecording)
|
||||
{
|
||||
return Long.compare(getStartTime(), otherRecording.getStartTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the count of pending replays.
|
||||
*/
|
||||
public void addPendingReplay()
|
||||
{
|
||||
mPendingReplayCount.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the count of pending replays.
|
||||
*/
|
||||
public void removePendingReplay()
|
||||
{
|
||||
mPendingReplayCount.decrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there are any remaining pending replays. Once the pending replay count is less than or equal to
|
||||
* zero, the recording can be deleted.
|
||||
*/
|
||||
public boolean hasPendingReplays()
|
||||
{
|
||||
return mPendingReplayCount.get() > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
@XmlSeeAlso({BroadcastifyConfiguration.class,
|
||||
IcecastTCPConfiguration.class,
|
||||
IcecastHTTPConfiguration.class,
|
||||
ShoutcastV1Configuration.class,
|
||||
ShoutcastV2Configuration.class})
|
||||
@XmlRootElement(name = "stream")
|
||||
public abstract class BroadcastConfiguration
|
||||
{
|
||||
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
|
||||
private String mName;
|
||||
private String mHost;
|
||||
private int mPort;
|
||||
private String mPassword;
|
||||
private long mDelay;
|
||||
private long mMaximumRecordingAge = 10 * 60 * 1000; //10 minutes default
|
||||
private boolean mEnabled = true;
|
||||
|
||||
public BroadcastConfiguration()
|
||||
{
|
||||
//No-arg constructor required for JAXB
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy/clone of the configuration
|
||||
*/
|
||||
public abstract BroadcastConfiguration copyOf();
|
||||
|
||||
/**
|
||||
* Broadcast audio streaming configuration. Describes the streaming server and configuration details.
|
||||
*/
|
||||
public BroadcastConfiguration(BroadcastFormat format)
|
||||
{
|
||||
mBroadcastFormat = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast server type
|
||||
*/
|
||||
@XmlAttribute(name = "type")
|
||||
public abstract BroadcastServerType getBroadcastServerType();
|
||||
|
||||
/**
|
||||
* Name identifying this broadcastAudio configuration.
|
||||
*/
|
||||
@XmlAttribute(name = "name")
|
||||
public String getName()
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the name name that identifies/describes this configuration
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
public void setName(String name)
|
||||
{
|
||||
mName = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration has a name value
|
||||
*/
|
||||
public boolean hasName()
|
||||
{
|
||||
return mName != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* BROADCAST server host name.
|
||||
*/
|
||||
@XmlAttribute(name = "host")
|
||||
public String getHost()
|
||||
{
|
||||
return mHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Host name or IP address of the streaming server
|
||||
*
|
||||
* @param host
|
||||
*/
|
||||
public void setHost(String host)
|
||||
{
|
||||
mHost = host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration has a host value
|
||||
*/
|
||||
public boolean hasHost()
|
||||
{
|
||||
return mHost != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* BROADCAST server port number;
|
||||
*/
|
||||
@XmlAttribute(name = "port")
|
||||
public int getPort()
|
||||
{
|
||||
return mPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Port number of the streaming server to where the broadcastAudio will be directed.
|
||||
*
|
||||
* @param port
|
||||
*/
|
||||
public void setPort(int port)
|
||||
{
|
||||
mPort = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration contains a port value
|
||||
*/
|
||||
public boolean hasPort()
|
||||
{
|
||||
return mPort > 0;
|
||||
}
|
||||
|
||||
public SocketAddress getAddress()
|
||||
{
|
||||
return new InetSocketAddress(getHost(), getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* Password to authenticate with the streaming server.
|
||||
*/
|
||||
@XmlAttribute(name = "password")
|
||||
public String getPassword()
|
||||
{
|
||||
return mPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the password used to authenticate the broadcastAudio broadcastAudio user to the streaming server
|
||||
*
|
||||
* @param password
|
||||
*/
|
||||
public void setPassword(String password)
|
||||
{
|
||||
mPassword = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration has a password value
|
||||
*/
|
||||
public boolean hasPassword()
|
||||
{
|
||||
return mPassword != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio broadcastAudio content type (e.g. mp3)
|
||||
*/
|
||||
@XmlAttribute(name = "format")
|
||||
public BroadcastFormat getBroadcastFormat()
|
||||
{
|
||||
return mBroadcastFormat;
|
||||
}
|
||||
|
||||
public void setBroadcastFormat(BroadcastFormat format)
|
||||
{
|
||||
mBroadcastFormat = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio broadcastAudio delay from recording start until the audio file is broadcastAudio to the server.
|
||||
*/
|
||||
@XmlAttribute(name = "delay")
|
||||
public long getDelay()
|
||||
{
|
||||
return mDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* User specified delay in milli-seconds from audio call start until when the audio is broadcastAudio to the server.
|
||||
* @param delay in milliseconds
|
||||
*/
|
||||
public void setDelay(long delay)
|
||||
{
|
||||
mDelay = delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets maximum recording age which determines the maximum amount of elapsed time that a recording can set in the
|
||||
* queue awaiting streaming before it is purged from the queue. This value is in addition to the delay setting.
|
||||
* @return age in milliseconds
|
||||
*/
|
||||
@XmlAttribute(name = "maximum_recording_age")
|
||||
public long getMaximumRecordingAge()
|
||||
{
|
||||
return mMaximumRecordingAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets maximum recording age which determines the maximum amount of elapsed time that a recording can set in the
|
||||
* queue awaiting streaming before it is purged from the queue. This value is in addition to the delay setting.
|
||||
* @param age in milliseconds
|
||||
*/
|
||||
public void setMaximumRecordingAge(long age)
|
||||
{
|
||||
mMaximumRecordingAge = age;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this broadcaster is enable, meaning that it will automatically connect on startup.
|
||||
*/
|
||||
@XmlAttribute(name = "enabled")
|
||||
public boolean isEnabled()
|
||||
{
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled)
|
||||
{
|
||||
mEnabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(o instanceof BroadcastConfiguration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BroadcastConfiguration that = (BroadcastConfiguration) o;
|
||||
|
||||
if (getBroadcastFormat() != that.getBroadcastFormat())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return getBroadcastServerType() == that.getBroadcastServerType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int result = getBroadcastFormat().hashCode();
|
||||
result = 31 * result + (getName() != null ? getName().hashCode() : 0);
|
||||
result = 31 * result + getBroadcastServerType().hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this configuration is valid. A minimal check will ensure that it contains at least a hostname and
|
||||
* port number.
|
||||
*/
|
||||
@XmlTransient
|
||||
public boolean isValid()
|
||||
{
|
||||
return mHost != null && mPort > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.gui.editor.DocumentListenerEditor;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public abstract class BroadcastConfigurationEditor extends DocumentListenerEditor<BroadcastConfiguration>
|
||||
{
|
||||
protected IconManager mIconManager;
|
||||
protected BroadcastModel mBroadcastModel;
|
||||
protected AliasModel mAliasModel;
|
||||
|
||||
public BroadcastConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
mAliasModel = aliasModel;
|
||||
mIconManager = iconManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the configuration with the new name and prompts the user to update any aliases that have a broadcast
|
||||
* channel alias id with the old name to update to the new name.
|
||||
*
|
||||
* @param broadcastConfiguration to assign a new channel name
|
||||
* @param newName to assign to the broadcast configuration. Note: assumes that validateConfiguration() has been
|
||||
* invoked to verify that newName is non-null and non-empty
|
||||
*/
|
||||
protected void updateConfigurationName(BroadcastConfiguration broadcastConfiguration, String newName)
|
||||
{
|
||||
String previousName = broadcastConfiguration.getName();
|
||||
|
||||
if(previousName == null || previousName.isEmpty())
|
||||
{
|
||||
broadcastConfiguration.setName(newName);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!previousName.equals(newName))
|
||||
{
|
||||
mAliasModel.renameBroadcastChannel(previousName, newName);
|
||||
broadcastConfiguration.setName(newName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a text field control for a non-null, non-empty value
|
||||
* @param field to validate
|
||||
* @param title to use for error dialog
|
||||
* @param message to use for error dialog
|
||||
* @return true if field contains a non-null, non-empty value
|
||||
*/
|
||||
protected boolean validateTextField(JTextField field, String title, String message)
|
||||
{
|
||||
String text = field.getText();
|
||||
|
||||
if(text == null || text.isEmpty())
|
||||
{
|
||||
JOptionPane.showMessageDialog(BroadcastConfigurationEditor.this, message, title,
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
field.requestFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the text field control that contains an integer value for non-null, non-empty and within the
|
||||
* specified min/max valid range.
|
||||
* @param field to validate
|
||||
* @param title to use for error dialog
|
||||
* @param message to use for error dialog
|
||||
* @param minValid value
|
||||
* @param maxValid value
|
||||
* @return true if field contains a non-null, non-empty value within the valid min/max range
|
||||
*/
|
||||
protected boolean validateIntegerTextField(JTextField field, String title, String message, int minValid, int maxValid)
|
||||
{
|
||||
if(validateTextField(field, title, message))
|
||||
{
|
||||
String text = field.getText();
|
||||
|
||||
try
|
||||
{
|
||||
int value = Integer.parseInt(text);
|
||||
|
||||
if(minValid <= value && value <= maxValid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(BroadcastConfigurationEditor.this, message, title,
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
field.requestFocus();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
public class BroadcastEvent
|
||||
{
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
private AudioBroadcaster mAudioBroadcaster;
|
||||
private Event mEvent;
|
||||
|
||||
/**
|
||||
* AliasEvent - event describing any changes to an alias
|
||||
*
|
||||
* @param configuration - alias that changed
|
||||
* @param event - change event
|
||||
*/
|
||||
public BroadcastEvent(BroadcastConfiguration configuration, Event event)
|
||||
{
|
||||
mBroadcastConfiguration = configuration;
|
||||
mEvent = event;
|
||||
}
|
||||
|
||||
public BroadcastEvent(AudioBroadcaster audioBroadcaster, Event event)
|
||||
{
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
mEvent = event;
|
||||
}
|
||||
|
||||
public BroadcastConfiguration getBroadcastConfiguration()
|
||||
{
|
||||
return mBroadcastConfiguration;
|
||||
}
|
||||
|
||||
public AudioBroadcaster getAudioBroadcaster()
|
||||
{
|
||||
return mAudioBroadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this event pertains to an audio broadcaster
|
||||
*/
|
||||
public boolean isAudioBroadcasterEvent()
|
||||
{
|
||||
return mAudioBroadcaster != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this event pertains to a broadcast configuration
|
||||
*/
|
||||
public boolean isBroadcastConfigurationEvent()
|
||||
{
|
||||
return mBroadcastConfiguration != null;
|
||||
}
|
||||
|
||||
public Event getEvent()
|
||||
{
|
||||
return mEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel events to describe the specific event
|
||||
*/
|
||||
public enum Event
|
||||
{
|
||||
BROADCASTER_ADD,
|
||||
BROADCASTER_QUEUE_CHANGE,
|
||||
BROADCASTER_STATE_CHANGE,
|
||||
BROADCASTER_STREAMED_COUNT_CHANGE,
|
||||
BROADCASTER_AGED_OFF_COUNT_CHANGE,
|
||||
BROADCASTER_DELETE,
|
||||
|
||||
CONFIGURATION_ADD,
|
||||
CONFIGURATION_CHANGE,
|
||||
CONFIGURATION_DELETE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfigurationEditor;
|
||||
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.v1.ShoutcastV1ConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2ConfigurationEditor;
|
||||
import io.github.dsheirer.audio.convert.IAudioConverter;
|
||||
import io.github.dsheirer.audio.convert.ISilenceGenerator;
|
||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||
import io.github.dsheirer.audio.convert.MP3SilenceGenerator;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EmptyEditor;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.record.AudioRecorder;
|
||||
import io.github.dsheirer.record.mp3.MP3Recorder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class BroadcastFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastFactory.class);
|
||||
|
||||
public static final int MP3_MONO_16_KHZ_BITRATE = 16;
|
||||
public static final boolean MP3_CONSTANT_BITRATE = false;
|
||||
|
||||
/**
|
||||
* Creates an audio streaming broadcaster for the configuration
|
||||
*
|
||||
* @param configuration describing the server and audio types
|
||||
* @return configured broadcaster or null
|
||||
*/
|
||||
public static AudioBroadcaster getBroadcaster(BroadcastConfiguration configuration)
|
||||
{
|
||||
if(configuration != null)
|
||||
{
|
||||
IAudioConverter converter = getAudioConverter(configuration);
|
||||
|
||||
if(converter != null)
|
||||
{
|
||||
switch(configuration.getBroadcastServerType())
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPAudioBroadcaster((BroadcastifyConfiguration) configuration);
|
||||
case ICECAST_TCP:
|
||||
return new IcecastTCPAudioBroadcaster((IcecastTCPConfiguration) configuration);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPAudioBroadcaster((IcecastHTTPConfiguration) configuration);
|
||||
case SHOUTCAST_V1:
|
||||
return new ShoutcastV1AudioBroadcaster((ShoutcastV1Configuration) configuration);
|
||||
case SHOUTCAST_V2:
|
||||
return new ShoutcastV2AudioBroadcaster((ShoutcastV2Configuration) configuration);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio convert to convert from 8 kHz PCM audio to the specified format
|
||||
*
|
||||
* @param configuration containing the requested output audio format
|
||||
* @return audio convert or null
|
||||
*/
|
||||
public static IAudioConverter getAudioConverter(BroadcastConfiguration configuration)
|
||||
{
|
||||
switch(configuration.getBroadcastFormat())
|
||||
{
|
||||
case MP3:
|
||||
return new MP3AudioConverter(MP3_MONO_16_KHZ_BITRATE, MP3_CONSTANT_BITRATE);
|
||||
default:
|
||||
mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a broadcastAudio configuration for the specified server type and format
|
||||
*
|
||||
* @param serverType for the configuration
|
||||
* @param format for the output audio format
|
||||
* @return constructed (empty) configuration
|
||||
*/
|
||||
public static BroadcastConfiguration getConfiguration(BroadcastServerType serverType, BroadcastFormat format)
|
||||
{
|
||||
switch(serverType)
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
return new BroadcastifyConfiguration(format);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPConfiguration(format);
|
||||
case ICECAST_TCP:
|
||||
return new IcecastTCPConfiguration(format);
|
||||
case SHOUTCAST_V1:
|
||||
return new ShoutcastV1Configuration(format);
|
||||
case SHOUTCAST_V2:
|
||||
return new ShoutcastV2Configuration(format);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
mLog.info("Unrecognized broadcastAudio server type: " + serverType.name());
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an editor for the specified broadcastAudio configuration
|
||||
*
|
||||
* @param configuration to modify or view
|
||||
* @param broadcastModel model for broadcastAudio configurations
|
||||
* @return an editor for the specified broadcastAudio configuration
|
||||
*/
|
||||
public static Editor<BroadcastConfiguration> getEditor(BroadcastConfiguration configuration,
|
||||
BroadcastModel broadcastModel,
|
||||
AliasModel aliasModel,
|
||||
IconManager iconManager)
|
||||
{
|
||||
Editor<BroadcastConfiguration> editor;
|
||||
|
||||
switch(configuration.getBroadcastServerType())
|
||||
{
|
||||
case BROADCASTIFY:
|
||||
editor = new BroadcastifyConfigurationEditor(broadcastModel, aliasModel, iconManager);
|
||||
break;
|
||||
case ICECAST_TCP:
|
||||
editor = new IcecastTCPConfigurationEditor(broadcastModel, aliasModel, iconManager);
|
||||
break;
|
||||
case ICECAST_HTTP:
|
||||
editor = new IcecastHTTPConfigurationEditor(broadcastModel, aliasModel, iconManager);
|
||||
break;
|
||||
case SHOUTCAST_V1:
|
||||
editor = new ShoutcastV1ConfigurationEditor(broadcastModel, aliasModel, iconManager);
|
||||
break;
|
||||
case SHOUTCAST_V2:
|
||||
editor = new ShoutcastV2ConfigurationEditor(broadcastModel, aliasModel, iconManager);
|
||||
break;
|
||||
default:
|
||||
editor = new EmptyEditor<BroadcastConfiguration>();
|
||||
break;
|
||||
}
|
||||
|
||||
editor.setItem(configuration);
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
|
||||
*/
|
||||
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
|
||||
{
|
||||
switch(broadcastFormat)
|
||||
{
|
||||
case MP3:
|
||||
return new MP3Recorder(path);
|
||||
default:
|
||||
mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format)
|
||||
{
|
||||
switch(format)
|
||||
{
|
||||
case MP3:
|
||||
return new MP3SilenceGenerator();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized broadcast format [" + format +
|
||||
"] can't create silence generator");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
public enum BroadcastFormat
|
||||
{
|
||||
MP3("audio/mpeg", ".mp3");
|
||||
|
||||
private String mValue;
|
||||
private String mFileExtension;
|
||||
|
||||
private BroadcastFormat(String value, String fileExtension)
|
||||
{
|
||||
mValue = value;
|
||||
mFileExtension = fileExtension;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public String getFileExtension()
|
||||
{
|
||||
return mFileExtension;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,772 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
|
||||
|
||||
public static final String TEMPORARY_STREAM_DIRECTORY = "streaming";
|
||||
public static final String TEMPORARY_STREAM_FILE_SUFFIX = "temporary_streaming_file_";
|
||||
|
||||
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
|
||||
|
||||
public static final int COLUMN_SERVER_ICON = 0;
|
||||
public static final int COLUMN_STREAM_NAME = 1;
|
||||
public static final int COLUMN_BROADCASTER_STATUS = 2;
|
||||
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 String[] COLUMN_NAMES = new String[]
|
||||
{"Streaming", "Name", "Status", "Queued", "Streamed", "Aged Off"};
|
||||
|
||||
private List<BroadcastConfiguration> mBroadcastConfigurations = new CopyOnWriteArrayList<>();
|
||||
private List<AudioRecording> mRecordingQueue = new CopyOnWriteArrayList<>();
|
||||
|
||||
private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
|
||||
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
||||
private IconManager mIconManager;
|
||||
private StreamManager mStreamManager;
|
||||
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
|
||||
|
||||
/**
|
||||
* Model for managing Broadcast configurations and any associated broadcaster instances.
|
||||
*/
|
||||
public BroadcastModel(IconManager iconManager)
|
||||
{
|
||||
mIconManager = iconManager;
|
||||
mStreamManager = new StreamManager(new CompletedRecordingListener(), BroadcastFormat.MP3,
|
||||
SystemProperties.getInstance().getApplicationFolder(TEMPORARY_STREAM_DIRECTORY));
|
||||
mStreamManager.start();
|
||||
|
||||
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
|
||||
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(),
|
||||
15l, 15l, TimeUnit.SECONDS);
|
||||
|
||||
removeOrphanedTemporaryRecordings();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of broadcastAudio configuration names
|
||||
*/
|
||||
public List<String> getBroadcastConfigurationNames()
|
||||
{
|
||||
List<String> names = new ArrayList<>();
|
||||
|
||||
for(BroadcastConfiguration configuration : mBroadcastConfigurations)
|
||||
{
|
||||
names.add(configuration.getName());
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current list of broadcastAudio configurations
|
||||
*/
|
||||
public List<BroadcastConfiguration> getBroadcastConfigurations()
|
||||
{
|
||||
return mBroadcastConfigurations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the list of broadcastAudio configurations to this model
|
||||
*/
|
||||
public void addBroadcastConfigurations(List<BroadcastConfiguration> configurations)
|
||||
{
|
||||
for(BroadcastConfiguration configuration : configurations)
|
||||
{
|
||||
addBroadcastConfiguration(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the broadcastAudio configuration to this model
|
||||
*/
|
||||
public void addBroadcastConfiguration(BroadcastConfiguration configuration)
|
||||
{
|
||||
if(configuration != null)
|
||||
{
|
||||
ensureUniqueName(configuration);
|
||||
|
||||
if(!mBroadcastConfigurations.contains(configuration))
|
||||
{
|
||||
mBroadcastConfigurations.add(configuration);
|
||||
|
||||
int index = mBroadcastConfigurations.size() - 1;
|
||||
|
||||
fireTableRowsInserted(index, index);
|
||||
|
||||
mBroadcastConfigurationMap.put(configuration.getName(), configuration);
|
||||
|
||||
process(new BroadcastEvent(configuration, BroadcastEvent.Event.CONFIGURATION_ADD));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param configuration
|
||||
*/
|
||||
private void ensureUniqueName(BroadcastConfiguration configuration)
|
||||
{
|
||||
if(configuration.getName() == null || configuration.getName().isEmpty())
|
||||
{
|
||||
configuration.setName("New Configuration");
|
||||
}
|
||||
|
||||
while(!isUniqueName(configuration.getName(), configuration))
|
||||
{
|
||||
String currentName = configuration.getName();
|
||||
|
||||
if(currentName.matches(UNIQUE_NAME_REGEX))
|
||||
{
|
||||
int currentVersion = 1;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Matcher m = Pattern.compile(UNIQUE_NAME_REGEX).matcher(currentName);
|
||||
|
||||
if(m.find())
|
||||
{
|
||||
String version = m.group(2);
|
||||
|
||||
try
|
||||
{
|
||||
currentVersion = Integer.parseInt(version);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Couldn't parse the version number -- keep incrementing until we find a winner
|
||||
}
|
||||
|
||||
currentVersion++;
|
||||
|
||||
sb.append(m.group(1)).append("(").append(currentVersion).append(")");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(configuration.getName()).append("(").append(currentVersion).append(")");
|
||||
}
|
||||
|
||||
configuration.setName(sb.toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(configuration.getName()).append("(2)");
|
||||
configuration.setName(sb.toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the name is unique among all of the broadcast configurations. Checks each of the configurations
|
||||
* in this model for a name collision. Assumes that the name argument will be assigned to the configuration
|
||||
* argument if the name is unique, therefore, the name will not be checked against the configuration argument for
|
||||
* a collision.
|
||||
*
|
||||
* @param name to check for uniqueness
|
||||
* @param configuration to ignore when checking the name for uniqueness
|
||||
* @return true if the name is not null and unique among all configurations managed by this model
|
||||
*/
|
||||
public boolean isUniqueName(String name, BroadcastConfiguration configuration)
|
||||
{
|
||||
if(name == null || name.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for(BroadcastConfiguration configurationToCompare : mBroadcastConfigurations)
|
||||
{
|
||||
if(configurationToCompare != configuration &&
|
||||
configurationToCompare.getName() != null &&
|
||||
configurationToCompare.getName().equals(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void removeBroadcastConfiguration(BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
if(broadcastConfiguration != null && mBroadcastConfigurations.contains(broadcastConfiguration))
|
||||
{
|
||||
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
|
||||
|
||||
mBroadcastConfigurations.remove(broadcastConfiguration);
|
||||
|
||||
mBroadcastConfigurationMap.remove(broadcastConfiguration.getName());
|
||||
|
||||
process(new BroadcastEvent(broadcastConfiguration, BroadcastEvent.Event.CONFIGURATION_DELETE));
|
||||
|
||||
fireTableRowsDeleted(index, index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the broadcaster associated with the stream name or null if there is no broadcaster setup for the name.
|
||||
*/
|
||||
public AudioBroadcaster getBroadcaster(String streamName)
|
||||
{
|
||||
return mBroadcasterMap.get(streamName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioPacket audioPacket)
|
||||
{
|
||||
if(audioPacket.hasMetadata() && audioPacket.getMetadata().isStreamable())
|
||||
{
|
||||
for(BroadcastChannel channel: audioPacket.getMetadata().getBroadcastChannels())
|
||||
{
|
||||
if(mBroadcasterMap.containsKey(channel.getChannelName()))
|
||||
{
|
||||
mStreamManager.receive(audioPacket);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new broadcaster for the broadcast configuration and adds it to the model
|
||||
*/
|
||||
private void createBroadcaster(BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
if(broadcastConfiguration != null && broadcastConfiguration.isEnabled() && broadcastConfiguration.isValid() &&
|
||||
!mBroadcasterMap.containsKey(broadcastConfiguration.getName()))
|
||||
{
|
||||
AudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
|
||||
{
|
||||
@Override
|
||||
public void receive(BroadcastEvent broadcastEvent)
|
||||
{
|
||||
process(broadcastEvent);
|
||||
}
|
||||
});
|
||||
|
||||
audioBroadcaster.start();
|
||||
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
|
||||
|
||||
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
fireTableRowsUpdated(index, index);
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down a broadcaster created from the configuration and remove it from this model
|
||||
*/
|
||||
private void deleteBroadcaster(String name)
|
||||
{
|
||||
if(name != null && mBroadcasterMap.containsKey(name))
|
||||
{
|
||||
AudioBroadcaster audioBroadcaster = mBroadcasterMap.remove(name);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
audioBroadcaster.stop();
|
||||
audioBroadcaster.removeListener();
|
||||
|
||||
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
fireTableRowsUpdated(index, index);
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_DELETE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the broadcast configuration identified by the stream name
|
||||
*/
|
||||
public BroadcastConfiguration getBroadcastConfiguration(String streamName)
|
||||
{
|
||||
return mBroadcastConfigurationMap.get(streamName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive broadcastAudio configuration events
|
||||
*/
|
||||
public void addListener(Listener<BroadcastEvent> listener)
|
||||
{
|
||||
mBroadcastEventBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving broadcastAudio configuration events
|
||||
*/
|
||||
public void removeListener(Listener<BroadcastEvent> listener)
|
||||
{
|
||||
mBroadcastEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the broadcastAudio configuration change event
|
||||
*/
|
||||
private void broadcast(BroadcastEvent event)
|
||||
{
|
||||
mBroadcastEventBroadcaster.broadcast(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a broadcast event from one of the broadcasters managed by this model
|
||||
*/
|
||||
public void process(BroadcastEvent broadcastEvent)
|
||||
{
|
||||
if(broadcastEvent.isBroadcastConfigurationEvent())
|
||||
{
|
||||
switch(broadcastEvent.getEvent())
|
||||
{
|
||||
case CONFIGURATION_ADD:
|
||||
createBroadcaster(broadcastEvent.getBroadcastConfiguration());
|
||||
break;
|
||||
case CONFIGURATION_CHANGE:
|
||||
BroadcastConfiguration broadcastConfiguration = broadcastEvent.getBroadcastConfiguration();
|
||||
int index = mBroadcastConfigurations.indexOf(broadcastConfiguration);
|
||||
|
||||
//Delete the existing broadcaster for any broadcast configuration changes
|
||||
String previousChannelName = cleanupMapAssociations(broadcastConfiguration);
|
||||
deleteBroadcaster(previousChannelName);
|
||||
|
||||
//If the configuration is enabled, create a new broadcaster after a brief delay
|
||||
if(broadcastConfiguration.isEnabled())
|
||||
{
|
||||
//Delay restarting the broadcaster to allow remote server time to cleanup
|
||||
ThreadPool.SCHEDULED.schedule(new DelayedBroadcasterStartup(broadcastConfiguration),
|
||||
3, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
fireTableRowsUpdated(index, index);
|
||||
break;
|
||||
case CONFIGURATION_DELETE:
|
||||
deleteBroadcaster(broadcastEvent.getBroadcastConfiguration().getName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if(broadcastEvent.isAudioBroadcasterEvent())
|
||||
{
|
||||
int row = mBroadcastConfigurations.indexOf(broadcastEvent.getAudioBroadcaster().getBroadcastConfiguration());
|
||||
|
||||
switch(broadcastEvent.getEvent())
|
||||
{
|
||||
case BROADCASTER_QUEUE_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_QUEUE_SIZE);
|
||||
}
|
||||
break;
|
||||
case BROADCASTER_STATE_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_STATUS);
|
||||
}
|
||||
break;
|
||||
case BROADCASTER_STREAMED_COUNT_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
|
||||
}
|
||||
break;
|
||||
case BROADCASTER_AGED_OFF_COUNT_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_AGED_OFF_COUNT);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Rebroadcast the event to any listeners of this model
|
||||
broadcast(broadcastEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the name of a broadcast configuration changes, remove the old map associations so that they can be replaced
|
||||
* with the new map associations. Remove any broadcaster that is associated with the old configuration name.
|
||||
*
|
||||
* @param broadcastConfiguration
|
||||
* @return previous or current channel name that has been replaced with the current configuration channel name so
|
||||
* that anything else associated with the previous name can be cleaned up.
|
||||
*/
|
||||
private String cleanupMapAssociations(BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
String oldName = null;
|
||||
|
||||
for(Map.Entry<String,BroadcastConfiguration> entry : mBroadcastConfigurationMap.entrySet())
|
||||
{
|
||||
if(entry.getValue() == broadcastConfiguration)
|
||||
{
|
||||
oldName = entry.getKey();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(oldName != null)
|
||||
{
|
||||
mBroadcastConfigurationMap.remove(oldName);
|
||||
}
|
||||
|
||||
mBroadcastConfigurationMap.put(broadcastConfiguration.getName(), broadcastConfiguration);
|
||||
|
||||
return oldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount()
|
||||
{
|
||||
return mBroadcastConfigurations.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount()
|
||||
{
|
||||
return COLUMN_NAMES.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(rowIndex <= mBroadcastConfigurations.size())
|
||||
{
|
||||
BroadcastConfiguration configuration = mBroadcastConfigurations.get(rowIndex);
|
||||
|
||||
if(configuration != null)
|
||||
{
|
||||
switch(columnIndex)
|
||||
{
|
||||
case COLUMN_SERVER_ICON:
|
||||
String iconPath = configuration.getBroadcastServerType().getIconPath();
|
||||
|
||||
if(iconPath != null && mIconManager != null)
|
||||
{
|
||||
return mIconManager.getScaledIcon(new ImageIcon(iconPath), 14);
|
||||
}
|
||||
break;
|
||||
case COLUMN_STREAM_NAME:
|
||||
return configuration.getName();
|
||||
case COLUMN_BROADCASTER_STATUS:
|
||||
AudioBroadcaster audioBroadcasterA = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterA != null)
|
||||
{
|
||||
return audioBroadcasterA.getBroadcastState();
|
||||
}
|
||||
else if(!configuration.isEnabled())
|
||||
{
|
||||
return BroadcastState.DISABLED;
|
||||
}
|
||||
else if(!configuration.isValid())
|
||||
{
|
||||
return BroadcastState.INVALID_SETTINGS;
|
||||
}
|
||||
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||
AudioBroadcaster audioBroadcasterB = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterB != null)
|
||||
{
|
||||
return audioBroadcasterB.getQueueSize();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||
AudioBroadcaster audioBroadcasterC = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterC != null)
|
||||
{
|
||||
return audioBroadcasterC.getStreamedAudioCount();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
|
||||
AudioBroadcaster audioBroadcasterD = mBroadcasterMap.get(configuration.getName());
|
||||
|
||||
if(audioBroadcasterD != null)
|
||||
{
|
||||
return audioBroadcasterD.getAgedOffAudioCount();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error accessing data in broadcast model", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex)
|
||||
{
|
||||
if(columnIndex == COLUMN_SERVER_ICON)
|
||||
{
|
||||
return ImageIcon.class;
|
||||
}
|
||||
|
||||
return String.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast configuration at the specified model row
|
||||
*/
|
||||
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)
|
||||
{
|
||||
if(0 <= column && column < COLUMN_NAMES.length)
|
||||
{
|
||||
return COLUMN_NAMES[column];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup method to remove a temporary recording file from disk.
|
||||
*
|
||||
* @param recording to remove
|
||||
*/
|
||||
private void removeRecording(AudioRecording recording)
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.delete(recording.getPath());
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error deleting temporary internet recording file: " + recording.getPath().toString() + " - " +
|
||||
ioe.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes any temporary stream recordings left-over from the previous application run.
|
||||
*
|
||||
* This should only be invoked on startup.
|
||||
*/
|
||||
private void removeOrphanedTemporaryRecordings()
|
||||
{
|
||||
Path path = SystemProperties.getInstance().getApplicationFolder(BroadcastModel.TEMPORARY_STREAM_DIRECTORY);
|
||||
|
||||
if(path != null && Files.isDirectory(path))
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(TEMPORARY_STREAM_FILE_SUFFIX);
|
||||
sb.append("*.*");
|
||||
|
||||
try(DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path, sb.toString()))
|
||||
{
|
||||
directoryStream.forEach(new Consumer<Path>()
|
||||
{
|
||||
@Override
|
||||
public void accept(Path path)
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.delete(path);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Couldn't delete orphaned temporary recording: " + path.toString(), ioe);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error discovering orphaned temporary stream recording files", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides delayed startup of a broadcaster to allow for the remote server to complete a disconnection.
|
||||
*/
|
||||
public class DelayedBroadcasterStartup implements Runnable
|
||||
{
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
|
||||
public DelayedBroadcasterStartup(BroadcastConfiguration configuration)
|
||||
{
|
||||
mBroadcastConfiguration = configuration;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
createBroadcaster(mBroadcastConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes completed audio recordings and distributes them to the audio broadcasters. Adds the recording to
|
||||
* the audio recording queue to be monitored for deletion.
|
||||
*/
|
||||
public class CompletedRecordingListener implements Listener<AudioRecording>
|
||||
{
|
||||
@Override
|
||||
public void receive(AudioRecording audioRecording)
|
||||
{
|
||||
Metadata metadata = audioRecording.getMetadata();
|
||||
|
||||
if(metadata != null && metadata.isStreamable())
|
||||
{
|
||||
for(BroadcastChannel broadcastChannel : metadata.getBroadcastChannels())
|
||||
{
|
||||
String channelName = broadcastChannel.getChannelName();
|
||||
|
||||
if(channelName != null)
|
||||
{
|
||||
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
audioRecording.addPendingReplay();
|
||||
audioBroadcaster.receive(audioRecording);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mRecordingQueue.add(audioRecording);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the recording queue and removes any recordings that have no pending replays by audio broadcasters
|
||||
*/
|
||||
public class RecordingDeletionMonitor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<AudioRecording> recordingsToDelete = new ArrayList<>();
|
||||
|
||||
Iterator<AudioRecording> it = mRecordingQueue.iterator();
|
||||
|
||||
AudioRecording recording;
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
recording = it.next();
|
||||
|
||||
if(!recording.hasPendingReplays())
|
||||
{
|
||||
recordingsToDelete.add(recording);
|
||||
}
|
||||
}
|
||||
|
||||
if(!recordingsToDelete.isEmpty())
|
||||
{
|
||||
for(AudioRecording recordingToDelete : recordingsToDelete)
|
||||
{
|
||||
mRecordingQueue.remove(recordingToDelete);
|
||||
removeRecording(recordingToDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error while checking audio recording queue for recordings to delete", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import com.jidesoft.swing.JideSplitPane;
|
||||
import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EmptyEditor;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
public class BroadcastPanel extends JPanel implements ActionListener, ListSelectionListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( BroadcastPanel.class );
|
||||
|
||||
private static final String NEW_BROADCAST_CONFIGURATION = "New";
|
||||
private static final String COPY_BROADCAST_CONFIGURATION = "Copy";
|
||||
private static final String DELETE_BROADCAST_CONFIGURATION = "Delete";
|
||||
|
||||
private BroadcastModel mBroadcastModel;
|
||||
private AliasModel mAliasModel;
|
||||
private IconManager mIconManager;
|
||||
|
||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||
private JideSplitPane mSplitPane;
|
||||
private Editor<BroadcastConfiguration> mEmptyEditor = new EmptyEditor<>("Configuration:");
|
||||
private Editor<BroadcastConfiguration> mEditor;
|
||||
private JButton mNewButton = new JButton(NEW_BROADCAST_CONFIGURATION);
|
||||
private JButton mCopyButton = new JButton(COPY_BROADCAST_CONFIGURATION);
|
||||
private JButton mDeleteButton = new JButton(DELETE_BROADCAST_CONFIGURATION);
|
||||
|
||||
public BroadcastPanel(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
mAliasModel = aliasModel;
|
||||
mIconManager = iconManager;
|
||||
mEditor = mEmptyEditor;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
|
||||
|
||||
mBroadcastStatusPanel = new BroadcastStatusPanel(mBroadcastModel);
|
||||
|
||||
mBroadcastStatusPanel.getTable().setAutoCreateRowSorter(true);
|
||||
mBroadcastStatusPanel.getTable().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
mBroadcastStatusPanel.getTable().getSelectionModel().addListSelectionListener(BroadcastPanel.this);
|
||||
|
||||
JPanel buttonsPanel = new JPanel();
|
||||
buttonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[]"));
|
||||
|
||||
mNewButton = new JButton("New ...");
|
||||
mNewButton.setToolTipText("Create a new broadcast audio configuration");
|
||||
mNewButton.addMouseListener(new MouseAdapter()
|
||||
{
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e)
|
||||
{
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
|
||||
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.BROADCASTIFY));
|
||||
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_TCP));
|
||||
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.ICECAST_HTTP));
|
||||
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.SHOUTCAST_V1));
|
||||
menu.add(new AddBroadcastConfigurationItem(BroadcastServerType.SHOUTCAST_V2));
|
||||
|
||||
menu.show(e.getComponent(), e.getX(), e.getY());
|
||||
}
|
||||
});
|
||||
|
||||
buttonsPanel.add(mNewButton);
|
||||
|
||||
mCopyButton.addActionListener(this);
|
||||
mCopyButton.setEnabled(false);
|
||||
mCopyButton.setToolTipText("Creates a copy of the currently selected streaming configuration and adds it");
|
||||
buttonsPanel.add(mCopyButton);
|
||||
|
||||
mDeleteButton.addActionListener(this);
|
||||
mDeleteButton.setEnabled(false);
|
||||
mDeleteButton.setToolTipText("Deletes the currently selected streaming configuration");
|
||||
buttonsPanel.add(mDeleteButton);
|
||||
|
||||
JPanel listAndButtonsPanel = new JPanel();
|
||||
|
||||
listAndButtonsPanel.setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill][]"));
|
||||
|
||||
listAndButtonsPanel.add(mBroadcastStatusPanel, "wrap");
|
||||
listAndButtonsPanel.add(buttonsPanel);
|
||||
|
||||
mSplitPane = new JideSplitPane(JideSplitPane.HORIZONTAL_SPLIT);
|
||||
mSplitPane.setDividerSize(5);
|
||||
mSplitPane.setShowGripper(true);
|
||||
mSplitPane.add(listAndButtonsPanel);
|
||||
|
||||
mSplitPane.add(mEditor);
|
||||
|
||||
add(mSplitPane);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
switch(e.getActionCommand())
|
||||
{
|
||||
case COPY_BROADCAST_CONFIGURATION:
|
||||
BroadcastConfiguration copySelected = getSelectedBroadcastConfiguration();
|
||||
mBroadcastModel.cloneBroadcastConfiguration(copySelected);
|
||||
break;
|
||||
case DELETE_BROADCAST_CONFIGURATION:
|
||||
BroadcastConfiguration deleteSelected = getSelectedBroadcastConfiguration();
|
||||
|
||||
if(deleteSelected != null)
|
||||
{
|
||||
int choice = JOptionPane.showConfirmDialog(BroadcastPanel.this, "Do you want to delete broadcast\n" +
|
||||
" configuration " + deleteSelected.getName() + "?", "Delete Broadcast Configuration?",
|
||||
JOptionPane.YES_NO_OPTION);
|
||||
|
||||
if(choice == JOptionPane.YES_OPTION)
|
||||
{
|
||||
mBroadcastModel.removeBroadcastConfiguration(deleteSelected);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selected configuration
|
||||
*/
|
||||
private BroadcastConfiguration getSelectedBroadcastConfiguration()
|
||||
{
|
||||
int viewRow = mBroadcastStatusPanel.getTable().getSelectedRow();
|
||||
|
||||
if(viewRow >= 0)
|
||||
{
|
||||
int modelRow = mBroadcastStatusPanel.getTable().convertRowIndexToModel(viewRow);
|
||||
|
||||
if(modelRow >= 0)
|
||||
{
|
||||
return mBroadcastModel.getConfigurationAt(modelRow);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void valueChanged(ListSelectionEvent e)
|
||||
{
|
||||
if (!e.getValueIsAdjusting())
|
||||
{
|
||||
BroadcastConfiguration selectedConfiguration = getSelectedBroadcastConfiguration();
|
||||
|
||||
//Enable buttons accordingly
|
||||
mCopyButton.setEnabled(selectedConfiguration != null);
|
||||
mDeleteButton.setEnabled(selectedConfiguration != null);
|
||||
|
||||
if (mEditor.isModified())
|
||||
{
|
||||
int option = JOptionPane.showConfirmDialog(
|
||||
BroadcastPanel.this,
|
||||
"Configuration settings have changed. Do you want to save these changes?",
|
||||
"Save Changes?",
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.QUESTION_MESSAGE);
|
||||
|
||||
if (option == JOptionPane.YES_OPTION)
|
||||
{
|
||||
mEditor.save();
|
||||
}
|
||||
}
|
||||
|
||||
mSplitPane.removePane(mEditor);
|
||||
|
||||
if (selectedConfiguration != null)
|
||||
{
|
||||
mEditor = BroadcastFactory.getEditor(selectedConfiguration, mBroadcastModel, mAliasModel,
|
||||
mIconManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
mEditor = mEmptyEditor;
|
||||
}
|
||||
|
||||
mSplitPane.add(mEditor);
|
||||
|
||||
revalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public class AddBroadcastConfigurationItem extends JMenuItem
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private BroadcastServerType mBroadcastServerType;
|
||||
|
||||
public AddBroadcastConfigurationItem(BroadcastServerType type)
|
||||
{
|
||||
super(type.toString(), mIconManager.getScaledIcon(new ImageIcon(type.getIconPath()), 14));
|
||||
|
||||
mBroadcastServerType = type;
|
||||
|
||||
setToolTipText("Add a new " + type.toString() + " audio stream configuration");
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
final BroadcastConfiguration configuration =
|
||||
BroadcastFactory.getConfiguration(mBroadcastServerType, BroadcastFormat.MP3);
|
||||
|
||||
if(configuration != null)
|
||||
{
|
||||
mBroadcastModel.addBroadcastConfiguration(configuration);
|
||||
|
||||
int modelRow = mBroadcastModel.getRowForConfiguration(configuration);
|
||||
int tableRow = mBroadcastStatusPanel.getTable().convertRowIndexToView(modelRow);
|
||||
mBroadcastStatusPanel.getTable().changeSelection(tableRow, 0, false, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
public enum BroadcastServerType
|
||||
{
|
||||
BROADCASTIFY("Broadcastify", "images/broadcastify.png"), //Icecast Server 2.3.2
|
||||
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"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||
UNKNOWN("Unknown", null);
|
||||
|
||||
private String mLabel;
|
||||
private String mIconPath;
|
||||
|
||||
BroadcastServerType(String label, String iconPath)
|
||||
{
|
||||
mLabel = label;
|
||||
mIconPath = iconPath;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public String getIconPath()
|
||||
{
|
||||
return mIconPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
public enum BroadcastState
|
||||
{
|
||||
/**
|
||||
* General error in configuration that causes the remote server to reject the connection
|
||||
*/
|
||||
CONFIGURATION_ERROR("Configuration Error", true),
|
||||
|
||||
/**
|
||||
* Connected to the broadcastAudio server and capable of streaming
|
||||
*/
|
||||
CONNECTED("Connected", false),
|
||||
|
||||
/**
|
||||
* Connection interrupted, attempting to reconnect.
|
||||
*/
|
||||
CONNECTING("Connecting", false),
|
||||
|
||||
/**
|
||||
* Indicates the configuration is disabled
|
||||
*/
|
||||
DISABLED("Disabled", true),
|
||||
|
||||
/**
|
||||
* Disconnected from the broadcastAudio server
|
||||
*/
|
||||
DISCONNECTED("Disconnected", false),
|
||||
|
||||
/**
|
||||
* Invalid credentials - user name or password
|
||||
*/
|
||||
INVALID_CREDENTIALS("Invalid User Name/Password", true),
|
||||
|
||||
/**
|
||||
* Invalid mount point
|
||||
*/
|
||||
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
|
||||
|
||||
/**
|
||||
* Invalid configuration settings
|
||||
*/
|
||||
INVALID_SETTINGS("Invalid Settings", true),
|
||||
|
||||
/**
|
||||
* Remote server max sources has been exceeded
|
||||
*/
|
||||
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
|
||||
|
||||
/**
|
||||
* Specified mount point is already in use
|
||||
*/
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
||||
|
||||
/**
|
||||
* Server is not known or reachable
|
||||
*/
|
||||
NO_SERVER("No Server", false),
|
||||
|
||||
/**
|
||||
* Initial state with no connection attempted.
|
||||
*/
|
||||
READY("Ready", false),
|
||||
|
||||
/**
|
||||
* Error while broadcasting stream data. Temporary error state to allow connection to be reset.
|
||||
*/
|
||||
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false),
|
||||
|
||||
/**
|
||||
* Unsupported audio format
|
||||
*/
|
||||
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true),
|
||||
|
||||
/**
|
||||
* Unspecified error
|
||||
*/
|
||||
ERROR("Error", true);
|
||||
|
||||
private String mLabel;
|
||||
private boolean mErrorState;
|
||||
|
||||
private BroadcastState(String label, boolean connected)
|
||||
{
|
||||
mLabel = label;
|
||||
mErrorState = connected;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public boolean isErrorState()
|
||||
{
|
||||
return mErrorState;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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 net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.*;
|
||||
|
||||
public class BroadcastStatusPanel extends JPanel
|
||||
{
|
||||
private JTable mTable;
|
||||
private JScrollPane mScrollPane;
|
||||
private BroadcastModel mBroadcastModel;
|
||||
|
||||
public BroadcastStatusPanel(BroadcastModel broadcastModel)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public JTable getTable()
|
||||
{
|
||||
return mTable;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
|
||||
|
||||
mTable = new JTable(mBroadcastModel);
|
||||
|
||||
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class);
|
||||
renderer.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS).setCellRenderer(new StatusCellRenderer());
|
||||
|
||||
mScrollPane = new JScrollPane(mTable);
|
||||
|
||||
add(mScrollPane);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom cell renderer for the broadcast state column.
|
||||
*/
|
||||
public class StatusCellRenderer extends DefaultTableCellRenderer
|
||||
{
|
||||
public StatusCellRenderer()
|
||||
{
|
||||
setOpaque(true);
|
||||
setHorizontalAlignment(SwingConstants.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
|
||||
boolean hasFocus, int row, int column)
|
||||
{
|
||||
JLabel component = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
|
||||
if(isSelected)
|
||||
{
|
||||
setBackground(table.getSelectionBackground());
|
||||
setForeground(table.getSelectionForeground());
|
||||
}
|
||||
else
|
||||
{
|
||||
if(value instanceof BroadcastState)
|
||||
{
|
||||
BroadcastState state = (BroadcastState)value;
|
||||
|
||||
if(state == BroadcastState.CONNECTED)
|
||||
{
|
||||
setBackground(Color.GREEN);
|
||||
setForeground(table.getForeground());
|
||||
}
|
||||
else if(state == BroadcastState.DISABLED)
|
||||
{
|
||||
setBackground(table.getBackground());
|
||||
setForeground(Color.LIGHT_GRAY);
|
||||
}
|
||||
else if(state == BroadcastState.INVALID_SETTINGS)
|
||||
{
|
||||
setBackground(Color.YELLOW);
|
||||
setForeground(table.getForeground());
|
||||
}
|
||||
else if(state.isErrorState())
|
||||
{
|
||||
setBackground(Color.RED);
|
||||
setForeground(table.getForeground());
|
||||
}
|
||||
else
|
||||
{
|
||||
setBackground(table.getBackground());
|
||||
setForeground(table.getForeground());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setForeground(table.getForeground());
|
||||
setBackground(table.getBackground());
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.channel.metadata.Metadata;
|
||||
|
||||
public interface IBroadcastMetadataUpdater
|
||||
{
|
||||
/**
|
||||
* Updates broadcast audio metadata
|
||||
*
|
||||
* @param metadata containing metadata attributes
|
||||
*/
|
||||
void update(Metadata metadata);
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.audio.AudioPacket;
|
||||
import io.github.dsheirer.record.AudioRecorder;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class StreamManager implements Listener<AudioPacket>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
|
||||
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
|
||||
|
||||
private static AtomicInteger sNextRecordingNumber = new AtomicInteger();
|
||||
|
||||
private Listener<AudioRecording> mAudioRecordingListener;
|
||||
private BroadcastFormat mBroadcastFormat;
|
||||
private Path mTempDirectory;
|
||||
private Map<Integer,AudioRecorder> mStreamRecorders = new HashMap<>();
|
||||
private Runnable mRecorderMonitor;
|
||||
private ScheduledFuture<?> mRecorderMonitorFuture;
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Stream manager processes all incoming audio packets and reassembles individual audio streams, converts audio
|
||||
* to desired output format and persists each stream to disc. Each recording is capped at a maximum length to
|
||||
* ensure that recordings don't run too long before they are streamed out and to ensure that inactive recordings
|
||||
* are closed in a timely fashion.
|
||||
*
|
||||
* Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion
|
||||
*
|
||||
* @param listener to receive completed audio recordings
|
||||
* @param tempDirectory where to store temporary audio recordings
|
||||
*/
|
||||
public StreamManager(Listener<AudioRecording> listener, BroadcastFormat broadcastFormat, Path tempDirectory)
|
||||
{
|
||||
assert (tempDirectory != null && Files.isDirectory(tempDirectory));
|
||||
|
||||
mAudioRecordingListener = listener;
|
||||
mBroadcastFormat = broadcastFormat;
|
||||
mTempDirectory = tempDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the stream manager. Schedules a processor to monitor for inactive temporary stream recorders
|
||||
* every 2 seconds.
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
if(mRecorderMonitor == null)
|
||||
{
|
||||
mRecorderMonitor = new RecorderMonitor();
|
||||
}
|
||||
|
||||
mRecorderMonitorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mRecorderMonitor,
|
||||
0,2, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the stream manager. Stops the inactive temporary stream recorder monitoring thread.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
if(mRecorderMonitorFuture != null)
|
||||
{
|
||||
mRecorderMonitorFuture.cancel(true);
|
||||
}
|
||||
|
||||
synchronized(mStreamRecorders)
|
||||
{
|
||||
List<Integer> streamKeys = new ArrayList<>(mStreamRecorders.keySet());
|
||||
|
||||
for(Integer streamKey : streamKeys)
|
||||
{
|
||||
removeRecorder(streamKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioPacket audioPacket)
|
||||
{
|
||||
if(mRunning.get() && audioPacket.hasMetadata())
|
||||
{
|
||||
synchronized(mStreamRecorders)
|
||||
{
|
||||
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
|
||||
|
||||
AudioPacket.Type type = audioPacket.getType();
|
||||
|
||||
if(type == AudioPacket.Type.AUDIO)
|
||||
{
|
||||
if(mStreamRecorders.containsKey(channelMetadataID))
|
||||
{
|
||||
AudioRecorder recorder = mStreamRecorders.get(channelMetadataID);
|
||||
|
||||
if(recorder != null)
|
||||
{
|
||||
recorder.receive(audioPacket);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
|
||||
mBroadcastFormat);
|
||||
recorder.start(ThreadPool.SCHEDULED);
|
||||
recorder.receive(audioPacket);
|
||||
mStreamRecorders.put(channelMetadataID, recorder);
|
||||
}
|
||||
}
|
||||
else if(type == AudioPacket.Type.END)
|
||||
{
|
||||
removeRecorder(channelMetadataID);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("Unrecognized Audio Packet Type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the recorder associated with the source channel ID.
|
||||
*
|
||||
* Note: this method invocation is not thread safe and must be invoked by a thread safe mechanism that protects the
|
||||
* mStreamRecorders map.
|
||||
*
|
||||
* @param sourceChannelID identifying the recorder
|
||||
*/
|
||||
private void removeRecorder(Integer sourceChannelID)
|
||||
{
|
||||
if(mStreamRecorders.containsKey(sourceChannelID))
|
||||
{
|
||||
AudioRecorder recorder = mStreamRecorders.remove(sourceChannelID);
|
||||
|
||||
recorder.close(new Listener<AudioRecorder>()
|
||||
{
|
||||
@Override
|
||||
public void receive(AudioRecorder audioRecorder)
|
||||
{
|
||||
AudioRecording audioRecording =
|
||||
new AudioRecording(audioRecorder.getPath(), audioRecorder.getMetadata(),
|
||||
audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength());
|
||||
|
||||
if(mAudioRecordingListener != null)
|
||||
{
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary streaming recording file path
|
||||
*/
|
||||
private Path getTemporaryRecordingPath()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
|
||||
sb.append(sNextRecordingNumber.incrementAndGet()).append("_");
|
||||
sb.append(TimeStamp.getLongTimeStamp("_"));
|
||||
sb.append(mBroadcastFormat.getFileExtension());
|
||||
|
||||
Path temporaryRecordingPath = mTempDirectory.resolve(sb.toString());
|
||||
|
||||
return temporaryRecordingPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording.
|
||||
*/
|
||||
public class RecorderMonitor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
synchronized(mStreamRecorders)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
mStreamRecorders.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getTimeRecordingStart() + MAXIMUM_RECORDER_LIFESPAN_MILLIS < now)
|
||||
.forEach(entry ->
|
||||
{
|
||||
mLog.info("cycling recorder - max temporary streaming recording time limit reached [" +
|
||||
entry.getValue().getPath().toString() + "]");
|
||||
|
||||
removeRecorder(entry.getKey());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
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.icecast.IcecastTCPConfiguration;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
public class BroadcastifyConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
private int mFeedID;
|
||||
|
||||
public BroadcastifyConfiguration()
|
||||
{
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcastify configuration for Icecast 2.3.2 compatible servers
|
||||
*
|
||||
* @param format of output audio (MP3)
|
||||
*/
|
||||
public BroadcastifyConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
setBitRate(16);
|
||||
setChannels(1);
|
||||
setSampleRate(8000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
BroadcastifyConfiguration copy = new BroadcastifyConfiguration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Icecast Configuration Parameters
|
||||
copy.setUserName(getUserName());
|
||||
copy.setMountPoint(getMountPoint());
|
||||
copy.setDescription(getDescription());
|
||||
copy.setGenre(getGenre());
|
||||
copy.setPublic(isPublic());
|
||||
copy.setURL(getURL());
|
||||
|
||||
//Broadcastify Configuration Parameters
|
||||
copy.setFeedID(getFeedID());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.BROADCASTIFY;
|
||||
}
|
||||
|
||||
@XmlAttribute(name="feed_id")
|
||||
public int getFeedID()
|
||||
{
|
||||
return mFeedID;
|
||||
}
|
||||
|
||||
public void setFeedID(int feedID)
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import com.radioreference.api.soap2.UserFeedBroadcast;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class BroadcastifyConfigurationEditor extends BroadcastConfigurationEditor implements Listener<UserFeedBroadcast>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyConfigurationEditor.class);
|
||||
|
||||
private static final int ONE_MINUTE_MS = 60000;
|
||||
|
||||
private JTextField mName;
|
||||
private JTextField mServer;
|
||||
private JTextField mPort;
|
||||
private JTextField mMountPoint;
|
||||
private JTextField mPassword;
|
||||
private JTextField mFeedID;
|
||||
private JSlider mDelay;
|
||||
private JLabel mDelayValue;
|
||||
private JSlider mMaximumRecordingAge;
|
||||
private JLabel mMaximumRecordingAgeValue;
|
||||
private JCheckBox mEnabled;
|
||||
private JButton mSaveButton;
|
||||
private JButton mResetButton;
|
||||
|
||||
public BroadcastifyConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
|
||||
{
|
||||
super(broadcastModel, aliasModel, iconManager);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("fill,wrap 2", "[align right][grow,fill]",
|
||||
"[][][][][][][][][][][][][][][grow,fill]"));
|
||||
setPreferredSize(new Dimension(150, 400));
|
||||
|
||||
|
||||
JLabel channelLabel = new JLabel("Broadcastify Stream");
|
||||
|
||||
ImageIcon broadcastifyIcon = mIconManager.getScaledIcon(
|
||||
new ImageIcon(BroadcastServerType.BROADCASTIFY.getIconPath()), 25);
|
||||
channelLabel.setIcon(broadcastifyIcon);
|
||||
add(channelLabel, "span, align center");
|
||||
|
||||
JButton lookupButton = new JButton("Lookup ...");
|
||||
lookupButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
final UserFeedSelectionDialog dialog = new UserFeedSelectionDialog(BroadcastifyConfigurationEditor.this);
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
dialog.setLocationRelativeTo(BroadcastifyConfigurationEditor.this);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
add(new JLabel(""));
|
||||
add(lookupButton, "grow,wrap");
|
||||
|
||||
add(new JLabel("Name:"));
|
||||
mName = new JTextField();
|
||||
mName.getDocument().addDocumentListener(this);
|
||||
mName.setToolTipText("Unique name for this stream");
|
||||
add(mName);
|
||||
|
||||
add(new JLabel("Server:"));
|
||||
mServer = new JTextField();
|
||||
mServer.getDocument().addDocumentListener(this);
|
||||
mServer.setToolTipText("Server address (e.g. audio3.broadcastify.com). See MyBCFY feed details, Technicals tab");
|
||||
add(mServer);
|
||||
|
||||
add(new JLabel("Port:"));
|
||||
mPort = new JTextField();
|
||||
mPort.getDocument().addDocumentListener(this);
|
||||
mPort.setToolTipText("Server port number (e.g. 80). See MyBCFY feed details, Technicals tab");
|
||||
add(mPort);
|
||||
|
||||
add(new JLabel("Mount:"));
|
||||
mMountPoint = new JTextField();
|
||||
mMountPoint.getDocument().addDocumentListener(this);
|
||||
mMountPoint.setToolTipText("Mount point (e.g. \\awlkejrlkjsd). See MyBCFY feed details, Technicals tab");
|
||||
add(mMountPoint);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
mPassword = new JTextField();
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
mPassword.setToolTipText("Password. See MyBCFY feed details, Technicals tab");
|
||||
add(mPassword);
|
||||
|
||||
add(new JLabel("Feed ID:"));
|
||||
mFeedID = new JTextField();
|
||||
mFeedID.getDocument().addDocumentListener(this);
|
||||
mFeedID.setToolTipText("Feed ID (optional). See MyBCFY feed details");
|
||||
add(mFeedID);
|
||||
|
||||
|
||||
add(new JLabel());
|
||||
mDelayValue = new JLabel("0 Minutes");
|
||||
add(mDelayValue);
|
||||
|
||||
add(new JLabel("Delay:"));
|
||||
mDelay = new JSlider(0, 60, 0);
|
||||
mDelay.setMajorTickSpacing(10);
|
||||
mDelay.setMinorTickSpacing(5);
|
||||
mDelay.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mDelayValue.setText(mDelay.getValue() + " Minutes");
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mDelay.setToolTipText("Audio broadcast delay in minutes");
|
||||
add(mDelay);
|
||||
|
||||
add(new JLabel());
|
||||
mMaximumRecordingAgeValue = new JLabel("1 Minute");
|
||||
add(mMaximumRecordingAgeValue);
|
||||
|
||||
add(new JLabel("Age Limit:"));
|
||||
mMaximumRecordingAge = new JSlider(1,60,5);
|
||||
mMaximumRecordingAge.setMajorTickSpacing(10);
|
||||
mMaximumRecordingAge.setMinorTickSpacing(5);
|
||||
mMaximumRecordingAge.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
int value = mMaximumRecordingAge.getValue();
|
||||
|
||||
mMaximumRecordingAgeValue.setText(value + " Minute" + (value > 1 ? "s" : ""));
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mMaximumRecordingAge.setToolTipText("Maximum recording age (in addition to delay) to stream");
|
||||
add(mMaximumRecordingAge);
|
||||
|
||||
mEnabled = new JCheckBox("Enabled");
|
||||
mEnabled.setToolTipText("Enable (checked) or disable this stream");
|
||||
mEnabled.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
add(new JLabel());
|
||||
add(mEnabled);
|
||||
|
||||
mSaveButton = new JButton("Save");
|
||||
mSaveButton.setEnabled(false);
|
||||
mSaveButton.setToolTipText("Click to save configuration information");
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
save();
|
||||
}
|
||||
});
|
||||
add(mSaveButton);
|
||||
|
||||
mResetButton = new JButton("Reset");
|
||||
mResetButton.setEnabled(false);
|
||||
mResetButton.setToolTipText("Click to reset any changes you've made\n since the last save");
|
||||
mResetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(false);
|
||||
setItem(getItem());
|
||||
}
|
||||
});
|
||||
add(mResetButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModified(boolean modified)
|
||||
{
|
||||
super.setModified(modified);
|
||||
|
||||
mSaveButton.setEnabled(modified);
|
||||
mResetButton.setEnabled(modified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
BroadcastifyConfiguration config = (BroadcastifyConfiguration) getItem();
|
||||
|
||||
mName.setText(config.getName());
|
||||
mServer.setText(config.getHost());
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
mMountPoint.setText(config.getMountPoint());
|
||||
mPassword.setText(config.getPassword());
|
||||
mFeedID.setText(String.valueOf(config.getFeedID()));
|
||||
mDelay.setValue((int) (config.getDelay() / ONE_MINUTE_MS));
|
||||
mMaximumRecordingAge.setValue((int)config.getMaximumRecordingAge() / ONE_MINUTE_MS);
|
||||
mEnabled.setSelected(config.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
mName.setText(null);
|
||||
mServer.setText(null);
|
||||
mPort.setText(null);
|
||||
mMountPoint.setText(null);
|
||||
mPassword.setText(null);
|
||||
mFeedID.setText(null);
|
||||
mDelay.setValue(0);
|
||||
mMaximumRecordingAge.setValue(1);
|
||||
mEnabled.setSelected(false);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
BroadcastifyConfiguration config = (BroadcastifyConfiguration) getItem();
|
||||
|
||||
if(validateConfiguration())
|
||||
{
|
||||
updateConfigurationName(config, mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
config.setPort(getPort());
|
||||
config.setMountPoint(mMountPoint.getText());
|
||||
config.setPassword(mPassword.getText());
|
||||
config.setFeedID(getFeedID());
|
||||
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
|
||||
config.setMaximumRecordingAge(mMaximumRecordingAge.getValue() * ONE_MINUTE_MS);
|
||||
config.setEnabled(mEnabled.isSelected());
|
||||
|
||||
setModified(false);
|
||||
|
||||
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user specified values in the controls prior to saving them to the configuration
|
||||
*/
|
||||
private boolean validateConfiguration()
|
||||
{
|
||||
String name = mName.getText();
|
||||
|
||||
//A unique stream name is required
|
||||
if(!mBroadcastModel.isUniqueName(name, getItem()))
|
||||
{
|
||||
String message = (name == null || name.isEmpty()) ?
|
||||
"Please specify a stream name." : "Stream name " + name +
|
||||
"is already in use.\nPlease choose another name.";
|
||||
|
||||
JOptionPane.showMessageDialog(BroadcastifyConfigurationEditor.this,
|
||||
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mName.requestFocus();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Server address is optional, but required
|
||||
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Port is optional, but required
|
||||
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Mount Point is optional, but required
|
||||
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Password is optional, but required
|
||||
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getPort()
|
||||
{
|
||||
String port = mPort.getText();
|
||||
|
||||
if(port != null && !port.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(port);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getFeedID()
|
||||
{
|
||||
String feedID = mFeedID.getText();
|
||||
|
||||
if(feedID != null && !feedID.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(feedID);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(UserFeedBroadcast userFeedBroadcast)
|
||||
{
|
||||
mName.setText(userFeedBroadcast.getDescr());
|
||||
mServer.setText(userFeedBroadcast.getHostname());
|
||||
mPort.setText(userFeedBroadcast.getPort());
|
||||
mMountPoint.setText(userFeedBroadcast.getMount());
|
||||
mPassword.setText(userFeedBroadcast.getPassword());
|
||||
mFeedID.setText(String.valueOf(userFeedBroadcast.getFeedId()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import com.radioreference.api.soap2.UserFeedBroadcast;
|
||||
import io.github.dsheirer.radioreference.RadioReferenceService;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.xml.rpc.ServiceException;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.net.UnknownHostException;
|
||||
import java.rmi.RemoteException;
|
||||
|
||||
public class UserFeedSelectionDialog extends JDialog implements DocumentListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( UserFeedSelectionDialog.class );
|
||||
|
||||
private Listener<UserFeedBroadcast> mListener;
|
||||
private JTextField mUserNameText;
|
||||
private JTextField mPasswordText;
|
||||
private JButton mLookupButton;
|
||||
private JList mUserFeedBroadcastList;
|
||||
private JButton mOKButton;
|
||||
|
||||
public UserFeedSelectionDialog(Listener<UserFeedBroadcast> listener)
|
||||
{
|
||||
mListener = listener;
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
JPanel panel = new JPanel();
|
||||
panel.setLayout(new MigLayout("","[align right,grow,fill][align left,grow,fill][]","[][][][][grow][]"));
|
||||
|
||||
panel.add(new JLabel("Please enter your credentials"), "align left,span");
|
||||
|
||||
panel.add(new JLabel("User Name:"));
|
||||
mUserNameText = new JTextField();
|
||||
mUserNameText.getDocument().addDocumentListener(this);
|
||||
panel.add(mUserNameText, "wrap");
|
||||
|
||||
panel.add(new JLabel("Password:"));
|
||||
mPasswordText = new JTextField();
|
||||
mPasswordText.getDocument().addDocumentListener(this);
|
||||
panel.add(mPasswordText, "wrap");
|
||||
|
||||
mLookupButton = new JButton("Get Feeds");
|
||||
mLookupButton.setEnabled(false);
|
||||
mLookupButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
mLookupButton.setEnabled(false);
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
RadioReferenceService service =
|
||||
new RadioReferenceService(mUserNameText.getText(), mPasswordText.getText());
|
||||
|
||||
UserFeedBroadcast[] feeds = service.getUserFeedBroadcasts();
|
||||
|
||||
mUserFeedBroadcastList.setListData(feeds);
|
||||
}
|
||||
catch(ServiceException se)
|
||||
{
|
||||
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
|
||||
"Unable to create radio reference web service client. See log file.",
|
||||
"Error", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mLog.error("Error creating the radio reference web service client", se);
|
||||
}
|
||||
catch(RemoteException re)
|
||||
{
|
||||
Throwable cause = re.getCause();
|
||||
|
||||
if(cause == null)
|
||||
{
|
||||
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
|
||||
re.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
else if(cause instanceof UnknownHostException)
|
||||
{
|
||||
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
|
||||
"Broadcastify server is unavailable", "No Server",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
else
|
||||
{
|
||||
JOptionPane.showMessageDialog(UserFeedSelectionDialog.this,
|
||||
"An unknown error has occurred. Please see log file.", "" +
|
||||
"Unknown Error", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mLog.error("Error retrieving user broadcast feed configurations from radio reference " +
|
||||
"service: ", re);
|
||||
}
|
||||
}
|
||||
|
||||
mLookupButton.setEnabled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
panel.add(mLookupButton,"grow,span");
|
||||
|
||||
mUserFeedBroadcastList = new JList();
|
||||
mUserFeedBroadcastList.setVisibleRowCount(3);
|
||||
mUserFeedBroadcastList.setLayoutOrientation(JList.VERTICAL);
|
||||
mUserFeedBroadcastList.setCellRenderer(new CellRenderer());
|
||||
mUserFeedBroadcastList.addListSelectionListener(new ListSelectionListener()
|
||||
{
|
||||
@Override
|
||||
public void valueChanged(ListSelectionEvent e)
|
||||
{
|
||||
mOKButton.setEnabled(mUserFeedBroadcastList.getSelectedValue() != null);
|
||||
}
|
||||
});
|
||||
|
||||
JScrollPane listScrollPane = new JScrollPane(mUserFeedBroadcastList);
|
||||
panel.add(listScrollPane, "grow,span");
|
||||
|
||||
mOKButton = new JButton("Ok");
|
||||
mOKButton.setEnabled(false);
|
||||
mOKButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive((UserFeedBroadcast)mUserFeedBroadcastList.getSelectedValue());
|
||||
}
|
||||
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
panel.add(mOKButton,"grow");
|
||||
|
||||
JButton cancelButton = new JButton("Cancel");
|
||||
cancelButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
panel.add(cancelButton, "wrap");
|
||||
|
||||
setTitle("Broadcastify Feed Lookup");
|
||||
setLayout(new BorderLayout());
|
||||
add(panel, BorderLayout.CENTER);
|
||||
setSize(300,250);
|
||||
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
setModalityType(ModalityType.APPLICATION_MODAL);
|
||||
}
|
||||
|
||||
private void updateButtons()
|
||||
{
|
||||
String username = mUserNameText.getText();
|
||||
String password = mPasswordText.getText();
|
||||
|
||||
mLookupButton.setEnabled(username != null && !username.isEmpty() && password != null && !password.isEmpty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e)
|
||||
{
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e)
|
||||
{
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e)
|
||||
{
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
private class CellRenderer extends JLabel implements ListCellRenderer<UserFeedBroadcast>
|
||||
{
|
||||
public CellRenderer()
|
||||
{
|
||||
setOpaque(true);
|
||||
setHorizontalAlignment(LEFT);
|
||||
setVerticalAlignment(CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<? extends UserFeedBroadcast> list,
|
||||
UserFeedBroadcast value,
|
||||
int index,
|
||||
boolean isSelected,
|
||||
boolean cellHasFocus)
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
setBackground(list.getSelectionBackground());
|
||||
setForeground(list.getSelectionForeground());
|
||||
}
|
||||
else
|
||||
{
|
||||
setBackground(list.getBackground());
|
||||
setForeground(list.getForeground());
|
||||
}
|
||||
|
||||
setText(value.getDescr());
|
||||
|
||||
return this; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
|
||||
public abstract class IcecastAudioBroadcaster extends AudioBroadcaster
|
||||
{
|
||||
private IBroadcastMetadataUpdater mMetadataUpdater;
|
||||
|
||||
public IcecastAudioBroadcaster(BroadcastConfiguration broadcastConfiguration)
|
||||
{
|
||||
super(broadcastConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Icecast broadcast configuration
|
||||
*/
|
||||
protected IcecastConfiguration getConfiguration()
|
||||
{
|
||||
return (IcecastConfiguration) getBroadcastConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IBroadcastMetadataUpdater getMetadataUpdater()
|
||||
{
|
||||
if(mMetadataUpdater == null)
|
||||
{
|
||||
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration());
|
||||
}
|
||||
|
||||
return mMetadataUpdater;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.http.HttpClientCodec;
|
||||
import org.apache.mina.http.HttpRequestImpl;
|
||||
import org.apache.mina.http.api.HttpMethod;
|
||||
import org.apache.mina.http.api.HttpRequest;
|
||||
import org.apache.mina.http.api.HttpVersion;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(IcecastBroadcastMetadataUpdater.class);
|
||||
private final static String UTF8 = "UTF-8";
|
||||
|
||||
private IcecastConfiguration mIcecastConfiguration;
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private AtomicBoolean mUpdating = new AtomicBoolean();
|
||||
private Queue<String> mMetadataQueue = new LinkedTransferQueue<>();
|
||||
private Map<String,String> mHTTPHeaders;
|
||||
private boolean mStackTraceLoggingSuppressed;
|
||||
|
||||
/**
|
||||
* Icecast song metadata updater. Each metadata update is processed in the order received and the dispatch of
|
||||
* the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
|
||||
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
|
||||
* sequence, those updates will be queued and processed in the order received.
|
||||
*/
|
||||
public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration)
|
||||
{
|
||||
mIcecastConfiguration = icecastConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket connector - lazy constructor.
|
||||
*/
|
||||
private NioSocketConnector getSocketConnector()
|
||||
{
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
// mSocketConnector.getFilterChain().addLast("logger", new LoggingFilter(IcecastBroadcastMetadataUpdater.class));
|
||||
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
|
||||
|
||||
//Each metadata update session is single-use, so we'll shut it down upon success or failure
|
||||
mSocketConnector.setHandler(new IoHandlerAdapter()
|
||||
{
|
||||
@Override
|
||||
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
||||
{
|
||||
//Single-use session - close it after we receive an error
|
||||
session.closeNow();
|
||||
|
||||
mLog.error("Metadata update failed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object message) throws Exception
|
||||
{
|
||||
//Single-use session - close it after we receive a response
|
||||
session.closeNow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mSocketConnector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a song metadata update to the remote server. Additional metadata updates received while the updater
|
||||
* is in the process of sending an existing metadata update will be queued and processed in received order.
|
||||
*
|
||||
* Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
|
||||
* and will remain in the update queue until the next metadata update is requested. However, this is a design
|
||||
* trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
|
||||
*/
|
||||
public void update(Metadata metadata)
|
||||
{
|
||||
mMetadataQueue.offer(getSong(metadata));
|
||||
|
||||
if(mUpdating.compareAndSet(false, true))
|
||||
{
|
||||
String song = mMetadataQueue.poll();
|
||||
|
||||
while(song != null)
|
||||
{
|
||||
HttpRequest updateRequest = createUpdateRequest(song);
|
||||
|
||||
if(updateRequest != null)
|
||||
{
|
||||
ThreadPool.SCHEDULED.schedule(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectFuture connectFuture = getSocketConnector()
|
||||
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
|
||||
mIcecastConfiguration.getPort()));
|
||||
connectFuture.awaitUninterruptibly();
|
||||
IoSession session = connectFuture.getSession();
|
||||
|
||||
if(session != null)
|
||||
{
|
||||
session.write(updateRequest);
|
||||
}
|
||||
}
|
||||
catch(UnresolvedAddressException uae)
|
||||
{
|
||||
//Do nothing - the server is temporarily unavailable
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
//Do nothing, the server is unavailable
|
||||
}
|
||||
else if(throwableCause instanceof UnresolvedAddressException)
|
||||
{
|
||||
//Do nothing - the server is temporarily unavailable
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!mStackTraceLoggingSuppressed)
|
||||
{
|
||||
mLog.error("Error sending metadata update. Future errors will " +
|
||||
"be suppressed", e);
|
||||
|
||||
mStackTraceLoggingSuppressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0l, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
//Fetch next metadata update to send
|
||||
song = mMetadataQueue.poll();
|
||||
}
|
||||
|
||||
mUpdating.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the song information for a metadata update
|
||||
*/
|
||||
private static String getSong(Metadata metadata)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(metadata != null)
|
||||
{
|
||||
String to = metadata.getPrimaryAddressTo().getIdentifier();
|
||||
|
||||
sb.append("TO:");
|
||||
|
||||
if(metadata.getPrimaryAddressTo().hasAlias())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressTo().getAlias().getName());
|
||||
}
|
||||
else if(metadata.getPrimaryAddressTo().hasIdentifier())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressTo().getIdentifier());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("UNKNOWN");
|
||||
}
|
||||
|
||||
|
||||
sb.append(" FROM:");
|
||||
|
||||
if(metadata.getPrimaryAddressFrom().hasAlias())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressFrom().getAlias().getName());
|
||||
}
|
||||
else if(metadata.getPrimaryAddressFrom().hasIdentifier())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressFrom().getIdentifier());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("UNKNOWN");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("Scanning ....");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTTP GET request to update the metadata on the remote server
|
||||
*/
|
||||
private HttpRequest createUpdateRequest(String song)
|
||||
{
|
||||
try
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("mode=updinfo");
|
||||
sb.append("&mount=").append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), UTF8));
|
||||
sb.append("&charset=UTF%2d8");
|
||||
sb.append("&song=").append(URLEncoder.encode(song, UTF8));
|
||||
|
||||
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.GET, "/admin/metadata",
|
||||
sb.toString(), getHTTPHeaders());
|
||||
|
||||
return request;
|
||||
}
|
||||
catch(UnsupportedEncodingException e)
|
||||
{
|
||||
//This should never happen
|
||||
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP headers to use for a metadata update request.
|
||||
*/
|
||||
private Map<String,String> getHTTPHeaders()
|
||||
{
|
||||
if(mHTTPHeaders == null)
|
||||
{
|
||||
mHTTPHeaders = new HashMap<>();
|
||||
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), mIcecastConfiguration.getBase64EncodedCredentials());
|
||||
}
|
||||
|
||||
return mHTTPHeaders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.util.Base64;
|
||||
|
||||
public abstract class IcecastConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
protected String mUserName = "source";
|
||||
private String mMountPoint = "/stream";
|
||||
private String mDescription;
|
||||
private String mGenre;
|
||||
private boolean mPublic;
|
||||
private int mBitRate = 16;
|
||||
private int mChannels = 1;
|
||||
private int mSampleRate = 8000;
|
||||
private String mURL;
|
||||
|
||||
public IcecastConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
}
|
||||
|
||||
public IcecastConfiguration()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encoded version of the username and password with prepended 'Basic ' tag.
|
||||
*/
|
||||
@XmlTransient
|
||||
public String getBase64EncodedCredentials()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(hasUserName())
|
||||
{
|
||||
sb.append(getUserName()).append(":").append(getPassword());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(getPassword());
|
||||
}
|
||||
|
||||
String base64 = Base64.getEncoder().encodeToString(sb.toString().getBytes());
|
||||
|
||||
return "Basic " + base64;
|
||||
}
|
||||
|
||||
@XmlAttribute( name="user_name" )
|
||||
public String getUserName()
|
||||
{
|
||||
return mUserName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName)
|
||||
{
|
||||
mUserName = userName;
|
||||
}
|
||||
|
||||
public boolean hasUserName()
|
||||
{
|
||||
return mUserName != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount point or path to the stream
|
||||
* @return mount point
|
||||
*/
|
||||
@XmlAttribute( name="mount_point" )
|
||||
public String getMountPoint()
|
||||
{
|
||||
return mMountPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mount point (path) for the stream
|
||||
* @param mountPoint
|
||||
*/
|
||||
public void setMountPoint(String mountPoint)
|
||||
{
|
||||
if(mountPoint != null && !mountPoint.isEmpty())
|
||||
{
|
||||
if(mountPoint.startsWith("/"))
|
||||
{
|
||||
mMountPoint = mountPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
mMountPoint = "/" + mountPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasMountPoint()
|
||||
{
|
||||
return mMountPoint != null;
|
||||
}
|
||||
|
||||
@XmlAttribute( name="description" )
|
||||
public String getDescription()
|
||||
{
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
public void setDescription(String description)
|
||||
{
|
||||
mDescription = description;
|
||||
}
|
||||
|
||||
public boolean hasDescription()
|
||||
{
|
||||
return mDescription != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream genre
|
||||
*/
|
||||
@XmlAttribute( name="genre" )
|
||||
public String getGenre()
|
||||
{
|
||||
return mGenre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream genre
|
||||
* @param genre
|
||||
*/
|
||||
public void setGenre(String genre)
|
||||
{
|
||||
mGenre = genre;
|
||||
}
|
||||
|
||||
public boolean hasGenre()
|
||||
{
|
||||
return mGenre != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public visibility of the broadcastAudio
|
||||
*/
|
||||
@XmlAttribute( name="public" )
|
||||
public boolean isPublic()
|
||||
{
|
||||
return mPublic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets public visibility of the broadcastAudio
|
||||
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||
*/
|
||||
public void setPublic(boolean isPublic)
|
||||
{
|
||||
mPublic = isPublic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of audio channels in the broadcastAudio
|
||||
*/
|
||||
@XmlAttribute( name="channels" )
|
||||
public int getChannels()
|
||||
{
|
||||
return mChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of audio channels in the broadcastAudio
|
||||
*/
|
||||
public void setChannels(int channels)
|
||||
{
|
||||
mChannels = channels;
|
||||
}
|
||||
|
||||
public boolean hasChannels()
|
||||
{
|
||||
return mChannels > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit rate in bits per second
|
||||
*/
|
||||
@XmlAttribute( name="bitrate" )
|
||||
public int getBitRate()
|
||||
{
|
||||
return mBitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit rate
|
||||
* @param bitRate in samples per second
|
||||
*/
|
||||
public void setBitRate(int bitRate)
|
||||
{
|
||||
mBitRate = bitRate;
|
||||
}
|
||||
|
||||
public boolean hasBitRate()
|
||||
{
|
||||
return mBitRate > 0;
|
||||
}
|
||||
|
||||
@XmlAttribute( name="sample_rate" )
|
||||
public int getSampleRate()
|
||||
{
|
||||
return mSampleRate;
|
||||
}
|
||||
|
||||
public void setSampleRate(int sampleRate)
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
public boolean hasSampleRate()
|
||||
{
|
||||
return mSampleRate > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL associated with the broadcastAudio where users can find additional details.
|
||||
*/
|
||||
@XmlAttribute( name="url" )
|
||||
public String getURL()
|
||||
{
|
||||
return mURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL associated with the broadcastAudio where users can find additional details.
|
||||
* @param url
|
||||
*/
|
||||
public void setURL(String url)
|
||||
{
|
||||
mURL = url;
|
||||
}
|
||||
|
||||
public boolean hasURL()
|
||||
{
|
||||
return mURL != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
if(!super.isValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(getUserName() == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(getMountPoint() == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(getChannels() != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(getSampleRate() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if(getBitRate() <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolDecoderException;
|
||||
import org.apache.mina.http.HttpClientCodec;
|
||||
import org.apache.mina.http.HttpException;
|
||||
import org.apache.mina.http.HttpRequestImpl;
|
||||
import org.apache.mina.http.api.DefaultHttpResponse;
|
||||
import org.apache.mina.http.api.HttpMethod;
|
||||
import org.apache.mina.http.api.HttpStatus;
|
||||
import org.apache.mina.http.api.HttpVersion;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
private Map<String,String> mHTTPHeaders;
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Creates an Icecast 2.4.x compatible broadcaster using HTTP 1.1 protocol. This broadcaster is
|
||||
* compatible with Icecast version 2.4.x and newer versions of the server software.
|
||||
*
|
||||
* Note: use @see IcecastTCPAudioBroadcaster for Icecast version 2.3.x and older.
|
||||
*
|
||||
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
|
||||
* IcecastHTTPIOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
|
||||
*
|
||||
* @param configuration for the Icecast stream
|
||||
*/
|
||||
public IcecastHTTPAudioBroadcaster(IcecastHTTPConfiguration configuration)
|
||||
{
|
||||
super(configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the audio frame or sequence
|
||||
*/
|
||||
@Override
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
{
|
||||
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
|
||||
{
|
||||
IoBuffer buffer = IoBuffer.allocate(audio.length);
|
||||
buffer.put(audio);
|
||||
buffer.flip();
|
||||
|
||||
mStreamingSession.write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
|
||||
* is currently connected to the remote server following any connection attempts.
|
||||
*
|
||||
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
|
||||
*
|
||||
* @return true if the audio handler can stream audio
|
||||
*/
|
||||
private boolean connect()
|
||||
{
|
||||
if(!connected() && canConnect() &&
|
||||
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
|
||||
mConnecting.compareAndSet(false, true))
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(10000);
|
||||
|
||||
// mSocketConnector.getFilterChain().addLast("logger",
|
||||
// new LoggingFilter(IcecastTCPAudioBroadcaster.class));
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec());
|
||||
mSocketConnector.setHandler(new IcecastHTTPIOHandler());
|
||||
}
|
||||
|
||||
mStreamingSession = null;
|
||||
|
||||
Runnable runnable = new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
{
|
||||
ConnectFuture future = mSocketConnector
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
future.awaitUninterruptibly();
|
||||
mStreamingSession = future.getSession();
|
||||
}
|
||||
catch(RuntimeIoException rie)
|
||||
{
|
||||
Throwable throwableCause = rie.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
}
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("Failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("Failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
|
||||
|
||||
}
|
||||
|
||||
return connected();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
if(mStreamingSession != null)
|
||||
{
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IO Handler for managing Icecast HTTP connection and credentials
|
||||
*/
|
||||
public class IcecastHTTPIOHandler extends IoHandlerAdapter
|
||||
{
|
||||
@Override
|
||||
public void sessionOpened(IoSession session) throws Exception
|
||||
{
|
||||
//Send stream configuration and user credentials upon connecting to remote server
|
||||
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_1, HttpMethod.PUT,
|
||||
getConfiguration().getMountPoint(), "", getHTTPHeaders());
|
||||
|
||||
session.write(request);
|
||||
}
|
||||
|
||||
private Map<String,String> getHTTPHeaders()
|
||||
{
|
||||
if(mHTTPHeaders == null)
|
||||
{
|
||||
mHTTPHeaders = new HashMap<>();
|
||||
mHTTPHeaders.put(IcecastHeader.ACCEPT.getValue(), "*/*");
|
||||
mHTTPHeaders.put(IcecastHeader.CONTENT_TYPE.getValue(), getConfiguration().getBroadcastFormat().getValue());
|
||||
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getBase64EncodedCredentials());
|
||||
mHTTPHeaders.put(IcecastHeader.PUBLIC.getValue(), getConfiguration().isPublic() ? "1" : "0");
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
|
||||
sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
|
||||
sb.append("channels=").append(getConfiguration().getChannels());
|
||||
mHTTPHeaders.put(IcecastHeader.AUDIO_INFO.getValue(), sb.toString());
|
||||
|
||||
if(getConfiguration().hasName())
|
||||
{
|
||||
mHTTPHeaders.put(IcecastHeader.NAME.getValue(), getConfiguration().getName());
|
||||
}
|
||||
|
||||
if(getConfiguration().hasDescription())
|
||||
{
|
||||
mHTTPHeaders.put(IcecastHeader.DESCRIPTION.getValue(), getConfiguration().getDescription());
|
||||
}
|
||||
|
||||
if(getConfiguration().hasURL())
|
||||
{
|
||||
mHTTPHeaders.put(IcecastHeader.URL.getValue(), getConfiguration().getURL());
|
||||
}
|
||||
|
||||
if(getConfiguration().hasGenre())
|
||||
{
|
||||
mHTTPHeaders.put(IcecastHeader.GENRE.getValue(), getConfiguration().getGenre());
|
||||
}
|
||||
|
||||
|
||||
if(getConfiguration().hasBitRate())
|
||||
{
|
||||
mHTTPHeaders.put(IcecastHeader.BITRATE.getValue(), String.valueOf(getConfiguration().getBitRate()));
|
||||
}
|
||||
}
|
||||
|
||||
return mHTTPHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mSocketConnector.dispose();
|
||||
mStreamingSession = null;
|
||||
mSocketConnector = null;
|
||||
|
||||
mConnecting.set(false);
|
||||
|
||||
super.sessionClosed(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(IoSession session, Throwable throwable) throws Exception
|
||||
{
|
||||
if(throwable instanceof ProtocolDecoderException)
|
||||
{
|
||||
Throwable cause = ((ProtocolDecoderException) throwable).getCause();
|
||||
|
||||
if(cause instanceof HttpException &&
|
||||
((HttpException) cause).getStatusCode() == HttpStatus.CLIENT_ERROR_LENGTH_REQUIRED.code())
|
||||
{
|
||||
//Ignore - Icecast 2.4 sometimes doesn't send any headers with their HTTP responses. Mina expects a
|
||||
//content-length for HTTP responses.
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("HTTP protocol decoder error", throwable);
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Broadcast error", throwable);
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object object) throws Exception
|
||||
{
|
||||
if(object instanceof DefaultHttpResponse)
|
||||
{
|
||||
DefaultHttpResponse response = (DefaultHttpResponse) object;
|
||||
|
||||
switch(response.getStatus())
|
||||
{
|
||||
case INFORMATIONAL_CONTINUE:
|
||||
break;
|
||||
case SUCCESS_OK:
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnecting.set(false);
|
||||
break;
|
||||
case CLIENT_ERROR_UNAUTHORIZED:
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
disconnect();
|
||||
break;
|
||||
case CLIENT_ERROR_FORBIDDEN:
|
||||
setBroadcastState(BroadcastState.CONFIGURATION_ERROR);
|
||||
disconnect();
|
||||
break;
|
||||
default:
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Icecast HTTP broadcaster - unrecognized message [ " + object.getClass() +
|
||||
"] received:" + object.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
|
||||
public class IcecastHTTPConfiguration extends IcecastConfiguration
|
||||
{
|
||||
public IcecastHTTPConfiguration()
|
||||
{
|
||||
//no-arg JAXB constructor
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Icecast 2.4.x+ compatible configuration
|
||||
*/
|
||||
public IcecastHTTPConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
setPassword("change me!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
IcecastHTTPConfiguration copy = new IcecastHTTPConfiguration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Icecast Configuration Parameters
|
||||
copy.setUserName(getUserName());
|
||||
copy.setMountPoint(getMountPoint());
|
||||
copy.setDescription(getDescription());
|
||||
copy.setGenre(getGenre());
|
||||
copy.setPublic(isPublic());
|
||||
copy.setBitRate(getBitRate());
|
||||
copy.setChannels(getChannels());
|
||||
copy.setSampleRate(getSampleRate());
|
||||
copy.setURL(getURL());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.ICECAST_HTTP;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class IcecastHTTPConfigurationEditor extends BroadcastConfigurationEditor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastHTTPConfigurationEditor.class );
|
||||
|
||||
private static final int ONE_MINUTE_MS = 60000;
|
||||
|
||||
private JTextField mName;
|
||||
private JTextField mServer;
|
||||
private JTextField mPort;
|
||||
private JTextField mMountPoint;
|
||||
private JTextField mUsername;
|
||||
private JTextField mPassword;
|
||||
private JTextField mDescription;
|
||||
private JTextField mGenre;
|
||||
private JCheckBox mPublic;
|
||||
private JSlider mDelay;
|
||||
private JLabel mDelayValue;
|
||||
private JSlider mMaximumRecordingAge;
|
||||
private JLabel mMaximumRecordingAgeValue;
|
||||
private JCheckBox mEnabled;
|
||||
private JButton mSaveButton;
|
||||
private JButton mResetButton;
|
||||
|
||||
public IcecastHTTPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
|
||||
{
|
||||
super(broadcastModel, aliasModel, iconManager);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
|
||||
"[][][][][][][][][][][][][][][][][grow,fill]" ) );
|
||||
setPreferredSize(new Dimension(150,400));
|
||||
|
||||
JLabel channelLabel = new JLabel("Icecast 2 (v2.4+) Stream");
|
||||
|
||||
ImageIcon icon = mIconManager.getScaledIcon(new ImageIcon(BroadcastServerType.ICECAST_HTTP.getIconPath()), 25);
|
||||
channelLabel.setIcon(icon);
|
||||
|
||||
add(channelLabel, "span, align center");
|
||||
|
||||
add(new JLabel("Name:"));
|
||||
mName = new JTextField();
|
||||
mName.getDocument().addDocumentListener(this);
|
||||
mName.setToolTipText("Unique name for this stream");
|
||||
add(mName);
|
||||
|
||||
add(new JLabel("Server:"));
|
||||
mServer = new JTextField();
|
||||
mServer.getDocument().addDocumentListener(this);
|
||||
mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
|
||||
add(mServer);
|
||||
|
||||
add(new JLabel("Port:"));
|
||||
mPort = new JTextField();
|
||||
mPort.getDocument().addDocumentListener(this);
|
||||
mPort.setToolTipText("Server port number (e.g. 80)");
|
||||
add(mPort);
|
||||
|
||||
add(new JLabel("Mount:"));
|
||||
mMountPoint = new JTextField();
|
||||
mMountPoint.getDocument().addDocumentListener(this);
|
||||
mMountPoint.setToolTipText("Mount point (e.g. /12345)");
|
||||
add(mMountPoint);
|
||||
|
||||
add(new JLabel("User Name:"));
|
||||
mUsername = new JTextField();
|
||||
mUsername.getDocument().addDocumentListener(this);
|
||||
mUsername.setToolTipText("User name for the stream. Default: source");
|
||||
add(mUsername);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
mPassword = new JTextField();
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
mPassword.setToolTipText("Password for the stream");
|
||||
add(mPassword);
|
||||
|
||||
add(new JLabel("Description:"));
|
||||
mDescription = new JTextField();
|
||||
mDescription.getDocument().addDocumentListener(this);
|
||||
mDescription.setToolTipText("Description of the stream contents");
|
||||
add(mDescription);
|
||||
|
||||
add(new JLabel("Genre:"));
|
||||
mGenre = new JTextField();
|
||||
mGenre.getDocument().addDocumentListener(this);
|
||||
mGenre.setToolTipText("Genre (e.g. public safety)");
|
||||
add(mGenre);
|
||||
|
||||
add(new JLabel("Public:"));
|
||||
mPublic = new JCheckBox();
|
||||
mPublic.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mPublic.setToolTipText("Designate stream as public (checked) or private");
|
||||
add(mPublic);
|
||||
|
||||
add(new JLabel());
|
||||
mDelayValue = new JLabel("0 Minutes");
|
||||
add(mDelayValue);
|
||||
|
||||
add(new JLabel("Delay:"));
|
||||
mDelay = new JSlider(0,60,0);
|
||||
mDelay.setMajorTickSpacing(10);
|
||||
mDelay.setMinorTickSpacing(5);
|
||||
mDelay.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mDelayValue.setText(mDelay.getValue() + " Minutes");
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mDelay.setToolTipText("Audio broadcast delay in minutes");
|
||||
add(mDelay);
|
||||
|
||||
add(new JLabel());
|
||||
mMaximumRecordingAgeValue = new JLabel("1 Minute");
|
||||
add(mMaximumRecordingAgeValue);
|
||||
|
||||
add(new JLabel("Age Limit:"));
|
||||
mMaximumRecordingAge = new JSlider(1,60,5);
|
||||
mMaximumRecordingAge.setMajorTickSpacing(10);
|
||||
mMaximumRecordingAge.setMinorTickSpacing(5);
|
||||
mMaximumRecordingAge.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
int value = mMaximumRecordingAge.getValue();
|
||||
|
||||
mMaximumRecordingAgeValue.setText(value + " Minute" + (value > 1 ? "s" : ""));
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mMaximumRecordingAge.setToolTipText("Maximum recording age (in addition to delay) to stream");
|
||||
add(mMaximumRecordingAge);
|
||||
|
||||
add(new JLabel("Enabled:"));
|
||||
mEnabled = new JCheckBox();
|
||||
mEnabled.setToolTipText("Enable (checked) or disable this stream");
|
||||
mEnabled.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
add(mEnabled);
|
||||
|
||||
mSaveButton = new JButton("Save");
|
||||
mSaveButton.setEnabled(false);
|
||||
mSaveButton.setToolTipText("Click to save configuration information");
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
save();
|
||||
}
|
||||
});
|
||||
add(mSaveButton);
|
||||
|
||||
mResetButton = new JButton("Reset");
|
||||
mResetButton.setEnabled(false);
|
||||
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
|
||||
mResetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(false);
|
||||
setItem(getItem());
|
||||
}
|
||||
});
|
||||
add(mResetButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModified(boolean modified)
|
||||
{
|
||||
super.setModified(modified);
|
||||
|
||||
mSaveButton.setEnabled(modified);
|
||||
mResetButton.setEnabled(modified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
|
||||
|
||||
mName.setText(config.getName());
|
||||
mServer.setText(config.getHost());
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
mMountPoint.setText(config.getMountPoint());
|
||||
mUsername.setText(config.getUserName());
|
||||
mPassword.setText(config.getPassword());
|
||||
mDescription.setText(config.getDescription());
|
||||
mGenre.setText(config.getGenre());
|
||||
mPublic.setSelected(config.isPublic());
|
||||
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
|
||||
mMaximumRecordingAge.setValue((int)config.getMaximumRecordingAge() / ONE_MINUTE_MS);
|
||||
mEnabled.setSelected(config.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
mName.setText(null);
|
||||
mServer.setText(null);
|
||||
mPort.setText(null);
|
||||
mMountPoint.setText(null);
|
||||
mUsername.setText(null);
|
||||
mPassword.setText(null);
|
||||
mDescription.setText(null);
|
||||
mGenre.setText(null);
|
||||
mPublic.setSelected(true);
|
||||
mDelay.setValue(0);
|
||||
mMaximumRecordingAge.setValue(1);
|
||||
mEnabled.setSelected(false);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
IcecastHTTPConfiguration config = (IcecastHTTPConfiguration)getItem();
|
||||
|
||||
if(validateConfiguration())
|
||||
{
|
||||
updateConfigurationName(config, mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
config.setPort(getPort());
|
||||
config.setMountPoint(mMountPoint.getText());
|
||||
config.setUserName(mUsername.getText());
|
||||
config.setPassword(mPassword.getText());
|
||||
config.setDescription(mDescription.getText());
|
||||
config.setGenre(mGenre.getText());
|
||||
config.setPublic(mPublic.isSelected());
|
||||
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
|
||||
config.setMaximumRecordingAge(mMaximumRecordingAge.getValue() * ONE_MINUTE_MS);
|
||||
config.setEnabled(mEnabled.isSelected());
|
||||
|
||||
setModified(false);
|
||||
|
||||
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user specified values in the controls prior to saving them to the configuration
|
||||
*/
|
||||
private boolean validateConfiguration()
|
||||
{
|
||||
String name = mName.getText();
|
||||
|
||||
//A unique stream name is required
|
||||
if(!mBroadcastModel.isUniqueName(name, getItem()))
|
||||
{
|
||||
String message = (name == null || name.isEmpty()) ?
|
||||
"Please specify a stream name." : "Stream name " + name +
|
||||
"is already in use.\nPlease choose another name.";
|
||||
|
||||
JOptionPane.showMessageDialog(IcecastHTTPConfigurationEditor.this,
|
||||
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mName.requestFocus();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Server address is optional, but required
|
||||
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Port is optional, but required
|
||||
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Mount Point is required
|
||||
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//User name is required
|
||||
if(!validateTextField(mUsername, "Invalid User Name", "Please specify a user name."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//Password is required
|
||||
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getPort()
|
||||
{
|
||||
String port = mPort.getText();
|
||||
|
||||
if(port != null && !port.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(port);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
public enum IcecastHeader
|
||||
{
|
||||
ACCEPT("Accept"),
|
||||
AUDIO_INFO("ice-audio-info"),
|
||||
AUTHORIZATION("Authorization"),
|
||||
BITRATE("ice-bitrate"),
|
||||
CONTENT_TYPE("Content-Type"),
|
||||
DESCRIPTION("ice-description"),
|
||||
EXPECT("Expect"),
|
||||
GENRE("ice-genre"),
|
||||
HOST("Host"),
|
||||
NAME("ice-name"),
|
||||
PUBLIC("ice-public"),
|
||||
URL("ice-url"),
|
||||
USER_AGENT("User-Agent"),
|
||||
|
||||
UNKNOWN("unknown");
|
||||
|
||||
private String mValue;
|
||||
|
||||
private IcecastHeader(String value)
|
||||
{
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
|
||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.core.write.WriteTimeoutException;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(IcecastTCPAudioBroadcaster.class);
|
||||
private final static String TERMINATOR = "\r\n";
|
||||
private final static String SEPARATOR = ":";
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
|
||||
private static final long CONNECTION_ATTEMPT_TIMEOUT = 5000; //5 seconds
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Creates an Icecast 2.3.2 compatible broadcaster using TCP and a pseudo HTTP 1.0 protocol. This broadcaster is
|
||||
* compatible with Icecast version 2.3.2 and older versions of the server software.
|
||||
*
|
||||
* Note: use @see IcecastHTTPAudioBroadcaster for Icecast version 2.4.x and newer.
|
||||
*
|
||||
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
|
||||
* ShoutcastV2IOHandler manages all interaction with the Icecast server and manages the overall broadcast state.
|
||||
*
|
||||
* @param configuration for the Icecast stream
|
||||
*/
|
||||
public IcecastTCPAudioBroadcaster(IcecastTCPConfiguration configuration)
|
||||
{
|
||||
super(configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the audio frame or sequence
|
||||
*/
|
||||
@Override
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
{
|
||||
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
|
||||
{
|
||||
mStreamingSession.write(audio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
|
||||
* is currently connected to the remote server following any connection attempts.
|
||||
*
|
||||
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
|
||||
*
|
||||
* @return true if the audio handler can stream audio
|
||||
*/
|
||||
private boolean connect()
|
||||
{
|
||||
if(!connected() && canConnect() &&
|
||||
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
|
||||
mConnecting.compareAndSet(false, true))
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
|
||||
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec",
|
||||
new ProtocolCodecFilter(new IcecastCodecFactory()));
|
||||
mSocketConnector.setHandler(new IcecastTCPIOHandler());
|
||||
}
|
||||
|
||||
mStreamingSession = null;
|
||||
|
||||
Runnable runnable = new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
{
|
||||
ConnectFuture future = mSocketConnector
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
|
||||
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
|
||||
mStreamingSession = future.getSession();
|
||||
}
|
||||
catch(UnresolvedAddressException uae)
|
||||
{
|
||||
//What do you do with this? Nothing.
|
||||
}
|
||||
catch(InterruptedException ie)
|
||||
{
|
||||
//We couldn't connect within the timeout period -- disconnect so that we can try again later
|
||||
disconnect();
|
||||
}
|
||||
catch(RuntimeIoException rie)
|
||||
{
|
||||
Throwable throwableCause = rie.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
}
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("[" + getStreamName() + "] failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
return connected();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
if(connected() && mStreamingSession != null)
|
||||
{
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IO Handler for managing Icecast TCP connection and credentials
|
||||
*/
|
||||
public class IcecastTCPIOHandler extends IoHandlerAdapter
|
||||
{
|
||||
/**
|
||||
* Sends stream configuration and user credentials upon connecting to remote server
|
||||
*/
|
||||
@Override
|
||||
public void sessionOpened(IoSession session) throws Exception
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("SOURCE ").append(getConfiguration().getMountPoint());
|
||||
sb.append(" HTTP/1.0").append(TERMINATOR);
|
||||
|
||||
sb.append("Authorization: ").append(getConfiguration().getBase64EncodedCredentials()).append(TERMINATOR);
|
||||
sb.append(IcecastHeader.USER_AGENT.getValue()).append(SEPARATOR)
|
||||
.append(SystemProperties.getInstance().getApplicationName()).append(TERMINATOR);
|
||||
sb.append(IcecastHeader.CONTENT_TYPE.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().getBroadcastFormat().getValue()).append(TERMINATOR);
|
||||
sb.append(IcecastHeader.PUBLIC.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().isPublic() ? "1" : "0").append(TERMINATOR);
|
||||
|
||||
sb.append(IcecastHeader.AUDIO_INFO.getValue()).append(SEPARATOR);
|
||||
sb.append("samplerate=").append(getConfiguration().getSampleRate()).append(";");
|
||||
sb.append("quality=").append(MP3AudioConverter.AUDIO_QUALITY).append(";");
|
||||
sb.append("channels=").append(getConfiguration().getChannels()).append(TERMINATOR);
|
||||
|
||||
if(getConfiguration().hasName())
|
||||
{
|
||||
sb.append(IcecastHeader.NAME.getValue()).append(SEPARATOR).append(getConfiguration().getName()).append(TERMINATOR);
|
||||
}
|
||||
|
||||
if(getConfiguration().hasGenre())
|
||||
{
|
||||
sb.append(IcecastHeader.GENRE.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().getGenre()).append(TERMINATOR);
|
||||
}
|
||||
|
||||
if(getConfiguration().hasDescription())
|
||||
{
|
||||
sb.append(IcecastHeader.DESCRIPTION.getValue()).append(SEPARATOR)
|
||||
.append(getConfiguration().getDescription()).append(TERMINATOR);
|
||||
}
|
||||
|
||||
sb.append(TERMINATOR).append(TERMINATOR);
|
||||
|
||||
session.write(sb.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mStreamingSession = null;
|
||||
|
||||
mConnecting.set(false);
|
||||
|
||||
super.sessionClosed(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
||||
{
|
||||
if(cause instanceof IOException)
|
||||
{
|
||||
IOException ioe = (IOException)cause;
|
||||
|
||||
if(ioe.getMessage() != null)
|
||||
{
|
||||
String reason = ioe.getMessage();
|
||||
|
||||
if(connected())
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else if(reason.startsWith("Connection reset"))
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else if(reason.startsWith("Operation timed out"))
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else if(ioe instanceof WriteTimeoutException)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object object) throws Exception
|
||||
{
|
||||
if(object instanceof String)
|
||||
{
|
||||
String message = (String) object;
|
||||
|
||||
if(message != null && !message.trim().isEmpty())
|
||||
{
|
||||
if(message.startsWith("HTTP/1.0 200 OK"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if(message.startsWith("HTTP/1.0 403 Mountpoint in use"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
|
||||
}
|
||||
else if(message.contains("Content-Type: text/html"))
|
||||
{
|
||||
//Disregard ... this normally follows the mount point in use error
|
||||
}
|
||||
else if(message.contains("Invalid Password") ||
|
||||
message.contains("Authentication Required"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
}
|
||||
else if(message.contains("HTTP/1.1 501"))
|
||||
{
|
||||
disconnect(); //So that we can reconnect later
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Unrecognized server response:" + message);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("[" + getStreamName() + "]Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
|
||||
"] received:" + object.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.xml.bind.annotation.XmlSeeAlso;
|
||||
|
||||
@XmlSeeAlso( {BroadcastifyConfiguration.class } )
|
||||
public class IcecastTCPConfiguration extends IcecastConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfiguration.class );
|
||||
|
||||
public IcecastTCPConfiguration()
|
||||
{
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Icecast 2.3.x and 2.4.x compatible configuration
|
||||
* @param format of audio
|
||||
*/
|
||||
public IcecastTCPConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
IcecastTCPConfiguration copy = new IcecastTCPConfiguration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Icecast Configuration Parameters
|
||||
copy.setUserName(getUserName());
|
||||
copy.setMountPoint(getMountPoint());
|
||||
copy.setDescription(getDescription());
|
||||
copy.setGenre(getGenre());
|
||||
copy.setPublic(isPublic());
|
||||
copy.setURL(getURL());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.ICECAST_TCP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class IcecastTCPConfigurationEditor extends BroadcastConfigurationEditor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( IcecastTCPConfigurationEditor.class );
|
||||
|
||||
private static final int ONE_MINUTE_MS = 60000;
|
||||
|
||||
private JTextField mName;
|
||||
private JTextField mServer;
|
||||
private JTextField mPort;
|
||||
private JTextField mMountPoint;
|
||||
private JTextField mPassword;
|
||||
private JTextField mDescription;
|
||||
private JTextField mGenre;
|
||||
private JCheckBox mPublic;
|
||||
private JSlider mDelay;
|
||||
private JLabel mDelayValue;
|
||||
private JSlider mMaximumRecordingAge;
|
||||
private JLabel mMaximumRecordingAgeValue;
|
||||
private JCheckBox mEnabled;
|
||||
private JButton mSaveButton;
|
||||
private JButton mResetButton;
|
||||
|
||||
public IcecastTCPConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
|
||||
{
|
||||
super(broadcastModel, aliasModel, iconManager);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "fill,wrap 2", "[align right][grow,fill]",
|
||||
"[][][][][][][][][][][][][][][][grow,fill]" ) );
|
||||
setPreferredSize(new Dimension(150,400));
|
||||
|
||||
JLabel channelLabel = new JLabel("Icecast (v2.3) Stream");
|
||||
|
||||
ImageIcon icon = mIconManager.getScaledIcon(new ImageIcon(BroadcastServerType.ICECAST_TCP.getIconPath()), 25);
|
||||
channelLabel.setIcon(icon);
|
||||
|
||||
add(channelLabel, "span, align center");
|
||||
|
||||
add(new JLabel("Name:"));
|
||||
mName = new JTextField();
|
||||
mName.getDocument().addDocumentListener(this);
|
||||
mName.setToolTipText("Unique name for this stream");
|
||||
add(mName);
|
||||
|
||||
add(new JLabel("Server:"));
|
||||
mServer = new JTextField();
|
||||
mServer.getDocument().addDocumentListener(this);
|
||||
mServer.setToolTipText("Server address (e.g. audio1.icecast.com or localhost)");
|
||||
add(mServer);
|
||||
|
||||
add(new JLabel("Port:"));
|
||||
mPort = new JTextField();
|
||||
mPort.getDocument().addDocumentListener(this);
|
||||
mPort.setToolTipText("Server port number (e.g. 80)");
|
||||
add(mPort);
|
||||
|
||||
add(new JLabel("Mount:"));
|
||||
mMountPoint = new JTextField();
|
||||
mMountPoint.getDocument().addDocumentListener(this);
|
||||
mMountPoint.setToolTipText("Mount point (e.g. /12345)");
|
||||
add(mMountPoint);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
mPassword = new JTextField();
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
mPassword.setToolTipText("Password for the stream");
|
||||
add(mPassword);
|
||||
|
||||
add(new JLabel("Description:"));
|
||||
mDescription = new JTextField();
|
||||
mDescription.getDocument().addDocumentListener(this);
|
||||
mDescription.setToolTipText("Description of the stream contents");
|
||||
add(mDescription);
|
||||
|
||||
add(new JLabel("Genre:"));
|
||||
mGenre = new JTextField();
|
||||
mGenre.getDocument().addDocumentListener(this);
|
||||
mGenre.setToolTipText("Genre (e.g. public safety)");
|
||||
add(mGenre);
|
||||
|
||||
add(new JLabel("Public:"));
|
||||
mPublic = new JCheckBox();
|
||||
mPublic.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mPublic.setToolTipText("Designate stream as public (checked) or private");
|
||||
add(mPublic);
|
||||
|
||||
add(new JLabel());
|
||||
mDelayValue = new JLabel("0 Minutes");
|
||||
add(mDelayValue);
|
||||
|
||||
add(new JLabel("Delay:"));
|
||||
mDelay = new JSlider(0,60,0);
|
||||
mDelay.setMajorTickSpacing(10);
|
||||
mDelay.setMinorTickSpacing(5);
|
||||
mDelay.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mDelayValue.setText(mDelay.getValue() + " Minutes");
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mDelay.setToolTipText("Audio broadcast delay in minutes");
|
||||
add(mDelay);
|
||||
|
||||
add(new JLabel());
|
||||
mMaximumRecordingAgeValue = new JLabel("1 Minute");
|
||||
add(mMaximumRecordingAgeValue);
|
||||
|
||||
add(new JLabel("Age Limit:"));
|
||||
mMaximumRecordingAge = new JSlider(1,60,5);
|
||||
mMaximumRecordingAge.setMajorTickSpacing(10);
|
||||
mMaximumRecordingAge.setMinorTickSpacing(5);
|
||||
mMaximumRecordingAge.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
int value = mMaximumRecordingAge.getValue();
|
||||
|
||||
mMaximumRecordingAgeValue.setText(value + " Minute" + (value > 1 ? "s" : ""));
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mMaximumRecordingAge.setToolTipText("Maximum recording age (in addition to delay) to stream");
|
||||
add(mMaximumRecordingAge);
|
||||
|
||||
add(new JLabel("Enabled:")); //Empty
|
||||
mEnabled = new JCheckBox();
|
||||
mEnabled.setToolTipText("Enable (checked) or disable this stream");
|
||||
mEnabled.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
add(mEnabled);
|
||||
|
||||
mSaveButton = new JButton("Save");
|
||||
mSaveButton.setEnabled(false);
|
||||
mSaveButton.setToolTipText("Click to save configuration information");
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
save();
|
||||
}
|
||||
});
|
||||
add(mSaveButton);
|
||||
|
||||
mResetButton = new JButton("Reset");
|
||||
mResetButton.setEnabled(false);
|
||||
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
|
||||
mResetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(false);
|
||||
setItem(getItem());
|
||||
}
|
||||
});
|
||||
add(mResetButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModified(boolean modified)
|
||||
{
|
||||
super.setModified(modified);
|
||||
|
||||
mSaveButton.setEnabled(modified);
|
||||
mResetButton.setEnabled(modified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
|
||||
|
||||
mName.setText(config.getName());
|
||||
mServer.setText(config.getHost());
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
mMountPoint.setText(config.getMountPoint());
|
||||
mPassword.setText(config.getPassword());
|
||||
mDescription.setText(config.getDescription());
|
||||
mGenre.setText(config.getGenre());
|
||||
mPublic.setSelected(config.isPublic());
|
||||
mDelay.setValue((int)(config.getDelay() / ONE_MINUTE_MS));
|
||||
mMaximumRecordingAge.setValue((int)config.getMaximumRecordingAge() / ONE_MINUTE_MS);
|
||||
mEnabled.setSelected(config.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
mName.setText(null);
|
||||
mServer.setText(null);
|
||||
mPort.setText(null);
|
||||
mMountPoint.setText(null);
|
||||
mPassword.setText(null);
|
||||
mDescription.setText(null);
|
||||
mGenre.setText(null);
|
||||
mPublic.setSelected(true);
|
||||
mDelay.setValue(0);
|
||||
mMaximumRecordingAge.setValue(1);
|
||||
mEnabled.setSelected(false);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
IcecastTCPConfiguration config = (IcecastTCPConfiguration)getItem();
|
||||
|
||||
if(validateConfiguration())
|
||||
{
|
||||
updateConfigurationName(config, mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
config.setPort(getPort());
|
||||
config.setMountPoint(mMountPoint.getText());
|
||||
config.setPassword(mPassword.getText());
|
||||
config.setDescription(mDescription.getText());
|
||||
config.setGenre(mGenre.getText());
|
||||
config.setPublic(mPublic.isSelected());
|
||||
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
|
||||
config.setMaximumRecordingAge(mMaximumRecordingAge.getValue() * ONE_MINUTE_MS);
|
||||
config.setEnabled(mEnabled.isSelected());
|
||||
|
||||
setModified(false);
|
||||
|
||||
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user specified values in the controls prior to saving them to the configuration
|
||||
*/
|
||||
private boolean validateConfiguration()
|
||||
{
|
||||
String name = mName.getText();
|
||||
|
||||
//A unique stream name is required
|
||||
if(!mBroadcastModel.isUniqueName(name, getItem()))
|
||||
{
|
||||
String message = (name == null || name.isEmpty()) ?
|
||||
"Please specify a stream name." : "Stream name " + name +
|
||||
"is already in use.\nPlease choose another name.";
|
||||
|
||||
JOptionPane.showMessageDialog(IcecastTCPConfigurationEditor.this,
|
||||
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mName.requestFocus();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Server address is optional, but required
|
||||
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Port is optional, but required
|
||||
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Mount Point is optional, but required
|
||||
if(!validateTextField(mMountPoint, "Invalid Mount Point", "Please specify a mount point."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Password is optional, but required
|
||||
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getPort()
|
||||
{
|
||||
String port = mPort.getText();
|
||||
|
||||
if(port != null && !port.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(port);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast.codec;
|
||||
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFactory;
|
||||
import org.apache.mina.filter.codec.ProtocolDecoder;
|
||||
import org.apache.mina.filter.codec.ProtocolEncoder;
|
||||
import org.apache.mina.filter.codec.textline.LineDelimiter;
|
||||
import org.apache.mina.filter.codec.textline.TextLineDecoder;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class IcecastCodecFactory implements ProtocolCodecFactory
|
||||
{
|
||||
private ProtocolEncoder mProtocolEncoder;
|
||||
private ProtocolDecoder mProtocolDecoder;
|
||||
|
||||
/**
|
||||
* Factory for codecs to process Icecast string protocol and byte array audio frames for use with Apache Mina
|
||||
*/
|
||||
public IcecastCodecFactory()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception
|
||||
{
|
||||
if(mProtocolEncoder == null)
|
||||
{
|
||||
mProtocolEncoder = new IcecastEncoder();
|
||||
}
|
||||
|
||||
return mProtocolEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception
|
||||
{
|
||||
if(mProtocolDecoder == null)
|
||||
{
|
||||
mProtocolDecoder = new TextLineDecoder(StandardCharsets.UTF_8, LineDelimiter.AUTO);
|
||||
}
|
||||
|
||||
return mProtocolDecoder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.icecast.codec;
|
||||
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.apache.mina.core.session.AttributeKey;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
|
||||
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
|
||||
import org.apache.mina.filter.codec.textline.LineDelimiter;
|
||||
import org.apache.mina.filter.codec.textline.TextLineEncoder;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class IcecastEncoder extends ProtocolEncoderAdapter
|
||||
{
|
||||
private static final AttributeKey ENCODER = new AttributeKey(TextLineEncoder.class, "encoder");
|
||||
private static final Charset mCharset = StandardCharsets.UTF_8;
|
||||
private static final int MAX_LINE_LENGTH = 2147483647;
|
||||
|
||||
/**
|
||||
* Icecast Protocol Encoder is an Apache Mina compliant protocol encoder that supports Icecast string-based
|
||||
* connection negotiation as well as byte array audio frame data.
|
||||
*/
|
||||
public IcecastEncoder()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception
|
||||
{
|
||||
//Audio frame data
|
||||
if(message instanceof byte[])
|
||||
{
|
||||
byte[] data = (byte[])message;
|
||||
|
||||
IoBuffer buf = IoBuffer.allocate(data.length).setAutoExpand(false);
|
||||
buf.put(data);
|
||||
buf.flip();
|
||||
out.write(buf);
|
||||
}
|
||||
//Icecast connection messages
|
||||
else if(message instanceof String)
|
||||
{
|
||||
CharsetEncoder encoder = (CharsetEncoder)session.getAttribute(ENCODER);
|
||||
|
||||
if(encoder == null)
|
||||
{
|
||||
encoder = mCharset.newEncoder();
|
||||
session.setAttribute(ENCODER, encoder);
|
||||
}
|
||||
|
||||
String value = (String)message;
|
||||
|
||||
IoBuffer buf = IoBuffer.allocate(value.length()).setAutoExpand(true);
|
||||
|
||||
buf.putString(value, encoder);
|
||||
|
||||
|
||||
if(buf.position() > MAX_LINE_LENGTH)
|
||||
{
|
||||
throw new IllegalArgumentException("Line length: " + buf.position());
|
||||
}
|
||||
else
|
||||
{
|
||||
buf.putString(LineDelimiter.UNIX.getValue(), encoder);
|
||||
buf.flip();
|
||||
out.write(buf);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("Can't encode - unrecognized object type: " + message.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
|
||||
public enum ShoutcastMetadata
|
||||
{
|
||||
AIM("icy-aim:"),
|
||||
AUDIO_BIT_RATE("icy-br:"),
|
||||
CHANNELS("icy-channels:"),
|
||||
CONTENT_TYPE("Content-Type:"),
|
||||
DESCRIPTION("icy-description:"),
|
||||
GENRE("icy-genre:"),
|
||||
ICE_AUDIO_INFO("ice-audio-info:"),
|
||||
ICQ("icy-icq:"),
|
||||
INTERNET_RELAY_CHAT("icy-irc:"),
|
||||
METADATA_INTERVAL("icy-metaint:"),
|
||||
METADATA_REQUEST("icy-metadata:"),
|
||||
PRE_BUFFER("icy-prebuffer:"),
|
||||
PUBLIC("icy-pub:"),
|
||||
RESET("icy-reset:"),
|
||||
SERVER("server:"),
|
||||
STREAM_NAME("icy-name:"),
|
||||
SONG_TITLE("icy-title:"),
|
||||
URL("icy-url:"),
|
||||
USER_AGENT("User-Agent:"),
|
||||
UNKNOWN("Unknown");
|
||||
|
||||
private String mTag;
|
||||
|
||||
private ShoutcastMetadata(String tag)
|
||||
{
|
||||
mTag = tag;
|
||||
}
|
||||
|
||||
public static final String COMMAND_TERMINATOR = "\n";
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return mTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the metadata tag and value with an end-of-line character, appropriate for sending over the network.
|
||||
*/
|
||||
public String encode(String value)
|
||||
{
|
||||
if(value != null && !value.isEmpty())
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(mTag).append(value).append(COMMAND_TERMINATOR);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String encode(boolean value)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(mTag).append(value ? "1" : "0").append(COMMAND_TERMINATOR);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String encode(BroadcastFormat broadcastFormat)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(mTag).append(broadcastFormat.getValue()).append(COMMAND_TERMINATOR);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public String encode(int value)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(mTag).append(String.valueOf(value)).append(COMMAND_TERMINATOR);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV1AudioBroadcaster extends AudioBroadcaster
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1AudioBroadcaster.class);
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
private IBroadcastMetadataUpdater mMetadataUpdater;
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Creates a Shoutcast v1 compatible broadcaster using TCP protocol.
|
||||
*
|
||||
* Note: use @see ShoutcastV2AudioBroadcaster for Shoutcast version 2.x and newer.
|
||||
*
|
||||
* This broadcaster uses the Apache Mina library for the streaming socket connection and for metadata updates. The
|
||||
* ShoutcastV1IOHandler manages all interaction with the Shoutcast server and manages the overall broadcast state.
|
||||
*
|
||||
* @param configuration for the Shoutcast stream
|
||||
*/
|
||||
public ShoutcastV1AudioBroadcaster(ShoutcastV1Configuration configuration)
|
||||
{
|
||||
super(configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shoutcast V1 broadcast configuration
|
||||
*/
|
||||
private ShoutcastV1Configuration getConfiguration()
|
||||
{
|
||||
return (ShoutcastV1Configuration) getBroadcastConfiguration();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IBroadcastMetadataUpdater getMetadataUpdater()
|
||||
{
|
||||
if(mMetadataUpdater == null)
|
||||
{
|
||||
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getConfiguration());
|
||||
}
|
||||
|
||||
return mMetadataUpdater;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the audio frame or sequence
|
||||
*/
|
||||
@Override
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
{
|
||||
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
|
||||
{
|
||||
IoBuffer buf = IoBuffer.allocate(audio.length).setAutoExpand(false);
|
||||
buf.put(audio);
|
||||
buf.flip();
|
||||
mStreamingSession.write(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
|
||||
* is currently connected to the remote server following any connection attempts.
|
||||
*
|
||||
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
|
||||
*
|
||||
* @return true if the audio handler can stream audio
|
||||
*/
|
||||
private boolean connect()
|
||||
{
|
||||
if(!connected() && canConnect() &&
|
||||
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
|
||||
mConnecting.compareAndSet(false, true))
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(10000);
|
||||
|
||||
// mSocketConnector.getFilterChain().addLast("logger",
|
||||
// new LoggingFilter(ShoutcastV1AudioBroadcaster.class));
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec",
|
||||
new ProtocolCodecFilter(new TextLineCodecFactory()));
|
||||
|
||||
mSocketConnector.setHandler(new ShoutcastIOHandler());
|
||||
}
|
||||
|
||||
mStreamingSession = null;
|
||||
|
||||
Runnable runnable = new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
{
|
||||
ConnectFuture future = mSocketConnector
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
future.awaitUninterruptibly();
|
||||
mStreamingSession = future.getSession();
|
||||
}
|
||||
catch(RuntimeIoException rie)
|
||||
{
|
||||
Throwable throwableCause = rie.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
}
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("Failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.debug("Failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
return connected();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
if(connected() && mStreamingSession != null)
|
||||
{
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* IO Handler for managing Icecast TCP connection and credentials
|
||||
*/
|
||||
public class ShoutcastIOHandler extends IoHandlerAdapter
|
||||
{
|
||||
/**
|
||||
* Sends stream configuration and user credentials upon connecting to remote server
|
||||
*/
|
||||
@Override
|
||||
public void sessionOpened(IoSession session) throws Exception
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
//Password
|
||||
sb.append(getConfiguration().getPassword()).append(ShoutcastMetadata.COMMAND_TERMINATOR);
|
||||
|
||||
//Metadata
|
||||
sb.append(ShoutcastMetadata.STREAM_NAME.encode(getConfiguration().getName()));
|
||||
sb.append(ShoutcastMetadata.PUBLIC.encode(getConfiguration().isPublic()));
|
||||
sb.append(ShoutcastMetadata.GENRE.encode(getConfiguration().getGenre()));
|
||||
sb.append(ShoutcastMetadata.DESCRIPTION.encode(getConfiguration().getDescription()));
|
||||
sb.append(ShoutcastMetadata.AUDIO_BIT_RATE.encode(getConfiguration().getBitRate()));
|
||||
|
||||
//End of connection string
|
||||
sb.append(ShoutcastMetadata.COMMAND_TERMINATOR);
|
||||
|
||||
session.write(sb.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mStreamingSession = null;
|
||||
|
||||
mConnecting.set(false);
|
||||
super.sessionClosed(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
||||
{
|
||||
if(cause instanceof IOException)
|
||||
{
|
||||
IOException ioe = (IOException)cause;
|
||||
|
||||
if(ioe.getMessage() != null)
|
||||
{
|
||||
String reason = ioe.getMessage();
|
||||
|
||||
if(reason.startsWith("Connection reset"))
|
||||
{
|
||||
mLog.info("Streaming connection reset by remote server - reestablishing connection");
|
||||
disconnect();
|
||||
}
|
||||
else if(reason.startsWith("Operation timed out"))
|
||||
{
|
||||
mLog.info("Streaming connection timed out - resetting connection");
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("Unrecognized IO error: " + reason + ". Streaming halted.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("Unspecified IO error - streaming halted.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Broadcast error", cause);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object object) throws Exception
|
||||
{
|
||||
if(object instanceof String)
|
||||
{
|
||||
String message = (String) object;
|
||||
|
||||
if(message != null && !message.trim().isEmpty())
|
||||
{
|
||||
if(message.startsWith("OK"))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if(message.startsWith("icy-caps:"))
|
||||
{
|
||||
//TODO: what does icy-caps:11 tell us?
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Unrecognized server response:" + message);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() +
|
||||
"] received:" + object.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.shoutcast.v1;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.http.HttpClientCodec;
|
||||
import org.apache.mina.http.HttpRequestImpl;
|
||||
import org.apache.mina.http.api.HttpMethod;
|
||||
import org.apache.mina.http.api.HttpRequest;
|
||||
import org.apache.mina.http.api.HttpVersion;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
|
||||
private final static String UTF8 = "UTF-8";
|
||||
|
||||
private ShoutcastV1Configuration mShoutcastV1Configuration;
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private AtomicBoolean mUpdating = new AtomicBoolean();
|
||||
private Queue<String> mMetadataQueue = new LinkedTransferQueue<>();
|
||||
private boolean mStackTraceLoggingSuppressed;
|
||||
|
||||
/**
|
||||
* Shoutcast song metadata updater. Each metadata update is processed in the order received and the dispatch of
|
||||
* the HTTP update request is processed in a separate thread (runnable) to avoid delaying streaming of this
|
||||
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
|
||||
* sequence, those updates will be queued and processed in the order received.
|
||||
*/
|
||||
public ShoutcastV1BroadcastMetadataUpdater(ShoutcastV1Configuration shoutcastV1Configuration)
|
||||
{
|
||||
mShoutcastV1Configuration = shoutcastV1Configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket connector - lazy constructor.
|
||||
*/
|
||||
private NioSocketConnector getSocketConnector()
|
||||
{
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
|
||||
// mSocketConnector.getFilterChain().addLast("logger",
|
||||
// new LoggingFilter(ShoutcastV1BroadcastMetadataUpdater.class));
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("http_client_codec", new HttpClientCodec());
|
||||
|
||||
//Each metadata update session is single-use, so we'll shut it down upon success or failure
|
||||
mSocketConnector.setHandler(new IoHandlerAdapter()
|
||||
{
|
||||
@Override
|
||||
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
||||
{
|
||||
//Single-use session - close it after we receive an error
|
||||
session.closeNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object message) throws Exception
|
||||
{
|
||||
//Single-use session - close it after we receive a response
|
||||
session.closeNow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mSocketConnector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a song metadata update to the remote server. Additional metadata updates received while the updater
|
||||
* is in the process of sending an existing metadata update will be queued and processed in received order.
|
||||
*
|
||||
* Note: due to thread timing, there is a slight chance that a concurrent update request will not be processed
|
||||
* and will remain in the update queue until the next metadata update is requested. However, this is a design
|
||||
* trade-off to avoid having a scheduled runnable repeatedly processing the update queue.
|
||||
*/
|
||||
public void update(Metadata metadata)
|
||||
{
|
||||
mMetadataQueue.offer(getSong(metadata));
|
||||
|
||||
if(mUpdating.compareAndSet(false, true))
|
||||
{
|
||||
String song = mMetadataQueue.poll();
|
||||
|
||||
while(song != null)
|
||||
{
|
||||
HttpRequest updateRequest = createUpdateRequest(song);
|
||||
|
||||
if(updateRequest != null)
|
||||
{
|
||||
ThreadPool.SCHEDULED.schedule(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
ConnectFuture connectFuture = getSocketConnector()
|
||||
.connect(new InetSocketAddress(mShoutcastV1Configuration.getHost(),
|
||||
mShoutcastV1Configuration.getPort()));
|
||||
connectFuture.awaitUninterruptibly();
|
||||
IoSession session = connectFuture.getSession();
|
||||
|
||||
if(session != null)
|
||||
{
|
||||
session.write(updateRequest);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
//Do nothing, the server is unavailable
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!mStackTraceLoggingSuppressed)
|
||||
{
|
||||
mLog.error("Error sending metadata update. Future errors will " +
|
||||
"be suppressed", e);
|
||||
|
||||
mStackTraceLoggingSuppressed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0l, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
//Fetch next metadata update to send
|
||||
song = mMetadataQueue.poll();
|
||||
}
|
||||
|
||||
mUpdating.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the song information for a metadata update
|
||||
*/
|
||||
private static String getSong(Metadata metadata)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(metadata != null)
|
||||
{
|
||||
String to = metadata.getPrimaryAddressTo().getIdentifier();
|
||||
|
||||
sb.append("TO:");
|
||||
|
||||
if(metadata.getPrimaryAddressTo().hasAlias())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressTo().getAlias().getName());
|
||||
}
|
||||
else if(metadata.getPrimaryAddressTo().hasIdentifier())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressTo().getIdentifier());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("UNKNOWN");
|
||||
}
|
||||
|
||||
|
||||
sb.append(" FROM:");
|
||||
|
||||
if(metadata.getPrimaryAddressFrom().hasAlias())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressFrom().getAlias().getName());
|
||||
}
|
||||
else if(metadata.getPrimaryAddressFrom().hasIdentifier())
|
||||
{
|
||||
sb.append(metadata.getPrimaryAddressFrom().getIdentifier());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("UNKNOWN");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("Scanning ....");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTTP GET request to update the metadata on the remote server
|
||||
*/
|
||||
private HttpRequest createUpdateRequest(String song)
|
||||
{
|
||||
try
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("pass=").append(mShoutcastV1Configuration.getPassword());
|
||||
sb.append("&mode=updinfo");
|
||||
sb.append("&song=").append(URLEncoder.encode(song, UTF8));
|
||||
|
||||
Map<String,String> headers = new HashMap<>();
|
||||
|
||||
HttpRequestImpl request = new HttpRequestImpl(HttpVersion.HTTP_1_0, HttpMethod.GET, "/admin.cgi",
|
||||
sb.toString(), headers);
|
||||
|
||||
return request;
|
||||
}
|
||||
catch(UnsupportedEncodingException e)
|
||||
{
|
||||
//This should never happen
|
||||
mLog.error("UTF-8 encoding is not supported - can't update song metadata");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
public class ShoutcastV1Configuration extends BroadcastConfiguration
|
||||
{
|
||||
private String mGenre;
|
||||
private String mDescription;
|
||||
private boolean mPublic;
|
||||
private int mChannels = 1;
|
||||
private int mBitRate = 16;
|
||||
|
||||
public ShoutcastV1Configuration()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
public ShoutcastV1Configuration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
ShoutcastV1Configuration copy = new ShoutcastV1Configuration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Icecast Configuration Parameters
|
||||
copy.setGenre(getGenre());
|
||||
copy.setPublic(isPublic());
|
||||
copy.setChannels(getChannels());
|
||||
copy.setBitRate(getBitRate());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.SHOUTCAST_V1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream genre
|
||||
*/
|
||||
@XmlAttribute(name="genre")
|
||||
public String getGenre()
|
||||
{
|
||||
return mGenre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream genre
|
||||
* @param genre
|
||||
*/
|
||||
public void setGenre(String genre)
|
||||
{
|
||||
mGenre = genre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream description
|
||||
*/
|
||||
@XmlAttribute(name="description")
|
||||
public String getDescription()
|
||||
{
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream description
|
||||
* @param description
|
||||
*/
|
||||
public void setDescription(String description)
|
||||
{
|
||||
mDescription = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public visibility of the broadcastAudio
|
||||
*/
|
||||
@XmlAttribute(name="public")
|
||||
public boolean isPublic()
|
||||
{
|
||||
return mPublic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets public visibility of the broadcastAudio
|
||||
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||
*/
|
||||
public void setPublic(boolean isPublic)
|
||||
{
|
||||
mPublic = isPublic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of audio channels in the broadcastAudio
|
||||
*/
|
||||
@XmlAttribute(name="channels")
|
||||
public int getChannels()
|
||||
{
|
||||
return mChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of audio channels in the broadcastAudio
|
||||
*/
|
||||
public void setChannels(int channels)
|
||||
{
|
||||
mChannels = channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit rate in bits per second
|
||||
*/
|
||||
@XmlAttribute(name="bitrate")
|
||||
public int getBitRate()
|
||||
{
|
||||
return mBitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit rate
|
||||
* @param bitRate in samples per second
|
||||
*/
|
||||
public void setBitRate(int bitRate)
|
||||
{
|
||||
mBitRate = bitRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return getName() != null && !getName().isEmpty() &&
|
||||
getHost() != null && !getHost().isEmpty() &&
|
||||
getPort() > 0 && getPort() < 65535 &&
|
||||
getPassword() != null && !getPassword().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class ShoutcastV1ConfigurationEditor extends BroadcastConfigurationEditor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1ConfigurationEditor.class);
|
||||
|
||||
private static final int ONE_MINUTE_MS = 60000;
|
||||
|
||||
private JTextField mName;
|
||||
private JTextField mServer;
|
||||
private JTextField mPort;
|
||||
private JTextField mPassword;
|
||||
private JTextField mGenre;
|
||||
private JTextField mDescription;
|
||||
private JCheckBox mPublic;
|
||||
private JSlider mDelay;
|
||||
private JLabel mDelayValue;
|
||||
private JSlider mMaximumRecordingAge;
|
||||
private JLabel mMaximumRecordingAgeValue;
|
||||
private JCheckBox mEnabled;
|
||||
private JButton mSaveButton;
|
||||
private JButton mResetButton;
|
||||
|
||||
public ShoutcastV1ConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
|
||||
{
|
||||
super(broadcastModel, aliasModel, iconManager);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("fill,wrap 2", "[align right][grow,fill]",
|
||||
"[][][][][][][][][][][][][][][][grow,fill]"));
|
||||
setPreferredSize(new Dimension(150, 400));
|
||||
|
||||
JLabel channelLabel = new JLabel("Shoutcast (v1.x) Stream");
|
||||
|
||||
ImageIcon icon = mIconManager.getScaledIcon(new ImageIcon(BroadcastServerType.SHOUTCAST_V1.getIconPath()), 25);
|
||||
channelLabel.setIcon(icon);
|
||||
|
||||
add(channelLabel, "span, align center");
|
||||
|
||||
add(new JLabel("Name:"));
|
||||
mName = new JTextField();
|
||||
mName.getDocument().addDocumentListener(this);
|
||||
mName.setToolTipText("Unique name for this stream");
|
||||
add(mName);
|
||||
|
||||
add(new JLabel("Server:"));
|
||||
mServer = new JTextField();
|
||||
mServer.getDocument().addDocumentListener(this);
|
||||
mServer.setToolTipText("Server address (e.g. audio1.shoutcast.com or localhost)");
|
||||
add(mServer);
|
||||
|
||||
add(new JLabel("Port:"));
|
||||
mPort = new JTextField();
|
||||
mPort.getDocument().addDocumentListener(this);
|
||||
mPort.setToolTipText("Server port number (e.g. 80)");
|
||||
add(mPort);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
mPassword = new JTextField();
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
mPassword.setToolTipText("Password for the stream");
|
||||
add(mPassword);
|
||||
|
||||
add(new JLabel("Genre:"));
|
||||
mGenre = new JTextField();
|
||||
mGenre.getDocument().addDocumentListener(this);
|
||||
mGenre.setToolTipText("Genre (e.g. public safety)");
|
||||
add(mGenre);
|
||||
|
||||
add(new JLabel("Description:"));
|
||||
mDescription = new JTextField();
|
||||
mDescription.getDocument().addDocumentListener(this);
|
||||
mDescription.setToolTipText("Description of the stream");
|
||||
add(mDescription);
|
||||
|
||||
add(new JLabel("Public:"));
|
||||
mPublic = new JCheckBox();
|
||||
mPublic.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mPublic.setToolTipText("Designate stream as public (checked) or private");
|
||||
add(mPublic);
|
||||
|
||||
add(new JLabel());
|
||||
mDelayValue = new JLabel("0 Minutes");
|
||||
add(mDelayValue);
|
||||
|
||||
add(new JLabel("Delay:"));
|
||||
mDelay = new JSlider(0, 60, 0);
|
||||
mDelay.setMajorTickSpacing(10);
|
||||
mDelay.setMinorTickSpacing(5);
|
||||
mDelay.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mDelayValue.setText(mDelay.getValue() + " Minutes");
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mDelay.setToolTipText("Audio broadcast delay in minutes");
|
||||
add(mDelay);
|
||||
|
||||
add(new JLabel());
|
||||
mMaximumRecordingAgeValue = new JLabel("1 Minute");
|
||||
add(mMaximumRecordingAgeValue);
|
||||
|
||||
add(new JLabel("Age Limit:"));
|
||||
mMaximumRecordingAge = new JSlider(1,60,5);
|
||||
mMaximumRecordingAge.setMajorTickSpacing(10);
|
||||
mMaximumRecordingAge.setMinorTickSpacing(5);
|
||||
mMaximumRecordingAge.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
int value = mMaximumRecordingAge.getValue();
|
||||
|
||||
mMaximumRecordingAgeValue.setText(value + " Minute" + (value > 1 ? "s" : ""));
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mMaximumRecordingAge.setToolTipText("Maximum recording age (in addition to delay) to stream");
|
||||
add(mMaximumRecordingAge);
|
||||
|
||||
add(new JLabel("Enabled:")); //Empty
|
||||
mEnabled = new JCheckBox();
|
||||
mEnabled.setToolTipText("Enable (checked) or disable this stream");
|
||||
mEnabled.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
add(mEnabled);
|
||||
|
||||
mSaveButton = new JButton("Save");
|
||||
mSaveButton.setEnabled(false);
|
||||
mSaveButton.setToolTipText("Click to save configuration information");
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
save();
|
||||
}
|
||||
});
|
||||
add(mSaveButton);
|
||||
|
||||
mResetButton = new JButton("Reset");
|
||||
mResetButton.setEnabled(false);
|
||||
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
|
||||
mResetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(false);
|
||||
setItem(getItem());
|
||||
}
|
||||
});
|
||||
add(mResetButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModified(boolean modified)
|
||||
{
|
||||
super.setModified(modified);
|
||||
|
||||
mSaveButton.setEnabled(modified);
|
||||
mResetButton.setEnabled(modified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
ShoutcastV1Configuration config = (ShoutcastV1Configuration) getItem();
|
||||
|
||||
mName.setText(config.getName());
|
||||
mServer.setText(config.getHost());
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
mPassword.setText(config.getPassword());
|
||||
mGenre.setText(config.getGenre());
|
||||
mDescription.setText(config.getDescription());
|
||||
mPublic.setSelected(config.isPublic());
|
||||
mDelay.setValue((int) (config.getDelay() / ONE_MINUTE_MS));
|
||||
mMaximumRecordingAge.setValue((int)config.getMaximumRecordingAge() / ONE_MINUTE_MS);
|
||||
mEnabled.setSelected(config.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
mName.setText(null);
|
||||
mServer.setText(null);
|
||||
mPort.setText(null);
|
||||
mPassword.setText(null);
|
||||
mGenre.setText(null);
|
||||
mDescription.setText(null);
|
||||
mPublic.setSelected(true);
|
||||
mDelay.setValue(0);
|
||||
mMaximumRecordingAge.setValue(1);
|
||||
mEnabled.setSelected(false);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
ShoutcastV1Configuration config = (ShoutcastV1Configuration) getItem();
|
||||
|
||||
if(validateConfiguration())
|
||||
{
|
||||
updateConfigurationName(config, mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
config.setPort(getPort());
|
||||
config.setPassword(mPassword.getText());
|
||||
config.setDescription(mDescription.getText());
|
||||
config.setGenre(mGenre.getText());
|
||||
config.setPublic(mPublic.isSelected());
|
||||
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
|
||||
config.setMaximumRecordingAge(mMaximumRecordingAge.getValue() * ONE_MINUTE_MS);
|
||||
config.setEnabled(mEnabled.isSelected());
|
||||
|
||||
setModified(false);
|
||||
|
||||
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user specified values in the controls prior to saving them to the configuration
|
||||
*/
|
||||
private boolean validateConfiguration()
|
||||
{
|
||||
String name = mName.getText();
|
||||
|
||||
//A unique stream name is required
|
||||
if(!mBroadcastModel.isUniqueName(name, getItem()))
|
||||
{
|
||||
String message = (name == null || name.isEmpty()) ?
|
||||
"Please specify a stream name." : "Stream name " + name +
|
||||
"is already in use.\nPlease choose another name.";
|
||||
|
||||
JOptionPane.showMessageDialog(ShoutcastV1ConfigurationEditor.this,
|
||||
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mName.requestFocus();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Server address is optional, but required
|
||||
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Port is optional, but required
|
||||
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Password is optional, but required
|
||||
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getPort()
|
||||
{
|
||||
String port = mPort.getText();
|
||||
|
||||
if(port != null && !port.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(port);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.shoutcast.v2;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.AuthenticateBroadcast;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.CacheableXMLMetadata;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.ConfigureIcyName;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.ConfigureIcyPublic;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.MP3Audio;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.NegotiateMaxPayloadSize;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.RequestCipher;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.SetupBroadcast;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.Standby;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.StreamMimeType;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.UltravoxMessage;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.UltravoxMessageFactory;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.UltravoxMessageType;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.UltravoxMetadata;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox.UltravoxProtocolFactory;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioBroadcaster.class);
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
private int mMaxPayloadSize = 16377;
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
private LinkedTransferQueue<UltravoxMessage> mMetadataMessageQueue = new LinkedTransferQueue<>();
|
||||
|
||||
|
||||
/**
|
||||
* Shoutcast 2.x (Ultravox 2.1) broadcaster. This broadcaster is compatible with Shoutcast 2.x and newer versions
|
||||
* of the server software.
|
||||
*
|
||||
* Note: use @see ShoutcastV1AudioBroadcaster for Shoutcast version 1.x and older.
|
||||
*
|
||||
* This broadcaster uses the Apache Mina library for the streaming socket connection. The
|
||||
* ShoutcastV2IOHandler manages all interaction with the Shoutcast server and manages the overall broadcast state.
|
||||
*
|
||||
* @param configuration for the Shoutcast V2 stream
|
||||
*/
|
||||
public ShoutcastV2AudioBroadcaster(ShoutcastV2Configuration configuration)
|
||||
{
|
||||
super(configuration);
|
||||
}
|
||||
|
||||
public ShoutcastV2Configuration getConfiguration()
|
||||
{
|
||||
return (ShoutcastV2Configuration) getBroadcastConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the audio frame or sequence
|
||||
*/
|
||||
@Override
|
||||
protected void broadcastAudio(byte[] audio)
|
||||
{
|
||||
//Dispatch any queued metadata messages
|
||||
UltravoxMessage metadataMessage = mMetadataMessageQueue.poll();
|
||||
|
||||
while(metadataMessage != null)
|
||||
{
|
||||
mStreamingSession.write(metadataMessage);
|
||||
metadataMessage = mMetadataMessageQueue.poll();
|
||||
}
|
||||
|
||||
//Dispatch audio message
|
||||
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
|
||||
{
|
||||
MP3Audio mp3Audio = new MP3Audio();
|
||||
mp3Audio.setPayload(audio);
|
||||
|
||||
mStreamingSession.write(mp3Audio);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(Metadata metadata)
|
||||
{
|
||||
List<UltravoxMessage> metadataMessages = getMetadataMessages(metadata);
|
||||
|
||||
if(!metadataMessages.isEmpty())
|
||||
{
|
||||
mMetadataMessageQueue.addAll(metadataMessages);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IBroadcastMetadataUpdater getMetadataUpdater()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)Connects the broadcaster to the remote server if it currently is disconnected and indicates if the broadcaster
|
||||
* is currently connected to the remote server following any connection attempts.
|
||||
*
|
||||
* Attempts to connect via this method when the broadcast state indicates an error condition will be ignored.
|
||||
*
|
||||
* @return true if the audio handler can stream audio
|
||||
*/
|
||||
private boolean connect()
|
||||
{
|
||||
if(!connected() && canConnect() &&
|
||||
(mLastConnectionAttempt + RECONNECT_INTERVAL_MILLISECONDS < System.currentTimeMillis()) &&
|
||||
mConnecting.compareAndSet(false, true))
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(10000);
|
||||
|
||||
// mSocketConnector.getFilterChain().addLast("logger",
|
||||
// new LoggingFilter(ShoutcastV2AudioBroadcaster.class));
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec",
|
||||
new ProtocolCodecFilter(new UltravoxProtocolFactory()));
|
||||
|
||||
mSocketConnector.setHandler(new ShoutcastV2IOHandler());
|
||||
}
|
||||
|
||||
mStreamingSession = null;
|
||||
|
||||
Runnable runnable = new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
{
|
||||
ConnectFuture future = mSocketConnector
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
future.awaitUninterruptibly();
|
||||
mStreamingSession = future.getSession();
|
||||
}
|
||||
catch(RuntimeIoException rie)
|
||||
{
|
||||
Throwable throwableCause = rie.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
}
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.error("Failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.error("Failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
return connected();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect from the remote broadcast server and cleanup input/output streams and socket connection
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
if(connected() && mStreamingSession != null)
|
||||
{
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the list of metadata in one or more Ultravox CacheableXMLMetadata messages according to the maximum negotiated
|
||||
* payload size for the current connection. Each entry in the list of metadata strings should be an xml encoded
|
||||
* value: <tag>value</tag>
|
||||
*
|
||||
* See UltravoxMetadata.TAG.asXML(String value)
|
||||
*
|
||||
* @param metadata containing audio metadata tags and attributes
|
||||
* @return a sequence of CacheableXMLMetadata messages sufficient to carry the complete set of metadata
|
||||
*/
|
||||
private List<UltravoxMessage> getMetadataMessages(Metadata metadata)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?><metadata>");
|
||||
|
||||
try
|
||||
{
|
||||
sb.append(UltravoxMetadata.ALBUM_TITLE.asXML(getConfiguration().getName()));
|
||||
|
||||
sb.append(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION.asXML(SystemProperties.getInstance().getApplicationName()));
|
||||
|
||||
if(getConfiguration().hasGenre())
|
||||
{
|
||||
sb.append(UltravoxMetadata.GENRE.asXML(getConfiguration().getGenre()));
|
||||
}
|
||||
|
||||
if(getConfiguration().hasURL())
|
||||
{
|
||||
sb.append(UltravoxMetadata.URL.asXML(getConfiguration().getURL()));
|
||||
}
|
||||
|
||||
if(metadata != null)
|
||||
{
|
||||
if(metadata.getPrimaryAddressTo().hasAlias())
|
||||
{
|
||||
sb.append(UltravoxMetadata.TITLE_2.asXML(metadata.getPrimaryAddressTo().getAlias().getName()));
|
||||
}
|
||||
else if(metadata.getPrimaryAddressTo().hasIdentifier())
|
||||
{
|
||||
sb.append(UltravoxMetadata.TITLE_2.asXML(metadata.getPrimaryAddressTo().getIdentifier()));
|
||||
}
|
||||
|
||||
if(metadata.getPrimaryAddressFrom().hasAlias())
|
||||
{
|
||||
sb.append(UltravoxMetadata.TITLE_3.asXML("From:" + metadata.getPrimaryAddressFrom().getAlias().getName()));
|
||||
}
|
||||
else if(metadata.getPrimaryAddressFrom().hasIdentifier())
|
||||
{
|
||||
sb.append(UltravoxMetadata.TITLE_3.asXML("From:" + metadata.getPrimaryAddressFrom().getIdentifier()));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(UltravoxMetadata.TITLE_2.asXML("Scanning ..."));
|
||||
}
|
||||
}
|
||||
catch(UnsupportedEncodingException uee)
|
||||
{
|
||||
mLog.error("UTF-8 Encoding is not supported - shoutcast/ultravox metadata will not be updated");
|
||||
}
|
||||
|
||||
sb.append("</metadata>");
|
||||
|
||||
byte[] xml = sb.toString().getBytes();
|
||||
|
||||
int pointer = 0;
|
||||
int messageCounter = 1;
|
||||
int messageCount = (int) Math.ceil((double) xml.length / (double) (mMaxPayloadSize - 6));
|
||||
|
||||
if(messageCount > 32)
|
||||
{
|
||||
messageCount = 32; //Max number of metadata messages in a sequence
|
||||
}
|
||||
|
||||
|
||||
List<UltravoxMessage> messages = new ArrayList<>();
|
||||
|
||||
while(pointer < xml.length && messageCounter <= messageCount)
|
||||
{
|
||||
int payloadSize = Math.min(mMaxPayloadSize - 6, xml.length - pointer);
|
||||
|
||||
byte[] payload = new byte[payloadSize + 6];
|
||||
|
||||
payload[1] = (byte) (0x01);
|
||||
payload[3] = (byte) (messageCount & 0xFF);
|
||||
payload[5] = (byte) (messageCounter & 0xFF);
|
||||
|
||||
System.arraycopy(xml, pointer, payload, 6, payloadSize);
|
||||
|
||||
CacheableXMLMetadata message = new CacheableXMLMetadata();
|
||||
message.setPayload(payload);
|
||||
messages.add(message);
|
||||
|
||||
pointer += payloadSize;
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* IO Handler for managing Shoutcast V2 connection and credentials
|
||||
*/
|
||||
public class ShoutcastV2IOHandler extends IoHandlerAdapter
|
||||
{
|
||||
/**
|
||||
* Sends stream configuration and user credentials upon connecting to remote server
|
||||
*/
|
||||
@Override
|
||||
public void sessionOpened(IoSession session) throws Exception
|
||||
{
|
||||
session.write(UltravoxMessageFactory.getMessage(UltravoxMessageType.REQUEST_CIPHER));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mStreamingSession = null;
|
||||
|
||||
mConnecting.set(false);
|
||||
super.sessionClosed(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
|
||||
{
|
||||
if(cause instanceof IOException)
|
||||
{
|
||||
IOException ioe = (IOException)cause;
|
||||
|
||||
if(ioe.getMessage() != null)
|
||||
{
|
||||
String reason = ioe.getMessage();
|
||||
|
||||
if(reason.startsWith("Connection reset"))
|
||||
{
|
||||
mLog.info("Streaming connection reset by remote server - reestablishing connection");
|
||||
disconnect();
|
||||
}
|
||||
else if(reason.startsWith("Operation timed out"))
|
||||
{
|
||||
mLog.info("Streaming connection timed out - resetting connection");
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("Unrecognized IO error: " + reason + ". Streaming halted.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
mLog.error("Unspecified IO error - streaming halted.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Broadcast error", cause);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object object) throws Exception
|
||||
{
|
||||
if(object instanceof UltravoxMessage)
|
||||
{
|
||||
UltravoxMessage ultravoxMessage = (UltravoxMessage) object;
|
||||
|
||||
switch(ultravoxMessage.getMessageType())
|
||||
{
|
||||
case REQUEST_CIPHER:
|
||||
String cipherKey = ((RequestCipher) ultravoxMessage).getCipher();
|
||||
|
||||
AuthenticateBroadcast authenticateBroadcast = new AuthenticateBroadcast();
|
||||
|
||||
authenticateBroadcast.setCredentials(cipherKey, getConfiguration().getStreamID(),
|
||||
getConfiguration().getUserID(), getConfiguration().getPassword());
|
||||
|
||||
session.write(authenticateBroadcast);
|
||||
break;
|
||||
case AUTHENTICATE_BROADCAST:
|
||||
if(ultravoxMessage.isErrorResponse())
|
||||
{
|
||||
String errorMessage = ultravoxMessage.getErrorMessage();
|
||||
|
||||
if(errorMessage.startsWith(AuthenticateBroadcast.STREAM_ID_ERROR))
|
||||
{
|
||||
setBroadcastState(BroadcastState.INVALID_MOUNT_POINT);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Invalid Credentials - response: " + ultravoxMessage.getPayload());
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StreamMimeType streamMimeType = new StreamMimeType();
|
||||
streamMimeType.setFormat(getConfiguration().getBroadcastFormat());
|
||||
session.write(streamMimeType);
|
||||
}
|
||||
break;
|
||||
case STREAM_MIME_TYPE:
|
||||
if(ultravoxMessage.isErrorResponse())
|
||||
{
|
||||
mLog.error("Unsupported Audio Format:" + getConfiguration().getBroadcastFormat().toString() +
|
||||
" - " + ultravoxMessage.getErrorMessage());
|
||||
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetupBroadcast setupBroadcast = new SetupBroadcast();
|
||||
|
||||
//Use the same value for average and minimum bit rates
|
||||
setupBroadcast.setBitRate(getConfiguration().getBitRate(), getConfiguration().getBitRate());
|
||||
|
||||
session.write(setupBroadcast);
|
||||
}
|
||||
break;
|
||||
case SETUP_BROADCAST:
|
||||
if(ultravoxMessage.isErrorResponse())
|
||||
{
|
||||
mLog.error("Unsupported Audio Bit Rate:" + getConfiguration().getBitRate() +
|
||||
" - " + ultravoxMessage.getErrorMessage());
|
||||
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
|
||||
}
|
||||
else
|
||||
{
|
||||
NegotiateMaxPayloadSize negotiateMaxPayloadSize = new NegotiateMaxPayloadSize();
|
||||
|
||||
negotiateMaxPayloadSize.setMaximumPayloadSize(16377, 4192);
|
||||
|
||||
session.write(negotiateMaxPayloadSize);
|
||||
}
|
||||
break;
|
||||
case NEGOTIATE_MAX_PAYLOAD_SIZE:
|
||||
if(ultravoxMessage.isErrorResponse())
|
||||
{
|
||||
mLog.error("Unsupported maximum payload size (18 min - 36 kbps max) - " +
|
||||
ultravoxMessage.getErrorMessage());
|
||||
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
|
||||
}
|
||||
else
|
||||
{
|
||||
mMaxPayloadSize = ((NegotiateMaxPayloadSize) ultravoxMessage).getMaximumPayloadSize();
|
||||
|
||||
ConfigureIcyPublic configureIcyPublic = new ConfigureIcyPublic();
|
||||
configureIcyPublic.setPublic(getConfiguration().isPublic());
|
||||
session.write(configureIcyPublic);
|
||||
}
|
||||
break;
|
||||
case CONFIGURE_ICY_PUBLIC:
|
||||
if(ultravoxMessage.isErrorResponse())
|
||||
{
|
||||
mLog.error("Error setting shoutcast stream as public - " + ultravoxMessage.getErrorMessage());
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConfigureIcyName configureIcyName = new ConfigureIcyName();
|
||||
configureIcyName.setName(getConfiguration().getName());
|
||||
session.write(configureIcyName);
|
||||
}
|
||||
break;
|
||||
case CONFIGURE_ICY_NAME:
|
||||
if(ultravoxMessage.isErrorResponse())
|
||||
{
|
||||
mLog.error("Error setting shoutcast stream name - " + ultravoxMessage.getErrorMessage());
|
||||
setBroadcastState(BroadcastState.CONFIGURATION_ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
session.write(new Standby());
|
||||
}
|
||||
break;
|
||||
case STANDBY:
|
||||
if(ultravoxMessage.isErrorResponse())
|
||||
{
|
||||
mLog.error("Error following stream configuration and standby - " +
|
||||
ultravoxMessage.getErrorMessage());
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.error("Unrecognized ultravox message:" + ultravoxMessage.getMessageType() +
|
||||
" payload:" + ultravoxMessage.getPayload());
|
||||
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug("Unrecognized message received from shoutcast v2 server:" + object.toString());
|
||||
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAttribute;
|
||||
|
||||
public class ShoutcastV2Configuration extends BroadcastConfiguration
|
||||
{
|
||||
private int mStreamID;
|
||||
private String mUserID;
|
||||
private int mBitRate = 16;
|
||||
private String mGenre;
|
||||
private String mURL;
|
||||
private boolean mPublic = true;
|
||||
|
||||
public ShoutcastV2Configuration()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
public ShoutcastV2Configuration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
ShoutcastV2Configuration copy = new ShoutcastV2Configuration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Shoutcast V2 Configuration Parameters
|
||||
copy.setStreamID(getStreamID());
|
||||
copy.setUserID(getUserID());
|
||||
copy.setBitRate(getBitRate());
|
||||
copy.setGenre(getGenre());
|
||||
copy.setURL(getURL());
|
||||
copy.setPublic(isPublic());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.SHOUTCAST_V2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream ID
|
||||
*/
|
||||
@XmlAttribute(name = "stream_id")
|
||||
public int getStreamID()
|
||||
{
|
||||
return mStreamID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stream ID
|
||||
*
|
||||
* @param streamID numeric stream identifier between 1 - 2147483647
|
||||
*/
|
||||
public void setStreamID(int streamID)
|
||||
{
|
||||
Validate.isTrue(1 <= streamID && streamID <= Integer.MAX_VALUE);
|
||||
mStreamID = streamID;
|
||||
}
|
||||
|
||||
/**
|
||||
* User ID
|
||||
*/
|
||||
@XmlAttribute(name = "user_id")
|
||||
public String getUserID()
|
||||
{
|
||||
return mUserID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user ID
|
||||
* @param userID
|
||||
*/
|
||||
public void setUserID(String userID)
|
||||
{
|
||||
mUserID = userID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit rate in kilobits per second
|
||||
*/
|
||||
@XmlAttribute(name = "bitrate")
|
||||
public int getBitRate()
|
||||
{
|
||||
return mBitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit rate
|
||||
* @param bitRate in kilobits per second
|
||||
*/
|
||||
public void setBitRate(int bitRate)
|
||||
{
|
||||
mBitRate = bitRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream genre
|
||||
*/
|
||||
@XmlAttribute(name = "genre")
|
||||
public String getGenre()
|
||||
{
|
||||
return mGenre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the genre tag for this stream
|
||||
*/
|
||||
public void setGenre(String genre)
|
||||
{
|
||||
mGenre = genre;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasGenre()
|
||||
{
|
||||
return mGenre != null && !mGenre.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Public visibility of the broadcastAudio
|
||||
*/
|
||||
@XmlAttribute(name = "public")
|
||||
public boolean isPublic()
|
||||
{
|
||||
return mPublic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets public visibility of the broadcastAudio
|
||||
* @param isPublic indicates if the broadcastAudio should be visible to the public
|
||||
*/
|
||||
public void setPublic(boolean isPublic)
|
||||
{
|
||||
mPublic = isPublic;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL associated with the broadcastAudio where users can find additional details.
|
||||
*/
|
||||
@XmlAttribute(name = "url")
|
||||
public String getURL()
|
||||
{
|
||||
return mURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL associated with the broadcastAudio where users can find additional details.
|
||||
* @param url
|
||||
*/
|
||||
public void setURL(String url)
|
||||
{
|
||||
mURL = url;
|
||||
}
|
||||
|
||||
public boolean hasURL()
|
||||
{
|
||||
return mURL != null && !mURL.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return super.isValid() && hasName() &&
|
||||
getUserID() != null && !getUserID().isEmpty() &&
|
||||
1 <= getStreamID() &&
|
||||
getPassword() != null && !getPassword().isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.shoutcast.v2;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfigurationEditor;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class ShoutcastV2ConfigurationEditor extends BroadcastConfigurationEditor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2ConfigurationEditor.class);
|
||||
|
||||
private static final int ONE_MINUTE_MS = 60000;
|
||||
|
||||
private JTextField mName;
|
||||
private JTextField mServer;
|
||||
private JTextField mPort;
|
||||
private JTextField mStreamID;
|
||||
private JTextField mUserID;
|
||||
private JTextField mPassword;
|
||||
private JTextField mGenre;
|
||||
private JCheckBox mPublic;
|
||||
private JSlider mDelay;
|
||||
private JLabel mDelayValue;
|
||||
private JSlider mMaximumRecordingAge;
|
||||
private JLabel mMaximumRecordingAgeValue;
|
||||
private JCheckBox mEnabled;
|
||||
private JButton mSaveButton;
|
||||
private JButton mResetButton;
|
||||
|
||||
public ShoutcastV2ConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel, IconManager iconManager)
|
||||
{
|
||||
super(broadcastModel, aliasModel, iconManager);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("fill,wrap 2", "[align right][grow,fill]",
|
||||
"[][][][][][][][][][][][][][][][grow,fill]"));
|
||||
setPreferredSize(new Dimension(150, 400));
|
||||
|
||||
JLabel channelLabel = new JLabel("Shoutcast (v2) Stream");
|
||||
|
||||
ImageIcon icon = mIconManager.getScaledIcon(new ImageIcon(BroadcastServerType.SHOUTCAST_V2.getIconPath()), 25);
|
||||
channelLabel.setIcon(icon);
|
||||
|
||||
add(channelLabel, "span, align center");
|
||||
|
||||
add(new JLabel("Name:"));
|
||||
mName = new JTextField();
|
||||
mName.getDocument().addDocumentListener(this);
|
||||
mName.setToolTipText("Unique name for this stream");
|
||||
add(mName);
|
||||
|
||||
add(new JLabel("Server:"));
|
||||
mServer = new JTextField();
|
||||
mServer.getDocument().addDocumentListener(this);
|
||||
mServer.setToolTipText("Server address (e.g. audio1.shoutcast.com or localhost)");
|
||||
add(mServer);
|
||||
|
||||
add(new JLabel("Port:"));
|
||||
mPort = new JTextField();
|
||||
mPort.getDocument().addDocumentListener(this);
|
||||
mPort.setToolTipText("Server port number (e.g. 80)");
|
||||
add(mPort);
|
||||
|
||||
add(new JLabel("Stream ID:"));
|
||||
mStreamID = new JTextField();
|
||||
mStreamID.getDocument().addDocumentListener(this);
|
||||
mStreamID.setToolTipText("Stream Identifier)");
|
||||
add(mStreamID);
|
||||
|
||||
add(new JLabel("User ID:"));
|
||||
mUserID = new JTextField();
|
||||
mUserID.getDocument().addDocumentListener(this);
|
||||
mUserID.setToolTipText("User ID/Name");
|
||||
add(mUserID);
|
||||
|
||||
add(new JLabel("Password:"));
|
||||
mPassword = new JTextField();
|
||||
mPassword.getDocument().addDocumentListener(this);
|
||||
mPassword.setToolTipText("Password for the stream");
|
||||
add(mPassword);
|
||||
|
||||
add(new JLabel("Genre:"));
|
||||
mGenre = new JTextField();
|
||||
mGenre.getDocument().addDocumentListener(this);
|
||||
mGenre.setToolTipText("Genre (e.g. public safety)");
|
||||
add(mGenre);
|
||||
|
||||
add(new JLabel("Public:"));
|
||||
mPublic = new JCheckBox();
|
||||
mPublic.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mPublic.setToolTipText("Designate stream as public (checked) or private");
|
||||
add(mPublic);
|
||||
|
||||
add(new JLabel());
|
||||
mDelayValue = new JLabel("0 Minutes");
|
||||
add(mDelayValue);
|
||||
|
||||
add(new JLabel("Delay:"));
|
||||
mDelay = new JSlider(0, 60, 0);
|
||||
mDelay.setMajorTickSpacing(10);
|
||||
mDelay.setMinorTickSpacing(5);
|
||||
mDelay.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
mDelayValue.setText(mDelay.getValue() + " Minutes");
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mDelay.setToolTipText("Audio broadcast delay in minutes");
|
||||
add(mDelay);
|
||||
|
||||
add(new JLabel());
|
||||
mMaximumRecordingAgeValue = new JLabel("1 Minute");
|
||||
add(mMaximumRecordingAgeValue);
|
||||
|
||||
add(new JLabel("Age Limit:"));
|
||||
mMaximumRecordingAge = new JSlider(1,60,5);
|
||||
mMaximumRecordingAge.setMajorTickSpacing(10);
|
||||
mMaximumRecordingAge.setMinorTickSpacing(5);
|
||||
mMaximumRecordingAge.addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e)
|
||||
{
|
||||
int value = mMaximumRecordingAge.getValue();
|
||||
|
||||
mMaximumRecordingAgeValue.setText(value + " Minute" + (value > 1 ? "s" : ""));
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
mMaximumRecordingAge.setToolTipText("Maximum recording age (in addition to delay) to stream");
|
||||
add(mMaximumRecordingAge);
|
||||
|
||||
add(new JLabel("Enabled:")); //Empty
|
||||
mEnabled = new JCheckBox();
|
||||
mEnabled.setToolTipText("Enable (checked) or disable this stream");
|
||||
mEnabled.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(true);
|
||||
}
|
||||
});
|
||||
add(mEnabled);
|
||||
|
||||
mSaveButton = new JButton("Save");
|
||||
mSaveButton.setEnabled(false);
|
||||
mSaveButton.setToolTipText("Click to save configuration information");
|
||||
mSaveButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
save();
|
||||
}
|
||||
});
|
||||
add(mSaveButton);
|
||||
|
||||
mResetButton = new JButton("Reset");
|
||||
mResetButton.setEnabled(false);
|
||||
mResetButton.setToolTipText("Click to reset any changes you've made since the last save");
|
||||
mResetButton.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
setModified(false);
|
||||
setItem(getItem());
|
||||
}
|
||||
});
|
||||
add(mResetButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModified(boolean modified)
|
||||
{
|
||||
super.setModified(modified);
|
||||
|
||||
mSaveButton.setEnabled(modified);
|
||||
mResetButton.setEnabled(modified);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(BroadcastConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
if(hasItem())
|
||||
{
|
||||
ShoutcastV2Configuration config = (ShoutcastV2Configuration) getItem();
|
||||
|
||||
mName.setText(config.getName());
|
||||
mServer.setText(config.getHost());
|
||||
mPort.setText(config.getPort() != 0 ? String.valueOf(config.getPort()) : null);
|
||||
mStreamID.setText(String.valueOf(config.getStreamID()));
|
||||
mUserID.setText(config.getUserID());
|
||||
mPassword.setText(config.getPassword());
|
||||
mGenre.setText(config.getGenre());
|
||||
mPublic.setSelected(config.isPublic());
|
||||
mDelay.setValue((int) (config.getDelay() / ONE_MINUTE_MS));
|
||||
mMaximumRecordingAge.setValue((int)config.getMaximumRecordingAge() / ONE_MINUTE_MS);
|
||||
mEnabled.setSelected(config.isEnabled());
|
||||
}
|
||||
else
|
||||
{
|
||||
mName.setText(null);
|
||||
mServer.setText(null);
|
||||
mPort.setText(null);
|
||||
mStreamID.setText(null);
|
||||
mUserID.setText(null);
|
||||
mPassword.setText(null);
|
||||
mGenre.setText(null);
|
||||
mPublic.setSelected(true);
|
||||
mDelay.setValue(0);
|
||||
mMaximumRecordingAge.setValue(1);
|
||||
mEnabled.setSelected(false);
|
||||
}
|
||||
|
||||
setModified(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
ShoutcastV2Configuration config = (ShoutcastV2Configuration) getItem();
|
||||
|
||||
if(validateConfiguration())
|
||||
{
|
||||
updateConfigurationName(config, mName.getText());
|
||||
config.setHost(mServer.getText());
|
||||
config.setPort(getPort());
|
||||
config.setUserID(mUserID.getText());
|
||||
config.setPassword(mPassword.getText());
|
||||
config.setStreamID(getStreamID());
|
||||
config.setGenre(mGenre.getText());
|
||||
config.setPublic(mPublic.isSelected());
|
||||
config.setDelay(mDelay.getValue() * ONE_MINUTE_MS);
|
||||
config.setMaximumRecordingAge(mMaximumRecordingAge.getValue() * ONE_MINUTE_MS);
|
||||
config.setEnabled(mEnabled.isSelected());
|
||||
|
||||
setModified(false);
|
||||
|
||||
mBroadcastModel.process(new BroadcastEvent(getItem(), BroadcastEvent.Event.CONFIGURATION_CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user specified values in the controls prior to saving them to the configuration
|
||||
*/
|
||||
private boolean validateConfiguration()
|
||||
{
|
||||
String name = mName.getText();
|
||||
|
||||
//A unique stream name is required
|
||||
if(!mBroadcastModel.isUniqueName(name, getItem()))
|
||||
{
|
||||
String message = (name == null || name.isEmpty()) ?
|
||||
"Please specify a stream name." : "Stream name " + name +
|
||||
"is already in use.\nPlease choose another name.";
|
||||
|
||||
JOptionPane.showMessageDialog(ShoutcastV2ConfigurationEditor.this,
|
||||
message, "Invalid Stream Name", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
mName.requestFocus();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Server address is required
|
||||
if(!validateTextField(mServer, "Invalid Server Address", "Please specify a server address."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Port is required
|
||||
if(!validateIntegerTextField(mPort, "Invalid Port Number", "Please specify a port number (1 <> 65535)", 1, 65535))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Stream ID is required
|
||||
if(!validateIntegerTextField(mStreamID, "Invalid Stream ID", "Please specify a stream ID of 1 or higher", 1, Integer.MAX_VALUE))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//User ID is required
|
||||
if(!validateTextField(mUserID, "Invalid User ID", "Please specify a user ID/name."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//Password is required
|
||||
if(!validateTextField(mPassword, "Invalid Password", "Please specify a password."))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getPort()
|
||||
{
|
||||
String port = mPort.getText();
|
||||
|
||||
if(port != null && !port.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(port);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an Integer from the port text control or returns 0 if the control doesn't contain an integer value.
|
||||
*/
|
||||
private int getStreamID()
|
||||
{
|
||||
String streamID = mStreamID.getText();
|
||||
|
||||
if(streamID != null && !streamID.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
return Integer.parseInt(streamID);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the number value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
import io.github.dsheirer.util.XTEA;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class AuthenticateBroadcast extends UltravoxMessage
|
||||
{
|
||||
public static final String STREAM_ID_ERROR = "Stream ID Error";
|
||||
|
||||
/**
|
||||
* Client request to server to send authentication credentials
|
||||
*/
|
||||
public AuthenticateBroadcast()
|
||||
{
|
||||
super(UltravoxMessageType.AUTHENTICATE_BROADCAST);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
AuthenticateBroadcast(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set credential to authenticate the stream with the server
|
||||
*
|
||||
* @param encryptionKey received from the server in the RequestCipher message
|
||||
* @param streamID that we're going to stream to
|
||||
* @param userID for the stream. Note: this is optional and can be null. The server currently ignores this value.
|
||||
* @param password for the stream.
|
||||
*/
|
||||
public void setCredentials(String encryptionKey, int streamID, String userID, String password)
|
||||
{
|
||||
XTEA xtea = new XTEA(encryptionKey);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append(ULTRAVOX_VERSION);
|
||||
sb.append(":");
|
||||
sb.append(streamID);
|
||||
sb.append(":");
|
||||
sb.append(userID != null ? encrypt(xtea, userID) : "");
|
||||
sb.append(":");
|
||||
sb.append(password != null ? encrypt(xtea, password) : "");
|
||||
|
||||
setPayload(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the value using the XTEA block cipher algorithm. The byte array is sliced into 8-byte chunks and each
|
||||
* chunk is encrypted into a 16 hexadecimal character string. All encrypted chunks are concatenated and returned
|
||||
* as a single string. If the value byte array is not an even multiple of 8 characters, it will be padded with
|
||||
* zero valued bytes to make it an even multiple of eight.
|
||||
*
|
||||
* @param xtea instance of the XTEA algorithm with a loaded encryption key
|
||||
* @param value to encode
|
||||
* @return value encrypted into one or more multiples of 16 hexadecimal characters
|
||||
*/
|
||||
private static String encrypt(XTEA xtea, byte[] value)
|
||||
{
|
||||
if(value.length % 8 != 0 || value.length == 0)
|
||||
{
|
||||
int newLength = ((value.length / 8) + 1) * 8;
|
||||
value = Arrays.copyOf(value, newLength);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(int x = 0; x < value.length; x += 8)
|
||||
{
|
||||
byte[] plaintext = Arrays.copyOfRange(value, x, x + 8);
|
||||
byte[] encrypted = xtea.encrypt(plaintext);
|
||||
|
||||
for(byte b: encrypted)
|
||||
{
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the value using the XTEA block cipher algorithm and returns the encrypted value as a 16 hex value
|
||||
*
|
||||
* @param xtea instance of the XTEA algorithm with a loaded encryption key
|
||||
* @param value to encode
|
||||
* @return encrypted password as 64-bit, 16 character hexadecimal value
|
||||
*/
|
||||
private static String encrypt(XTEA xtea, String value)
|
||||
{
|
||||
return encrypt(xtea, value.getBytes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class CacheableXMLMetadata extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to send cacheable XML metadata
|
||||
*/
|
||||
public CacheableXMLMetadata()
|
||||
{
|
||||
super(UltravoxMessageType.CACHEABLE_XML_METADATA);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
CacheableXMLMetadata(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class ConfigureIcyGenre extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to configure the ICY-GENRE metadata
|
||||
*/
|
||||
public ConfigureIcyGenre()
|
||||
{
|
||||
super(UltravoxMessageType.CONFIGURE_ICY_GENRE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
ConfigureIcyGenre(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
public void setGenre(String genre)
|
||||
{
|
||||
setPayload(genre);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class ConfigureIcyName extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to configure the ICY-NAME metadata
|
||||
*/
|
||||
public ConfigureIcyName()
|
||||
{
|
||||
super(UltravoxMessageType.CONFIGURE_ICY_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
ConfigureIcyName(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
public void setName(String name)
|
||||
{
|
||||
setPayload(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class ConfigureIcyPublic extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to configure the ICY-PUBLIC metadata
|
||||
*/
|
||||
public ConfigureIcyPublic()
|
||||
{
|
||||
super(UltravoxMessageType.CONFIGURE_ICY_PUBLIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
ConfigureIcyPublic(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
public void setPublic(boolean publicStream)
|
||||
{
|
||||
setPayload(publicStream ? "1" : "0");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class ConfigureIcyURL extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to configure the ICY-URL metadata
|
||||
*/
|
||||
public ConfigureIcyURL()
|
||||
{
|
||||
super(UltravoxMessageType.CONFIGURE_ICY_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
ConfigureIcyURL(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
public void setURL(String url)
|
||||
{
|
||||
setPayload(url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class FlushCachedMetadata extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to flush any currently cached stream metadata
|
||||
*/
|
||||
public FlushCachedMetadata()
|
||||
{
|
||||
super(UltravoxMessageType.FLUSH_CACHED_METADATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
FlushCachedMetadata(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class MP3Audio extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to send authentication credentials
|
||||
*
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*/
|
||||
public MP3Audio()
|
||||
{
|
||||
super(UltravoxMessageType.MP3_DATA);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
MP3Audio(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class NegotiateBufferSize extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to negotiate the server's buffer size
|
||||
*/
|
||||
public NegotiateBufferSize()
|
||||
{
|
||||
super(UltravoxMessageType.NEGOTIATE_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
NegotiateBufferSize(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the desired and minimum requested buffer sizes to send to the server
|
||||
* @param desired buffer size in kilobytes
|
||||
* @param minimum buffer size in kilobytes
|
||||
*/
|
||||
public void setBufferSize(int desired, int minimum)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(desired).append(":").append(minimum);
|
||||
|
||||
setPayload(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiated buffer size from a server response ultravox.
|
||||
*
|
||||
* @return server negotiated buffer size, or zero if this is not a server response ultravox, or this is an error
|
||||
* server response ultravox, or the server response ultravox buffer size cannot be determined from the ultravox payload.
|
||||
*/
|
||||
public int getBufferSize()
|
||||
{
|
||||
String payload = getPayload();
|
||||
|
||||
if(payload != null && payload.startsWith("ACK:"))
|
||||
{
|
||||
try
|
||||
{
|
||||
int bufferSize = Integer.parseInt(payload.substring(4));
|
||||
return bufferSize;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
public class NegotiateMaxPayloadSize extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to negotiate maximum payload size
|
||||
*/
|
||||
public NegotiateMaxPayloadSize()
|
||||
{
|
||||
super(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
NegotiateMaxPayloadSize(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum payload size in bytes
|
||||
*
|
||||
* @param desiredMaximum payload size (no larger than 16377)
|
||||
* @param minimum acceptable payload size (no larger than 16377)
|
||||
*/
|
||||
public void setMaximumPayloadSize(int desiredMaximum, int minimum)
|
||||
{
|
||||
Validate.isTrue(desiredMaximum <= 16377);
|
||||
Validate.isTrue(minimum <= 16377);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(desiredMaximum).append(":").append(minimum);
|
||||
|
||||
setPayload(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Negotiated maximum payload size from a server response ultravox.
|
||||
*
|
||||
* @return server negotiated maximum payload size, or zero if this is not a server response ultravox, or this is an
|
||||
* error server response ultravox, or the server response ultravox maximum payload size cannot be determined from the
|
||||
* ultravox payload.
|
||||
*/
|
||||
public int getMaximumPayloadSize()
|
||||
{
|
||||
String payload = getPayload();
|
||||
|
||||
if(payload != null && payload.startsWith("ACK:"))
|
||||
{
|
||||
try
|
||||
{
|
||||
int maximumPayloadSize = Integer.parseInt(payload.substring(4));
|
||||
return maximumPayloadSize;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the value
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.shoutcast.v2.ultravox;
|
||||
|
||||
public class PassThroughXMLMetadata extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to send pass-through XML metadata
|
||||
*/
|
||||
public PassThroughXMLMetadata()
|
||||
{
|
||||
super(UltravoxMessageType.PASS_THROUGH_XML_METADATA);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
PassThroughXMLMetadata(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class RequestCipher extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server for encryption key
|
||||
*/
|
||||
public RequestCipher()
|
||||
{
|
||||
super(UltravoxMessageType.REQUEST_CIPHER);
|
||||
setPayload(ULTRAVOX_VERSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
RequestCipher(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cipher key returned from the server to use for encrypting user id and password credentials
|
||||
*
|
||||
* @return cipher key or null
|
||||
*/
|
||||
public String getCipher()
|
||||
{
|
||||
if(isValidResponse())
|
||||
{
|
||||
return getPayload().replace(VALID_RESPONSE_PAYLOAD_PREFIX, "");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class SetupBroadcast extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to specify the stream bit rate
|
||||
*/
|
||||
public SetupBroadcast()
|
||||
{
|
||||
super(UltravoxMessageType.SETUP_BROADCAST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
SetupBroadcast(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the average and maximum bit rates. Set both to the same value for a Constant Bit Rate (CBR) stream.
|
||||
*
|
||||
* @param average bit rate of the stream in kbps
|
||||
* @param maximum bit rate of the stream in kbps
|
||||
*/
|
||||
public void setBitRate(int average, int maximum)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(average).append(":").append(maximum);
|
||||
|
||||
setPayload(sb.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class Standby extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to notify that configuration is complete, prepare for streaming
|
||||
*/
|
||||
public Standby()
|
||||
{
|
||||
super(UltravoxMessageType.STANDBY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
Standby(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
|
||||
public class StreamMimeType extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to set stream MIME type
|
||||
*/
|
||||
public StreamMimeType()
|
||||
{
|
||||
super(UltravoxMessageType.STREAM_MIME_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
*
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
StreamMimeType(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the broadcastAudio format or MIME type
|
||||
* @param format of the stream
|
||||
*/
|
||||
public void setFormat(BroadcastFormat format)
|
||||
{
|
||||
setPayload(format.getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class TerminateBroadcast extends UltravoxMessage
|
||||
{
|
||||
/**
|
||||
* Client request to server to terminate the broadcastAudio
|
||||
*/
|
||||
public TerminateBroadcast()
|
||||
{
|
||||
super(UltravoxMessageType.TERMINATE_BROADCAST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server response to client request
|
||||
* Package private constructor. Use the UltravoxMessageFactory for this constructor.
|
||||
*
|
||||
* @param data bytes received from the server
|
||||
*/
|
||||
TerminateBroadcast(byte[] data)
|
||||
{
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.shoutcast.v2.ultravox;
|
||||
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolDecoderAdapter;
|
||||
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class UltravoxDecoder extends ProtocolDecoderAdapter
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(UltravoxDecoder.class);
|
||||
|
||||
@Override
|
||||
public void decode(IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput out) throws Exception
|
||||
{
|
||||
byte[] message = new byte[ioBuffer.limit()];
|
||||
|
||||
ioBuffer.get(message);
|
||||
|
||||
UltravoxMessage ultravoxMessage = UltravoxMessageFactory.getMessage(message);
|
||||
|
||||
if(ultravoxMessage != null)
|
||||
{
|
||||
out.write(ultravoxMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
out.write(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.shoutcast.v2.ultravox;
|
||||
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
|
||||
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
|
||||
|
||||
public class UltravoxEncoder extends ProtocolEncoderAdapter
|
||||
{
|
||||
@Override
|
||||
public void encode(IoSession ioSession, Object message, ProtocolEncoderOutput out) throws Exception
|
||||
{
|
||||
if(message instanceof UltravoxMessage)
|
||||
{
|
||||
byte[] data = ((UltravoxMessage)message).getMessage();
|
||||
|
||||
IoBuffer buf = IoBuffer.allocate(data.length).setAutoExpand(false);
|
||||
buf.put(data);
|
||||
buf.flip();
|
||||
out.write(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public abstract class UltravoxMessage
|
||||
{
|
||||
public static final String ULTRAVOX_VERSION = "2.1";
|
||||
public static final String VALID_RESPONSE_PREFIX = "ACK";
|
||||
public static final String VALID_RESPONSE_PAYLOAD_PREFIX = "ACK:";
|
||||
public static final String ERROR_RESPONSE_PREFIX = "NAK:";
|
||||
|
||||
public static final int[] SYNC = {0,1,2,3,4,5,6,7};
|
||||
public static final int[] RESERVED = {8,9,10,11};
|
||||
public static final int REQUIRED_DELIVERY = 12;
|
||||
public static final int[] SEND_QUEUE_PRIORITY = {13,14,15};
|
||||
public static final int[] MESSAGE_CLASS = {16,17,18,19};
|
||||
public static final int[] MESSAGE_CLASS_AND_TYPE = {16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31};
|
||||
public static final int[] PAYLOAD_LENGTH = {32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47};
|
||||
|
||||
private BinaryMessage mMessage;
|
||||
|
||||
/**
|
||||
* Constructs a ultravox from the byte array
|
||||
*/
|
||||
public UltravoxMessage(byte[] data)
|
||||
{
|
||||
mMessage = new BinaryMessage(data.length * 8);
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
for(byte b: data)
|
||||
{
|
||||
mMessage.setByte(pointer, b);
|
||||
pointer += 8;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an empty ultravox of the specified length
|
||||
*
|
||||
* @param ultravoxMessageType type of ultravox
|
||||
*/
|
||||
public UltravoxMessage(UltravoxMessageType ultravoxMessageType)
|
||||
{
|
||||
mMessage = new BinaryMessage(56);
|
||||
|
||||
setSync();
|
||||
setMessageType(ultravoxMessageType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message byte array
|
||||
*
|
||||
* @return ultravox bytes
|
||||
*/
|
||||
public byte[] getMessage()
|
||||
{
|
||||
return mMessage.toByteArray();
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return getMessageType().name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ultravox sync bits to the predefined sync pattern: 01011010 (0x5A)
|
||||
*/
|
||||
private void setSync()
|
||||
{
|
||||
mMessage.setInt(0x5A, SYNC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ultravox ultravox class
|
||||
*/
|
||||
public UltravoxMessageClass getMessageClass()
|
||||
{
|
||||
return UltravoxMessageClass.fromValue(mMessage.getInt(MESSAGE_CLASS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ultravox ultravox type
|
||||
* @return
|
||||
*/
|
||||
public UltravoxMessageType getMessageType()
|
||||
{
|
||||
return UltravoxMessageType.fromValue(mMessage.getInt(MESSAGE_CLASS_AND_TYPE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ultravox bits according to the ultravox class and type
|
||||
* @param messageType of ultravox ultravox
|
||||
*/
|
||||
private void setMessageType(UltravoxMessageType messageType)
|
||||
{
|
||||
mMessage.setInt(messageType.getValue(), MESSAGE_CLASS_AND_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this ultravox has the required delivery flag set
|
||||
*/
|
||||
public boolean isRequiredDelivery()
|
||||
{
|
||||
return mMessage.get(REQUIRED_DELIVERY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the required delivery flag according to the argument
|
||||
*/
|
||||
public void setRequiredDelivery(boolean required)
|
||||
{
|
||||
if(required)
|
||||
{
|
||||
mMessage.set(REQUIRED_DELIVERY);
|
||||
}
|
||||
else
|
||||
{
|
||||
mMessage.clear(REQUIRED_DELIVERY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send queue priority (0 - 7) for this ultravox
|
||||
*/
|
||||
public int getSendQueuePriority()
|
||||
{
|
||||
return mMessage.getInt(SEND_QUEUE_PRIORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets send queue priority for this ultravox
|
||||
* @param priority 0 - 7
|
||||
*/
|
||||
public void setSendQueuePriority(int priority)
|
||||
{
|
||||
Validate.isTrue(0 <= priority && priority <= 7);
|
||||
|
||||
mMessage.setInt(priority, SEND_QUEUE_PRIORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Message payload
|
||||
*/
|
||||
public String getPayload()
|
||||
{
|
||||
int payloadLength = mMessage.getInt(PAYLOAD_LENGTH);
|
||||
|
||||
byte[] payload = new byte[payloadLength];
|
||||
|
||||
int payloadPointer = 48;
|
||||
|
||||
for(int x = 0; x < payloadLength; x++)
|
||||
{
|
||||
payload[x] = mMessage.getByte(payloadPointer);
|
||||
payloadPointer += 8;
|
||||
}
|
||||
|
||||
//Strip the trailing 0x00 null value from a server response
|
||||
if(payload[payload.length - 1] == 0)
|
||||
{
|
||||
return new String(Arrays.copyOf(payload, payload.length - 1));
|
||||
}
|
||||
|
||||
return new String(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ultravox payload, updates the ultravox length field, and appends a trailing 0x00 byte after the payload.
|
||||
*/
|
||||
public void setPayload(String payload)
|
||||
{
|
||||
setPayload(payload.getBytes());
|
||||
}
|
||||
|
||||
public void setPayload(byte[] payload)
|
||||
{
|
||||
int messageBitLength = (7 + payload.length) * 8;
|
||||
|
||||
mMessage.setSize(messageBitLength);
|
||||
mMessage.setInt(payload.length, PAYLOAD_LENGTH);
|
||||
|
||||
int payloadPointer = 48;
|
||||
|
||||
for(byte payloadByte: payload)
|
||||
{
|
||||
mMessage.setByte(payloadPointer, payloadByte);
|
||||
payloadPointer += 8;
|
||||
}
|
||||
|
||||
mMessage.setByte(payloadPointer, (byte)0x00);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the response is a valid (ie non-error) response
|
||||
*/
|
||||
public boolean isValidResponse()
|
||||
{
|
||||
String payload = getPayload();
|
||||
|
||||
return payload != null && payload.startsWith(VALID_RESPONSE_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the response message contains an error message.
|
||||
*/
|
||||
public boolean isErrorResponse()
|
||||
{
|
||||
String payload = getPayload();
|
||||
|
||||
return payload != null && payload.startsWith(ERROR_RESPONSE_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Textual message available when the error response flag is set.
|
||||
*/
|
||||
public String getErrorMessage()
|
||||
{
|
||||
String payload = getPayload();
|
||||
|
||||
if(payload != null)
|
||||
{
|
||||
return payload.replace(ERROR_RESPONSE_PREFIX, "");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public enum UltravoxMessageClass
|
||||
{
|
||||
OPERATIONS(0x0),
|
||||
BROADCASTER(0x1),
|
||||
LISTENER(0x2),
|
||||
CACHEABLE_METADATA_1(0x3),
|
||||
CACHEABLE_METADATA_2(0x4),
|
||||
PASS_THROUGH_METADATA_1(0x5),
|
||||
PASS_THROUGH_METADATA_2(0x6),
|
||||
ENCODED_DATA(0x7),
|
||||
ADVANCED_ENCODED_DATA(0x8),
|
||||
FRAMED_DATA(0x9),
|
||||
CACHEABLE_BINARY_METADATA(0xA),
|
||||
UNKNOWN(0xF);
|
||||
|
||||
private int mValue;
|
||||
|
||||
private UltravoxMessageClass(int value)
|
||||
{
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public static UltravoxMessageClass fromValue(int value)
|
||||
{
|
||||
if(0x0 <= value && value <= 0xA)
|
||||
{
|
||||
return values()[value];
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public class UltravoxMessageFactory
|
||||
{
|
||||
/**
|
||||
* Creates an empty ultravox of the specified ultravox type for sending to the server
|
||||
*/
|
||||
public static UltravoxMessage getMessage(UltravoxMessageType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case AUTHENTICATE_BROADCAST:
|
||||
return new AuthenticateBroadcast();
|
||||
case CACHEABLE_XML_METADATA:
|
||||
return new CacheableXMLMetadata();
|
||||
case CONFIGURE_ICY_GENRE:
|
||||
return new ConfigureIcyGenre();
|
||||
case CONFIGURE_ICY_NAME:
|
||||
return new ConfigureIcyName();
|
||||
case CONFIGURE_ICY_PUBLIC:
|
||||
return new ConfigureIcyPublic();
|
||||
case CONFIGURE_ICY_URL:
|
||||
return new ConfigureIcyURL();
|
||||
case FLUSH_CACHED_METADATA:
|
||||
return new FlushCachedMetadata();
|
||||
case MP3_DATA:
|
||||
return new MP3Audio();
|
||||
case NEGOTIATE_BUFFER_SIZE:
|
||||
return new NegotiateBufferSize();
|
||||
case NEGOTIATE_MAX_PAYLOAD_SIZE:
|
||||
return new NegotiateMaxPayloadSize();
|
||||
case PASS_THROUGH_XML_METADATA:
|
||||
return new PassThroughXMLMetadata();
|
||||
case REQUEST_CIPHER:
|
||||
return new RequestCipher();
|
||||
case SETUP_BROADCAST:
|
||||
return new SetupBroadcast();
|
||||
case STANDBY:
|
||||
return new Standby();
|
||||
case STREAM_MIME_TYPE:
|
||||
return new StreamMimeType();
|
||||
case TERMINATE_BROADCAST:
|
||||
return new TerminateBroadcast();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ultravox ultravox from the server response bytes.
|
||||
*
|
||||
* @param bytes in network order (big-endian).
|
||||
*
|
||||
* @return constructed ultravox or null if the ultravox type cannot be determined or is unrecognized
|
||||
*/
|
||||
public static UltravoxMessage getMessage(byte[] bytes)
|
||||
{
|
||||
UltravoxMessageType type = getMessageType(bytes);
|
||||
|
||||
switch(type)
|
||||
{
|
||||
case AUTHENTICATE_BROADCAST:
|
||||
return new AuthenticateBroadcast(bytes);
|
||||
case CACHEABLE_XML_METADATA:
|
||||
return new CacheableXMLMetadata(bytes);
|
||||
case CONFIGURE_ICY_GENRE:
|
||||
return new ConfigureIcyGenre(bytes);
|
||||
case CONFIGURE_ICY_NAME:
|
||||
return new ConfigureIcyName(bytes);
|
||||
case CONFIGURE_ICY_PUBLIC:
|
||||
return new ConfigureIcyPublic(bytes);
|
||||
case CONFIGURE_ICY_URL:
|
||||
return new ConfigureIcyURL(bytes);
|
||||
case FLUSH_CACHED_METADATA:
|
||||
return new FlushCachedMetadata(bytes);
|
||||
case MP3_DATA:
|
||||
return new MP3Audio(bytes);
|
||||
case NEGOTIATE_BUFFER_SIZE:
|
||||
return new NegotiateBufferSize(bytes);
|
||||
case NEGOTIATE_MAX_PAYLOAD_SIZE:
|
||||
return new NegotiateMaxPayloadSize(bytes);
|
||||
case PASS_THROUGH_XML_METADATA:
|
||||
return new PassThroughXMLMetadata(bytes);
|
||||
case REQUEST_CIPHER:
|
||||
return new RequestCipher(bytes);
|
||||
case SETUP_BROADCAST:
|
||||
return new SetupBroadcast(bytes);
|
||||
case STANDBY:
|
||||
return new Standby(bytes);
|
||||
case STREAM_MIME_TYPE:
|
||||
return new StreamMimeType(bytes);
|
||||
case TERMINATE_BROADCAST:
|
||||
return new TerminateBroadcast(bytes);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the ultravox type from the server response byte array
|
||||
*
|
||||
* @param data in little-endian format
|
||||
* @return ultravox type or UNKNOWN if the ultravox type cannot be determined.
|
||||
*/
|
||||
private static UltravoxMessageType getMessageType(byte[] data)
|
||||
{
|
||||
if(data != null && data.length > 4)
|
||||
{
|
||||
int value = (data[2] << 8) + data[3];
|
||||
|
||||
return UltravoxMessageType.fromValue(value);
|
||||
}
|
||||
|
||||
return UltravoxMessageType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
public enum UltravoxMessageType
|
||||
{
|
||||
AUTHENTICATE_BROADCAST(0x1001),
|
||||
SETUP_BROADCAST(0x1002),
|
||||
NEGOTIATE_BUFFER_SIZE(0x1003),
|
||||
STANDBY(0x1004),
|
||||
TERMINATE_BROADCAST(0x1005),
|
||||
FLUSH_CACHED_METADATA(0x1006),
|
||||
NEGOTIATE_MAX_PAYLOAD_SIZE(0x1008),
|
||||
REQUEST_CIPHER(0x1009),
|
||||
STREAM_MIME_TYPE(0x1040),
|
||||
CONFIGURE_ICY_NAME(0x1100),
|
||||
CONFIGURE_ICY_GENRE(0x1101),
|
||||
CONFIGURE_ICY_URL(0x1102),
|
||||
CONFIGURE_ICY_PUBLIC(0x1103),
|
||||
CACHEABLE_XML_METADATA(0x3902),
|
||||
PASS_THROUGH_XML_METADATA(0x5902),
|
||||
MP3_DATA(0x7000),
|
||||
UNKNOWN(0x0000);
|
||||
|
||||
private int mValue;
|
||||
|
||||
private UltravoxMessageType(int value)
|
||||
{
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public static UltravoxMessageType fromValue(int value)
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 0x1001:
|
||||
return AUTHENTICATE_BROADCAST;
|
||||
case 0x1002:
|
||||
return SETUP_BROADCAST;
|
||||
case 0x1003:
|
||||
return NEGOTIATE_BUFFER_SIZE;
|
||||
case 0x1004:
|
||||
return STANDBY;
|
||||
case 0x1005:
|
||||
return TERMINATE_BROADCAST;
|
||||
case 0x1006:
|
||||
return FLUSH_CACHED_METADATA;
|
||||
case 0x1008:
|
||||
return NEGOTIATE_MAX_PAYLOAD_SIZE;
|
||||
case 0x1009:
|
||||
return REQUEST_CIPHER;
|
||||
case 0x1040:
|
||||
return STREAM_MIME_TYPE;
|
||||
case 0x1100:
|
||||
return CONFIGURE_ICY_NAME;
|
||||
case 0x1101:
|
||||
return CONFIGURE_ICY_GENRE;
|
||||
case 0x1102:
|
||||
return CONFIGURE_ICY_URL;
|
||||
case 0x1103:
|
||||
return CONFIGURE_ICY_PUBLIC;
|
||||
case 0x3902:
|
||||
return CACHEABLE_XML_METADATA;
|
||||
case 0x5902:
|
||||
return PASS_THROUGH_XML_METADATA;
|
||||
case 0x7000:
|
||||
return MP3_DATA;
|
||||
default:
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v2.ultravox;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
public enum UltravoxMetadata
|
||||
{
|
||||
ALBUM_TITLE("TALB"),
|
||||
ARTIST("TPE1"),
|
||||
AUDIO_SOURCE_WEBPAGE("WOAS"),
|
||||
BROADCAST_CLIENT_APPLICATION("TENC"),
|
||||
COMMENT("COMM"),
|
||||
CUSTOM_TEXT_FIELD("TXXX"),
|
||||
ENCODE_SETTINGS("TSSE"),
|
||||
GENERAL_BINARY_OBJECT("GEOB"),
|
||||
GENRE("TCON"),
|
||||
OWNER("TOWN"),
|
||||
RADIO_STATION("TRSN"),
|
||||
RADIO_STATION_OWNER("TRSO"),
|
||||
RADIO_STATION_WEBPAGE("WORS"),
|
||||
RECORDING_TIME("TDRC"),
|
||||
SOURCE_MEDIA_TYPE("TMED"),
|
||||
TITLE_1("TIT1"),
|
||||
TITLE_2("TIT2"),
|
||||
TITLE_3("TIT3"),
|
||||
URL("URL"),
|
||||
UNKNOWN("UNKNOWN");
|
||||
|
||||
private String mXMLTag;
|
||||
|
||||
private UltravoxMetadata(String xmlTag)
|
||||
{
|
||||
mXMLTag = xmlTag;
|
||||
}
|
||||
|
||||
public String getXMLTag()
|
||||
{
|
||||
return mXMLTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the value as an XML tag stream
|
||||
* @param value to enclose in XML tags
|
||||
* @return xml tag wrapped value
|
||||
*/
|
||||
public String asXML(String value) throws UnsupportedEncodingException
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<").append(getXMLTag()).append(">");
|
||||
sb.append(new String(value.getBytes(), "UTF-8"));
|
||||
sb.append("</").append(getXMLTag()).append(">");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 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.shoutcast.v2.ultravox;
|
||||
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFactory;
|
||||
import org.apache.mina.filter.codec.ProtocolDecoder;
|
||||
import org.apache.mina.filter.codec.ProtocolEncoder;
|
||||
|
||||
public class UltravoxProtocolFactory implements ProtocolCodecFactory
|
||||
{
|
||||
private ProtocolEncoder mEncoder;
|
||||
private ProtocolDecoder mDecoder;
|
||||
|
||||
@Override
|
||||
public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception
|
||||
{
|
||||
if(mEncoder == null)
|
||||
{
|
||||
mEncoder = new UltravoxEncoder();
|
||||
}
|
||||
|
||||
return mEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception
|
||||
{
|
||||
if(mDecoder == null)
|
||||
{
|
||||
mDecoder = new UltravoxDecoder();
|
||||
}
|
||||
|
||||
return mDecoder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user