2026-03-31 12:06:23 +02:00
|
|
|
import { Component, inject } from '@angular/core';
|
|
|
|
|
import { NgClass } from '@angular/common';
|
2024-10-02 01:14:42 +02:00
|
|
|
import {
|
|
|
|
|
LocalTrack,
|
|
|
|
|
VideoTrack,
|
|
|
|
|
RemoteTrackPublication,
|
|
|
|
|
VideoQuality,
|
|
|
|
|
} from 'livekit-client';
|
2024-07-02 19:19:05 +02:00
|
|
|
import { TrackComponent } from '../track/track.component';
|
2024-10-02 01:14:42 +02:00
|
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
|
|
|
import { TestFeedService } from 'src/app/services/test-feed.service';
|
|
|
|
|
import { InfoDialogComponent } from '../dialogs/info-dialog/info-dialog.component';
|
2025-12-12 15:33:56 +01:00
|
|
|
import { ProcessorDialogComponent } from '../dialogs/processor-dialog/processor-dialog.component';
|
2026-03-31 12:06:23 +02:00
|
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
|
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
|
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
|
|
|
import { MatSelectModule } from '@angular/material/select';
|
2024-07-02 19:19:05 +02:00
|
|
|
|
|
|
|
|
@Component({
|
2025-07-14 22:18:12 +02:00
|
|
|
selector: 'app-video-track',
|
|
|
|
|
templateUrl: './video-track.component.html',
|
2026-03-31 12:06:23 +02:00
|
|
|
styleUrl: './video-track.component.css',
|
|
|
|
|
imports: [NgClass, MatIconModule, MatTooltipModule, MatFormFieldModule, MatSelectModule],
|
2024-07-02 19:19:05 +02:00
|
|
|
})
|
|
|
|
|
export class VideoTrackComponent extends TrackComponent {
|
2024-10-02 01:14:42 +02:00
|
|
|
muteVideoIcon: string = 'videocam';
|
|
|
|
|
maxVideoQuality: string;
|
2024-07-02 19:19:05 +02:00
|
|
|
|
2024-10-24 11:36:52 +02:00
|
|
|
videoZoom = false;
|
|
|
|
|
|
2025-12-12 15:33:56 +01:00
|
|
|
// Processor state
|
|
|
|
|
processor: any;
|
|
|
|
|
mode: 'disabled' | 'background-blur' | 'virtual-background' | undefined =
|
|
|
|
|
'virtual-background';
|
|
|
|
|
backgroundType: 'image' | 'screen' = 'image';
|
|
|
|
|
tracking: boolean = false;
|
|
|
|
|
scale: number = 1;
|
|
|
|
|
horizontalPosition: number = 0;
|
|
|
|
|
blurRadius: number = 10;
|
|
|
|
|
processorEnabled: boolean = false;
|
|
|
|
|
screenShareTrack: MediaStreamTrack | undefined;
|
|
|
|
|
|
|
|
|
|
segmentationMethod: 'mediapipe' | 'chroma' = 'mediapipe';
|
|
|
|
|
modelAssetPath: string = 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite';
|
|
|
|
|
chromaKey = {
|
|
|
|
|
autoDetect: true,
|
|
|
|
|
autoDetectThreshold: [70, 70, 70] as [number, number, number],
|
|
|
|
|
hueRange: [60, 130] as [number, number],
|
|
|
|
|
saturationRange: [50, 255] as [number, number],
|
|
|
|
|
valueRange: [50, 255] as [number, number],
|
|
|
|
|
sampleRegion: { startX: 0.05, endX: 0.2, startY: 0.08, endY: 0.25 },
|
|
|
|
|
autoDetectFrameInterval: 30
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-31 12:06:23 +02:00
|
|
|
private dialog = inject(MatDialog);
|
|
|
|
|
|
2024-10-02 01:14:42 +02:00
|
|
|
constructor(
|
|
|
|
|
protected override testFeedService: TestFeedService,
|
|
|
|
|
) {
|
|
|
|
|
super(testFeedService);
|
|
|
|
|
}
|
2024-07-02 19:19:05 +02:00
|
|
|
|
2024-10-02 01:14:42 +02:00
|
|
|
async muteUnmuteVideo() {
|
2024-10-04 13:25:44 +02:00
|
|
|
if (this._track?.isMuted) {
|
2024-10-02 01:14:42 +02:00
|
|
|
this.muteVideoIcon = 'videocam';
|
2024-10-04 13:25:44 +02:00
|
|
|
await (this._track as LocalTrack).unmute();
|
2024-10-02 01:14:42 +02:00
|
|
|
} else {
|
|
|
|
|
this.muteVideoIcon = 'videocam_off';
|
2024-10-04 13:25:44 +02:00
|
|
|
await (this._track as LocalTrack).mute();
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-10-02 01:14:42 +02:00
|
|
|
}
|
2024-07-02 19:19:05 +02:00
|
|
|
|
2024-10-02 01:14:42 +02:00
|
|
|
async onQualityChange() {
|
|
|
|
|
let videoQuality: VideoQuality;
|
|
|
|
|
switch (this.maxVideoQuality) {
|
|
|
|
|
case 'LOW':
|
|
|
|
|
videoQuality = VideoQuality.LOW;
|
|
|
|
|
break;
|
|
|
|
|
case 'MEDIUM':
|
|
|
|
|
videoQuality = VideoQuality.MEDIUM;
|
|
|
|
|
break;
|
|
|
|
|
case 'HIGH':
|
|
|
|
|
videoQuality = VideoQuality.HIGH;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
videoQuality = VideoQuality.HIGH;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-10-02 01:14:42 +02:00
|
|
|
await (this.trackPublication as RemoteTrackPublication).setVideoQuality(
|
|
|
|
|
videoQuality
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-07-02 19:19:05 +02:00
|
|
|
|
2024-10-02 01:14:42 +02:00
|
|
|
openInfoDialog() {
|
|
|
|
|
const updateFunction = async (): Promise<string> => {
|
|
|
|
|
const videoLayers: any[] = [];
|
2024-10-04 13:25:44 +02:00
|
|
|
let stats = await (this._track! as VideoTrack).getRTCStatsReport();
|
2024-10-28 14:30:22 +01:00
|
|
|
let codecs = new Map();
|
2024-10-02 01:14:42 +02:00
|
|
|
stats?.forEach((report) => {
|
2024-10-28 14:30:22 +01:00
|
|
|
if (report.type === 'codec') {
|
|
|
|
|
// Store for matching with codecId in 'outbound-rtp' or 'inbound-rtp' reports
|
|
|
|
|
codecs.set(report.id, report);
|
|
|
|
|
}
|
2024-10-02 01:14:42 +02:00
|
|
|
if (report.type === 'outbound-rtp' || report.type === 'inbound-rtp') {
|
|
|
|
|
videoLayers.push({
|
|
|
|
|
codecId: report.codecId,
|
|
|
|
|
scalabilityMode: report.scalabilityMode,
|
|
|
|
|
rid: report.rid,
|
|
|
|
|
active: report.active,
|
|
|
|
|
frameWidth: report.frameWidth,
|
|
|
|
|
frameHeight: report.frameHeight,
|
|
|
|
|
framesPerSecond: report.framesPerSecond,
|
2024-10-04 13:25:44 +02:00
|
|
|
bytesReceived: report.bytesReceived,
|
|
|
|
|
bytesSent: report.bytesSent,
|
2024-10-02 01:14:42 +02:00
|
|
|
});
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-10-02 01:14:42 +02:00
|
|
|
});
|
2024-10-28 14:30:22 +01:00
|
|
|
videoLayers.forEach((layer) => {
|
|
|
|
|
if (codecs.has(layer.codecId)) {
|
|
|
|
|
layer.codec = codecs.get(layer.codecId).mimeType;
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-10-02 01:14:42 +02:00
|
|
|
return JSON.stringify(videoLayers, null, 2);
|
|
|
|
|
};
|
|
|
|
|
this.dialog.open(InfoDialogComponent, {
|
|
|
|
|
data: {
|
|
|
|
|
title: 'Video Track Layers Info',
|
2024-10-04 13:25:44 +02:00
|
|
|
subtitle: this.finalElementRefId,
|
2024-10-02 01:14:42 +02:00
|
|
|
updateFunction,
|
2025-07-14 22:18:12 +02:00
|
|
|
updateInterval: 700,
|
2024-10-02 01:14:42 +02:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-07-02 19:19:05 +02:00
|
|
|
|
2025-12-12 15:33:56 +01:00
|
|
|
openProcessorDialog() {
|
|
|
|
|
this.dialog.open(ProcessorDialogComponent, {
|
|
|
|
|
data: {
|
|
|
|
|
videoTrack: this,
|
|
|
|
|
},
|
|
|
|
|
width: '1400px',
|
|
|
|
|
maxWidth: '95vw',
|
|
|
|
|
maxHeight: '95vh',
|
|
|
|
|
});
|
2024-10-02 01:14:42 +02:00
|
|
|
}
|
2024-10-24 11:36:52 +02:00
|
|
|
|
|
|
|
|
toggleVideoZoom() {
|
|
|
|
|
this.videoZoom = !this.videoZoom;
|
2024-10-28 19:12:16 +01:00
|
|
|
let newWidth = this.videoZoom ? '1500px' : '120px';
|
2024-10-24 11:36:52 +02:00
|
|
|
this.elementRef.nativeElement.style.width = newWidth;
|
|
|
|
|
}
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|