2024-07-02 19:19:05 +02:00
|
|
|
import { LiveAnnouncer } from '@angular/cdk/a11y';
|
|
|
|
import { COMMA, ENTER } from '@angular/cdk/keycodes';
|
|
|
|
import { Component, Inject, inject } from '@angular/core';
|
|
|
|
import { MatChipInputEvent } from '@angular/material/chips';
|
|
|
|
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
2024-11-21 14:54:49 +01:00
|
|
|
import { VideoCodec } from '@livekit/protocol';
|
2024-07-02 19:19:05 +02:00
|
|
|
import { LocalParticipant } from 'livekit-client';
|
|
|
|
|
2024-11-21 14:54:49 +01:00
|
|
|
import {
|
|
|
|
EgressClient,
|
|
|
|
EncodedFileOutput,
|
|
|
|
EncodedFileType,
|
|
|
|
EncodedOutputs,
|
|
|
|
IngressInfo,
|
|
|
|
IngressInput,
|
|
|
|
IngressVideoEncodingPreset,
|
|
|
|
Room,
|
|
|
|
RoomCompositeOptions,
|
|
|
|
RoomServiceClient,
|
|
|
|
SegmentedFileOutput,
|
|
|
|
SegmentedFileProtocol,
|
|
|
|
StreamOutput,
|
|
|
|
StreamProtocol,
|
|
|
|
} from 'livekit-server-sdk';
|
2025-02-06 18:59:29 +01:00
|
|
|
import {
|
|
|
|
DEFAULT_URI_HTTP_ONLY_AUDIO,
|
|
|
|
DEFAULT_URI_HTTP_ONLY_VIDEO,
|
|
|
|
DEFAULT_URI_HTTP_VIDEO_AUDIO,
|
|
|
|
DEFAULT_URI_RTSP,
|
|
|
|
DEFAULT_URI_SRT,
|
|
|
|
RoomApiService,
|
|
|
|
} from 'src/app/services/room-api.service';
|
2024-07-02 19:19:05 +02:00
|
|
|
|
|
|
|
@Component({
|
2024-11-21 14:54:49 +01:00
|
|
|
selector: 'app-room-api-dialog',
|
|
|
|
templateUrl: './room-api-dialog.component.html',
|
|
|
|
styleUrls: ['./room-api-dialog.component.css'],
|
2024-07-02 19:19:05 +02:00
|
|
|
})
|
|
|
|
export class RoomApiDialogComponent {
|
2024-11-21 14:54:49 +01:00
|
|
|
room: Room;
|
|
|
|
localParticipant: LocalParticipant;
|
|
|
|
roomServiceClient: RoomServiceClient;
|
|
|
|
egressClient: EgressClient;
|
|
|
|
|
|
|
|
apiRoomName: string;
|
|
|
|
apiParticipantIdentity: string;
|
|
|
|
apiTrackSid: string;
|
|
|
|
muteTrack: boolean = true;
|
|
|
|
|
|
|
|
egressRoomName: string;
|
|
|
|
egressId: string;
|
|
|
|
audioTrackId: string;
|
|
|
|
videoTrackId: string;
|
|
|
|
|
|
|
|
ROOM_COMPOSITE_LAYOUTS = ['grid', 'speaker', 'single-speaker'];
|
|
|
|
roomCompositeLayoutSelected: string = 'grid';
|
|
|
|
roomCompositeAudioOnly: boolean = false;
|
|
|
|
roomCompositeVideoOnly: boolean = false;
|
|
|
|
|
|
|
|
fileOutputSelected: boolean = true;
|
|
|
|
streamOutputSelected: boolean = false;
|
|
|
|
s3Endpoint: string = 'http://localhost:9100'; // 'http://minio:9000'
|
|
|
|
rtmpUrls: string[] = ['rtmp://172.17.0.1:1936/live/'];
|
|
|
|
segmentOutputSelected: boolean = false;
|
|
|
|
segmentDuration: number = 6;
|
|
|
|
|
|
|
|
ingressRoomName: string;
|
|
|
|
ingressId: string;
|
|
|
|
inputTypeSelected: IngressInput = IngressInput.URL_INPUT;
|
|
|
|
ingressWithVideo: boolean = true;
|
|
|
|
ingressWithAudio: boolean = false;
|
|
|
|
ingressVideoCodecSelected: VideoCodec = VideoCodec.H264_BASELINE;
|
|
|
|
ingressSimulcast: boolean = true;
|
2024-11-21 16:11:33 +01:00
|
|
|
ingressEnableTranscoding: boolean = false;
|
2024-11-21 14:54:49 +01:00
|
|
|
ingressVideoEncodingPresetSelected?: IngressVideoEncodingPreset = undefined;
|
|
|
|
|
2025-02-03 15:35:41 +01:00
|
|
|
INGRESS_URL_TYPES: string[] = ['HTTP', 'SRT', 'RTSP'];
|
|
|
|
ingressUrlType: string = 'HTTP';
|
2025-02-06 18:59:29 +01:00
|
|
|
ingressUrlUri: string;
|
2025-02-03 15:35:41 +01:00
|
|
|
|
2024-11-21 14:54:49 +01:00
|
|
|
response: string;
|
|
|
|
|
|
|
|
INGRESS_INPUT_TYPES: { value: IngressInput; viewValue: string }[] = [
|
|
|
|
{ value: IngressInput.URL_INPUT, viewValue: 'URL' },
|
|
|
|
{ value: IngressInput.RTMP_INPUT, viewValue: 'RTMP' },
|
|
|
|
{ value: IngressInput.WHIP_INPUT, viewValue: 'WHIP' },
|
|
|
|
];
|
|
|
|
INGRESS_VIDEO_CODECS: { value: VideoCodec; viewValue: string }[] = [
|
|
|
|
{ value: VideoCodec.H264_BASELINE, viewValue: 'H264' },
|
|
|
|
{ value: VideoCodec.VP8, viewValue: 'VP8' },
|
|
|
|
];
|
|
|
|
INGRESS_VIDEO_ENCODING_PRESETS: {
|
|
|
|
value: IngressInput | undefined;
|
|
|
|
viewValue: string;
|
|
|
|
}[] = [{ value: undefined, viewValue: 'undefined' }].concat(
|
|
|
|
Object.keys(IngressVideoEncodingPreset)
|
|
|
|
.filter((key) => isNaN(Number(key)))
|
|
|
|
.map((key) => {
|
|
|
|
return {
|
|
|
|
value: IngressVideoEncodingPreset[key as any],
|
|
|
|
viewValue: key.toString(),
|
|
|
|
} as any;
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
// s3Config = {
|
|
|
|
// endpoint: this.s3Endpoint,
|
|
|
|
// metadata: { "mytag": "myvalue" },
|
|
|
|
// tagging: "mytagging"
|
|
|
|
// };
|
|
|
|
|
|
|
|
announcer = inject(LiveAnnouncer);
|
|
|
|
addOnBlur = true;
|
|
|
|
readonly separatorKeysCodes = [ENTER, COMMA] as const;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
private roomApiService: RoomApiService,
|
|
|
|
public dialogRef: MatDialogRef<RoomApiDialogComponent>,
|
|
|
|
@Inject(MAT_DIALOG_DATA) public data: any
|
|
|
|
) {
|
|
|
|
this.room = data.room;
|
|
|
|
this.localParticipant = data.localParticipant;
|
|
|
|
this.apiRoomName = this.room?.name;
|
|
|
|
this.apiParticipantIdentity = this.localParticipant?.identity;
|
|
|
|
this.apiTrackSid = this.localParticipant?.videoTrackPublications
|
|
|
|
.values()
|
|
|
|
.next().value?.trackSid!;
|
|
|
|
this.egressRoomName = this.room?.name;
|
|
|
|
this.audioTrackId = this.localParticipant?.audioTrackPublications
|
|
|
|
.values()
|
|
|
|
.next().value?.trackSid!;
|
|
|
|
this.videoTrackId = this.localParticipant?.videoTrackPublications
|
|
|
|
.values()
|
|
|
|
.next().value?.trackSid!;
|
|
|
|
this.ingressRoomName = this.room?.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
async listRooms() {
|
|
|
|
console.log('Listing rooms');
|
|
|
|
try {
|
|
|
|
const rooms = await this.roomApiService.listRooms();
|
|
|
|
this.response = JSON.stringify(rooms, null, 4);
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async deleteRoom() {
|
|
|
|
console.log('Deleting room');
|
|
|
|
try {
|
|
|
|
await this.roomApiService.deleteRoom(this.apiRoomName);
|
|
|
|
this.response = 'Room deleted';
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
|
|
|
console.log(JSON.stringify(error));
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async deleteAllRooms() {
|
|
|
|
console.log('Deleting all rooms');
|
|
|
|
try {
|
|
|
|
const promises: Promise<void>[] = [];
|
|
|
|
const rooms = await this.roomApiService.listRooms();
|
|
|
|
rooms.forEach((r) => {
|
|
|
|
promises.push(this.roomApiService.deleteRoom(r.name));
|
|
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
this.response = 'Deleted ' + promises.length + ' rooms';
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async listParticipants() {
|
|
|
|
console.log('Listing participants');
|
|
|
|
try {
|
|
|
|
const participants = await this.roomApiService.listParticipants(
|
|
|
|
this.apiRoomName
|
|
|
|
);
|
|
|
|
this.response = JSON.stringify(participants, null, 4);
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async getParticipant() {
|
|
|
|
console.log('Getting participant');
|
|
|
|
try {
|
|
|
|
const participant = await this.roomApiService.getParticipant(
|
|
|
|
this.apiRoomName,
|
|
|
|
this.apiParticipantIdentity
|
|
|
|
);
|
|
|
|
this.response = JSON.stringify(participant, null, 4);
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async removeParticipant() {
|
|
|
|
console.log('Removing participant');
|
|
|
|
try {
|
|
|
|
await this.roomApiService.removeParticipant(
|
|
|
|
this.apiRoomName,
|
|
|
|
this.apiParticipantIdentity
|
|
|
|
);
|
|
|
|
this.response = 'Participant removed';
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async mutePublishedTrack() {
|
|
|
|
console.log(`${this.muteTrack ? 'Muting' : 'Unmuting'} track`);
|
|
|
|
try {
|
|
|
|
await this.roomApiService.mutePublishedTrack(
|
|
|
|
this.apiRoomName,
|
|
|
|
this.apiParticipantIdentity,
|
|
|
|
this.apiTrackSid,
|
|
|
|
this.muteTrack
|
|
|
|
);
|
|
|
|
this.response = `Track ${this.muteTrack ? 'muted' : 'unmuted'}`;
|
|
|
|
this.muteTrack = !this.muteTrack;
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async listEgress() {
|
|
|
|
console.log('Listing egress');
|
|
|
|
try {
|
|
|
|
const egress = await this.roomApiService.listEgress();
|
|
|
|
this.response = JSON.stringify(egress, null, 4);
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async startRoomCompositeEgress() {
|
|
|
|
console.log('Starting room composite egress');
|
|
|
|
try {
|
|
|
|
const encodedOutputs = this.getEncodedOutputs();
|
|
|
|
const roomCompositeOptions: RoomCompositeOptions = {
|
|
|
|
layout: this.roomCompositeLayoutSelected,
|
|
|
|
audioOnly: this.roomCompositeAudioOnly,
|
|
|
|
videoOnly: this.roomCompositeVideoOnly,
|
|
|
|
};
|
|
|
|
const egress = await this.roomApiService.startRoomCompositeEgress(
|
|
|
|
this.egressRoomName,
|
|
|
|
roomCompositeOptions,
|
|
|
|
encodedOutputs
|
|
|
|
);
|
|
|
|
this.response = JSON.stringify(egress, null, 4);
|
|
|
|
this.egressId = egress.egressId;
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async startTrackCompositeEgress() {
|
|
|
|
console.log('Starting track composite egress');
|
|
|
|
try {
|
|
|
|
const encodedOutputs = this.getEncodedOutputs();
|
|
|
|
const egress = await this.roomApiService.startTrackCompositeEgress(
|
|
|
|
this.egressRoomName,
|
|
|
|
this.audioTrackId,
|
|
|
|
this.videoTrackId,
|
|
|
|
encodedOutputs
|
|
|
|
);
|
|
|
|
this.response = JSON.stringify(egress, null, 4);
|
|
|
|
this.egressId = egress.egressId;
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async startTrackEgress() {
|
|
|
|
console.log('Starting track egress');
|
|
|
|
try {
|
|
|
|
const egress = await this.roomApiService.startTrackEgress(
|
|
|
|
this.egressRoomName,
|
|
|
|
!!this.audioTrackId ? this.audioTrackId : this.videoTrackId
|
|
|
|
);
|
|
|
|
this.response = JSON.stringify(egress, null, 4);
|
|
|
|
this.egressId = egress.egressId;
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async stopEgress() {
|
|
|
|
console.log('Stopping egress');
|
|
|
|
try {
|
|
|
|
await this.roomApiService.stopEgress(this.egressId);
|
|
|
|
this.response = 'Egress stopped';
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async listIngress() {
|
|
|
|
console.log('Listing ingress');
|
|
|
|
try {
|
|
|
|
const ingress = await this.roomApiService.listIngress();
|
|
|
|
this.response = JSON.stringify(ingress, null, 4);
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async createIngress() {
|
|
|
|
console.log('Creating ingress');
|
|
|
|
try {
|
|
|
|
const ingress = await this.roomApiService.createIngress(
|
|
|
|
this.ingressRoomName,
|
|
|
|
this.inputTypeSelected,
|
2025-02-03 15:35:41 +01:00
|
|
|
this.ingressUrlType,
|
2025-02-06 18:59:29 +01:00
|
|
|
this.ingressUrlUri,
|
2024-11-21 14:54:49 +01:00
|
|
|
this.ingressWithAudio,
|
|
|
|
this.ingressWithVideo,
|
|
|
|
this.ingressVideoCodecSelected,
|
|
|
|
this.ingressSimulcast,
|
2024-11-21 16:11:33 +01:00
|
|
|
this.ingressEnableTranscoding,
|
2024-11-21 14:54:49 +01:00
|
|
|
this.ingressVideoEncodingPresetSelected
|
|
|
|
);
|
|
|
|
this.response = JSON.stringify(ingress, null, 4);
|
|
|
|
this.ingressId = ingress.ingressId;
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
|
|
|
console.warn(error);
|
|
|
|
console.warn(error.code);
|
|
|
|
console.warn(error.msg);
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async deleteIngress() {
|
|
|
|
console.log('Deleting ingress');
|
|
|
|
try {
|
|
|
|
await this.roomApiService.deleteIngress(this.ingressId);
|
|
|
|
this.response = 'Ingress deleted';
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async deleteAllIngress() {
|
|
|
|
console.log('Deleting all ingress');
|
|
|
|
try {
|
|
|
|
const promises: Promise<IngressInfo>[] = [];
|
|
|
|
const ingresses = await this.roomApiService.listIngress();
|
|
|
|
ingresses.forEach((i) => {
|
|
|
|
promises.push(this.roomApiService.deleteIngress(i.ingressId));
|
|
|
|
});
|
|
|
|
await Promise.all(promises);
|
|
|
|
this.response = 'Deleted ' + promises.length + ' ingresses';
|
|
|
|
} catch (error: any) {
|
|
|
|
this.response = error;
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
2024-07-02 19:19:05 +02:00
|
|
|
|
2024-11-21 14:54:49 +01:00
|
|
|
addRtmpUrl(event: MatChipInputEvent): void {
|
|
|
|
const value = (event.value || '').trim();
|
|
|
|
if (value) {
|
|
|
|
this.rtmpUrls.push(value);
|
2024-11-18 13:59:43 +01:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
event.chipInput!.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
removeRtmpUrl(url: string): void {
|
|
|
|
const index = this.rtmpUrls.indexOf(url);
|
|
|
|
if (index >= 0) {
|
|
|
|
this.rtmpUrls.splice(index, 1);
|
|
|
|
this.announcer.announce(`Removed ${url}`);
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private getEncodedOutputs(): EncodedOutputs {
|
|
|
|
// this.s3Config.endpoint = this.s3Endpoint;
|
|
|
|
const encodedOutputs: EncodedOutputs = {};
|
|
|
|
if (this.fileOutputSelected) {
|
|
|
|
encodedOutputs.file = new EncodedFileOutput({
|
|
|
|
fileType: EncodedFileType.DEFAULT_FILETYPE,
|
|
|
|
filepath: 'room-composite-{room_id}-{room_name}-{time}',
|
|
|
|
});
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
if (this.streamOutputSelected) {
|
|
|
|
encodedOutputs.stream = new StreamOutput({
|
|
|
|
urls: this.rtmpUrls,
|
|
|
|
protocol: StreamProtocol.RTMP,
|
|
|
|
});
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|
2024-11-21 14:54:49 +01:00
|
|
|
if (this.segmentOutputSelected) {
|
|
|
|
encodedOutputs.segments = new SegmentedFileOutput({
|
|
|
|
protocol: SegmentedFileProtocol.HLS_PROTOCOL,
|
|
|
|
segmentDuration: this.segmentDuration,
|
|
|
|
// s3: this.s3Config
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return encodedOutputs;
|
|
|
|
}
|
2024-07-02 19:19:05 +02:00
|
|
|
}
|