Package org.red5.server.net.rtmp.codec

Source Code of org.red5.server.net.rtmp.codec.RTMPProtocolDecoder

/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2014 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.codec;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.mina.core.buffer.IoBuffer;
import org.red5.io.amf.AMF;
import org.red5.io.amf.Output;
import org.red5.io.amf3.AMF3;
import org.red5.io.object.DataTypes;
import org.red5.io.object.Deserializer;
import org.red5.io.object.Input;
import org.red5.io.object.StreamAction;
import org.red5.io.utils.BufferUtils;
import org.red5.server.api.IConnection.Encoding;
import org.red5.server.api.Red5;
import org.red5.server.net.protocol.HandshakeFailedException;
import org.red5.server.net.protocol.ProtocolException;
import org.red5.server.net.protocol.RTMPDecodeState;
import org.red5.server.net.rtmp.RTMPConnection;
import org.red5.server.net.rtmp.RTMPUtils;
import org.red5.server.net.rtmp.event.Abort;
import org.red5.server.net.rtmp.event.Aggregate;
import org.red5.server.net.rtmp.event.AudioData;
import org.red5.server.net.rtmp.event.BytesRead;
import org.red5.server.net.rtmp.event.ChunkSize;
import org.red5.server.net.rtmp.event.ClientBW;
import org.red5.server.net.rtmp.event.FlexMessage;
import org.red5.server.net.rtmp.event.FlexStreamSend;
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.SWFResponse;
import org.red5.server.net.rtmp.event.ServerBW;
import org.red5.server.net.rtmp.event.SetBuffer;
import org.red5.server.net.rtmp.event.Unknown;
import org.red5.server.net.rtmp.event.VideoData;
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.SharedObjectTypeMapping;
import org.red5.server.service.Call;
import org.red5.server.service.PendingCall;
import org.red5.server.so.FlexSharedObjectMessage;
import org.red5.server.so.ISharedObjectEvent;
import org.red5.server.so.ISharedObjectMessage;
import org.red5.server.so.SharedObjectMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* RTMP protocol decoder.
*/
public class RTMPProtocolDecoder implements Constants, IEventDecoder {

  protected static Logger log = LoggerFactory.getLogger(RTMPProtocolDecoder.class);

  /** Constructs a new RTMPProtocolDecoder. */
  public RTMPProtocolDecoder() {
  }

  /**
   * Decode all available objects in buffer.
   *
   * @param conn RTMP connection
   * @param buffer IoBuffer of data to be decoded
   * @return a list of decoded objects, may be empty if nothing could be decoded
   */
  public List<Object> decodeBuffer(RTMPConnection conn, IoBuffer buffer) {
    // decoded results
    List<Object> result = null;
    if (conn != null) {
      log.trace("Decoding for connection - session id: {}", conn.getSessionId());
      try {
        // instance list to hold results
        result = new LinkedList<Object>();
        // get the local decode state
        RTMPDecodeState state = conn.getDecoderState();
        log.trace("{}", state);
        if (!conn.getSessionId().equals(state.getSessionId())) {
          log.warn("Session decode overlap: {} != {}", conn.getSessionId(), state.getSessionId());
        }
        while (buffer.hasRemaining()) {
          final int remaining = buffer.remaining();
          if (state.canStartDecoding(remaining)) {
            log.trace("Can start decoding");
            state.startDecoding();
          } else {
            log.trace("Cannot start decoding");
            break;
          }
          final Object decodedObject = decode(conn, state, buffer);
          if (state.hasDecodedObject()) {
            log.trace("Has decoded object");
            if (decodedObject != null) {
              result.add(decodedObject);
            }
          } else if (state.canContinueDecoding()) {
            log.trace("Can continue decoding");
            continue;
          } else {
            log.trace("Cannot continue decoding");
            break;
          }
        }
      } catch (HandshakeFailedException hfe) {
        // close the connection
        log.warn("Closing connection because decoding failed during handshake: {}", conn, hfe);
        // clear buffer if something is wrong in protocol decoding
        buffer.clear();
        // close connection because we can't parse data from it
        conn.close();
      } catch (Exception ex) {
        // catch any non-handshake exception in the decoding
        // close the connection
        log.warn("Closing connection because decoding failed: {}", conn, ex);
        // clear the buffer to eliminate memory leaks when we can't parse protocol
        buffer.clear();
        // close connection because we can't parse data from it
        conn.close();
      } finally {
        buffer.compact();
      }
    } else {
      log.error("Decoding buffer failed, no current connection!?");
    }
    return result;
  }

  /**
   * Decodes the buffer data.
   *
   * @param conn RTMP connection
   * @param state Stores state for the protocol, ProtocolState is just a marker interface
   * @param in IoBuffer of data to be decoded
   * @return one of three possible values:
   *      1. null : the object could not be decoded, or some data was skipped, just continue
   *       2. ProtocolState : the decoder was unable to decode the whole object, refer to the protocol state
   *       3. Object : something was decoded, continue
   * @throws Exception on error
   */
  public Object decode(RTMPConnection conn, RTMPDecodeState state, IoBuffer in) throws ProtocolException {
    if (log.isTraceEnabled()) {
      log.trace("Decoding for {}", conn.getSessionId());
    }
    try {
      final byte connectionState = conn.getStateCode();
      switch (connectionState) {
        case RTMP.STATE_CONNECTED:
          return decodePacket(conn, state, in);
        case RTMP.STATE_CONNECT:
          return decodeHandshakeS1(conn, state, in);
        case RTMP.STATE_HANDSHAKE:
          return decodeHandshakeS2(conn, state, in);
        case RTMP.STATE_ERROR:
        case RTMP.STATE_DISCONNECTING:
        case RTMP.STATE_DISCONNECTED:
          // throw away any remaining input data:
          in.position(in.limit());
          return null;
        default:
          throw new IllegalStateException("Invalid RTMP state: " + connectionState);
      }
    } catch (ProtocolException pe) {
      // raise to caller unmodified
      throw pe;
    } catch (RuntimeException e) {
      throw new ProtocolException("Error during decoding", e);
    } finally {
      if (log.isTraceEnabled()) {
        log.trace("Decoding finished for {}", conn.getSessionId());
      }
    }
  }

  /**
   * Decodes handshake message for step 1, RTMP.STATE_CONNECT.
   *
   * @param conn Connection
   * @param state protocol decode state
   * @param in IoBuffer
   * @return IoBuffer
   */
  public IoBuffer decodeHandshakeS1(RTMPConnection conn, RTMPDecodeState state, IoBuffer in) {
    // first step: client has connected and handshaking is not complete
    if (log.isDebugEnabled()) {
      log.debug("decodeHandshake - state: {} buffer: {}", state, in);
    }
    // number of byte remaining in the buffer
    int remaining = in.remaining();
    if (remaining < HANDSHAKE_SIZE + 1) {
      log.debug("Handshake init too small, buffering. remaining: {}", remaining);
      state.bufferDecoding(HANDSHAKE_SIZE + 1);
    } else {
      final IoBuffer hs = IoBuffer.allocate(HANDSHAKE_SIZE);
      in.get(); // skip the header byte
      BufferUtils.put(hs, in, HANDSHAKE_SIZE);
      hs.flip();
      conn.setStateCode(RTMP.STATE_HANDSHAKE);
      return hs;
    }
    return null;
  }

  /**
   * Decodes handshake message for step 2, RTMP.STATE_HANDSHAKE.
   *
   * @param conn Connection
   * @param state protocol decode state
   * @param in IoBuffer
   * @return IoBuffer
   */
  public IoBuffer decodeHandshakeS2(RTMPConnection conn, RTMPDecodeState state, IoBuffer in) {
    // second step: all handshake data received, collecting handshake reply data
    log.debug("decodeHandshake - state: {} buffer: {}", state, in);
    // number of byte remaining in the buffer
    int remaining = in.remaining();
    // TODO Paul: re-examine how remaining data is buffered between handshake reply and next message when using rtmpe
    // connections sending partial tcp are getting dropped
    log.debug("Handshake reply");
    // how many bytes left to get
    int required = state.getDecoderBufferAmount();
    log.trace("Handshake reply - required: {} remaining: {}", required, remaining);
    if (remaining < HANDSHAKE_SIZE) {
      log.debug("Handshake reply too small, buffering. remaining: {}", remaining);
      state.bufferDecoding(HANDSHAKE_SIZE);
    } else {
      in.skip(HANDSHAKE_SIZE);
      conn.setStateCode(RTMP.STATE_CONNECTED);
      state.continueDecoding();
    }
    return null;
  }

  /**
   * Decodes an IoBuffer into a Packet.
   *
   * @param conn Connection
   * @param state RTMP protocol state
   * @param in IoBuffer
   * @return Packet
   */
  public Packet decodePacket(RTMPConnection conn, RTMPDecodeState state, IoBuffer in) {
    if (log.isTraceEnabled()) {
      log.trace("decodePacket - state: {} buffer: {}", state, in);
    }
    final int remaining = in.remaining();
    // We need at least one byte
    if (remaining < 1) {
      state.bufferDecoding(1);
      return null;
    }
    final int position = in.position();
    byte headerByte = in.get();
    int headerValue;
    int byteCount;
    if ((headerByte & 0x3f) == 0) {
      // Two byte header
      if (remaining < 2) {
        in.position(position);
        state.bufferDecoding(2);
        return null;
      }
      headerValue = (headerByte & 0xff) << 8 | (in.get() & 0xff);
      byteCount = 2;
    } else if ((headerByte & 0x3f) == 1) {
      // Three byte header
      if (remaining < 3) {
        in.position(position);
        state.bufferDecoding(3);
        return null;
      }
      headerValue = (headerByte & 0xff) << 16 | (in.get() & 0xff) << 8 | (in.get() & 0xff);
      byteCount = 3;
    } else {
      // Single byte header
      headerValue = headerByte & 0xff;
      byteCount = 1;
    }
    final int channelId = RTMPUtils.decodeChannelId(headerValue, byteCount);
    if (channelId < 0) {
      throw new ProtocolException("Bad channel id: " + channelId);
    }
    RTMP rtmp = conn.getState();
    // Get the header size and length
    byte headerSize = RTMPUtils.decodeHeaderSize(headerValue, byteCount);
    int headerLength = RTMPUtils.getHeaderLength(headerSize);
    Header lastHeader = rtmp.getLastReadHeader(channelId);
    headerLength += byteCount - 1;
    switch (headerSize) {
      case HEADER_NEW:
      case HEADER_SAME_SOURCE:
      case HEADER_TIMER_CHANGE:
        if (remaining >= headerLength) {
          int timeValue = RTMPUtils.readUnsignedMediumInt(in);
          if (timeValue == 0xffffff) {
            headerLength += 4;
          }
        }
        break;
      case HEADER_CONTINUE:
        if (lastHeader != null && lastHeader.getExtendedTimestamp() != 0) {
          headerLength += 4;
        }
        break;
      default:
        throw new ProtocolException("Unexpected header size " + headerSize + " check for error");
    }
    if (remaining < headerLength) {
      log.trace("Header too small (hlen: {}), buffering. remaining: {}", headerLength, remaining);
      in.position(position);
      state.bufferDecoding(headerLength);
      return null;
    }
    // Move the position back to the start
    in.position(position);
    final Header header = decodeHeader(in, lastHeader);
    if (header == null) {
      throw new ProtocolException("Header is null, check for error");
    }
    rtmp.setLastReadHeader(channelId, header);
    // check to see if this is a new packets or continue decoding an existing one
    Packet packet = rtmp.getLastReadPacket(channelId);
    if (packet == null) {
      packet = new Packet(header.clone());
      rtmp.setLastReadPacket(channelId, packet);
    }
    final IoBuffer buf = packet.getData();
    final int readRemaining = header.getSize() - buf.position();
    final int chunkSize = rtmp.getReadChunkSize();
    final int readAmount = (readRemaining > chunkSize) ? chunkSize : readRemaining;
    if (in.remaining() < readAmount) {
      log.debug("Chunk too small, buffering ({},{})", in.remaining(), readAmount);
      // skip the position back to the start
      in.position(position);
      state.bufferDecoding(headerLength + readAmount);
      return null;
    }
    BufferUtils.put(buf, in, readAmount);
    if (buf.position() < header.getSize()) {
      state.continueDecoding();
      return null;
    }
    if (buf.position() > header.getSize()) {
      log.warn("Packet size expanded from {} to {} ({})", new Object[] { (header.getSize()), buf.position(), header });
    }
    buf.flip();
    try {
      final IRTMPEvent message = decodeMessage(conn, packet.getHeader(), buf);
      message.setHeader(packet.getHeader());
      // Unfortunately flash will, especially when resetting a video stream with a new key frame, sometime
      // send an earlier time stamp.  To avoid dropping it, we just give it the minimal increment since the
      // last message.  But to avoid relative time stamps being mis-computed, we don't reset the header we stored.
      final Header lastReadHeader = rtmp.getLastReadPacketHeader(channelId);
      if (lastReadHeader != null && (message instanceof AudioData || message instanceof VideoData)
          && RTMPUtils.compareTimestamps(lastReadHeader.getTimer(), packet.getHeader().getTimer()) >= 0) {
        log.trace("Non-monotonically increasing timestamps; type: {}; adjusting to {}; ts: {}; last: {}", new Object[] { header.getDataType(),
            lastReadHeader.getTimer() + 1, header.getTimer(), lastReadHeader.getTimer() });
        message.setTimestamp(lastReadHeader.getTimer() + 1);
      } else {
        message.setTimestamp(header.getTimer());
      }
      rtmp.setLastReadPacketHeader(channelId, packet.getHeader());
      packet.setMessage(message);
      if (message instanceof ChunkSize) {
        ChunkSize chunkSizeMsg = (ChunkSize) message;
        rtmp.setReadChunkSize(chunkSizeMsg.getSize());
      } else if (message instanceof Abort) {
        log.debug("Abort packet detected");
        // The client is aborting a message; reset the packet
        // because the next chunk on that stream will start a new packet.
        Abort abort = (Abort) message;
        rtmp.setLastReadPacket(abort.getChannelId(), null);
        packet = null;
      }
      // collapse the time stamps on the last packet so that it works right for chunk type 3 later
      lastHeader = rtmp.getLastReadHeader(channelId);
      lastHeader.setTimerBase(header.getTimer());
    } finally {
      rtmp.setLastReadPacket(channelId, null);
    }
    return packet;
  }

  /**
   * Decodes packet header.
   *
   * @param in Input IoBuffer
   * @param lastHeader Previous header
   * @return Decoded header
   */
  public Header decodeHeader(IoBuffer in, Header lastHeader) {
    if (log.isTraceEnabled()) {
      log.trace("decodeHeader - lastHeader: {} buffer: {}", lastHeader, in);
    }
    byte headerByte = in.get();
    int headerValue;
    int byteCount = 1;
    if ((headerByte & 0x3f) == 0) {
      // Two byte header
      headerValue = (headerByte & 0xff) << 8 | (in.get() & 0xff);
      byteCount = 2;
    } else if ((headerByte & 0x3f) == 1) {
      // Three byte header
      headerValue = (headerByte & 0xff) << 16 | (in.get() & 0xff) << 8 | (in.get() & 0xff);
      byteCount = 3;
    } else {
      // Single byte header
      headerValue = headerByte & 0xff;
      byteCount = 1;
    }
    final int channelId = RTMPUtils.decodeChannelId(headerValue, byteCount);
    final int headerSize = RTMPUtils.decodeHeaderSize(headerValue, byteCount);
    Header header = new Header();
    header.setChannelId(channelId);
    if (headerSize != HEADER_NEW && lastHeader == null) {
      log.error("Last header null not new, headerSize: {}, channelId {}", headerSize, channelId);
      //this will trigger an error status, which in turn will disconnect the "offending" flash player
      //preventing a memory leak and bringing the whole server to its knees
      return null;
    }
    int timeValue;
    switch (headerSize) {
      case HEADER_NEW:
        // an absolute time value
        timeValue = RTMPUtils.readUnsignedMediumInt(in);
        header.setSize(RTMPUtils.readUnsignedMediumInt(in));
        header.setDataType(in.get());
        header.setStreamId(RTMPUtils.readReverseInt(in));
        if (timeValue == 0xffffff) {
          timeValue = (int) (in.getUnsignedInt() & Integer.MAX_VALUE);
          header.setExtendedTimestamp(timeValue);
        }
        header.setTimerBase(timeValue);
        header.setTimerDelta(0);
        break;
      case HEADER_SAME_SOURCE:
        // a delta time value
        timeValue = RTMPUtils.readUnsignedMediumInt(in);
        header.setSize(RTMPUtils.readUnsignedMediumInt(in));
        header.setDataType(in.get());
        header.setStreamId(lastHeader.getStreamId());
        if (timeValue == 0xffffff) {
          timeValue = (int) (in.getUnsignedInt() & Integer.MAX_VALUE);
          header.setExtendedTimestamp(timeValue);
        } else if (timeValue == 0 && header.getDataType() == TYPE_AUDIO_DATA) {
          // header.setIsGarbage(true);
          log.trace("Audio with zero delta; setting to garbage; ChannelId: {}; DataType: {}; HeaderSize: {}", new Object[] { header.getChannelId(), header.getDataType(),
              headerSize });
        }
        header.setTimerBase(lastHeader.getTimerBase());
        header.setTimerDelta(timeValue);
        break;
      case HEADER_TIMER_CHANGE:
        // a delta time value
        timeValue = RTMPUtils.readUnsignedMediumInt(in);
        header.setSize(lastHeader.getSize());
        header.setDataType(lastHeader.getDataType());
        header.setStreamId(lastHeader.getStreamId());
        if (timeValue == 0xffffff) {
          timeValue = (int) (in.getUnsignedInt() & Integer.MAX_VALUE);
          header.setExtendedTimestamp(timeValue);
        } else if (timeValue == 0 && header.getDataType() == TYPE_AUDIO_DATA) {
          // header.setIsGarbage(true);
          log.trace("Audio with zero delta; setting to garbage; ChannelId: {}; DataType: {}; HeaderSize: {}", new Object[] { header.getChannelId(), header.getDataType(),
              headerSize });
        }
        header.setTimerBase(lastHeader.getTimerBase());
        header.setTimerDelta(timeValue);
        break;
      case HEADER_CONTINUE:
        header.setSize(lastHeader.getSize());
        header.setDataType(lastHeader.getDataType());
        header.setStreamId(lastHeader.getStreamId());
        header.setTimerBase(lastHeader.getTimerBase());
        header.setTimerDelta(lastHeader.getTimerDelta());
        if (lastHeader.getExtendedTimestamp() != 0) {
          timeValue = (int) (in.getUnsignedInt() & Integer.MAX_VALUE);
          header.setExtendedTimestamp(timeValue);
          log.trace("HEADER_CONTINUE with extended timestamp: {}", timeValue);
        }
        break;
      default:
        log.error("Unexpected header size: {}", headerSize);
        return null;
    }
    log.trace("CHUNK, D, {}, {}", header, headerSize);
    return header;
  }

  /**
   * Decodes RTMP message event.
   *
   * @param conn RTMP connection
   * @param header RTMP header
   * @param in Input IoBuffer
   * @return RTMP event
   */
  public IRTMPEvent decodeMessage(RTMPConnection conn, Header header, IoBuffer in) {
    IRTMPEvent message;
    byte dataType = header.getDataType();
    switch (dataType) {
      case TYPE_INVOKE:
        message = decodeInvoke(conn.getEncoding(), in);
        break;
      case TYPE_NOTIFY:
        if (header.getStreamId() == 0) {
          message = decodeNotify(conn.getEncoding(), in, header);
        } else {
          message = decodeStreamMetadata(in);
        }
        break;
      case TYPE_AUDIO_DATA:
        message = decodeAudioData(in);
        message.setSourceType(Constants.SOURCE_TYPE_LIVE);
        break;
      case TYPE_VIDEO_DATA:
        message = decodeVideoData(in);
        message.setSourceType(Constants.SOURCE_TYPE_LIVE);
        break;
      case TYPE_AGGREGATE:
        message = decodeAggregate(in);
        break;
      case TYPE_FLEX_SHARED_OBJECT: // represents an SO in an AMF3 container
        message = decodeFlexSharedObject(in);
        break;
      case TYPE_SHARED_OBJECT:
        message = decodeSharedObject(in);
        break;
      case TYPE_FLEX_MESSAGE:
        message = decodeFlexMessage(in);
        break;
      case TYPE_FLEX_STREAM_SEND:
        message = decodeFlexStreamSend(in);
        break;
      case TYPE_PING:
        message = decodePing(in);
        break;
      case TYPE_BYTES_READ:
        message = decodeBytesRead(in);
        break;
      case TYPE_CHUNK_SIZE:
        message = decodeChunkSize(in);
        break;
      case TYPE_SERVER_BANDWIDTH:
        message = decodeServerBW(in);
        break;
      case TYPE_CLIENT_BANDWIDTH:
        message = decodeClientBW(in);
        break;
      case TYPE_ABORT:
        message = decodeAbort(in);
        break;
      default:
        log.warn("Unknown object type: {}", dataType);
        message = decodeUnknown(dataType, in);
        break;
    }
    return message;
  }

  public IRTMPEvent decodeAbort(IoBuffer in) {
    return new Abort(in.getInt());
  }

  /**
   * Decodes server bandwidth.
   *
   * @param in IoBuffer
   * @return RTMP event
   */
  private IRTMPEvent decodeServerBW(IoBuffer in) {
    return new ServerBW(in.getInt());
  }

  /**
   * Decodes client bandwidth.
   *
   * @param in
   *            Byte buffer
   * @return RTMP event
   */
  private IRTMPEvent decodeClientBW(IoBuffer in) {
    return new ClientBW(in.getInt(), in.get());
  }

  /** {@inheritDoc} */
  public Unknown decodeUnknown(byte dataType, IoBuffer in) {
    return new Unknown(dataType, in);
  }

  /** {@inheritDoc} */
  public Aggregate decodeAggregate(IoBuffer in) {
    return new Aggregate(in);
  }

  /** {@inheritDoc} */
  public ChunkSize decodeChunkSize(IoBuffer in) {
    int chunkSize = in.getInt();
    log.debug("Decoded chunk size: {}", chunkSize);
    return new ChunkSize(chunkSize);
  }

  /** {@inheritDoc} */
  public ISharedObjectMessage decodeFlexSharedObject(IoBuffer in) {
    byte encoding = in.get();
    Input input;
    if (encoding == 0) {
      input = new org.red5.io.amf.Input(in);
    } else if (encoding == 3) {
      input = new org.red5.io.amf3.Input(in);
    } else {
      throw new RuntimeException("Unknown SO encoding: " + encoding);
    }
    String name = input.getString();
    // Read version of SO to modify
    int version = in.getInt();
    // Read persistence informations
    boolean persistent = in.getInt() == 2;
    // Skip unknown bytes
    in.skip(4);
    // create our shared object message
    final SharedObjectMessage so = new FlexSharedObjectMessage(null, name, version, persistent);
    doDecodeSharedObject(so, in, input);
    return so;
  }

  /** {@inheritDoc} */
  public ISharedObjectMessage decodeSharedObject(IoBuffer in) {
    final Input input = new org.red5.io.amf.Input(in);
    String name = input.getString();
    // Read version of SO to modify
    int version = in.getInt();
    // Read persistence informations
    boolean persistent = in.getInt() == 2;
    // Skip unknown bytes
    in.skip(4);
    // create our shared object message
    final SharedObjectMessage so = new SharedObjectMessage(null, name, version, persistent);
    doDecodeSharedObject(so, in, input);
    return so;
  }

  /**
   * Perform the actual decoding of the shared object contents.
   *
   * @param so
   * @param in
   * @param input
   */
  protected void doDecodeSharedObject(SharedObjectMessage so, IoBuffer in, Input input) {
    // Parse request body
    Input amf3Input = new org.red5.io.amf3.Input(in);
    while (in.hasRemaining()) {
      final ISharedObjectEvent.Type type = SharedObjectTypeMapping.toType(in.get());
      if (type == null) {
        in.skip(in.remaining());
        return;
      }
      String key = null;
      Object value = null;

      // if (log.isDebugEnabled())
      // log.debug("type: "+SharedObjectTypeMapping.toString(type));

      // SharedObjectEvent event = new SharedObjectEvent(,null,null);
      final int length = in.getInt();
      if (type == ISharedObjectEvent.Type.CLIENT_STATUS) {
        // Status code
        key = input.getString();
        // Status level
        value = input.getString();
      } else if (type == ISharedObjectEvent.Type.CLIENT_UPDATE_DATA) {
        key = null;
        // Map containing new attribute values
        final Map<String, Object> map = new HashMap<String, Object>();
        final int start = in.position();
        while (in.position() - start < length) {
          String tmp = input.getString();
          map.put(tmp, Deserializer.deserialize(input, Object.class));
        }
        value = map;
      } else if (type != ISharedObjectEvent.Type.SERVER_SEND_MESSAGE && type != ISharedObjectEvent.Type.CLIENT_SEND_MESSAGE) {
        if (length > 0) {
          key = input.getString();
          if (length > key.length() + 2) {
            // determine if the object is encoded with amf3
            byte objType = in.get();
            in.position(in.position() - 1);
            Input propertyInput;
            if (objType == AMF.TYPE_AMF3_OBJECT && !(input instanceof org.red5.io.amf3.Input)) {
              // The next parameter is encoded using AMF3
              propertyInput = amf3Input;
            } else {
              // The next parameter is encoded using AMF0
              propertyInput = input;
            }
            value = Deserializer.deserialize(propertyInput, Object.class);
          }
        }
      } else {
        final int start = in.position();
        // the "send" event seems to encode the handler name as complete AMF string including the string type byte
        key = Deserializer.deserialize(input, String.class);
        // read parameters
        final List<Object> list = new LinkedList<Object>();
        while (in.position() - start < length) {
          byte objType = in.get();
          in.position(in.position() - 1);
          // determine if the object is encoded with amf3
          Input propertyInput;
          if (objType == AMF.TYPE_AMF3_OBJECT && !(input instanceof org.red5.io.amf3.Input)) {
            // The next parameter is encoded using AMF3
            propertyInput = amf3Input;
          } else {
            // The next parameter is encoded using AMF0
            propertyInput = input;
          }
          Object tmp = Deserializer.deserialize(propertyInput, Object.class);
          list.add(tmp);
        }
        value = list;
      }
      so.addEvent(type, key, value);
    }
  }

  /** {@inheritDoc} */
  public Notify decodeNotify(Encoding encoding, IoBuffer in) {
    return decodeNotify(encoding, in, null);
  }

  /**
   * Decode a Notify.
   *
   * @param encoding
   * @param in
   * @param header
   * @return decoded notify result
   */
  public Notify decodeNotify(Encoding encoding, IoBuffer in, Header header) {
    Notify notify = new Notify();
    int start = in.position();
    Input input;
    // for response, the action string and invokeId is always encoded as AMF0 we use the first byte to decide which encoding to use
    byte tmp = in.get();
    in.position(start);
    if (encoding == Encoding.AMF3 && tmp == AMF.TYPE_AMF3_OBJECT) {
      input = new org.red5.io.amf3.Input(in);
      ((org.red5.io.amf3.Input) input).enforceAMF3();
    } else {
      input = new org.red5.io.amf.Input(in);
    }
    // get the action
    String action = Deserializer.deserialize(input, String.class);
    if (log.isTraceEnabled()) {
      log.trace("Action " + action);
    }
    //throw a runtime exception if there is no action
    if (action != null) {
      //TODO Handle NetStream.send? Where and how?
      if (header != null && header.getStreamId() != 0 && !isStreamCommand(action)) {
        // don't decode "NetStream.send" requests
        in.position(start);
        notify.setData(in.asReadOnlyBuffer());
        return notify;
      }
      if (header == null || header.getStreamId() == 0) {
        int invokeId = Deserializer.<Number> deserialize(input, Number.class).intValue();
        if (invokeId != 0) {
          throw new RuntimeException("Notify invoke / transaction id was non-zero");
        }
      }
      // now go back to the actual encoding to decode parameters
      if (encoding == Encoding.AMF3) {
        input = new org.red5.io.amf3.Input(in);
        ((org.red5.io.amf3.Input) input).enforceAMF3();
      } else {
        input = new org.red5.io.amf.Input(in);
      }
      // get / set the parameters if there any
      Object[] params = handleParameters(in, notify, input);
      // determine service information
      final int dotIndex = action.lastIndexOf('.');
      String serviceName = (dotIndex == -1) ? null : action.substring(0, dotIndex);
      // pull off the prefixes since java doesn't allow this on a method name
      if (serviceName != null && (serviceName.startsWith("@") || serviceName.startsWith("|"))) {
        serviceName = serviceName.substring(1);
      }
      String serviceMethod = (dotIndex == -1) ? action : action.substring(dotIndex + 1, action.length());
      // pull off the prefixes since java doesnt allow this on a method name
      if (serviceMethod.startsWith("@") || serviceMethod.startsWith("|")) {
        serviceMethod = serviceMethod.substring(1);
      }
      Call call = new Call(serviceName, serviceMethod, params);
      notify.setCall(call);
      return notify;
    } else {
      //TODO replace this with something better as time permits
      throw new RuntimeException("Action was null");
    }
  }

  /** {@inheritDoc} */
  public Invoke decodeInvoke(Encoding encoding, IoBuffer in) {
    Invoke invoke = new Invoke();
    int start = in.position();
    Input input;
    // for response, the action string and invokeId is always encoded as AMF0 we use the first byte to decide which encoding to use.
    byte tmp = in.get();
    in.position(start);
    if (encoding == Encoding.AMF3 && tmp == AMF.TYPE_AMF3_OBJECT) {
      input = new org.red5.io.amf3.Input(in);
      ((org.red5.io.amf3.Input) input).enforceAMF3();
    } else {
      input = new org.red5.io.amf.Input(in);
    }
    // get the action
    String action = Deserializer.deserialize(input, String.class);
    if (log.isTraceEnabled()) {
      log.trace("Action " + action);
    }
    //throw a runtime exception if there is no action
    if (action != null) {
      invoke.setTransactionId(Deserializer.<Number> deserialize(input, Number.class).intValue());
      // now go back to the actual encoding to decode parameters
      if (encoding == Encoding.AMF3) {
        input = new org.red5.io.amf3.Input(in);
        ((org.red5.io.amf3.Input) input).enforceAMF3();
      } else {
        input = new org.red5.io.amf.Input(in);
      }
      // get / set the parameters if there any
      Object[] params = handleParameters(in, invoke, input);
      // determine service information
      final int dotIndex = action.lastIndexOf('.');
      String serviceName = (dotIndex == -1) ? null : action.substring(0, dotIndex);
      // pull off the prefixes since java doesnt allow this on a method name
      if (serviceName != null && (serviceName.startsWith("@") || serviceName.startsWith("|"))) {
        serviceName = serviceName.substring(1);
      }
      String serviceMethod = (dotIndex == -1) ? action : action.substring(dotIndex + 1, action.length());
      // pull off the prefixes since java doesn't allow this on a method name
      if (serviceMethod.startsWith("@") || serviceMethod.startsWith("|")) {
        serviceMethod = serviceMethod.substring(1);
      }
      PendingCall call = new PendingCall(serviceName, serviceMethod, params);
      invoke.setCall(call);
      return invoke;
    } else {
      //TODO replace this with something better as time permits
      throw new RuntimeException("Action was null");
    }
  }

  /**
   * Decodes ping event.
   *
   * @param in IoBuffer
   * @return Ping event
   */
  public Ping decodePing(IoBuffer in) {
    Ping ping = null;
    if (log.isTraceEnabled()) {
      // gets the raw data as hex without changing the data or pointer
      String hexDump = in.getHexDump();
      log.trace("Ping dump: {}", hexDump);
    }
    // control type
    short type = in.getShort();
    switch (type) {
      case Ping.CLIENT_BUFFER:
        ping = new SetBuffer(in.getInt(), in.getInt());
        break;
      case Ping.PING_SWF_VERIFY:
        // only contains the type (2 bytes)
        ping = new Ping(type);
        break;
      case Ping.PONG_SWF_VERIFY:
        byte[] bytes = new byte[42];
        in.get(bytes);
        ping = new SWFResponse(bytes);
        break;
      default:
        //STREAM_BEGIN, STREAM_PLAYBUFFER_CLEAR, STREAM_DRY, RECORDED_STREAM
        //PING_CLIENT, PONG_SERVER
        //BUFFER_EMPTY, BUFFER_FULL
        ping = new Ping(type, in.getInt());
        break;
    }
    return ping;
  }

  /** {@inheritDoc} */
  public BytesRead decodeBytesRead(IoBuffer in) {
    return new BytesRead(in.getInt());
  }

  /** {@inheritDoc} */
  public AudioData decodeAudioData(IoBuffer in) {
    return new AudioData(in.asReadOnlyBuffer());
  }

  /** {@inheritDoc} */
  public VideoData decodeVideoData(IoBuffer in) {
    return new VideoData(in.asReadOnlyBuffer());
  }

  /**
   * Decodes stream meta data, to include onMetaData, onCuePoint, and onFI.
   *
   * @param in
   * @return Notify
   */
  @SuppressWarnings("unchecked")
  public Notify decodeStreamMetadata(IoBuffer in) {
    Encoding encoding = ((RTMPConnection) Red5.getConnectionLocal()).getEncoding();
    Input input = null;

    // check to see if the encoding is set to AMF3.
    // if it is then check to see if first byte is set to AMF0
    byte amfVersion = 0x00;
    if (encoding == Encoding.AMF3) {
      amfVersion = in.get();
    }
   
    // reset the position back to 0
    in.position(0);
   
    //make a pre-emptive copy of the incoming buffer here to prevent issues that occur fairly often
    IoBuffer copy = in.duplicate();
   
   
    if (encoding == Encoding.AMF0 || amfVersion != AMF.TYPE_AMF3_OBJECT ) {
      input = new org.red5.io.amf.Input(copy);
    } else {
      org.red5.io.amf3.Input.RefStorage refStorage = new org.red5.io.amf3.Input.RefStorage();
      input = new org.red5.io.amf3.Input(copy, refStorage);
    }
    //get the first datatype
    byte dataType = input.readDataType();
    if (dataType == DataTypes.CORE_STRING) {
      String setData = input.readString(String.class);
      if ("@setDataFrame".equals(setData)) {
        // get the second datatype
        byte dataType2 = input.readDataType();
        log.debug("Dataframe method type: {}", dataType2);
        String onCueOrOnMeta = input.readString(String.class);
        // get the params datatype
        byte object = input.readDataType();
        log.debug("Dataframe params type: {}", object);
        Map<Object, Object> params;
        if (object == DataTypes.CORE_MAP) {
          // the params are sent as a Mixed-Array. Required to support the RTMP publish provided by ffmpeg/xuggler
          params = (Map<Object, Object>) input.readMap(null);
        } else {
          // read the params as a standard object
          params = (Map<Object, Object>) input.readObject(Object.class);
        }
        log.debug("Dataframe: {} params: {}", onCueOrOnMeta, params.toString());

        IoBuffer buf = IoBuffer.allocate(1024);
        buf.setAutoExpand(true);
        Output out = new Output(buf);
        out.writeString(onCueOrOnMeta);
        out.writeMap(params);

        buf.flip();
        return new Notify(buf);
      } else if ("onFI".equals(setData)) {
        // the onFI request contains 2 items relative to the publishing client application
        // sd = system date (12-07-2011)
        // st = system time (09:11:33.387)
        byte object = input.readDataType();
        log.debug("onFI params type: {}", object);
        Map<Object, Object> params;
        if (object == DataTypes.CORE_MAP) {
          // the params are sent as a Mixed-Array
          params = (Map<Object, Object>) input.readMap(null);
        } else {
          // read the params as a standard object
          params = (Map<Object, Object>) input.readObject(Object.class);
        }
        log.debug("onFI params: {}", params.toString());
      } else {
        log.info("Unhandled request: {}", setData);
        if (log.isDebugEnabled()) {
          byte object = input.readDataType();
          log.debug("Params type: {}", object);
          if (object == DataTypes.CORE_MAP) {
            Map<Object, Object> params = (Map<Object, Object>) input.readMap(null);
            log.debug("Params: {}", params.toString());
          } else {
            log.debug("The unknown request was did not provide a parameter map");
          }
        }
      }
    }
    return new Notify(in.asReadOnlyBuffer());
  }

  /**
   * Decodes FlexMessage event.
   *
   * @param in IoBuffer
   * @return FlexMessage event
   */
  public FlexMessage decodeFlexMessage(IoBuffer in) {
    // TODO: Unknown byte, probably encoding as with Flex SOs?
    byte flexByte = in.get();
    log.trace("Flex byte: {}", flexByte);
    // Encoding of message params can be mixed - some params may be in AMF0, others in AMF3,
    // but according to AMF3 spec, we should collect AMF3 references for the whole message body (through all params)
    org.red5.io.amf3.Input.RefStorage refStorage = new org.red5.io.amf3.Input.RefStorage();

    Input input = new org.red5.io.amf.Input(in);
    String action = Deserializer.deserialize(input, String.class);
    int transactionId = Deserializer.<Number> deserialize(input, Number.class).intValue();
    FlexMessage msg = new FlexMessage();
    msg.setTransactionId(transactionId);
    Object[] params = new Object[] {};
    if (in.hasRemaining()) {
      ArrayList<Object> paramList = new ArrayList<Object>();
      final Object obj = Deserializer.deserialize(input, Object.class);
      if (obj != null) {
        paramList.add(obj);
      }
      while (in.hasRemaining()) {
        // Check for AMF3 encoding of parameters
        byte objectEncodingType = in.get();
        in.position(in.position() - 1);
        log.debug("Object encoding: {}", objectEncodingType);
        switch (objectEncodingType) {
          case AMF.TYPE_AMF3_OBJECT:
          case AMF3.TYPE_VECTOR_NUMBER:
          case AMF3.TYPE_VECTOR_OBJECT:
            // The next parameter is encoded using AMF3
            input = new org.red5.io.amf3.Input(in, refStorage);
            // Vectors with number and object have to have AMF3 forced
            ((org.red5.io.amf3.Input) input).enforceAMF3();
            break;
          case AMF3.TYPE_VECTOR_INT:
          case AMF3.TYPE_VECTOR_UINT:
            // The next parameter is encoded using AMF3
            input = new org.red5.io.amf3.Input(in, refStorage);
            break;
          default:
            // The next parameter is encoded using AMF0
            input = new org.red5.io.amf.Input(in);
        }
        paramList.add(Deserializer.deserialize(input, Object.class));
      }
      params = paramList.toArray();
      if (log.isTraceEnabled()) {
        log.trace("Parameter count: {}", paramList.size());
        for (int i = 0; i < params.length; i++) {
          log.trace(" > {}: {}", i, params[i]);
        }
      }
    }
    final int dotIndex = action.lastIndexOf('.');
    String serviceName = (dotIndex == -1) ? null : action.substring(0, dotIndex);
    String serviceMethod = (dotIndex == -1) ? action : action.substring(dotIndex + 1, action.length());
    log.debug("Service name: {} method: {}", serviceName, serviceMethod);
    PendingCall call = new PendingCall(serviceName, serviceMethod, params);
    msg.setCall(call);
    return msg;
  }

  public Notify decodeFlexStreamSend(IoBuffer in) {
    // grab the initial limit
    int limit = in.limit();
   
    // remove the first byte
    in.position(1);
    in.compact();
    in.rewind();
   
    // set the limit back to the original minus the one
    // byte that we removed from the buffer
    in.limit(limit-1);
   
    return new FlexStreamSend(in.asReadOnlyBuffer());
  }

  /**
   * Checks if the passed action is a reserved stream method.
   *
   * @param action
   *            Action to check
   * @return <code>true</code> if passed action is a reserved stream method,
   *         <code>false</code> otherwise
   */
  private boolean isStreamCommand(String action) {
    switch (StreamAction.getEnum(action)) {
      case CREATE_STREAM:
      case DELETE_STREAM:
      case RELEASE_STREAM:
      case PUBLISH:
      case PLAY:
      case PLAY2:
      case SEEK:
      case PAUSE:
      case PAUSE_RAW:
      case CLOSE_STREAM:
      case RECEIVE_VIDEO:
      case RECEIVE_AUDIO:
        return true;
      default:
        log.debug("Stream action {} is not a recognized command", action);
        return false;
    }
  }

  /**
   * Sets incoming connection parameters and / or returns encoded parameters for use in a call.
   *
   * @param in
   * @param notify
   * @param input
   * @return parameters array
   */
  private Object[] handleParameters(IoBuffer in, Notify notify, Input input) {
    Object[] params = new Object[] {};
    if (in.hasRemaining()) {
      List<Object> paramList = new ArrayList<Object>();
      final Object obj = Deserializer.deserialize(input, Object.class);
      if (obj instanceof Map) {
        // Before the actual parameters we sometimes (connect) get a map of parameters, this is usually null, but if set should be
        // passed to the connection object.
        @SuppressWarnings("unchecked")
        final Map<String, Object> connParams = (Map<String, Object>) obj;
        notify.setConnectionParams(connParams);
      } else if (obj != null) {
        paramList.add(obj);
      }
      while (in.hasRemaining()) {
        paramList.add(Deserializer.deserialize(input, Object.class));
      }
      params = paramList.toArray();
      if (log.isDebugEnabled()) {
        log.debug("Num params: {}", paramList.size());
        for (int i = 0; i < params.length; i++) {
          log.debug(" > {}: {}", i, params[i]);
        }
      }
    }
    return params;
  }

}
TOP

Related Classes of org.red5.server.net.rtmp.codec.RTMPProtocolDecoder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.