/**
*
*/
package video.serverProxy;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import video.lib.Configuration;
import video.lib.Constants;
import video.lib.NumberConvertor;
import video.lib.RtspMessage;
import video.lib.RtspMethod;
import video.lib.RtspRequest;
import video.lib.RtspResponse;
import video.lib.RtspStatusCode;
import video.lib.RtspTransportHeader;
import video.lib.RtspTransportHeader.RtspTransport;
import video.transport.TransportService;
/**
* @author yuezhu
*
*/
public class RtspServerHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RtspServerHandler.class);
protected static final AttributeKey TOKEN = new AttributeKey(RtspServerHandler.class, "token");
private static TransportService service;
private final IoSession session;
private Long clientSessionId = -1L;
private String rtspSessionId = null;
// Map MINA session ID to handler
private static Map<Long, RtspServerHandler> clientSessionIdMap = new ConcurrentHashMap<Long, RtspServerHandler>();
// Map RTSP session ID to handler
private static Map<String, RtspServerHandler> rtspSessionIdMap = new ConcurrentHashMap<String, RtspServerHandler>();
// Map cseq to RTSP method
private final Map<String, RtspMethod> cseqMap = new ConcurrentHashMap<String, RtspMethod>();
// Map URI to track
private final Map<URI, ServerTrack> trackList = new ConcurrentHashMap<URI, ServerTrack>();
/**
* Get handler by client session ID
* @param id
* @return
*/
public static RtspServerHandler getByClientSessionId(long id) {
return clientSessionIdMap.get(Long.valueOf(id));
}
/**
* Get handler by RTSP session ID
* @param id
* @return
*/
public static RtspServerHandler getByRtspSessionId(String id) {
return rtspSessionIdMap.get(id);
}
public static void putRtspSessionId(String id, RtspServerHandler handler) {
rtspSessionIdMap.put(id, handler);
}
/**
* Insert client session ID into the front of the message raw byte
* @param in
* @return
*/
private byte[] insertClientSessionId(byte[] in) {
byte[] byteArray = new byte[in.length + (Long.SIZE / Byte.SIZE)];
System.arraycopy(NumberConvertor.toByteArray(clientSessionId), 0, byteArray, 0, Long.SIZE / Byte.SIZE);
System.arraycopy(in, 0, byteArray, Long.SIZE / Byte.SIZE, in.length);
LOGGER.debug("Insert client session ID=" + clientSessionId);
return byteArray;
}
/**
* Get message raw bytes
* @param in
* @return
*/
public static byte[] getMessageBytes(byte[] in) {
int length = in.length - (Long.SIZE / Byte.SIZE);
byte[] byteArray = new byte[length];
System.arraycopy(in, Long.SIZE / Byte.SIZE, byteArray, 0, length);
return byteArray;
}
/**
* Get client session ID
* @param in
* @return
*/
public static long getClientSessionId(byte[] in) {
byte[] longBytes = new byte[Long.SIZE / Byte.SIZE];
System.arraycopy(in, 0, longBytes, 0, Long.SIZE / Byte.SIZE);
// byte[] longBytes = Arrays.copyOf(in, Long.SIZE / Byte.SIZE);
return NumberConvertor.toLongValue(longBytes);
}
public RtspServerHandler(IoSession session, TransportService transportService, Long clientSessionId) {
this.session = session;
service = transportService;
this.clientSessionId = clientSessionId;
// Save session ID for de-multiplexing.
clientSessionIdMap.put(clientSessionId, this);
LOGGER.debug("Client session ID put: " + clientSessionId);
}
/**
* This method will be invoked when a RTSP message received from the client side proxy.
* @param message
*/
public void onMessageReceivedFromRemote(RtspMessage message) {
LOGGER.debug("RtspMessage received from remote:\n"
+ "--------------------------------------------------------------------------------\n"
+ message.toString()
+ "--------------------------------------------------------------------------------");
RtspMethod method = null;
switch (message.getType()) {
case REQUEST:
// It's a request from remote. We save its request method.
RtspRequest request = (RtspRequest) message;
method = request.getMethod();
saveRequestMethod(request);
// Then dispatch the response and pass it to the server.
switch (method) {
case SETUP:
LOGGER.debug("SETUP request.");
boolean isSupported = processSetupRequest(request);
if (!isSupported) {
String cseq = request.getField("CSeq");
RtspResponse r = RtspResponse.newInstance(RtspStatusCode.NotImplemented, cseq);
retrieveRequestMethod(r);
sendRtspMessageToRemote(r);
return;
}
break;
case OPTIONS:
LOGGER.debug("OPTIONS request.");
break;
case DESCRIBE:
LOGGER.debug("DESCRIBE request.");
break;
case PLAY:
LOGGER.debug("PLAY request.");
break;
case PAUSE:
LOGGER.debug("PAUSE request.");
break;
case TEARDOWN:
LOGGER.debug("TEARDOWN request.");
break;
case GET_PARAMETER:
LOGGER.debug("GET_PARAMETER request.");
break;
case SET_PARAMETER:
LOGGER.debug("SET_PARAMETER request.");
break;
default:
LOGGER.warn("Request method " + method + " is not supported.");
break;
}
break;
// End of REQUEST case.
case RESPONSE:
// It's a response from the server. We find the method of its
// corresponding request.
RtspResponse response = (RtspResponse) message;
method = retrieveRequestMethod(response);
// Then dispatch the response and pass it to remote.
switch (method) {
case OPTIONS:
LOGGER.debug("OPTIONS response.");
break;
case GET_PARAMETER:
LOGGER.debug("GET_PARAMETER response.");
break;
case SET_PARAMETER:
LOGGER.debug("SET_PARAMETER response.");
break;
default:
LOGGER.warn("Server shouldn't issue such a request of method " + method);
return;
}
break;
// End of RESPONSE case.
}
sendRtspMessageToServer(message);
}
/**
* This method will be invoked when RTSP message received from the living streaming server.
* @param message
*/
public void onMessageReceivedFromServer(RtspMessage message) {
LOGGER.debug("RtspMessage received from the server:\n"
+ "--------------------------------------------------------------------------------\n"
+ message.toString()
+ "--------------------------------------------------------------------------------");
RtspMethod method = null;
switch (message.getType()) {
case REQUEST:
// It's a request from the server. We save its request method.
RtspRequest request = (RtspRequest) message;
method = request.getMethod();
saveRequestMethod(request);
// Then dispatch the response and pass it to remote.
switch (method) {
case OPTIONS:
LOGGER.debug("OPTIONS request.");
break;
case GET_PARAMETER:
LOGGER.debug("GET_PARAMETER request.");
break;
case SET_PARAMETER:
LOGGER.debug("SET_PARAMETER request.");
break;
case UNKNOWN:
LOGGER.debug("UNKNOWN request.");
break;
default:
LOGGER.warn("Server shouldn't issue such a request of method " + method);
return;
}
break;
// End of REQUEST case.
case RESPONSE:
// It's a response from the server. We find the method of its
// corresponding request.
RtspResponse response = (RtspResponse) message;
method = retrieveRequestMethod(response);
if (method == null) {
LOGGER.error("Cannot retrieve corresponding RTSP method with CSeq " + response.getField("CSeq"));
return;
}
// Then dispatch the response and pass it to remote.
switch (method) {
case SETUP:
LOGGER.debug("SETUP response.");
processSetupResponse(response);
break;
case DESCRIBE:
LOGGER.debug("DESCRIBE response.");
break;
case GET_PARAMETER:
LOGGER.debug("GET_PARAMETER response.");
break;
case SET_PARAMETER:
LOGGER.debug("SET_PARAMETER response.");
break;
case OPTIONS:
LOGGER.debug("OPTIONS response.");
break;
case PAUSE:
LOGGER.debug("PAUSE response.");
break;
case PLAY:
LOGGER.debug("PLAY response.");
break;
case TEARDOWN:
LOGGER.debug("TEARDOWN response.");
processTeardownResponse(response);
break;
case UNKNOWN:
LOGGER.debug("UNKNOWN response.");
break;
default:
LOGGER.warn("Error since its corresponding request method " + method + " is not supported.");
return;
}
if (response.getStatusCode() == RtspStatusCode.NotFound) {
close();
}
break;
// End of RESPONSE case.
}
sendRtspMessageToRemote(message);
LOGGER.debug("RtspMessage sent to the remote:\n"
+ "--------------------------------------------------------------------------------\n"
+ message.toString()
+ "--------------------------------------------------------------------------------");
}
/**
* Process SETUP request received from the client
* @param request
*/
private boolean processSetupRequest(RtspRequest request) {
LOGGER.debug("Got SETUP request.");
RtspTransportHeader transportHeader = new RtspTransportHeader(request.getField("Transport"));
// FIXME: Here this is a hack. We save the request URI with its client ports
// into the session.
// This is problematic in the case that two successive SETUP requests arrive
// at the proxy, and the former will be replaced.
// Select the first option. Here we assume that the server will choose this
// one.
RtspTransport rtspTransport = null;
for (int i = 0; i < transportHeader.size(); i++) {
rtspTransport = transportHeader.get(i);
// Modify all UDP client ports
if (rtspTransport.getLowerTransport() == RtspTransportHeader.LowerTransport.UDP) {
session.setAttribute("clientPort", rtspTransport.getClientPort());
// Set new client ports so that the server will send RTP/RTCP packets to the new ports.
int[] newClientPort = {
Configuration.getInt("server.rtp.port", Constants.SERVER_RTP_PORT),
Configuration.getInt("server.rtcp.port", Constants.SERVER_RTCP_PORT)
};
LOGGER.debug("Before modifying client port: " + transportHeader.toString());
// Modify transport headers.
rtspTransport.setClientPort(newClientPort);
transportHeader.set(i, rtspTransport);
LOGGER.debug("After modifying client port: " + transportHeader.toString());
// Proxy selects this transport method and discard others.
transportHeader.removeAllExcept(i);
break;
} else if (rtspTransport.getLowerTransport() == RtspTransportHeader.LowerTransport.TCP) {
// UDP based transport must be supported.
// TCP based transport is not supported. Just discard it.
transportHeader.remove(i);
LOGGER.warn("Transport # " + i + " is TCP based, which is not supported currently.");
} else {
LOGGER.error("Transport is unspecified.");
}
}
if (transportHeader.size() == 0) {
// TODO:
// No available transport option
// This is actually an error case because RTSP must provide an UDP transport option.
LOGGER.error("No UDP transport option");
return false;
}
// Set modified transport headers into request message.
request.setField("Transport", transportHeader.toString());
// Save its request URI into session in order to assign a track for this URI when getting this SETUP response.
session.setAttribute("requestUri", request.getRequestUri());
return true;
}
private void processSetupResponse(RtspResponse response) {
LOGGER.debug("Got SETUP response.");
RtspTransportHeader transportHeader = new RtspTransportHeader(response.getField("Transport"));
// Get the transport header.
RtspTransport rtspTransport = transportHeader.get(0);
// Create a new Track object.
ServerTrack serverTrack = addServerTrack((URI) session.getAttribute("requestUri"), rtspTransport.getSsrc());
// Save server socket address into track.
// This is a workaround in the case that the server doesn't include a SSRC in the transport response.
// When we get an RTP packet in the RtpServerPacketHandler, we will get the SSRC, and
// we are able to match the SSRC with the server socket address.
InetAddress serverAddress = null;
if (rtspTransport.getSource() != null) {
// The server specified the source.
try {
serverAddress = InetAddress.getByName(rtspTransport.getSource());
} catch (UnknownHostException e) {
LOGGER.warn("Unknown host: " + rtspTransport.getSource());
}
} else {
// The server didn't specify the source. We still can get the server address from the session.
serverAddress = ((InetSocketAddress) session.getRemoteAddress()).getAddress();
}
// /////////////////////
// Set response transport header
// At this time the transport type should be UDP based because we have selected an UDP transport type
// when sending the SETUP request.
// /////////////////////
if (rtspTransport.getLowerTransport() == RtspTransportHeader.LowerTransport.UDP) {
// Get server ports. The media server will send RTP/RTCP packets from
// these ports. Now we are ready to set server socket address.
serverTrack.setServerSocketAddress(serverAddress, rtspTransport.getServerPort()[0], rtspTransport.getServerPort()[1]);
// Retrieve the client ports from the session.
rtspTransport.setClientPort((int[]) session.getAttribute("clientPort"));
} else if (rtspTransport.getLowerTransport() == RtspTransportHeader.LowerTransport.TCP) {
LOGGER.error("Transport is TCP based, which is not supported currently.");
} else {
LOGGER.error("Transport is unspecified.");
}
// Assign a SSRC for this SETUP message. This SSRC is proxy specified and
// will be included in the RTP packets in the future.
rtspTransport.setSsrc(serverTrack.getProxySsrc().toHexString());
transportHeader.set(0, rtspTransport);
//LOGGER.debug("Transport header to be sent: " + transportHeader.toString());
response.setField("Transport", transportHeader.toString());
rtspSessionId = response.getField("Session");
if (rtspSessionId != null && !rtspSessionIdMap.containsKey(rtspSessionId)) {
LOGGER.debug("Got RTSP session ID, put it into rtspSessionIDMap");
rtspSessionIdMap.put(rtspSessionId, this);
}
}
// FIXME: this.close() will block when executing session.close(false). It's
// weird.
private void processTeardownResponse(RtspResponse response) {
if (response.getStatusCode() == RtspStatusCode.OK) {
close();
}
}
/**
* Adds a new Track associated with this ProxySession.
*
* @param url
* The URL used as a control reference for the Track
* @param serverSsrc
* the SSRC id given by the server or null if not provided
* @return a reference to the newly created Track
*/
private synchronized ServerTrack addServerTrack(URI uri, String serverSsrc) {
ServerTrack serverTrack = new ServerTrack(service, uri);
if (serverSsrc != null) {
LOGGER.debug("serverSsrc is available: " + serverSsrc);
serverTrack.setServerSsrc(serverSsrc);
}
trackList.put(uri, serverTrack);
return serverTrack;
}
// ////////////////////////////
private void saveRequestMethod(RtspRequest request) {
cseqMap.put(request.getField("CSeq"), request.getMethod());
}
private synchronized RtspMethod retrieveRequestMethod(RtspResponse response) {
RtspMethod rtn = null;
String cseq = response.getField("CSeq");
if (cseq != null) {
if (cseqMap.containsKey(cseq)) {
rtn = cseqMap.get(cseq);
cseqMap.remove(cseq);
return rtn;
} else {
LOGGER.error("CSeq is illegal.");
return null;
}
} else {
LOGGER.debug("CSeq is not specified.");
return RtspMethod.UNKNOWN;
}
}
// ////////////////////////////
private void sendRtspMessageToServer(RtspMessage message) {
if (message == null) {
LOGGER.error("RtspMessage is null.");
return;
}
//message.setProxy();
switch (message.getType()) {
case REQUEST:
RtspRequest request = (RtspRequest) message;
session.write(request);
break;
case RESPONSE:
RtspResponse response = (RtspResponse) message;
session.write(response);
break;
}
LOGGER.debug("RTSP message sent to the server.");
}
public void sendRtspMessageToRemote(RtspMessage message) {
if (message == null) {
LOGGER.error("RtspMessage is null.");
return;
}
byte[] byteArray;
//message.setProxy();
switch (message.getType()) {
case REQUEST:
RtspRequest request = (RtspRequest) message;
try {
byteArray = insertClientSessionId(request.toString().getBytes());
service.getRTSPTransportChannel().send(byteArray);
} catch (IOException e) {
LOGGER.error("Failed to send RTSP request via transport service: " + e.toString());
}
break;
case RESPONSE:
RtspResponse response = (RtspResponse) message;
try {
byteArray = insertClientSessionId(response.toString().getBytes());
service.getRTSPTransportChannel().send(byteArray);
} catch (IOException e) {
LOGGER.error("Failed to send RTSP response via transport service: " + e.toString());
}
break;
}
}
/**
* Synchronized close.
*/
public synchronized void close() {
if ((session != null) && session.isConnected()) {
session.close(true);
// session.getService().dispose();
// CloseFuture future = session.close(true);
// future.awaitUninterruptibly();
}
// close all associated tracks
for (Map.Entry<URI, ServerTrack> entry : trackList.entrySet()) {
entry.getValue().close();
}
if (clientSessionIdMap.containsKey(clientSessionId)) {
clientSessionIdMap.remove(clientSessionId);
}
if (rtspSessionId != null && rtspSessionIdMap.containsKey(rtspSessionId)) {
rtspSessionIdMap.remove(rtspSessionId);
}
}
}