mirror of https://github.com/OpenVidu/openvidu.git
openvidu-browser: media reconnections support
parent
8fc8834d25
commit
4ae63984a2
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue