diff --git a/openvidu-testapp/package.json b/openvidu-testapp/package.json
index a5b9d472..8380b829 100644
--- a/openvidu-testapp/package.json
+++ b/openvidu-testapp/package.json
@@ -21,6 +21,7 @@
"@angular/platform-browser": "^16.2.11",
"@angular/platform-browser-dynamic": "^16.2.11",
"@angular/router": "^16.2.11",
+ "@livekit/protocol": "^1.27.1",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"json-stringify-safe": "5.0.1",
diff --git a/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.html b/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.html
index 3d07f4d2..f2280846 100644
--- a/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.html
+++ b/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.html
@@ -23,10 +23,6 @@
-
-
-
@@ -53,6 +49,9 @@
Mute
+
+
+
@@ -79,10 +78,6 @@
-
-
-
@@ -184,6 +179,9 @@
+
+
+
diff --git a/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.ts
index 41c80a56..3ea9d7be 100644
--- a/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.ts
+++ b/openvidu-testapp/src/app/components/dialogs/room-api-dialog/room-api-dialog.component.ts
@@ -5,7 +5,7 @@ import { MatChipInputEvent } from '@angular/material/chips';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { LocalParticipant } from 'livekit-client';
-import { EgressClient, EncodedFileOutput, EncodedFileType, EncodedOutputs, IngressInput, Room, RoomCompositeOptions, RoomServiceClient, SegmentedFileOutput, SegmentedFileProtocol, StreamOutput, StreamProtocol } from 'livekit-server-sdk';
+import { DirectFileOutput, EgressClient, EncodedFileOutput, EncodedFileType, EncodedOutputs, IngressInfo, IngressInput, Room, RoomCompositeOptions, RoomServiceClient, SegmentedFileOutput, SegmentedFileProtocol, StreamOutput, StreamProtocol } from 'livekit-server-sdk';
import { RoomApiService } from 'src/app/services/room-api.service';
@Component({
@@ -44,11 +44,11 @@ export class RoomApiDialogComponent {
ingressRoomName: string;
ingressId: string;
- inputTypeSelected: IngressInput = IngressInput.RTMP_INPUT;
+ inputTypeSelected: IngressInput = IngressInput.URL_INPUT;
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' },
- { value: IngressInput.URL_INPUT, viewValue: 'URL' },
];
response: string;
@@ -89,16 +89,6 @@ export class RoomApiDialogComponent {
}
}
- async getRoom() {
- console.log('Getting room');
- try {
- const room = await this.roomApiService.getRoom(this.apiRoomName);
- this.response = JSON.stringify(room, null, 4);
- } catch (error: any) {
- this.response = 'Error [' + error.error.msg + ']';
- }
- }
-
async deleteRoom() {
console.log('Deleting room');
try {
@@ -110,6 +100,21 @@ export class RoomApiDialogComponent {
}
}
+ async deleteAllRooms() {
+ console.log('Deleting all rooms');
+ try {
+ const promises: Promise[] = [];
+ 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 [' + error.error.msg + ']';
+ }
+ }
+
async listParticipants() {
console.log('Listing participants');
try {
@@ -161,16 +166,6 @@ export class RoomApiDialogComponent {
}
}
- async getEgress() {
- console.log('Getting egress');
- try {
- const egress = await this.roomApiService.getEgress(this.egressId);
- this.response = JSON.stringify(egress, null, 4);
- } catch (error: any) {
- this.response = 'Error [' + error.error.msg + ']';
- }
- }
-
async startRoomCompositeEgress() {
console.log('Starting room composite egress');
try {
@@ -182,7 +177,7 @@ export class RoomApiDialogComponent {
}
const egress = await this.roomApiService.startRoomCompositeEgress(this.egressRoomName, roomCompositeOptions, encodedOutputs);
this.response = JSON.stringify(egress, null, 4);
- this.egressId = egress.egress_id;
+ this.egressId = egress.egressId;
} catch (error: any) {
this.response = 'Error [' + error.error.msg + ']';
}
@@ -194,7 +189,7 @@ export class RoomApiDialogComponent {
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.egress_id;
+ this.egressId = egress.egressId;
} catch (error: any) {
this.response = 'Error [' + error.error.msg + ']';
}
@@ -203,10 +198,9 @@ export class RoomApiDialogComponent {
async startTrackEgress() {
console.log('Starting track egress');
try {
- const encodedOutputs = this.getEncodedOutputs();
- const egress = await this.roomApiService.startTrackEgress(this.egressRoomName, !!this.audioTrackId ? this.audioTrackId : this.videoTrackId, encodedOutputs);
+ const egress = await this.roomApiService.startTrackEgress(this.egressRoomName, !!this.audioTrackId ? this.audioTrackId : this.videoTrackId);
this.response = JSON.stringify(egress, null, 4);
- this.egressId = egress.egress_id;
+ this.egressId = egress.egressId;
} catch (error: any) {
this.response = 'Error [' + error.error.msg + ']';
}
@@ -237,7 +231,7 @@ export class RoomApiDialogComponent {
try {
const ingress = await this.roomApiService.createIngress(this.ingressRoomName, this.inputTypeSelected);
this.response = JSON.stringify(ingress, null, 4);
- this.ingressId = ingress.ingress_id;
+ this.ingressId = ingress.ingressId;
} catch (error: any) {
this.response = 'Error [' + error.error.msg + ']';
}
@@ -253,6 +247,21 @@ export class RoomApiDialogComponent {
}
}
+ async deleteAllIngress() {
+ console.log('Deleting all ingress');
+ try {
+ const promises: Promise[] = [];
+ 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 [' + error.error.msg + ']';
+ }
+ }
+
addRtmpUrl(event: MatChipInputEvent): void {
const value = (event.value || '').trim();
if (value) {
diff --git a/openvidu-testapp/src/app/services/room-api.service.ts b/openvidu-testapp/src/app/services/room-api.service.ts
index a8feca66..538ce548 100644
--- a/openvidu-testapp/src/app/services/room-api.service.ts
+++ b/openvidu-testapp/src/app/services/room-api.service.ts
@@ -1,202 +1,267 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
-import { firstValueFrom } from 'rxjs';
import * as _ from 'lodash';
-import { AccessToken, EncodedOutputs, EncodingOptions, EncodingOptionsPreset, IngressInput, RoomCompositeOptions, VideoGrant } from 'livekit-server-sdk';
+import {
+ AccessToken,
+ CreateIngressOptions,
+ DirectFileOutput,
+ EgressClient,
+ EgressInfo,
+ EncodedOutputs,
+ EncodingOptions,
+ EncodingOptionsPreset,
+ IngressClient,
+ IngressInfo,
+ IngressInput,
+ IngressVideoEncodingPreset,
+ ParticipantInfo,
+ Room,
+ RoomCompositeOptions,
+ RoomServiceClient,
+ TrackCompositeOptions,
+ TrackInfo,
+ TrackSource,
+ VideoGrant,
+} from 'livekit-server-sdk';
import { LivekitParamsService } from './livekit-params.service';
@Injectable({
- providedIn: 'root'
+ providedIn: 'root',
})
export class RoomApiService {
+ private roomServiceClient: RoomServiceClient;
+ private egressClient: EgressClient;
+ private ingressClient: IngressClient;
- private ADMIN_PERMISSIONS: VideoGrant = {
- roomCreate: true,
- roomList: true,
- roomRecord: true,
- roomAdmin: true,
- ingressAdmin: true,
- canPublish: true,
- canSubscribe: true,
- canPublishData: true,
- canUpdateOwnMetadata: true,
+ constructor(
+ private http: HttpClient,
+ private livekitParamsService: LivekitParamsService
+ ) {
+ this.roomServiceClient = new RoomServiceClient(
+ this.getRestUrl(),
+ this.livekitParamsService.getParams().livekitApiKey,
+ this.livekitParamsService.getParams().livekitApiSecret
+ );
+ this.egressClient = new EgressClient(
+ this.getRestUrl(),
+ this.livekitParamsService.getParams().livekitApiKey,
+ this.livekitParamsService.getParams().livekitApiSecret
+ );
+ this.ingressClient = new IngressClient(
+ this.getRestUrl(),
+ this.livekitParamsService.getParams().livekitApiKey,
+ this.livekitParamsService.getParams().livekitApiSecret
+ );
+ }
+
+ async createToken(
+ permissions: VideoGrant,
+ participantName?: string,
+ roomName?: string
+ ): Promise {
+ const at = new AccessToken(
+ this.livekitParamsService.getParams().livekitApiKey,
+ this.livekitParamsService.getParams().livekitApiSecret,
+ { identity: participantName }
+ );
+ if (roomName) {
+ permissions.room = roomName;
+ }
+ at.addGrant(permissions);
+ return at.toJwt();
+ }
+
+ /*
+ * RoomService API
+ * https://docs.livekit.io/reference/server/server-apis/
+ */
+
+ async listRooms(): Promise {
+ return this.roomServiceClient.listRooms();
+ }
+
+ async deleteRoom(roomName: string): Promise {
+ return await this.roomServiceClient.deleteRoom(roomName);
+ }
+
+ async listParticipants(roomName: string): Promise {
+ return await this.roomServiceClient.listParticipants(roomName);
+ }
+
+ async getParticipant(
+ roomName: string,
+ participantIdentity: string
+ ): Promise {
+ return await this.roomServiceClient.getParticipant(
+ roomName,
+ participantIdentity
+ );
+ }
+
+ async removeParticipant(
+ roomName: string,
+ participantIdentity: string
+ ): Promise {
+ return await this.roomServiceClient.removeParticipant(
+ roomName,
+ participantIdentity
+ );
+ }
+
+ async mutePublishedTrack(
+ roomName: string,
+ participantIdentity: string,
+ track_sid: string,
+ muted: boolean
+ ): Promise {
+ return await this.roomServiceClient.mutePublishedTrack(
+ roomName,
+ participantIdentity,
+ track_sid,
+ muted
+ );
+ }
+
+ async listEgress(): Promise {
+ return await this.egressClient.listEgress({});
+ }
+
+ async startRoomCompositeEgress(
+ roomName: string,
+ roomCompositeOptions: RoomCompositeOptions,
+ encodedOutputs: EncodedOutputs,
+ encodingOptions?: EncodingOptionsPreset | EncodingOptions
+ ): Promise {
+ if (encodedOutputs.file) {
+ encodedOutputs.file.filepath = 'RoomComposite-{room_id}-{room_name}-{time}';
+ }
+ if (encodingOptions) {
+ roomCompositeOptions.encodingOptions = encodingOptions;
+ }
+ return await this.egressClient.startRoomCompositeEgress(
+ roomName,
+ encodedOutputs,
+ roomCompositeOptions
+ );
+ }
+
+ async startTrackCompositeEgress(
+ roomName: string,
+ audioTrackId: string,
+ videoTrackId: string,
+ encodedOutputs: EncodedOutputs,
+ encodingOptions?: EncodingOptionsPreset | EncodingOptions
+ ): Promise {
+ if (encodedOutputs.file) {
+ encodedOutputs.file.filepath =
+ 'TrackComposite-{room_id}-{room_name}-{time}-{publisher_identity}';
+ }
+ const trackCompositeOptions: TrackCompositeOptions = {
+ audioTrackId,
+ videoTrackId,
};
-
- constructor(private http: HttpClient, private livekitParamsService: LivekitParamsService) { }
-
- async createToken(permissions: VideoGrant, participantName?: string, roomName?: string): Promise {
- const at = new AccessToken(this.livekitParamsService.getParams().livekitApiKey, this.livekitParamsService.getParams().livekitApiSecret, { identity: participantName });
- if (roomName) {
- permissions.room = roomName;
- }
- at.addGrant(permissions);
- return at.toJwt();
+ if (encodingOptions) {
+ trackCompositeOptions.encodingOptions = encodingOptions;
}
+ return await this.egressClient.startTrackCompositeEgress(
+ roomName,
+ encodedOutputs,
+ trackCompositeOptions
+ );
+ }
- private globalAdminToken(): Promise {
- return this.createToken(this.ADMIN_PERMISSIONS, 'GLOBAL_ADMIN', undefined);
+ async startTrackEgress(
+ roomName: string,
+ track_id: string,
+ output?: DirectFileOutput | string
+ ): Promise {
+ if (!output) {
+ let outputAux = {
+ filepath:
+ 'Track-{room_id}-{room_name}-{time}-{publisher_identity}-{track_id}-{track_type}-{track_source}',
+ };
+ output = outputAux as DirectFileOutput;
}
+ return await this.egressClient.startTrackEgress(roomName, output, track_id);
+ }
- private roomAdminToken(roomName: string): Promise {
- return this.createToken(this.ADMIN_PERMISSIONS, 'ROOM_ADMIN', roomName);
- }
+ async stopEgress(egressId: string): Promise {
+ return await this.egressClient.stopEgress(egressId);
+ }
- /*
- * RoomService API
- * https://docs.livekit.io/reference/server/server-apis/
- */
+ async listIngress(): Promise {
+ const ingressClient: IngressClient = new IngressClient(
+ this.getRestUrl(),
+ this.livekitParamsService.getParams().livekitApiKey,
+ this.livekitParamsService.getParams().livekitApiSecret
+ );
+ return await ingressClient.listIngress({});
+ }
- async listRooms() {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('RoomService', 'ListRooms'), {}, this.getRestOptions(token)));
- }
+ async createIngress(
+ room_name: string,
+ input_type: IngressInput
+ ): Promise {
+ const ingressClient: IngressClient = new IngressClient(
+ this.getRestUrl(),
+ this.livekitParamsService.getParams().livekitApiKey,
+ this.livekitParamsService.getParams().livekitApiSecret
+ );
+ let options: CreateIngressOptions = {
+ name: input_type + '-' + room_name,
+ roomName: room_name,
+ participantIdentity: 'IngressParticipantIdentity',
+ participantName: 'MyIngress',
+ participantMetadata: 'IngressParticipantMetadata',
+ url: 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
+ // video: {
+ // encodingOptions: {
+ // video_codec: VideoCodec.VP8,
+ // frame_rate: 30,
+ // layers: [
+ // {
+ // quality: VideoQuality.HIGH,
+ // width: 1920,
+ // height: 1080,
+ // bitrate: 4500000,
+ // },
+ // ],
+ // },
+ // } as any,
+ video: {
+ name: 'pelicula',
+ source: TrackSource.SCREEN_SHARE,
+ encodingOptions: {
+ case: 'preset',
+ value:
+ IngressVideoEncodingPreset.H264_540P_25FPS_2_LAYERS_HIGH_MOTION,
+ },
+ } as any,
+ // audio: {
+ // source: TrackSource.MICROPHONE,
+ // preset: IngressAudioEncodingPreset.OPUS_MONO_64KBS,
+ // } as any,
+ };
+ const ingressInfo = await ingressClient.createIngress(input_type, options);
+ return ingressInfo;
+ }
- async getRoom(roomName: string) {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('RoomService', 'ListRooms'), { names: [roomName] }, this.getRestOptions(token)));
- }
+ async deleteIngress(ingressId: string): Promise {
+ const ingressClient: IngressClient = new IngressClient(
+ this.getRestUrl(),
+ this.livekitParamsService.getParams().livekitApiKey,
+ this.livekitParamsService.getParams().livekitApiSecret
+ );
+ return await ingressClient.deleteIngress(ingressId);
+ }
- async deleteRoom(roomName: string) {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('RoomService', 'DeleteRoom'), { room: roomName }, this.getRestOptions(token)));
- }
-
- async listParticipants(roomName: string) {
- const token = await this.roomAdminToken(roomName);
- return await firstValueFrom(this.http.post(this.getUrl('RoomService', 'ListParticipants'), { room: roomName }, this.getRestOptions(token)));
- }
-
- async getParticipant(roomName: string, participantIdentity: string) {
- const token = await this.roomAdminToken(roomName);
- return await firstValueFrom(this.http.post(this.getUrl('RoomService', 'GetParticipant'), { room: roomName, identity: participantIdentity }, this.getRestOptions(token)));
- }
-
- async removeParticipant(roomName: string, participantIdentity: string) {
- const token = await this.roomAdminToken(roomName);
- return await firstValueFrom(this.http.post(this.getUrl('RoomService', 'RemoveParticipant'), { room: roomName, identity: participantIdentity }, this.getRestOptions(token)));
- }
-
- async mutePublishedTrack(roomName: string, participantIdentity: string, track_sid: string, muted: boolean) {
- const token = await this.roomAdminToken(roomName);
- return await firstValueFrom(this.http.post(this.getUrl('RoomService', 'MutePublishedTrack'), { room: roomName, identity: participantIdentity, track_sid, muted }, this.getRestOptions(token)));
- }
-
- /**
- * Egress API
- * https://docs.livekit.io/egress-ingress/egress/overview/#api
- */
-
- async listEgress() {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('Egress', 'ListEgress'), {}, this.getRestOptions(token)));
- }
-
- async getEgress(egress_id: string) {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('Egress', 'ListEgress'), { egress_id }, this.getRestOptions(token)));
- }
-
- // {room_id} {room_name} {time}
- async startRoomCompositeEgress(room_name: string, compositeOptions: RoomCompositeOptions, encodedOutputs: EncodedOutputs, encodingOptions?: EncodingOptionsPreset | EncodingOptions) {
- const token = await this.globalAdminToken();
- if (encodedOutputs.file) {
- encodedOutputs.file.filepath = 'track-{room_id}-{room_name}-{time}';
- }
- return await firstValueFrom(this.http.post(this.getUrl('Egress', 'StartRoomCompositeEgress'), {
- room_name,
- layout: compositeOptions?.layout,
- audio_only: compositeOptions?.audioOnly,
- video_only: compositeOptions?.videoOnly,
- custom_base_url: compositeOptions?.customBaseUrl,
- file_outputs: encodedOutputs?.file ? [encodedOutputs?.file] : [],
- segment_outputs: encodedOutputs?.segments ? [encodedOutputs?.segments] : [],
- stream_outputs: encodedOutputs?.stream ? [encodedOutputs?.stream] : [],
- preset: encodingOptions
- }, this.getRestOptions(token)));
- }
-
- // {room_id} {room_name} {time} {publisher_identity}
- async startTrackCompositeEgress(room_name: string, audio_track_id: string, video_track_id: string, encodedOutputs: EncodedOutputs, encodingOptions?: EncodingOptionsPreset | EncodingOptions) {
- const token = await this.globalAdminToken();
- if (encodedOutputs.file) {
- encodedOutputs.file.filepath = 'track-{room_id}-{room_name}-{time}-{publisher_identity}';
- }
- return await firstValueFrom(this.http.post(this.getUrl('Egress', 'StartTrackCompositeEgress'), {
- room_name,
- audio_track_id,
- video_track_id,
- file_outputs: encodedOutputs?.file ? [encodedOutputs?.file] : [],
- segment_outputs: encodedOutputs?.segments ? [encodedOutputs?.segments] : [],
- stream_outputs: encodedOutputs?.stream ? [encodedOutputs?.stream] : [],
- preset: encodingOptions
- }, this.getRestOptions(token)));
- }
-
- // {room_id} {room_name} {time} {publisher_identity} {track_id} {track_type} {track_source}
- async startTrackEgress(room_name: string, track_id: string, encodedOutputs: EncodedOutputs) {
- const token = await this.roomAdminToken(room_name);
- if (encodedOutputs.file) {
- encodedOutputs.file.filepath = 'track-{room_id}-{room_name}-{time}-{publisher_identity}-{track_id}-{track_type}-{track_source}';
- }
- return await firstValueFrom(this.http.post(this.getUrl('Egress', 'StartTrackEgress'), {
- room_name,
- track_id,
- file: encodedOutputs?.file,
- // websocket_url: ""
- }, this.getRestOptions(token)));
- }
-
- async stopEgress(egress_id: string) {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('Egress', 'StopEgress'), { egress_id }, this.getRestOptions(token)));
- }
-
- /**
- * Ingress API
- * https://docs.livekit.io/egress-ingress/ingress/overview/#api
- */
-
- async listIngress() {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('Ingress', 'ListIngress'), {}, this.getRestOptions(token)));
- }
-
- async createIngress(room_name: string, input_type: IngressInput) {
- const token = await this.roomAdminToken(room_name);
- let options = {
- room_name,
- input_type,
- name: 'MyIngress',
- participant_identity: 'IngressParticipantIdentity',
- participant_name: 'IngressParticipantName',
- url: 'http://playertest.longtailvideo.com/adaptive/wowzaid3/playlist.m3u8'
- }
- if (input_type === IngressInput.WHIP_INPUT) {
- (options as any).bypass_transcoding = true;
- }
- return await firstValueFrom(this.http.post(this.getUrl('Ingress', 'CreateIngress'), options, this.getRestOptions(token)));
- }
-
- async deleteIngress(ingress_id: string) {
- const token = await this.globalAdminToken();
- return await firstValueFrom(this.http.post(this.getUrl('Ingress', 'DeleteIngress'), { ingress_id }, this.getRestOptions(token)));
- }
-
- private getUrl(endpoint: string, method: string) {
- const wsUrl = this.livekitParamsService.getParams().livekitUrl;
- const protocol = (wsUrl.startsWith('wss:') || wsUrl.startsWith('https:')) ? 'https' : 'http';
- const restUrl = `${protocol}://${wsUrl.substring(wsUrl.indexOf('//') + 2).replace(/\/$/, "")}`;
- return `${restUrl}/twirp/livekit.${endpoint}/${method}`;
- }
-
- private getRestOptions(token: string) {
- return {
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': 'Bearer ' + token
- }
- }
- }
-
-}
\ No newline at end of file
+ private getRestUrl() {
+ const wsUrl = this.livekitParamsService.getParams().livekitUrl;
+ const protocol =
+ wsUrl.startsWith('wss:') || wsUrl.startsWith('https:') ? 'https' : 'http';
+ return `${protocol}://${wsUrl
+ .substring(wsUrl.indexOf('//') + 2)
+ .replace(/\/$/, '')}`;
+ }
+}