ov-components: Improves virtual background support handling

Adds a more robust mechanism to manage virtual background support, including:

- Displaying a warning message when the browser does not support virtual backgrounds.
- Disabling background selection buttons when virtual backgrounds are not supported.
- Optimizing background processor initialization and attachment for different browsers (handling lazy loading for Firefox).
- Centralizing the check for virtual background support in the OpenViduService.

This change ensures a better user experience by clearly indicating when virtual backgrounds are unavailable and preventing users from attempting to use an unsupported feature.
master
CSantosM 2026-01-26 13:13:47 +01:00
parent 6cfa44c4f1
commit 7208cb3a65
16 changed files with 196 additions and 44 deletions

View File

@ -13,6 +13,13 @@
</button>
}
@if (!isVirtualBackgroundSupported()) {
<div class="not-supported-message">
<p class="warning-title">{{ 'PANEL.BACKGROUND.NOT_SUPPORTED' | translate }}</p>
<p class="warning-description">{{ 'PANEL.BACKGROUND.NOT_SUPPORTED_DESCRIPTION' | translate }}</p>
</div>
}
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
<div>
<h4 class="background-title">{{ 'PANEL.BACKGROUND.BLURRED_SECTION' | translate }}</h4>
@ -24,6 +31,7 @@
[class.active-effect-btn]="backgroundSelectedId === effect.id"
(click)="applyBackground(effect)"
[attr.id]="effect.id + '-btn'"
[disabled]="!isVirtualBackgroundSupported()"
[matTooltip]="
effect.type === effectType.NONE
? ('PANEL.BACKGROUND.NO_EFFECTS' | translate)
@ -44,7 +52,8 @@
class="effect-button"
[id]="'effect-' + effect.id"
[class.active-effect-btn]="backgroundSelectedId === effect.id"
(click)="applyBackground(effect)"
[class.disabled]="!isVirtualBackgroundSupported()"
(click)="isVirtualBackgroundSupported() && applyBackground(effect)"
>
<img [src]="effect.thumbnail" />
</div>

View File

@ -12,6 +12,26 @@
margin: 10px 0;
font-weight: 300;
}
.not-supported-message {
padding: 15px;
margin: 10px;
background-color: var(--ov-warn-color, #ff9800);
border-radius: var(--ov-surface-radius);
color: var(--ov-text-surface-color);
.warning-title {
font-weight: 500;
margin: 0 0 8px 0;
}
.warning-description {
font-size: 0.9em;
margin: 0;
opacity: 0.9;
}
}
.effects-container {
display: block !important;
overflow-y: auto;
@ -33,6 +53,11 @@
cursor: pointer;
}
.effect-button.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.active-effect-btn {
border: 2px solid var(--ov-accent-action-color);
}

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, computed, EventEmitter, Input, OnInit, Output, Signal } from '@angular/core';
import { Subscription } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
import { PanelType } from '../../../models/panel.model';
@ -23,7 +23,7 @@ export class BackgroundEffectsPanelComponent implements OnInit {
effectType = EffectType;
backgroundImages: BackgroundEffect[] = [];
noEffectAndBlurredBackground: BackgroundEffect[] = [];
private backgrounds: BackgroundEffect[];
private backgrounds: BackgroundEffect[] = [];
private backgroundSubs: Subscription;
/**
@ -38,6 +38,14 @@ export class BackgroundEffectsPanelComponent implements OnInit {
private cd: ChangeDetectorRef
) {}
/**
* Computed signal that reactively tracks if virtual background is supported.
* Updates automatically when browser support changes.
*/
readonly isVirtualBackgroundSupported: Signal<boolean> = computed(() =>
this.backgroundService.isVirtualBackgroundSupported()
);
ngOnInit(): void {
this.subscribeToBackgroundSelected();
this.backgrounds = this.backgroundService.getBackgrounds();

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "没有效果和模糊的背景",
"NO_EFFECTS": "没有背景效果",
"BLURRED_EFFECT": "模糊的背景",
"IMAGES_SECTION": "背景图像"
"IMAGES_SECTION": "背景图像",
"NOT_SUPPORTED": "此浏览器不支持虚拟背景",
"NOT_SUPPORTED_DESCRIPTION": "您的浏览器不支持背景效果。此功能需要 GPU 加速,可能已被禁用或不可用。"
},
"RECORDING": {
"TITLE": "录音",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "Keine Effekte und unscharfer Hintergrund",
"NO_EFFECTS": "Kein Hintergrundeffekt",
"BLURRED_EFFECT": "Unscharfer Hintergrund",
"IMAGES_SECTION": "Hintergrundbilder"
"IMAGES_SECTION": "Hintergrundbilder",
"NOT_SUPPORTED": "Virtuelle Hintergründe werden in diesem Browser nicht unterstützt",
"NOT_SUPPORTED_DESCRIPTION": "Ihr Browser unterstützt keine Hintergrundeffekte. Diese Funktion erfordert GPU-Beschleunigung, die möglicherweise deaktiviert oder nicht verfügbar ist."
},
"RECORDING": {
"TITLE": "Aufnahme",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "No effects and blurred background",
"NO_EFFECTS": "No background effect",
"BLURRED_EFFECT": "Blurred background",
"IMAGES_SECTION": "Background images"
"IMAGES_SECTION": "Background images",
"NOT_SUPPORTED": "Virtual backgrounds are not supported in this browser",
"NOT_SUPPORTED_DESCRIPTION": "Your browser does not support background effects. This feature requires GPU acceleration which may be disabled or unavailable."
},
"RECORDING": {
"TITLE": "Recording",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "Sin efectos y fondo desenfocado",
"NO_EFFECTS": "Sin efecto",
"BLURRED_EFFECT": "Fondo desenfocado",
"IMAGES_SECTION": "Imágenes de fondo"
"IMAGES_SECTION": "Imágenes de fondo",
"NOT_SUPPORTED": "Los efectos de fondo virtuales no son compatibles con este navegador",
"NOT_SUPPORTED_DESCRIPTION": "Tu navegador no admite efectos de fondo. Esta función requiere aceleración GPU que puede estar deshabilitada o no disponible."
},
"RECORDING": {
"TITLE": "Grabación",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "Aucun effet et arrière-plan flou",
"NO_EFFECTS": "Aucun effet de fond",
"BLURRED_EFFECT": "Arrière-plan flou",
"IMAGES_SECTION": "Images d'arrière-plan"
"IMAGES_SECTION": "Images d'arrière-plan",
"NOT_SUPPORTED": "Les arrière-plans virtuels ne sont pas pris en charge dans ce navigateur",
"NOT_SUPPORTED_DESCRIPTION": "Votre navigateur ne prend pas en charge les effets d'arrière-plan. Cette fonctionnalité nécessite l'accélération GPU qui peut être désactivée ou non disponible."
},
"RECORDING": {
"TITLE": "Enregistrement",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "कोई प्रभाव नहीं है और पृष्ठभूमि धुंधली है",
"NO_EFFECTS": "कोई पृष्ठभूमि प्रभाव नहीं है",
"BLURRED_EFFECT": "पृष्ठभूमि धुंधली है",
"IMAGES_SECTION": "पृष्ठभूमि छवियां"
"IMAGES_SECTION": "पृष्ठभूमि छवियां",
"NOT_SUPPORTED": "इस ब्राउज़र में वर्चुअल बैकग्राउंड समर्थित नहीं है",
"NOT_SUPPORTED_DESCRIPTION": "आपका ब्राउज़र बैकग्राउंड इफेक्ट्स का समर्थन नहीं करता है। इस सुविधा के लिए GPU त्वरण की आवश्यकता है जो अक्षम या अनुपलब्ध हो सकता है।"
},
"RECORDING": {
"TITLE": "रिकॉर्डिंग",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "Nessun effetto e sfondo sfocato",
"NO_EFFECTS": "Nessun effetto di sfondo",
"BLURRED_EFFECT": "Sfondo sfocato",
"IMAGES_SECTION": "Immagini di sfondo"
"IMAGES_SECTION": "Immagini di sfondo",
"NOT_SUPPORTED": "Gli sfondi virtuali non sono supportati in questo browser",
"NOT_SUPPORTED_DESCRIPTION": "Il tuo browser non supporta gli effetti di sfondo. Questa funzione richiede l'accelerazione GPU che potrebbe essere disabilitata o non disponibile."
},
"RECORDING": {
"TITLE": "Registrazione",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "エフェクトなし、ぼやけた背景",
"NO_EFFECTS": "背景エフェクトなし",
"BLURRED_EFFECT": "ぼやけた背景",
"IMAGES_SECTION": "背景画像"
"IMAGES_SECTION": "背景画像",
"NOT_SUPPORTED": "このブラウザでは仮想背景はサポートされていません",
"NOT_SUPPORTED_DESCRIPTION": "お使いのブラウザは背景効果をサポートしていません。この機能にはGPUアクセラレーションが必要ですが、無効化されているか利用できません。"
},
"RECORDING": {
"TITLE": "レコーディング",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "Geen effecten en onscherpe achtergrond",
"NO_EFFECTS": "Geen achtergrondeffect",
"BLURRED_EFFECT": "Onscherpe achtergrond",
"IMAGES_SECTION": "Achtergrondafbeeldingen"
"IMAGES_SECTION": "Achtergrondafbeeldingen",
"NOT_SUPPORTED": "Virtuele achtergronden worden niet ondersteund in deze browser",
"NOT_SUPPORTED_DESCRIPTION": "Uw browser ondersteunt geen achtergrondeffecten. Deze functie vereist GPU-versnelling die mogelijk uitgeschakeld of niet beschikbaar is."
},
"RECORDING": {
"TITLE": "Opname",

View File

@ -120,7 +120,9 @@
"BLURRED_SECTION": "Sem efeitos e fundo desfocado",
"NO_EFFECTS": "Sem efeito de fundo",
"BLURRED_EFFECT": "Fundo desfocado",
"IMAGES_SECTION": "Imagens de fundo"
"IMAGES_SECTION": "Imagens de fundo",
"NOT_SUPPORTED": "Fundos virtuais não são suportados neste navegador",
"NOT_SUPPORTED_DESCRIPTION": "Seu navegador não suporta efeitos de fundo. Este recurso requer aceleração de GPU que pode estar desabilitada ou não disponível."
},
"RECORDING": {
"TITLE": "Gravação",

View File

@ -1,8 +1,14 @@
import { Injectable } from '@angular/core';
import { BackgroundProcessor, /*BackgroundProcessorWrapper,*/ SwitchBackgroundProcessorOptions } from '@livekit/track-processors';
import { Injectable, signal, Signal } from '@angular/core';
import {
BackgroundProcessor,
supportsBackgroundProcessors,
supportsModernBackgroundProcessors,
/*BackgroundProcessorWrapper,*/ SwitchBackgroundProcessorOptions
} from '@livekit/track-processors';
import {
AudioCaptureOptions,
ConnectionState,
createLocalTracks,
CreateLocalTracksOptions,
E2EEOptions,
ExternalE2EEKeyProvider,
@ -13,14 +19,12 @@ import {
RoomOptions,
Track,
VideoCaptureOptions,
VideoPresets,
createLocalTracks
VideoPresets
} from 'livekit-client';
import { ILogger } from '../../models/logger.model';
import { OpenViduComponentsConfigService } from '../config/directive-config.service';
import { DeviceService } from '../device/device.service';
import { LoggerService } from '../logger/logger.service';
import { PlatformService } from '../platform/platform.service';
import { StorageService } from '../storage/storage.service';
// TODO: Remove this once livekit-client exports it
@ -58,8 +62,20 @@ export class OpenViduService {
/**
* Background processor for video tracks. Initialized in disabled mode.
* This processor is shared between prejoin and in-room states.
* Only initialized if browser supports background processing (GPU available).
*/
private backgroundProcessor: BackgroundProcessorWrapper;
private backgroundProcessor?: BackgroundProcessorWrapper;
/**
* Signal to track if background processor is supported (requires GPU).
* Set to false if browser doesn't support it or processor initialization fails.
*/
private _isBackgroundProcessorSupported = signal(false);
/**
* Public readonly signal for background processor support status.
*/
readonly isBackgroundProcessorSupported: Signal<boolean> = this._isBackgroundProcessorSupported.asReadonly();
/**
* @internal
@ -69,11 +85,33 @@ export class OpenViduService {
private deviceService: DeviceService,
private storageService: StorageService,
private configService: OpenViduComponentsConfigService,
private platformService: PlatformService
) {
this.log = this.loggerSrv.get('OpenViduService');
// this.isSttReadyObs = this._isSttReady.asObservable();
this.backgroundProcessor = BackgroundProcessor({ mode: 'disabled' });
// Check if browser supports background processors
if (!supportsBackgroundProcessors()) {
this.log.w('Background processors not supported in this browser (GPU may be disabled)');
this._isBackgroundProcessorSupported.set(false);
return;
}
// Only initialize processor immediately for browsers supporting modern processors
// Browsers without modern support (e.g., Firefox) will initialize on-demand
if (supportsModernBackgroundProcessors()) {
try {
this.backgroundProcessor = BackgroundProcessor({ mode: 'disabled' });
this._isBackgroundProcessorSupported.set(true);
this.log.d('Background processor initialized at startup (modern processors supported)');
} catch (error: any) {
this.log.w('Failed to initialize background processor:', error?.message || error);
this._isBackgroundProcessorSupported.set(false);
}
} else {
// Mark as supported but don't initialize yet - will be created on-demand
this._isBackgroundProcessorSupported.set(true);
this.log.d('Background processors supported but not modern - will initialize on-demand');
}
}
/**
@ -275,6 +313,8 @@ export class OpenViduService {
return this.localTracks;
}
/**
* Switches the background mode on the local video track.
* Works both in prejoin and in-room states.
@ -285,20 +325,36 @@ export class OpenViduService {
* @internal
*/
async switchBackgroundMode(options: SwitchBackgroundProcessorOptions): Promise<void> {
// For Firefox, attach processor only when an effect is activated
if (this.platformService.isFirefox()) {
await this.handleFirefoxProcessor(options.mode);
if (!this.isBackgroundProcessorSupported()) {
this.log.w('Background processor not supported (GPU disabled). Virtual background is disabled.');
return;
}
await this.backgroundProcessor.switchTo(options);
this.log.d('Background mode switched:', options);
try {
// For browsers without modern processor support: attach processor on-demand when effect is activated
if (!supportsModernBackgroundProcessors()) {
await this.handleLazyProcessorAttachment(options.mode);
}
// If processor exists, switch mode (either pre-initialized or just created on-demand)
if (this.backgroundProcessor) {
await this.backgroundProcessor.switchTo(options);
this.log.d('Background mode switched:', options);
}
} catch (error: any) {
this.log.e('Failed to switch background mode:', error?.message || error);
this._isBackgroundProcessorSupported.set(false);
// Don't throw - gracefully disable virtual background instead of crashing
}
}
/**
* Applies the background processor handling for Firefox browser.
* Handles lazy processor attachment for browsers without modern processor support.
* Creates and attaches processor on-demand when effect is activated.
* This is used for browsers like Firefox that don't support modern background processors.
* @internal
*/
private async handleFirefoxProcessor(mode: SwitchBackgroundProcessorOptions['mode']): Promise<void> {
private async handleLazyProcessorAttachment(mode: SwitchBackgroundProcessorOptions['mode']): Promise<void> {
const videoTrack = await this.getVideoTrack();
if (!videoTrack) return;
@ -306,13 +362,25 @@ export class OpenViduService {
const isDisabled = mode === 'disabled';
if (!isDisabled && !hasProcessor) {
this.log.d('Firefox: Attaching processor on effect activation');
await videoTrack.setProcessor(this.backgroundProcessor);
try {
// Create processor on-demand if not already created
if (!this.backgroundProcessor) {
this.log.d('Creating background processor on-demand');
this.backgroundProcessor = BackgroundProcessor({ mode: 'disabled' });
}
this.log.d('Attaching processor on effect activation (lazy loading)');
await videoTrack.setProcessor(this.backgroundProcessor);
} catch (error: any) {
this.log.w('Failed to attach background processor (GPU may be disabled):', error?.message || error);
this._isBackgroundProcessorSupported.set(false);
// Continue without crashing - virtual background will be disabled
}
return;
}
if (isDisabled && hasProcessor) {
this.log.d('Firefox: Stopping processor on effect deactivation');
this.log.d('Stopping processor on effect deactivation');
await videoTrack.stopProcessor();
}
}
@ -390,15 +458,24 @@ export class OpenViduService {
}
// Apply background processor to video track (initialized in disabled mode)
// For Firefox: skip processor attachment to avoid performance issues (applied only when effect is activated)
// For other browsers: attach processor for smooth transitions
// For browsers with modern processor support: attach processor immediately for smooth transitions
// For browsers without modern support: skip attachment, will be applied on-demand when effect is activated
const videoTrack = newLocalTracks.find((t) => t.kind === Track.Kind.Video) as LocalVideoTrack | undefined;
debugger;
if (videoTrack && !this.platformService.isFirefox()) {
await videoTrack.setProcessor(this.backgroundProcessor);
this.log.d('Background processor applied to newly created video track');
} else if (videoTrack && this.platformService.isFirefox()) {
this.log.d('Firefox detected: skipping processor attachment for better performance');
if (videoTrack && supportsModernBackgroundProcessors()) {
if (this.isBackgroundProcessorSupported() && this.backgroundProcessor) {
try {
await videoTrack.setProcessor(this.backgroundProcessor);
this.log.d('Background processor applied to newly created video track');
} catch (error: any) {
this.log.w('Failed to apply background processor (GPU may be disabled):', error?.message || error);
this._isBackgroundProcessorSupported.set(false);
// Continue without crashing - virtual background will be disabled
}
} else {
this.log.d('Background processor not supported (GPU disabled or not available)');
}
} else if (videoTrack && !supportsModernBackgroundProcessors()) {
this.log.d('Modern background processors not supported - will apply processor on-demand when effect is activated');
}
// Mute tracks if devices are disabled

View File

@ -406,10 +406,9 @@ export class ParticipantService {
// Update Signal - create new reference to trigger reactivity
// The Observable will automatically emit via toObservable()
if (this.localParticipant) {
const updatedParticipant = Object.assign(
Object.create(Object.getPrototypeOf(this.localParticipant)),
{ ...this.localParticipant }
);
const updatedParticipant = Object.assign(Object.create(Object.getPrototypeOf(this.localParticipant)), {
...this.localParticipant
});
this.localParticipantWritableSignal.set(updatedParticipant);
} else {
this.localParticipantWritableSignal.set(undefined);

View File

@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { computed, Injectable, Signal } from '@angular/core';
import { SwitchBackgroundProcessorOptions } from '@livekit/track-processors';
import { BehaviorSubject, Observable } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../models/background-effect.model';
@ -59,6 +59,14 @@ export class VirtualBackgroundService {
return this.backgrounds;
}
/**
* Computed signal that checks if virtual background is supported (requires GPU).
* Reactively tracks the support status from OpenViduService.
*/
readonly isVirtualBackgroundSupported: Signal<boolean> = computed(() =>
this.openviduService.isBackgroundProcessorSupported()
);
isBackgroundApplied(): boolean {
const bgSelected = this.backgroundIdSelected.getValue();
return !!bgSelected && bgSelected !== 'no_effect';
@ -80,6 +88,12 @@ export class VirtualBackgroundService {
* The background processor is centralized in OpenViduService for consistency.
*/
async applyBackground(bg: BackgroundEffect) {
// Check if virtual background is supported before proceeding
if (!this.isVirtualBackgroundSupported()) {
this.log.w('Virtual background not supported (GPU disabled). Skipping background application.');
return;
}
// If the background is already applied, do nothing
if (this.backgroundIsAlreadyApplied(bg.id)) return;