mirror of https://github.com/OpenVidu/openvidu.git
ov-components: Enhanced camera track handling and processor application in virtual background service
parent
72e7469012
commit
68d855a245
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue