mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 00:57:31 -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:
252
src/main/java/io/github/dsheirer/audio/AudioChannelPanel.java
Normal file
252
src/main/java/io/github/dsheirer/audio/AudioChannelPanel.java
Normal file
@@ -0,0 +1,252 @@
|
||||
/*******************************************************************************
|
||||
* 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;
|
||||
|
||||
import io.github.dsheirer.audio.output.AudioOutput;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.ColorSetting;
|
||||
import io.github.dsheirer.settings.Setting;
|
||||
import io.github.dsheirer.settings.SettingChangeListener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class AudioChannelPanel extends JPanel
|
||||
implements Listener<AudioEvent>, SettingChangeListener
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioChannelPanel.class);
|
||||
|
||||
public static final String PROPERTY_PREFIX = "audio.channel.panel.color.";
|
||||
public static final String PROPERTY_COLOR_BACKGROUND = PROPERTY_PREFIX + "background";
|
||||
public static final String PROPERTY_COLOR_LABEL = PROPERTY_PREFIX + "label";
|
||||
public static final String PROPERTY_COLOR_MUTED = PROPERTY_PREFIX + "muted";
|
||||
public static final String PROPERTY_COLOR_VALUE = PROPERTY_PREFIX + "value";
|
||||
|
||||
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
|
||||
|
||||
private Color mBackgroundColor;
|
||||
private Color mLabelColor;
|
||||
private Color mMutedColor;
|
||||
private Color mValueColor;
|
||||
|
||||
private IconManager mIconManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
private AudioOutput mAudioOutput;
|
||||
|
||||
private JLabel mMutedLabel = new JLabel("M");
|
||||
private JLabel mChannelName = new JLabel(" ");
|
||||
private JLabel mToAlias = new JLabel(" ");
|
||||
private JLabel mTo = new JLabel("-----");
|
||||
|
||||
private boolean mConfigured = false;
|
||||
|
||||
public AudioChannelPanel(IconManager iconManager, SettingsManager settingsManager, AudioOutput audioOutput)
|
||||
{
|
||||
mIconManager = iconManager;
|
||||
mSettingsManager = settingsManager;
|
||||
mSettingsManager.addListener(this);
|
||||
|
||||
mAudioOutput = audioOutput;
|
||||
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.addAudioEventListener(this);
|
||||
mAudioOutput.setMetadataListener(new AudioMetadataProcessor());
|
||||
}
|
||||
|
||||
mBackgroundColor = SystemProperties.getInstance().get(PROPERTY_COLOR_BACKGROUND, Color.BLACK);
|
||||
mLabelColor = SystemProperties.getInstance().get(PROPERTY_COLOR_LABEL, Color.LIGHT_GRAY);
|
||||
mMutedColor = SystemProperties.getInstance().get(PROPERTY_COLOR_MUTED, Color.RED);
|
||||
mValueColor = SystemProperties.getInstance().get(PROPERTY_COLOR_VALUE, Color.GREEN);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("align center center, insets 0 0 0 0",
|
||||
"[][][align right]0[grow,fill]", ""));
|
||||
setBackground(mBackgroundColor);
|
||||
|
||||
mMutedLabel.setFont(mFont);
|
||||
mMutedLabel.setForeground(mMutedColor);
|
||||
mMutedLabel.setVisible(false);
|
||||
add(mMutedLabel);
|
||||
|
||||
mChannelName = new JLabel(mAudioOutput != null ? mAudioOutput.getChannelName() : " ");
|
||||
mChannelName.setFont(mFont);
|
||||
mChannelName.setForeground(mLabelColor);
|
||||
add(mChannelName);
|
||||
|
||||
mToAlias.setFont(mFont);
|
||||
mToAlias.setForeground(mValueColor);
|
||||
add(mToAlias);
|
||||
|
||||
mTo.setFont(mFont);
|
||||
mTo.setForeground(mValueColor);
|
||||
add(mTo,"wmin 10lp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final AudioEvent audioEvent)
|
||||
{
|
||||
switch(audioEvent.getType())
|
||||
{
|
||||
case AUDIO_STOPPED:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
resetLabels();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case AUDIO_MUTED:
|
||||
case AUDIO_UNMUTED:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mMutedLabel.setVisible(mAudioOutput.isMuted());
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the from and to labels. Note: this does not happen on the swing
|
||||
* event thread. Only invoke from the swing thread.
|
||||
*/
|
||||
private void resetLabels()
|
||||
{
|
||||
updateLabel(mTo, null, mToAlias, null);
|
||||
|
||||
mConfigured = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the alias label with text and icon from the alias. Note: this
|
||||
* does not occur on the Swing event thread -- wrap any calls to this
|
||||
* method with an event thread call.
|
||||
*/
|
||||
private void updateLabel(JLabel textLabel, String value, JLabel iconLabel, Alias alias)
|
||||
{
|
||||
if(value != null)
|
||||
{
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
iconLabel.setIcon(mIconManager.getIcon(alias.getIconName(), 18));
|
||||
|
||||
String aliasName = alias.getName();
|
||||
|
||||
//Truncate label if length exceeds 33 characters - hack!
|
||||
if(aliasName.length() > 33)
|
||||
{
|
||||
textLabel.setText(aliasName.substring(0,30) + "...");
|
||||
}
|
||||
else
|
||||
{
|
||||
textLabel.setText(aliasName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
iconLabel.setIcon(null);
|
||||
textLabel.setText(value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
iconLabel.setIcon(null);
|
||||
textLabel.setText("-----");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes audio metadata to update this panel's display values
|
||||
*/
|
||||
public class AudioMetadataProcessor implements Listener<Metadata>
|
||||
{
|
||||
@Override
|
||||
public void receive(final Metadata metadata)
|
||||
{
|
||||
if(metadata.isUpdated() || !mConfigured)
|
||||
{
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
updateLabel(mTo, metadata.getPrimaryAddressTo().getIdentifier(),
|
||||
mToAlias, metadata.getPrimaryAddressTo().getAlias());
|
||||
|
||||
mConfigured = true;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void settingChanged(Setting setting)
|
||||
{
|
||||
if(setting instanceof ColorSetting)
|
||||
{
|
||||
ColorSetting colorSetting = (ColorSetting) setting;
|
||||
|
||||
switch(colorSetting.getColorSettingName())
|
||||
{
|
||||
case CHANNEL_STATE_LABEL_DECODER:
|
||||
if(mTo != null)
|
||||
{
|
||||
mTo.setForeground(mLabelColor);
|
||||
}
|
||||
if(mToAlias != null)
|
||||
{
|
||||
mToAlias.setForeground(mLabelColor);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void settingDeleted(Setting setting)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*******************************************************************************
|
||||
* 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;
|
||||
|
||||
import io.github.dsheirer.audio.output.AudioOutput;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
|
||||
public class AudioChannelsPanel extends JPanel
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public AudioChannelsPanel(IconManager iconManager, SettingsManager settingsManager, IAudioController controller)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[][sg abc,grow,fill][][sg abc,grow,fill]", "[grow,fill]"));
|
||||
|
||||
setBackground(Color.BLACK);
|
||||
|
||||
List<AudioOutput> outputs = controller.getAudioOutputs();
|
||||
|
||||
addSeparator();
|
||||
|
||||
for(int x = 0; x < outputs.size(); x++)
|
||||
{
|
||||
add(new AudioChannelPanel(iconManager, settingsManager, outputs.get(x)));
|
||||
|
||||
if(x < outputs.size() - 1)
|
||||
{
|
||||
addSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
/* Add an empty channel panel so that the panel is sized appropriately
|
||||
* for either a single channel or two channels */
|
||||
if(outputs.size() == 1)
|
||||
{
|
||||
addSeparator();
|
||||
add(new AudioChannelPanel(iconManager, settingsManager, null), "growx");
|
||||
}
|
||||
}
|
||||
|
||||
private void addSeparator()
|
||||
{
|
||||
JSeparator separator = new JSeparator(JSeparator.VERTICAL);
|
||||
separator.setBackground(Color.DARK_GRAY);
|
||||
add(separator);
|
||||
}
|
||||
}
|
||||
36
src/main/java/io/github/dsheirer/audio/AudioEvent.java
Normal file
36
src/main/java/io/github/dsheirer/audio/AudioEvent.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
public class AudioEvent
|
||||
{
|
||||
private Type mType;
|
||||
private String mChannel;
|
||||
|
||||
public AudioEvent( Type event, String channel )
|
||||
{
|
||||
mType = event;
|
||||
mChannel = channel;
|
||||
}
|
||||
|
||||
public Type getType()
|
||||
{
|
||||
return mType;
|
||||
}
|
||||
|
||||
public String getChannel()
|
||||
{
|
||||
return mChannel;
|
||||
}
|
||||
|
||||
public enum Type
|
||||
{
|
||||
AUDIO_MUTED,
|
||||
AUDIO_UNMUTED,
|
||||
AUDIO_SQUELCHED,
|
||||
AUDIO_UNSQUELCHED,
|
||||
AUDIO_STARTED,
|
||||
AUDIO_STOPPED,
|
||||
AUDIO_CONTINUATION,
|
||||
AUDIO_CONFIGURATION_CHANGE_STARTED,
|
||||
AUDIO_CONFIGURATION_CHANGE_COMPLETE;
|
||||
}
|
||||
}
|
||||
16
src/main/java/io/github/dsheirer/audio/AudioException.java
Normal file
16
src/main/java/io/github/dsheirer/audio/AudioException.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
public class AudioException extends Exception
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public AudioException( String description )
|
||||
{
|
||||
super( description );
|
||||
}
|
||||
|
||||
public AudioException( String description, Throwable throwable )
|
||||
{
|
||||
super( description, throwable );
|
||||
}
|
||||
}
|
||||
85
src/main/java/io/github/dsheirer/audio/AudioFormats.java
Normal file
85
src/main/java/io/github/dsheirer/audio/AudioFormats.java
Normal file
@@ -0,0 +1,85 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.DataLine;
|
||||
import javax.sound.sampled.Line;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
|
||||
public class AudioFormats
|
||||
{
|
||||
public static final AudioFormat.Encoding IMBE_ENCODING =
|
||||
new AudioFormat.Encoding( "IMBE" );
|
||||
|
||||
public static final boolean LITTLE_ENDIAN = false;
|
||||
public static final boolean BIG_ENDIAN = true;
|
||||
|
||||
public static final float IMBE_FRAME_RATE = 50;
|
||||
public static final float IMBE_SAMPLE_RATE = 50;
|
||||
public static final float PCM_8KHZ_RATE = 8000;
|
||||
public static final float PCM_48KHZ_RATE = 48000;
|
||||
|
||||
public static final int IMBE_FRAME_SIZE_BYTES = 18;
|
||||
public static final int IMBE_SAMPLE_SIZE_BITS = 144;
|
||||
public static final int ONE_CHANNEL = 1;
|
||||
public static final int TWO_CHANNELS = 2;
|
||||
public static final int PCM_SAMPLE_SIZE_BITS = 16;
|
||||
public static final int PCM_FRAME_SIZE_BYTES_MONO = 2;
|
||||
public static final int PCM_FRAME_SIZE_BYTES_STEREO = 4;
|
||||
|
||||
public static AudioFormat IMBE_AUDIO_FORMAT =
|
||||
new AudioFormat( IMBE_ENCODING,
|
||||
IMBE_SAMPLE_RATE,
|
||||
IMBE_SAMPLE_SIZE_BITS,
|
||||
ONE_CHANNEL,
|
||||
IMBE_FRAME_SIZE_BYTES,
|
||||
IMBE_FRAME_RATE,
|
||||
LITTLE_ENDIAN );
|
||||
|
||||
public static AudioFormat PCM_SIGNED_8KHZ_16BITS_MONO =
|
||||
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
|
||||
PCM_8KHZ_RATE,
|
||||
PCM_SAMPLE_SIZE_BITS,
|
||||
ONE_CHANNEL,
|
||||
PCM_FRAME_SIZE_BYTES_MONO,
|
||||
PCM_8KHZ_RATE,
|
||||
LITTLE_ENDIAN );
|
||||
|
||||
public static AudioFormat PCM_SIGNED_8KHZ_16BITS_STEREO =
|
||||
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
|
||||
PCM_8KHZ_RATE,
|
||||
PCM_SAMPLE_SIZE_BITS,
|
||||
TWO_CHANNELS,
|
||||
PCM_FRAME_SIZE_BYTES_STEREO,
|
||||
PCM_8KHZ_RATE,
|
||||
LITTLE_ENDIAN );
|
||||
|
||||
public static AudioFormat PCM_SIGNED_48KHZ_16BITS_MONO =
|
||||
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
|
||||
PCM_48KHZ_RATE,
|
||||
PCM_SAMPLE_SIZE_BITS,
|
||||
ONE_CHANNEL,
|
||||
PCM_FRAME_SIZE_BYTES_MONO,
|
||||
PCM_48KHZ_RATE,
|
||||
LITTLE_ENDIAN );
|
||||
|
||||
public static AudioFormat PCM_SIGNED_48KHZ_16BITS_STEREO =
|
||||
new AudioFormat( AudioFormat.Encoding.PCM_SIGNED,
|
||||
PCM_48KHZ_RATE,
|
||||
PCM_SAMPLE_SIZE_BITS,
|
||||
TWO_CHANNELS,
|
||||
PCM_FRAME_SIZE_BYTES_STEREO,
|
||||
PCM_48KHZ_RATE,
|
||||
LITTLE_ENDIAN );
|
||||
|
||||
/**
|
||||
* Source Data Line Info for a 48 kHz, 16-bits signed PCM one channel
|
||||
*/
|
||||
public static final Line.Info MONO_SOURCE_DATALINE_INFO =
|
||||
new DataLine.Info( SourceDataLine.class, PCM_SIGNED_8KHZ_16BITS_MONO);
|
||||
|
||||
/**
|
||||
* Source Data Line Info for a 48 kHz, 16-bits signed PCM two channels
|
||||
*/
|
||||
public static final Line.Info STEREO_SOURCE_DATALINE_INFO =
|
||||
new DataLine.Info( SourceDataLine.class, PCM_SIGNED_8KHZ_16BITS_STEREO);
|
||||
}
|
||||
567
src/main/java/io/github/dsheirer/audio/AudioManager.java
Normal file
567
src/main/java/io/github/dsheirer/audio/AudioManager.java
Normal file
@@ -0,0 +1,567 @@
|
||||
/*******************************************************************************
|
||||
* 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;
|
||||
|
||||
import io.github.dsheirer.audio.output.AudioOutput;
|
||||
import io.github.dsheirer.audio.output.MonoAudioOutput;
|
||||
import io.github.dsheirer.audio.output.StereoAudioOutput;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.Mixer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AudioManager implements Listener<AudioPacket>, IAudioController
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioManager.class);
|
||||
|
||||
public static final int AUDIO_TIMEOUT = 1000; //1 second
|
||||
|
||||
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
|
||||
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
||||
|
||||
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
|
||||
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
|
||||
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
|
||||
private AudioOutputConnection mLowestPriorityConnection;
|
||||
private int mAvailableConnectionCount;
|
||||
|
||||
private Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
|
||||
|
||||
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||
|
||||
private ScheduledFuture<?> mProcessingTask;
|
||||
private MixerManager mMixerManager;
|
||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
||||
|
||||
|
||||
/**
|
||||
* Processes all audio produced by the decoding channels and routes audio
|
||||
* packets to any combination of outputs based on any alias audio routing
|
||||
* options specified by the user.
|
||||
*/
|
||||
public AudioManager(MixerManager mixerManager)
|
||||
{
|
||||
mMixerManager = mixerManager;
|
||||
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the saved mixer configuration or a default configuration for audio playback.
|
||||
*/
|
||||
private void loadSettings()
|
||||
{
|
||||
MixerChannelConfiguration configuration = null;
|
||||
|
||||
SystemProperties properties = SystemProperties.getInstance();
|
||||
|
||||
Mixer defaultMixer = AudioSystem.getMixer(null);
|
||||
|
||||
String mixer = properties.get(AUDIO_MIXER_PROPERTY, defaultMixer.getMixerInfo().getName());
|
||||
|
||||
String channels = properties.get(AUDIO_CHANNELS_PROPERTY, MixerChannel.MONO.name());
|
||||
|
||||
MixerChannelConfiguration[] mixerConfigurations = mMixerManager.getOutputMixers();
|
||||
|
||||
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
|
||||
{
|
||||
if(mixerConfig.matches(mixer, channels))
|
||||
{
|
||||
configuration = mixerConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if(configuration == null)
|
||||
{
|
||||
configuration = getDefaultConfiguration();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(configuration, false);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't set stored audio mixer/channel configuration - using default", e);
|
||||
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(getDefaultConfiguration());
|
||||
}
|
||||
catch(Exception e2)
|
||||
{
|
||||
mLog.error("Couldn't set default audio mixer/channel configuration - no audio will be available", e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default audio playback configuration with a mono audio playback channel.
|
||||
*/
|
||||
private MixerChannelConfiguration getDefaultConfiguration()
|
||||
{
|
||||
/* Use the system default mixer and mono channel as default startup */
|
||||
Mixer defaultMixer = AudioSystem.getMixer(null);
|
||||
|
||||
return new MixerChannelConfiguration(defaultMixer, MixerChannel.MONO);
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
if(mProcessingTask != null)
|
||||
{
|
||||
mProcessingTask.cancel(true);
|
||||
}
|
||||
|
||||
mAudioPacketQueue.clear();
|
||||
|
||||
mProcessingTask = null;
|
||||
|
||||
mChannelConnectionMap.clear();
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
connection.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputConnections.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void receive(AudioPacket packet)
|
||||
{
|
||||
mAudioPacketQueue.add(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks each audio channel assignment and disconnects any inactive connections
|
||||
*/
|
||||
private void disconnectInactiveChannelAssignments()
|
||||
{
|
||||
boolean changed = false;
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
if(connection.isInactive() && mChannelConnectionMap.containsKey(connection.getChannelMetadataID()))
|
||||
{
|
||||
mChannelConnectionMap.remove(connection.getChannelMetadataID());
|
||||
connection.disconnect();
|
||||
mAvailableConnectionCount++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(changed)
|
||||
{
|
||||
updateLowestPriorityAssignment();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the lowest priority channel connection where the a higher value indicates a lower priority.
|
||||
*/
|
||||
private void updateLowestPriorityAssignment()
|
||||
{
|
||||
mLowestPriorityConnection = null;
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
if(connection.isConnected() &&
|
||||
(mLowestPriorityConnection == null || mLowestPriorityConnection.getPriority() < connection.getPriority()))
|
||||
{
|
||||
mLowestPriorityConnection = connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures audio playback to use the configuration specified in the entry argument.
|
||||
*
|
||||
* @param entry to use in configuring the audio playback setup.
|
||||
* @throws AudioException if there is an error
|
||||
*/
|
||||
@Override
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
|
||||
{
|
||||
setMixerChannelConfiguration(entry, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures audio playback to use the configuration specified in the entry argument.
|
||||
*
|
||||
* @param entry to use in configuring the audio playback setup.
|
||||
* @param saveSettings to save the audio playback configuration settings in the properties file.
|
||||
* @throws AudioException if there is an error
|
||||
*/
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolean saveSettings) throws AudioException
|
||||
{
|
||||
if(entry != null && (entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO))
|
||||
{
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
||||
|
||||
if(mProcessingTask != null)
|
||||
{
|
||||
mProcessingTask.cancel(true);
|
||||
}
|
||||
|
||||
disposeCurrentConfiguration();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer());
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(mono));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(mono.getChannelName(), mono);
|
||||
break;
|
||||
case STEREO:
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT);
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(left));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(left.getChannelName(), left);
|
||||
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT);
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(right));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(right.getChannelName(), right);
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel "
|
||||
+ "configuration: " + entry.getMixerChannel());
|
||||
}
|
||||
|
||||
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioPacketProcessor(),
|
||||
0, 15, TimeUnit.MILLISECONDS);
|
||||
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
||||
|
||||
if(saveSettings)
|
||||
{
|
||||
SystemProperties properties = SystemProperties.getInstance();
|
||||
properties.set(AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName());
|
||||
properties.set(AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all channel assignments and terminates all audio outputs in preparation for complete shutdown or change
|
||||
* to another mixer/channel configuration
|
||||
*/
|
||||
private void disposeCurrentConfiguration()
|
||||
{
|
||||
mChannelConnectionMap.clear();
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
connection.dispose();
|
||||
}
|
||||
|
||||
mAvailableConnectionCount = 0;
|
||||
|
||||
mAudioOutputConnections.clear();
|
||||
|
||||
mAudioOutputMap.clear();
|
||||
|
||||
mLowestPriorityConnection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current audio playback mixer channel configuration setting.
|
||||
*/
|
||||
@Override
|
||||
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
|
||||
{
|
||||
return mMixerChannelConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of audio outputs available for the current mixer channel configuration
|
||||
*/
|
||||
@Override
|
||||
public List<AudioOutput> getAudioOutputs()
|
||||
{
|
||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputMap.values());
|
||||
|
||||
Collections.sort(outputs, new Comparator<AudioOutput>()
|
||||
{
|
||||
@Override
|
||||
public int compare(AudioOutput first, AudioOutput second)
|
||||
{
|
||||
return first.getChannelName().compareTo(second.getChannelName());
|
||||
}
|
||||
});
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an audio event listener to receive audio event notifications.
|
||||
*/
|
||||
@Override
|
||||
public void addControllerListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mControllerBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an audio event listener from receiving audio event notifications.
|
||||
*/
|
||||
@Override
|
||||
public void removeControllerListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mControllerBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an audio output connection for the packet if one is available, or overrides an existing lower priority
|
||||
* connection. Returns null if no connection is available for the audio packet.
|
||||
*
|
||||
* @param audioPacket from a decoding channel source
|
||||
* @return an audio output connection or null
|
||||
*/
|
||||
private AudioOutputConnection getConnection(AudioPacket audioPacket)
|
||||
{
|
||||
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
|
||||
|
||||
//Use an existing connection
|
||||
if(mChannelConnectionMap.containsKey(channelMetadataID))
|
||||
{
|
||||
return mChannelConnectionMap.get(channelMetadataID);
|
||||
}
|
||||
|
||||
//Connect to an unused, available connection
|
||||
if(mAvailableConnectionCount > 0)
|
||||
{
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
if(connection.isDisconnected())
|
||||
{
|
||||
connection.connect(channelMetadataID, audioPacket.getMetadata().getAudioPriority());
|
||||
mChannelConnectionMap.put(channelMetadataID, connection);
|
||||
mAvailableConnectionCount--;
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Preempt an existing lower priority connection and connect when this is a higher priority packet
|
||||
else
|
||||
{
|
||||
int priority = audioPacket.getMetadata().getAudioPriority();
|
||||
|
||||
AudioOutputConnection connection = mLowestPriorityConnection;
|
||||
|
||||
if(connection != null && priority < connection.getPriority())
|
||||
{
|
||||
mChannelConnectionMap.remove(connection.getChannelMetadataID());
|
||||
|
||||
connection.connect(channelMetadataID, priority);
|
||||
|
||||
mChannelConnectionMap.put(channelMetadataID, connection);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public class AudioPacketProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
disconnectInactiveChannelAssignments();
|
||||
|
||||
if(mAudioPacketQueue != null)
|
||||
{
|
||||
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
||||
|
||||
mAudioPacketQueue.drainTo(packets);
|
||||
|
||||
for(AudioPacket packet : packets)
|
||||
{
|
||||
/* Don't process any packet's marked as do not monitor */
|
||||
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO)
|
||||
{
|
||||
AudioOutputConnection connection = getConnection(packet);
|
||||
|
||||
if(connection != null)
|
||||
{
|
||||
connection.receive(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Encountered error while processing audio packets", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio output connection manages a connection between a source and an audio
|
||||
* output and maintains current state information about the audio activity
|
||||
* received form the source.
|
||||
*/
|
||||
public class AudioOutputConnection
|
||||
{
|
||||
private static final int DISCONNECTED = -1;
|
||||
|
||||
private AudioOutput mAudioOutput;
|
||||
private int mPriority = 0;
|
||||
private int mChannelMetadataID = DISCONNECTED;
|
||||
|
||||
public AudioOutputConnection(AudioOutput audioOutput)
|
||||
{
|
||||
mAudioOutput = audioOutput;
|
||||
}
|
||||
|
||||
public void receive(AudioPacket packet)
|
||||
{
|
||||
if(packet.hasMetadata() && packet.getMetadata().getMetadataID() == mChannelMetadataID)
|
||||
{
|
||||
int priority = packet.getMetadata().getAudioPriority();
|
||||
|
||||
if(mPriority != priority)
|
||||
{
|
||||
mPriority = priority;
|
||||
updateLowestPriorityAssignment();
|
||||
}
|
||||
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.receive(packet);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(packet.hasMetadata())
|
||||
{
|
||||
mLog.error("Received audio packet from channel metadata [" + packet.getMetadata().getMetadataID() +
|
||||
"] however this assignment is currently connected to metadata [" + mChannelMetadataID + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Received audio packet with no metadata - cannot route audio packet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the audio output and prepares this connection for disposal
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
mAudioOutput.dispose();
|
||||
mAudioOutput = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this assignment is currently disconnected from a channel source
|
||||
*/
|
||||
public boolean isDisconnected()
|
||||
{
|
||||
return mChannelMetadataID == DISCONNECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this assignment is currently connected to a channel source
|
||||
*/
|
||||
public boolean isConnected()
|
||||
{
|
||||
return !isDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects this assignment to the indicated source so that audio
|
||||
* packets from this source can be sent to the audio output
|
||||
*/
|
||||
public void connect(int source, int priority)
|
||||
{
|
||||
mChannelMetadataID = source;
|
||||
mPriority = priority;
|
||||
updateLowestPriorityAssignment();
|
||||
|
||||
mAudioOutput.updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently connected source or -1 if disconnected
|
||||
*/
|
||||
public int getChannelMetadataID()
|
||||
{
|
||||
return mChannelMetadataID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects this assignment from the source and prevents any audio
|
||||
* from being routed to the audio output until another source is assigned
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
mChannelMetadataID = DISCONNECTED;
|
||||
mPriority = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if audio output is current inactive, meaning that the
|
||||
* audio output hasn't recently processed any audio packets.
|
||||
*/
|
||||
public boolean isInactive()
|
||||
{
|
||||
return (mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT) < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getPriority()
|
||||
{
|
||||
return mPriority;
|
||||
}
|
||||
}
|
||||
}
|
||||
188
src/main/java/io/github/dsheirer/audio/AudioModule.java
Normal file
188
src/main/java/io/github/dsheirer/audio/AudioModule.java
Normal file
@@ -0,0 +1,188 @@
|
||||
/*******************************************************************************
|
||||
* 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;
|
||||
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.filter.polyphase.PolyphaseFIRDecimatingFilter_RB;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
/**
|
||||
* Provides packaging of demodulated audio sample buffers into audio packets for
|
||||
* broadcast to registered audio packet listeners. Includes audio packet
|
||||
* metadata in constructed audio packets.
|
||||
*
|
||||
* Incorporates audio squelch state listener to control if audio packets are
|
||||
* broadcast or ignored.
|
||||
*/
|
||||
public class AudioModule extends Module implements IAudioPacketProvider, IFilteredRealBufferListener,
|
||||
ISquelchStateListener, Listener<RealBuffer>
|
||||
{
|
||||
protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||
|
||||
private static float[] mDecimationCoefficients;
|
||||
|
||||
static
|
||||
{
|
||||
//Band-pass filter to both decimate (48kHz to 8kHz) and block 0 - 300 Hz (LTR signalling).
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
|
||||
.sampleRate(48000)
|
||||
.stopFrequency1(150)
|
||||
.passFrequencyBegin(300)
|
||||
.passFrequencyEnd(3500)
|
||||
.stopFrequency2(4000)
|
||||
.stopRipple(0.0004)
|
||||
.passRipple(0.008)
|
||||
.build();
|
||||
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
try
|
||||
{
|
||||
if(!designer.isValid())
|
||||
{
|
||||
throw new FilterDesignException("Couldn't design the audio decimation filter");
|
||||
}
|
||||
|
||||
mDecimationCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
catch(FilterDesignException e)
|
||||
{
|
||||
mLog.debug("Error designing filter", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||
private Listener<AudioPacket> mAudioPacketListener;
|
||||
private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter;
|
||||
private Metadata mMetadata;
|
||||
|
||||
public AudioModule(Metadata metadata)
|
||||
{
|
||||
mMetadata = metadata;
|
||||
|
||||
mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f);
|
||||
mAudioDecimationFilter.setListener(new Listener<RealBuffer>()
|
||||
{
|
||||
@Override
|
||||
public void receive(RealBuffer realBuffer)
|
||||
{
|
||||
if(mAudioPacketListener != null)
|
||||
{
|
||||
AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mMetadata.copyOf());
|
||||
mAudioPacketListener.receive(packet);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
mSquelchStateListener = null;
|
||||
mAudioPacketListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(ScheduledExecutorService executor)
|
||||
{
|
||||
/* No start operations provided */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
/* Issue an end audio packet in case a recorder is still rolling */
|
||||
if(mAudioPacketListener != null)
|
||||
{
|
||||
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes demodulated audio samples into audio packets with current audio
|
||||
* metadata and sends to the registered listener
|
||||
*/
|
||||
@Override
|
||||
public void receive(RealBuffer buffer)
|
||||
{
|
||||
if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
mAudioDecimationFilter.receive(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<RealBuffer> getFilteredRealBufferListener()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAudioPacketListener(Listener<AudioPacket> listener)
|
||||
{
|
||||
mAudioPacketListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAudioPacketListener()
|
||||
{
|
||||
mAudioPacketListener = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<SquelchState> getSquelchStateListener()
|
||||
{
|
||||
return mSquelchStateListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for squelch state listener
|
||||
*/
|
||||
public class SquelchStateListener implements Listener<SquelchState>
|
||||
{
|
||||
@Override
|
||||
public void receive(SquelchState state)
|
||||
{
|
||||
if(state == SquelchState.SQUELCH && mAudioPacketListener != null)
|
||||
{
|
||||
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
|
||||
}
|
||||
|
||||
mSquelchState = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/main/java/io/github/dsheirer/audio/AudioPacket.java
Normal file
78
src/main/java/io/github/dsheirer/audio/AudioPacket.java
Normal file
@@ -0,0 +1,78 @@
|
||||
/*******************************************************************************
|
||||
* 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;
|
||||
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
|
||||
public class AudioPacket
|
||||
{
|
||||
private Type mType;
|
||||
private RealBuffer mAudioData;
|
||||
private Metadata mMetadata;
|
||||
|
||||
public AudioPacket(Type type, Metadata metadata)
|
||||
{
|
||||
mType = type;
|
||||
mMetadata = metadata;
|
||||
}
|
||||
|
||||
public AudioPacket(float[] audio, Metadata metadata)
|
||||
{
|
||||
this(Type.AUDIO, metadata);
|
||||
|
||||
mAudioData = new RealBuffer(audio);
|
||||
}
|
||||
|
||||
public boolean hasMetadata()
|
||||
{
|
||||
return mMetadata != null;
|
||||
}
|
||||
|
||||
public Metadata getMetadata()
|
||||
{
|
||||
return mMetadata;
|
||||
}
|
||||
|
||||
public void setMetadata(Metadata metadata)
|
||||
{
|
||||
mMetadata = metadata;
|
||||
}
|
||||
|
||||
public Type getType()
|
||||
{
|
||||
return mType;
|
||||
}
|
||||
|
||||
public RealBuffer getAudioBuffer()
|
||||
{
|
||||
return mAudioData;
|
||||
}
|
||||
|
||||
public boolean hasAudioBuffer()
|
||||
{
|
||||
return mAudioData != null;
|
||||
}
|
||||
|
||||
public enum Type
|
||||
{
|
||||
AUDIO,
|
||||
END;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AudioPacketRateCounter implements Listener<AudioPacket>
|
||||
{
|
||||
private final static Logger mLog =
|
||||
LoggerFactory.getLogger( AudioPacketRateCounter.class );
|
||||
|
||||
private Listener<AudioPacket> mListener;
|
||||
private long mCount;
|
||||
private long mStartTime = -1;
|
||||
|
||||
private String mName;
|
||||
|
||||
public AudioPacketRateCounter()
|
||||
{
|
||||
mName = "UNNAMED";
|
||||
}
|
||||
|
||||
public AudioPacketRateCounter( String name )
|
||||
{
|
||||
mName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( AudioPacket packet )
|
||||
{
|
||||
if( mStartTime == -1 )
|
||||
{
|
||||
mStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
mCount += packet.getAudioBuffer().getSamples().length;
|
||||
|
||||
long elapsed = System.currentTimeMillis() - mStartTime;
|
||||
|
||||
if( elapsed != 0 )
|
||||
{
|
||||
mLog.debug( "[" + mName + "] Audio Rate: " + ( mCount / elapsed ) + " kHz" );
|
||||
}
|
||||
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( packet );
|
||||
}
|
||||
}
|
||||
|
||||
public void setListener( Listener<AudioPacket> listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
}
|
||||
413
src/main/java/io/github/dsheirer/audio/AudioPanel.java
Normal file
413
src/main/java/io/github/dsheirer/audio/AudioPanel.java
Normal file
@@ -0,0 +1,413 @@
|
||||
/*******************************************************************************
|
||||
* 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;
|
||||
|
||||
import io.github.dsheirer.audio.output.AudioOutput;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.SourceManager;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.FloatControl;
|
||||
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;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
|
||||
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
|
||||
|
||||
private static ImageIcon MUTED_ICON = IconManager.getScaledIcon(
|
||||
new ImageIcon("images/audio_muted.png"), 20);
|
||||
private static ImageIcon UNMUTED_ICON = IconManager.getScaledIcon(
|
||||
new ImageIcon("images/audio_unmuted.png"), 20);
|
||||
|
||||
private IconManager mIconManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
private SourceManager mSourceManager;
|
||||
private IAudioController mController;
|
||||
|
||||
private JButton mMuteButton;
|
||||
private AudioChannelsPanel mAudioChannelsPanel;
|
||||
|
||||
public AudioPanel(IconManager iconManager, SettingsManager settingsManager, SourceManager sourceManager,
|
||||
IAudioController controller)
|
||||
{
|
||||
mIconManager = iconManager;
|
||||
mSettingsManager = settingsManager;
|
||||
mSourceManager = sourceManager;
|
||||
mController = controller;
|
||||
|
||||
mController.addControllerListener(this);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
|
||||
setBackground(Color.BLACK);
|
||||
|
||||
mMuteButton = new MuteButton();
|
||||
mMuteButton.setBackground(getBackground());
|
||||
add(mMuteButton);
|
||||
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconManager, mSettingsManager, mController);
|
||||
|
||||
add(mAudioChannelsPanel);
|
||||
|
||||
addMouseListener(new MouseSelectionListener());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioEvent event)
|
||||
{
|
||||
switch(event.getType())
|
||||
{
|
||||
case AUDIO_CONFIGURATION_CHANGE_STARTED:
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
remove(mAudioChannelsPanel);
|
||||
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconManager, mSettingsManager, mController);
|
||||
|
||||
add(mAudioChannelsPanel);
|
||||
|
||||
mAudioChannelsPanel.repaint();
|
||||
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio output mute control menu item.
|
||||
*/
|
||||
public class AudioOutputMuteItem extends JMenuItem
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private AudioOutput mAudioOutput;
|
||||
|
||||
public AudioOutputMuteItem(AudioOutput audioOutput)
|
||||
{
|
||||
super(audioOutput.isMuted() ? "Unmute" : "Mute");
|
||||
|
||||
mAudioOutput = audioOutput;
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
mAudioOutput.setMuted(!mAudioOutput.isMuted());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse listener
|
||||
*/
|
||||
public class MouseSelectionListener implements MouseListener
|
||||
{
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent event)
|
||||
{
|
||||
if(SwingUtilities.isRightMouseButton(event))
|
||||
{
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
/* Audio mixer/output selection menus */
|
||||
JMenu outputMenu = new JMenu("Audio Output");
|
||||
|
||||
MixerChannelConfiguration[] mixerConfigurations =
|
||||
mSourceManager.getMixerManager().getOutputMixers();
|
||||
|
||||
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
|
||||
{
|
||||
MixerSelectionItem mixerItem = new MixerSelectionItem(mixerConfig);
|
||||
|
||||
try
|
||||
{
|
||||
MixerChannelConfiguration current = mController.getMixerChannelConfiguration();
|
||||
|
||||
if(current != null && current.equals(mixerConfig))
|
||||
{
|
||||
mixerItem.setSelected(true);
|
||||
}
|
||||
}
|
||||
catch(AudioException e)
|
||||
{
|
||||
mLog.error("Error while detecting current mixer "
|
||||
+ "channel configuration", e);
|
||||
}
|
||||
|
||||
outputMenu.add(mixerItem);
|
||||
}
|
||||
|
||||
popup.add(outputMenu);
|
||||
|
||||
/* Audio output mute and volume control */
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
{
|
||||
JMenu menu = new JMenu("Channel: " + output.getChannelName());
|
||||
|
||||
menu.add(new AudioOutputMuteItem(output));
|
||||
|
||||
if(output.hasGainControl())
|
||||
{
|
||||
JMenu volume = new JMenu("Volume");
|
||||
volume.add(new VolumeSlider(output.getGainControl()));
|
||||
menu.add(volume);
|
||||
}
|
||||
|
||||
popup.add(menu);
|
||||
}
|
||||
|
||||
popup.show(event.getComponent(), event.getX(), event.getY());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio volume (gain) adjustment slider control
|
||||
*/
|
||||
public class VolumeSlider extends JSlider
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private FloatControl mFloatControl;
|
||||
|
||||
public VolumeSlider(FloatControl control)
|
||||
{
|
||||
super(0, 100, 0);
|
||||
|
||||
setMajorTickSpacing(25);
|
||||
setMinorTickSpacing(5);
|
||||
setPaintTicks(true);
|
||||
setPaintLabels(true);
|
||||
|
||||
mFloatControl = control;
|
||||
|
||||
setValue(getIntegerValue(mFloatControl.getValue()));
|
||||
|
||||
addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent event)
|
||||
{
|
||||
mFloatControl.shift(mFloatControl.getValue(),
|
||||
getFloatValue(VolumeSlider.this.getValue()),
|
||||
1000);
|
||||
}
|
||||
});
|
||||
|
||||
addMouseListener(new MouseListener()
|
||||
{
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent event)
|
||||
{
|
||||
if(event.getClickCount() == 2)
|
||||
{
|
||||
VolumeSlider.this.setValue(50);
|
||||
}
|
||||
}
|
||||
|
||||
public void mouseReleased(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mousePressed(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mouseExited(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mouseEntered(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the integer value to a floating point value to use in the
|
||||
* float control. Assumes an integer value of 50 is the 0.0 dB mid
|
||||
* point (ie no gain ) value.
|
||||
*/
|
||||
private int getIntegerValue(float value)
|
||||
{
|
||||
if(value == 0.0f)
|
||||
{
|
||||
return 50;
|
||||
}
|
||||
else if(value < 0.0f)
|
||||
{
|
||||
float ratio = value / mFloatControl.getMinimum();
|
||||
|
||||
return 50 - (int) (ratio * 50.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
float ratio = value / mFloatControl.getMaximum();
|
||||
|
||||
return 50 + (int) (ratio * 50.0f);
|
||||
}
|
||||
}
|
||||
|
||||
private float getFloatValue(int value)
|
||||
{
|
||||
if(value == 50)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else if(value < 50)
|
||||
{
|
||||
return (float) (50 - value) / 50.0f * mFloatControl.getMinimum();
|
||||
}
|
||||
else
|
||||
{
|
||||
return (float) (value - 50) / 50.0f * mFloatControl.getMaximum();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixer/Channel configuration selection item
|
||||
*/
|
||||
public class MixerSelectionItem extends JMenuItem
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private MixerChannelConfiguration mConfiguration;
|
||||
|
||||
public MixerSelectionItem(MixerChannelConfiguration config)
|
||||
{
|
||||
super(config.toString());
|
||||
|
||||
mConfiguration = config;
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
mController.setMixerChannelConfiguration(mConfiguration);
|
||||
}
|
||||
catch(AudioException e1)
|
||||
{
|
||||
mLog.error("Couldn't set mixer channel configuration "
|
||||
+ "to: " + mConfiguration.toString());
|
||||
|
||||
JOptionPane.showMessageDialog(MixerSelectionItem.this,
|
||||
"Couldn't set [" + mConfiguration.toString() +
|
||||
"] as the audio output device");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute button to mute all audio output channels exposed by the audio
|
||||
* controller
|
||||
*/
|
||||
public class MuteButton extends JButton
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private boolean mMuted = false;
|
||||
|
||||
public MuteButton()
|
||||
{
|
||||
setIcon(UNMUTED_ICON);
|
||||
setBorderPainted(false);
|
||||
getAccessibleContext().setAccessibleName("Mute");
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
mMuted = !mMuted;
|
||||
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
{
|
||||
output.setMuted(mMuted);
|
||||
}
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
|
||||
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/main/java/io/github/dsheirer/audio/AudioUtils.java
Normal file
57
src/main/java/io/github/dsheirer/audio/AudioUtils.java
Normal file
@@ -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;
|
||||
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
public class AudioUtils
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioUtils.class);
|
||||
|
||||
/**
|
||||
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
|
||||
*/
|
||||
public static byte[] convertTo16BitSamples(List<AudioPacket> audioPackets)
|
||||
{
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
for(AudioPacket packet: audioPackets)
|
||||
{
|
||||
//Converting from 32-bit floats to signed 16-bit samples
|
||||
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
|
||||
stream.write(buffer.array());
|
||||
}
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Error writing converted PCM bytes to output stream");
|
||||
}
|
||||
|
||||
return stream.toByteArray();
|
||||
}
|
||||
}
|
||||
24
src/main/java/io/github/dsheirer/audio/IAudioController.java
Normal file
24
src/main/java/io/github/dsheirer/audio/IAudioController.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.audio.output.AudioOutput;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface for controlling one or more audio channels
|
||||
*/
|
||||
public interface IAudioController
|
||||
{
|
||||
/* Current Mixer and MixerChannel configuration */
|
||||
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException;
|
||||
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException;
|
||||
|
||||
/* Audio Output(s) */
|
||||
public List<AudioOutput> getAudioOutputs();
|
||||
|
||||
/* Controller Audio Event Listener */
|
||||
public void addControllerListener( Listener<AudioEvent> listener );
|
||||
public void removeControllerListener( Listener<AudioEvent> listener );
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface IAudioPacketListener
|
||||
{
|
||||
public Listener<AudioPacket> getAudioPacketListener();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface IAudioPacketProvider
|
||||
{
|
||||
public void setAudioPacketListener( Listener<AudioPacket> listener );
|
||||
public void removeAudioPacketListener();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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;
|
||||
|
||||
/**
|
||||
* Standard voice inversion foldover frequencies
|
||||
*
|
||||
* Reference: http://en.wikipedia.org/wiki/Voice_inversion
|
||||
*/
|
||||
public enum InversionFrequency
|
||||
{
|
||||
HZ_2500( 2500 ),
|
||||
HZ_2550( 2550 ),
|
||||
HZ_2600( 2600 ),
|
||||
HZ_2632( 2632 ),
|
||||
HZ_2675( 2675 ),
|
||||
HZ_2718( 2718 ),
|
||||
HZ_2800( 2800 ),
|
||||
HZ_2868( 2868 ),
|
||||
HZ_2950( 2950 ),
|
||||
HZ_3023( 3023 ),
|
||||
HZ_3075( 3075 ),
|
||||
HZ_3107( 3107 ),
|
||||
HZ_3150( 3150 ),
|
||||
HZ_3196( 3196 ),
|
||||
HZ_3333( 3333 ),
|
||||
HZ_3339( 3339 ),
|
||||
HZ_3400( 3400 ),
|
||||
HZ_3496( 3496 ),
|
||||
HZ_3729( 3729 ),
|
||||
HZ_4096( 4096 );
|
||||
|
||||
private int mFrequency;
|
||||
|
||||
private InversionFrequency( int frequency )
|
||||
{
|
||||
mFrequency = frequency;
|
||||
}
|
||||
|
||||
public int getFrequency()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* 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.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAudioConverter
|
||||
{
|
||||
/**
|
||||
* Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
|
||||
*/
|
||||
public byte[] convert(List<AudioPacket> audioPackets);
|
||||
|
||||
/**
|
||||
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the
|
||||
* remaining bytes to produce a full frame.
|
||||
*/
|
||||
public byte[] flush();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* 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.convert;
|
||||
|
||||
public interface ISilenceGenerator
|
||||
{
|
||||
byte[] generate(long duration);
|
||||
}
|
||||
@@ -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.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.audio.AudioUtils;
|
||||
import net.sourceforge.lame.lowlevel.LameEncoder;
|
||||
import net.sourceforge.lame.mp3.Lame;
|
||||
import net.sourceforge.lame.mp3.MPEGMode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MP3AudioConverter implements IAudioConverter
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger( MP3AudioConverter.class );
|
||||
public static final int AUDIO_QUALITY = Lame.QUALITY_LOW;
|
||||
private LameEncoder mEncoder;
|
||||
private ByteArrayOutputStream mMP3Stream = new ByteArrayOutputStream();
|
||||
private byte[] mMP3Buffer;
|
||||
|
||||
/**
|
||||
* Converts PCM 8kHz 16-bit Little Endian audio packets to Mono, Low Quality MP3 compressed audio.
|
||||
*
|
||||
* @param bitRate for converted MP3 audio
|
||||
* @param variableBitRate (VBR) true or false for constant bit rate (CBR)
|
||||
*/
|
||||
public MP3AudioConverter(int bitRate, boolean variableBitRate)
|
||||
{
|
||||
mEncoder = new LameEncoder(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
|
||||
bitRate, MPEGMode.MONO, AUDIO_QUALITY, variableBitRate);
|
||||
|
||||
mMP3Buffer = new byte[mEncoder.getPCMBufferSize()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] convert(List<AudioPacket> audioPackets)
|
||||
{
|
||||
mMP3Stream.reset();
|
||||
|
||||
byte[] pcmBytes = AudioUtils.convertTo16BitSamples(audioPackets);
|
||||
|
||||
int pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length);
|
||||
|
||||
int mp3BufferSize = 0;
|
||||
|
||||
int pcmBytesPosition = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer)))
|
||||
{
|
||||
pcmBytesPosition += pcmBufferSize;
|
||||
pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition);
|
||||
mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize);
|
||||
}
|
||||
|
||||
return mMP3Stream.toByteArray();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("There was an error converting audio to MP3: " + e.getMessage());
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] flush()
|
||||
{
|
||||
byte[] lastPartialFrame = new byte[mEncoder.getMP3BufferSize()];
|
||||
|
||||
int length = mEncoder.encodeFinish(lastPartialFrame);
|
||||
|
||||
return Arrays.copyOf(lastPartialFrame, length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*******************************************************************************
|
||||
* 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.convert;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MP3FrameInspector
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MP3FrameInspector.class);
|
||||
|
||||
public static void inspect(byte[] frames)
|
||||
{
|
||||
if(frames == null || frames.length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(frames.length % 144 != 0)
|
||||
{
|
||||
log("Frame byte array not multiple of 144 byte frame size - length:" + frames.length, frames);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check for erroneous frame sync
|
||||
for(int x = 0; x < frames.length - 1; x++)
|
||||
{
|
||||
if(frames[x] == (byte)0xFF && (frames[x + 1] & 0xE0) == 0xE0 && (x % 144) != 0)
|
||||
{
|
||||
log("Bad frame sync detected at byte " + x + " in frame " + (x / 144), frames);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Check MP3 headers
|
||||
|
||||
// log("Frames ...", frames);
|
||||
}
|
||||
|
||||
private static void log(String message, byte[] frames)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(message).append("\n");
|
||||
|
||||
int frameCounter = 0;
|
||||
sb.append(frameCounter++).append(" ");
|
||||
|
||||
for(int x = 0; x < frames.length; x++)
|
||||
{
|
||||
if(x > 0 && x % 144 == 0)
|
||||
{
|
||||
sb.append("\n");
|
||||
sb.append(frameCounter++).append(" ");
|
||||
}
|
||||
|
||||
sb.append(String.format("%02X ", frames[x]));
|
||||
}
|
||||
|
||||
mLog.info(sb.toString());
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.info("Starting ...");
|
||||
MP3SilenceGenerator gen = new MP3SilenceGenerator();
|
||||
|
||||
byte[] audio = gen.generate(173);
|
||||
|
||||
inspect(audio);
|
||||
|
||||
mLog.info("Finished");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*******************************************************************************
|
||||
* 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.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.record.mp3.MP3Recorder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
|
||||
|
||||
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
|
||||
private byte[] mPreviousPartialFrameData;
|
||||
|
||||
/**
|
||||
* Generates MP3 audio silence frames
|
||||
*/
|
||||
public MP3SilenceGenerator()
|
||||
{
|
||||
}
|
||||
|
||||
public byte[] generate(long duration)
|
||||
{
|
||||
int length = (int)(duration * 8); //8000 Hz sample rate
|
||||
float[] silence = new float[length];
|
||||
AudioPacket silencePacket = new AudioPacket(silence, null);
|
||||
List<AudioPacket> silencePackets = new ArrayList<>();
|
||||
silencePackets.add(silencePacket);
|
||||
|
||||
byte[] frameData = mGenerator.convert(silencePackets);
|
||||
|
||||
frameData = merge(mPreviousPartialFrameData, frameData);
|
||||
|
||||
if(frameData != null && frameData.length > 0)
|
||||
{
|
||||
if(frameData.length < 144)
|
||||
{
|
||||
mPreviousPartialFrameData = frameData;
|
||||
return null;
|
||||
}
|
||||
else if((frameData.length % 144) == 0)
|
||||
{
|
||||
mPreviousPartialFrameData = null;
|
||||
return frameData;
|
||||
}
|
||||
else
|
||||
{
|
||||
int integralFrameLength = (int)(frameData.length / 144) * 144;
|
||||
mPreviousPartialFrameData = Arrays.copyOfRange(frameData, integralFrameLength, frameData.length);
|
||||
return Arrays.copyOf(frameData, integralFrameLength);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] merge(byte[] a, byte[] b)
|
||||
{
|
||||
if(a == null && b == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if(a == null)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
else if(b == null)
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a,0,c,0,a.length);
|
||||
System.arraycopy(b,0,c,a.length,b.length);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
for(long x = 243; x < 500; x ++)
|
||||
{
|
||||
MP3SilenceGenerator generator = new MP3SilenceGenerator();
|
||||
|
||||
byte[] silence = generator.generate(x);
|
||||
|
||||
if(silence != null && silence.length > 0)
|
||||
{
|
||||
mLog.debug("Silence:" + x + " Length:" + silence.length);
|
||||
MP3FrameInspector.inspect(silence);
|
||||
}
|
||||
}
|
||||
|
||||
mLog.debug("Finished");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.invert;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
|
||||
import io.github.dsheirer.dsp.filter.Window.WindowType;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
|
||||
/**
|
||||
* Based on the AudioType applied, produces normal, muted, or audio invert
|
||||
* audio output.
|
||||
*/
|
||||
public class AudioAdapter
|
||||
{
|
||||
private static int mGain = 5;
|
||||
private Oscillator mSineWaveGenerator;
|
||||
private FloatFIRFilter mPostInversionFilter;
|
||||
private FloatFIRFilter mAudioHighPassFilter;
|
||||
private AudioType mAudioType = AudioType.NORMAL;
|
||||
private int mSampleRate;
|
||||
|
||||
public AudioAdapter( int sampleRate )
|
||||
{
|
||||
mSampleRate = sampleRate;
|
||||
mSineWaveGenerator = new Oscillator( 3000, mSampleRate );
|
||||
mAudioHighPassFilter = new FloatFIRFilter(
|
||||
FilterFactory.getHighPass( mSampleRate, 200, 1600, 48,
|
||||
WindowType.HAMMING, true ), mGain );
|
||||
mPostInversionFilter = new FloatFIRFilter(
|
||||
FilterFactory.getLowPass( mSampleRate, 3000, 5000, 48,
|
||||
WindowType.HAMMING, true ), mGain );
|
||||
}
|
||||
|
||||
public void setAudioType( AudioType type )
|
||||
{
|
||||
if( type != null && type != mAudioType )
|
||||
{
|
||||
mAudioType = type;
|
||||
|
||||
int inversionFrequency = type.getAudioInversionFrequency();
|
||||
|
||||
if( inversionFrequency != 0 )
|
||||
{
|
||||
mSineWaveGenerator.setFrequency( inversionFrequency );;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float get( float sample )
|
||||
{
|
||||
float retVal;
|
||||
|
||||
switch( mAudioType )
|
||||
{
|
||||
case NORMAL:
|
||||
retVal = mAudioHighPassFilter.get( sample );
|
||||
break;
|
||||
case MUTE:
|
||||
retVal = 0.0f;
|
||||
break;
|
||||
case INV2632:
|
||||
case INV2718:
|
||||
case INV2868:
|
||||
case INV3023:
|
||||
case INV3107:
|
||||
case INV3196:
|
||||
case INV3333:
|
||||
case INV3339:
|
||||
case INV3496:
|
||||
case INV3729:
|
||||
case INV4096:
|
||||
default:
|
||||
retVal = (float)( mPostInversionFilter.get(
|
||||
mAudioHighPassFilter.get( sample ) *
|
||||
mSineWaveGenerator.getFloat() ) );
|
||||
|
||||
mSineWaveGenerator.rotate();
|
||||
break;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.invert;
|
||||
|
||||
import io.github.dsheirer.audio.InversionFrequency;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
import io.github.dsheirer.dsp.filter.Filters;
|
||||
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
|
||||
/**
|
||||
* Applies audio inversion (or un-inversion) to a broadcast of float audio samples
|
||||
* by multiplying each successive sample by either a 1 or a -1 value.
|
||||
*/
|
||||
public class AudioInverter implements RealSampleListener
|
||||
{
|
||||
private Oscillator mSineWaveGenerator;
|
||||
private FloatFIRFilter mPostInversionLowPassFilter;
|
||||
private RealSampleListener mListener;
|
||||
|
||||
public AudioInverter( int inversionFrequency, int sampleRate )
|
||||
{
|
||||
mSineWaveGenerator = new Oscillator( inversionFrequency, sampleRate );
|
||||
|
||||
mPostInversionLowPassFilter = new FloatFIRFilter(
|
||||
Filters.FIRLP_55TAP_48000FS_3000FC.getCoefficients(), 1.04f );
|
||||
|
||||
mPostInversionLowPassFilter.setListener( new FilteredSampleProcessor() );
|
||||
}
|
||||
|
||||
public AudioInverter( InversionFrequency frequency, int sampleRate )
|
||||
{
|
||||
this( frequency.getFrequency(), sampleRate );
|
||||
}
|
||||
|
||||
public void setListener( RealSampleListener listener )
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
//Multiply the sample by the folding frequency and then send it to
|
||||
//the low pass filter
|
||||
if( mPostInversionLowPassFilter != null )
|
||||
{
|
||||
mPostInversionLowPassFilter.receive(
|
||||
sample * mSineWaveGenerator.getFloat() );
|
||||
|
||||
mSineWaveGenerator.rotate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple class to receive the output of the FIR low pass filter and then
|
||||
* send it to the registered listener(s)
|
||||
*/
|
||||
class FilteredSampleProcessor implements RealSampleListener
|
||||
{
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
if( mListener != null )
|
||||
{
|
||||
mListener.receive( sample );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/main/java/io/github/dsheirer/audio/invert/AudioType.java
Normal file
87
src/main/java/io/github/dsheirer/audio/invert/AudioType.java
Normal file
@@ -0,0 +1,87 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.invert;
|
||||
|
||||
public enum AudioType
|
||||
{
|
||||
NORMAL( "Normal Audio", "Clear", 0 ),
|
||||
MUTE( "Muted Audio", "Muted", 0 ),
|
||||
INV2500( "Inverted Audio 2500Hz", "Inverted", 2500 ),
|
||||
INV2550( "Inverted Audio 2550Hz", "Inverted", 2550 ),
|
||||
INV2600( "Inverted Audio 2600Hz", "Inverted", 2600 ),
|
||||
INV2632( "Inverted Audio 2632Hz", "Inverted", 2632 ),
|
||||
INV2675( "Inverted Audio 2675Hz", "Inverted", 2675 ),
|
||||
INV2718( "Inverted Audio 2718Hz", "Inverted", 2718 ),
|
||||
INV2725( "Inverted Audio 2725Hz", "Inverted", 2725 ),
|
||||
INV2750( "Inverted Audio 2750Hz", "Inverted", 2750 ),
|
||||
INV2760( "Inverted Audio 2760Hz", "Inverted", 2760 ),
|
||||
INV2775( "Inverted Audio 2775Hz", "Inverted", 2775 ),
|
||||
INV2800( "Inverted Audio 2800Hz", "Inverted", 2800 ),
|
||||
INV2825( "Inverted Audio 2825Hz", "Inverted", 2825 ),
|
||||
INV2868( "Inverted Audio 2868Hz", "Inverted", 2868 ),
|
||||
INV3023( "Inverted Audio 3023Hz", "Inverted", 3023 ),
|
||||
INV3107( "Inverted Audio 3107Hz", "Inverted", 3107 ),
|
||||
INV3196( "Inverted Audio 3196Hz", "Inverted", 3196 ),
|
||||
INV3333( "Inverted Audio 3333Hz", "Inverted", 3333 ),
|
||||
INV3339( "Inverted Audio 3339Hz", "Inverted", 3339 ),
|
||||
INV3360( "Inverted Audio 3360Hz", "Inverted", 3360 ),
|
||||
INV3375( "Inverted Audio 3375Hz", "Inverted", 3375 ),
|
||||
INV3400( "Inverted Audio 3400Hz", "Inverted", 3400 ),
|
||||
INV3450( "Inverted Audio 3450Hz", "Inverted", 3450 ),
|
||||
INV3496( "Inverted Audio 3496Hz", "Inverted", 3496 ),
|
||||
INV3729( "Inverted Audio 3729Hz", "Inverted", 3729 ),
|
||||
INV4096( "Inverted Audio 4096Hz", "Inverted", 4096 ),
|
||||
INV4300( "Inverted Audio 4300Hz", "Inverted", 4300 ),
|
||||
INV4500( "Inverted Audio 4500Hz", "Inverted", 4500 ),
|
||||
INV4700( "Inverted Audio 4700Hz", "Inverted", 4700 ),
|
||||
INV4900( "Inverted Audio 4900Hz", "Inverted", 4900 );
|
||||
|
||||
private String mDisplayString;
|
||||
private String mShortDisplayString;
|
||||
private int mAudioInversionFrequency;
|
||||
|
||||
AudioType( String displayString,
|
||||
String shortDisplayString,
|
||||
int inversionFrequency )
|
||||
{
|
||||
mDisplayString = displayString;
|
||||
mShortDisplayString = shortDisplayString;
|
||||
mAudioInversionFrequency = inversionFrequency;
|
||||
}
|
||||
|
||||
public String getDisplayString()
|
||||
{
|
||||
return mDisplayString;
|
||||
}
|
||||
|
||||
public String getShortDisplayString()
|
||||
{
|
||||
return mShortDisplayString;
|
||||
}
|
||||
|
||||
public int getAudioInversionFrequency()
|
||||
{
|
||||
return mAudioInversionFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mDisplayString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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.invert;
|
||||
|
||||
|
||||
public interface IAudioTypeListener
|
||||
{
|
||||
public void setAudioType( AudioType type );
|
||||
}
|
||||
443
src/main/java/io/github/dsheirer/audio/output/AudioOutput.java
Normal file
443
src/main/java/io/github/dsheirer/audio/output/AudioOutput.java
Normal file
@@ -0,0 +1,443 @@
|
||||
/*******************************************************************************
|
||||
* 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.output;
|
||||
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.channel.metadata.Metadata;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.BooleanControl;
|
||||
import javax.sound.sampled.Control;
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import javax.sound.sampled.Line;
|
||||
import javax.sound.sampled.LineEvent;
|
||||
import javax.sound.sampled.LineListener;
|
||||
import javax.sound.sampled.LineUnavailableException;
|
||||
import javax.sound.sampled.Mixer;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
|
||||
private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
|
||||
private int mBufferStartThreshold;
|
||||
private int mBufferStopThreshold;
|
||||
|
||||
private Listener<Metadata> mMetadataListener;
|
||||
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||
|
||||
private ScheduledFuture<?> mProcessorTask;
|
||||
|
||||
private SourceDataLine mOutput;
|
||||
private Mixer mMixer;
|
||||
private MixerChannel mMixerChannel;
|
||||
private FloatControl mGainControl;
|
||||
private BooleanControl mMuteControl;
|
||||
|
||||
private AudioEvent mAudioStartEvent;
|
||||
private AudioEvent mAudioStopEvent;
|
||||
|
||||
private boolean mCanProcessAudio = false;
|
||||
private long mLastActivity = System.currentTimeMillis();
|
||||
|
||||
/**
|
||||
* Single audio channel playback with automatic starting and stopping of the
|
||||
* underlying sourcedataline specified by the mixer and mixer channel
|
||||
* arguments.
|
||||
*
|
||||
* Maintains an internal non-blocking audio packet queue and processes this
|
||||
* queue 25 times a second (every 40 ms).
|
||||
*
|
||||
* @param mixer to obtain source data line
|
||||
* @param mixerChannel either mono or left/right stereo
|
||||
* @param audioFormat to use during playback
|
||||
* @param lineInfo to use when obtaining the source data line
|
||||
* @param requestedBufferSize of approximately 1 second of audio
|
||||
*/
|
||||
public AudioOutput(Mixer mixer,
|
||||
MixerChannel mixerChannel,
|
||||
AudioFormat audioFormat,
|
||||
Line.Info lineInfo,
|
||||
int requestedBufferSize)
|
||||
{
|
||||
mMixer = mixer;
|
||||
mMixerChannel = mixerChannel;
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = (SourceDataLine) mMixer.getLine(lineInfo);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
mOutput.open(audioFormat, requestedBufferSize);
|
||||
|
||||
//Start threshold: buffer is full with 10% or less of capacity remaining
|
||||
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
|
||||
|
||||
//Stop threshold: buffer is empty with 90% or more capacity available
|
||||
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
|
||||
|
||||
mOutput.addLineListener(this);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
mGainControl = (FloatControl) gain;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
/* Run the queue processor task every 40 milliseconds or 25 times a second */
|
||||
mProcessorTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(),
|
||||
0, 40, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED,
|
||||
getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED,
|
||||
getChannelName());
|
||||
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
mLog.error("Couldn't obtain audio source data line for "
|
||||
+ "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
public void reset()
|
||||
{
|
||||
broadcast(new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName()));
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mCanProcessAudio = false;
|
||||
|
||||
if(mProcessorTask != null)
|
||||
{
|
||||
mProcessorTask.cancel(true);
|
||||
}
|
||||
|
||||
mProcessorTask = null;
|
||||
|
||||
mBuffer.clear();
|
||||
|
||||
mAudioEventBroadcaster.dispose();
|
||||
mAudioEventBroadcaster = null;
|
||||
mMetadataListener = null;
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
mOutput.close();
|
||||
}
|
||||
|
||||
mOutput = null;
|
||||
mGainControl = null;
|
||||
mMuteControl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into a byte buffer format appropriate for
|
||||
* the underlying source data line.
|
||||
*/
|
||||
protected abstract ByteBuffer convert(AudioPacket packet);
|
||||
|
||||
/**
|
||||
* Audio output channel name
|
||||
*/
|
||||
public String getChannelName()
|
||||
{
|
||||
return mMixerChannel.getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixer Channel for this audio output
|
||||
*/
|
||||
protected MixerChannel getMixerChannel()
|
||||
{
|
||||
return mMixerChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a single listener to receive audio start and audio stop events
|
||||
*/
|
||||
public void addAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mAudioEventBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
public void removeAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mAudioEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an audio event to the registered listener
|
||||
*/
|
||||
private void broadcast(AudioEvent audioEvent)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(audioEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a single listener to receive the audio metadata from each
|
||||
* audio packet
|
||||
*/
|
||||
public void setMetadataListener(Listener<Metadata> listener)
|
||||
{
|
||||
mMetadataListener = listener;
|
||||
}
|
||||
|
||||
public void removeAudioMetadataListener()
|
||||
{
|
||||
mMetadataListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts audio metadata to the registered listener
|
||||
*/
|
||||
private void broadcast(Metadata metadata)
|
||||
{
|
||||
if(mMetadataListener != null)
|
||||
{
|
||||
mMetadataListener.receive(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of either last buffer received or last buffer processed
|
||||
*/
|
||||
public long getLastActivityTimestamp()
|
||||
{
|
||||
return mLastActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last activity timestamp to current system time
|
||||
*/
|
||||
public void updateTimestamp()
|
||||
{
|
||||
mLastActivity = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioPacket packet)
|
||||
{
|
||||
if(mCanProcessAudio)
|
||||
{
|
||||
//Update the activity timestamp so that this audio output doesn't
|
||||
//get disconnected before it starts processing the audio stream
|
||||
updateTimestamp();
|
||||
|
||||
mBuffer.add(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public class BufferProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
|
||||
public BufferProcessor()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
/* The processing flag ensures that only one instance of the
|
||||
* processor can run at any given time */
|
||||
if(mProcessing.compareAndSet(false, true))
|
||||
{
|
||||
List<AudioPacket> packets = new ArrayList<AudioPacket>();
|
||||
|
||||
mBuffer.drainTo(packets);
|
||||
|
||||
for(AudioPacket packet : packets)
|
||||
{
|
||||
if(packet.getType() == AudioPacket.Type.AUDIO)
|
||||
{
|
||||
broadcast(packet.getMetadata());
|
||||
|
||||
ByteBuffer buffer = convert(packet);
|
||||
|
||||
int wrote = 0;
|
||||
|
||||
if(!mOutput.isRunning())
|
||||
{
|
||||
int toWrite = mOutput.available();
|
||||
|
||||
if(toWrite > buffer.array().length)
|
||||
{
|
||||
toWrite = buffer.array().length;
|
||||
}
|
||||
|
||||
//Top off the buffer and check if we can start it
|
||||
wrote += mOutput.write(buffer.array(), 0, toWrite);
|
||||
|
||||
checkStart();
|
||||
}
|
||||
|
||||
if(mOutput.isRunning() && wrote < buffer.array().length)
|
||||
{
|
||||
//Blocking write
|
||||
wrote += mOutput.write(buffer.array(), wrote,
|
||||
buffer.array().length - wrote);
|
||||
}
|
||||
|
||||
updateTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
checkStop();
|
||||
|
||||
mProcessing.set(false);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error while processing audio buffers", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts audio playback once audio buffer is almost full and remaining
|
||||
* capacity falls below the start threshold.
|
||||
*/
|
||||
private void checkStart()
|
||||
{
|
||||
if(mCanProcessAudio &&
|
||||
!mOutput.isRunning() &&
|
||||
mOutput.available() <= mBufferStartThreshold)
|
||||
{
|
||||
mOutput.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops audio playback and drains the audio buffer to empty when the
|
||||
* audio buffer is mostly empty and the available buffer capacity
|
||||
* exceeds the stop threshold
|
||||
*/
|
||||
private void checkStop()
|
||||
{
|
||||
if(mCanProcessAudio &&
|
||||
mOutput.isRunning() &&
|
||||
mOutput.available() >= mBufferStopThreshold)
|
||||
{
|
||||
mOutput.drain();
|
||||
mOutput.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mute state for this audio output channel
|
||||
*/
|
||||
public void setMuted(boolean muted)
|
||||
{
|
||||
if(mMuteControl != null)
|
||||
{
|
||||
mMuteControl.setValue(muted);
|
||||
|
||||
broadcast(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED :
|
||||
AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current mute state for this audio output channel
|
||||
*/
|
||||
public boolean isMuted()
|
||||
{
|
||||
if(mMuteControl != null)
|
||||
{
|
||||
return mMuteControl.getValue();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gain/volume control for this audio output channel, if one is available.
|
||||
*/
|
||||
public FloatControl getGainControl()
|
||||
{
|
||||
return mGainControl;
|
||||
}
|
||||
|
||||
public boolean hasGainControl()
|
||||
{
|
||||
return mGainControl != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the source data line playback state and broadcasts audio events
|
||||
* to the registered listener as the state changes
|
||||
*/
|
||||
@Override
|
||||
public void update(LineEvent event)
|
||||
{
|
||||
LineEvent.Type type = event.getType();
|
||||
|
||||
if(type == LineEvent.Type.START)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(mAudioStartEvent);
|
||||
}
|
||||
else if(type == LineEvent.Type.STOP)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*******************************************************************************
|
||||
* 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.output;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
/**
|
||||
* Mono Audio output implementation
|
||||
*/
|
||||
public class MonoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 8000;
|
||||
|
||||
public MonoAudioOutput(Mixer mixer)
|
||||
{
|
||||
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into mono audio frames.
|
||||
*/
|
||||
protected ByteBuffer convert(AudioPacket packet)
|
||||
{
|
||||
if(packet.hasAudioBuffer())
|
||||
{
|
||||
float[] samples = packet.getAudioBuffer().getSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
for(float sample : samples)
|
||||
{
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*******************************************************************************
|
||||
* 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.output;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
/**
|
||||
* Stereo audio output implementation.
|
||||
*/
|
||||
public class StereoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 16000;
|
||||
|
||||
public StereoAudioOutput(Mixer mixer, MixerChannel channel)
|
||||
{
|
||||
super(mixer, channel, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO,
|
||||
BUFFER_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into stereo audio frames with the mixer
|
||||
* channel containing the audio and the other channel containing zero
|
||||
* valued (silent) samples.
|
||||
*/
|
||||
protected ByteBuffer convert(AudioPacket packet)
|
||||
{
|
||||
if(packet.hasAudioBuffer())
|
||||
{
|
||||
float[] samples = packet.getAudioBuffer().getSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 4)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
if(getMixerChannel() == MixerChannel.LEFT)
|
||||
{
|
||||
for(float sample : samples)
|
||||
{
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
shortBuffer.put((short) 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(float sample : samples)
|
||||
{
|
||||
shortBuffer.put((short) 0);
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface ISquelchStateListener
|
||||
{
|
||||
public Listener<SquelchState> getSquelchStateListener();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
public interface ISquelchStateProvider
|
||||
{
|
||||
public void setSquelchStateListener( Listener<SquelchState> listener );
|
||||
public void removeSquelchStateListener();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 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.squelch;
|
||||
|
||||
|
||||
/**
|
||||
* Provides squelch control based on presense of decoded audio samples.
|
||||
*
|
||||
* NOTE: this class is not yet fully implemented with audio sample processing
|
||||
* and currently only provides user driven squelch control
|
||||
*/
|
||||
public class ManualSquelchController extends SquelchController
|
||||
{
|
||||
public ManualSquelchController()
|
||||
{
|
||||
mSquelchMode = SquelchMode.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSquelchMode( SquelchMode mode )
|
||||
{
|
||||
//TODO: add functionality
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 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.squelch;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.real.RealBuffer;
|
||||
import io.github.dsheirer.sample.real.RealSampleListener;
|
||||
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
|
||||
|
||||
/**
|
||||
* Squelch control - receives single audio samples or real audio buffers and
|
||||
* adjusts squelch state either based on the audio samples or based on external
|
||||
* control inputs.
|
||||
*
|
||||
* Assembles received single audio samples into real sample buffers for
|
||||
* consumption by an external audio processor, like the AudioPacketProcessor
|
||||
* class.
|
||||
*/
|
||||
public abstract class SquelchController
|
||||
implements Listener<RealBuffer>, RealSampleListener
|
||||
{
|
||||
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
|
||||
private Listener<RealBuffer> mAudioListener;
|
||||
protected SquelchState mSquelchState = SquelchState.UNSQUELCH;
|
||||
protected SquelchMode mSquelchMode = SquelchMode.MANUAL;
|
||||
|
||||
private RealToRealBufferAssembler mSampleAssembler = new RealToRealBufferAssembler( 512 );
|
||||
|
||||
public SquelchController()
|
||||
{
|
||||
mSampleAssembler.setListener( this );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( float sample )
|
||||
{
|
||||
mSampleAssembler.receive( sample );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( RealBuffer buffer )
|
||||
{
|
||||
if( mAudioListener != null )
|
||||
{
|
||||
mAudioListener.receive( buffer );
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void setSquelchMode( SquelchMode mode );
|
||||
|
||||
public SquelchMode getSquelchMode()
|
||||
{
|
||||
return mSquelchMode;
|
||||
}
|
||||
|
||||
public void setSquelchState( SquelchState state )
|
||||
{
|
||||
mSquelchState = state;
|
||||
}
|
||||
|
||||
public SquelchState getSquelchState()
|
||||
{
|
||||
return mSquelchState;
|
||||
}
|
||||
|
||||
public boolean isSquelched()
|
||||
{
|
||||
switch( mSquelchMode )
|
||||
{
|
||||
case AUTOMATIC:
|
||||
case MANUAL:
|
||||
return mSquelchState == SquelchState.SQUELCH;
|
||||
case NONE:
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void addSquelchStateListener( Listener<SquelchState> listener )
|
||||
{
|
||||
if( listener != null )
|
||||
{
|
||||
mSquelchStateBroadcaster.addListener( listener );
|
||||
|
||||
/* Send current squelch state to the listener */
|
||||
listener.receive( mSquelchState );
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSquelchStateListener( Listener<SquelchState> listener )
|
||||
{
|
||||
mSquelchStateBroadcaster.removeListener( listener );
|
||||
}
|
||||
|
||||
public void setAudioListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mAudioListener = listener;
|
||||
}
|
||||
|
||||
public void removeAudioListener( Listener<RealBuffer> listener )
|
||||
{
|
||||
mAudioListener = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
/**
|
||||
* Squelch state for a decoding channel. The state determines the handling of
|
||||
* any audio packets processed by the channel state.
|
||||
*/
|
||||
public enum SquelchMode
|
||||
{
|
||||
/**
|
||||
* Automatic: decoded message activity dictates squelch state and determines
|
||||
* if audio is passed
|
||||
*/
|
||||
AUTOMATIC,
|
||||
|
||||
/**
|
||||
* Manually applied squelch noise level threshold determines if audio is
|
||||
* passed.
|
||||
*/
|
||||
MANUAL,
|
||||
|
||||
/**
|
||||
* No Squelch - no squelch is applied and all audio is passed
|
||||
*/
|
||||
NONE;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.github.dsheirer.audio.squelch;
|
||||
|
||||
public enum SquelchState
|
||||
{
|
||||
SQUELCH,
|
||||
UNSQUELCH;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 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.squelch;
|
||||
|
||||
public class TrunkingSquelchController extends SquelchController
|
||||
{
|
||||
private boolean mAllowSquelchOverride = true;
|
||||
|
||||
public TrunkingSquelchController()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides control over audio stream as directed by an external trunking
|
||||
* channel state.
|
||||
*
|
||||
* Override automatic squelch control by setting the squelch mode to none.
|
||||
*
|
||||
* Disable automatic control override by setting allowManualOverride to false;
|
||||
*
|
||||
* Note: this control does not respond to squelch mode manual.
|
||||
*/
|
||||
public TrunkingSquelchController( boolean allowSquelchOverride )
|
||||
{
|
||||
mAllowSquelchOverride = allowSquelchOverride;
|
||||
|
||||
mSquelchMode = SquelchMode.AUTOMATIC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSquelchMode( SquelchMode mode )
|
||||
{
|
||||
if( mode == SquelchMode.AUTOMATIC ||
|
||||
( mode == SquelchMode.NONE && mAllowSquelchOverride ) )
|
||||
{
|
||||
mSquelchMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user