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-header-container">
|
||||
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
<div class="panel-container" id="background-effects-container" [class.prejoin-mode]="mode === 'prejoin'">
|
||||
@if (mode === 'meeting') {
|
||||
<div class="panel-header-container">
|
||||
<h3 class="panel-title">{{ 'PANEL.BACKGROUND.TITLE' | translate }}</h3>
|
||||
|
||||
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
} @else {
|
||||
<button class="pansel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="effects-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
|
||||
<div>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<void>();
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,89 +1,115 @@
|
|||
<div class="prejoin-container" id="prejoin-container">
|
||||
<!-- Loading State -->
|
||||
<div *ngIf="isLoading" class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<mat-spinner [diameter]="40"></mat-spinner>
|
||||
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
<!-- 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>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div *ngIf="!isLoading" class="prejoin-main">
|
||||
<!-- Header with Language Selector -->
|
||||
<div class="prejoin-header" *ngIf="!isMinimal">
|
||||
<ov-lang-selector [compact]="true" class="language-selector" (onLangChanged)="onLangChanged.emit($event)"> </ov-lang-selector>
|
||||
<!-- Loading State -->
|
||||
@if (isLoading) {
|
||||
<div class="loading-overlay">
|
||||
<div class="loading-content">
|
||||
<mat-spinner [diameter]="40"></mat-spinner>
|
||||
<span class="loading-text">{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Main Content with Side Panel Layout -->
|
||||
<div class="prejoin-content">
|
||||
<!-- Main Card -->
|
||||
<div class="prejoin-main">
|
||||
<!-- Video Preview Section -->
|
||||
<div class="video-preview-section">
|
||||
<div class="video-preview-container">
|
||||
<div class="video-frame">
|
||||
<ov-media-element
|
||||
[track]="videoTrack"
|
||||
[showAvatar]="!videoTrack || videoTrack.isMuted"
|
||||
[avatarName]="participantName"
|
||||
[avatarColor]="'hsl(48, 100%, 50%)'"
|
||||
[isLocal]="true"
|
||||
class="video-element"
|
||||
>
|
||||
</ov-media-element>
|
||||
|
||||
<!-- Video Preview Section -->
|
||||
<div class="video-preview-section">
|
||||
<div class="video-preview-container">
|
||||
<div class="video-frame">
|
||||
<ov-media-element
|
||||
[track]="videoTrack"
|
||||
[showAvatar]="!videoTrack || videoTrack.isMuted"
|
||||
[avatarName]="participantName"
|
||||
[avatarColor]="'hsl(48, 100%, 50%)'"
|
||||
[isLocal]="true"
|
||||
class="video-element"
|
||||
>
|
||||
</ov-media-element>
|
||||
<!-- Video Controls Overlay -->
|
||||
<div class="video-overlay">
|
||||
<div class="device-controls">
|
||||
<div class="control-group" *ngIf="showCameraButton">
|
||||
<ov-video-devices-select
|
||||
[compact]="true"
|
||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-video-devices-select>
|
||||
</div>
|
||||
|
||||
<!-- Video Controls Overlay -->
|
||||
<div class="video-overlay">
|
||||
<div class="device-controls">
|
||||
<div class="control-group" *ngIf="showCameraButton">
|
||||
<ov-video-devices-select
|
||||
[compact]="true"
|
||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||
(onVideoEnabledChanged)="videoEnabledChanged($event)"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-video-devices-select>
|
||||
</div>
|
||||
<div class="control-group" *ngIf="showMicrophoneButton">
|
||||
<ov-audio-devices-select
|
||||
[compact]="true"
|
||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
||||
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-audio-devices-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group" *ngIf="showMicrophoneButton">
|
||||
<ov-audio-devices-select
|
||||
[compact]="true"
|
||||
(onAudioDeviceChanged)="onAudioDeviceChanged.emit($event)"
|
||||
(onAudioEnabledChanged)="audioEnabledChanged($event)"
|
||||
(onDeviceSelectorClicked)="onDeviceSelectorClicked()"
|
||||
class="device-selector"
|
||||
>
|
||||
</ov-audio-devices-select>
|
||||
<!-- 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>
|
||||
|
||||
@if (showBackgroundPanel) {
|
||||
<ov-background-effects-panel [mode]="'prejoin'" (onClose)="closeBackgroundPanel()"> </ov-background-effects-panel>
|
||||
} @else {
|
||||
<!-- Configuration Section -->
|
||||
<div class="configuration-section">
|
||||
<!-- Participant Name Input -->
|
||||
<div class="input-section" *ngIf="showParticipantName">
|
||||
<ov-participant-name-input
|
||||
[isPrejoinPage]="true"
|
||||
[error]="!!_error"
|
||||
(onNameUpdated)="onParticipantNameChanged($event)"
|
||||
(onEnterPressed)="onEnterPressed()"
|
||||
class="name-input"
|
||||
>
|
||||
</ov-participant-name-input>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="!!_error" class="error-message">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<span class="error-text">{{ _error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Join Button -->
|
||||
<div class="join-section">
|
||||
<button
|
||||
mat-flat-button
|
||||
(click)="join()"
|
||||
class="join-button"
|
||||
[disabled]="showParticipantName && !participantName"
|
||||
>
|
||||
<mat-icon class="join-icon">videocam</mat-icon>
|
||||
{{ 'PREJOIN.JOIN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Section -->
|
||||
<div class="configuration-section">
|
||||
<!-- Participant Name Input -->
|
||||
<div class="input-section" *ngIf="showParticipantName">
|
||||
<ov-participant-name-input
|
||||
[isPrejoinPage]="true"
|
||||
[error]="!!_error"
|
||||
(onNameUpdated)="onParticipantNameChanged($event)"
|
||||
(onEnterPressed)="onEnterPressed()"
|
||||
class="name-input"
|
||||
>
|
||||
</ov-participant-name-input>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div *ngIf="!!_error" class="error-message">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<span class="error-text">{{ _error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Join Button -->
|
||||
<div class="join-section">
|
||||
<button mat-flat-button (click)="join()" class="join-button" [disabled]="showParticipantName && !participantName">
|
||||
<mat-icon class="join-icon">videocam</mat-icon>
|
||||
{{ 'PREJOIN.JOIN' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<any>();
|
||||
|
||||
_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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,39 +1,33 @@
|
|||
<div class="language-selector-container">
|
||||
<!-- Compact version (icon only) -->
|
||||
<button
|
||||
*ngIf="compact"
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
class="compact-lang-button"
|
||||
[matTooltip]="'Change language'"
|
||||
>
|
||||
<mat-icon>translate</mat-icon>
|
||||
</button>
|
||||
|
||||
<!-- Full version (with text) -->
|
||||
<button
|
||||
*ngIf="!compact"
|
||||
mat-flat-button
|
||||
[matMenuTriggerFor]="menu"
|
||||
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>
|
||||
</button>
|
||||
@if (compact) {
|
||||
<!-- Compact version (icon only) -->
|
||||
<button mat-icon-button [matMenuTriggerFor]="langMenu" class="compact-lang-button" [matTooltip]="'Change language'" disableRipple>
|
||||
<mat-icon>translate</mat-icon>
|
||||
</button>
|
||||
} @else {
|
||||
<!-- Full version (with text) -->
|
||||
<button mat-flat-button [matMenuTriggerFor]="langMenu" class="full-lang-button">
|
||||
<!-- <mat-icon class="lang-icon">translate</mat-icon> -->
|
||||
<span class="lang-name">
|
||||
{{ langSelected?.name }}
|
||||
<mat-icon class="expand-icon">expand_more</mat-icon>
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Language Menu -->
|
||||
<mat-menu #menu="matMenu" class="language-menu">
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let lang of languages"
|
||||
(click)="onLangSelected(lang.lang)"
|
||||
[attr.id]="'lang-opt-' + lang.lang"
|
||||
[class.selected]="langSelected?.lang === lang.lang"
|
||||
class="language-option"
|
||||
>
|
||||
<mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon>
|
||||
<span class="lang-option-name">{{ lang.name }}</span>
|
||||
</button>
|
||||
<mat-menu #langMenu="matMenu" class="language-menu">
|
||||
@for (lang of languages; track lang.lang) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onLangSelected(lang.lang)"
|
||||
[attr.id]="'lang-opt-' + lang.lang"
|
||||
[class.selected]="langSelected?.lang === lang.lang"
|
||||
class="language-option"
|
||||
>
|
||||
<mat-icon *ngIf="langSelected?.lang === lang.lang" class="check-icon">check</mat-icon>
|
||||
<span class="lang-option-name">{{ lang.name }}</span>
|
||||
</button>
|
||||
}
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<BackgroundOptions>;
|
||||
const currentProcessor = cameraTrack.getProcessor() as ProcessorWrapper<BackgroundOptions>;
|
||||
|
||||
// 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<BackgroundOptions>
|
||||
) {
|
||||
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<BackgroundOptions>): Promise<void> {
|
||||
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.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue