ov-components: Enhanced camera track handling and processor application in virtual background service

master
Carlos Santos 2025-08-20 13:24:44 +02:00
parent 72e7469012
commit 68d855a245
11 changed files with 323 additions and 222 deletions

View File

@ -1,10 +1,17 @@
<div class="panel-container" id="background-effects-container"> <div class="panel-container" id="background-effects-container" [class.prejoin-mode]="mode === 'prejoin'">
@if (mode === 'meeting') {
<div class="panel-header-container"> <div class="panel-header-container">
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3> <h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()"> <button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
</div> </div>
} @else {
<button class="pansel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>arrow_back</mat-icon>
</button>
}
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none"> <div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
<div> <div>

View File

@ -1,3 +1,12 @@
.prejoin-mode {
margin-top: 0px;
max-height: 100%;
min-height: 100%;
.effects-container {
padding: 0px;
}
}
.background-title { .background-title {
color: var(--ov-text-surface-color); color: var(--ov-text-surface-color);
} }

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../../models/background-effect.model'; import { BackgroundEffect, EffectType } from '../../../models/background-effect.model';
import { PanelType } from '../../../models/panel.model'; import { PanelType } from '../../../models/panel.model';
@ -16,6 +16,9 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
standalone: false standalone: false
}) })
export class BackgroundEffectsPanelComponent implements OnInit { export class BackgroundEffectsPanelComponent implements OnInit {
@Input() mode: 'prejoin' | 'meeting' = 'meeting';
@Output() onClose = new EventEmitter<void>();
backgroundSelectedId: string; backgroundSelectedId: string;
effectType = EffectType; effectType = EffectType;
backgroundImages: BackgroundEffect[] = []; backgroundImages: BackgroundEffect[] = [];
@ -53,8 +56,12 @@ export class BackgroundEffectsPanelComponent implements OnInit {
} }
close() { close() {
if (this.mode === 'prejoin') {
this.onClose.emit();
} else {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS); this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
} }
}
async applyBackground(effect: BackgroundEffect) { async applyBackground(effect: BackgroundEffect) {
await this.backgroundService.applyBackground(effect); await this.backgroundService.applyBackground(effect);

View File

@ -54,7 +54,7 @@
::ng-deep .lang-selector .expand-more-icon, ::ng-deep .lang-selector .expand-more-icon,
::ng-deep .lang-selector mat-icon { ::ng-deep .lang-selector mat-icon {
color: var(--ov-secondary-action-color) !important; color: var(--ov-text-surface-color) !important;
} }
::ng-deep .lang-selector div, ::ng-deep .lang-selector div,

View File

@ -1,19 +1,22 @@
<div class="prejoin-container" id="prejoin-container"> <div class="prejoin-container" id="prejoin-container">
<!-- Top Language Toolbar -->
<div class="top-toolbar" *ngIf="!isMinimal">
<ov-lang-selector [compact]="false" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
</div>
<!-- Loading State --> <!-- Loading State -->
<div *ngIf="isLoading" class="loading-overlay"> @if (isLoading) {
<div class="loading-overlay">
<div class="loading-content"> <div class="loading-content">
<mat-spinner [diameter]="40"></mat-spinner> <mat-spinner [diameter]="40"></mat-spinner>
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span> <span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
</div> </div>
</div> </div>
} @else {
<!-- Main Content --> <!-- Main Content with Side Panel Layout -->
<div *ngIf="!isLoading" class="prejoin-main"> <div class="prejoin-content">
<!-- Header with Language Selector --> <!-- Main Card -->
<div class="prejoin-header" *ngIf="!isMinimal"> <div class="prejoin-main">
<ov-lang-selector [compact]="true" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
</div>
<!-- Video Preview Section --> <!-- Video Preview Section -->
<div class="video-preview-section"> <div class="video-preview-section">
<div class="video-preview-container"> <div class="video-preview-container">
@ -52,11 +55,26 @@
</ov-audio-devices-select> </ov-audio-devices-select>
</div> </div>
</div> </div>
<!-- Virtual Background Button -->
<div class="background-control" *ngIf="backgroundEffectEnabled">
<button
mat-icon-button
class="background-button"
(click)="toggleBackgroundPanel()"
[matTooltip]="'Virtual Backgrounds'"
>
<mat-icon>auto_fix_high</mat-icon>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@if (showBackgroundPanel) {
<ov-background-effects-panel [mode]="'prejoin'" (onClose)="closeBackgroundPanel()"> </ov-background-effects-panel>
} @else {
<!-- Configuration Section --> <!-- Configuration Section -->
<div class="configuration-section"> <div class="configuration-section">
<!-- Participant Name Input --> <!-- Participant Name Input -->
@ -79,11 +97,19 @@
<!-- Join Button --> <!-- Join Button -->
<div class="join-section"> <div class="join-section">
<button mat-flat-button (click)="join()" class="join-button" [disabled]="showParticipantName && !participantName"> <button
mat-flat-button
(click)="join()"
class="join-button"
[disabled]="showParticipantName && !participantName"
>
<mat-icon class="join-icon">videocam</mat-icon> <mat-icon class="join-icon">videocam</mat-icon>
{{ 'PREJOIN.JOIN' | translate }} {{ 'PREJOIN.JOIN' | translate }}
</button> </button>
</div> </div>
</div> </div>
}
</div> </div>
</div> </div>
}
</div>

View File

@ -12,6 +12,42 @@
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
transition: all 0.3s ease;
.prejoin-content {
display: flex;
justify-content: center;
width: 100%;
.prejoin-main {
max-width: 480px;
width: 100%;
}
}
}
@keyframes slideInFromRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
// Top Language Toolbar
.top-toolbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
justify-content: flex-end;
padding: 20px 24px;
background: transparent;
} }
// Loading State // Loading State
@ -51,50 +87,23 @@
max-width: 520px; max-width: 520px;
background: var(--ov-surface-color, #ffffff); background: var(--ov-surface-color, #ffffff);
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.08),
0 2px 8px rgba(0, 0, 0, 0.04);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
.prejoin-header {
padding: 16px 20px 0;
display: flex;
justify-content: flex-end;
::ng-deep .language-selector {
.mat-mdc-button {
padding: 8px 12px;
border-radius: var(--ov-surface-radius);
background-color: transparent;
border: 1px solid var(--ov-border-color, #e0e0e0);
transition: all 0.2s ease;
&:hover {
background-color: var(--ov-hover-color, #f5f5f5);
}
} }
mat-icon { // Video Preview Section (moved to top with no padding)
color: var(--ov-text-secondary-color, #666) !important;
font-size: 18px;
}
}
}
// Video Preview Section
.video-preview-section { .video-preview-section {
padding: 24px 24px 20px; padding: 0;
.video-preview-container { .video-preview-container {
position: relative; position: relative;
width: 100%; width: 100%;
aspect-ratio: 16/9; aspect-ratio: 4/3; // Changed from 16/9 to 4/3 for taller video
border-radius: var(--ov-surface-radius); border-radius: var(--ov-surface-radius) var(--ov-surface-radius) 0 0; // Only top corners rounded
overflow: hidden; overflow: hidden;
background: #000; background: #000;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
.video-frame { .video-frame {
width: 100%; width: 100%;
@ -111,6 +120,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 0;
} }
} }
} }
@ -120,14 +130,54 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
// background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 100%);
padding: 16px; padding: 16px;
z-index: 9999; z-index: 9999;
display: flex;
justify-content: center;
align-items: flex-end;
.device-controls { .device-controls {
display: flex; display: flex;
gap: 12px; gap: 12px;
justify-content: center; }
.background-control {
position: absolute;
bottom: 16px;
left: 16px;
.background-button {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 16px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
color: #333333;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
&:hover {
color: rgba(255, 255, 255, 1);
}
&:active {
transform: translateY(-1px);
transition: all 0.15s ease;
}
mat-icon {
font-size: 22px;
width: 22px;
height: 22px;
opacity: 0.9;
transition: opacity 0.2s ease;
}
&:hover mat-icon {
opacity: 1;
}
}
} }
} }
} }
@ -135,7 +185,7 @@
// Configuration Section // Configuration Section
.configuration-section { .configuration-section {
padding: 0 24px 24px; padding: 24px 24px 24px; // Added top padding since video has no padding
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
@ -258,7 +308,7 @@
} }
.video-preview-section { .video-preview-section {
padding: 16px 20px 12px; padding: 0px 0px 12px;
.video-preview-container { .video-preview-container {
aspect-ratio: 4/3; aspect-ratio: 4/3;
@ -270,8 +320,8 @@
gap: 16px; gap: 16px;
} }
.prejoin-header { .top-toolbar {
padding: 12px 16px 0; padding: 16px 20px;
} }
} }
@ -280,10 +330,6 @@
padding: 12px; padding: 12px;
} }
.video-preview-section {
padding: 12px 16px 8px;
}
.configuration-section { .configuration-section {
padding: 0 16px 16px; padding: 0 16px 16px;
} }
@ -300,16 +346,20 @@
} }
} }
} }
.top-toolbar {
padding: 12px 16px;
}
} }
@media (max-height: 640px) { @media (max-height: 640px) {
.prejoin-container { .prejoin-container {
align-items: flex-start; align-items: flex-start;
padding-top: 20px; padding-top: 60px; // Add space for top toolbar
} }
.video-preview-section .video-preview-container { .video-preview-section .video-preview-container {
aspect-ratio: 16/9; aspect-ratio: 4/3; // Keep the taller aspect ratio even on small screens
} }
} }

