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)');
|
||||
if (!this.isRoomAvailable()) {
|
||||
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) => {
|
||||
if (!!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.jsonRpcClient.close(4101, "Reconnection fault");
|
||||
} else {
|
||||
|
|
|
@ -971,7 +971,7 @@ export class Session implements EventDispatcher {
|
|||
* @hidden
|
||||
*/
|
||||
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) {
|
||||
this.leave(true, reason);
|
||||
}
|
||||
|
@ -981,15 +981,15 @@ export class Session implements EventDispatcher {
|
|||
* @hidden
|
||||
*/
|
||||
onRecoveredConnection(): void {
|
||||
console.warn('Recovered connection in Session ' + this.sessionId);
|
||||
// this.ee.emitEvent('connectionRecovered', []);
|
||||
console.info('Recovered connection in Session ' + this.sessionId);
|
||||
this.reconnectBrokenStreams();
|
||||
this.ee.emitEvent('reconnected', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onMediaError(params): void {
|
||||
|
||||
console.error('Media error: ' + JSON.stringify(params));
|
||||
const err = params.error;
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -431,7 +431,7 @@ export class Stream implements EventDispatcher {
|
|||
*/
|
||||
subscribe(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.initWebRtcPeerReceive()
|
||||
this.initWebRtcPeerReceive(false)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
|
@ -447,7 +447,7 @@ export class Stream implements EventDispatcher {
|
|||
publish(): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isLocalStreamReadyToPublish) {
|
||||
this.initWebRtcPeerSend()
|
||||
this.initWebRtcPeerSend(false)
|
||||
.then(() => {
|
||||
resolve();
|
||||
})
|
||||
|
@ -473,19 +473,9 @@ export class Stream implements EventDispatcher {
|
|||
*/
|
||||
disposeWebRtcPeer(): void {
|
||||
if (!!this.webRtcPeer) {
|
||||
const isSenderAndCustomTrack: boolean = !!this.outboundStreamOpts &&
|
||||
typeof MediaStreamTrack !== 'undefined' && this.outboundStreamOpts.publisherProperties.videoSource instanceof MediaStreamTrack;
|
||||
this.webRtcPeer.dispose(isSenderAndCustomTrack);
|
||||
}
|
||||
if (!!this.speechEvent) {
|
||||
if (!!this.speechEvent.stop) {
|
||||
this.speechEvent.stop();
|
||||
}
|
||||
delete this.speechEvent;
|
||||
}
|
||||
|
||||
this.webRtcPeer.dispose();
|
||||
this.stopWebRtcStats();
|
||||
|
||||
}
|
||||
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;
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -767,6 +763,17 @@ export class Stream implements EventDispatcher {
|
|||
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 setSpeechEventIfNotExists(): boolean {
|
||||
|
@ -782,10 +789,15 @@ export class Stream implements EventDispatcher {
|
|||
return false;
|
||||
}
|
||||
|
||||
private initWebRtcPeerSend(): Promise<any> {
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
initWebRtcPeerSend(reconnect: boolean): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (!reconnect) {
|
||||
this.initHarkEvents(); // Init hark events for the local stream
|
||||
}
|
||||
|
||||
const userMediaConstraints = {
|
||||
audio: this.isSendAudio(),
|
||||
|
@ -804,13 +816,18 @@ export class Stream implements EventDispatcher {
|
|||
console.debug('Sending SDP offer to publish as '
|
||||
+ this.streamId, sdpOfferParam);
|
||||
|
||||
const method = reconnect ? 'reconnectStream' : 'publishVideo';
|
||||
let params;
|
||||
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');
|
||||
}
|
||||
|
||||
this.session.openvidu.sendRequest('publishVideo', {
|
||||
sdpOffer: sdpOfferParam,
|
||||
params = {
|
||||
doLoopback: this.displayMyRemote() || false,
|
||||
hasAudio: this.isSendAudio(),
|
||||
hasVideo: this.isSendVideo(),
|
||||
|
@ -820,7 +837,11 @@ export class Stream implements EventDispatcher {
|
|||
frameRate: !!this.frameRate ? this.frameRate : -1,
|
||||
videoDimensions: JSON.stringify(this.videoDimensions),
|
||||
filter: this.outboundStreamOpts.publisherProperties.filter
|
||||
}, (error, response) => {
|
||||
}
|
||||
}
|
||||
params['sdpOffer'] = sdpOfferParam;
|
||||
|
||||
this.session.openvidu.sendRequest(method, params, (error, response) => {
|
||||
if (error) {
|
||||
if (error.code === 401) {
|
||||
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()) {
|
||||
this.remotePeerSuccessfullyEstablished();
|
||||
}
|
||||
if (reconnect) {
|
||||
this.ee.emitEvent('stream-reconnected-by-publisher', []);
|
||||
} else {
|
||||
this.ee.emitEvent('stream-created-by-publisher', []);
|
||||
}
|
||||
this.initWebRtcStats();
|
||||
console.info("'Publisher' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "published") + " to session");
|
||||
resolve();
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
console.info("'Publisher' successfully published to session");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (reconnect) {
|
||||
this.disposeWebRtcPeer();
|
||||
}
|
||||
if (this.displayMyRemote()) {
|
||||
this.webRtcPeer = new WebRtcPeerSendrecv(options);
|
||||
} else {
|
||||
this.webRtcPeer = new WebRtcPeerSendonly(options);
|
||||
}
|
||||
this.webRtcPeer.generateOffer().then(offer => {
|
||||
successCallback(offer);
|
||||
this.webRtcPeer.addIceConnectionStateChangeListener('publisher of ' + this.connection.connectionId);
|
||||
this.webRtcPeer.generateOffer().then(sdpOffer => {
|
||||
successCallback(sdpOffer);
|
||||
}).catch(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) => {
|
||||
|
||||
const offerConstraints = {
|
||||
|
@ -881,10 +913,12 @@ export class Stream implements EventDispatcher {
|
|||
const successCallback = (sdpOfferParam) => {
|
||||
console.debug('Sending SDP offer to subscribe to '
|
||||
+ this.streamId, sdpOfferParam);
|
||||
this.session.openvidu.sendRequest('receiveVideoFrom', {
|
||||
sender: this.streamId,
|
||||
sdpOffer: sdpOfferParam
|
||||
}, (error, response) => {
|
||||
|
||||
const method = reconnect ? 'reconnectStream' : 'receiveVideoFrom';
|
||||
const params = { sdpOffer: sdpOfferParam };
|
||||
params[reconnect ? 'stream' : 'sender'] = this.streamId;
|
||||
|
||||
this.session.openvidu.sendRequest(method, params, (error, response) => {
|
||||
if (error) {
|
||||
reject(new Error('Error on recvVideoFrom: ' + JSON.stringify(error)));
|
||||
} else {
|
||||
|
@ -901,6 +935,7 @@ export class Stream implements EventDispatcher {
|
|||
}
|
||||
const needsTimeoutOnProcessAnswer = this.session.countDownForIonicIosSubscribersActive;
|
||||
this.webRtcPeer.processAnswer(response.sdpAnswer, needsTimeoutOnProcessAnswer).then(() => {
|
||||
console.info("'Subscriber' (" + this.streamId + ") successfully " + (reconnect ? "reconnected" : "subscribed"));
|
||||
this.remotePeerSuccessfullyEstablished();
|
||||
this.initWebRtcStats();
|
||||
resolve();
|
||||
|
@ -912,9 +947,10 @@ export class Stream implements EventDispatcher {
|
|||
};
|
||||
|
||||
this.webRtcPeer = new WebRtcPeerRecvonly(options);
|
||||
this.webRtcPeer.addIceConnectionStateChangeListener(this.streamId);
|
||||
this.webRtcPeer.generateOffer()
|
||||
.then(offer => {
|
||||
successCallback(offer);
|
||||
.then(sdpOffer => {
|
||||
successCallback(sdpOffer);
|
||||
})
|
||||
.catch(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()`
|
||||
* - "forceDisconnectByServer": you have been evicted from the Session 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;
|
||||
|
||||
|
|
|
@ -97,34 +97,15 @@ export class WebRtcPeer {
|
|||
/**
|
||||
* This method frees the resources used by WebRtcPeer
|
||||
*/
|
||||
dispose(videoSourceIsMediaStreamTrack: boolean) {
|
||||
dispose() {
|
||||
console.debug('Disposing WebRtcPeer');
|
||||
try {
|
||||
if (this.pc) {
|
||||
if (this.pc.signalingState === 'closed') {
|
||||
return;
|
||||
}
|
||||
this.pc.close();
|
||||
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) {
|
||||
console.warn('Exception disposing webrtc peer ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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