Merge branch 'master' into fix-generateOffer

pull/577/head
Juan Navarro 2021-06-11 11:46:33 +02:00
commit 36f3d305d3
67 changed files with 1229 additions and 5118 deletions

View File

@ -10,20 +10,20 @@
}, },
"description": "OpenVidu Browser", "description": "OpenVidu Browser",
"devDependencies": { "devDependencies": {
"@types/node": "14.14.32", "@types/node": "15.12.2",
"@types/platform": "1.3.3", "@types/platform": "1.3.3",
"browserify": "17.0.0", "browserify": "17.0.0",
"grunt": "1.3.0", "grunt": "1.4.1",
"grunt-cli": "1.3.2", "grunt-cli": "1.4.3",
"grunt-contrib-copy": "1.0.0", "grunt-contrib-copy": "1.0.0",
"grunt-contrib-sass": "2.0.0", "grunt-contrib-sass": "2.0.0",
"grunt-contrib-uglify": "5.0.0", "grunt-contrib-uglify": "5.0.1",
"grunt-contrib-watch": "1.1.0", "grunt-contrib-watch": "1.1.0",
"grunt-postcss": "0.9.0", "grunt-postcss": "0.9.0",
"grunt-string-replace": "1.3.1", "grunt-string-replace": "1.3.1",
"grunt-ts": "6.0.0-beta.22", "grunt-ts": "6.0.0-beta.22",
"terser": "5.6.0", "terser": "5.7.0",
"tsify": "5.0.2", "tsify": "5.0.4",
"tslint": "6.1.3", "tslint": "6.1.3",
"typedoc": "0.19.2", "typedoc": "0.19.2",
"typescript": "4.0.7" "typescript": "4.0.7"
@ -38,9 +38,9 @@
"scripts": { "scripts": {
"browserify": "VERSION=${VERSION:-dev}; mkdir -p static/js/ && cd src && ../node_modules/browserify/bin/cmd.js Main.ts -p [ tsify ] --exclude kurento-browser-extensions --debug -o ../static/js/openvidu-browser-$VERSION.js -v", "browserify": "VERSION=${VERSION:-dev}; mkdir -p static/js/ && cd src && ../node_modules/browserify/bin/cmd.js Main.ts -p [ tsify ] --exclude kurento-browser-extensions --debug -o ../static/js/openvidu-browser-$VERSION.js -v",
"browserify-prod": "VERSION=${VERSION:-dev}; mkdir -p static/js/ && cd src && ../node_modules/browserify/bin/cmd.js --debug Main.ts -p [ tsify ] --exclude kurento-browser-extensions | ../node_modules/terser/bin/terser --source-map content=inline --output ../static/js/openvidu-browser-$VERSION.min.js", "browserify-prod": "VERSION=${VERSION:-dev}; mkdir -p static/js/ && cd src && ../node_modules/browserify/bin/cmd.js --debug Main.ts -p [ tsify ] --exclude kurento-browser-extensions | ../node_modules/terser/bin/terser --source-map content=inline --output ../static/js/openvidu-browser-$VERSION.min.js",
"build": "cd src/OpenVidu && ./../../node_modules/typescript/bin/tsc && cd ../.. && ./node_modules/typescript/bin/tsc --declaration src/index.ts --outDir ./lib --sourceMap --lib dom,es5,es2015.promise,scripthost", "build": "cd src/OpenVidu && ./../../node_modules/typescript/bin/tsc && cd ../.. && ./node_modules/typescript/bin/tsc --declaration src/index.ts --outDir ./lib --sourceMap --target es5 --lib dom,es5,es2015.promise,scripthost",
"docs": "./generate-docs.sh" "docs": "./generate-docs.sh"
}, },
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"version": "2.17.0" "version": "2.18.0"
} }

View File

@ -67,6 +67,7 @@ let platform: PlatformUtils;
export class OpenVidu { export class OpenVidu {
private jsonRpcClient: any; private jsonRpcClient: any;
private masterNodeHasCrashed = false;
/** /**
* @hidden * @hidden
@ -104,6 +105,10 @@ export class OpenVidu {
* @hidden * @hidden
*/ */
finalUserId: string; finalUserId: string;
/**
* @hidden
*/
mediaServer: string;
/** /**
* @hidden * @hidden
*/ */
@ -744,7 +749,8 @@ export class OpenVidu {
onconnected: onConnectSucces, onconnected: onConnectSucces,
ondisconnect: this.disconnectCallback.bind(this), ondisconnect: this.disconnectCallback.bind(this),
onreconnecting: this.reconnectingCallback.bind(this), onreconnecting: this.reconnectingCallback.bind(this),
onreconnected: this.reconnectedCallback.bind(this) onreconnected: this.reconnectedCallback.bind(this),
ismasternodecrashed: this.isMasterNodeCrashed.bind(this)
}, },
rpc: { rpc: {
requestTimeout: 10000, requestTimeout: 10000,
@ -761,12 +767,30 @@ export class OpenVidu {
networkQualityLevelChanged: this.session.onNetworkQualityLevelChangedChanged.bind(this.session), networkQualityLevelChanged: this.session.onNetworkQualityLevelChangedChanged.bind(this.session),
filterEventDispatched: this.session.onFilterEventDispatched.bind(this.session), filterEventDispatched: this.session.onFilterEventDispatched.bind(this.session),
iceCandidate: this.session.recvIceCandidate.bind(this.session), iceCandidate: this.session.recvIceCandidate.bind(this.session),
mediaError: this.session.onMediaError.bind(this.session) mediaError: this.session.onMediaError.bind(this.session),
masterNodeCrashedNotification: this.onMasterNodeCrashedNotification.bind(this)
} }
}; };
this.jsonRpcClient = new RpcBuilder.clients.JsonRpcClient(config); this.jsonRpcClient = new RpcBuilder.clients.JsonRpcClient(config);
} }
/**
* @hidden
*/
onMasterNodeCrashedNotification(response): void {
console.error('Master Node has crashed');
this.masterNodeHasCrashed = true;
this.session.onLostConnection("nodeCrashed");
this.jsonRpcClient.close(4103, "Master Node has crashed");
}
/**
* @hidden
*/
getWsReadyState(): number {
return this.jsonRpcClient.getReadyState();
}
/** /**
* @hidden * @hidden
*/ */
@ -1009,10 +1033,14 @@ export class OpenVidu {
if (!!this.session.connection) { if (!!this.session.connection) {
this.sendRequest('connect', { sessionId: this.session.connection.rpcSessionId }, (error, response) => { this.sendRequest('connect', { sessionId: this.session.connection.rpcSessionId }, (error, response) => {
if (!!error) { if (!!error) {
logger.error(error); if (this.isMasterNodeCrashed()) {
logger.warn('Websocket was able to reconnect to OpenVidu Server, but your Connection was already destroyed due to timeout. You are no longer a participant of the Session and your media streams have been destroyed'); logger.warn('Master Node has crashed!');
this.session.onLostConnection("networkDisconnect"); } else {
this.jsonRpcClient.close(4101, "Reconnection fault"); logger.error(error);
logger.warn('Websocket was able to reconnect to OpenVidu Server, but your Connection was already destroyed due to timeout. You are no longer a participant of the Session and your media streams have been destroyed');
this.session.onLostConnection("networkDisconnect");
this.jsonRpcClient.close(4101, "Reconnection fault");
}
} else { } else {
this.jsonRpcClient.resetPing(); this.jsonRpcClient.resetPing();
this.session.onRecoveredConnection(); this.session.onRecoveredConnection();
@ -1030,6 +1058,10 @@ export class OpenVidu {
} }
} }
private isMasterNodeCrashed() {
return this.masterNodeHasCrashed;
}
private isRoomAvailable(): boolean { private isRoomAvailable(): boolean {
if (this.session !== undefined && this.session instanceof Session) { if (this.session !== undefined && this.session instanceof Session) {
return true; return true;

View File

@ -86,10 +86,6 @@ export class Publisher extends StreamManager {
* @hidden * @hidden
*/ */
screenShareResizeInterval: NodeJS.Timer; screenShareResizeInterval: NodeJS.Timer;
/**
* @hidden
*/
IEAdapter: any;
/** /**
* @hidden * @hidden

View File

@ -197,7 +197,7 @@ export class Session extends EventDispatcher {
* #### Events dispatched * #### Events dispatched
* *
* The [[Session]] object of the local participant will dispatch a `sessionDisconnected` event. * The [[Session]] object of the local participant will dispatch a `sessionDisconnected` event.
* This event will automatically unsubscribe the leaving participant from every Subscriber object of the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) * This event will automatically unsubscribe the leaving participant from every Subscriber object of the session (this includes closing the RTCPeerConnection and disposing all MediaStreamTracks)
* and also deletes any HTML video element associated to each Subscriber (only those [created by OpenVidu Browser](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)). * and also deletes any HTML video element associated to each Subscriber (only those [created by OpenVidu Browser](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)).
* For every video removed, each Subscriber object will dispatch a `videoElementDestroyed` event. * For every video removed, each Subscriber object will dispatch a `videoElementDestroyed` event.
* Call `event.preventDefault()` upon event `sessionDisconnected` to avoid this behavior and take care of disposing and cleaning all the Subscriber objects yourself. * Call `event.preventDefault()` upon event `sessionDisconnected` to avoid this behavior and take care of disposing and cleaning all the Subscriber objects yourself.
@ -210,7 +210,7 @@ export class Session extends EventDispatcher {
* or/and `Session.disconnect()` in the previous session). See [[StreamEvent]] and [[VideoElementEvent]] to learn more. * or/and `Session.disconnect()` in the previous session). See [[StreamEvent]] and [[VideoElementEvent]] to learn more.
* *
* The [[Session]] object of every other participant connected to the session will dispatch a `streamDestroyed` event if the disconnected participant was publishing. * The [[Session]] object of every other participant connected to the session will dispatch a `streamDestroyed` event if the disconnected participant was publishing.
* This event will automatically unsubscribe the Subscriber object from the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) * This event will automatically unsubscribe the Subscriber object from the session (this includes closing the RTCPeerConnection and disposing all MediaStreamTracks)
* and also deletes any HTML video element associated to that Subscriber (only those [created by OpenVidu Browser](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)). * and also deletes any HTML video element associated to that Subscriber (only those [created by OpenVidu Browser](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)).
* For every video removed, the Subscriber object will dispatch a `videoElementDestroyed` event. * For every video removed, the Subscriber object will dispatch a `videoElementDestroyed` event.
* Call `event.preventDefault()` upon event `streamDestroyed` to avoid this default behavior and take care of disposing and cleaning the Subscriber object yourself. * Call `event.preventDefault()` upon event `streamDestroyed` to avoid this default behavior and take care of disposing and cleaning the Subscriber object yourself.
@ -437,7 +437,7 @@ export class Session extends EventDispatcher {
* Call `event.preventDefault()` upon event `streamDestroyed` if you want to clean the Publisher object on your own or re-publish it in a different Session. * Call `event.preventDefault()` upon event `streamDestroyed` if you want to clean the Publisher object on your own or re-publish it in a different Session.
* *
* The [[Session]] object of every other participant connected to the session will dispatch a `streamDestroyed` event. * The [[Session]] object of every other participant connected to the session will dispatch a `streamDestroyed` event.
* This event will automatically unsubscribe the Subscriber object from the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) and * This event will automatically unsubscribe the Subscriber object from the session (this includes closing the RTCPeerConnection and disposing all MediaStreamTracks) and
* delete any HTML video element associated to it (only those [created by OpenVidu Browser](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)). * delete any HTML video element associated to it (only those [created by OpenVidu Browser](/en/stable/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)).
* For every video removed, the Subscriber object will dispatch a `videoElementDestroyed` event. * For every video removed, the Subscriber object will dispatch a `videoElementDestroyed` event.
* Call `event.preventDefault()` upon event `streamDestroyed` to avoid this default behavior and take care of disposing and cleaning the Subscriber object on your own. * Call `event.preventDefault()` upon event `streamDestroyed` to avoid this default behavior and take care of disposing and cleaning the Subscriber object on your own.
@ -781,7 +781,7 @@ export class Session extends EventDispatcher {
onParticipantLeft(msg): void { onParticipantLeft(msg): void {
if (this.remoteConnections.size > 0) { if (this.remoteConnections.size > 0) {
this.getRemoteConnection(msg.connectionId).then(connection => { this.getRemoteConnection(msg.connectionId, 'onParticipantLeft').then(connection => {
if (!!connection.stream) { if (!!connection.stream) {
const stream = connection.stream; const stream = connection.stream;
@ -823,7 +823,7 @@ export class Session extends EventDispatcher {
// Get the existing Connection created on 'onParticipantJoined' for // Get the existing Connection created on 'onParticipantJoined' for
// existing participants or create a new one for new participants // existing participants or create a new one for new participants
let connection: Connection; let connection: Connection;
this.getRemoteConnection(response.id) this.getRemoteConnection(response.id, 'onParticipantPublished')
.then(con => { .then(con => {
// Update existing Connection // Update existing Connection
@ -848,7 +848,7 @@ export class Session extends EventDispatcher {
// Your stream has been forcedly unpublished from the session // Your stream has been forcedly unpublished from the session
this.stopPublisherStream(msg.reason); this.stopPublisherStream(msg.reason);
} else { } else {
this.getRemoteConnection(msg.connectionId) this.getRemoteConnection(msg.connectionId, 'onParticipantUnpublished')
.then(connection => { .then(connection => {
@ -965,7 +965,7 @@ export class Session extends EventDispatcher {
// Your stream has been forcedly changed (filter feature) // Your stream has been forcedly changed (filter feature)
callback(this.connection); callback(this.connection);
} else { } else {
this.getRemoteConnection(msg.connectionId) this.getRemoteConnection(msg.connectionId, 'onStreamPropertyChanged')
.then(connection => { .then(connection => {
callback(connection); callback(connection);
}) })
@ -1411,7 +1411,7 @@ export class Session extends EventDispatcher {
}); });
} }
private getRemoteConnection(connectionId: string): Promise<Connection> { private getRemoteConnection(connectionId: string, operation: string): Promise<Connection> {
return new Promise<Connection>((resolve, reject) => { return new Promise<Connection>((resolve, reject) => {
const connection = this.remoteConnections.get(connectionId); const connection = this.remoteConnections.get(connectionId);
if (!!connection) { if (!!connection) {
@ -1419,9 +1419,8 @@ export class Session extends EventDispatcher {
resolve(connection); resolve(connection);
} else { } else {
// Remote connection not found. Reject with OpenViduError // Remote connection not found. Reject with OpenViduError
const errorMessage = 'Remote connection ' + connectionId + " unknown when 'onParticipantLeft'. " + const errorMessage = 'Remote connection ' + connectionId + " unknown when '" + operation + "'. " +
'Existing remote connections: ' + JSON.stringify(this.remoteConnections.keys()); 'Existing remote connections: ' + JSON.stringify(this.remoteConnections.keys());
reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, errorMessage)); reject(new OpenViduError(OpenViduErrorName.GENERIC_ERROR, errorMessage));
} }
}); });
@ -1487,6 +1486,7 @@ export class Session extends EventDispatcher {
} }
this.openvidu.role = opts.role; this.openvidu.role = opts.role;
this.openvidu.finalUserId = opts.finalUserId; this.openvidu.finalUserId = opts.finalUserId;
this.openvidu.mediaServer = opts.mediaServer;
this.capabilities = { this.capabilities = {
subscribe: true, subscribe: true,
publish: this.openvidu.role !== 'SUBSCRIBER', publish: this.openvidu.role !== 'SUBSCRIBER',

View File

@ -214,6 +214,10 @@ export class Stream {
* @hidden * @hidden
*/ */
ee = new EventEmitter(); ee = new EventEmitter();
/**
* @hidden
*/
reconnectionEventEmitter: EventEmitter | undefined;
/** /**
@ -277,6 +281,19 @@ export class Stream {
} }
/**
* Recreates the media connection with the server. This entails the disposal of the previous RTCPeerConnection and the re-negotiation
* of a new one, that will apply the same properties.
*
* This method can be useful in those situations were there the media connection breaks and OpenVidu is not able to recover on its own
* for any kind of unanticipated reason (see [Automatic reconnection](/en/latest/advanced-features/automatic-reconnection/)).
*
* @returns A Promise (to which you can optionally subscribe to) that is resolved if the reconnection operation was successful and rejected with an Error object if not
*/
public reconnect(): Promise<void> {
return this.reconnectStream('API');
}
/** /**
* Applies an audio/video filter to the stream. * Applies an audio/video filter to the stream.
* *
@ -461,11 +478,13 @@ export class Stream {
* @hidden * @hidden
*/ */
disposeWebRtcPeer(): void { disposeWebRtcPeer(): void {
let webrtcId;
if (!!this.webRtcPeer) { if (!!this.webRtcPeer) {
this.webRtcPeer.dispose(); this.webRtcPeer.dispose();
this.stopWebRtcStats(); webrtcId = this.webRtcPeer.id;
} }
logger.info((!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + "WebRTCPeer from 'Stream' with id [" + this.streamId + '] is now closed'); this.stopWebRtcStats();
logger.info((!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + "RTCPeerConnection with id [" + webrtcId + "] from 'Stream' with id [" + this.streamId + '] is now closed');
} }
/** /**
@ -774,7 +793,7 @@ export class Stream {
return false; return false;
} }
if (this.isLocal() && !!this.session.openvidu.advancedConfiguration.forceMediaReconnectionAfterNetworkDrop) { if (this.isLocal() && !!this.session.openvidu.advancedConfiguration.forceMediaReconnectionAfterNetworkDrop) {
logger.warn('OpenVidu Browser advanced configuration option "forceMediaReconnectionAfterNetworkDrop" is enabled. Stream ' + this.streamId + ' will force a reconnection'); logger.warn(`OpenVidu Browser advanced configuration option "forceMediaReconnectionAfterNetworkDrop" is enabled. Stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) will force a reconnection`);
return true; return true;
} }
const iceConnectionState: RTCIceConnectionState = this.getRTCPeerConnection().iceConnectionState; const iceConnectionState: RTCIceConnectionState = this.getRTCPeerConnection().iceConnectionState;
@ -802,10 +821,42 @@ export class Stream {
initWebRtcPeerSend(reconnect: boolean): Promise<void> { initWebRtcPeerSend(reconnect: boolean): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!reconnect) { if (reconnect) {
if (this.reconnectionEventEmitter == undefined) {
// There is no ongoing reconnection
this.reconnectionEventEmitter = new EventEmitter();
} else {
// Ongoing reconnection
console.warn(`Trying to reconnect stream ${this.streamId} (Publisher) but an ongoing reconnection process is active. Waiting for response...`);
this.reconnectionEventEmitter.once('success', () => {
resolve();
});
this.reconnectionEventEmitter.once('error', error => {
reject(error);
});
return;
}
} else {
// MediaStream will already have hark events for reconnected streams
this.initHarkEvents(); // Init hark events for the local stream this.initHarkEvents(); // Init hark events for the local stream
} }
const finalResolve = () => {
if (reconnect) {
this.reconnectionEventEmitter?.emitEvent('success');
delete this.reconnectionEventEmitter;
}
resolve();
}
const finalReject = error => {
if (reconnect) {
this.reconnectionEventEmitter?.emitEvent('error', [error]);
delete this.reconnectionEventEmitter;
}
reject(error);
}
const successOfferCallback = (sdpOfferParam) => { const successOfferCallback = (sdpOfferParam) => {
logger.debug('Sending SDP offer to publish as ' logger.debug('Sending SDP offer to publish as '
+ this.streamId, sdpOfferParam); + this.streamId, sdpOfferParam);
@ -839,9 +890,9 @@ export class Stream {
this.session.openvidu.sendRequest(method, params, (error, response) => { this.session.openvidu.sendRequest(method, params, (error, response) => {
if (error) { if (error) {
if (error.code === 401) { if (error.code === 401) {
reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish")); finalReject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish"));
} else { } else {
reject('Error on publishVideo: ' + JSON.stringify(error)); finalReject('Error on publishVideo: ' + JSON.stringify(error));
} }
} else { } else {
this.webRtcPeer.processRemoteAnswer(response.sdpAnswer) this.webRtcPeer.processRemoteAnswer(response.sdpAnswer)
@ -861,10 +912,11 @@ export class Stream {
} }
this.initWebRtcStats(); this.initWebRtcStats();
logger.info("'Publisher' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "published") + " to session"); logger.info("'Publisher' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "published") + " to session");
resolve();
finalResolve();
}) })
.catch(error => { .catch(error => {
reject(error); finalReject(error);
}); });
} }
}); });
@ -876,8 +928,8 @@ export class Stream {
video: this.hasVideo, video: this.hasVideo,
}, },
simulcast: false, simulcast: false,
onicecandidate: this.connection.sendIceCandidate.bind(this.connection), onIceCandidate: this.connection.sendIceCandidate.bind(this.connection),
onexception: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) }, onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) },
iceServers: this.getIceServersConf(), iceServers: this.getIceServersConf(),
mediaStream: this.mediaStream, mediaStream: this.mediaStream,
}; };
@ -896,10 +948,10 @@ export class Stream {
.then(() => { .then(() => {
successOfferCallback(sdpOffer.sdp); successOfferCallback(sdpOffer.sdp);
}).catch(error => { }).catch(error => {
reject(new Error('(publish) SDP process local offer error: ' + JSON.stringify(error))); finalReject(new Error('(publish) SDP process local offer error: ' + JSON.stringify(error)));
}); });
}).catch(error => { }).catch(error => {
reject(new Error('(publish) SDP create offer error: ' + JSON.stringify(error))); finalReject(new Error('(publish) SDP create offer error: ' + JSON.stringify(error)));
}); });
}); });
} }
@ -909,20 +961,87 @@ export class Stream {
*/ */
initWebRtcPeerReceive(reconnect: boolean): Promise<void> { initWebRtcPeerReceive(reconnect: boolean): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (reconnect) {
if (this.reconnectionEventEmitter == undefined) {
// There is no ongoing reconnection
this.reconnectionEventEmitter = new EventEmitter();
} else {
// Ongoing reconnection
console.warn(`Trying to reconnect stream ${this.streamId} (Subscriber) but an ongoing reconnection process is active. Waiting for response...`);
this.reconnectionEventEmitter.once('success', () => {
resolve();
});
this.reconnectionEventEmitter.once('error', error => {
reject(error);
});
return;
}
}
const finalResolve = () => {
logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
this.remotePeerSuccessfullyEstablished(reconnect);
this.initWebRtcStats();
if (reconnect) {
this.reconnectionEventEmitter?.emitEvent('success');
delete this.reconnectionEventEmitter;
}
resolve();
}
const finalReject = error => {
if (reconnect) {
this.reconnectionEventEmitter?.emitEvent('error', [error]);
delete this.reconnectionEventEmitter;
}
reject(error);
}
if (this.session.openvidu.mediaServer === 'mediasoup') {
// Server initiates negotiation
this.initWebRtcPeerReceiveFromServer(reconnect)
.then(() => finalResolve())
.catch(error => finalReject(error));
} else {
// Client initiates negotiation
this.initWebRtcPeerReceiveFromClient(reconnect)
.then(() => finalResolve())
.catch(error => finalReject(error));
}
});
}
/**
* @hidden
*/
initWebRtcPeerReceiveFromClient(reconnect: boolean): Promise<void> {
return new Promise((resolve, reject) => {
this.completeWebRtcPeerReceive(reconnect).then(response => {
this.webRtcPeer.processRemoteAnswer(response.sdpAnswer)
.then(() => resolve()).catch(error => reject(error));
}).catch(error => reject(error));
});
}
/**
* @hidden
*/
initWebRtcPeerReceiveFromServer(reconnect: boolean): Promise<void> {
return new Promise((resolve, reject) => {
// Server initiates negotiation
this.session.openvidu.sendRequest('prepareReceiveVideoFrom', { sender: this.streamId, reconnect }, (error, response) => { this.session.openvidu.sendRequest('prepareReceiveVideoFrom', { sender: this.streamId, reconnect }, (error, response) => {
if (error) { if (error) {
reject(new Error('Error on prepareReceiveVideoFrom: ' + JSON.stringify(error))); reject(new Error('Error on prepareReceiveVideoFrom: ' + JSON.stringify(error)));
} else { } else {
this.completeWebRtcPeerReceive(response.sdpOffer, reconnect) this.completeWebRtcPeerReceive(reconnect, response.sdpOffer)
.then(() => { .then(() => resolve()).catch(error => reject(error));
logger.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
this.remotePeerSuccessfullyEstablished(reconnect);
this.initWebRtcStats();
resolve();
})
.catch(error => {
reject(error);
});
} }
}); });
}); });
@ -931,25 +1050,29 @@ export class Stream {
/** /**
* @hidden * @hidden
*/ */
completeWebRtcPeerReceive(sdpOffer: string, reconnect: boolean): Promise<void> { completeWebRtcPeerReceive(reconnect: boolean, sdpOfferByServer?: string): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
logger.debug("'Session.subscribe(Stream)' called"); logger.debug("'Session.subscribe(Stream)' called");
const successAnswerCallback = (sdpAnswer) => { const sendSdpToServer = (sdpString: string) => {
logger.debug('Sending SDP answer to subscribe to '
+ this.streamId, sdpAnswer); logger.debug(`Sending local SDP ${(!!sdpOfferByServer ? 'answer' : 'offer')} to subscribe to ${this.streamId}`, sdpString);
const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom'; const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom';
const params = {}; const params = {};
params[reconnect ? 'stream' : 'sender'] = this.streamId; params[reconnect ? 'stream' : 'sender'] = this.streamId;
params[reconnect ? 'sdpString' : 'sdpAnswer'] = sdpAnswer; if (!!sdpOfferByServer) {
params[reconnect ? 'sdpString' : 'sdpAnswer'] = sdpString;
} else {
params['sdpOffer'] = sdpString;
}
this.session.openvidu.sendRequest(method, params, (error, response) => { this.session.openvidu.sendRequest(method, params, (error, response) => {
if (error) { if (error) {
reject(new Error('Error on ' + method + ' : ' + JSON.stringify(error))); reject(new Error('Error on ' + method + ' : ' + JSON.stringify(error)));
} else { } else {
resolve(); resolve(response);
} }
}); });
}; };
@ -960,29 +1083,47 @@ export class Stream {
video: this.hasVideo, video: this.hasVideo,
}, },
simulcast: false, simulcast: false,
onicecandidate: this.connection.sendIceCandidate.bind(this.connection), onIceCandidate: this.connection.sendIceCandidate.bind(this.connection),
onexception: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) }, onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => { this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]) },
iceServers: this.getIceServersConf(), iceServers: this.getIceServersConf(),
}; };
if (reconnect) {
this.disposeWebRtcPeer();
}
this.webRtcPeer = new WebRtcPeerRecvonly(config); this.webRtcPeer = new WebRtcPeerRecvonly(config);
this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId); this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId);
this.webRtcPeer.processRemoteOffer(sdpOffer)
.then(() => { if (!!sdpOfferByServer) {
this.webRtcPeer.processRemoteOffer(sdpOfferByServer).then(() => {
this.webRtcPeer.createAnswer().then(sdpAnswer => { this.webRtcPeer.createAnswer().then(sdpAnswer => {
this.webRtcPeer.processLocalAnswer(sdpAnswer) this.webRtcPeer.processLocalAnswer(sdpAnswer).then(() => {
.then(() => { sendSdpToServer(sdpAnswer.sdp!);
successAnswerCallback(sdpAnswer.sdp); }).catch(error => {
}).catch(error => { reject(new Error('(subscribe) SDP process local answer error: ' + JSON.stringify(error)));
reject(new Error('(subscribe) SDP process local answer error: ' + JSON.stringify(error))); });
});
}).catch(error => { }).catch(error => {
reject(new Error('(subscribe) SDP create answer error: ' + JSON.stringify(error))); reject(new Error('(subscribe) SDP create answer error: ' + JSON.stringify(error)));
}); });
}) }).catch(error => {
.catch(error => {
reject(new Error('(subscribe) SDP process remote offer error: ' + JSON.stringify(error))); reject(new Error('(subscribe) SDP process remote offer error: ' + JSON.stringify(error)));
}); });
} else {
this.webRtcPeer.createOffer().then(sdpOffer => {
this.webRtcPeer.processLocalOffer(sdpOffer).then(() => {
sendSdpToServer(sdpOffer.sdp!);
}).catch(error => {
reject(new Error('(subscribe) SDP process local offer error: ' + JSON.stringify(error)));
});
}).catch(error => {
reject(new Error('(subscribe) SDP create offer error: ' + JSON.stringify(error)));
});
}
}); });
} }
@ -1048,6 +1189,137 @@ export class Stream {
} }
} }
private onIceConnectionStateExceptionHandler(exceptionName: ExceptionEventName, message: string, data?: any): void {
switch (exceptionName) {
case ExceptionEventName.ICE_CONNECTION_FAILED:
this.onIceConnectionFailed();
break;
case ExceptionEventName.ICE_CONNECTION_DISCONNECTED:
this.onIceConnectionDisconnected();
break;
}
this.session.emitEvent('exception', [new ExceptionEvent(this.session, exceptionName, this, message, data)]);
}
private onIceConnectionFailed() {
// Immediately reconnect, as this is a terminal error
logger.log(`[ICE_CONNECTION_FAILED] Handling ICE_CONNECTION_FAILED event. Reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')})`);
this.reconnectStreamAndLogResultingIceConnectionState(ExceptionEventName.ICE_CONNECTION_FAILED);
}
private onIceConnectionDisconnected() {
// Wait to see if the ICE connection is able to reconnect
logger.log(`[ICE_CONNECTION_DISCONNECTED] Handling ICE_CONNECTION_DISCONNECTED event. Waiting for ICE to be restored and reconnect stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) if not possible`);
const timeout = this.session.openvidu.advancedConfiguration.iceConnectionDisconnectedExceptionTimeout || 4000;
this.awaitWebRtcPeerConnectionState(timeout).then(state => {
switch (state) {
case 'failed':
// Do nothing, as an ICE_CONNECTION_FAILED event will have already raised
logger.warn(`[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) is now failed after ICE_CONNECTION_DISCONNECTED`);
break;
case 'connected':
case 'completed':
logger.log(`[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) automatically restored after ICE_CONNECTION_DISCONNECTED. Current ICE connection state: ${state}`);
break;
case 'closed':
case 'checking':
case 'new':
case 'disconnected':
// Rest of states
logger.warn(`[ICE_CONNECTION_DISCONNECTED] ICE connection of stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) couldn't be restored after ICE_CONNECTION_DISCONNECTED event. Current ICE connection state after ${timeout} ms: ${state}`);
this.reconnectStreamAndLogResultingIceConnectionState(ExceptionEventName.ICE_CONNECTION_DISCONNECTED);
break;
}
});
}
private async reconnectStreamAndLogResultingIceConnectionState(event: string) {
try {
const finalIceStateAfterReconnection = await this.reconnectStreamAndReturnIceConnectionState(event);
switch (finalIceStateAfterReconnection) {
case 'connected':
case 'completed':
logger.log(`[${event}] Stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) successfully reconnected after ${event}. Current ICE connection state: ${finalIceStateAfterReconnection}`);
break;
default:
logger.error(`[${event}] Stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) failed to reconnect after ${event}. Current ICE connection state: ${finalIceStateAfterReconnection}`);
break;
}
} catch (error) {
logger.error(`[${event}] Error reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) after ${event}: ${error}`);
}
}
private async reconnectStreamAndReturnIceConnectionState(event: string): Promise<RTCIceConnectionState> {
logger.log(`[${event}] Reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) after event ${event}`);
try {
await this.reconnectStream(event);
const timeout = this.session.openvidu.advancedConfiguration.iceConnectionDisconnectedExceptionTimeout || 4000;
return this.awaitWebRtcPeerConnectionState(timeout);
} catch (error) {
logger.warn(`[${event}] Error reconnecting stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}). Reason: ${error}`);
return this.awaitWebRtcPeerConnectionState(1);
}
}
private async reconnectStream(event: string) {
const isWsConnected = await this.isWebsocketConnected(event, 3000);
if (isWsConnected) {
// There is connection to openvidu-server. The RTCPeerConnection is the only one broken
logger.log(`[${event}] Trying to reconnect stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) and the websocket is opened`);
if (this.isLocal()) {
return this.initWebRtcPeerSend(true);
} else {
return this.initWebRtcPeerReceive(true);
}
} else {
// There is no connection to openvidu-server. Nothing can be done. The automatic reconnection
// feature should handle a possible reconnection of RTCPeerConnection in case network comes back
const errorMsg = `[${event}] Trying to reconnect stream ${this.streamId} (${(this.isLocal() ? 'Publisher' : 'Subscriber')}) but the websocket wasn't opened`;
logger.error(errorMsg);
throw Error(errorMsg);
}
}
private isWebsocketConnected(event: string, msResponseTimeout: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const wsReadyState = this.session.openvidu.getWsReadyState();
if (wsReadyState === 1) {
const responseTimeout = setTimeout(() => {
console.warn(`[${event}] Websocket timeout of ${msResponseTimeout}ms`);
resolve(false);
}, msResponseTimeout);
this.session.openvidu.sendRequest('echo', {}, (error, response) => {
clearTimeout(responseTimeout);
if (!!error) {
console.warn(`[${event}] Websocket 'echo' returned error: ${error}`);
resolve(false);
} else {
resolve(true);
}
});
} else {
console.warn(`[${event}] Websocket readyState is ${wsReadyState}`);
resolve(false);
}
});
}
private async awaitWebRtcPeerConnectionState(timeout: number): Promise<RTCIceConnectionState> {
let state = this.getRTCPeerConnection().iceConnectionState;
const interval = 150;
const intervals = Math.ceil(timeout / interval);
for (let i = 0; i < intervals; i++) {
state = this.getRTCPeerConnection().iceConnectionState;
if (state === 'connected' || state === 'completed') {
break;
}
// Sleep
await new Promise((resolve) => setTimeout(resolve, interval));
}
return state;
}
private initWebRtcStats(): void { private initWebRtcStats(): void {
this.webRtcStats = new WebRtcStats(this); this.webRtcStats = new WebRtcStats(this);
this.webRtcStats.initWebRtcStats(); this.webRtcStats.initWebRtcStats();

View File

@ -16,6 +16,7 @@
*/ */
import { Stream } from './Stream'; import { Stream } from './Stream';
import { Subscriber } from './Subscriber';
import { EventDispatcher } from './EventDispatcher'; import { EventDispatcher } from './EventDispatcher';
import { StreamManagerVideo } from '../OpenViduInternal/Interfaces/Public/StreamManagerVideo'; import { StreamManagerVideo } from '../OpenViduInternal/Interfaces/Public/StreamManagerVideo';
import { Event } from '../OpenViduInternal/Events/Event'; import { Event } from '../OpenViduInternal/Events/Event';
@ -24,6 +25,7 @@ import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent'
import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode';
import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger'; import { OpenViduLogger } from '../OpenViduInternal/Logger/OpenViduLogger';
import { PlatformUtils } from '../OpenViduInternal/Utils/Platform'; import { PlatformUtils } from '../OpenViduInternal/Utils/Platform';
import { ExceptionEvent, ExceptionEventName } from '../OpenViduInternal/Events/ExceptionEvent';
/** /**
* @hidden * @hidden
@ -90,19 +92,23 @@ export class StreamManager extends EventDispatcher {
/** /**
* @hidden * @hidden
*/ */
firstVideoElement?: StreamManagerVideo; protected firstVideoElement?: StreamManagerVideo;
/** /**
* @hidden * @hidden
*/ */
lazyLaunchVideoElementCreatedEvent = false; protected element: HTMLElement;
/**
* @hidden
*/
element: HTMLElement;
/** /**
* @hidden * @hidden
*/ */
protected canPlayListener: EventListener; protected canPlayListener: EventListener;
/**
* @hidden
*/
private streamPlayingEventExceptionTimeout?: NodeJS.Timeout;
/**
* @hidden
*/
private lazyLaunchVideoElementCreatedEvent = false;
/** /**
* @hidden * @hidden
@ -138,7 +144,11 @@ export class StreamManager extends EventDispatcher {
} }
this.canPlayListener = () => { this.canPlayListener = () => {
if (this.stream.isLocal()) { this.deactivateStreamPlayingEventExceptionTimeout();
if (this.remote) {
logger.info("Remote 'Stream' with id [" + this.stream.streamId + '] video is now playing');
this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'videoPlaying')]);
} else {
if (!this.stream.displayMyRemote()) { if (!this.stream.displayMyRemote()) {
logger.info("Your local 'Stream' with id [" + this.stream.streamId + '] video is now playing'); logger.info("Your local 'Stream' with id [" + this.stream.streamId + '] video is now playing');
this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'videoPlaying')]); this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'videoPlaying')]);
@ -146,9 +156,6 @@ export class StreamManager extends EventDispatcher {
logger.info("Your own remote 'Stream' with id [" + this.stream.streamId + '] video is now playing'); logger.info("Your own remote 'Stream' with id [" + this.stream.streamId + '] video is now playing');
this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'remoteVideoPlaying')]); this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'remoteVideoPlaying')]);
} }
} else {
logger.info("Remote 'Stream' with id [" + this.stream.streamId + '] video is now playing');
this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.videos[0].video, this, 'videoPlaying')]);
} }
this.ee.emitEvent('streamPlaying', [new StreamManagerEvent(this, 'streamPlaying', undefined)]); this.ee.emitEvent('streamPlaying', [new StreamManagerEvent(this, 'streamPlaying', undefined)]);
}; };
@ -274,7 +281,7 @@ export class StreamManager extends EventDispatcher {
this.initializeVideoProperties(video); this.initializeVideoProperties(video);
if (this.stream.isLocal() && this.stream.displayMyRemote()) { if (!this.remote && this.stream.displayMyRemote()) {
if (video.srcObject !== this.stream.getMediaStream()) { if (video.srcObject !== this.stream.getMediaStream()) {
video.srcObject = this.stream.getMediaStream(); video.srcObject = this.stream.getMediaStream();
} }
@ -305,7 +312,7 @@ export class StreamManager extends EventDispatcher {
id: video.id, id: video.id,
canplayListenerAdded: false canplayListenerAdded: false
}); });
logger.info('New video element associated to ', this); logger.info('New video element associated to ', this);
return returnNumber; return returnNumber;
@ -386,7 +393,7 @@ export class StreamManager extends EventDispatcher {
* - `interval`: (number) how frequently the analyser polls the audio stream to check if speaking has started/stopped or audio volume has changed. Default **100** (ms) * - `interval`: (number) how frequently the analyser polls the audio stream to check if speaking has started/stopped or audio volume has changed. Default **100** (ms)
* - `threshold`: (number) the volume at which _publisherStartSpeaking_, _publisherStopSpeaking_ events will be fired. Default **-50** (dB) * - `threshold`: (number) the volume at which _publisherStartSpeaking_, _publisherStopSpeaking_ events will be fired. Default **-50** (dB)
*/ */
updatePublisherSpeakingEventsOptions(publisherSpeakingEventsOptions): void { updatePublisherSpeakingEventsOptions(publisherSpeakingEventsOptions: { interval?: number, threshold?: number }): void {
const currentHarkOptions = !!this.stream.harkOptions ? this.stream.harkOptions : (this.stream.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {}); const currentHarkOptions = !!this.stream.harkOptions ? this.stream.harkOptions : (this.stream.session.openvidu.advancedConfiguration.publisherSpeakingEventsOptions || {});
const newInterval = (typeof publisherSpeakingEventsOptions.interval === 'number') ? const newInterval = (typeof publisherSpeakingEventsOptions.interval === 'number') ?
publisherSpeakingEventsOptions.interval : ((typeof currentHarkOptions.interval === 'number') ? currentHarkOptions.interval : 100); publisherSpeakingEventsOptions.interval : ((typeof currentHarkOptions.interval === 'number') ? currentHarkOptions.interval : 100);
@ -408,7 +415,7 @@ export class StreamManager extends EventDispatcher {
* @hidden * @hidden
*/ */
initializeVideoProperties(video: HTMLVideoElement): void { initializeVideoProperties(video: HTMLVideoElement): void {
if (!(this.stream.isLocal() && this.stream.displayMyRemote())) { if (!(!this.remote && this.stream.displayMyRemote())) {
// Avoid setting the MediaStream into the srcObject if remote subscription before publishing // Avoid setting the MediaStream into the srcObject if remote subscription before publishing
if (video.srcObject !== this.stream.getMediaStream()) { if (video.srcObject !== this.stream.getMediaStream()) {
// If srcObject already set don't do it again // If srcObject already set don't do it again
@ -492,6 +499,7 @@ export class StreamManager extends EventDispatcher {
*/ */
addPlayEventToFirstVideo() { addPlayEventToFirstVideo() {
if ((!!this.videos[0]) && (!!this.videos[0].video) && (!this.videos[0].canplayListenerAdded)) { if ((!!this.videos[0]) && (!!this.videos[0].video) && (!this.videos[0].canplayListenerAdded)) {
this.activateStreamPlayingEventExceptionTimeout();
this.videos[0].video.addEventListener('canplay', this.canPlayListener); this.videos[0].video.addEventListener('canplay', this.canPlayListener);
this.videos[0].canplayListenerAdded = true; this.videos[0].canplayListenerAdded = true;
} }
@ -533,6 +541,7 @@ export class StreamManager extends EventDispatcher {
*/ */
removeSrcObject(streamManagerVideo: StreamManagerVideo) { removeSrcObject(streamManagerVideo: StreamManagerVideo) {
streamManagerVideo.video.srcObject = null; streamManagerVideo.video.srcObject = null;
this.deactivateStreamPlayingEventExceptionTimeout();
} }
/* Private methods */ /* Private methods */
@ -557,4 +566,28 @@ export class StreamManager extends EventDispatcher {
video.style.webkitTransform = 'unset'; video.style.webkitTransform = 'unset';
} }
private activateStreamPlayingEventExceptionTimeout() {
if (!this.remote) {
// ExceptionEvent NO_STREAM_PLAYING_EVENT is only for subscribers
return;
}
if (this.streamPlayingEventExceptionTimeout != null) {
// The timeout is already activated
return;
}
// Trigger ExceptionEvent NO_STREAM_PLAYING_EVENT if after timeout there is no 'canplay' event
const msTimeout = this.stream.session.openvidu.advancedConfiguration.noStreamPlayingEventExceptionTimeout || 4000;
this.streamPlayingEventExceptionTimeout = setTimeout(() => {
const msg = 'StreamManager of Stream ' + this.stream.streamId + ' (' + (this.remote ? 'Subscriber' : 'Publisher') + ') did not trigger "streamPlaying" event in ' + msTimeout + ' ms';
logger.warn(msg);
this.stream.session.emitEvent('exception', [new ExceptionEvent(this.stream.session, ExceptionEventName.NO_STREAM_PLAYING_EVENT, (<any>this) as Subscriber, msg)]);
delete this.streamPlayingEventExceptionTimeout;
}, msTimeout);
}
private deactivateStreamPlayingEventExceptionTimeout() {
clearTimeout(this.streamPlayingEventExceptionTimeout as any);
delete this.streamPlayingEventExceptionTimeout;
}
} }

View File

@ -39,6 +39,7 @@ export class ConnectionEvent extends Event {
* - "forceDisconnectByServer": the remote user has been evicted from the Session by the application * - "forceDisconnectByServer": the remote user has been evicted from the Session by the application
* - "sessionClosedByServer": the Session has been closed by the application * - "sessionClosedByServer": the Session has been closed by the application
* - "networkDisconnect": the remote user network connection has dropped * - "networkDisconnect": the remote user network connection has dropped
* - "nodeCrashed": a node has crashed in the server side
* *
* For `connectionCreated` event an empty string * For `connectionCreated` event an empty string
*/ */

View File

@ -60,14 +60,14 @@ export abstract class Event {
/** /**
* Prevents the default behavior of the event. The following events have a default behavior: * Prevents the default behavior of the event. The following events have a default behavior:
* *
* - `sessionDisconnected`: dispatched by [[Session]] object, automatically unsubscribes the leaving participant from every Subscriber object of the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) * - `sessionDisconnected`: dispatched by [[Session]] object, automatically unsubscribes the leaving participant from every Subscriber object of the session (this includes closing the RTCPeerConnection and disposing all MediaStreamTracks)
* and also deletes any HTML video element associated to each Subscriber (only those created by OpenVidu Browser, either by passing a valid parameter as `targetElement` in method [[Session.subscribe]] or * and also deletes any HTML video element associated to each Subscriber (only those created by OpenVidu Browser, either by passing a valid parameter as `targetElement` in method [[Session.subscribe]] or
* by calling [[Subscriber.createVideoElement]]). For every video removed, each Subscriber object will also dispatch a `videoElementDestroyed` event. * by calling [[Subscriber.createVideoElement]]). For every video removed, each Subscriber object will also dispatch a `videoElementDestroyed` event.
* *
* - `streamDestroyed`: * - `streamDestroyed`:
* - If dispatched by a [[Publisher]] (*you* have unpublished): automatically stops all media tracks and deletes any HTML video element associated to it (only those created by OpenVidu Browser, either by passing a valid parameter as `targetElement` * - If dispatched by a [[Publisher]] (*you* have unpublished): automatically stops all media tracks and deletes any HTML video element associated to it (only those created by OpenVidu Browser, either by passing a valid parameter as `targetElement`
* in method [[OpenVidu.initPublisher]] or by calling [[Publisher.createVideoElement]]). For every video removed, the Publisher object will also dispatch a `videoElementDestroyed` event. * in method [[OpenVidu.initPublisher]] or by calling [[Publisher.createVideoElement]]). For every video removed, the Publisher object will also dispatch a `videoElementDestroyed` event.
* - If dispatched by [[Session]] (*other user* has unpublished): automatically unsubscribes the proper Subscriber object from the session (this includes closing the WebRTCPeer connection and disposing all MediaStreamTracks) * - If dispatched by [[Session]] (*other user* has unpublished): automatically unsubscribes the proper Subscriber object from the session (this includes closing the RTCPeerConnection and disposing all MediaStreamTracks)
* and also deletes any HTML video element associated to that Subscriber (only those created by OpenVidu Browser, either by passing a valid parameter as `targetElement` in method [[Session.subscribe]] or * and also deletes any HTML video element associated to that Subscriber (only those created by OpenVidu Browser, either by passing a valid parameter as `targetElement` in method [[Session.subscribe]] or
* by calling [[Subscriber.createVideoElement]]). For every video removed, the Subscriber object will also dispatch a `videoElementDestroyed` event. * by calling [[Subscriber.createVideoElement]]). For every video removed, the Subscriber object will also dispatch a `videoElementDestroyed` event.
*/ */

View File

@ -17,6 +17,7 @@
import { Session } from '../../OpenVidu/Session'; import { Session } from '../../OpenVidu/Session';
import { Stream } from '../../OpenVidu/Stream'; import { Stream } from '../../OpenVidu/Stream';
import { Subscriber } from '../../OpenVidu/Subscriber';
import { Event } from './Event'; import { Event } from './Event';
@ -36,7 +37,9 @@ export enum ExceptionEventName {
* The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState) * The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState)
* of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `failed` status. * of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `failed` status.
* *
* This is a terminal error that won't have any kind of possible recovery. * This is a terminal error that won't have any kind of possible recovery. If the client is still connected to OpenVidu Server,
* then an automatic reconnection process of the media stream is immediately performed. If the ICE connection has broken due to
* a total network drop, then no automatic reconnection process will be possible.
* *
* [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object. * [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object.
*/ */
@ -46,18 +49,45 @@ export enum ExceptionEventName {
* The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState) * The [ICE connection state](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState)
* of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `disconnected` status. * of an [RTCPeerConnection](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection) reached `disconnected` status.
* *
* This is not a terminal error, and it is possible for the ICE connection to be reconnected. * This is not a terminal error, and it is possible for the ICE connection to be reconnected. If the client is still connected to
* OpenVidu Server and after certain timeout the ICE connection has not reached a success or terminal status, then an automatic
* reconnection process of the media stream is performed. If the ICE connection has broken due to a total network drop, then no
* automatic reconnection process will be possible.
*
* You can customize the timeout for the reconnection attempt with property [[OpenViduAdvancedConfiguration.iceConnectionDisconnectedExceptionTimeout]],
* which by default is 4000 milliseconds.
* *
* [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object. * [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Stream]] object.
*/ */
ICE_CONNECTION_DISCONNECTED = 'ICE_CONNECTION_DISCONNECTED' ICE_CONNECTION_DISCONNECTED = 'ICE_CONNECTION_DISCONNECTED',
/**
* A [[Subscriber]] object has not fired event `streamPlaying` after certain timeout. `streamPlaying` event belongs to [[StreamManagerEvent]]
* category. It wraps Web API native event [canplay](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canplay_event).
*
* OpenVidu Browser can take care of the video players (see [here](/en/latest/cheatsheet/manage-videos/#let-openvidu-take-care-of-the-video-players)),
* or you can take care of video players on your own (see [here](/en/latest/cheatsheet/manage-videos/#you-take-care-of-the-video-players)).
* Either way, whenever a [[Subscriber]] object is commanded to attach its [[Stream]] to a video element, it is supposed to fire `streamPlaying`
* event shortly after. If it does not, then we can safely assume that something wrong has happened while playing the remote video and the
* application may be notified through this specific ExceptionEvent.
*
* The timeout can be configured with property [[OpenViduAdvancedConfiguration.noStreamPlayingEventExceptionTimeout]]. By default it is 4000 milliseconds.
*
* This is just an informative exception. It only means that a remote Stream that is supposed to be playing by a video player has not done so
* in a reasonable time. But the lack of the event can be caused by multiple reasons. If a Subscriber is not playing its Stream, the origin
* of the problem could be located at the Publisher side. Or may be caused by a transient network problem. But it also could be a problem with
* autoplay permissions. Bottom line, the cause can be very varied, and depending on the application the lack of the event could even be expected.
*
* [[ExceptionEvent]] objects with this [[ExceptionEvent.name]] will have as [[ExceptionEvent.origin]] property a [[Subscriber]] object.
*/
NO_STREAM_PLAYING_EVENT = 'NO_STREAM_PLAYING_EVENT'
} }
/** /**
* Defines event `exception` dispatched by [[Session]] object. * Defines event `exception` dispatched by [[Session]] object.
* *
* This event acts as a global handler for asynchronous errors that may be triggered for multiple reasons and from multiple origins. * This event acts as a global handler for asynchronous errors that may be triggered for multiple reasons and from multiple origins. To see the different
* types of exceptions go to [[ExceptionEventName]].
*/ */
export class ExceptionEvent extends Event { export class ExceptionEvent extends Event {
@ -70,8 +100,9 @@ export class ExceptionEvent extends Event {
* Object affected by the exception. Depending on the [[ExceptionEvent.name]] property: * Object affected by the exception. Depending on the [[ExceptionEvent.name]] property:
* - [[Session]]: `ICE_CANDIDATE_ERROR` * - [[Session]]: `ICE_CANDIDATE_ERROR`
* - [[Stream]]: `ICE_CONNECTION_FAILED`, `ICE_CONNECTION_DISCONNECTED` * - [[Stream]]: `ICE_CONNECTION_FAILED`, `ICE_CONNECTION_DISCONNECTED`
* - [[Subscriber]]: `NO_STREAM_PLAYING_EVENT`
*/ */
origin: Session | Stream; origin: Session | Stream | Subscriber;
/** /**
* Informative description of the exception * Informative description of the exception
@ -86,7 +117,7 @@ export class ExceptionEvent extends Event {
/** /**
* @hidden * @hidden
*/ */
constructor(session: Session, name: ExceptionEventName, origin: Session | Stream, message: string, data?: any) { constructor(session: Session, name: ExceptionEventName, origin: Session | Stream | Subscriber, message: string, data?: any) {
super(false, session, 'exception'); super(false, session, 'exception');
this.name = name; this.name = name;
this.origin = origin; this.origin = origin;

View File

@ -46,7 +46,7 @@ export class RecordingEvent extends Event {
* - "recordingStoppedByServer": the recording has been gracefully stopped by the application * - "recordingStoppedByServer": the recording has been gracefully stopped by the application
* - "sessionClosedByServer": the Session has been closed by the application * - "sessionClosedByServer": the Session has been closed by the application
* - "automaticStop": see [Automatic stop of recordings](/en/stable/advanced-features/recording/#automatic-stop-of-recordings) * - "automaticStop": see [Automatic stop of recordings](/en/stable/advanced-features/recording/#automatic-stop-of-recordings)
* - "mediaServerDisconnect": OpenVidu Media Node has crashed or lost its connection. A new Media Node instance is active and the recording has been stopped (no media streams are available in the new Media Node) * - "nodeCrashed": a node has crashed in the server side
* *
* For 'recordingStarted' empty string * For 'recordingStarted' empty string
*/ */

View File

@ -39,6 +39,8 @@ export class SessionDisconnectedEvent extends Event {
* Session object will always have previously dispatched a `reconnecting` event. If the reconnection process succeeds, * Session object will always have previously dispatched a `reconnecting` event. If the reconnection process succeeds,
* Session object will dispatch a `reconnected` event. If it fails, Session object will dispatch a SessionDisconnectedEvent * Session object will dispatch a `reconnected` event. If it fails, Session object will dispatch a SessionDisconnectedEvent
* with reason "networkDisconnect" * with reason "networkDisconnect"
* - "nodeCrashed": a node has crashed in the server side. You can use this reason to ask your application's backend to reconnect
* to a new session to replace the crashed one
*/ */
reason: string; reason: string;

View File

@ -48,7 +48,7 @@ export class StreamEvent extends Event {
* - "forceDisconnectByServer": the user has been evicted from the Session by the application * - "forceDisconnectByServer": the user has been evicted from the Session by the application
* - "sessionClosedByServer": the Session has been closed by the application * - "sessionClosedByServer": the Session has been closed by the application
* - "networkDisconnect": the user's network connection has dropped * - "networkDisconnect": the user's network connection has dropped
* - "mediaServerDisconnect": OpenVidu Media Node has crashed or lost its connection. A new Media Node instance is active and no media streams are available in the Media Node * - "nodeCrashed": a node has crashed in the server side
* *
* For 'streamCreated' empty string * For 'streamCreated' empty string
*/ */

View File

@ -21,7 +21,10 @@ import { StreamManager } from '../../OpenVidu/StreamManager';
/** /**
* Defines the following events: * Defines the following events:
* - `streamPlaying`: dispatched by [[StreamManager]] ([[Publisher]] and [[Subscriber]]) whenever its media stream starts playing (one of its videos has media * - `streamPlaying`: dispatched by [[StreamManager]] ([[Publisher]] and [[Subscriber]]) whenever its media stream starts playing (one of its videos has media
* and has begun to play). This event will be dispatched when these 3 conditions are met 1) The StreamManager has no video associated in the DOM 2) It is associated to one video 3) That video starts playing * and has begun to play). This event will be dispatched when these 3 conditions are met:
* 1. The StreamManager has no video associated in the DOM
* 2. It is associated to one video
* 3. That video starts playing. Internally the expected Web API event is [HTMLMediaElement.canplay](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canplay_event)
* - `streamAudioVolumeChange`: dispatched by [[StreamManager]] ([[Publisher]] and [[Subscriber]]) when the volume of its Stream's audio track * - `streamAudioVolumeChange`: dispatched by [[StreamManager]] ([[Publisher]] and [[Subscriber]]) when the volume of its Stream's audio track
* changes. Only applies if [[Stream.hasAudio]] is `true`. The frequency this event is fired with is defined by property `interval` of * changes. Only applies if [[Stream.hasAudio]] is `true`. The frequency this event is fired with is defined by property `interval` of
* [[OpenViduAdvancedConfiguration.publisherSpeakingEventsOptions]] (default 100ms) * [[OpenViduAdvancedConfiguration.publisherSpeakingEventsOptions]] (default 100ms)

View File

@ -31,4 +31,5 @@ export interface LocalConnectionOptions {
turnUsername: string; turnUsername: string;
turnCredential: string; turnCredential: string;
version: string; version: string;
mediaServer: string;
} }

View File

@ -39,7 +39,10 @@ export interface OpenViduAdvancedConfiguration {
* *
* This sets the global default configuration that will affect all streams, but you can later customize these values for each specific stream by calling [[StreamManager.updatePublisherSpeakingEventsOptions]] * This sets the global default configuration that will affect all streams, but you can later customize these values for each specific stream by calling [[StreamManager.updatePublisherSpeakingEventsOptions]]
*/ */
publisherSpeakingEventsOptions?: any; publisherSpeakingEventsOptions?: {
interval?: number;
threshold?: number;
};
/** /**
* Determines the automatic reconnection process policy. Whenever the client's network drops, OpenVidu Browser starts a reconnection process with OpenVidu Server. After network is recovered, OpenVidu Browser automatically * Determines the automatic reconnection process policy. Whenever the client's network drops, OpenVidu Browser starts a reconnection process with OpenVidu Server. After network is recovered, OpenVidu Browser automatically
@ -52,4 +55,20 @@ export interface OpenViduAdvancedConfiguration {
*/ */
forceMediaReconnectionAfterNetworkDrop?: boolean; forceMediaReconnectionAfterNetworkDrop?: boolean;
} /**
* The milliseconds that must elapse after triggering [[ExceptionEvent]] of name [`ICE_CONNECTION_DISCONNECTED`](/en/latest/api/openvidu-browser/enums/exceptioneventname.html#ice_connection_disconnected) to perform an automatic reconnection process of the affected media stream.
* This automatic reconnection process can only take place if the client still has network connection to OpenVidu Server. If the ICE connection has broken because of a total network drop,
* then no reconnection process will be possible at all.
*
* Default to `4000`.
*/
iceConnectionDisconnectedExceptionTimeout?: number;
/**
* The milliseconds that must elapse for the [[ExceptionEvent]] of name [`NO_STREAM_PLAYING_EVENT`](/en/latest/api/openvidu-browser/enums/exceptioneventname.html#no_stream_playing_event) to be fired.
*
* Default to `4000`.
*/
noStreamPlayingEventExceptionTimeout?: number;
}

View File

@ -270,6 +270,10 @@ function JsonRpcClient(configuration) {
pingNextNum = 0; pingNextNum = 0;
usePing(); usePing();
} }
this.getReadyState = function () {
return ws.getReadyState();
}
} }

View File

@ -66,8 +66,12 @@ function WebSocketWithReconnection(config) {
if (closing) { if (closing) {
Logger.debug("Connection closed by user"); Logger.debug("Connection closed by user");
} else { } else {
Logger.debug("Connection closed unexpectecly. Reconnecting..."); if (config.ismasternodecrashed()) {
reconnect(MAX_RETRIES, 1); Logger.error("Master Node has crashed. Stopping reconnection process");
} else {
Logger.debug("Connection closed unexpectedly. Reconnecting...");
reconnect(MAX_RETRIES, 1);
}
} }
} else { } else {
Logger.debug("Close callback from previous websocket. Ignoring it"); Logger.debug("Close callback from previous websocket. Ignoring it");
@ -147,6 +151,10 @@ function WebSocketWithReconnection(config) {
}; };
registerMessageHandler(); registerMessageHandler();
}; };
this.getReadyState = () => {
return ws.readyState;
}
} }
module.exports = WebSocketWithReconnection; module.exports = WebSocketWithReconnection;

View File

@ -37,18 +37,19 @@ export interface WebRtcPeerConfiguration {
video: boolean video: boolean
}; };
simulcast: boolean; simulcast: boolean;
onicecandidate: (event: RTCIceCandidate) => void; onIceCandidate: (event: RTCIceCandidate) => void;
onexception: (exceptionName: ExceptionEventName, message: string, data?: any) => void; onIceConnectionStateException: (exceptionName: ExceptionEventName, message: string, data?: any) => void;
iceServers: RTCIceServer[] | undefined;
iceServers?: RTCIceServer[];
mediaStream?: MediaStream | null; mediaStream?: MediaStream | null;
mode?: 'sendonly' | 'recvonly' | 'sendrecv'; mode?: 'sendonly' | 'recvonly' | 'sendrecv';
id?: string; id?: string;
} }
export class WebRtcPeer { export class WebRtcPeer {
public pc: RTCPeerConnection; pc: RTCPeerConnection;
public remoteCandidatesQueue: RTCIceCandidate[] = []; remoteCandidatesQueue: RTCIceCandidate[] = [];
public localCandidatesQueue: RTCIceCandidate[] = []; localCandidatesQueue: RTCIceCandidate[] = [];
// Same as WebRtcPeerConfiguration but without optional fields. // Same as WebRtcPeerConfiguration but without optional fields.
protected configuration: Required<WebRtcPeerConfiguration>; protected configuration: Required<WebRtcPeerConfiguration>;
@ -61,10 +62,15 @@ export class WebRtcPeer {
this.configuration = { this.configuration = {
...configuration, ...configuration,
iceServers: (!!configuration.iceServers && configuration.iceServers.length > 0) ? configuration.iceServers : freeice(), iceServers:
mediaStream: !!configuration.mediaStream !!configuration.iceServers &&
? configuration.mediaStream configuration.iceServers.length > 0
: null, ? configuration.iceServers
: freeice(),
mediaStream:
configuration.mediaStream !== undefined
? configuration.mediaStream
: null,
mode: !!configuration.mode ? configuration.mode : "sendrecv", mode: !!configuration.mode ? configuration.mode : "sendrecv",
id: !!configuration.id ? configuration.id : this.generateUniqueId(), id: !!configuration.id ? configuration.id : this.generateUniqueId(),
}; };
@ -74,7 +80,7 @@ export class WebRtcPeer {
this.pc.addEventListener('icecandidate', (event: RTCPeerConnectionIceEvent) => { this.pc.addEventListener('icecandidate', (event: RTCPeerConnectionIceEvent) => {
if (event.candidate != null) { if (event.candidate != null) {
const candidate: RTCIceCandidate = event.candidate; const candidate: RTCIceCandidate = event.candidate;
this.configuration.onicecandidate(candidate); this.configuration.onIceCandidate(candidate);
if (candidate.candidate !== '') { if (candidate.candidate !== '') {
this.localCandidatesQueue.push(<RTCIceCandidate>{ candidate: candidate.candidate }); this.localCandidatesQueue.push(<RTCIceCandidate>{ candidate: candidate.candidate });
} }
@ -91,6 +97,10 @@ export class WebRtcPeer {
}); });
} }
get id(): string {
return this.configuration.id;
}
/** /**
* This method frees the resources used by WebRtcPeer * This method frees the resources used by WebRtcPeer
*/ */
@ -310,12 +320,12 @@ export class WebRtcPeer {
// Possible network disconnection // Possible network disconnection
const msg1 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection'; const msg1 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection';
logger.warn(msg1); logger.warn(msg1);
this.configuration.onexception(ExceptionEventName.ICE_CONNECTION_DISCONNECTED, msg1); this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_DISCONNECTED, msg1);
break; break;
case 'failed': case 'failed':
const msg2 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') to "failed"'; const msg2 = 'IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') to "failed"';
logger.error(msg2); logger.error(msg2);
this.configuration.onexception(ExceptionEventName.ICE_CONNECTION_FAILED, msg2); this.configuration.onIceConnectionStateException(ExceptionEventName.ICE_CONNECTION_FAILED, msg2);
break; break;
case 'closed': case 'closed':
logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "closed"'); logger.log('IceConnectionState of RTCPeerConnection ' + this.configuration.id + ' (' + otherId + ') change to "closed"');

View File

@ -139,6 +139,8 @@ public class ProtocolElements {
// ENDTODO // ENDTODO
public static final String VIDEODATA_METHOD = "videoData"; public static final String VIDEODATA_METHOD = "videoData";
public static final String ECHO_METHOD = "echo";
// ---------------------------- SERVER RESPONSES & EVENTS ----------------- // ---------------------------- SERVER RESPONSES & EVENTS -----------------
@ -150,6 +152,7 @@ public class ProtocolElements {
public static final String PARTICIPANTJOINED_VALUE_PARAM = "value"; public static final String PARTICIPANTJOINED_VALUE_PARAM = "value";
public static final String PARTICIPANTJOINED_SESSION_PARAM = "session"; public static final String PARTICIPANTJOINED_SESSION_PARAM = "session";
public static final String PARTICIPANTJOINED_VERSION_PARAM = "version"; public static final String PARTICIPANTJOINED_VERSION_PARAM = "version";
public static final String PARTICIPANTJOINED_MEDIASERVER_PARAM = "mediaServer";
public static final String PARTICIPANTJOINED_RECORD_PARAM = "record"; public static final String PARTICIPANTJOINED_RECORD_PARAM = "record";
public static final String PARTICIPANTJOINED_ROLE_PARAM = "role"; public static final String PARTICIPANTJOINED_ROLE_PARAM = "role";
public static final String PARTICIPANTJOINED_COTURNIP_PARAM = "coturnIp"; public static final String PARTICIPANTJOINED_COTURNIP_PARAM = "coturnIp";

View File

@ -10,7 +10,7 @@
</parent> </parent>
<artifactId>openvidu-java-client</artifactId> <artifactId>openvidu-java-client</artifactId>
<version>2.17.0</version> <version>2.18.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>OpenVidu Java Client</name> <name>OpenVidu Java Client</name>

File diff suppressed because it is too large Load Diff

View File

@ -33,5 +33,5 @@
"docs": "./generate-docs.sh" "docs": "./generate-docs.sh"
}, },
"typings": "lib/index.d.ts", "typings": "lib/index.d.ts",
"version": "2.17.0" "version": "2.18.0"
} }

View File

@ -206,10 +206,10 @@ export class OpenVidu {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request); reject(error.request);
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message); reject(error.message);
} }
}); });
}); });
@ -259,10 +259,10 @@ export class OpenVidu {
} else if (error.request) { } else if (error.request) {
// The request was made but no response was received `error.request` is an instance of XMLHttpRequest // The request was made but no response was received `error.request` is an instance of XMLHttpRequest
// in the browser and an instance of http.ClientRequest in node.js // in the browser and an instance of http.ClientRequest in node.js
console.error(error.request); reject(new Error(error.request));
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message); reject(new Error(error.message));
} }
}); });
}); });
@ -304,10 +304,10 @@ export class OpenVidu {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request); reject(new Error(error.request));
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message); reject(new Error(error.message));
} }
}); });
}); });
@ -352,10 +352,10 @@ export class OpenVidu {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request); reject(new Error(error.request));
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message); reject(new Error(error.message));
} }
}); });
}); });
@ -398,10 +398,10 @@ export class OpenVidu {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request); reject(new Error(error.request));
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message); reject(new Error(error.message));
} }
}); });
}); });
@ -479,11 +479,9 @@ export class OpenVidu {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request);
reject(error); reject(error);
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message);
reject(new Error(error.message)); reject(new Error(error.message));
} }
}); });
@ -649,10 +647,10 @@ export class OpenVidu {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request); reject(new Error(error.request));
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message); reject(new Error(error.message));
} }
}); });
}); });
@ -668,7 +666,6 @@ export class OpenVidu {
try { try {
url = new URL(this.hostname); url = new URL(this.hostname);
} catch (error) { } catch (error) {
console.error('URL format incorrect', error);
throw new Error('URL format incorrect: ' + error); throw new Error('URL format incorrect: ' + error);
} }
this.host = url.protocol + '//' + url.host; this.host = url.protocol + '//' + url.host;

View File

@ -511,11 +511,9 @@ export class Session {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request);
reject(new Error(error.request)); reject(new Error(error.request));
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message);
reject(new Error(error.message)); reject(new Error(error.message));
} }
}); });
@ -642,11 +640,9 @@ export class Session {
// The request was made but no response was received // The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js // http.ClientRequest in node.js
console.error(error.request);
reject(new Error(error.request)); reject(new Error(error.request));
} else { } else {
// Something happened in setting up the request that triggered an Error // Something happened in setting up the request that triggered an Error
console.error('Error', error.message);
reject(new Error(error.message)); reject(new Error(error.message));
} }
} }

View File

@ -128,10 +128,12 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300
# All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true # All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true
# when a codec can not be forced, transcoding will be allowed # when a codec can not be forced, transcoding will be allowed
# Values: VP8, H264, NONE
# Default value is VP8 # Default value is VP8
# OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 # OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8
# Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied # Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied
# Values: true | false
# Default value is false # Default value is false
# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false # OPENVIDU_STREAMS_ALLOW_TRANSCODING=false

View File

@ -9,11 +9,11 @@ services:
# #
# Default Application # Default Application
# #
# Openvidu-Call Version: 2.17.0 # Openvidu-Call Version: 2.18.0
# #
# -------------------------------------------------------------- # --------------------------------------------------------------
app: app:
image: openvidu/openvidu-call:2.18.0-beta8 image: openvidu/openvidu-call:2.18.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
environment: environment:

View File

@ -11,7 +11,7 @@
# #
# This file will be overridden when update OpenVidu Platform # This file will be overridden when update OpenVidu Platform
# #
# Openvidu Version: 2.17.0 # Openvidu Version: 2.18.0
# #
# Installation Mode: On Premises # Installation Mode: On Premises
# #
@ -22,7 +22,7 @@ version: '3.1'
services: services:
openvidu-server: openvidu-server:
image: openvidu/openvidu-server:2.18.0-dev2 image: openvidu/openvidu-server:2.18.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh'] entrypoint: ['/usr/local/bin/entrypoint.sh']
@ -65,7 +65,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
redis: redis:
image: openvidu/openvidu-redis:2.0.0 image: openvidu/openvidu-redis:3.0.0
restart: always restart: always
network_mode: host network_mode: host
environment: environment:
@ -75,7 +75,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn: coturn:
image: openvidu/openvidu-coturn:4.0.0-dev2 image: openvidu/openvidu-coturn:4.0.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
environment: environment:
@ -96,7 +96,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
nginx: nginx:
image: openvidu/openvidu-proxy:6.0.0-dev1 image: openvidu/openvidu-proxy:7.0.0-dev1
restart: on-failure restart: on-failure
network_mode: host network_mode: host
volumes: volumes:

View File

@ -243,10 +243,12 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300
# All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true # All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true
# when a codec can not be forced, transcoding will be allowed # when a codec can not be forced, transcoding will be allowed
# Values: VP8, H264, NONE
# Default value is VP8 # Default value is VP8
# OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 # OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8
# Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied # Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied
# Values: true | false
# Default value is false # Default value is false
# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false # OPENVIDU_STREAMS_ALLOW_TRANSCODING=false

View File

@ -9,11 +9,11 @@ services:
# #
# Default Application # Default Application
# #
# Openvidu-Call Version: 2.17.0 # Openvidu-Call Version: 2.18.0
# #
# -------------------------------------------------------------- # --------------------------------------------------------------
app: app:
image: openvidu/openvidu-call:2.18.0-beta8 image: openvidu/openvidu-call:2.18.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
environment: environment:

View File

@ -11,7 +11,7 @@
# #
# This file will be overridden when update OpenVidu Platform # This file will be overridden when update OpenVidu Platform
# #
# Openvidu Version: 2.17.0 # Openvidu Version: 2.18.0
# #
# Installation Mode: On Premises # Installation Mode: On Premises
# #
@ -22,7 +22,7 @@ version: '3.1'
services: services:
openvidu-server: openvidu-server:
image: openvidu/openvidu-server-pro:2.18.0-beta17 image: openvidu/openvidu-server-pro:2.18.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh'] entrypoint: ['/usr/local/bin/entrypoint.sh']
@ -76,7 +76,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
redis: redis:
image: openvidu/openvidu-redis:2.0.0 image: openvidu/openvidu-redis:3.0.0
restart: always restart: always
network_mode: host network_mode: host
environment: environment:
@ -86,7 +86,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn: coturn:
image: openvidu/openvidu-coturn:4.0.0-dev2 image: openvidu/openvidu-coturn:4.0.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
environment: environment:

View File

@ -6,7 +6,7 @@
# #
# This docker-compose file coordinates all services of OpenVidu CE Platform. # This docker-compose file coordinates all services of OpenVidu CE Platform.
# #
# Openvidu Version: 2.17.0 # Openvidu Version: 2.18.0
# #
# Installation Mode: On Premises # Installation Mode: On Premises
# #
@ -16,7 +16,7 @@ version: '3.1'
services: services:
media-node-controller: media-node-controller:
image: openvidu/media-node-controller:4.0.0-dev1 image: openvidu/media-node-controller:4.0.0
restart: always restart: always
ulimits: ulimits:
core: -1 core: -1

View File

@ -243,10 +243,12 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300
# All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true # All sessions of OpenVidu will try to force this codec. If OPENVIDU_STREAMS_ALLOW_TRANSCODING=true
# when a codec can not be forced, transcoding will be allowed # when a codec can not be forced, transcoding will be allowed
# Values: VP8, H264, NONE
# Default value is VP8 # Default value is VP8
# OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8 # OPENVIDU_STREAMS_FORCED_VIDEO_CODEC=VP8
# Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied # Allow transcoding if codec specified in OPENVIDU_STREAMS_FORCED_VIDEO_CODEC can not be applied
# Values: true | false
# Default value is false # Default value is false
# OPENVIDU_STREAMS_ALLOW_TRANSCODING=false # OPENVIDU_STREAMS_ALLOW_TRANSCODING=false

View File

@ -7,12 +7,12 @@ DEBUG=${DEBUG:-false}
OUTPUT=$(mktemp -t openvidu-autodiscover-XXX --suffix .json) OUTPUT=$(mktemp -t openvidu-autodiscover-XXX --suffix .json)
docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 describe-instances \ docker run --rm amazon/aws-cli:"${AWS_CLI_DOCKER_TAG}" ec2 describe-instances \
--output text \ --output text \
--filters "Name=instance-state-name,Values=running" \ --filters "Name=instance-state-name,Values=running" \
"Name=tag:ov-cluster-member,Values=kms" \ "Name=tag:ov-cluster-member,Values=kms" \
"Name=tag:ov-stack-name,Values=${AWS_STACK_NAME}" \ "Name=tag:ov-stack-name,Values=${AWS_STACK_NAME}" \
"Name=tag:ov-stack-region,Values=${AWS_DEFAULT_REGION}" \ "Name=tag:ov-stack-region,Values=${AWS_DEFAULT_REGION}" \
--query 'Reservations[*].Instances[*].{id:InstanceId,ip:PrivateIpAddress}' > ${OUTPUT} --query 'Reservations[*].Instances[*].{id:InstanceId,ip:PrivateIpAddress}' > "${OUTPUT}"
cat ${OUTPUT} | jq --raw-input --slurp 'split("\n") | map(split("\t")) | .[0:-1] | map( { "id": .[0], "ip": .[1] } )' cat "${OUTPUT}" | jq --raw-input --slurp 'split("\n") | map(split("\t")) | .[0:-1] | map( { "id": .[0], "ip": .[1] } )'

View File

@ -8,4 +8,4 @@ DEBUG=${DEBUG:-false}
ID=$1 ID=$1
[ -z "${ID}" ] && { echo "Must provide instance ID"; exit 1; } [ -z "${ID}" ] && { echo "Must provide instance ID"; exit 1; }
docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 terminate-instances --instance-ids ${ID} --output json docker run --rm amazon/aws-cli:"${AWS_CLI_DOCKER_TAG}" ec2 terminate-instances --instance-ids "${ID}" --output json

View File

@ -23,7 +23,7 @@ exit_on_error () {
"UnauthorizedOperation") "UnauthorizedOperation")
MSG_COD=$(cat ${ERROUTPUT} | awk -F: '{ print $3 }') MSG_COD=$(cat ${ERROUTPUT} | awk -F: '{ print $3 }')
MSG_DEC=$(docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} sts decode-authorization-message --encoded-message ${MSG_COD}) MSG_DEC=$(docker run --rm amazon/aws-cli:"${AWS_CLI_DOCKER_TAG}" sts decode-authorization-message --encoded-message "${MSG_COD}")
echo -e "Unauthorized " $(cat ${MSG_DEC}) >&2 echo -e "Unauthorized " $(cat ${MSG_DEC}) >&2
exit 1 exit 1
@ -43,21 +43,21 @@ if [[ -n "${CUSTOM_VOLUME_SIZE}" ]]; then
AWS_VOLUME_SIZE="${CUSTOM_VOLUME_SIZE}" AWS_VOLUME_SIZE="${CUSTOM_VOLUME_SIZE}"
fi fi
docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 run-instances \ docker run --rm amazon/aws-cli:"${AWS_CLI_DOCKER_TAG}" ec2 run-instances \
--image-id ${AWS_IMAGE_ID} --count 1 \ --image-id "${AWS_IMAGE_ID}" --count 1 \
--instance-type ${AWS_INSTANCE_TYPE} \ --instance-type "${AWS_INSTANCE_TYPE}" \
--key-name ${AWS_KEY_NAME} \ --key-name "${AWS_KEY_NAME}" \
--subnet-id ${AWS_SUBNET_ID} \ --subnet-id "${AWS_SUBNET_ID}" \
--tag-specifications "ResourceType=instance,Tags=[{Key='Name',Value='Kurento Media Server'},{Key='ov-cluster-member',Value='kms'},{Key='ov-stack-name',Value='${AWS_STACK_NAME}'},{Key='ov-stack-region',Value='${AWS_DEFAULT_REGION}'}]" \ --tag-specifications "ResourceType=instance,Tags=[{Key='Name',Value='Kurento Media Server'},{Key='ov-cluster-member',Value='kms'},{Key='ov-stack-name',Value='${AWS_STACK_NAME}'},{Key='ov-stack-region',Value='${AWS_DEFAULT_REGION}'}]" \
--iam-instance-profile Name="OpenViduInstanceProfile-${AWS_STACK_NAME}-${AWS_DEFAULT_REGION}" \ --iam-instance-profile Name="OpenViduInstanceProfile-${AWS_STACK_NAME}-${AWS_DEFAULT_REGION}" \
--block-device-mappings "DeviceName=/dev/sda1,Ebs={DeleteOnTermination=True,VolumeType='gp2',VolumeSize='${AWS_VOLUME_SIZE}'}" \ --block-device-mappings "DeviceName=/dev/sda1,Ebs={DeleteOnTermination=True,VolumeType='gp2',VolumeSize='${AWS_VOLUME_SIZE}'}" \
--security-group-ids ${AWS_SECURITY_GROUP} > ${OUTPUT} 2> ${ERROUTPUT} --security-group-ids "${AWS_SECURITY_GROUP}" > "${OUTPUT}" 2> "${ERROUTPUT}"
docker run --rm amazon/aws-cli:${AWS_CLI_DOCKER_TAG} ec2 wait instance-running --instance-ids $(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId') docker run --rm amazon/aws-cli:"${AWS_CLI_DOCKER_TAG}" ec2 wait instance-running --instance-ids $(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId')
# Generating the output # Generating the output
KMS_IP=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .NetworkInterfaces[0] | .PrivateIpAddress') KMS_IP=$(cat "${OUTPUT}" | jq --raw-output ' .Instances[] | .NetworkInterfaces[0] | .PrivateIpAddress')
KMS_ID=$(cat ${OUTPUT} | jq --raw-output ' .Instances[] | .InstanceId') KMS_ID=$(cat "${OUTPUT}" | jq --raw-output ' .Instances[] | .InstanceId')
jq -n \ jq -n \
--arg id "${KMS_ID}" \ --arg id "${KMS_ID}" \

View File

@ -9,11 +9,11 @@ services:
# #
# Default Application # Default Application
# #
# Openvidu-Call Version: 2.17.0 # Openvidu-Call Version: 2.18.0
# #
# -------------------------------------------------------------- # --------------------------------------------------------------
app: app:
image: openvidu/openvidu-call:2.18.0-beta8 image: openvidu/openvidu-call:2.18.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
environment: environment:

View File

@ -11,7 +11,7 @@
# #
# This file will be overridden when update OpenVidu Platform # This file will be overridden when update OpenVidu Platform
# #
# Openvidu Version: 2.17.0 # Openvidu Version: 2.18.0
# #
# Installation Mode: On Premises # Installation Mode: On Premises
# #
@ -22,7 +22,7 @@ version: '3.1'
services: services:
openvidu-server: openvidu-server:
image: openvidu/openvidu-server-pro:2.18.0-beta17 image: openvidu/openvidu-server-pro:2.18.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
entrypoint: ['/usr/local/bin/entrypoint.sh'] entrypoint: ['/usr/local/bin/entrypoint.sh']
@ -52,7 +52,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
redis: redis:
image: openvidu/openvidu-redis:2.0.0 image: openvidu/openvidu-redis:3.0.0
restart: always restart: always
network_mode: host network_mode: host
environment: environment:
@ -62,7 +62,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
coturn: coturn:
image: openvidu/openvidu-coturn:4.0.0-dev2 image: openvidu/openvidu-coturn:4.0.0
restart: on-failure restart: on-failure
network_mode: host network_mode: host
environment: environment:
@ -84,7 +84,7 @@ services:
max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}" max-size: "${DOCKER_LOGS_MAX_SIZE:-100M}"
nginx: nginx:
image: openvidu/openvidu-proxy:6.0.0-dev1 image: openvidu/openvidu-proxy:7.0.0-dev1
restart: on-failure restart: on-failure
network_mode: host network_mode: host
volumes: volumes:

View File

@ -1,3 +1,5 @@
{xframe_options}
{app_upstream} {app_upstream}
upstream openviduserver { upstream openviduserver {

View File

@ -0,0 +1 @@
add_header X-Frame-Options SAMEORIGIN;

View File

@ -1,4 +1,5 @@
add_header X-Frame-Options SAMEORIGIN; {xframe_options}
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";

View File

@ -47,6 +47,7 @@ CERTIFICATES_CONF="${CERTIFICATES_LIVE_FOLDER}/certificates.conf"
[ -z "${PUBLIC_IP}" ] && export PUBLIC_IP=auto-ipv4 [ -z "${PUBLIC_IP}" ] && export PUBLIC_IP=auto-ipv4
[ -z "${ALLOWED_ACCESS_TO_DASHBOARD}" ] && export ALLOWED_ACCESS_TO_DASHBOARD=all [ -z "${ALLOWED_ACCESS_TO_DASHBOARD}" ] && export ALLOWED_ACCESS_TO_DASHBOARD=all
[ -z "${ALLOWED_ACCESS_TO_RESTAPI}" ] && export ALLOWED_ACCESS_TO_RESTAPI=all [ -z "${ALLOWED_ACCESS_TO_RESTAPI}" ] && export ALLOWED_ACCESS_TO_RESTAPI=all
[ -z "${XFRAME_SAMEORIGIN}" ] && export XFRAME_SAMEORIGIN=false
# Show input enviroment variables # Show input enviroment variables
printf "\n =======================================" printf "\n ======================================="
@ -228,6 +229,12 @@ elif [[ "${WITH_APP}" == "false" ]]; then
sed -e '/{app_config}/{r default_nginx_conf/global/app_config_default.conf' -e 'd}' -i /etc/nginx/conf.d/* sed -e '/{app_config}/{r default_nginx_conf/global/app_config_default.conf' -e 'd}' -i /etc/nginx/conf.d/*
fi fi
if [[ "${XFRAME_SAMEORIGIN}" == "true" ]]; then
sed -e '/{xframe_options}/{r default_nginx_conf/global/xframe_sameorigin.conf' -e 'd}' -i /etc/nginx/conf.d/*
elif [[ "${XFRAME_SAMEORIGIN}" == "false" ]]; then
sed -i '/{xframe_options}/d' /etc/nginx/conf.d/*
fi
if [[ "${SUPPORT_DEPRECATED_API}" == "true" ]]; then if [[ "${SUPPORT_DEPRECATED_API}" == "true" ]]; then
sed -e '/{deprecated_api_ce}/{r default_nginx_conf/global/ce/deprecated_api_ce.conf' -e 'd}' -i /etc/nginx/conf.d/* sed -e '/{deprecated_api_ce}/{r default_nginx_conf/global/ce/deprecated_api_ce.conf' -e 'd}' -i /etc/nginx/conf.d/*
sed -e '/{deprecated_api_pro}/{r default_nginx_conf/global/pro/deprecated_api_pro.conf' -e 'd}' -i /etc/nginx/conf.d/* sed -e '/{deprecated_api_pro}/{r default_nginx_conf/global/pro/deprecated_api_pro.conf' -e 'd}' -i /etc/nginx/conf.d/*

View File

@ -4,7 +4,7 @@ MAINTAINER info@openvidu.io
ARG CHROME_VERSION ARG CHROME_VERSION
# Install Chrome # Install Chrome
RUN apt-get update && apt-get -y upgrade && apt-get install -y wget sudo RUN apt-get update && apt-get -y upgrade && apt-get install -y wget sudo fonts-noto
RUN wget http://dl.google.com/linux/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \ RUN wget http://dl.google.com/linux/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
&& apt install -y ./google-chrome-stable_${CHROME_VERSION}_amd64.deb \ && apt install -y ./google-chrome-stable_${CHROME_VERSION}_amd64.deb \
&& rm google-chrome-stable_${CHROME_VERSION}_amd64.deb \ && rm google-chrome-stable_${CHROME_VERSION}_amd64.deb \

View File

@ -14,6 +14,7 @@ RUN apt-get update && apt-get -y upgrade && apt-get install -y \
pulseaudio \ pulseaudio \
xvfb \ xvfb \
jq \ jq \
fonts-noto \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install chrome # Install chrome

View File

@ -12,7 +12,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>OpenVidu Server</name> <name>OpenVidu Server</name>
<version>2.17.0</version> <version>2.18.0</version>
<description>OpenVidu Server</description> <description>OpenVidu Server</description>
<url>https://openvidu.io</url> <url>https://openvidu.io</url>

View File

@ -7183,6 +7183,11 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true "dev": true
}, },
"jsnlog": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/jsnlog/-/jsnlog-2.30.0.tgz",
"integrity": "sha512-o3ROQVkhek+dkc7/9TXlB4TNtxUpYsRLOBJHZYk3Vy0B5zRBmfv9tyr56PrjcgEXuy06ARgfLTANY0+ImhzzGA=="
},
"json-parse-better-errors": { "json-parse-better-errors": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
@ -8728,14 +8733,15 @@
} }
}, },
"openvidu-browser": { "openvidu-browser": {
"version": "2.17.0", "version": "2.18.0",
"resolved": "https://registry.npmjs.org/openvidu-browser/-/openvidu-browser-2.17.0.tgz", "resolved": "https://registry.npmjs.org/openvidu-browser/-/openvidu-browser-2.18.0.tgz",
"integrity": "sha512-wholLLrs2R1Rf05+QPOH4eIO9LaKO9UvoinGNHU9WoP32L9cU6WeqL0sH3gHhUdiYPqGwTNRPjC3qk/ltjUbDA==", "integrity": "sha512-a9HPAG8p2vG9XGThPUbZWPI5sO1OFmrxKQdCZM3RLmasXQa2Lwj7X1Aicsznb7RtDBg/lD8NdNwAQjm4ZDt2Nw==",
"requires": { "requires": {
"freeice": "2.2.2", "freeice": "2.2.2",
"hark": "1.2.3", "hark": "1.2.3",
"jsnlog": "2.30.0",
"platform": "1.3.6", "platform": "1.3.6",
"uuid": "8.3.1", "uuid": "8.3.2",
"wolfy87-eventemitter": "5.2.9" "wolfy87-eventemitter": "5.2.9"
} }
}, },
@ -13579,9 +13585,9 @@
"dev": true "dev": true
}, },
"uuid": { "uuid": {
"version": "8.3.1", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==" "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}, },
"validate-npm-package-name": { "validate-npm-package-name": {
"version": "3.0.0", "version": "3.0.0",

View File

@ -1,55 +1,55 @@
{ {
"dependencies": { "dependencies": {
"@angular/animations": "11.2.4", "@angular/animations": "11.2.4",
"@angular/cdk": "11.2.3", "@angular/cdk": "11.2.3",
"@angular/common": "11.2.4", "@angular/common": "11.2.4",
"@angular/compiler": "11.2.4", "@angular/compiler": "11.2.4",
"@angular/core": "11.2.4", "@angular/core": "11.2.4",
"@angular/flex-layout": "11.0.0-beta.33", "@angular/flex-layout": "11.0.0-beta.33",
"@angular/forms": "11.2.4", "@angular/forms": "11.2.4",
"@angular/material": "11.2.3", "@angular/material": "11.2.3",
"@angular/platform-browser": "11.2.4", "@angular/platform-browser": "11.2.4",
"@angular/platform-browser-dynamic": "11.2.4", "@angular/platform-browser-dynamic": "11.2.4",
"@angular/router": "11.2.4", "@angular/router": "11.2.4",
"core-js": "3.9.1", "core-js": "3.9.1",
"jquery": "3.6.0", "jquery": "3.6.0",
"openvidu-browser": "2.17.0", "openvidu-browser": "2.18.0",
"rxjs": "6.6.6", "rxjs": "6.6.6",
"tslib": "2.1.0", "tslib": "2.1.0",
"zone.js": "0.11.4" "zone.js": "0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "0.1102.3", "@angular-devkit/build-angular": "0.1102.3",
"@angular/cli": "11.2.3", "@angular/cli": "11.2.3",
"@angular/compiler-cli": "11.2.4", "@angular/compiler-cli": "11.2.4",
"@angular/language-service": "11.2.4", "@angular/language-service": "11.2.4",
"@types/jasmine": "3.6.6", "@types/jasmine": "3.6.6",
"@types/node": "14.14.32", "@types/node": "14.14.32",
"codelyzer": "6.0.1", "codelyzer": "6.0.1",
"jasmine-core": "3.6.0", "jasmine-core": "3.6.0",
"jasmine-spec-reporter": "6.0.0", "jasmine-spec-reporter": "6.0.0",
"karma": "6.1.2", "karma": "6.1.2",
"karma-chrome-launcher": "3.1.0", "karma-chrome-launcher": "3.1.0",
"karma-coverage-istanbul-reporter": "3.0.3", "karma-coverage-istanbul-reporter": "3.0.3",
"karma-jasmine": "4.0.1", "karma-jasmine": "4.0.1",
"karma-jasmine-html-reporter": "1.5.4", "karma-jasmine-html-reporter": "1.5.4",
"protractor": "7.0.0", "protractor": "7.0.0",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"tslint": "6.1.3", "tslint": "6.1.3",
"typescript": "4.1.5" "typescript": "4.1.5"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "./node_modules/@angular/cli/bin/ng build --base-href /dashboard/ --output-path ../main/resources/static/dashboard", "build": "./node_modules/@angular/cli/bin/ng build --base-href /dashboard/ --output-path ../main/resources/static/dashboard",
"build-prod": "./node_modules/@angular/cli/bin/ng build --prod --base-href /dashboard/ --output-path ../main/resources/static/dashboard", "build-prod": "./node_modules/@angular/cli/bin/ng build --prod --base-href /dashboard/ --output-path ../main/resources/static/dashboard",
"e2e": "ng e2e", "e2e": "ng e2e",
"lint": "ng lint", "lint": "ng lint",
"ng": "ng", "ng": "ng",
"postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points",
"start": "ng serve", "start": "ng serve",
"test": "ng test" "test": "ng test"
}, },
"version": "0.0.0" "version": "0.0.0"
} }

View File

@ -61,6 +61,7 @@ import io.openvidu.java.client.VideoCodec;
import io.openvidu.server.OpenViduServer; import io.openvidu.server.OpenViduServer;
import io.openvidu.server.cdr.CDREventName; import io.openvidu.server.cdr.CDREventName;
import io.openvidu.server.config.Dotenv.DotenvFormatException; import io.openvidu.server.config.Dotenv.DotenvFormatException;
import io.openvidu.server.core.MediaServer;
import io.openvidu.server.recording.RecordingNotification; import io.openvidu.server.recording.RecordingNotification;
import io.openvidu.server.rest.RequestMappings; import io.openvidu.server.rest.RequestMappings;
@ -246,6 +247,10 @@ public class OpenviduConfig {
return false; return false;
} }
public MediaServer getMediaServer() {
return MediaServer.kurento;
}
public String getOpenViduRecordingPath() { public String getOpenViduRecordingPath() {
return this.openviduRecordingPath; return this.openviduRecordingPath;
} }

View File

@ -19,8 +19,114 @@ package io.openvidu.server.core;
public enum EndReason { public enum EndReason {
unsubscribe, unpublish, disconnect, forceUnpublishByUser, forceUnpublishByServer, forceDisconnectByUser, /**
forceDisconnectByServer, lastParticipantLeft, recordingStoppedByServer, sessionClosedByServer, networkDisconnect, * A user called the RPC operation to unsubscribe from a remote stream. Applies
mediaServerDisconnect, mediaServerReconnect, nodeCrashed, openviduServerStopped, automaticStop * to webrtcConnectionDestroyed
*/
unsubscribe,
/**
* A user called the RPC operation to unpublish a local stream. Applies to
* webrtcConnectionDestroyed
*/
unpublish,
/**
* A user called the RPC operation to leave the session. Applies to
* webrtcConnectionDestroyed and participantLeft. Can trigger other events with
* lastParticipantLeft
*/
disconnect,
/**
* A user called the RPC operation to force the unpublishing of a remote stream.
* Applies to webrtcConnectionDestroyed
*/
forceUnpublishByUser,
/**
* The server application called the REST operation to force the unpublishing of
* a user's stream. Applies to webrtcConnectionDestroyed
*/
forceUnpublishByServer,
/**
* A user called the RPC operation to force the disconnection of a remote user.
* Applies to webrtcConnectionDestroyed and participantLeft. Can trigger other
* events with lastParticipantLeft
*/
forceDisconnectByUser,
/**
* The server application called the REST operation to force the disconnection
* of a user. Applies to webrtcConnectionDestroyed and participantLeft. Can
* trigger other events with lastParticipantLeft
*/
forceDisconnectByServer,
/**
* The last participant left the session, which caused the session to be closed.
* Applies to webrtcConnectionDestroyed, participantLeft, recordingStatusChanged
* and sessionDestroyed. Can be triggered from other events with other end
* reasons (disconnect, forceDisconnectByUser, forceDisconnectByServer,
* networkDisconnect)
*/
lastParticipantLeft,
/**
* The server application called the REST operation to stop a recording. Applies
* to recordingStatusChanged
*/
recordingStoppedByServer,
/**
* The server application called the REST operation to close a session. Applies
* to webrtcConnectionDestroyed, participantLeft, recordingStatusChanged and
* sessionDestroyed
*/
sessionClosedByServer,
/**
* A user left the session because of a network disconnection. Applies to
* webrtcConnectionDestroyed and participantLeft. Can trigger other events with
* lastParticipantLeft
*/
networkDisconnect,
/**
* A media server disconnected. This is reserved for Media Nodes being
* gracefully removed from an OpenVidu Pro cluster. Applies to
* webrtcConnectionDestroyed, participantLeft, recordingStatusChanged and
* sessionDestroyed
*/
mediaServerDisconnect,
/**
* A media server disconnected, but was able to reconnect again. Nevertheless
* all of the media endpoints were destroyed in the process. Applies to
* webrtcConnectionDestroyed and recordingStatusChanged
*/
mediaServerReconnect,
/**
* A node has crashed. For now this means a Media Node has crashed. Applies to
* webrtcConnectionDestroyed, participantLeft, recordingStatusChanged and
* sessionDestroyed
*/
nodeCrashed,
/**
* OpenVidu Server has gracefully stopped. This is reserved for OpenVidu Pro
* restart operation. Applies to webrtcConnectionDestroyed, participantLeft,
* recordingStatusChanged and sessionDestroyed
*/
openviduServerStopped,
/**
* A recording has been stopped automatically
* (https://docs.openvidu.io/en/latest/advanced-features/recording/#automatic-stop-of-recordings).
* Applies to recordingStatusChanged
*/
automaticStop
} }

View File

@ -0,0 +1,7 @@
package io.openvidu.server.core;
public enum MediaServer {
kurento, mediasoup
}

View File

@ -153,15 +153,18 @@ public class SessionEventsHandler {
ProtocolElements.PARTICIPANTJOINED_METHOD, notifParams); ProtocolElements.PARTICIPANTJOINED_METHOD, notifParams);
} }
} }
result.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM, participant.getParticipantPublicId()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM, participant.getParticipantPublicId());
result.addProperty(ProtocolElements.PARTICIPANTJOINED_FINALUSERID_PARAM, participant.getFinalUserId()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_FINALUSERID_PARAM, participant.getFinalUserId());
result.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getActiveAt()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getActiveAt());
result.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM, participant.getFullMetadata()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM, participant.getFullMetadata());
result.add(ProtocolElements.PARTICIPANTJOINED_VALUE_PARAM, resultArray); result.add(ProtocolElements.PARTICIPANTJOINED_VALUE_PARAM, resultArray);
result.addProperty(ProtocolElements.PARTICIPANTJOINED_SESSION_PARAM, participant.getSessionId()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_SESSION_PARAM, participant.getSessionId());
result.addProperty(ProtocolElements.PARTICIPANTJOINED_VERSION_PARAM, result.addProperty(ProtocolElements.PARTICIPANTJOINED_VERSION_PARAM,
openviduBuildConfig.getOpenViduServerVersion()); openviduBuildConfig.getOpenViduServerVersion());
result.addProperty(ProtocolElements.PARTICIPANTJOINED_MEDIASERVER_PARAM,
this.openviduConfig.getMediaServer().name());
if (participant.getToken() != null) { if (participant.getToken() != null) {
result.addProperty(ProtocolElements.PARTICIPANTJOINED_RECORD_PARAM, participant.getToken().record()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_RECORD_PARAM, participant.getToken().record());
if (participant.getToken().getRole() != null) { if (participant.getToken().getRole() != null) {
@ -638,6 +641,10 @@ public class SessionEventsHandler {
rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, new JsonObject()); rpcNotificationService.sendResponse(participant.getParticipantPrivateId(), transactionId, new JsonObject());
} }
public void onEcho(String participantPrivateId, Integer transactionId) {
rpcNotificationService.sendResponse(participantPrivateId, transactionId, new JsonObject());
}
/** /**
* This handler must be called before cleaning any sessions or recordings hosted * This handler must be called before cleaning any sessions or recordings hosted
* by the crashed Media Node * by the crashed Media Node

View File

@ -116,15 +116,8 @@ public abstract class SessionManager {
public abstract void prepareSubscription(Participant participant, String senderPublicId, boolean reconnect, public abstract void prepareSubscription(Participant participant, String senderPublicId, boolean reconnect,
Integer id); Integer id);
// TODO: REMOVE ON 2.18.0 public abstract void subscribe(Participant participant, String senderName, String sdpString, Integer transactionId,
public abstract void subscribe(Participant participant, String senderName, String sdpAnwser, Integer transactionId, boolean initByServer);
boolean is2180);
// END TODO
// TODO: UNCOMMENT ON 2.18.0
// public abstract void subscribe(Participant participant, String senderName,
// String sdpAnwser, Integer transactionId);
// END TODO
public abstract void unsubscribe(Participant participant, String senderName, Integer transactionId); public abstract void unsubscribe(Participant participant, String senderName, Integer transactionId);
@ -180,13 +173,11 @@ public abstract class SessionManager {
public abstract Participant publishIpcam(Session session, MediaOptions mediaOptions, public abstract Participant publishIpcam(Session session, MediaOptions mediaOptions,
ConnectionProperties connectionProperties) throws Exception; ConnectionProperties connectionProperties) throws Exception;
public abstract void reconnectStream(Participant participant, String streamId, String sdpOffer, public abstract void reconnectPublisher(Participant participant, String streamId, String sdpOffer,
Integer transactionId); Integer transactionId);
// TODO: REMOVE ON 2.18.0 public abstract void reconnectSubscriber(Participant participant, String streamId, String sdpString,
public abstract void reconnectStream2170(Participant participant, String streamId, String sdpOffer, Integer transactionId, boolean initByServer);
Integer transactionId);
// END TODO
public abstract String getParticipantPrivateIdFromStreamId(String sessionId, String streamId) public abstract String getParticipantPrivateIdFromStreamId(String sessionId, String streamId)
throws OpenViduException; throws OpenViduException;
@ -194,6 +185,10 @@ public abstract class SessionManager {
public abstract void onVideoData(Participant participant, Integer transactionId, Integer height, Integer width, public abstract void onVideoData(Participant participant, Integer transactionId, Integer height, Integer width,
Boolean videoActive, Boolean audioActive); Boolean videoActive, Boolean audioActive);
public void onEcho(String participantPrivateId, Integer requestId) {
sessionEventsHandler.onEcho(participantPrivateId, requestId);
}
/** /**
* Returns a Session given its id * Returns a Session given its id
* *
@ -308,8 +303,10 @@ public abstract class SessionManager {
} }
public Session storeSessionNotActive(String sessionId, SessionProperties sessionProperties) { public Session storeSessionNotActive(String sessionId, SessionProperties sessionProperties) {
Session sessionNotActive = new Session(sessionId, sessionProperties, openviduConfig, recordingManager); Session sessionNotActive = this
return this.storeSessionNotActive(sessionNotActive); .storeSessionNotActive(new Session(sessionId, sessionProperties, openviduConfig, recordingManager));
sessionEventsHandler.onSessionCreated(sessionNotActive);
return sessionNotActive;
} }
public Session storeSessionNotActive(Session sessionNotActive) { public Session storeSessionNotActive(Session sessionNotActive) {

View File

@ -24,7 +24,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.Lock;
import java.util.function.Function; import java.util.function.Function;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
@ -220,240 +220,100 @@ public class KurentoParticipant extends Participant {
} }
KurentoParticipant kSender = (KurentoParticipant) sender; KurentoParticipant kSender = (KurentoParticipant) sender;
if (kSender.streaming && kSender.getPublisher() != null) {
if (kSender.streaming && kSender.getPublisher() != null final Lock closingReadLock = kSender.getPublisher().closingLock.readLock();
&& kSender.getPublisher().closingLock.readLock().tryLock()) { if (closingReadLock.tryLock()) {
try {
log.debug("PARTICIPANT {}: Creating a subscriber endpoint to user {}", this.getParticipantPublicId(),
senderName);
SubscriberEndpoint subscriber = getNewOrExistingSubscriber(senderName);
try { try {
CountDownLatch subscriberLatch = new CountDownLatch(1);
Endpoint oldMediaEndpoint = subscriber.createEndpoint(subscriberLatch); SubscriberEndpoint subscriber = initializeSubscriberEndpoint(kSender);
try { try {
if (!subscriberLatch.await(KurentoSession.ASYNC_LATCH_TIMEOUT, TimeUnit.SECONDS)) { String sdpOffer = subscriber.prepareSubscription(kSender.getPublisher());
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE, log.trace("PARTICIPANT {}: Subscribing SdpOffer is {}", this.getParticipantPublicId(),
"Timeout reached when creating subscriber endpoint"); sdpOffer);
} log.info("PARTICIPANT {}: offer prepared to receive media from {} in room {}",
} catch (InterruptedException e) { this.getParticipantPublicId(), senderName, this.session.getSessionId());
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE, return sdpOffer;
"Interrupted when creating subscriber endpoint: " + e.getMessage()); } catch (KurentoServerException e) {
} log.error("Exception preparing subscriber endpoint for user {}: {}",
if (oldMediaEndpoint != null) { this.getParticipantPublicId(), e.getMessage());
log.warn( this.subscribers.remove(senderName);
"PARTICIPANT {}: Two threads are trying to create at " releaseSubscriberEndpoint(senderName, (KurentoParticipant) sender, subscriber, null, false);
+ "the same time a subscriber endpoint for user {}",
this.getParticipantPublicId(), senderName);
return null; return null;
} }
if (subscriber.getEndpoint() == null) { } finally {
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE, closingReadLock.unlock();
"Unable to create subscriber endpoint");
}
String subscriberEndpointName = calculateSubscriberEndpointName(kSender);
subscriber.setEndpointName(subscriberEndpointName);
subscriber.getEndpoint().setName(subscriberEndpointName);
subscriber.setStreamId(kSender.getPublisherStreamId());
endpointConfig.addEndpointListeners(subscriber, "subscriber");
} catch (OpenViduException e) {
this.subscribers.remove(senderName);
throw e;
} }
log.debug("PARTICIPANT {}: Created subscriber endpoint for user {}", this.getParticipantPublicId(),
senderName);
try {
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");
} }
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 receiveMediaFrom2180(Participant sender, String sdpAnswer, boolean silent) { public String receiveMedia(Participant sender, String sdpString, boolean silent, boolean initByServer) {
final String senderName = sender.getParticipantPublicId(); final String senderName = sender.getParticipantPublicId();
log.info("PARTICIPANT {}: Request to receive media from {} in room {}", this.getParticipantPublicId(), log.info("PARTICIPANT {}: Request to receive media from {} in room {}", this.getParticipantPublicId(),
senderName, this.session.getSessionId()); senderName, this.session.getSessionId());
log.trace("PARTICIPANT {}: SdpAnswer for {} is {}", this.getParticipantPublicId(), senderName, sdpAnswer); log.trace("PARTICIPANT {}: Sdp string for {} is {}", this.getParticipantPublicId(), senderName, sdpString);
if (senderName.equals(this.getParticipantPublicId())) { if (senderName.equals(this.getParticipantPublicId())) {
log.warn("PARTICIPANT {}: trying to configure loopback by subscribing", 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"); throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE, "Can loopback only when publishing media");
} }
KurentoParticipant kSender = (KurentoParticipant) sender; KurentoParticipant kSender = (KurentoParticipant) sender;
if (kSender.streaming && kSender.getPublisher() != null) {
if (kSender.streaming && kSender.getPublisher() != null final Lock closingReadLock = kSender.getPublisher().closingLock.readLock();
&& kSender.getPublisher().closingLock.readLock().tryLock()) { if (closingReadLock.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 { try {
subscriber.subscribe(sdpAnswer, kSender.getPublisher());
log.info("PARTICIPANT {}: Is now receiving video from {} in room {}", this.getParticipantPublicId(),
senderName, this.session.getSessionId());
if (!silent // If initialized by server SubscriberEndpoint was created on
&& !ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(this.getParticipantPublicId())) { // prepareReceiveMediaFrom. If initialized by client must be created now
endpointConfig.getCdr().recordNewSubscriber(this, sender.getPublisherStreamId(), final SubscriberEndpoint subscriber = initByServer ? getSubscriber(senderName)
sender.getParticipantPublicId(), subscriber.createdAt()); : initializeSubscriberEndpoint(kSender);
}
} catch (KurentoServerException e) {
// TODO Check object status when KurentoClient sets this info in the object
if (e.getCode() == 40101) {
log.warn(
"Publisher endpoint was already released when trying to connect a subscriber endpoint to it",
e);
} else {
log.error("Exception connecting subscriber endpoint to publisher endpoint", e);
}
this.subscribers.remove(senderName);
releaseSubscriberEndpoint(senderName, (KurentoParticipant) sender, subscriber, null, false);
}
} 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 String receiveMediaFrom2170(Participant sender, String sdpOffer, 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 {}: SdpOffer for {} is {}", this.getParticipantPublicId(), senderName, sdpOffer);
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 {
log.debug("PARTICIPANT {}: Creating a subscriber endpoint to user {}", this.getParticipantPublicId(),
senderName);
SubscriberEndpoint subscriber = getNewOrExistingSubscriber(senderName);
try {
CountDownLatch subscriberLatch = new CountDownLatch(1);
Endpoint oldMediaEndpoint = subscriber.createEndpoint(subscriberLatch);
try {
if (!subscriberLatch.await(KurentoSession.ASYNC_LATCH_TIMEOUT, TimeUnit.SECONDS)) {
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
"Timeout reached when creating subscriber endpoint");
}
} catch (InterruptedException e) {
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
"Interrupted when creating subscriber endpoint: " + e.getMessage());
}
if (oldMediaEndpoint != null) {
log.warn(
"PARTICIPANT {}: Two threads are trying to create at "
+ "the same time a subscriber endpoint for user {}",
this.getParticipantPublicId(), senderName);
return null;
}
if (subscriber.getEndpoint() == null) { if (subscriber.getEndpoint() == null) {
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE, throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
"Unable to create subscriber endpoint"); "Unable to create subscriber endpoint");
} }
try {
String subscriberEndpointName = calculateSubscriberEndpointName(kSender); String sdpAnswer = subscriber.subscribe(sdpString, kSender.getPublisher());
log.info("PARTICIPANT {}: Is now receiving video from {} in room {}",
subscriber.setEndpointName(subscriberEndpointName); this.getParticipantPublicId(), senderName, this.session.getSessionId());
subscriber.getEndpoint().setName(subscriberEndpointName); if (!silent && !ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
subscriber.setStreamId(kSender.getPublisherStreamId()); .equals(this.getParticipantPublicId())) {
endpointConfig.getCdr().recordNewSubscriber(this, sender.getPublisherStreamId(),
endpointConfig.addEndpointListeners(subscriber, "subscriber"); sender.getParticipantPublicId(), subscriber.createdAt());
}
} catch (OpenViduException e) { return sdpAnswer;
this.subscribers.remove(senderName); } catch (KurentoServerException e) {
throw e; // TODO Check object status when KurentoClient sets this info in the object
if (e.getCode() == 40101) {
log.warn(
"Publisher endpoint was already released when trying to connect a subscriber endpoint to it",
e);
} else {
log.error("Exception connecting subscriber endpoint to publisher endpoint", e);
}
this.subscribers.remove(senderName);
releaseSubscriberEndpoint(senderName, (KurentoParticipant) sender, subscriber, null, false);
return null;
}
} finally {
closingReadLock.unlock();
} }
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);
log.info("PARTICIPANT {}: Is now receiving video from {} in room {}", this.getParticipantPublicId(),
senderName, this.session.getSessionId());
if (!silent
&& !ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(this.getParticipantPublicId())) {
endpointConfig.getCdr().recordNewSubscriber(this, 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) {
log.warn(
"Publisher endpoint was already released when trying to connect a subscriber endpoint to it",
e);
} else {
log.error("Exception connecting subscriber endpoint to publisher endpoint", e);
}
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");
} }
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 cancelReceivingMedia(KurentoParticipant senderKurentoParticipant, EndReason reason, boolean silent) { public void cancelReceivingMedia(KurentoParticipant senderKurentoParticipant, EndReason reason, boolean silent) {
@ -461,7 +321,8 @@ public class KurentoParticipant extends Participant {
final PublisherEndpoint pub = senderKurentoParticipant.publisher; final PublisherEndpoint pub = senderKurentoParticipant.publisher;
if (pub != null) { if (pub != null) {
try { try {
if (pub.closingLock.writeLock().tryLock(15, TimeUnit.SECONDS)) { final Lock closingWriteLock = pub.closingLock.writeLock();
if (closingWriteLock.tryLock(15, TimeUnit.SECONDS)) {
try { try {
log.info("PARTICIPANT {}: cancel receiving media from {}", this.getParticipantPublicId(), log.info("PARTICIPANT {}: cancel receiving media from {}", this.getParticipantPublicId(),
senderName); senderName);
@ -478,7 +339,7 @@ public class KurentoParticipant extends Participant {
this.getParticipantPublicId(), senderName, this.session.getSessionId()); this.getParticipantPublicId(), senderName, this.session.getSessionId());
} }
} finally { } finally {
pub.closingLock.writeLock().unlock(); closingWriteLock.unlock();
} }
} else { } else {
log.error( log.error(
@ -585,15 +446,66 @@ public class KurentoParticipant extends Participant {
return this.getParticipantPublicId() + "_" + senderParticipant.getPublisherStreamId(); return this.getParticipantPublicId() + "_" + senderParticipant.getPublisherStreamId();
} }
private SubscriberEndpoint initializeSubscriberEndpoint(Participant kSender) {
String senderName = kSender.getParticipantPublicId();
log.debug("PARTICIPANT {}: Creating a subscriber endpoint to user {}", this.getParticipantPublicId(),
senderName);
SubscriberEndpoint subscriber = getNewOrExistingSubscriber(senderName);
try {
CountDownLatch subscriberLatch = new CountDownLatch(1);
Endpoint oldMediaEndpoint = subscriber.createEndpoint(subscriberLatch);
try {
if (!subscriberLatch.await(KurentoSession.ASYNC_LATCH_TIMEOUT, TimeUnit.SECONDS)) {
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
"Timeout reached when creating subscriber endpoint");
}
} catch (InterruptedException e) {
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE,
"Interrupted when creating subscriber endpoint: " + e.getMessage());
}
if (oldMediaEndpoint != null) {
log.warn(
"PARTICIPANT {}: Two threads are trying to create at "
+ "the same time a subscriber endpoint for user {}",
this.getParticipantPublicId(), senderName);
return null;
}
if (subscriber.getEndpoint() == null) {
throw new OpenViduException(Code.MEDIA_ENDPOINT_ERROR_CODE, "Unable to create subscriber endpoint");
}
String subscriberEndpointName = calculateSubscriberEndpointName(kSender);
subscriber.setEndpointName(subscriberEndpointName);
subscriber.getEndpoint().setName(subscriberEndpointName);
subscriber.setStreamId(kSender.getPublisherStreamId());
endpointConfig.addEndpointListeners(subscriber, "subscriber");
} catch (OpenViduException e) {
this.subscribers.remove(senderName);
throw e;
}
log.debug("PARTICIPANT {}: Created subscriber endpoint for user {}", this.getParticipantPublicId(), senderName);
return subscriber;
}
private void releasePublisherEndpoint(EndReason reason, Long kmsDisconnectionTime) { private void releasePublisherEndpoint(EndReason reason, Long kmsDisconnectionTime) {
if (publisher != null && publisher.getEndpoint() != null) { if (publisher != null && publisher.getEndpoint() != null) {
final ReadWriteLock closingLock = publisher.closingLock;
try { try {
if (closingLock.writeLock().tryLock(15, TimeUnit.SECONDS)) { final Lock closingWriteLock = publisher.closingLock.writeLock();
if (closingWriteLock.tryLock(15, TimeUnit.SECONDS)) {
try { try {
this.releasePublisherEndpointAux(reason, kmsDisconnectionTime); this.releasePublisherEndpointAux(reason, kmsDisconnectionTime);
} finally { } finally {
closingLock.writeLock().unlock(); closingWriteLock.unlock();
} }
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {

View File

@ -31,6 +31,8 @@ import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.annotation.PreDestroy;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.kurento.client.GenericMediaElement; import org.kurento.client.GenericMediaElement;
import org.kurento.client.IceCandidate; import org.kurento.client.IceCandidate;
@ -65,6 +67,7 @@ import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.FinalUser; import io.openvidu.server.core.FinalUser;
import io.openvidu.server.core.IdentifierPrefixes; import io.openvidu.server.core.IdentifierPrefixes;
import io.openvidu.server.core.MediaOptions; import io.openvidu.server.core.MediaOptions;
import io.openvidu.server.core.MediaServer;
import io.openvidu.server.core.Participant; import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session; import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.SessionManager;
@ -111,9 +114,13 @@ public class KurentoSessionManager extends SessionManager {
if (sessionNotActive == null && this.isInsecureParticipant(participant.getParticipantPrivateId())) { if (sessionNotActive == null && this.isInsecureParticipant(participant.getParticipantPrivateId())) {
// Insecure user directly call joinRoom RPC method, without REST API use // Insecure user directly call joinRoom RPC method, without REST API use
sessionNotActive = new Session(sessionId, new SessionProperties.Builder() SessionProperties.Builder builder = new SessionProperties.Builder().mediaMode(MediaMode.ROUTED)
.mediaMode(MediaMode.ROUTED).recordingMode(RecordingMode.ALWAYS).build(), openviduConfig, .recordingMode(RecordingMode.ALWAYS);
recordingManager); // forcedVideoCodec to NONE if mediasoup
if (MediaServer.mediasoup.equals(openviduConfig.getMediaServer())) {
builder.forcedVideoCodec(VideoCodec.NONE);
}
sessionNotActive = new Session(sessionId, builder.build(), openviduConfig, recordingManager);
} }
try { try {
@ -385,7 +392,7 @@ public class KurentoSessionManager extends SessionManager {
// Modify sdp if forced codec is defined // Modify sdp if forced codec is defined
if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) {
kurentoOptions.sdpOffer = sdpMunging.forceCodec(kurentoOptions.sdpOffer, participant, true, false, kurentoOptions.sdpOffer = sdpMunging.forceCodec(kurentoOptions.sdpOffer, participant, true, false,
isTranscodingAllowed, forcedVideoCodec, false); isTranscodingAllowed, forcedVideoCodec);
CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.client, CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.publish, WebrtcDebugEventType.sdpOfferMunged, kurentoOptions.sdpOffer)); WebrtcDebugEventOperation.publish, WebrtcDebugEventType.sdpOfferMunged, kurentoOptions.sdpOffer));
} }
@ -573,7 +580,7 @@ public class KurentoSessionManager extends SessionManager {
// Modify server's SDPOffer if forced codec is defined // Modify server's SDPOffer if forced codec is defined
if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) {
sdpOffer = sdpMunging.forceCodec(sdpOffer, participant, false, false, isTranscodingAllowed, sdpOffer = sdpMunging.forceCodec(sdpOffer, participant, false, false, isTranscodingAllowed,
forcedVideoCodec, true); forcedVideoCodec);
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.server, CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.server,
WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpOfferMunged, sdpOffer)); WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpOfferMunged, sdpOffer));
@ -594,12 +601,12 @@ public class KurentoSessionManager extends SessionManager {
} }
@Override @Override
public void subscribe(Participant participant, String senderName, String sdpAnswer, Integer transactionId, public void subscribe(Participant participant, String senderName, String sdpString, Integer transactionId,
boolean is2180) { boolean initByServer) {
Session session = null; Session session = null;
try { try {
log.debug("Request [SUBSCRIBE] remoteParticipant={} sdpAnswer={} ({})", senderName, sdpAnswer, log.debug("Request [SUBSCRIBE] remoteParticipant={} sdpString={} ({})", senderName, sdpString,
participant.getParticipantPublicId()); participant.getParticipantPublicId());
KurentoParticipant kParticipant = (KurentoParticipant) participant; KurentoParticipant kParticipant = (KurentoParticipant) participant;
@ -625,48 +632,44 @@ public class KurentoSessionManager extends SessionManager {
String subscriberEndpointName = kParticipant.calculateSubscriberEndpointName(senderParticipant); String subscriberEndpointName = kParticipant.calculateSubscriberEndpointName(senderParticipant);
// TODO: REMOVE ON 2.18.0 if (initByServer) {
if (is2180) {
// Server initiated negotiation. sdpString is the SDP Answer of the client
// Client's SDPAnswer to the server's SDPOffer
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client, CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpAnswer, sdpAnswer)); WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpAnswer, sdpString));
kParticipant.receiveMediaFrom2180(senderParticipant, sdpAnswer, false); kParticipant.receiveMedia(senderParticipant, sdpString, false, true);
sessionEventsHandler.onSubscribe(participant, session, transactionId, null); sessionEventsHandler.onSubscribe(participant, session, transactionId, null);
} else { } else {
// Client initiated negotiation. sdpString is the SDP Offer of the client
boolean isTranscodingAllowed = session.getSessionProperties().isTranscodingAllowed(); boolean isTranscodingAllowed = session.getSessionProperties().isTranscodingAllowed();
VideoCodec forcedVideoCodec = session.getSessionProperties().forcedVideoCodec(); VideoCodec forcedVideoCodec = session.getSessionProperties().forcedVideoCodec();
String sdpOffer = sdpString;
// Modify sdp if forced codec is defined // Modify sdp if forced codec is defined
if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) {
sdpAnswer = sdpMunging.forceCodec(sdpAnswer, participant, false, false, isTranscodingAllowed, sdpOffer = sdpMunging.forceCodec(sdpString, participant, false, false, isTranscodingAllowed,
forcedVideoCodec, false); forcedVideoCodec);
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client, CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpOfferMunged, sdpAnswer)); WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpOfferMunged, sdpOffer));
} }
String finalSdpAnswer = kParticipant.receiveMediaFrom2170(senderParticipant, sdpAnswer, false); String sdpAnswer = kParticipant.receiveMedia(senderParticipant, sdpOffer, false, false);
if (finalSdpAnswer == null) { if (sdpAnswer == null) {
throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE,
"Unable to generate SDP answer when subscribing '" + participant.getParticipantPublicId() "Unable to generate SDP answer when subscribing '" + participant.getParticipantPublicId()
+ "' to '" + senderName + "'"); + "' to '" + senderName + "'");
} }
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.server, CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.server,
WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpAnswer, finalSdpAnswer)); WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
sessionEventsHandler.onSubscribe(participant, session, finalSdpAnswer, transactionId, null); sessionEventsHandler.onSubscribe(participant, session, sdpAnswer, transactionId, null);
} }
// END TODO
// TODO: UNCOMMENT ON 2.18.0
// CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
// WebrtcDebugEventOperation.subscribe, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
// String remoteSdpAnswer = kParticipant.receiveMediaFrom(senderParticipant, sdpAnswer, false);
// sessionEventsHandler.onSubscribe(participant, session, transactionId, null);
// END TODO
} catch (OpenViduException e) { } catch (OpenViduException e) {
log.error("PARTICIPANT {}: Error subscribing to {}", participant.getParticipantPublicId(), senderName, e); log.error("PARTICIPANT {}: Error subscribing to {}", participant.getParticipantPublicId(), senderName, e);
@ -774,7 +777,6 @@ public class KurentoSessionManager extends SessionManager {
log.info("No session '{}' exists yet. Created one on KMS '{}' with ip '{}'", session.getSessionId(), log.info("No session '{}' exists yet. Created one on KMS '{}' with ip '{}'", session.getSessionId(),
kms.getId(), kms.getIp()); kms.getId(), kms.getIp());
sessionEventsHandler.onSessionCreated(session);
return session; return session;
} }
@ -1176,184 +1178,127 @@ public class KurentoSessionManager extends SessionManager {
return kParticipant; return kParticipant;
} }
// TODO: REMOVE ON 2.18.0
@Override @Override
public void reconnectStream2170(Participant participant, String streamId, String sdpOffer, Integer transactionId) { public void reconnectPublisher(Participant participant, String streamId, String sdpString, Integer transactionId) {
KurentoParticipant kParticipant = (KurentoParticipant) participant; KurentoParticipant kParticipant = (KurentoParticipant) participant;
KurentoSession kSession = kParticipant.getSession(); KurentoSession kSession = kParticipant.getSession();
boolean isPublisher = streamId.equals(participant.getPublisherStreamId()); reconnectPublisher(kSession, kParticipant, streamId, sdpString, transactionId);
}
@Override
public void reconnectSubscriber(Participant participant, String streamId, String sdpString, Integer transactionId,
boolean initByServer) {
KurentoParticipant kParticipant = (KurentoParticipant) participant;
KurentoSession kSession = kParticipant.getSession();
reconnectSubscriber(kSession, kParticipant, streamId, sdpString, transactionId, initByServer);
}
private String mungeSdpOffer(Session kSession, Participant participant, String sdpOffer, boolean isPublisher) {
boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed(); boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed();
VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec(); VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec();
boolean sdpOfferHasBeenMunged = false;
String originalSdpOffer = sdpOffer;
// Modify sdp if forced codec is defined // Modify sdp if forced codec is defined
if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) {
sdpOfferHasBeenMunged = true; return sdpMunging.forceCodec(sdpOffer, participant, isPublisher, true, isTranscodingAllowed,
sdpOffer = sdpMunging.forceCodec(sdpOffer, participant, isPublisher, true, isTranscodingAllowed, forcedVideoCodec);
forcedVideoCodec, false);
} }
return null;
}
if (isPublisher) { private void reconnectPublisher(KurentoSession kSession, KurentoParticipant kParticipant, String streamId,
String sdpOffer, Integer transactionId) {
CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.client, String sdpOfferMunged = mungeSdpOffer(kSession, kParticipant, sdpOffer, true);
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpOffer, originalSdpOffer));
if (sdpOfferHasBeenMunged) {
CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpOfferMunged, sdpOffer));
}
// Reconnect publisher CDR.log(new WebrtcDebugEvent(kParticipant, streamId, WebrtcDebugEventIssuer.client,
final KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) kParticipant.getPublisher() WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpOffer, sdpOffer));
.getMediaOptions(); if (sdpOfferMunged != null) {
sdpOffer = sdpOfferMunged;
CDR.log(new WebrtcDebugEvent(kParticipant, streamId, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpOfferMunged, sdpOffer));
}
// Reconnect publisher
final KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) kParticipant.getPublisher().getMediaOptions();
// 1) Disconnect broken PublisherEndpoint from its PassThrough
PublisherEndpoint publisher = kParticipant.getPublisher();
final PassThrough passThru = publisher.disconnectFromPassThrough();
// 2) Destroy the broken PublisherEndpoint and nothing else
publisher.cancelStatsLoop.set(true);
kParticipant.releaseElement(kParticipant.getParticipantPublicId(), publisher.getEndpoint());
// 3) Create a new PublisherEndpoint connecting it to the previous PassThrough
kParticipant.resetPublisherEndpoint(kurentoOptions, passThru);
kParticipant.createPublishingEndpoint(kurentoOptions, streamId);
String sdpAnswer = kParticipant.publishToRoom(sdpOffer, kurentoOptions.doLoopback, true);
log.debug("SDP Answer for publishing reconnection PARTICIPANT {}: {}", kParticipant.getParticipantPublicId(),
sdpAnswer);
CDR.log(new WebrtcDebugEvent(kParticipant, streamId, WebrtcDebugEventIssuer.server,
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
sessionEventsHandler.onPublishMedia(kParticipant, kParticipant.getPublisherStreamId(),
kParticipant.getPublisher().createdAt(), kSession.getSessionId(), kurentoOptions, sdpAnswer,
new HashSet<Participant>(), transactionId, null);
}
// 1) Disconnect broken PublisherEndpoint from its PassThrough private void reconnectSubscriber(KurentoSession kSession, KurentoParticipant kParticipant, String streamId,
PublisherEndpoint publisher = kParticipant.getPublisher(); String sdpString, Integer transactionId, boolean initByServer) {
final PassThrough passThru = publisher.disconnectFromPassThrough();
// 2) Destroy the broken PublisherEndpoint and nothing else String senderPrivateId = kSession.getParticipantPrivateIdFromStreamId(streamId);
publisher.cancelStatsLoop.set(true); if (senderPrivateId != null) {
kParticipant.releaseElement(participant.getParticipantPublicId(), publisher.getEndpoint());
// 3) Create a new PublisherEndpoint connecting it to the previous PassThrough KurentoParticipant sender = (KurentoParticipant) kSession.getParticipantByPrivateId(senderPrivateId);
kParticipant.resetPublisherEndpoint(kurentoOptions, passThru); String subscriberEndpointName = kParticipant.calculateSubscriberEndpointName(sender);
kParticipant.createPublishingEndpoint(kurentoOptions, streamId);
String sdpAnswer = kParticipant.publishToRoom(sdpOffer, kurentoOptions.doLoopback, true);
log.debug("SDP Answer for publishing reconnection PARTICIPANT {}: {}", participant.getParticipantPublicId(),
sdpAnswer);
CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.server, if (initByServer) {
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(), // Server initiated negotiation
kParticipant.getPublisher().createdAt(), kSession.getSessionId(), kurentoOptions, sdpAnswer,
new HashSet<Participant>(), transactionId, null);
} else { final String sdpAnswer = sdpString;
// Reconnect subscriber CDR.log(new WebrtcDebugEvent(kParticipant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
String senderPrivateId = kSession.getParticipantPrivateIdFromStreamId(streamId); WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
if (senderPrivateId != null) {
KurentoParticipant sender = (KurentoParticipant) kSession.getParticipantByPrivateId(senderPrivateId); kParticipant.receiveMedia(sender, sdpAnswer, true, true);
String subscriberEndpointName = kParticipant.calculateSubscriberEndpointName(sender);
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client, log.debug("SDP Answer for subscribing reconnection PARTICIPANT {}: {}",
WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpOffer, kParticipant.getParticipantPublicId(), sdpAnswer);
originalSdpOffer));
if (sdpOfferHasBeenMunged) { sessionEventsHandler.onSubscribe(kParticipant, kSession, sdpAnswer, transactionId, null);
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
} else {
// Client initiated negotiation
String sdpOffer = sdpString;
CDR.log(new WebrtcDebugEvent(kParticipant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpOffer, sdpOffer));
String sdpOfferMunged = mungeSdpOffer(kSession, kParticipant, sdpOffer, false);
if (sdpOfferMunged != null) {
sdpOffer = sdpOfferMunged;
CDR.log(new WebrtcDebugEvent(kParticipant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpOfferMunged, WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpOfferMunged,
sdpOffer)); sdpOffer));
} }
String sdpAnswer = kParticipant.receiveMediaFrom2170(sender, sdpOffer, true); kParticipant.cancelReceivingMedia(sender, null, true);
String sdpAnswer = kParticipant.receiveMedia(sender, sdpOffer, true, false);
if (sdpAnswer == null) { if (sdpAnswer == null) {
throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE,
"Unable to generate SDP answer when reconnecting subscriber to '" + streamId + "'"); "Unable to generate SDP answer when reconnecting subscriber to '" + streamId + "'");
} }
log.debug("SDP Answer for subscribing reconnection PARTICIPANT {}: {}", log.debug("SDP Answer for subscribing reconnection PARTICIPANT {}: {}",
participant.getParticipantPublicId(), sdpAnswer); kParticipant.getParticipantPublicId(), sdpAnswer);
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.server, CDR.log(new WebrtcDebugEvent(kParticipant, subscriberEndpointName, WebrtcDebugEventIssuer.server,
WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpAnswer, sdpAnswer)); WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
sessionEventsHandler.onSubscribe(participant, kSession, sdpAnswer, transactionId, null); sessionEventsHandler.onSubscribe(kParticipant, kSession, sdpAnswer, transactionId, null);
} else {
throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE,
"Stream '" + streamId + "' does not exist in Session '" + kSession.getSessionId() + "'");
} }
}
}
// END TODO
@Override
public void reconnectStream(Participant participant, String streamId, String sdpOfferOrAnswer,
Integer transactionId) {
KurentoParticipant kParticipant = (KurentoParticipant) participant;
KurentoSession kSession = kParticipant.getSession();
boolean isPublisher = streamId.equals(participant.getPublisherStreamId());
if (isPublisher) {
// Reconnect publisher
String sdpOffer = sdpOfferOrAnswer;
boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed();
VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec();
boolean sdpOfferHasBeenMunged = false;
final String originalSdpOffer = sdpOffer;
// Modify sdp if forced codec is defined
if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) {
sdpOfferHasBeenMunged = true;
sdpOffer = sdpMunging.forceCodec(sdpOffer, participant, isPublisher, true, isTranscodingAllowed,
forcedVideoCodec, false);
}
CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpOffer, originalSdpOffer));
if (sdpOfferHasBeenMunged) {
CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpOfferMunged, sdpOffer));
}
// Reconnect publisher
final KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) kParticipant.getPublisher()
.getMediaOptions();
// 1) Disconnect broken PublisherEndpoint from its PassThrough
PublisherEndpoint publisher = kParticipant.getPublisher();
final PassThrough passThru = publisher.disconnectFromPassThrough();
// 2) Destroy the broken PublisherEndpoint and nothing else
publisher.cancelStatsLoop.set(true);
kParticipant.releaseElement(participant.getParticipantPublicId(), publisher.getEndpoint());
// 3) Create a new PublisherEndpoint connecting it to the previous PassThrough
kParticipant.resetPublisherEndpoint(kurentoOptions, passThru);
kParticipant.createPublishingEndpoint(kurentoOptions, streamId);
String sdpAnswer = kParticipant.publishToRoom(sdpOffer, kurentoOptions.doLoopback, true);
log.debug("SDP Answer for publishing reconnection PARTICIPANT {}: {}", participant.getParticipantPublicId(),
sdpAnswer);
CDR.log(new WebrtcDebugEvent(participant, streamId, WebrtcDebugEventIssuer.server,
WebrtcDebugEventOperation.reconnectPublisher, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(),
kParticipant.getPublisher().createdAt(), kSession.getSessionId(), kurentoOptions, sdpAnswer,
new HashSet<Participant>(), transactionId, null);
} else { } else {
throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE,
// Reconnect subscriber "Stream '" + streamId + "' does not exist in Session '" + kSession.getSessionId() + "'");
final String sdpAnswer = sdpOfferOrAnswer;
String senderPrivateId = kSession.getParticipantPrivateIdFromStreamId(streamId);
if (senderPrivateId != null) {
KurentoParticipant sender = (KurentoParticipant) kSession.getParticipantByPrivateId(senderPrivateId);
String subscriberEndpointName = kParticipant.calculateSubscriberEndpointName(sender);
CDR.log(new WebrtcDebugEvent(participant, subscriberEndpointName, WebrtcDebugEventIssuer.client,
WebrtcDebugEventOperation.reconnectSubscriber, WebrtcDebugEventType.sdpAnswer, sdpAnswer));
kParticipant.receiveMediaFrom2180(sender, sdpAnswer, true);
log.debug("SDP Answer for subscribing reconnection PARTICIPANT {}: {}",
participant.getParticipantPublicId(), sdpAnswer);
sessionEventsHandler.onSubscribe(participant, kSession, sdpAnswer, transactionId, null);
} else {
throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE,
"Stream '" + streamId + "' does not exist in Session '" + kSession.getSessionId() + "'");
}
} }
} }
@ -1444,4 +1389,11 @@ public class KurentoSessionManager extends SessionManager {
return lessLoadedKms; return lessLoadedKms;
} }
@PreDestroy
@Override
public void close() {
super.close();
this.kmsManager.closeAllKurentoClients();
}
} }

View File

@ -58,31 +58,24 @@ public class SubscriberEndpoint extends MediaEndpoint {
offerOptions.setOfferToReceiveVideo(publisher.getMediaOptions().hasVideo()); offerOptions.setOfferToReceiveVideo(publisher.getMediaOptions().hasVideo());
String sdpOffer = generateOffer(offerOptions); String sdpOffer = generateOffer(offerOptions);
gatherCandidates();
return sdpOffer; return sdpOffer;
} }
public synchronized String subscribe(String sdpAnswer, PublisherEndpoint publisher) { public synchronized String subscribe(String sdpString, PublisherEndpoint publisher) {
// TODO: REMOVE ON 2.18.0 if (this.publisherStreamId == null) {
if (this.createdAt == null) { // Client initiated negotiation
// 2.17.0
registerOnIceCandidateEventListener(publisher.getOwner().getParticipantPublicId()); registerOnIceCandidateEventListener(publisher.getOwner().getParticipantPublicId());
this.createdAt = System.currentTimeMillis(); this.createdAt = System.currentTimeMillis();
String realSdpAnswer = processOffer(sdpAnswer); String realSdpAnswer = processOffer(sdpString);
gatherCandidates(); gatherCandidates();
publisher.connect(this.getEndpoint(), false); publisher.connect(this.getEndpoint(), false);
this.publisherStreamId = publisher.getStreamId(); this.publisherStreamId = publisher.getStreamId();
return realSdpAnswer; return realSdpAnswer;
} else { } else {
// 2.18.0 // Server initiated negotiation
processAnswer(sdpAnswer); return processAnswer(sdpString);
gatherCandidates();
return null;
} }
// END TODO
// TODO: UNCOMMENT ON 2.18.0
// processAnswer(sdpAnswer);
// END TODO
} }
@Override @Override

View File

@ -31,7 +31,6 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.kurento.jsonrpc.client.JsonRpcWSConnectionListener; import org.kurento.jsonrpc.client.JsonRpcWSConnectionListener;
@ -343,8 +342,7 @@ public abstract class KmsManager {
@PostConstruct @PostConstruct
protected abstract void postConstructInitKurentoClients(); protected abstract void postConstructInitKurentoClients();
@PreDestroy public void closeAllKurentoClients() {
public void close() {
log.info("Closing all KurentoClients"); log.info("Closing all KurentoClients");
this.kmss.values().forEach(kms -> { this.kmss.values().forEach(kms -> {
if (kms.getKurentoClientReconnectTimer() != null) { if (kms.getKurentoClientReconnectTimer() != null) {

View File

@ -78,12 +78,18 @@ public class Recording {
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString()) RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
.outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo); .outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo);
if (RecordingUtils.IS_COMPOSED(outputMode) && hasVideo) { if (RecordingUtils.IS_COMPOSED(outputMode) && hasVideo) {
builder.resolution(json.get("resolution").getAsString()); if (json.has("resolution")) {
builder.frameRate(json.get("frameRate").getAsInt()); builder.resolution(json.get("resolution").getAsString());
RecordingLayout recordingLayout = RecordingLayout.valueOf(json.get("recordingLayout").getAsString()); }
builder.recordingLayout(recordingLayout); if (json.has("frameRate")) {
if (RecordingLayout.CUSTOM.equals(recordingLayout)) { builder.frameRate(json.get("frameRate").getAsInt());
builder.customLayout(json.get("customLayout").getAsString()); }
if (json.has("recordingLayout")) {
RecordingLayout recordingLayout = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
builder.recordingLayout(recordingLayout);
if (RecordingLayout.CUSTOM.equals(recordingLayout) && json.has("customLayout")) {
builder.customLayout(json.get("customLayout").getAsString());
}
} }
} }
this.recordingProperties = builder.build(); this.recordingProperties = builder.build();

View File

@ -64,6 +64,7 @@ import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord; import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason; import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.MediaServer;
import io.openvidu.server.core.Participant; import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session; import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionEventsHandler; import io.openvidu.server.core.SessionEventsHandler;
@ -768,6 +769,8 @@ public class RecordingManager {
// Check Kurento Media Server write permissions in recording path // Check Kurento Media Server write permissions in recording path
if (this.kmsManager.getKmss().isEmpty()) { if (this.kmsManager.getKmss().isEmpty()) {
log.warn("No KMSs were defined in KMS_URIS array. Recording path check aborted"); log.warn("No KMSs were defined in KMS_URIS array. Recording path check aborted");
} else if (MediaServer.mediasoup.equals(openviduConfig.getMediaServer())) {
log.warn("Using mediasoup. Recording path check aborted");
} else { } else {
Kms kms = null; Kms kms = null;
try { try {

View File

@ -17,12 +17,12 @@
package io.openvidu.server.rest; package io.openvidu.server.rest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController @Controller
@CrossOrigin @CrossOrigin
@RequestMapping(RequestMappings.ACCEPT_CERTIFICATE) @RequestMapping(RequestMappings.ACCEPT_CERTIFICATE)
public class CertificateRestController { public class CertificateRestController {

View File

@ -65,6 +65,7 @@ import io.openvidu.java.client.VideoCodec;
import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason; import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.IdentifierPrefixes; import io.openvidu.server.core.IdentifierPrefixes;
import io.openvidu.server.core.MediaServer;
import io.openvidu.server.core.Participant; import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session; import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.SessionManager;
@ -120,7 +121,7 @@ public class SessionRestController {
} }
Session sessionNotActive = sessionManager.storeSessionNotActive(sessionId, sessionProperties); Session sessionNotActive = sessionManager.storeSessionNotActive(sessionId, sessionProperties);
log.info("New session {} initialized {}", sessionId, this.sessionManager.getSessionsWithNotActive().stream() log.info("New session {} created {}", sessionId, this.sessionManager.getSessionsWithNotActive().stream()
.map(Session::getSessionId).collect(Collectors.toList()).toString()); .map(Session::getSessionId).collect(Collectors.toList()).toString());
return new ResponseEntity<>(sessionNotActive.toJson(false, false).toString(), RestUtils.getResponseHeaders(), return new ResponseEntity<>(sessionNotActive.toJson(false, false).toString(), RestUtils.getResponseHeaders(),
@ -756,10 +757,15 @@ public class SessionRestController {
} }
builder = builder.customSessionId(customSessionId); builder = builder.customSessionId(customSessionId);
} }
if (forcedVideoCodec != null) { // forcedVideoCodec to NONE if mediasoup
builder = builder.forcedVideoCodec(VideoCodec.valueOf(forcedVideoCodec)); if (MediaServer.mediasoup.equals(openviduConfig.getMediaServer())) {
builder = builder.forcedVideoCodec(VideoCodec.NONE);
} else { } else {
builder = builder.forcedVideoCodec(openviduConfig.getOpenviduForcedCodec()); if (forcedVideoCodec != null) {
builder = builder.forcedVideoCodec(VideoCodec.valueOf(forcedVideoCodec));
} else {
builder = builder.forcedVideoCodec(openviduConfig.getOpenviduForcedCodec());
}
} }
if (allowTranscoding != null) { if (allowTranscoding != null) {
builder = builder.allowTranscoding(allowTranscoding); builder = builder.allowTranscoding(allowTranscoding);

View File

@ -172,6 +172,9 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
case ProtocolElements.VIDEODATA_METHOD: case ProtocolElements.VIDEODATA_METHOD:
updateVideoData(rpcConnection, request); updateVideoData(rpcConnection, request);
break; break;
case ProtocolElements.ECHO_METHOD:
echo(rpcConnection, request);
break;
default: default:
log.error("Unrecognized request {}", request); log.error("Unrecognized request {}", request);
break; break;
@ -355,18 +358,7 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
String senderStreamId = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM); String senderStreamId = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM);
String senderPublicId = parseSenderPublicIdFromStreamId(senderStreamId); String senderPublicId = parseSenderPublicIdFromStreamId(senderStreamId);
boolean reconnect = false; boolean reconnect = getBooleanParam(request, ProtocolElements.PREPARERECEIVEVIDEO_RECONNECT_PARAM);
// TODO: REMOVE ON 2.18.0
if (request.getParams().has(ProtocolElements.PREPARERECEIVEVIDEO_RECONNECT_PARAM)) {
reconnect = getBooleanParam(request, ProtocolElements.PREPARERECEIVEVIDEO_RECONNECT_PARAM);
}
// END TODO
// TODO: UNCOMMENT ON 2.18.0
// boolean reconnect = getBooleanParam(request,
// ProtocolElements.PREPARERECEIVEVIDEO_RECONNECT_PARAM);
// END TODO
sessionManager.prepareSubscription(participant, senderPublicId, reconnect, request.getId()); sessionManager.prepareSubscription(participant, senderPublicId, reconnect, request.getId());
} }
@ -382,28 +374,15 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
String senderStreamId = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM); String senderStreamId = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SENDER_PARAM);
String senderPublicId = parseSenderPublicIdFromStreamId(senderStreamId); String senderPublicId = parseSenderPublicIdFromStreamId(senderStreamId);
// TODO: REMOVE ON 2.18.0
if (request.getParams().has(ProtocolElements.RECEIVEVIDEO_SDPOFFER_PARAM)) { if (request.getParams().has(ProtocolElements.RECEIVEVIDEO_SDPOFFER_PARAM)) {
// 2.17.0: initiative held by browser when subscribing // Client initiated negotiation (comes with SDP Offer)
// The request comes with an SDPOffer
String sdpOffer = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SDPOFFER_PARAM); String sdpOffer = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SDPOFFER_PARAM);
sessionManager.subscribe(participant, senderPublicId, sdpOffer, request.getId(), false); sessionManager.subscribe(participant, senderPublicId, sdpOffer, request.getId(), false);
} else if (request.getParams().has(ProtocolElements.RECEIVEVIDEO_SDPANSWER_PARAM)) { } else if (request.getParams().has(ProtocolElements.RECEIVEVIDEO_SDPANSWER_PARAM)) {
// 2.18.0: initiative held by server when subscribing // Server initiated negotiation (comes with SDP Answer)
// This is the final call after prepareReceiveVidoFrom, comes with SDPAnswer
String sdpAnswer = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SDPANSWER_PARAM); String sdpAnswer = getStringParam(request, ProtocolElements.RECEIVEVIDEO_SDPANSWER_PARAM);
sessionManager.subscribe(participant, senderPublicId, sdpAnswer, request.getId(), true); sessionManager.subscribe(participant, senderPublicId, sdpAnswer, request.getId(), true);
} }
// END TODO
// TODO: UNCOMMENT ON 2.18.0
/*
* String sdpAnswer = getStringParam(request,
* ProtocolElements.RECEIVEVIDEO_SDPANSWER_PARAM);
* sessionManager.subscribe(participant, senderPublicId, sdpAnswer,
* request.getId());
*/
// END TODO
} }
private void unsubscribeFromVideo(RpcConnection rpcConnection, Request<JsonObject> request) { private void unsubscribeFromVideo(RpcConnection rpcConnection, Request<JsonObject> request) {
@ -667,40 +646,28 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
} catch (OpenViduException e) { } catch (OpenViduException e) {
return; return;
} }
String streamId = getStringParam(request, ProtocolElements.RECONNECTSTREAM_STREAM_PARAM); String streamId = getStringParam(request, ProtocolElements.RECONNECTSTREAM_STREAM_PARAM);
boolean isPublisher = streamId.equals(participant.getPublisherStreamId());
// TODO: REMOVE ON 2.18.0 String sdpString = null;
if (request.getParams().has(ProtocolElements.RECONNECTSTREAM_SDPOFFER_PARAM)) { if (request.getParams().has(ProtocolElements.RECONNECTSTREAM_SDPOFFER_PARAM)) {
// 2.17.0 sdpString = getStringParam(request, ProtocolElements.RECONNECTSTREAM_SDPOFFER_PARAM);
try {
String sdpOffer = getStringParam(request, ProtocolElements.RECONNECTSTREAM_SDPOFFER_PARAM);
sessionManager.reconnectStream(participant, streamId, sdpOffer, request.getId());
} catch (OpenViduException e) {
this.notificationService.sendErrorResponse(participant.getParticipantPrivateId(), request.getId(),
new JsonObject(), e);
}
} else if (request.getParams().has(ProtocolElements.RECONNECTSTREAM_SDPSTRING_PARAM)) { } else if (request.getParams().has(ProtocolElements.RECONNECTSTREAM_SDPSTRING_PARAM)) {
// 2.18.0 sdpString = getStringParam(request, ProtocolElements.RECONNECTSTREAM_SDPSTRING_PARAM);
String sdpString = getStringParam(request, ProtocolElements.RECONNECTSTREAM_SDPSTRING_PARAM);
try {
sessionManager.reconnectStream(participant, streamId, sdpString, request.getId());
} catch (OpenViduException e) {
this.notificationService.sendErrorResponse(participant.getParticipantPrivateId(), request.getId(),
new JsonObject(), e);
}
} }
// END TODO
// TODO: UNCOMMENT ON 2.18.0 try {
/* if (isPublisher) {
* String sdpString = getStringParam(request, sessionManager.reconnectPublisher(participant, streamId, sdpString, request.getId());
* ProtocolElements.RECONNECTSTREAM_SDPSTRING_PARAM); try { } else {
* sessionManager.reconnectStream(participant, streamId, sdpString, boolean initByServer = request.getParams().has(ProtocolElements.RECONNECTSTREAM_SDPSTRING_PARAM);
* request.getId()); } catch (OpenViduException e) { sessionManager.reconnectSubscriber(participant, streamId, sdpString, request.getId(), initByServer);
* this.notificationService.sendErrorResponse(participant. }
* getParticipantPrivateId(), request.getId(), new JsonObject(), e); } } catch (OpenViduException e) {
*/ this.notificationService.sendErrorResponse(participant.getParticipantPrivateId(), request.getId(),
// END TODO new JsonObject(), e);
}
} }
private void updateVideoData(RpcConnection rpcConnection, Request<JsonObject> request) { private void updateVideoData(RpcConnection rpcConnection, Request<JsonObject> request) {
@ -717,6 +684,10 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
} }
} }
private void echo(RpcConnection rpcConnection, Request<JsonObject> request) {
sessionManager.onEcho(rpcConnection.getParticipantPrivateId(), request.getId());
}
public void leaveRoomAfterConnClosed(String participantPrivateId, EndReason reason) { public void leaveRoomAfterConnClosed(String participantPrivateId, EndReason reason) {
try { try {
sessionManager.evictParticipant(this.sessionManager.getParticipant(participantPrivateId), null, null, sessionManager.evictParticipant(this.sessionManager.getParticipant(participantPrivateId), null, null,

View File

@ -68,7 +68,7 @@ public class SDPMunging {
* ordering of formats. Browsers (tested with Chrome 84) honor this change and * 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. * use the first codec provided in the answer, so this operation actually works.
*/ */
public String setCodecPreference(VideoCodec codec, String sdp, boolean applyHeavyMunging) throws OpenViduException { public String setCodecPreference(VideoCodec codec, String sdp) throws OpenViduException {
String codecStr = codec.name(); String codecStr = codec.name();
log.info("[setCodecPreference] codec: {}", codecStr); log.info("[setCodecPreference] codec: {}", codecStr);
@ -156,9 +156,8 @@ public class SDPMunging {
lines[sl] = newLine.toString().trim(); lines[sl] = newLine.toString().trim();
} }
if (applyHeavyMunging) { lines = cleanLinesWithRemovedCodecs(unusedCodecPts, lines);
lines = cleanLinesWithRemovedCodecs(unusedCodecPts, lines);
}
return String.join("\r\n", lines) + "\r\n"; return String.join("\r\n", lines) + "\r\n";
} }
@ -166,8 +165,7 @@ public class SDPMunging {
* Return a SDP modified to force a specific codec * Return a SDP modified to force a specific codec
*/ */
public String forceCodec(String sdp, Participant participant, boolean isPublisher, boolean isReconnecting, public String forceCodec(String sdp, Participant participant, boolean isPublisher, boolean isReconnecting,
boolean isTranscodingAllowed, VideoCodec forcedVideoCodec, boolean applyHeavyMunging) boolean isTranscodingAllowed, VideoCodec forcedVideoCodec) throws OpenViduException {
throws OpenViduException {
try { try {
if (supportedVideoCodecs.contains(forcedVideoCodec)) { if (supportedVideoCodecs.contains(forcedVideoCodec)) {
String mungedSdpOffer; String mungedSdpOffer;
@ -178,7 +176,7 @@ public class SDPMunging {
participant.getParticipantPublicId(), participant.getSessionId(), isPublisher, !isPublisher, participant.getParticipantPublicId(), participant.getSessionId(), isPublisher, !isPublisher,
isReconnecting, sdp); isReconnecting, sdp);
mungedSdpOffer = this.setCodecPreference(forcedVideoCodec, sdp, applyHeavyMunging); mungedSdpOffer = this.setCodecPreference(forcedVideoCodec, sdp);
log.debug( log.debug(
"PARTICIPANT '{}' in Session '{}'. Is Publisher: '{}'. Is Subscriber: '{}'." "PARTICIPANT '{}' in Session '{}'. Is Publisher: '{}'. Is Subscriber: '{}'."

View File

@ -88,7 +88,7 @@ public class SDPMungingTest {
private void initTestsSetCodecPrevalence(VideoCodec codec, String sdpNameFile) throws IOException { private void initTestsSetCodecPrevalence(VideoCodec codec, String sdpNameFile) throws IOException {
this.oldSdp = getSdpFile(sdpNameFile); this.oldSdp = getSdpFile(sdpNameFile);
this.newSdp = this.sdpMungin.setCodecPreference(codec, oldSdp, false); this.newSdp = this.sdpMungin.setCodecPreference(codec, oldSdp);
this.forceCodecPayloads = new ArrayList<>(); this.forceCodecPayloads = new ArrayList<>();
// Get all Payload-Type for video Codec // Get all Payload-Type for video Codec

View File

@ -344,6 +344,13 @@ public class OpenViduEventManager {
return dimension; return dimension;
} }
public void stopVideoTracksOfVideoElement(WebElement videoElement, String parentSelector) {
String script = "return (document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ")
+ "#" + videoElement.getAttribute("id")
+ "').srcObject.getVideoTracks().forEach(track => track.stop()))";
((JavascriptExecutor) driver).executeScript(script);
}
private boolean hasAudioTracks(WebElement videoElement, String parentSelector) { private boolean hasAudioTracks(WebElement videoElement, String parentSelector) {
String script = "return ((document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ") String script = "return ((document.querySelector('" + parentSelector + (parentSelector.isEmpty() ? "" : " ")
+ "#" + videoElement.getAttribute("id") + "').srcObject.getAudioTracks().length > 0)" + "#" + videoElement.getAttribute("id") + "').srcObject.getAudioTracks().length > 0)"

View File

@ -481,6 +481,38 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest {
gracefullyLeaveParticipants(4); gracefullyLeaveParticipants(4);
} }
@Test
@DisplayName("ExceptionEvent test")
void exceptionEventTest() throws Exception {
setupBrowser("chrome");
log.info("ExceptionEvent test");
user.getDriver().findElement(By.id("add-user-btn")).click();
user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .send-audio-checkbox")).click();
user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .join-btn")).click();
user.getEventManager().waitUntilEventReaches("streamCreated", 1);
user.getEventManager().waitUntilEventReaches("streamPlaying", 1);
// Stop video track
WebElement video = user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 video"));
this.user.getEventManager().stopVideoTracksOfVideoElement(video, "#openvidu-instance-0");
user.getDriver().findElement(By.id("add-user-btn")).click();
user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .publish-checkbox")).click();
user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).click();
user.getEventManager().waitUntilEventReaches("exception", 1);
Assert.assertTrue("Wrong ExceptionEvent type", user.getDriver()
.findElement(By.cssSelector("#openvidu-instance-1 .mat-expansion-panel:last-child .event-content"))
.getAttribute("textContent").equals("NO_STREAM_PLAYING_EVENT"));
gracefullyLeaveParticipants(2);
}
@Test @Test
@DisplayName("Subscribe Unsubscribe") @DisplayName("Subscribe Unsubscribe")
void subscribeUnsubscribeTest() throws Exception { void subscribeUnsubscribeTest() throws Exception {

View File

@ -1,49 +1,49 @@
{ {
"dependencies": { "dependencies": {
"@angular/animations": "8.2.14", "@angular/animations": "8.2.14",
"@angular/cdk": "8.2.3", "@angular/cdk": "8.2.3",
"@angular/common": "8.2.14", "@angular/common": "8.2.14",
"@angular/compiler": "8.2.14", "@angular/compiler": "8.2.14",
"@angular/core": "8.2.14", "@angular/core": "8.2.14",
"@angular/flex-layout": "8.0.0-beta.27", "@angular/flex-layout": "8.0.0-beta.27",
"@angular/forms": "8.2.14", "@angular/forms": "8.2.14",
"@angular/http": "7.2.15", "@angular/http": "7.2.15",
"@angular/material": "8.2.3", "@angular/material": "8.2.3",
"@angular/platform-browser": "8.2.14", "@angular/platform-browser": "8.2.14",
"@angular/platform-browser-dynamic": "8.2.14", "@angular/platform-browser-dynamic": "8.2.14",
"@angular/router": "8.2.14", "@angular/router": "8.2.14",
"colormap": "2.3.1", "colormap": "2.3.1",
"core-js": "3.4.7", "core-js": "3.4.7",
"hammerjs": "2.0.8", "hammerjs": "2.0.8",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"openvidu-browser": "2.17.0", "openvidu-browser": "2.18.0",
"openvidu-node-client": "2.17.0", "openvidu-node-client": "2.18.0",
"rxjs": "6.5.3", "rxjs": "6.5.3",
"zone.js": "0.10.2" "zone.js": "0.10.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "0.803.20", "@angular-devkit/build-angular": "0.803.20",
"@angular/cli": "8.3.20", "@angular/cli": "8.3.20",
"@angular/compiler-cli": "8.2.14", "@angular/compiler-cli": "8.2.14",
"@angular/language-service": "8.2.14", "@angular/language-service": "8.2.14",
"@types/jasmine": "3.5.0", "@types/jasmine": "3.5.0",
"@types/jasminewd2": "2.0.8", "@types/jasminewd2": "2.0.8",
"@types/node": "12.12.14", "@types/node": "12.12.14",
"codelyzer": "5.2.0", "codelyzer": "5.2.0",
"ts-node": "8.5.4", "ts-node": "8.5.4",
"tslint": "5.20.1", "tslint": "5.20.1",
"typescript": "3.5.3" "typescript": "3.5.3"
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"name": "openvidu-testapp", "name": "openvidu-testapp",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "ng build", "build": "ng build",
"e2e": "ng e2e", "e2e": "ng e2e",
"lint": "ng lint", "lint": "ng lint",
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"test": "ng test" "test": "ng test"
}, },
"version": "2.17.0" "version": "2.18.0"
} }

View File

@ -6,7 +6,7 @@ import {
import { import {
OpenVidu, Session, Subscriber, Publisher, Event, StreamEvent, ConnectionEvent, OpenVidu, Session, Subscriber, Publisher, Event, StreamEvent, ConnectionEvent,
SessionDisconnectedEvent, SignalEvent, RecordingEvent, SessionDisconnectedEvent, SignalEvent, RecordingEvent,
PublisherSpeakingEvent, PublisherProperties, StreamPropertyChangedEvent, ConnectionPropertyChangedEvent, OpenViduError, NetworkQualityLevelChangedEvent PublisherSpeakingEvent, PublisherProperties, StreamPropertyChangedEvent, ConnectionPropertyChangedEvent, OpenViduError, NetworkQualityLevelChangedEvent, ExceptionEvent
} from 'openvidu-browser'; } from 'openvidu-browser';
import { import {
OpenVidu as OpenViduAPI, OpenVidu as OpenViduAPI,
@ -138,7 +138,8 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
publisherStartSpeaking: false, publisherStartSpeaking: false,
publisherStopSpeaking: false, publisherStopSpeaking: false,
reconnecting: true, reconnecting: true,
reconnected: true reconnected: true,
exception: true
}; };
// Session properties dialog // Session properties dialog
@ -247,8 +248,9 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
signal: false, signal: false,
publisherStartSpeaking: true, publisherStartSpeaking: true,
publisherStopSpeaking: true, publisherStopSpeaking: true,
reconnecting: true, reconnecting: false,
reconnected: true reconnected: false,
exception: false
}, true); }, true);
this.session.connect(token, this.clientData) this.session.connect(token, this.clientData)
@ -508,6 +510,15 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
} }
} }
if (this.sessionEvents.exception !== oldValues.exception || firstTime) {
this.session.off('exception');
if (this.sessionEvents.exception) {
this.session.on('exception', (event: ExceptionEvent) => {
this.updateEventList('exception', event.name, event);
});
}
}
} }
syncInitPublisher() { syncInitPublisher() {
@ -665,7 +676,8 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
publisherStartSpeaking: this.sessionEvents.publisherStartSpeaking, publisherStartSpeaking: this.sessionEvents.publisherStartSpeaking,
publisherStopSpeaking: this.sessionEvents.publisherStopSpeaking, publisherStopSpeaking: this.sessionEvents.publisherStopSpeaking,
reconnecting: this.sessionEvents.reconnecting, reconnecting: this.sessionEvents.reconnecting,
reconnected: this.sessionEvents.reconnected reconnected: this.sessionEvents.reconnected,
exception: this.sessionEvents.exception
}; };
const dialogRef = this.dialog.open(EventsDialogComponent, { const dialogRef = this.dialog.open(EventsDialogComponent, {
@ -699,7 +711,8 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy {
publisherStartSpeaking: result.publisherStartSpeaking, publisherStartSpeaking: result.publisherStartSpeaking,
publisherStopSpeaking: result.publisherStopSpeaking, publisherStopSpeaking: result.publisherStopSpeaking,
reconnecting: result.reconnecting, reconnecting: result.reconnecting,
reconnected: result.reconnected reconnected: result.reconnected,
exception: result.exception
}; };
document.getElementById('session-events-btn-' + this.index).classList.remove('cdk-program-focused'); document.getElementById('session-events-btn-' + this.index).classList.remove('cdk-program-focused');
}); });

View File

@ -62,7 +62,7 @@
<version.webdrivermanager>4.2.2</version.webdrivermanager> <version.webdrivermanager>4.2.2</version.webdrivermanager>
<version.openvidu.java.client>2.17.0</version.openvidu.java.client> <version.openvidu.java.client>2.18.0</version.openvidu.java.client>
<version.openvidu.client>1.1.0</version.openvidu.client> <version.openvidu.client>1.1.0</version.openvidu.client>
<version.openvidu.test.browsers>1.1.0</version.openvidu.test.browsers> <version.openvidu.test.browsers>1.1.0</version.openvidu.test.browsers>