From 90fd0ef44edf7236cb613c5fa6ed32bfce77da02 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Fri, 14 Mar 2025 18:55:01 +0100 Subject: [PATCH] ov-components: add recordingStreamBaseUrl directive and integrate with config service for dynamic stream URL construction --- .../directives/api/api.directive.module.ts | 3 + .../api/videoconference.directive.ts | 66 +++++++++++++++++++ .../config/directive-config.service.ts | 15 +++++ .../services/recording/recording.service.ts | 18 +++-- .../src/app/openvidu-call/call.component.html | 1 + 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts index ee9d0cdf..ea56db2d 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/api.directive.module.ts @@ -36,6 +36,7 @@ import { MinimalDirective, ParticipantNameDirective, PrejoinDirective, + RecordingStreamBaseUrlDirective, TokenDirective, TokenErrorDirective, VideoEnabledDirective @@ -56,6 +57,7 @@ import { PrejoinDisplayParticipantName, VideoEnabledDirective, AudioEnabledDirective, + RecordingStreamBaseUrlDirective, ToolbarCameraButtonDirective, ToolbarMicrophoneButtonDirective, ToolbarScreenshareButtonDirective, @@ -100,6 +102,7 @@ import { PrejoinDisplayParticipantName, VideoEnabledDirective, AudioEnabledDirective, + RecordingStreamBaseUrlDirective, ToolbarCameraButtonDirective, ToolbarMicrophoneButtonDirective, ToolbarScreenshareButtonDirective, diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts index bc5e2079..eaff5c44 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/directives/api/videoconference.directive.ts @@ -725,3 +725,69 @@ export class AudioEnabledDirective implements OnDestroy { } } } + +/** + * The **recordingStreamBaseUrl** directive sets the base URL for retrieving recording streams. + * The complete request URL is dynamically constructed by concatenating the supplied URL, the + * internally managed recordingId, and the `/stream` segment. + * + * The final URL format will be: + * + * {recordingStreamBaseUrl}/{recordingId}/stream + * + * Default: `"/{recordingId}/stream"` + * + * It is essential that the resulting route is declared and configured on your backend, as it is + * used for serving and accessing the recording streams. + * + * @example + * + * + */ +@Directive({ + selector: 'ov-videoconference[recordingStreamBaseUrl]' +}) +export class RecordingStreamBaseUrlDirective implements AfterViewInit, OnDestroy { + /** + * @ignore + */ + @Input() set recordingStreamBaseUrl(url: string) { + this.update(url); + } + + /** + * @ignore + */ + constructor( + private elementRef: ElementRef, + private libService: OpenViduComponentsConfigService + ) {} + + /** + * @ignore + */ + ngAfterViewInit(): void { + this.update(this.recordingStreamBaseUrl); + } + + /** + * @ignore + */ + ngOnDestroy(): void { + this.clear(); + } + + /** + * @ignore + */ + clear() { + this.update(''); + } + + /** + * @ignore + */ + update(value: string) { + if (value) this.libService.setRecordingStreamBaseUrl(value); + } +} diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts index 20f85e24..8eda896c 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/config/directive-config.service.ts @@ -33,6 +33,9 @@ export class OpenViduComponentsConfigService { private audioEnabled = >new BehaviorSubject(true); audioEnabled$: Observable; + private recordingStreamBaseUrl = >new BehaviorSubject(''); + recordingStreamBaseUrl$: Observable; + //Toolbar settings private cameraButton = >new BehaviorSubject(true); cameraButton$: Observable; @@ -121,6 +124,7 @@ export class OpenViduComponentsConfigService { this.prejoinDisplayParticipantName$ = this.prejoinDisplayParticipantName.asObservable(); this.videoEnabled$ = this.videoEnabled.asObservable(); this.audioEnabled$ = this.audioEnabled.asObservable(); + this.recordingStreamBaseUrl$ = this.recordingStreamBaseUrl.asObservable(); //Toolbar observables this.cameraButton$ = this.cameraButton.asObservable(); this.microphoneButton$ = this.microphoneButton.asObservable(); @@ -214,6 +218,17 @@ export class OpenViduComponentsConfigService { return this.audioEnabled.getValue(); } + setRecordingStreamBaseUrl(recordingStreamBaseUrl: string) { + this.recordingStreamBaseUrl.next(recordingStreamBaseUrl); + } + + getRecordingStreamBaseUrl(): string { + let baseUrl = this.recordingStreamBaseUrl.getValue(); + // Add trailing slash if not present + baseUrl += baseUrl.endsWith('/') ? '' : '/'; + return baseUrl; + } + //Toolbar settings setCameraButton(cameraButton: boolean) { diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/recording/recording.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/recording/recording.service.ts index 46fa345f..acc83a60 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/recording/recording.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/recording/recording.service.ts @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { RecordingInfo, RecordingStatus, RecordingStatusInfo } from '../../models/recording.model'; import { ActionService } from '../action/action.service'; -import { GlobalConfigService } from '../config/global-config.service'; +import { LoggerService } from '../logger/logger.service'; +import { ILogger } from '../../models/logger.model'; +import { OpenViduComponentsConfigService } from '../config/directive-config.service'; @Injectable({ providedIn: 'root' @@ -13,19 +15,19 @@ export class RecordingService { */ recordingStatusObs: Observable; private recordingTimeInterval: NodeJS.Timeout; - private API_RECORDINGS_PREFIX = 'call/api/recordings/'; private recordingStatus = >new BehaviorSubject({ status: RecordingStatus.STOPPED, recordingList: [] as RecordingInfo[], recordingElapsedTime: new Date(0, 0, 0, 0, 0, 0, 0) }); + private log: ILogger; /** * @internal */ - constructor(private actionService: ActionService, private globalService: GlobalConfigService) { + constructor(private actionService: ActionService, private libService: OpenViduComponentsConfigService, private loggerService: LoggerService) { + this.log = this.loggerService.get('RecordingService'); this.recordingStatusObs = this.recordingStatus.asObservable(); - this.API_RECORDINGS_PREFIX = this.globalService.getBaseHref() + this.API_RECORDINGS_PREFIX; } /** @@ -146,9 +148,10 @@ export class RecordingService { */ playRecording(recording: RecordingInfo) { // Only COMPOSED recording is supported. The extension will allways be 'mp4'. - console.log('Playing recording', recording); + this.log.d('Playing recording', recording); const queryParamForAvoidCache = `?t=${new Date().getTime()}`; - const streamRecordingUrl = `${this.API_RECORDINGS_PREFIX}${recording.id}/stream${queryParamForAvoidCache}`; + const baseUrl = this.libService.getRecordingStreamBaseUrl(); + const streamRecordingUrl = `${baseUrl}${recording.id}/stream${queryParamForAvoidCache}`; this.actionService.openRecordingPlayerDialog(streamRecordingUrl); } @@ -161,7 +164,8 @@ export class RecordingService { // Only COMPOSED recording is supported. The extension will allways be 'mp4'. const queryParamForAvoidCache = `?t=${new Date().getTime()}`; const link = document.createElement('a'); - link.href = `${this.API_RECORDINGS_PREFIX}${recording.id}/stream${queryParamForAvoidCache}`; + const baseUrl = this.libService.getRecordingStreamBaseUrl(); + link.href = `${baseUrl}${recording.id}/stream${queryParamForAvoidCache}`; link.download = recording.filename || 'openvidu-recording.mp4'; link.dispatchEvent( new MouseEvent('click', { diff --git a/openvidu-components-angular/src/app/openvidu-call/call.component.html b/openvidu-components-angular/src/app/openvidu-call/call.component.html index b265f8b4..23068389 100644 --- a/openvidu-components-angular/src/app/openvidu-call/call.component.html +++ b/openvidu-components-angular/src/app/openvidu-call/call.component.html @@ -11,6 +11,7 @@ [participantName]="'Participant'" [videoEnabled]="true" [audioEnabled]="true" + [recordingStreamBaseUrl]="'call/api/recordings'" [toolbarCameraButton]="true" [toolbarMicrophoneButton]="true" [toolbarScreenshareButton]="true"