mirror of https://github.com/OpenVidu/openvidu.git
ov-components: Refactor audio and video device components to use reactive signals for improved performance and cleaner code
parent
f4264a2a8a
commit
ed64c3a305
|
|
@ -1,18 +1,18 @@
|
|||
<div class="audio-device-selector" [class.compact]="compact">
|
||||
<!-- Unified Device Button (Compact Mode) -->
|
||||
@if (compact) {
|
||||
@if (hasAudioDevices) {
|
||||
@if (hasAudioDevices()) {
|
||||
<div class="unified-device-button">
|
||||
<!-- Main toggle button -->
|
||||
<button
|
||||
mat-flat-button
|
||||
class="toggle-section"
|
||||
[disabled]="!hasAudioDevices || microphoneStatusChanging"
|
||||
[disabled]="!hasAudioDevices() || microphoneStatusChanging"
|
||||
[class.device-enabled]="isMicrophoneEnabled"
|
||||
[class.device-disabled]="!isMicrophoneEnabled"
|
||||
(click)="toggleMic($event)"
|
||||
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
|
||||
[matTooltipDisabled]="!hasAudioDevices"
|
||||
[matTooltipDisabled]="!hasAudioDevices()"
|
||||
id="microphone-button"
|
||||
>
|
||||
<mat-icon [id]="isMicrophoneEnabled ? 'mic' : 'mic_off'">{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<!-- Normal Mode - Input Style Selector -->
|
||||
<div class="normal-device-selector">
|
||||
<!-- Input-style Device Selector -->
|
||||
<div class="device-input-selector" [class.disabled]="!hasAudioDevices || !isMicrophoneEnabled">
|
||||
<div class="device-input-selector" [class.disabled]="!hasAudioDevices() || !isMicrophoneEnabled">
|
||||
<!-- When microphone is enabled -->
|
||||
@if (isMicrophoneEnabled) {
|
||||
<div class="device-input-selector">
|
||||
|
|
@ -50,23 +50,23 @@
|
|||
mat-flat-button
|
||||
id="audio-dropdown"
|
||||
class="selector-button"
|
||||
[disabled]="microphoneStatusChanging || microphones.length <= 1"
|
||||
[disabled]="microphoneStatusChanging || microphones().length <= 1"
|
||||
[matMenuTriggerFor]="microphoneMenu"
|
||||
[attr.aria-expanded]="false"
|
||||
>
|
||||
<mat-icon class="device-icon">mic</mat-icon>
|
||||
<span class="selected-device-name">{{ microphoneSelected?.label || 'No microphone selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="microphones.length > 1">expand_more</mat-icon>
|
||||
<span class="selected-device-name">{{ microphoneSelected()?.label || 'No microphone selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="microphones().length > 1">expand_more</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
@if (hasAudioDevices) {
|
||||
@if (hasAudioDevices()) {
|
||||
<!-- When microphone is disabled -->
|
||||
<div class="device-input-selector disabled">
|
||||
<div class="selector-button disabled">
|
||||
<mat-icon class="device-icon">mic_off</mat-icon>
|
||||
<span class="selected-device-name">
|
||||
{{ !hasAudioDevices ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }}
|
||||
{{ !hasAudioDevices() ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -84,14 +84,14 @@
|
|||
|
||||
<!-- Device Selection Menu (Shared) -->
|
||||
<mat-menu #microphoneMenu="matMenu" class="device-menu">
|
||||
@for (microphone of microphones; track microphone.device) {
|
||||
@for (microphone of microphones(); track microphone.device) {
|
||||
<button
|
||||
mat-menu-item
|
||||
id="option-{{ microphone.label }}"
|
||||
(click)="onMicrophoneSelected({ value: microphone })"
|
||||
[class.selected]="microphone.device === microphoneSelected.device"
|
||||
[class.selected]="microphone.device === microphoneSelected()?.device"
|
||||
>
|
||||
<mat-icon *ngIf="microphone.device === microphoneSelected.device">check</mat-icon>
|
||||
<mat-icon *ngIf="microphone.device === microphoneSelected()?.device">check</mat-icon>
|
||||
<span>{{ microphone.label }}</span>
|
||||
</button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Component, effect, EventEmitter, Input, OnInit, Output, Signal, WritableSignal } from '@angular/core';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { ILogger } from '../../../models/logger.model';
|
||||
import { ParticipantModel } from '../../../models/participant.model';
|
||||
import { DeviceService } from '../../../services/device/device.service';
|
||||
import { LoggerService } from '../../../services/logger/logger.service';
|
||||
import { ParticipantService } from '../../../services/participant/participant.service';
|
||||
|
|
@ -17,19 +15,20 @@ import { StorageService } from '../../../services/storage/storage.service';
|
|||
styleUrls: ['./audio-devices.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class AudioDevicesComponent implements OnInit, OnDestroy {
|
||||
export class AudioDevicesComponent implements OnInit {
|
||||
@Input() compact: boolean = false;
|
||||
@Output() onAudioDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onAudioEnabledChanged = new EventEmitter<boolean>();
|
||||
|
||||
microphoneStatusChanging: boolean;
|
||||
hasAudioDevices: boolean;
|
||||
isMicrophoneEnabled: boolean;
|
||||
microphoneSelected: CustomDevice | undefined;
|
||||
microphones: CustomDevice[] = [];
|
||||
private localParticipantSubscription: Subscription;
|
||||
microphoneStatusChanging: boolean = false;
|
||||
isMicrophoneEnabled: boolean = false;
|
||||
private log: ILogger;
|
||||
|
||||
// Expose signals directly from service (reactive)
|
||||
protected readonly microphones: WritableSignal<CustomDevice[]>;
|
||||
protected readonly microphoneSelected: WritableSignal<CustomDevice | undefined>;
|
||||
protected readonly hasAudioDevices: Signal<boolean>;
|
||||
|
||||
constructor(
|
||||
private deviceSrv: DeviceService,
|
||||
private storageSrv: StorageService,
|
||||
|
|
@ -37,24 +36,24 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
|
|||
private loggerSrv: LoggerService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('AudioDevicesComponent');
|
||||
this.microphones = this.deviceSrv.microphones;
|
||||
this.microphoneSelected = this.deviceSrv.microphoneSelected;
|
||||
this.hasAudioDevices = this.deviceSrv.hasAudioDevices;
|
||||
|
||||
// Use effect instead of subscription for reactive updates
|
||||
effect(() => {
|
||||
const participant = this.participantService.localParticipantSignal();
|
||||
if (participant) {
|
||||
this.isMicrophoneEnabled = participant.isMicrophoneEnabled;
|
||||
this.storageSrv.setMicrophoneEnabled(this.isMicrophoneEnabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.subscribeToParticipantMediaProperties();
|
||||
this.hasAudioDevices = this.deviceSrv.hasAudioDeviceAvailable();
|
||||
if (this.hasAudioDevices) {
|
||||
this.microphones = this.deviceSrv.getMicrophones();
|
||||
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
|
||||
}
|
||||
|
||||
this.isMicrophoneEnabled = this.participantService.isMyMicrophoneEnabled();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.microphones = [];
|
||||
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
async toggleMic(event: any) {
|
||||
event.stopPropagation();
|
||||
this.microphoneStatusChanging = true;
|
||||
|
|
@ -72,8 +71,7 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
|
|||
this.microphoneStatusChanging = true;
|
||||
await this.participantService.switchMicrophone(device.device);
|
||||
this.deviceSrv.setMicSelected(device.device);
|
||||
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
|
||||
this.onAudioDeviceChanged.emit(this.microphoneSelected);
|
||||
this.onAudioDeviceChanged.emit(this.microphoneSelected());
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.e('Error switching microphone', error);
|
||||
|
|
@ -89,17 +87,4 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
|
|||
compareObjectDevices(o1: CustomDevice, o2: CustomDevice): boolean {
|
||||
return o1.label === o2.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* This subscription is necessary to update the microphone status when the user changes it from toolbar and
|
||||
* the settings panel is opened. With this, the microphone status is updated in the settings panel.
|
||||
*/
|
||||
private subscribeToParticipantMediaProperties() {
|
||||
this.localParticipantSubscription = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
|
||||
if (p) {
|
||||
this.isMicrophoneEnabled = p.isMicrophoneEnabled;
|
||||
this.storageSrv.setMicrophoneEnabled(this.isMicrophoneEnabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<div class="video-device-selector" [class.compact]="compact">
|
||||
<!-- Unified Device Button (Compact Mode) -->
|
||||
@if (compact) {
|
||||
@if (hasVideoDevices) {
|
||||
@if (hasVideoDevices()) {
|
||||
<div class="unified-device-button">
|
||||
<!-- Main toggle button -->
|
||||
<button
|
||||
mat-flat-button
|
||||
class="toggle-section"
|
||||
[disabled]="!hasVideoDevices || cameraStatusChanging"
|
||||
[disabled]="!hasVideoDevices() || cameraStatusChanging"
|
||||
[class.device-enabled]="isCameraEnabled"
|
||||
[class.device-disabled]="!isCameraEnabled"
|
||||
(click)="toggleCam($event)"
|
||||
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
|
||||
[matTooltipDisabled]="!hasVideoDevices"
|
||||
[matTooltipDisabled]="!hasVideoDevices()"
|
||||
id="camera-button"
|
||||
>
|
||||
<mat-icon [id]="isCameraEnabled ? 'videocam' : 'videocam_off'">
|
||||
|
|
@ -51,15 +51,15 @@
|
|||
id="video-dropdown"
|
||||
class="selector-button"
|
||||
[matMenuTriggerFor]="cameraMenu"
|
||||
[disabled]="cameraStatusChanging || cameras.length <= 1"
|
||||
[disabled]="cameraStatusChanging || cameras().length <= 1"
|
||||
>
|
||||
<mat-icon class="device-icon">videocam</mat-icon>
|
||||
<span class="selected-device-name">{{ cameraSelected?.label || 'No camera selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="cameras.length > 1">expand_more</mat-icon>
|
||||
<span class="selected-device-name">{{ cameraSelected()?.label || 'No camera selected' }}</span>
|
||||
<mat-icon class="dropdown-icon" *ngIf="cameras().length > 1">expand_more</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
@if (hasVideoDevices) {
|
||||
@if (hasVideoDevices()) {
|
||||
<!-- Disabled state message -->
|
||||
<div class="device-input-selector disabled">
|
||||
<div class="selector-button disabled">
|
||||
|
|
@ -80,14 +80,14 @@
|
|||
|
||||
<!-- Device Selection Menu (Shared) -->
|
||||
<mat-menu #cameraMenu="matMenu" class="device-menu">
|
||||
@for (camera of cameras; track camera.device) {
|
||||
@for (camera of cameras(); track camera.device) {
|
||||
<button
|
||||
mat-menu-item
|
||||
id="option-{{ camera.label }}"
|
||||
(click)="onCameraSelected({ value: camera })"
|
||||
[class.selected]="camera.device === cameraSelected?.device"
|
||||
[class.selected]="camera.device === cameraSelected()?.device"
|
||||
>
|
||||
<mat-icon *ngIf="camera.device === cameraSelected?.device" class="check-icon">check</mat-icon>
|
||||
<mat-icon *ngIf="camera.device === cameraSelected()?.device" class="check-icon">check</mat-icon>
|
||||
<span>{{ camera.label }}</span>
|
||||
</button>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Component, effect, EventEmitter, Input, OnInit, Output, Signal, WritableSignal } from '@angular/core';
|
||||
import { CustomDevice } from '../../../models/device.model';
|
||||
import { ILogger } from '../../../models/logger.model';
|
||||
import { ParticipantModel } from '../../../models/participant.model';
|
||||
import { DeviceService } from '../../../services/device/device.service';
|
||||
import { LoggerService } from '../../../services/logger/logger.service';
|
||||
import { ParticipantService } from '../../../services/participant/participant.service';
|
||||
|
|
@ -17,18 +15,18 @@ import { StorageService } from '../../../services/storage/storage.service';
|
|||
styleUrls: ['./video-devices.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class VideoDevicesComponent implements OnInit, OnDestroy {
|
||||
export class VideoDevicesComponent implements OnInit {
|
||||
@Input() compact: boolean = false;
|
||||
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
|
||||
@Output() onVideoEnabledChanged = new EventEmitter<boolean>();
|
||||
@Output() onVideoDevicesLoaded = new EventEmitter<CustomDevice[]>();
|
||||
|
||||
cameraStatusChanging: boolean;
|
||||
isCameraEnabled: boolean;
|
||||
cameraSelected: CustomDevice | undefined;
|
||||
hasVideoDevices: boolean;
|
||||
cameras: CustomDevice[] = [];
|
||||
localParticipantSubscription: Subscription;
|
||||
cameraStatusChanging: boolean = false;
|
||||
isCameraEnabled: boolean = false;
|
||||
|
||||
protected readonly cameras: WritableSignal<CustomDevice[]>;
|
||||
protected readonly cameraSelected: WritableSignal<CustomDevice | undefined>;
|
||||
protected readonly hasVideoDevices: Signal<boolean>;
|
||||
|
||||
private log: ILogger;
|
||||
|
||||
|
|
@ -39,26 +37,26 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
|
|||
private loggerSrv: LoggerService
|
||||
) {
|
||||
this.log = this.loggerSrv.get('VideoDevicesComponent');
|
||||
this.cameras = this.deviceSrv.cameras;
|
||||
this.cameraSelected = this.deviceSrv.cameraSelected;
|
||||
this.hasVideoDevices = this.deviceSrv.hasVideoDevices;
|
||||
|
||||
// Use effect instead of subscription for reactive updates
|
||||
effect(() => {
|
||||
const participant = this.participantService.localParticipantSignal();
|
||||
if (participant) {
|
||||
this.isCameraEnabled = participant.isCameraEnabled;
|
||||
this.storageSrv.setCameraEnabled(this.isCameraEnabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.subscribeToParticipantMediaProperties();
|
||||
|
||||
this.hasVideoDevices = this.deviceSrv.hasVideoDeviceAvailable();
|
||||
if (this.hasVideoDevices) {
|
||||
this.cameras = this.deviceSrv.getCameras();
|
||||
this.cameraSelected = this.deviceSrv.getCameraSelected();
|
||||
}
|
||||
|
||||
this.onVideoDevicesLoaded.emit(this.cameras);
|
||||
// Emit initial device list (reactively)
|
||||
this.onVideoDevicesLoaded.emit(this.cameras());
|
||||
this.isCameraEnabled = this.participantService.isMyCameraEnabled();
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.cameras = [];
|
||||
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
async toggleCam(event: any) {
|
||||
event.stopPropagation();
|
||||
this.cameraStatusChanging = true;
|
||||
|
|
@ -75,14 +73,10 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
|
|||
|
||||
// Is New deviceId different from the old one?
|
||||
if (this.deviceSrv.needUpdateVideoTrack(device)) {
|
||||
|
||||
this.cameraStatusChanging = true;
|
||||
|
||||
await this.participantService.switchCamera(device.device);
|
||||
|
||||
this.deviceSrv.setCameraSelected(device.device);
|
||||
this.cameraSelected = device;
|
||||
this.onVideoDeviceChanged.emit(this.cameraSelected);
|
||||
this.onVideoDeviceChanged.emit(this.cameraSelected());
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.e('Error switching camera', error);
|
||||
|
|
@ -98,17 +92,4 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
|
|||
compareObjectDevices(o1: CustomDevice, o2: CustomDevice): boolean {
|
||||
return o1.label === o2.label;
|
||||
}
|
||||
|
||||
/**
|
||||
* This subscription is necessary to update the camera status when the user changes it from toolbar and
|
||||
* the settings panel is opened. With this, the camera status is updated in the settings panel.
|
||||
*/
|
||||
private subscribeToParticipantMediaProperties() {
|
||||
this.localParticipantSubscription = this.participantService.localParticipant$.subscribe((p: ParticipantModel | undefined) => {
|
||||
if (p) {
|
||||
this.isCameraEnabled = p.isCameraEnabled;
|
||||
this.storageSrv.setCameraEnabled(this.isCameraEnabled);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,9 +421,12 @@ export class OpenViduService {
|
|||
|
||||
// Video device
|
||||
if (videoDeviceId === true) {
|
||||
options.video = this.deviceService.hasVideoDeviceAvailable()
|
||||
? { deviceId: this.deviceService.getCameraSelected()?.device || 'default' }
|
||||
: false;
|
||||
if (this.deviceService.hasVideoDeviceAvailable()) {
|
||||
const selectedCamera = this.deviceService.getCameraSelected();
|
||||
options.video = { deviceId: selectedCamera?.device || 'default' };
|
||||
} else {
|
||||
options.video = false;
|
||||
}
|
||||
} else if (videoDeviceId === false) {
|
||||
options.video = false;
|
||||
} else {
|
||||
|
|
@ -433,8 +436,8 @@ export class OpenViduService {
|
|||
// Audio device
|
||||
if (audioDeviceId === true) {
|
||||
if (this.deviceService.hasAudioDeviceAvailable()) {
|
||||
audioDeviceId = this.deviceService.getMicrophoneSelected()?.device || 'default';
|
||||
(options.audio as AudioCaptureOptions).deviceId = audioDeviceId;
|
||||
const selectedMic = this.deviceService.getMicrophoneSelected();
|
||||
(options.audio as AudioCaptureOptions).deviceId = selectedMic?.device || 'default';
|
||||
} else {
|
||||
options.audio = false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue