/**
   * Retransmits a packet to {@link #channel}. If the destination supports the RTX format, the
   * packet will be encapsulated in RTX, otherwise, the packet will be retransmitted as-is.
   *
   * @param pkt the packet to retransmit.
   * @param after the {@code TransformEngine} in the chain of {@code TransformEngine}s of the
   *     associated {@code MediaStream} after which the injection of {@code pkt} is to begin
   * @return {@code true} if the packet was successfully retransmitted, {@code false} otherwise.
   */
  public boolean retransmit(RawPacket pkt, TransformEngine after) {
    boolean destinationSupportsRtx = channel.getRtxPayloadType() != -1;
    boolean retransmitPlain;

    if (destinationSupportsRtx) {
      long rtxSsrc = getPairedSsrc(pkt.getSSRC());

      if (rtxSsrc == -1) {
        logger.warn("Cannot find SSRC for RTX, retransmitting plain.");
        retransmitPlain = true;
      } else {
        retransmitPlain = !encapsulateInRtxAndTransmit(pkt, rtxSsrc);
      }
    } else {
      retransmitPlain = true;
    }

    if (retransmitPlain) {
      MediaStream mediaStream = channel.getStream();

      if (mediaStream != null) {
        try {
          mediaStream.injectPacket(pkt, /* data */ true, after);
        } catch (TransmissionFailedException tfe) {
          logger.warn("Failed to retransmit a packet.");
          return false;
        }
      }
    }

    return true;
  }
  /**
   * Tries to find an SSRC paired with {@code ssrc} in an FID group in one of the channels from
   * {@link #channel}'s {@code Content}. Returns -1 on failure.
   *
   * @param pkt the {@code RawPacket} that holds the RTP packet for which to find a paired SSRC.
   * @return An SSRC paired with {@code ssrc} in an FID group, or -1.
   */
  private long getRtxSsrc(RawPacket pkt) {
    StreamRTPManager receiveRTPManager =
        channel.getStream().getRTPTranslator().findStreamRTPManagerByReceiveSSRC(pkt.getSSRC());

    MediaStreamTrackReceiver receiver = null;
    if (receiveRTPManager != null) {
      MediaStream receiveStream = receiveRTPManager.getMediaStream();
      if (receiveStream != null) {
        receiver = receiveStream.getMediaStreamTrackReceiver();
      }
    }

    if (receiver == null) {
      return -1;
    }

    RTPEncoding encoding = receiver.resolveRTPEncoding(pkt);
    if (encoding == null) {
      logger.warn(
          "encoding_not_found"
              + ",stream_hash="
              + channel.getStream().hashCode()
              + " ssrc="
              + pkt.getSSRCAsLong());
      return -1;
    }

    return encoding.getRTXSSRC();
  }
  /**
   * {@inheritDoc}
   *
   * <p>Uses the direction of the media stream as a fallback. TODO: return the direction of the
   * GTalk session?
   */
  @Override
  public MediaDirection getDirection(MediaType mediaType) {
    MediaStream stream = getMediaHandler().getStream(mediaType);
    if (stream != null) {
      MediaDirection direction = stream.getDirection();
      return direction == null ? MediaDirection.INACTIVE : direction;
    }

    return MediaDirection.INACTIVE;
  }
  /**
   * Iterate through all the <tt>ReceiveStream</tt>s that this <tt>MediaStream</tt> has and make
   * <tt>RTCPReportBlock</tt>s for all of them.
   *
   * @param time
   * @return
   */
  private RTCPReportBlock[] makeRTCPReportBlocks(long time) {
    MediaStream stream = getStream();
    // State validation.
    if (stream == null) {
      logger.warn("stream is null.");
      return MIN_RTCP_REPORTS_BLOCKS_ARRAY;
    }

    StreamRTPManager streamRTPManager = stream.getStreamRTPManager();
    if (streamRTPManager == null) {
      logger.warn("streamRTPManager is null.");
      return MIN_RTCP_REPORTS_BLOCKS_ARRAY;
    }

    Collection<ReceiveStream> receiveStreams = streamRTPManager.getReceiveStreams();

    if (receiveStreams == null || receiveStreams.size() == 0) {
      logger.info("There are no receive streams to build report " + "blocks for.");
      return MIN_RTCP_REPORTS_BLOCKS_ARRAY;
    }

    SSRCCache cache = streamRTPManager.getSSRCCache();
    if (cache == null) {
      logger.info("cache is null.");
      return MIN_RTCP_REPORTS_BLOCKS_ARRAY;
    }

    // Create the return object.
    Collection<RTCPReportBlock> rtcpReportBlocks = new ArrayList<RTCPReportBlock>();

    // Populate the return object.
    for (ReceiveStream receiveStream : receiveStreams) {
      // Dig into the guts of FMJ and get the stats for the current
      // receiveStream.
      SSRCInfo info = cache.cache.get((int) receiveStream.getSSRC());

      if (!info.ours && info.sender) {
        RTCPReportBlock rtcpReportBlock = info.makeReceiverReport(time);
        rtcpReportBlocks.add(rtcpReportBlock);
      }
    }

    return rtcpReportBlocks.toArray(new RTCPReportBlock[rtcpReportBlocks.size()]);
  }
  /**
   * Encapsulates {@code pkt} in the RTX format, using {@code rtxSsrc} as its SSRC, and transmits it
   * to {@link #channel} by injecting it in the {@code MediaStream}.
   *
   * @param pkt the packet to transmit.
   * @param rtxSsrc the SSRC for the RTX stream.
   * @return {@code true} if the packet was successfully retransmitted, {@code false} otherwise.
   */
  private boolean encapsulateInRtxAndTransmit(RawPacket pkt, long rtxSsrc) {
    byte[] buf = pkt.getBuffer();
    int len = pkt.getLength();
    int off = pkt.getOffset();
    byte[] newBuf = buf;
    if (buf.length < len + 2) {
      // FIXME The byte array newly allocated and assigned to newBuf must
      // be made known to pkt eventually.
      newBuf = new byte[len + 2];
    }

    int osn = pkt.getSequenceNumber();
    int headerLength = pkt.getHeaderLength();
    int payloadLength = len - headerLength;
    System.arraycopy(buf, off, newBuf, 0, headerLength);
    // FIXME If newBuf is actually buf, then we will override the first two
    // bytes of the payload bellow.
    newBuf[headerLength] = (byte) ((osn >> 8) & 0xff);
    newBuf[headerLength + 1] = (byte) (osn & 0xff);
    System.arraycopy(buf, off + headerLength, newBuf, headerLength + 2, payloadLength);
    // FIXME We tried to extend the payload of pkt by two bytes above but
    // we never told pkt that its length has increased by these two bytes.

    MediaStream mediaStream = channel.getStream();
    if (mediaStream != null) {
      pkt.setSSRC((int) rtxSsrc);
      // Only call getNextRtxSequenceNumber() when we're sure we're going
      // to transmit a packet, because it consumes a sequence number.
      pkt.setSequenceNumber(getNextRtxSequenceNumber(rtxSsrc));
      try {
        mediaStream.injectPacket(pkt, /* data */ true, /* after */ null);
      } catch (TransmissionFailedException tfe) {
        logger.warn("Failed to transmit an RTX packet.");
        return false;
      }
    }

    return true;
  }
  /**
   * Encapsulates {@code pkt} in the RTX format, using {@code rtxSsrc} as its SSRC, and transmits it
   * to {@link #channel} by injecting it in the {@code MediaStream}.
   *
   * @param pkt the packet to transmit.
   * @param rtxSsrc the SSRC for the RTX stream.
   * @param after the {@code TransformEngine} in the chain of {@code TransformEngine}s of the
   *     associated {@code MediaStream} after which the injection of {@code pkt} is to begin
   * @return {@code true} if the packet was successfully retransmitted, {@code false} otherwise.
   */
  private boolean encapsulateInRtxAndTransmit(RawPacket pkt, long rtxSsrc, TransformEngine after) {
    byte[] buf = pkt.getBuffer();
    int len = pkt.getLength();
    int off = pkt.getOffset();

    byte[] newBuf = new byte[len + 2];
    RawPacket rtxPkt = new RawPacket(newBuf, 0, len + 2);

    int osn = pkt.getSequenceNumber();
    int headerLength = pkt.getHeaderLength();
    int payloadLength = pkt.getPayloadLength();

    // Copy the header.
    System.arraycopy(buf, off, newBuf, 0, headerLength);

    // Set the OSN field.
    newBuf[headerLength] = (byte) ((osn >> 8) & 0xff);
    newBuf[headerLength + 1] = (byte) (osn & 0xff);

    // Copy the payload.
    System.arraycopy(buf, off + headerLength, newBuf, headerLength + 2, payloadLength);

    MediaStream mediaStream = channel.getStream();
    if (mediaStream != null) {
      rtxPkt.setSSRC((int) rtxSsrc);
      rtxPkt.setPayloadType(rtxPayloadType);
      // Only call getNextRtxSequenceNumber() when we're sure we're going
      // to transmit a packet, because it consumes a sequence number.
      rtxPkt.setSequenceNumber(getNextRtxSequenceNumber(rtxSsrc));
      try {
        mediaStream.injectPacket(rtxPkt, /* data */ true, after);
      } catch (TransmissionFailedException tfe) {
        logger.warn("Failed to transmit an RTX packet.");
        return false;
      }
    }

    return true;
  }
  /**
   * Notifies this instance that the dynamic payload types of the associated {@link MediaStream}
   * have changed.
   */
  public void onDynamicPayloadTypesChanged() {
    rtxPayloadType = -1;
    rtxAssociatedPayloadType = -1;

    MediaStream mediaStream = channel.getStream();

    Map<Byte, MediaFormat> mediaFormatMap = mediaStream.getDynamicRTPPayloadTypes();

    Iterator<Map.Entry<Byte, MediaFormat>> it = mediaFormatMap.entrySet().iterator();

    while (it.hasNext() && rtxPayloadType == -1) {
      Map.Entry<Byte, MediaFormat> entry = it.next();
      MediaFormat format = entry.getValue();
      if (!Constants.RTX.equalsIgnoreCase(format.getEncoding())) {
        continue;
      }

      // XXX(gp) we freak out if multiple codecs with RTX support are
      // present.
      rtxPayloadType = entry.getKey();
      rtxAssociatedPayloadType = Byte.parseByte(format.getFormatParameters().get("apt"));
    }
  }