mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
516 lines
20 KiB
Java
516 lines
20 KiB
Java
/*******************************************************************************
|
|
* 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.v2;
|
|
|
|
import audio.AudioPacket;
|
|
import audio.broadcast.AudioBroadcaster;
|
|
import audio.broadcast.BroadcastFactory;
|
|
import audio.broadcast.BroadcastFormat;
|
|
import audio.broadcast.BroadcastState;
|
|
import audio.broadcast.shoutcast.v2.message.AuthenticateBroadcast;
|
|
import audio.broadcast.shoutcast.v2.message.ConfigureIcyGenre;
|
|
import audio.broadcast.shoutcast.v2.message.ConfigureIcyName;
|
|
import audio.broadcast.shoutcast.v2.message.ConfigureIcyPublic;
|
|
import audio.broadcast.shoutcast.v2.message.ConfigureIcyURL;
|
|
import audio.broadcast.shoutcast.v2.message.MP3Audio;
|
|
import audio.broadcast.shoutcast.v2.message.NegotiateMaxPayloadSize;
|
|
import audio.broadcast.shoutcast.v2.message.RequestCipher;
|
|
import audio.broadcast.shoutcast.v2.message.SetupBroadcast;
|
|
import audio.broadcast.shoutcast.v2.message.Standby;
|
|
import audio.broadcast.shoutcast.v2.message.UltravoxMessage;
|
|
import audio.broadcast.shoutcast.v2.message.UltravoxMessageFactory;
|
|
import audio.broadcast.shoutcast.v2.message.UltravoxMessageType;
|
|
import audio.broadcast.shoutcast.v2.message.UltravoxMetadata;
|
|
import audio.broadcast.shoutcast.v2.message.XMLMetadata;
|
|
import audio.metadata.AudioMetadata;
|
|
import controller.ThreadPoolManager;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import properties.SystemProperties;
|
|
import record.wave.AudioPacketMonoWaveReader;
|
|
|
|
import java.io.DataInputStream;
|
|
import java.io.DataOutputStream;
|
|
import java.io.IOException;
|
|
import java.net.Socket;
|
|
import java.net.SocketAddress;
|
|
import java.net.UnknownHostException;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster
|
|
{
|
|
private final static Logger mLog = LoggerFactory.getLogger( ShoutcastV2AudioBroadcaster.class );
|
|
|
|
private static final long RECONNECT_INTERVAL_MILLISECONDS = 15000; //15 seconds
|
|
private static final boolean GET_RESPONSE = true;
|
|
private static final boolean NO_RESPONSE_EXPECTED = false;
|
|
private long mLastConnectionAttempt = 0;
|
|
|
|
private Socket mSocket;
|
|
private DataOutputStream mOutputStream;
|
|
private DataInputStream mInputStream;
|
|
private int mMaxPayloadSize = 16377;
|
|
private List<AudioPacket> mPacketsToBroadcast = new ArrayList<>();
|
|
|
|
/**
|
|
* Creates a Shoutcast Version 2 broadcaster.
|
|
* @param configuration details for shoutcast version 2
|
|
*/
|
|
public ShoutcastV2AudioBroadcaster(ThreadPoolManager threadPoolManager, ShoutcastV2Configuration configuration)
|
|
{
|
|
super(threadPoolManager, configuration);
|
|
}
|
|
|
|
/**
|
|
* Shoutcast V2 Configuration information
|
|
*/
|
|
private ShoutcastV2Configuration getShoutcastConfiguration()
|
|
{
|
|
return (ShoutcastV2Configuration)getBroadcastConfiguration();
|
|
}
|
|
|
|
@Override
|
|
protected void broadcastMetadata(AudioMetadata metadata)
|
|
{
|
|
mLog.debug("Request to broadcast audio metadata to shoutcast v2 server - needs coding");
|
|
}
|
|
|
|
/**
|
|
* Broadcast audio
|
|
*/
|
|
@Override
|
|
protected void broadcastAudio(byte[] audio)
|
|
{
|
|
//If we're connected, send the audio, otherwise discard it
|
|
if(connect())
|
|
{
|
|
List<UltravoxMessage> audioMessages = getAudioMessages(audio);
|
|
|
|
try
|
|
{
|
|
for(UltravoxMessage audioMessage: audioMessages)
|
|
{
|
|
send(audioMessage, NO_RESPONSE_EXPECTED);
|
|
}
|
|
}
|
|
catch(IOException e)
|
|
{
|
|
mLog.error("Error while dispatching audio", e);
|
|
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates if the audio handler is currently connected to the remote server and capable of streaming audio.
|
|
*
|
|
* This method will attempt to establish a connection or reconnect to the streaming server.
|
|
* @return true if the audio handler can stream audio
|
|
*/
|
|
private boolean connect()
|
|
{
|
|
if(!connected())
|
|
{
|
|
createConnection();
|
|
}
|
|
|
|
return connected();
|
|
}
|
|
|
|
|
|
/**
|
|
* Disconnect from the remote server
|
|
*/
|
|
public void disconnect()
|
|
{
|
|
// TerminateBroadcast terminate = (TerminateBroadcast)UltravoxMessageFactory
|
|
// .getMessage(UltravoxMessageType.TERMINATE_BROADCAST);
|
|
// response = send(terminate, out, in);
|
|
// mLog.debug("Terminate Response:" + (response.getPayload() != null ? response.getPayload() : ""));
|
|
|
|
}
|
|
|
|
/**
|
|
* Sends the message. When get response is true, reads from the input stream and converts the response to a
|
|
* message that is returned.
|
|
*
|
|
* @param message to send to the shoutcast/ultravox server
|
|
* @param getResponse true if the sent message will generate a response
|
|
* @return response message when getResponse is set to true
|
|
* @throws IOException if there is an issue while communicating with the server
|
|
*/
|
|
private UltravoxMessage send(UltravoxMessage message, boolean getResponse) throws IOException
|
|
{
|
|
byte[] bytes = message.getMessage();
|
|
|
|
mOutputStream.write(bytes);
|
|
|
|
if(getResponse)
|
|
{
|
|
try
|
|
{
|
|
Thread.sleep(50);
|
|
}
|
|
catch (InterruptedException e)
|
|
{
|
|
e.printStackTrace();
|
|
}
|
|
|
|
int available = mInputStream.available();
|
|
|
|
if(available > 0)
|
|
{
|
|
byte[] buffer = new byte[available];
|
|
int read = mInputStream.read(buffer);
|
|
|
|
return UltravoxMessageFactory.getMessage(buffer);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Loads the encoded audio byte array into a list of ultravox messages ready for transmission
|
|
* @param audio to embed as message payload
|
|
* @return list of messages containing the audio payload
|
|
*/
|
|
private List<UltravoxMessage> getAudioMessages(byte[] audio)
|
|
{
|
|
List<UltravoxMessage> messages = new ArrayList<>();
|
|
|
|
int pointer = 0;
|
|
|
|
while(pointer < audio.length)
|
|
{
|
|
int payloadSize = Math.min(mMaxPayloadSize, audio.length - pointer);
|
|
|
|
byte[] payload = new byte[payloadSize];
|
|
|
|
System.arraycopy(audio, pointer, payload, 0, payloadSize);
|
|
|
|
MP3Audio mp3Audio = (MP3Audio)UltravoxMessageFactory.getMessage(UltravoxMessageType.MP3_DATA);
|
|
mp3Audio.setPayload(payload);
|
|
messages.add(mp3Audio);
|
|
|
|
pointer += payloadSize;
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
/**
|
|
* Encodes the list of metadata in one or more Ultravox XMLMetadata 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 xmlMetadata list of xml encoded values
|
|
* @return a sequence of XMLMetadata messages sufficient to carry the complete set of metadata
|
|
*/
|
|
private List<UltravoxMessage> getMetadataMessages(List<String> xmlMetadata)
|
|
{
|
|
List<UltravoxMessage> messages = new ArrayList<>();
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<metadata>\n");
|
|
|
|
for(String metadata: xmlMetadata)
|
|
{
|
|
sb.append(metadata);
|
|
}
|
|
|
|
sb.append("\n</metadata>");
|
|
|
|
mLog.debug("Metadata:" + sb.toString());
|
|
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
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
XMLMetadata message = (XMLMetadata)UltravoxMessageFactory.getMessage(UltravoxMessageType.XML_METADATA);
|
|
message.setPayload(payload);
|
|
messages.add(message);
|
|
|
|
pointer += payloadSize;
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates a connnection to the remote server using the shoutcast configuration information. Once disconnected
|
|
* following a successful connection, attempts to reestablish a connection on a set interval
|
|
*/
|
|
private void createConnection()
|
|
{
|
|
if(!connected() && System.currentTimeMillis() - mLastConnectionAttempt >= RECONNECT_INTERVAL_MILLISECONDS)
|
|
{
|
|
mLastConnectionAttempt = System.currentTimeMillis();
|
|
|
|
createSocket();
|
|
|
|
if (mSocket.isConnected())
|
|
{
|
|
RequestCipher requestCipher = (RequestCipher) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.REQUEST_CIPHER);
|
|
|
|
try
|
|
{
|
|
UltravoxMessage response = send(requestCipher, GET_RESPONSE);
|
|
|
|
if (response instanceof RequestCipher)
|
|
{
|
|
RequestCipher cipherResponse = (RequestCipher) response;
|
|
|
|
AuthenticateBroadcast authenticateBroadcast = (AuthenticateBroadcast) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.AUTHENTICATE_BROADCAST);
|
|
authenticateBroadcast.setCredentials(cipherResponse.getCipher(),
|
|
getShoutcastConfiguration().getStreamID(), getShoutcastConfiguration().getUserID(),
|
|
getShoutcastConfiguration().getPassword());
|
|
response = send(authenticateBroadcast, GET_RESPONSE);
|
|
|
|
if (response.isErrorResponse())
|
|
{
|
|
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
|
return;
|
|
}
|
|
|
|
UltravoxMessage streamMimeType = UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.STREAM_MIME_TYPE);
|
|
streamMimeType.setPayload(getShoutcastConfiguration().getBroadcastFormat().getValue());
|
|
response = send(streamMimeType, GET_RESPONSE);
|
|
|
|
if (response.isErrorResponse())
|
|
{
|
|
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
|
|
mLog.error("Audio format [" + getShoutcastConfiguration().getBroadcastFormat() + "] not supported");
|
|
return;
|
|
}
|
|
|
|
SetupBroadcast setupBroadcast = (SetupBroadcast) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.SETUP_BROADCAST);
|
|
setupBroadcast.setBitRate(getShoutcastConfiguration().getBitRate(),
|
|
getShoutcastConfiguration().getBitRate());
|
|
response = send(setupBroadcast, GET_RESPONSE);
|
|
|
|
if (response.isErrorResponse())
|
|
{
|
|
setBroadcastState(BroadcastState.UNSUPPORTED_AUDIO_FORMAT);
|
|
mLog.error("Audio bit rate [" + getShoutcastConfiguration().getBitRate() + "] not supported");
|
|
return;
|
|
}
|
|
|
|
NegotiateMaxPayloadSize negotiateMaxPayloadSize = (NegotiateMaxPayloadSize) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.NEGOTIATE_MAX_PAYLOAD_SIZE);
|
|
negotiateMaxPayloadSize.setMaximumPayloadSize(mMaxPayloadSize, 14000);
|
|
response = send(negotiateMaxPayloadSize, GET_RESPONSE);
|
|
mMaxPayloadSize = ((NegotiateMaxPayloadSize) response).getMaximumPayloadSize();
|
|
|
|
List<String> metadata = new ArrayList<>();
|
|
|
|
if (getShoutcastConfiguration().getStreamName() != null)
|
|
{
|
|
metadata.add(UltravoxMetadata.TITLE_1.asXML(getShoutcastConfiguration().getStreamName()));
|
|
|
|
ConfigureIcyName icyName = (ConfigureIcyName) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.CONFIGURE_ICY_NAME);
|
|
icyName.setName(getShoutcastConfiguration().getStreamName());
|
|
response = send(icyName, NO_RESPONSE_EXPECTED);
|
|
}
|
|
|
|
if (getShoutcastConfiguration().getGenre() != null)
|
|
{
|
|
metadata.add(UltravoxMetadata.GENRE.asXML(getShoutcastConfiguration().getGenre()));
|
|
|
|
ConfigureIcyGenre icyGenre = (ConfigureIcyGenre) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.CONFIGURE_ICY_GENRE);
|
|
icyGenre.setGenre(getShoutcastConfiguration().getGenre());
|
|
response = send(icyGenre, NO_RESPONSE_EXPECTED);
|
|
}
|
|
|
|
if (getShoutcastConfiguration().getURL() != null)
|
|
{
|
|
ConfigureIcyURL icyURL = (ConfigureIcyURL) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.CONFIGURE_ICY_URL);
|
|
icyURL.setURL(getShoutcastConfiguration().getURL());
|
|
response = send(icyURL, NO_RESPONSE_EXPECTED);
|
|
}
|
|
|
|
ConfigureIcyPublic icyPublic = (ConfigureIcyPublic) UltravoxMessageFactory
|
|
.getMessage(UltravoxMessageType.CONFIGURE_ICY_PUBLIC);
|
|
icyPublic.setPublic(getShoutcastConfiguration().isPublic());
|
|
response = send(icyPublic, NO_RESPONSE_EXPECTED);
|
|
|
|
Standby standby = (Standby) UltravoxMessageFactory.getMessage(UltravoxMessageType.STANDBY);
|
|
response = send(standby, GET_RESPONSE);
|
|
|
|
if (response.isValidResponse())
|
|
{
|
|
setBroadcastState(BroadcastState.CONNECTED);
|
|
}
|
|
else
|
|
{
|
|
setBroadcastState(BroadcastState.DISCONNECTED);
|
|
mLog.error("Error message after configuration and sending standby:" +
|
|
(response.getError() != null ? response.getError() : "no error message"));
|
|
}
|
|
|
|
metadata.add(UltravoxMetadata.BROADCAST_CLIENT_APPLICATION
|
|
.asXML(SystemProperties.getInstance().getApplicationName()));
|
|
|
|
mLog.debug("Fetching metadata messages from metadata...");
|
|
List<UltravoxMessage> messages = getMetadataMessages(metadata);
|
|
|
|
for (UltravoxMessage message : messages)
|
|
{
|
|
mLog.debug("Sending metadata message");
|
|
send(message, GET_RESPONSE);
|
|
}
|
|
|
|
mLog.debug("We're connected!!!");
|
|
}
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
setBroadcastState(BroadcastState.ERROR);
|
|
mLog.error("Error while creating connection to server", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a socket connection to the remote server and sets the state to CONNECTING.
|
|
*/
|
|
private void createSocket()
|
|
{
|
|
if(mSocket == null)
|
|
{
|
|
try
|
|
{
|
|
mSocket = new Socket(getShoutcastConfiguration().getHost(),
|
|
getShoutcastConfiguration().getPort());
|
|
mOutputStream = new DataOutputStream(mSocket.getOutputStream());
|
|
mInputStream = new DataInputStream(mSocket.getInputStream());
|
|
}
|
|
catch(UnknownHostException uhe)
|
|
{
|
|
setBroadcastState(BroadcastState.NO_SERVER);
|
|
mLog.error("Unknown host or port. Unable to create connection to streaming server host[" +
|
|
getShoutcastConfiguration().getHost() + "] and port[" +
|
|
getShoutcastConfiguration().getPort() + "] - will reattempt connection periodically");
|
|
return;
|
|
}
|
|
catch(IOException ioe)
|
|
{
|
|
setBroadcastState(BroadcastState.ERROR);
|
|
|
|
mLog.error("Error connecting to streaming server host[" +
|
|
getShoutcastConfiguration().getHost() + "] and port[" +
|
|
getShoutcastConfiguration().getPort() + "]", ioe);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(mSocket.isConnected())
|
|
{
|
|
setBroadcastState(BroadcastState.CONNECTING);
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
SocketAddress address = getShoutcastConfiguration().getAddress();
|
|
mSocket.connect(address);
|
|
setBroadcastState(BroadcastState.CONNECTING);
|
|
}
|
|
catch(UnknownHostException uhe)
|
|
{
|
|
setBroadcastState(BroadcastState.NO_SERVER);
|
|
}
|
|
catch(IOException e)
|
|
{
|
|
setBroadcastState(BroadcastState.ERROR);
|
|
|
|
mLog.error("Error connecting to streaming server host[" +
|
|
getShoutcastConfiguration().getHost() + "] and port[" +
|
|
getShoutcastConfiguration().getPort() + "]", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static UltravoxMessage send(UltravoxMessage message,
|
|
DataOutputStream out,
|
|
DataInputStream in,
|
|
int maxPayloadSize) throws IOException
|
|
{
|
|
byte[] bytes = message.getMessage();
|
|
|
|
out.write(bytes);
|
|
|
|
try
|
|
{
|
|
Thread.sleep(50);
|
|
}
|
|
catch (InterruptedException e)
|
|
{
|
|
e.printStackTrace();
|
|
}
|
|
|
|
int available = in.available();
|
|
|
|
if(available > 0)
|
|
{
|
|
byte[] buffer = new byte[available];
|
|
int read = in.read(buffer);
|
|
|
|
return UltravoxMessageFactory.getMessage(buffer);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|