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 { 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,
|
||||
|
|
|
@ -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-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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,45 +1,65 @@
|
|||
video {
|
||||
height: 90px;
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.parent-div {
|
||||
position: relative;
|
||||
height: 90px;
|
||||
position: relative;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.top-div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.bottom-div {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.video-btn {
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
height: 16px;
|
||||
float: left;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
height: 16px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.quality-option {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.video-btn:hover {
|
||||
color: #4d4d4d;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.video-btn.top-row {
|
||||
margin-top: 0;
|
||||
display: inline-flex;
|
||||
float: right;
|
||||
margin-top: 0;
|
||||
display: inline-flex;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.video-btn mat-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
line-height: 16px;
|
||||
}
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,70 +1,134 @@
|
|||
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']
|
||||
selector: 'app-video-track',
|
||||
templateUrl: './video-track.component.html',
|
||||
styleUrls: ['./video-track.component.css'],
|
||||
})
|
||||
export class VideoTrackComponent extends TrackComponent {
|
||||
@ViewChild('videoElement') elementRef: ElementRef;
|
||||
elementRefId: string = '';
|
||||
|
||||
@ViewChild('videoElement') elementRef: ElementRef;
|
||||
elementRefId: string = '';
|
||||
muteVideoIcon: string = 'videocam';
|
||||
blurIcon: string = 'blur_on';
|
||||
|
||||
muteVideoIcon: string = 'videocam';
|
||||
blurIcon: string = 'blur_on';
|
||||
private _videoTrack: VideoTrack | undefined;
|
||||
maxVideoQuality: string;
|
||||
|
||||
private _videoTrack: VideoTrack | undefined;
|
||||
@Input() set videoTrack(videoTrack: VideoTrack | undefined) {
|
||||
this._videoTrack = videoTrack;
|
||||
this.track = this._videoTrack;
|
||||
|
||||
private videoCaptureOptions: VideoCaptureOptions;
|
||||
private screenShareCaptureOptions: ScreenShareCaptureOptions;
|
||||
this.setupTrackEventListeners();
|
||||
|
||||
@Input() set videoTrack(videoTrack: VideoTrack | undefined) {
|
||||
this._videoTrack = videoTrack;
|
||||
this.track = this._videoTrack;
|
||||
let id = '';
|
||||
id = `${this.getTrackOrigin()}--video--${this._videoTrack?.source}--${
|
||||
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 = '';
|
||||
id = `${this.getTrackOrigin()}--video--${this._videoTrack?.source}--${this._videoTrack?.sid}`;
|
||||
if (this._videoTrack?.sid !== this._videoTrack?.mediaStreamID) {
|
||||
id += `--${this._videoTrack?.mediaStreamID}`;
|
||||
constructor(
|
||||
protected override testFeedService: TestFeedService,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
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) {
|
||||
this._videoTrack?.attach(this.elementRef.nativeElement);
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
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 {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.cdk-overlay-pane:has(.mat-mdc-select-panel) {
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue