/******************************************************************************* * 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 * ******************************************************************************/ 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 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 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 getAudioMessages(byte[] audio) { List 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: value * * 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 getMetadataMessages(List xmlMetadata) { List messages = new ArrayList<>(); StringBuilder sb = new StringBuilder(); sb.append("\n\n"); for(String metadata: xmlMetadata) { sb.append(metadata); } sb.append("\n"); 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 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 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; } }