org.kurento.room.endpoint.PublisherEndpoint Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kurento-room-sdk Show documentation
Show all versions of kurento-room-sdk Show documentation
Kurento Room SDK - Java API for developing Kurento Room apps
/*
* (C) Copyright 2013 Kurento (http://kurento.org/)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kurento.room.endpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import org.kurento.client.Continuation;
import org.kurento.client.ListenerSubscription;
import org.kurento.client.MediaElement;
import org.kurento.client.MediaPipeline;
import org.kurento.client.MediaType;
import org.kurento.client.PassThrough;
import org.kurento.client.WebRtcEndpoint;
import org.kurento.room.api.MutedMediaType;
import org.kurento.room.exception.RoomException;
import org.kurento.room.exception.RoomException.Code;
import org.kurento.room.internal.Participant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Publisher aspect of the {@link MediaEndpoint}.
*
* @author Radu Tom Vlad
*/
public class PublisherEndpoint extends MediaEndpoint {
private final static Logger log = LoggerFactory.getLogger(PublisherEndpoint.class);
private PassThrough passThru = null;
private ListenerSubscription passThruSubscription = null;
private Map elements = new HashMap();
private LinkedList elementIds = new LinkedList();
private boolean connected = false;
private Map elementsErrorSubscriptions = new HashMap();
public PublisherEndpoint(boolean web, Participant owner, String endpointName,
MediaPipeline pipeline) {
super(web, owner, endpointName, pipeline, log);
}
@Override
protected void internalEndpointInitialization(final CountDownLatch endpointLatch) {
super.internalEndpointInitialization(endpointLatch);
passThru = new PassThrough.Builder(getPipeline()).build();
passThruSubscription = registerElemErrListener(passThru);
}
@Override
public synchronized void unregisterErrorListeners() {
super.unregisterErrorListeners();
unregisterElementErrListener(passThru, passThruSubscription);
for (String elemId : elementIds) {
unregisterElementErrListener(elements.get(elemId), elementsErrorSubscriptions.remove(elemId));
}
}
/**
* @return all media elements created for this publisher, except for the main element (
* {@link WebRtcEndpoint})
*/
public synchronized Collection getMediaElements() {
if (passThru != null) {
elements.put(passThru.getId(), passThru);
}
return elements.values();
}
/**
* Initializes this media endpoint for publishing media and processes the SDP offer or answer. If
* the internal endpoint is an {@link WebRtcEndpoint}, it first registers an event listener for
* the ICE candidates and instructs the endpoint to start gathering the candidates. If required,
* it connects to itself (after applying the intermediate media elements and the
* {@link PassThrough}) to allow loopback of the media stream.
*
* @param sdpType
* indicates the type of the sdpString (offer or answer)
* @param sdpString
* offer or answer from the remote peer
* @param doLoopback
* loopback flag
* @param loopbackAlternativeSrc
* alternative loopback source
* @param loopbackConnectionType
* how to connect the loopback source
* @return the SDP response (the answer if processing an offer SDP, otherwise is the updated offer
* generated previously by this endpoint)
*/
public synchronized String publish(SdpType sdpType, String sdpString, boolean doLoopback,
MediaElement loopbackAlternativeSrc, MediaType loopbackConnectionType) {
registerOnIceCandidateEventListener();
if (doLoopback) {
if (loopbackAlternativeSrc == null) {
connect(this.getEndpoint(), loopbackConnectionType);
} else {
connectAltLoopbackSrc(loopbackAlternativeSrc, loopbackConnectionType);
}
} else {
innerConnect();
}
String sdpResponse = null;
switch (sdpType) {
case ANSWER :
sdpResponse = processAnswer(sdpString);
break;
case OFFER :
sdpResponse = processOffer(sdpString);
break;
default :
throw new RoomException(Code.MEDIA_SDP_ERROR_CODE, "Sdp type not supported: " + sdpType);
}
gatherCandidates();
return sdpResponse;
}
public synchronized String preparePublishConnection() {
return generateOffer();
}
public synchronized void connect(MediaElement sink) {
if (!connected) {
innerConnect();
}
internalSinkConnect(passThru, sink);
}
public synchronized void connect(MediaElement sink, MediaType type) {
if (!connected) {
innerConnect();
}
internalSinkConnect(passThru, sink, type);
}
public synchronized void disconnectFrom(MediaElement sink) {
internalSinkDisconnect(passThru, sink);
}
public synchronized void disconnectFrom(MediaElement sink, MediaType type) {
internalSinkDisconnect(passThru, sink, type);
}
/**
* Changes the media passing through a chain of media elements by applying the specified
* element/shaper. The element is plugged into the stream only if the chain has been initialized
* (a.k.a. media streaming has started), otherwise it is left ready for when the connections
* between elements will materialize and the streaming begins.
*
* @param shaper
* {@link MediaElement} that will be linked to the end of the chain (e.g. a filter)
* @return the element's id
* @throws RoomException
* if thrown, the media element was not added
*/
public String apply(MediaElement shaper) throws RoomException {
return apply(shaper, null);
}
/**
* Same as {@link #apply(MediaElement)}, can specify the media type that will be streamed through
* the shaper element.
*
* @param shaper
* {@link MediaElement} that will be linked to the end of the chain (e.g. a filter)
* @param type
* indicates which type of media will be connected to the shaper ({@link MediaType}), if
* null then the connection is mixed
* @return the element's id
* @throws RoomException
* if thrown, the media element was not added
*/
public synchronized String apply(MediaElement shaper, MediaType type) throws RoomException {
String id = shaper.getId();
if (id == null) {
throw new RoomException(Code.MEDIA_WEBRTC_ENDPOINT_ERROR_CODE,
"Unable to connect media element with null id");
}
if (elements.containsKey(id)) {
throw new RoomException(Code.MEDIA_WEBRTC_ENDPOINT_ERROR_CODE,
"This endpoint already has a media element with id " + id);
}
MediaElement first = null;
if (!elementIds.isEmpty()) {
first = elements.get(elementIds.getFirst());
}
if (connected) {
if (first != null) {
internalSinkConnect(first, shaper, type);
} else {
internalSinkConnect(this.getEndpoint(), shaper, type);
}
internalSinkConnect(shaper, passThru, type);
}
elementIds.addFirst(id);
elements.put(id, shaper);
elementsErrorSubscriptions.put(id, registerElemErrListener(shaper));
return id;
}
/**
* Removes the media element object found from the media chain structure. The object is released.
* If the chain is connected, both adjacent remaining elements will be interconnected.
*
* @param shaper
* {@link MediaElement} that will be removed from the chain
* @throws RoomException
* if thrown, the media element was not removed
*/
public synchronized void revert(MediaElement shaper) throws RoomException {
final String elementId = shaper.getId();
if (!elements.containsKey(elementId)) {
throw new RoomException(Code.MEDIA_ENDPOINT_ERROR_CODE, "This endpoint (" + getEndpointName()
+ ") has no media element with id " + elementId);
}
MediaElement element = elements.remove(elementId);
unregisterElementErrListener(element, elementsErrorSubscriptions.remove(elementId));
// careful, the order in the elems list is reverted
if (connected) {
String nextId = getNext(elementId);
String prevId = getPrevious(elementId);
// next connects to prev
MediaElement prev = null;
MediaElement next = null;
if (nextId != null) {
next = elements.get(nextId);
} else {
next = this.getEndpoint();
}
if (prevId != null) {
prev = elements.get(prevId);
} else {
prev = passThru;
}
internalSinkConnect(next, prev);
}
elementIds.remove(elementId);
element.release(new Continuation() {
@Override
public void onSuccess(Void result) throws Exception {
log.trace("EP {}: Released media element {}", getEndpointName(), elementId);
}
@Override
public void onError(Throwable cause) throws Exception {
log.error("EP {}: Failed to release media element {}", getEndpointName(), elementId, cause);
}
});
}
@Override
public synchronized void mute(MutedMediaType muteType) {
MediaElement sink = passThru;
if (!elements.isEmpty()) {
String sinkId = elementIds.peekLast();
if (!elements.containsKey(sinkId)) {
throw new RoomException(Code.MEDIA_ENDPOINT_ERROR_CODE, "This endpoint ("
+ getEndpointName() + ") has no media element with id " + sinkId
+ " (should've been connected to the internal ep)");
}
sink = elements.get(sinkId);
} else {
log.debug("Will mute connection of WebRTC and PassThrough (no other elems)");
}
switch (muteType) {
case ALL :
internalSinkDisconnect(this.getEndpoint(), sink);
break;
case AUDIO :
internalSinkDisconnect(this.getEndpoint(), sink, MediaType.AUDIO);
break;
case VIDEO :
internalSinkDisconnect(this.getEndpoint(), sink, MediaType.VIDEO);
break;
}
resolveCurrentMuteType(muteType);
}
@Override
public synchronized void unmute() {
MediaElement sink = passThru;
if (!elements.isEmpty()) {
String sinkId = elementIds.peekLast();
if (!elements.containsKey(sinkId)) {
throw new RoomException(Code.MEDIA_ENDPOINT_ERROR_CODE, "This endpoint ("
+ getEndpointName() + ") has no media element with id " + sinkId
+ " (should've been connected to the internal ep)");
}
sink = elements.get(sinkId);
} else {
log.debug("Will unmute connection of WebRTC and PassThrough (no other elems)");
}
internalSinkConnect(this.getEndpoint(), sink);
setMuteType(null);
}
private String getNext(String uid) {
int idx = elementIds.indexOf(uid);
if (idx < 0 || idx + 1 == elementIds.size()) {
return null;
}
return elementIds.get(idx + 1);
}
private String getPrevious(String uid) {
int idx = elementIds.indexOf(uid);
if (idx <= 0) {
return null;
}
return elementIds.get(idx - 1);
}
private void connectAltLoopbackSrc(MediaElement loopbackAlternativeSrc,
MediaType loopbackConnectionType) {
if (!connected) {
innerConnect();
}
internalSinkConnect(loopbackAlternativeSrc, this.getEndpoint(), loopbackConnectionType);
}
private void innerConnect() {
if (this.getEndpoint() == null) {
throw new RoomException(Code.MEDIA_ENDPOINT_ERROR_CODE, "Can't connect null endpoint (ep: "
+ getEndpointName() + ")");
}
MediaElement current = this.getEndpoint();
String prevId = elementIds.peekLast();
while (prevId != null) {
MediaElement prev = elements.get(prevId);
if (prev == null) {
throw new RoomException(Code.MEDIA_ENDPOINT_ERROR_CODE, "No media element with id "
+ prevId + " (ep: " + getEndpointName() + ")");
}
internalSinkConnect(current, prev);
current = prev;
prevId = getPrevious(prevId);
}
internalSinkConnect(current, passThru);
connected = true;
}
private void internalSinkConnect(final MediaElement source, final MediaElement sink) {
source.connect(sink, new Continuation() {
@Override
public void onSuccess(Void result) throws Exception {
log.debug("EP {}: Elements have been connected (source {} -> sink {})", getEndpointName(),
source.getId(), sink.getId());
}
@Override
public void onError(Throwable cause) throws Exception {
log.warn("EP {}: Failed to connect media elements (source {} -> sink {})",
getEndpointName(), source.getId(), sink.getId(), cause);
}
});
}
/**
* Same as {@link #internalSinkConnect(MediaElement, MediaElement)}, but can specify the type of
* the media that will be streamed.
*
* @see #internalSinkConnect(MediaElement, MediaElement)
* @param source
* @param sink
* @param type
* if null, {@link #internalSinkConnect(MediaElement, MediaElement)} will be used instead
*/
private void internalSinkConnect(final MediaElement source, final MediaElement sink,
final MediaType type) {
if (type == null) {
internalSinkConnect(source, sink);
} else {
source.connect(sink, type, new Continuation() {
@Override
public void onSuccess(Void result) throws Exception {
log.debug("EP {}: {} media elements have been connected (source {} -> sink {})",
getEndpointName(), type, source.getId(), sink.getId());
}
@Override
public void onError(Throwable cause) throws Exception {
log.warn("EP {}: Failed to connect {} media elements (source {} -> sink {})",
getEndpointName(), type, source.getId(), sink.getId(), cause);
}
});
}
}
private void internalSinkDisconnect(final MediaElement source, final MediaElement sink) {
source.disconnect(sink, new Continuation() {
@Override
public void onSuccess(Void result) throws Exception {
log.debug("EP {}: Elements have been disconnected (source {} -> sink {})",
getEndpointName(), source.getId(), sink.getId());
}
@Override
public void onError(Throwable cause) throws Exception {
log.warn("EP {}: Failed to disconnect media elements (source {} -> sink {})",
getEndpointName(), source.getId(), sink.getId(), cause);
}
});
}
/**
* Same as {@link #internalSinkDisconnect(MediaElement, MediaElement)}, but can specify the type
* of the media that will be disconnected.
*
* @see #internalSinkConnect(MediaElement, MediaElement)
* @param source
* @param sink
* @param type
* if null, {@link #internalSinkConnect(MediaElement, MediaElement)} will be used instead
*/
private void internalSinkDisconnect(final MediaElement source, final MediaElement sink,
final MediaType type) {
if (type == null) {
internalSinkDisconnect(source, sink);
} else {
source.disconnect(sink, type, new Continuation() {
@Override
public void onSuccess(Void result) throws Exception {
log.debug("EP {}: {} media elements have been disconnected (source {} -> sink {})",
getEndpointName(), type, source.getId(), sink.getId());
}
@Override
public void onError(Throwable cause) throws Exception {
log.warn("EP {}: Failed to disconnect {} media elements (source {} -> sink {})",
getEndpointName(), type, source.getId(), sink.getId(), cause);
}
});
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy