package org.red5.server.net.rtmp;
/*
* 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 static org.red5.server.api.ScopeUtils.getScopeService;
import java.util.HashMap;
import java.util.Map;
import org.red5.logging.Red5LoggerFactory;
import org.red5.server.api.IContext;
import org.red5.server.api.IGlobalScope;
import org.red5.server.api.IScope;
import org.red5.server.api.IScopeHandler;
import org.red5.server.api.IServer;
import org.red5.server.api.Red5;
import org.red5.server.api.ScopeUtils;
import org.red5.server.api.IConnection.Encoding;
import org.red5.server.api.service.IPendingServiceCall;
import org.red5.server.api.service.IServiceCall;
import org.red5.server.api.so.ISharedObject;
import org.red5.server.api.so.ISharedObjectSecurity;
import org.red5.server.api.so.ISharedObjectSecurityService;
import org.red5.server.api.so.ISharedObjectService;
import org.red5.server.api.stream.IClientBroadcastStream;
import org.red5.server.api.stream.IClientStream;
import org.red5.server.api.stream.IStreamService;
import org.red5.server.exception.ClientRejectedException;
import org.red5.server.exception.ScopeNotFoundException;
import org.red5.server.exception.ScopeShuttingDownException;
import org.red5.server.messaging.IConsumer;
import org.red5.server.messaging.OOBControlMessage;
import org.red5.server.net.rtmp.codec.RTMP;
import org.red5.server.net.rtmp.event.ChunkSize;
import org.red5.server.net.rtmp.event.Invoke;
import org.red5.server.net.rtmp.event.Notify;
import org.red5.server.net.rtmp.event.Ping;
import org.red5.server.net.rtmp.message.Header;
import org.red5.server.net.rtmp.status.Status;
import org.red5.server.net.rtmp.status.StatusObject;
import org.red5.server.net.rtmp.status.StatusObjectService;
import org.red5.server.service.Call;
import org.red5.server.so.ISharedObjectEvent;
import org.red5.server.so.SharedObjectEvent;
import org.red5.server.so.SharedObjectMessage;
import org.red5.server.so.SharedObjectService;
import org.red5.server.stream.IBroadcastScope;
import org.red5.server.stream.StreamService;
import org.slf4j.Logger;
/**
* RTMP events handler.
*/
public class RTMPHandler extends BaseRTMPHandler {
/**
* Logger
*/
protected static Logger log = Red5LoggerFactory.getLogger(RTMPHandler.class);
/**
* Status object service.
*/
protected StatusObjectService statusObjectService;
/**
* Red5 server instance.
*/
protected IServer server;
/**
* Setter for server object.
*
* @param server Red5 server instance
*/
public void setServer(IServer server) {
this.server = server;
}
/**
* Setter for status object service.
*
* @param statusObjectService Status object service.
*/
public void setStatusObjectService(StatusObjectService statusObjectService) {
this.statusObjectService = statusObjectService;
}
/** {@inheritDoc} */
@Override
protected void onChunkSize(RTMPConnection conn, Channel channel,
Header source, ChunkSize chunkSize) {
for (IClientStream stream : conn.getStreams()) {
if (stream instanceof IClientBroadcastStream) {
IClientBroadcastStream bs = (IClientBroadcastStream) stream;
IBroadcastScope scope = (IBroadcastScope) bs.getScope()
.getBasicScope(IBroadcastScope.TYPE,
bs.getPublishedName());
if (scope == null) {
continue;
}
OOBControlMessage setChunkSize = new OOBControlMessage();
setChunkSize.setTarget("ClientBroadcastStream");
setChunkSize.setServiceName("chunkSize");
if (setChunkSize.getServiceParamMap() == null) {
setChunkSize.setServiceParamMap(new HashMap<String, Object>());
}
setChunkSize.getServiceParamMap().put("chunkSize", chunkSize.getSize());
scope.sendOOBControlMessage((IConsumer) null, setChunkSize);
log.debug("Sending chunksize {} to {}", chunkSize, bs.getProvider());
}
}
}
/**
* Remoting call invocation handler.
*
* @param conn RTMP connection
* @param call Service call
*/
protected void invokeCall(RTMPConnection conn, IServiceCall call) {
final IScope scope = conn.getScope();
if (scope.hasHandler()) {
final IScopeHandler handler = scope.getHandler();
log.debug("Scope: {}", scope);
log.debug("Handler: {}", handler);
if (!handler.serviceCall(conn, call)) {
// XXX: What do do here? Return an error?
return;
}
}
final IContext context = scope.getContext();
log.debug("Context: {}", context);
context.getServiceInvoker().invoke(call, scope);
}
/**
* Remoting call invocation handler.
*
* @param conn
* RTMP connection
* @param call
* Service call
* @param service
* Server-side service object
* @return <code>true</code> if the call was performed, otherwise
* <code>false</code>
*/
private boolean invokeCall(RTMPConnection conn, IServiceCall call,
Object service) {
final IScope scope = conn.getScope();
final IContext context = scope.getContext();
log.debug("Scope: {}", scope);
log.debug("Service: {}", service);
log.debug("Context: {}", context);
return context.getServiceInvoker().invoke(call, service);
}
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override
protected void onInvoke(RTMPConnection conn, Channel channel,
Header source, Notify invoke, RTMP rtmp) {
log.debug("Invoke: {}", invoke);
// Get call
final IServiceCall call = invoke.getCall();
// method name
final String action = call.getServiceMethodName();
// If it's a callback for server remote call then pass it over to
// callbacks handler and return
if ("_result".equals(action) || "_error".equals(action)) {
handlePendingCallResult(conn, invoke);
return;
}
boolean disconnectOnReturn = false;
// If this is not a service call then handle connection...
if (call.getServiceName() == null) {
log.debug("call: {}", call);
if (!conn.isConnected()) {
// Handle connection
if (action.equals(ACTION_CONNECT)) {
log.debug("connect");
// Get parameters passed from client to
// NetConnection#connection
final Map<String, Object> params = invoke.getConnectionParams();
// Get hostname
String host = getHostname((String) params.get("tcUrl"));
// Check up port
if (host.endsWith(":1935")) {
// Remove default port from connection string
host = host.substring(0, host.length() - 5);
}
// App name as path, but without query string if there is
// one
String path = (String) params.get("app");
if (path.indexOf("?") != -1) {
int idx = path.indexOf("?");
params.put("queryString", path.substring(idx));
path = path.substring(0, idx);
}
params.put("path", path);
final String sessionId = null;
conn.setup(host, path, sessionId, params);
try {
// Lookup server scope when connected
// Use host and application name
IGlobalScope global = server.lookupGlobal(host, path);
if (global == null) {
call.setStatus(Call.STATUS_SERVICE_NOT_FOUND);
if (call instanceof IPendingServiceCall) {
StatusObject status = getStatus(NC_CONNECT_INVALID_APPLICATION);
status.setDescription("No scope \"" + path
+ "\" on this server.");
((IPendingServiceCall) call).setResult(status);
}
log.info("No application scope found for {} on host {}. Misspelled or missing application folder?", path, host);
disconnectOnReturn = true;
} else {
final IContext context = global.getContext();
IScope scope = null;
try {
scope = context.resolveScope(global, path);
} catch (ScopeNotFoundException err) {
call.setStatus(Call.STATUS_SERVICE_NOT_FOUND);
if (call instanceof IPendingServiceCall) {
StatusObject status = getStatus(NC_CONNECT_REJECTED);
status.setDescription("No scope \"" + path
+ "\" on this server.");
((IPendingServiceCall) call)
.setResult(status);
}
log.info("Scope {} not found on {}", path, host);
disconnectOnReturn = true;
} catch (ScopeShuttingDownException err) {
call.setStatus(Call.STATUS_APP_SHUTTING_DOWN);
if (call instanceof IPendingServiceCall) {
StatusObject status = getStatus(NC_CONNECT_APPSHUTDOWN);
status.setDescription("Application at \""
+ path
+ "\" is currently shutting down.");
((IPendingServiceCall) call)
.setResult(status);
}
log.info("Application at {} currently shutting down on {}", path, host);
disconnectOnReturn = true;
}
if (scope != null) {
log.info("Connecting to: {}", scope);
// Setup application's classloader to be used for deserializing
//System.out.println(">>>>> rtmp handler: " + Thread.currentThread().getContextClassLoader());
//System.out.println(">>>>> rtmp handler (class): " + getClass().getClassLoader());
ClassLoader loader = scope.getClassLoader();
if (loader == null) {
// Fallback, should never happen
loader = getClass().getClassLoader();
}
Thread.currentThread().setContextClassLoader(loader);
boolean okayToConnect;
try {
log.debug("Conn {}, scope {}, call {}", new Object[]{conn, scope, call});
log.debug("Call args {}", call.getArguments());
if (call.getArguments() != null) {
okayToConnect = conn.connect(scope, call.getArguments());
} else {
okayToConnect = conn.connect(scope);
}
if (okayToConnect) {
log.debug("Connected - Client: {}", conn.getClient());
call.setStatus(Call.STATUS_SUCCESS_RESULT);
if (call instanceof IPendingServiceCall) {
IPendingServiceCall pc = (IPendingServiceCall) call;
//send fmsver and capabilities
StatusObject result = getStatus(NC_CONNECT_SUCCESS);
result.setAdditional("fmsVer", Red5.getFMSVersion());
result.setAdditional("capabilities", Integer.valueOf(31));
pc.setResult(result);
}
// Measure initial roundtrip time after connecting
conn.ping(new Ping(Ping.STREAM_CLEAR, 0, -1));
conn.startRoundTripMeasurement();
} else {
log.debug("Connect failed");
call.setStatus(Call.STATUS_ACCESS_DENIED);
if (call instanceof IPendingServiceCall) {
IPendingServiceCall pc = (IPendingServiceCall) call;
pc.setResult(getStatus(NC_CONNECT_REJECTED));
}
disconnectOnReturn = true;
}
} catch (ClientRejectedException rejected) {
log.debug("Connect rejected");
call.setStatus(Call.STATUS_ACCESS_DENIED);
if (call instanceof IPendingServiceCall) {
IPendingServiceCall pc = (IPendingServiceCall) call;
StatusObject status = getStatus(NC_CONNECT_REJECTED);
if (rejected.getReason() != null) {
status.setApplication(rejected.getReason());
}
pc.setResult(status);
}
disconnectOnReturn = true;
}
}
}
} catch (RuntimeException e) {
call.setStatus(Call.STATUS_GENERAL_EXCEPTION);
if (call instanceof IPendingServiceCall) {
IPendingServiceCall pc = (IPendingServiceCall) call;
pc.setResult(getStatus(NC_CONNECT_FAILED));
}
log.error("Error connecting {}", e);
disconnectOnReturn = true;
}
// Evaluate request for AMF3 encoding
if (Integer.valueOf(3).equals(params.get("objectEncoding"))
&& call instanceof IPendingServiceCall) {
Object pcResult = ((IPendingServiceCall) call)
.getResult();
Map<String, Object> result;
if (pcResult instanceof Map) {
result = (Map<String, Object>) pcResult;
result.put("objectEncoding", 3);
} else if (pcResult instanceof StatusObject) {
result = new HashMap<String, Object>();
StatusObject status = (StatusObject) pcResult;
result.put("code", status.getCode());
result.put("description", status.getDescription());
result.put("application", status.getApplication());
result.put("level", status.getLevel());
result.put("objectEncoding", 3);
((IPendingServiceCall) call).setResult(result);
}
rtmp.setEncoding(Encoding.AMF3);
}
}
} else if (action.equals(ACTION_DISCONNECT)) {
conn.close();
} else if (action.equals(ACTION_CREATE_STREAM)
|| action.equals(ACTION_DELETE_STREAM)
|| action.equals(ACTION_RELEASE_STREAM)
|| action.equals(ACTION_PUBLISH)
|| action.equals(ACTION_PLAY) || action.equals(ACTION_SEEK)
|| action.equals(ACTION_PAUSE)
|| action.equals(ACTION_PAUSE_RAW)
|| action.equals(ACTION_CLOSE_STREAM)
|| action.equals(ACTION_RECEIVE_VIDEO)
|| action.equals(ACTION_RECEIVE_AUDIO)) {
IStreamService streamService = (IStreamService) getScopeService(
conn.getScope(), IStreamService.class,
StreamService.class);
Status status = null;
try {
if (!invokeCall(conn, call, streamService)) {
status = getStatus(NS_INVALID_ARGUMENT).asStatus();
status.setDescription(String.format("Failed to %s (stream id: %d)", action, source.getStreamId()));
}
} catch (Throwable err) {
log.error("Error while invoking {} on stream service. {}", action, err);
status = getStatus(NS_FAILED).asStatus();
status.setDescription(String.format("Error while invoking %s (stream id: %d)", action, source.getStreamId()));
status.setDetails(err.getMessage());
}
if (status != null) {
channel.sendStatus(status);
}
} else {
invokeCall(conn, call);
}
} else if (conn.isConnected()) {
// Service calls, must be connected.
invokeCall(conn, call);
} else {
// Warn user attempts to call service without being connected
log.warn("Not connected, closing connection");
conn.close();
}
if (invoke instanceof Invoke) {
if ((source.getStreamId() != 0)
&& (call.getStatus() == Call.STATUS_SUCCESS_VOID || call
.getStatus() == Call.STATUS_SUCCESS_NULL)) {
// This fixes a bug in the FP on Intel Macs.
log.debug("Method does not have return value, do not reply");
return;
}
boolean sendResult = true;
if (call instanceof IPendingServiceCall) {
IPendingServiceCall psc = (IPendingServiceCall) call;
Object result = psc.getResult();
if (result instanceof DeferredResult) {
// Remember the deferred result to be sent later
DeferredResult dr = (DeferredResult) result;
dr.setServiceCall(psc);
dr.setChannel(channel);
dr.setInvokeId(invoke.getInvokeId());
conn.registerDeferredResult(dr);
sendResult = false;
}
}
;
if (sendResult) {
// The client expects a result for the method call.
Invoke reply = new Invoke();
reply.setCall(call);
reply.setInvokeId(invoke.getInvokeId());
channel.write(reply);
if (disconnectOnReturn) {
conn.close();
}
}
}
}
public StatusObject getStatus(String code) {
return statusObjectService.getStatusObject(code);
}
/** {@inheritDoc} */
@Override
protected void onPing(RTMPConnection conn, Channel channel, Header source,
Ping ping) {
switch (ping.getValue1()) {
case Ping.CLIENT_BUFFER:
if (ping.getValue2() != 0) {
// The client wants to set the buffer time
IClientStream stream = conn.getStreamById(ping.getValue2());
int buffer = ping.getValue3();
if (stream != null) {
stream.setClientBufferDuration(buffer);
log.info("Setting client buffer on stream: {}", buffer);
} else {
// Remember buffer time to set until stream will be
// created
conn.rememberStreamBufferDuration(ping.getValue2(),
buffer);
log.info("Remembering client buffer on stream: {}", buffer);
}
} else {
// XXX: should we store the buffer time for future streams?
log.warn("Unhandled ping: {}", ping);
}
break;
case Ping.PONG_SERVER:
// This is the response to an IConnection.ping request
conn.pingReceived(ping);
break;
default:
log.warn("Unhandled ping: {}", ping);
}
}
/**
* Create and send SO message stating that a SO could not be created.
*
* @param conn
* @param name
* @param persistent
*/
private void sendSOCreationFailed(RTMPConnection conn, String name,
boolean persistent) {
SharedObjectMessage msg = new SharedObjectMessage(name, 0, persistent);
msg.addEvent(new SharedObjectEvent(
ISharedObjectEvent.Type.CLIENT_STATUS, "error",
SO_CREATION_FAILED));
conn.getChannel((byte) 3).write(msg);
}
/** {@inheritDoc} */
@Override
protected void onSharedObject(RTMPConnection conn, Channel channel,
Header source, SharedObjectMessage object) {
final ISharedObject so;
final String name = object.getName();
final boolean persistent = object.isPersistent();
final IScope scope = conn.getScope();
if (scope == null) {
// The scope already has been deleted.
sendSOCreationFailed(conn, name, persistent);
return;
}
ISharedObjectService sharedObjectService = (ISharedObjectService) getScopeService(
scope, ISharedObjectService.class, SharedObjectService.class,
false);
if (!sharedObjectService.hasSharedObject(scope, name)) {
ISharedObjectSecurityService security = (ISharedObjectSecurityService) ScopeUtils
.getScopeService(scope, ISharedObjectSecurityService.class);
if (security != null) {
// Check handlers to see if creation is allowed
for (ISharedObjectSecurity handler : security
.getSharedObjectSecurity()) {
if (!handler.isCreationAllowed(scope, name, persistent)) {
sendSOCreationFailed(conn, name, persistent);
return;
}
}
}
if (!sharedObjectService
.createSharedObject(scope, name, persistent)) {
sendSOCreationFailed(conn, name, persistent);
return;
}
}
so = sharedObjectService.getSharedObject(scope, name);
if (so.isPersistentObject() != persistent) {
SharedObjectMessage msg = new SharedObjectMessage(name, 0,
persistent);
msg.addEvent(new SharedObjectEvent(
ISharedObjectEvent.Type.CLIENT_STATUS, "error",
SO_PERSISTENCE_MISMATCH));
conn.getChannel((byte) 3).write(msg);
}
so.dispatchEvent(object);
}
}