mirror of https://github.com/OpenVidu/openvidu.git
openvidu-testapp: new features to test simulcast, dynacast and adaptive stream
parent
fdc3b82122
commit
88baf99368
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue