diff --git a/openvidu-browser/src/OpenVidu/OpenVidu.ts b/openvidu-browser/src/OpenVidu/OpenVidu.ts index e10a1af9..7def45a7 100644 --- a/openvidu-browser/src/OpenVidu/OpenVidu.ts +++ b/openvidu-browser/src/OpenVidu/OpenVidu.ts @@ -885,9 +885,16 @@ export class OpenVidu { /** * @hidden */ - addAlreadyProvidedTracks(myConstraints: CustomMediaStreamConstraints, mediaStream: MediaStream) { + addAlreadyProvidedTracks(myConstraints: CustomMediaStreamConstraints, mediaStream: MediaStream, stream?: Stream) { if (!!myConstraints.videoTrack) { mediaStream.addTrack(myConstraints.videoTrack); + if (!!stream) { + if (!!myConstraints.constraints.video) { + stream.lastVideoTrackConstraints = myConstraints.constraints.video; + } else { + stream.lastVideoTrackConstraints = myConstraints.videoTrack.getConstraints(); + } + } } if (!!myConstraints.audioTrack) { mediaStream.addTrack(myConstraints.audioTrack); diff --git a/openvidu-browser/src/OpenVidu/Publisher.ts b/openvidu-browser/src/OpenVidu/Publisher.ts index 960c7807..73dc0b71 100644 --- a/openvidu-browser/src/OpenVidu/Publisher.ts +++ b/openvidu-browser/src/OpenVidu/Publisher.ts @@ -144,6 +144,10 @@ export class Publisher extends StreamManager { } + publishVideo(value: boolean): void; + publishVideo(value: false, freeResource?: boolean): void; + publishVideo(value: true, track?: MediaStreamTrack): void; + /** * Publish or unpublish the video stream (if available). Calling this method twice in a row passing same value will have no effect * @@ -160,13 +164,53 @@ export class Publisher extends StreamManager { * See [[StreamPropertyChangedEvent]] to learn more. * * @param value `true` to publish the video stream, `false` to unpublish it + * @param freeResource `true` to free the hardware resource associated to the video track, `false` to keep access to it. Not freeing the resource makes the operation much more efficient, but depending on + * the platform two side-effects can be introduced: the video device may not be accessible by other applications and the access light of webcams may remain on. This is platform-dependent: some browsers + * will not present the side-effects even when not freeing the resource. openvidu-browser will try to restore the video track automatically calling [[publishVideo]] again with parameter `value` to `true`, + * but if that is not possible parameter `track` can be provided to force a specific `MediaStreamTrack`. + * @param track A `MediaStreamTrack` to be used when restoring the video track. This parameter can be useful if the Publisher was unpublished with parameter `freeResource` to true, and openvidu-browser is + * not able to successfully re-create the video track as it was before unpublishing. In this way previous track settings will be ignored and this track will be used instead. */ - publishVideo(value: boolean): void { + publishVideo(value: boolean, param?: boolean | MediaStreamTrack): void { + if (this.stream.videoActive !== value) { + const affectedMediaStream: MediaStream = this.stream.displayMyRemote() ? this.stream.localMediaStreamWhenSubscribedToRemote! : this.stream.getMediaStream(); + let mustRestartMediaStream = false; affectedMediaStream.getVideoTracks().forEach((track) => { track.enabled = value; + if (!value && param === true) { + track.stop(); + } else if (value && track.readyState === 'ended') { + // Resource was freed + mustRestartMediaStream = true; + } }); + + if (mustRestartMediaStream) { + const oldVideoTrack = affectedMediaStream.getVideoTracks()[0]; + affectedMediaStream.removeTrack(oldVideoTrack); + + const replaceVideoTrack = (tr: MediaStreamTrack) => { + affectedMediaStream.addTrack(tr); + if (this.stream.isLocalStreamPublished) { + this.replaceTrackInRtcRtpSender(tr); + } + } + + if (!!param && param instanceof MediaStreamTrack) { + replaceVideoTrack(param); + } else { + navigator.mediaDevices.getUserMedia({ audio: false, video: this.stream.lastVideoTrackConstraints }) + .then(mediaStream => { + replaceVideoTrack(mediaStream.getVideoTracks()[0]); + }) + .catch(error => { + console.error(error); + }); + } + } + if (!!this.session && !!this.stream.streamId) { this.session.openvidu.sendRequest( 'streamPropertyChanged', @@ -295,6 +339,7 @@ export class Publisher extends StreamManager { let removedTrack: MediaStreamTrack; if (track.kind === 'video') { removedTrack = mediaStream.getVideoTracks()[0]; + this.stream.lastVideoTrackConstraints = track.getConstraints(); } else { removedTrack = mediaStream.getAudioTracks()[0]; } @@ -309,29 +354,6 @@ export class Publisher extends StreamManager { }); } - const replaceTrackInRtcRtpSender = (): Promise => { - return new Promise((resolve, reject) => { - const senders: RTCRtpSender[] = this.stream.getRTCPeerConnection().getSenders(); - let sender: RTCRtpSender | undefined; - if (track.kind === 'video') { - sender = senders.find(s => !!s.track && s.track.kind === 'video'); - if (!sender) { - return reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object')); - } - } else if (track.kind === 'audio') { - sender = senders.find(s => !!s.track && s.track.kind === 'audio'); - if (!sender) { - return reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object')); - } - } else { - return reject(new Error('Unknown track kind ' + track.kind)); - } - (sender as RTCRtpSender).replaceTrack(track) - .then(() => resolve()) - .catch(error => reject(error)); - }); - } - // Set field "enabled" of the new track to the previous value const trackOriginalEnabledValue: boolean = track.enabled; if (track.kind === 'video') { @@ -343,7 +365,7 @@ export class Publisher extends StreamManager { if (this.stream.isLocalStreamPublished) { // Only if the Publisher has been published is necessary to call native Web API RTCRtpSender.replaceTrack // If it has not been published yet, replacing it on the MediaStream object is enough - await replaceTrackInRtcRtpSender(); + await this.replaceTrackInRtcRtpSender(track); return await replaceTrackInMediaStream(); } else { // Publisher not published. Simply replace the track on the local MediaStream @@ -404,7 +426,7 @@ export class Publisher extends StreamManager { if (!track.contentHint?.length) { // contentHint for audio: "", "speech", "speech-recognition", "music". // https://w3c.github.io/mst-content-hint/#audio-content-hints - track.contentHint = ""; + track.contentHint = ''; logger.info(`Audio track Content Hint set: '${track.contentHint}'`); } } @@ -453,8 +475,9 @@ export class Publisher extends StreamManager { const settings: MediaTrackSettings = mediaStream.getVideoTracks()[0].getSettings(); const newWidth = settings.width; const newHeight = settings.height; - if (this.stream.isLocalStreamPublished && - (newWidth !== this.stream.videoDimensions.width || newHeight !== this.stream.videoDimensions.height)) { + const widthChanged = newWidth != null && newWidth !== this.stream.videoDimensions.width; + const heightChanged = newHeight != null && newHeight !== this.stream.videoDimensions.height; + if (this.stream.isLocalStreamPublished && (widthChanged || heightChanged)) { this.openvidu.sendVideoDimensionsChangedEvent( this, 'screenResized', @@ -591,7 +614,7 @@ export class Publisher extends StreamManager { !!myConstraints.audioTrack && myConstraints.constraints?.video === false || !!myConstraints.videoTrack && myConstraints.constraints?.audio === false) { // No need to call getUserMedia at all. MediaStreamTracks already provided - successCallback(this.openvidu.addAlreadyProvidedTracks(myConstraints, new MediaStream())); + successCallback(this.openvidu.addAlreadyProvidedTracks(myConstraints, new MediaStream(), this.stream)); // Return as we do not need to process further return; } @@ -620,9 +643,10 @@ export class Publisher extends StreamManager { getMediaError(error); }); } else { + this.stream.lastVideoTrackConstraints = constraintsAux.video; navigator.mediaDevices.getUserMedia(constraintsAux) .then(mediaStream => { - this.openvidu.addAlreadyProvidedTracks(myConstraints, mediaStream); + this.openvidu.addAlreadyProvidedTracks(myConstraints, mediaStream, this.stream); getMediaSuccess(mediaStream, definedAudioConstraint); }) .catch(error => { @@ -744,4 +768,27 @@ export class Publisher extends StreamManager { } } + private replaceTrackInRtcRtpSender(track: MediaStreamTrack): Promise { + return new Promise((resolve, reject) => { + const senders: RTCRtpSender[] = this.stream.getRTCPeerConnection().getSenders(); + let sender: RTCRtpSender | undefined; + if (track.kind === 'video') { + sender = senders.find(s => !!s.track && s.track.kind === 'video'); + if (!sender) { + return reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object')); + } + } else if (track.kind === 'audio') { + sender = senders.find(s => !!s.track && s.track.kind === 'audio'); + if (!sender) { + return reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object')); + } + } else { + return reject(new Error('Unknown track kind ' + track.kind)); + } + (sender as RTCRtpSender).replaceTrack(track) + .then(() => resolve()) + .catch(error => reject(error)); + }); + } + } diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index 9dccc610..4e5e2408 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -219,6 +219,10 @@ export class Stream { * @hidden */ reconnectionEventEmitter: EventEmitter | undefined; + /** + * @hidden + */ + lastVideoTrackConstraints: MediaTrackConstraints | boolean | undefined; /**