ov-components: Refactored and fixed device service

pull/839/head
Carlos Santos 2024-07-30 15:45:22 +02:00
parent 7b560b447c
commit 5072804e67
6 changed files with 126 additions and 166 deletions

View File

@ -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.

View File

@ -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));
}
/**

View File

@ -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);
}
}
}

View File

@ -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 <CustomDevice>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 <CustomDevice>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 [];
}
}

View File

@ -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<LocalTrack[]> 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<LocalTrack[]> {
async createLocalTracks(
videoDeviceId: string | boolean | undefined = undefined,
audioDeviceId: string | boolean | undefined = undefined
): Promise<LocalTrack[]> {
// 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: {}

View File

@ -79,11 +79,11 @@ export class OpenviduWebComponentComponent {
/**
* @internal
*/
_videoEnabled: boolean = false;
_videoEnabled: boolean = true;
/**
* @internal
*/
_audioEnabled: boolean = false;
_audioEnabled: boolean = true;
/**
* @internal
*/