/**
   * Sets the values of the properties of a specific <tt>ColibriConferenceIQ</tt> to the values of
   * the respective properties of this instance. Thus, the specified <tt>iq</tt> may be thought of
   * as a description of this instance.
   *
   * <p><b>Note</b>: The copying of the values is deep i.e. the <tt>Contents</tt>s of this instance
   * are described in the specified <tt>iq</tt>.
   *
   * @param iq the <tt>ColibriConferenceIQ</tt> to set the values of the properties of this instance
   *     on
   */
  public void describeDeep(ColibriConferenceIQ iq) {
    describeShallow(iq);

    if (isRecording()) {
      ColibriConferenceIQ.Recording recordingIQ =
          new ColibriConferenceIQ.Recording(State.ON.toString());
      recordingIQ.setDirectory(getRecordingDirectory());
      iq.setRecording(recordingIQ);
    }
    for (Content content : getContents()) {
      ColibriConferenceIQ.Content contentIQ = iq.getOrCreateContent(content.getName());

      for (Channel channel : content.getChannels()) {
        if (channel instanceof SctpConnection) {
          ColibriConferenceIQ.SctpConnection sctpConnectionIQ =
              new ColibriConferenceIQ.SctpConnection();

          channel.describe(sctpConnectionIQ);
          contentIQ.addSctpConnection(sctpConnectionIQ);
        } else {
          ColibriConferenceIQ.Channel channelIQ = new ColibriConferenceIQ.Channel();

          channel.describe(channelIQ);
          contentIQ.addChannel(channelIQ);
        }
      }
    }
  }
  /**
   * Notifies this <tt>Conference</tt> that the ordered list of <tt>Endpoint</tt>s of {@link
   * #speechActivity} i.e. the dominant speaker history has changed.
   *
   * <p>This instance notifies the video <tt>Channel</tt>s about the change so that they may update
   * their last-n lists and report to this instance which <tt>Endpoint</tt>s are to be asked for
   * video keyframes.
   */
  private void speechActivityEndpointsChanged() {
    List<Endpoint> endpoints = null;

    for (Content content : getContents()) {
      if (MediaType.VIDEO.equals(content.getMediaType())) {
        Set<Endpoint> endpointsToAskForKeyframes = null;

        endpoints = speechActivity.getEndpoints();
        for (Channel channel : content.getChannels()) {
          if (!(channel instanceof RtpChannel)) continue;

          RtpChannel rtpChannel = (RtpChannel) channel;
          List<Endpoint> channelEndpointsToAskForKeyframes =
              rtpChannel.speechActivityEndpointsChanged(endpoints);

          if ((channelEndpointsToAskForKeyframes != null)
              && !channelEndpointsToAskForKeyframes.isEmpty()) {
            if (endpointsToAskForKeyframes == null) {
              endpointsToAskForKeyframes = new HashSet<>();
            }
            endpointsToAskForKeyframes.addAll(channelEndpointsToAskForKeyframes);
          }
        }

        if ((endpointsToAskForKeyframes != null) && !endpointsToAskForKeyframes.isEmpty()) {
          content.askForKeyframes(endpointsToAskForKeyframes);
        }
      }
    }
  }
  /**
   * Finds a <tt>Channel</tt> of this <tt>Conference</tt> which receives a specific SSRC and is with
   * a specific <tt>MediaType</tt>.
   *
   * @param receiveSSRC the SSRC of a received RTP stream whose receiving <tt>Channel</tt> in this
   *     <tt>Conference</tt> is to be found
   * @param mediaType the <tt>MediaType</tt> of the <tt>Channel</tt> to be found
   * @return the <tt>Channel</tt> in this <tt>Conference</tt> which receives the specified
   *     <tt>ssrc</tt> and is with the specified <tt>mediaType</tt>; otherwise, <tt>null</tt>
   */
  public Channel findChannelByReceiveSSRC(long receiveSSRC, MediaType mediaType) {
    for (Content content : getContents()) {
      if (mediaType.equals(content.getMediaType())) {
        Channel channel = content.findChannelByReceiveSSRC(receiveSSRC);

        if (channel != null) return channel;
      }
    }
    return null;
  }
  /**
   * Expires this <tt>Conference</tt>, its <tt>Content</tt>s and their respective <tt>Channel</tt>s.
   * Releases the resources acquired by this instance throughout its life time and prepares it to be
   * garbage collected.
   */
  public void expire() {
    synchronized (this) {
      if (expired) return;
      else expired = true;
    }

    EventAdmin eventAdmin = videobridge.getEventAdmin();
    if (eventAdmin != null) eventAdmin.sendEvent(EventFactory.conferenceExpired(this));

    setRecording(false);
    if (recorderEventHandler != null) {
      recorderEventHandler.close();
      recorderEventHandler = null;
    }

    Videobridge videobridge = getVideobridge();

    try {
      videobridge.expireConference(this);
    } finally {
      // Expire the Contents of this Conference.
      for (Content content : getContents()) {
        try {
          content.expire();
        } catch (Throwable t) {
          logger.warn(
              "Failed to expire content " + content.getName() + " of conference " + getID() + "!",
              t);
          if (t instanceof InterruptedException) Thread.currentThread().interrupt();
          else if (t instanceof ThreadDeath) throw (ThreadDeath) t;
        }
      }

      // Close the transportManagers of this Conference. Normally, there
      // will be no TransportManager left to close at this point because
      // all Channels have expired and the last Channel to be removed from
      // a TransportManager closes the TransportManager. However, a
      // Channel may have expired before it has learned of its
      // TransportManager and then the TransportManager will not close.
      closeTransportManagers();

      if (logger.isInfoEnabled()) {
        logger.info(
            "Expired conference " + getID() + ". " + videobridge.getConferenceCountString());
      }
    }
  }
  /**
   * Checks whether media recording is currently enabled for this <tt>Conference</tt>.
   *
   * @return <tt>true</tt> if media recording is currently enabled for this <tt>Conference</tt>,
   *     false otherwise.
   */
  public boolean isRecording() {
    boolean recording = this.recording;

    // if one of the contents is not recording, stop all recording
    if (recording) {
      synchronized (contents) {
        for (Content content : contents) {
          MediaType mediaType = content.getMediaType();

          if (!MediaType.VIDEO.equals(mediaType) && !MediaType.AUDIO.equals(mediaType)) continue;
          if (!content.isRecording()) recording = false;
        }
      }
    }
    if (this.recording != recording) setRecording(recording);

    return this.recording;
  }
  /**
   * Expires a specific <tt>Content</tt> of this <tt>Conference</tt> (i.e. if the specified
   * <tt>content</tt> is not in the list of <tt>Content</tt>s of this <tt>Conference</tt>, does
   * nothing).
   *
   * @param content the <tt>Content</tt> to be expired by this <tt>Conference</tt>
   */
  public void expireContent(Content content) {
    boolean expireContent;

    synchronized (contents) {
      if (contents.contains(content)) {
        contents.remove(content);
        expireContent = true;
      } else expireContent = false;
    }
    if (expireContent) content.expire();
  }
  /**
   * Gets a <tt>Content</tt> of this <tt>Conference</tt> which has a specific name. If a
   * <tt>Content</tt> of this <tt>Conference</tt> with the specified <tt>name</tt> does not exist at
   * the time the method is invoked, the method initializes a new <tt>Content</tt> instance with the
   * specified <tt>name</tt> and adds it to the list of <tt>Content</tt>s of this
   * <tt>Conference</tt>.
   *
   * @param name the name of the <tt>Content</tt> which is to be returned
   * @return a <tt>Content</tt> of this <tt>Conference</tt> which has the specified <tt>name</tt>
   */
  public Content getOrCreateContent(String name) {
    Content content;

    synchronized (contents) {
      for (Content aContent : contents) {
        if (aContent.getName().equals(name)) {
          aContent.touch(); // It seems the content is still active.
          return aContent;
        }
      }

      content = new Content(this, name);
      if (isRecording()) {
        content.setRecording(true, getRecordingPath());
      }
      contents.add(content);
    }

    if (logger.isInfoEnabled()) {
      /*
       * The method Videobridge.getChannelCount() should better be
       * executed outside synchronized blocks in order to reduce the risks
       * of causing deadlocks.
       */
      Videobridge videobridge = getVideobridge();

      logger.info(
          "Created content "
              + name
              + " of conference "
              + getID()
              + ". "
              + videobridge.getConferenceCountString());
    }

    return content;
  }
  /**
   * Attempts to enable or disable media recording for this <tt>Conference</tt>.
   *
   * @param recording whether to enable or disable recording.
   * @return the state of the media recording for this <tt>Conference</tt> after the attempt to
   *     enable (or disable).
   */
  public boolean setRecording(boolean recording) {
    if (recording != this.recording) {
      if (recording) {
        // try enable recording
        if (logger.isDebugEnabled()) {
          logger.debug("Starting recording for conference with id=" + getID());
        }

        String path = getRecordingPath();
        boolean failedToStart = !checkRecordingDirectory(path);

        if (!failedToStart) {
          RecorderEventHandler handler = getRecorderEventHandler();

          if (handler == null) failedToStart = true;
        }
        if (!failedToStart) {
          EndpointRecorder endpointRecorder = getEndpointRecorder();

          if (endpointRecorder == null) {
            failedToStart = true;
          } else {
            for (Endpoint endpoint : getEndpoints()) endpointRecorder.updateEndpoint(endpoint);
          }
        }

        /*
         * The Recorders of the Contents need to share a single
         * Synchronizer, we take it from the first Recorder.
         */
        boolean first = true;
        Synchronizer synchronizer = null;

        for (Content content : contents) {
          MediaType mediaType = content.getMediaType();

          if (!MediaType.VIDEO.equals(mediaType) && !MediaType.AUDIO.equals(mediaType)) {
            continue;
          }

          if (!failedToStart) failedToStart = !content.setRecording(true, path);
          if (failedToStart) break;

          if (first) {
            first = false;
            synchronizer = content.getRecorder().getSynchronizer();
          } else {
            Recorder recorder = content.getRecorder();

            if (recorder != null) recorder.setSynchronizer(synchronizer);
          }

          content.feedKnownSsrcsToSynchronizer();
        }

        if (failedToStart) {
          recording = false;
          logger.warn("Failed to start media recording for conference " + getID());
        }
      }

      // either we were asked to disable recording, or we failed to
      // enable it
      if (!recording) {
        if (logger.isDebugEnabled()) {
          logger.debug("Stopping recording for conference with id=" + getID());
        }

        for (Content content : contents) {
          MediaType mediaType = content.getMediaType();

          if (MediaType.AUDIO.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
            content.setRecording(false, null);
          }
        }

        if (recorderEventHandler != null) recorderEventHandler.close();
        recorderEventHandler = null;
        recordingPath = null;
        recordingDirectory = null;

        if (endpointRecorder != null) endpointRecorder.close();
        endpointRecorder = null;
      }

      this.recording = recording;
    }

    return this.recording;
  }