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 { AppRoutingModule } from './app.routing';
import { VideoResolutionComponent } from './components/dialogs/options-dialog/video-resolution/video-resolution.component';
import { InfoDialogComponent } from './components/dialogs/info-dialog/info-dialog.component';
@NgModule({
declarations: [
@ -63,6 +64,7 @@ import { VideoResolutionComponent } from './components/dialogs/options-dialog/vi
CallbackPipe,
OptionsDialogComponent,
VideoResolutionComponent,
InfoDialogComponent,
],
imports: [
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-label>scalabilityMode</mat-label>
<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-form-field>
<mat-form-field class="inner-text">

View File

@ -56,6 +56,9 @@
<div class="row">
<mat-card-title class="room-mat-card-title">{{room.name}}</mat-card-title>
<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">
<mat-icon aria-label="Send message button">chat</mat-icon>
</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 { EventsDialogComponent } from '../dialogs/events-dialog/events-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({
selector: 'app-openvidu-instance',
@ -1052,7 +1054,7 @@ export class OpenviduInstanceComponent {
});
dialogRef.afterClosed().subscribe((result) => {
if (!!result) {
this.roomOptions = result;
this.roomOptions = result.roomOptions;
this.createLocalTracksOptions = result.createLocalTracksOptions;
this.screenShareCaptureOptions = result.screenShareCaptureOptions;
this.trackPublishOptions = result.trackPublishOptions;
@ -1132,4 +1134,44 @@ export class OpenviduInstanceComponent {
}
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"
(newTrackEvent)="events.push($event)"></app-audio-track>
</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"
[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>

View File

@ -1,16 +1,33 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { TrackPublication, LocalParticipant, Track, TrackEvent, LocalTrack, RemoteTrack, TrackEventCallbacks } from 'livekit-client';
import { TestAppEvent, TestFeedService } from 'src/app/services/test-feed.service';
import {
TrackPublication,
LocalParticipant,
Track,
TrackEvent,
LocalTrack,
RemoteTrack,
TrackEventCallbacks,
LocalTrackPublication,
RemoteTrackPublication,
} from 'livekit-client';
import {
TestAppEvent,
TestFeedService,
} from 'src/app/services/test-feed.service';
@Component({
selector: 'app-track',
template: '',
styleUrls: []
styleUrls: [],
})
export class TrackComponent {
@Output()
newTrackEvent = new EventEmitter<{ eventType: TrackEvent, eventCategory: 'TrackEvent', eventContent: any, eventDescription: string }>();
newTrackEvent = new EventEmitter<{
eventType: TrackEvent;
eventCategory: 'TrackEvent';
eventContent: any;
eventDescription: string;
}>();
@Input()
trackPublication: TrackPublication;
@ -23,28 +40,117 @@ export class TrackComponent {
protected track: Track | undefined;
constructor(private testFeedService: TestFeedService) { }
trackSubscribed: boolean = true;
trackEnabled: boolean = true;
constructor(protected testFeedService: TestFeedService) {}
protected async unpublishTrack() {
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
let callbacks: TrackEventCallbacks;
let events: TrackEvent;
this.track?.on(TrackEvent.Message, () => { this.newTrackEvent.emit({ eventType: TrackEvent.Message, 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 }) })
this.track
?.on(TrackEvent.Message, () => {
this.newTrackEvent.emit({
eventType: TrackEvent.Message,
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 {
@ -62,5 +168,4 @@ export class TrackComponent {
updateEventList(event: TestAppEvent) {
this.testFeedService.pushNewEvent({ user: this.index, event });
}
}

View File

@ -27,6 +27,10 @@ video {
float: left;
}
.quality-option {
float: right;
}
.video-btn:hover {
color: #4d4d4d;
}
@ -43,3 +47,19 @@ video {
height: 14px;
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">
<video #videoElement [id]="elementRefId" [ngClass]="getTrackOrigin()"></video>
<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">
<mat-icon aria-label="Mute/Unmute video" class="mat-icon material-icons" role="img"
aria-hidden="true">{{muteVideoIcon}}</mat-icon>
</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">
<mat-icon aria-label="Unpublish track" class="mat-icon material-icons" role="img"
aria-hidden="true">stop</mat-icon>
</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">
<mat-icon aria-label="Blur video" class="mat-icon material-icons" role="img"
aria-hidden="true">{{blurIcon}}</mat-icon>

View File

@ -1,14 +1,24 @@
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 { MatDialog } from '@angular/material/dialog';
import { TestFeedService } from 'src/app/services/test-feed.service';
import { InfoDialogComponent } from '../dialogs/info-dialog/info-dialog.component';
@Component({
selector: 'app-video-track',
templateUrl: './video-track.component.html',
styleUrls: ['./video-track.component.css']
styleUrls: ['./video-track.component.css'],
})
export class VideoTrackComponent extends TrackComponent {
@ViewChild('videoElement') elementRef: ElementRef;
elementRefId: string = '';
@ -16,9 +26,7 @@ export class VideoTrackComponent extends TrackComponent {
blurIcon: string = 'blur_on';
private _videoTrack: VideoTrack | undefined;
private videoCaptureOptions: VideoCaptureOptions;
private screenShareCaptureOptions: ScreenShareCaptureOptions;
maxVideoQuality: string;
@Input() set videoTrack(videoTrack: VideoTrack | undefined) {
this._videoTrack = videoTrack;
@ -27,7 +35,9 @@ export class VideoTrackComponent extends TrackComponent {
this.setupTrackEventListeners();
let id = '';
id = `${this.getTrackOrigin()}--video--${this._videoTrack?.source}--${this._videoTrack?.sid}`;
id = `${this.getTrackOrigin()}--video--${this._videoTrack?.source}--${
this._videoTrack?.sid
}`;
if (this._videoTrack?.sid !== this._videoTrack?.mediaStreamID) {
id += `--${this._videoTrack?.mediaStreamID}`;
}
@ -39,6 +49,13 @@ export class VideoTrackComponent extends TrackComponent {
}
}
constructor(
protected override testFeedService: TestFeedService,
private dialog: MatDialog
) {
super(testFeedService);
}
ngAfterViewInit() {
this._videoTrack?.attach(this.elementRef.nativeElement);
}
@ -57,6 +74,54 @@ export class VideoTrackComponent extends TrackComponent {
}
}
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,
});
}
});
return JSON.stringify(videoLayers, null, 2);
};
this.dialog.open(InfoDialogComponent, {
data: {
title: 'Video Track Layers Info',
subtitle: this.elementRefId,
updateFunction,
},
});
}
async blur() {
if (this.blurIcon == 'blur_on') {
// await (this._videoTrack! as LocalVideoTrack).setProcessor(BackgroundBlur());
@ -66,5 +131,4 @@ export class VideoTrackComponent extends TrackComponent {
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 {
width: 140px;
}
.cdk-overlay-pane:has(.mat-mdc-select-panel) {
min-width: fit-content;
}