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

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

/*
* 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.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.mina.core.buffer.IoBuffer;
import org.red5.io.object.Output;
import org.red5.io.object.Serializer;
import org.red5.io.utils.BufferUtils;
import org.red5.server.api.IConnection.Encoding;
import org.red5.server.api.Red5;
import org.red5.server.api.service.IPendingServiceCall;
import org.red5.server.api.service.IServiceCall;
import org.red5.server.api.stream.IClientStream;
import org.red5.server.exception.ClientDetailsException;
import org.red5.server.net.ICommand;
import org.red5.server.net.rtmp.RTMPConnection;
import org.red5.server.net.rtmp.RTMPUtils;
import org.red5.server.net.rtmp.codec.RTMP.LiveTimestampMapping;
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.event.VideoData.FrameType;
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.net.rtmp.status.Status;
import org.red5.server.net.rtmp.status.StatusCodes;
import org.red5.server.net.rtmp.status.StatusObject;
import org.red5.server.service.Call;
import org.red5.server.so.ISharedObjectEvent;
import org.red5.server.so.ISharedObjectMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* RTMP protocol encoder encodes RTMP messages and packets to byte buffers.
*/
public class RTMPProtocolEncoder implements Constants, IEventEncoder {

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

  /**
   * Tolerance (in milliseconds) for late media on streams. A set of levels based on this
   * value will be determined.
   */
  private long baseTolerance = 15000;

  /**
   * Middle tardiness level, between base and this value disposable frames
   * will be dropped. Between this and highest value regular interframes will be dropped.
   */
  private long midTolerance = baseTolerance + (long) (baseTolerance * 0.3);

  /**
   * Highest tardiness level before dropping key frames
   */
  private long highestTolerance = baseTolerance + (long) (baseTolerance * 0.6);

  /**
   * Indicates if we should drop live packets with future timestamp
   * (i.e, when publisher bandwidth is limited) - EXPERIMENTAL
   */
  private boolean dropLiveFuture;

  /**
   * Encodes object with given protocol state to byte buffer
   *
   * @param message    Object to encode
   * @return        IoBuffer with encoded data
   * @throws Exception    Any decoding exception
   */
  public IoBuffer encode(Object message) throws Exception {
    try {
      return encodePacket((Packet) message);
    } catch (Exception e) {
      log.error("Error encoding", e);
    }
    return null;
  }

  /**
   * Encode packet.
   *
   * @param packet      RTMP packet
   * @return            Encoded data
   */
  public IoBuffer encodePacket(Packet packet) {
    IoBuffer out = null;
    final Header header = packet.getHeader();
    final int channelId = header.getChannelId();
    log.trace("Channel id: {}", channelId);
    final IRTMPEvent message = packet.getMessage();
    if (message instanceof ChunkSize) {
      ChunkSize chunkSizeMsg = (ChunkSize) message;
      ((RTMPConnection) Red5.getConnectionLocal()).getState().setWriteChunkSize(chunkSizeMsg.getSize());
    }
    // normally the message is expected not to be dropped
    if (!dropMessage(channelId, message)) {
      IoBuffer data = encodeMessage(header, message);
      if (data != null) {
        RTMP rtmp = ((RTMPConnection) Red5.getConnectionLocal()).getState();
        if (data.position() != 0) {
          data.flip();
        } else {
          data.rewind();
        }
        int dataLen = data.limit();
        header.setSize(dataLen);
        // get last header
        Header lastHeader = rtmp.getLastWriteHeader(channelId);
        // maximum header size with extended timestamp (Chunk message header type 0 with 11 byte)
        int headerSize = 18;
        // set last write header
        rtmp.setLastWriteHeader(channelId, header);
        // set last write packet
        rtmp.setLastWritePacket(channelId, packet);
        int chunkSize = rtmp.getWriteChunkSize();
        // maximum chunk header size with extended timestamp
        int chunkHeaderSize = 7;
        int numChunks = (int) Math.ceil(dataLen / (float) chunkSize);
        int bufSize = dataLen + headerSize + (numChunks > 0 ? (numChunks - 1) * chunkHeaderSize : 0);
        out = IoBuffer.allocate(bufSize, false);
        // encode the header
        encodeHeader(header, lastHeader, out);
        if (numChunks == 1) {
          // we can do it with a single copy
          BufferUtils.put(out, data, dataLen);
        } else {
          int extendedTimestamp = header.getExtendedTimestamp();
          for (int i = 0; i < numChunks - 1; i++) {
            BufferUtils.put(out, data, chunkSize);
            dataLen -= chunkSize;
            RTMPUtils.encodeHeaderByte(out, HEADER_CONTINUE, channelId);
            if (extendedTimestamp != 0) {
              out.putInt(extendedTimestamp);
            }
          }
          BufferUtils.put(out, data, dataLen);
        }
        data.free();
        out.flip();
        data = null;
      }
    }
    message.release();
    return out;
  }

  /**
   * Determine if this message should be dropped for lateness. Live publish data
   * does not come through this section, only outgoing data does.
   *
   * - determine latency between server and client using ping
   * - ping timestamp is unsigned int (4 bytes) and is set from value on sender
   *
   * 1st drop disposable frames - lowest mark
   * 2nd drop interframes - middle
   * 3rd drop key frames - high mark
   *
   * @param channelId the channel ID
   * @param message the message
   * @return true to drop; false to send
   */
  protected boolean dropMessage(int channelId, IRTMPEvent message) {
    boolean isLive = message.getSourceType() == Constants.SOURCE_TYPE_LIVE;
    log.trace("Connection type: {}", (isLive ? "Live" : "VOD"));   
    if (!isLive) {
      return false;
    }   
    //whether or not the packet will be dropped
    boolean drop = false;
    //whether or not the packet is video data
    boolean isVideo = false;
    if (message instanceof Ping) {
      final Ping pingMessage = (Ping) message;
      if (pingMessage.getEventType() == Ping.STREAM_PLAYBUFFER_CLEAR) {
        // client buffer cleared, make sure to reset timestamps for this stream
        final int channel = (4 + ((pingMessage.getValue2() - 1) * 5));
        ((RTMPConnection) Red5.getConnectionLocal()).getState().clearLastTimestampMapping(channel, channel + 1, channel + 2);
      }
      // never drop pings
      return false;
    }
    // we only drop audio or video data
    if ((isVideo = message instanceof VideoData) || message instanceof AudioData) {
      if (message.getTimestamp() == 0) {
        // never drop initial packages, also this could be the first packet after
        // MP4 seeking and therefore mess with the timestamp mapping
        return false;
      }
      // get connection
      RTMPConnection conn = (RTMPConnection) Red5.getConnectionLocal();
      log.trace("Connection: {}", conn);
      // get state
      RTMP rtmp = conn.getState();
      // determine working type
      long timestamp = (message.getTimestamp() & 0xFFFFFFFFL);
      LiveTimestampMapping mapping = rtmp.getLastTimestampMapping(channelId);
      // just get the current time ONCE per packet
      long now = System.currentTimeMillis();
      if (mapping == null || timestamp < mapping.getLastStreamTime()) {
        log.trace("Resetting clock time ({}) to stream time ({})", now, timestamp);
        // either first time through, or time stamps were reset
        mapping = rtmp.new LiveTimestampMapping(now, timestamp);
        rtmp.setLastTimestampMapping(channelId, mapping);
      }
      mapping.setLastStreamTime(timestamp);
      long clockTimeOfMessage = mapping.getClockStartTime() + timestamp - mapping.getStreamStartTime();
      //determine tardiness / how late it is
      long tardiness = clockTimeOfMessage - now;
      //TDJ: EXPERIMENTAL dropping for LIVE packets in future (default false)
      if (isLive && dropLiveFuture) {
        tardiness = Math.abs(tardiness);
      }
      //subtract the ping time / latency from the tardiness value
      if (conn != null) {
        int lastPingTime = conn.getLastPingTime();
        log.trace("Last ping time for connection: {} {} ms", conn.getId(), lastPingTime);
        if (lastPingTime > 0) {
          tardiness -= lastPingTime;
        }
        //subtract the buffer time
        int streamId = conn.getStreamIdForChannel(channelId);
        IClientStream stream = conn.getStreamById(streamId);
        if (stream != null) {
          int clientBufferDuration = stream.getClientBufferDuration();
          if (clientBufferDuration > 0) {
            //two times the buffer duration seems to work best with vod
            if (isLive) {
              tardiness -= clientBufferDuration;
            } else {
              tardiness -= clientBufferDuration * 2;
            }
          }
          log.trace("Client buffer duration: {}", clientBufferDuration);
        }
      }

      //TODO: how should we differ handling based on live or vod?

      //TODO: if we are VOD do we "pause" the provider when we are consistently late?

      if (log.isTraceEnabled()) {
        log.trace("Packet timestamp: {}; tardiness: {}; now: {}; message clock time: {}, dropLiveFuture: {}", new Object[] { timestamp, tardiness, now, clockTimeOfMessage,
          dropLiveFuture });
      }
      //anything coming in less than the base will be allowed to pass, it will not be
      //dropped or manipulated
      if (tardiness < baseTolerance) {
        //frame is below lowest bounds, let it go
      } else if (tardiness > highestTolerance) {
        //frame is really late, drop it no matter what type
        log.trace("Dropping late message: {}", message);
        //if we're working with video, indicate that we will need a key frame to proceed
        if (isVideo) {
          mapping.setKeyFrameNeeded(true);
        }
        //drop it
        drop = true;
      } else {
        if (isVideo) {
          VideoData video = (VideoData) message;
          if (video.getFrameType() == FrameType.KEYFRAME) {
            //if its a key frame the inter and disposible checks can be skipped
            log.trace("Resuming stream with key frame; message: {}", message);
            mapping.setKeyFrameNeeded(false);
          } else if (tardiness >= baseTolerance && tardiness < midTolerance) {
            //drop disposable frames
            if (video.getFrameType() == FrameType.DISPOSABLE_INTERFRAME) {
              log.trace("Dropping disposible frame; message: {}", message);
              drop = true;
            }
          } else if (tardiness >= midTolerance && tardiness <= highestTolerance) {
            //drop inter-frames and disposable frames
            log.trace("Dropping disposible or inter frame; message: {}", message);
            drop = true;
          }
        }
      }
    }
    log.trace("Message was{}dropped", (drop ? " " : " not "));
    return drop;
  }

  /**
   * Determine type of header to use.
   *
   * @param header      RTMP message header
   * @param lastHeader  Previous header
   * @return            Header type to use.
   */
  private byte getHeaderType(final Header header, final Header lastHeader) {
    if (lastHeader == null) {
      return HEADER_NEW;
    }
    final Integer lastFullTs = ((RTMPConnection) Red5.getConnectionLocal()).getState().getLastFullTimestampWritten(header.getChannelId());
    if (lastFullTs == null) {
      return HEADER_NEW;
    }
    final byte headerType;
    final long diff = RTMPUtils.diffTimestamps(header.getTimer(), lastHeader.getTimer());
    final long timeSinceFullTs = RTMPUtils.diffTimestamps(header.getTimer(), lastFullTs);
    if (header.getStreamId() != lastHeader.getStreamId() || diff < 0 || timeSinceFullTs >= 250) {
      // New header mark if header for another stream
      headerType = HEADER_NEW;
    } else if (header.getSize() != lastHeader.getSize() || header.getDataType() != lastHeader.getDataType()) {
      // Same source header if last header data type or size differ
      headerType = HEADER_SAME_SOURCE;
    } else if (header.getTimer() != lastHeader.getTimer() + lastHeader.getTimerDelta()) {
      // Timer change marker if there's time gap between header time stamps
      headerType = HEADER_TIMER_CHANGE;
    } else {
      // Continue encoding
      headerType = HEADER_CONTINUE;
    }
    return headerType;
  }

  /**
   * Calculate number of bytes necessary to encode the header.
   *
   * @param header      RTMP message header
   * @param lastHeader  Previous header
   * @return            Calculated size
   */
  private int calculateHeaderSize(final Header header, final Header lastHeader) {
    final byte headerType = getHeaderType(header, lastHeader);
    int channelIdAdd = 0;
    int channelId = header.getChannelId();
    if (channelId > 320) {
      channelIdAdd = 2;
    } else if (channelId > 63) {
      channelIdAdd = 1;
    }
    return RTMPUtils.getHeaderLength(headerType) + channelIdAdd;
  }

  /**
   * Encode RTMP header.
   *
   * @param header      RTMP message header
   * @param lastHeader  Previous header
   * @return            Encoded header data
   */
  public IoBuffer encodeHeader(final Header header, final Header lastHeader) {
    final IoBuffer result = IoBuffer.allocate(calculateHeaderSize(header, lastHeader));
    encodeHeader(header, lastHeader, result);
    return result;
  }

  /**
   * Encode RTMP header into given IoBuffer.
   *
   * @param header      RTMP message header
   * @param lastHeader  Previous header
   * @param buf         Buffer to write encoded header to
   */
  public void encodeHeader(final Header header, final Header lastHeader, final IoBuffer buf) {
    final byte headerType = getHeaderType(header, lastHeader);
    RTMPUtils.encodeHeaderByte(buf, headerType, header.getChannelId());
    final int timer;
    switch (headerType) {
      case HEADER_NEW:
        timer = header.getTimer();
        if (timer < 0 || timer >= 0xffffff) {
          RTMPUtils.writeMediumInt(buf, 0xffffff);
        } else {
          RTMPUtils.writeMediumInt(buf, timer);
        }
        RTMPUtils.writeMediumInt(buf, header.getSize());
        buf.put(header.getDataType());
        RTMPUtils.writeReverseInt(buf, header.getStreamId());
        if (timer < 0 || timer >= 0xffffff) {
          buf.putInt(timer);
          header.setExtendedTimestamp(timer);
        }
        header.setTimerBase(timer);
        header.setTimerDelta(0);
        RTMPConnection conn = (RTMPConnection) Red5.getConnectionLocal();
        if (conn != null) {
          conn.getState().setLastFullTimestampWritten(header.getChannelId(), timer);
        }
        break;
      case HEADER_SAME_SOURCE:
        timer = (int) RTMPUtils.diffTimestamps(header.getTimer(), lastHeader.getTimer());
        if (timer < 0 || timer >= 0xffffff) {
          RTMPUtils.writeMediumInt(buf, 0xffffff);
        } else {
          RTMPUtils.writeMediumInt(buf, timer);
        }
        RTMPUtils.writeMediumInt(buf, header.getSize());
        buf.put(header.getDataType());
        if (timer < 0 || timer >= 0xffffff) {
          buf.putInt(timer);
          header.setExtendedTimestamp(timer);
        }
        header.setTimerBase(header.getTimer() - timer);
        header.setTimerDelta(timer);
        break;
      case HEADER_TIMER_CHANGE:
        timer = (int) RTMPUtils.diffTimestamps(header.getTimer(), lastHeader.getTimer());
        if (timer < 0 || timer >= 0xffffff) {
          RTMPUtils.writeMediumInt(buf, 0xffffff);
          buf.putInt(timer);
          header.setExtendedTimestamp(timer);
        } else {
          RTMPUtils.writeMediumInt(buf, timer);
        }
        header.setTimerBase(header.getTimer() - timer);
        header.setTimerDelta(timer);
        break;
      case HEADER_CONTINUE:
        timer = (int) RTMPUtils.diffTimestamps(header.getTimer(), lastHeader.getTimer());
        header.setTimerBase(header.getTimer() - timer);
        header.setTimerDelta(timer);
        if (lastHeader.getExtendedTimestamp() != 0) {
          buf.putInt(lastHeader.getExtendedTimestamp());
          header.setExtendedTimestamp(lastHeader.getExtendedTimestamp());
        }
        break;
      default:
        break;
    }
    log.trace("CHUNK, E, {}, {}", header, headerType);
  }

  /**
   * Encode message.
   *
   * @param header      RTMP message header
   * @param message     RTMP message (event)
   * @return            Encoded message data
   */
  public IoBuffer encodeMessage(Header header, IRTMPEvent message) {
    IServiceCall call = null;
    switch (header.getDataType()) {
      case TYPE_CHUNK_SIZE:
        return encodeChunkSize((ChunkSize) message);
      case TYPE_INVOKE:
        log.trace("Invoke {}", message);
        call = ((Invoke) message).getCall();
        if (call != null) {
          log.debug("{}", call.toString());
          Object[] args = call.getArguments();
          if (args != null && args.length > 0) {
            Object a0 = args[0];
            if (a0 instanceof Status) {
              Status status = (Status) a0;
              //code: NetStream.Seek.Notify
              if (StatusCodes.NS_SEEK_NOTIFY.equals(status.getCode())) {
                //desc: Seeking 25000 (stream ID: 1).
                int seekTime = Integer.valueOf(status.getDescription().split(" ")[1]);
                log.trace("Seek to time: {}", seekTime);
                //audio and video channels
                int[] channels = new int[] { 5, 6 };
                //if its a seek notification, reset the "mapping" for audio (5) and video (6)
                RTMP rtmp = ((RTMPConnection) Red5.getConnectionLocal()).getState();
                for (int channelId : channels) {
                  LiveTimestampMapping mapping = rtmp.getLastTimestampMapping(channelId);
                  if (mapping != null) {
                    long timestamp = mapping.getClockStartTime() + (seekTime & 0xFFFFFFFFL);
                    log.trace("Setting last stream time to: {}", timestamp);
                    mapping.setLastStreamTime(timestamp);
                  } else {
                    log.trace("No ts mapping for channel id: {}", channelId);
                  }
                }
              }
            }
          }
        }
        return encodeInvoke((Invoke) message);
      case TYPE_NOTIFY:
        log.trace("Notify {}", message);
        call = ((Notify) message).getCall();
        if (call == null) {
          return encodeStreamMetadata((Notify) message);
        } else {
          return encodeNotify((Notify) message);
        }
      case TYPE_PING:
        if (message instanceof SetBuffer) {
          return encodePing((SetBuffer) message);
        } else if (message instanceof SWFResponse) {
          return encodePing((SWFResponse) message);
        } else {
          return encodePing((Ping) message);
        }
      case TYPE_BYTES_READ:
        return encodeBytesRead((BytesRead) message);
      case TYPE_AGGREGATE:
        log.trace("Encode aggregate message");
        return encodeAggregate((Aggregate) message);
      case TYPE_AUDIO_DATA:
        log.trace("Encode audio message");
        return encodeAudioData((AudioData) message);
      case TYPE_VIDEO_DATA:
        log.trace("Encode video message");
        return encodeVideoData((VideoData) message);
      case TYPE_FLEX_SHARED_OBJECT:
        return encodeFlexSharedObject((ISharedObjectMessage) message);
      case TYPE_SHARED_OBJECT:
        return encodeSharedObject((ISharedObjectMessage) message);
      case TYPE_SERVER_BANDWIDTH:
        return encodeServerBW((ServerBW) message);
      case TYPE_CLIENT_BANDWIDTH:
        return encodeClientBW((ClientBW) message);
      case TYPE_FLEX_MESSAGE:
        return encodeFlexMessage((FlexMessage) message);
      case TYPE_FLEX_STREAM_SEND:
        return encodeFlexStreamSend((FlexStreamSend) message);
      default:
        log.warn("Unknown object type: {}", header.getDataType());
    }
    return null;
  }

  /**
   * Encode server-side bandwidth event.
   *
   * @param serverBW    Server-side bandwidth event
   * @return            Encoded event data
   */
  private IoBuffer encodeServerBW(ServerBW serverBW) {
    final IoBuffer out = IoBuffer.allocate(4);
    out.putInt(serverBW.getBandwidth());
    return out;
  }

  /**
   * Encode client-side bandwidth event.
   *
   * @param clientBW    Client-side bandwidth event
   * @return            Encoded event data
   */
  private IoBuffer encodeClientBW(ClientBW clientBW) {
    final IoBuffer out = IoBuffer.allocate(5);
    out.putInt(clientBW.getBandwidth());
    out.put(clientBW.getLimitType());
    return out;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeChunkSize(ChunkSize chunkSize) {
    final IoBuffer out = IoBuffer.allocate(4);
    out.putInt(chunkSize.getSize());
    return out;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeFlexSharedObject(ISharedObjectMessage so) {
    final IoBuffer out = IoBuffer.allocate(128);
    out.setAutoExpand(true);
    out.put((byte) 0x00); // unknown (not AMF version)
    doEncodeSharedObject(so, out);
    return out;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeSharedObject(ISharedObjectMessage so) {
    final IoBuffer out = IoBuffer.allocate(128);
    out.setAutoExpand(true);
    doEncodeSharedObject(so, out);
    return out;
  }

  /**
   * Perform the actual encoding of the shared object contents.
   *
   * @param so shared object
   * @param out output buffer
   */
  private void doEncodeSharedObject(ISharedObjectMessage so, IoBuffer out) {
    final Encoding encoding = Red5.getConnectionLocal().getEncoding();
    final Output output = new org.red5.io.amf.Output(out);
    final Output amf3output = new org.red5.io.amf3.Output(out);
    output.putString(so.getName());
    // SO version
    out.putInt(so.getVersion());
    // Encoding (this always seems to be 2 for persistent shared objects)
    out.putInt(so.isPersistent() ? 2 : 0);
    // unknown field
    out.putInt(0);
    int mark, len;
    for (final ISharedObjectEvent event : so.getEvents()) {
      final ISharedObjectEvent.Type eventType = event.getType();
      byte type = SharedObjectTypeMapping.toByte(eventType);
      switch (eventType) {
        case SERVER_CONNECT:
        case CLIENT_INITIAL_DATA:
        case CLIENT_CLEAR_DATA:
          out.put(type);
          out.putInt(0);
          break;
        case SERVER_DELETE_ATTRIBUTE:
        case CLIENT_DELETE_DATA:
        case CLIENT_UPDATE_ATTRIBUTE:
          out.put(type);
          mark = out.position();
          out.skip(4); // we will be back
          output.putString(event.getKey());
          len = out.position() - mark - 4;
          out.putInt(mark, len);
          break;
        case SERVER_SET_ATTRIBUTE:
        case CLIENT_UPDATE_DATA:
          if (event.getKey() == null) {
            // Update multiple attributes in one request
            Map<?, ?> initialData = (Map<?, ?>) event.getValue();
            for (Object o : initialData.keySet()) {
              out.put(type);
              mark = out.position();
              out.skip(4); // we will be back
              String key = (String) o;
              output.putString(key);
              if (encoding == Encoding.AMF3) {
                Serializer.serialize(amf3output, initialData.get(key));
              } else {
                Serializer.serialize(output, initialData.get(key));
              }
              len = out.position() - mark - 4;
              out.putInt(mark, len);
            }
          } else {
            out.put(type);
            mark = out.position();
            out.skip(4); // we will be back
            output.putString(event.getKey());
            if (encoding == Encoding.AMF3) {
              Serializer.serialize(amf3output, event.getValue());
            } else {
              Serializer.serialize(output, event.getValue());
            }
            len = out.position() - mark - 4;
            out.putInt(mark, len);
          }
          break;
        case CLIENT_SEND_MESSAGE:
        case SERVER_SEND_MESSAGE:
          // Send method name and value
          out.put(type);
          mark = out.position();
          out.skip(4);
          // Serialize name of the handler to call...
          Serializer.serialize(output, event.getKey());
          try {
            List<?> arguments = (List<?>) event.getValue();
            if (arguments != null) {
              // ...and the arguments
              for (Object arg : arguments) {
                if (encoding == Encoding.AMF3) {
                  Serializer.serialize(amf3output, arg);
                } else {
                  Serializer.serialize(output, arg);
                }
              }
            } else {
              // serialize a null as the arguments
              if (encoding == Encoding.AMF3) {
                Serializer.serialize(amf3output, null);
              } else {
                Serializer.serialize(output, null);
              }
            }
          } catch (Exception ex) {
            log.warn("Exception encoding args for event: {}", event, ex);
          }
          len = out.position() - mark - 4;
          //log.debug(len);
          out.putInt(mark, len);
          //log.info(out.getHexDump());
          break;
        case CLIENT_STATUS:
          out.put(type);
          final String status = event.getKey();
          final String message = (String) event.getValue();
          out.putInt(message.length() + status.length() + 4);
          output.putString(message);
          output.putString(status);
          break;
        default:
          log.warn("Unknown event: {}", eventType);
          // XXX: need to make this work in server or client mode
          out.put(type);
          mark = out.position();
          out.skip(4); // we will be back
          output.putString(event.getKey());
          if (encoding == Encoding.AMF3) {
            Serializer.serialize(amf3output, event.getValue());
          } else {
            Serializer.serialize(output, event.getValue());
          }
          len = out.position() - mark - 4;
          out.putInt(mark, len);
          break;
      }
    }
  }

  /** {@inheritDoc} */
  public IoBuffer encodeNotify(Notify notify) {
    return encodeCommand(notify);
  }

  /** {@inheritDoc} */
  public IoBuffer encodeInvoke(Invoke invoke) {
    return encodeCommand(invoke);
  }

  /**
   * Encode notification event.
   *
   * @param invoke            Notification event
   * @return                  Encoded event data
   */
  protected IoBuffer encodeCommand(Notify invoke) {
    IoBuffer out = IoBuffer.allocate(1024);
    out.setAutoExpand(true);
    encodeCommand(out, invoke);
    return out;
  }

  /**
   * Encode command event and fill given byte buffer.
   *
   * @param out Buffer to fill
   * @param command Command event
   */
  protected void encodeCommand(IoBuffer out, ICommand command) {
    // TODO: tidy up here
    Output output = new org.red5.io.amf.Output(out);
    final IServiceCall call = command.getCall();
    final boolean isPending = (call.getStatus() == Call.STATUS_PENDING);
    log.debug("Call: {} pending: {}", call, isPending);
    if (!isPending) {
      log.debug("Call has been executed, send result");
      Serializer.serialize(output, call.isSuccess() ? "_result" : "_error");
    } else {
      log.debug("This is a pending call, send request");
      final String action = (call.getServiceName() == null) ? call.getServiceMethodName() : call.getServiceName() + '.' + call.getServiceMethodName();
      Serializer.serialize(output, action); // seems right
    }
    if (command instanceof Invoke) {
      Serializer.serialize(output, Integer.valueOf(command.getTransactionId()));
      Serializer.serialize(output, command.getConnectionParams());
    }
    if (call.getServiceName() == null && "connect".equals(call.getServiceMethodName())) {
      // response to initial connect, always use AMF0
      output = new org.red5.io.amf.Output(out);
    } else {
      if (Red5.getConnectionLocal().getEncoding() == Encoding.AMF3) {
        output = new org.red5.io.amf3.Output(out);
      } else {
        output = new org.red5.io.amf.Output(out);
      }
    }
    if (!isPending && (command instanceof Invoke)) {
      IPendingServiceCall pendingCall = (IPendingServiceCall) call;
      if (!call.isSuccess()) {
        log.debug("Call was not successful");
        StatusObject status = generateErrorResult(StatusCodes.NC_CALL_FAILED, call.getException());
        pendingCall.setResult(status);
      }
      Object res = pendingCall.getResult();
      log.debug("Writing result: {}", res);
      Serializer.serialize(output, res);
    } else {
      log.debug("Writing params");
      final Object[] args = call.getArguments();
      if (args != null) {
        for (Object element : args) {
          if (element instanceof ByteBuffer) {
            // a byte buffer indicates that serialization is already complete, send raw
            final ByteBuffer buf = (ByteBuffer) element;
            buf.mark();
            try {
              out.put(buf);
            } finally {
              buf.reset();
            }
          } else {
            // standard serialize
            Serializer.serialize(output, element);
          }
        }
      }
    }
    if (command.getData() != null) {
      out.setAutoExpand(true);
      out.put(command.getData());
    }
  }

  /** {@inheritDoc} */
  public IoBuffer encodePing(Ping ping) {
    int len;
    short type = ping.getEventType();
    switch (type) {
      case Ping.CLIENT_BUFFER:
        len = 10;
        break;
      case Ping.PONG_SWF_VERIFY:
        len = 44;
        break;
      default:
        len = 6;
    }
    final IoBuffer out = IoBuffer.allocate(len);
    out.putShort(type);
    switch (type) {
      case Ping.STREAM_BEGIN:
      case Ping.STREAM_PLAYBUFFER_CLEAR:
      case Ping.STREAM_DRY:
      case Ping.RECORDED_STREAM:
      case Ping.PING_CLIENT:
      case Ping.PONG_SERVER:
      case Ping.BUFFER_EMPTY:
      case Ping.BUFFER_FULL:
        out.putInt(ping.getValue2());
        break;
      case Ping.CLIENT_BUFFER:
        if (ping instanceof SetBuffer) {
          SetBuffer setBuffer = (SetBuffer) ping;
          out.putInt(setBuffer.getStreamId());
          out.putInt(setBuffer.getBufferLength());
        } else {
          out.putInt(ping.getValue2());
          out.putInt(ping.getValue3());
        }
        break;
      case Ping.PING_SWF_VERIFY:
        break;
      case Ping.PONG_SWF_VERIFY:
        out.put(((SWFResponse) ping).getBytes());
        break;
    }
    // this may not be needed anymore
    if (ping.getValue4() != Ping.UNDEFINED) {
      out.putInt(ping.getValue4());
    }
    return out;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeBytesRead(BytesRead bytesRead) {
    final IoBuffer out = IoBuffer.allocate(4);
    out.putInt(bytesRead.getBytesRead());
    return out;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeAggregate(Aggregate aggregate) {
    final IoBuffer result = aggregate.getData();
    return result;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeAudioData(AudioData audioData) {
    final IoBuffer result = audioData.getData();
    return result;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeVideoData(VideoData videoData) {
    final IoBuffer result = videoData.getData();
    return result;
  }

  /** {@inheritDoc} */
  public IoBuffer encodeUnknown(Unknown unknown) {
    final IoBuffer result = unknown.getData();
    return result;
  }

  public IoBuffer encodeStreamMetadata(Notify metaData) {
    final IoBuffer result = metaData.getData();
    return result;
  }

  /**
   * Generate error object to return for given exception.
   *
   * @param code call
   * @param error error
   * @return status object
   */
  protected StatusObject generateErrorResult(String code, Throwable error) {
    // Construct error object to return
    String message = "";
    while (error != null && error.getCause() != null) {
      error = error.getCause();
    }
    if (error != null && error.getMessage() != null) {
      message = error.getMessage();
    }
    StatusObject status = new StatusObject(code, "error", message);
    if (error instanceof ClientDetailsException) {
      // Return exception details to client
      status.setApplication(((ClientDetailsException) error).getParameters());
      if (((ClientDetailsException) error).includeStacktrace()) {
        List<String> stack = new ArrayList<String>();
        for (StackTraceElement element : error.getStackTrace()) {
          stack.add(element.toString());
        }
        status.setAdditional("stacktrace", stack);
      }
    } else if (error != null) {
      status.setApplication(error.getClass().getCanonicalName());
      List<String> stack = new ArrayList<String>();
      for (StackTraceElement element : error.getStackTrace()) {
        stack.add(element.toString());
      }
      status.setAdditional("stacktrace", stack);
    }
    return status;
  }

  /**
   * Encodes Flex message event.
   *
   * @param msg                Flex message event
   * @return                   Encoded data
   */
  public IoBuffer encodeFlexMessage(FlexMessage msg) {
    IoBuffer out = IoBuffer.allocate(1024);
    out.setAutoExpand(true);
    // Unknown byte, always 0?
    out.put((byte) 0);
    encodeCommand(out, msg);
    return out;
  }

  public IoBuffer encodeFlexStreamSend(FlexStreamSend msg) {
    final IoBuffer result = msg.getData();
    return result;
  }

  private void updateTolerance() {
    midTolerance = baseTolerance + (long) (baseTolerance * 0.3);
    highestTolerance = baseTolerance + (long) (baseTolerance * 0.6);
  }

  public void setBaseTolerance(long baseTolerance) {
    this.baseTolerance = baseTolerance;
    //update high and low tolerance
    updateTolerance();
  }

  /**
   * Setter for dropLiveFuture
   */
  public void setDropLiveFuture(boolean dropLiveFuture) {
    this.dropLiveFuture = dropLiveFuture;
  }

  public long getBaseTolerance() {
    return baseTolerance;
  }

}
TOP

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

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.