/**
   * Notifies this {@code SimulcastReceiver} that a specific {@code SimulcastReceiver} has detected
   * the start of a new video frame in the RTP stream that it represents. Determines whether any of
   * {@link #simulcastLayers} other than {@code source} have been paused/stopped by the remote peer.
   * The determination is based on counting (video) frames.
   *
   * @param source the {@code SimulcastLayer} which is the source of the event i.e. which has
   *     detected the start of a new video frame in the RTP stream that it represents
   * @param pkt the {@code RawPacket} which was received by {@code source} and possibly influenced
   *     the decision that a new view frame was started in the RTP stream represented by {@code
   *     source}
   * @param layers the set of {@code SimulcastLayer}s managed by this {@code SimulcastReceiver}.
   *     Explicitly provided to the method in order to avoid invocations of {@link
   *     #getSimulcastLayers()} because the latter makes a copy at the time of this writing.
   */
  private void simulcastLayerFrameStarted(
      SimulcastLayer source, RawPacket pkt, SimulcastLayer[] layers) {
    // Allow the value of the constant TIMEOUT_ON_FRAME_COUNT to disable (at
    // compile time) the frame-based approach to the detection of layer
    // drops.
    if (TIMEOUT_ON_FRAME_COUNT <= 1) return;

    // Timeouts in layers caused by source may occur only based on the span
    // (of time or received frames) during which source has received
    // TIMEOUT_ON_FRAME_COUNT number of frames. The current method
    // invocation signals the receipt of 1 frame by source.
    int indexOfLastSourceOccurrenceInHistory = -1;
    int sourceFrameCount = 0;
    int ix = 0;

    for (Iterator<SimulcastLayer> it = simulcastLayerFrameHistory.iterator(); it.hasNext(); ++ix) {
      if (it.next() == source) {
        if (indexOfLastSourceOccurrenceInHistory != -1) {
          // Prune simulcastLayerFrameHistory so that it does not
          // become unnecessarily long.
          it.remove();
        } else if (++sourceFrameCount >= TIMEOUT_ON_FRAME_COUNT - 1) {
          // The span of TIMEOUT_ON_FRAME_COUNT number of frames
          // received by source only is to be examined for the
          // purposes of timeouts. The current method invocations
          // signals the receipt of 1 frame by source so
          // TIMEOUT_ON_FRAME_COUNT - 1 occurrences of source in
          // simulcastLayerFrameHistory is enough.
          indexOfLastSourceOccurrenceInHistory = ix;
        }
      }
    }

    if (indexOfLastSourceOccurrenceInHistory != -1) {
      // Presumably, if a SimulcastLayer is active, all SimulcastLayers
      // before it (according to SimulcastLayer's order) are active as
      // well. Consequently, timeouts may occur in SimulcastLayers which
      // are after source.
      boolean maybeTimeout = false;

      for (SimulcastLayer layer : layers) {
        if (maybeTimeout) {
          // There's no point in timing layer out if it's timed out
          // already.
          if (layer.isStreaming()) {
            maybeTimeout(source, pkt, layer, indexOfLastSourceOccurrenceInHistory);
          }
        } else if (layer == source) {
          maybeTimeout = true;
        }
      }
    }

    // As previously stated, the current method invocation signals the
    // receipt of 1 frame by source.
    simulcastLayerFrameHistory.add(0, source);
    // TODO Prune simulcastLayerFrameHistory by forgetting so that it does
    // not become too long.
  }
  /**
   * Determines whether {@code effect} has been paused/stopped by the remote peer. The determination
   * is based on counting frames and is triggered by the receipt of (a piece of) a new (video) frame
   * by {@code cause}.
   *
   * @param cause the {@code SimulcastLayer} which has received (a piece of) a new (video) frame and
   *     has thus triggered a check on {@code effect}
   * @param pkt the {@code RawPacket} which was received by {@code cause} and possibly influenced
   *     the decision to trigger a check on {@code effect}
   * @param effect the {@code SimulcastLayer} which is to be checked whether it looks like it has
   *     been paused/stopped by the remote peer
   * @param endIndexInSimulcastLayerFrameHistory
   */
  private void maybeTimeout(
      SimulcastLayer cause,
      RawPacket pkt,
      SimulcastLayer effect,
      int endIndexInSimulcastLayerFrameHistory) {
    Iterator<SimulcastLayer> it = simulcastLayerFrameHistory.iterator();
    boolean timeout = true;

    for (int ix = 0; it.hasNext() && ix < endIndexInSimulcastLayerFrameHistory; ++ix) {
      if (it.next() == effect) {
        timeout = false;
        break;
      }
    }
    if (timeout) {
      effect.maybeTimeout(pkt);

      if (!effect.isStreaming()) {
        // Since effect has been determined to have been paused/stopped
        // by the remote peer, its possible presence in
        // simulcastLayerFrameHistory is irrelevant now. In other words,
        // remove effect from simulcastLayerFrameHistory.
        while (it.hasNext()) {
          if (it.next() == effect) it.remove();
        }
      }
    }
  }