openvidu-components: Emitted event for updating recording list

Non moderators participants didn't update their recording list because they didn't know when a recording has been deleted or stopped. Now all standard participants know all recording events using custom signals and emitted an event to the application with the aim of updating the recording list.
pull/803/head
Carlos Santos 2023-04-28 17:50:11 +02:00
parent 9be838308c
commit fb101bc2c7
12 changed files with 92 additions and 14 deletions

View File

@ -15,6 +15,7 @@
(onStartRecordingClicked)="_onStartRecordingClicked()" (onStartRecordingClicked)="_onStartRecordingClicked()"
(onStopRecordingClicked)="_onStopRecordingClicked()" (onStopRecordingClicked)="_onStopRecordingClicked()"
(onDeleteRecordingClicked)="_onDeleteRecordingClicked($event)" (onDeleteRecordingClicked)="_onDeleteRecordingClicked($event)"
(onForceRecordingUpdate)="_onForceRecordingUpdate()"
></ov-recording-activity> ></ov-recording-activity>
<ov-broadcasting-activity <ov-broadcasting-activity
*ngIf="showBroadcastingActivity" *ngIf="showBroadcastingActivity"

View File

@ -29,6 +29,13 @@ export class ActivitiesPanelComponent implements OnInit {
*/ */
@Output() onDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>(); @Output() onDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when a participant needs update the recordings information
* (usually when recording is stopped by the session moderator or recording panel is opened).
* The recordings should be updated using the REST API.
*/
@Output() onForceRecordingUpdate: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when start broadcasting button has been clicked. * Provides event notifications that fire when start broadcasting button has been clicked.
* The broadcasting should be started using the REST API. * The broadcasting should be started using the REST API.
@ -112,6 +119,10 @@ export class ActivitiesPanelComponent implements OnInit {
this.onStopBroadcastingClicked.emit(); this.onStopBroadcastingClicked.emit();
} }
_onForceRecordingUpdate() {
this.onForceRecordingUpdate.emit();
}
private subscribeToPanelToggling() { private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelOpenedObs.subscribe((ev: PanelEvent) => { this.panelSubscription = this.panelService.panelOpenedObs.subscribe((ev: PanelEvent) => {
if (ev.type === PanelType.ACTIVITIES && !!ev.expand) { if (ev.type === PanelType.ACTIVITIES && !!ev.expand) {

View File

@ -117,6 +117,7 @@
</button> </button>
<button <button
*ngIf="isSessionCreator"
mat-icon-button mat-icon-button
class="delete-recording-btn" class="delete-recording-btn"
id="delete-recording-btn" id="delete-recording-btn"

View File

@ -5,6 +5,8 @@ import { ActionService } from '../../../../services/action/action.service';
import { OpenViduAngularConfigService } from '../../../../services/config/openvidu-angular.config.service'; import { OpenViduAngularConfigService } from '../../../../services/config/openvidu-angular.config.service';
import { ParticipantService } from '../../../../services/participant/participant.service'; import { ParticipantService } from '../../../../services/participant/participant.service';
import { RecordingService } from '../../../../services/recording/recording.service'; import { RecordingService } from '../../../../services/recording/recording.service';
import { OpenViduService } from '../../../../services/openvidu/openvidu.service';
import { Signal } from '../../../../models/signal.model';
@Component({ @Component({
selector: 'ov-recording-activity', selector: 'ov-recording-activity',
@ -36,6 +38,13 @@ export class RecordingActivityComponent implements OnInit {
*/ */
@Output() onDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>(); @Output() onDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when a participant needs update the recordings information
* (usually when recording is stopped by the session moderator or recording panel is opened).
* The recordings should be updated using the REST API.
*/
@Output() onForceRecordingUpdate: EventEmitter<void> = new EventEmitter<void>();
/** /**
* @internal * @internal
*/ */
@ -76,6 +85,7 @@ export class RecordingActivityComponent implements OnInit {
private recordingStatusSubscription: Subscription; private recordingStatusSubscription: Subscription;
private recordingListSubscription: Subscription; private recordingListSubscription: Subscription;
private recordingErrorSub: Subscription; private recordingErrorSub: Subscription;
private forceRecordingUpdateSub: Subscription;
/** /**
* @internal * @internal
@ -85,6 +95,7 @@ export class RecordingActivityComponent implements OnInit {
private participantService: ParticipantService, private participantService: ParticipantService,
private libService: OpenViduAngularConfigService, private libService: OpenViduAngularConfigService,
private actionService: ActionService, private actionService: ActionService,
private openviduService: OpenViduService,
private cd: ChangeDetectorRef private cd: ChangeDetectorRef
) {} ) {}
@ -94,6 +105,7 @@ export class RecordingActivityComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.subscribeToRecordingStatus(); this.subscribeToRecordingStatus();
this.subscribeToRecordingActivityDirective(); this.subscribeToRecordingActivityDirective();
this.subscribeToForceRecordingUpdate();
this.isSessionCreator = this.participantService.amIModerator(); this.isSessionCreator = this.participantService.amIModerator();
} }
@ -104,6 +116,7 @@ export class RecordingActivityComponent implements OnInit {
if (this.recordingStatusSubscription) this.recordingStatusSubscription.unsubscribe(); if (this.recordingStatusSubscription) this.recordingStatusSubscription.unsubscribe();
if (this.recordingListSubscription) this.recordingListSubscription.unsubscribe(); if (this.recordingListSubscription) this.recordingListSubscription.unsubscribe();
if (this.recordingErrorSub) this.recordingErrorSub.unsubscribe(); if (this.recordingErrorSub) this.recordingErrorSub.unsubscribe();
if (this.forceRecordingUpdateSub) this.forceRecordingUpdateSub.unsubscribe();
} }
/** /**
@ -156,8 +169,10 @@ export class RecordingActivityComponent implements OnInit {
deleteRecording(id: string) { deleteRecording(id: string) {
const succsessCallback = () => { const succsessCallback = () => {
this.onDeleteRecordingClicked.emit(id); this.onDeleteRecordingClicked.emit(id);
// Sending signal to all participants with the aim of updating their recordings list
this.openviduService.sendSignal(Signal.RECORDING_DELETED, this.openviduService.getRemoteConnections());
}; };
this.actionService.openDeleteRecordingDialog(succsessCallback); this.actionService.openDeleteRecordingDialog(succsessCallback.bind(this));
} }
/** /**
@ -180,6 +195,10 @@ export class RecordingActivityComponent implements OnInit {
if (ev?.info) { if (ev?.info) {
if (this.recordingStatus !== RecordingStatus.FAILED) { if (this.recordingStatus !== RecordingStatus.FAILED) {
this.oldRecordingStatus = this.recordingStatus; this.oldRecordingStatus = this.recordingStatus;
if (ev.info.status === RecordingStatus.STOPPED && !this.isSessionCreator) {
// Force update recording list when recording is stopped by moderator
this.onForceRecordingUpdate.emit();
}
} }
this.recordingStatus = ev.info.status; this.recordingStatus = ev.info.status;
this.recordingAlive = ev.info.status === RecordingStatus.STARTED; this.recordingAlive = ev.info.status === RecordingStatus.STARTED;
@ -192,6 +211,8 @@ export class RecordingActivityComponent implements OnInit {
private subscribeToRecordingActivityDirective() { private subscribeToRecordingActivityDirective() {
this.recordingListSubscription = this.libService.recordingsListObs.subscribe((recordingList: RecordingInfo[]) => { this.recordingListSubscription = this.libService.recordingsListObs.subscribe((recordingList: RecordingInfo[]) => {
this.recordingsList = recordingList; this.recordingsList = recordingList;
// si envio aqui el signal, estos eventos serian infinitos
// this.openviduService.sendSignal(Signal.RECORDING_DELETED, this.openviduService.getRemoteConnections());
this.cd.markForCheck(); this.cd.markForCheck();
}); });
@ -202,4 +223,10 @@ export class RecordingActivityComponent implements OnInit {
} }
}); });
} }
private subscribeToForceRecordingUpdate() {
this.forceRecordingUpdateSub = this.recordingService.forceUpdateRecordingsObs.subscribe(() => {
this.onForceRecordingUpdate.emit();
});
}
} }

View File

@ -64,29 +64,23 @@ export class SessionComponent implements OnInit, OnDestroy {
@ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>; @ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>;
@ContentChild('panel', { read: TemplateRef }) panelTemplate: TemplateRef<any>; @ContentChild('panel', { read: TemplateRef }) panelTemplate: TemplateRef<any>;
@ContentChild('layout', { read: TemplateRef }) layoutTemplate: TemplateRef<any>; @ContentChild('layout', { read: TemplateRef }) layoutTemplate: TemplateRef<any>;
@Input() usedInPrejoinPage = false; @Input() usedInPrejoinPage = false;
@Output() onNodeCrashed = new EventEmitter<any>(); @Output() onNodeCrashed = new EventEmitter<any>();
session: Session; session: Session;
sessionScreen: Session; sessionScreen: Session;
sideMenu: MatSidenav; sideMenu: MatSidenav;
sidenavMode: SidenavMode = SidenavMode.SIDE; sidenavMode: SidenavMode = SidenavMode.SIDE;
settingsPanelOpened: boolean; settingsPanelOpened: boolean;
drawer: MatDrawerContainer; drawer: MatDrawerContainer;
preparing: boolean = true; preparing: boolean = true;
private isSessionCreator: boolean = false;
protected readonly SIDENAV_WIDTH_LIMIT_MODE = 790; protected readonly SIDENAV_WIDTH_LIMIT_MODE = 790;
protected menuSubscription: Subscription; protected menuSubscription: Subscription;
protected layoutWidthSubscription: Subscription; protected layoutWidthSubscription: Subscription;
protected updateLayoutInterval: NodeJS.Timer; protected updateLayoutInterval: NodeJS.Timer;
private captionLanguageSubscription: Subscription; private captionLanguageSubscription: Subscription;
protected log: ILogger; protected log: ILogger;
constructor( constructor(
@ -168,6 +162,7 @@ export class SessionComponent implements OnInit, OnDestroy {
async ngOnInit() { async ngOnInit() {
if (!this.usedInPrejoinPage) { if (!this.usedInPrejoinPage) {
this.isSessionCreator = this.participantService.amIModerator();
if (!this.openviduService.getScreenToken()) { if (!this.openviduService.getScreenToken()) {
// Hide screenshare button if screen token does not exist // Hide screenshare button if screen token does not exist
this.libService.screenshareButton.next(false); this.libService.screenshareButton.next(false);
@ -439,6 +434,11 @@ export class SessionComponent implements OnInit, OnDestroy {
private subscribeToBroadcastingEvents() { private subscribeToBroadcastingEvents() {
this.session.on('broadcastStarted', () => this.broadcastingService.startBroadcasting()); this.session.on('broadcastStarted', () => this.broadcastingService.startBroadcasting());
this.session.on('broadcastStopped', () => this.broadcastingService.stopBroadcasting()); this.session.on('broadcastStopped', () => this.broadcastingService.stopBroadcasting());
if (!this.isSessionCreator) {
// Listen to recording delete events from moderator
this.session.on(`signal:${Signal.RECORDING_DELETED}`, () => this.recordingService.forceUpdateRecordings());
}
} }
private startUpdateLayoutInterval() { private startUpdateLayoutInterval() {

View File

@ -100,6 +100,7 @@
(onStartRecordingClicked)="onStartRecordingClicked('panel')" (onStartRecordingClicked)="onStartRecordingClicked('panel')"
(onStopRecordingClicked)="onStopRecordingClicked('panel')" (onStopRecordingClicked)="onStopRecordingClicked('panel')"
(onDeleteRecordingClicked)="onDeleteRecordingClicked($event)" (onDeleteRecordingClicked)="onDeleteRecordingClicked($event)"
(onForceRecordingUpdate)="onForceRecordingUpdate()"
(onStartBroadcastingClicked)="onStartBroadcastingClicked($event)" (onStartBroadcastingClicked)="onStartBroadcastingClicked($event)"
(onStopBroadcastingClicked)="onStopBroadcastingClicked('panel')" (onStopBroadcastingClicked)="onStopBroadcastingClicked('panel')"
></ov-activities-panel> ></ov-activities-panel>

View File

@ -378,6 +378,13 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
*/ */
@Output() onActivitiesPanelDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>(); @Output() onActivitiesPanelDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when a participant needs update the recordings information
* (usually when recording is stopped by the session moderator or recording panel is opened) from {@link ActivitiesPanelComponent}.
* The recordings should be updated using the REST API.
*/
@Output() onActivitiesPanelForceRecordingUpdate: EventEmitter<void> = new EventEmitter<void>();
/** /**
* Provides event notifications that fire when play recording button is clicked from {@link ActivitiesPanelComponent}. * Provides event notifications that fire when play recording button is clicked from {@link ActivitiesPanelComponent}.
*/ */
@ -692,6 +699,14 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
this.onActivitiesPanelDeleteRecordingClicked.emit(recordingId); this.onActivitiesPanelDeleteRecordingClicked.emit(recordingId);
} }
/**
* @internal
*/
onForceRecordingUpdate() {
this.onActivitiesPanelForceRecordingUpdate.emit();
}
/** /**
* @internal * @internal
*/ */

View File

@ -3,5 +3,6 @@
*/ */
export enum Signal { export enum Signal {
NICKNAME_CHANGED = 'nicknameChanged', NICKNAME_CHANGED = 'nicknameChanged',
CHAT = 'chat' CHAT = 'chat',
RECORDING_DELETED = 'recordingDeleted',
} }

View File

@ -469,6 +469,9 @@ export class OpenViduService {
/** /**
* @internal * @internal
*
* @param type: type of signal
* @param connections: if undefined, the signal will be sent to all participants
*/ */
sendSignal(type: Signal, connections?: Connection[], data?: any): void { sendSignal(type: Signal, connections?: Connection[], data?: any): void {
const signalOptions: SignalOptions = { const signalOptions: SignalOptions = {
@ -611,7 +614,7 @@ export class OpenViduService {
needSendNicknameSignal(): boolean { needSendNicknameSignal(): boolean {
let oldNickname: string; let oldNickname: string;
try { try {
const connData = JSON.parse(this.webcamSession.connection.data.split('%/%')[0]); const connData = JSON.parse(this.cleanConnectionData(this.webcamSession.connection.data));
oldNickname = connData.clientData; oldNickname = connData.clientData;
} catch (error) { } catch (error) {
this.log.e(error); this.log.e(error);
@ -637,7 +640,7 @@ export class OpenViduService {
// Avoid screen connections // Avoid screen connections
const remoteCameraConnections: Connection[] = Array.from(this.webcamSession.remoteConnections.values()).filter((conn) => { const remoteCameraConnections: Connection[] = Array.from(this.webcamSession.remoteConnections.values()).filter((conn) => {
let type: VideoType; let type: VideoType;
type = JSON.parse(conn.data).type; type = JSON.parse(this.cleanConnectionData(conn.data)).type;
return type !== VideoType.SCREEN; return type !== VideoType.SCREEN;
}); });
return remoteCameraConnections; return remoteCameraConnections;
@ -656,4 +659,8 @@ export class OpenViduService {
} }
} }
} }
private cleanConnectionData(data: string): string {
return data.split('%/%')[0];
}
} }

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RecordingEvent } from 'openvidu-browser'; import { RecordingEvent } from 'openvidu-browser';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { RecordingInfo, RecordingStatus } from '../../models/recording.model'; import { RecordingInfo, RecordingStatus } from '../../models/recording.model';
import { ActionService } from '../action/action.service'; import { ActionService } from '../action/action.service';
@ -13,6 +13,10 @@ export class RecordingService {
*/ */
recordingStatusObs: Observable<{ info: RecordingInfo; time?: Date } | undefined>; recordingStatusObs: Observable<{ info: RecordingInfo; time?: Date } | undefined>;
/**
* @internal
*/
forceUpdateRecordingsObs: Subject<void> = new Subject();
private recordingTime: Date | undefined; private recordingTime: Date | undefined;
private recordingTimeInterval: NodeJS.Timer; private recordingTimeInterval: NodeJS.Timer;
private currentRecording: RecordingInfo = { status: RecordingStatus.STOPPED }; private currentRecording: RecordingInfo = { status: RecordingStatus.STOPPED };
@ -86,7 +90,6 @@ export class RecordingService {
const recordingId = recording.id; const recordingId = recording.id;
// Only COMPOSED recording is supported. The extension will allways be 'mp4'. // Only COMPOSED recording is supported. The extension will allways be 'mp4'.
const extension = 'mp4'; //recording.url?.split('.').pop() || 'mp4'; const extension = 'mp4'; //recording.url?.split('.').pop() || 'mp4';
const link = document.createElement('a'); const link = document.createElement('a');
link.href = `/recordings/${recordingId}/${recordingId}.${extension}`; link.href = `/recordings/${recordingId}/${recordingId}.${extension}`;
link.download = `${recordingId}.${extension}`; link.download = `${recordingId}.${extension}`;
@ -104,6 +107,10 @@ export class RecordingService {
}, 100); }, 100);
} }
forceUpdateRecordings() {
this.forceUpdateRecordingsObs.next();
}
private startRecordingTime() { private startRecordingTime() {
this.recordingTime = new Date(); this.recordingTime = new Date();
this.recordingTime.setHours(0, 0, 0, 0); this.recordingTime.setHours(0, 0, 0, 0);

View File

@ -4,7 +4,7 @@
[lang]="'en'" [lang]="'en'"
[captionsLang]="" [captionsLang]=""
[captionsLangOptions]="[{ name: 'Spanish', ISO: 'es-ES' },{ name: 'English', ISO: 'en-US' }]" [captionsLangOptions]="[{ name: 'Spanish', ISO: 'es-ES' },{ name: 'English', ISO: 'en-US' }]"
[prejoin]="false" [prejoin]="true"
[participantName]="'Participant'" [participantName]="'Participant'"
[videoMuted]="false" [videoMuted]="false"
[audioMuted]="false" [audioMuted]="false"
@ -41,6 +41,7 @@
(onActivitiesPanelStartRecordingClicked)="onStartRecordingClicked()" (onActivitiesPanelStartRecordingClicked)="onStartRecordingClicked()"
(onActivitiesPanelStopRecordingClicked)="onStopRecordingClicked()" (onActivitiesPanelStopRecordingClicked)="onStopRecordingClicked()"
(onActivitiesPanelDeleteRecordingClicked)="onDeleteRecordingClicked($event)" (onActivitiesPanelDeleteRecordingClicked)="onDeleteRecordingClicked($event)"
(onActivitiesPanelForceRecordingUpdate)="onForceRecordingUpdate()"
(onActivitiesPanelStartBroadcastingClicked)="onStartBroadcastingClicked($event)" (onActivitiesPanelStartBroadcastingClicked)="onStartBroadcastingClicked($event)"
(onActivitiesPanelStopBroadcastingClicked)="onStopBroadcastingClicked()" (onActivitiesPanelStopBroadcastingClicked)="onStopBroadcastingClicked()"
(onToolbarStopBroadcastingClicked)="onStopBroadcastingClicked()" (onToolbarStopBroadcastingClicked)="onStopBroadcastingClicked()"

View File

@ -123,6 +123,7 @@ export class CallComponent implements OnInit {
this.recordingList = await this.restService.stopRecording(this.sessionId); this.recordingList = await this.restService.stopRecording(this.sessionId);
console.log('RECORDING LIST ', this.recordingList); console.log('RECORDING LIST ', this.recordingList);
} catch (error) { } catch (error) {
console.log(await this.restService.getRecordings())
this.recordingError = error; this.recordingError = error;
} }
} }
@ -137,6 +138,11 @@ export class CallComponent implements OnInit {
} }
} }
async onForceRecordingUpdate() {
console.warn('FORCE RECORDING UPDATE');
this.recordingList = await this.restService.getRecordings();
}
private async requestForTokens() { private async requestForTokens() {
const { broadcastingEnabled, recordingEnabled, recordings, cameraToken, screenToken, isRecordingActive, isBroadcastingActive } = const { broadcastingEnabled, recordingEnabled, recordings, cameraToken, screenToken, isRecordingActive, isBroadcastingActive } =
await this.restService.getTokensFromBackend(this.sessionId); await this.restService.getTokensFromBackend(this.sessionId);