/** * Extracts a list of endpoint IDs from a list of {@link Endpoint}s. * * @param endpoints the list of {@link Endpoint}s. * @return the list of IDs of endpoints in {@code endpoints}. */ private List<String> getIDs(List<Endpoint> endpoints) { if (endpoints != null && !endpoints.isEmpty()) { List<String> endpointIds = new LinkedList<>(); for (Endpoint endpoint : endpoints) { endpointIds.add(endpoint.getID()); } return endpointIds; } return null; }
/** * Notifies this instance that the ordered list of endpoints (specified as a list of endpoint IDs) * in the conference has changed. * * @param endpointIds the new ordered list of endpoints (specified as a list of endpoint IDs) in * the conference. * @return the list of IDs of endpoints which were added to the list of forwarded endpoints as a * result of the call. */ private synchronized List<String> speechActivityEndpointIdsChanged(List<String> endpointIds) { if (conferenceSpeechActivityEndpoints.equals(endpointIds)) { if (logger.isDebugEnabled()) { logger.debug("Conference endpoints have not changed."); } return null; } else { List<String> newEndpoints = new LinkedList<>(endpointIds); newEndpoints.removeAll(conferenceSpeechActivityEndpoints); conferenceSpeechActivityEndpoints = endpointIds; return update(newEndpoints); } }
/** * Checks whether RTP packets from {@code sourceChannel} should be forwarded to {@link #channel}. * * @param sourceChannel the channel. * @return {@code true} iff RTP packets from {@code sourceChannel} should be forwarded to {@link * #channel}. */ public boolean isForwarded(Channel sourceChannel) { if (lastN < 0 && currentLastN < 0) { // If Last-N is disabled, we forward everything. return true; } if (sourceChannel == null) { logger.warn("Invalid sourceChannel: null."); return false; } Endpoint channelEndpoint = sourceChannel.getEndpoint(); if (channelEndpoint == null) { logger.warn("sourceChannel has no endpoint."); return false; } if (forwardedEndpoints == INITIAL_EMPTY_LIST) { // LastN is enabled, but we haven't yet initialized the list of // endpoints in the conference. initializeConferenceEndpoints(); } // This may look like a place to optimize, because we query an unordered // list (in O(n)) and it executes on each video packet if lastN is // enabled. However, the size of forwardedEndpoints is restricted to // lastN and so small enough that it is not worth optimizing. return forwardedEndpoints.contains(channelEndpoint.getID()); }
/** * 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()); } }
/** * Sets the list of "pinned" endpoints (i.e. endpoints for which video should always be forwarded, * regardless of {@code lastN}). * * @param newPinnedEndpointIds the list of endpoint IDs to set. */ public void setPinnedEndpointIds(List<String> newPinnedEndpointIds) { if (logger.isDebugEnabled()) { logger.debug("Setting pinned endpoints: " + newPinnedEndpointIds.toString()); } List<String> endpointsToAskForKeyframe = null; synchronized (this) { // Since we have the lock anyway, call update() inside, so it // doesn't have to obtain it again. But keep the call to // askForKeyframes() outside. if (!pinnedEndpoints.equals(newPinnedEndpointIds)) { pinnedEndpoints = Collections.unmodifiableList(newPinnedEndpointIds); endpointsToAskForKeyframe = update(); } } askForKeyframes(endpointsToAskForKeyframe); }
/** * Notifies this instance that the ordered list of endpoints in the conference has changed. * * @param endpoints the new ordered list of endpoints in the conference. * @return the list of endpoints which were added to the list of forwarded endpoints as a result * of the call, or {@code null} if none were added. */ public List<Endpoint> speechActivityEndpointsChanged(List<Endpoint> endpoints) { List<String> newEndpointIdList = getIDs(endpoints); List<String> enteringEndpointIds = speechActivityEndpointIdsChanged(newEndpointIdList); if (logger.isDebugEnabled()) { logger.debug( "New list of conference endpoints: " + newEndpointIdList.toString() + "; entering endpoints: " + (enteringEndpointIds == null ? "none" : enteringEndpointIds.toString())); } List<Endpoint> ret = new LinkedList<>(); if (enteringEndpointIds != null) { for (Endpoint endpoint : endpoints) { if (enteringEndpointIds.contains(endpoint.getID())) { ret.add(endpoint); } } } return ret; }
/** * 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; }
/** @return the number of streams currently being forwarded. */ public int getN() { return forwardedEndpoints.size(); }