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.BitSet; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import org.apache.mina.common.ByteBuffer; import org.red5.server.BaseConnection; import org.red5.server.api.IBWControllable; import org.red5.server.api.IBandwidthConfigure; import org.red5.server.api.IConnectionBWConfig; import org.red5.server.api.IContext; import org.red5.server.api.IScope; import org.red5.server.api.Red5; import org.red5.server.api.scheduling.IScheduledJob; import org.red5.server.api.scheduling.ISchedulingService; 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.stream.IClientBroadcastStream; import org.red5.server.api.stream.IClientStream; import org.red5.server.api.stream.IPlaylistSubscriberStream; import org.red5.server.api.stream.ISingleItemSubscriberStream; import org.red5.server.api.stream.IStreamCapableConnection; import org.red5.server.api.stream.IStreamService; import org.red5.server.exception.ClientRejectedException; import org.red5.server.net.rtmp.codec.RTMP; import org.red5.server.net.rtmp.event.BytesRead; 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.ServerBW; import org.red5.server.net.rtmp.event.VideoData; import org.red5.server.net.rtmp.message.Packet; import org.red5.server.service.Call; import org.red5.server.service.PendingCall; import org.red5.server.stream.ClientBroadcastStream; import org.red5.server.stream.IBWControlContext; import org.red5.server.stream.IBWControlService; import org.red5.server.stream.OutputStream; import org.red5.server.stream.PlaylistSubscriberStream; import org.red5.server.stream.StreamService; import org.red5.server.stream.VideoCodecFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; /** * RTMP connection. Stores information about client streams, data transfer * channels, pending RPC calls, bandwidth configuration, used encoding * (AMF0/AMF3), connection state (is alive, last ping time and ping result) and * session. */ public abstract class RTMPConnection extends BaseConnection implements IStreamCapableConnection, IServiceCapableConnection { /** * Logger */ private static Logger log = LoggerFactory.getLogger(RTMPConnection.class); /** * Video codec factory constant */ private static final String VIDEO_CODEC_FACTORY = "videoCodecFactory"; // private Context context; /** * Connection channels * * @see org.red5.server.net.rtmp.Channel */ private ConcurrentMap<Integer, Channel> channels = new ConcurrentHashMap<Integer, Channel>(); /** * Client streams * * @see org.red5.server.api.stream.IClientStream */ private ConcurrentMap<Integer, IClientStream> streams = new ConcurrentHashMap<Integer, IClientStream>(); private final BitSet reservedStreams = new BitSet(); /** * Identifier for remote calls. */ private AtomicInteger invokeId = new AtomicInteger(1); /** * Hash map that stores pending calls and ids as pairs. */ private ConcurrentMap<Integer, IPendingServiceCall> pendingCalls = new ConcurrentHashMap<Integer, IPendingServiceCall>(); /** * Deferred results set. * * @see org.red5.server.net.rtmp.DeferredResult */ private final HashSet<DeferredResult> deferredResults = new HashSet<DeferredResult>(); /** * Last ping roundtrip time */ private AtomicInteger lastPingTime = new AtomicInteger(-1); /** * Timestamp when last ping command was sent. */ private AtomicLong lastPingSent = new AtomicLong(0); /** * Timestamp when last ping result was received. */ private AtomicLong lastPongReceived = new AtomicLong(0); /** * Name of quartz job that keeps connection alive. */ private String keepAliveJobName; /** * Ping interval in ms to detect dead clients. */ private volatile int pingInterval = 5000; /** * Maximum time in ms after a client is disconnected because of inactivity. */ private volatile int maxInactivity = 60000; /** * Data read interval */ private int bytesReadInterval = 120 * 1024; /** * Number of bytes to read next. */ private int nextBytesRead = 120 * 1024; /** * Number of bytes the client reported to have received. */ private long clientBytesRead = 0; /** * Bandwidth configure. */ private IConnectionBWConfig bwConfig; /** * Bandwidth context used by bandwidth controller. */ private IBWControlContext bwContext; /** * Map for pending video packets and stream IDs. */ private ConcurrentMap<Integer, AtomicInteger> pendingVideos = new ConcurrentHashMap<Integer, AtomicInteger>(); /** * Number of streams used. */ private AtomicInteger usedStreams = new AtomicInteger(0); /** * AMF version, AMF0 by default. */ private volatile Encoding encoding = Encoding.AMF0; /** * Remembered stream buffer durations. */ private ConcurrentMap<Integer, Integer> streamBuffers = new ConcurrentHashMap<Integer, Integer>(); /** * Service that is waiting for handshake. */ private ISchedulingService waitForHandshakeService; /** * Name of job that is waiting for a valid handshake. */ private String waitForHandshakeJob; /** * Maximum time in milliseconds to wait for a valid handshake. */ private volatile int maxHandshakeTimeout = 5000; protected volatile int clientId; /** * protocol state */ protected volatile RTMP state; private ISchedulingService schedulingService; /** * Creates anonymous RTMP connection without scope. * * @param type Connection type */ public RTMPConnection(String type) { // We start with an anonymous connection without a scope. // These parameters will be set during the call of "connect" later. // super(null, ""); temp fix to get things to compile. super(type, null, null, 0, null, null, null); } public int getId() { return clientId; } public void setId(int clientId) { this.clientId = clientId; } public RTMP getState() { getReadLock().lock(); try { return state; } finally { getReadLock().unlock(); } } public byte getStateCode() { getReadLock().lock(); try { return state.getState(); } finally { getReadLock().unlock(); } } public void setStateCode(byte code) { getWriteLock().lock(); try { state.setState(code); } finally { getWriteLock().unlock(); } } public void setState(RTMP state) { getWriteLock().lock(); try { log.debug("Set state: {}", state); this.state = state; } finally { getWriteLock().unlock(); } } @Override public boolean connect(IScope newScope, Object[] params) { try { boolean success = super.connect(newScope, params); if (success) { getWriteLock().lock(); try { // XXX Bandwidth control service should not be bound to // a specific scope because it's designed to control // the bandwidth system-wide. if (getScope() != null && getScope().getContext() != null) { IBWControlService bwController = (IBWControlService) getScope() .getContext().getBean(IBWControlService.KEY); bwContext = bwController.registerBWControllable(this); } unscheduleWaitForHandshakeJob(); } finally { getWriteLock().unlock(); } } return success; } catch (ClientRejectedException e) { log.warn("client rejected, unscheduling waitForHandshakeJob", e); unscheduleWaitForHandshakeJob(); throw e; } } private void unscheduleWaitForHandshakeJob() { getWriteLock().lock(); try { if (waitForHandshakeJob != null) { waitForHandshakeService.removeScheduledJob(waitForHandshakeJob); waitForHandshakeJob = null; waitForHandshakeService = null; log.debug("Removed waitForHandshakeJob for: {}", getId()); } } finally { getWriteLock().unlock(); } } /** * Initialize connection. * * @param host Connection host * @param path Connection path * @param sessionId Connection session id * @param params Params passed from client */ public void setup(String host, String path, String sessionId, Map<String, Object> params) { this.host = host; this.path = path; this.sessionId = sessionId; this.params = params; if (params.get("objectEncoding") == Integer.valueOf(3)) { log.info("Setting object encoding to AMF3"); encoding = Encoding.AMF3; } } /** * Return AMF protocol encoding used by this connection. * * @return AMF encoding used by connection */ public Encoding getEncoding() { return encoding; } /** * Getter for next available channel id. * * @return Next available channel id */ public int getNextAvailableChannelId() { int result = 4; while (isChannelUsed(result)) { result++; } return result; } /** * Checks whether channel is used. * * @param channelId Channel id * @return <code>true</code> if channel is in use, <code>false</code> * otherwise */ public boolean isChannelUsed(int channelId) { return channels.get(channelId) != null; } /** * Return channel by id. * * @param channelId Channel id * @return Channel by id */ public Channel getChannel(int channelId) { final Channel value = new Channel(this, channelId); Channel result = channels.putIfAbsent(channelId, value); if (result == null) { result = value; } return result; } /** * Closes channel. * * @param channelId Channel id */ public void closeChannel(int channelId) { channels.remove(channelId); } /** * Getter for client streams. * * @return Client streams as array */ protected Collection<IClientStream> getStreams() { return streams.values(); } /** {@inheritDoc} */ public int reserveStreamId() { int result = -1; getWriteLock().lock(); try { for (int i = 0; true; i++) { if (!reservedStreams.get(i)) { reservedStreams.set(i); result = i; break; } } } finally { getWriteLock().unlock(); } return result + 1; } /** * Creates output stream object from stream id. Output stream consists of * audio, data and video channels. * * @see org.red5.server.stream.OutputStream * * @param streamId Stream id * @return Output stream object */ public OutputStream createOutputStream(int streamId) { int channelId = (4 + ((streamId - 1) * 5)); final Channel data = getChannel(channelId++); final Channel video = getChannel(channelId++); final Channel audio = getChannel(channelId++); // final Channel unknown = getChannel(channelId++); // final Channel ctrl = getChannel(channelId++); return new OutputStream(video, audio, data); } /** * Getter for video codec factory. * * @return Video codec factory */ public VideoCodecFactory getVideoCodecFactory() { final IContext context = scope.getContext(); ApplicationContext appCtx = context.getApplicationContext(); if (!appCtx.containsBean(VIDEO_CODEC_FACTORY)) { return null; } return (VideoCodecFactory) appCtx.getBean(VIDEO_CODEC_FACTORY); } /** {@inheritDoc} */ public IClientBroadcastStream newBroadcastStream(int streamId) { getReadLock().lock(); try{ if (!reservedStreams.get(streamId - 1)) { // StreamId has not been reserved before return null; } } finally { getReadLock().unlock(); } if (streams.get(streamId - 1) != null) { // Another stream already exists with this id return null; } /** * Picking up the ClientBroadcastStream defined as a spring * prototype in red5-common.xml */ ClientBroadcastStream cbs = (ClientBroadcastStream) scope .getContext().getBean("clientBroadcastStream"); Integer buffer = streamBuffers.get(streamId - 1); if (buffer != null) cbs.setClientBufferDuration(buffer); cbs.setStreamId(streamId); cbs.setConnection(this); cbs.setName(createStreamName()); cbs.setScope(this.getScope()); registerStream(cbs); usedStreams.incrementAndGet(); return cbs; } /** * {@inheritDoc} To be implemented. */ public ISingleItemSubscriberStream newSingleItemSubscriberStream( int streamId) { // TODO implement it return null; } /** {@inheritDoc} */ public IPlaylistSubscriberStream newPlaylistSubscriberStream(int streamId) { getReadLock().lock(); try { if (!reservedStreams.get(streamId - 1)) { // StreamId has not been reserved before return null; } } finally { getReadLock().unlock(); } if (streams.get(streamId - 1) != null) { // Another stream already exists with this id return null; } /** * Picking up the PlaylistSubscriberStream defined as a Spring * prototype in red5-common.xml */ PlaylistSubscriberStream pss = (PlaylistSubscriberStream) scope .getContext().getBean("playlistSubscriberStream"); Integer buffer = streamBuffers.get(streamId - 1); if (buffer != null) pss.setClientBufferDuration(buffer); pss.setName(createStreamName()); pss.setConnection(this); pss.setScope(this.getScope()); pss.setStreamId(streamId); registerStream(pss); usedStreams.incrementAndGet(); return pss; } public void addClientStream(IClientStream stream) { int streamId = stream.getStreamId(); getWriteLock().lock(); try { if (reservedStreams.get(streamId - 1)) { return; } reservedStreams.set(streamId - 1); } finally { getWriteLock().unlock(); } streams.put(streamId - 1, stream); usedStreams.incrementAndGet(); } public void removeClientStream(int streamId) { unreserveStreamId(streamId); } /** * Getter for used stream count. * * @return Value for property 'usedStreamCount'. */ protected int getUsedStreamCount() { return usedStreams.get(); } /** {@inheritDoc} */ public IClientStream getStreamById(int id) { if (id <= 0) { return null; } return streams.get(id - 1); } /** * Return stream id for given channel id. * * @param channelId Channel id * @return ID of stream that channel belongs to */ public int getStreamIdForChannel(int channelId) { if (channelId < 4) { return 0; } return ((channelId - 4) / 5) + 1; } /** * Return stream by given channel id. * * @param channelId Channel id * @return Stream that channel belongs to */ public IClientStream getStreamByChannelId(int channelId) { if (channelId < 4) { return null; } return streams.get(getStreamIdForChannel(channelId) - 1); } /** * Store a stream in the connection. * * @param stream */ private void registerStream(IClientStream stream) { streams.put(stream.getStreamId() - 1, stream); } /** * Remove a stream from the connection. * * @param stream */ @SuppressWarnings("unused") private void unregisterStream(IClientStream stream) { streams.remove(stream.getStreamId()); } /** {@inheritDoc} */ @Override public void close() { getWriteLock().lock(); try { if (keepAliveJobName != null) { schedulingService.removeScheduledJob(keepAliveJobName); keepAliveJobName = null; } } finally { getWriteLock().unlock(); } Red5.setConnectionLocal(this); IStreamService streamService = (IStreamService) getScopeService(scope, IStreamService.class, StreamService.class); if (streamService != null) { for (Map.Entry<Integer, IClientStream> entry : streams .entrySet()) { IClientStream stream = entry.getValue(); if (stream != null) { log.debug("Closing stream: {}", stream.getStreamId()); streamService.deleteStream(this, stream.getStreamId()); usedStreams.decrementAndGet(); } } streams.clear(); } channels.clear(); getWriteLock().lock(); try { if (bwContext != null && getScope() != null && getScope().getContext() != null) { IBWControlService bwController = (IBWControlService) getScope() .getContext().getBean(IBWControlService.KEY); bwController.unregisterBWControllable(bwContext); bwContext = null; } } finally { getWriteLock().unlock(); } super.close(); } /** {@inheritDoc} */ public void unreserveStreamId(int streamId) { getWriteLock().lock(); try { deleteStreamById(streamId); if (streamId > 0) { reservedStreams.clear(streamId - 1); } } finally { getWriteLock().unlock(); } } /** {@inheritDoc} */ public void deleteStreamById(int streamId) { if (streamId > 0) { if (streams.get(streamId - 1) != null) { pendingVideos.remove(streamId); usedStreams.decrementAndGet(); streams.remove(streamId - 1); streamBuffers.remove(streamId - 1); } } } /** * Handler for ping event. * * @param ping Ping event context */ public void ping(Ping ping) { getChannel(2).write(ping); } /** * Write raw byte buffer. * * @param out Byte buffer */ public abstract void rawWrite(ByteBuffer out); /** * Write packet. * * @param out Packet */ public abstract void write(Packet out); /** * Update number of bytes to read next value. */ protected void updateBytesRead() { getWriteLock().lock(); try { long bytesRead = getReadBytes(); if (bytesRead >= nextBytesRead) { BytesRead sbr = new BytesRead((int) bytesRead); getChannel(2).write(sbr); // @todo: what do we want to see printed here? // log.info(sbr); nextBytesRead += bytesReadInterval; } } finally { getWriteLock().unlock(); } } /** * Read number of received bytes. * * @param bytes Number of bytes */ public void receivedBytesRead(int bytes) { getWriteLock().lock(); try { log .debug( "Client received {} bytes, written {} bytes, {} messages pending", new Object[] { bytes, getWrittenBytes(), getPendingMessages() }); clientBytesRead = bytes; } finally { getWriteLock().unlock(); } } /** * Get number of bytes the client reported to have received. * * @return Number of bytes */ public long getClientBytesRead() { getReadLock().lock(); try { return clientBytesRead; } finally { getReadLock().unlock(); } } /** {@inheritDoc} */ public void invoke(IServiceCall call) { invoke(call, 3); } /** * Generate next invoke id. * * @return Next invoke id for RPC */ public int getInvokeId() { return invokeId.incrementAndGet(); } /** * Register pending call (remote function call that is yet to finish). * * @param invokeId Deferred operation id * @param call Call service */ public void registerPendingCall(int invokeId, IPendingServiceCall call) { pendingCalls.put(invokeId, call); } /** {@inheritDoc} */ public void invoke(IServiceCall call, int channel) { // We need to use Invoke for all calls to the client Invoke invoke = new Invoke(); invoke.setCall(call); invoke.setInvokeId(getInvokeId()); if (call instanceof IPendingServiceCall) { registerPendingCall(invoke.getInvokeId(), (IPendingServiceCall) call); } getChannel(channel).write(invoke); } /** {@inheritDoc} */ public void invoke(String method) { invoke(method, null, null); } /** {@inheritDoc} */ public void invoke(String method, Object[] params) { invoke(method, params, null); } /** {@inheritDoc} */ public void invoke(String method, IPendingServiceCallback callback) { invoke(method, null, callback); } /** {@inheritDoc} */ public void invoke(String method, Object[] params, IPendingServiceCallback callback) { IPendingServiceCall call = new PendingCall(method, params); if (callback != null) { call.registerCallback(callback); } invoke(call); } /** {@inheritDoc} */ public void notify(IServiceCall call) { notify(call, (byte) 3); } /** {@inheritDoc} */ public void notify(IServiceCall call, int channel) { Notify notify = new Notify(); notify.setCall(call); getChannel(channel).write(notify); } /** {@inheritDoc} */ public void notify(String method) { notify(method, null); } /** {@inheritDoc} */ public void notify(String method, Object[] params) { IServiceCall call = new Call(method, params); notify(call); } /** {@inheritDoc} */ public IBandwidthConfigure getBandwidthConfigure() { getReadLock().lock(); try { return bwConfig; } finally { getReadLock().unlock(); } } /** {@inheritDoc} */ public IBWControllable getParentBWControllable() { // TODO return the client object return null; } /** {@inheritDoc} */ public void setBandwidthConfigure(IBandwidthConfigure config) { if (!(config instanceof IConnectionBWConfig)) { return; } IConnectionBWConfig connectionBWConfig = (IConnectionBWConfig) config; // Notify client about new bandwidth settings (in bytes per second) if (connectionBWConfig.getDownstreamBandwidth() > 0) { ServerBW serverBW = new ServerBW((int) connectionBWConfig .getDownstreamBandwidth() / 8); getChannel(2).write(serverBW); } if (connectionBWConfig.getUpstreamBandwidth() > 0) { ClientBW clientBW = new ClientBW((int) connectionBWConfig .getUpstreamBandwidth() / 8, (byte) 0); getChannel(2).write(clientBW); } getWriteLock().lock(); try { this.bwConfig = connectionBWConfig; if (connectionBWConfig.getUpstreamBandwidth() > 0) { // Update generation of BytesRead messages // TODO: what are the correct values here? bytesReadInterval = (int) connectionBWConfig.getUpstreamBandwidth() / 8; nextBytesRead = (int) getWrittenBytes(); } if (bwContext != null) { IBWControlService bwController = (IBWControlService) getScope() .getContext().getBean(IBWControlService.KEY); bwController.updateBWConfigure(bwContext); } } finally { getWriteLock().unlock(); } } /** {@inheritDoc} */ @Override public long getReadBytes() { // TODO Auto-generated method stub return 0; } /** {@inheritDoc} */ @Override public long getWrittenBytes() { // TODO Auto-generated method stub return 0; } /** * Get pending call service by id. * * @param invokeId * Pending call service id * @return Pending call service object */ protected IPendingServiceCall getPendingCall(int invokeId) { return pendingCalls.get(invokeId); } /** * Retrieve pending call service by id. The call will be removed afterwards. * * @param invokeId * Pending call service id * @return Pending call service object */ protected IPendingServiceCall retrievePendingCall(int invokeId) { return pendingCalls.remove(invokeId); } /** * Generates new stream name. * * @return New stream name */ protected String createStreamName() { return UUID.randomUUID().toString(); } /** * Mark message as being written. * * @param message * Message to mark */ protected void writingMessage(Packet message) { if (message.getMessage() instanceof VideoData) { int streamId = message.getHeader().getStreamId(); final AtomicInteger value = new AtomicInteger(); AtomicInteger old = pendingVideos.putIfAbsent(streamId, value); if (old == null) { old = value; } old.incrementAndGet(); } } /** * Increases number of read messages by one. Updates number of bytes read. */ protected void messageReceived() { readMessages.incrementAndGet(); // Trigger generation of BytesRead messages updateBytesRead(); } /** * Mark message as sent. * * @param message * Message to mark */ protected void messageSent(Packet message) { if (message.getMessage() instanceof VideoData) { int streamId = message.getHeader().getStreamId(); AtomicInteger pending = pendingVideos.get(streamId); if (pending != null) { pending.decrementAndGet(); } } writtenMessages.incrementAndGet(); } /** * Increases number of dropped messages. */ protected void messageDropped() { droppedMessages.incrementAndGet(); } /** {@inheritDoc} */ @Override public long getPendingVideoMessages(int streamId) { AtomicInteger count = pendingVideos.get(streamId); long result = (count != null ? count.intValue() - getUsedStreamCount() : 0); return (result > 0 ? result : 0); } /** {@inheritDoc} */ public void ping() { long newPingTime = System.currentTimeMillis(); log.debug("Pinging client with id {} at {}, last ping sent at {}", new Object[]{ getId(), newPingTime, lastPingSent.get() }); if (lastPingSent.get() == 0) { lastPongReceived.set(newPingTime); } Ping pingRequest = new Ping(); pingRequest.setValue1((short) Ping.PING_CLIENT); lastPingSent.set(newPingTime); int now = (int) (newPingTime & 0xffffffff); pingRequest.setValue2(now); pingRequest.setValue3(Ping.UNDEFINED); ping(pingRequest); } /** * Marks that pingback was received. * * @param pong * Ping object */ protected void pingReceived(Ping pong) { long now = System.currentTimeMillis(); long previousReceived = lastPongReceived.get(); log.debug("Pong from client id {} at {} with value {}, previous received at {}", new Object[]{ getId(), now , pong.getValue2(), previousReceived }); if (lastPongReceived.compareAndSet(previousReceived, now)) { lastPingTime.set(((int) (previousReceived & 0xffffffff)) - pong.getValue2()); } } /** {@inheritDoc} */ public int getLastPingTime() { return lastPingTime.get(); } /** * Setter for ping interval. * * @param pingInterval Interval in ms to ping clients. Set to <code>0</code> to * disable ghost detection code. */ public void setPingInterval(int pingInterval) { this.pingInterval = pingInterval; } /** * Setter for maximum inactivity. * * @param maxInactivity Maximum time in ms after which a client is disconnected in * case of inactivity. */ public void setMaxInactivity(int maxInactivity) { this.maxInactivity = maxInactivity; } /** * Starts measurement. */ public void startRoundTripMeasurement() { if (pingInterval <= 0) { // Ghost detection code disabled return; } getWriteLock().lock(); try { if (keepAliveJobName == null) { // log.debug("Scope null = {}", (scope == null)); // log.debug("getScope null = {}", (getScope() == null)); // log.debug("Context null = {}", (scope.getContext() == null)); // ISchedulingService schedulingService = (ISchedulingService) // scope.getContext().getBean(ISchedulingService.BEAN_NAME); keepAliveJobName = schedulingService.addScheduledJob(pingInterval, new KeepAliveJob()); } log.debug("Keep alive job name {} for client id {}", keepAliveJobName, getId()); } finally { getWriteLock().unlock(); } } /** * Sets the scheduling service. * * @param schedulingService scheduling service */ public void setSchedulingService(ISchedulingService schedulingService) { getWriteLock().lock(); try { this.schedulingService = schedulingService; } finally { getWriteLock().unlock(); } } /** * Inactive state event handler. */ protected abstract void onInactive(); /** {@inheritDoc} */ @Override public String toString() { // http://java.sun.com/j2se/1.5.0/docs/api/java/lang/String.html#format( // java.lang.String,%20java.lang.Object...) Object[] args = new Object[] { getClass().getSimpleName(), getRemoteAddress(), getRemotePort(), getHost(), getReadBytes(), getWrittenBytes() }; return String.format( "%1$s from %2$s : %3$s to %4$s (in: %5$s out %6$s )", args); } /** * Registers deferred result. * * @param result Result to register */ protected void registerDeferredResult(DeferredResult result) { getWriteLock().lock(); try { deferredResults.add(result); } finally { getWriteLock().unlock(); } } /** * Unregister deferred result * * @param result * Result to unregister */ protected void unregisterDeferredResult(DeferredResult result) { getWriteLock().lock(); try { deferredResults.remove(result); } finally { getWriteLock().unlock(); } } protected void rememberStreamBufferDuration(int streamId, int bufferDuration) { streamBuffers.put(streamId - 1, bufferDuration); } /** * Set maximum time to wait for valid handshake in milliseconds. * * @param maxHandshakeTimeout Maximum time in milliseconds */ public void setMaxHandshakeTimeout(int maxHandshakeTimeout) { this.maxHandshakeTimeout = maxHandshakeTimeout; } /** * Start waiting for a valid handshake. * * @param service * The scheduling service to use */ protected void startWaitForHandshake(ISchedulingService service) { getWriteLock().lock(); try { waitForHandshakeService = service; waitForHandshakeJob = service.addScheduledOnceJob(maxHandshakeTimeout, new WaitForHandshakeJob()); } finally { getWriteLock().unlock(); } } /** * Quartz job that keeps connection alive and disconnects if client is dead. */ private class KeepAliveJob implements IScheduledJob { private final AtomicLong lastBytesRead = new AtomicLong(0); private volatile long lastBytesReadTime = 0; /** {@inheritDoc} */ public void execute(ISchedulingService service) { long thisRead = getReadBytes(); long previousReadBytes = lastBytesRead.get(); if (thisRead > previousReadBytes) { // Client sent data since last check and thus is not dead. No // need to ping. if (lastBytesRead.compareAndSet(previousReadBytes, thisRead)) lastBytesReadTime = System.currentTimeMillis(); return; } // Client didn't send response to ping command // and didn't sent data for too long, disconnect if (lastPongReceived.get() > 0 && (lastPingSent.get() - lastPongReceived.get() > maxInactivity) && !(System.currentTimeMillis() - lastBytesReadTime < maxInactivity)) { getWriteLock().lock(); try { log.debug("Keep alive job name {}", keepAliveJobName); if (log.isDebugEnabled()) { log.debug("Scheduled job list"); for (String jobName : service.getScheduledJobNames()) { log.debug("Job: {}", jobName); } } service.removeScheduledJob(keepAliveJobName); keepAliveJobName = null; } finally { getWriteLock().unlock(); } log.warn("Closing {}, with id {}, due to too much inactivity ({}ms), " + "last ping sent {}ms ago", new Object[] { RTMPConnection.this, getId(), (lastPingSent.get() - lastPongReceived.get()), (System.currentTimeMillis() - lastPingSent.get())}); onInactive(); return; } // Send ping command to client to trigger sending of data. ping(); } } /** * Quartz job that waits for a valid handshake and disconnects the client if * none is received. */ private class WaitForHandshakeJob implements IScheduledJob { /** {@inheritDoc} */ public void execute(ISchedulingService service) { getWriteLock().lock(); try { waitForHandshakeJob = null; waitForHandshakeService = null; // Client didn't send a valid handshake, disconnect log.warn("Closing {}, with id {} due to long handshake", RTMPConnection.this, getId()); } finally { getWriteLock().unlock(); } onInactive(); } } }