import { Injectable } from '@angular/core'; import { Device, OpenVidu, OpenViduError, OpenViduErrorName } from 'openvidu-browser'; import { CameraType, DeviceType, CustomDevice } from '../../models/device.model'; import { ILogger } from '../../models/logger.model'; import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service'; import { LoggerService } from '../logger/logger.service'; import { PlatformService } from '../platform/platform.service'; import { StorageService } from '../storage/storage.service'; /** * @internal */ @Injectable({ providedIn: 'root' }) export class DeviceService { private OV: OpenVidu = null; private devices: Device[]; private cameras: CustomDevice[] = []; private microphones: CustomDevice[] = []; private cameraSelected: CustomDevice; private microphoneSelected: CustomDevice; private log: ILogger; private videoDevicesDisabled: boolean; private audioDevicesDisabled: boolean; // Initialized with Storage.VIDEO_MUTED info saved on storage 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, private libSrv: OpenViduAngularConfigService ) { this.log = this.loggerSrv.get('DevicesService'); this.OV = new OpenVidu(); } async forceUpdate() { await this.initializeDevices(); } async initializeDevices() { 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(); const customDevices = this.initializeCustomDevices(this.devices); this.cameras = customDevices.cameras; this.microphones = customDevices.microphones; this._isVideoMuted = this.storageSrv.isVideoMuted() || this.libSrv.videoMuted.getValue(); this._isAudioMuted = this.storageSrv.isAudioMuted() || this.libSrv.audioMuted.getValue(); this.log.d('Media devices', customDevices); } private initializeCustomDevices(defaultVDevices: Device[]) { const FIRST_POSITION = 0; 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 (defaultMicrophones.length > 0) { defaultMicrophones.forEach((device: Device) => { customDevices.microphones.push({ label: device.label, device: device.deviceId }); }); // Setting microphone selected const storageMicrophone = this.getMicrophoneFromStogare(); if (!!storageMicrophone) { this.microphoneSelected = storageMicrophone; } else if (customDevices.microphones.length > 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 (defaultCameras.length > 0) { defaultCameras.forEach((device: Device, 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; } } customDevices.cameras.push(myDevice); }); // Setting camera selected const storageCamera = this.getCameraFromStorage(); if (!!storageCamera) { this.cameraSelected = storageCamera; } else if (customDevices.cameras.length > 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; } isVideoMuted(): boolean { return this.hasVideoDeviceAvailable() && this._isVideoMuted; } isAudioMuted(): boolean { return this.hasAudioDeviceAvailable() && this._isAudioMuted; } getCameraSelected(): CustomDevice { return this.cameraSelected; } getMicrophoneSelected(): CustomDevice { return this.microphoneSelected; } setCameraSelected(deviceField: any) { this.cameraSelected = this.getCameraByDeviceField(deviceField); this.saveCameraToStorage(this.cameraSelected); } setMicSelected(deviceField: any) { this.microphoneSelected = this.getMicrophoneByDeviceField(deviceField); this.saveMicrophoneToStorage(this.microphoneSelected); } needUpdateVideoTrack(newVideoSource: string): boolean { return this.cameraSelected.device !== newVideoSource; } needUpdateAudioTrack(newAudioSource: string): boolean { return this.microphoneSelected.device !== newAudioSource; } getCameras(): CustomDevice[] { return this.cameras; } getMicrophones(): CustomDevice[] { return this.microphones; } hasVideoDeviceAvailable(): boolean { return !this.videoDevicesDisabled && this.cameras.length > 0; } hasAudioDeviceAvailable(): boolean { return !this.audioDevicesDisabled && this.microphones.length > 0; } cameraNeedsMirror(deviceField: string): boolean { return this.getCameraByDeviceField(deviceField)?.type === CameraType.FRONT; } areEmptyLabels(): boolean { return !!this.cameras.find((device) => device.label === '') || !!this.microphones.find((device) => device.label === ''); } disableVideoDevices() { this.videoDevicesDisabled = true; } disableAudioDevices() { this.audioDevicesDisabled = true; } clear() { // this.OV = new OpenVidu(); this.devices = []; this.cameras = []; this.microphones = []; this.cameraSelected = null; this.microphoneSelected = null; this.videoDevicesDisabled = false; this.audioDevicesDisabled = false; } private getCameraByDeviceField(deviceField: any): CustomDevice { return this.cameras.find((opt: CustomDevice) => opt.device === deviceField || opt.label === deviceField); } private getMicrophoneByDeviceField(deviceField: any): CustomDevice { return this.microphones.find((opt: CustomDevice) => opt.device === deviceField || opt.label === deviceField); } private getMicrophoneFromStogare(): CustomDevice { const storageDevice: CustomDevice = this.storageSrv.getAudioDevice(); if (!!storageDevice && this.microphones.some((device) => device.device === storageDevice.device)) { return storageDevice; } } private getCameraFromStorage() { const storageDevice: CustomDevice = 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); } }