/******************************************************************************* * 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.icecast; import audio.broadcast.BroadcastState; import audio.broadcast.icecast.codec.IcecastCodecFactory; import audio.convert.MP3AudioConverter; 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 properties.SystemProperties; import util.ThreadPool; 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 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 = 30000; //30 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(10000); // mSocketConnector.getFilterChain().addLast("logger", // new LoggingFilter(IcecastTCPAudioBroadcaster.class)); 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())); 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(connected()) { if(mStreamingSession != null) { mStreamingSession.closeNow(); } } } /** * 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 { //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 cause) throws Exception { if(cause instanceof IOException) { IOException ioe = (IOException)cause; if(ioe.getMessage() != null) { String reason = ioe.getMessage(); if(connected()) { mLog.info("Streaming connection error detected - resetting connection - " + reason); disconnect(); connect(); } else if(reason.startsWith("Connection reset")) { mLog.info("Streaming connection reset by remote server - reestablishing connection"); disconnect(); connect(); } else if(reason.startsWith("Operation timed out")) { mLog.info("Streaming connection timed out - resetting connection"); disconnect(); connect(); } else { setBroadcastState(BroadcastState.ERROR); disconnect(); mLog.error("Unrecognized IO error: " + reason + ". Streaming halted.", cause); } } else if(ioe instanceof WriteTimeoutException) { mLog.info("Network write timeout error - resetting connection"); disconnect(); connect(); } else { setBroadcastState(BroadcastState.ERROR); disconnect(); mLog.error("Unspecified IO error - streaming halted.", cause); } } else { mLog.error("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("Invalid Password") || message.contains("Authentication Required")) { setBroadcastState(BroadcastState.INVALID_CREDENTIALS); } else { mLog.error("Unrecognized server response:" + message); setBroadcastState(BroadcastState.ERROR); } } } else { mLog.error("Icecast TCP broadcaster - unrecognized message [ " + object.getClass() + "] received:" + object.toString()); } } } }