package org.red5.server.net.rtmpt;
/*
* RED5 Open Source Flash Server - http://www.osflash.org/red5
*
* Copyright (c) 2006-2009 by respective authors (see below). All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free Software
* Foundation; either version 2.1 of the License, or (at your option) any later
* version.
*
* This library 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with this library; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.mina.common.ByteBuffer;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.net.rtmp.IRTMPConnManager;
import org.red5.server.net.rtmp.RTMPConnection;
import org.red5.server.net.rtmp.codec.RTMP;
import org.red5.server.net.servlet.ServletUtils;
import org.slf4j.Logger;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
/**
* Servlet that handles all RTMPT requests.
*
* @author The Red5 Project (red5@osflash.org)
* @author Joachim Bauch (jojo@struktur.de)
*/
public class RTMPTServlet extends HttpServlet {
/**
* Serialization UID
*/
private static final long serialVersionUID = 5925399677454936613L;
/**
* Logger
*/
protected static Logger log = Red5LoggerFactory.getLogger(RTMPTServlet.class);
/**
* HTTP request method to use for RTMPT calls.
*/
private static final String REQUEST_METHOD = "POST";
/**
* Content-Type to use for RTMPT requests / responses.
*/
private static final String CONTENT_TYPE = "application/x-fcs";
/**
* Try to generate responses that contain at least 32768 bytes data.
* Increasing this value results in better stream performance, but also
* increases the latency.
*/
private static final int RESPONSE_TARGET_SIZE = 32768;
/**
* Web app context
*/
protected WebApplicationContext appCtx;
/**
* Reference to RTMPT handler;
*/
private static RTMPTHandler handler;
private static IRTMPConnManager rtmpConnManager;
public void setRtmpConnManager(IRTMPConnManager rtmpConnManager) {
RTMPTServlet.rtmpConnManager = rtmpConnManager;
}
/**
* Set the RTMPTHandler to use in this servlet.
*
* @param handler handler
*/
public void setHandler(RTMPTHandler handler) {
RTMPTServlet.handler = handler;
}
/**
* Return an error message to the client.
*
* @param message
* Message
* @param resp
* Servlet response
* @throws IOException
* I/O exception
*/
protected void handleBadRequest(String message, HttpServletResponse resp)
throws IOException {
resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
resp.setContentType("text/plain");
resp.setContentLength(message.length());
resp.getWriter().write(message);
resp.flushBuffer();
}
/**
* Return a single byte to the client.
*
* @param message
* Message
* @param resp
* Servlet response
* @throws IOException
* I/O exception
*/
protected void returnMessage(byte message, HttpServletResponse resp)
throws IOException {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader("Connection", "Keep-Alive");
resp.setHeader("Cache-Control", "no-cache");
resp.setContentType(CONTENT_TYPE);
resp.setContentLength(1);
resp.getWriter().write(message);
resp.flushBuffer();
}
/**
* Return a message to the client.
*
* @param message
* Message
* @param resp
* Servlet response
* @throws IOException
* I/O exception
*/
protected void returnMessage(String message, HttpServletResponse resp)
throws IOException {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader("Connection", "Keep-Alive");
resp.setHeader("Cache-Control", "no-cache");
resp.setContentType(CONTENT_TYPE);
resp.setContentLength(message.length());
resp.getWriter().write(message);
resp.flushBuffer();
}
/**
* Return raw data to the client.
*
* @param client
* RTMP connection
* @param buffer
* Raw data as byte buffer
* @param resp
* Servlet response
* @throws IOException
* I/O exception
*/
protected void returnMessage(RTMPTConnection client, ByteBuffer buffer,
HttpServletResponse resp) throws IOException {
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader("Connection", "Keep-Alive");
resp.setHeader("Cache-Control", "no-cache");
resp.setContentType(CONTENT_TYPE);
log.debug("Sending {} bytes", buffer.limit());
resp.setContentLength(buffer.limit() + 1);
ServletOutputStream output = resp.getOutputStream();
output.write(client.getPollingDelay());
ServletUtils.copy(buffer.asInputStream(), output);
buffer.release();
buffer = null;
}
/**
* Return the client id from a url like /send/123456/12 -> 123456
*
* @param req
* Servlet request
* @return Client id
*/
protected Integer getClientId(HttpServletRequest req) {
String path = req.getPathInfo();
if (path.equals("")) {
return null;
}
while (path.length() > 1 && path.charAt(0) == '/') {
path = path.substring(1);
}
int endPos = path.indexOf('/');
if (endPos != -1) {
path = path.substring(0, endPos);
}
try {
return Integer.parseInt(path);
} catch (NumberFormatException e) {
return null;
}
}
/**
* Get the RTMPT client for a session.
*
* @param req
* Servlet request
* @return RTMP client connection
*/
protected RTMPTConnection getClientConnection(HttpServletRequest req) {
final Integer id = getClientId(req);
return getConnection(id);
}
/**
* Skip data sent by the client.
*
* @param req
* Servlet request
* @throws IOException
* I/O exception
*/
protected void skipData(HttpServletRequest req) throws IOException {
ByteBuffer data = ByteBuffer.allocate(req.getContentLength());
ServletUtils.copy(req.getInputStream(), data.asOutputStream());
data.flip();
data.release();
data = null;
}
/**
* Send pending messages to client.
*
* @param client
* RTMP connection
* @param resp
* Servlet response
* @throws IOException
* I/O exception
*/
protected void returnPendingMessages(RTMPTConnection client,
HttpServletResponse resp) throws IOException {
ByteBuffer data = client.getPendingMessages(RESPONSE_TARGET_SIZE);
if (data == null) {
// no more messages to send...
if (client.isClosing()) {
// Tell client to close connection
returnMessage((byte) 0, resp);
} else {
returnMessage(client.getPollingDelay(), resp);
}
return;
}
returnMessage(client, data, resp);
}
/**
* Start a new RTMPT session.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleOpen(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Skip sent data
skipData(req);
// TODO: should we evaluate the pathinfo?
RTMPTConnection connection = createConnection();
connection.setServlet(this);
if (connection.getId() == 0) {
// no more clients are available for serving
returnMessage((byte) 0, resp);
return;
}
// Return connection id to client
returnMessage(connection.getId() + "\n", resp);
}
/**
* Close a RTMPT session.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleClose(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Skip sent data
skipData(req);
RTMPTConnection connection = getClientConnection(req);
if (connection == null) {
handleBadRequest("Close: unknown client with id: "
+ getClientId(req), resp);
return;
}
removeConnection(connection.getId());
connection.setServletRequest(req);
handler.connectionClosed(connection, connection.getState());
returnMessage((byte) 0, resp);
connection.realClose();
}
/**
* Add data for an established session.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleSend(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
RTMPTConnection connection = getClientConnection(req);
if (connection == null) {
handleBadRequest("Send: unknown client with id: "
+ getClientId(req), resp);
return;
} else if (connection.getStateCode() == RTMP.STATE_DISCONNECTED) {
removeConnection(connection.getId());
handleBadRequest("Connection already closed.", resp);
return;
}
connection.setServletRequest(req);
// Put the received data in a ByteBuffer
int length = req.getContentLength();
ByteBuffer data = ByteBuffer.allocate(length);
ServletUtils.copy(req.getInputStream(), data.asOutputStream());
data.flip();
// Decode the objects in the data
List<?> messages = connection.decode(data);
data.release();
data = null;
if (messages == null || messages.isEmpty()) {
returnMessage(connection.getPollingDelay(), resp);
return;
}
// Execute the received RTMP messages
for (Object message : messages) {
try {
handler.messageReceived(connection, connection.getState(), message);
} catch (Exception e) {
log.error("Could not process message.", e);
}
}
// Send results to client
returnPendingMessages(connection, resp);
}
/**
* Poll RTMPT session for updates.
*
* @param req
* Servlet request
* @param resp
* Servlet response
* @throws ServletException
* Servlet exception
* @throws IOException
* I/O exception
*/
protected void handleIdle(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// Skip sent data
skipData(req);
RTMPTConnection connection = getClientConnection(req);
if (connection == null) {
handleBadRequest("Idle: unknown client with id: "
+ getClientId(req), resp);
return;
} else if (connection.isClosing()) {
// Tell client to close the connection
returnMessage((byte) 0, resp);
connection.realClose();
return;
} else if (connection.getStateCode() == RTMP.STATE_DISCONNECTED) {
removeConnection(connection.getId());
handleBadRequest("Connection already closed.", resp);
return;
}
connection.setServletRequest(req);
returnPendingMessages(connection, resp);
}
/**
* Main entry point for the servlet.
*
* @param req
* Request object
* @param resp
* Response object
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//log.debug("Request - method: {} content type: {} path: {}", new Object[]{req.getMethod(), req.getContentType(), req.getServletPath()});
if (!REQUEST_METHOD.equals(req.getMethod())
|| req.getContentLength() == 0
|| !CONTENT_TYPE.equals(req.getContentType())) {
// Bad request - return simple error page
handleBadRequest("Bad request, only RTMPT supported.", resp);
return;
}
//get the path
String path = req.getServletPath();
// XXX Paul: since the only current difference in the type of request
// that we are interested in is the 'second' character, we can double
// the speed of this entry point by using a switch on the second
// character.
char p = path.charAt(1);
switch (p) {
case 'o': // OPEN_REQUEST
handleOpen(req, resp);
break;
case 'c': // CLOSE_REQUEST
handleClose(req, resp);
break;
case 's': // SEND_REQUEST
handleSend(req, resp);
break;
case 'i': // IDLE_REQUEST
handleIdle(req, resp);
break;
case 'f': // HTTPIdent request (ident and ident2)
//if HTTPIdent is requested send back some Red5 info
//http://livedocs.adobe.com/flashmediaserver/3.0/docs/help.html?content=08_xmlref_011.html
String ident = "<fcs><Company>Red5</Company><Team>Red5 Server</Team></fcs>";
resp.setStatus(HttpServletResponse.SC_OK);
resp.setHeader("Connection", "Keep-Alive");
resp.setHeader("Cache-Control", "no-cache");
resp.setContentType(CONTENT_TYPE);
resp.setContentLength(ident.length());
resp.getWriter().write(ident);
resp.flushBuffer();
break;
default:
handleBadRequest(
"RTMPT command " + path + " is not supported.", resp);
}
}
/** {@inheritDoc} */
@Override
public void init() throws ServletException {
super.init();
ServletContext ctx = getServletContext();
appCtx = WebApplicationContextUtils
.getWebApplicationContext(ctx);
if (appCtx == null) {
appCtx = (WebApplicationContext) ctx.getAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
}
/** {@inheritDoc} */
@Override
public void destroy() {
// Cleanup connections
Collection<RTMPConnection> conns = rtmpConnManager.removeConnections();
for (RTMPConnection conn : conns) {
conn.close();
}
super.destroy();
}
/**
* A connection has been closed that was created by this servlet.
*
* @param conn
*/
protected void notifyClosed(RTMPTConnection conn) {
rtmpConnManager.removeConnection(conn.getId());
}
protected RTMPTConnection getConnection(int clientId) {
RTMPTConnection connection = (RTMPTConnection) rtmpConnManager
.getConnection(clientId);
if (connection == null) {
log.warn("Null connection for clientId: {}", clientId);
}
return connection;
}
protected RTMPTConnection createConnection() {
RTMPTConnection conn = (RTMPTConnection) rtmpConnManager
.createConnection(RTMPTConnection.class);
conn.setHandler(handler);
conn.setDecoder(handler.getCodecFactory().getSimpleDecoder());
conn.setEncoder(handler.getCodecFactory().getSimpleEncoder());
handler.connectionOpened(conn, conn.getState());
return conn;
}
protected void removeConnection(int clientId) {
rtmpConnManager.removeConnection(clientId);
}
}