openvidu-testapp: new features to test simulcast, dynacast and adaptive stream

pull/848/head
pabloFuente 2024-10-02 01:14:42 +02:00
parent fdc3b82122
commit 88baf99368
14 changed files with 506 additions and 100 deletions

View File

@ -45,6 +45,7 @@ import { TableVideoComponent } from './components/users-table/table-video.compon
import { CallbackPipe } from './pipes/callback.pipe'; import { CallbackPipe } from './pipes/callback.pipe';
import { AppRoutingModule } from './app.routing'; import { AppRoutingModule } from './app.routing';
import { VideoResolutionComponent } from './components/dialogs/options-dialog/video-resolution/video-resolution.component'; import { VideoResolutionComponent } from './components/dialogs/options-dialog/video-resolution/video-resolution.component';
import { InfoDialogComponent } from './components/dialogs/info-dialog/info-dialog.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -63,6 +64,7 @@ import { VideoResolutionComponent } from './components/dialogs/options-dialog/vi
CallbackPipe, CallbackPipe,
OptionsDialogComponent, OptionsDialogComponent,
VideoResolutionComponent, VideoResolutionComponent,
InfoDialogComponent,
], ],
imports: [ imports: [
FormsModule, FormsModule,

View File

@ -0,0 +1,13 @@
mat-form-field {
width: 100%;
}
textarea {
font-size: 13px !important;
line-height: 18px !important;
}
#subtitle {
font-size: 12px;
margin: 0 10px 0 10px;
}

View File

@ -0,0 +1,27 @@
<div>
<h2 mat-dialog-title>{{ title }}</h2>
<p *ngIf="subtitle" id="subtitle">{{ subtitle }}</p>
<mat-dialog-content>
<mat-form-field>
<textarea
id="info-text-area"
matInput
cdkTextareaAutosize
cdkAutosizeMinRows="10"
#autosize="cdkTextareaAutosize"
[(ngModel)]="textAreaValue"
readonly="true"
></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button id="close-dialog-btn" [mat-dialog-close]="{}">
CLOSE
</button>
<button mat-raised-button (click)="updateValue()" id="update-value-btn">
Update
</button>
</mat-dialog-actions>
</div>

View File

@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InfoDialogComponent } from './info-dialog.component';
describe('InfoDialogComponent', () => {
let component: InfoDialogComponent;
let fixture: ComponentFixture<InfoDialogComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InfoDialogComponent]
});
fixture = TestBed.createComponent(InfoDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,84 @@
import { Component, Inject, NgZone, ViewChild } from '@angular/core';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { take } from 'rxjs/operators';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'app-info-dialog',
templateUrl: './info-dialog.component.html',
styleUrls: ['./info-dialog.component.css'],
})
export class InfoDialogComponent {
title: string;
subtitle: string;
updateFunction: () => Promise<string>;
textAreaValue: string;
@ViewChild('autosize') autosize: CdkTextareaAutosize;
constructor(
@Inject(MAT_DIALOG_DATA)
public data: {
title: string;
subtitle: string;
updateFunction: () => Promise<string>;
},
private _ngZone: NgZone
) {
this.title = data.title;
this.subtitle = data.subtitle;
this.updateFunction = data.updateFunction;
this.updateValue();
// this.publisher
// .getSenders()
// .filter((sender) => {
// return sender.track?.kind === 'video';
// })[0]
// .getStats()
// .then((stats) => {
// stats.forEach((report) => {
// if (
// report.type === 'outbound-rtp' ||
// report.type === 'remote-inbound-rtp'
// ) {
// console.log(report.type);
// console.log(report);
// this.textAreaValue = report.framesPerSecond;
// }
// });
// });
// this.publisher.getConnectedAddress().then((address) => {
// this.textAreaValue = address! + '\n';
// this.textAreaValue += this.publisher.getConnectionState() + '\n';
// this.textAreaValue += this.publisher.getICEConnectionState() + '\n';
// this.textAreaValue += this.publisher.getSignallingState() + '\n';
// this.textAreaValue += this.publisher.getLocalDescription()!.sdp + '\n';
// this.textAreaValue += this.publisher.getRemoteDescription()!.sdp + '\n';
// this.subscriber.getConnectedAddress().then((address) => {
// this.textAreaValue += address! + '\n';
// this.textAreaValue += this.subscriber.getConnectionState() + '\n';
// this.textAreaValue += this.subscriber.getICEConnectionState() + '\n';
// this.textAreaValue += this.subscriber.getSignallingState() + '\n';
// this.textAreaValue += this.subscriber.getLocalDescription()!.sdp + '\n';
// this.textAreaValue +=
// this.subscriber.getRemoteDescription()!.sdp + '\n';
// });
// });
}
async updateValue() {
this.textAreaValue = await this.updateFunction();
this.triggerResize();
}
triggerResize() {
// Wait for changes to be applied, then trigger textarea resize.
this._ngZone.onStable
.pipe(take(1))
.subscribe(() => this.autosize.resizeToFitContent(true));
}
}

View File

@ -155,7 +155,8 @@
<mat-form-field id="trackPublish-scalabilityMode"> <mat-form-field id="trackPublish-scalabilityMode">
<mat-label>scalabilityMode</mat-label> <mat-label>scalabilityMode</mat-label>
<mat-select [(value)]="trackPublishOptions.scalabilityMode"> <mat-select [(value)]="trackPublishOptions.scalabilityMode">
<mat-option *ngFor="let mode of ['L1T1','L1T2','L1T3','L2T1','L2T1h','L2T1_KEY','L2T2','L2T2h','L2T2_KEY','L2T3','L2T3h','L2T3_KEY','L3T1','L3T1h','L3T1_KEY','L3T2','L3T2h','L3T2_KEY','L3T3','L3T3h','L3T3_KEY']" [value]="mode">{{mode}}</mat-option> <mat-option *ngFor="let mode of ['L1T1','L1T2','L1T3','L2T1','L2T1h','L2T1_KEY','L2T2','L2T2h','L2T2_KEY','L2T3','L2T3h','L2T3_KEY','L3T1','L3T1h','L3T1_KEY','L3T2','L3T2h','L3T2_KEY','L3T3','L3T3h','L3T3_KEY']"
[value]="mode" [ngClass]="'mode-' + mode">{{mode}}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="inner-text"> <mat-form-field class="inner-text">

View File

@ -56,6 +56,9 @@
<div class="row"> <div class="row">
<mat-card-title class="room-mat-card-title">{{room.name}}</mat-card-title> <mat-card-title class="room-mat-card-title">{{room.name}}</mat-card-title>
<div class="room-actions"> <div class="room-actions">
<button class="peer-info-btn" (click)="openInfoDialog()" title="PCTransports info">
<mat-icon aria-label="PCTransports info button">info</mat-icon>
</button>
<button class="message-btn" (click)="sendData()" title="Broadcast message to room"> <button class="message-btn" (click)="sendData()" title="Broadcast message to room">
<mat-icon aria-label="Send message button">chat</mat-icon> <mat-icon aria-label="Send message button">chat</mat-icon>
</button> </button>

View File

@ -43,6 +43,8 @@ import { RoomApiDialogComponent } from '../dialogs/room-api-dialog/room-api-dial
import { RoomApiService } from 'src/app/services/room-api.service'; import { RoomApiService } from 'src/app/services/room-api.service';
import { EventsDialogComponent } from '../dialogs/events-dialog/events-dialog.component'; import { EventsDialogComponent } from '../dialogs/events-dialog/events-dialog.component';
import { OptionsDialogComponent } from '../dialogs/options-dialog/options-dialog.component'; import { OptionsDialogComponent } from '../dialogs/options-dialog/options-dialog.component';
import PCTransport from 'livekit-client/dist/src/room/PCTransport';
import { InfoDialogComponent } from '../dialogs/info-dialog/info-dialog.component';
@Component({ @Component({
selector: 'app-openvidu-instance', selector: 'app-openvidu-instance',
@ -1052,7 +1054,7 @@ export class OpenviduInstanceComponent {
}); });
dialogRef.afterClosed().subscribe((result) => { dialogRef.afterClosed().subscribe((result) => {
if (!!result) { if (!!result) {
this.roomOptions = result; this.roomOptions = result.roomOptions;
this.createLocalTracksOptions = result.createLocalTracksOptions; this.createLocalTracksOptions = result.createLocalTracksOptions;
this.screenShareCaptureOptions = result.screenShareCaptureOptions; this.screenShareCaptureOptions = result.screenShareCaptureOptions;
this.trackPublishOptions = result.trackPublishOptions; this.trackPublishOptions = result.trackPublishOptions;
@ -1132,4 +1134,44 @@ export class OpenviduInstanceComponent {
} }
this.room?.localParticipant.publishData(data, options); this.room?.localParticipant.publishData(data, options);
} }
openInfoDialog() {
const updateFunction = async (): Promise<string> => {
const pub: PCTransport = this.getPublisherPC()!;
const sub: PCTransport = this.getSubscriberPC()!;
return JSON.stringify(
{
publisher: {
connectedAddress: await pub.getConnectedAddress(),
connectionState: pub.getConnectionState(),
iceConnectionState: pub.getICEConnectionState(),
signallingState: pub.getSignallingState(),
},
subscriber: {
connectedAddress: await sub.getConnectedAddress(),
connectionState: sub.getConnectionState(),
iceConnectionState: sub.getICEConnectionState(),
signallingState: sub.getSignallingState(),
},
},
null,
2
);
};
this.dialog.open(InfoDialogComponent, {
data: {
title: 'PCTransports info',
updateFunction,
},
minWidth: '50vh'
});
}
getPublisherPC(): PCTransport | undefined {
return this.room?.localParticipant.engine.pcManager?.publisher;
}
getSubscriberPC(): PCTransport | undefined {
return this.room?.localParticipant.engine.pcManager?.subscriber;
}
} }

View File

@ -55,9 +55,9 @@
[localParticipant]="localParticipant" [index]="index" [localParticipant]="localParticipant" [index]="index"
(newTrackEvent)="events.push($event)"></app-audio-track> (newTrackEvent)="events.push($event)"></app-audio-track>
</div> </div>
<app-video-track *ngFor="let trackPublication of participant.videoTrackPublications | keyvalue" <app-video-track *ngFor="let trackPublication of participant.videoTrackPublications | keyvalue ; index as i"
[trackPublication]="trackPublication.value" [videoTrack]="trackPublication.value.videoTrack" [trackPublication]="trackPublication.value" [videoTrack]="trackPublication.value.videoTrack"
[localParticipant]="localParticipant" [index]="index" [localParticipant]="localParticipant" [index]="index"
(newTrackEvent)="events.push($event)"></app-video-track> (newTrackEvent)="events.push($event)" [attr.id]="'user-' + index + '-' + participant.identity + '-video-' + i"></app-video-track>
</div> </div>
</div> </div>

View File

@ -1,16 +1,33 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { TrackPublication, LocalParticipant, Track, TrackEvent, LocalTrack, RemoteTrack, TrackEventCallbacks } from 'livekit-client'; import {
import { TestAppEvent, TestFeedService } from 'src/app/services/test-feed.service'; TrackPublication,
LocalParticipant,
Track,
TrackEvent,
LocalTrack,
RemoteTrack,
TrackEventCallbacks,
LocalTrackPublication,
RemoteTrackPublication,
} from 'livekit-client';
import {
TestAppEvent,
TestFeedService,
} from 'src/app/services/test-feed.service';
@Component({ @Component({
selector: 'app-track', selector: 'app-track',
template: '', template: '',
styleUrls: [] styleUrls: [],
}) })
export class TrackComponent { export class TrackComponent {
@Output() @Output()
newTrackEvent = new EventEmitter<{ eventType: TrackEvent, eventCategory: 'TrackEvent', eventContent: any, eventDescription: string }>(); newTrackEvent = new EventEmitter<{
eventType: TrackEvent;
eventCategory: 'TrackEvent';
eventContent: any;
eventDescription: string;
}>();
@Input() @Input()
trackPublication: TrackPublication; trackPublication: TrackPublication;
@ -23,28 +40,117 @@ export class TrackComponent {
protected track: Track | undefined; protected track: Track | undefined;
constructor(private testFeedService: TestFeedService) { } trackSubscribed: boolean = true;
trackEnabled: boolean = true;
constructor(protected testFeedService: TestFeedService) {}
protected async unpublishTrack() { protected async unpublishTrack() {
await this.localParticipant?.unpublishTrack(this.track as LocalTrack); await this.localParticipant?.unpublishTrack(this.track as LocalTrack);
} }
protected setupTrackEventListeners() { protected async toggleSubscribeTrack() {
this.trackSubscribed = !this.trackSubscribed;
await (this.trackPublication as RemoteTrackPublication).setSubscribed(
this.trackSubscribed
);
}
protected async toggleEnableTrack() {
this.trackEnabled = !this.trackEnabled;
await (this.trackPublication as RemoteTrackPublication).setEnabled(
this.trackEnabled
);
}
protected setupTrackEventListeners() {
// This is a link to the complete list of Track events // This is a link to the complete list of Track events
let callbacks: TrackEventCallbacks; let callbacks: TrackEventCallbacks;
let events: TrackEvent; let events: TrackEvent;
this.track?.on(TrackEvent.Message, () => { this.newTrackEvent.emit({ eventType: TrackEvent.Message, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) this.track
.on(TrackEvent.Muted, () => { this.newTrackEvent.emit({ eventType: TrackEvent.Muted, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) ?.on(TrackEvent.Message, () => {
.on(TrackEvent.Unmuted, () => { this.newTrackEvent.emit({ eventType: TrackEvent.Unmuted, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) this.newTrackEvent.emit({
.on(TrackEvent.AudioSilenceDetected, () => { this.newTrackEvent.emit({ eventType: TrackEvent.AudioSilenceDetected, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) eventType: TrackEvent.Message,
.on(TrackEvent.Restarted, () => { this.newTrackEvent.emit({ eventType: TrackEvent.Restarted, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) eventCategory: 'TrackEvent',
.on(TrackEvent.Ended, () => { this.newTrackEvent.emit({ eventType: TrackEvent.Ended, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) eventContent: {},
.on(TrackEvent.VisibilityChanged, (visible: boolean) => { this.newTrackEvent.emit({ eventType: TrackEvent.VisibilityChanged, eventCategory: 'TrackEvent', eventContent: { visible, track: this.track }, eventDescription: `${this.track!.source} is visible: ${visible}` }) }) eventDescription: this.track!.source,
.on(TrackEvent.VideoDimensionsChanged, (dimensions: Track.Dimensions) => { this.newTrackEvent.emit({ eventType: TrackEvent.VideoDimensionsChanged, eventCategory: 'TrackEvent', eventContent: { dimensions, track: this.track }, eventDescription: `${this.track!.source} ${JSON.stringify(dimensions)}` }) }) });
.on(TrackEvent.UpstreamPaused, () => { this.newTrackEvent.emit({ eventType: TrackEvent.UpstreamPaused, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) })
.on(TrackEvent.UpstreamResumed, () => { this.newTrackEvent.emit({ eventType: TrackEvent.UpstreamResumed, eventCategory: 'TrackEvent', eventContent: {}, eventDescription: this.track!.source }) }) .on(TrackEvent.Muted, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.Muted,
eventCategory: 'TrackEvent',
eventContent: {},
eventDescription: this.track!.source,
});
})
.on(TrackEvent.Unmuted, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.Unmuted,
eventCategory: 'TrackEvent',
eventContent: {},
eventDescription: this.track!.source,
});
})
.on(TrackEvent.AudioSilenceDetected, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.AudioSilenceDetected,
eventCategory: 'TrackEvent',
eventContent: {},
eventDescription: this.track!.source,
});
})
.on(TrackEvent.Restarted, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.Restarted,
eventCategory: 'TrackEvent',
eventContent: {},
eventDescription: this.track!.source,
});
})
.on(TrackEvent.Ended, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.Ended,
eventCategory: 'TrackEvent',
eventContent: {},
eventDescription: this.track!.source,
});
})
.on(TrackEvent.VisibilityChanged, (visible: boolean) => {
this.newTrackEvent.emit({
eventType: TrackEvent.VisibilityChanged,
eventCategory: 'TrackEvent',
eventContent: { visible, track: this.track },
eventDescription: `${this.track!.source} is visible: ${visible}`,
});
})
.on(TrackEvent.VideoDimensionsChanged, (dimensions: Track.Dimensions) => {
this.newTrackEvent.emit({
eventType: TrackEvent.VideoDimensionsChanged,
eventCategory: 'TrackEvent',
eventContent: { dimensions, track: this.track },
eventDescription: `${this.track?.source} ${JSON.stringify(
dimensions
)}`,
});
})
.on(TrackEvent.UpstreamPaused, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.UpstreamPaused,
eventCategory: 'TrackEvent',
eventContent: {},
eventDescription: this.track!.source,
});
})
.on(TrackEvent.UpstreamResumed, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.UpstreamResumed,
eventCategory: 'TrackEvent',
eventContent: {},
eventDescription: this.track!.source,
});
});
} }
protected getTrackOrigin(): string { protected getTrackOrigin(): string {
@ -62,5 +168,4 @@ export class TrackComponent {
updateEventList(event: TestAppEvent) { updateEventList(event: TestAppEvent) {
this.testFeedService.pushNewEvent({ user: this.index, event }); this.testFeedService.pushNewEvent({ user: this.index, event });
} }
} }

View File

@ -1,45 +1,65 @@
video { video {
height: 90px; height: 90px;
width: 120px; width: 120px;
} }
.parent-div { .parent-div {
position: relative; position: relative;
height: 90px; height: 90px;
} }
.top-div { .top-div {
position: absolute; position: absolute;
top: 0; top: 0;
} }
.bottom-div { .bottom-div {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
} }
.video-btn { .video-btn {
border: none; border: none;
background: rgba(255, 255, 255, 0.75); background: rgba(255, 255, 255, 0.75);
cursor: pointer; cursor: pointer;
padding: 0; padding: 0;
height: 16px; height: 16px;
float: left; float: left;
}
.quality-option {
float: right;
} }
.video-btn:hover { .video-btn:hover {
color: #4d4d4d; color: #4d4d4d;
} }
.video-btn.top-row { .video-btn.top-row {
margin-top: 0; margin-top: 0;
display: inline-flex; display: inline-flex;
float: right; float: right;
} }
.video-btn mat-icon { .video-btn mat-icon {
font-size: 14px; font-size: 14px;
width: 14px; width: 14px;
height: 14px; height: 14px;
line-height: 16px; line-height: 16px;
} }
#max-video-quality {
width: 16px;
height: 16px;
font-size: 10px;
font-weight: bold;
}
::ng-deep #max-video-quality * {
height: 16px !important;
width: 8px !important;
min-height: 16px !important;
min-width: 8px !important;
padding: 0 !important;
border-color: transparent !important;
}

View File

@ -1,16 +1,36 @@
<div class="parent-div"> <div class="parent-div">
<video #videoElement [id]="elementRefId" [ngClass]="getTrackOrigin()"></video> <video #videoElement [id]="elementRefId" [ngClass]="getTrackOrigin()"></video>
<div class="bottom-div"> <div class="bottom-div">
<button *ngIf="localParticipant" (click)="muteUnmuteVideo()" class="video-btn" matTooltip="Mute/Unmute video" <button *ngIf="localParticipant" (click)="muteUnmuteVideo()" class="video-btn mute-unmute-video" matTooltip="Mute/Unmute video"
matTooltipClass="custom-tooltip"> matTooltipClass="custom-tooltip">
<mat-icon aria-label="Mute/Unmute video" class="mat-icon material-icons" role="img" <mat-icon aria-label="Mute/Unmute video" class="mat-icon material-icons" role="img"
aria-hidden="true">{{muteVideoIcon}}</mat-icon> aria-hidden="true">{{muteVideoIcon}}</mat-icon>
</button> </button>
<button *ngIf="localParticipant" (click)="unpublishTrack()" class="video-btn" matTooltip="Unpublish track" <button *ngIf="localParticipant" (click)="unpublishTrack()" class="video-btn publish-unpublish-video" matTooltip="Unpublish track"
matTooltipClass="custom-tooltip"> matTooltipClass="custom-tooltip">
<mat-icon aria-label="Unpublish track" class="mat-icon material-icons" role="img" <mat-icon aria-label="Unpublish track" class="mat-icon material-icons" role="img"
aria-hidden="true">stop</mat-icon> aria-hidden="true">stop</mat-icon>
</button> </button>
<button *ngIf="!localParticipant" (click)="toggleEnableTrack()" class="video-btn toggle-video-enabled" matTooltip="Toggle track enabled"
matTooltipClass="custom-tooltip">
<mat-icon aria-label="Toggle track enabled" class="mat-icon material-icons" role="img"
aria-hidden="true">{{trackEnabled ? 'toggle_off' : 'toggle_on'}}</mat-icon>
</button>
<button *ngIf="!localParticipant" (click)="toggleSubscribeTrack()" class="video-btn toggle-video-subscribed" matTooltip="Toggle track subscription"
matTooltipClass="custom-tooltip">
<mat-icon aria-label="Toggle track subscription" class="mat-icon material-icons" role="img"
aria-hidden="true">{{trackSubscribed ? 'stop' : 'play_arrow'}}</mat-icon>
</button>
<mat-form-field *ngIf="!localParticipant" id="max-video-quality" class="video-btn quality-option" matTooltip="Set video quality" matTooltipClass="custom-tooltip">
<mat-select [(value)]="maxVideoQuality" (selectionChange)="onQualityChange()">
<mat-option *ngFor="let q of ['LOW', 'MEDIUM', 'HIGH']" [value]="q" [ngClass]="'mode-' + q">{{q}}</mat-option>
</mat-select>
</mat-form-field>
<button (click)="openInfoDialog()" class="video-btn video-track-info" matTooltip="Open info dialog"
matTooltipClass="custom-tooltip">
<mat-icon aria-label="Open info dialog" class="mat-icon material-icons" role="img"
aria-hidden="true">info</mat-icon>
</button>
<!--<button *ngIf="isLocal" (click)="blur()" class="video-btn"> <!--<button *ngIf="isLocal" (click)="blur()" class="video-btn">
<mat-icon aria-label="Blur video" class="mat-icon material-icons" role="img" <mat-icon aria-label="Blur video" class="mat-icon material-icons" role="img"
aria-hidden="true">{{blurIcon}}</mat-icon> aria-hidden="true">{{blurIcon}}</mat-icon>

View File

@ -1,70 +1,134 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { LocalTrack, VideoTrack, VideoCaptureOptions, ScreenShareCaptureOptions } from 'livekit-client'; import {
LocalTrack,
VideoTrack,
VideoCaptureOptions,
ScreenShareCaptureOptions,
RemoteTrack,
RemoteTrackPublication,
VideoQuality,
} from 'livekit-client';
import { TrackComponent } from '../track/track.component'; import { TrackComponent } from '../track/track.component';
import { MatDialog } from '@angular/material/dialog';
import { TestFeedService } from 'src/app/services/test-feed.service';
import { InfoDialogComponent } from '../dialogs/info-dialog/info-dialog.component';
@Component({ @Component({
selector: 'app-video-track', selector: 'app-video-track',
templateUrl: './video-track.component.html', templateUrl: './video-track.component.html',
styleUrls: ['./video-track.component.css'] styleUrls: ['./video-track.component.css'],
}) })
export class VideoTrackComponent extends TrackComponent { export class VideoTrackComponent extends TrackComponent {
@ViewChild('videoElement') elementRef: ElementRef;
elementRefId: string = '';
@ViewChild('videoElement') elementRef: ElementRef; muteVideoIcon: string = 'videocam';
elementRefId: string = ''; blurIcon: string = 'blur_on';
muteVideoIcon: string = 'videocam'; private _videoTrack: VideoTrack | undefined;
blurIcon: string = 'blur_on'; maxVideoQuality: string;
private _videoTrack: VideoTrack | undefined; @Input() set videoTrack(videoTrack: VideoTrack | undefined) {
this._videoTrack = videoTrack;
this.track = this._videoTrack;
private videoCaptureOptions: VideoCaptureOptions; this.setupTrackEventListeners();
private screenShareCaptureOptions: ScreenShareCaptureOptions;
@Input() set videoTrack(videoTrack: VideoTrack | undefined) { let id = '';
this._videoTrack = videoTrack; id = `${this.getTrackOrigin()}--video--${this._videoTrack?.source}--${
this.track = this._videoTrack; this._videoTrack?.sid
}`;
if (this._videoTrack?.sid !== this._videoTrack?.mediaStreamID) {
id += `--${this._videoTrack?.mediaStreamID}`;
}
id = id.replace(/[^0-9a-z-A-Z_-]+/g, '');
this.elementRefId = id;
this.setupTrackEventListeners(); if (this.elementRef) {
this._videoTrack?.attach(this.elementRef.nativeElement);
}
}
let id = ''; constructor(
id = `${this.getTrackOrigin()}--video--${this._videoTrack?.source}--${this._videoTrack?.sid}`; protected override testFeedService: TestFeedService,
if (this._videoTrack?.sid !== this._videoTrack?.mediaStreamID) { private dialog: MatDialog
id += `--${this._videoTrack?.mediaStreamID}`; ) {
super(testFeedService);
}
ngAfterViewInit() {
this._videoTrack?.attach(this.elementRef.nativeElement);
}
ngOnDestroy() {
this._videoTrack?.detach(this.elementRef.nativeElement);
}
async muteUnmuteVideo() {
if (this._videoTrack?.isMuted) {
this.muteVideoIcon = 'videocam';
await (this._videoTrack as LocalTrack).unmute();
} else {
this.muteVideoIcon = 'videocam_off';
await (this._videoTrack as LocalTrack).mute();
}
}
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;
}
await (this.trackPublication as RemoteTrackPublication).setVideoQuality(
videoQuality
);
}
openInfoDialog() {
const updateFunction = async (): Promise<string> => {
const videoLayers: any[] = [];
let stats = await this._videoTrack?.getRTCStatsReport();
stats?.forEach((report) => {
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,
});
} }
id = id.replace(/[^0-9a-z-A-Z_-]+/g, ''); });
this.elementRefId = id; return JSON.stringify(videoLayers, null, 2);
};
this.dialog.open(InfoDialogComponent, {
data: {
title: 'Video Track Layers Info',
subtitle: this.elementRefId,
updateFunction,
},
});
}
if (this.elementRef) { async blur() {
this._videoTrack?.attach(this.elementRef.nativeElement); if (this.blurIcon == 'blur_on') {
} // await (this._videoTrack! as LocalVideoTrack).setProcessor(BackgroundBlur());
this.blurIcon = 'blur_off';
} else {
// await (this._videoTrack! as LocalVideoTrack).stopProcessor();
this.blurIcon = 'blur_on';
} }
}
ngAfterViewInit() {
this._videoTrack?.attach(this.elementRef.nativeElement);
}
ngOnDestroy() {
this._videoTrack?.detach(this.elementRef.nativeElement);
}
async muteUnmuteVideo() {
if (this._videoTrack?.isMuted) {
this.muteVideoIcon = 'videocam';
await (this._videoTrack as LocalTrack).unmute();
} else {
this.muteVideoIcon = 'videocam_off';
await (this._videoTrack as LocalTrack).mute();
}
}
async blur() {
if (this.blurIcon == 'blur_on') {
// await (this._videoTrack! as LocalVideoTrack).setProcessor(BackgroundBlur());
this.blurIcon = 'blur_off';
} else {
// await (this._videoTrack! as LocalVideoTrack).stopProcessor();
this.blurIcon = 'blur_on';
}
}
} }

View File

@ -105,3 +105,7 @@ button.mat-icon-custom .mat-mdc-button-touch-target {
app-options-dialog .mat-mdc-form-field-infix { app-options-dialog .mat-mdc-form-field-infix {
width: 140px; width: 140px;
} }
.cdk-overlay-pane:has(.mat-mdc-select-panel) {
min-width: fit-content;
}