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

View File

@ -92,35 +92,12 @@ describe('Testing videoconference EVENTS', () => {
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 () => {
await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#video-devices-form');
await utils.clickOn('#video-devices-form');
await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-dropdown');
await utils.waitForElement('#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.waitForElement('ov-video-devices-select');
await utils.waitForElement('#video-devices-form');
await utils.clickOn('#video-devices-form');
await utils.waitForElement('#video-dropdown');
await utils.clickOn('#video-dropdown');
await utils.waitForElement('#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();
});
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 () => {
await browser.get(`${url}&fakeDevices=true`);
await utils.checkPrejoinIsPresent();
await utils.waitForElement('#audio-devices-form');
await utils.clickOn('#audio-devices-form');
await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#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.waitForElement('ov-audio-devices-select');
await utils.waitForElement('#audio-devices-form');
await utils.clickOn('#audio-devices-form');
await utils.waitForElement('#audio-dropdown');
await utils.clickOn('#audio-dropdown');
await utils.waitForElement('#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 utils.checkPrejoinIsPresent();
await utils.waitForElement('#lang-btn-compact');
await utils.clickOn('#lang-btn-compact');
await utils.waitForElement('.language-selector');
await utils.clickOn('.language-selector');
await browser.sleep(500);
await utils.clickOn('#lang-opt-es');
@ -269,8 +223,8 @@ describe('Testing videoconference EVENTS', () => {
await browser.sleep(500);
await utils.waitForElement('#settings-container');
await utils.waitForElement('.lang-button');
await utils.clickOn('.lang-button');
await utils.waitForElement('.full-lang-button');
await utils.clickOn('.full-lang-button');
await browser.sleep(500);
await utils.clickOn('#lang-opt-es');
@ -398,7 +352,7 @@ describe('Testing videoconference EVENTS', () => {
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';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -410,9 +364,15 @@ describe('Testing videoconference EVENTS', () => {
// Checking if onRecordingStartRequested has been received
await utils.waitForElement(`#onRecordingStartRequested-${roomName}`);
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 () => {
await browser.get(`${url}&prejoin=false`);
@ -446,7 +406,7 @@ describe('Testing videoconference EVENTS', () => {
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';
await browser.get(`${url}&prejoin=false&roomName=${roomName}`);
@ -472,8 +432,6 @@ describe('Testing videoconference EVENTS', () => {
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 () => {
let element;
const roomName = 'deleteRecordingEvent';

View File

@ -33,7 +33,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
await browser.get(`${url}&fakeDevices=true`);
let videoDevices = await utils.waitForElement('#video-devices-form');
let videoDevices = await utils.waitForElement('#video-dropdown');
await videoDevices.click();
let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click();
@ -63,7 +63,7 @@ describe('Media Devices: Virtual Device Replacement and Permissions Handling', (
await browser.sleep(500);
await utils.clickOn('#video-opt');
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();
let element = await utils.waitForElement('#option-custom_fake_video_1');
await element.click();
@ -130,16 +130,15 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
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 utils.checkPrejoinIsPresent();
let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).toBeFalse();
button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).toBeFalse();
await utils.waitForElement('#no-video-device-message');
await utils.waitForElement('#no-audio-device-message');
expect(await utils.isPresent('#backgrounds-button')).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 utils.checkPrejoinIsPresent();
await utils.clickOn('#join-button');
@ -151,7 +150,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
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 utils.checkSessionIsPresent();
await utils.checkToolbarIsPresent();
@ -161,7 +160,7 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
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 utils.checkToolbarIsPresent();
await utils.togglePanel('settings');
@ -170,11 +169,9 @@ describe('Media Devices: UI Behavior Without Media Device Permissions', () => {
expect(await utils.isPresent('.settings-container')).toBeTrue();
await utils.clickOn('#video-opt');
expect(await utils.isPresent('ov-video-devices-select')).toBeTrue();
let button = await utils.waitForElement('#camera-button');
expect(await button.isEnabled()).toBeFalse();
await utils.waitForElement('#no-video-device-message');
await utils.clickOn('#audio-opt');
expect(await utils.isPresent('ov-audio-devices-select')).toBeTrue();
button = await utils.waitForElement('#microphone-button');
expect(await button.isEnabled()).toBeFalse();
await utils.waitForElement('#no-audio-device-message');
});
});

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
<div class="audio-device-selector" [class.compact]="compact">
<!-- Unified Device Button (Compact Mode) -->
@if (compact) {
@if (hasAudioDevices) {
<div class="unified-device-button">
<!-- Main toggle button -->
<button
@ -12,17 +13,31 @@
(click)="toggleMic($event)"
[matTooltip]="isMicrophoneEnabled ? ('TOOLBAR.MUTE_AUDIO' | translate) : ('TOOLBAR.UNMUTE_AUDIO' | translate)"
[matTooltipDisabled]="!hasAudioDevices"
id="microphone-button"
>
<mat-icon>{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
<mat-icon [id]="isMicrophoneEnabled ? 'mic' : 'mic_off'">{{ isMicrophoneEnabled ? 'mic' : 'mic_off' }}</mat-icon>
</button>
<!-- Dropdown section -->
@if (microphones.length > 1 && isMicrophoneEnabled) {
<button mat-flat-button class="dropdown-section" [matMenuTriggerFor]="microphoneMenu" [disabled]="microphoneStatusChanging">
@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 {
<!-- Normal Mode - Input Style Selector -->
<div class="normal-device-selector">
@ -33,6 +48,7 @@
<div class="device-input-selector">
<button
mat-flat-button
id="audio-dropdown"
class="selector-button"
[disabled]="microphoneStatusChanging || microphones.length <= 1"
[matMenuTriggerFor]="microphoneMenu"
@ -44,6 +60,7 @@
</button>
</div>
} @else {
@if (hasAudioDevices) {
<!-- When microphone is disabled -->
<div class="device-input-selector disabled">
<div class="selector-button disabled">
@ -53,15 +70,24 @@
</span>
</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>
}
<!-- Device Selection Menu (Shared) -->
<mat-menu #microphoneMenu="matMenu" class="device-menu">
@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"
>
@ -70,12 +96,4 @@
</button>
}
</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>

View File

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

View File

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