All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.red5.client.net.rtmp.BaseRTMPClientHandler Maven / Gradle / Ivy

There is a newer version: 2.0.12
Show newest version
/*
 * RED5 Open Source Flash Server - https://github.com/Red5/ Copyright 2006-2015 by respective authors (see below). All rights reserved. Licensed under the Apache License, Version
 * 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless
 * required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and limitations under the License.
 */

package org.red5.client.net.rtmp;

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

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.IServiceInvoker;
import org.red5.server.api.so.IClientSharedObject;
import org.red5.server.api.stream.IClientStream;
import org.red5.server.messaging.IMessage;
import org.red5.server.net.ICommand;
import org.red5.server.net.rtmp.BaseRTMPHandler;
import org.red5.server.net.rtmp.Channel;
import org.red5.server.net.rtmp.DeferredResult;
import org.red5.server.net.rtmp.RTMPConnection;
import org.red5.server.net.rtmp.codec.RTMP;
import org.red5.server.net.rtmp.event.ChunkSize;
import org.red5.server.net.rtmp.event.ClientBW;
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.event.SWFResponse;
import org.red5.server.net.rtmp.event.ServerBW;
import org.red5.server.net.rtmp.message.Header;
import org.red5.server.net.rtmp.status.StatusCodes;
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.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * Base class for clients (RTMP and RTMPT)
 */
public abstract class BaseRTMPClientHandler extends BaseRTMPHandler implements IRTMPClient {

    /**
     * Connection scheme / protocol
     */
    protected String protocol = "rtmp";

    /**
     * Connection parameters
     */
    protected Map connectionParams;

    /**
     * Connect call arguments
     */
    private Object[] connectArguments;

    /**
     * Connection callback
     */
    private IPendingServiceCallback connectCallback;

    /**
     * Service provider
     */
    private Object serviceProvider;

    /**
     * Service invoker
     */
    private IServiceInvoker serviceInvoker = new ServiceInvoker();

    /**
     * Shared objects map
     */
    private volatile ConcurrentMap sharedObjects = new ConcurrentHashMap<>(1, 0.9f, 1);

    /**
     * Net stream handling
     */
    private volatile CopyOnWriteArraySet streamDataList = new CopyOnWriteArraySet<>();

    /**
     * Task to start on connection close
     */
    private Runnable connectionClosedHandler;

    /**
     * Task to start on connection errors
     */
    private ClientExceptionHandler exceptionHandler;

    /**
     * Stream event dispatcher
     */
    private IEventDispatcher streamEventDispatcher;

    /**
     * Stream event handler
     */
    private INetStreamEventHandler streamEventHandler;

    /**
     * Associated RTMP connection
     */
    protected volatile RTMPConnection conn;

    /**
     * Whether or not the bandwidth done has been invoked
     */
    protected boolean bandwidthCheckDone;

    /**
     * Whether or not the client is subscribed
     */
    protected boolean subscribed;

    /**
     * Whether or not to use swf verification
     */
    private boolean swfVerification;

    private int bytesReadWindow = 2500000;

    private int bytesWrittenWindow = 2500000;

    /**
     * For handling threading / scheduling in the clients.
     */
    protected ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

    protected BaseRTMPClientHandler() {
    }

    /**
     * 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
     */
    @Override
    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
     */
    @Override
    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);
    }

    /**
     * Creates the default connection parameters collection. Many implementations of this handler will create a tcUrl if not found, it is
     * created with the current server url.
     *
     * @param server
     *            the server location
     * @param port
     *            the port for the protocol
     * @param application
     *            the application name at the given server
     * @return connection parameters map
     */
    @Override
    public Map makeDefaultConnectionParams(String server, int port, String application) {
        Map params = new ObjectMap<>();
        params.put("app", application);
        params.put("objectEncoding", Integer.valueOf(0));
        params.put("fpad", Boolean.FALSE);
        params.put("flashVer", "FMLE/3.0 (compatible; Red5Client)"); // old value WIN 11,2,202,235
        params.put("audioCodecs", Integer.valueOf(0x0FFF)); // old value 3575 = 0x0E0F
        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(0x00FF)); // old value 252 = 0x0FC
        params.put("audioFourCcInfoMap", Collections.singletonMap("*", Integer.valueOf(4)));
        //params.put("audioFourCcInfoMap", Collections.singletonMap(AudioCodec.AAC.getFourcc(), Integer.valueOf(4)));
        params.put("videoFourCcInfoMap", Collections.singletonMap("*", Integer.valueOf(4)));
        //params.put("videoFourCcInfoMap", Collections.singletonMap(VideoCodec.AVC.getFourcc(), Integer.valueOf(4)));
        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
     */
    @Override
    public void connect(String server, int port, Map 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
     */
    @Override
    public void connect(String server, int port, Map 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
     */
    @Override
    public void connect(String server, int port, Map connectionParams, IPendingServiceCallback connectCallback, Object[] connectCallArguments) {
        log.debug("connect server: {} port {} connect - params: {} callback: {} args: {}", new Object[] { server, port, connectionParams, connectCallback, Arrays.toString(connectCallArguments) });
        log.info("{}://{}:{}/{}", new Object[] { protocol, server, port, connectionParams.get("app") });
        this.connectionParams = connectionParams;
        this.connectArguments = connectCallArguments;
        if (!connectionParams.containsKey("objectEncoding")) {
            connectionParams.put("objectEncoding", 0);
        }
        this.connectCallback = connectCallback;
        startConnector(server, port);
    }

    /**
     * Register object that provides methods that can be called by the server.
     *
     * @param serviceProvider
     *            Service provider
     */
    @Override
    public void setServiceProvider(Object serviceProvider) {
        this.serviceProvider = serviceProvider;
    }

    /**
     * Sets a handler for connection close.
     *
     * @param connectionClosedHandler
     *            close handler
     */
    @Override
    public void setConnectionClosedHandler(Runnable connectionClosedHandler) {
        log.debug("setConnectionClosedHandler: {}", connectionClosedHandler);
        this.connectionClosedHandler = connectionClosedHandler;
    }

    /**
     * Sets a handler for exceptions.
     *
     * @param exceptionHandler
     *            exception handler
     */
    @Override
    public void setExceptionHandler(ClientExceptionHandler exceptionHandler) {
        log.debug("setExceptionHandler: {}", exceptionHandler);
        this.exceptionHandler = exceptionHandler;
    }

    /**
     * Connect to client shared object.
     *
     * @param name
     *            Client shared object name
     * @param persistent
     *            SO persistence flag
     * @return Client shared object instance
     */
    @Override
    public 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.isPersistent() != 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");
        // set read and write chunk sizes
        RTMP state = conn.getState();
        state.setReadChunkSize(chunkSize.getSize());
        state.setWriteChunkSize(chunkSize.getSize());
        log.info("ChunkSize configured: {}", chunkSize);
    }

    /** {@inheritDoc} */
    @Override
    protected void onPing(RTMPConnection conn, Channel channel, Header source, Ping ping) {
        log.trace("onPing");
        switch (ping.getEventType()) {
            case Ping.PING_CLIENT:
            case Ping.STREAM_BEGIN:
            case Ping.RECORDED_STREAM:
            case Ping.STREAM_PLAYBUFFER_CLEAR:
                // the server wants to measure the RTT
                Ping pong = new Ping();
                pong.setEventType(Ping.PONG_SERVER);
                pong.setValue2((int) (System.currentTimeMillis() & 0xffffffff));
                conn.ping(pong);
                break;
            case Ping.STREAM_DRY:
                log.debug("Stream indicates there is no data available");
                break;
            case Ping.CLIENT_BUFFER:
                // set the client buffer
                IClientStream stream = null;
                // get the stream id
                Number streamId = ping.getValue2();
                // get requested buffer size in milliseconds
                int buffer = ping.getValue3();
                log.debug("Client sent a buffer size: {} ms for stream id: {}", buffer, streamId);
                // the client wants to set the buffer time
                stream = conn.getStreamById(streamId);
                if (stream != null) {
                    stream.setClientBufferDuration(buffer);
                    log.info("Setting client buffer on stream: {}", buffer);
                }
                // catch-all to make sure buffer size is set
                if (stream == null) {
                    // remember buffer time until stream is created
                    conn.rememberStreamBufferDuration(streamId.intValue(), buffer);
                    log.info("Remembering client buffer on stream: {}", buffer);
                }
                break;
            case Ping.PING_SWF_VERIFY:
                log.debug("SWF verification ping");
                // TODO get the swf verification bytes from the handshake
                SWFResponse swfPong = new SWFResponse(new byte[42]);
                conn.ping(swfPong);
                break;
            case Ping.BUFFER_EMPTY:
                log.debug("Buffer empty ping");

                break;
            case Ping.BUFFER_FULL:
                log.debug("Buffer full ping");

                break;
            default:
                log.debug("Unhandled ping: {}", ping);
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void onServerBandwidth(RTMPConnection conn, Channel channel, ServerBW message) {
        log.trace("onServerBandwidth");
        // if the size is not equal to our read size send a client bw control message
        int bandwidth = message.getBandwidth();
        if (bandwidth != bytesReadWindow) {
            ClientBW clientBw = new ClientBW(bandwidth, (byte) 2);
            channel.write(clientBw);
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void onClientBandwidth(RTMPConnection conn, Channel channel, ClientBW message) {
        log.trace("onClientBandwidth");
        // if the size is not equal to our write size send a server bw control message
        int bandwidth = message.getBandwidth();
        if (bandwidth != bytesWrittenWindow) {
            ServerBW serverBw = new ServerBW(bandwidth);
            channel.write(serverBw);
        }
    }

    /** {@inheritDoc} */
    @Override
    protected void onSharedObject(RTMPConnection conn, Channel channel, Header source, SharedObjectMessage object) {
        log.trace("onSharedObject");
        ClientSharedObject so = sharedObjects.get(object.getName());
        if (so != null) {
            if (so.isPersistent() == object.isPersistent()) {
                log.debug("Received SO request: {}", object);
                so.dispatchEvent(object);
            } else {
                log.error("Ignoring request for wrong-persistent SO: {}", object);
            }
        } else {
            log.error("Ignoring request for non-existend SO: {}", object);
        }
    }

    /**
     * Called when negotiating bandwidth.
     */
    public void onBWCheck() {
        log.debug("onBWCheck");
    }

    /**
     * Called when negotiating bandwidth.
     *
     * @param params
     *            bw parameters
     */
    public void onBWCheck(Object params) {
        log.debug("onBWCheck: {}", params);
    }

    /**
     * Called when bandwidth has been configured.
     *
     * @param params
     *            bw parameters
     */
    public void onBWDone(Object params) {
        log.debug("onBWDone: {}", params);
        bandwidthCheckDone = true;
    }

    /**
     * Called when bandwidth has been configured.
     */
    public void onBWDone() {
        log.debug("onBWDone");
        bandwidthCheckDone = true;
    }

    /**
     * Invoke a method on the server.
     *
     * @param method
     *            Method name
     * @param callback
     *            Callback handler
     */
    @Override
    public void invoke(String method, IPendingServiceCallback callback) {
        log.debug("invoke method: {} params {} callback {}", new Object[] { method, callback });
        // get it from the conn manager
        if (conn != null) {
            conn.invoke(method, callback);
        } else {
            log.info("Connection was null");
            PendingCall result = new PendingCall(method);
            result.setStatus(Call.STATUS_NOT_CONNECTED);
            callback.resultReceived(result);
        }
    }

    /**
     * Invoke a method on the server and pass parameters.
     *
     * @param method
     *            Method
     * @param params
     *            Method call parameters
     * @param callback
     *            Callback object
     */
    @Override
    public void invoke(String method, Object[] params, IPendingServiceCallback callback) {
        log.debug("invoke method: {} params {} callback {}", new Object[] { method, params, callback });
        if (conn != null) {
            conn.invoke(method, params, callback);
        } else {
            log.info("Connection was null");
            PendingCall result = new PendingCall(method, params);
            result.setStatus(Call.STATUS_NOT_CONNECTED);
            callback.resultReceived(result);
        }
    }

    /**
     * Disconnect the first connection in the connection map
     */
    @Override
    public void disconnect() {
        log.debug("disconnect - connection: {}", conn);
        if (conn != null) {
            streamDataList.clear();
            conn.close();
        }
    }

    @Override
    public void createStream(IPendingServiceCallback callback) {
        log.debug("createStream - callback: {}", callback);
        IPendingServiceCallback wrapper = new CreateStreamCallBack(callback);
        invoke("createStream", null, wrapper);
    }

    public void releaseStream(IPendingServiceCallback callback, Object[] params) {
        log.debug("releaseStream - callback: {}", callback);
        IPendingServiceCallback wrapper = new ReleaseStreamCallBack(callback);
        invoke("releaseStream", params, wrapper);
    }

    public void deleteStream(IPendingServiceCallback callback) {
        log.debug("deleteStream - callback: {}", callback);
        IPendingServiceCallback wrapper = new DeleteStreamCallBack(callback);
        invoke("deleteStream", null, wrapper);
    }

    public void subscribe(IPendingServiceCallback callback, Object[] params) {
        log.debug("subscribe - callback: {}", callback);
        IPendingServiceCallback wrapper = new SubscribeStreamCallBack(callback);
        invoke("FCSubscribe", params, wrapper);
    }

    @Override
    public void publish(Number streamId, String name, String mode, INetStreamEventHandler handler) {
        log.debug("publish - stream id: {}, name: {}, mode: {}", streamId, name, mode);
        // setup the netstream handler
        if (handler != null) {
            final int sid = streamId.intValue();
            NetStreamPrivateData streamData = streamDataList.stream().filter(s -> s.getStreamId() == sid).findFirst().orElse(null);
            if (streamData != null) {
                log.debug("Setting handler on stream data - handler: {}", handler);
                streamData.handler = handler;
            } else {
                log.debug("Stream data not found for stream id: {}", streamId);
            }
        }
        // setup publish parameters
        final Object[] params = new Object[2];
        params[0] = name;
        params[1] = mode;
        // call publish
        PendingCall pendingCall = new PendingCall("publish", params);
        conn.invoke(pendingCall, getChannelForStreamId(streamId));
    }

    @Override
    public void unpublish(Number streamId) {
        log.debug("unpublish stream {}", streamId);
        PendingCall pendingCall = new PendingCall("publish", new Object[] { false });
        // this cannot handle a null streamId, so force to 1.0 if null
        if (streamId == null) {
            streamId = 1.0;
        }
        conn.invoke(pendingCall, getChannelForStreamId(streamId));
    }

    @Override
    public void publishStreamData(Number streamId, IMessage message) {
        // get the stream data by index of the list
        final int sid = streamId.intValue();
        NetStreamPrivateData streamData = streamDataList.stream().filter(s -> s.getStreamId() == sid).findFirst().orElse(null);
        if (streamData != null) {
            if (streamData.connConsumer != null) {
                streamData.connConsumer.pushMessage(null, message);
            } else {
                log.warn("Connection consumer was not found for stream id: {}", streamId);
            }
        } else {
            log.warn("Stream data not found for stream id: {} in {}", streamId, streamDataList);
        }
    }

    @Override
    public void play(Number streamId, String name, int start, int length) {
        log.debug("play stream {}, name: {}, start {}, length {}", new Object[] { streamId, name, start, length });
        if (conn != null) {
            // get the channel
            int channel = getChannelForStreamId(streamId);
            // send our requested buffer size
            ping(Ping.CLIENT_BUFFER, streamId, 2000);
            // send our request for a/v
            PendingCall receiveAudioCall = new PendingCall("receiveAudio");
            conn.invoke(receiveAudioCall, channel);
            PendingCall receiveVideoCall = new PendingCall("receiveVideo");
            conn.invoke(receiveVideoCall, channel);
            // call play
            Object[] params = new Object[3];
            params[0] = name;
            params[1] = (start >= 1000 || start <= -1000) ? start : start * 1000;
            params[2] = (length >= 1000 || length <= -1000) ? length : length * 1000;
            PendingCall pendingCall = new PendingCall("play", params);
            conn.invoke(pendingCall, channel);
        } else {
            log.info("Connection was null ?");
        }
    }

    /**
     * Dynamic streaming play method.
     *
     * The following properties are supported on the play options:
     *
     * 
     *         streamName: String. The name of the stream to play or the new stream to switch to.
     *         oldStreamName: String. The name of the initial stream that needs to be switched out. This is not needed and ignored
     *                        when play2 is used for just playing the stream and not switching to a new stream.
     *         start: Number. The start time of the new stream to play, just as supported by the existing play API. and it has the
     *                        same defaults. This is ignored when the method is called for switching (in other words, the transition
     *                        is either NetStreamPlayTransition.SWITCH or NetStreamPlayTransitions.SWAP)
     *         len: Number. The duration of the playback, just as supported by the existing play API and has the same defaults.
     *         transition: String. The transition mode for the playback command. It could be one of the following:
     *                             NetStreamPlayTransitions.RESET
     *                             NetStreamPlayTransitions.APPEND
     *                             NetStreamPlayTransitions.SWITCH
     *                             NetStreamPlayTransitions.SWAP
     * 
* * NetStreamPlayTransitions: * *
     *             APPEND : String = "append" - Adds the stream to a playlist and begins playback with the first stream.
     *             APPEND_AND_WAIT : String = "appendAndWait" - Builds a playlist without starting to play it from the first stream.
     *             RESET : String = "reset" - Clears any previous play calls and plays the specified stream immediately.
     *             RESUME : String = "resume" - Requests data from the new connection starting from the point at which the previous connection ended.
     *             STOP : String = "stop" - Stops playing the streams in a playlist.
     *             SWAP : String = "swap" - Replaces a content stream with a different content stream and maintains the rest of the playlist.
     *             SWITCH : String = "switch" - Switches from playing one stream to another stream, typically with streams of the same content.
     * 
* * @see ActionScript guide to dynamic * streaming * @see NetStreamPlayTransitions */ @Override public void play2(Number streamId, Map playOptions) { log.debug("play2 options: {}", playOptions.toString()); /* * { streamName=streams/new.flv, oldStreamName=streams/old.flv, start=0, len=-1, offset=12.195, transition=switch } */ // get the transition type String transition = (String) playOptions.get("transition"); if (conn != null) { if ("NetStreamPlayTransitions.STOP".equals(transition)) { PendingCall pendingCall = new PendingCall("play", new Object[] { Boolean.FALSE }); conn.invoke(pendingCall, getChannelForStreamId(streamId)); } else if ("NetStreamPlayTransitions.RESET".equals(transition)) { // just reset the currently playing stream } else { Object[] params = new Object[6]; params[0] = playOptions.get("streamName").toString(); Object o = playOptions.get("start"); params[1] = o instanceof Integer ? (Integer) o : Integer.valueOf((String) o); o = playOptions.get("len"); params[2] = o instanceof Integer ? (Integer) o : Integer.valueOf((String) o); // new parameters for playback params[3] = transition; params[4] = playOptions.get("offset"); params[5] = playOptions.get("oldStreamName"); // do call PendingCall pendingCall = new PendingCall("play2", params); conn.invoke(pendingCall, getChannelForStreamId(streamId)); } } else { log.info("Connection was null ?"); } } /** * Sends a ping. * * @param pingType * the type of ping * @param streamId * streams id * @param param * ping parameter */ public void ping(short pingType, Number streamId, int param) { conn.ping(new Ping(pingType, streamId, param)); } /** {@inheritDoc} */ @Override public void connectionOpened(RTMPConnection conn) { log.trace("connectionOpened - conn: {}", conn); executor.submit(() -> { Thread.currentThread().setName(String.format("ClientOpener@%s", conn.getSessionId())); // Send "connect" call to the server Channel channel = conn.getChannel(3); PendingCall pendingCall = new PendingCall("connect"); pendingCall.setArguments(connectArguments); Invoke invoke = new Invoke(pendingCall); invoke.setConnectionParams(connectionParams); invoke.setTransactionId(1); // register any other callback if (connectCallback != null) { pendingCall.registerCallback(connectCallback); } conn.registerPendingCall(invoke.getTransactionId(), pendingCall); log.debug("Writing 'connect' invoke: {}, invokeId: {}", invoke, invoke.getTransactionId()); channel.write(invoke); }); } @Override public void connectionClosed(RTMPConnection conn) { log.debug("connectionClosed"); super.connectionClosed(conn); byte stateCode = conn.getStateCode(); // submit close handler only if we're not yet disconnected if (stateCode != RTMP.STATE_DISCONNECTED) { if (connectionClosedHandler != null) { executor.submit(connectionClosedHandler); } } // shutdown the executor when we're disconnected if (stateCode == RTMP.STATE_DISCONNECTED) { log.debug("Shutting down executor"); executor.shutdown(); } } /** {@inheritDoc} */ @Override protected void onCommand(RTMPConnection conn, Channel channel, Header source, ICommand command) { log.trace("onCommand: {}, id: {}", command, command.getTransactionId()); final IServiceCall call = command.getCall(); final String methodName = call.getServiceMethodName(); log.debug("Service name: {} args[0]: {}", methodName, (call.getArguments().length != 0 ? call.getArguments()[0] : "")); if ("_result".equals(methodName) || "_error".equals(methodName)) { final IPendingServiceCall pendingCall = conn.getPendingCall(command.getTransactionId()); 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("Setting encoding to AMF3"); conn.getState().setEncoding(IConnection.Encoding.AMF3); } } } handlePendingCallResult(conn, (Invoke) command); return; } // potentially used twice so get the value once boolean onStatus = "onStatus".equals(methodName); if (onStatus) { log.debug("onStatus"); Number streamId = (Number) (source.getStreamId() != null ? source.getStreamId().doubleValue() : 1.0d); if (log.isDebugEnabled()) { log.debug("Stream id from header: {}", streamId); try { // XXX create 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 log.debug("Client id from status: {}", objMap.get("clientid")); } catch (Exception e) { log.warn("Exception mapping call args", e); } } if (streamId != null) { // try lookup by stream id, if null return the first one final int sid = streamId.intValue(); NetStreamPrivateData streamData = streamDataList.stream().filter(s -> s.getStreamId() == sid).findFirst().orElse(null); if (streamData == null) { log.warn("Stream data was null for id: {} entry count: {}", streamId, streamDataList.size()); } else if (streamData.handler != null) { log.debug("Got stream data and handler"); streamData.handler.onStreamEvent((Notify) command); } } } // if this client supports service methods, forward the call if (serviceProvider == null) { // client doesn't support calling methods on him call.setStatus(Call.STATUS_METHOD_NOT_FOUND); call.setException(new MethodNotFoundException(methodName)); log.info("No service provider / method for: {}; to handle calls like onBWCheck, add a service provider", methodName); } else { try { serviceInvoker.invoke(call, serviceProvider); } catch (Exception ex) { Object[] callArgs = call.getArguments(); log.warn("Exception invoking method: {} args[0] type: {}", methodName, (callArgs.length > 0 ? callArgs[0].getClass() : "null"), ex); call.setStatus(Call.STATUS_METHOD_NOT_FOUND); call.setException(ex); } } 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.setTransactionId(command.getTransactionId()); dr.setServiceCall(psc); dr.setChannel(channel); conn.registerDeferredResult(dr); } else if (!onStatus) { if ("onBWCheck".equals(methodName)) { onBWCheck(call.getArguments().length > 0 ? call.getArguments()[0] : null); Invoke reply = new Invoke(); reply.setCall(call); reply.setTransactionId(command.getTransactionId()); channel.write(reply); } else if ("onBWDone".equals(methodName)) { onBWDone(call.getArguments().length > 0 ? call.getArguments()[0] : null); } else { Invoke reply = new Invoke(); reply.setCall(call); reply.setTransactionId(command.getTransactionId()); log.debug("Sending empty call reply: {}", reply); channel.write(reply); } } } } /** * Handle any exceptions that occur. * * @param throwable * Exception thrown */ public void handleException(Throwable throwable) { log.debug("Handle exception: {} with: {}", throwable.getMessage(), exceptionHandler); if (exceptionHandler != null) { exceptionHandler.handleException(throwable); } else { log.error("Connection exception", throwable); throw new RuntimeException(throwable); } } /** * Returns a channel based on the given stream id. * * @param streamId * stream id * @return the channel for this stream id */ protected int getChannelForStreamId(Number streamId) { return (streamId.intValue() - 1) * 5 + 4; } /** * Sets the protocol. * * @param protocol * the data protocol to use. * @throws Exception * thrown */ public void setProtocol(String protocol) throws Exception { this.protocol = protocol; } /** * Sets a reference to the connection associated with this client handler. * * @param conn * connection */ public void setConnection(RTMPConnection conn) { this.conn = conn; this.conn.setHandler(this); if (conn.getExecutor() == null) { // setup executor ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(1); executor.setDaemon(true); executor.setMaxPoolSize(1); executor.initialize(); conn.setExecutor(executor); } } /** * Returns the connection associated with this client. * * @return conn connection */ @Override public RTMPConnection getConnection() { return conn; } /** * Enables or disables SWF verification. * * @param enabled * state of SWF verification */ public void setSwfVerification(boolean enabled) { swfVerification = enabled; } /** * Returns true if swf verification is enabled * * @return the swfVerification */ public boolean isSwfVerification() { return swfVerification; } /** * Returns true if bandwidth done has been invoked * * @return the bandwidthCheckDone */ public boolean isBandwidthCheckDone() { return bandwidthCheckDone; } /** * Returns true if this client is subscribed * * @return subscribed state */ public boolean isSubscribed() { return subscribed; } /** * @return the connectionParams */ public Map getConnectionParams() { return connectionParams; } /** * Setter for stream event dispatcher (useful for saving playing stream to file) * * @param streamEventDispatcher * event dispatcher */ @Override public void setStreamEventDispatcher(IEventDispatcher streamEventDispatcher) { this.streamEventDispatcher = streamEventDispatcher; } /** * Setter for the stream event handler. * * @param streamEventHandler * event handler */ public void setStreamEventHandler(INetStreamEventHandler streamEventHandler) { this.streamEventHandler = streamEventHandler; } private class NetStream extends AbstractClientStream implements IEventDispatcher { private IEventDispatcher dispatcher; public NetStream(IEventDispatcher dispatcher) { this.dispatcher = dispatcher; } @Override public void close() { log.debug("NetStream close"); } @Override public void start() { log.debug("NetStream start"); } @Override public void stop() { log.debug("NetStream stop"); } @Override public void dispatchEvent(IEvent event) { log.debug("NetStream dispatchEvent: {}", event); if (dispatcher != null) { dispatcher.dispatchEvent(event); } } } private class CreateStreamCallBack implements IPendingServiceCallback { private IPendingServiceCallback wrapped; public CreateStreamCallBack(IPendingServiceCallback wrapped) { log.debug("CreateStreamCallBack {}", wrapped.getClass().getName()); this.wrapped = wrapped; } @Override public void resultReceived(IPendingServiceCall call) { // get the result as base object Object callResult = call.getResult(); if (callResult != null) { // we expect a number consisting of the stream id, but we'll check for an object map as well int streamId = -1; if (callResult instanceof Number) { streamId = ((Number) callResult).intValue(); } else if (callResult instanceof Map) { Map map = (Map) callResult; // XXX(paul) log out the map contents log.warn("CreateStreamCallBack resultReceived - map: {}", map); if (map.containsKey("streamId")) { Object tmpStreamId = map.get("streamId"); if (tmpStreamId instanceof Number) { streamId = ((Number) tmpStreamId).intValue(); } else { log.warn("CreateStreamCallBack resultReceived - stream id is not a number: {}", tmpStreamId); } } } log.debug("CreateStreamCallBack resultReceived - stream id: {} call: {} connection: {}", streamId, call, conn); if (conn != null && streamId != -1) { log.debug("Setting new net stream"); NetStream stream = new NetStream(streamEventDispatcher); stream.setConnection(conn); stream.setStreamId(streamId); conn.addClientStream(stream); NetStreamPrivateData streamData = new NetStreamPrivateData(streamId); streamData.outputStream = conn.createOutputStream(streamId); streamData.connConsumer = new ConnectionConsumer(conn, streamData.outputStream.getVideo(), streamData.outputStream.getAudio(), streamData.outputStream.getData()); streamDataList.add(streamData); log.debug("streamDataList: {}", streamDataList); } wrapped.resultReceived(call); } else { log.warn("CreateStreamCallBack resultReceived - call result is null"); } } } private class ReleaseStreamCallBack implements IPendingServiceCallback { private IPendingServiceCallback wrapped; public ReleaseStreamCallBack(IPendingServiceCallback wrapped) { log.debug("ReleaseStreamCallBack {}", wrapped.getClass().getName()); this.wrapped = wrapped; } @Override public void resultReceived(IPendingServiceCall call) { wrapped.resultReceived(call); } } private class DeleteStreamCallBack implements IPendingServiceCallback { private IPendingServiceCallback wrapped; public DeleteStreamCallBack(IPendingServiceCallback wrapped) { log.debug("DeleteStreamCallBack {}", wrapped.getClass().getName()); this.wrapped = wrapped; } @Override public void resultReceived(IPendingServiceCall call) { // get the result as base object Object callResult = call.getResult(); if (callResult != null) { // we expect a number consisting of the stream id, but we'll check for an object map as well final Number streamId = (Number) (callResult instanceof Number ? callResult : (callResult instanceof Map ? ((Map) callResult).get("streamId") : 1.0)); log.debug("DeleteStreamCallBack resultReceived - stream id: {} call: {} connection: {}", streamId, call, conn); if (conn != null) { log.debug("Deleting net stream"); conn.removeClientStream(streamId); // send a delete notify? final int sid = streamId.intValue(); NetStreamPrivateData streamData = streamDataList.stream().filter(s -> s.getStreamId() == sid).findFirst().orElse(null); if (streamData != null) { streamDataList.remove(streamData); } else { log.warn("Stream data not found for stream id: {}", streamId); } } wrapped.resultReceived(call); } else { log.warn("DeleteStreamCallBack resultReceived - call result is null"); } } } private class SubscribeStreamCallBack implements IPendingServiceCallback { private IPendingServiceCallback wrapped; public SubscribeStreamCallBack(IPendingServiceCallback wrapped) { log.debug("SubscribeStreamCallBack {}", wrapped.getClass().getName()); this.wrapped = wrapped; } @Override public void resultReceived(IPendingServiceCall call) { log.debug("resultReceived", call); if (call.getResult() instanceof ObjectMap) { ObjectMap map = (ObjectMap) call.getResult(); if (map.containsKey("code")) { String code = (String) map.get("code"); log.debug("Code: {}", code); if (StatusCodes.NS_PLAY_START.equals(code)) { subscribed = true; } } } wrapped.resultReceived(call); } } private final class NetStreamPrivateData { public volatile INetStreamEventHandler handler; public volatile OutputStream outputStream; public volatile ConnectionConsumer connConsumer; private final int streamId; NetStreamPrivateData(int streamId) { this.streamId = streamId; if (streamEventHandler != null) { handler = streamEventHandler; } } public int getStreamId() { return streamId; } @Override public int hashCode() { return streamId; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy