openvidu-browser: media reconnections support

pull/391/head
pabloFuente 2020-02-14 20:51:52 +01:00
parent 8fc8834d25
commit 4ae63984a2
5 changed files with 145 additions and 73 deletions

View File

@ -919,6 +919,8 @@ export class OpenVidu {
console.warn('Websocket connection lost (reconnecting)'); console.warn('Websocket connection lost (reconnecting)');
if (!this.isRoomAvailable()) { if (!this.isRoomAvailable()) {
alert('Connection error. Please reload page.'); alert('Connection error. Please reload page.');
} else {
this.session.emitEvent('reconnecting', []);
} }
} }
@ -928,6 +930,7 @@ export class OpenVidu {
this.sendRequest('connect', { sessionId: this.session.connection.rpcSessionId }, (error, response) => { this.sendRequest('connect', { sessionId: this.session.connection.rpcSessionId }, (error, response) => {
if (!!error) { if (!!error) {
console.error(error); console.error(error);
console.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.session.onLostConnection("networkDisconnect");
this.jsonRpcClient.close(4101, "Reconnection fault"); this.jsonRpcClient.close(4101, "Reconnection fault");
} else { } else {

View File

@ -971,7 +971,7 @@ export class Session implements EventDispatcher {
* @hidden * @hidden
*/ */
onLostConnection(reason: string): void { onLostConnection(reason: string): void {
console.warn('Lost connection in session ' + this.sessionId + ' waiting for reconnect'); console.warn('Lost connection in Session ' + this.sessionId);
if (!!this.sessionId && !this.connection.disposed) { if (!!this.sessionId && !this.connection.disposed) {
this.leave(true, reason); this.leave(true, reason);
} }
@ -981,15 +981,15 @@ export class Session implements EventDispatcher {
* @hidden * @hidden
*/ */
onRecoveredConnection(): void { onRecoveredConnection(): void {
console.warn('Recovered connection in Session ' + this.sessionId); console.info('Recovered connection in Session ' + this.sessionId);
// this.ee.emitEvent('connectionRecovered', []); this.reconnectBrokenStreams();
this.ee.emitEvent('reconnected', []);
} }
/** /**
* @hidden * @hidden
*/ */
onMediaError(params): void { onMediaError(params): void {
console.error('Media error: ' + JSON.stringify(params)); console.error('Media error: ' + JSON.stringify(params));
const err = params.error; const err = params.error;
if (err) { if (err) {
@ -1030,6 +1030,25 @@ export class Session implements EventDispatcher {
}); });
} }
/**
* @hidden
*/
reconnectBrokenStreams(): void {
console.info('Re-establishing media connections');
// Re-establish Publisher stream
if (!!this.connection.stream && this.connection.stream.streamIceConnectionStateBroken()) {
console.warn('Re-establishing Publisher ' + this.connection.stream.streamId);
this.connection.stream.initWebRtcPeerSend(true);
}
// Re-establish Subscriber streams
for (let remoteConnection of Object.values(this.remoteConnections)) {
if (!!remoteConnection.stream && remoteConnection.stream.streamIceConnectionStateBroken()) {
console.warn('Re-establishing Subscriber ' + remoteConnection.stream.streamId);
remoteConnection.stream.initWebRtcPeerReceive(true);
}
}
}
/** /**
* @hidden * @hidden
*/ */

View File

@ -431,7 +431,7 @@ export class Stream implements EventDispatcher {
*/ */
subscribe(): Promise<any> { subscribe(): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.initWebRtcPeerReceive() this.initWebRtcPeerReceive(false)
.then(() => { .then(() => {
resolve(); resolve();
}) })
@ -447,7 +447,7 @@ export class Stream implements EventDispatcher {
publish(): Promise<any> { publish(): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.isLocalStreamReadyToPublish) { if (this.isLocalStreamReadyToPublish) {
this.initWebRtcPeerSend() this.initWebRtcPeerSend(false)
.then(() => { .then(() => {
resolve(); resolve();
}) })
@ -473,19 +473,9 @@ export class Stream implements EventDispatcher {
*/ */
disposeWebRtcPeer(): void { disposeWebRtcPeer(): void {
if (!!this.webRtcPeer) { if (!!this.webRtcPeer) {
const isSenderAndCustomTrack: boolean = !!this.outboundStreamOpts && this.webRtcPeer.dispose();
typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack; this.stopWebRtcStats();
this.webRtcPeer.dispose(isSenderAndCustomTrack);
} }
if (!!this.speechEvent) {
if (!!this.speechEvent.stop) {
this.speechEvent.stop();
}
delete this.speechEvent;
}
this.stopWebRtcStats();
console.info((!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + "WebRTCPeer from 'Stream' with id [" + this.streamId + '] is now closed'); console.info((!!this.outboundStreamOpts ? 'Outbound ' : 'Inbound ') + "WebRTCPeer from 'Stream' with id [" + this.streamId + '] is now closed');
} }
@ -502,6 +492,12 @@ export class Stream implements EventDispatcher {
}); });
delete this.mediaStream; delete this.mediaStream;
} }
if (!!this.speechEvent) {
if (!!this.speechEvent.stop) {
this.speechEvent.stop();
}
delete this.speechEvent;
}
console.info((!!this.outboundStreamOpts ? 'Local ' : 'Remote ') + "MediaStream from 'Stream' with id [" + this.streamId + '] is now disposed'); console.info((!!this.outboundStreamOpts ? 'Local ' : 'Remote ') + "MediaStream from 'Stream' with id [" + this.streamId + '] is now disposed');
} }
@ -767,6 +763,17 @@ export class Stream implements EventDispatcher {
return this.webRtcPeer.localCandidatesQueue; return this.webRtcPeer.localCandidatesQueue;
} }
/**
* @hidden
*/
streamIceConnectionStateBroken() {
if (!this.getWebRtcPeer() || !this.getRTCPeerConnection()) {
return false;
}
const iceConnectionState: RTCIceConnectionState = this.getRTCPeerConnection().iceConnectionState;
return iceConnectionState === 'disconnected' || iceConnectionState === 'failed';
}
/* Private methods */ /* Private methods */
private setSpeechEventIfNotExists(): boolean { private setSpeechEventIfNotExists(): boolean {
@ -782,10 +789,15 @@ export class Stream implements EventDispatcher {
return false; return false;
} }
private initWebRtcPeerSend(): Promise<any> { /**
* @hidden
*/
initWebRtcPeerSend(reconnect: boolean): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.initHarkEvents(); // Init hark events for the local stream if (!reconnect) {
this.initHarkEvents(); // Init hark events for the local stream
}
const userMediaConstraints = { const userMediaConstraints = {
audio: this.isSendAudio(), audio: this.isSendAudio(),
@ -804,23 +816,32 @@ export class Stream implements EventDispatcher {
console.debug('Sending SDP offer to publish as ' console.debug('Sending SDP offer to publish as '
+ this.streamId, sdpOfferParam); + this.streamId, sdpOfferParam);
let typeOfVideo = ''; const method = reconnect ? 'reconnectStream' : 'publishVideo';
if (this.isSendVideo()) { let params;
typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA'); if (reconnect) {
params = {
stream: this.streamId
}
} else {
let typeOfVideo = '';
if (this.isSendVideo()) {
typeOfVideo = (typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack) ? 'CUSTOM' : (this.isSendScreen() ? 'SCREEN' : 'CAMERA');
}
params = {
doLoopback: this.displayMyRemote() || false,
hasAudio: this.isSendAudio(),
hasVideo: this.isSendVideo(),
audioActive: this.audioActive,
videoActive: this.videoActive,
typeOfVideo,
frameRate: !!this.frameRate ? this.frameRate : -1,
videoDimensions: JSON.stringify(this.videoDimensions),
filter: this.outboundStreamOpts.publisherProperties.filter
}
} }
params['sdpOffer'] = sdpOfferParam;
this.session.openvidu.sendRequest('publishVideo', { this.session.openvidu.sendRequest(method, params, (error, response) => {
sdpOffer: sdpOfferParam,
doLoopback: this.displayMyRemote() || false,
hasAudio: this.isSendAudio(),
hasVideo: this.isSendVideo(),
audioActive: this.audioActive,
videoActive: this.videoActive,
typeOfVideo,
frameRate: !!this.frameRate ? this.frameRate : -1,
videoDimensions: JSON.stringify(this.videoDimensions),
filter: this.outboundStreamOpts.publisherProperties.filter
}, (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")); reject(new OpenViduError(OpenViduErrorName.OPENVIDU_PERMISSION_DENIED, "You don't have permissions to publish"));
@ -837,32 +858,43 @@ export class Stream implements EventDispatcher {
if (this.displayMyRemote()) { if (this.displayMyRemote()) {
this.remotePeerSuccessfullyEstablished(); this.remotePeerSuccessfullyEstablished();
} }
this.ee.emitEvent('stream-created-by-publisher', []); if (reconnect) {
this.ee.emitEvent('stream-reconnected-by-publisher', []);
} else {
this.ee.emitEvent('stream-created-by-publisher', []);
}
this.initWebRtcStats(); this.initWebRtcStats();
console.info("'Publisher' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "published") + " to session");
resolve(); resolve();
}) })
.catch(error => { .catch(error => {
reject(error); reject(error);
}); });
console.info("'Publisher' successfully published to session");
} }
}); });
}; };
if (reconnect) {
this.disposeWebRtcPeer();
}
if (this.displayMyRemote()) { if (this.displayMyRemote()) {
this.webRtcPeer = new WebRtcPeerSendrecv(options); this.webRtcPeer = new WebRtcPeerSendrecv(options);
} else { } else {
this.webRtcPeer = new WebRtcPeerSendonly(options); this.webRtcPeer = new WebRtcPeerSendonly(options);
} }
this.webRtcPeer.generateOffer().then(offer => { this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId);
successCallback(offer); this.webRtcPeer.generateOffer().then(sdpOffer => {
successCallback(sdpOffer);
}).catch(error => { }).catch(error => {
reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error))); reject(new Error('(publish) SDP offer error: ' + JSON.stringify(error)));
}); });
}); });
} }
private initWebRtcPeerReceive(): Promise<any> { /**
* @hidden
*/
initWebRtcPeerReceive(reconnect: boolean): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const offerConstraints = { const offerConstraints = {
@ -881,10 +913,12 @@ export class Stream implements EventDispatcher {
const successCallback = (sdpOfferParam) => { const successCallback = (sdpOfferParam) => {
console.debug('Sending SDP offer to subscribe to ' console.debug('Sending SDP offer to subscribe to '
+ this.streamId, sdpOfferParam); + this.streamId, sdpOfferParam);
this.session.openvidu.sendRequest('receiveVideoFrom', {
sender: this.streamId, const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom';
sdpOffer: sdpOfferParam const params = { sdpOffer: sdpOfferParam };
}, (error, response) => { params[reconnect ? 'stream' : 'sender'] = this.streamId;
this.session.openvidu.sendRequest(method, params, (error, response) => {
if (error) { if (error) {
reject(new Error('Error on recvVideoFrom: ' + JSON.stringify(error))); reject(new Error('Error on recvVideoFrom: ' + JSON.stringify(error)));
} else { } else {
@ -901,6 +935,7 @@ export class Stream implements EventDispatcher {
} }
const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive; const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive;
this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => { this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => {
console.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
this.remotePeerSuccessfullyEstablished(); this.remotePeerSuccessfullyEstablished();
this.initWebRtcStats(); this.initWebRtcStats();
resolve(); resolve();
@ -912,9 +947,10 @@ export class Stream implements EventDispatcher {
}; };
this.webRtcPeer = new WebRtcPeerRecvonly(options); this.webRtcPeer = new WebRtcPeerRecvonly(options);
this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId);
this.webRtcPeer.generateOffer() this.webRtcPeer.generateOffer()
.then(offer => { .then(sdpOffer => {
successCallback(offer); successCallback(sdpOffer);
}) })
.catch(error => { .catch(error => {
reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error))); reject(new Error('(subscribe) SDP offer error: ' + JSON.stringify(error)));

View File

@ -29,7 +29,10 @@ export class SessionDisconnectedEvent extends Event {
* - "forceDisconnectByUser": you have been evicted from the Session by other user calling `Session.forceDisconnect()` * - "forceDisconnectByUser": you have been evicted from the Session by other user calling `Session.forceDisconnect()`
* - "forceDisconnectByServer": you have been evicted from the Session by the application * - "forceDisconnectByServer": you have 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": your network connection has dropped * - "networkDisconnect": your network connection has dropped. Before a SessionDisconnectedEvent with this reason is triggered,
* 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
* with reason "networkDisconnect"
*/ */
reason: string; reason: string;

View File

@ -97,34 +97,15 @@ export class WebRtcPeer {
/** /**
* This method frees the resources used by WebRtcPeer * This method frees the resources used by WebRtcPeer
*/ */
dispose(videoSourceIsMediaStreamTrack: boolean) { dispose() {
console.debug('Disposing WebRtcPeer'); console.debug('Disposing WebRtcPeer');
try { if (this.pc) {
if (this.pc) { if (this.pc.signalingState === 'closed') {
if (this.pc.signalingState === 'closed') { return;
return;
}
this.remoteCandidatesQueue = [];
this.localCandidatesQueue = [];
// Stop senders
for (const sender of this.pc.getSenders()) {
if (!videoSourceIsMediaStreamTrack) {
if (!!sender.track) {
sender.track.stop();
}
}
this.pc.removeTrack(sender);
}
// Stop receivers
for (const receiver of this.pc.getReceivers()) {
if (!!receiver.track) {
receiver.track.stop();
}
}
this.pc.close();
} }
} catch (err) { this.pc.close();
console.warn('Exception disposing webrtc peer ' + err); this.remoteCandidatesQueue = [];
this.localCandidatesQueue = [];
} }
} }
@ -264,6 +245,36 @@ export class WebRtcPeer {
}); });
} }
addIceConnectionStateChangeListener(otherId: string) {
this.pc.oniceconnectionstatechange = () => {
const iceConnectionState: RTCIceConnectionState = this.pc.iceConnectionState;
switch (iceConnectionState) {
case 'disconnected':
// Possible network disconnection
console.warn('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "disconnected". Possible network disconnection');
break;
case 'failed':
console.error('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') to "failed"');
break;
case 'closed':
console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "closed"');
break;
case 'new':
console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "new"');
break;
case 'checking':
console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "checking"');
break;
case 'connected':
console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "connected"');
break;
case 'completed':
console.log('IceConnectionState of RTCPeerConnection ' + this.id + ' (' + otherId + ') change to "completed"');
break;
}
}
}
} }