View File

@ -19,8 +19,6 @@ import { TranslateService } from '../../services/translate/translate.service';
import { LocalTrack } from 'livekit-client'; import { LocalTrack } from 'livekit-client';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
import { LangOption } from '../../models/lang.model'; import { LangOption } from '../../models/lang.model';
import { VirtualBackgroundService } from '../../services/virtual-background/virtual-background.service';
import { BackgroundEffect } from '../../models/background-effect.model';
/** /**
* @internal * @internal
@ -44,7 +42,6 @@ export class PreJoinComponent implements OnInit, OnDestroy {
@Output() onReadyToJoin = new EventEmitter<any>(); @Output() onReadyToJoin = new EventEmitter<any>();
_error: string | undefined; _error: string | undefined;
windowSize: number; windowSize: number;
isLoading = true; isLoading = true;
participantName: string | undefined = ''; participantName: string | undefined = '';
@ -59,9 +56,8 @@ export class PreJoinComponent implements OnInit, OnDestroy {
showParticipantName: boolean = true; showParticipantName: boolean = true;
// Future feature preparation // Future feature preparation
backgroundEffectEnabled: boolean = false; backgroundEffectEnabled: boolean = true; // Enable virtual backgrounds by default
availableBackgroundEffects: BackgroundEffect[] = []; showBackgroundPanel: boolean = false;
selectedBackgroundEffect: BackgroundEffect | undefined;
videoTrack: LocalTrack | undefined; videoTrack: LocalTrack | undefined;
audioTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined;
@ -81,11 +77,9 @@ export class PreJoinComponent implements OnInit, OnDestroy {
private cdkSrv: CdkOverlayService, private cdkSrv: CdkOverlayService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private translateService: TranslateService, private translateService: TranslateService,
private virtualBackgroundService: VirtualBackgroundService,
private changeDetector: ChangeDetectorRef private changeDetector: ChangeDetectorRef
) { ) {
this.log = this.loggerSrv.get('PreJoinComponent'); this.log = this.loggerSrv.get('PreJoinComponent');
this.availableBackgroundEffects = this.virtualBackgroundService.getBackgrounds();
} }
async ngOnInit() { async ngOnInit() {
@ -221,14 +215,19 @@ export class PreJoinComponent implements OnInit, OnDestroy {
} }
/** /**
* Future method for background effects * Toggle virtual background panel visibility
* @param effect - The background effect to apply
*/ */
onBackgroundEffectChanged(effect: string) { toggleBackgroundPanel() {
// TODO: Implement background effect logic this.showBackgroundPanel = !this.showBackgroundPanel;
// this.selectedBackgroundEffect = effect; this.changeDetector.markForCheck();
// this.log.d('Background effect changed to:', effect); }
// this.virtualBackgroundService.applyBackground(this.virtualBackgroundService.getBackgrounds()[0]);
/**
* Close virtual background panel
*/
closeBackgroundPanel() {
this.showBackgroundPanel = false;
this.changeDetector.markForCheck();
} }
/** /**

View File

@ -10,7 +10,8 @@
&.compact { &.compact {
.unified-device-button { .unified-device-button {
display: flex; display: flex;
background: var(--ov-secondary-action-color); background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(20px);
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
transition: all 0.2s ease; transition: all 0.2s ease;

View File

@ -1,32 +1,25 @@
<div class="language-selector-container"> <div class="language-selector-container">
@if (compact) {
<!-- Compact version (icon only) --> <!-- Compact version (icon only) -->
<button <button mat-icon-button [matMenuTriggerFor]="langMenu" class="compact-lang-button" [matTooltip]="'Change language'" disableRipple>
*ngIf="compact"
mat-icon-button
[matMenuTriggerFor]="menu"
class="compact-lang-button"
[matTooltip]="'Change language'"
>
<mat-icon>translate</mat-icon> <mat-icon>translate</mat-icon>
</button> </button>
} @else {
<!-- Full version (with text) --> <!-- Full version (with text) -->
<button <button mat-flat-button [matMenuTriggerFor]="langMenu" class="full-lang-button">
*ngIf="!compact" <!-- <mat-icon class="lang-icon">translate</mat-icon> -->
mat-flat-button <span class="lang-name">
[matMenuTriggerFor]="menu" {{ langSelected?.name }}
class="full-lang-button"
>
<mat-icon class="lang-icon">translate</mat-icon>
<span class="lang-name">{{ langSelected?.name }}</span>
<mat-icon class="expand-icon">expand_more</mat-icon> <mat-icon class="expand-icon">expand_more</mat-icon>
</span>
</button> </button>
}
<!-- Language Menu --> <!-- Language Menu -->
<mat-menu #menu="matMenu" class="language-menu"> <mat-menu #langMenu="matMenu" class="language-menu">
@for (lang of languages; track lang.lang) {
<button <button
mat-menu-item mat-menu-item
*ngFor="let lang of languages"
(click)="onLangSelected(lang.lang)" (click)="onLangSelected(lang.lang)"
[attr.id]="'lang-opt-' + lang.lang" [attr.id]="'lang-opt-' + lang.lang"
[class.selected]="langSelected?.lang === lang.lang" [class.selected]="langSelected?.lang === lang.lang"
@ -35,5 +28,6 @@
<mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon> <mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon>
<span class="lang-option-name">{{ lang.name }}</span> <span class="lang-option-name">{{ lang.name }}</span>
</button> </button>
}
</mat-menu> </mat-menu>
</div> </div>

View File

@ -12,12 +12,6 @@
transition: all 0.2s ease; transition: all 0.2s ease;
color: var(--ov-text-secondary-color, #666); color: var(--ov-text-secondary-color, #666);
&:hover {
background: rgba(255, 255, 255, 1);
border-color: var(--ov-primary-action-color, #4285f4);
transform: scale(1.02);
}
mat-icon { mat-icon {
font-size: 18px; font-size: 18px;
width: 18px; width: 18px;
@ -46,12 +40,14 @@
font-size: 18px; font-size: 18px;
width: 18px; width: 18px;
height: 18px; height: 18px;
color: var(--ov-text-secondary-color, #666); color: var(--ov-text-surface-color, #666);
} }
.lang-name { .lang-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
display: inline-block !important;
color: var(--ov-text-surface-color) !important;
} }
.expand-icon { .expand-icon {
@ -62,21 +58,19 @@
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
&[aria-expanded="true"] .expand-icon { &[aria-expanded='true'] .expand-icon {
transform: rotate(180deg); transform: rotate(180deg);
} }
} }
} }
} }
::ng-deep .language-menu { ::ng-deep .language-menu.mat-mdc-menu-panel {
.mat-mdc-menu-panel {
border-radius: 12px; border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
border: 1px solid var(--ov-border-color, #e0e0e0); border: 1px solid var(--ov-border-color, #e0e0e0);
overflow: hidden; overflow: hidden;
background: var(--ov-surface-color, #ffffff); background: var(--ov-surface-color, #ffffff);
}
.language-option { .language-option {
display: flex; display: flex;
@ -86,6 +80,7 @@
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
font-size: 14px; font-size: 14px;
min-height: 48px; min-height: 48px;
color: var(--ov-text-surface-color);
&:hover { &:hover {
background-color: var(--ov-hover-color, #f5f5f5); background-color: var(--ov-hover-color, #f5f5f5);
@ -109,11 +104,9 @@
.lang-option-name { .lang-option-name {
flex: 1; flex: 1;
font-weight: 500; font-weight: 500;
color: var(--ov-text-primary-color, #333);
} }
&.selected .lang-option-name { &.selected .lang-option-name {
color: var(--ov-primary-action-color, #4285f4);
font-weight: 600; font-weight: 600;
} }
} }

View File

@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { BackgroundEffect, EffectType } from '../../models/background-effect.model'; import { BackgroundEffect, EffectType } from '../../models/background-effect.model';
import { ParticipantService } from '../participant/participant.service'; import { ParticipantService } from '../participant/participant.service';
import { OpenViduService } from '../openvidu/openvidu.service';
import { StorageService } from '../storage/storage.service'; import { StorageService } from '../storage/storage.service';
import { LocalTrack } from 'livekit-client'; import { LocalVideoTrack, Track } from 'livekit-client';
import { BackgroundBlur, BackgroundOptions, ProcessorWrapper, VirtualBackground } from '@livekit/track-processors'; import { BackgroundBlur, BackgroundOptions, ProcessorWrapper, VirtualBackground } from '@livekit/track-processors';
import { LoggerService } from '../logger/logger.service'; import { LoggerService } from '../logger/logger.service';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { ParticipantTrackPublication } from '../../models/participant.model';
/** /**
* @internal * @internal
@ -49,6 +49,7 @@ export class VirtualBackgroundService {
private log: ILogger; private log: ILogger;
constructor( constructor(
private participantService: ParticipantService, private participantService: ParticipantService,
private openviduService: OpenViduService,
private storageService: StorageService, private storageService: StorageService,
private loggerSrv: LoggerService private loggerSrv: LoggerService
) { ) {
@ -79,11 +80,12 @@ export class VirtualBackgroundService {
// If the background is already applied, do nothing // If the background is already applied, do nothing
if (this.backgroundIsAlreadyApplied(bg.id)) return; if (this.backgroundIsAlreadyApplied(bg.id)) return;
const cameraTracks = this.getCameraTracks(); const cameraTrack = this.getCameraTrack();
if (!cameraTracks) { if (!cameraTrack) {
this.log.e('No camera tracks found. Cannot apply background.'); this.log.e('No camera track found. Cannot apply background.');
return; return;
} }
try { try {
// If no effect is selected, remove the background // If no effect is selected, remove the background
if (bg.type === EffectType.NONE) { if (bg.type === EffectType.NONE) {
@ -91,8 +93,7 @@ export class VirtualBackgroundService {
return; return;
} }
const localTrack = cameraTracks[0].track as LocalTrack; const currentProcessor = cameraTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
const currentProcessor = localTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
// Check if the background is the same type as the previous one // Check if the background is the same type as the previous one
if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) { if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) {
@ -104,7 +105,7 @@ export class VirtualBackgroundService {
this.log.e('No processor found for the background effect.'); this.log.e('No processor found for the background effect.');
return; return;
} }
await this.applyProcessorToCameraTracks(cameraTracks, newProcessor); await this.applyProcessorToCameraTrack(cameraTrack, newProcessor);
} }
this.storageService.setBackground(bg.id); this.storageService.setBackground(bg.id);
@ -128,15 +129,14 @@ export class VirtualBackgroundService {
async removeBackground() { async removeBackground() {
if (this.isBackgroundApplied()) { if (this.isBackgroundApplied()) {
this.backgroundIdSelected.next('no_effect'); this.backgroundIdSelected.next('no_effect');
const tracks = this.participantService.getLocalParticipant()?.tracks; const cameraTrack = this.getCameraTrack();
const promises = tracks?.map(async (t) => { if (cameraTrack) {
try { try {
await (t.track as LocalTrack).stopProcessor(); await cameraTrack.stopProcessor();
} catch (e) { } catch (e) {
this.log.w('Error stopping processor:', e); this.log.w('Error stopping processor:', e);
} }
}); }
await Promise.all(promises || []);
this.storageService.removeBackground(); this.storageService.removeBackground();
} }
} }
@ -160,26 +160,41 @@ export class VirtualBackgroundService {
return undefined; return undefined;
} }
private async applyProcessorToCameraTracks( /**
cameraTracks: ParticipantTrackPublication[], * Gets the camera track from either the published tracks (if in room) or local tracks (if in prejoin)
processor: ProcessorWrapper<BackgroundOptions> * @returns The camera LocalTrack or undefined if not found
) { * @private
const promises = cameraTracks.map((track) => { */
return (track.track as LocalTrack).setProcessor(processor); private getCameraTrack(): LocalVideoTrack | undefined {
}); // First, try to get from published tracks (when in room)
if (this.openviduService.isRoomConnected()) {
const localParticipant = this.participantService.getLocalParticipant();
const cameraTrackPublication = localParticipant?.cameraTracks?.[0];
if (cameraTrackPublication?.track) {
return cameraTrackPublication.track as LocalVideoTrack;
}
}
await Promise.all(promises || []); // Fallback to local tracks (when in prejoin or tracks not yet published)
const localTracks = this.openviduService.getLocalTracks();
const cameraTrack = localTracks.find((track) => track.kind === Track.Kind.Video);
return cameraTrack as LocalVideoTrack | undefined;
}
/**
* Applies a background processor to the camera track
* @param cameraTrack The camera track to apply the processor to
* @param processor The background processor to apply
* @private
*/
private async applyProcessorToCameraTrack(cameraTrack: LocalVideoTrack, processor: ProcessorWrapper<BackgroundOptions>): Promise<void> {
await cameraTrack.setProcessor(processor);
} }
private backgroundIsAlreadyApplied(backgroundId: string): boolean { private backgroundIsAlreadyApplied(backgroundId: string): boolean {
return backgroundId === this.backgroundIdSelected.getValue(); return backgroundId === this.backgroundIdSelected.getValue();
} }
private getCameraTracks(): ParticipantTrackPublication[] | undefined {
const localParticipant = this.participantService.getLocalParticipant();
return localParticipant?.cameraTracks;
}
/** /**
* Replaces the current background effect with a new one by updating the processor options. * Replaces the current background effect with a new one by updating the processor options.
* *