ov-components: implement read-only mode and customizable controls for recording activity

master
Carlos Santos 2025-07-18 12:08:49 +02:00
parent 98c7e3f751
commit b659400c88
21 changed files with 686 additions and 185 deletions

View File

@ -17,6 +17,8 @@
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)" (onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)" (onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)" (onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
></ov-recording-activity> ></ov-recording-activity>
<ov-broadcasting-activity <ov-broadcasting-activity
*ngIf="showBroadcastingActivity" *ngIf="showBroadcastingActivity"

View File

@ -54,6 +54,21 @@ export class ActivitiesPanelComponent implements OnInit {
*/ */
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>(); @Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
/**
* @internal
* Provides event notifications that fire when view recordings button has been clicked.
* This event is triggered when the user wants to view all recordings in an external page.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* @internal
* Provides event notifications that fire when view recording button has been clicked.
* This event is triggered when the user wants to view a specific recording in an external page.
* It provides the recording ID as event data.
*/
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/** /**
* Provides event notifications that fire when start broadcasting button is clicked. * Provides event notifications that fire when start broadcasting button is clicked.
* It provides the {@link BroadcastingStartRequestedEvent} payload as event data. * It provides the {@link BroadcastingStartRequestedEvent} payload as event data.

View File

@ -27,7 +27,10 @@
<mat-icon class="blink" *ngIf="recordingStatus === recStatusEnum.STARTED">radio_button_checked</mat-icon> <mat-icon class="blink" *ngIf="recordingStatus === recStatusEnum.STARTED">radio_button_checked</mat-icon>
</div> </div>
<h3 matListItemTitle class="activity-title">{{ 'PANEL.RECORDING.TITLE' | translate }}</h3> <h3 matListItemTitle class="activity-title">{{ 'PANEL.RECORDING.TITLE' | translate }}</h3>
<p matListItemLine class="activity-subtitle">{{ 'PANEL.RECORDING.SUBTITLE' | translate }}</p>
<p matListItemLine class="activity-subtitle">
{{ isReadOnlyMode ? ('PANEL.RECORDING.VIEW_ONLY_SUBTITLE' | translate) : ('PANEL.RECORDING.SUBTITLE' | translate) }}
</p>
<div class="activity-action-buttons" matListItemMeta> <div class="activity-action-buttons" matListItemMeta>
<div <div
id="recording-status" id="recording-status"
@ -56,53 +59,85 @@
<!-- Empty state content --> <!-- Empty state content -->
<div *ngIf="recordingList.length === 0" class="empty-state"> <div *ngIf="recordingList.length === 0" class="empty-state">
<h2 class="recording-title">{{ 'PANEL.RECORDING.CONTENT_TITLE' | translate }}</h2> <h2 class="recording-title">
<span class="recording-subtitle">{{ 'PANEL.RECORDING.CONTENT_SUBTITLE' | translate }}</span> {{
isReadOnlyMode
? ('PANEL.RECORDING.VIEW_ONLY_CONTENT_TITLE' | translate)
: ('PANEL.RECORDING.CONTENT_TITLE' | translate)
}}
</h2>
<span class="recording-subtitle">
{{
isReadOnlyMode
? recordingList.length === 0
? ('PANEL.RECORDING.NO_RECORDINGS_AVAILABLE' | translate)
: ('PANEL.RECORDING.VIEW_ONLY_CONTENT_SUBTITLE' | translate)
: ('PANEL.RECORDING.CONTENT_SUBTITLE' | translate)
}}
</span>
</div> </div>
<!-- Recording control buttons --> <!-- Recording control buttons -->
<div class="item recording-action-buttons"> @if (!isReadOnlyMode) {
<!-- Stop recording button --> <div class="item recording-action-buttons">
<button *ngIf="recordingAlive" mat-flat-button id="stop-recording-btn" (click)="stopRecording()"> <!-- Stop recording button -->
<span>{{ 'TOOLBAR.STOP_RECORDING' | translate }}</span> <button *ngIf="recordingAlive" mat-flat-button id="stop-recording-btn" (click)="stopRecording()">
</button> <span>{{ 'TOOLBAR.STOP_RECORDING' | translate }}</span>
<!-- Start recording button -->
<div
*ngIf="recordingStatus === recStatusEnum.STOPPED"
[matTooltip]="!hasRoomTracksPublished ? ('PANEL.RECORDING.NO_TRACKS_PUBLISHED' | translate) : ''"
[matTooltipDisabled]="hasRoomTracksPublished"
class="start-recording-button-container"
>
<button
[disabled]="!hasRoomTracksPublished"
[ngClass]="{ 'disable-recording-btn': !hasRoomTracksPublished }"
mat-flat-button
id="start-recording-btn"
(click)="startRecording()"
>
<span>{{ 'TOOLBAR.START_RECORDING' | translate }}</span>
</button> </button>
</div>
<!-- Recording status messages --> <!-- Start recording button -->
<div class="recording-status-messages"> <div
<span *ngIf="recordingStatus === recStatusEnum.STARTING" class="recording-message"> *ngIf="recordingStatus === recStatusEnum.STOPPED"
{{ 'PANEL.RECORDING.STARTING' | translate }} [matTooltip]="!hasRoomTracksPublished ? ('PANEL.RECORDING.NO_TRACKS_PUBLISHED' | translate) : ''"
</span> [matTooltipDisabled]="hasRoomTracksPublished"
class="start-recording-button-container"
<span *ngIf="recordingStatus === recStatusEnum.STOPPING" class="recording-message"> >
{{ 'PANEL.RECORDING.STOPPING' | translate }} <button
</span> [disabled]="!hasRoomTracksPublished"
[ngClass]="{ 'disable-recording-btn': !hasRoomTracksPublished }"
<div *ngIf="recordingStatus === recStatusEnum.FAILED" class="recording-error-container"> mat-flat-button
<span class="recording-error">Message: {{ recordingError }}</span> id="start-recording-btn"
<button mat-flat-button id="reset-recording-status-btn" (click)="resetStatus()"> (click)="startRecording()"
<span>{{ 'PANEL.RECORDING.ACCEPT' | translate }}</span> >
<span>{{ 'TOOLBAR.START_RECORDING' | translate }}</span>
</button> </button>
</div> </div>
<!-- View all recordings button -->
<div class="item recording-action-buttons">
<button mat-flat-button id="view-recordings-btn" (click)="viewAllRecordings()" class="view-recordings-button">
<span>{{ 'TOOLBAR.VIEW_RECORDINGS' | translate }}</span>
<mat-icon class="external-link-icon">open_in_new</mat-icon>
</button>
</div>
<!-- Recording status messages -->
<div class="recording-status-messages">
<span *ngIf="recordingStatus === recStatusEnum.STARTING" class="recording-message">
{{ 'PANEL.RECORDING.STARTING' | translate }}
</span>
<span *ngIf="recordingStatus === recStatusEnum.STOPPING" class="recording-message">
{{ 'PANEL.RECORDING.STOPPING' | translate }}
</span>
<div *ngIf="recordingStatus === recStatusEnum.FAILED" class="recording-error-container">
<span class="recording-error">Message: {{ recordingError }}</span>
<button mat-flat-button id="reset-recording-status-btn" (click)="resetStatus()">
<span>{{ 'PANEL.RECORDING.ACCEPT' | translate }}</span>
</button>
</div>
</div>
</div> </div>
</div> } @else {
<!-- View all recordings button -->
<div class="item recording-action-buttons">
<button mat-flat-button id="view-recordings-btn" (click)="viewAllRecordings()" class="view-recordings-button">
<span>{{ 'TOOLBAR.VIEW_RECORDINGS' | translate }}</span>
<mat-icon class="external-link-icon">open_in_new</mat-icon>
</button>
</div>
}
</div> </div>
<mat-divider *ngIf="recordingList.length > 0"></mat-divider> <mat-divider *ngIf="recordingList.length > 0"></mat-divider>
@ -139,37 +174,72 @@
</div> </div>
<!-- Recording action buttons --> <!-- Recording action buttons -->
<div *ngIf="recording.status !== recStatusEnum.STARTED" id="recording-action-buttons" class="recording-actions"> @if (!isReadOnlyMode) {
<button <div *ngIf="recording.status !== recStatusEnum.STARTED" id="recording-action-buttons" class="recording-actions">
mat-icon-button <button
(click)="play(recording)" *ngIf="showControls.play"
id="play-recording-btn" mat-icon-button
matTooltip="{{ 'PANEL.RECORDING.PLAY' | translate }}" (click)="play(recording)"
class="action-button play-button" id="play-recording-btn"
> matTooltip="{{ 'PANEL.RECORDING.PLAY' | translate }}"
<mat-icon>play_arrow</mat-icon> class="action-button play-button"
</button> >
<mat-icon>play_arrow</mat-icon>
</button>
<button @if (showControls.externalView) {
mat-icon-button <div
(click)="download(recording)" *ngIf="recording.status !== recStatusEnum.STARTED"
id="download-recording-btn" id="recording-action-buttons"
matTooltip="{{ 'PANEL.RECORDING.DOWNLOAD' | translate }}" class="recording-actions"
class="action-button download-button" >
> <button
<mat-icon>download</mat-icon> mat-icon-button
</button> (click)="onViewRecordingClicked.emit(recording.id)"
id="watch-recording-btn"
matTooltip="{{ 'PANEL.RECORDING.WATCH' | translate }}"
class="action-button watch-button"
>
<mat-icon>open_in_new</mat-icon>
</button>
</div>
}
<button <button
mat-icon-button *ngIf="showControls.download"
(click)="deleteRecording(recording)" mat-icon-button
id="delete-recording-btn" (click)="download(recording)"
matTooltip="{{ 'PANEL.RECORDING.DELETE' | translate }}" id="download-recording-btn"
class="action-button delete-button" matTooltip="{{ 'PANEL.RECORDING.DOWNLOAD' | translate }}"
> class="action-button download-button"
<mat-icon>delete</mat-icon> >
</button> <mat-icon>download</mat-icon>
</div> </button>
<button
*ngIf="showControls.delete"
mat-icon-button
(click)="deleteRecording(recording)"
id="delete-recording-btn"
matTooltip="{{ 'PANEL.RECORDING.DELETE' | translate }}"
class="action-button delete-button"
>
<mat-icon>delete</mat-icon>
</button>
</div>
} @else {
<div *ngIf="recording.status !== recStatusEnum.STARTED" id="recording-action-buttons" class="recording-actions">
<button
mat-icon-button
(click)="onViewRecordingClicked.emit(recording.id)"
id="watch-recording-btn"
matTooltip="{{ 'PANEL.RECORDING.WATCH' | translate }}"
class="action-button watch-button"
>
<mat-icon>open_in_new</mat-icon>
</button>
</div>
}
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</div> </div>

View File

@ -145,8 +145,7 @@
} }
.recording-action-buttons { .recording-action-buttons {
margin-top: 20px; margin: 5px 0px;
margin-bottom: 20px;
} }
#start-recording-btn { #start-recording-btn {
@ -155,6 +154,17 @@
color: var(--ov-secondary-action-color); color: var(--ov-secondary-action-color);
} }
#view-recordings-btn {
width: 100%;
background-color: var(--ov-accent-action-color);
color: var(--ov-secondary-action-color);
margin-bottom: 10px;
mat-icon {
margin-right: 8px;
}
}
.start-recording-button-container { .start-recording-button-container {
width: 100%; width: 100%;
display: inline-block; display: inline-block;

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, OnDestroy, Output } from '@angular/core';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
import { import {
RecordingDeleteRequestedEvent, RecordingDeleteRequestedEvent,
@ -16,6 +16,7 @@ import { RecordingService } from '../../../../services/recording/recording.servi
import { OpenViduService } from '../../../../services/openvidu/openvidu.service'; import { OpenViduService } from '../../../../services/openvidu/openvidu.service';
import { ILogger } from '../../../../models/logger.model'; import { ILogger } from '../../../../models/logger.model';
import { LoggerService } from '../../../../services/logger/logger.service'; import { LoggerService } from '../../../../services/logger/logger.service';
import { OpenViduComponentsConfigService } from '../../../../services/config/directive-config.service';
/** /**
* The **RecordingActivityComponent** is the component that allows showing the recording activity. * The **RecordingActivityComponent** is the component that allows showing the recording activity.
@ -31,7 +32,7 @@ import { LoggerService } from '../../../../services/logger/logger.service';
// TODO: Allow to add more than one recording type // TODO: Allow to add more than one recording type
// TODO: Allow to choose where the recording is stored (s3, google cloud, etc) // TODO: Allow to choose where the recording is stored (s3, google cloud, etc)
// TODO: Allow to choose the layout of the recording // TODO: Allow to choose the layout of the recording
export class RecordingActivityComponent implements OnInit { export class RecordingActivityComponent implements OnInit, OnDestroy {
/** /**
* @internal * @internal
*/ */
@ -67,6 +68,20 @@ export class RecordingActivityComponent implements OnInit {
*/ */
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>(); @Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
/**
* @internal
* Provides event notifications that fire when view recordings button has been clicked.
* This event is triggered when the user wants to view all recordings in an external page.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* @internal
* This event is fired when the user clicks on the view recording button.
* It provides the recording ID as event data.
*/
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/** /**
* @internal * @internal
*/ */
@ -108,6 +123,27 @@ export class RecordingActivityComponent implements OnInit {
* @internal * @internal
*/ */
mouseHovering: boolean = false; mouseHovering: boolean = false;
/**
* @internal
*/
isReadOnlyMode: boolean = false;
/**
* @internal
*/
viewButtonText: string = 'PANEL.RECORDING.VIEW';
/**
* @internal
*/
showControls: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean } = {
play: true,
download: true,
delete: true,
externalView: false
};
private log: ILogger; private log: ILogger;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -120,7 +156,8 @@ export class RecordingActivityComponent implements OnInit {
private actionService: ActionService, private actionService: ActionService,
private openviduService: OpenViduService, private openviduService: OpenViduService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private loggerSrv: LoggerService private loggerSrv: LoggerService,
private libService: OpenViduComponentsConfigService
) { ) {
this.log = this.loggerSrv.get('RecordingActivityComponent'); this.log = this.loggerSrv.get('RecordingActivityComponent');
} }
@ -131,6 +168,7 @@ export class RecordingActivityComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.subscribeToRecordingStatus(); this.subscribeToRecordingStatus();
this.subscribeToTracksChanges(); this.subscribeToTracksChanges();
this.subscribeToConfigChanges();
} }
/** /**
@ -240,6 +278,46 @@ export class RecordingActivityComponent implements OnInit {
this.recordingService.playRecording(recording); this.recordingService.playRecording(recording);
} }
/**
* @internal
*/
viewRecording(recording: RecordingInfo) {
// This method can be overridden or emit a custom event for navigation
// For now, it uses the same behavior as play, but can be customized
if (!recording.filename) {
this.log.e('Error viewing recording. Recording filename is undefined');
return;
}
const payload: RecordingPlayClickedEvent = {
roomName: this.openviduService.getRoomName(),
recordingId: recording.id
};
this.onRecordingPlayClicked.emit(payload);
// You can customize this to navigate to a different page instead
this.recordingService.playRecording(recording);
}
/**
* @internal
*/
viewAllRecordings() {
this.onViewRecordingsClicked.emit();
}
private subscribeToConfigChanges() {
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => {
this.isReadOnlyMode = readOnly;
this.cd.markForCheck();
});
this.libService.recordingActivityShowControls$
.pipe(takeUntil(this.destroy$))
.subscribe((controls: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean }) => {
this.showControls = controls;
this.cd.markForCheck();
});
}
private subscribeToRecordingStatus() { private subscribeToRecordingStatus() {
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => { this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, recordingList, error } = event; const { status, recordingList, error } = event;

View File

@ -137,6 +137,14 @@
</span> </span>
</button> </button>
<!-- View recordings button -->
@if (!isMinimal && showViewRecordingsButton) {
<button mat-menu-item id="view-recordings-btn" (click)="onViewRecordingsClicked.emit()">
<mat-icon>video_library</mat-icon>
<span>{{ 'TOOLBAR.VIEW_RECORDINGS' | translate }}</span>
</button>
}
<!-- Broadcasting button --> <!-- Broadcasting button -->
<button <button
*ngIf="!isMinimal && showBroadcastingButton" *ngIf="!isMinimal && showBroadcastingButton"

View File

@ -145,6 +145,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> = @Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new EventEmitter<BroadcastingStopRequestedEvent>(); new EventEmitter<BroadcastingStopRequestedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/** /**
* @ignore * @ignore
*/ */
@ -240,6 +246,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
showRecordingButton: boolean = true; showRecordingButton: boolean = true;
/**
* @ignore
*/
showViewRecordingsButton: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -312,6 +323,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
recordingStatus: RecordingStatus = RecordingStatus.STOPPED; recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
isRecordingReadOnlyMode: boolean = false;
/** /**
* @ignore * @ignore
*/ */
@ -602,17 +618,19 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToFullscreenChanged() { private subscribeToFullscreenChanged() {
fromEvent(document, 'fullscreenchange').pipe(takeUntil(this.destroy$)).subscribe(() => { fromEvent(document, 'fullscreenchange')
const isFullscreen = Boolean(document.fullscreenElement); .pipe(takeUntil(this.destroy$))
if (isFullscreen) { .subscribe(() => {
this.cdkOverlayService.setSelector('#session-container'); const isFullscreen = Boolean(document.fullscreenElement);
} else { if (isFullscreen) {
this.cdkOverlayService.setSelector('body'); this.cdkOverlayService.setSelector('#session-container');
} } else {
this.isFullscreenActive = isFullscreen; this.cdkOverlayService.setSelector('body');
this.onFullscreenEnabledChanged.emit(this.isFullscreenActive); }
this.cd.detectChanges(); this.isFullscreenActive = isFullscreen;
}); this.onFullscreenEnabledChanged.emit(this.isFullscreenActive);
this.cd.detectChanges();
});
} }
private subscribeToMenuToggling() { private subscribeToMenuToggling() {
@ -661,6 +679,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
} }
private subscribeToRecordingStatus() { private subscribeToRecordingStatus() {
this.libService.recordingActivityReadOnly$.pipe(takeUntil(this.destroy$)).subscribe((readOnly: boolean) => {
this.isRecordingReadOnlyMode = readOnly;
this.cd.markForCheck();
});
this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => { this.recordingService.recordingStatusObs.pipe(takeUntil(this.destroy$)).subscribe((event: RecordingStatusInfo) => {
const { status, startedAt } = event; const { status, startedAt } = event;
this.recordingStatus = status; this.recordingStatus = status;
@ -696,6 +719,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.brandingLogo = value; this.brandingLogo = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.toolbarViewRecordingsButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showViewRecordingsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => { this.libService.cameraButton$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
this.showCameraButton = value; this.showCameraButton = value;
this.cd.markForCheck(); this.cd.markForCheck();
@ -766,8 +794,9 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.cd.markForCheck(); this.cd.markForCheck();
}); });
this.libService.toolbarAdditionalButtonsPosition$.pipe(takeUntil(this.destroy$)).subscribe( this.libService.toolbarAdditionalButtonsPosition$
(value: ToolbarAdditionalButtonsPosition) => { .pipe(takeUntil(this.destroy$))
.subscribe((value: ToolbarAdditionalButtonsPosition) => {
// Using Promise.resolve() to defer change detection until the next microtask. // Using Promise.resolve() to defer change detection until the next microtask.
// This ensures that Angular's change detection has the latest value before updating the view. // This ensures that Angular's change detection has the latest value before updating the view.
// Without this, Angular's OnPush strategy might not immediately reflect the change, // Without this, Angular's OnPush strategy might not immediately reflect the change,
@ -777,8 +806,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.additionalButtonsPosition = value; this.additionalButtonsPosition = value;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
} });
);
} }
private subscribeToCaptionsToggling() { private subscribeToCaptionsToggling() {

View File

@ -69,6 +69,7 @@
(onRecordingStartRequested)="onRecordingStartRequested.emit($event)" (onRecordingStartRequested)="onRecordingStartRequested.emit($event)"
(onRecordingStopRequested)="onRecordingStopRequested.emit($event)" (onRecordingStopRequested)="onRecordingStopRequested.emit($event)"
(onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)" (onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
> >
<ng-template #toolbarAdditionalButtons> <ng-template #toolbarAdditionalButtons>
<ng-container *ngTemplateOutlet="openviduAngularToolbarAdditionalButtonsTemplate"></ng-container> <ng-container *ngTemplateOutlet="openviduAngularToolbarAdditionalButtonsTemplate"></ng-container>
@ -133,6 +134,8 @@
(onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)" (onRecordingDeleteRequested)="onRecordingDeleteRequested.emit($event)"
(onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)" (onRecordingDownloadClicked)="onRecordingDownloadClicked.emit($event)"
(onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)" (onRecordingPlayClicked)="onRecordingPlayClicked.emit($event)"
(onViewRecordingClicked)="onViewRecordingClicked.emit($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked.emit()"
(onBroadcastingStartRequested)="onBroadcastingStartRequested.emit($event)" (onBroadcastingStartRequested)="onBroadcastingStartRequested.emit($event)"
(onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)" (onBroadcastingStopRequested)="onBroadcastingStopRequested.emit($event)"
></ov-activities-panel> ></ov-activities-panel>

View File

@ -326,6 +326,13 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
*/ */
@Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>(); @Output() onRecordingPlayClicked: EventEmitter<RecordingPlayClickedEvent> = new EventEmitter<RecordingPlayClickedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recording button.
* It provides the recording ID as event data.
*/
@Output() onViewRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/** /**
* Provides event notifications that fire when download recording button is clicked from {@link ActivitiesPanelComponent}. * Provides event notifications that fire when download recording button is clicked from {@link ActivitiesPanelComponent}.
* It provides the {@link RecordingDownloadClickedEvent} payload as event data. * It provides the {@link RecordingDownloadClickedEvent} payload as event data.
@ -346,6 +353,12 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
@Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> = @Output() onBroadcastingStopRequested: EventEmitter<BroadcastingStopRequestedEvent> =
new EventEmitter<BroadcastingStopRequestedEvent>(); new EventEmitter<BroadcastingStopRequestedEvent>();
/**
* @internal
* This event is fired when the user clicks on the view recordings button.
*/
@Output() onViewRecordingsClicked: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when Room is created for the local participant. * Provides event notifications that fire when Room is created for the local participant.
* It provides the {@link https://openvidu.io/latest/docs/getting-started/#room Room} payload as event data. * It provides the {@link https://openvidu.io/latest/docs/getting-started/#room Room} payload as event data.
@ -620,91 +633,80 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
} }
private subscribeToVideconferenceDirectives() { private subscribeToVideconferenceDirectives() {
this.libService.token$ this.libService.token$.pipe(skip(1), takeUntil(this.destroy$)).subscribe((token: string) => {
.pipe( try {
skip(1), if (!token) {
takeUntil(this.destroy$) this.log.e('Token is empty');
) return;
.subscribe((token: string) => {
try {
if (!token) {
this.log.e('Token is empty');
return;
}
const livekitUrl = this.libService.getLivekitUrl();
this.openviduService.initializeAndSetToken(token, livekitUrl);
this.log.d('Token has been successfully set. Room is ready to join');
this.isRoomReady = true;
this.showPrejoin = false;
} catch (error) {
this.log.e('Error trying to set token', error);
this._tokenError = error;
} }
});
this.libService.tokenError$ const livekitUrl = this.libService.getLivekitUrl();
.pipe(takeUntil(this.destroy$)) this.openviduService.initializeAndSetToken(token, livekitUrl);
.subscribe((error: any) => { this.log.d('Token has been successfully set. Room is ready to join');
if (!error) return; this.isRoomReady = true;
this.showPrejoin = false;
this.log.e('Token error received', error); } catch (error) {
this.log.e('Error trying to set token', error);
this._tokenError = error; this._tokenError = error;
}
});
if (!this.showPrejoin) { this.libService.tokenError$.pipe(takeUntil(this.destroy$)).subscribe((error: any) => {
this.actionService.openDialog(error.name, error.message, false); if (!error) return;
}
});
this.libService.prejoin$ this.log.e('Token error received', error);
.pipe(takeUntil(this.destroy$)) this._tokenError = error;
.subscribe((value: boolean) => {
this.showPrejoin = value;
if (!this.showPrejoin) {
// Emit token ready if the prejoin page won't be shown
// Ensure we have a participant name before proceeding with the join if (!this.showPrejoin) {
this.log.d('Prejoin page is hidden, checking participant name'); this.actionService.openDialog(error.name, error.message, false);
// Check if we have a participant name already }
if (this.latestParticipantName) { });
// We have a name, proceed immediately
this._onReadyToJoin(); this.libService.prejoin$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
} else { this.showPrejoin = value;
// No name yet - set up a one-time subscription to wait for it if (!this.showPrejoin) {
this.libService.participantName$ // Emit token ready if the prejoin page won't be shown
.pipe(
filter((name) => !!name), // Ensure we have a participant name before proceeding with the join
take(1), this.log.d('Prejoin page is hidden, checking participant name');
takeUntil(this.destroy$) // Check if we have a participant name already
) if (this.latestParticipantName) {
.subscribe(() => { // We have a name, proceed immediately
// Now we have the name in latestParticipantName this._onReadyToJoin();
this._onReadyToJoin(); } else {
}); // No name yet - set up a one-time subscription to wait for it
// Add safety timeout in case name never arrives this.libService.participantName$
setTimeout(() => { .pipe(
if (!this.latestParticipantName) { filter((name) => !!name),
this.log.w('No participant name received after timeout, proceeding anyway'); take(1),
const storedName = this.storageSrv.getParticipantName(); takeUntil(this.destroy$)
if (storedName) { )
this.latestParticipantName = storedName; .subscribe(() => {
this.libService.setParticipantName(storedName); // Now we have the name in latestParticipantName
} this._onReadyToJoin();
this._onReadyToJoin(); });
// Add safety timeout in case name never arrives
setTimeout(() => {
if (!this.latestParticipantName) {
this.log.w('No participant name received after timeout, proceeding anyway');
const storedName = this.storageSrv.getParticipantName();
if (storedName) {
this.latestParticipantName = storedName;
this.libService.setParticipantName(storedName);
} }
}, 1000); this._onReadyToJoin();
} }
}, 1000);
} }
// this.cd.markForCheck(); }
}); // this.cd.markForCheck();
});
this.libService.participantName$ this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
.pipe(takeUntil(this.destroy$)) if (name) {
.subscribe((name: string) => { this.latestParticipantName = name;
if (name) { this.storageSrv.setParticipantName(name);
this.latestParticipantName = name; }
this.storageSrv.setParticipantName(name); });
}
});
} }
} }

View File

@ -10,7 +10,10 @@ import {
FallbackLogoDirective, FallbackLogoDirective,
LayoutRemoteParticipantsDirective, LayoutRemoteParticipantsDirective,
PrejoinDisplayParticipantName, PrejoinDisplayParticipantName,
ToolbarBrandingLogoDirective ToolbarBrandingLogoDirective,
ToolbarViewRecordingsButtonDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective
} from './internals.directive'; } from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive'; import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import { import {
@ -65,6 +68,8 @@ const directives = [
PrejoinDirective, PrejoinDirective,
PrejoinDisplayParticipantName, PrejoinDisplayParticipantName,
VideoEnabledDirective, VideoEnabledDirective,
RecordingActivityReadOnlyDirective,
RecordingActivityShowControlsDirective,
AudioEnabledDirective, AudioEnabledDirective,
ShowDisconnectionDialogDirective, ShowDisconnectionDialogDirective,
RecordingStreamBaseUrlDirective, RecordingStreamBaseUrlDirective,
@ -84,6 +89,7 @@ const directives = [
ToolbarDisplayLogoDirective, ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective, ToolbarSettingsButtonDirective,
ToolbarAdditionalButtonsPossitionDirective, ToolbarAdditionalButtonsPossitionDirective,
ToolbarViewRecordingsButtonDirective,
StreamDisplayParticipantNameDirective, StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective, StreamDisplayAudioDetectionDirective,
StreamVideoControlsDirective, StreamVideoControlsDirective,

View File

@ -161,3 +161,181 @@ export class PrejoinDisplayParticipantName implements OnDestroy {
this.libService.setPrejoinDisplayParticipantName(value); this.libService.setPrejoinDisplayParticipantName(value);
} }
} }
/**
* @internal
*
* The **recordingActivityReadOnly** directive sets the recording activity panel to read-only mode.
* In this mode, users can only view recordings without the ability to start, stop, or delete them.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `false`
*
* @example
* <ov-videoconference [recordingActivityReadOnly]="true"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityReadOnly]',
standalone: false
})
export class RecordingActivityReadOnlyDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityReadOnly(value: boolean) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update(false);
}
/**
* @ignore
*/
update(value: boolean) {
this.libService.setRecordingActivityReadOnly(value);
}
}
/**
*
* @internal
*
* The **recordingActivityShowControls** directive allows to show/hide specific recording controls (play, download, delete, externalView).
* You can pass an object with boolean properties to control which buttons are shown.
*
* It is only available for {@link VideoconferenceComponent}.
*
* Default: `{ play: true, download: true, delete: true, externalView: false }`
*
* @example
* <ov-videoconference [recordingActivityShowControls]="{ play: false, download: true, delete: false }"></ov-videoconference>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityShowControls]',
standalone: false
})
export class RecordingActivityShowControlsDirective implements OnDestroy {
/**
* @ignore
*/
@Input() set recordingActivityShowControls(value: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean }) {
this.update(value);
}
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
/**
* @ignore
*/
ngOnDestroy(): void {
this.clear();
}
/**
* @ignore
*/
clear() {
this.update({ play: true, download: true, delete: true, externalView: false });
}
/**
* @ignore
*/
update(value: { play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean }) {
this.libService.setRecordingActivityShowControls(value);
}
}
/**
* @internal
* The **viewRecordingsButton** directive allows show/hide the view recordings toolbar button.
*
* Default: `false`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarViewRecordingsButton]="true"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [viewRecordingsButton]="true"></ov-toolbar>
*
* When the button is clicked, it will fire the `onViewRecordingsClicked` event.
*/
@Directive({
selector: 'ov-videoconference[toolbarViewRecordingsButton], ov-toolbar[viewRecordingsButton]',
standalone: false
})
export class ToolbarViewRecordingsButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarViewRecordingsButton(value: boolean) {
this.viewRecordingsValue = value;
this.update(this.viewRecordingsValue);
}
/**
* @ignore
*/
@Input() set viewRecordingsButton(value: boolean) {
this.viewRecordingsValue = value;
this.update(this.viewRecordingsValue);
}
private viewRecordingsValue: boolean = false;
/**
* @ignore
*/
constructor(
public elementRef: ElementRef,
private libService: OpenViduComponentsConfigService
) {}
ngAfterViewInit() {
this.update(this.viewRecordingsValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.viewRecordingsValue = false;
this.update(true);
}
private update(value: boolean) {
if (this.libService.getToolbarViewRecordingsButton() !== value) {
this.libService.setToolbarViewRecordingsButton(value);
}
}
}

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "参与者", "PARTICIPANTS": "参与者",
"CHAT": "聊天", "CHAT": "聊天",
"ACTIVITIES": "活动", "ACTIVITIES": "活动",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。" "NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"VIEW_RECORDINGS": "查看录像"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "设置", "SETTINGS": "设置",
@ -115,6 +116,10 @@
"SUBTITLE": "为后人记录你的会议", "SUBTITLE": "为后人记录你的会议",
"CONTENT_TITLE": "记录你的视频通话", "CONTENT_TITLE": "记录你的视频通话",
"CONTENT_SUBTITLE": "当录音完成后,你将可以轻松地下载它", "CONTENT_SUBTITLE": "当录音完成后,你将可以轻松地下载它",
"VIEW_ONLY_SUBTITLE": "查看和访问房间录音",
"VIEW_ONLY_CONTENT_TITLE": "视频通话录音",
"VIEW_ONLY_CONTENT_SUBTITLE": "在这里您可以访问所有可用的录音",
"WATCH": "观看",
"STARTING": "开始录音", "STARTING": "开始录音",
"STOPPING": "停止录制", "STOPPING": "停止录制",
"IN_PROGRESS": "录音中", "IN_PROGRESS": "录音中",
@ -126,7 +131,8 @@
"DOWNLOAD": "下载", "DOWNLOAD": "下载",
"RECORDINGS": "录制", "RECORDINGS": "录制",
"NO_MODERATOR": "只有主持人可以开始录音", "NO_MODERATOR": "只有主持人可以开始录音",
"NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。" "NO_TRACKS_PUBLISHED": "请分享音频或视频以开始录制。",
"NO_RECORDINGS_AVAILABLE": "目前没有可用的录音"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "直播", "TITLE": "直播",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "Participants", "PARTICIPANTS": "Participants",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Activities", "ACTIVITIES": "Activities",
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording." "NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"VIEW_RECORDINGS": "View recordings"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Settings", "SETTINGS": "Settings",
@ -115,6 +116,13 @@
"SUBTITLE": "Record your meeting for posterity", "SUBTITLE": "Record your meeting for posterity",
"CONTENT_TITLE": "Record your video call", "CONTENT_TITLE": "Record your video call",
"CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease", "CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease",
"VIEW_ONLY_TITLE": "Available recordings",
"VIEW_ONLY_SUBTITLE": "View and access room recordings",
"VIEW_ONLY_CONTENT_TITLE": "Video call recordings",
"VIEW_ONLY_CONTENT_SUBTITLE": "Here you can access all available recordings",
"VIEW": "View",
"WATCH": "Watch",
"ACCESS": "Access",
"STARTING": "Starting recording", "STARTING": "Starting recording",
"STOPPING": "Stopping recording", "STOPPING": "Stopping recording",
"IN_PROGRESS": "Recording in progress ...", "IN_PROGRESS": "Recording in progress ...",
@ -126,7 +134,9 @@
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"RECORDINGS": "RECORDINGS", "RECORDINGS": "RECORDINGS",
"NO_MODERATOR": "Only the MODERATOR can start the recording", "NO_MODERATOR": "Only the MODERATOR can start the recording",
"NO_TRACKS_PUBLISHED": "Share audio or video to start recording." "NO_TRACKS_PUBLISHED": "Share audio or video to start recording.",
"NO_RECORDINGS_AVAILABLE": "No recordings available at this time",
"BROWSE_RECORDINGS": "Browse saved recordings"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "Participantes", "PARTICIPANTS": "Participantes",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Actividades", "ACTIVITIES": "Actividades",
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar." "NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"VIEW_RECORDINGS": "Ver grabaciones"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Ajustes", "SETTINGS": "Ajustes",
@ -115,6 +116,10 @@
"SUBTITLE": "Graba tus llamadas para la posteridad", "SUBTITLE": "Graba tus llamadas para la posteridad",
"CONTENT_TITLE": "Graba tu video conferencia", "CONTENT_TITLE": "Graba tu video conferencia",
"CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad", "CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad",
"VIEW_ONLY_SUBTITLE": "Visualiza y accede a las grabaciones de la sala",
"VIEW_ONLY_CONTENT_TITLE": "Grabaciones de la video conferencia",
"VIEW_ONLY_CONTENT_SUBTITLE": "Aquí puedes acceder a todas las grabaciones disponibles",
"WATCH": "Visualizar",
"STARTING": "Iniciando grabación...", "STARTING": "Iniciando grabación...",
"STOPPING": "Parando grabación", "STOPPING": "Parando grabación",
"IN_PROGRESS": "Grabación en curso", "IN_PROGRESS": "Grabación en curso",
@ -126,7 +131,8 @@
"DOWNLOAD": "Descargar", "DOWNLOAD": "Descargar",
"RECORDINGS": "GRABACIONES", "RECORDINGS": "GRABACIONES",
"NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación", "NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación",
"NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar." "NO_TRACKS_PUBLISHED": "Comparte audio o video para poder empezar a grabar.",
"NO_RECORDINGS_AVAILABLE": "No hay grabaciones disponibles en este momento"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "Participants", "PARTICIPANTS": "Participants",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITES": "Activités", "ACTIVITES": "Activités",
"NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement." "NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement.",
"VIEW_RECORDINGS": "Voir les enregistrements"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Paramètres", "SETTINGS": "Paramètres",
@ -115,6 +116,10 @@
"SUBTITLE": "Enregistrez votre réunion pour la postérité", "SUBTITLE": "Enregistrez votre réunion pour la postérité",
"CONTENT_TITLE": "Enregistrez votre appel vidéo", "CONTENT_TITLE": "Enregistrez votre appel vidéo",
"CONTENT_SUBTITLE": "Une fois l'enregistrement terminé, vous pourrez le télécharger facilement", "CONTENT_SUBTITLE": "Une fois l'enregistrement terminé, vous pourrez le télécharger facilement",
"VIEW_ONLY_SUBTITLE": "Visualisez et accédez aux enregistrements de la salle",
"VIEW_ONLY_CONTENT_TITLE": "Enregistrements d'appel vidéo",
"VIEW_ONLY_CONTENT_SUBTITLE": "Ici vous pouvez accéder à tous les enregistrements disponibles",
"WATCH": "Regarder",
"STARTING": "Début de l'enregistrement", "STARTING": "Début de l'enregistrement",
"STOPPING": "Arrêt de l'enregistrement", "STOPPING": "Arrêt de l'enregistrement",
"IN_PROGRESS": "Enregistrement en cours", "IN_PROGRESS": "Enregistrement en cours",
@ -126,7 +131,8 @@
"DOWNLOAD": "Télécharger", "DOWNLOAD": "Télécharger",
"RECORDINGS": "ENREGISTREMENTS", "RECORDINGS": "ENREGISTREMENTS",
"NO_MODERATOR": "Seul le MODERATEUR peut lancer l'enregistrement", "NO_MODERATOR": "Seul le MODERATEUR peut lancer l'enregistrement",
"NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement." "NO_TRACKS_PUBLISHED": "Partagez de laudio ou de la vidéo pour commencer lenregistrement.",
"NO_RECORDINGS_AVAILABLE": "Aucun enregistrement disponible pour le moment"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "सदस्य", "PARTICIPANTS": "सदस्य",
"CHAT": "बातचीत", "CHAT": "बातचीत",
"ACTIVITIES": "गतिविधियाँ", "ACTIVITIES": "गतिविधियाँ",
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।" "NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"VIEW_RECORDINGS": "रिकॉर्डिंग देखें"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "सेटिंग्स", "SETTINGS": "सेटिंग्स",
@ -115,6 +116,10 @@
"SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें", "SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें",
"CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें", "CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें",
"CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे", "CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे",
"VIEW_ONLY_SUBTITLE": "कमरे की रिकॉर्डिंग देखें और एक्सेस करें",
"VIEW_ONLY_CONTENT_TITLE": "वीडियो कॉल रिकॉर्डिंग",
"VIEW_ONLY_CONTENT_SUBTITLE": "यहाँ आप सभी उपलब्ध रिकॉर्डिंग तक पहुँच सकते हैं",
"WATCH": "देखना",
"STARTING": "रिकॉर्डिंग शुरू कर रहा है", "STARTING": "रिकॉर्डिंग शुरू कर रहा है",
"STOPPING": "रिकॉर्डिंग बंद करना", "STOPPING": "रिकॉर्डिंग बंद करना",
"IN_PROGRESS": "रिकॉर्डिंग चल रही है", "IN_PROGRESS": "रिकॉर्डिंग चल रही है",
@ -126,7 +131,8 @@
"DOWNLOAD": "डाउनलोड", "DOWNLOAD": "डाउनलोड",
"RECORDINGS": "रिकॉर्डिंग", "RECORDINGS": "रिकॉर्डिंग",
"NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है", "NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है",
"NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।" "NO_TRACKS_PUBLISHED": "रिकॉर्डिंग शुरू करने के लिए ऑडियो या वीडियो साझा करें।",
"NO_RECORDINGS_AVAILABLE": "इस समय कोई रिकॉर्डिंग उपलब्ध नहीं है"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "स्ट्रीमिंग", "TITLE": "स्ट्रीमिंग",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "Partecipanti", "PARTICIPANTS": "Partecipanti",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Attività", "ACTIVITIES": "Attività",
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione." "NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"VIEW_RECORDINGS": "Visualizza registrazioni"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Impostazioni", "SETTINGS": "Impostazioni",
@ -115,6 +116,10 @@
"SUBTITLE": "Registra la tua riunione per i posteri", "SUBTITLE": "Registra la tua riunione per i posteri",
"CONTENT_TITLE": "Registra la tua videochiamata", "CONTENT_TITLE": "Registra la tua videochiamata",
"CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità", "CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità",
"VIEW_ONLY_SUBTITLE": "Visualizza e accedi alle registrazioni della sala",
"VIEW_ONLY_CONTENT_TITLE": "Registrazioni di videochiamate",
"VIEW_ONLY_CONTENT_SUBTITLE": "Qui puoi accedere a tutte le registrazioni disponibili",
"WATCH": "Guardare",
"STARTING": "Avvio della registrazione", "STARTING": "Avvio della registrazione",
"STOPPING": "Interruzione della registrazione", "STOPPING": "Interruzione della registrazione",
"IN_PROGRESS": "Registrazione in corso", "IN_PROGRESS": "Registrazione in corso",
@ -126,7 +131,8 @@
"DOWNLOAD": "Scarica", "DOWNLOAD": "Scarica",
"RECORDINGS": "REGISTRAZIONI", "RECORDINGS": "REGISTRAZIONI",
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione", "NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione",
"NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione." "NO_TRACKS_PUBLISHED": "Condividi audio o video per iniziare la registrazione.",
"NO_RECORDINGS_AVAILABLE": "Nessuna registrazione disponibile al momento"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "参加者", "PARTICIPANTS": "参加者",
"CHAT": "チャット", "CHAT": "チャット",
"ACTIVITIES": "アクティビティ", "ACTIVITIES": "アクティビティ",
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。" "NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"VIEW_RECORDINGS": "録画を表示"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "設定", "SETTINGS": "設定",
@ -115,6 +116,10 @@
"SUBTITLE": "会議を録画して保存する", "SUBTITLE": "会議を録画して保存する",
"CONTENT_TITLE": "ビデオ通話を録音する", "CONTENT_TITLE": "ビデオ通話を録音する",
"CONTENT_SUBTITLE": "録画が完了したら、簡単にダウンロードできます", "CONTENT_SUBTITLE": "録画が完了したら、簡単にダウンロードできます",
"VIEW_ONLY_SUBTITLE": "ルームの録画を表示してアクセスする",
"VIEW_ONLY_CONTENT_TITLE": "ビデオ通話の録画",
"VIEW_ONLY_CONTENT_SUBTITLE": "ここで利用可能なすべての録画にアクセスできます",
"WATCH": "視聴する",
"STARTING": "録画開始", "STARTING": "録画開始",
"STOPPING": "録音停止", "STOPPING": "録音停止",
"IN_PROGRESS": "録画中", "IN_PROGRESS": "録画中",
@ -126,7 +131,8 @@
"DOWNLOAD": "保存", "DOWNLOAD": "保存",
"RECORDINGS": "録画", "RECORDINGS": "録画",
"NO_MODERATOR": "録画を開始できるのは、モデレーターのみです", "NO_MODERATOR": "録画を開始できるのは、モデレーターのみです",
"NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。" "NO_TRACKS_PUBLISHED": "録音を開始するには、音声または動画を共有してください。",
"NO_RECORDINGS_AVAILABLE": "現在利用可能な録画はありません"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "ストリーミング", "TITLE": "ストリーミング",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "Deelnemers", "PARTICIPANTS": "Deelnemers",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Activiteiten", "ACTIVITIES": "Activiteiten",
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen." "NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"VIEW_RECORDINGS": "Opnames bekijken"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Instellingen", "SETTINGS": "Instellingen",
@ -115,6 +116,10 @@
"SUBTITLE": "Neem uw vergadering op voor het nageslacht", "SUBTITLE": "Neem uw vergadering op voor het nageslacht",
"CONTENT_TITLE": "Neem uw videogesprek op", "CONTENT_TITLE": "Neem uw videogesprek op",
"CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden", "CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden",
"VIEW_ONLY_SUBTITLE": "Bekijk en toegang tot kameropnames",
"VIEW_ONLY_CONTENT_TITLE": "Videogesprek opnames",
"VIEW_ONLY_CONTENT_SUBTITLE": "Hier heeft u toegang tot alle beschikbare opnames",
"WATCH": "Bekijken",
"STARTING": "Beginnen met opnemen", "STARTING": "Beginnen met opnemen",
"STOPPING": "Opname stoppen", "STOPPING": "Opname stoppen",
"IN_PROGRESS": "Opname in uitvoering", "IN_PROGRESS": "Opname in uitvoering",
@ -126,7 +131,8 @@
"DOWNLOAD": "Downloaden", "DOWNLOAD": "Downloaden",
"RECORDINGS": "OPNAME", "RECORDINGS": "OPNAME",
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten", "NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten",
"NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen." "NO_TRACKS_PUBLISHED": "Deel audio of video om met opnemen te beginnen.",
"NO_RECORDINGS_AVAILABLE": "Momenteel zijn er geen opnames beschikbaar"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -56,7 +56,8 @@
"PARTICIPANTS": "Participantes", "PARTICIPANTS": "Participantes",
"CHAT": "Chat", "CHAT": "Chat",
"ACTIVITIES": "Actividades", "ACTIVITIES": "Actividades",
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar." "NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
"VIEW_RECORDINGS": "Ver gravações"
}, },
"STREAM": { "STREAM": {
"SETTINGS": "Configurações", "SETTINGS": "Configurações",
@ -115,6 +116,10 @@
"SUBTITLE": "Grave a sua reunião para a posteridade", "SUBTITLE": "Grave a sua reunião para a posteridade",
"CONTENT_TITLE": "Grave a sua videochamada", "CONTENT_TITLE": "Grave a sua videochamada",
"CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade", "CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade",
"VIEW_ONLY_SUBTITLE": "Visualize e acesse gravações da sala",
"VIEW_ONLY_CONTENT_TITLE": "Gravações de videochamada",
"VIEW_ONLY_CONTENT_SUBTITLE": "Aqui você pode acessar todas as gravações disponíveis",
"WATCH": "Assistir",
"STARTING": "Começar a gravação", "STARTING": "Começar a gravação",
"STOPPING": "Parando a gravação", "STOPPING": "Parando a gravação",
"IN_PROGRESS": "Gravação em andamento", "IN_PROGRESS": "Gravação em andamento",
@ -126,7 +131,8 @@
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"RECORDINGS": "GRAVAÇÕES", "RECORDINGS": "GRAVAÇÕES",
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação", "NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação",
"NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar." "NO_TRACKS_PUBLISHED": "Compartilhe áudio ou vídeo para começar a gravar.",
"NO_RECORDINGS_AVAILABLE": "Nenhuma gravação disponível no momento"
}, },
"STREAMING": { "STREAMING": {
"TITLE": "Streaming", "TITLE": "Streaming",

View File

@ -96,6 +96,8 @@ export class OpenViduComponentsConfigService {
backgroundEffectsButton$: Observable<boolean>; backgroundEffectsButton$: Observable<boolean>;
private recordingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true); private recordingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
recordingButton$: Observable<boolean>; recordingButton$: Observable<boolean>;
private toolbarViewRecordingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(false);
toolbarViewRecordingsButton$: Observable<boolean>;
private broadcastingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true); private broadcastingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
broadcastingButton$: Observable<boolean>; broadcastingButton$: Observable<boolean>;
private recordingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true); private recordingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true);
@ -103,6 +105,14 @@ export class OpenViduComponentsConfigService {
private broadcastingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true); private broadcastingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true);
broadcastingActivity$: Observable<boolean>; broadcastingActivity$: Observable<boolean>;
// Recording activity configuration
private recordingActivityReadOnly = <BehaviorSubject<boolean>>new BehaviorSubject(false);
recordingActivityReadOnly$: Observable<boolean>;
private recordingActivityShowControls = <BehaviorSubject<{ play?: boolean; download?: boolean; delete?: boolean }>>(
new BehaviorSubject({ play: true, download: true, delete: true })
);
recordingActivityShowControls$: Observable<{ play?: boolean; download?: boolean; delete?: boolean; externalView?: boolean }>;
// Admin // Admin
private adminRecordingsList: BehaviorSubject<RecordingInfo[]> = new BehaviorSubject(<RecordingInfo[]>[]); private adminRecordingsList: BehaviorSubject<RecordingInfo[]> = new BehaviorSubject(<RecordingInfo[]>[]);
adminRecordingsList$: Observable<RecordingInfo[]>; adminRecordingsList$: Observable<RecordingInfo[]>;
@ -142,6 +152,7 @@ export class OpenViduComponentsConfigService {
this.displayLogo$ = this.displayLogo.asObservable(); this.displayLogo$ = this.displayLogo.asObservable();
this.brandingLogo$ = this.brandingLogo.asObservable(); this.brandingLogo$ = this.brandingLogo.asObservable();
this.recordingButton$ = this.recordingButton.asObservable(); this.recordingButton$ = this.recordingButton.asObservable();
this.toolbarViewRecordingsButton$ = this.toolbarViewRecordingsButton.asObservable();
this.broadcastingButton$ = this.broadcastingButton.asObservable(); this.broadcastingButton$ = this.broadcastingButton.asObservable();
this.toolbarSettingsButton$ = this.toolbarSettingsButton.asObservable(); this.toolbarSettingsButton$ = this.toolbarSettingsButton.asObservable();
this.captionsButton$ = this.captionsButton.asObservable(); this.captionsButton$ = this.captionsButton.asObservable();
@ -154,6 +165,8 @@ export class OpenViduComponentsConfigService {
this.participantItemMuteButton$ = this.participantItemMuteButton.asObservable(); this.participantItemMuteButton$ = this.participantItemMuteButton.asObservable();
// Recording activity observables // Recording activity observables
this.recordingActivity$ = this.recordingActivity.asObservable(); this.recordingActivity$ = this.recordingActivity.asObservable();
this.recordingActivityReadOnly$ = this.recordingActivityReadOnly.asObservable();
this.recordingActivityShowControls$ = this.recordingActivityShowControls.asObservable();
// Broadcasting activity // Broadcasting activity
this.broadcastingActivity$ = this.broadcastingActivity.asObservable(); this.broadcastingActivity$ = this.broadcastingActivity.asObservable();
// Admin dashboard // Admin dashboard
@ -357,6 +370,18 @@ export class OpenViduComponentsConfigService {
return this.recordingButton.getValue(); return this.recordingButton.getValue();
} }
setToolbarViewRecordingsButton(toolbarViewRecordingsButton: boolean) {
this.toolbarViewRecordingsButton.next(toolbarViewRecordingsButton);
}
getToolbarViewRecordingsButton(): boolean {
return this.toolbarViewRecordingsButton.getValue();
}
showToolbarViewRecordingsButton(): boolean {
return this.getToolbarViewRecordingsButton();
}
setBroadcastingButton(broadcastingButton: boolean) { setBroadcastingButton(broadcastingButton: boolean) {
this.broadcastingButton.next(broadcastingButton); this.broadcastingButton.next(broadcastingButton);
} }
@ -468,4 +493,22 @@ export class OpenViduComponentsConfigService {
setLayoutRemoteParticipants(participants: ParticipantModel[] | undefined) { setLayoutRemoteParticipants(participants: ParticipantModel[] | undefined) {
this.layoutRemoteParticipants.next(participants); this.layoutRemoteParticipants.next(participants);
} }
// Recording Activity Configuration
setRecordingActivityReadOnly(readOnly: boolean) {
this.recordingActivityReadOnly.next(readOnly);
}
isRecordingActivityReadOnly(): boolean {
return this.recordingActivityReadOnly.getValue();
}
setRecordingActivityShowControls(controls: { play?: boolean; download?: boolean; delete?: boolean }) {
this.recordingActivityShowControls.next(controls);
}
getRecordingActivityShowControls(): { play?: boolean; download?: boolean; delete?: boolean } {
return this.recordingActivityShowControls.getValue();
}
} }