/**
   * 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 there was a change in the value of a property of an
   * <tt>Endpoint</tt> participating in this multipoint conference.
   *
   * @param endpoint the <tt>Endpoint</tt> which is the source of the event/notification and is
   *     participating in this multipoint conference
   * @param ev a <tt>PropertyChangeEvent</tt> which specifies the source of the event/notification,
   *     the name of the property and the old and new values of that property
   */
  private void endpointPropertyChange(Endpoint endpoint, PropertyChangeEvent ev) {
    String propertyName = ev.getPropertyName();
    boolean maybeRemoveEndpoint;

    if (Endpoint.SCTP_CONNECTION_PROPERTY_NAME.equals(propertyName)) {
      // The SctpConnection of/associated with an Endpoint has changed. We
      // may want to fire initial events over that SctpConnection (as soon
      // as it is ready).
      SctpConnection oldValue = (SctpConnection) ev.getOldValue();
      SctpConnection newValue = (SctpConnection) ev.getNewValue();

      endpointSctpConnectionChanged(endpoint, oldValue, newValue);

      // The SctpConnection may have expired.
      maybeRemoveEndpoint = (newValue == null);
    } else if (Endpoint.CHANNELS_PROPERTY_NAME.equals(propertyName)) {
      // An RtpChannel may have expired.
      maybeRemoveEndpoint = true;
    } else {
      maybeRemoveEndpoint = false;
    }
    if (maybeRemoveEndpoint) {
      // It looks like there is a chance that the Endpoint may have
      // expired. Endpoints are held by this Conference via WeakReferences
      // but WeakReferences are unpredictable. We have functionality
      // though which could benefit from discovering that an Endpoint has
      // expired as quickly as possible (e.g. ConferenceSpeechActivity).
      // Consequently, try to expedite the removal of expired Endpoints.
      if (endpoint.getSctpConnection() == null && endpoint.getChannelCount(null) == 0) {
        removeEndpoint(endpoint);
      }
    }
  }
 /**
  * Used to send a message to a subset of endpoints in the call, primary use case being a message
  * that has originated from an endpoint (as opposed to a message originating from the bridge and
  * being sent to all endpoints in the call, for that see broadcastMessageOnDataChannels below
  *
  * @param msg
  * @param endpoints
  */
 public void sendMessageOnDataChannels(String msg, List<Endpoint> endpoints) {
   for (Endpoint endpoint : endpoints) {
     try {
       endpoint.sendMessageOnDataChannel(msg);
     } catch (IOException e) {
       logger.error("Failed to send message on data channel.", e);
     }
   }
 }
  /**
   * 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);
      }
    }
  }
  /**
   * Updates an <tt>Endpoint</tt> of this <tt>Conference</tt> with the information contained in
   * <tt>colibriEndpoint</tt>. The ID of <tt>colibriEndpoint</tt> is used to select the
   * <tt>Endpoint</tt> to update.
   *
   * @param colibriEndpoint a <tt>ColibriConferenceIQ.Endpoint</tt> instance that contains
   *     information to be set on an <tt>Endpoint</tt> instance of this <tt>Conference</tt>.
   */
  void updateEndpoint(ColibriConferenceIQ.Endpoint colibriEndpoint) {
    String id = colibriEndpoint.getId();

    if (id != null) {
      Endpoint endpoint = getEndpoint(id);

      if (endpoint != null) {
        String oldDisplayName = endpoint.getDisplayName();
        String newDisplayName = colibriEndpoint.getDisplayName();

        if ((oldDisplayName == null && newDisplayName != null)
            || (oldDisplayName != null && !oldDisplayName.equals(newDisplayName))) {
          endpoint.setDisplayName(newDisplayName);

          if (isRecording() && endpointRecorder != null) endpointRecorder.updateEndpoint(endpoint);

          EventAdmin eventAdmin = getVideobridge().getEventAdmin();
          if (eventAdmin != null) {
            eventAdmin.sendEvent(EventFactory.endpointDisplayNameChanged(endpoint));
          }
        }
      }
    }
  }
  /**
   * Removes a specific <tt>Endpoint</tt> instance from this list of <tt>Endpoint</tt>s
   * participating in this multipoint conference.
   *
   * @param endpoint the <tt>Endpoint</tt> to remove
   * @return <tt>true</tt> if the list of <tt>Endpoint</tt>s participating in this multipoint
   *     conference changed as a result of the execution of the method; otherwise, <tt>false</tt>
   */
  private boolean removeEndpoint(Endpoint endpoint) {
    boolean removed = false;

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

        if (e == null || e == endpoint) {
          i.remove();
          removed = true;
        }
      }

      if (endpoint != null) {
        endpoint.expire();
      }
    }

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

    return removed;
  }
 /**
  * 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())
       + "\"}";
 }