text: // Generated by CoffeeScript 2.5.1
  (function () {
    // RTMP/RTMPE/RTMPT/RTMPTE server
  
    // RTMP specification is available at:
    // http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf
    var AVC_PACKET_TYPE_END_OF_SEQUENCE,
      AVC_PACKET_TYPE_NALU,
      AVC_PACKET_TYPE_SEQUENCE_HEADER,
      Bits,
      DEBUG_INCOMING_RTMP_PACKETS,
      DEBUG_INCOMING_STREAM_DATA,
      DEBUG_OUTGOING_RTMP_PACKETS,
      EXTENDED_TIMESTAMP_TYPE_ABSOLUTE,
      EXTENDED_TIMESTAMP_TYPE_DELTA,
      EXTENDED_TIMESTAMP_TYPE_NOT_USED,
      RTMPServer,
      RTMPSession,
      RTMPTSession,
      RTMPT_SEND_REQUEST_BUFFER_SIZE,
      SESSION_STATE_HANDSHAKE_DONE,
      SESSION_STATE_HANDSHAKE_ONGOING,
      SESSION_STATE_NEW,
      Sequent,
      TIMESTAMP_ROUNDOFF,
      aac,
      api,
      avstreams,
      clearQueuedRTMPMessages,
      clientMaxId,
      codec_utils,
      config,
      convertPTSToMilliseconds,
      counter,
      createAMF0CommandMessage,
      createAMF0CommandMessageParams,
      createAMF0Data,
      createAMF0DataMessage,
      createAMF0DataMessageParams,
      createAMF0ECMAArray,
      createAMF0Object,
      createAMF0PropertyList,
      createAMF0StrictArray,
      createAudioMessage,
      createMessageHeader,
      createRTMPAggregateMessage,
      createRTMPMessage,
      createRTMPType1Message,
      createVideoMessage,
      crypto,
      flushRTMPMessages,
      flv,
      generateClientID,
      generateNewClientID,
      generateNewSessionID,
      generateSessionID,
      h264,
      logger,
      net,
      parseAMF0CommandMessage,
      parseAMF0Data,
      parseAMF0DataMessage,
      parseAMF0ECMAArray,
      parseAMF0Object,
      parseAMF0StrictArray,
      parseAcknowledgementMessage,
      parseIEEE754Double,
      parseUserControlMessage,
      queueAudioMessage,
      queueRTMPMessages,
      queueVideoMessage,
      queuedRTMPMessages,
      rtmp_handshake,
      rtmptSessions,
      rtmptSessionsCount,
      serializeAMF0DataMessage,
      sessions,
      sessionsCount,
      url,
      splice = [].splice;
  
    net = require('net');
  
    url = require('url');
  
    crypto = require('crypto');
  
    Sequent = require('sequent');
  
    rtmp_handshake = require('./rtmp_handshake');
  
    codec_utils = require('./codec_utils');
  
    config = require('./config');
  
    h264 = require('./h264');
  
    aac = require('./aac');
  
    flv = require('./flv');
  
    avstreams = require('./avstreams');
  
    logger = require('./logger');
  
    Bits = require('./bits');
  
    // enum
    SESSION_STATE_NEW = 1;
  
    SESSION_STATE_HANDSHAKE_ONGOING = 2;
  
    SESSION_STATE_HANDSHAKE_DONE = 3;
  
    AVC_PACKET_TYPE_SEQUENCE_HEADER = 0;
  
    AVC_PACKET_TYPE_NALU = 1;
  
    AVC_PACKET_TYPE_END_OF_SEQUENCE = 2;
  
    EXTENDED_TIMESTAMP_TYPE_NOT_USED = 'not-used';
  
    EXTENDED_TIMESTAMP_TYPE_ABSOLUTE = 'absolute';
  
    EXTENDED_TIMESTAMP_TYPE_DELTA = 'delta';
  
    TIMESTAMP_ROUNDOFF = 4294967296; // 32 bits
  
    DEBUG_INCOMING_STREAM_DATA = false;
  
    DEBUG_INCOMING_RTMP_PACKETS = false;
  
    DEBUG_OUTGOING_RTMP_PACKETS = false;
  
    RTMPT_SEND_REQUEST_BUFFER_SIZE = 10;
  
    // Number of active sessions
    sessionsCount = 0;
  
    // Active sessions
    sessions = {};
  
    // Number of active RTMPT sessions
    rtmptSessionsCount = 0;
  
    // Active RTMPT sessions
    rtmptSessions = {};
  
    // The newest client ID
    clientMaxId = 0;
  
    queuedRTMPMessages = {};
  
    // Generate a new client ID without collision
    generateNewClientID = function () {
      var clientID;
      clientID = generateClientID();
      while (sessions[clientID] != null) {
        clientID = generateClientID();
      }
      return clientID;
    };
  
    // Generate a new random client ID (like Cookie)
    generateClientID = function () {
      var clientID, i, j, numPossible, possible;
      possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
      numPossible = possible.length;
      clientID = '';
      for (i = j = 0; j <= 7; i = ++j) {
        clientID += possible.charAt((Math.random() * numPossible) | 0);
      }
      return clientID;
    };
  
    parseAcknowledgementMessage = function (buf) {
      var sequenceNumber;
      sequenceNumber =
        buf[0] * Math.pow(256, 3) + (buf[1] << 16) + (buf[2] << 8) + buf[3];
      return {
        sequenceNumber: sequenceNumber
      };
    };
  
    convertPTSToMilliseconds = function (pts) {
      return Math.floor(pts / 90);
    };
  
    createAudioMessage = function (params) {
      var audioMessage;
      // TODO: Use type 1/2/3
      return (audioMessage = createRTMPMessage(
        {
          chunkStreamID: 4,
          timestamp: params.timestamp,
          messageTypeID: 0x08, // Audio Data
          messageStreamID: 1,
          body: params.body
        },
        params.chunkSize
      ));
    };
  
    clearQueuedRTMPMessages = function (stream) {
      if (queuedRTMPMessages[stream.id] != null) {
        return (queuedRTMPMessages[stream.id] = []);
      }
    };
  
    queueRTMPMessages = function (stream, messages, params) {
      var j, len1, message;
      for (j = 0, len1 = messages.length; j < len1; j++) {
        message = messages[j];
        message.originalTimestamp = message.timestamp;
      }
      if (queuedRTMPMessages[stream.id] != null) {
        queuedRTMPMessages[stream.id].push(...messages);
      } else {
        queuedRTMPMessages[stream.id] = [...messages]; // Prevent copying array as reference
      }
      return flushRTMPMessages(stream, params);
    };
  
    queueVideoMessage = function (stream, params) {
      params.avType = 'video';
      params.chunkStreamID = 4;
      params.messageTypeID = 0x09; // Video Data
      params.messageStreamID = 1;
      params.originalTimestamp = params.timestamp;
      if (queuedRTMPMessages[stream.id] != null) {
        queuedRTMPMessages[stream.id].push(params);
      } else {
        queuedRTMPMessages[stream.id] = [params];
      }
      return setImmediate(function () {
        return flushRTMPMessages(stream);
      });
    };
  
    queueAudioMessage = function (stream, params) {
      params.avType = 'audio';
      params.chunkStreamID = 4;
      params.messageTypeID = 0x08; // Audio Data
      params.messageStreamID = 1;
      params.originalTimestamp = params.timestamp;
      if (queuedRTMPMessages[stream.id] != null) {
        queuedRTMPMessages[stream.id].push(params);
      } else {
        queuedRTMPMessages[stream.id] = [params];
      }
      return setImmediate(function () {
        return flushRTMPMessages(stream);
      });
    };
  
    createVideoMessage = function (params) {
      var videoMessage;
      // TODO: Use type 1/2/3
      return (videoMessage = createRTMPMessage(
        {
          chunkStreamID: 4,
          timestamp: params.timestamp,
          messageTypeID: 0x09, // Video Data
          messageStreamID: 1,
          body: params.body
        },
        params.chunkSize
      ));
    };
  
    parseUserControlMessage = function (buf) {
      var eventData, eventType, message;
      eventType = (buf[0] << 8) + buf[1];
      eventData = buf.slice(2);
      message = {
        eventType: eventType,
        eventData: eventData
      };
      if (eventType === 3) {
        // SetBufferLength
        // first 4 bytes: stream ID
        message.streamID =
          (eventData[0] << 24) +
          (eventData[1] << 16) +
          (eventData[2] << 8) +
          eventData[3];
        // next 4 bytes: buffer length in milliseconds
        message.bufferLength =
          (eventData[4] << 24) +
          (eventData[5] << 16) +
          (eventData[6] << 8) +
          eventData[7];
      }
      return message;
    };
  
    parseIEEE754Double = function (buf) {
      var bitIndex, bitValue, byteIndex, exponent, fraction, i, j, sign, value;
      sign = buf[0] >> 7; // 1 == negative
      exponent = ((buf[0] & 0b1111111) << 4) + (buf[1] >> 4);
      exponent -= 1023; // because 1023 means zero
      fraction = 1;
      for (i = j = 0; j <= 51; i = ++j) {
        byteIndex = 1 + parseInt((i + 4) / 8);
        bitIndex = 7 - ((i + 4) % 8);
        bitValue = (buf[byteIndex] >> bitIndex) & 0b1;
        if (bitValue > 0) {
          fraction += Math.pow(2, -(i + 1));
        }
      }
      value = fraction * Math.pow(2, exponent);
      if (sign === 1) {
        value = -value;
      }
      return value;
    };
  
    parseAMF0StrictArray = function (buf) {
      var arr, len, readLen, result;
      arr = [];
      len = (buf[0] << 24) + (buf[1] << 16) + (buf[2] << 8) + buf[3];
      readLen = 4;
      while (--len >= 0) {
        result = parseAMF0Data(buf.slice(readLen));
        arr.push(result.value);
        readLen += result.readLen;
      }
      return {
        value: arr,
        readLen: readLen
      };
    };
  
    parseAMF0ECMAArray = function (buf) {
      var count, result;
      // associative-count
      count = (buf[0] << 24) + (buf[1] << 16) + (buf[2] << 8) + buf[3];
      result = parseAMF0Object(buf.slice(4), count);
      result.readLen += 4;
      return result;
    };
  
    parseAMF0Object = function (buf, maxItems = null) {
      var bufLen, items, name, nameLen, obj, readLen, result;
      obj = {};
      bufLen = buf.length;
      readLen = 0;
      items = 0;
      if (maxItems == null || maxItems > 0) {
        while (readLen < bufLen) {
          nameLen = (buf[readLen++] << 8) + buf[readLen++];
          if (nameLen > 0) {
            // object-end-marker will follow
            name = buf.toString('utf8', readLen, readLen + nameLen);
            readLen += nameLen;
          } else {
            name = null;
          }
          result = parseAMF0Data(buf.slice(readLen));
          readLen += result.readLen;
          if (result.type === 'object-end-marker') {
            break;
          } else {
            items++;
            if (maxItems != null && items > maxItems) {
              logger.warn(
                `warn: illegal AMF0 data: force break because items (${items}) > maxItems (${maxItems})`
              );
              break;
            }
          }
          if (name != null) {
            obj[name] = result.value;
          } else {
            logger.warn(
              `warn: illegal AMF0 data: object key for value ${result.value} is zero length`
            );
          }
        }
      }
      return {
        value: obj,
        readLen: readLen
      };
    };
  
    // Do opposite of parseAMF0DataMessage()
    serializeAMF0DataMessage = function (parsedObject) {
      var bufs, j, len1, object, ref;
      bufs = [];
      ref = parsedObject.objects;
      for (j = 0, len1 = ref.length; j < len1; j++) {
        object = ref[j];
        bufs.push(createAMF0Data(object.value));
      }
      return Buffer.concat(bufs);
    };
  
    // Decode AMF0 data message buffer into AMF0 packets
    parseAMF0DataMessage = function (buf) {
      var amf0Packets, remainingLen, result;
      amf0Packets = [];
      remainingLen = buf.length;
      while (remainingLen > 0) {
        result = parseAMF0Data(buf);
        amf0Packets.push(result);
        remainingLen -= result.readLen;
        buf = buf.slice(result.readLen);
      }
      return {
        objects: amf0Packets
      };
    };
  
    // Decode buffer into AMF0 packets
    parseAMF0CommandMessage = function (buf) {
      var amf0Packets, e, remainingLen, result;
      amf0Packets = [];
      remainingLen = buf.length;
      while (remainingLen > 0) {
        try {
          result = parseAMF0Data(buf);
        } catch (error) {
          e = error;
          logger.error('[rtmp] error parsing AMF0 command (maybe a bug); buf:');
          logger.error(buf);
          throw e;
        }
        amf0Packets.push(result);
        remainingLen -= result.readLen;
        buf = buf.slice(result.readLen);
      }
      return {
        command: amf0Packets[0].value,
        transactionID: amf0Packets[1].value,
        objects: amf0Packets.slice(2)
      };
    };
  
    parseAMF0Data = function (buf) {
      var date, i, result, strLen, time, type, value;
      i = 0;
      type = buf[i++];
      if (type === 0x00) {
        // number-marker
        value = buf.readDoubleBE(i);
        return {
          type: 'number',
          value: value,
          readLen: i + 8
        };
      } else if (type === 0x01) {
        // boolean-marker
        value = buf[i] === 0x00 ? false : true;
        return {
          type: 'boolean',
          value: value,
          readLen: i + 1
        };
      } else if (type === 0x02) {
        // string-marker
        strLen = (buf[i++] << 8) + buf[i++];
        value = buf.toString('utf8', i, i + strLen);
        return {
          type: 'string',
          value: value,
          readLen: i + strLen
        };
      } else if (type === 0x03) {
        // object-marker
        result = parseAMF0Object(buf.slice(i));
        return {
          type: 'object',
          value: result.value,
          readLen: i + result.readLen
        };
      } else if (type === 0x05) {
        // null-marker
        return {
          type: 'null',
          value: null,
          readLen: i
        };
      } else if (type === 0x06) {
        // undefined-marker
        return {
          type: 'undefined',
          value: void 0,
          readLen: i
        };
      } else if (type === 0x08) {
        // ecma-array-marker
        result = parseAMF0ECMAArray(buf.slice(i));
        return {
          type: 'array',
          value: result.value,
          readLen: i + result.readLen
        };
      } else if (type === 0x09) {
        // object-end-marker
        return {
          type: 'object-end-marker',
          readLen: i
        };
      } else if (type === 0x0a) {
        // strict-array-marker
        result = parseAMF0StrictArray(buf.slice(i));
        return {
          type: 'strict-array',
          value: result.value,
          readLen: i + result.readLen
        };
      } else if (type === 0x0b) {
        // date-marker
        time = buf.readDoubleBE(i);
        date = new Date(time);
        return {
          type: 'date',
          value: date,
          readLen: i + 10 // 8 (time) + 2 (time-zone)
        };
      } else {
        throw new Error(`Unknown AMF0 data type: ${type}`);
      }
    };
  
    createAMF0Data = function (data) {
      var buf, strBytes, strLen, type;
      type = typeof data;
      buf = null;
      if (type === 'number') {
        buf = Buffer.alloc(9);
        buf[0] = 0x00; // number-marker
        buf.writeDoubleBE(data, 1);
      } else if (type === 'boolean') {
        buf = Buffer.alloc(2);
        buf[0] = 0x01; // boolean-marker
        buf[1] = data ? 0x01 : 0x00;
      } else if (type === 'string') {
        buf = Buffer.alloc(3);
        buf[0] = 0x02; // string-marker
        strBytes = Buffer.from(data, 'utf8');
        strLen = strBytes.length;
        buf[1] = (strLen >> 8) & 0xff;
        buf[2] = strLen & 0xff;
        buf = Buffer.concat([buf, strBytes], 3 + strLen);
      } else if (data === null) {
        buf = Buffer.from([0x05]); // null-marker
      } else if (type === 'undefined') {
        buf = Buffer.from([0x06]); // undefined-marker
      } else if (data instanceof Date) {
        buf = Buffer.alloc(11);
        buf[0] = 0x0b; // date-marker
        buf.writeDoubleBE(data.getTime(), 1);
        // Time-zone should be 0x0000
        buf[9] = 0;
        buf[10] = 0;
      } else if (data instanceof Array) {
        buf = Buffer.from([0x0a]); // strict-array-marker
        buf = createAMF0StrictArray(data, buf);
      } else if (type === 'object') {
        buf = createAMF0Object(data);
      } else {
        throw new Error(`Unknown data type \"${type}\" for data ${data}`);
      }
      return buf;
    };
  
    createAMF0StrictArray = function (arr, buf = null) {
      var arrLen, bufs, i, j, len1, totalLength, value, valueBytes;
      bufs = [];
      totalLength = 0;
      if (buf != null) {
        bufs.push(buf);
        totalLength += buf.length;
      }
      // array-count (U32)
      arrLen = arr.length;
      bufs.push(
        Buffer.from([
          (arrLen >>> 24) & 0xff,
          (arrLen >>> 16) & 0xff,
          (arrLen >>> 8) & 0xff,
          arrLen & 0xff
        ])
      );
      totalLength += 4;
      for (i = j = 0, len1 = arr.length; j < len1; i = ++j) {
        value = arr[i];
        valueBytes = createAMF0Data(value);
        bufs.push(valueBytes);
        totalLength += valueBytes.length;
      }
      return Buffer.concat(bufs, totalLength);
    };
  
    createAMF0Object = function (obj) {
      var buf;
      buf = Buffer.from([0x03]); // object-marker
      return createAMF0PropertyList(obj, buf);
    };
  
    createAMF0ECMAArray = function (obj) {
      var buf, count;
      count = Object.keys(obj).length;
      buf = Buffer.from([
        // ecma-array-marker
        0x08,
        // array-count
        (count >>> 24) & 0xff,
        (count >>> 16) & 0xff,
        (count >>> 8) & 0xff,
        count & 0xff
      ]);
      return createAMF0PropertyList(obj, buf);
    };
  
    createAMF0PropertyList = function (obj, buf = null) {
      var bufs,
        dataBytes,
        name,
        nameBytes,
        nameLen,
        nameLenBytes,
        totalLength,
        value;
      bufs = [];
      totalLength = 0;
      if (buf != null) {
        bufs.push(buf);
        totalLength += buf.length;
      }
      for (name in obj) {
        value = obj[name];
        nameBytes = Buffer.from(name, 'utf8');
        nameLen = nameBytes.length;
        nameLenBytes = Buffer.alloc(2);
        nameLenBytes[0] = (nameLen >> 8) & 0xff;
        nameLenBytes[1] = nameLen & 0xff;
        dataBytes = createAMF0Data(value);
        bufs.push(nameLenBytes, nameBytes, dataBytes);
        totalLength += 2 + nameLen + dataBytes.length;
      }
      // Add object-end-marker
      bufs.push(Buffer.from([0x00, 0x00, 0x09]));
      totalLength += 3;
      return Buffer.concat(bufs, totalLength);
    };
  
    counter = 0;
  
    flushRTMPMessages = function (stream, params) {
      var allSessions,
        buf,
        bufs,
        clientID,
        filteredMsgs,
        i,
        j,
        k,
        l,
        len1,
        len2,
        len3,
        len4,
        len5,
        len6,
        m,
        msgs,
        n,
        o,
        ref,
        rtmpMessage,
        rtmpMessagesToSend,
        session;
      if (stream == null) {
        logger.error('[rtmp] error: flushRTMPMessages: Invalid stream');
        return;
      }
      if (
        (params != null ? params.forceFlush : void 0) !== true &&
        queuedRTMPMessages[stream.id].length < config.rtmpMessageQueueSize
      ) {
        return;
      }
      // not enough buffer
      rtmpMessagesToSend = queuedRTMPMessages[stream.id];
      queuedRTMPMessages[stream.id] = [];
      for (i = j = 0, len1 = rtmpMessagesToSend.length; j < len1; i = ++j) {
        rtmpMessage = rtmpMessagesToSend[i];
        rtmpMessage.index = i;
      }
      // Move audio before video if the PTS are the same
      rtmpMessagesToSend.sort(function (a, b) {
        var cmp;
        cmp = a.originalTimestamp - b.originalTimestamp;
        if (cmp === 0) {
          if (a.avType == null || b.avType == null) {
            cmp = 0;
          } else if (a.avType === b.avType) {
            cmp = 0;
          } else if (a.avType === 'audio') {
            // a=audio b=video
            cmp = -1;
          } else if (b.avType === 'audio') {
            // a=video b=audio
            cmp = 1;
          }
        }
        if (cmp === 0) {
          cmp = a.index - b.index; // keep the original order
        }
        return cmp;
      });
      if (rtmpMessagesToSend.length === 0) {
        return;
      }
      // nothing to send
      allSessions = [];
      for (clientID in rtmptSessions) {
        session = rtmptSessions[clientID];
        allSessions.push(session.rtmpSession);
      }
      for (clientID in sessions) {
        session = sessions[clientID];
        allSessions.push(session);
      }
      for (k = 0, len2 = allSessions.length; k < len2; k++) {
        session = allSessions[k];
        if (((ref = session.stream) != null ? ref.id : void 0) !== stream.id) {
          // The session is not associated with current stream
          continue;
        }
        msgs = null;
        // If video starts with an inter frame, Flash Player might
        // shows images looks like a glitch until the first keyframe.
        if (session.isWaitingForKeyFrame) {
          if (config.rtmpWaitForKeyFrame) {
            if (stream.isVideoStarted) {
              // has video stream
              for (
                i = l = 0, len3 = rtmpMessagesToSend.length;
                l < len3;
                i = ++l
              ) {
                rtmpMessage = rtmpMessagesToSend[i];
                if (rtmpMessage.avType === 'video' && rtmpMessage.isKeyFrame) {
                  logger.info(
                    `[rtmp:client=${session.clientid}] started playing stream ${stream.id}`
                  );
                  session.startPlaying();
                  session.playStartTimestamp = rtmpMessage.originalTimestamp;
                  session.playStartDateTime = Date.now(); // TODO: Should we use slower process.hrtime()?
                  session.isWaitingForKeyFrame = false;
                  msgs = rtmpMessagesToSend.slice(i);
                  break; // audio only
                }
              }
            } else {
              logger.info(
                `[rtmp:client=${session.clientid}] started playing stream ${stream.id}`
              );
              session.startPlaying();
              session.playStartTimestamp =
                rtmpMessagesToSend[0].originalTimestamp;
              session.playStartDateTime = Date.now();
              session.isWaitingForKeyFrame = false;
              msgs = rtmpMessagesToSend; // Do not wait for a keyframe
            }
          } else {
            logger.info(
              `[rtmp:client=${session.clientid}] started playing stream ${stream.id}`
            );
            session.startPlaying();
            session.playStartTimestamp = rtmpMessagesToSend[0].originalTimestamp;
            session.playStartDateTime = Date.now();
            session.isWaitingForKeyFrame = false;
            msgs = rtmpMessagesToSend;
          }
        } else {
          msgs = rtmpMessagesToSend;
        }
        if (msgs == null) {
          continue;
        }
        if (session.isPlaying) {
          for (m = 0, len4 = msgs.length; m < len4; m++) {
            rtmpMessage = msgs[m];
            // get milliseconds elapsed since play start
            rtmpMessage.timestamp =
              session.getScaledTimestamp(rtmpMessage.originalTimestamp) %
              TIMESTAMP_ROUNDOFF;
          }
          if (session.isResuming) {
            // Remove audio messages which are already sent until
            // the first video message comes
            filteredMsgs = [];
            for (i = n = 0, len5 = msgs.length; n < len5; i = ++n) {
              rtmpMessage = msgs[i];
              if (rtmpMessage.avType == null) {
                filteredMsgs.push(rtmpMessage);
              } else if (rtmpMessage.avType === 'video') {
                filteredMsgs.push(...msgs.slice(i));
                session.isResuming = false;
                break;
              } else if (rtmpMessage.timestamp > session.lastSentTimestamp) {
                filteredMsgs.push(rtmpMessage);
              } else {
                logger.debug(
                  `[rtmp:client=${session.clientid}] skipped message (timestamp=${rtmpMessage.timestamp} <= lastSentTimestamp=${session.lastSentTimestamp})`
                );
              }
            }
          } else {
            filteredMsgs = msgs;
          }
          if (
            (params != null ? params.hasControlMessage : void 0) !== true &&
            filteredMsgs.length > 1
          ) {
            buf = createRTMPAggregateMessage(filteredMsgs, session.chunkSize);
            if (DEBUG_OUTGOING_RTMP_PACKETS) {
              logger.info(
                `send RTMP agg msg: ${buf.length} bytes; time=` +
                  filteredMsgs
                    .map(function (item) {
                      var ref1, ref2;
                      return `${
                        (ref1 =
                          (ref2 = item.avType) != null ? ref2[0] : void 0) != null
                          ? ref1
                          : 'other'
                      }${item.avType === 'video' && item.isKeyFrame ? '(key)' : ''}:${item.timestamp}${item.avType === 'video' && item.compositionTime !== 0 ? `(cmp=${item.timestamp + item.compositionTime})` : ''}`;
                    })
                    .join(',')
              );
            }
            session.sendData(buf);
          } else {
            bufs = [];
            for (o = 0, len6 = filteredMsgs.length; o < len6; o++) {
              rtmpMessage = filteredMsgs[o];
              bufs.push(createRTMPMessage(rtmpMessage, session.chunkSize));
            }
            buf = Buffer.concat(bufs);
            if (DEBUG_OUTGOING_RTMP_PACKETS) {
              logger.info(
                `send RTMP msg: ${buf.length} bytes; time=` +
                  filteredMsgs
                    .map(function (item) {
                      var ref1, ref2;
                      return `${
                        (ref1 =
                          (ref2 = item.avType) != null ? ref2[0] : void 0) != null
                          ? ref1
                          : 'other'
                      }:${item.timestamp}`;
                    })
                    .join(',')
              );
            }
            session.sendData(buf);
          }
          session.lastSentTimestamp =
            filteredMsgs[filteredMsgs.length - 1].timestamp;
        }
      }
    };
  
    // RTMP Message Header used in Aggregate Message
    createMessageHeader = function (params) {
      var payloadLength;
      payloadLength = params.body.length;
      if (params.messageTypeID == null) {
        logger.warn(
          '[rtmp] warning: createMessageHeader(): messageTypeID is not set'
        );
      }
      if (params.timestamp == null) {
        logger.warn(
          '[rtmp] warning: createMessageHeader(): timestamp is not set'
        );
      }
      if (params.messageStreamID == null) {
        logger.warn(
          '[rtmp] warning: createMessageHeader(): messageStreamID is not set'
        );
      }
      // 6.1.1.  Message Header
      return Buffer.from([
        params.messageTypeID,
        // Payload length (3 bytes) big-endian
        (payloadLength >> 16) & 0xff,
        (payloadLength >> 8) & 0xff,
        payloadLength & 0xff,
        // Timestamp (4 bytes) big-endian (unusual format; not sure)
        (params.timestamp >>> 16) & 0xff,
        (params.timestamp >>> 8) & 0xff,
        params.timestamp & 0xff,
        (params.timestamp >>> 24) & 0xff,
        // Stream ID (3 bytes) big-endian
        (params.messageStreamID >> 16) & 0xff,
        (params.messageStreamID >> 8) & 0xff,
        params.messageStreamID & 0xff
      ]);
    };
  
    // All sub-messages must have the same chunk stream ID
    createRTMPAggregateMessage = function (rtmpMessages, chunkSize) {
      var aggregateBody,
        aggregateTimestamp,
        bufs,
        header,
        j,
        len,
        len1,
        rtmpMessage,
        totalLength;
      bufs = [];
      totalLength = 0;
      aggregateTimestamp = null;
      for (j = 0, len1 = rtmpMessages.length; j < len1; j++) {
        rtmpMessage = rtmpMessages[j];
        if (aggregateTimestamp == null) {
          aggregateTimestamp = rtmpMessage.timestamp;
        }
        header = createMessageHeader(rtmpMessage);
        len = header.length + rtmpMessage.body.length;
        // Back pointer (UI32)
        bufs.push(
          header,
          rtmpMessage.body,
          Buffer.from([
            (len >>> 24) & 0xff,
            (len >>> 16) & 0xff,
            (len >>> 8) & 0xff,
            len & 0xff
          ])
        );
        totalLength += len + 4;
      }
      aggregateBody = Buffer.concat(bufs, totalLength);
      return createRTMPMessage(
        {
          chunkStreamID: 4,
          timestamp: aggregateTimestamp,
          messageTypeID: 22, // Aggregate Message
          messageStreamID: 1,
          body: aggregateBody
        },
        chunkSize
      );
    };
  
    createRTMPType1Message = function (params) {
      var body,
        bodyLength,
        extendedTimestamp,
        formatTypeID,
        header,
        ordinaryTimestampBytes,
        useExtendedTimestamp;
      bodyLength = params.body.length;
      formatTypeID = 1;
      if (params.body == null) {
        logger.warn(
          '[rtmp] warning: createRTMPType1Message(): body is not set for RTMP message'
        );
      }
      if (params.chunkStreamID == null) {
        logger.warn(
          '[rtmp] warning: createRTMPType1Message(): chunkStreamID is not set for RTMP message'
        );
      }
      if (params.timestampDelta == null) {
        logger.warn(
          '[rtmp] warning: createRTMPType1Message(): timestampDelta is not set for RTMP message'
        );
      }
      if (params.messageStreamID == null) {
        logger.warn(
          '[rtmp] warning: createRTMPType1Message(): messageStreamID is not set for RTMP message'
        );
      }
      useExtendedTimestamp = false;
      if (params.timestampDelta >= 0xffffff) {
        useExtendedTimestamp = true;
        ordinaryTimestampBytes = [0xff, 0xff, 0xff];
      } else {
        ordinaryTimestampBytes = [
          (params.timestampDelta >> 16) & 0xff,
          (params.timestampDelta >> 8) & 0xff,
          params.timestampDelta & 0xff
        ];
      }
      // Header for Type 1 Chunk Message Header
      header = Buffer.from([
        // Format (2 bits), Chunk Stream ID (6 bits)
        (formatTypeID << 6) | params.chunkStreamID,
        // Timestamp Delta (3 bytes)
        ordinaryTimestampBytes[0],
        ordinaryTimestampBytes[1],
        ordinaryTimestampBytes[2],
        // Message Length (3 bytes)
        (bodyLength >> 16) & 0xff,
        (bodyLength >> 8) & 0xff,
        bodyLength & 0xff,
        // Message Type ID (1 byte)
        params.messageTypeID
      ]);
      if (useExtendedTimestamp) {
        extendedTimestamp = Buffer.from([
          (params.timestampDelta >> 24) & 0xff,
          (params.timestampDelta >> 16) & 0xff,
          (params.timestampDelta >> 8) & 0xff,
          params.timestampDelta & 0xff
        ]);
        header = Buffer.concat([header, extendedTimestamp], 12);
      }
      body = params.body;
      return Buffer.concat([header, body], 8 + bodyLength);
    };
  
    createRTMPMessage = function (params, chunkSize = 128) {
      var body,
        bodyChunk,
        bodyChunkLen,
        bodyLength,
        bufs,
        extendedTimestampBuf,
        formatTypeID,
        timestamp,
        totalLength,
        type3Header,
        useExtendedTimestamp;
      bodyLength = params.body.length;
      // TODO: Use format type ID 1 and 2
      formatTypeID = 0;
      if (params.body == null) {
        logger.warn(
          '[rtmp] warning: createRTMPMessage(): body is not set for RTMP message'
        );
      }
      if (params.chunkStreamID == null) {
        logger.warn(
          '[rtmp] warning: createRTMPMessage(): chunkStreamID is not set for RTMP message'
        );
      }
      if (params.timestamp == null) {
        logger.warn(
          '[rtmp] warning: createRTMPMessage(): timestamp is not set for RTMP message'
        );
      }
      if (params.messageStreamID == null) {
        logger.warn(
          '[rtmp] warning: createRTMPMessage(): messageStreamID is not set for RTMP message'
        );
      }
      useExtendedTimestamp = false;
      if (params.timestamp >= 0xffffff) {
        useExtendedTimestamp = true;
        timestamp = [0xff, 0xff, 0xff];
        extendedTimestampBuf = Buffer.from([
          (params.timestamp >> 24) & 0xff,
          (params.timestamp >> 16) & 0xff,
          (params.timestamp >> 8) & 0xff,
          params.timestamp & 0xff
        ]);
      } else {
        timestamp = [
          (params.timestamp >> 16) & 0xff,
          (params.timestamp >> 8) & 0xff,
          params.timestamp & 0xff
        ];
      }
      bufs = [
        // Header for Type 0 Chunk Message Header
        Buffer.from([
          // Format (2 bits), Chunk Stream ID (6 bits)
          (formatTypeID << 6) | params.chunkStreamID,
          // Timestamp (3 bytes)
          timestamp[0],
          timestamp[1],
          timestamp[2],
          // Message Length (3 bytes)
          (bodyLength >> 16) & 0xff,
          (bodyLength >> 8) & 0xff,
          bodyLength & 0xff,
          // Message Type ID (1 byte)
          params.messageTypeID,
          // Message Stream ID (4 bytes) little-endian
          params.messageStreamID & 0xff,
          (params.messageStreamID >>> 8) & 0xff,
          (params.messageStreamID >>> 16) & 0xff,
          (params.messageStreamID >>> 24) & 0xff
        ])
      ];
      totalLength = 12;
      if (useExtendedTimestamp) {
        bufs.push(extendedTimestampBuf);
        totalLength += 4;
      }
      body = params.body;
      if (bodyLength > chunkSize) {
        bufs.push(body.slice(0, chunkSize));
        totalLength += chunkSize;
        body = body.slice(chunkSize);
        bodyLength -= chunkSize;
        // Use Format Type 3 for remaining chunks
        type3Header = Buffer.from([(3 << 6) | params.chunkStreamID]);
        if (useExtendedTimestamp) {
          type3Header = Buffer.concat([type3Header, extendedTimestampBuf]);
        }
        while (true) {
          bodyChunk = body.slice(0, chunkSize);
          bodyChunkLen = bodyChunk.length;
          bufs.push(type3Header, bodyChunk);
          totalLength += type3Header.length + bodyChunkLen;
          body = body.slice(bodyChunkLen);
          bodyLength -= bodyChunkLen;
          if (bodyLength === 0) {
            break;
          }
        }
      } else {
        bufs.push(body);
        totalLength += bodyLength;
      }
      return Buffer.concat(bufs, totalLength);
    };
  
    createAMF0DataMessage = function (params, chunkSize) {
      return createRTMPMessage(createAMF0DataMessageParams(params), chunkSize);
    };
  
    createAMF0DataMessageParams = function (params) {
      var amf0Bytes, j, len, len1, obj, ref;
      len = 0;
      ref = params.objects;
      for (j = 0, len1 = ref.length; j < len1; j++) {
        obj = ref[j];
        len += obj.length;
      }
      amf0Bytes = Buffer.concat(params.objects, len);
      return {
        chunkStreamID: params.chunkStreamID,
        timestamp: params.timestamp,
        messageTypeID: 0x12, // AMF0 Data
        messageStreamID: params.messageStreamID,
        body: amf0Bytes
      };
    };
  
    createAMF0CommandMessage = function (params, chunkSize) {
      return createRTMPMessage(createAMF0CommandMessageParams(params), chunkSize);
    };
  
    createAMF0CommandMessageParams = function (params) {
      var amf0Bytes, commandBuf, j, len, len1, obj, ref, transactionIDBuf;
      commandBuf = createAMF0Data(params.command);
      transactionIDBuf = createAMF0Data(params.transactionID);
      len = commandBuf.length + transactionIDBuf.length;
      ref = params.objects;
      for (j = 0, len1 = ref.length; j < len1; j++) {
        obj = ref[j];
        len += obj.length;
      }
      amf0Bytes = Buffer.concat(
        [commandBuf, transactionIDBuf, ...params.objects],
        len
      );
      return {
        chunkStreamID: params.chunkStreamID,
        timestamp: params.timestamp,
        messageTypeID: 0x14, // AMF0 Command
        messageStreamID: params.messageStreamID,
        body: amf0Bytes
      };
    };
  
    RTMPSession = class RTMPSession {
      constructor(socket) {
        logger.debug('[rtmp] created a new session');
        this.listeners = {};
        this.state = SESSION_STATE_NEW;
        this.socket = socket;
        this.chunkSize = 128;
        this.receiveChunkSize = 128;
        this.previousChunkMessage = {};
        this.isPlaying = false;
        this.clientid = generateNewClientID();
        this.useEncryption = false;
        this.receiveTimestamp = null;
        this.lastSentAckBytes = 0;
        this.receivedBytes = 0;
        this.stream = null; // AVStream
        this.seekedDuringPause = false;
        this.lastSentTimestamp = null;
        this.isResuming = false;
        // Some broadcaster software like Wirecast does not send Window Acknowledgement Size,
        // so it seems we have to set a default value.
        this.windowAckSize = 2500000;
      }
  
      toString() {
        return `${this.clientid}: addr=${this.socket.remoteAddress} port=${this.socket.remotePort}`;
      }
  
      startPlaying() {
        this.isPlaying = true;
        return (this.isResuming = false);
      }
  
      parseVideoMessage(buf) {
        var info, isEOS, nalUnitGlob, nalUnits, pps, sps;
        info = flv.parseVideo(buf);
        nalUnitGlob = null;
        isEOS = false;
        switch (info.videoDataTag.avcPacketType) {
          case flv.AVC_PACKET_TYPE_SEQUENCE_HEADER:
            // Retain AVC configuration
            this.avcInfo = info.avcDecoderConfigurationRecord;
            if (this.avcInfo.numOfSPS > 1) {
              logger.warn(
                `warn: flv:parseVideo(): numOfSPS is ${numOfSPS} > 1 (may not work)`
              );
            }
            if (this.avcInfo.numOfPPS > 1) {
              logger.warn(
                `warn: flv:parseVideo(): numOfPPS is ${numOfPPS} > 1 (may not work)`
              );
            }
            sps = h264.concatWithStartCodePrefix(this.avcInfo.sps);
            pps = h264.concatWithStartCodePrefix(this.avcInfo.pps);
            nalUnitGlob = Buffer.concat([sps, pps]);
            break;
          case flv.AVC_PACKET_TYPE_NALU:
            if (this.avcInfo == null) {
              throw new Error(
                '[rtmp:publish] malformed video data: avcInfo is missing'
              );
            }
            // TODO: This must be too heavy and needs better alternative.
            nalUnits = flv.splitNALUnits(
              info.nalUnits,
              this.avcInfo.nalUnitLengthSize
            );
            nalUnitGlob = h264.concatWithStartCodePrefix(nalUnits);
            break;
          case flv.AVC_PACKET_TYPE_EOS:
            isEOS = true;
            break;
          default:
            throw new Error(
              `unknown AVCPacketType: ${flv.AVC_PACKET_TYPE_SEQUENCE_HEADER}`
            );
        }
        return {
          info: info,
          nalUnitGlob: nalUnitGlob,
          isEOS: isEOS
        };
      }
  
      parseAudioMessage(buf) {
        var adtsFrame, adtsHeader, info, stream;
        info = flv.parseAudio(buf);
        adtsFrame = null;
        stream = this.stream;
        if (stream == null) {
          throw new Error('[rtmp] Stream not set for this session');
        }
        switch (info.audioDataTag.aacPacketType) {
          case flv.AAC_PACKET_TYPE_SEQUENCE_HEADER:
            if (info.audioSpecificConfig != null) {
              stream.updateConfig({
                audioSpecificConfig: info.audioSpecificConfig,
                audioASCInfo: info.ascInfo
              });
            } else {
              logger.warn('[rtmp] skipping empty AudioSpecificConfig');
            }
            break;
          case flv.AAC_PACKET_TYPE_RAW:
            if (stream.audioASCInfo == null) {
              logger.error(
                '[rtmp:publish] malformed audio data: AudioSpecificConfig is missing'
              );
            }
            // TODO: This must be a little heavy and needs better alternative.
            adtsHeader = Buffer.from(
              aac.createADTSHeader(stream.audioASCInfo, info.rawDataBlock.length)
            );
            adtsFrame = Buffer.concat([adtsHeader, info.rawDataBlock]);
            break;
          default:
            throw new Error(
              `[rtmp:publish] unknown AAC_PACKET_TYPE: ${info.audioDataTag.aacPacketType}`
            );
        }
        return {
          info: info,
          adtsFrame: adtsFrame
        };
      }
  
      clearTimeout() {
        if (this.timeoutTimer != null) {
          clearTimeout(this.timeoutTimer);
          return (this.timeoutTimer = null);
        }
      }
  
      scheduleTimeout() {
        if (this.isTearedDown) {
          return;
        }
        this.clearTimeout();
        this.lastTimeoutScheduledTime = Date.now();
        return (this.timeoutTimer = setTimeout(() => {
          if (this.isTearedDown) {
            return;
          }
          if (this.timeoutTimer == null) {
            return;
          }
          if (
            Date.now() - this.lastTimeoutScheduledTime <
            config.rtmpSessionTimeoutMs
          ) {
            return;
          }
          logger.info(`[rtmp:client=${this.clientid}] session timeout`);
          return this.teardown();
        }, config.rtmpSessionTimeoutMs));
      }
  
      schedulePing() {
        this.lastPingScheduledTime = Date.now();
        if (this.pingTimer != null) {
          clearTimeout(this.pingTimer);
        }
        return (this.pingTimer = setTimeout(() => {
          if (
            Date.now() - this.lastPingScheduledTime <
            config.rtmpPingTimeoutMs
          ) {
            logger.debug('[rtmp] ping timeout canceled');
          }
          return this.ping();
        }, config.rtmpPingTimeoutMs));
      }
  
      ping() {
        var currentTimestamp, pingRequest;
        currentTimestamp = this.getCurrentTimestamp();
        pingRequest = createRTMPMessage({
          chunkStreamID: 2,
          timestamp: currentTimestamp,
          messageTypeID: 0x04, // User Control Message
          messageStreamID: 0,
          body: Buffer.from([
            // Event Type: 6=PingRequest
            0,
            6,
            // Server Timestamp
            (currentTimestamp >> 24) & 0xff,
            (currentTimestamp >> 16) & 0xff,
            (currentTimestamp >> 8) & 0xff,
            currentTimestamp & 0xff
          ])
        });
        return this.sendData(pingRequest);
      }
  
      stopPlaying() {
        this.isPlaying = false;
        return (this.isWaitingForKeyFrame = false);
      }
  
      teardown() {
        var base, e, ref;
        if (this.isTearedDown) {
          logger.debug('[rtmp] already teared down');
          return;
        }
        this.isTearedDown = true;
        this.clearTimeout();
        this.stopPlaying();
        if (
          ((ref = this.stream) != null ? ref.type : void 0) ===
          avstreams.STREAM_TYPE_RECORDED
        ) {
          if (typeof (base = this.stream).teardown === 'function') {
            base.teardown();
          }
        }
        if (this.cipherIn != null) {
          this.cipherIn.final();
          this.cipherIn = null;
        }
        if (this.cipherOut != null) {
          this.cipherOut.final();
          this.cipherOut = null;
        }
        try {
          this.socket.end();
        } catch (error) {
          e = error;
          logger.error(`[rtmp] socket.end error: ${e}`);
        }
        return this.emit('teardown');
      }
  
      getCurrentTimestamp() {
        return Date.now() - this.playStartDateTime;
      }
  
      getScaledTimestamp(timestamp) {
        var ts;
        ts = timestamp - this.playStartTimestamp;
        if (ts < 0) {
          ts = 0;
        }
        return ts;
      }
  
      createVideoMessage(params) {
        params.chunkStreamID = 4;
        params.messageTypeID = 0x09; // Video Data
        params.messageStreamID = 1;
        return this.createAVMessage(params);
      }
  
      createAudioMessage(params) {
        params.chunkStreamID = 4;
        params.messageTypeID = 0x08; // Audio Data
        params.messageStreamID = 1;
        return this.createAVMessage(params);
      }
  
      createAVMessage(params) {
        var msg, thisTimestamp;
        thisTimestamp = this.getScaledTimestamp(params.timestamp);
        if (
          this.lastAVTimestamp != null &&
          params.body.length <= this.chunkSize
        ) {
          // Use Type 1 if chunking is not needed
          params.timestampDelta =
            (thisTimestamp - this.lastAVTimestamp) % TIMESTAMP_ROUNDOFF;
          msg = createRTMPType1Message(params);
        } else {
          // Use Type 0
          msg = createRTMPMessage(params, this.chunkSize);
        }
        this.lastAVTimestamp = thisTimestamp;
        return msg;
      }
  
      concatenate(arr) {
        var i, item, j, len, len1;
        if (Buffer.isBuffer(arr)) {
          return arr;
        }
        if (!(arr instanceof Array)) {
          return;
        }
        len = 0;
        for (i = j = 0, len1 = arr.length; j < len1; i = ++j) {
          item = arr[i];
          if (item != null) {
            len += item.length;
          } else {
            arr[i] = Buffer.alloc(0);
          }
        }
        return Buffer.concat(arr, len);
      }
  
      emit(event, ...args) {
        var j, len1, listener, ref;
        if (this.listeners[event] == null) {
          return;
        }
        ref = this.listeners[event];
        for (j = 0, len1 = ref.length; j < len1; j++) {
          listener = ref[j];
          listener(...args);
        }
      }
  
      on(event, listener) {
        if (this.listeners[event] == null) {
          this.listeners[event] = [listener];
        } else {
          this.listeners[event].push(listener);
        }
      }
  
      removeListener(event, listener) {
        var _listener, actualIndex, i, j, len1, listeners, ref, removedCount;
        listeners = this.listeners[event];
        if (listeners == null) {
          return;
        }
        removedCount = 0;
        for (i = j = 0, len1 = listeners.length; j < len1; i = ++j) {
          _listener = listeners[i];
          if (_listener === listener) {
            logger.debug(`[rtmp] removed listener for ${event}`);
            actualIndex = i - removedCount;
            splice.apply(
              listeners,
              [actualIndex, actualIndex - actualIndex + 1].concat((ref = []))
            ),
              ref;
            removedCount++;
          }
        }
      }
  
      sendData(arr) {
        var buf, item, j, len, len1;
        if (arr == null) {
          return;
        }
        if (Buffer.isBuffer(arr)) {
          buf = arr;
        } else {
          len = 0;
          for (j = 0, len1 = arr.length; j < len1; j++) {
            item = arr[j];
            len += item.length;
          }
          buf = Buffer.concat(arr, len);
        }
        if (this.useEncryption) {
          buf = this.encrypt(buf);
        }
        return this.emit('data', buf);
      }
  
      rejectConnect(commandMessage, callback) {
        var _error, close, streamBegin0;
        streamBegin0 = createRTMPMessage({
          chunkStreamID: 2,
          timestamp: 0,
          messageTypeID: 0x04, // User Control Message
          messageStreamID: 0,
          body: Buffer.from([
            // Stream Begin (see 7.1.7. User Control Message Events)
            0,
            0,
            // Stream ID of the stream that became functional
            0,
            0,
            0,
            0
          ])
        });
        _error = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_error',
          transactionID: 1,
          objects: [
            createAMF0Data(null),
            createAMF0Object({
              level: 'error',
              code: 'NetConnection.Connect.Rejected',
              description: `[ Server.Reject ] : (_defaultRoot_, ) : Invalid application name(/${this.app}).`
            })
          ]
        });
        close = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: 'close',
          transactionID: 0,
          objects: [createAMF0Data(null)]
        });
        return callback(null, this.concatenate([streamBegin0, _error, close]));
      }
  
      respondConnect(commandMessage, callback) {
        var app,
          connectResult,
          onBWDone,
          ref,
          setPeerBandwidth,
          streamBegin0,
          windowAck;
        app = commandMessage.objects[0].value.app;
        app = app.replace(/\/$/, ''); // JW Player adds / at the end
        this.app = app;
        if (
          app !== config.liveApplicationName &&
          app !== config.recordedApplicationName
        ) {
          logger.warn(
            `[rtmp:client=${this.clientid}] requested invalid app name: ${app}`
          );
          this.rejectConnect(commandMessage, callback);
          return;
        }
        // TODO: use @chunkSize for createRTMPMessage()?
        windowAck = createRTMPMessage({
          chunkStreamID: 2,
          timestamp: 0,
          messageTypeID: 0x05, // Window Acknowledgement Size
          messageStreamID: 0,
          // 0x140000 == 1310720
          // 0x2625a0 == 2500000
          // Acknowledgement Window Size (4 bytes)
          //        0, 0x14, 0, 0
          body: Buffer.from([0, 0x26, 0x25, 0xa0])
        });
        setPeerBandwidth = createRTMPMessage({
          chunkStreamID: 2,
          timestamp: 0,
          messageTypeID: 0x06, // Set Peer Bandwidth
          messageStreamID: 0,
          body: Buffer.from([
            // Window acknowledgement size (4 bytes)
            0,
            0x26,
            0x25,
            0xa0,
            // Limit Type
            0x02
          ])
        });
        streamBegin0 = createRTMPMessage({
          chunkStreamID: 2,
          timestamp: 0,
          messageTypeID: 0x04, // User Control Message
          messageStreamID: 0,
          body: Buffer.from([
            // Stream Begin (see 7.1.7. User Control Message Events)
            0,
            0,
            // Stream ID of the stream that became functional
            0,
            0,
            0,
            0
          ])
        });
        // 7.2.1.1.  connect
        connectResult = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_result',
          transactionID: 1, // Always 1
          objects: [
            createAMF0Object({
              fmsVer: 'FMS/3,0,4,423',
              capabilities: 31
            }),
            createAMF0Object({
              level: 'status',
              code: 'NetConnection.Connect.Success',
              description: 'Connection succeeded.',
              objectEncoding: (ref = this.objectEncoding) != null ? ref : 0
            })
          ]
        });
        onBWDone = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: 'onBWDone',
          transactionID: 0,
          objects: [createAMF0Data(null)]
        });
        return callback(
          null,
          this.concatenate([
            windowAck,
            setPeerBandwidth,
            streamBegin0,
            connectResult,
            onBWDone
          ])
        );
      }
  
      encrypt(data) {
        var isSingleByte, result;
        isSingleByte = typeof data === 'number';
        if (isSingleByte) {
          data = Buffer.from([data]);
        }
        result = this.cipherIn.update(data);
        if (isSingleByte) {
          return result[0];
        } else {
          return result;
        }
      }
  
      decrypt(data) {
        var isSingleByte, result;
        isSingleByte = typeof data === 'number';
        if (isSingleByte) {
          data = Buffer.from([data]);
        }
        result = this.cipherOut.update(data);
        if (isSingleByte) {
          return result[0];
        } else {
          return result;
        }
      }
  
      respondHandshake(c0c1, callback) {
        return rtmp_handshake.generateS0S1S2(c0c1, (err, s0s1s2, keys) => {
          var type, zeroBytes;
          type = s0s1s2[0];
          if (type === 6) {
            this.useEncryption = true;
            logger.info(`[rtmp:client=${this.clientid}] enabled encryption`);
            this.clientPublicKey = keys.clientPublicKey;
            this.dh = keys.dh;
            this.sharedSecret = this.dh.computeSecret(this.clientPublicKey);
            this.keyOut = codec_utils
              .calcHmac(this.dh.getPublicKey(), this.sharedSecret)
              .slice(0, 16);
            this.keyIn = codec_utils
              .calcHmac(this.clientPublicKey, this.sharedSecret)
              .slice(0, 16);
            this.cipherOut = crypto.createCipheriv('rc4', this.keyOut, '');
            this.cipherIn = crypto.createCipheriv('rc4', this.keyIn, '');
            zeroBytes = Buffer.alloc(1536);
            zeroBytes.fill(0);
            this.encrypt(zeroBytes);
            this.decrypt(zeroBytes);
          }
          return callback(null, s0s1s2);
        });
      }
  
      parseRTMPMessages(rtmpMessage) {
        var chunkBasicHeader,
          chunkBody,
          chunkMessageHeader,
          chunkPayloadSize,
          consumedLen,
          headerLen,
          message,
          messages,
          previousChunk,
          remainingMessageLen;
        messages = [];
        consumedLen = 0;
        while (rtmpMessage.length > 1) {
          headerLen = 0;
          message = {};
          // RTMP Chunk Format
  
          // ------------------
          // Basic Header (1-3 bytes)
          // ------------------
          // Message Header (0/3/7/11 bytes)
          // ------------------
          // Extended Timestamp (0/4 bytes)
          // ------------------
          // Chunk Data (variable size)
          // ------------------
  
          // 5.3.1.1. Chunk Basic Header
  
          // Chunk basic header 1 (1 byte)
          // For chunk stream IDs 2-63
          // ---------------
          // fmt (2 bits)
          // chunk stream id (6 bits)
          // ---------------
  
          // Chunk basic header 2 (2 bytes)
          // For chunk stream IDs 64-319
          // ---------------
          // fmt (2 bits)
          // 0 (6 bits)
          // chunk stream id - 64 (8 bits)
          // ---------------
  
          // Chunk basic header 3 (3 bytes)
          // For chunk stream IDs 64-65599
          // ---------------
          // fmt (2 bits)
          // 1 (6 bits)
          // chunk stream id - 64 (16 bits)
          // ---------------
          chunkBasicHeader = rtmpMessage[0];
          message.formatType = chunkBasicHeader >> 6;
          message.chunkStreamID = chunkBasicHeader & 0b111111;
          if (message.chunkStreamID === 0) {
            // Chunk basic header 2
            if (rtmpMessage.length < 2) {
              // buffer is incomplete
              break;
            }
            message.chunkStreamID = rtmpMessage[1] + 64;
            chunkMessageHeader = rtmpMessage.slice(2);
            headerLen += 2;
          } else if (message.chunkStreamID === 1) {
            // Chunk basic header 3
            if (rtmpMessage.length < 3) {
              // buffer is incomplete
              break;
            }
            message.chunkStreamID = (rtmpMessage[1] << 8) + rtmpMessage[2] + 64;
            chunkMessageHeader = rtmpMessage.slice(3);
            headerLen += 3; // Chunk basic header 1
          } else {
            chunkMessageHeader = rtmpMessage.slice(1);
            headerLen += 1;
          }
          // 5.3.1.2. Chunk Message Header
  
          // 5.3.1.2.1 Type 0 chunk header (11 bytes)
          // ---------------
          // timestamp (3 bytes)
          //   Absolute timestamp of the message.
          //   The value of 0xffffff indicates the presence of
          //   Extended Timestamp field.
          // message length (3 bytes)
          // message type id (1 byte)
          // message stream id (4 bytes) - little endian
          // ---------------
  
          // 5.3.1.2.2 Type 1 chunk header (7 bytes)
          // This chunk has the same stream ID as the preceding chunk.
          // ---------------
          // timestamp delta (3 bytes)
          // message length (3 bytes)
          // message type id (1 byte)
          // ---------------
  
          // 5.3.1.2.3. Type 2 chunk header (3 bytes)
          // This chunk has the same stream ID and message length as
          // the preceding chunk.
          // ---------------
          // timestamp delta (3 bytes)
          // ---------------
  
          // 5.3.1.2.4. Type 3 chunk header (0 byte)
          // This chunk has the same stream ID, message length, and
          // timestamp delta as the preceding chunk.
          // ---------------
          // ---------------
          if (message.formatType === 0) {
            // Type 0 (11 bytes)
            if (chunkMessageHeader.length < 11) {
              // buffer is incomplete
              break;
            }
            message.timestamp =
              (chunkMessageHeader[0] << 16) +
              (chunkMessageHeader[1] << 8) +
              chunkMessageHeader[2];
            if (message.timestamp === 0xffffff) {
              message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_ABSOLUTE;
            } else {
              message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_NOT_USED;
            }
            message.timestampDelta = 0;
            message.messageLength =
              (chunkMessageHeader[3] << 16) +
              (chunkMessageHeader[4] << 8) +
              chunkMessageHeader[5];
            message.messageTypeID = chunkMessageHeader[6];
            message.messageStreamID = chunkMessageHeader.readUInt32LE(7); // TODO: signed or unsigned?
            chunkBody = chunkMessageHeader.slice(11);
            headerLen += 11;
          } else if (message.formatType === 1) {
            // Type 1 (7 bytes)
            if (chunkMessageHeader.length < 7) {
              // buffer is incomplete
              break;
            }
            message.timestampDelta =
              (chunkMessageHeader[0] << 16) +
              (chunkMessageHeader[1] << 8) +
              chunkMessageHeader[2];
            if (message.timestampDelta === 0xffffff) {
              message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_DELTA;
            } else {
              message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_NOT_USED;
            }
            message.messageLength =
              (chunkMessageHeader[3] << 16) +
              (chunkMessageHeader[4] << 8) +
              chunkMessageHeader[5];
            message.messageTypeID = chunkMessageHeader[6];
            previousChunk = this.previousChunkMessage[message.chunkStreamID];
            if (previousChunk != null) {
              message.timestamp = previousChunk.timestamp;
              message.messageStreamID = previousChunk.messageStreamID;
            } else {
              throw new Error(
                `${this.clientid}: Chunk reference error for type 1: previous chunk for id ${message.chunkStreamID} is not found (possibly a bug)`
              );
            }
            chunkBody = chunkMessageHeader.slice(7);
            headerLen += 7;
          } else if (message.formatType === 2) {
            // Type 2 (3 bytes)
            if (chunkMessageHeader.length < 3) {
              // buffer is incomplete
              break;
            }
            message.timestampDelta =
              (chunkMessageHeader[0] << 16) +
              (chunkMessageHeader[1] << 8) +
              chunkMessageHeader[2];
            if (message.timestampDelta === 0xffffff) {
              message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_DELTA;
            } else {
              message.extendedTimestampType = EXTENDED_TIMESTAMP_TYPE_NOT_USED;
            }
            previousChunk = this.previousChunkMessage[message.chunkStreamID];
            if (previousChunk != null) {
              message.timestamp = previousChunk.timestamp;
              message.messageStreamID = previousChunk.messageStreamID;
              message.messageLength = previousChunk.messageLength;
              message.messageTypeID = previousChunk.messageTypeID;
            } else {
              throw new Error(
                `${this.clientid}: Chunk reference error for type 2: previous chunk for id ${message.chunkStreamID} is not found (possibly a bug)`
              );
            }
            chunkBody = chunkMessageHeader.slice(3);
            headerLen += 3;
          } else if (message.formatType === 3) {
            // Type 3 (0 byte)
            previousChunk = this.previousChunkMessage[message.chunkStreamID];
            if (previousChunk != null) {
              message.timestamp = previousChunk.timestamp;
              message.messageStreamID = previousChunk.messageStreamID;
              message.messageLength = previousChunk.messageLength;
              message.timestampDelta = previousChunk.timestampDelta;
              message.extendedTimestampType = previousChunk.extendedTimestampType;
              message.messageTypeID = previousChunk.messageTypeID;
            } else {
              throw new Error(
                `${this.clientid}: Chunk reference error for type 3: previous chunk for id ${message.chunkStreamID} is not found (possibly a bug)`
              );
            }
            chunkBody = chunkMessageHeader;
          } else {
            throw new Error(`Unknown format type: ${formatType}`);
          }
          // 5.3.1.3. Extended Timestamp
          if (
            message.extendedTimestampType === EXTENDED_TIMESTAMP_TYPE_ABSOLUTE
          ) {
            if (chunkBody.length < 4) {
              // buffer is incomplete
              break;
            }
            message.timestamp =
              chunkBody[0] * Math.pow(256, 3) +
              (chunkBody[1] << 16) +
              (chunkBody[2] << 8) +
              chunkBody[3];
            chunkBody = chunkBody.slice(4);
            headerLen += 4;
          } else if (
            message.extendedTimestampType === EXTENDED_TIMESTAMP_TYPE_DELTA
          ) {
            if (chunkBody.length < 4) {
              // buffer is incomplete
              break;
            }
            message.timestampDelta =
              chunkBody[0] * Math.pow(256, 3) +
              (chunkBody[1] << 16) +
              (chunkBody[2] << 8) +
              chunkBody[3];
            chunkBody = chunkBody.slice(4);
            headerLen += 4;
          }
          previousChunk = this.previousChunkMessage[message.chunkStreamID];
          if (previousChunk != null && previousChunk.isIncomplete) {
            remainingMessageLen =
              message.messageLength - previousChunk.body.length;
          } else {
            remainingMessageLen = message.messageLength;
          }
          chunkPayloadSize = Math.min(this.receiveChunkSize, remainingMessageLen);
          if (chunkBody.length < chunkPayloadSize) {
            // buffer is incomplete
            break;
          }
          // We have enough buffer for this chunk
          rtmpMessage = chunkBody.slice(chunkPayloadSize);
          chunkBody = chunkBody.slice(0, chunkPayloadSize);
          consumedLen += headerLen + chunkPayloadSize;
          if (previousChunk != null && previousChunk.isIncomplete) {
            // Do not count timestampDelta
            message.body = Buffer.concat([previousChunk.body, chunkBody]);
          } else {
            message.body = chunkBody;
            // Calculate timestamp for this message
            if (message.timestampDelta != null) {
              if (message.timestamp == null) {
                throw new Error(
                  'timestamp delta is given, but base timestamp is not known'
                );
              }
              message.timestamp += message.timestampDelta;
              // Handle timestamp overflow
              if (message.timestamp > TIMESTAMP_ROUNDOFF) {
                message.timestamp %= TIMESTAMP_ROUNDOFF;
              }
            }
          }
          if (message.body.length >= message.messageLength) {
            // message is completed
            // TODO: Is this check redundant?
            if (message.body.length !== message.messageLength) {
              logger.warn(
                "[rtmp] warning: message lengths don't match: " +
                  `got=${message.body.length} expected=${message.messageLength}`
              );
            }
            messages.push(message);
          } else {
            message.isIncomplete = true;
          }
          this.previousChunkMessage[message.chunkStreamID] = message;
          if (messages.length === 1) {
            break;
          }
        }
        return {
          consumedLen: consumedLen,
          rtmpMessages: messages
        };
      }
  
      // releaseStream()
      respondReleaseStream(requestCommand, callback) {
        var _result, ref, streamName;
        streamName =
          (ref = requestCommand.objects[1]) != null ? ref.value : void 0;
        logger.debug(`[rtmp] releaseStream: ${this.app}/${streamName}`);
        // TODO: Destroy stream here?
        _result = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_result',
          transactionID: requestCommand.transactionID,
          objects: [createAMF0Data(null), createAMF0Data(null)]
        });
        return callback(null, _result);
      }
  
      // @setDataFrame
      receiveSetDataFrame(requestData) {
        if (requestData.objects[1].value === 'onMetaData') {
          return logger.debug('[rtmp:receive] received @setDataFrame onMetaData');
        } else {
          throw new Error(
            `Unknown @setDataFrame: ${requestData.objects[1].value}`
          );
        }
      }
  
      respondFCUnpublish(requestCommand, callback) {
        var _result, ref, streamName, unpublishSuccess;
        streamName =
          (ref = requestCommand.objects[1]) != null ? ref.value : void 0;
        logger.info(`[rtmp] FCUnpublish: ${streamName}`);
        _result = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_result',
          transactionID: requestCommand.transactionID,
          objects: [createAMF0Data(null), createAMF0Data(null)]
        });
        unpublishSuccess = createAMF0CommandMessage(
          {
            chunkStreamID: 4,
            timestamp: 0,
            messageStreamID: 1,
            command: 'onStatus',
            transactionID: requestCommand.transactionID,
            objects: [
              createAMF0Data(null),
              createAMF0Object({
                level: 'status',
                code: 'NetStream.Unpublish.Success',
                description: '',
                details: streamName,
                clientid: this.clientid
              })
            ]
          },
          this.chunkSize
        );
        return callback(null, this.concatenate([_result, unpublishSuccess]));
      }
  
      // 7.2.2.6. publish
      respondPublish(requestCommand, callback) {
        var j,
          kv,
          len1,
          match,
          pair,
          pairs,
          params,
          publishStart,
          publishingName,
          publishingType,
          ref,
          ref1,
          stream,
          streamName,
          urlInfo;
        this.receiveTimestamp = null;
        publishingName =
          (ref = requestCommand.objects[1]) != null ? ref.value : void 0;
        if (typeof publishingName !== 'string') {
          publishStart = createAMF0CommandMessage(
            {
              chunkStreamID: 4,
              timestamp: 0,
              messageStreamID: 1,
              command: 'onStatus',
              transactionID: requestCommand.transactionID,
              objects: [
                createAMF0Data(null),
                createAMF0Object({
                  level: 'error',
                  code: 'NetStream.Publish.Start',
                  description: 'Publishing Name parameter is invalid.',
                  details: this.app,
                  clientid: this.clientid
                })
              ]
            },
            this.chunkSize
          );
          return callback(null, publishStart);
        }
        // Strip query string part from a string like:
        // "livestream?videoKeyframeFrequency=5&totalDatarate=248"
        urlInfo = url.parse(publishingName);
        if (urlInfo.query != null) {
          pairs = urlInfo.query.split('&');
          params = {};
          for (j = 0, len1 = pairs.length; j < len1; j++) {
            pair = pairs[j];
            kv = pair.split('=');
            params[kv[0]] = kv[1];
          }
          // TODO: Use this information for something
          // totalDatarate: Total kbps for video + audio
          logger.info(JSON.stringify(params));
        }
        publishingName = this.app + '/' + urlInfo.pathname;
        this.streamId = publishingName;
        stream = avstreams.get(this.streamId);
        if (stream != null) {
          stream.reset();
        } else {
          stream = avstreams.create(this.streamId);
          stream.type = avstreams.STREAM_TYPE_LIVE;
        }
        this.stream = stream;
        // TODO: Check if streamId is already used
        publishingType =
          (ref1 = requestCommand.objects[2]) != null ? ref1.value : void 0;
        // publishingType should be lowercase ('live') but Wirecast uses uppercase ('LIVE')
        if (publishingType.toLowerCase() !== 'live') {
          logger.warn(
            `[rtmp] warn: publishing type other than 'live' is not supported (got ${publishingType}); assuming 'live'`
          );
        }
        logger.info(
          `[rtmp] publish: stream=${publishingName} publishingType=${publishingType}`
        );
        // strip query string from publishingName
        if ((match = /^(.*?)\?/.exec(publishingName)) != null) {
          streamName = match[1];
        } else {
          streamName = publishingName;
        }
        this.isFirstVideoReceived = false;
        this.isFirstAudioReceived = false;
        publishStart = createAMF0CommandMessage(
          {
            chunkStreamID: 4,
            timestamp: 0,
            messageStreamID: 1,
            command: 'onStatus',
            transactionID: requestCommand.transactionID,
            objects: [
              createAMF0Data(null),
              createAMF0Object({
                level: 'status',
                code: 'NetStream.Publish.Start',
                description: '',
                details: streamName,
                clientid: this.clientid
              })
            ]
          },
          this.chunkSize
        );
        return callback(null, publishStart);
      }
  
      respondWithError(requestCommand, callback) {
        var _error;
        _error = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_error',
          transactionID: requestCommand.transactionID,
          objects: [
            createAMF0Data(null),
            createAMF0Object({
              level: 'error',
              code: '',
              description: 'Request failed.',
              details: this.app,
              clientid: this.clientid
            })
          ]
        });
        return callback(null, _error);
      }
  
      // FCPublish()
      respondFCPublish(requestCommand, callback) {
        var _result, ref, streamName;
        streamName =
          (ref = requestCommand.objects[1]) != null ? ref.value : void 0;
        logger.debug(`[rtmp] FCPublish: ${this.app}/${streamName}`);
        _result = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_result',
          transactionID: requestCommand.transactionID,
          objects: [createAMF0Data(null), createAMF0Data(null)]
        });
        return callback(null, _result);
      }
  
      respondCreateStream(requestCommand, callback) {
        var _result;
        _result = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_result',
          transactionID: requestCommand.transactionID, // may be 2
          objects: [
            createAMF0Data(null),
            createAMF0Data(1) // stream id
          ]
        });
        return callback(null, _result);
      }
  
      respondPlay(commandMessage, callback, streamId = null) {
        var _error,
          close,
          codecConfigs,
          dataStart,
          messages,
          metadata,
          onMetaData,
          playReset,
          playStart,
          ref,
          rtmpSampleAccess,
          setChunkSize,
          stream,
          streamBegin1,
          streamIsRecorded;
        if (streamId == null) {
          streamId =
            this.app +
            '/' +
            ((ref = commandMessage.objects[1]) != null ? ref.value : void 0);
        }
        logger.info(
          `[rtmp:client=${this.clientid}] requested stream ${streamId}`
        );
        this.chunkSize = config.rtmpPlayChunkSize;
        this.stream = avstreams.get(streamId);
        if (this.stream == null) {
          logger.error(
            `[rtmp:client=${this.clientid}] error: stream not found: ${streamId}`
          );
          _error = createAMF0CommandMessage({
            chunkStreamID: 3,
            timestamp: 0,
            messageStreamID: 0,
            command: '_error',
            transactionID: commandMessage.transactionID,
            objects: [
              createAMF0Data(null),
              createAMF0Object({
                level: 'error',
                code: 'NetStream.Play.StreamNotFound',
                description: '',
                details: streamId,
                clientid: this.clientid
              })
            ]
          });
          close = createAMF0CommandMessage({
            chunkStreamID: 3,
            timestamp: 0,
            messageStreamID: 0,
            command: 'close',
            transactionID: 0,
            objects: [createAMF0Data(null)]
          });
          callback(null, this.concatenate([_error, close]));
          return;
        }
        // 5.4.1.  Set Chunk Size (1)
        setChunkSize = createRTMPMessage({
          chunkStreamID: 2,
          timestamp: 0,
          messageTypeID: 0x01, // Set Chunk Size
          messageStreamID: 0,
          body: Buffer.from([
            (this.chunkSize >>> 24) & 0x7f,
            (this.chunkSize >>> 16) & 0xff,
            (this.chunkSize >>> 8) & 0xff,
            this.chunkSize & 0xff
          ]) // top bit must be zero
        });
        logger.debug(
          `[rtmp:client=${this.clientid}] stream type: ${this.stream.type}`
        );
        if (this.stream.isRecorded()) {
          streamIsRecorded = createRTMPMessage(
            {
              chunkStreamID: 2,
              timestamp: 0,
              messageTypeID: 0x04, // User Control Message
              messageStreamID: 0,
              body: Buffer.from([
                // StreamIsRecorded (see 7.1.7. User Control Message Events)
                0,
                4,
                // Stream ID of the recorded stream
                0,
                0,
                0,
                1
              ])
            },
            this.chunkSize
          );
        }
        streamBegin1 = createRTMPMessage(
          {
            chunkStreamID: 2,
            timestamp: 0,
            messageTypeID: 0x04, // User Control Message
            messageStreamID: 0,
            body: Buffer.from([
              // Stream Begin (see 7.1.7. User Control Message Events)
              0,
              0,
              // Stream ID of the stream that became functional
              0,
              0,
              0,
              1
            ])
          },
          this.chunkSize
        );
        playReset = createAMF0CommandMessage(
          {
            chunkStreamID: 4,
            timestamp: 0,
            messageStreamID: 1,
            command: 'onStatus',
            transactionID: 0,
            objects: [
              createAMF0Data(null),
              createAMF0Object({
                level: 'status',
                code: 'NetStream.Play.Reset',
                description: `Playing and resetting ${streamId}.`,
                details: streamId,
                clientid: this.clientid
              })
            ]
          },
          this.chunkSize
        );
        playStart = createAMF0CommandMessage(
          {
            chunkStreamID: 4,
            timestamp: 0,
            messageStreamID: 1,
            command: 'onStatus',
            transactionID: 0,
            objects: [
              createAMF0Data(null),
              createAMF0Object({
                level: 'status',
                code: 'NetStream.Play.Start',
                description: `Started playing ${streamId}.`,
                details: streamId,
                clientid: this.clientid
              })
            ]
          },
          this.chunkSize
        );
        rtmpSampleAccess = createAMF0DataMessage(
          {
            chunkStreamID: 4,
            timestamp: 0,
            messageStreamID: 1,
            objects: [
              createAMF0Data('|RtmpSampleAccess'),
              createAMF0Data(false),
              createAMF0Data(false)
            ]
          },
          this.chunkSize
        );
        dataStart = createAMF0DataMessage(
          {
            chunkStreamID: 4,
            timestamp: 0,
            messageStreamID: 1,
            objects: [
              createAMF0Data('onStatus'),
              createAMF0Object({
                code: 'NetStream.Data.Start'
              })
            ]
          },
          this.chunkSize
        );
        metadata = {
          canSeekToEnd: false,
          cuePoints: [],
          hasMetadata: true,
          hasCuePoints: false
        };
        if (this.stream != null) {
          stream = this.stream;
          if (stream != null) {
            if (stream.isVideoStarted) {
              metadata.hasVideo = true;
              metadata.framerate = stream.videoFrameRate;
              metadata.height = stream.videoHeight;
              metadata.videocodecid = config.flv.videocodecid; // TODO
              metadata.videodatarate = config.videoBitrateKbps; // TODO
              metadata.width = stream.videoWidth;
              metadata.avclevel = stream.videoAVCLevel;
              metadata.avcprofile = stream.videoAVCProfile;
            }
            if (stream.isAudioStarted) {
              metadata.hasAudio = true;
              metadata.audiocodecid = config.flv.audiocodecid; // TODO
              metadata.audiodatarate = config.audioBitrateKbps; // TODO
              metadata.audiodelay = 0;
              metadata.audiosamplerate = stream.audioSampleRate;
              metadata.stereo = stream.audioChannels > 1;
              metadata.audiochannels = stream.audioChannels;
              metadata.aacaot = stream.audioObjectType;
            }
            if (stream.isRecorded()) {
              metadata.duration = stream.durationSeconds;
              // timestamp of the last tag in the recorded stream
              metadata.lasttimestamp = stream.lastTagTimestamp;
            }
          } else {
            logger.error(
              `[rtmp] error: respondPlay: no such stream: ${stream.id}`
            );
          }
        } else {
          logger.error(
            '[rtmp] error: respondPlay: stream not set for this session'
          );
        }
        logger.debug('[rtmp] metadata:');
        logger.debug(metadata);
        onMetaData = createAMF0DataMessage(
          {
            chunkStreamID: 4,
            timestamp: 0,
            messageStreamID: 1,
            objects: [createAMF0Data('onMetaData'), createAMF0Data(metadata)]
          },
          this.chunkSize
        );
        codecConfigs = this.getCodecConfigs(0);
        messages = [setChunkSize];
        if (this.stream.isRecorded()) {
          messages.push(streamIsRecorded);
        }
        messages.push(
          streamBegin1,
          playReset,
          playStart,
          rtmpSampleAccess,
          dataStart,
          onMetaData,
          codecConfigs
        );
        callback(null, this.concatenate(messages));
        if (this.stream.isRecorded()) {
          this.stream.play();
        }
        // ready for playing
        this.isWaitingForKeyFrame = true;
        this.seekedDuringPause = false;
        if (config.rtmpWaitForKeyFrame) {
          return logger.info(
            `[rtmp:client=${this.clientid}] waiting for keyframe`
          );
        }
      }
  
      // Returns a Buffer contains both SPS and PPS
      getCodecConfigs(timestamp = 0) {
        var ascInfo,
          audioConfigMessage,
          buf,
          configMessages,
          ppsLen,
          spsLen,
          stream,
          videoConfigMessage;
        configMessages = [];
        stream = this.stream;
        if (stream == null) {
          logger.error(
            '[rtmp] error: getCodecConfigs: stream not set for this session'
          );
          return Buffer.from([]);
        }
        if (stream.isVideoStarted) {
          if (stream.spsNALUnit == null || stream.ppsNALUnit == null) {
            logger.error(
              '[rtmp] error: getCodecConfigs: SPS or PPS is not present'
            );
            return Buffer.from([]);
          }
          // video
          spsLen = stream.spsNALUnit.length;
          ppsLen = stream.ppsNALUnit.length;
          buf = Buffer.from([
            // VIDEODATA tag (Appeared in Adobe's Video File Format Spec v10.1 E.4.3.1 VIDEODATA
            (1 << 4) | config.flv.videocodecid, // 1=key frame
            0x00, // 0=AVC sequence header (configuration data)
            0x00, // composition time
            0x00, // composition time
            0x00, // composition time
  
            // AVC decoder configuration: described in ISO 14496-15 5.2.4.1.1 Syntax
            0x01, // version
            ...stream.spsNALUnit.slice(1, 4),
            0xff, // 6 bits reserved (0b111111) + 2 bits nal size length - 1 (0b11)
            0xe1, // 3 bits reserved (0b111) + 5 bits number of sps (0b00001)
            (spsLen >> 8) & 0xff,
            spsLen & 0xff,
            ...stream.spsNALUnit,
            0x01, // number of PPS (1)
            (ppsLen >> 8) & 0xff,
            ppsLen & 0xff,
            ...stream.ppsNALUnit
          ]);
          videoConfigMessage = createVideoMessage({
            body: buf,
            timestamp: timestamp,
            chunkSize: this.chunkSize
          });
          configMessages.push(videoConfigMessage);
        }
        if (stream.isAudioStarted) {
          // audio
          // TODO: support other than AAC too?
          buf = flv.createAACAudioDataTag({
            aacPacketType: flv.AAC_PACKET_TYPE_SEQUENCE_HEADER
          });
          ascInfo = stream.audioASCInfo;
          if (ascInfo != null) {
            // Flash Player won't play audio if explicit hierarchical
            // signaling of SBR is used
            if (
              ascInfo.explicitHierarchicalSBR &&
              config.rtmpDisableHierarchicalSBR
            ) {
              logger.debug(
                '[rtmp] converting hierarchical signaling of SBR' +
                  ` (AudioSpecificConfig=0x${stream.audioSpecificConfig.toString(
                    'hex'
                  )})` +
                  ' to backward compatible signaling'
              );
              buf = buf.concat(aac.createAudioSpecificConfig(ascInfo));
              buf = Buffer.from(buf);
            } else {
              buf = Buffer.concat([Buffer.from(buf), stream.audioSpecificConfig]);
            }
            logger.debug(
              `[rtmp] sending AudioSpecificConfig: 0x${buf.toString('hex')}`
            );
          } else {
            buf = buf.concat(
              aac.createAudioSpecificConfig({
                audioObjectType: stream.audioObjectType,
                samplingFrequency: stream.audioSampleRate,
                channels: stream.audioChannels,
                frameLength: 1024 // TODO: How to detect 960?
              })
            );
            // Convert buf from array to Buffer
            buf = Buffer.from(buf);
          }
          audioConfigMessage = createAudioMessage({
            body: buf,
            timestamp: timestamp,
            chunkSize: this.chunkSize
          });
          configMessages.push(audioConfigMessage);
        }
        return this.concatenate(configMessages);
      }
  
      //  respondPauseRaw: (requestCommand, callback) ->
      //    lastTimestamp = @stream.rtmpLastTimestamp ? 0
  
      //    _result = createAMF0CommandMessage
      //      chunkStreamID: 3
      //      timestamp: lastTimestamp
      //      messageStreamID: 0
      //      command: '_result'
      //      transactionID: requestCommand.transactionID
      //      objects: [
      //        createAMF0Data(null)
      //        createAMF0Data(null)
      //      ]
  
      //    callback null, _result
      respondSeek(requestCommand, callback) {
        var _isPaused, _isPlaying, msec, ref;
        msec = requestCommand.objects[1].value;
        logger.info(`[rtmp:client=${this.clientid}] seek to ${msec / 1000} sec`);
        msec = Math.floor(msec);
        this.lastSentTimestamp = null;
        if (
          ((ref = this.stream) != null ? ref.type : void 0) ===
          avstreams.STREAM_TYPE_RECORDED
        ) {
          clearQueuedRTMPMessages(this.stream);
          _isPlaying = this.isPlaying;
          this.isPlaying = false;
          _isPaused = this.stream.isPaused();
          if (!_isPaused) {
            this.stream.pause();
          }
          return this.stream.seek(msec / 1000, (err, actualStartTime) => {
            var seq;
            if (err) {
              logger.error(`seek failed: ${err}`);
              return;
            }
            // restore the value of @isPlaying
            this.isPlaying = _isPlaying;
            seq = new Sequent();
            // If the stream had not been paused, start playing
            if (!_isPaused) {
              this.stream.sendVideoPacketsSinceLastKeyFrame(msec / 1000, () => {
                this.stream.resume();
                this.seekedDuringPause = false;
                return seq.done();
              });
            } else {
              this.seekedDuringPause = true;
              seq.done();
            }
            return seq.wait(1, () => {
              var codecConfigs,
                dataStart,
                metadata,
                onMetaData,
                playStart,
                rtmpSampleAccess,
                seekNotify,
                setChunkSize,
                stream,
                streamBegin1,
                streamEOF1,
                streamIsRecorded;
              streamEOF1 = createRTMPMessage({
                chunkStreamID: 2,
                timestamp: 0,
                messageTypeID: 0x04, // User Control Message
                messageStreamID: 0,
                body: Buffer.from([
                  // Stream EOF (see 7.1.7. User Control Message Events)
                  0,
                  1,
                  // Stream ID of the stream that reaches EOF
                  0,
                  0,
                  0,
                  1
                ])
              });
              // 5.4.1.  Set Chunk Size (1)
              setChunkSize = createRTMPMessage({
                chunkStreamID: 2,
                timestamp: 0,
                messageTypeID: 0x01, // Set Chunk Size
                messageStreamID: 0,
                body: Buffer.from([
                  (this.chunkSize >>> 24) & 0x7f,
                  (this.chunkSize >>> 16) & 0xff,
                  (this.chunkSize >>> 8) & 0xff,
                  this.chunkSize & 0xff
                ]) // top bit must be zero
              });
              streamIsRecorded = createRTMPMessage(
                {
                  chunkStreamID: 2,
                  timestamp: 0,
                  messageTypeID: 0x04, // User Control Message
                  messageStreamID: 0,
                  body: Buffer.from([
                    // StreamIsRecorded (see 7.1.7. User Control Message Events)
                    0,
                    4,
                    // Stream ID of the recorded stream
                    0,
                    0,
                    0,
                    1
                  ])
                },
                this.chunkSize
              );
              streamBegin1 = createRTMPMessage(
                {
                  chunkStreamID: 2,
                  timestamp: 0,
                  messageTypeID: 0x04, // User Control Message
                  messageStreamID: 0,
                  body: Buffer.from([
                    // Stream Begin (see 7.1.7. User Control Message Events)
                    0,
                    0,
                    // Stream ID of the stream that became functional
                    0,
                    0,
                    0,
                    1
                  ])
                },
                this.chunkSize
              );
              seekNotify = createAMF0CommandMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  command: 'onStatus',
                  transactionID: requestCommand.transactionID,
                  objects: [
                    createAMF0Data(null),
                    createAMF0Object({
                      level: 'status',
                      code: 'NetStream.Seek.Notify',
                      description: `Seeking ${msec} (stream ID: 1).`,
                      details: this.stream.id,
                      clientid: this.clientid
                    })
                  ]
                },
                this.chunkSize
              );
              playStart = createAMF0CommandMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  command: 'onStatus',
                  transactionID: 0,
                  objects: [
                    createAMF0Data(null),
                    createAMF0Object({
                      level: 'status',
                      code: 'NetStream.Play.Start',
                      description: `Started playing ${this.stream.id}.`,
                      details: this.stream.id,
                      clientid: this.clientid
                    })
                  ]
                },
                this.chunkSize
              );
              rtmpSampleAccess = createAMF0DataMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  objects: [
                    createAMF0Data('|RtmpSampleAccess'),
                    createAMF0Data(false),
                    createAMF0Data(false)
                  ]
                },
                this.chunkSize
              );
              // TODO: onStatus('NetStream.Data.Start')
              dataStart = createAMF0DataMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  objects: [
                    createAMF0Data('onStatus'),
                    createAMF0Object({
                      code: 'NetStream.Data.Start'
                    })
                  ]
                },
                this.chunkSize
              );
              metadata = {
                canSeekToEnd: false,
                cuePoints: [],
                hasMetadata: true,
                hasCuePoints: false
              };
              stream = this.stream;
              if (stream.isVideoStarted) {
                metadata.hasVideo = true;
                metadata.framerate = stream.videoFrameRate;
                metadata.height = stream.videoHeight;
                metadata.videocodecid = config.flv.videocodecid; // TODO
                metadata.videodatarate = config.videoBitrateKbps; // TODO
                metadata.width = stream.videoWidth;
                metadata.avclevel = stream.videoAVCLevel;
                metadata.avcprofile = stream.videoAVCProfile;
              }
              if (stream.isAudioStarted) {
                metadata.hasAudio = true;
                metadata.audiocodecid = config.flv.audiocodecid; // TODO
                metadata.audiodatarate = config.audioBitrateKbps; // TODO
                metadata.audiodelay = 0;
                metadata.audiosamplerate = stream.audioSampleRate;
                metadata.stereo = stream.audioChannels > 1;
                metadata.audiochannels = stream.audioChannels;
                metadata.aacaot = stream.audioObjectType;
              }
              metadata.duration = stream.durationSeconds;
              // timestamp of the last tag in the recorded file
              metadata.lasttimestamp = stream.lastTagTimestamp;
              // timestamp of the last video key frame
              logger.debug('[rtmp] metadata:');
              logger.debug(metadata);
              onMetaData = createAMF0DataMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  objects: [
                    createAMF0Data('onMetaData'),
                    createAMF0Data(metadata)
                  ]
                },
                this.chunkSize
              );
              codecConfigs = this.getCodecConfigs(msec);
              // TODO: Should we send all video packets since last key frame?
  
              // Send all suite regardless of _isPaused
              return callback(
                null,
                this.concatenate([
                  streamEOF1,
                  setChunkSize,
                  streamIsRecorded,
                  streamBegin1,
                  seekNotify,
                  playStart,
                  rtmpSampleAccess,
                  dataStart,
                  onMetaData,
                  codecConfigs // live
                ])
              );
            });
          });
        } else {
          return this.respondPlay(requestCommand, callback);
        }
      }
  
      respondPause(requestCommand, callback) {
        var base,
          doPause,
          msec,
          pauseNotify,
          ref,
          ref1,
          ref2,
          seekMsec,
          streamEOF1;
        doPause = requestCommand.objects[1].value === true;
        msec = requestCommand.objects[2].value;
        if (doPause) {
          // playing -> pause
          this.isPlaying = false;
          this.isWaitingForKeyFrame = false;
          if (
            ((ref = this.stream) != null ? ref.type : void 0) ===
            avstreams.STREAM_TYPE_RECORDED
          ) {
            if (typeof (base = this.stream).pause === 'function') {
              base.pause();
            }
            logger.info(
              `[rtmp:client=${this.clientid}] stream ${
                this.stream.id
              } paused at ${msec / 1000} sec (client player time)`
            );
            streamEOF1 = createRTMPMessage({
              chunkStreamID: 2,
              timestamp: 0,
              messageTypeID: 0x04, // User Control Message
              messageStreamID: 0,
              body: Buffer.from([
                // Stream EOF (see 7.1.7. User Control Message Events)
                0,
                1,
                // Stream ID of the stream that reaches EOF
                0,
                0,
                0,
                1
              ])
            });
            pauseNotify = createAMF0CommandMessage(
              {
                chunkStreamID: 4,
                timestamp: msec,
                messageStreamID: 1,
                command: 'onStatus',
                transactionID: requestCommand.transactionID,
                objects: [
                  createAMF0Data(null),
                  createAMF0Object({
                    level: 'status',
                    code: 'NetStream.Pause.Notify',
                    description: `Pausing ${this.stream.id}.`,
                    details: this.stream.id,
                    clientid: this.clientid
                  })
                ]
              },
              this.chunkSize
            );
            return callback(
              null,
              this.concatenate([
                streamEOF1,
                pauseNotify // live stream
              ])
            );
          } else {
            return callback(null); // pausing -> resume
          }
        } else {
          if (
            ((ref1 = this.stream) != null ? ref1.type : void 0) ===
            avstreams.STREAM_TYPE_RECORDED
          ) {
            clearQueuedRTMPMessages(this.stream);
            // RTMP 1.0 spec says that the server only sends messages with timestamps
            // greater than the specified msec, but it appears that Flash Player expects
            // to include the specified msec when msec is 0.
            if (msec === 0) {
              seekMsec = 0;
            } else {
              seekMsec = msec + 1;
            }
            return this.stream.seek(seekMsec / 1000, (err, actualStartTime) => {
              var codecConfigs,
                dataStart,
                metadata,
                onMetaData,
                playStart,
                rtmpSampleAccess,
                seq,
                setChunkSize,
                stream,
                streamBegin1,
                streamIsRecorded,
                unpauseNotify;
              if (err) {
                logger.error(`[rtmp] seek failed: ${err}`);
                return;
              }
              // 5.4.1.  Set Chunk Size (1)
              setChunkSize = createRTMPMessage({
                chunkStreamID: 2,
                timestamp: 0,
                messageTypeID: 0x01, // Set Chunk Size
                messageStreamID: 0,
                body: Buffer.from([
                  (this.chunkSize >>> 24) & 0x7f,
                  (this.chunkSize >>> 16) & 0xff,
                  (this.chunkSize >>> 8) & 0xff,
                  this.chunkSize & 0xff
                ]) // top bit must be zero
              });
              if (this.stream.isRecorded()) {
                streamIsRecorded = createRTMPMessage(
                  {
                    chunkStreamID: 2,
                    timestamp: 0,
                    messageTypeID: 0x04, // User Control Message
                    messageStreamID: 0,
                    body: Buffer.from([
                      // StreamIsRecorded (see 7.1.7. User Control Message Events)
                      0,
                      4,
                      // Stream ID of the recorded stream
                      0,
                      0,
                      0,
                      1
                    ])
                  },
                  this.chunkSize
                );
              }
              streamBegin1 = createRTMPMessage(
                {
                  chunkStreamID: 2,
                  timestamp: 0,
                  messageTypeID: 0x04, // User Control Message
                  messageStreamID: 0,
                  body: Buffer.from([
                    // Stream Begin (see 7.1.7. User Control Message Events)
                    0,
                    0,
                    // Stream ID of the stream that became functional
                    0,
                    0,
                    0,
                    1
                  ])
                },
                this.chunkSize
              );
              unpauseNotify = createAMF0CommandMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  command: 'onStatus',
                  transactionID: requestCommand.transactionID,
                  objects: [
                    createAMF0Data(null),
                    createAMF0Object({
                      level: 'status',
                      code: 'NetStream.Unpause.Notify',
                      description: `Unpausing ${this.stream.id}.`,
                      details: this.stream.id,
                      clientid: this.clientid
                    })
                  ]
                },
                this.chunkSize
              );
              playStart = createAMF0CommandMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  command: 'onStatus',
                  transactionID: 0,
                  objects: [
                    createAMF0Data(null),
                    createAMF0Object({
                      level: 'status',
                      code: 'NetStream.Play.Start',
                      description: `Started playing ${this.stream.id}.`,
                      details: this.stream.id,
                      clientid: this.clientid
                    })
                  ]
                },
                this.chunkSize
              );
              rtmpSampleAccess = createAMF0DataMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  objects: [
                    createAMF0Data('|RtmpSampleAccess'),
                    createAMF0Data(false),
                    createAMF0Data(false)
                  ]
                },
                this.chunkSize
              );
              // TODO: onStatus('NetStream.Data.Start')
              dataStart = createAMF0DataMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  objects: [
                    createAMF0Data('onStatus'),
                    createAMF0Object({
                      code: 'NetStream.Data.Start'
                    })
                  ]
                },
                this.chunkSize
              );
              metadata = {
                canSeekToEnd: false,
                cuePoints: [],
                hasMetadata: true,
                hasCuePoints: false
              };
              stream = this.stream;
              if (stream.isVideoStarted) {
                metadata.hasVideo = true;
                metadata.framerate = stream.videoFrameRate;
                metadata.height = stream.videoHeight;
                metadata.videocodecid = config.flv.videocodecid; // TODO
                metadata.videodatarate = config.videoBitrateKbps; // TODO
                metadata.width = stream.videoWidth;
                metadata.avclevel = stream.videoAVCLevel;
                metadata.avcprofile = stream.videoAVCProfile;
              }
              if (stream.isAudioStarted) {
                metadata.hasAudio = true;
                metadata.audiocodecid = config.flv.audiocodecid; // TODO
                metadata.audiodatarate = config.audioBitrateKbps; // TODO
                metadata.audiodelay = 0;
                metadata.audiosamplerate = stream.audioSampleRate;
                metadata.stereo = stream.audioChannels > 1;
                metadata.audiochannels = stream.audioChannels;
                metadata.aacaot = stream.audioObjectType;
              }
              metadata.duration = stream.durationSeconds;
              // timestamp of the last tag in the recorded file
              metadata.lasttimestamp = stream.lastTagTimestamp;
              // timestamp of the last video key frame
              logger.debug('[rtmp] metadata:');
              logger.debug(metadata);
              onMetaData = createAMF0DataMessage(
                {
                  chunkStreamID: 4,
                  timestamp: msec,
                  messageStreamID: 1,
                  objects: [
                    createAMF0Data('onMetaData'),
                    createAMF0Data(metadata)
                  ]
                },
                this.chunkSize
              );
              codecConfigs = this.getCodecConfigs(msec);
              callback(
                null,
                this.concatenate([
                  setChunkSize,
                  streamIsRecorded,
                  streamBegin1,
                  unpauseNotify,
                  playStart,
                  rtmpSampleAccess,
                  dataStart,
                  onMetaData,
                  codecConfigs
                ])
              );
              seq = new Sequent();
              this.startPlaying();
              if (this.seekedDuringPause) {
                this.stream.sendVideoPacketsSinceLastKeyFrame(
                  seekMsec / 1000,
                  () => {
                    return seq.done();
                  }
                );
              } else {
                this.isResuming = true;
                seq.done();
              }
              return seq.wait(1, () => {
                var isResumed;
                isResumed = this.stream.resume();
                this.seekedDuringPause = false;
                if (!isResumed) {
                  return logger.debug(
                    `[rtmp:client=${this.clientid}] cannot resume (EOF reached)`
                  );
                } else {
                  return logger.info(
                    `[rtmp:client=${this.clientid}] resumed at ${
                      msec / 1000
                    } sec (client player time)`
                  );
                }
              });
            });
          } else {
            this.startPlaying();
            return this.respondPlay(
              requestCommand,
              callback,
              (ref2 = this.stream) != null ? ref2.id : void 0
            );
          }
        }
      }
  
      closeStream(callback) {
        this.isPlaying = false;
        this.isWaitingForKeyFrame = false;
        return callback(null);
      }
  
      deleteStream(requestCommand, callback) {
        var _result;
        this.isPlaying = false;
        this.isWaitingForKeyFrame = false;
        _result = createAMF0CommandMessage({
          chunkStreamID: 3,
          timestamp: 0,
          messageStreamID: 0,
          command: '_result',
          transactionID: requestCommand.transactionID,
          objects: [createAMF0Data(null), createAMF0Data(null)]
        });
        return callback(null, _result);
      }
  
      handleAMFDataMessage(dataMessage, callback) {
        callback(null);
        if (dataMessage.objects.length === 0) {
          logger.warn('[rtmp:receive] empty AMF data');
        }
        switch (dataMessage.objects[0].value) {
          case '@setDataFrame':
            this.receiveSetDataFrame(dataMessage);
            break;
          default:
            logger.warn(
              `[rtmp:receive] unknown (not implemented) AMF data: ${dataMessage.objects[0].value}`
            );
            logger.debug(dataMessage);
        }
      }
  
      handleAMFCommandMessage(commandMessage, callback) {
        var ref, ref1, ref2, streamId;
        switch (commandMessage.command) {
          case 'connect':
            // Retain objectEncoding for later use
            //   3=AMF3, 0=AMF0
            this.objectEncoding =
              (ref = commandMessage.objects[0]) != null
                ? (ref1 = ref.value) != null
                  ? ref1.objectEncoding
                  : void 0
                : void 0;
            return this.respondConnect(commandMessage, callback);
          case 'createStream':
            return this.respondCreateStream(commandMessage, callback);
          case 'play':
            streamId =
              (ref2 = commandMessage.objects[1]) != null ? ref2.value : void 0;
            return this.respondPlay(commandMessage, callback);
          case 'closeStream':
            return this.closeStream(callback);
          case 'deleteStream':
            return this.deleteStream(commandMessage, callback);
          case 'pause':
            return this.respondPause(commandMessage, callback);
          case 'pauseRaw':
            logger.debug('[rtmp] ignoring pauseRaw');
            return callback(null);
          //        @respondPauseRaw commandMessage, callback
          // Methods used for publishing from the client
          case 'seek':
            return this.respondSeek(commandMessage, callback);
          case 'releaseStream':
            return this.respondReleaseStream(commandMessage, callback);
          case 'FCPublish':
            return this.respondFCPublish(commandMessage, callback);
          case 'publish':
            return this.respondPublish(commandMessage, callback);
          case 'FCUnpublish':
            return this.respondFCUnpublish(commandMessage, callback);
          default:
            logger.warn(
              `[rtmp:receive] unknown (not implemented) AMF command: ${commandMessage.command}`
            );
            logger.debug(commandMessage);
            //        @respondWithError commandMessage, callback
            return callback(null); // ignore
        }
      }
  
      createAck() {
        if (DEBUG_OUTGOING_RTMP_PACKETS) {
          logger.info('createAck');
        }
        return createRTMPMessage({
          chunkStreamID: 2,
          timestamp: 0, // TODO: Is zero OK?
          messageTypeID: 3, // Acknowledgement
          messageStreamID: 0,
          // number of bytes received so far (4 bytes)
          body: Buffer.from([
            (this.receivedBytes >>> 24) & 0xff,
            (this.receivedBytes >>> 16) & 0xff,
            (this.receivedBytes >>> 8) & 0xff,
            this.receivedBytes & 0xff
          ])
        });
      }
  
      handleData(buf, callback) {
        var consumeNextRTMPMessage, onConsumeAllPackets, outputs, seq;
        this.scheduleTimeout();
        outputs = [];
        seq = new Sequent();
        if (this.windowAckSize != null) {
          this.receivedBytes += buf.length;
          if (
            this.receivedBytes - this.lastSentAckBytes >
            this.windowAckSize / 2
          ) {
            outputs.push(this.createAck());
            this.lastSentAckBytes = this.receivedBytes;
          }
        }
        if (this.state === SESSION_STATE_NEW) {
          if (this.tmpBuf != null) {
            buf = Buffer.concat(
              [this.tmpBuf, buf],
              this.tmpBuf.length + buf.length
            );
            this.tmpBuf = null;
          }
          if (buf.length < 1537) {
            logger.debug('[rtmp] waiting for C0+C1');
            this.tmpBuf = buf;
            return;
          }
          this.tmpBuf = null;
          this.state = SESSION_STATE_HANDSHAKE_ONGOING;
          this.respondHandshake(buf, callback);
          return;
        } else if (this.state === SESSION_STATE_HANDSHAKE_ONGOING) {
          if (this.tmpBuf != null) {
            buf = Buffer.concat(
              [this.tmpBuf, buf],
              this.tmpBuf.length + buf.length
            );
            this.tmpBuf = null;
          }
          if (buf.length < 1536) {
            logger.debug('[rtmp] waiting for C2');
            this.tmpBuf = buf;
            return;
          }
          this.tmpBuf = null;
          // TODO: should we validate C2?
          //      c2Message = buf[0..1535]
          this.state = SESSION_STATE_HANDSHAKE_DONE;
          logger.debug('[rtmp] handshake success');
          if (buf.length <= 1536) {
            callback(null);
            return;
          }
          buf = buf.slice(1536);
        }
        if (this.state !== SESSION_STATE_HANDSHAKE_DONE) {
          logger.error(`[rtmp:receive] unknown session state: ${this.state}`);
          return callback(new Error('Unknown session state'));
        } else {
          if (this.useEncryption) {
            buf = this.decrypt(buf);
          }
          if (this.tmpBuf != null) {
            buf = Buffer.concat(
              [this.tmpBuf, buf],
              this.tmpBuf.length + buf.length
            );
            this.tmpBuf = null;
          }
          onConsumeAllPackets = () => {
            var outbuf;
            outbuf = this.concatenate(outputs);
            if (this.useEncryption) {
              outbuf = this.encrypt(outbuf);
            }
            return callback(null, outbuf);
          };
          consumeNextRTMPMessage = () => {
            var acknowledgementMessage,
              audioData,
              bufferLength,
              commandMessage,
              dataMessage,
              debugMsg,
              dts,
              e,
              j,
              len1,
              msec,
              newChunkSize,
              packageJson,
              parseResult,
              pts,
              ref,
              results,
              rtmpMessage,
              stream,
              streamID,
              timestamp,
              userControlMessage,
              videoData;
            if (buf == null) {
              onConsumeAllPackets();
              return;
            }
            parseResult = this.parseRTMPMessages(buf);
            if (parseResult.consumedLen === 0) {
              // not consumed at all
              this.tmpBuf = buf;
              // no message to process
              onConsumeAllPackets();
              return;
            } else if (parseResult.consumedLen < buf.length) {
              // consumed a part of buffer
              buf = buf.slice(parseResult.consumedLen);
            } else {
              buf = null;
            }
            seq.reset();
            seq.wait(parseResult.rtmpMessages.length, function (err, output) {
              if (err != null) {
                logger.error(`[rtmp:receive] ignoring invalid packet (${err})`);
              }
              if (output != null) {
                outputs.push(output);
              }
              return consumeNextRTMPMessage();
            });
            ref = parseResult.rtmpMessages;
            results = [];
            for (j = 0, len1 = ref.length; j < len1; j++) {
              rtmpMessage = ref[j];
              switch (rtmpMessage.messageTypeID) {
                case 1: // Set Chunk Size
                  newChunkSize =
                    rtmpMessage.body[0] * Math.pow(256, 3) +
                    (rtmpMessage.body[1] << 16) +
                    (rtmpMessage.body[2] << 8) +
                    rtmpMessage.body[3];
                  if (DEBUG_INCOMING_RTMP_PACKETS) {
                    logger.info(`[rtmp:receive] Set Chunk Size: ${newChunkSize}`);
                  }
                  this.receiveChunkSize = newChunkSize;
                  results.push(seq.done());
                  break;
                case 3: // Acknowledgement
                  acknowledgementMessage = parseAcknowledgementMessage(
                    rtmpMessage.body
                  );
                  if (DEBUG_INCOMING_RTMP_PACKETS) {
                    logger.info(
                      `[rtmp:receive] Ack: ${acknowledgementMessage.sequenceNumber}`
                    );
                  }
                  results.push(seq.done());
                  break;
                case 4: // User Control Message
                  userControlMessage = parseUserControlMessage(rtmpMessage.body);
                  if (userControlMessage.eventType === 3) {
                    // SetBufferLength
                    streamID =
                      (userControlMessage.eventData[0] << 24) +
                      (userControlMessage.eventData[1] << 16) +
                      (userControlMessage.eventData[2] << 8) +
                      userControlMessage.eventData[3];
                    bufferLength =
                      (userControlMessage.eventData[4] << 24) +
                      (userControlMessage.eventData[5] << 16) +
                      (userControlMessage.eventData[6] << 8) +
                      userControlMessage.eventData[7];
                    if (DEBUG_INCOMING_RTMP_PACKETS) {
                      logger.info(
                        `[rtmp:receive] SetBufferLength: streamID=${streamID} bufferLength=${bufferLength}`
                      );
                    }
                  } else if (userControlMessage.eventType === 7) {
                    timestamp =
                      (userControlMessage.eventData[0] << 24) +
                      (userControlMessage.eventData[1] << 16) +
                      (userControlMessage.eventData[2] << 8) +
                      userControlMessage.eventData[3];
                    if (DEBUG_INCOMING_RTMP_PACKETS) {
                      logger.info(
                        `[rtmp:receive] PingResponse: timestamp=${timestamp}`
                      );
                    }
                  } else {
                    if (DEBUG_INCOMING_RTMP_PACKETS) {
                      logger.info('[rtmp:receive] User Control Message');
                      logger.info(userControlMessage);
                    }
                  }
                  results.push(seq.done());
                  break;
                case 5: // Window Acknowledgement Size
                  this.windowAckSize =
                    (rtmpMessage.body[0] << 24) +
                    (rtmpMessage.body[1] << 16) +
                    (rtmpMessage.body[2] << 8) +
                    rtmpMessage.body[3];
                  if (DEBUG_INCOMING_RTMP_PACKETS) {
                    logger.info(
                      `[rtmp:receive] WindowAck: ${this.windowAckSize}`
                    );
                  }
                  results.push(seq.done());
                  break;
                case 8: // Audio Message (incoming)
                  if (DEBUG_INCOMING_RTMP_PACKETS) {
                    logger.info('[rtmp:receive] Audio Message');
                  }
                  audioData = this.parseAudioMessage(rtmpMessage.body);
                  if (audioData.adtsFrame != null) {
                    if (!this.isFirstAudioReceived) {
                      this.emit('audio_start', this.stream.id);
                      this.isFirstAudioReceived = true;
                    }
                    pts = dts = flv.convertMsToPTS(rtmpMessage.timestamp);
                    this.emit(
                      'audio_data',
                      this.stream.id,
                      pts,
                      dts,
                      audioData.adtsFrame
                    );
                  }
                  results.push(seq.done());
                  break;
                case 9: // Video Message (incoming)
                  if (DEBUG_INCOMING_RTMP_PACKETS) {
                    logger.info('[rtmp:receive] Video Message');
                  }
                  videoData = this.parseVideoMessage(rtmpMessage.body);
                  if (videoData.nalUnitGlob != null) {
                    if (!this.isFirstVideoReceived) {
                      this.emit('video_start', this.stream.id);
                      this.isFirstVideoReceived = true;
                    }
                    dts = rtmpMessage.timestamp;
                    pts = dts + videoData.info.videoDataTag.compositionTime;
                    pts = flv.convertMsToPTS(pts);
                    dts = flv.convertMsToPTS(dts);
                    this.emit(
                      'video_data',
                      this.stream.id,
                      pts,
                      dts,
                      videoData.nalUnitGlob
                    ); // TODO pts, dts
                  }
                  if (videoData.isEOS) {
                    logger.info(
                      `[rtmp:client=${this.clientid}] received EOS for stream: ${this.stream.id}`
                    );
                    stream = avstreams.get(this.stream.id);
                    if (stream == null) {
                      logger.error(
                        `[rtmp:client=${this.clientid}] error: unknown stream: ${this.stream.id}`
                      );
                    }
                    stream.emit('end');
                  }
                  results.push(seq.done());
                  break;
                case 15: // AMF3 data message
                  try {
                    dataMessage = parseAMF0DataMessage(rtmpMessage.body.slice(1));
                  } catch (error) {
                    e = error;
                    logger.error(
                      `[rtmp] error: failed to parse AMF0 data message: ${e.stack}`
                    );
                    logger.error(
                      `messageTypeID=${rtmpMessage.messageTypeID} body:`
                    );
                    Bits.hexdump(rtmpMessage.body);
                    seq.done(e);
                  }
                  if (dataMessage != null) {
                    if (DEBUG_INCOMING_RTMP_PACKETS) {
                      logger.info('[rtmp:receive] AMF3 data:');
                      logger.info(dataMessage);
                    }
                    results.push(
                      this.handleAMFDataMessage(dataMessage, function (
                        err,
                        output
                      ) {
                        if (err != null) {
                          logger.error(`[rtmp:receive] packet error: ${err}`);
                        }
                        if (output != null) {
                          outputs.push(output);
                        }
                        return seq.done();
                      })
                    );
                  } else {
                    results.push(void 0);
                  }
                  break;
                case 17: // AMF3 command (0x11)
                  // Does the first byte == 0x00 mean AMF0?
                  commandMessage = parseAMF0CommandMessage(
                    rtmpMessage.body.slice(1)
                  );
                  if (DEBUG_INCOMING_RTMP_PACKETS) {
                    debugMsg = `[rtmp:receive] AMF3 command: ${commandMessage.command}`;
                    if (commandMessage.command === 'pause') {
                      msec = commandMessage.objects[2].value;
                      if (commandMessage.objects[1].value === true) {
                        debugMsg += ` (doPause=true msec=${msec})`;
                      } else {
                        debugMsg += ` (doPause=false msec=${msec})`;
                      }
                    } else if (commandMessage.command === 'seek') {
                      msec = commandMessage.objects[1].value;
                      debugMsg += ` (msec=${msec})`;
                    }
                    logger.debug(debugMsg);
                  }
                  results.push(
                    this.handleAMFCommandMessage(commandMessage, function (
                      err,
                      output
                    ) {
                      if (err != null) {
                        logger.error(`[rtmp:receive] packet error: ${err}`);
                      }
                      if (output != null) {
                        outputs.push(output);
                      }
                      return seq.done();
                    })
                  );
                  break;
                case 18: // AMF0 data message
                  try {
                    dataMessage = parseAMF0DataMessage(rtmpMessage.body);
                  } catch (error) {
                    e = error;
                    logger.error(
                      `[rtmp] error: failed to parse AMF0 data message: ${e.stack}`
                    );
                    logger.error(
                      `messageTypeID=${rtmpMessage.messageTypeID} body:`
                    );
                    Bits.hexdump(rtmpMessage.body);
                    seq.done(e);
                  }
                  if (dataMessage != null) {
                    if (DEBUG_INCOMING_RTMP_PACKETS) {
                      logger.info('[rtmp:receive] AMF0 data:');
                      logger.info(dataMessage);
                    }
                    results.push(
                      this.handleAMFDataMessage(dataMessage, function (
                        err,
                        output
                      ) {
                        if (err != null) {
                          logger.error(`[rtmp:receive] packet error: ${err}`);
                        }
                        if (output != null) {
                          outputs.push(output);
                        }
                        return seq.done();
                      })
                    );
                  } else {
                    results.push(void 0);
                  }
                  break;
                case 20: // AMF0 command
                  commandMessage = parseAMF0CommandMessage(rtmpMessage.body);
                  if (DEBUG_INCOMING_RTMP_PACKETS) {
                    logger.info(
                      `[rtmp:receive] AMF0 command: ${commandMessage.command}`
                    );
                  }
                  results.push(
                    this.handleAMFCommandMessage(commandMessage, function (
                      err,
                      output
                    ) {
                      if (err != null) {
                        logger.error(`[rtmp:receive] packet error: ${err}`);
                      }
                      if (output != null) {
                        outputs.push(output);
                      }
                      return seq.done();
                    })
                  );
                  break;
                default:
                  logger.error('----- BUG -----');
                  logger.error(
                    `[rtmp:receive] received unknown (not implemented) message type ID: ${rtmpMessage.messageTypeID}`
                  );
                  logger.error(rtmpMessage);
                  packageJson = require('./package.json');
                  logger.error(`server version: ${packageJson.version}`);
                  logger.error(
                    'Please report this bug along with the video file or relevant part of'
                  );
                  logger.error(
                    'pcap file, and the full (uncut) output of node-rtsp-rtsp-server. Thanks.'
                  );
                  logger.error(
                    'https://github.com/iizukanao/node-rtsp-rtmp-server/issues'
                  );
                  logger.error('---------------');
                  results.push(seq.done());
              }
            }
            return results;
          };
          return consumeNextRTMPMessage();
        }
      }
    };
  
    RTMPServer = class RTMPServer {
      constructor(opts) {
        var ref;
        this.eventListeners = {};
        this.port =
          (ref = opts != null ? opts.rtmpServerPort : void 0) != null
            ? ref
            : 1935;
        this.server = net.createServer((c) => {
          var sess;
          c.clientId = ++clientMaxId;
          sess = new RTMPSession(c);
          logger.info(`[rtmp:client=${sess.clientid}] connected`);
          sessions[c.clientId] = sess;
          sessionsCount++;
          c.rtmpSession = sess;
          sess.on('data', function (data) {
            if (data != null && data.length > 0) {
              return c.write(data);
            }
          });
          sess.on('video_start', (...args) => {
            return this.emit('video_start', ...args);
          });
          sess.on('audio_start', (...args) => {
            return this.emit('audio_start', ...args);
          });
          sess.on('video_data', (...args) => {
            return this.emit('video_data', ...args);
          });
          sess.on('audio_data', (...args) => {
            return this.emit('audio_data', ...args);
          });
          c.on('close', () => {
            logger.info(`[rtmp:client=${sess.clientid}] disconnected`);
            if (sessions[c.clientId] != null) {
              sessions[c.clientId].teardown();
              delete sessions[c.clientId];
              sessionsCount--;
            }
            return this.dumpSessions();
          });
          c.on('error', function (err) {
            logger.error(`[rtmp:client=${sess.clientid}] socket error: ${err}`);
            return c.destroy();
          });
          c.on('data', (data) => {
            return c.rtmpSession.handleData(data, function (err, output) {
              if (err) {
                return logger.error(`[rtmp] error: ${err}`);
              } else if (output != null) {
                if (output.length > 0) {
                  return c.write(output);
                }
              }
            });
          });
          return this.dumpSessions();
        });
      }
  
      start(opts, callback) {
        var ref, serverPort;
        serverPort =
          (ref = opts != null ? opts.port : void 0) != null ? ref : this.port;
        logger.debug(`[rtmp] starting server on port ${serverPort}`);
        return this.server.listen(serverPort, '0.0.0.0', 511, () => {
          logger.info(`[rtmp] server started on port ${serverPort}`);
          return typeof callback === 'function' ? callback() : void 0;
        });
      }
  
      stop(callback) {
        return this.server.close(callback);
      }
  
      on(event, listener) {
        if (this.eventListeners[event] != null) {
          this.eventListeners[event].push(listener);
        } else {
          this.eventListeners[event] = [listener];
        }
      }
  
      emit(event, ...args) {
        var j, len1, listener, ref;
        if (this.eventListeners[event] != null) {
          ref = this.eventListeners[event];
          for (j = 0, len1 = ref.length; j < len1; j++) {
            listener = ref[j];
            listener(...args);
          }
        }
      }
  
      dumpSessions() {
        var rtmptSession, session, sessionID;
        logger.raw(`[rtmp: ${sessionsCount} sessions]`);
        for (sessionID in sessions) {
          session = sessions[sessionID];
          logger.raw(' ' + session.toString());
        }
        if (rtmptSessionsCount > 0) {
          logger.raw(`[rtmpt: ${rtmptSessionsCount} sessions]`);
          for (sessionID in rtmptSessions) {
            rtmptSession = rtmptSessions[sessionID];
            logger.raw(' ' + rtmptSession.toString());
          }
        }
      }
  
      teardownRTMPTClient(socket) {
        var ref, tsession;
        if (socket.rtmptClientID != null) {
          logger.debug(`[rtmp] teardownRTMPTClient: ${socket.rtmptClientID}`);
          tsession = rtmptSessions[socket.rtmptClientID];
          if (tsession != null) {
            if ((ref = tsession.rtmpSession) != null) {
              ref.teardown();
            }
            delete rtmptSessions[socket.rtmptClientID];
            return rtmptSessionsCount--;
          }
        }
      }
  
      updateConfig(newConfig) {
        return (config = newConfig);
      }
  
      // Packets must be come in DTS ascending order
      sendVideoPacket(stream, nalUnits, pts, dts) {
        var buf,
          compositionTimeMs,
          firstByte,
          hasKeyFrame,
          j,
          k,
          len1,
          len2,
          message,
          nalUnit,
          nalUnitType,
          payloadLen,
          timestamp,
          totalBytes;
        if (DEBUG_INCOMING_STREAM_DATA) {
          totalBytes = 0;
          for (j = 0, len1 = nalUnits.length; j < len1; j++) {
            nalUnit = nalUnits[j];
            totalBytes += nalUnit.length;
          }
          logger.info(
            `received video: stream=${stream.id} ${totalBytes} bytes; ${
              nalUnits.length
            } NAL units (${nalUnits
              .map(function (nalu) {
                return nalu[0] & 0x1f;
              })
              .join(',')}); pts=${pts}`
          );
        }
        if (dts > pts) {
          throw new Error(`pts must be >= dts (pts=${pts} dts=${dts})`);
        }
        timestamp = convertPTSToMilliseconds(dts);
        if (sessionsCount + rtmptSessionsCount === 0) {
          return;
        }
        message = [];
        hasKeyFrame = false;
        // This format may be AVCSample in 5.3.4.2.1 of ISO 14496-15
        for (k = 0, len2 = nalUnits.length; k < len2; k++) {
          nalUnit = nalUnits[k];
          nalUnitType = h264.getNALUnitType(nalUnit);
          if (
            config.dropH264AccessUnitDelimiter &&
            nalUnitType === h264.NAL_UNIT_TYPE_ACCESS_UNIT_DELIMITER
          ) {
            // ignore access unit delimiters
            continue;
          }
          if (nalUnitType === h264.NAL_UNIT_TYPE_IDR_PICTURE) {
            // 5
            hasKeyFrame = true;
          }
          payloadLen = nalUnit.length;
          // The length of this data is specified in
          // configuration data that has already been sent
          message.push(
            Buffer.from([
              (payloadLen >>> 24) & 0xff,
              (payloadLen >>> 16) & 0xff,
              (payloadLen >>> 8) & 0xff,
              payloadLen & 0xff
            ])
          );
          message.push(nalUnit);
        }
        if (message.length === 0) {
          return;
        }
        // Add VIDEODATA tag
        // message is empty
        if (hasKeyFrame) {
          // IDR picture (key frame)
          firstByte = (1 << 4) | config.flv.videocodecid; // non-IDR picture (inter frame)
        } else {
          firstByte = (2 << 4) | config.flv.videocodecid;
        }
        // Composition time offset: composition time (PTS) - decoding time (DTS)
        compositionTimeMs = Math.floor((pts - dts) / 90); // convert to milliseconds
        if (compositionTimeMs > 0x7fffff) {
          // composition time is signed 24-bit integer
          compositionTimeMs = 0x7fffff;
        }
        message.unshift(
          Buffer.from([
            // VIDEODATA tag
            firstByte,
            AVC_PACKET_TYPE_NALU,
            // Composition time (signed 24-bit integer)
            // See ISO 14496-12, 8.15.3 for details
            (compositionTimeMs >> 16) & 0xff, // composition time (PTS - DTS)
            (compositionTimeMs >> 8) & 0xff,
            compositionTimeMs & 0xff
          ])
        );
        buf = Buffer.concat(message);
        queueVideoMessage(stream, {
          body: buf,
          timestamp: timestamp,
          isKeyFrame: hasKeyFrame,
          compositionTime: compositionTimeMs
        });
        stream.rtmpLastTimestamp = timestamp;
      }
  
      sendAudioPacket(stream, rawDataBlock, timestamp) {
        var buf, headerBytes;
        if (DEBUG_INCOMING_STREAM_DATA) {
          logger.info(
            `received audio: stream=${stream.id} ${rawDataBlock.length} bytes; timestamp=${timestamp}`
          );
        }
        timestamp = convertPTSToMilliseconds(timestamp);
        if (sessionsCount + rtmptSessionsCount === 0) {
          return;
        }
        // TODO: support other than AAC too?
        headerBytes = Buffer.from(
          flv.createAACAudioDataTag({
            aacPacketType: flv.AAC_PACKET_TYPE_RAW
          })
        );
        buf = Buffer.concat([headerBytes, rawDataBlock], rawDataBlock.length + 2);
        queueAudioMessage(stream, {
          body: buf,
          timestamp: timestamp
        });
        stream.rtmpLastTimestamp = timestamp;
      }
  
      sendEOS(stream) {
        var lastTimestamp, playComplete, playStop, ref, streamEOF1;
        logger.debug(`[rtmp] sendEOS for stream ${stream.id}`);
        lastTimestamp = (ref = stream.rtmpLastTimestamp) != null ? ref : 0;
        playComplete = createAMF0DataMessageParams({
          chunkStreamID: 4,
          timestamp: lastTimestamp,
          messageStreamID: 1,
          objects: [
            createAMF0Data('onPlayStatus'),
            createAMF0Object({
              level: 'status',
              code: 'NetStream.Play.Complete',
              duration: 0,
              bytes: 0
            })
          ]
        });
        playStop = createAMF0CommandMessageParams({
          chunkStreamID: 4, // 5?
          timestamp: lastTimestamp,
          messageStreamID: 1,
          command: 'onStatus',
          transactionID: 0,
          objects: [
            createAMF0Data(null),
            createAMF0Object({
              level: 'status',
              code: 'NetStream.Play.Stop',
              description: `Stopped playing ${stream.id}.`,
              clientid: this.clientid,
              reason: '',
              details: stream.id
            })
          ]
        });
        streamEOF1 = {
          chunkStreamID: 2,
          timestamp: 0,
          messageTypeID: 0x04, // User Control Message
          messageStreamID: 0,
          body: Buffer.from([
            // Stream EOF (see 7.1.7. User Control Message Events)
            0,
            1,
            // Stream ID of the stream that reaches EOF
            0,
            0,
            0,
            1
          ])
        };
        return queueRTMPMessages(stream, [playComplete, playStop, streamEOF1], {
          forceFlush: true,
          hasControlMessage: true
        });
      }
  
      handleRTMPTRequest(req, callback) {
        var client, command, index, info, match, response, results, session;
        // /fcs/ident2 will be handled in another place
        if (
          (match = /^\/([^\/]+)\/([^\/]+)(?:\/([^\/]+))?/.exec(req.uri)) != null
        ) {
          command = match[1];
          client = match[2];
          index = match[3];
          if (index == null) {
            index = client;
          }
          if (command === 'fcs' && index === 'ident2') {
            response = `HTTP/1.1 400 RTMPT command /fcs/ident2 is not supported
  Cache-Control: no-cache
  Content-Type: text/plain
  Content-Length: 0
  Connection: keep-alive
  
  `.replace(/\n/g, '\r\n');
            return callback(null, response);
          } else if (command === 'open') {
            session = new RTMPTSession(req.socket, () => {
              rtmptSessions[session.id] = session;
              rtmptSessionsCount++;
              session.respondOpen(req, callback);
              return this.dumpSessions();
            });
            session.on('video_start', (...args) => {
              return this.emit('video_start', ...args);
            });
            session.on('audio_start', (...args) => {
              return this.emit('audio_start', ...args);
            });
            session.on('video_data', (...args) => {
              return this.emit('video_data', ...args);
            });
            return session.on('audio_data', (...args) => {
              return this.emit('audio_data', ...args);
            });
          } else if (command === 'idle') {
            session = rtmptSessions[client];
            if (session != null) {
              // TODO: Do we have to sort requests by index?
              index = parseInt(index);
              session.respondIdle(req, callback);
              if (session.requestBuffer != null) {
                return (session.requestBuffer.nextIndex = index + 1);
              } else {
                return (session.requestBuffer = {
                  nextIndex: index + 1,
                  reqs: []
                });
              }
            } else {
              return callback(new Error('No such session'));
            }
          } else if (command === 'send') {
            session = rtmptSessions[client];
            if (session != null) {
              index = parseInt(index);
              if (session.requestBuffer != null) {
                if (index > session.requestBuffer.nextIndex) {
                  // If HTTP-tunneling (RTMPT or RTMPTE) is used, Flash Player
                  // may send requests in parallel using multiple connections.
                  // So we have to buffer and sort the requests.
                  session.requestBuffer.reqs.push({
                    req: req,
                    index: index,
                    callback: callback
                  });
                  session.requestBuffer.reqs.sort(function (a, b) {
                    return a.index - b.index;
                  });
                } else if (index === session.requestBuffer.nextIndex) {
                  session.respondSend(req, callback);
                  session.requestBuffer.nextIndex = index + 1;
                } else {
                  logger.warn(`[rtmpt] received stale request: ${index}`);
                }
                // Discard old requests
                if (
                  session.requestBuffer.reqs.length > 0 &&
                  index - session.requestBuffer.reqs[0].index >
                    RTMPT_SEND_REQUEST_BUFFER_SIZE
                ) {
                  info = session.requestBuffer.reqs[0];
                  if (info.index === session.requestBuffer.nextIndex + 1) {
                    logger.warn(
                      `[rtmpt] discarded lost request: ${session.requestBuffer.nextIndex}`
                    );
                  } else {
                    logger.warn(
                      `[rtmpt] discarded lost requests: ${
                        session.requestBuffer.nextIndex
                      }-${info.index - 1}`
                    );
                  }
                  session.requestBuffer.nextIndex = info.index;
                }
                results = [];
                // Consume buffered requests
                while (
                  session.requestBuffer.reqs.length > 0 &&
                  session.requestBuffer.reqs[0].index ===
                    session.requestBuffer.nextIndex
                ) {
                  info = session.requestBuffer.reqs.shift();
                  // TODO: Call respondSend with setImmediate()?
                  session.respondSend(info.req, info.callback);
                  results.push(
                    (session.requestBuffer.nextIndex = info.index + 1)
                  );
                }
                return results;
              } else {
                // TODO: Does index start at zero?
                session.requestBuffer = {
                  nextIndex: index + 1,
                  reqs: []
                };
                return session.respondSend(req, callback);
              }
            } else {
              return callback(new Error('No such session'));
            }
          } else if (command === 'close') {
            session = rtmptSessions[client];
            if (session != null) {
              return session.respondClose(req, callback);
            } else {
              return callback(new Error('No such session'));
            }
          } else {
            return callback(new Error(`Unknown command: ${command}`));
          }
        } else {
          return callback(new Error(`Unknown URI: ${req.uri}`));
        }
      }
    };
  
    // Generate a new sessionID without collision
    generateNewSessionID = function (callback) {
      return generateSessionID(function (err, sid) {
        if (err) {
          callback(err);
          return;
        }
        if (rtmptSessions[sid] != null) {
          return generateNewSessionID(callback);
        } else {
          return callback(null, sid);
        }
      });
    };
  
    // Generate a new random session ID
    // NOTE: Session ID must be 31 characters or less
    generateSessionID = function (callback) {
      return crypto.randomBytes(16, function (err, buf) {
        var sid;
        if (err) {
          return callback(err);
        } else {
          sid = buf.toString('hex').slice(0, 31);
          return callback(null, sid);
        }
      });
    };
  
    RTMPTSession = class RTMPTSession {
      constructor(socket, callback) {
        this.creationDate = new Date(); // for debug
        this.eventListeners = {};
        this.socket = socket;
        this.pollingDelay = 1;
        this.pendingResponses = [];
        this.requestBuffer = null;
        this.rtmpSession = new RTMPSession(socket);
        this.rtmpSession.on('data', (data) => {
          this.scheduleTimeout();
          return this.pendingResponses.push(data);
        });
        this.rtmpSession.on('video_start', (...args) => {
          return this.emit('video_start', ...args);
        });
        this.rtmpSession.on('audio_start', (...args) => {
          return this.emit('audio_start', ...args);
        });
        this.rtmpSession.on('video_data', (...args) => {
          return this.emit('video_data', ...args);
        });
        this.rtmpSession.on('audio_data', (...args) => {
          return this.emit('audio_data', ...args);
        });
        this.rtmpSession.on('teardown', () => {
          logger.info(`[rtmpt:${this.rtmpSession.clientid}] received teardown`);
          return this.close();
        });
        generateNewSessionID((err, sid) => {
          if (err) {
            return callback(err);
          } else {
            this.id = sid;
            this.socket.rtmptClientID = this.id;
            this.scheduleTimeout();
            return typeof callback === 'function' ? callback(null) : void 0;
          }
        });
      }
  
      toString() {
        return `${this.id}: rtmp_session=${this.rtmpSession.clientid} created_at=${this.creationDate}`;
      }
  
      on(event, listener) {
        if (this.eventListeners[event] != null) {
          this.eventListeners[event].push(listener);
        } else {
          this.eventListeners[event] = [listener];
        }
      }
  
      emit(event, ...args) {
        var j, len1, listener, ref;
        if (this.eventListeners[event] != null) {
          ref = this.eventListeners[event];
          for (j = 0, len1 = ref.length; j < len1; j++) {
            listener = ref[j];
            listener(...args);
          }
        }
      }
  
      clearTimeout() {
        if (this.timeoutTimer != null) {
          clearTimeout(this.timeoutTimer);
          return (this.timeoutTimer = null);
        }
      }
  
      scheduleTimeout() {
        if (this.isClosed) {
          return;
        }
        this.clearTimeout();
        this.lastTimeoutScheduledTime = Date.now();
        return (this.timeoutTimer = setTimeout(() => {
          if (this.isClosed) {
            return;
          }
          if (this.timeoutTimer == null) {
            return;
          }
          if (
            Date.now() - this.lastTimeoutScheduledTime <
            config.rtmptSessionTimeoutMs
          ) {
            return;
          }
          logger.info(`[rtmpt] session timeout: ${this.id}`);
          return this.close();
        }, config.rtmptSessionTimeoutMs));
      }
  
      close() {
        if (this.isClosed) {
          return;
        }
        // already closed
        logger.info(`[rtmpt:${this.rtmpSession.clientid}] close`);
        this.isClosed = true;
        this.clearTimeout();
        if (this.rtmpSession != null) {
          this.rtmpSession.teardown();
          this.rtmpSession = null;
        }
        if (rtmptSessions[this.id] != null) {
          delete rtmptSessions[this.id];
          return rtmptSessionsCount--;
        }
      }
  
      createHTTPResponse(buf) {
        var allBytes, contentLength, header;
        this.scheduleTimeout();
        if (buf != null) {
          contentLength = buf.length;
        } else {
          contentLength = 0;
        }
        header = `HTTP/1.1 200 OK
  Cache-Control: no-cache
  Content-Length: ${contentLength}
  Connection: keep-alive
  Content-Type: application/x-fcs
  
  `.replace(/\n/g, '\r\n');
        allBytes = Buffer.from(header, 'utf8');
        if (buf != null) {
          allBytes = Buffer.concat([allBytes, buf], allBytes.length + buf.length);
        }
        return allBytes;
      }
  
      respondOpen(req, callback) {
        var body, bodyBytes;
        this.scheduleTimeout();
        body = this.id + '\n';
        bodyBytes = Buffer.from(body, 'utf8');
        return callback(null, this.createHTTPResponse(bodyBytes));
      }
  
      respondIdle(req, callback) {
        var allBytes, bufs, j, len1, ref, resp, totalLength;
        this.scheduleTimeout();
        bufs = [Buffer.from([this.pollingDelay])];
        totalLength = 1;
        ref = this.pendingResponses;
        for (j = 0, len1 = ref.length; j < len1; j++) {
          resp = ref[j];
          bufs.push(resp);
          totalLength += resp.length;
        }
        this.pendingResponses = [];
        allBytes = Buffer.concat(bufs, totalLength);
        return callback(null, this.createHTTPResponse(allBytes));
      }
  
      respondSend(req, callback) {
        this.scheduleTimeout();
        return this.rtmpSession.handleData(req.rawbody, (err, output) => {
          var allBytes, interval;
          if (err) {
            logger.error(`[rtmpt:send-resp] Error: ${err}`);
            return callback(err);
          } else if (output != null) {
            interval = Buffer.from([this.pollingDelay]);
            allBytes = Buffer.concat([interval, output], 1 + output.length);
            return callback(null, this.createHTTPResponse(allBytes));
          } else {
            // No response from me
            allBytes = Buffer.from([this.pollingDelay]);
            return callback(null, this.createHTTPResponse(allBytes));
          }
        });
      }
  
      respondClose(req, callback) {
        var allBytes;
        allBytes = Buffer.from([this.pollingDelay]);
        this.close();
        return callback(null, this.createHTTPResponse(allBytes));
      }
    };
  
    api = {
      RTMPServer: RTMPServer
    };
  
    module.exports = api;
  }.call(this));
  