/**
   * Gets an <tt>Endpoint</tt> participating in this <tt>Conference</tt> which has a specific
   * identifier/ID. If an <tt>Endpoint</tt> participating in this <tt>Conference</tt> with the
   * specified <tt>id</tt> does not exist at the time the method is invoked, the method optionally
   * initializes a new <tt>Endpoint</tt> instance with the specified <tt>id</tt> and adds it to the
   * list of <tt>Endpoint</tt>s participating in this <tt>Conference</tt>.
   *
   * @param id the identifier/ID of the <tt>Endpoint</tt> which is to be returned
   * @return an <tt>Endpoint</tt> participating in this <tt>Conference</tt> which has the specified
   *     <tt>id</tt> or <tt>null</tt> if there is no such <tt>Endpoint</tt> and <tt>create</tt>
   *     equals <tt>false</tt>
   */
  private Endpoint getEndpoint(String id, boolean create) {
    Endpoint endpoint = null;
    boolean changed = false;

    synchronized (endpoints) {
      for (Iterator<WeakReference<Endpoint>> i = endpoints.iterator(); i.hasNext(); ) {
        Endpoint e = i.next().get();

        if (e == null) {
          i.remove();
          changed = true;
        } else if (e.getID().equals(id)) {
          endpoint = e;
        }
      }

      if (create && endpoint == null) {
        endpoint = new Endpoint(id, this);
        // The propertyChangeListener will weakly reference this
        // Conference and will unregister itself from the endpoint
        // sooner or later.
        endpoint.addPropertyChangeListener(propertyChangeListener);
        endpoints.add(new WeakReference<>(endpoint));
        changed = true;

        EventAdmin eventAdmin = videobridge.getEventAdmin();
        if (eventAdmin != null) eventAdmin.sendEvent(EventFactory.endpointCreated(endpoint));
      }
    }

    if (changed) firePropertyChange(ENDPOINTS_PROPERTY_NAME, null, null);

    return endpoint;
  }
  /**
   * Notifies this instance that {@link #speechActivity} has identified a speaker switch event in
   * this multipoint conference and there is now a new dominant speaker.
   */
  private void dominantSpeakerChanged() {
    Endpoint dominantSpeaker = speechActivity.getDominantEndpoint();

    if (logger.isTraceEnabled()) {
      logger.trace(
          "The dominant speaker in conference "
              + getID()
              + " is now the endpoint "
              + ((dominantSpeaker == null) ? "(null)" : dominantSpeaker.getID())
              + ".");
    }

    if (dominantSpeaker != null) {
      broadcastMessageOnDataChannels(createDominantSpeakerEndpointChangeEvent(dominantSpeaker));

      if (isRecording() && (recorderEventHandler != null))
        recorderEventHandler.dominantSpeakerChanged(dominantSpeaker);
    }
  }
  /**
   * Notifies this instance that a specific <tt>SctpConnection</tt> has become ready i.e. connected
   * to a/the remote peer and operational.
   *
   * @param sctpConnection the <tt>SctpConnection</tt> which has become ready and is the cause of
   *     the method invocation
   */
  private void sctpConnectionReady(SctpConnection sctpConnection) {
    /*
     * We want to fire initial events over the SctpConnection as soon as it
     * is ready, we do not want to fire them multiple times i.e. every time
     * the SctpConnection becomes ready.
     */
    sctpConnection.removeChannelListener(webRtcDataStreamListener);

    if (!isExpired() && !sctpConnection.isExpired() && sctpConnection.isReady()) {
      Endpoint endpoint = sctpConnection.getEndpoint();

      if (endpoint != null) endpoint = getEndpoint(endpoint.getID());
      if (endpoint != null) {
        /*
         * It appears that this Conference, the SctpConnection and the
         * Endpoint are in states which allow them to fire the initial
         * events.
         */
        Endpoint dominantSpeaker = speechActivity.getDominantEndpoint();

        if (dominantSpeaker != null) {
          try {
            endpoint.sendMessageOnDataChannel(
                createDominantSpeakerEndpointChangeEvent(dominantSpeaker));
          } catch (IOException e) {
            logger.error("Failed to send message on data channel.", e);
          }
        }

        /*
         * Determining the instant at which an SctpConnection associated
         * with an Endpoint becomes ready (i.e. connected to the remote
         * peer and operational) is a multi-step ordeal. The Conference
         * class implements the procedure so do not make other classes
         * implement it as well.
         */
        endpoint.sctpConnectionReady(sctpConnection);
      }
    }
  }
  /**
   * Maybe send a data channel command to he associated simulcast sender to make it stop streaming
   * its hq stream, if it's not being watched by any participant.
   */
  public void maybeSendStopHighQualityStreamCommand() {
    if (nativeSimulcast || !hasLayers()) {
      // In native simulcast the client adjusts its layers autonomously so
      // we don't need (nor we can) to control it with data channel
      // messages.
      return;
    }

    Endpoint oldEndpoint = getSimulcastEngine().getVideoChannel().getEndpoint();

    SimulcastLayer[] oldSimulcastLayers = getSimulcastLayers();

    SctpConnection sctpConnection;
    if (oldSimulcastLayers != null
        && oldSimulcastLayers.length > 1
        /* oldEndpoint != null is implied*/
        && (sctpConnection = oldEndpoint.getSctpConnection()) != null
        && sctpConnection.isReady()
        && !sctpConnection.isExpired()) {
      // we have an old endpoint and it has an SCTP connection that is
      // ready and not expired. if nobody else is watching the old
      // endpoint, stop its hq stream.

      boolean stopHighQualityStream = true;
      for (Endpoint e :
          getSimulcastEngine().getVideoChannel().getContent().getConference().getEndpoints()) {
        // TODO(gp) need some synchronization here. What if the selected
        // endpoint changes while we're in the loop?

        if (oldEndpoint != e && (oldEndpoint == e.getEffectivelySelectedEndpoint())
            || e.getEffectivelySelectedEndpoint() == null) {
          // somebody is watching the old endpoint or somebody has not
          // yet signaled its selected endpoint to the bridge, don't
          // stop the hq stream.
          stopHighQualityStream = false;
          break;
        }
      }

      if (stopHighQualityStream) {
        // TODO(gp) this assumes only a single hq stream.

        logDebug(
            getSimulcastEngine().getVideoChannel().getEndpoint().getID()
                + " notifies "
                + oldEndpoint.getID()
                + " to stop "
                + "its HQ stream.");

        SimulcastLayer hqLayer = oldSimulcastLayers[oldSimulcastLayers.length - 1];

        StopSimulcastLayerCommand command = new StopSimulcastLayerCommand(hqLayer);

        String json = mapper.toJson(command);

        try {
          oldEndpoint.sendMessageOnDataChannel(json);
        } catch (IOException e1) {
          logError(oldEndpoint.getID() + " failed to send " + "message on data channel.", e1);
        }
      }
    }
  }
  /**
   * Maybe send a data channel command to the associated <tt>Endpoint</tt> to make it start
   * streaming its hq stream, if it's being watched by some receiver.
   */
  public void maybeSendStartHighQualityStreamCommand() {
    if (nativeSimulcast || !hasLayers()) {
      // In native simulcast the client adjusts its layers autonomously so
      // we don't need (nor we can) to control it with data channel
      // messages.
      return;
    }

    Endpoint newEndpoint = getSimulcastEngine().getVideoChannel().getEndpoint();
    SimulcastLayer[] newSimulcastLayers = getSimulcastLayers();

    SctpConnection sctpConnection;
    if (newSimulcastLayers == null
        || newSimulcastLayers.length <= 1
        /* newEndpoint != null is implied */
        || (sctpConnection = newEndpoint.getSctpConnection()) == null
        || !sctpConnection.isReady()
        || sctpConnection.isExpired()) {
      return;
    }

    // we have a new endpoint and it has an SCTP connection that is
    // ready and not expired. if somebody else is watching the new
    // endpoint, start its hq stream.

    boolean startHighQualityStream = false;

    for (Endpoint e :
        getSimulcastEngine().getVideoChannel().getContent().getConference().getEndpoints()) {
      // TODO(gp) need some synchronization here. What if the
      // selected endpoint changes while we're in the loop?

      if (e == newEndpoint) continue;

      Endpoint eSelectedEndpoint = e.getEffectivelySelectedEndpoint();

      if (newEndpoint == eSelectedEndpoint) {
        // somebody is watching the new endpoint or somebody has not
        // yet signaled its selected endpoint to the bridge, start
        // the hq stream.

        if (logger.isDebugEnabled()) {
          Map<String, Object> map = new HashMap<String, Object>(3);

          map.put("e", e);
          map.put("newEndpoint", newEndpoint);
          map.put("maybe", eSelectedEndpoint == null ? "(maybe) " : "");

          StringCompiler sc =
              new StringCompiler(map).c("{e.id} is {maybe} watching {newEndpoint.id}.");

          logDebug(sc.toString().replaceAll("\\s+", " "));
        }

        startHighQualityStream = true;
        break;
      }
    }

    if (startHighQualityStream) {
      // TODO(gp) this assumes only a single hq stream.

      logDebug(
          getSimulcastEngine().getVideoChannel().getEndpoint().getID()
              + " notifies "
              + newEndpoint.getID()
              + " to start its HQ stream.");

      SimulcastLayer hqLayer = newSimulcastLayers[newSimulcastLayers.length - 1];
      ;
      StartSimulcastLayerCommand command = new StartSimulcastLayerCommand(hqLayer);
      String json = mapper.toJson(command);

      try {
        newEndpoint.sendMessageOnDataChannel(json);
      } catch (IOException e) {
        logError(newEndpoint.getID() + " failed to send message on data channel.", e);
      }
    }
  }
 /**
  * Initializes a new <tt>String</tt> to be sent over an <tt>SctpConnection</tt> in order to notify
  * an <tt>Endpoint</tt> that the dominant speaker in this multipoint conference has changed to a
  * specific <tt>Endpoint</tt>.
  *
  * @param dominantSpeaker the dominant speaker in this multipoint conference
  * @return a new <tt>String</tt> to be sent over an <tt>SctpConnection</tt> in order to notify an
  *     <tt>Endpoint</tt> that the dominant speaker in this multipoint conference has changed to
  *     <tt>dominantSpeaker</tt>
  */
 private String createDominantSpeakerEndpointChangeEvent(Endpoint dominantSpeaker) {
   return "{\"colibriClass\":\"DominantSpeakerEndpointChangeEvent\","
       + "\"dominantSpeakerEndpoint\":\""
       + JSONValue.escape(dominantSpeaker.getID())
       + "\"}";
 }