/**
   * Extracts a list of endpoint IDs from a list of {@link Endpoint}s.
   *
   * @param endpoints the list of {@link Endpoint}s.
   * @return the list of IDs of endpoints in {@code endpoints}.
   */
  private List<String> getIDs(List<Endpoint> endpoints) {
    if (endpoints != null && !endpoints.isEmpty()) {
      List<String> endpointIds = new LinkedList<>();
      for (Endpoint endpoint : endpoints) {
        endpointIds.add(endpoint.getID());
      }
      return endpointIds;
    }

    return null;
  }
  /**
   * Notifies this instance that the ordered list of endpoints (specified as a list of endpoint IDs)
   * in the conference has changed.
   *
   * @param endpointIds the new ordered list of endpoints (specified as a list of endpoint IDs) in
   *     the conference.
   * @return the list of IDs of endpoints which were added to the list of forwarded endpoints as a
   *     result of the call.
   */
  private synchronized List<String> speechActivityEndpointIdsChanged(List<String> endpointIds) {
    if (conferenceSpeechActivityEndpoints.equals(endpointIds)) {
      if (logger.isDebugEnabled()) {
        logger.debug("Conference endpoints have not changed.");
      }
      return null;
    } else {
      List<String> newEndpoints = new LinkedList<>(endpointIds);
      newEndpoints.removeAll(conferenceSpeechActivityEndpoints);

      conferenceSpeechActivityEndpoints = endpointIds;

      return update(newEndpoints);
    }
  }
  /**
   * Checks whether RTP packets from {@code sourceChannel} should be forwarded to {@link #channel}.
   *
   * @param sourceChannel the channel.
   * @return {@code true} iff RTP packets from {@code sourceChannel} should be forwarded to {@link
   *     #channel}.
   */
  public boolean isForwarded(Channel sourceChannel) {
    if (lastN < 0 && currentLastN < 0) {
      // If Last-N is disabled, we forward everything.
      return true;
    }

    if (sourceChannel == null) {
      logger.warn("Invalid sourceChannel: null.");
      return false;
    }

    Endpoint channelEndpoint = sourceChannel.getEndpoint();
    if (channelEndpoint == null) {
      logger.warn("sourceChannel has no endpoint.");
      return false;
    }

    if (forwardedEndpoints == INITIAL_EMPTY_LIST) {
      // LastN is enabled, but we haven't yet initialized the list of
      // endpoints in the conference.
      initializeConferenceEndpoints();
    }

    // This may look like a place to optimize, because we query an unordered
    // list (in O(n)) and it executes on each video packet if lastN is
    // enabled. However, the size of  forwardedEndpoints is restricted to
    // lastN and so small enough that it is not worth optimizing.
    return forwardedEndpoints.contains(channelEndpoint.getID());
  }
  /**
   * Initializes the local list of endpoints ({@link #speechActivityEndpointsChanged(List)}) with
   * the current endpoints from the conference.
   */
  public synchronized void initializeConferenceEndpoints() {
    speechActivityEndpointsChanged(channel.getConferenceSpeechActivity().getEndpoints());

    if (logger.isDebugEnabled()) {
      logger.debug(
          "Initialized the list of endpoints: " + conferenceSpeechActivityEndpoints.toString());
    }
  }
  /**
   * Sets the list of "pinned" endpoints (i.e. endpoints for which video should always be forwarded,
   * regardless of {@code lastN}).
   *
   * @param newPinnedEndpointIds the list of endpoint IDs to set.
   */
  public void setPinnedEndpointIds(List<String> newPinnedEndpointIds) {
    if (logger.isDebugEnabled()) {
      logger.debug("Setting pinned endpoints: " + newPinnedEndpointIds.toString());
    }
    List<String> endpointsToAskForKeyframe = null;
    synchronized (this) {
      // Since we have the lock anyway, call update() inside, so it
      // doesn't have to obtain it again. But keep the call to
      // askForKeyframes() outside.
      if (!pinnedEndpoints.equals(newPinnedEndpointIds)) {
        pinnedEndpoints = Collections.unmodifiableList(newPinnedEndpointIds);

        endpointsToAskForKeyframe = update();
      }
    }

    askForKeyframes(endpointsToAskForKeyframe);
  }
  /**
   * Notifies this instance that the ordered list of endpoints in the conference has changed.
   *
   * @param endpoints the new ordered list of endpoints in the conference.
   * @return the list of endpoints which were added to the list of forwarded endpoints as a result
   *     of the call, or {@code null} if none were added.
   */
  public List<Endpoint> speechActivityEndpointsChanged(List<Endpoint> endpoints) {
    List<String> newEndpointIdList = getIDs(endpoints);
    List<String> enteringEndpointIds = speechActivityEndpointIdsChanged(newEndpointIdList);

    if (logger.isDebugEnabled()) {
      logger.debug(
          "New list of conference endpoints: "
              + newEndpointIdList.toString()
              + "; entering endpoints: "
              + (enteringEndpointIds == null ? "none" : enteringEndpointIds.toString()));
    }

    List<Endpoint> ret = new LinkedList<>();
    if (enteringEndpointIds != null) {
      for (Endpoint endpoint : endpoints) {
        if (enteringEndpointIds.contains(endpoint.getID())) {
          ret.add(endpoint);
        }
      }
    }

    return ret;
  }
 /**
  * Sends a keyframe request to the endpoints specified in {@code endpointIds}
  *
  * @param endpointIds the list of IDs of endpoints to which to send a request for a keyframe.
  */
 private void askForKeyframes(List<String> endpointIds) {
   // TODO: Execute asynchronously.
   if (endpointIds != null && !endpointIds.isEmpty()) {
     channel.getContent().askForKeyframesById(endpointIds);
   }
 }
  /**
   * Recalculates the list of forwarded endpoints based on the current values of the various
   * parameters of this instance ({@link #lastN}, {@link #conferenceSpeechActivityEndpoints}, {@link
   * #pinnedEndpoints}).
   *
   * @param newConferenceEndpoints A list of endpoints which entered the conference since the last
   *     call to this method. They need not be asked for keyframes, because they were never filtered
   *     by this {@link #LastNController(VideoChannel)}.
   * @return the list of IDs of endpoints which were added to {@link #forwardedEndpoints} (i.e. of
   *     endpoints * "entering last-n") as a result of this call. Returns {@code null} if no
   *     endpoints were added.
   */
  private synchronized List<String> update(List<String> newConferenceEndpoints) {
    List<String> newForwardedEndpoints = new LinkedList<>();
    String ourEndpointId = getEndpointId();

    if (conferenceSpeechActivityEndpoints == INITIAL_EMPTY_LIST) {
      conferenceSpeechActivityEndpoints =
          getIDs(channel.getConferenceSpeechActivity().getEndpoints());
      newConferenceEndpoints = conferenceSpeechActivityEndpoints;
    }

    if (lastN < 0 && currentLastN < 0) {
      // Last-N is disabled, we forward everything.
      newForwardedEndpoints.addAll(conferenceSpeechActivityEndpoints);
      if (ourEndpointId != null) {
        newForwardedEndpoints.remove(ourEndpointId);
      }
    } else {
      // Here we have lastN >= 0 || currentLastN >= 0 which implies
      // currentLastN >= 0.

      // Pinned endpoints are always forwarded.
      newForwardedEndpoints.addAll(getPinnedEndpoints());
      // As long as they are still endpoints in the conference.
      newForwardedEndpoints.retainAll(conferenceSpeechActivityEndpoints);

      if (newForwardedEndpoints.size() > currentLastN) {
        // What do we want in this case? It looks like a contradictory
        // request from the client, but maybe it makes for a good API
        // on the client to allow the pinned to override last-n.
        // Unfortunately, this will not play well with Adaptive-Last-N
        // or changes to Last-N for other reasons.
      } else if (newForwardedEndpoints.size() < currentLastN) {
        for (String endpointId : conferenceSpeechActivityEndpoints) {
          if (newForwardedEndpoints.size() < currentLastN) {
            if (!endpointId.equals(ourEndpointId) && !newForwardedEndpoints.contains(endpointId)) {
              newForwardedEndpoints.add(endpointId);
            }
          } else {
            break;
          }
        }
      }
    }

    List<String> enteringEndpoints;
    if (forwardedEndpoints.equals(newForwardedEndpoints)) {
      // We want forwardedEndpoints != INITIAL_EMPTY_LIST
      forwardedEndpoints = newForwardedEndpoints;

      enteringEndpoints = null;
    } else {
      enteringEndpoints = new ArrayList<>(newForwardedEndpoints);
      enteringEndpoints.removeAll(forwardedEndpoints);

      if (logger.isDebugEnabled()) {
        logger.debug(
            "Forwarded endpoints changed: "
                + forwardedEndpoints.toString()
                + " -> "
                + newForwardedEndpoints.toString()
                + ". Entering: "
                + enteringEndpoints.toString());
      }

      forwardedEndpoints = Collections.unmodifiableList(newForwardedEndpoints);

      if (lastN >= 0 || currentLastN >= 0) {
        // TODO: we may want to do this asynchronously.
        channel.sendLastNEndpointsChangeEventOnDataChannel(forwardedEndpoints, enteringEndpoints);
      }
    }

    // If lastN is disabled, the endpoints entering forwardedEndpoints were
    // never filtered, so they don't need to be asked for keyframes.
    if (lastN < 0 && currentLastN < 0) {
      enteringEndpoints = null;
    }

    if (enteringEndpoints != null && newConferenceEndpoints != null) {
      // Endpoints just entering the conference need not be asked for
      // keyframes.
      enteringEndpoints.removeAll(newConferenceEndpoints);
    }

    return enteringEndpoints;
  }
 /** @return the number of streams currently being forwarded. */
 public int getN() {
   return forwardedEndpoints.size();
 }