/* * RED5 Open Source Flash Server - http://code.google.com/p/red5/ * * Copyright 2006-2012 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.server.net.rtmp; import java.util.HashSet; import java.util.Set; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.IoSession; import org.red5.server.api.event.IEventDispatcher; 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.stream.IClientStream; import org.red5.server.net.protocol.ProtocolState; import org.red5.server.net.rtmp.codec.RTMP; import org.red5.server.net.rtmp.event.BytesRead; import org.red5.server.net.rtmp.event.ChunkSize; import org.red5.server.net.rtmp.event.IRTMPEvent; 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.Unknown; import org.red5.server.net.rtmp.message.Constants; import org.red5.server.net.rtmp.message.Header; import org.red5.server.net.rtmp.message.Packet; import org.red5.server.net.rtmp.message.StreamAction; import org.red5.server.net.rtmp.status.StatusCodes; import org.red5.server.so.SharedObjectMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; /** * Base class for all RTMP handlers. * * @author The Red5 Project (red5@osflash.org) */ public abstract class BaseRTMPHandler implements IRTMPHandler, Constants, StatusCodes, ApplicationContextAware { /** * Logger */ private static Logger log = LoggerFactory.getLogger(BaseRTMPHandler.class); /** * Application context */ private ApplicationContext appCtx; // XXX: HACK HACK HACK to support stream ids private static ThreadLocal<Integer> streamLocal = new ThreadLocal<Integer>(); /** * Getter for stream ID. * * @return Stream ID */ // XXX: HACK HACK HACK to support stream ids public static int getStreamId() { return streamLocal.get().intValue(); } /** * Setter for stream Id. * * @param id * Stream id */ private static void setStreamId(int id) { streamLocal.set(id); } /** {@inheritDoc} */ public void setApplicationContext(ApplicationContext appCtx) throws BeansException { this.appCtx = appCtx; } /** {@inheritDoc} */ public void connectionOpened(RTMPConnection conn, RTMP state) { log.trace("connectionOpened - conn: {} state: {}", conn, state); if (appCtx != null) { ISchedulingService service = (ISchedulingService) appCtx.getBean(ISchedulingService.BEAN_NAME); conn.startWaitForHandshake(service); } } /** {@inheritDoc} */ public void messageReceived(Object in, IoSession session) throws Exception { RTMPConnection conn = (RTMPConnection) session.getAttribute(RTMPConnection.RTMP_CONNECTION_KEY); IRTMPEvent message = null; try { final Packet packet = (Packet) in; message = packet.getMessage(); final Header header = packet.getHeader(); final Channel channel = conn.getChannel(header.getChannelId()); final IClientStream stream = conn.getStreamById(header.getStreamId()); log.trace("Message received, header: {}", header); // XXX: HACK HACK HACK to support stream ids BaseRTMPHandler.setStreamId(header.getStreamId()); // increase number of received messages conn.messageReceived(); // set the source of the message message.setSource(conn); // process based on data type switch (header.getDataType()) { case TYPE_CHUNK_SIZE: onChunkSize(conn, channel, header, (ChunkSize) message); break; case TYPE_INVOKE: case TYPE_FLEX_MESSAGE: onInvoke(conn, channel, header, (Invoke) message, (RTMP) session.getAttribute(ProtocolState.SESSION_KEY)); IPendingServiceCall call = ((Invoke) message).getCall(); if (message.getHeader().getStreamId() != 0 && call.getServiceName() == null && StreamAction.PUBLISH.equals(call.getServiceMethodName())) { if (stream != null) { // Only dispatch if stream really was created ((IEventDispatcher) stream).dispatchEvent(message); } } break; case TYPE_NOTIFY: // just like invoke, but does not return if (((Notify) message).getData() != null && stream != null) { // Stream metadata ((IEventDispatcher) stream).dispatchEvent(message); } else { onInvoke(conn, channel, header, (Notify) message, (RTMP) session.getAttribute(ProtocolState.SESSION_KEY)); } break; case TYPE_FLEX_STREAM_SEND: if (stream != null) { ((IEventDispatcher) stream).dispatchEvent(message); } break; case TYPE_PING: onPing(conn, channel, header, (Ping) message); break; case TYPE_BYTES_READ: onStreamBytesRead(conn, channel, header, (BytesRead) message); break; case TYPE_AGGREGATE: log.debug("Aggregate type data - header timer: {} size: {}", header.getTimer(), header.getSize()); case TYPE_AUDIO_DATA: case TYPE_VIDEO_DATA: //mark the event as from a live source //log.trace("Marking message as originating from a Live source"); message.setSourceType(Constants.SOURCE_TYPE_LIVE); // NOTE: If we respond to "publish" with "NetStream.Publish.BadName", // the client sends a few stream packets before stopping. We need to ignore them. if (stream != null) { ((IEventDispatcher) stream).dispatchEvent(message); } break; case TYPE_FLEX_SHARED_OBJECT: case TYPE_SHARED_OBJECT: onSharedObject(conn, channel, header, (SharedObjectMessage) message); break; case Constants.TYPE_CLIENT_BANDWIDTH: //onBWDone log.debug("Client bandwidth: {}", message); break; case Constants.TYPE_SERVER_BANDWIDTH: log.debug("Server bandwidth: {}", message); break; default: log.debug("Unknown type: {}", header.getDataType()); } if (message instanceof Unknown) { log.info("Message type unknown: {}", message); } } catch (RuntimeException e) { log.error("Exception", e); } // XXX this may be causing 'missing' data if previous methods are not making copies // before buffering etc.. if (message != null) { message.release(); } } /** {@inheritDoc} */ public void messageSent(RTMPConnection conn, Object message) { log.trace("Message sent"); if (message instanceof IoBuffer) { return; } // increase number of sent messages conn.messageSent((Packet) message); } /** {@inheritDoc} */ public void connectionClosed(RTMPConnection conn, RTMP state) { state.setState(RTMP.STATE_DISCONNECTED); conn.close(); } /** * Return hostname for URL. * * @param url * URL * @return Hostname from that URL */ protected String getHostname(String url) { log.debug("url: {}", url); String[] parts = url.split("/"); if (parts.length == 2) { return ""; } else { String host = parts[2]; // strip out default port in case the client added the port explicitly if (host.endsWith(":1935")) { // remove default port from connection string return host.substring(0, host.length() - 5); } return host; } } /** * Handler for pending call result. Dispatches results to all pending call * handlers. * * @param conn * Connection * @param invoke * Pending call result event context */ protected void handlePendingCallResult(RTMPConnection conn, Notify invoke) { final IServiceCall call = invoke.getCall(); final IPendingServiceCall pendingCall = conn.retrievePendingCall(invoke.getInvokeId()); if (pendingCall != null) { // The client sent a response to a previously made call. Object[] args = call.getArguments(); if (args != null && args.length > 0) { // TODO: can a client return multiple results? pendingCall.setResult(args[0]); } Set<IPendingServiceCallback> callbacks = pendingCall.getCallbacks(); if (!callbacks.isEmpty()) { HashSet<IPendingServiceCallback> tmp = new HashSet<IPendingServiceCallback>(); tmp.addAll(callbacks); for (IPendingServiceCallback callback : tmp) { try { callback.resultReceived(pendingCall); } catch (Exception e) { log.error("Error while executing callback {}", callback, e); } } } } } /** * Chunk size change event handler. Abstract, to be implemented in * subclasses. * * @param conn * Connection * @param channel * Channel * @param source * Header * @param chunkSize * New chunk size */ protected abstract void onChunkSize(RTMPConnection conn, Channel channel, Header source, ChunkSize chunkSize); /** * Invocation event handler. * * @param conn * Connection * @param channel * Channel * @param source * Header * @param invoke * Invocation event context * @param rtmp * RTMP connection state */ protected abstract void onInvoke(RTMPConnection conn, Channel channel, Header source, Notify invoke, RTMP rtmp); /** * Ping event handler. * * @param conn * Connection * @param channel * Channel * @param source * Header * @param ping * Ping event context */ protected abstract void onPing(RTMPConnection conn, Channel channel, Header source, Ping ping); /** * Stream bytes read event handler. * * @param conn * Connection * @param channel * Channel * @param source * Header * @param streamBytesRead * Bytes read event context */ protected void onStreamBytesRead(RTMPConnection conn, Channel channel, Header source, BytesRead streamBytesRead) { conn.receivedBytesRead(streamBytesRead.getBytesRead()); } /** * Shared object event handler. * * @param conn * Connection * @param channel * Channel * @param source * Header * @param object * Shared object event context */ protected abstract void onSharedObject(RTMPConnection conn, Channel channel, Header source, SharedObjectMessage object); }