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 java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.red5.io.object.Deserializer; import org.red5.io.object.Serializer; import org.red5.io.utils.ObjectMap; import org.red5.server.api.IConnection; import org.red5.server.api.event.IEvent; import org.red5.server.api.event.IEventDispatcher; import org.red5.server.api.service.IPendingServiceCall; import org.red5.server.api.service.IPendingServiceCallback; import org.red5.server.api.service.IServiceCall; import org.red5.server.api.service.IServiceCapableConnection; import org.red5.server.api.service.IServiceInvoker; import org.red5.server.api.so.IClientSharedObject; import org.red5.server.messaging.IMessage; import org.red5.server.net.rtmp.codec.RTMP; import org.red5.server.net.rtmp.codec.RTMPCodecFactory; 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.service.Call; import org.red5.server.service.MethodNotFoundException; import org.red5.server.service.PendingCall; import org.red5.server.service.ServiceInvoker; import org.red5.server.so.ClientSharedObject; import org.red5.server.so.SharedObjectMessage; import org.red5.server.stream.AbstractClientStream; import org.red5.server.stream.OutputStream; import org.red5.server.stream.consumer.ConnectionConsumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base class for clients (RTMP and RTMPT) */ public abstract class BaseRTMPClientHandler extends BaseRTMPHandler { private static final Logger log = LoggerFactory .getLogger(BaseRTMPClientHandler.class); /** * Connection parameters */ private Map<String, Object> connectionParams; /** * Connect call arguments */ private Object[] connectArguments = null; /** * Connection callback */ private IPendingServiceCallback connectCallback; /** * Service provider */ private Object serviceProvider; /** * Service invoker */ private IServiceInvoker serviceInvoker = new ServiceInvoker(); /** * Shared objects map */ private ConcurrentMap<String, ClientSharedObject> sharedObjects = new ConcurrentHashMap<String, ClientSharedObject>(); private final RTMPClientConnManager connManager = new RTMPClientConnManager(); private ConcurrentMap<Object, NetStreamPrivateData> streamDataMap = new ConcurrentHashMap<Object, NetStreamPrivateData>(); /** * Task to start on connection close */ private Runnable connectionClosedHandler; /** * Task to start on connection errors */ private ClientExceptionHandler exceptionHandler; private RTMPCodecFactory codecFactory; private IEventDispatcher streamEventDispatcher = null; protected BaseRTMPClientHandler() { codecFactory = new RTMPCodecFactory(); codecFactory.setDeserializer(new Deserializer()); codecFactory.setSerializer(new Serializer()); codecFactory.init(); } public RTMPClientConnManager getConnManager() { return connManager; } public void setConnectionClosedHandler(Runnable connectionClosedHandler) { this.connectionClosedHandler = connectionClosedHandler; } public void setExceptionHandler(ClientExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; } /** * Start network connection to server * * @param server * Server * @param port * Connection port */ protected abstract void startConnector(String server, int port); /** * Connect RTMP client to server's application via given port * * @param server Server * @param port Connection port * @param application Application at that server */ public void connect(String server, int port, String application) { log.debug("connect server: {} port {} application {}", new Object[] { server, port, application }); connect(server, port, application, null); } /** * Connect RTMP client to server's application via given port with given * connection callback * * @param server Server * @param port Connection port * @param application Application at that server * @param connectCallback Connection callback */ public void connect(String server, int port, String application, IPendingServiceCallback connectCallback) { log.debug( "connect server: {} port {} application {} connectCallback {}", new Object[] { server, port, application, connectCallback }); connect(server, port, makeDefaultConnectionParams(server, port, application), connectCallback); } /** * @param server Server * @param port Connection port * @param application Application at that server * @return default connection parameters */ public Map<String, Object> makeDefaultConnectionParams(String server, int port, String application) { Map<String, Object> params = new ObjectMap<String, Object>(); params.put("app", application); params.put("objectEncoding", Integer.valueOf(0)); params.put("fpad", Boolean.FALSE); params.put("flashVer", "WIN 9,0,115,0"); params.put("audioCodecs", Integer.valueOf(1639)); params.put("videoFunction", Integer.valueOf(1)); params.put("pageUrl", null); params.put("path", application); params.put("capabilities", Integer.valueOf(15)); params.put("swfUrl", null); params.put("videoCodecs", Integer.valueOf(252)); return params; } /** * Connect RTMP client to server via given port and with given connection * parameters * * @param server Server * @param port Connection port * @param connectionParams Connection parameters */ public void connect(String server, int port, Map<String, Object> connectionParams) { log.debug("connect server: {} port {} connectionParams {}", new Object[] { server, port, connectionParams }); connect(server, port, connectionParams, null); } /** * Connect RTMP client to server's application via given port * * @param server Server * @param port Connection port * @param connectionParams Connection parameters * @param connectCallback Connection callback */ public void connect(String server, int port, Map<String, Object> connectionParams, IPendingServiceCallback connectCallback) { connect(server, port, connectionParams, connectCallback, null); } /** * Connect RTMP client to server's application via given port * * @param server Server * @param port Connection port * @param connectionParams Connection parameters * @param connectCallback Connection callback * @param connectCallArguments Arguments for 'connect' call */ public void connect(String server, int port, Map<String, Object> connectionParams, IPendingServiceCallback connectCallback, Object[] connectCallArguments) { log .debug( "connect server: {} port {} connectionParams {} connectCallback {} conectCallArguments {}", new Object[] { server, port, connectionParams, connectCallback, Arrays.toString(connectCallArguments) }); this.connectionParams = connectionParams; this.connectArguments = connectCallArguments; if (!connectionParams.containsKey("objectEncoding")) { connectionParams.put("objectEncoding", (int) 0); } this.connectCallback = connectCallback; startConnector(server, port); } /** * Register object that provides methods that can be called by the server. * * @param serviceProvider Service provider */ public void setServiceProvider(Object serviceProvider) { this.serviceProvider = serviceProvider; } /** * Connect to client shared object. * * @param name Client shared object name * @param persistent SO persistence flag * @return Client shared object instance */ public synchronized IClientSharedObject getSharedObject(String name, boolean persistent) { log.debug("getSharedObject name: {} persistent {}", new Object[] { name, persistent }); ClientSharedObject result = sharedObjects.get(name); if (result != null) { if (result.isPersistentObject() != persistent) { throw new RuntimeException( "Already connected to a shared object with this name, but with different persistence."); } return result; } result = new ClientSharedObject(name, persistent); sharedObjects.put(name, result); return result; } /** {@inheritDoc} */ @Override protected void onChunkSize(RTMPConnection conn, Channel channel, Header source, ChunkSize chunkSize) { log.debug("onChunkSize"); // TODO: implement this log.info("ChunkSize is not implemented yet: {}", chunkSize); } /** {@inheritDoc} */ @Override protected void onPing(RTMPConnection conn, Channel channel, Header source, Ping ping) { log.debug("onPing"); switch (ping.getValue1()) { case Ping.PING_CLIENT: case Ping.STREAM_CLEAR: case Ping.STREAM_RESET: case Ping.STREAM_PLAYBUFFER_CLEAR: // The server wants to measure the RTT Ping pong = new Ping(); pong.setValue1((short) Ping.PONG_SERVER); int now = (int) (System.currentTimeMillis() & 0xffffffff); pong.setValue2(now); pong.setValue3(Ping.UNDEFINED); conn.ping(pong); break; default: log.warn("Unhandled ping: {}", ping); } } /** {@inheritDoc} */ @Override protected void onSharedObject(RTMPConnection conn, Channel channel, Header source, SharedObjectMessage object) { log.debug("onSharedObject"); ClientSharedObject so = sharedObjects.get(object.getName()); if (so == null) { log.error("Ignoring request for non-existend SO: {}", object); return; } if (so.isPersistentObject() != object.isPersistent()) { log.error("Ignoring request for wrong-persistent SO: {}", object); return; } log.debug("Received SO request: {}", object); so.dispatchEvent(object); } /** * Invoke a method on the server. * * @param method Method name * @param callback Callback handler */ public void invoke(String method, IPendingServiceCallback callback) { log.debug("invoke method: {} params {} callback {}", new Object[] { method, callback }); // get it from the conn manager RTMPConnection conn = connManager.getConnection(); if (conn == null) { log.info("Connection was null 2"); } if (conn instanceof IServiceCapableConnection) { conn.invoke(method, callback); } } /** * Invoke a method on the server and pass parameters. * * @param method Method * @param params Method call parameters * @param callback Callback object */ public void invoke(String method, Object[] params, IPendingServiceCallback callback) { log.debug("invoke method: {} params {} callback {}", new Object[] { method, params, callback }); // get it from the conn manager RTMPConnection conn = connManager.getConnection(); if (conn == null) { log.info("Connection was null 2"); } if (conn instanceof IServiceCapableConnection) { ((IServiceCapableConnection) conn).invoke(method, params, callback); } } /** * Disconnect the first connection in the connection map */ public void disconnect() { log.debug("disconnect"); IConnection conn = connManager.getConnection(); if (conn == null) { log.info("Connection was null"); } else { streamDataMap.clear(); conn.close(); } } public void createStream(IPendingServiceCallback callback) { IPendingServiceCallback wrapper = new CreateStreamCallBack(callback); invoke("createStream", null, wrapper); } public void publish(int streamId, String name, String mode, INetStreamEventHandler handler) { log.debug("publish stream {}, name: {}, mode {}", new Object[] { streamId, name, mode }); RTMPConnection conn = connManager.getConnection(); Object[] params = new Object[2]; params[0] = name; params[1] = mode; PendingCall pendingCall = new PendingCall("publish", params); conn.invoke(pendingCall, (streamId - 1) * 5 + 4); if (handler != null) { NetStreamPrivateData streamData = streamDataMap.get(streamId); if (streamData != null) { streamData.handler = handler; } } } public void unpublish(int streamId) { log.debug("unpublish stream {}", streamId); PendingCall pendingCall = new PendingCall("publish", new Object[] { false }); connManager.getConnection().invoke(pendingCall, (streamId - 1) * 5 + 4); } public void publishStreamData(int streamId, IMessage message) { NetStreamPrivateData streamData = streamDataMap.get(streamId); if (streamData != null && streamData.connConsumer != null) { streamData.connConsumer.pushMessage(null, message); } } public void play(int streamId, String name, int start, int length) { log.debug("play stream {}, name: {}, start {}, length {}", new Object[] { streamId, name, start, length }); RTMPConnection conn = connManager.getConnection(); if (conn == null) { log.info("Connection was null ?"); } Object[] params = new Object[3]; params[0] = name; params[1] = start; params[2] = length; PendingCall pendingCall = new PendingCall("play", params); conn.invoke(pendingCall, (streamId - 1) * 5 + 4); } /** {@inheritDoc} */ @Override public void connectionOpened(RTMPConnection conn, RTMP state) { log.debug("connectionOpened"); // Send "connect" call to the server Channel channel = conn.getChannel((byte) 3); PendingCall pendingCall = new PendingCall("connect"); pendingCall.setArguments(connectArguments); Invoke invoke = new Invoke(pendingCall); invoke.setConnectionParams(connectionParams); invoke.setInvokeId(1); if (connectCallback != null) { pendingCall.registerCallback(connectCallback); } conn.registerPendingCall(invoke.getInvokeId(), pendingCall); log.debug("Writing 'connect' invoke: {}, invokeId: {}", invoke, invoke .getInvokeId()); channel.write(invoke); } @Override public void connectionClosed(RTMPConnection conn, RTMP state) { log.debug("connectionClosed"); super.connectionClosed(conn, state); if (connectionClosedHandler != null) { connectionClosedHandler.run(); } } /** {@inheritDoc} */ @Override protected void onInvoke(RTMPConnection conn, Channel channel, Header source, Notify invoke, RTMP rtmp) { if (invoke.getType() == IEvent.Type.STREAM_DATA) { log.debug("Ignoring stream data notify with header: {}", source); return; } log.debug("onInvoke: {}, invokeId: {}", invoke, invoke.getInvokeId()); final IServiceCall call = invoke.getCall(); if (call.getServiceMethodName().equals("_result") || call.getServiceMethodName().equals("_error")) { final IPendingServiceCall pendingCall = conn.getPendingCall(invoke .getInvokeId()); log.debug("Received result for pending call {}", pendingCall); if (pendingCall != null) { if ("connect".equals(pendingCall.getServiceMethodName())) { Integer encoding = (Integer) connectionParams .get("objectEncoding"); if (encoding != null && encoding.intValue() == 3) { log.debug("set encoding to AMF3"); rtmp.setEncoding(IConnection.Encoding.AMF3); } } } handlePendingCallResult(conn, invoke); return; } // potentially used twice so get the value once boolean onStatus = call.getServiceMethodName().equals("onStatus"); if (onStatus) { // XXX better to serialize ObjectMap to Status object ObjectMap<?, ?> objMap = (ObjectMap<?, ?>) call.getArguments()[0]; // should keep this as an Object to stay compatible with FMS3 etc Object clientId = (Object) objMap.get("clientid"); if (clientId == null) { clientId = source.getStreamId(); } log.debug("Client id: {}", clientId); if (clientId != null) { NetStreamPrivateData streamData = streamDataMap.get(clientId); if (streamData != null && streamData.handler != null) { streamData.handler.onStreamEvent(invoke); } } } if (serviceProvider == null) { // Client doesn't support calling methods on him call.setStatus(Call.STATUS_METHOD_NOT_FOUND); call.setException(new MethodNotFoundException(call .getServiceMethodName())); } else { serviceInvoker.invoke(call, serviceProvider); } if (call instanceof IPendingServiceCall) { IPendingServiceCall psc = (IPendingServiceCall) call; Object result = psc.getResult(); log.debug("Pending call result is: {}", result); if (result instanceof DeferredResult) { DeferredResult dr = (DeferredResult) result; dr.setInvokeId(invoke.getInvokeId()); dr.setServiceCall(psc); dr.setChannel(channel); conn.registerDeferredResult(dr); } else if (!onStatus) { Invoke reply = new Invoke(); reply.setCall(call); reply.setInvokeId(invoke.getInvokeId()); log.debug("Sending empty call reply: {}", reply); channel.write(reply); } } } /** * Setter for codec factory * * @param factory Codec factory to use */ public void setCodecFactory(RTMPCodecFactory factory) { this.codecFactory = factory; } /** * Getter for codec factory * * @return Codec factory */ public RTMPCodecFactory getCodecFactory() { return this.codecFactory; } public void handleException(Throwable throwable) { if (exceptionHandler != null) { exceptionHandler.handleException(throwable); } else { log.error("Connection exception", throwable); throw new RuntimeException(throwable); } } /** * Setter for stream event dispatcher (useful for saving playing stream to * file) * * @param streamEventDispatcher event dispatcher */ public void setStreamEventDispatcher(IEventDispatcher streamEventDispatcher) { this.streamEventDispatcher = streamEventDispatcher; } private class NetStream extends AbstractClientStream implements IEventDispatcher { private IEventDispatcher dispatcher; public NetStream(IEventDispatcher dispatcher) { this.dispatcher = dispatcher; } public void close() { // do nothing } public void start() { // do nothing } public void stop() { // do nothing } public void dispatchEvent(IEvent event) { if (dispatcher != null) dispatcher.dispatchEvent(event); } } private class CreateStreamCallBack implements IPendingServiceCallback { private IPendingServiceCallback wrapped; public CreateStreamCallBack(IPendingServiceCallback wrapped) { this.wrapped = wrapped; } public void resultReceived(IPendingServiceCall call) { Integer streamIdInt = (Integer) call.getResult(); log.debug("Stream id: {}", streamIdInt); RTMPConnection conn = connManager.getConnection(); if (conn != null && streamIdInt != null) { NetStream stream = new NetStream(streamEventDispatcher); stream.setConnection(conn); stream.setStreamId(streamIdInt); conn.addClientStream(stream); NetStreamPrivateData streamData = new NetStreamPrivateData(); streamData.outputStream = conn.createOutputStream(streamIdInt); streamData.connConsumer = new ConnectionConsumer(conn, streamData.outputStream.getVideo().getId(), streamData.outputStream.getAudio().getId(), streamData.outputStream.getData().getId()); streamDataMap.put(streamIdInt, streamData); } wrapped.resultReceived(call); } } private class NetStreamPrivateData { public volatile INetStreamEventHandler handler; public volatile OutputStream outputStream; public volatile ConnectionConsumer connConsumer; } }