mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 09:07:27 -05:00
337 lines
13 KiB
Java
337 lines
13 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.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());
|
|
}
|
|
}
|
|
}
|
|
}
|