ov-components: Update device selection logic and improve UI handling for audio and video devices

master
Carlos Santos 2025-08-22 12:23:40 +02:00
parent 03fb7c0a93
commit ee30c3ce95
9 changed files with 189 additions and 176 deletions

View File

@ -89,7 +89,7 @@ describe('Testing API Directives', () => {
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact'); await utils.waitForElement('.language-selector');
const element = await utils.waitForElement('#join-button'); const element = await utils.waitForElement('#join-button');
expect(await element.getText()).toEqual('Unirme ahora'); expect(await element.getText()).toEqual('Unirme ahora');
@ -108,20 +108,20 @@ describe('Testing API Directives', () => {
const panelTitle = await utils.waitForElement('.panel-title'); const panelTitle = await utils.waitForElement('.panel-title');
expect(await panelTitle.getText()).toEqual('Configuración'); expect(await panelTitle.getText()).toEqual('Configuración');
const element = await utils.waitForElement('#lang-selected-name'); const element = await utils.waitForElement('.lang-name');
expect(await element.getAttribute('innerText')).toEqual('Español'); expect(await element.getAttribute('innerText')).toEqual('Español expand_more');
}); });
it('should override the LANG OPTIONS', async () => { it('should override the LANG OPTIONS', async () => {
await browser.get(`${url}&prejoin=true&langOptions=true`); await browser.get(`${url}&prejoin=true&langOptions=true`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact'); await utils.waitForElement('.language-selector');
await utils.clickOn('#lang-btn-compact'); await utils.clickOn('.language-selector');
await browser.sleep(500); await browser.sleep(500);
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2); expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
await utils.clickOn('.lang-menu-opt'); await utils.clickOn('.language-option');
await browser.sleep(500); await browser.sleep(500);
await utils.clickOn('#join-button'); await utils.clickOn('#join-button');
@ -136,12 +136,12 @@ describe('Testing API Directives', () => {
await browser.sleep(500); await browser.sleep(500);
await utils.waitForElement('#settings-container'); await utils.waitForElement('#settings-container');
await utils.waitForElement('.lang-button'); await utils.waitForElement('.full-lang-button');
await utils.clickOn('.lang-button'); await utils.clickOn('.full-lang-button');
await browser.sleep(500); await browser.sleep(500);
expect(await utils.getNumberOfElements('.lang-menu-opt')).toEqual(2); expect(await utils.getNumberOfElements('.language-option')).toEqual(2);
}); });
it('should show the PREJOIN page', async () => { it('should show the PREJOIN page', async () => {

View File

@ -92,35 +92,12 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue(); expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
}); });
it('should receive the onVideoEnabledChanged event when clicking on the settings panel', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.clickOn('#video-opt');
await utils.waitForElement('ov-video-devices-select');
await utils.clickOn('ov-video-devices-select #camera-button');
// Checking if onVideoEnabledChanged has been received
await utils.waitForElement('#onVideoEnabledChanged-false');
expect(await utils.isPresent('#onVideoEnabledChanged-false')).toBeTrue();
await utils.clickOn('ov-video-devices-select #camera-button');
await utils.waitForElement('#onVideoEnabledChanged-true');
expect(await utils.isPresent('#onVideoEnabledChanged-true')).toBeTrue();
});
it('should receive the onVideoDeviceChanged event on prejoin', async () => { it('should receive the onVideoDeviceChanged event on prejoin', async () => {
await browser.get(`${url}&fakeDevices=true`); await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
await utils.waitForElement('#video-devices-form'); await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-devices-form'); await utils.clickOn('#video-dropdown');
await utils.waitForElement('#option-custom_fake_video_1'); await utils.waitForElement('#option-custom_fake_video_1');
await utils.clickOn('#option-custom_fake_video_1'); await utils.clickOn('#option-custom_fake_video_1');
@ -142,8 +119,8 @@ describe('Testing videoconference EVENTS', () => {
await utils.clickOn('#video-opt'); await utils.clickOn('#video-opt');
await utils.waitForElement('ov-video-devices-select'); await utils.waitForElement('ov-video-devices-select');
await utils.waitForElement('#video-devices-form'); await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-devices-form'); await utils.clickOn('#video-dropdown');
await utils.waitForElement('#option-custom_fake_video_1'); await utils.waitForElement('#option-custom_fake_video_1');
await utils.clickOn('#option-custom_fake_video_1'); await utils.clickOn('#option-custom_fake_video_1');
@ -184,35 +161,12 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue(); expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
}); });
it('should receive the onAudioEnabledChanged event when clicking on the settings panel', async () => {
await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.clickOn('#audio-opt');
await utils.waitForElement('ov-audio-devices-select');
await utils.clickOn('ov-audio-devices-select #microphone-button');
// Checking if onAudioEnabledChanged has been received
await utils.waitForElement('#onAudioEnabledChanged-false');
expect(await utils.isPresent('#onAudioEnabledChanged-false')).toBeTrue();
await utils.clickOn('ov-audio-devices-select #microphone-button');
await utils.waitForElement('#onAudioEnabledChanged-true');
expect(await utils.isPresent('#onAudioEnabledChanged-true')).toBeTrue();
});
it('should receive the onAudioDeviceChanged event on prejoin', async () => { it('should receive the onAudioDeviceChanged event on prejoin', async () => {
await browser.get(`${url}&fakeDevices=true`); await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
await utils.waitForElement('#audio-devices-form'); await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-devices-form'); await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#option-custom_fake_audio_1'); await utils.waitForElement('#option-custom_fake_audio_1');
await utils.clickOn('#option-custom_fake_audio_1'); await utils.clickOn('#option-custom_fake_audio_1');
@ -234,8 +188,8 @@ describe('Testing videoconference EVENTS', () => {
await utils.clickOn('#audio-opt'); await utils.clickOn('#audio-opt');
await utils.waitForElement('ov-audio-devices-select'); await utils.waitForElement('ov-audio-devices-select');
await utils.waitForElement('#audio-devices-form'); await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-devices-form'); await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#option-custom_fake_audio_1'); await utils.waitForElement('#option-custom_fake_audio_1');
await utils.clickOn('#option-custom_fake_audio_1'); await utils.clickOn('#option-custom_fake_audio_1');
@ -248,8 +202,8 @@ describe('Testing videoconference EVENTS', () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact'); await utils.waitForElement('.language-selector');
await utils.clickOn('#lang-btn-compact'); await utils.clickOn('.language-selector');
await browser.sleep(500); await browser.sleep(500);
await utils.clickOn('#lang-opt-es'); await utils.clickOn('#lang-opt-es');
@ -269,8 +223,8 @@ describe('Testing videoconference EVENTS', () => {
await browser.sleep(500); await browser.sleep(500);
await utils.waitForElement('#settings-container'); await utils.waitForElement('#settings-container');
await utils.waitForElement('.lang-button'); await utils.waitForElement('.full-lang-button');
await utils.clickOn('.lang-button'); await utils.clickOn('.full-lang-button');
await browser.sleep(500); await browser.sleep(500);
await utils.clickOn('#lang-opt-es'); await utils.clickOn('#lang-opt-es');
@ -398,7 +352,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onSettingsPanelStatusChanged-false')).toBeTrue(); expect(await utils.isPresent('#onSettingsPanelStatusChanged-false')).toBeTrue();
}); });
it('should receive the onRecordingStartRequested event when clicking toolbar button', async () => { fit('should receive the onRecordingStartRequested and onRecordingStopRequested event when clicking toolbar button', async () => {
const roomName = 'recordingToolbarEvent'; const roomName = 'recordingToolbarEvent';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`); await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -410,9 +364,15 @@ describe('Testing videoconference EVENTS', () => {
// Checking if onRecordingStartRequested has been received // Checking if onRecordingStartRequested has been received
await utils.waitForElement(`#onRecordingStartRequested-${roomName}`); await utils.waitForElement(`#onRecordingStartRequested-${roomName}`);
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue(); expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onRecordingStopRequested event when clicking toolbar button', async () => {}); await utils.waitForElement('.activity-status.started');
await utils.toggleRecordingFromToolbar();
// Checking if onRecordingStopRequested has been received
await utils.waitForElement(`#onRecordingStopRequested-${roomName}`);
expect(await utils.isPresent(`#onRecordingStopRequested-${roomName}`)).toBeTrue();
});
xit('should receive the onBroadcastingStopRequested event when clicking toolbar button', async () => { xit('should receive the onBroadcastingStopRequested event when clicking toolbar button', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
@ -446,7 +406,7 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent('#onBroadcastingStopRequested')).toBeTrue(); expect(await utils.isPresent('#onBroadcastingStopRequested')).toBeTrue();
}); });
it('should receive the onRecordingStartRequested when clicking from activities panel', async () => { it('should receive the onRecordingStartRequested and onRecordingStopRequested when clicking from activities panel', async () => {
const roomName = 'recordingActivitiesEvent'; const roomName = 'recordingActivitiesEvent';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`); await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -472,8 +432,6 @@ describe('Testing videoconference EVENTS', () => {
expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue(); expect(await utils.isPresent(`#onRecordingStartRequested-${roomName}`)).toBeTrue();
}); });
xit('should receive the onRecordingStopRequested when clicking from activities panel', async () => {});
xit('should receive the onRecordingDeleteRequested event', async () => { xit('should receive the onRecordingDeleteRequested event', async () => {
let element; let element;
const roomName = 'deleteRecordingEvent'; const roomName = 'deleteRecordingEvent';

View File

@ -33,7 +33,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
await browser.get(`${url}&fakeDevices=true`); await browser.get(`${url}&fakeDevices=true`);
let videoDevices = await utils.waitForElement('#video-devices-form'); let videoDevices = await utils.waitForElement('#video-dropdown');
await videoDevices.click(); await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1'); let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click(); await element.click();
@ -63,7 +63,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
await browser.sleep(500); await browser.sleep(500);
await utils.clickOn('#video-opt'); await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
let videoDevices = await utils.waitForElement('#video-devices-form'); let videoDevices = await utils.waitForElement('#video-dropdown');
await videoDevices.click(); await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1'); let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click(); await element.click();
@ -130,16 +130,15 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
await browser.quit(); await browser.quit();
}); });
it('should disable camera and microphone buttons in the prejoin page when permissions are denied', async () => { it('should camera and microphone buttons be disabled in the prejoin page when permissions are denied', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
let button = await utils.waitForElement('#camera-button'); await utils.waitForElement('#no-video-device-message');
expect(await button.isEnabled()).toBeFalse(); await utils.waitForElement('#no-audio-device-message');
button = await utils.waitForElement('#microphone-button'); expect(await utils.isPresent('#backgrounds-button')).toBeFalse();
expect(await button.isEnabled()).toBeFalse();
}); });
it('should disable camera and microphone buttons in the room page when permissions are denied', async () => { it('should camera and microphone buttons be disabled in the room page when permissions are denied', async () => {
await browser.get(`${url}`); await browser.get(`${url}`);
await utils.checkPrejoinIsPresent(); await utils.checkPrejoinIsPresent();
await utils.clickOn('#join-button'); await utils.clickOn('#join-button');
@ -151,7 +150,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
}); });
it('should disable camera and microphone buttons in the room page without prejoin when permissions are denied', async () => { it('should camera and microphone buttons be disabled in the room page without prejoin when permissions are denied', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkSessionIsPresent(); await utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
@ -161,7 +160,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
expect(await button.isEnabled()).toBeFalse(); expect(await button.isEnabled()).toBeFalse();
}); });
it('should disable camera and microphone device selection buttons in settings when permissions are denied', async () => { it('should show an audio and video device warning in settings when permissions are denied', async () => {
await browser.get(`${url}&prejoin=false`); await browser.get(`${url}&prejoin=false`);
await utils.checkToolbarIsPresent(); await utils.checkToolbarIsPresent();
await utils.togglePanel('settings'); await utils.togglePanel('settings');
@ -170,11 +169,9 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
expect(await utils.isPresent('.settings-container')).toBeTrue(); expect(await utils.isPresent('.settings-container')).toBeTrue();
await utils.clickOn('#video-opt'); await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
let button = await utils.waitForElement('#camera-button'); await utils.waitForElement('#no-video-device-message');
expect(await button.isEnabled()).toBeFalse();
await utils.clickOn('#audio-opt'); await utils.clickOn('#audio-opt');
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue(); expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
button = await utils.waitForElement('#microphone-button'); await utils.waitForElement('#no-audio-device-message');
expect(await button.isEnabled()).toBeFalse();
}); });
}); });

View File

@ -1,4 +1,4 @@
<div class="prejoin-container" id="prejoin-container"> <div class="prejoin-container" id="prejoin-container" [class.name-error]="!!_error">
<!-- Top Language Toolbar --> <!-- Top Language Toolbar -->
<div class="top-toolbar" *ngIf="!isMinimal"> <div class="top-toolbar" *ngIf="!isMinimal">
<ov-lang-selector [compact]="false" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector> <ov-lang-selector [compact]="false" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
@ -39,6 +39,7 @@
[compact]="true" [compact]="true"
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)" (onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
(onVideoEnabledChanged)="videoEnabledChanged($event)" (onVideoEnabledChanged)="videoEnabledChanged($event)"
(onVideoDevicesLoaded)="onVideoDevicesLoaded($event)"
class="device-selector" class="device-selector"
> >
</ov-video-devices-select> </ov-video-devices-select>
@ -57,17 +58,20 @@
</div> </div>
<!-- Virtual Background Button --> <!-- Virtual Background Button -->
<div class="background-control" *ngIf="backgroundEffectEnabled"> @if (backgroundEffectEnabled && hasVideoDevices) {
<button <div class="background-control">
mat-icon-button <button
class="background-button" mat-icon-button
(click)="toggleBackgroundPanel()" class="background-button"
[matTooltip]="'Virtual Backgrounds'" (click)="toggleBackgroundPanel()"
[disabled]="!isVideoEnabled" [matTooltip]="'Virtual Backgrounds'"
> [disabled]="!isVideoEnabled"
<mat-icon class="material-symbols-outlined">background_replace</mat-icon> id="backgrounds-button"
</button> >
</div> <mat-icon class="material-symbols-outlined">background_replace</mat-icon>
</button>
</div>
}
</div> </div>
</div> </div>
</div> </div>
@ -81,7 +85,7 @@
<!-- Configuration Section --> <!-- Configuration Section -->
<div class="configuration-section"> <div class="configuration-section">
<!-- Participant Name Input --> <!-- Participant Name Input -->
<div class="input-section" *ngIf="showParticipantName"> <div class="participant-name-container input-section" *ngIf="showParticipantName">
<ov-participant-name-input <ov-participant-name-input
[isPrejoinPage]="true" [isPrejoinPage]="true"
[error]="!!_error" [error]="!!_error"
@ -93,7 +97,7 @@
</div> </div>
<!-- Error Message --> <!-- Error Message -->
<div *ngIf="!!_error" class="error-message"> <div *ngIf="!!_error" class="error-message" id="token-error">
<mat-icon class="error-icon">error_outline</mat-icon> <mat-icon class="error-icon">error_outline</mat-icon>
<span class="error-text">{{ _error }}</span> <span class="error-text">{{ _error }}</span>
</div> </div>
@ -104,9 +108,9 @@
mat-flat-button mat-flat-button
(click)="join()" (click)="join()"
class="join-button" class="join-button"
id="join-button"
[disabled]="showParticipantName && !participantName" [disabled]="showParticipantName && !participantName"
> >
<mat-icon class="join-icon">videocam</mat-icon>
{{ 'PREJOIN.JOIN' | translate }} {{ 'PREJOIN.JOIN' | translate }}
</button> </button>
</div> </div>

View File

@ -14,6 +14,12 @@
position: relative; position: relative;
transition: all 0.3s ease; transition: all 0.3s ease;
&.name-error {
.prejoin-main {
min-height: fit-content;
}
}
.prejoin-content { .prejoin-content {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -103,6 +103,7 @@ export class PreJoinComponent implements OnInit, OnDestroy {
videoTrack: LocalTrack | undefined; videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined;
isVideoEnabled: boolean = false; isVideoEnabled: boolean = false;
hasVideoDevices: boolean = true;
private tracks: LocalTrack[]; private tracks: LocalTrack[];
private log: ILogger; private log: ILogger;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -247,6 +248,10 @@ export class PreJoinComponent implements OnInit, OnDestroy {
this.onVideoEnabledChanged.emit(enabled); this.onVideoEnabledChanged.emit(enabled);
} }
onVideoDevicesLoaded(devices: CustomDevice[]) {
this.hasVideoDevices = devices.length > 0;
}
async audioEnabledChanged(enabled: boolean) { async audioEnabledChanged(enabled: boolean) {
if (enabled && !this.audioTrack) { if (enabled && !this.audioTrack) {
const newAudioTrack = await this.openviduService.createLocalTracks(false, true); const newAudioTrack = await this.openviduService.createLocalTracks(false, true);

View File

@ -1,28 +1,43 @@
<div class="audio-device-selector" [class.compact]="compact"> <div class="audio-device-selector" [class.compact]="compact">
<!-- Unified Device Button (Compact Mode) --> <!-- Unified Device Button (Compact Mode) -->
@if (compact) { @if (compact) {
<div class="unified-device-button"> @if (hasAudioDevices) {
<!-- Main toggle button --> <div class="unified-device-button">
<button <!-- Main toggle button -->
mat-flat-button <button
class="toggle-section" mat-flat-button
[disabled]="!hasAudioDevices || microphoneStatusChanging" class="toggle-section"
[class.device-enabled]="isMicrophoneEnabled" [disabled]="!hasAudioDevices || microphoneStatusChanging"
[class.device-disabled]="!isMicrophoneEnabled" [class.device-enabled]="isMicrophoneEnabled"
(click)="toggleMic($event)" [class.device-disabled]="!isMicrophoneEnabled"
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)" (click)="toggleMic($event)"
[matTooltipDisabled]="!hasAudioDevices" [matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
> [matTooltipDisabled]="!hasAudioDevices"
<mat-icon>{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon> id="microphone-button"
</button> >
<mat-icon [id]="isMicrophoneEnabled ? 'mic' : 'mic_off'">{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
<!-- Dropdown section -->
@if (microphones.length > 1 && isMicrophoneEnabled) {
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="microphoneMenu" [disabled]="microphoneStatusChanging">
<mat-icon>expand_more</mat-icon>
</button> </button>
}
</div> <!-- Dropdown section -->
@if (isMicrophoneEnabled) {
<button
mat-flat-button
id="audio-dropdown"
class="dropdown-section"
[matMenuTriggerFor]="microphoneMenu"
[disabled]="microphoneStatusChanging"
>
<mat-icon>expand_more</mat-icon>
</button>
}
</div>
} @else {
<!-- No Microphone Available -->
<div id="no-audio-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
</div>
}
} @else { } @else {
<!-- Normal Mode - Input Style Selector --> <!-- Normal Mode - Input Style Selector -->
<div class="normal-device-selector"> <div class="normal-device-selector">
@ -33,6 +48,7 @@
<div class="device-input-selector"> <div class="device-input-selector">
<button <button
mat-flat-button mat-flat-button
id="audio-dropdown"
class="selector-button" class="selector-button"
[disabled]="microphoneStatusChanging || microphones.length <= 1" [disabled]="microphoneStatusChanging || microphones.length <= 1"
[matMenuTriggerFor]="microphoneMenu" [matMenuTriggerFor]="microphoneMenu"
@ -44,24 +60,34 @@
</button> </button>
</div> </div>
} @else { } @else {
<!-- When microphone is disabled --> @if (hasAudioDevices) {
<div class="device-input-selector disabled"> <!-- When microphone is disabled -->
<div class="selector-button disabled"> <div class="device-input-selector disabled">
<mat-icon class="device-icon">mic_off</mat-icon> <div class="selector-button disabled">
<span class="selected-device-name"> <mat-icon class="device-icon">mic_off</mat-icon>
{{ !hasAudioDevices ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }} <span class="selected-device-name">
</span> {{ !hasAudioDevices ? ('PREJOIN.NO_AUDIO_DEVICE' | translate) : 'Microphone disabled' }}
</span>
</div>
</div> </div>
</div> } @else {
<!-- No Microphone Available -->
<div id="no-audio-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
</div>
}
} }
</div> </div>
</div> </div>
} }
<!-- Device Selection Menu (Shared) --> <!-- Device Selection Menu (Shared) -->
<mat-menu #microphoneMenu="matMenu" class="device-menu"> <mat-menu #microphoneMenu="matMenu" class="device-menu">
@for (microphone of microphones; track microphone.device) { @for (microphone of microphones; track microphone.device) {
<button <button
mat-menu-item mat-menu-item
id="option-{{ microphone.label }}"
(click)="onMicrophoneSelected({ value: microphone })" (click)="onMicrophoneSelected({ value: microphone })"
[class.selected]="microphone.device === microphoneSelected.device" [class.selected]="microphone.device === microphoneSelected.device"
> >
@ -70,12 +96,4 @@
</button> </button>
} }
</mat-menu> </mat-menu>
<!-- No Microphone Available -->
@if (microphones.length === 0) {
<div class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_AUDIO_DEVICE' | translate }}</span>
</div>
}
</div> </div>

View File

@ -1,28 +1,45 @@
<div class="video-device-selector" [class.compact]="compact"> <div class="video-device-selector" [class.compact]="compact">
<!-- Unified Device Button (Compact Mode) --> <!-- Unified Device Button (Compact Mode) -->
@if (compact) { @if (compact) {
<div class="unified-device-button"> @if (hasVideoDevices) {
<!-- Main toggle button --> <div class="unified-device-button">
<button <!-- Main toggle button -->
mat-flat-button <button
class="toggle-section" mat-flat-button
[disabled]="!hasVideoDevices || cameraStatusChanging" class="toggle-section"
[class.device-enabled]="isCameraEnabled" [disabled]="!hasVideoDevices || cameraStatusChanging"
[class.device-disabled]="!isCameraEnabled" [class.device-enabled]="isCameraEnabled"
(click)="toggleCam($event)" [class.device-disabled]="!isCameraEnabled"
[matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)" (click)="toggleCam($event)"
[matTooltipDisabled]="!hasVideoDevices" [matTooltip]="isCameraEnabled ? ('TOOLBAR.STOP_VIDEO' | translate) : ('TOOLBAR.START_VIDEO' | translate)"
> [matTooltipDisabled]="!hasVideoDevices"
<mat-icon>{{ isCameraEnabled ? 'videocam' : 'videocam_off' }}</mat-icon> id="camera-button"
</button> >
<mat-icon [id]="isCameraEnabled ? 'videocam' : 'videocam_off'">
<!-- Dropdown section --> {{ isCameraEnabled ? 'videocam' : 'videocam_off' }}
@if (isCameraEnabled && cameras.length > 1) { </mat-icon>
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="cameraMenu" [disabled]="cameraStatusChanging">
<mat-icon>expand_more</mat-icon>
</button> </button>
}
</div> <!-- Dropdown section -->
@if (isCameraEnabled) {
<button
mat-flat-button
id="video-dropdown"
class="dropdown-section"
[matMenuTriggerFor]="cameraMenu"
[disabled]="cameraStatusChanging"
>
<mat-icon>expand_more</mat-icon>
</button>
}
</div>
} @else {
<!-- No Camera Available -->
<div id="no-video-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
</div>
}
} @else { } @else {
<!-- Normal Mode - Input-style Selector --> <!-- Normal Mode - Input-style Selector -->
<div class="normal-device-selector"> <div class="normal-device-selector">
@ -31,6 +48,7 @@
<div class="device-input-selector"> <div class="device-input-selector">
<button <button
mat-flat-button mat-flat-button
id="video-dropdown"
class="selector-button" class="selector-button"
[matMenuTriggerFor]="cameraMenu" [matMenuTriggerFor]="cameraMenu"
[disabled]="cameraStatusChanging || cameras.length <= 1" [disabled]="cameraStatusChanging || cameras.length <= 1"
@ -41,13 +59,21 @@
</button> </button>
</div> </div>
} @else { } @else {
<!-- Disabled state message --> @if (hasVideoDevices) {
<div class="device-input-selector disabled"> <!-- Disabled state message -->
<div class="selector-button disabled"> <div class="device-input-selector disabled">
<mat-icon class="device-icon">videocam_off</mat-icon> <div class="selector-button disabled">
<span class="selected-device-name">{{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }}</span> <mat-icon class="device-icon">videocam_off</mat-icon>
<span class="selected-device-name">{{ 'PANEL.SETTINGS.DISABLED_VIDEO' | translate }}</span>
</div>
</div> </div>
</div> } @else {
<!-- No Camera Available -->
<div id="no-video-device-message" class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
</div>
}
} }
</div> </div>
} }
@ -55,18 +81,15 @@
<!-- Device Selection Menu (Shared) --> <!-- Device Selection Menu (Shared) -->
<mat-menu #cameraMenu="matMenu" class="device-menu"> <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 (click)="onCameraSelected({ value: camera })" [class.selected]="camera.device === cameraSelected?.device"> <button
mat-menu-item
id="option-{{ camera.label }}"
(click)="onCameraSelected({ value: camera })"
[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> <span>{{ camera.label }}</span>
</button> </button>
} }
</mat-menu> </mat-menu>
<!-- No Camera Available -->
@if (cameras.length === 0) {
<div class="no-device-message">
<mat-icon class="warning-icon">warning</mat-icon>
<span>{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</span>
</div>
}
</div> </div>

View File

@ -19,6 +19,7 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
@Input() compact: boolean = false; @Input() compact: boolean = false;
@Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>(); @Output() onVideoDeviceChanged = new EventEmitter<CustomDevice>();
@Output() onVideoEnabledChanged = new EventEmitter<boolean>(); @Output() onVideoEnabledChanged = new EventEmitter<boolean>();
@Output() onVideoDevicesLoaded = new EventEmitter<CustomDevice[]>();
cameraStatusChanging: boolean; cameraStatusChanging: boolean;
isCameraEnabled: boolean; isCameraEnabled: boolean;
@ -42,6 +43,7 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
this.cameraSelected = this.deviceSrv.getCameraSelected(); this.cameraSelected = this.deviceSrv.getCameraSelected();
} }
this.onVideoDevicesLoaded.emit(this.cameras);
this.isCameraEnabled = this.participantService.isMyCameraEnabled(); this.isCameraEnabled = this.participantService.isMyCameraEnabled();
} }