mirror of https://github.com/OpenVidu/openvidu.git
Merge branch 'mediasoup' into other2
commit
db65999d1b
|
@ -99,14 +99,6 @@ export class Session extends EventDispatcher {
|
|||
*/
|
||||
remoteStreamsCreated: ObjMap<boolean> = {};
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
isFirstIonicIosSubscriber = true;
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
countDownForIonicIosSubscribersActive = true;
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
|
@ -724,11 +716,6 @@ export class Session extends EventDispatcher {
|
|||
streamEvent.callDefaultBehavior();
|
||||
|
||||
delete this.remoteStreamsCreated[stream.streamId];
|
||||
|
||||
if (Object.keys(this.remoteStreamsCreated).length === 0) {
|
||||
this.isFirstIonicIosSubscriber = true;
|
||||
this.countDownForIonicIosSubscribersActive = true;
|
||||
}
|
||||
}
|
||||
delete this.remoteConnections[connection.connectionId];
|
||||
this.ee.emitEvent('connectionDestroyed', [new ConnectionEvent(false, this, 'connectionDestroyed', connection, msg.reason)]);
|
||||
|
@ -798,12 +785,6 @@ export class Session extends EventDispatcher {
|
|||
// Deleting the remote stream
|
||||
const streamId: string = connection.stream.streamId;
|
||||
delete this.remoteStreamsCreated[streamId];
|
||||
|
||||
if (Object.keys(this.remoteStreamsCreated).length === 0) {
|
||||
this.isFirstIonicIosSubscriber = true;
|
||||
this.countDownForIonicIosSubscribersActive = true;
|
||||
}
|
||||
|
||||
connection.removeStream(streamId);
|
||||
})
|
||||
.catch(openViduError => {
|
||||
|
|
|
@ -822,7 +822,7 @@ export class Stream extends EventDispatcher {
|
|||
simulcast: false
|
||||
};
|
||||
|
||||
const successCallback = (sdpOfferParam) => {
|
||||
const successOfferCallback = (sdpOfferParam) => {
|
||||
logger.debug('Sending SDP offer to publish as '
|
||||
+ this.streamId, sdpOfferParam);
|
||||
|
||||
|
@ -830,7 +830,8 @@ export class Stream extends EventDispatcher {
|
|||
let params;
|
||||
if (reconnect) {
|
||||
params = {
|
||||
stream: this.streamId
|
||||
stream: this.streamId,
|
||||
sdpString: sdpOfferParam
|
||||
}
|
||||
} else {
|
||||
let typeOfVideo = '';
|
||||
|
@ -846,10 +847,10 @@ export class Stream extends EventDispatcher {
|
|||
typeOfVideo,
|
||||
frameRate: !!this.frameRate ? this.frameRate : -1,
|
||||
videoDimensions: JSON.stringify(this.videoDimensions),
|
||||
filter: this.outboundStreamOpts.publisherProperties.filter
|
||||
filter: this.outboundStreamOpts.publisherProperties.filter,
|
||||
sdpOffer: sdpOfferParam
|
||||
}
|
||||
}
|
||||
params['sdpOffer'] = sdpOfferParam;
|
||||
|
||||
this.session.openvidu.sendRequest(method, params, (error, response) => {
|
||||
if (error) {
|
||||
|
@ -859,7 +860,7 @@ export class Stream extends EventDispatcher {
|
|||
reject('Error on publishVideo: ' + JSON.stringify(error));
|
||||
}
|
||||
} else {
|
||||
this.webRtcPeer.processAnswer(response.sdpAnswer, false)
|
||||
this.webRtcPeer.processRemoteAnswer(response.sdpAnswer)
|
||||
.then(() => {
|
||||
this.streamId = response.id;
|
||||
this.creationTime = response.createdAt;
|
||||
|
@ -894,10 +895,15 @@ export class Stream extends EventDispatcher {
|
|||
this.webRtcPeer = new WebRtcPeerSendonly(options);
|
||||
}
|
||||
this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId);
|
||||
this.webRtcPeer.generateOffer().then(sdpOffer => {
|
||||
successCallback(sdpOffer);
|
||||
this.webRtcPeer.createOffer().then(sdpOffer => {
|
||||
this.webRtcPeer.processLocalOffer(sdpOffer)
|
||||
.then(() => {
|
||||
successOfferCallback(sdpOffer.sdp);
|
||||
}).catch(error => {
|
||||
reject(new Error('(publish) SDP process local offer error: ' + JSON.stringify(error)));
|
||||
});
|
||||
}).catch(error => {
|
||||
reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error)));
|
||||
reject(new Error('(publish) SDP create offer error: ' + JSON.stringify(error)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -906,6 +912,30 @@ export class Stream extends EventDispatcher {
|
|||
* @hidden
|
||||
*/
|
||||
initWebRtcPeerReceive(reconnect: boolean): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.session.openvidu.sendRequest('prepareReceiveVideFrom', { sender: this.streamId, reconnect }, (error, response) => {
|
||||
if (error) {
|
||||
reject(new Error('Error on prepareReceiveVideFrom: ' + JSON.stringify(error)));
|
||||
} else {
|
||||
this.completeWebRtcPeerReceive(response.sdpOffer, reconnect)
|
||||
.then(() => {
|
||||
logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
|
||||
this.remotePeerSuccessfullyEstablished();
|
||||
this.initWebRtcStats();
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
completeWebRtcPeerReceive(sdpOffer: string, reconnect: boolean): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const offerConstraints = {
|
||||
|
@ -921,50 +951,41 @@ export class Stream extends EventDispatcher {
|
|||
simulcast: false
|
||||
};
|
||||
|
||||
const successCallback = (sdpOfferParam) => {
|
||||
logger.debug('Sending SDP offer to subscribe to '
|
||||
+ this.streamId, sdpOfferParam);
|
||||
const successAnswerCallback = (sdpAnswer) => {
|
||||
logger.debug('Sending SDP answer to subscribe to '
|
||||
+ this.streamId, sdpAnswer);
|
||||
|
||||
const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom';
|
||||
const params = { sdpOffer: sdpOfferParam };
|
||||
const params = {};
|
||||
params[reconnect ? 'stream' : 'sender'] = this.streamId;
|
||||
params[reconnect ? 'sdpString' : 'sdpAnswer'] = sdpAnswer;
|
||||
|
||||
this.session.openvidu.sendRequest(method, params, (error, response) => {
|
||||
if (error) {
|
||||
reject(new Error('Error on recvVideoFrom: ' + JSON.stringify(error)));
|
||||
reject(new Error('Error on receiveVideFrom: ' + JSON.stringify(error)));
|
||||
} else {
|
||||
// Ios Ionic. Limitation: some bug in iosrtc cordova plugin makes it necessary
|
||||
// to add a timeout before calling PeerConnection#setRemoteDescription during
|
||||
// some time (400 ms) from the moment first subscriber stream is received
|
||||
if (this.session.isFirstIonicIosSubscriber) {
|
||||
this.session.isFirstIonicIosSubscriber = false;
|
||||
setTimeout(() => {
|
||||
// After 400 ms Ionic iOS subscribers won't need to run
|
||||
// PeerConnection#setRemoteDescription after 250 ms timeout anymore
|
||||
this.session.countDownForIonicIosSubscribersActive = false;
|
||||
}, 400);
|
||||
}
|
||||
const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive;
|
||||
this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => {
|
||||
logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
|
||||
this.remotePeerSuccessfullyEstablished();
|
||||
this.initWebRtcStats();
|
||||
resolve();
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.webRtcPeer = new WebRtcPeerRecvonly(options);
|
||||
this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId);
|
||||
this.webRtcPeer.generateOffer()
|
||||
.then(sdpOffer => {
|
||||
successCallback(sdpOffer);
|
||||
this.webRtcPeer.processRemoteOffer(sdpOffer)
|
||||
.then(() => {
|
||||
this.webRtcPeer.createAnswer().then(sdpAnswer => {
|
||||
this.webRtcPeer.processLocalAnswer(sdpAnswer)
|
||||
.then(() => {
|
||||
successAnswerCallback(sdpAnswer.sdp);
|
||||
}).catch(error => {
|
||||
reject(new Error('(subscribe) SDP process local answer error: ' + JSON.stringify(error)));
|
||||
});
|
||||
}).catch(error => {
|
||||
reject(new Error('(subscribe) SDP create answer error: ' + JSON.stringify(error)));
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error)));
|
||||
reject(new Error('(subscribe) SDP process remote offer error: ' + JSON.stringify(error)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -117,10 +117,10 @@ export class WebRtcPeer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Function that creates an offer, sets it as local description and returns the offer param
|
||||
* to send to OpenVidu Server (will be the remote description of other peer)
|
||||
* Creates an SDP offer from the local RTCPeerConnection to send to the other peer
|
||||
* Only if the negotiation was initiated by the this peer
|
||||
*/
|
||||
generateOffer(): Promise<string> {
|
||||
createOffer(): Promise<RTCSessionDescriptionInit> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let offerAudio, offerVideo = true;
|
||||
|
||||
|
@ -140,52 +140,32 @@ export class WebRtcPeer {
|
|||
logger.debug('RTCPeerConnection constraints: ' + JSON.stringify(constraints));
|
||||
|
||||
if (platform.name === 'Safari' && platform.ua!!.indexOf('Safari') !== -1) {
|
||||
|
||||
// Safari (excluding Ionic), at least on iOS just seems to support unified plan, whereas in other browsers is not yet ready and considered experimental
|
||||
if (offerAudio) {
|
||||
this.pc.addTransceiver('audio', {
|
||||
direction: this.configuration.mode,
|
||||
});
|
||||
}
|
||||
|
||||
if (offerVideo) {
|
||||
this.pc.addTransceiver('video', {
|
||||
direction: this.configuration.mode,
|
||||
});
|
||||
}
|
||||
|
||||
this.pc
|
||||
.createOffer()
|
||||
this.pc.createOffer()
|
||||
.then(offer => {
|
||||
logger.debug('Created SDP offer');
|
||||
return this.pc.setLocalDescription(offer);
|
||||
})
|
||||
.then(() => {
|
||||
const localDescription = this.pc.localDescription;
|
||||
|
||||
if (!!localDescription) {
|
||||
logger.debug('Local description set', localDescription.sdp);
|
||||
resolve(localDescription.sdp);
|
||||
} else {
|
||||
reject('Local description is not defined');
|
||||
}
|
||||
resolve(offer);
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
|
||||
} else {
|
||||
|
||||
// Rest of platforms
|
||||
this.pc.createOffer(constraints).then(offer => {
|
||||
logger.debug('Created SDP offer');
|
||||
return this.pc.setLocalDescription(offer);
|
||||
})
|
||||
.then(() => {
|
||||
const localDescription = this.pc.localDescription;
|
||||
if (!!localDescription) {
|
||||
logger.debug('Local description set', localDescription.sdp);
|
||||
resolve(localDescription.sdp);
|
||||
} else {
|
||||
reject('Local description is not defined');
|
||||
}
|
||||
this.pc.createOffer(constraints)
|
||||
.then(offer => {
|
||||
logger.debug('Created SDP offer');
|
||||
resolve(offer);
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
}
|
||||
|
@ -193,10 +173,94 @@ export class WebRtcPeer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Function invoked when a SDP answer is received. Final step in SDP negotiation, the peer
|
||||
* just needs to set the answer as its remote description
|
||||
* Creates an SDP answer from the local RTCPeerConnection to send to the other peer
|
||||
* Only if the negotiation was initiated by the other peer
|
||||
*/
|
||||
processAnswer(sdpAnswer: string, needsTimeoutOnProcessAnswer: boolean): Promise<string> {
|
||||
createAnswer(): Promise<RTCSessionDescriptionInit> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let offerAudio, offerVideo = true;
|
||||
if (!!this.configuration.mediaConstraints) {
|
||||
offerAudio = (typeof this.configuration.mediaConstraints.audio === 'boolean') ?
|
||||
this.configuration.mediaConstraints.audio : true;
|
||||
offerVideo = (typeof this.configuration.mediaConstraints.video === 'boolean') ?
|
||||
this.configuration.mediaConstraints.video : true;
|
||||
}
|
||||
const constraints: RTCOfferOptions = {
|
||||
offerToReceiveAudio: offerAudio,
|
||||
offerToReceiveVideo: offerVideo
|
||||
};
|
||||
this.pc.createAnswer(constraints).then(sdpAnswer => {
|
||||
resolve(sdpAnswer);
|
||||
}).catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This peer initiated negotiation. Step 1/4 of SDP offer-answer protocol
|
||||
*/
|
||||
processLocalOffer(offer: RTCSessionDescriptionInit): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pc.setLocalDescription(offer)
|
||||
.then(() => {
|
||||
const localDescription = this.pc.localDescription;
|
||||
if (!!localDescription) {
|
||||
logger.debug('Local description set', localDescription.sdp);
|
||||
resolve();
|
||||
} else {
|
||||
reject('Local description is not defined');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Other peer initiated negotiation. Step 2/4 of SDP offer-answer protocol
|
||||
*/
|
||||
processRemoteOffer(sdpOffer: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const offer: RTCSessionDescriptionInit = {
|
||||
type: 'offer',
|
||||
sdp: sdpOffer
|
||||
};
|
||||
logger.debug('SDP offer received, setting remote description', offer);
|
||||
|
||||
if (this.pc.signalingState === 'closed') {
|
||||
reject('RTCPeerConnection is closed when trying to set remote description');
|
||||
}
|
||||
this.setRemoteDescription(offer)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Other peer initiated negotiation. Step 3/4 of SDP offer-answer protocol
|
||||
*/
|
||||
processLocalAnswer(answer: RTCSessionDescriptionInit): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.debug('SDP answer created, setting local description');
|
||||
if (this.pc.signalingState === 'closed') {
|
||||
reject('RTCPeerConnection is closed when trying to set local description');
|
||||
}
|
||||
this.pc.setLocalDescription(answer)
|
||||
.then(() => resolve())
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This peer initiated negotiation. Step 4/4 of SDP offer-answer protocol
|
||||
*/
|
||||
processRemoteAnswer(sdpAnswer: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const answer: RTCSessionDescriptionInit = {
|
||||
type: 'answer',
|
||||
|
@ -205,34 +269,19 @@ export class WebRtcPeer {
|
|||
logger.debug('SDP answer received, setting remote description');
|
||||
|
||||
if (this.pc.signalingState === 'closed') {
|
||||
reject('RTCPeerConnection is closed');
|
||||
reject('RTCPeerConnection is closed when trying to set remote description');
|
||||
}
|
||||
|
||||
this.setRemoteDescription(answer, needsTimeoutOnProcessAnswer, resolve, reject);
|
||||
|
||||
this.setRemoteDescription(answer)
|
||||
.then(() => resolve())
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setRemoteDescription(answer: RTCSessionDescriptionInit, needsTimeoutOnProcessAnswer: boolean, resolve: (value?: string | PromiseLike<string> | undefined) => void, reject: (reason?: any) => void) {
|
||||
if (platform['isIonicIos']) {
|
||||
// Ionic iOS platform
|
||||
if (needsTimeoutOnProcessAnswer) {
|
||||
// 400 ms have not elapsed yet since first remote stream triggered Stream#initWebRtcPeerReceive
|
||||
setTimeout(() => {
|
||||
logger.info('setRemoteDescription run after timeout for Ionic iOS device');
|
||||
this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error));
|
||||
}, 250);
|
||||
} else {
|
||||
// 400 ms have elapsed
|
||||
this.pc.setRemoteDescription(new RTCSessionDescription(answer)).then(() => resolve()).catch(error => reject(error));
|
||||
}
|
||||
} else {
|
||||
// Rest of platforms
|
||||
this.pc.setRemoteDescription(answer).then(() => resolve()).catch(error => reject(error));
|
||||
}
|
||||
async setRemoteDescription(sdp: RTCSessionDescriptionInit): Promise<void> {
|
||||
return this.pc.setRemoteDescription(sdp);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -49,7 +49,9 @@ public class OpenViduException extends JsonRpcErrorException {
|
|||
|
||||
DOCKER_NOT_FOUND(709), RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707),
|
||||
RECORDING_DELETE_ERROR_CODE(706), RECORDING_LIST_ERROR_CODE(705), RECORDING_STOP_ERROR_CODE(704),
|
||||
RECORDING_START_ERROR_CODE(703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701);
|
||||
RECORDING_START_ERROR_CODE(703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701),
|
||||
|
||||
FORCED_CODEC_NOT_FOUND_IN_SDPOFFER(800);
|
||||
|
||||
private int value;
|
||||
|
||||
|
|
|
@ -70,6 +70,10 @@ public class ProtocolElements {
|
|||
|
||||
public static final String UNPUBLISHVIDEO_METHOD = "unpublishVideo";
|
||||
|
||||
public static final String PREPARERECEIVEVIDEO_METHOD = "prepareReceiveVideFrom";
|
||||
public static final String PREPARERECEIVEVIDEO_SDPOFFER_PARAM = "sdpOffer";
|
||||
public static final String PREPARERECEIVEVIDEO_RECONNECT_PARAM = "reconnect";
|
||||
|
||||
public static final String RECEIVEVIDEO_METHOD = "receiveVideoFrom";
|
||||
public static final String RECEIVEVIDEO_SDPOFFER_PARAM = "sdpOffer";
|
||||
public static final String RECEIVEVIDEO_SENDER_PARAM = "sender";
|
||||
|
@ -120,7 +124,7 @@ public class ProtocolElements {
|
|||
|
||||
public static final String RECONNECTSTREAM_METHOD = "reconnectStream";
|
||||
public static final String RECONNECTSTREAM_STREAM_PARAM = "stream";
|
||||
public static final String RECONNECTSTREAM_SDPOFFER_PARAM = "sdpOffer";
|
||||
public static final String RECONNECTSTREAM_SDPSTRING_PARAM = "sdpString";
|
||||
|
||||
// ---------------------------- SERVER RESPONSES & EVENTS -----------------
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/bin/bash -x
|
||||
|
||||
if [[ -z "$BASEHREF_VERSION" ]]; then
|
||||
echo "Example of use: \"BASEHREF_VERSION=2.12.0 ${0}\"" 1>&2
|
||||
|
@ -9,7 +9,7 @@ fi
|
|||
grep -rl '/en/stable/' ./src | xargs sed -i -e 's|/en/stable/|/en/'${BASEHREF_VERSION}'/|g'
|
||||
|
||||
# Generate JavaDoc
|
||||
mvn javadoc:javadoc
|
||||
mvn javadoc:javadoc -DadditionalJOption=-Xdoclint:none
|
||||
rm -rf ../../openvidu.io/api/openvidu-java-client/*
|
||||
cp -R ./target/site/apidocs/. ../../openvidu.io-docs/docs/api/openvidu-java-client
|
||||
|
||||
|
|
|
@ -473,6 +473,8 @@ public class Session {
|
|||
json.addProperty("defaultRecordingLayout", properties.defaultRecordingLayout().name());
|
||||
json.addProperty("defaultCustomLayout", properties.defaultCustomLayout());
|
||||
json.addProperty("customSessionId", properties.customSessionId());
|
||||
json.addProperty("forcedVideoCodec", properties.forcedVideoCodec().name());
|
||||
json.addProperty("allowTranscoding", properties.isTranscodingAllowed());
|
||||
StringEntity params = null;
|
||||
try {
|
||||
params = new StringEntity(json.toString());
|
||||
|
@ -535,6 +537,12 @@ public class Session {
|
|||
if (json.has("defaultCustomLayout")) {
|
||||
builder.defaultCustomLayout(json.get("defaultCustomLayout").getAsString());
|
||||
}
|
||||
if (json.has("forcedVideoCodec")) {
|
||||
builder.forcedVideoCodec(VideoCodec.valueOf(json.get("forcedVideoCodec").getAsString()));
|
||||
}
|
||||
if (json.has("allowTranscoding")) {
|
||||
builder.allowTranscoding(json.get("allowTranscoding").getAsBoolean());
|
||||
}
|
||||
if (this.properties != null && this.properties.customSessionId() != null) {
|
||||
builder.customSessionId(this.properties.customSessionId());
|
||||
} else if (json.has("customSessionId")) {
|
||||
|
@ -587,6 +595,9 @@ public class Session {
|
|||
json.addProperty("defaultOutputMode", this.properties.defaultOutputMode().name());
|
||||
json.addProperty("defaultRecordingLayout", this.properties.defaultRecordingLayout().name());
|
||||
json.addProperty("defaultCustomLayout", this.properties.defaultCustomLayout());
|
||||
json.addProperty("forcedVideoCodec", this.properties.forcedVideoCodec().name());
|
||||
json.addProperty("allowTranscoding", this.properties.isTranscodingAllowed());
|
||||
|
||||
JsonObject connections = new JsonObject();
|
||||
connections.addProperty("numberOfElements", this.getActiveConnections().size());
|
||||
JsonArray jsonArrayConnections = new JsonArray();
|
||||
|
|
|
@ -30,6 +30,8 @@ public class SessionProperties {
|
|||
private RecordingLayout defaultRecordingLayout;
|
||||
private String defaultCustomLayout;
|
||||
private String customSessionId;
|
||||
private VideoCodec forcedVideoCodec;
|
||||
private boolean allowTranscoding;
|
||||
|
||||
/**
|
||||
* Builder for {@link io.openvidu.java.client.SessionProperties}
|
||||
|
@ -42,6 +44,8 @@ public class SessionProperties {
|
|||
private RecordingLayout defaultRecordingLayout = RecordingLayout.BEST_FIT;
|
||||
private String defaultCustomLayout = "";
|
||||
private String customSessionId = "";
|
||||
private VideoCodec forcedVideoCodec = VideoCodec.VP8;
|
||||
private boolean allowTranscoding = false;
|
||||
|
||||
/**
|
||||
* Returns the {@link io.openvidu.java.client.SessionProperties} object properly
|
||||
|
@ -49,7 +53,8 @@ public class SessionProperties {
|
|||
*/
|
||||
public SessionProperties build() {
|
||||
return new SessionProperties(this.mediaMode, this.recordingMode, this.defaultOutputMode,
|
||||
this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId);
|
||||
this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId,
|
||||
this.forcedVideoCodec, this.allowTranscoding);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,6 +143,28 @@ public class SessionProperties {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method to define which video codec do you want to be forcibly used for this session.
|
||||
* This allows browsers/clients to use the same codec avoiding transcoding in the media server.
|
||||
* If the browser/client is not compatible with the specified codec and {@link #allowTranscoding(boolean)}
|
||||
* is <code>false</code> and exception will occur.
|
||||
*
|
||||
* If forcedVideoCodec is set to NONE, no codec will be forced.
|
||||
*/
|
||||
public SessionProperties.Builder forcedVideoCodec(VideoCodec forcedVideoCodec) {
|
||||
this.forcedVideoCodec = forcedVideoCodec;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method to define if you want to allow transcoding in the media server or not
|
||||
* when {@link #forcedVideoCodec(VideoCodec)} is not compatible with the browser/client.
|
||||
*/
|
||||
public SessionProperties.Builder allowTranscoding(boolean allowTranscoding) {
|
||||
this.allowTranscoding = allowTranscoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected SessionProperties() {
|
||||
|
@ -147,16 +174,21 @@ public class SessionProperties {
|
|||
this.defaultRecordingLayout = RecordingLayout.BEST_FIT;
|
||||
this.defaultCustomLayout = "";
|
||||
this.customSessionId = "";
|
||||
this.forcedVideoCodec = VideoCodec.VP8;
|
||||
this.allowTranscoding = false;
|
||||
}
|
||||
|
||||
private SessionProperties(MediaMode mediaMode, RecordingMode recordingMode, OutputMode outputMode,
|
||||
RecordingLayout layout, String defaultCustomLayout, String customSessionId) {
|
||||
RecordingLayout layout, String defaultCustomLayout, String customSessionId,
|
||||
VideoCodec forcedVideoCodec, boolean allowTranscoding) {
|
||||
this.mediaMode = mediaMode;
|
||||
this.recordingMode = recordingMode;
|
||||
this.defaultOutputMode = outputMode;
|
||||
this.defaultRecordingLayout = layout;
|
||||
this.defaultCustomLayout = defaultCustomLayout;
|
||||
this.customSessionId = customSessionId;
|
||||
this.forcedVideoCodec = forcedVideoCodec;
|
||||
this.allowTranscoding = allowTranscoding;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -231,4 +263,19 @@ public class SessionProperties {
|
|||
return this.customSessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines which video codec is being forced to be used in the browser/client
|
||||
*/
|
||||
public VideoCodec forcedVideoCodec() {
|
||||
return this.forcedVideoCodec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if transcoding is allowed or not when {@link #forcedVideoCodec}
|
||||
* is not a compatible codec with the browser/client.
|
||||
*/
|
||||
public boolean isTranscodingAllowed() {
|
||||
return this.allowTranscoding;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
|
||||
*
|
||||
* 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 io.openvidu.java.client;
|
||||
|
||||
/**
|
||||
* See {@link io.openvidu.java.client.SessionProperties.Builder#forcedVideoCodec(VideoCodec)}
|
||||
*/
|
||||
public enum VideoCodec {
|
||||
VP8, VP9, H264, NONE
|
||||
}
|
|
@ -26,6 +26,7 @@ import { RecordingLayout } from './RecordingLayout';
|
|||
import { RecordingMode } from './RecordingMode';
|
||||
import { SessionProperties } from './SessionProperties';
|
||||
import { TokenOptions } from './TokenOptions';
|
||||
import { VideoCodec } from './VideoCodec';
|
||||
|
||||
|
||||
export class Session {
|
||||
|
@ -83,6 +84,8 @@ export class Session {
|
|||
this.properties.recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL;
|
||||
this.properties.defaultOutputMode = !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED;
|
||||
this.properties.defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT;
|
||||
this.properties.allowTranscoding = !!this.properties.allowTranscoding ? this.properties.allowTranscoding : false;
|
||||
this.properties.forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : VideoCodec.VP8;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -402,7 +405,10 @@ export class Session {
|
|||
defaultOutputMode: !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED,
|
||||
defaultRecordingLayout: !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT,
|
||||
defaultCustomLayout: !!this.properties.defaultCustomLayout ? this.properties.defaultCustomLayout : '',
|
||||
customSessionId: !!this.properties.customSessionId ? this.properties.customSessionId : ''
|
||||
customSessionId: !!this.properties.customSessionId ? this.properties.customSessionId : '',
|
||||
forcedVideoCodec: !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : VideoCodec.VP8,
|
||||
allowTranscoding: !!this.properties.allowTranscoding ? this.properties.allowTranscoding : false
|
||||
|
||||
});
|
||||
|
||||
axios.post(
|
||||
|
@ -467,7 +473,9 @@ export class Session {
|
|||
mediaMode: json.mediaMode,
|
||||
recordingMode: json.recordingMode,
|
||||
defaultOutputMode: json.defaultOutputMode,
|
||||
defaultRecordingLayout: json.defaultRecordingLayout
|
||||
defaultRecordingLayout: json.defaultRecordingLayout,
|
||||
forcedVideoCodec: !!json.forcedVideoCodec ? json.forcedVideoCodec : VideoCodec.VP8,
|
||||
allowTranscoding: !!json.allowTranscoding ? json.allowTranscoding : false
|
||||
};
|
||||
if (!!customSessionId) {
|
||||
this.properties.customSessionId = customSessionId;
|
||||
|
|
|
@ -19,6 +19,7 @@ import { MediaMode } from './MediaMode';
|
|||
import { Recording } from './Recording';
|
||||
import { RecordingLayout } from './RecordingLayout';
|
||||
import { RecordingMode } from './RecordingMode';
|
||||
import { VideoCodec } from './VideoCodec';
|
||||
|
||||
/**
|
||||
* See [[OpenVidu.createSession]]
|
||||
|
@ -64,4 +65,21 @@ export interface SessionProperties {
|
|||
* If this parameter is undefined or an empty string, OpenVidu Server will generate a random sessionId for you.
|
||||
*/
|
||||
customSessionId?: string;
|
||||
|
||||
/**
|
||||
* It defines which video codec do you want to be forcibly used for this session.
|
||||
* This allows browsers/clients to use the same codec avoiding transcoding in the media server.
|
||||
* If the browser/client is not compatible with the specified codec and [[allowTranscoding]]
|
||||
* is <code>false</code> and exception will occur.
|
||||
*
|
||||
* If forcedVideoCodec is set to NONE, no codec will be forced.
|
||||
*/
|
||||
forcedVideoCodec?: VideoCodec;
|
||||
|
||||
/**
|
||||
* It defines if you want to allow transcoding in the media server or not
|
||||
* when [[forcedVideoCodec]] is not compatible with the browser/client.
|
||||
*/
|
||||
allowTranscoding?: boolean;
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* See [[SessionProperties.forcedVideoCodec]]
|
||||
*/
|
||||
export enum VideoCodec {
|
||||
|
||||
VP8 = 'VP8',
|
||||
VP9 = 'VP9',
|
||||
H264 = 'H264',
|
||||
NONE = 'NONE'
|
||||
|
||||
}
|
|
@ -10,3 +10,4 @@ export * from './Recording';
|
|||
export * from './RecordingProperties';
|
||||
export * from './Connection';
|
||||
export * from './Publisher';
|
||||
export * from './VideoCodec';
|
|
@ -137,6 +137,13 @@ OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900
|
|||
# (property 'OPENVIDU_SESSIONS_GARBAGE_INTERVAL' to 0) this property is ignored
|
||||
OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600
|
||||
|
||||
# All sessions of OpenVidu will try to force this codec. If OPENVIDU_ALLOW_TRANSCODING=true
|
||||
# when a codec can not be forced, transcoding will be allowed
|
||||
# OPENVIDU_FORCED_CODEC=VP8
|
||||
|
||||
# Allow transcoding if codec specified in OPENVIDU_FORCED_CODEC can not be applied
|
||||
# OPENVIDU_ALLOW_TRANSCODING=false
|
||||
|
||||
# Call Detail Record enabled
|
||||
# Whether to enable Call Detail Record or not
|
||||
# Values: true | false
|
||||
|
@ -149,7 +156,7 @@ OPENVIDU_CDR_PATH=/opt/openvidu/cdr
|
|||
# --------------------------
|
||||
# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server-dev
|
||||
# Uncomment the next line and define this variable with KMS image that you want use
|
||||
# KMS_IMAGE=kurento/kurento-media-server-dev:6.14.0
|
||||
# KMS_IMAGE=kurento/kurento-media-server:6.14.0
|
||||
|
||||
# Kurento Media Server Level logs
|
||||
# -------------------------------
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
# --------------------------
|
||||
# Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server-dev
|
||||
# Uncomment the next line and define this variable with KMS image that you want use
|
||||
# KMS_IMAGE=kurento/kurento-media-server-dev:6.14.0
|
||||
# KMS_IMAGE=kurento/kurento-media-server:6.14.0
|
||||
|
||||
# Kurento Media Server Level logs
|
||||
# -------------------------------
|
||||
|
|
|
@ -178,6 +178,13 @@ OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH=1000
|
|||
# 0 means unconstrained
|
||||
OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300
|
||||
|
||||
# All sessions of OpenVidu will try to force this codec. If OPENVIDU_ALLOW_TRANSCODING=true
|
||||
# when a codec can not be forced, transcoding will be allowed
|
||||
# OPENVIDU_FORCED_CODEC=VP8
|
||||
|
||||
# Allow transcoding if codec specified in OPENVIDU_FORCED_CODEC can not be applied
|
||||
# OPENVIDU_ALLOW_TRANSCODING=false
|
||||
|
||||
# true to enable OpenVidu Webhook service. false' otherwise
|
||||
# Values: true | false
|
||||
OPENVIDU_WEBHOOK=false
|
||||
|
|
|
@ -71,6 +71,7 @@ import io.openvidu.server.utils.MediaNodeStatusManager;
|
|||
import io.openvidu.server.utils.MediaNodeStatusManagerDummy;
|
||||
import io.openvidu.server.utils.QuarantineKiller;
|
||||
import io.openvidu.server.utils.QuarantineKillerDummy;
|
||||
import io.openvidu.server.utils.SDPMunging;
|
||||
import io.openvidu.server.webhook.CDRLoggerWebhook;
|
||||
|
||||
/**
|
||||
|
@ -195,6 +196,12 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
|||
return new GeoLocationByIpDummy();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SDPMunging sdpMunging() {
|
||||
return new SDPMunging();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public QuarantineKiller quarantineKiller() {
|
||||
|
|
|
@ -37,6 +37,10 @@ import java.util.Map;
|
|||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
|
@ -48,11 +52,8 @@ import org.springframework.beans.factory.annotation.Value;
|
|||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import io.openvidu.java.client.OpenViduRole;
|
||||
import io.openvidu.java.client.VideoCodec;
|
||||
import io.openvidu.server.OpenViduServer;
|
||||
import io.openvidu.server.cdr.CDREventName;
|
||||
import io.openvidu.server.config.Dotenv.DotenvFormatException;
|
||||
|
@ -176,6 +177,10 @@ public class OpenviduConfig {
|
|||
|
||||
protected int openviduSessionsGarbageThreshold;
|
||||
|
||||
private VideoCodec openviduForcedCodec;
|
||||
|
||||
private boolean openviduAllowTranscoding;
|
||||
|
||||
private String dotenvPath;
|
||||
|
||||
// Derived properties
|
||||
|
@ -190,6 +195,14 @@ public class OpenviduConfig {
|
|||
return this.coturnRedisDbname;
|
||||
}
|
||||
|
||||
public boolean isOpenviduAllowingTranscoding() {
|
||||
return openviduAllowTranscoding;
|
||||
}
|
||||
|
||||
public VideoCodec getOpenviduForcedCodec() {
|
||||
return openviduForcedCodec;
|
||||
}
|
||||
|
||||
public String getCoturnDatabasePassword() {
|
||||
return this.coturnRedisPassword;
|
||||
}
|
||||
|
@ -335,20 +348,20 @@ public class OpenviduConfig {
|
|||
public OpenViduRole[] getRolesFromRecordingNotification() {
|
||||
OpenViduRole[] roles;
|
||||
switch (this.openviduRecordingNotification) {
|
||||
case none:
|
||||
roles = new OpenViduRole[0];
|
||||
break;
|
||||
case moderator:
|
||||
roles = new OpenViduRole[] { OpenViduRole.MODERATOR };
|
||||
break;
|
||||
case publisher_moderator:
|
||||
roles = new OpenViduRole[] { OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
|
||||
break;
|
||||
case all:
|
||||
roles = new OpenViduRole[] { OpenViduRole.SUBSCRIBER, OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
|
||||
break;
|
||||
default:
|
||||
roles = new OpenViduRole[] { OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
|
||||
case none:
|
||||
roles = new OpenViduRole[0];
|
||||
break;
|
||||
case moderator:
|
||||
roles = new OpenViduRole[] { OpenViduRole.MODERATOR };
|
||||
break;
|
||||
case publisher_moderator:
|
||||
roles = new OpenViduRole[] { OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
|
||||
break;
|
||||
case all:
|
||||
roles = new OpenViduRole[] { OpenViduRole.SUBSCRIBER, OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
|
||||
break;
|
||||
default:
|
||||
roles = new OpenViduRole[] { OpenViduRole.PUBLISHER, OpenViduRole.MODERATOR };
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
@ -500,6 +513,9 @@ public class OpenviduConfig {
|
|||
openviduSessionsGarbageInterval = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_INTERVAL");
|
||||
openviduSessionsGarbageThreshold = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_THRESHOLD");
|
||||
|
||||
openviduForcedCodec = asEnumValue("OPENVIDU_FORCED_CODEC", VideoCodec.class);
|
||||
openviduAllowTranscoding = asBoolean("OPENVIDU_ALLOW_TRANSCODING");
|
||||
|
||||
kmsUrisList = checkKmsUris();
|
||||
|
||||
checkCoturnIp();
|
||||
|
|
|
@ -29,12 +29,12 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
import io.openvidu.client.internal.ProtocolElements;
|
||||
|
@ -211,6 +211,8 @@ public class Session implements SessionInterface {
|
|||
json.addProperty("mediaMode", this.sessionProperties.mediaMode().name());
|
||||
json.addProperty("recordingMode", this.sessionProperties.recordingMode().name());
|
||||
json.addProperty("defaultOutputMode", this.sessionProperties.defaultOutputMode().name());
|
||||
json.addProperty("forcedVideoCodec", this.sessionProperties.forcedVideoCodec().name());
|
||||
json.addProperty("allowTranscoding", this.sessionProperties.isTranscodingAllowed());
|
||||
if (RecordingUtils.IS_COMPOSED(this.sessionProperties.defaultOutputMode())) {
|
||||
json.addProperty("defaultRecordingLayout", this.sessionProperties.defaultRecordingLayout().name());
|
||||
if (RecordingLayout.CUSTOM.equals(this.sessionProperties.defaultRecordingLayout())) {
|
||||
|
|
|
@ -279,14 +279,24 @@ public class SessionEventsHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public void onSubscribe(Participant participant, Session session, String sdpAnswer, Integer transactionId,
|
||||
public void onPrepareSubscription(Participant participant, Session session, String sdpOffer, Integer transactionId,
|
||||
OpenViduException error) {
|
||||
if (error != null) {
|
||||
rpcNotificationService.sendErrorResponse(participant.getParticipantPrivateId(), transactionId, null, error);
|
||||
return;
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty(ProtocolElements.PREPARERECEIVEVIDEO_SDPOFFER_PARAM, sdpOffer);
|
||||
rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, result);
|
||||
}
|
||||
|
||||
public void onSubscribe(Participant participant, Session session, Integer transactionId,
|
||||
OpenViduException error) {
|
||||
if (error != null) {
|
||||
rpcNotificationService.sendErrorResponse(participant.getParticipantPrivateId(), transactionId, null, error);
|
||||
return;
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty(ProtocolElements.RECEIVEVIDEO_SDPANSWER_PARAM, sdpAnswer);
|
||||
rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, result);
|
||||
|
||||
if (ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(participant.getParticipantPublicId())) {
|
||||
|
|
|
@ -32,17 +32,17 @@ import java.util.stream.Collectors;
|
|||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.kurento.jsonrpc.message.Request;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
import io.openvidu.client.internal.ProtocolElements;
|
||||
|
@ -106,7 +106,9 @@ public abstract class SessionManager {
|
|||
public abstract void unpublishVideo(Participant participant, Participant moderator, Integer transactionId,
|
||||
EndReason reason);
|
||||
|
||||
public abstract void subscribe(Participant participant, String senderName, String sdpOffer, Integer transactionId);
|
||||
public abstract void prepareSubscription(Participant participant, String senderPublicId, boolean reconnect, Integer id);
|
||||
|
||||
public abstract void subscribe(Participant participant, String senderName, String sdpAnwser, Integer transactionId);
|
||||
|
||||
public abstract void unsubscribe(Participant participant, String senderName, Integer transactionId);
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@ import io.openvidu.server.core.MediaOptions;
|
|||
import io.openvidu.server.core.Participant;
|
||||
import io.openvidu.server.kurento.endpoint.MediaEndpoint;
|
||||
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
|
||||
import io.openvidu.server.kurento.endpoint.SdpType;
|
||||
import io.openvidu.server.kurento.endpoint.SubscriberEndpoint;
|
||||
import io.openvidu.server.recording.service.RecordingManager;
|
||||
|
||||
|
@ -169,15 +168,15 @@ public class KurentoParticipant extends Participant {
|
|||
return session;
|
||||
}
|
||||
|
||||
public String publishToRoom(SdpType sdpType, String sdpString, boolean doLoopback, boolean silent) {
|
||||
log.info("PARTICIPANT {}: Request to publish video in room {} (sdp type {})", this.getParticipantPublicId(),
|
||||
this.session.getSessionId(), sdpType);
|
||||
log.trace("PARTICIPANT {}: Publishing Sdp ({}) is {}", this.getParticipantPublicId(), sdpType, sdpString);
|
||||
public String publishToRoom(String sdpOffer, boolean doLoopback, boolean silent) {
|
||||
log.info("PARTICIPANT {}: Request to publish video in room {})", this.getParticipantPublicId(),
|
||||
this.session.getSessionId());
|
||||
log.trace("PARTICIPANT {}: Publishing Sdp Offer is {}", this.getParticipantPublicId(), sdpOffer);
|
||||
|
||||
String sdpResponse = this.getPublisher().publish(sdpType, sdpString, doLoopback);
|
||||
String sdpAnswer = this.getPublisher().publish(sdpOffer, doLoopback);
|
||||
this.streaming = true;
|
||||
|
||||
log.trace("PARTICIPANT {}: Publishing Sdp ({}) is {}", this.getParticipantPublicId(), sdpType, sdpResponse);
|
||||
log.trace("PARTICIPANT {}: Publishing Sdp Answer is {}", this.getParticipantPublicId(), sdpAnswer);
|
||||
log.info("PARTICIPANT {}: Is now publishing video in room {}", this.getParticipantPublicId(),
|
||||
this.session.getSessionId());
|
||||
|
||||
|
@ -191,7 +190,7 @@ public class KurentoParticipant extends Participant {
|
|||
publisher.getMediaOptions(), publisher.createdAt());
|
||||
}
|
||||
|
||||
return sdpResponse;
|
||||
return sdpAnswer;
|
||||
}
|
||||
|
||||
public void unpublishMedia(EndReason reason, long kmsDisconnectionTime) {
|
||||
|
@ -204,12 +203,11 @@ public class KurentoParticipant extends Participant {
|
|||
this.getParticipantPublicId());
|
||||
}
|
||||
|
||||
public String receiveMediaFrom(Participant sender, String sdpOffer, boolean silent) {
|
||||
public String prepareReceiveMediaFrom(Participant sender) {
|
||||
final String senderName = sender.getParticipantPublicId();
|
||||
|
||||
log.info("PARTICIPANT {}: Request to receive media from {} in room {}", this.getParticipantPublicId(),
|
||||
log.info("PARTICIPANT {}: Request to prepare receive media from {} in room {}", this.getParticipantPublicId(),
|
||||
senderName, this.session.getSessionId());
|
||||
log.trace("PARTICIPANT {}: SdpOffer for {} is {}", this.getParticipantPublicId(), senderName, sdpOffer);
|
||||
|
||||
if (senderName.equals(this.getParticipantPublicId())) {
|
||||
log.warn("PARTICIPANT {}: trying to configure loopback by subscribing", this.getParticipantPublicId());
|
||||
|
@ -269,8 +267,56 @@ public class KurentoParticipant extends Participant {
|
|||
log.debug("PARTICIPANT {}: Created subscriber endpoint for user {}", this.getParticipantPublicId(),
|
||||
senderName);
|
||||
try {
|
||||
String sdpAnswer = subscriber.subscribe(sdpOffer, kSender.getPublisher());
|
||||
log.trace("PARTICIPANT {}: Subscribing SdpAnswer is {}", this.getParticipantPublicId(), sdpAnswer);
|
||||
String sdpOffer = subscriber.prepareSubscription(kSender.getPublisher());
|
||||
log.trace("PARTICIPANT {}: Subscribing SdpOffer is {}", this.getParticipantPublicId(), sdpOffer);
|
||||
log.info("PARTICIPANT {}: offer prepared to receive media from {} in room {}",
|
||||
this.getParticipantPublicId(), senderName, this.session.getSessionId());
|
||||
return sdpOffer;
|
||||
} catch (KurentoServerException e) {
|
||||
log.error("Exception preparing subscriber endpoint for user {}: {}", this.getParticipantPublicId(),
|
||||
e.getMessage());
|
||||
this.subscribers.remove(senderName);
|
||||
releaseSubscriberEndpoint(senderName, (KurentoParticipant) sender, subscriber, null, false);
|
||||
return null;
|
||||
}
|
||||
} finally {
|
||||
kSender.getPublisher().closingLock.readLock().unlock();
|
||||
}
|
||||
} else {
|
||||
log.error(
|
||||
"PublisherEndpoint of participant {} of session {} is closed. Participant {} couldn't subscribe to it ",
|
||||
senderName, sender.getSessionId(), this.participantPublicId);
|
||||
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
|
||||
"Unable to create subscriber endpoint. Publisher endpoint of participant " + senderName
|
||||
+ "is closed");
|
||||
}
|
||||
}
|
||||
|
||||
public void receiveMediaFrom(Participant sender, String sdpAnswer, boolean silent) {
|
||||
final String senderName = sender.getParticipantPublicId();
|
||||
|
||||
log.info("PARTICIPANT {}: Request to receive media from {} in room {}", this.getParticipantPublicId(),
|
||||
senderName, this.session.getSessionId());
|
||||
log.trace("PARTICIPANT {}: SdpAnswer for {} is {}", this.getParticipantPublicId(), senderName, sdpAnswer);
|
||||
|
||||
if (senderName.equals(this.getParticipantPublicId())) {
|
||||
log.warn("PARTICIPANT {}: trying to configure loopback by subscribing", this.getParticipantPublicId());
|
||||
throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE, "Can loopback only when publishing media");
|
||||
}
|
||||
|
||||
KurentoParticipant kSender = (KurentoParticipant) sender;
|
||||
|
||||
if (kSender.streaming && kSender.getPublisher() != null
|
||||
&& kSender.getPublisher().closingLock.readLock().tryLock()) {
|
||||
|
||||
try {
|
||||
final SubscriberEndpoint subscriber = getSubscriber(senderName);
|
||||
if (subscriber.getEndpoint() == null) {
|
||||
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE, "Unable to create subscriber endpoint");
|
||||
}
|
||||
|
||||
try {
|
||||
subscriber.subscribe(sdpAnswer, kSender.getPublisher());
|
||||
log.info("PARTICIPANT {}: Is now receiving video from {} in room {}", this.getParticipantPublicId(),
|
||||
senderName, this.session.getSessionId());
|
||||
|
||||
|
@ -279,8 +325,6 @@ public class KurentoParticipant extends Participant {
|
|||
endpointConfig.getCdr().recordNewSubscriber(this, this.session.getSessionId(),
|
||||
sender.getPublisherStreamId(), sender.getParticipantPublicId(), subscriber.createdAt());
|
||||
}
|
||||
|
||||
return sdpAnswer;
|
||||
} catch (KurentoServerException e) {
|
||||
// TODO Check object status when KurentoClient sets this info in the object
|
||||
if (e.getCode() == 40101) {
|
||||
|
@ -292,7 +336,6 @@ public class KurentoParticipant extends Participant {
|
|||
}
|
||||
this.subscribers.remove(senderName);
|
||||
releaseSubscriberEndpoint(senderName, (KurentoParticipant) sender, subscriber, null, false);
|
||||
return null;
|
||||
}
|
||||
} finally {
|
||||
kSender.getPublisher().closingLock.readLock().unlock();
|
||||
|
|
|
@ -30,7 +30,9 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.openvidu.java.client.*;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.kurento.client.GenericMediaElement;
|
||||
import org.kurento.client.IceCandidate;
|
||||
|
@ -42,12 +44,16 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
import io.openvidu.client.internal.ProtocolElements;
|
||||
import io.openvidu.java.client.MediaMode;
|
||||
import io.openvidu.java.client.Recording;
|
||||
import io.openvidu.java.client.RecordingLayout;
|
||||
import io.openvidu.java.client.RecordingMode;
|
||||
import io.openvidu.java.client.RecordingProperties;
|
||||
import io.openvidu.java.client.SessionProperties;
|
||||
import io.openvidu.java.client.VideoCodec;
|
||||
import io.openvidu.server.core.EndReason;
|
||||
import io.openvidu.server.core.FinalUser;
|
||||
import io.openvidu.server.core.IdentifierPrefixes;
|
||||
|
@ -58,12 +64,12 @@ import io.openvidu.server.core.SessionManager;
|
|||
import io.openvidu.server.core.Token;
|
||||
import io.openvidu.server.kurento.endpoint.KurentoFilter;
|
||||
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
|
||||
import io.openvidu.server.kurento.endpoint.SdpType;
|
||||
import io.openvidu.server.kurento.kms.Kms;
|
||||
import io.openvidu.server.kurento.kms.KmsManager;
|
||||
import io.openvidu.server.rpc.RpcHandler;
|
||||
import io.openvidu.server.utils.GeoLocation;
|
||||
import io.openvidu.server.utils.JsonUtils;
|
||||
import io.openvidu.server.utils.SDPMunging;
|
||||
|
||||
public class KurentoSessionManager extends SessionManager {
|
||||
|
||||
|
@ -78,6 +84,9 @@ public class KurentoSessionManager extends SessionManager {
|
|||
@Autowired
|
||||
private KurentoParticipantEndpointConfig kurentoEndpointConfig;
|
||||
|
||||
@Autowired
|
||||
private SDPMunging sdpMunging;
|
||||
|
||||
@Override
|
||||
/* Protected by Session.closingLock.readLock */
|
||||
public void joinRoom(Participant participant, String sessionId, Integer transactionId) {
|
||||
|
@ -361,6 +370,28 @@ public class KurentoSessionManager extends SessionManager {
|
|||
|
||||
KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) mediaOptions;
|
||||
KurentoParticipant kParticipant = (KurentoParticipant) participant;
|
||||
KurentoSession kSession = kParticipant.getSession();
|
||||
boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed();
|
||||
VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec();
|
||||
|
||||
// Modify sdp if forced codec is defined
|
||||
if (forcedVideoCodec != VideoCodec.NONE) {
|
||||
String sdpOffer = kurentoOptions.sdpOffer;
|
||||
try {
|
||||
log.debug("PARTICIPANT '{}' in Session '{}' SDP Offer before munging: \n {}",
|
||||
participant.getParticipantPublicId(), kSession.getSessionId(), kurentoOptions.sdpOffer);
|
||||
kurentoOptions.sdpOffer = this.sdpMunging.setCodecPreference(forcedVideoCodec, sdpOffer);
|
||||
} catch (OpenViduException e) {
|
||||
String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT"
|
||||
+ participant.getParticipantPublicId() + "' publishing in Session: '"
|
||||
+ kSession.getSessionId() + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpAnswer;
|
||||
if(!isTranscodingAllowed) {
|
||||
throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage);
|
||||
}
|
||||
log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId()
|
||||
+ " publishing in Session: '" + kSession.getSessionId() + "'. Transcoding will be allowed");
|
||||
}
|
||||
}
|
||||
|
||||
log.debug(
|
||||
"Request [PUBLISH_MEDIA] isOffer={} sdp={} "
|
||||
|
@ -368,9 +399,6 @@ public class KurentoSessionManager extends SessionManager {
|
|||
kurentoOptions.isOffer, kurentoOptions.sdpOffer, kurentoOptions.doLoopback, kurentoOptions.rtspUri,
|
||||
participant.getParticipantPublicId());
|
||||
|
||||
SdpType sdpType = kurentoOptions.isOffer ? SdpType.OFFER : SdpType.ANSWER;
|
||||
KurentoSession kSession = kParticipant.getSession();
|
||||
|
||||
kParticipant.createPublishingEndpoint(mediaOptions, null);
|
||||
|
||||
/*
|
||||
|
@ -395,7 +423,7 @@ public class KurentoSessionManager extends SessionManager {
|
|||
}
|
||||
}
|
||||
|
||||
sdpAnswer = kParticipant.publishToRoom(sdpType, kurentoOptions.sdpOffer, kurentoOptions.doLoopback, false);
|
||||
sdpAnswer = kParticipant.publishToRoom(kurentoOptions.sdpOffer, kurentoOptions.doLoopback, false);
|
||||
|
||||
if (sdpAnswer == null) {
|
||||
OpenViduException e = new OpenViduException(Code.MEDIA_SDP_ERROR_CODE,
|
||||
|
@ -502,16 +530,85 @@ public class KurentoSessionManager extends SessionManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(Participant participant, String senderName, String sdpOffer, Integer transactionId) {
|
||||
String sdpAnswer = null;
|
||||
public void prepareSubscription(Participant participant, String senderPublicId, boolean reconnect,
|
||||
Integer transactionId) {
|
||||
String sdpOffer = null;
|
||||
Session session = null;
|
||||
try {
|
||||
log.debug("Request [SUBSCRIBE] remoteParticipant={} sdpOffer={} ({})", senderName, sdpOffer,
|
||||
log.debug("Request [SUBSCRIBE] remoteParticipant={} sdpOffer={} ({})", senderPublicId, sdpOffer,
|
||||
participant.getParticipantPublicId());
|
||||
|
||||
KurentoParticipant kParticipant = (KurentoParticipant) participant;
|
||||
session = ((KurentoParticipant) participant).getSession();
|
||||
Participant senderParticipant = session.getParticipantByPublicId(senderPublicId);
|
||||
|
||||
if (senderParticipant == null) {
|
||||
log.warn(
|
||||
"PARTICIPANT {}: Requesting to recv media from user {} "
|
||||
+ "in session {} but user could not be found",
|
||||
participant.getParticipantPublicId(), senderPublicId, session.getSessionId());
|
||||
throw new OpenViduException(Code.USER_NOT_FOUND_ERROR_CODE,
|
||||
"User '" + senderPublicId + " not found in session '" + session.getSessionId() + "'");
|
||||
}
|
||||
if (!senderParticipant.isStreaming()) {
|
||||
log.warn(
|
||||
"PARTICIPANT {}: Requesting to recv media from user {} "
|
||||
+ "in session {} but user is not streaming media",
|
||||
participant.getParticipantPublicId(), senderPublicId, session.getSessionId());
|
||||
throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE,
|
||||
"User '" + senderPublicId + " not streaming media in session '" + session.getSessionId() + "'");
|
||||
}
|
||||
|
||||
if (reconnect) {
|
||||
kParticipant.cancelReceivingMedia(((KurentoParticipant) senderParticipant), null, true);
|
||||
}
|
||||
|
||||
sdpOffer = kParticipant.prepareReceiveMediaFrom(senderParticipant);
|
||||
if (sdpOffer == null) {
|
||||
throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, "Unable to generate SDP offer when subscribing '"
|
||||
+ participant.getParticipantPublicId() + "' to '" + senderPublicId + "'");
|
||||
}
|
||||
} catch (OpenViduException e) {
|
||||
log.error("PARTICIPANT {}: Error preparing subscription to {}", participant.getParticipantPublicId(),
|
||||
senderPublicId, e);
|
||||
sessionEventsHandler.onPrepareSubscription(participant, session, null, transactionId, e);
|
||||
}
|
||||
if (sdpOffer != null) {
|
||||
sessionEventsHandler.onPrepareSubscription(participant, session, sdpOffer, transactionId, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(Participant participant, String senderName, String sdpAnswer, Integer transactionId) {
|
||||
Session session = null;
|
||||
try {
|
||||
log.debug("Request [SUBSCRIBE] remoteParticipant={} sdpAnswer={} ({})", senderName, sdpAnswer,
|
||||
participant.getParticipantPublicId());
|
||||
|
||||
KurentoParticipant kParticipant = (KurentoParticipant) participant;
|
||||
session = ((KurentoParticipant) participant).getSession();
|
||||
Participant senderParticipant = session.getParticipantByPublicId(senderName);
|
||||
boolean isTranscodingAllowed = session.getSessionProperties().isTranscodingAllowed();
|
||||
VideoCodec forcedVideoCodec = session.getSessionProperties().forcedVideoCodec();
|
||||
|
||||
// Modify sdp if forced codec is defined
|
||||
if (forcedVideoCodec != VideoCodec.NONE) {
|
||||
try {
|
||||
log.debug("PARTICIPANT '{}' in Session '{}' SDP Answer before munging: \n {}",
|
||||
participant.getParticipantPublicId(), session.getSessionId(), sdpAnswer);
|
||||
sdpAnswer = this.sdpMunging.setCodecPreference(forcedVideoCodec, sdpAnswer);
|
||||
} catch (OpenViduException e) {
|
||||
String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '"
|
||||
+ participant.getParticipantPublicId() + "' subscribing in Session: '"
|
||||
+ session.getSessionId() + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpAnswer;
|
||||
|
||||
if(!isTranscodingAllowed) {
|
||||
throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage);
|
||||
}
|
||||
log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId()
|
||||
+ " subscribing in Session: '" + session.getSessionId() + "'. Transcoding will be allowed");
|
||||
}
|
||||
}
|
||||
|
||||
if (senderParticipant == null) {
|
||||
log.warn(
|
||||
|
@ -530,18 +627,11 @@ public class KurentoSessionManager extends SessionManager {
|
|||
"User '" + senderName + " not streaming media in session '" + session.getSessionId() + "'");
|
||||
}
|
||||
|
||||
sdpAnswer = kParticipant.receiveMediaFrom(senderParticipant, sdpOffer, false);
|
||||
if (sdpAnswer == null) {
|
||||
throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE,
|
||||
"Unable to generate SDP answer when subscribing '" + participant.getParticipantPublicId()
|
||||
+ "' to '" + senderName + "'");
|
||||
}
|
||||
kParticipant.receiveMediaFrom(senderParticipant, sdpAnswer, false);
|
||||
sessionEventsHandler.onSubscribe(participant, session, transactionId, null);
|
||||
} catch (OpenViduException e) {
|
||||
log.error("PARTICIPANT {}: Error subscribing to {}", participant.getParticipantPublicId(), senderName, e);
|
||||
sessionEventsHandler.onSubscribe(participant, session, null, transactionId, e);
|
||||
}
|
||||
if (sdpAnswer != null) {
|
||||
sessionEventsHandler.onSubscribe(participant, session, sdpAnswer, transactionId, null);
|
||||
sessionEventsHandler.onSubscribe(participant, session, transactionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1046,11 +1136,34 @@ public class KurentoSessionManager extends SessionManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void reconnectStream(Participant participant, String streamId, String sdpOffer, Integer transactionId) {
|
||||
public void reconnectStream(Participant participant, String streamId, String sdpString, Integer transactionId) {
|
||||
|
||||
KurentoParticipant kParticipant = (KurentoParticipant) participant;
|
||||
KurentoSession kSession = kParticipant.getSession();
|
||||
boolean isPublisher = streamId.equals(participant.getPublisherStreamId());
|
||||
boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed();
|
||||
VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec();
|
||||
|
||||
if (streamId.equals(participant.getPublisherStreamId())) {
|
||||
// Modify sdp if forced codec is defined
|
||||
if (forcedVideoCodec != VideoCodec.NONE) {
|
||||
try {
|
||||
log.debug("PARTICIPANT '{}' in Session '{}' reconnecting SDP before munging: \n {}",
|
||||
participant.getParticipantPublicId(), kSession.getSessionId(), sdpString);
|
||||
sdpString = sdpMunging.setCodecPreference(forcedVideoCodec, sdpString);
|
||||
} catch (OpenViduException e) {
|
||||
String errorMessage = "Error in reconnect and forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '"
|
||||
+ participant.getParticipantPublicId() + "' " + (isPublisher ? "publishing" : "subscribing")
|
||||
+ " in Session: '" + kSession.getSessionId() + "'\nException: "
|
||||
+ e.getMessage() + "\nSDP:\n" + sdpString;
|
||||
if(!isTranscodingAllowed) {
|
||||
throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage);
|
||||
}
|
||||
log.info("Codec: '" + forcedVideoCodec + "' is not supported for PARTICIPANT: '" + participant.getParticipantPublicId()
|
||||
+ "' " + (isPublisher ? "publishing" : "subscribing") + " in Session: '" + kSession.getSessionId() + "'. Transcoding will be allowed");
|
||||
}
|
||||
}
|
||||
|
||||
if (isPublisher) {
|
||||
|
||||
// Reconnect publisher
|
||||
final KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) kParticipant.getPublisher()
|
||||
|
@ -1067,8 +1180,7 @@ public class KurentoSessionManager extends SessionManager {
|
|||
// 3) Create a new PublisherEndpoint connecting it to the previous PassThrough
|
||||
kParticipant.resetPublisherEndpoint(kurentoOptions, passThru);
|
||||
kParticipant.createPublishingEndpoint(kurentoOptions, streamId);
|
||||
SdpType sdpType = kurentoOptions.isOffer ? SdpType.OFFER : SdpType.ANSWER;
|
||||
String sdpAnswer = kParticipant.publishToRoom(sdpType, sdpOffer, kurentoOptions.doLoopback, true);
|
||||
String sdpAnswer = kParticipant.publishToRoom(sdpString, kurentoOptions.doLoopback, true);
|
||||
|
||||
sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(),
|
||||
kParticipant.getPublisher().createdAt(), kSession.getSessionId(), kurentoOptions, sdpAnswer,
|
||||
|
@ -1080,13 +1192,8 @@ public class KurentoSessionManager extends SessionManager {
|
|||
String senderPrivateId = kSession.getParticipantPrivateIdFromStreamId(streamId);
|
||||
if (senderPrivateId != null) {
|
||||
KurentoParticipant sender = (KurentoParticipant) kSession.getParticipantByPrivateId(senderPrivateId);
|
||||
kParticipant.cancelReceivingMedia(sender, null, true);
|
||||
String sdpAnswer = kParticipant.receiveMediaFrom(sender, sdpOffer, true);
|
||||
if (sdpAnswer == null) {
|
||||
throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE,
|
||||
"Unable to generate SDP answer when reconnecting subscriber to '" + streamId + "'");
|
||||
}
|
||||
sessionEventsHandler.onSubscribe(participant, kSession, sdpAnswer, transactionId, null);
|
||||
kParticipant.receiveMediaFrom(sender, sdpString, true);
|
||||
sessionEventsHandler.onSubscribe(participant, kSession, transactionId, null);
|
||||
} else {
|
||||
throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE,
|
||||
"Stream '" + streamId + "' does not exist in Session '" + kSession.getSessionId() + "'");
|
||||
|
@ -1158,5 +1265,4 @@ public class KurentoSessionManager extends SessionManager {
|
|||
filter.removeEventListener(pub.removeListener(eventType));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -516,6 +516,24 @@ public abstract class MediaEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
protected String generateOffer() throws OpenViduException {
|
||||
if (this.isWeb()) {
|
||||
if (webEndpoint == null) {
|
||||
throw new OpenViduException(Code.MEDIA_WEBRTC_ENDPOINT_ERROR_CODE,
|
||||
"Can't generate offer when WebRtcEndpoint is null (ep: " + endpointName + ")");
|
||||
}
|
||||
return webEndpoint.generateOffer();
|
||||
} else if (this.isPlayerEndpoint()) {
|
||||
return "";
|
||||
} else {
|
||||
if (endpoint == null) {
|
||||
throw new OpenViduException(Code.MEDIA_RTP_ENDPOINT_ERROR_CODE,
|
||||
"Can't generate offer when RtpEndpoint is null (ep: " + endpointName + ")");
|
||||
}
|
||||
return endpoint.generateOffer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If supported, it registers a listener for when a new {@link IceCandidate} is
|
||||
* gathered by the internal endpoint ({@link WebRtcEndpoint}) and sends it to
|
||||
|
|
|
@ -175,57 +175,42 @@ public class PublisherEndpoint extends MediaEndpoint {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
* offer. 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)
|
||||
* @param sdpOffer SDP offer from the remote peer
|
||||
* @param doLoopback loopback flag
|
||||
* @return the SDP answer
|
||||
*/
|
||||
public synchronized String publish(SdpType sdpType, String sdpString, boolean doLoopback) {
|
||||
public synchronized String publish(String sdpOffer, boolean doLoopback) {
|
||||
String sdpResponse = processOffer(sdpOffer);
|
||||
registerOnIceCandidateEventListener(this.getOwner().getParticipantPublicId());
|
||||
if (doLoopback) {
|
||||
connect(this.getEndpoint());
|
||||
connect(this.getEndpoint(), false);
|
||||
} else {
|
||||
innerConnect();
|
||||
innerConnect(false);
|
||||
}
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
String sdpResponse = null;
|
||||
switch (sdpType) {
|
||||
case ANSWER:
|
||||
sdpResponse = processAnswer(sdpString);
|
||||
break;
|
||||
case OFFER:
|
||||
sdpResponse = processOffer(sdpString);
|
||||
break;
|
||||
default:
|
||||
throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, "Sdp type not supported: " + sdpType);
|
||||
}
|
||||
gatherCandidates();
|
||||
return sdpResponse;
|
||||
}
|
||||
|
||||
public synchronized void connect(MediaElement sink) {
|
||||
public synchronized void connect(MediaElement sink, boolean blocking) {
|
||||
if (!connected) {
|
||||
innerConnect();
|
||||
innerConnect(blocking);
|
||||
}
|
||||
internalSinkConnect(passThru, sink);
|
||||
internalSinkConnect(passThru, sink, blocking);
|
||||
this.enableIpCameraIfNecessary();
|
||||
}
|
||||
|
||||
public synchronized void connect(MediaElement sink, MediaType type) {
|
||||
public synchronized void connect(MediaElement sink, MediaType type, boolean blocking) {
|
||||
if (!connected) {
|
||||
innerConnect();
|
||||
innerConnect(blocking);
|
||||
}
|
||||
internalSinkConnect(passThru, sink, type);
|
||||
internalSinkConnect(passThru, sink, type, blocking);
|
||||
this.enableIpCameraIfNecessary();
|
||||
}
|
||||
|
||||
|
@ -289,11 +274,11 @@ public class PublisherEndpoint extends MediaEndpoint {
|
|||
}
|
||||
if (connected) {
|
||||
if (first != null) {
|
||||
internalSinkConnect(first, shaper, type);
|
||||
internalSinkConnect(first, shaper, type, false);
|
||||
} else {
|
||||
internalSinkConnect(this.getEndpoint(), shaper, type);
|
||||
internalSinkConnect(this.getEndpoint(), shaper, type, false);
|
||||
}
|
||||
internalSinkConnect(shaper, passThru, type);
|
||||
internalSinkConnect(shaper, passThru, type, false);
|
||||
}
|
||||
elementIds.addFirst(id);
|
||||
elements.put(id, shaper);
|
||||
|
@ -343,7 +328,7 @@ public class PublisherEndpoint extends MediaEndpoint {
|
|||
} else {
|
||||
prev = passThru;
|
||||
}
|
||||
internalSinkConnect(next, prev);
|
||||
internalSinkConnect(next, prev, false);
|
||||
}
|
||||
elementIds.remove(elementId);
|
||||
if (releaseElement) {
|
||||
|
@ -408,13 +393,13 @@ public class PublisherEndpoint extends MediaEndpoint {
|
|||
}
|
||||
switch (muteType) {
|
||||
case ALL:
|
||||
internalSinkConnect(this.getEndpoint(), sink);
|
||||
internalSinkConnect(this.getEndpoint(), sink, false);
|
||||
break;
|
||||
case AUDIO:
|
||||
internalSinkConnect(this.getEndpoint(), sink, MediaType.AUDIO);
|
||||
internalSinkConnect(this.getEndpoint(), sink, MediaType.AUDIO, false);
|
||||
break;
|
||||
case VIDEO:
|
||||
internalSinkConnect(this.getEndpoint(), sink, MediaType.VIDEO);
|
||||
internalSinkConnect(this.getEndpoint(), sink, MediaType.VIDEO, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -440,7 +425,7 @@ public class PublisherEndpoint extends MediaEndpoint {
|
|||
return elementIds.get(idx - 1);
|
||||
}
|
||||
|
||||
private void innerConnect() {
|
||||
private void innerConnect(boolean blocking) {
|
||||
if (this.getEndpoint() == null) {
|
||||
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
|
||||
"Can't connect null endpoint (ep: " + getEndpointName() + ")");
|
||||
|
@ -453,28 +438,32 @@ public class PublisherEndpoint extends MediaEndpoint {
|
|||
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
|
||||
"No media element with id " + prevId + " (ep: " + getEndpointName() + ")");
|
||||
}
|
||||
internalSinkConnect(current, prev);
|
||||
internalSinkConnect(current, prev, blocking);
|
||||
current = prev;
|
||||
prevId = getPrevious(prevId);
|
||||
}
|
||||
internalSinkConnect(current, passThru);
|
||||
internalSinkConnect(current, passThru, blocking);
|
||||
connected = true;
|
||||
}
|
||||
|
||||
private void internalSinkConnect(final MediaElement source, final MediaElement sink) {
|
||||
source.connect(sink, new Continuation<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) throws Exception {
|
||||
log.debug("EP {}: Elements have been connected (source {} -> sink {})", getEndpointName(),
|
||||
source.getId(), sink.getId());
|
||||
}
|
||||
private void internalSinkConnect(final MediaElement source, final MediaElement sink, boolean blocking) {
|
||||
if (blocking) {
|
||||
source.connect(sink);
|
||||
} else {
|
||||
source.connect(sink, new Continuation<Void>() {
|
||||
@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);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onError(Throwable cause) throws Exception {
|
||||
log.warn("EP {}: Failed to connect media elements (source {} -> sink {})", getEndpointName(),
|
||||
source.getId(), sink.getId(), cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -488,23 +477,28 @@ public class PublisherEndpoint extends MediaEndpoint {
|
|||
* be used instead
|
||||
* @see #internalSinkConnect(MediaElement, MediaElement)
|
||||
*/
|
||||
private void internalSinkConnect(final MediaElement source, final MediaElement sink, final MediaType type) {
|
||||
private void internalSinkConnect(final MediaElement source, final MediaElement sink, final MediaType type,
|
||||
boolean blocking) {
|
||||
if (type == null) {
|
||||
internalSinkConnect(source, sink);
|
||||
internalSinkConnect(source, sink, blocking);
|
||||
} else {
|
||||
source.connect(sink, type, new Continuation<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) throws Exception {
|
||||
log.debug("EP {}: {} media elements have been connected (source {} -> sink {})", getEndpointName(),
|
||||
type, source.getId(), sink.getId());
|
||||
}
|
||||
if (blocking) {
|
||||
source.connect(sink, type);
|
||||
} else {
|
||||
source.connect(sink, type, new Continuation<Void>() {
|
||||
@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);
|
||||
}
|
||||
});
|
||||
@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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
package io.openvidu.server.kurento.endpoint;
|
||||
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.kurento.client.MediaPipeline;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -38,8 +37,6 @@ import io.openvidu.server.kurento.core.KurentoParticipant;
|
|||
public class SubscriberEndpoint extends MediaEndpoint {
|
||||
private final static Logger log = LoggerFactory.getLogger(SubscriberEndpoint.class);
|
||||
|
||||
private AtomicBoolean connectedToPublisher = new AtomicBoolean(false);
|
||||
|
||||
private String publisherStreamId;
|
||||
|
||||
public SubscriberEndpoint(EndpointType endpointType, KurentoParticipant owner, String endpointName,
|
||||
|
@ -47,23 +44,18 @@ public class SubscriberEndpoint extends MediaEndpoint {
|
|||
super(endpointType, owner, endpointName, pipeline, openviduConfig, log);
|
||||
}
|
||||
|
||||
public synchronized String subscribe(String sdpOffer, PublisherEndpoint publisher) {
|
||||
public synchronized String prepareSubscription(PublisherEndpoint publisher) {
|
||||
registerOnIceCandidateEventListener(publisher.getOwner().getParticipantPublicId());
|
||||
publisher.connect(this.getEndpoint(), true);
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
String sdpAnswer = processOffer(sdpOffer);
|
||||
gatherCandidates();
|
||||
publisher.connect(this.getEndpoint());
|
||||
setConnectedToPublisher(true);
|
||||
this.publisherStreamId = publisher.getStreamId();
|
||||
return sdpAnswer;
|
||||
String sdpOffer = generateOffer();
|
||||
gatherCandidates();
|
||||
return sdpOffer;
|
||||
}
|
||||
|
||||
public boolean isConnectedToPublisher() {
|
||||
return connectedToPublisher.get();
|
||||
}
|
||||
|
||||
public void setConnectedToPublisher(boolean connectedToPublisher) {
|
||||
this.connectedToPublisher.set(connectedToPublisher);
|
||||
public synchronized void subscribe(String sdpAnswer, PublisherEndpoint publisher) {
|
||||
processAnswer(sdpAnswer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -111,7 +111,7 @@ public class CompositeWrapper {
|
|||
|
||||
public void connectPublisherEndpoint(PublisherEndpoint endpoint) throws OpenViduException {
|
||||
HubPort hubPort = new HubPort.Builder(composite).build();
|
||||
endpoint.connect(hubPort);
|
||||
endpoint.connect(hubPort, false);
|
||||
String streamId = endpoint.getOwner().getPublisherStreamId();
|
||||
this.hubPorts.put(streamId, hubPort);
|
||||
this.publisherEndpoints.put(streamId, endpoint);
|
||||
|
|
|
@ -390,14 +390,14 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
MediaProfileSpecType profile) {
|
||||
switch (profile) {
|
||||
case WEBM:
|
||||
publisherEndpoint.connect(recorder, MediaType.AUDIO);
|
||||
publisherEndpoint.connect(recorder, MediaType.VIDEO);
|
||||
publisherEndpoint.connect(recorder, MediaType.AUDIO, false);
|
||||
publisherEndpoint.connect(recorder, MediaType.VIDEO, false);
|
||||
break;
|
||||
case WEBM_AUDIO_ONLY:
|
||||
publisherEndpoint.connect(recorder, MediaType.AUDIO);
|
||||
publisherEndpoint.connect(recorder, MediaType.AUDIO, false);
|
||||
break;
|
||||
case WEBM_VIDEO_ONLY:
|
||||
publisherEndpoint.connect(recorder, MediaType.VIDEO);
|
||||
publisherEndpoint.connect(recorder, MediaType.VIDEO, false);
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unsupported profile when single stream recording: " + profile);
|
||||
|
|
|
@ -24,6 +24,12 @@ import java.util.Map;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -40,12 +46,6 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
|||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.internal.ProtocolElements;
|
||||
import io.openvidu.java.client.MediaMode;
|
||||
|
@ -55,6 +55,7 @@ import io.openvidu.java.client.RecordingLayout;
|
|||
import io.openvidu.java.client.RecordingMode;
|
||||
import io.openvidu.java.client.RecordingProperties;
|
||||
import io.openvidu.java.client.SessionProperties;
|
||||
import io.openvidu.java.client.VideoCodec;
|
||||
import io.openvidu.server.config.OpenviduConfig;
|
||||
import io.openvidu.server.core.EndReason;
|
||||
import io.openvidu.server.core.IdentifierPrefixes;
|
||||
|
@ -102,6 +103,8 @@ public class SessionRestController {
|
|||
String defaultOutputModeString;
|
||||
String defaultRecordingLayoutString;
|
||||
String defaultCustomLayout;
|
||||
String forcedVideoCodec;
|
||||
Boolean allowTranscoding;
|
||||
try {
|
||||
mediaModeString = (String) params.get("mediaMode");
|
||||
recordingModeString = (String) params.get("recordingMode");
|
||||
|
@ -109,6 +112,8 @@ public class SessionRestController {
|
|||
defaultRecordingLayoutString = (String) params.get("defaultRecordingLayout");
|
||||
defaultCustomLayout = (String) params.get("defaultCustomLayout");
|
||||
customSessionId = (String) params.get("customSessionId");
|
||||
forcedVideoCodec = (String) params.get("forcedVideoCodec");
|
||||
allowTranscoding = (Boolean) params.get("allowTranscoding");
|
||||
} catch (ClassCastException e) {
|
||||
return this.generateErrorResponse("Type error in some parameter", "/api/sessions",
|
||||
HttpStatus.BAD_REQUEST);
|
||||
|
@ -150,6 +155,16 @@ public class SessionRestController {
|
|||
builder = builder.customSessionId(customSessionId);
|
||||
}
|
||||
builder = builder.defaultCustomLayout((defaultCustomLayout != null) ? defaultCustomLayout : "");
|
||||
if (forcedVideoCodec != null) {
|
||||
builder = builder.forcedVideoCodec(VideoCodec.valueOf(forcedVideoCodec));
|
||||
} else {
|
||||
builder = builder.forcedVideoCodec(openviduConfig.getOpenviduForcedCodec());
|
||||
}
|
||||
if (allowTranscoding != null) {
|
||||
builder = builder.allowTranscoding(allowTranscoding);
|
||||
} else {
|
||||
builder = builder.allowTranscoding(openviduConfig.isOpenviduAllowingTranscoding());
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
return this.generateErrorResponse("RecordingMode " + params.get("recordingMode") + " | "
|
||||
|
|
|
@ -26,6 +26,11 @@ import java.util.concurrent.ConcurrentMap;
|
|||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.kurento.jsonrpc.DefaultJsonRpcHandler;
|
||||
import org.kurento.jsonrpc.Session;
|
||||
|
@ -37,11 +42,6 @@ import org.slf4j.LoggerFactory;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
import io.openvidu.client.internal.ProtocolElements;
|
||||
|
@ -126,6 +126,9 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
|
|||
case ProtocolElements.ONICECANDIDATE_METHOD:
|
||||
onIceCandidate(rpcConnection, request);
|
||||
break;
|
||||
case ProtocolElements.PREPARERECEIVEVIDEO_METHOD:
|
||||
prepareReceiveVideoFrom(rpcConnection, request);
|
||||
break;
|
||||
case ProtocolElements.RECEIVEVIDEO_METHOD:
|
||||
receiveVideoFrom(rpcConnection, request);
|
||||
break;
|
||||
|
@ -333,31 +336,35 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
|
|||
}
|
||||
}
|
||||
|
||||
private void receiveVideoFrom(RpcConnection rpcConnection, Request<JsonObject> request) {
|
||||
private void prepareReceiveVideoFrom(RpcConnection rpcConnection, Request<JsonObject> request) {
|
||||
Participant participant;
|
||||
try {
|
||||
participant = sanityCheckOfSession(rpcConnection, "subscribe");
|
||||
participant = sanityCheckOfSession(rpcConnection, "prepareReceiveVideFrom");
|
||||
} catch (OpenViduException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
String senderPublicId = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM);
|
||||
String senderStreamId = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM);
|
||||
String senderPublicId = parseSenderPublicIdFromStreamId(senderStreamId);
|
||||
boolean reconnect = getBooleanParam(request, ProtocolElements.PREPARERECEIVEVIDEO_RECONNECT_PARAM);
|
||||
|
||||
// Parse sender public id from stream id
|
||||
if (senderPublicId.startsWith(IdentifierPrefixes.STREAM_ID + "IPC_")
|
||||
&& senderPublicId.contains(IdentifierPrefixes.IPCAM_ID)) {
|
||||
// If IPCAM
|
||||
senderPublicId = senderPublicId.substring(senderPublicId.indexOf("_" + IdentifierPrefixes.IPCAM_ID) + 1,
|
||||
senderPublicId.length());
|
||||
} else {
|
||||
// Not IPCAM
|
||||
senderPublicId = senderPublicId.substring(
|
||||
senderPublicId.lastIndexOf(IdentifierPrefixes.PARTICIPANT_PUBLIC_ID), senderPublicId.length());
|
||||
sessionManager.prepareSubscription(participant, senderPublicId, reconnect, request.getId());
|
||||
}
|
||||
|
||||
private void receiveVideoFrom(RpcConnection rpcConnection, Request<JsonObject> request) {
|
||||
Participant participant;
|
||||
try {
|
||||
participant = sanityCheckOfSession(rpcConnection, "receiveVideoFrom");
|
||||
} catch (OpenViduException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sdpOffer = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SDPOFFER_PARAM);
|
||||
String senderStreamId = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM);
|
||||
String sdpAnswer = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SDPANSWER_PARAM);
|
||||
|
||||
sessionManager.subscribe(participant, senderPublicId, sdpOffer, request.getId());
|
||||
String senderPublicId = parseSenderPublicIdFromStreamId(senderStreamId);
|
||||
|
||||
sessionManager.subscribe(participant, senderPublicId, sdpAnswer, request.getId());
|
||||
}
|
||||
|
||||
private void unsubscribeFromVideo(RpcConnection rpcConnection, Request<JsonObject> request) {
|
||||
|
@ -622,9 +629,9 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
|
|||
return;
|
||||
}
|
||||
String streamId = getStringParam(request, ProtocolElements.RECONNECTSTREAM_STREAM_PARAM);
|
||||
String sdpOffer = getStringParam(request, ProtocolElements.RECONNECTSTREAM_SDPOFFER_PARAM);
|
||||
String sdpString = getStringParam(request, ProtocolElements.RECONNECTSTREAM_SDPSTRING_PARAM);
|
||||
try {
|
||||
sessionManager.reconnectStream(participant, streamId, sdpOffer, request.getId());
|
||||
sessionManager.reconnectStream(participant, streamId, sdpString, request.getId());
|
||||
} catch (OpenViduException e) {
|
||||
this.notificationService.sendErrorResponse(participant.getParticipantPrivateId(), request.getId(),
|
||||
new JsonObject(), e);
|
||||
|
@ -800,4 +807,20 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
|
|||
.equals(this.sessionManager.getParticipantPrivateIdFromStreamId(sessionId, streamId));
|
||||
}
|
||||
|
||||
private String parseSenderPublicIdFromStreamId(String streamId) {
|
||||
String senderPublicId;
|
||||
// Parse sender public id from stream id
|
||||
if (streamId.startsWith(IdentifierPrefixes.STREAM_ID + "IPC_")
|
||||
&& streamId.contains(IdentifierPrefixes.IPCAM_ID)) {
|
||||
// If IPCAM
|
||||
senderPublicId = streamId.substring(streamId.indexOf("_" + IdentifierPrefixes.IPCAM_ID) + 1,
|
||||
streamId.length());
|
||||
} else {
|
||||
// Not IPCAM
|
||||
senderPublicId = streamId.substring(streamId.lastIndexOf(IdentifierPrefixes.PARTICIPANT_PUBLIC_ID),
|
||||
streamId.length());
|
||||
}
|
||||
return senderPublicId;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
package io.openvidu.server.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
import io.openvidu.java.client.VideoCodec;
|
||||
|
||||
public class SDPMunging {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SDPMunging.class);
|
||||
|
||||
/**
|
||||
* `codec` is a uppercase SDP-style codec name: "VP8", "H264".
|
||||
*
|
||||
* This looks for all video m-sections (lines starting with "m=video"),
|
||||
* then searches all of its related PayloadTypes trying to find those which
|
||||
* correspond to the preferred codec. If any is found, they are moved to the
|
||||
* front of the PayloadTypes list in the m= line, without removing the other
|
||||
* codecs that might be present.
|
||||
*
|
||||
* If our preferred codec is not found, the m= line is left without changes.
|
||||
*
|
||||
* This works based on the basis that RFC 3264 "Offer/Answer Model SDP" section
|
||||
* 6.1 "Unicast Streams" allows the answerer to list media formats in a
|
||||
* different order of preference from what it got in the offer:
|
||||
*
|
||||
* > Although the answerer MAY list the formats in their desired order of
|
||||
* > preference, it is RECOMMENDED that unless there is a specific reason,
|
||||
* > the answerer list formats in the same relative order they were
|
||||
* > present in the offer.
|
||||
*
|
||||
* Here we have a specific reason, thus we use this allowance to change the
|
||||
* ordering of formats. Browsers (tested with Chrome 84) honor this change and
|
||||
* use the first codec provided in the answer, so this operation actually works.
|
||||
*/
|
||||
public String setCodecPreference(VideoCodec codec, String sdp) throws OpenViduException {
|
||||
String codecStr = codec.name();
|
||||
log.info("[setCodecPreference] codec: {}", codecStr);
|
||||
|
||||
List<String> codecPts = new ArrayList<String>();
|
||||
String[] lines = sdp.split("\\R+");
|
||||
Pattern ptRegex = Pattern.compile(String.format("a=rtpmap:(\\d+) %s/90000", codecStr));
|
||||
|
||||
for (int sl = 0; sl < lines.length; sl++) {
|
||||
String sdpLine = lines[sl];
|
||||
|
||||
if (!sdpLine.startsWith("m=video")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// m-section found. Prepare an array to store PayloadTypes.
|
||||
codecPts.clear();
|
||||
|
||||
// Search the m-section to find our codec's PayloadType, if any.
|
||||
for (int ml = sl + 1; ml < lines.length; ml++) {
|
||||
String mediaLine = lines[ml];
|
||||
|
||||
// Abort if we reach the next m-section.
|
||||
if (mediaLine.startsWith("m=")) {
|
||||
break;
|
||||
}
|
||||
|
||||
Matcher ptMatch = ptRegex.matcher(mediaLine);
|
||||
if (ptMatch.find()) {
|
||||
// PayloadType found.
|
||||
String pt = ptMatch.group(1);
|
||||
codecPts.add(pt);
|
||||
|
||||
// Search the m-section to find the APT subtype, if any.
|
||||
Pattern aptRegex = Pattern.compile(String.format("a=fmtp:(\\d+) apt=%s", pt));
|
||||
|
||||
for (int al = sl + 1; al < lines.length; al++) {
|
||||
String aptLine = lines[al];
|
||||
|
||||
// Abort if we reach the next m-section.
|
||||
if (aptLine.startsWith("m=")) {
|
||||
break;
|
||||
}
|
||||
|
||||
Matcher aptMatch = aptRegex.matcher(aptLine);
|
||||
if (aptMatch.find()) {
|
||||
// APT found.
|
||||
String apt = aptMatch.group(1);
|
||||
codecPts.add(apt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (codecPts.isEmpty()) {
|
||||
throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, "The specified forced codec " + codecStr + " is not present in the SDP");
|
||||
}
|
||||
|
||||
// Build a new m= line where any PayloadTypes found have been moved
|
||||
// to the front of the PT list.
|
||||
StringBuilder newLine = new StringBuilder(sdpLine.length());
|
||||
List<String> lineParts = new ArrayList<String>(Arrays.asList(sdpLine.split(" ")));
|
||||
|
||||
if (lineParts.size() < 4) {
|
||||
log.error("[setCodecPreference] BUG in m= line: Expects at least 4 fields: '{}'", sdpLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add "m=video", Port, and Protocol.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
newLine.append(lineParts.remove(0) + " ");
|
||||
}
|
||||
|
||||
// Add the PayloadTypes that correspond to our preferred codec.
|
||||
for (String pt : codecPts) {
|
||||
lineParts.remove(pt);
|
||||
newLine.append(pt + " ");
|
||||
}
|
||||
|
||||
// Add the rest of PayloadTypes.
|
||||
newLine.append(String.join(" ", lineParts));
|
||||
|
||||
// Replace the original m= line with the one we just built.
|
||||
lines[sl] = newLine.toString();
|
||||
}
|
||||
|
||||
return String.join("\r\n", lines);
|
||||
}
|
||||
|
||||
}
|
|
@ -42,6 +42,9 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300
|
|||
OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900
|
||||
OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600
|
||||
|
||||
OPENVIDU_FORCED_CODEC=VP8
|
||||
OPENVIDU_ALLOW_TRANSCODING=false
|
||||
|
||||
COTURN_REDIS_IP=127.0.0.1
|
||||
COTURN_REDIS_DBNAME=0
|
||||
COTURN_REDIS_PASSWORD=turn
|
||||
|
|
|
@ -41,3 +41,7 @@ mat-radio-button:first-child {
|
|||
#record-div {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
#allow-transcoding-div {
|
||||
margin-bottom: 10px;
|
||||
}
|
|
@ -38,6 +38,18 @@
|
|||
<input matInput placeholder="DefaultCustomLayout" type="text"
|
||||
[(ngModel)]="sessionProperties.defaultCustomLayout">
|
||||
</mat-form-field>
|
||||
<div id="allow-transcoding-div">
|
||||
<mat-checkbox class="checkbox-form" [(ngModel)]="sessionProperties.allowTranscoding"
|
||||
id="allow-transcoding-checkbox">Allow Transcoding</mat-checkbox>
|
||||
</div>
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="ForcedVideoCodec" [(ngModel)]="sessionProperties.forcedVideoCodec"
|
||||
id="forced-video-codec-select">
|
||||
<mat-option *ngFor="let enumerator of enumToArray(forceVideoCodec)" [value]="enumerator">
|
||||
<span [attr.id]="'option-' + enumerator">{{ enumerator }}</span>
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="CustomSessionId" type="text" [(ngModel)]="sessionProperties.customSessionId">
|
||||
</mat-form-field>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
|
||||
import { SessionProperties, MediaMode, Recording, RecordingMode, RecordingLayout, TokenOptions } from 'openvidu-node-client';
|
||||
import { SessionProperties, MediaMode, Recording, RecordingMode, RecordingLayout, TokenOptions, VideoCodec } from 'openvidu-node-client';
|
||||
|
||||
@Component({
|
||||
selector: 'app-session-properties-dialog',
|
||||
|
@ -24,6 +24,7 @@ export class SessionPropertiesDialogComponent {
|
|||
recordingMode = RecordingMode;
|
||||
defaultOutputMode = Recording.OutputMode;
|
||||
defaultRecordingLayout = RecordingLayout;
|
||||
forceVideoCodec = VideoCodec;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<SessionPropertiesDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data) {
|
||||
|
|
|
@ -18,7 +18,8 @@ import {
|
|||
TokenOptions,
|
||||
OpenViduRole,
|
||||
RecordingProperties,
|
||||
Recording
|
||||
Recording,
|
||||
VideoCodec
|
||||
} from 'openvidu-node-client';
|
||||
import { MatDialog, MAT_CHECKBOX_CLICK_ACTION } from '@angular/material';
|
||||
import { ExtensionDialogComponent } from '../dialogs/extension-dialog/extension-dialog.component';
|
||||
|
@ -92,7 +93,9 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
|
|||
defaultOutputMode: Recording.OutputMode.COMPOSED,
|
||||
defaultRecordingLayout: RecordingLayout.BEST_FIT,
|
||||
defaultCustomLayout: '',
|
||||
customSessionId: ''
|
||||
customSessionId: '',
|
||||
forcedVideoCodec: VideoCodec.VP8,
|
||||
allowTranscoding: false
|
||||
};
|
||||
|
||||
publisherProperties: PublisherProperties = {
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
<button class="video-btn events-btn bottom-left-rounded" title="Publisher events" (click)="openPublisherEventsDialog()">
|
||||
<mat-icon aria-label="Publisher events" class="mat-icon material-icons" role="img" aria-hidden="true">notifications</mat-icon>
|
||||
</button>
|
||||
<button class="video-btn events-btn bottom-left-rounded" title="Peer Connection Stats" (click)="showStats()">
|
||||
<mat-icon aria-label="Peer Connection Stats" class="mat-icon material-icons" role="img" aria-hidden="true">info</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bottom-div">
|
||||
<button class="video-btn pub-btn" title="Publish/Unpublish" (click)="pubUnpub()">
|
||||
|
|
|
@ -727,4 +727,11 @@ export class VideoComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
async showStats() {
|
||||
let stats = await this.streamManager.stream.getWebRtcPeer().pc.getStats(null);
|
||||
stats.forEach(report => {
|
||||
console.log(report);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue