/**
  * Ctor.
  *
  * @param videoChannel The <tt>VideoChannel</tt> associated to this <tt>SimulcastEngine</tt>.
  */
 public SimulcastEngine(VideoChannel videoChannel) {
   this.videoChannel = videoChannel;
   simulcastReceiver =
       new SimulcastReceiver(
           this,
           ServiceUtils.getService(videoChannel.getBundleContext(), ConfigurationService.class));
   this.logger =
       Logger.getLogger(classLogger, videoChannel.getContent().getConference().getLogger());
 }
  /**
   * 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());
    }
  }
 /** @return the ID of the endpoint of our channel. */
 private String getEndpointId() {
   if (endpointId == null) {
     Endpoint endpoint = channel.getEndpoint();
     if (endpoint != null) {
       endpointId = endpoint.getID();
     }
   }
   return endpointId;
 }
 /**
  * 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;
  }