package org.red5.server.icy;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.filterchain.IoFilter;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.SocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import org.red5.server.icy.codec.ICYDecoder;
import org.red5.server.icy.codec.ICYEncoder;
import org.red5.server.icy.codec.ICYDecoder.ReadState;
import org.red5.server.icy.message.AACFrame;
import org.red5.server.icy.message.MP3Frame;
import org.red5.server.icy.message.NSVFrame;
import org.red5.server.icy.nsv.NSVStreamConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SHOUTcast / ICY protocol handler.
*
* @author Paul Gregoire (mondain@gmail.com)
*/
public class ICYHandler extends IoHandlerAdapter {
private static Logger log = LoggerFactory.getLogger(ICYHandler.class);
private static final String userAgent = "Mozilla/4.0 (compatible; Red5 Server/NSV plugin)";
private static ProtocolCodecFactory codecFactory = new ProtocolCodecFactory() {
//coders can be shared
private ProtocolEncoder icyEncoder = new ICYEncoder();
private ProtocolDecoder icyDecoder = new ICYDecoder();
public ProtocolEncoder getEncoder(IoSession session) {
return icyEncoder;
}
public ProtocolDecoder getDecoder(IoSession session) {
return icyDecoder;
}
};
private static IoFilter codecFilter = new ProtocolCodecFilter(codecFactory);
private String host = "0.0.0.0";
private int port = 8001;
private int mode = 0;
private SocketAcceptor acceptor;
private IoBuffer outBuffer;
private IICYHandler handler;
public NSVStreamConfig config;
private boolean connected;
//private long lastDataTs;
private String password;
//thread sleep period
private int waitTime = 50;
//data timeout in milliseconds
private long dataTimeout = 10000;
//password has been accepted
private boolean validated;
//determines how to notify players that the video is upside down
private boolean notifyFlipped;
private String audioType;
public void start() {
log.debug("Starting icy socket handler");
switch (mode) {
case 1: // client mode
// create a singular HttpClient object
HttpClient client = new HttpClient();
// use proxy if specified
if (System.getProperty("http.proxyHost") != null && System.getProperty("http.proxyPort") != null) {
HostConfiguration config = client.getHostConfiguration();
config.setProxy(System.getProperty("http.proxyHost").toString(), Integer.parseInt( System.getProperty("http.proxyPort")));
}
// establish a connection within 5 seconds
client.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
//get the params for the client
HttpClientParams params = client.getParams();
params.setParameter(HttpMethodParams.USER_AGENT, userAgent);
//get registry file
HttpMethod method = new GetMethod(host);
//follow any 302's although there should not be any
method.setFollowRedirects(true);
// execute the method
try {
int code = client.executeMethod(method);
log.debug("HTTP response code: {}", code);
String resp = method.getResponseBodyAsString();
log.trace("Response: {}", resp);
//TODO pipe input stream into mina
//input = method.getResponseBodyAsStream();
} catch (HttpException he) {
log.error("Http error connecting to {}", host, he);
} catch (IOException ioe) {
log.error("Unable to connect to {}", host, ioe);
} finally {
//client mode is automatically validated
validated = true;
if (method != null) {
method.releaseConnection();
}
}
break;
case 0: // server mode
try {
acceptor = new NioSocketAcceptor();
acceptor.setReuseAddress(true);
acceptor.setHandler(this);
if (log.isDebugEnabled()) {
acceptor.getFilterChain().addLast("logger", new LoggingFilter());
}
acceptor.getFilterChain().addLast("codec", codecFilter);
acceptor.getSessionConfig().setReadBufferSize(1024);
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
if ("".equals(host)) {
acceptor.setDefaultLocalAddress(new InetSocketAddress(port));
acceptor.bind();
} else {
Set<SocketAddress> addresses = new HashSet<SocketAddress>();
addresses.add(new InetSocketAddress(host, port));
acceptor.bind(addresses);
}
log.info("icy listening on port {}", port);
connected = true;
} catch (IOException ioe) {
log.debug("Unable to setup connector on host: {} port: {}", host, port);
log.error("Unable to setup connector", ioe);
}
break;
default:
log.debug("Unhandled mode: {}", mode);
}
outBuffer = IoBuffer.allocate(16);
outBuffer.setAutoExpand(true);
}
public void reset() {
log.debug("Resetting icy socket");
connected = false;
validated = false;
//lastDataTs = 0L;
}
public void stop() {
log.debug("Stopping icy socket");
reset();
acceptor.unbind();
}
@Override
public void sessionOpened(IoSession session) throws Exception {
super.sessionOpened(session);
//add the password so it can be retrieved in the decoder
log.debug("Adding password to session");
session.setAttribute("password", password);
}
@Override
public void sessionClosed(IoSession session) throws Exception {
//reset local props
reset();
super.sessionClosed(session);
}
@SuppressWarnings("unchecked")
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
log.info("Incomming: {}", session.getRemoteAddress().toString());
log.trace("Message: {}", message.getClass().getName());
/*
if (lastDataTs > 0) {
long delta = System.currentTimeMillis() - lastDataTs;
if (delta > dataTimeout) {
log.debug("Data too late exit time: {} > timeout: {}", delta, dataTimeout);
//disconnect if late?
stop();
}
}
lastDataTs = System.currentTimeMillis();
log.debug("Data ts: {}", lastDataTs);
*/
//check state
ReadState state = (ReadState) session.getAttribute("state");
log.debug("Current state: {}", state);
//stream config
NSVStreamConfig config = null;
switch (state) {
case Header:
if (validated) {
break;
}
validated = true;
case Failed:
outBuffer.put((byte[]) message);
//flip it!
outBuffer.flip();
//respond to the client
session.write(outBuffer);
break;
case Ready:
//handle meta
log.debug("Pulling metadata");
Map<String, Object> metaData = (Map<String, Object>) session.getAttribute("meta");
if (metaData != null) {
handler.onMetaData(metaData);
} else {
log.debug("Metadata was null for the session");
}
//reset mode based on type
log.debug("Checking type, resetting mode. Current mode: {}", mode);
String[] type = ((String) metaData.get("type")).split("/");
if (mode == 3 || mode == 2) {
if (type[0].equals("video")) {
mode = (mode == 3) ? 0 : 1;
} else {
audioType = type[1];
}
} else {
if (type[0].equals("audio")) {
if (mode == 0) {
mode = 3;
} else {
mode = 2;
}
audioType = type[1];
}
}
//notify handler of mode change
handler.reset(type[0], type[1]);
//audio only
if (mode == 2 || mode == 3) {
//handler.onAudioData(bits);
}
//lookup stream config
config = (NSVStreamConfig) session.getAttribute("nsvconfig");
if (config != null) {
log.debug("NSV stream config found in session. Previous audio type: {}", audioType);
log.debug("NSV types - audio: {} video: {}", config.audioFormat, config.videoFormat);
handler.onConnected(config.videoFormat, config.audioFormat);
//use standard codec meta tags.
if (metaData == null) {
metaData = new HashMap<String, Object>();
}
//upside down format. Send negative values?
if (notifyFlipped) {
metaData.put("width", config.videoWidth);
metaData.put("height", config.videoHeight);
metaData.put("flipped", "true");
} else {
metaData.put("width", config.videoWidth * -1);
metaData.put("height", config.videoHeight * -1);
}
metaData.put("frameRate", config.frameRate);
metaData.put("videoCodec", config.videoFormat);
metaData.put("audioCodec", config.audioFormat);
//send updated meta data
handler.onMetaData(metaData);
//get any aux data
Map<String, IoBuffer> aux = (Map<String, IoBuffer>) session.removeAttribute("aux");
if (aux != null) {
for (Map.Entry<String, IoBuffer> entry : aux.entrySet()) {
handler.onAuxData(entry.getKey(), entry.getValue());
}
}
}
//set to packet state
session.setAttribute("state", ReadState.Packet);
//allow fall through to packet
case Packet:
//lookup stream config
config = (NSVStreamConfig) session.getAttribute("nsvconfig");
//check for a frame
if (message instanceof NSVFrame) {
//got a frame, writing to config
handler.onFrameData((NSVFrame) message);
} else if (message instanceof AACFrame) {
//got a frame
handler.onAudioData(((AACFrame) message).getPayload());
} else if (message instanceof MP3Frame) {
//got a frame
handler.onAudioData(((MP3Frame) message).getPayload());
}
break;
default:
log.debug("Unhandled state");
}
log.trace("Buffered frames: {}", handler.queueSize());
}
@Override
public void exceptionCaught(IoSession session, Throwable ex) throws Exception {
log.debug("Exception occurred {}", session.getRemoteAddress().toString());
if (log.isDebugEnabled()) {
//we want the stacktrace only if debugging
log.warn("Exception: {}", ex);
}
session.close(true);
//if we "stop" here then the port will need to be re-established
reset();
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getMode() {
return mode;
}
public void setMode(int mode) {
this.mode = mode;
}
public IICYHandler getHandler() {
return handler;
}
public void setHandler(IICYHandler handler) {
this.handler = handler;
}
public void setPassword(String password) {
this.password = password;
}
public int getWaitTime() {
return waitTime;
}
public void setWaitTime(int waitTime) {
this.waitTime = waitTime;
}
public long getDataTimeout() {
return dataTimeout;
}
public void setDataTimeout(long dataTimeout) {
this.dataTimeout = dataTimeout;
}
public boolean isNotifyFlipped() {
return notifyFlipped;
}
public void setNotifyFlipped(boolean notifyFlipped) {
this.notifyFlipped = notifyFlipped;
}
public boolean isConnected() {
return connected;
}
}