From 68d855a245a400e7639c831b8d0f30b56f4f6bec Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Wed, 20 Aug 2025 13:24:44 +0200 Subject: [PATCH] ov-components: Enhanced camera track handling and processor application in virtual background service --- .../background-effects-panel.component.html | 19 +- .../background-effects-panel.component.scss | 9 + .../background-effects-panel.component.ts | 11 +- .../settings-panel.component.scss | 2 +- .../pre-join/pre-join.component.html | 176 ++++++++++-------- .../pre-join/pre-join.component.scss | 138 +++++++++----- .../components/pre-join/pre-join.component.ts | 29 ++- .../settings/device-selector-shared.scss | 3 +- .../lang-selector.component.html | 62 +++--- .../lang-selector.component.scss | 29 ++- .../virtual-background.service.ts | 67 ++++--- 11 files changed, 323 insertions(+), 222 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.html index 6388dabd..49daab22 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.html @@ -1,10 +1,17 @@ -
-
-

{{ 'PANEL.BACKGROUND.TITLE' | translate }}

- +
+ } @else { + -
+ }
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.scss index be921ec2..f29a2026 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.scss @@ -1,3 +1,12 @@ +.prejoin-mode { + margin-top: 0px; + max-height: 100%; + min-height: 100%; + + .effects-container { + padding: 0px; + } +} .background-title { color: var(--ov-text-surface-color); } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.ts index 47bf03ae..1861f8b1 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/background-effects-panel/background-effects-panel.component.ts @@ -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 { BackgroundEffect, EffectType } from '../../../models/background-effect.model'; import { PanelType } from '../../../models/panel.model'; @@ -16,6 +16,9 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v standalone: false }) export class BackgroundEffectsPanelComponent implements OnInit { + @Input() mode: 'prejoin' | 'meeting' = 'meeting'; + @Output() onClose = new EventEmitter(); + backgroundSelectedId: string; effectType = EffectType; backgroundImages: BackgroundEffect[] = []; @@ -53,7 +56,11 @@ export class BackgroundEffectsPanelComponent implements OnInit { } close() { - this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS); + if (this.mode === 'prejoin') { + this.onClose.emit(); + } else { + this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS); + } } async applyBackground(effect: BackgroundEffect) { diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/settings-panel/settings-panel.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/settings-panel/settings-panel.component.scss index a0bc72ec..b1dc8afa 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/settings-panel/settings-panel.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/panel/settings-panel/settings-panel.component.scss @@ -54,7 +54,7 @@ ::ng-deep .lang-selector .expand-more-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, diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html index 827b4723..a25bd53c 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.html @@ -1,89 +1,115 @@
- -
-
- - {{ 'PREJOIN.PREPARING' | translate }} -
+ +
+
- -
- -
- + + @if (isLoading) { +
+
+ + {{ 'PREJOIN.PREPARING' | translate }} +
+ } @else { + +
+ +
+ +
+
+
+ + - -
-
-
- - + +
+
+
+ + +
- -
-
-
- - -
+
+ + +
+
-
- - + +
+ +
+ + @if (showBackgroundPanel) { + + } @else { + +
+ +
+ + +
+ + +
+ error_outline + {{ _error }} +
+ + +
+ +
+
+ }
- - -
- -
- - -
- - -
- error_outline - {{ _error }} -
- - -
- -
-
-
+ }
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss index f504195f..46ebf578 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.scss @@ -12,6 +12,42 @@ padding: 20px; box-sizing: border-box; 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 @@ -51,50 +87,23 @@ max-width: 520px; background: var(--ov-surface-color, #ffffff); 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; display: flex; flex-direction: column; - } - .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 { - color: var(--ov-text-secondary-color, #666) !important; - font-size: 18px; - } - } + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); } - // Video Preview Section + // Video Preview Section (moved to top with no padding) .video-preview-section { - padding: 24px 24px 20px; + padding: 0; .video-preview-container { position: relative; width: 100%; - aspect-ratio: 16/9; - border-radius: var(--ov-surface-radius); + aspect-ratio: 4/3; // Changed from 16/9 to 4/3 for taller video + border-radius: var(--ov-surface-radius) var(--ov-surface-radius) 0 0; // Only top corners rounded overflow: hidden; background: #000; - box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15); .video-frame { width: 100%; @@ -111,6 +120,7 @@ width: 100%; height: 100%; object-fit: cover; + border-radius: 0; } } } @@ -120,14 +130,54 @@ bottom: 0; left: 0; right: 0; - // background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 100%); padding: 16px; z-index: 9999; + display: flex; + justify-content: center; + align-items: flex-end; .device-controls { display: flex; 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 { - padding: 0 24px 24px; + padding: 24px 24px 24px; // Added top padding since video has no padding display: flex; flex-direction: column; gap: 20px; @@ -258,7 +308,7 @@ } .video-preview-section { - padding: 16px 20px 12px; + padding: 0px 0px 12px; .video-preview-container { aspect-ratio: 4/3; @@ -270,8 +320,8 @@ gap: 16px; } - .prejoin-header { - padding: 12px 16px 0; + .top-toolbar { + padding: 16px 20px; } } @@ -280,10 +330,6 @@ padding: 12px; } - .video-preview-section { - padding: 12px 16px 8px; - } - .configuration-section { padding: 0 16px 16px; } @@ -300,16 +346,20 @@ } } } + + .top-toolbar { + padding: 12px 16px; + } } @media (max-height: 640px) { .prejoin-container { align-items: flex-start; - padding-top: 20px; + padding-top: 60px; // Add space for top toolbar } .video-preview-section .video-preview-container { - aspect-ratio: 16/9; + aspect-ratio: 4/3; // Keep the taller aspect ratio even on small screens } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts index e48dd8bd..cc5bc932 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/pre-join/pre-join.component.ts @@ -19,8 +19,6 @@ import { TranslateService } from '../../services/translate/translate.service'; import { LocalTrack } from 'livekit-client'; import { CustomDevice } from '../../models/device.model'; import { LangOption } from '../../models/lang.model'; -import { VirtualBackgroundService } from '../../services/virtual-background/virtual-background.service'; -import { BackgroundEffect } from '../../models/background-effect.model'; /** * @internal @@ -44,7 +42,6 @@ export class PreJoinComponent implements OnInit, OnDestroy { @Output() onReadyToJoin = new EventEmitter(); _error: string | undefined; - windowSize: number; isLoading = true; participantName: string | undefined = ''; @@ -59,9 +56,8 @@ export class PreJoinComponent implements OnInit, OnDestroy { showParticipantName: boolean = true; // Future feature preparation - backgroundEffectEnabled: boolean = false; - availableBackgroundEffects: BackgroundEffect[] = []; - selectedBackgroundEffect: BackgroundEffect | undefined; + backgroundEffectEnabled: boolean = true; // Enable virtual backgrounds by default + showBackgroundPanel: boolean = false; videoTrack: LocalTrack | undefined; audioTrack: LocalTrack | undefined; @@ -81,11 +77,9 @@ export class PreJoinComponent implements OnInit, OnDestroy { private cdkSrv: CdkOverlayService, private openviduService: OpenViduService, private translateService: TranslateService, - private virtualBackgroundService: VirtualBackgroundService, private changeDetector: ChangeDetectorRef ) { this.log = this.loggerSrv.get('PreJoinComponent'); - this.availableBackgroundEffects = this.virtualBackgroundService.getBackgrounds(); } async ngOnInit() { @@ -221,14 +215,19 @@ export class PreJoinComponent implements OnInit, OnDestroy { } /** - * Future method for background effects - * @param effect - The background effect to apply + * Toggle virtual background panel visibility */ - onBackgroundEffectChanged(effect: string) { - // TODO: Implement background effect logic - // this.selectedBackgroundEffect = effect; - // this.log.d('Background effect changed to:', effect); - // this.virtualBackgroundService.applyBackground(this.virtualBackgroundService.getBackgrounds()[0]); + toggleBackgroundPanel() { + this.showBackgroundPanel = !this.showBackgroundPanel; + this.changeDetector.markForCheck(); + } + + /** + * Close virtual background panel + */ + closeBackgroundPanel() { + this.showBackgroundPanel = false; + this.changeDetector.markForCheck(); } /** diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss index 928bc499..7a69a25f 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/device-selector-shared.scss @@ -10,7 +10,8 @@ &.compact { .unified-device-button { display: flex; - background: var(--ov-secondary-action-color); + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(20px); border-radius: 12px; overflow: hidden; transition: all 0.2s ease; diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html index 720d19f5..4aaa05f3 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.html @@ -1,39 +1,33 @@
- - - - - + @if (compact) { + + + } @else { + + + } - - + + @for (lang of languages; track lang.lang) { + + }
diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss index 629ec95b..3f143723 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/components/settings/lang-selector/lang-selector.component.scss @@ -12,12 +12,6 @@ transition: all 0.2s ease; 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 { font-size: 18px; width: 18px; @@ -46,12 +40,14 @@ font-size: 18px; width: 18px; height: 18px; - color: var(--ov-text-secondary-color, #666); + color: var(--ov-text-surface-color, #666); } .lang-name { font-size: 14px; font-weight: 500; + display: inline-block !important; + color: var(--ov-text-surface-color) !important; } .expand-icon { @@ -62,21 +58,19 @@ transition: transform 0.2s ease; } - &[aria-expanded="true"] .expand-icon { + &[aria-expanded='true'] .expand-icon { transform: rotate(180deg); } } } } -::ng-deep .language-menu { - .mat-mdc-menu-panel { - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); - border: 1px solid var(--ov-border-color, #e0e0e0); - overflow: hidden; - background: var(--ov-surface-color, #ffffff); - } +::ng-deep .language-menu.mat-mdc-menu-panel { + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + border: 1px solid var(--ov-border-color, #e0e0e0); + overflow: hidden; + background: var(--ov-surface-color, #ffffff); .language-option { display: flex; @@ -86,6 +80,7 @@ transition: background-color 0.2s ease; font-size: 14px; min-height: 48px; + color: var(--ov-text-surface-color); &:hover { background-color: var(--ov-hover-color, #f5f5f5); @@ -109,11 +104,9 @@ .lang-option-name { flex: 1; font-weight: 500; - color: var(--ov-text-primary-color, #333); } &.selected .lang-option-name { - color: var(--ov-primary-action-color, #4285f4); font-weight: 600; } } diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/virtual-background/virtual-background.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/virtual-background/virtual-background.service.ts index 377c5696..a4ad0259 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/virtual-background/virtual-background.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/virtual-background/virtual-background.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { BackgroundEffect, EffectType } from '../../models/background-effect.model'; import { ParticipantService } from '../participant/participant.service'; +import { OpenViduService } from '../openvidu/openvidu.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 { LoggerService } from '../logger/logger.service'; import { ILogger } from '../../models/logger.model'; -import { ParticipantTrackPublication } from '../../models/participant.model'; /** * @internal @@ -49,6 +49,7 @@ export class VirtualBackgroundService { private log: ILogger; constructor( private participantService: ParticipantService, + private openviduService: OpenViduService, private storageService: StorageService, private loggerSrv: LoggerService ) { @@ -79,11 +80,12 @@ export class VirtualBackgroundService { // If the background is already applied, do nothing if (this.backgroundIsAlreadyApplied(bg.id)) return; - const cameraTracks = this.getCameraTracks(); - if (!cameraTracks) { - this.log.e('No camera tracks found. Cannot apply background.'); + const cameraTrack = this.getCameraTrack(); + if (!cameraTrack) { + this.log.e('No camera track found. Cannot apply background.'); return; } + try { // If no effect is selected, remove the background if (bg.type === EffectType.NONE) { @@ -91,8 +93,7 @@ export class VirtualBackgroundService { return; } - const localTrack = cameraTracks[0].track as LocalTrack; - const currentProcessor = localTrack.getProcessor() as ProcessorWrapper; + const currentProcessor = cameraTrack.getProcessor() as ProcessorWrapper; // Check if the background is the same type as the previous one if (this.hasSameTypeAsPreviousOne(bg.type) && currentProcessor) { @@ -104,7 +105,7 @@ export class VirtualBackgroundService { this.log.e('No processor found for the background effect.'); return; } - await this.applyProcessorToCameraTracks(cameraTracks, newProcessor); + await this.applyProcessorToCameraTrack(cameraTrack, newProcessor); } this.storageService.setBackground(bg.id); @@ -128,15 +129,14 @@ export class VirtualBackgroundService { async removeBackground() { if (this.isBackgroundApplied()) { this.backgroundIdSelected.next('no_effect'); - const tracks = this.participantService.getLocalParticipant()?.tracks; - const promises = tracks?.map(async (t) => { + const cameraTrack = this.getCameraTrack(); + if (cameraTrack) { try { - await (t.track as LocalTrack).stopProcessor(); + await cameraTrack.stopProcessor(); } catch (e) { this.log.w('Error stopping processor:', e); } - }); - await Promise.all(promises || []); + } this.storageService.removeBackground(); } } @@ -160,26 +160,41 @@ export class VirtualBackgroundService { return undefined; } - private async applyProcessorToCameraTracks( - cameraTracks: ParticipantTrackPublication[], - processor: ProcessorWrapper - ) { - const promises = cameraTracks.map((track) => { - return (track.track as LocalTrack).setProcessor(processor); - }); + /** + * Gets the camera track from either the published tracks (if in room) or local tracks (if in prejoin) + * @returns The camera LocalTrack or undefined if not found + * @private + */ + 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): Promise { + await cameraTrack.setProcessor(processor); } private backgroundIsAlreadyApplied(backgroundId: string): boolean { 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. *