#30 - adds support for Shoutcast V1 streaming. Resolves issue where temporary MP3 recording files were not being flushed at the end, causing partial frames to be streamed resulting in repeated remote server connection resets.

Note: Shoutcast V1 streaming to a Shoutcast V2.5.x server seems to be problematic.  The newer server tracks MP3 boundaries in order to insert advertising and this appears to have problems keeping sync with the MP3 stream.  I spent significant time eliminating MP3 framing errors in my code.  You can see these 'Bad frame' errors in the sc_serv.log and the shoutcast server will eventually reset the connection after a period of time or after a number of framing errors occur.  I anticipate that streaming to a Shoutcast V2 server using the ultravox (ie V2.x) protocol may resolve this issue.
This commit is contained in:
Denny
2016-12-29 05:59:14 -05:00
parent 0cebe7e5d9
commit a4e3c53ba8
16 changed files with 1114 additions and 384 deletions

View File

@@ -0,0 +1,320 @@
/*******************************************************************************
* 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 audio.broadcast.shoutcast.v1;
import alias.AliasModel;
import audio.broadcast.BroadcastConfiguration;
import audio.broadcast.BroadcastConfigurationEditor;
import audio.broadcast.BroadcastEvent;
import audio.broadcast.BroadcastModel;
import audio.broadcast.BroadcastServerType;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import settings.SettingsManager;
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 JCheckBox mEnabled;
private JButton mSaveButton;
private JButton mResetButton;
public ShoutcastV1ConfigurationEditor(BroadcastModel broadcastModel, AliasModel aliasModel,
SettingsManager settingsManager)
{
super(broadcastModel, aliasModel, settingsManager);
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 = mSettingsManager.getImageIcon(BroadcastServerType.SHOUTCAST_V1.getIconName(), 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("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));
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);
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.setDescription(mDescription.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.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;
}
}