openvidu-browser: OpenVidu.getUserMedia fix. generateMediaConstraints refactoring

pull/405/head
pabloFuente 2020-02-20 20:14:46 +01:00
parent 26750e6167
commit df4db147f5
3 changed files with 253 additions and 160 deletions

View File

@ -23,6 +23,7 @@ import { StreamPropertyChangedEvent } from '../OpenViduInternal/Events/StreamPro
import { Device } from '../OpenViduInternal/Interfaces/Public/Device';
import { OpenViduAdvancedConfiguration } from '../OpenViduInternal/Interfaces/Public/OpenViduAdvancedConfiguration';
import { PublisherProperties } from '../OpenViduInternal/Interfaces/Public/PublisherProperties';
import { CustomMediaStreamConstraints } from '../OpenViduInternal/Interfaces/Private/CustomMediaStreamConstraints';
import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError';
import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode';
@ -551,20 +552,41 @@ export class OpenVidu {
});
}
this.generateMediaConstraints(options)
.then(constraints => {
this.generateMediaConstraints(options).then(myConstraints => {
if (!!myConstraints.videoTrack && !!myConstraints.audioTrack ||
!!myConstraints.audioTrack && myConstraints.constraints?.video === false ||
!!myConstraints.videoTrack && myConstraints.constraints?.audio === false) {
// No need to call getUserMedia at all. Both tracks provided, or only AUDIO track provided or only VIDEO track provided
resolve(this.addAlreadyProvidedTracks(myConstraints, new MediaStream()));
} else {
// getUserMedia must be called. AUDIO or VIDEO are requesting a new track
// Delete already provided constraints for audio or video
if (!!myConstraints.videoTrack) {
delete myConstraints.constraints!.video;
}
if (!!myConstraints.audioTrack) {
delete myConstraints.constraints!.audio;
}
let mustAskForAudioTrackLater = false;
if (typeof options.videoSource === 'string') {
// Video is deviceId or screen sharing
if (options.videoSource === 'screen' ||
options.videoSource === 'window' ||
(platform.name === 'Electron' && options.videoSource.startsWith('screen:'))) {
// Screen sharing
mustAskForAudioTrackLater = options.audioSource !== null && options.audioSource !== false;
// Video is screen sharing
mustAskForAudioTrackLater = !myConstraints.audioTrack && (options.audioSource !== null && options.audioSource !== false);
if (navigator.mediaDevices['getDisplayMedia'] && platform.name !== 'Electron') {
// getDisplayMedia supported
navigator.mediaDevices['getDisplayMedia']({ video: true })
.then(mediaStream => {
this.addAlreadyProvidedTracks(myConstraints, mediaStream);
if (mustAskForAudioTrackLater) {
askForAudioStreamOnly(mediaStream, constraints);
askForAudioStreamOnly(mediaStream, <MediaStreamConstraints>myConstraints.constraints);
return;
} else {
resolve(mediaStream);
@ -575,15 +597,21 @@ export class OpenVidu {
const errorMessage = error.toString();
reject(new OpenViduError(errorName, errorMessage));
});
}
return;
} else {
// getDisplayMedia NOT supported. Can perform getUserMedia below with already calculated constraints
}
} else {
// Video is deviceId. Can perform getUserMedia below with already calculated constraints
}
}
const constraintsAux = mustAskForAudioTrackLater ? { video: constraints.video } : constraints;
// Use already calculated constraints
const constraintsAux = mustAskForAudioTrackLater ? { video: myConstraints.constraints!.video } : myConstraints.constraints;
navigator.mediaDevices.getUserMedia(constraintsAux)
.then(mediaStream => {
this.addAlreadyProvidedTracks(myConstraints, mediaStream);
if (mustAskForAudioTrackLater) {
askForAudioStreamOnly(mediaStream, constraints);
askForAudioStreamOnly(mediaStream, <MediaStreamConstraints>myConstraints.constraints);
return;
} else {
resolve(mediaStream);
@ -599,8 +627,8 @@ export class OpenVidu {
}
reject(new OpenViduError(errorName, errorMessage));
});
})
.catch((error: OpenViduError) => {
}
}).catch((error: OpenViduError) => {
reject(error);
});
});
@ -638,85 +666,112 @@ export class OpenVidu {
/**
* @hidden
*/
generateMediaConstraints(publisherProperties: PublisherProperties): Promise<MediaStreamConstraints> {
return new Promise<MediaStreamConstraints>((resolve, reject) => {
let audio, video;
generateMediaConstraints(publisherProperties: PublisherProperties): Promise<CustomMediaStreamConstraints> {
return new Promise<CustomMediaStreamConstraints>((resolve, reject) => {
if (publisherProperties.audioSource === null || publisherProperties.audioSource === false) {
audio = false;
} else if (publisherProperties.audioSource === undefined) {
audio = true;
} else {
audio = publisherProperties.audioSource;
const myConstraints: CustomMediaStreamConstraints = {
audioTrack: undefined,
videoTrack: undefined,
constraints: {
audio: undefined,
video: undefined
}
}
const audioSource = publisherProperties.audioSource;
const videoSource = publisherProperties.videoSource;
if (publisherProperties.videoSource === null || publisherProperties.videoSource === false) {
video = false;
} else {
video = {
height: {
ideal: 480
},
width: {
ideal: 640
// CASE 1: null/false
if (audioSource === null || audioSource === false) {
// No audio track
myConstraints.constraints!.audio = false;
}
};
if (videoSource === null || videoSource === false) {
// No video track
myConstraints.constraints!.video = false;
}
if (audio === false && video === false) {
if (myConstraints.constraints!.audio === false && myConstraints.constraints!.video === false) {
// ERROR! audioSource and videoSource cannot be both false at the same time
reject(new OpenViduError(OpenViduErrorName.NO_INPUT_SOURCE_SET,
"Properties 'audioSource' and 'videoSource' cannot be set to false or null at the same time"));
}
const mediaConstraints: MediaStreamConstraints = {
audio,
video
};
if (typeof mediaConstraints.audio === 'string') {
mediaConstraints.audio = { deviceId: { exact: mediaConstraints.audio } };
// CASE 2: MediaStreamTracks
if (typeof MediaStreamTrack !== 'undefined' && audioSource instanceof MediaStreamTrack) {
// Already provided audio track
myConstraints.audioTrack = audioSource;
}
if (typeof MediaStreamTrack !== 'undefined' && videoSource instanceof MediaStreamTrack) {
// Already provided video track
myConstraints.videoTrack = videoSource;
}
if (mediaConstraints.video) {
// CASE 3: Default tracks
if (audioSource === undefined) {
myConstraints.constraints!.audio = true;
}
if (videoSource === undefined) {
myConstraints.constraints!.video = {
width: {
ideal: 640
},
height: {
ideal: 480
}
};
}
// CASE 3.5: give values to resolution and frameRate if video not null/false
if (videoSource !== null && videoSource !== false) {
if (!!publisherProperties.resolution) {
const widthAndHeight = publisherProperties.resolution.toLowerCase().split('x');
const width = Number(widthAndHeight[0]);
const height = Number(widthAndHeight[1]);
(mediaConstraints.video as any).width.ideal = width;
(mediaConstraints.video as any).height.ideal = height;
const idealWidth = Number(widthAndHeight[0]);
const idealHeight = Number(widthAndHeight[1]);
myConstraints.constraints!.video = {
width: {
ideal: idealWidth
},
height: {
ideal: idealHeight
}
}
}
if (!!publisherProperties.frameRate) {
(mediaConstraints.video as any).frameRate = { ideal: publisherProperties.frameRate };
(<MediaTrackConstraints>myConstraints.constraints!.video).frameRate = { ideal: publisherProperties.frameRate };
}
}
if (!!publisherProperties.videoSource && typeof publisherProperties.videoSource === 'string') {
// CASE 4: deviceId or screen sharing
if (typeof audioSource === 'string') {
myConstraints.constraints!.audio = { deviceId: { exact: audioSource } };
}
if (typeof videoSource === 'string') {
if (publisherProperties.videoSource === 'screen' ||
publisherProperties.videoSource === 'window' ||
(platform.name === 'Electron' && publisherProperties.videoSource.startsWith('screen:'))) {
if (!this.isScreenShare(videoSource)) {
if (!myConstraints.constraints!.video) {
myConstraints.constraints!.video = {};
}
(<MediaTrackConstraints>myConstraints.constraints!.video)['deviceId'] = { exact: videoSource };
} else {
// Screen sharing
if (!this.checkScreenSharingCapabilities()) {
const error = new OpenViduError(OpenViduErrorName.SCREEN_SHARING_NOT_SUPPORTED, 'You can only screen share in desktop Chrome, Firefox, Opera or Electron. Detected client: ' + platform.name);
console.error(error);
reject(error);
} else {
if (platform.name === 'Electron') {
const prefix = "screen:";
const videoSourceString: string = publisherProperties.videoSource;
const videoSourceString: string = videoSource;
const electronScreenId = videoSourceString.substr(videoSourceString.indexOf(prefix) + prefix.length);
(<any>mediaConstraints['video']) = {
(<any>myConstraints.constraints!.video) = {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: electronScreenId
}
};
resolve(mediaConstraints);
resolve(myConstraints);
} else {
@ -732,7 +787,7 @@ export class OpenVidu {
reject(error);
} else {
const extensionId = this.advancedConfiguration.screenShareChromeExtension!.split('/').pop()!!.trim();
screenSharing.getChromeExtensionStatus(extensionId, (status) => {
screenSharing.getChromeExtensionStatus(extensionId, status => {
if (status === 'installed-disabled') {
const error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_DISABLED, 'You must enable the screen extension');
console.error(error);
@ -744,18 +799,19 @@ export class OpenVidu {
reject(error);
}
});
return;
}
} else {
mediaConstraints.video = screenConstraints;
resolve(mediaConstraints);
myConstraints.constraints!.video = screenConstraints;
resolve(myConstraints);
}
});
return;
} else {
if (navigator.mediaDevices['getDisplayMedia']) {
// getDisplayMedia support (Chrome >= 72, Firefox >= 66)
resolve(mediaConstraints);
resolve(myConstraints);
} else {
// Default screen sharing extension for Chrome/Opera, or is Firefox < 66
const firefoxString = platform.name!.indexOf('Firefox') !== -1 ? publisherProperties.videoSource : undefined;
@ -765,41 +821,37 @@ export class OpenVidu {
if (error === 'not-installed') {
const extensionUrl = !!this.advancedConfiguration.screenShareChromeExtension ? this.advancedConfiguration.screenShareChromeExtension :
'https://chrome.google.com/webstore/detail/openvidu-screensharing/lfcgfepafnobdloecchnfaclibenjold';
const error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, extensionUrl);
console.error(error);
reject(error);
const err = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_NOT_INSTALLED, extensionUrl);
console.error(err);
reject(err);
} else if (error === 'installed-disabled') {
const error = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_DISABLED, 'You must enable the screen extension');
console.error(error);
reject(error);
const err = new OpenViduError(OpenViduErrorName.SCREEN_EXTENSION_DISABLED, 'You must enable the screen extension');
console.error(err);
reject(err);
} else if (error === 'permission-denied') {
const error = new OpenViduError(OpenViduErrorName.SCREEN_CAPTURE_DENIED, 'You must allow access to one window of your desktop');
const err = new OpenViduError(OpenViduErrorName.SCREEN_CAPTURE_DENIED, 'You must allow access to one window of your desktop');
console.error(err);
reject(err);
} else {
const err = new OpenViduError(OpenViduErrorName.GENERIC_ERROR, 'Unknown error when accessing screen share');
console.error(err);
console.error(error);
reject(error);
reject(err);
}
} else {
mediaConstraints.video = screenConstraints.video;
resolve(mediaConstraints);
myConstraints.constraints!.video = screenConstraints.video;
resolve(myConstraints);
}
});
return;
}
}
}
}
}
}
publisherProperties.videoSource = 'screen';
}
}
} else {
// tslint:disable-next-line:no-string-literal
mediaConstraints.video['deviceId'] = { exact: publisherProperties.videoSource };
resolve(mediaConstraints);
}
} else {
resolve(mediaConstraints);
}
} else {
resolve(mediaConstraints);
}
resolve(myConstraints);
});
}
@ -912,6 +964,18 @@ export class OpenVidu {
}
}
/**
* @hidden
*/
addAlreadyProvidedTracks(myConstraints: CustomMediaStreamConstraints, mediaStream: MediaStream) {
if (!!myConstraints.videoTrack) {
mediaStream.addTrack(myConstraints.videoTrack);
}
if (!!myConstraints.audioTrack) {
mediaStream.addTrack(myConstraints.audioTrack);
}
return mediaStream;
}
/* Private methods */
@ -961,4 +1025,10 @@ export class OpenVidu {
}
}
private isScreenShare(videoSource: string) {
return videoSource === 'screen' ||
videoSource === 'window' ||
(platform.name === 'Electron' && videoSource.startsWith('screen:'))
}
}

View File

@ -279,7 +279,22 @@ export class Publisher extends StreamManager {
*/
replaceTrack(track: MediaStreamTrack): Promise<any> {
return new Promise((resolve, reject) => {
this.stream.getRTCPeerConnection().getSenders()[0].replaceTrack(track).then(() => {
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) {
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) {
reject(new Error('There\'s no replaceable track for that kind of MediaStreamTrack in this Publisher object'))
}
} else {
reject(new Error('Unknown track kind ' + track.kind));
}
(<any>sender).replaceTrack(track).then(() => {
let removedTrack: MediaStreamTrack;
if (track.kind === 'video') {
removedTrack = this.stream.getMediaStream().getVideoTracks()[0];
@ -567,30 +582,17 @@ export class Publisher extends StreamManager {
}
}
// Check if new constraints need to be generated. No constraints needed if
// - video track is given and no audio
// - audio track is given and no video
// - both video and audio tracks are given
if ((typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack && !this.properties.audioSource)
|| (typeof MediaStreamTrack !== 'undefined' && this.properties.audioSource instanceof MediaStreamTrack && !this.properties.videoSource)
|| (typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack && this.properties.audioSource instanceof MediaStreamTrack)) {
const mediaStream = new MediaStream();
if (typeof MediaStreamTrack !== 'undefined' && this.properties.videoSource instanceof MediaStreamTrack) {
mediaStream.addTrack((<MediaStreamTrack>this.properties.videoSource));
}
if (typeof MediaStreamTrack !== 'undefined' && this.properties.audioSource instanceof MediaStreamTrack) {
mediaStream.addTrack((<MediaStreamTrack>this.properties.audioSource));
}
// MediaStreamTracks are handled within callback - just call callback with new MediaStream() and let it handle the sources
successCallback(mediaStream);
this.openvidu.generateMediaConstraints(this.properties)
.then(myConstraints => {
if (myConstraints.constraints === undefined) {
// No need to call getUserMedia at all. MediaStreamTracks already provided
successCallback(this.openvidu.addAlreadyProvidedTracks(myConstraints, new MediaStream()));
// Return as we do not need to process further
return;
}
this.openvidu.generateMediaConstraints(this.properties)
.then(myConstraints => {
constraints = myConstraints;
constraints = myConstraints.constraints;
const outboundStreamOptions = {
mediaConstraints: constraints,
@ -605,19 +607,18 @@ export class Publisher extends StreamManager {
this.setPermissionDialogTimer(timeForDialogEvent);
if (this.stream.isSendScreen() && navigator.mediaDevices['getDisplayMedia'] && platform.name !== 'Electron') {
navigator.mediaDevices['getDisplayMedia']({ video: true })
.then(mediaStream => {
this.openvidu.addAlreadyProvidedTracks(myConstraints, mediaStream);
getMediaSuccess(mediaStream, definedAudioConstraint);
})
.catch(error => {
getMediaError(error);
});
} else {
navigator.mediaDevices.getUserMedia(constraintsAux)
.then(mediaStream => {
this.openvidu.addAlreadyProvidedTracks(myConstraints, mediaStream);
getMediaSuccess(mediaStream, definedAudioConstraint);
})
.catch(error => {

View File

@ -0,0 +1,22 @@
/*
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
export interface CustomMediaStreamConstraints {
constraints: MediaStreamConstraints | undefined;
audioTrack: MediaStreamTrack | undefined;
videoTrack: MediaStreamTrack | undefined;
}