コード例 #1
0
  /**
   * 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;
  }
コード例 #2
0
  /**
   * 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);
      }
    }
  }
コード例 #3
0
 /**
  * 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);
     }
   }
 }
コード例 #4
0
ファイル: Http.java プロジェクト: Carboni/jenkins-jobs
  /**
   * Sends a POST request with a file and returns the response.
   *
   * @param endpoint The endpoint to send the request to.
   * @param file The file to upload
   * @param responseClass The class to deserialise the Json response to. Can be null if no response
   *     message is expected.
   * @param <T> The type to deserialise the response to.
   * @return A {@link Response} containing the deserialised body, if any.
   * @throws IOException If an error occurs.
   * @see MultipartEntityBuilder
   */
  private <T> Response<T> post(Endpoint endpoint, File file, Class<T> responseClass)
      throws IOException {
    if (file == null) {
      return post(endpoint, responseClass);
    } // deal with null case

    // Create the request
    HttpPost post = new HttpPost(endpoint.url());
    post.setHeaders(combineHeaders());

    // Add fields as text pairs
    MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
    // Add file as binary
    FileBody fileBody = new FileBody(file);
    multipartEntityBuilder.addPart("file", fileBody);

    // Set the body
    post.setEntity(multipartEntityBuilder.build());

    // Send the request and process the response
    try (CloseableHttpResponse response = httpClient().execute(post)) {
      if (String.class.isAssignableFrom(responseClass)) {
        String body = getResponseString(response);
        return new Response(response.getStatusLine(), body);
      } else {
        T body = deserialiseResponseMessage(response, responseClass);
        return new Response<>(response.getStatusLine(), body);
      }
    }
  }
コード例 #5
0
ファイル: Http.java プロジェクト: Carboni/jenkins-jobs
  /**
   * Sends a POST request with a file and returns the response.
   *
   * @param endpoint The endpoint to send the request to.
   * @param file The file to upload
   * @param responseClass The class to deserialise the Json response to. Can be null if no response
   *     message is expected.
   * @param fields Any name-value pairs to serialise
   * @param <T> The type to deserialise the response to.
   * @return A {@link Response} containing the deserialised body, if any.
   * @throws IOException If an error occurs.
   * @see MultipartEntityBuilder
   */
  public <T> Response<T> post(
      Endpoint endpoint, File file, Class<T> responseClass, NameValuePair... fields)
      throws IOException {
    if (file == null) {
      return post(endpoint, responseClass, fields);
    } // deal with null case

    // Create the request
    HttpPost post = new HttpPost(endpoint.url());
    post.setHeaders(combineHeaders());

    // Add fields as text pairs
    MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
    for (NameValuePair field : fields) {
      multipartEntityBuilder.addTextBody(field.getName(), field.getValue());
    }
    // Add file as binary
    FileBody bin = new FileBody(file);
    multipartEntityBuilder.addPart("file", bin);

    // Set the body
    post.setEntity(multipartEntityBuilder.build());

    // Send the request and process the response
    try (CloseableHttpResponse response = httpClient().execute(post)) {
      T body = deserialiseResponseMessage(response, responseClass);
      return new Response<>(response.getStatusLine(), body);
    }
  }
コード例 #6
0
ファイル: Http.java プロジェクト: Carboni/jenkins-jobs
  /**
   * Sends a GET request and returns the response.
   *
   * @param endpoint The endpoint to send the request to.
   * @param headers Any additional headers to send with this request. You can use {@link
   *     org.apache.http.HttpHeaders} constants for header names.
   * @return A {@link Path} to the downloaded content, if any.
   * @throws IOException If an error occurs.
   * @see java.nio.file.Files#probeContentType(Path)
   */
  public Response<Path> get(Endpoint endpoint, NameValuePair... headers) throws IOException {

    // Create the request
    HttpGet get = new HttpGet(endpoint.url());
    get.setHeaders(combineHeaders(headers));
    Path tempFile = null;

    // Send the request and process the response
    try (CloseableHttpResponse response = httpClient().execute(get)) {

      if (response.getStatusLine().getStatusCode() != HttpStatus.OK_200) {
        return null;
      } // If bad response return null

      // Request the content
      HttpEntity entity = response.getEntity();

      // Download the content to a temporary file
      if (entity != null) {
        tempFile = Files.createTempFile("download", "file");
        try (InputStream input = entity.getContent();
            OutputStream output = Files.newOutputStream(tempFile)) {
          IOUtils.copy(input, output);
        }
      }

      return new Response<>(response.getStatusLine(), tempFile);
    }
  }
コード例 #7
0
ファイル: DTDTest.java プロジェクト: julienfromentc/jaxws
 public void testEntity() throws Exception {
   int port = PortAllocator.getFreePort();
   String address = "http://localhost:" + port + "/entity";
   Endpoint endpoint = Endpoint.create(new MyEndpoint());
   endpoint.publish(address);
   try {
     HTTPResponseInfo rInfo = sendEntity(address);
     String resp = rInfo.getResponseBody();
     if (resp.contains("x1y1")) {
       fail("Entity is getting resolved");
     }
     int code = rInfo.getResponseCode();
     assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, code);
   } finally {
     endpoint.stop();
   }
 }
コード例 #8
0
  public boolean equals(Object obj) {
    if (obj != null && obj instanceof LiveRef) {
      LiveRef ref = (LiveRef) obj;

      return (ep.equals(ref.ep) && id.equals(ref.id) && isLocal == ref.isLocal);
    } else {
      return false;
    }
  }
コード例 #9
0
  /**
   * 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);
    }
  }
コード例 #10
0
  /**
   * Initializes a new <tt>SctpConnection</tt> instance.
   *
   * @param id the string identifier of this connection instance
   * @param content the <tt>Content</tt> which is initializing the new instance
   * @param endpoint the <tt>Endpoint</tt> of newly created instance
   * @param remoteSctpPort the SCTP port used by remote peer
   * @param channelBundleId the ID of the channel-bundle this <tt>SctpConnection</tt> is to be a
   *     part of (or <tt>null</tt> if no it is not to be a part of a channel-bundle).
   * @throws Exception if an error occurs while initializing the new instance
   */
  public SctpConnection(
      String id, Content content, Endpoint endpoint, int remoteSctpPort, String channelBundleId)
      throws Exception {
    super(content, id, channelBundleId);

    setEndpoint(endpoint.getID());

    this.remoteSctpPort = remoteSctpPort;
    this.debugId = generateDebugId();
  }
コード例 #11
0
  /**
   * 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);
      }
    }
  }
コード例 #12
0
ファイル: Http.java プロジェクト: Carboni/jenkins-jobs
  /**
   * Sends a POST request and returns the response.
   *
   * @param endpoint The endpoint to send the request to.
   * @param responseClass The class to deserialise the Json response to. Can be null if no response
   *     message is expected.
   * @param headers Any additional headers to send with this request. You can use {@link
   *     org.apache.http.HttpHeaders} constants for header names.
   * @param <T> The type to deserialise the response to.
   * @return A {@link Response} containing the deserialised body, if any.
   * @throws IOException If an error occurs.
   */
  public <T> Response<T> delete(Endpoint endpoint, Class<T> responseClass, NameValuePair... headers)
      throws IOException {

    // Create the request
    HttpDelete delete = new HttpDelete(endpoint.url());
    delete.setHeaders(combineHeaders(headers));

    // Send the request and process the response
    try (CloseableHttpResponse response = httpClient().execute(delete)) {
      T body = deserialiseResponseMessage(response, responseClass);
      return new Response<>(response.getStatusLine(), body);
    }
  }
コード例 #13
0
ファイル: Http.java プロジェクト: Carboni/jenkins-jobs
  /**
   * Sends a GET request and returns the response.
   *
   * @param endpoint The endpoint to send the request to.
   * @param responseClass The class to deserialise the Json response to. Can be null if no response
   *     message is expected.
   * @param headers Any additional headers to send with this request. You can use {@link
   *     org.apache.http.HttpHeaders} constants for header names.
   * @param <T> The type to deserialise the response to.
   * @return A {@link Response} containing the deserialised body, if any.
   * @throws IOException If an error occurs.
   */
  public <T> Response<T> get(Endpoint endpoint, Class<T> responseClass, NameValuePair... headers)
      throws IOException {

    // Create the request
    HttpGet get = new HttpGet(endpoint.url());
    get.setHeaders(combineHeaders(headers));

    // Send the request and process the response
    try (CloseableHttpResponse response = httpClient().execute(get)) {
      // System.out.println(response);
      T body = deserialiseResponseMessage(response, responseClass);
      return new Response<>(response.getStatusLine(), body);
    }
  }
コード例 #14
0
  /**
   * 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));
          }
        }
      }
    }
  }
コード例 #15
0
ファイル: Http.java プロジェクト: Carboni/jenkins-jobs
  /**
   * Sends a POST request and returns the response.
   *
   * <p>Specifically for the use case where we have no requestMessage
   *
   * @param endpoint The endpoint to send the request to.
   * @param responseClass The class to deserialise the Json response to. Can be null if no response
   *     message is expected.
   * @param headers Any additional headers to send with this request. You can use {@link
   *     org.apache.http.HttpHeaders} constants for header names.
   * @param <T> The type to deserialise the response to.
   * @return A {@link Response} containing the deserialised body, if any.
   * @throws IOException If an error occurs.
   */
  public <T> Response<T> post(Endpoint endpoint, Class<T> responseClass, NameValuePair... headers)
      throws IOException {

    // Create the request
    HttpPost post = new HttpPost(endpoint.url());
    post.setHeaders(combineHeaders(headers));

    // Add the request message if there is one
    post.setEntity(serialiseRequestMessage(null));

    // Send the request and process the response
    try (CloseableHttpResponse response = httpClient().execute(post)) {
      T body = deserialiseResponseMessage(response, responseClass);
      return new Response<>(response.getStatusLine(), body);
    }
  }
コード例 #16
0
  /**
   * 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;
  }
コード例 #17
0
 /** {@inheritDoc} */
 @Override
 protected void onEndpointChanged(Endpoint oldValue, Endpoint newValue) {
   if (oldValue != null) oldValue.setSctpConnection(null);
   if (newValue != null) newValue.setSctpConnection(this);
 }
コード例 #18
0
 /** Export the object to accept incoming calls. */
 public void exportObject(Target target) throws RemoteException {
   ep.exportObject(target);
 }
コード例 #19
0
 /**
  * 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())
       + "\"}";
 }
コード例 #20
0
 public Channel getChannel() throws RemoteException {
   if (ch == null) {
     ch = ep.getChannel();
   }
   return ch;
 }
コード例 #21
0
  /**
   * 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);
        }
      }
    }
  }
コード例 #22
0
  /**
   * 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);
      }
    }
  }