From 5072804e67fcc5bb6baa2e1ce6095bdd9528f880 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Tue, 30 Jul 2024 15:45:22 +0200 Subject: [PATCH] ov-components: Refactored and fixed device service --- .../components/pre-join/pre-join.component.ts | 23 ++- .../videoconference.component.ts | 2 +- .../api/videoconference.directive.ts | 50 ++++- .../src/lib/services/device/device.service.ts | 191 +++++------------- .../lib/services/openvidu/openvidu.service.ts | 22 +- .../openvidu-webcomponent.component.ts | 4 +- 6 files changed, 126 insertions(+), 166 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts index d9fb1d0a..3bcb017c 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts @@ -73,17 +73,7 @@ export class PreJoinComponent implements OnInit, OnDestroy { async ngOnInit() { this.subscribeToPrejoinDirectives(); - try { - const cameraEnabled = this.storageService.isCameraEnabled(); - const microphoneEnabled = this.storageService.isMicrophoneEnabled(); - this.tracks = await this.openviduService.createLocalTracks(cameraEnabled, microphoneEnabled); - this.openviduService.setLocalTracks(this.tracks); - this.videoTrack = this.tracks.find((track) => track.kind === 'video'); - this.audioTrack = this.tracks.find((track) => track.kind === 'audio'); - } catch (error) { - this.log.e('Error creating local tracks:', error); - } - + await this.initializeDevices(); this.windowSize = window.innerWidth; this.isLoading = false; } @@ -105,6 +95,17 @@ export class PreJoinComponent implements OnInit, OnDestroy { } } + private async initializeDevices() { + try { + this.tracks = await this.openviduService.createLocalTracks(); + this.openviduService.setLocalTracks(this.tracks); + this.videoTrack = this.tracks.find((track) => track.kind === 'video'); + this.audioTrack = this.tracks.find((track) => track.kind === 'audio'); + } catch (error) { + this.log.e('Error creating local tracks:', error); + } + } + onDeviceSelectorClicked() { // Some devices as iPhone do not show the menu panels correctly // Updating the container where the panel is added fix the problem. diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.ts index e33d1f09..a9d8eb79 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/videoconference/videoconference.component.ts @@ -481,7 +481,7 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni } this.openviduAngularLayoutTemplate = this.defaultLayoutTemplate; } - this.deviceSrv.forceInitDevices().then(() => (this.loading = false)); + this.deviceSrv.initializeDevices().then(() => (this.loading = false)); } /** diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts index 255b027c..6f530e66 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts @@ -4,6 +4,7 @@ import { CaptionsLangOption } from '../../models/caption.model'; import { OpenViduComponentsConfigService } from '../../services/config/openvidu-components-angular.config.service'; import { TranslateService } from '../../services/translate/translate.service'; import { LangOption } from '../../models/lang.model'; +import { StorageService } from '../../services/storage/storage.service'; /** * The **livekitUrl** directive sets the livekitUrl to grant a participant access to a Room. @@ -614,7 +615,8 @@ export class VideoEnabledDirective implements OnDestroy { */ constructor( public elementRef: ElementRef, - private libService: OpenViduComponentsConfigService + private libService: OpenViduComponentsConfigService, + private storageService: StorageService ) {} /** @@ -634,9 +636,25 @@ export class VideoEnabledDirective implements OnDestroy { /** * @ignore */ - update(value: boolean) { - if (this.libService.isVideoEnabled() !== value) { - this.libService.setVideoEnabled(value); + update(enabled: boolean) { + const storageIsEnabled = this.storageService.isCameraEnabled(); + + // Determine the final enabled state of the camera + let finalEnabledState: boolean; + if (enabled) { + // If enabled is true, respect the storage value if it's false + finalEnabledState = storageIsEnabled !== false; + } else { + // If enabled is false, disable the camera + finalEnabledState = false; + } + + // Update the storage with the final state + this.storageService.setCameraEnabled(finalEnabledState); + + // Ensure libService state is consistent with the final enabled state + if (this.libService.isVideoEnabled() !== finalEnabledState) { + this.libService.setVideoEnabled(finalEnabledState); } } } @@ -668,7 +686,8 @@ export class AudioEnabledDirective implements OnDestroy { */ constructor( public elementRef: ElementRef, - private libService: OpenViduComponentsConfigService + private libService: OpenViduComponentsConfigService, + private storageService: StorageService ) {} ngOnDestroy(): void { @@ -685,9 +704,24 @@ export class AudioEnabledDirective implements OnDestroy { /** * @ignore */ - update(value: boolean) { - if (this.libService.isAudioEnabled() !== value) { - this.libService.setAudioEnabled(value); + update(enabled: boolean) { + const storageIsEnabled = this.storageService.isMicrophoneEnabled(); + + // Determine the final enabled state of the microphone + let finalEnabledState: boolean; + if (enabled) { + // If enabled is true, respect the storage value if it's false + finalEnabledState = storageIsEnabled !== false; + } else { + // If enabled is false, disable the camera + finalEnabledState = false; + } + + // Update the storage with the final state + this.storageService.setMicrophoneEnabled(finalEnabledState); + + if (this.libService.isAudioEnabled() !== enabled) { + this.libService.setAudioEnabled(enabled); } } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/device/device.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/device/device.service.ts index def0edab..7756be0c 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/device/device.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/device/device.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { CameraType, CustomDevice, DeviceType } from '../../models/device.model'; import { ILogger } from '../../models/logger.model'; -import { OpenViduComponentsConfigService } from '../config/openvidu-components-angular.config.service'; import { LoggerService } from '../logger/logger.service'; import { PlatformService } from '../platform/platform.service'; import { StorageService } from '../storage/storage.service'; @@ -17,24 +16,17 @@ export class DeviceService { private devices: MediaDeviceInfo[]; private cameras: CustomDevice[] = []; private microphones: CustomDevice[] = []; - private cameraSelected: CustomDevice | undefined; - private microphoneSelected: CustomDevice | undefined; + private cameraSelected?: CustomDevice; + private microphoneSelected?: CustomDevice; private log: ILogger; private videoDevicesEnabled: boolean = true; private audioDevicesEnabled: boolean = true; - - // Initialized with Storage.CAMERA_ENABLED info saved on storage - private _isCameraEnabled: boolean; - // Initialized with Storage.MICROPHONE_ENABLED info saved on storage - private _isMicrophoneEnabled: boolean; - // Whether the media devices permission have been rejected or not private deviceAccessDeniedError: boolean = false; constructor( private loggerSrv: LoggerService, private platformSrv: PlatformService, - private storageSrv: StorageService, - private libSrv: OpenViduComponentsConfigService + private storageSrv: StorageService ) { this.log = this.loggerSrv.get('DevicesService'); } @@ -43,26 +35,21 @@ export class DeviceService { * Initialize media devices and select a devices checking in local storage (if exists) or * first devices found by default */ - async forceInitDevices() { + async initializeDevices() { this.clear(); try { this.devices = await this.getLocalDevices(); - } catch (error) { - this.log.e('Error getting media devices', error); - } finally { if (this.deviceAccessDeniedError) { this.log.w('Media devices permissions were not granted.'); - } else { - this.initializeCustomDevices(); - this.updateAudioDeviceSelected(); - this.updateVideoDeviceSelected(); - - this._isCameraEnabled = this.storageSrv.isCameraEnabled() || this.libSrv.isVideoEnabled(); - this._isMicrophoneEnabled = this.storageSrv.isMicrophoneEnabled() || this.libSrv.isAudioEnabled(); - - this.log.d('Media devices', this.cameras, this.microphones); + return; } + + this.initializeCustomDevices(); + this.updateSelectedDevices(); + this.log.d('Media devices', this.cameras, this.microphones); + } catch (error) { + this.log.e('Error getting media devices', error); } } @@ -76,87 +63,52 @@ export class DeviceService { } } - private initializeCustomDevices(updateSelected: boolean = true): void { - const FIRST_POSITION = 0; - const defaultMicrophones: MediaDeviceInfo[] = this.devices.filter((device) => device.kind === DeviceType.AUDIO_INPUT); - const defaultCameras: MediaDeviceInfo[] = this.devices.filter((device) => device.kind === DeviceType.VIDEO_INPUT); + private initializeCustomDevices(): void { + this.cameras = this.devices + .filter((d) => d.kind === DeviceType.VIDEO_INPUT) + .map((d) => this.createCustomDevice(d, CameraType.BACK)); + this.microphones = this.devices + .filter((d) => d.kind === DeviceType.AUDIO_INPUT) + .map((d) => ({ label: d.label, device: d.deviceId })); - if (defaultMicrophones.length > 0) { - this.microphones = []; - defaultMicrophones.forEach((device: MediaDeviceInfo) => { - this.microphones.push({ label: device.label, device: device.deviceId }); - }); - } - - if (defaultCameras.length > 0) { - this.cameras = []; - defaultCameras.forEach((device: MediaDeviceInfo, index: number) => { - const myDevice: CustomDevice = { - label: device.label, - device: device.deviceId, - type: CameraType.BACK - }; - if (this.platformSrv.isMobile()) { - // We assume front video device has 'front' in its label in Mobile devices - if (myDevice.label.toLowerCase().includes(CameraType.FRONT.toLowerCase())) { - myDevice.type = CameraType.FRONT; - } - } else { - // We assume first device is web camera in Browser Desktop - if (index === FIRST_POSITION) { - myDevice.type = CameraType.FRONT; - } + if (this.platformSrv.isMobile()) { + this.cameras.forEach((c) => { + if (c.label.toLowerCase().includes(CameraType.FRONT.toLowerCase())) { + c.type = CameraType.FRONT; } - this.cameras.push(myDevice); }); + } else if (this.cameras.length > 0) { + this.cameras[0].type = CameraType.FRONT; } } - private updateAudioDeviceSelected() { - // Setting microphone selected - if (this.microphones.length > 0) { - const storageMicrophone = this.getMicrophoneFromStogare(); - if (!!storageMicrophone) { - this.microphoneSelected = storageMicrophone; - } else if (this.microphones.length > 0) { - if (this.deviceAccessDeniedError && this.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 = this.microphones[1]; - } else { - this.microphoneSelected = this.microphones[0]; - } - } - } + private createCustomDevice(device: MediaDeviceInfo, defaultType: CameraType): CustomDevice { + return { + label: device.label, + device: device.deviceId, + type: defaultType + }; } - private updateVideoDeviceSelected() { - // Setting camera selected - if (this.cameras.length > 0) { - const storageCamera = this.getCameraFromStorage(); - if (!!storageCamera) { - this.cameraSelected = storageCamera; - } else if (this.cameras.length > 0) { - if (this.deviceAccessDeniedError && this.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 = this.cameras[1]; - } else { - this.cameraSelected = this.cameras[0]; - } - } - } + private updateSelectedDevices() { + this.cameraSelected = this.getDeviceFromStorage(this.cameras, this.storageSrv.getVideoDevice()) || this.cameras[0]; + this.microphoneSelected = this.getDeviceFromStorage(this.microphones, this.storageSrv.getAudioDevice()) || this.microphones[0]; + } + + private getDeviceFromStorage(devices: CustomDevice[], storageDevice: CustomDevice | null): CustomDevice | undefined { + if (!storageDevice) return; + return devices.find((d) => d.device === storageDevice.device); } /** * @internal */ isCameraEnabled(): boolean { - return this.hasVideoDeviceAvailable() && this._isCameraEnabled; + return this.hasVideoDeviceAvailable() && this.storageSrv.isCameraEnabled(); } isMicrophoneEnabled(): boolean { - return this.hasAudioDeviceAvailable() && this._isMicrophoneEnabled; + return this.hasAudioDeviceAvailable() && this.storageSrv.isMicrophoneEnabled(); } getCameraSelected(): CustomDevice | undefined { @@ -168,13 +120,15 @@ export class DeviceService { } setCameraSelected(deviceId: any) { - this.cameraSelected = this.getCameraByDeviceField(deviceId); - this.saveCameraToStorage(this.cameraSelected); + this.cameraSelected = this.getDeviceById(this.cameras, deviceId); + const saveFunction = (device) => this.storageSrv.setVideoDevice(device); + this.saveDeviceToStorage(this.cameraSelected, saveFunction); } - setMicSelected(deviceField: any) { - this.microphoneSelected = this.getMicrophoneByDeviceField(deviceField); - this.saveMicrophoneToStorage(this.microphoneSelected); + setMicSelected(deviceId: string) { + this.microphoneSelected = this.getDeviceById(this.microphones, deviceId); + const saveFunction = (device) => this.storageSrv.setAudioDevice(device); + this.saveDeviceToStorage(this.microphoneSelected, saveFunction); } needUpdateVideoTrack(newDevice: CustomDevice): boolean { @@ -201,18 +155,6 @@ export class DeviceService { return this.audioDevicesEnabled && this.microphones.length > 0; } - cameraNeedsMirror(deviceField: string): boolean { - return this.getCameraByDeviceField(deviceField)?.type === CameraType.FRONT; - } - - disableVideoDevices() { - this.videoDevicesEnabled = false; - } - - disableAudioDevices() { - this.audioDevicesEnabled = false; - } - clear() { this.devices = []; this.cameras = []; @@ -223,34 +165,12 @@ export class DeviceService { this.audioDevicesEnabled = true; } - private getCameraByDeviceField(deviceField: any): CustomDevice { - return this.cameras.find((opt: CustomDevice) => opt.device === deviceField || opt.label === deviceField); + private getDeviceById(devices: CustomDevice[], deviceId: string): CustomDevice | undefined { + return devices.find((d) => d.device === deviceId); } - private getMicrophoneByDeviceField(deviceField: any): CustomDevice { - return this.microphones.find((opt: CustomDevice) => opt.device === deviceField || opt.label === deviceField); - } - - private getMicrophoneFromStogare(): CustomDevice | undefined { - const storageDevice: CustomDevice | null = this.storageSrv.getAudioDevice(); - if (!!storageDevice && this.microphones.some((device) => device.device === storageDevice.device)) { - return storageDevice; - } - } - - private getCameraFromStorage() { - const storageDevice: CustomDevice | null = this.storageSrv.getVideoDevice(); - if (!!storageDevice && this.cameras.some((device) => device.device === storageDevice.device)) { - return storageDevice; - } - } - - private saveCameraToStorage(cam: CustomDevice) { - this.storageSrv.setVideoDevice(cam); - } - - private saveMicrophoneToStorage(mic: CustomDevice) { - this.storageSrv.setAudioDevice(mic); + private saveDeviceToStorage(device: CustomDevice | undefined, saveFunction: (device: CustomDevice) => void) { + if (device) saveFunction(device); } /** @@ -262,17 +182,14 @@ export class DeviceService { // Forcing media permissions request. let localTracks: LocalTrack[] = []; try { - try { - localTracks = await createLocalTracks({ audio: true, video: true }); - localTracks.forEach((track) => track.stop()); - } catch (error) { - this.log.e('Error getting local audio tracks', error); - } + localTracks = await createLocalTracks({ audio: true, video: true }); + localTracks.forEach((track) => track.stop()); const devices = this.platformSrv.isFirefox() ? await this.getMediaDevicesFirefox() : await Room.getLocalDevices(); return devices.filter((d: MediaDeviceInfo) => d.label && d.deviceId && d.deviceId !== 'default'); } catch (error) { this.log.e('Error getting local devices', error); + this.deviceAccessDeniedError = true; return []; } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts index acd4fa5d..5b27e4eb 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/openvidu/openvidu.service.ts @@ -47,7 +47,7 @@ export class OpenViduService { constructor( private loggerSrv: LoggerService, private deviceService: DeviceService, - private storageSrv: StorageService + private storageService: StorageService ) { this.log = this.loggerSrv.get('OpenViduService'); // this.isSttReadyObs = this._isSttReady.asObservable(); @@ -91,7 +91,7 @@ export class OpenViduService { try { await this.room.connect(this.livekitUrl, this.livekitToken); this.log.d(`Successfully connected to room ${this.room.name}`); - const participantName = this.storageSrv.getParticipantName(); + const participantName = this.storageService.getParticipantName(); if (participantName) { this.room.localParticipant.setName(participantName); } @@ -194,13 +194,21 @@ export class OpenViduService { } /** - * Allows create local tracks without connecting to a room. - * @param videoDeviceId string to specific device, false to disable video, true to default device - * @param audioDeviceId string to specific device, false to disable audio, true to default device - * @returns Promise with the created tracks + * Creates local tracks for video and audio devices. + * + * @param videoDeviceId - The ID of the video device to use. If not provided, the default video device will be used. + * @param audioDeviceId - The ID of the audio device to use. If not provided, the default audio device will be used. + * @returns A promise that resolves to an array of LocalTrack objects representing the created tracks. * @internal */ - async createLocalTracks(videoDeviceId: string | boolean, audioDeviceId: string | boolean): Promise { + async createLocalTracks( + videoDeviceId: string | boolean | undefined = undefined, + audioDeviceId: string | boolean | undefined = undefined + ): Promise { + // If video and audio device IDs are not provided, check if they are enabled and use the default devices + if (videoDeviceId === undefined) videoDeviceId = this.deviceService.isCameraEnabled(); + if (audioDeviceId === undefined) audioDeviceId = this.deviceService.isMicrophoneEnabled(); + let options: CreateLocalTracksOptions = { audio: { echoCancellation: true, noiseSuppression: true }, video: {} diff --git a/openvidu-components-angular/src/app/openvidu-webcomponent/openvidu-webcomponent.component.ts b/openvidu-components-angular/src/app/openvidu-webcomponent/openvidu-webcomponent.component.ts index faac56ce..0190fc61 100644 --- a/openvidu-components-angular/src/app/openvidu-webcomponent/openvidu-webcomponent.component.ts +++ b/openvidu-components-angular/src/app/openvidu-webcomponent/openvidu-webcomponent.component.ts @@ -79,11 +79,11 @@ export class OpenviduWebComponentComponent { /** * @internal */ - _videoEnabled: boolean = false; + _videoEnabled: boolean = true; /** * @internal */ - _audioEnabled: boolean = false; + _audioEnabled: boolean = true; /** * @internal */