From e63ef15d1832a12bf20ad5ca69ae82303b8ddd4b Mon Sep 17 00:00:00 2001 From: csantosm <4a.santos@gmail.com> Date: Tue, 22 Feb 2022 15:17:15 +0100 Subject: [PATCH] openvidu-components: Fixed bug replacing tracks in user-settings --- .../src/lib/services/device/device.service.ts | 43 ++++++++--- .../lib/services/openvidu/openvidu.service.ts | 77 ++++++++++++++----- 2 files changed, 90 insertions(+), 30 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-angular/src/lib/services/device/device.service.ts b/openvidu-components-angular/projects/openvidu-angular/src/lib/services/device/device.service.ts index b28fcd22..5f610e99 100644 --- a/openvidu-components-angular/projects/openvidu-angular/src/lib/services/device/device.service.ts +++ b/openvidu-components-angular/projects/openvidu-angular/src/lib/services/device/device.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Device, OpenVidu } from 'openvidu-browser'; +import { Device, OpenVidu, OpenViduError, OpenViduErrorName } from 'openvidu-browser'; import { CameraType, DeviceType, CustomDevice } from '../../models/device.model'; import { ILogger } from '../../models/logger.model'; @@ -26,6 +26,7 @@ export class DeviceService { private _isVideoMuted: boolean; // Initialized with Storage.AUDIO_MUTED info saved on storage private _isAudioMuted: boolean; + private deviceAccessDeniedError: boolean = false; constructor(private loggerSrv: LoggerService, private platformSrv: PlatformService, private storageSrv: StorageService) { this.log = this.loggerSrv.get('DevicesService'); @@ -37,13 +38,19 @@ export class DeviceService { } async initializeDevices() { - // Forcing media permissions request. - // Sometimes, browser doens't launch the media permissions modal. - const mediaStream = await this.OV.getUserMedia({ audioSource: undefined, videoSource: undefined }); - mediaStream?.getAudioTracks().forEach((track) => track.stop()); - mediaStream?.getVideoTracks().forEach((track) => track.stop()); + + try { + // Forcing media permissions request. + // Sometimes, browser doens't launch the media permissions modal. + const mediaStream = await this.OV.getUserMedia({ audioSource: undefined, videoSource: undefined }); + mediaStream?.getAudioTracks().forEach((track) => track.stop()); + mediaStream?.getVideoTracks().forEach((track) => track.stop()); + } catch (error) { + this.deviceAccessDeniedError = (error).name === OpenViduErrorName.DEVICE_ACCESS_DENIED; + } this.devices = await this.OV.getDevices(); + console.log(this.devices); const customDevices = this.initializeCustomDevices(this.devices); this.cameras = customDevices.cameras; this.microphones = customDevices.microphones; @@ -56,14 +63,14 @@ export class DeviceService { private initializeCustomDevices(defaultVDevices: Device[]) { const FIRST_POSITION = 0; - const defaultMicrophones = defaultVDevices.filter((device) => device.kind === DeviceType.AUDIO_INPUT); - const defaultCameras = defaultVDevices.filter((device) => device.kind === DeviceType.VIDEO_INPUT); + const defaultMicrophones: Device[] = defaultVDevices.filter((device) => device.kind === DeviceType.AUDIO_INPUT); + const defaultCameras: Device[] = defaultVDevices.filter((device) => device.kind === DeviceType.VIDEO_INPUT); const customDevices: { cameras: CustomDevice[]; microphones: CustomDevice[] } = { cameras: [], microphones: [] }; - if (this.hasAudioDeviceAvailable) { + if (defaultMicrophones.length > 0) { defaultMicrophones.forEach((device: Device) => { customDevices.microphones.push({ label: device.label, device: device.deviceId }); }); @@ -73,11 +80,17 @@ export class DeviceService { if (!!storageMicrophone) { this.microphoneSelected = storageMicrophone; } else if (customDevices.microphones.length > 0) { - this.microphoneSelected = customDevices.microphones[0]; + if(this.deviceAccessDeniedError && customDevices.microphones.length > 1){ + // We assume that the default device is already in use + // Assign an alternative device with the aim of avoiding the DEVICE_ALREADY_IN_USE error + this.microphoneSelected = customDevices.microphones[1]; + } else { + this.microphoneSelected = customDevices.microphones[0]; + } } } - if (this.hasVideoDeviceAvailable) { + if (defaultCameras.length > 0) { defaultCameras.forEach((device: Device, index: number) => { const myDevice: CustomDevice = { label: device.label, @@ -103,7 +116,13 @@ export class DeviceService { if (!!storageCamera) { this.cameraSelected = storageCamera; } else if (customDevices.cameras.length > 0) { - this.cameraSelected = customDevices.cameras[0]; + if(this.deviceAccessDeniedError && customDevices.cameras.length > 1){ + // We assume that the default device is already in use + // Assign an alternative device with the aim of avoiding the DEVICE_ALREADY_IN_USE error + this.cameraSelected = customDevices.cameras[1]; + } else { + this.cameraSelected = customDevices.cameras[0]; + } } } return customDevices; diff --git a/openvidu-components-angular/projects/openvidu-angular/src/lib/services/openvidu/openvidu.service.ts b/openvidu-components-angular/projects/openvidu-angular/src/lib/services/openvidu/openvidu.service.ts index a6922f77..d55d3bcb 100644 --- a/openvidu-components-angular/projects/openvidu-angular/src/lib/services/openvidu/openvidu.service.ts +++ b/openvidu-components-angular/projects/openvidu-angular/src/lib/services/openvidu/openvidu.service.ts @@ -1,5 +1,14 @@ import { Injectable } from '@angular/core'; -import { Connection, OpenVidu, Publisher, PublisherProperties, Session, SignalOptions } from 'openvidu-browser'; +import { + Connection, + OpenVidu, + OpenViduError, + OpenViduErrorName, + Publisher, + PublisherProperties, + Session, + SignalOptions +} from 'openvidu-browser'; import { LoggerService } from '../logger/logger.service'; @@ -107,7 +116,6 @@ export class OpenViduService { * Initialize a publisher checking devices saved on storage or if participant have devices available. */ async initDefaultPublisher(targetElement: string | HTMLElement): Promise { - const hasVideoDevices = this.deviceService.hasVideoDeviceAvailable(); const hasAudioDevices = this.deviceService.hasAudioDeviceAvailable(); const isVideoActive = !this.deviceService.isVideoMuted(); @@ -272,29 +280,35 @@ export class OpenViduService { async replaceTrack(videoType: VideoType, props: PublisherProperties) { try { this.log.d(`Replacing ${videoType} track`, props); + if (videoType === VideoType.CAMERA) { + let mediaStream: MediaStream; + const oldMediaStream = this.participantService.getMyCameraPublisher().stream.getMediaStream(); + const isFirefoxPlatform = this.platformService.isFirefox(); const isReplacingAudio = !!props.audioSource; const isReplacingVideo = !!props.videoSource; - if (this.platformService.isFirefox()) { - // Firefox throw an exception trying to get a new MediaStreamTrack if the older one is not stopped - // NotReadableError: Concurrent mic process limit. Stopping tracks before call to getUserMedia - if (isReplacingVideo) { - this.participantService.getMyCameraPublisher().stream.getMediaStream().getVideoTracks()[0].stop(); - } else if (isReplacingAudio) { - this.participantService.getMyCameraPublisher().stream.getMediaStream().getAudioTracks()[0].stop(); + if (isReplacingVideo) { + if (isFirefoxPlatform) { + // Firefox throw an exception trying to get a new MediaStreamTrack if the older one is not stopped + // NotReadableError: Concurrent mic process limit. Stopping tracks before call to getUserMedia + oldMediaStream.getVideoTracks()[0].stop(); } - } - - const track = await this.OV.getUserMedia(props); - if (isReplacingAudio) { - // Replace audio track - const audioTrack: MediaStreamTrack = track.getAudioTracks()[0]; - await this.participantService.getMyCameraPublisher().replaceTrack(audioTrack); - } else if (isReplacingVideo) { + mediaStream = await this.createMediaStream(props); // Replace video track - const videoTrack: MediaStreamTrack = track.getVideoTracks()[0]; + const videoTrack: MediaStreamTrack = mediaStream.getVideoTracks()[0]; await this.participantService.getMyCameraPublisher().replaceTrack(videoTrack); + + } else if (isReplacingAudio) { + if (isFirefoxPlatform) { + // Firefox throw an exception trying to get a new MediaStreamTrack if the older one is not stopped + // NotReadableError: Concurrent mic process limit. Stopping tracks before call to getUserMedia + oldMediaStream.getAudioTracks()[0].stop(); + } + mediaStream = await this.createMediaStream(props); + // Replace audio track + const audioTrack: MediaStreamTrack = mediaStream.getAudioTracks()[0]; + await this.participantService.getMyCameraPublisher().replaceTrack(audioTrack); } } else if (videoType === VideoType.SCREEN) { const newScreenMediaStream = await this.OVScreen.getUserMedia(props); @@ -307,6 +321,33 @@ export class OpenViduService { } } + private async createMediaStream(pp: PublisherProperties): Promise{ + let mediaStream: MediaStream; + const isFirefoxPlatform = this.platformService.isFirefox(); + const isReplacingAudio = !!pp.audioSource; + const isReplacingVideo = !!pp.videoSource; + + try { + mediaStream = await this.OV.getUserMedia(pp); + } catch (error) { + if ((error).name === OpenViduErrorName.DEVICE_ACCESS_DENIED) { + if (isFirefoxPlatform) { + this.log.w('The device requested is not available. Restoring the older one'); + // The track requested is not available so we are getting the old tracks ids for recovering the track + if (isReplacingVideo) { + pp.videoSource = this.deviceService.getCameraSelected().device; + } else if (isReplacingAudio) { + pp.audioSource = this.deviceService.getMicrophoneSelected().device; + } + mediaStream = await this.OV.getUserMedia(pp); + // TODO show error alert informing that the new device is not available + } + } + } finally { + return mediaStream; + } + } + needSendNicknameSignal(): boolean { const oldNickname: string = JSON.parse(this.webcamSession.connection.data).clientData; return oldNickname !== this.participantService.getWebcamNickname();