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(/\/$/, '')}`; + } +}