openvidu-components: Added recording

openvidu-components: Fixed recording directive name
pull/732/head
csantosm 2022-06-02 10:57:47 +02:00
parent 206a44d881
commit 44110a6246
50 changed files with 1612 additions and 313 deletions

View File

@ -0,0 +1,24 @@
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
/**
* @internal
*/
@Component({
selector: 'app-delete-dialog',
template: `
<div mat-dialog-content>{{'PANEL.RECORDING.DELETE_QUESTION' | translate}}</div>
<div mat-dialog-actions>
<button mat-button (click)="close()">CANCEL</button>
<button mat-button cdkFocusInitial (click)="close(true)" id="delete-recording-confirm-btn">{{'PANEL.RECORDING.DELETE' | translate}}</button>
</div>
`,
styles: [``]
})
export class DeleteDialogComponent {
constructor(public dialogRef: MatDialogRef<DeleteDialogComponent>) {}
close(succsess = false) {
this.dialogRef.close(succsess);
}
}

View File

@ -0,0 +1,39 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
/**
* @internal
*/
@Component({
selector: 'app-recording-dialog',
template: `
<div mat-dialog-content>
<video controls autoplay>
<source [src]="src" [type]="type" />
</video>
</div>
<div mat-dialog-actions *ngIf="data.showActionButtons" align="end">
<button mat-button (click)="close()">{{ 'PANEL.CLOSE' | translate }}</button>
</div>
`,
styles: [
`
video {
max-height: 64vh;
max-width: 100%;
}
`
]
})
export class RecordingDialogComponent {
src: string;
type: string;
constructor(public dialogRef: MatDialogRef<RecordingDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {
this.src = data.src;
this.type = data.type;
}
close() {
this.dialogRef.close();
}
}

View File

@ -35,6 +35,7 @@
::ng-deep .mat-expansion-panel-header {
padding: 0px 10px !important;
height: 65px !important;
}
::ng-deep .mat-list-base .mat-list-item .mat-list-item-content,
@ -44,6 +45,7 @@
::ng-deep mat-expansion-panel .mat-expansion-panel-body {
padding: 0px !important;
min-height: 400px;
}
::ng-deep .mat-expansion-panel-header-description {
flex-grow: 0 !important;

View File

@ -1,14 +1,23 @@
<div class="panel-container" id="activities-container" fxLayout="column" fxLayoutAlign="space-evenly none">
<div class="panel-header-container" fxFlex="55px" fxLayoutAlign="start center">
<h3 class="panel-title">Activities</h3>
<button class="panel-close-button" mat-icon-button matTooltip="Close" (click)="close()">
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="activities-body-container" fxFlex="75%" fxLayoutAlign="space-evenly none">
<mat-accordion>
<ov-recording-activity></ov-recording-activity>
<ov-recording-activity
*ngIf="showRecordingActivity"
id="recording-activity"
[expanded]="expandedPanel === 'recording'"
(onStartRecordingClicked)="_onStartRecordingClicked()"
(onStopRecordingClicked)="_onStopRecordingClicked()"
(onDownloadRecordingClicked)="_onDownloadRecordingClicked($event)"
(onDeleteRecordingClicked)="_onDeleteRecordingClicked($event)"
(onPlayRecordingClicked)="_onPlayRecordingClicked($event)"
></ov-recording-activity>
</mat-accordion>
</div>
</div>

View File

@ -1,23 +1,131 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { PanelType } from '../../../models/panel.model';
import { OpenViduAngularConfigService } from '../../../services/config/openvidu-angular.config.service';
import { PanelService } from '../../../services/panel/panel.service';
/**
* @internal
*/
@Component({
selector: 'ov-activities-panel',
templateUrl: './activities-panel.component.html',
styleUrls: ['../panel.component.css','./activities-panel.component.css']
styleUrls: ['../panel.component.css', './activities-panel.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ActivitiesPanelComponent implements OnInit {
constructor(private panelService: PanelService) {}
/**
* Provides event notifications that fire when start recording button has been clicked.
* The recording should be stated using the OpenVidu REST API.
*/
@Output() onStartRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
ngOnInit(): void {}
/**
* Provides event notifications that fire when stop recording button has been clicked.
* The recording should be stopped using the OpenVidu REST API.
*/
@Output() onStopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
ngOnDestroy() {}
/**
* Provides event notifications that fire when download recording button has been clicked.
* The recording should be downloaded using the OpenVidu REST API.
*/
@Output() onDownloadRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when delete recording button has been clicked.
* The recording should be deleted using the OpenVidu REST API.
*/
@Output() onDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when play recording button has been clicked.
*/
@Output() onPlayRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* @internal
*/
expandedPanel: string = '';
/**
* @internal
*/
showRecordingActivity: boolean = true;
private panelSubscription: Subscription;
private recordingActivitySub: Subscription;
/**
* @internal
*/
constructor(private panelService: PanelService, private libService: OpenViduAngularConfigService, private cd: ChangeDetectorRef) {}
/**
* @internal
*/
ngOnInit(): void {
this.subscribeToPanelToggling();
this.subscribeToActivitiesPanelDirective();
}
/**
* @internal
*/
ngOnDestroy() {
if (this.panelSubscription) this.panelSubscription.unsubscribe();
if (this.recordingActivitySub) this.recordingActivitySub.unsubscribe();
}
/**
* @internal
*/
close() {
this.panelService.togglePanel(PanelType.ACTIVITIES);
}
/**
* @internal
*/
_onStartRecordingClicked() {
this.onStartRecordingClicked.emit();
}
/**
* @internal
*/
_onStopRecordingClicked() {
this.onStopRecordingClicked.emit();
}
/**
* @internal
*/
_onDownloadRecordingClicked(recordingId: string) {
this.onDownloadRecordingClicked.emit(recordingId);
}
/**
* @internal
*/
_onDeleteRecordingClicked(recordingId: string) {
this.onDeleteRecordingClicked.emit(recordingId);
}
/**
* @internal
*/
_onPlayRecordingClicked(recordingId: string) {
this.onPlayRecordingClicked.emit(recordingId);
}
private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelOpenedObs.subscribe(
(ev: { opened: boolean; type?: PanelType | string; expand?: string }) => {
if (ev.type === PanelType.ACTIVITIES) {
this.expandedPanel = ev.expand;
}
}
);
}
private subscribeToActivitiesPanelDirective() {
this.recordingActivitySub = this.libService.recordingActivity.subscribe((value: boolean) => {
this.showRecordingActivity = value;
this.cd.markForCheck();
});
}
}

View File

@ -1,25 +1,89 @@
#recording-status {
color: var(--ov-text-color);
display: inline;
padding: 3px;
font-size: 11px;
padding: 5px;
font-size: 12px;
border-radius: var(--ov-panel-radius);
}
.time-container {
padding: 2px;
}
.recording-icon {
font-size: 32px !important;
height: 32px !important;
width: 32px !important;
}
.recording-duration {
background-color: var(--ov-light-color);
padding: 4px 8px;
border-radius: var(--ov-panel-radius);
font-weight: 500;
}
.recording-duration mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.started {
background-color: #005704;
background-color: #3b7430 !important;
color: var(--ov-text-color);
}
.activity-icon.started, .failed {
background-color: var(--ov-warn-color) !important;
color: var(--ov-text-color);
}
.stopped {
background-color: var(--ov-light-color);
color: var(--ov-panel-text-color) !important;
}
#recording-file-item {
padding: 0px 16px;
.pending {
background-color: #ffd79b !important;
color: var(--ov-panel-text-color) !important;
}
.recording-action-buttons{
margin: auto;
.panel-body-container {
padding: 10px;
}
.panel-body-container > .content {
align-items: stretch;
justify-content: center;
display: flex;
flex-direction: column;
box-flex: 1;
flex-grow: 1;
text-align: center;
}
.recording-error {
color: var(--ov-warn-color);
font-weight: 600;
}
.recording-name {
font-size: 16px;
font-weight: bold;
}
.recording-date {
font-size: 12px !important;
font-style: italic;
}
.not-allowed-message {
margin-top: 10px;
font-weight: bold;
}
.recording-action-buttons {
margin-top: 20px;
margin-bottom: 20px;
}
#start-recording-btn {
@ -32,6 +96,17 @@
color: var(--ov-text-color);
}
.delete-recording-btn {
color: var(--ov-warn-color);
}
mat-expansion-panel {
margin: 0pc 0px 15px 0px;
margin: 0px 0px 15px 0px;
}
.blink {
animation: blinker 1.5s linear infinite !important;
}
@keyframes blinker {
50% { opacity: 0.4; }
}

View File

@ -1,89 +1,203 @@
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import { Component, OnInit, Output, EventEmitter, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { RecordingStatus } from '../../../../models/recording.model';
import { RecordingService, RecordingInfo } from '../../../../services/recording/recording.service';
import { OpenViduRole } from '../../../../models/participant.model';
import { RecordingInfo, RecordingStatus } from '../../../../models/recording.model';
import { ActionService } from '../../../../services/action/action.service';
import { OpenViduAngularConfigService } from '../../../../services/config/openvidu-angular.config.service';
import { ParticipantService } from '../../../../services/participant/participant.service';
import { RecordingService } from '../../../../services/recording/recording.service';
/**
* @internal
*/
@Component({
selector: 'ov-recording-activity',
templateUrl: './recording-activity.component.html',
styleUrls: ['./recording-activity.component.css', '../activities-panel.component.css']
styleUrls: ['./recording-activity.component.css', '../activities-panel.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RecordingActivityComponent implements OnInit {
/**
* @internal
*/
@Input() expanded: boolean;
/**
* Provides event notifications that fire when start recording button has been clicked.
* The recording should be stopped using the REST API.
*/
@Output() startRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
@Output() onStartRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when stop recording button has been clicked.
* The recording should be stopped using the REST API.
*/
@Output() stopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
recStatusEnum = RecordingStatus;
isSessionCreator = true;
recording: RecordingInfo;
recordingSubscription: Subscription;
@Output() onStopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when download recording button has been clicked.
* The recording should be downloaded using the REST API.
*/
@Output() onDownloadRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when delete recording button has been clicked.
* The recording should be deleted using the REST API.
*/
@Output() onDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when play recording button has been clicked.
*/
@Output() onPlayRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* @internal
*/
recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @internal
*/
opened: boolean = false;
constructor(private recordingService: RecordingService) {}
/**
* @internal
*/
recStatusEnum = RecordingStatus;
/**
* @internal
*/
isSessionCreator = false;
/**
* @internal
*/
liveRecording: RecordingInfo;
/**
* @internal
*/
recordingsList: RecordingInfo[] = [];
/**
* @internal
*/
recordingError: any;
private recordingStatusSubscription: Subscription;
private recordingListSubscription: Subscription;
private recordingErrorSub: Subscription;
/**
* @internal
*/
constructor(
private recordingService: RecordingService,
private participantService: ParticipantService,
private libService: OpenViduAngularConfigService,
private actionService: ActionService,
private cd: ChangeDetectorRef
) {}
/**
* @internal
*/
ngOnInit(): void {
this.subscribeToRecordingStatus();
this.subscribeToRecordingActivityDirective();
this.isSessionCreator = this.participantService.getMyRole() === OpenViduRole.MODERATOR;
}
/**
* @internal
*/
ngOnDestroy() {
if (this.recordingSubscription) this.recordingSubscription.unsubscribe();
if (this.recordingStatusSubscription) this.recordingStatusSubscription.unsubscribe();
if (this.recordingListSubscription) this.recordingListSubscription.unsubscribe();
if (this.recordingErrorSub) this.recordingErrorSub.unsubscribe();
}
/**
* @internal
*/
panelOpened() {
//TODO EMITIR EVENTO
//TODO emit event
this.opened = true;
}
/**
* @internal
*/
panelClosed() {
//TODO EMITIR EVENTO
//TODO emit event
this.opened = false;
}
/**
* @internal
*/
startRecording() {
console.log('START RECORDING');
this.startRecordingClicked.emit();
//TODO: REMOVE
const info: RecordingInfo = {
status: RecordingStatus.STARTED,
id: '1',
name: 'akajo',
reason: null
};
this.recordingService.startRecording(<any>info);
}
stopRecording() {
console.log('STOP RECORDING');
this.stopRecordingClicked.emit();
//TODO: REMOVE
const info: RecordingInfo = {
status: RecordingStatus.STOPPED,
id: '1',
name: 'akajo',
reason: 'lalal'
};
this.recordingService.stopRecording(<any>info);
this.onStartRecordingClicked.emit();
this.recordingService.updateStatus(RecordingStatus.STARTING);
}
subscribeToRecordingStatus() {
this.recordingSubscription = this.recordingService.recordingStatusObs.subscribe((info: RecordingInfo) => {
if (info) {
this.recordingStatus = info.status;
if (info.status === RecordingStatus.STARTED) {
this.recording = info;
} else {
this.recording = null;
/**
* @internal
*/
stopRecording() {
this.onStopRecordingClicked.emit();
this.recordingService.updateStatus(RecordingStatus.STOPPING);
}
/**
* @internal
*/
deleteRecording(id: string) {
const succsessCallback = () => {
this.onDeleteRecordingClicked.emit(id);
};
this.actionService.openDeleteRecordingDialog(succsessCallback);
}
/**
* @internal
*/
download(recordingId: string) {
this.onDownloadRecordingClicked.emit(recordingId);
}
/**
* @internal
*/
play(recordingId: string) {
this.onPlayRecordingClicked.emit(recordingId);
}
private subscribeToRecordingStatus() {
this.recordingStatusSubscription = this.recordingService.recordingStatusObs.subscribe(
(ev: { info: RecordingInfo; time?: Date }) => {
if (ev?.info) {
this.recordingStatus = ev.info.status;
if (ev.info.status === RecordingStatus.STARTED) {
this.liveRecording = ev.info;
} else {
this.liveRecording = null;
}
}
this.cd.markForCheck();
}
);
}
private subscribeToRecordingActivityDirective() {
this.recordingListSubscription = this.libService.recordingsListObs.subscribe((recordingList: RecordingInfo[]) => {
this.recordingsList = recordingList;
this.cd.markForCheck();
});
this.recordingErrorSub = this.libService.recordingErrorObs.subscribe((error: any) => {
console.log(error);
if (error) {
this.recordingService.updateStatus(RecordingStatus.FAILED);
this.recordingError = error.error?.message || error.message || error;
console.log('REC ERROR', this.recordingError)
}
});
}

View File

@ -20,6 +20,12 @@ export class BackgroundEffectsPanelComponent implements OnInit {
private backgrounds: BackgroundEffect[];
private backgroundSubs: Subscription;
/**
* @internal
* @param panelService
* @param backgroundService
* @param cd
*/
constructor(private panelService: PanelService, private backgroundService: VirtualBackgroundService, private cd: ChangeDetectorRef) { }
ngOnInit(): void {

View File

@ -176,11 +176,13 @@ export class SessionComponent implements OnInit {
this.updateLayoutInterval = setInterval(() => this.layoutService.update(), 50);
});
this.menuSubscription = this.panelService.panelOpenedObs.pipe(skip(1)).subscribe((ev: { opened: boolean; type?: PanelType }) => {
if (this.sideMenu) {
ev.opened ? this.sideMenu.open() : this.sideMenu.close();
}
});
this.menuSubscription = this.panelService.panelOpenedObs
.pipe(skip(1))
.subscribe((ev: { opened: boolean; type?: PanelType | string }) => {
if (this.sideMenu) {
ev.opened ? this.sideMenu.open() : this.sideMenu.close();
}
});
}
protected subscribeToLayoutWidth() {

View File

@ -113,16 +113,4 @@
input {
caret-color: #ffffff !important;
}
.poster_img {
position: absolute;
z-index: 888;
max-width: 70%;
max-height: 70%;
bottom: 0;
top: 0;
margin: auto;
right: 0;
left: 0;
}

View File

@ -70,8 +70,8 @@
flex-direction: column;
}
#recording-tag {
padding: 0 15px;
.recording-tag {
padding: 0 10px;
background-color: var(--ov-warn-color);
border-radius: var(--ov-panel-radius);
width: fit-content;
@ -79,17 +79,18 @@
text-align: center;
line-height: 20px;
margin: auto;
animation: blinker 1.5s linear infinite;
}
#recording-tag mat-icon {
.recording-tag mat-icon {
font-size: 16px;
display: inline;
vertical-align: sub;
margin-right: 5px;
}
.blink {
animation: blinker 1.5s linear infinite;
}
#point {
width: 10px;
height: 10px;
@ -140,5 +141,5 @@
}
@keyframes blinker {
50% { opacity: 0.2; }
}
50% { opacity: 0.3; }
}

View File

@ -2,11 +2,12 @@
<div fxFlex="20%" fxLayoutAlign="start center" id="info-container">
<div>
<img *ngIf="!isMinimal && showLogo" id="branding-logo" src="assets/images/logo.png" ovLogo />
<div id="session-info-container" [class.colapsed]="isRecording">
<div id="session-info-container" [class.colapsed]="recordingStatus === _recordingStatus.STARTED">
<span id="session-name" *ngIf="!isMinimal && session && session.sessionId && showSessionName">{{ session.sessionId }}</span>
<div id="recording-tag" *ngIf="isRecording">
<mat-icon>radio_button_checked</mat-icon>
<span>REC</span>
<div *ngIf="recordingStatus === _recordingStatus.STARTED" id="recording-tag" class="recording-tag">
<mat-icon class="blink">radio_button_checked</mat-icon>
<span class="blink">REC</span>
<span> | {{ recordingTime | date: 'H:mm:ss' }}</span>
</div>
</div>
</div>
@ -33,7 +34,9 @@
[class.warn-btn]="!isWebcamVideoActive"
>
<mat-icon *ngIf="isWebcamVideoActive" matTooltip="{{ 'TOOLBAR.MUTE_VIDEO' | translate }}" id="videocam">videocam</mat-icon>
<mat-icon *ngIf="!isWebcamVideoActive" matTooltip="{{ 'TOOLBAR.UNMUTE_VIDEO' | translate }}" id="videocam_off">videocam_off</mat-icon>
<mat-icon *ngIf="!isWebcamVideoActive" matTooltip="{{ 'TOOLBAR.UNMUTE_VIDEO' | translate }}" id="videocam_off"
>videocam_off</mat-icon
>
</button>
<!-- Screenshare button -->
@ -49,31 +52,19 @@
<mat-icon *ngIf="isScreenShareActive" matTooltip="{{ 'TOOLBAR.DISABLE_SCREEN' | translate }}">screen_share</mat-icon>
</button>
<!-- Fullscreen button -->
<button
mat-icon-button
id="fullscreen-btn"
*ngIf="!isMinimal && showFullscreenButton && !showBackgroundEffectsButton"
(click)="toggleFullscreen()"
[disabled]="isConnectionLost"
[class.active-btn]="isFullscreenActive"
>
<mat-icon *ngIf="isFullscreenActive" matTooltip="{{ 'TOOLBAR.EXIT_FULLSCREEN' | translate }}">fullscreen_exit</mat-icon>
<mat-icon *ngIf="!isFullscreenActive" matTooltip="{{ 'TOOLBAR.FULLSCREEN' | translate }}">fullscreen</mat-icon>
</button>
<!-- More options button -->
<button
mat-icon-button
id="more-options-btn"
*ngIf="!isMinimal && showBackgroundEffectsButton"
*ngIf="!isMinimal && showMoreOptionsButton"
[matMenuTriggerFor]="menu"
[disabled]="isConnectionLost"
>
<mat-icon matTooltip="{{ 'TOOLBAR.MORE_OPTIONS' | translate }}">more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<mat-menu #menu="matMenu" id="more-options-menu">
<!-- Fullscreen button -->
<button mat-menu-item id="fullscreen-btn" (click)="toggleFullscreen()">
<button *ngIf="showFullscreenButton" mat-menu-item id="fullscreen-btn" (click)="toggleFullscreen()">
<mat-icon *ngIf="!isFullscreenActive">fullscreen</mat-icon>
<span *ngIf="!isFullscreenActive">{{ 'TOOLBAR.FULLSCREEN' | translate }}</span>
@ -82,15 +73,23 @@
</button>
<!-- Recording button -->
<!-- <button
*ngIf="!isMinimal && showActivitiesPanelButton"
<button
*ngIf="!isMinimal && showRecordingButton"
mat-menu-item
id="recording-btn"
(click)="toggleActivitiesPanel('recording')"
[disabled]="
recordingStatus === _recordingStatus.STARTING || recordingStatus === _recordingStatus.STOPPING || !isSessionCreator
"
(click)="toggleRecording()"
>
<mat-icon color="warn">radio_button_checked</mat-icon>
<span>Recording</span>
</button> -->
<span *ngIf="recordingStatus === _recordingStatus.STOPPED || recordingStatus === _recordingStatus.STOPPING">
{{ 'TOOLBAR.START_RECORDING' | translate }}
</span>
<span *ngIf="recordingStatus === _recordingStatus.STARTED || recordingStatus === _recordingStatus.STARTING">
{{ 'TOOLBAR.STOP_RECORDING' | translate }}
</span>
</button>
<!-- Virtual background button -->
<button
@ -117,17 +116,17 @@
</div>
<div fxFlex="20%" fxFlexOrder="3" fxLayoutAlign="end center" id="menu-buttons-container">
<!-- Default activities button -->
<!-- <button
<button
mat-icon-button
id="activities-panel-btn"
*ngIf="!isMinimal && showActivitiesPanelButton"
matTooltip="Activities"
matTooltip="{{ 'TOOLBAR.ACTIVITIES' | translate }}"
(click)="toggleActivitiesPanel()"
[disabled]="isConnectionLost"
[class.active-btn]="isActivitiesOpened"
>
<mat-icon>category</mat-icon>
</button> -->
</button>
<!-- Default participants button -->
<button

View File

@ -31,11 +31,11 @@ import {
ToolbarAdditionalButtonsDirective,
ToolbarAdditionalPanelButtonsDirective
} from '../../directives/template/openvidu-angular.directive';
import { ParticipantAbstractModel } from '../../models/participant.model';
import { OpenViduRole, ParticipantAbstractModel } from '../../models/participant.model';
import { PlatformService } from '../../services/platform/platform.service';
import { MatMenuTrigger } from '@angular/material/menu';
import { RecordingInfo, RecordingService } from '../../services/recording/recording.service';
import { RecordingStatus } from '../../models/recording.model';
import { RecordingService } from '../../services/recording/recording.service';
import { RecordingInfo, RecordingStatus } from '../../models/recording.model';
import { TranslateService } from '../../services/translate/translate.service';
/**
@ -165,17 +165,24 @@ export class ToolbarComponent implements OnInit, OnDestroy {
*/
@Output() onParticipantsPanelButtonClicked: EventEmitter<void> = new EventEmitter<any>();
/**
* @internal
* TODO: WIP
* Provides event notifications that fire when background effects button has been clicked.
*/
// @Output() onBackgroundEffectsButtonClicked: EventEmitter<void> = new EventEmitter<any>();
/**
* Provides event notifications that fire when chat panel button has been clicked.
*/
@Output() onChatPanelButtonClicked: EventEmitter<void> = new EventEmitter<any>();
/**
* Provides event notifications that fire when activities panel button has been clicked.
*/
@Output() onActivitiesPanelButtonClicked: EventEmitter<void> = new EventEmitter<any>();
/**
* Provides event notifications that fire when start recording button has been clicked.
*/
@Output() onStartRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when stop recording button has been clicked.
*/
@Output() onStopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* @ignore
*/
@ -257,6 +264,17 @@ export class ToolbarComponent implements OnInit, OnDestroy {
* @ignore
*/
showLeaveButton: boolean = true;
/**
* @ignore
*/
showRecordingButton: boolean = true;
/**
* @ignore
*/
showMoreOptionsButton: boolean = true;
/**
* @ignore
*/
@ -286,7 +304,21 @@ export class ToolbarComponent implements OnInit, OnDestroy {
/**
* @ignore
*/
isRecording: boolean = false;
recordingStatus: RecordingStatus = RecordingStatus.STOPPED;
/**
* @ignore
*/
_recordingStatus = RecordingStatus;
/**
* @ignore
*/
recordingTime: Date;
/**
* @ignore
*/
isSessionCreator: boolean = false;
private log: ILogger;
private minimalSub: Subscription;
@ -297,8 +329,8 @@ export class ToolbarComponent implements OnInit, OnDestroy {
private fullscreenButtonSub: Subscription;
private backgroundEffectsButtonSub: Subscription;
private leaveButtonSub: Subscription;
private recordingButtonSub: Subscription;
private recordingSubscription: Subscription;
private activitiesPanelButtonSub: Subscription;
private participantsPanelButtonSub: Subscription;
private chatPanelButtonSub: Subscription;
@ -375,6 +407,7 @@ export class ToolbarComponent implements OnInit, OnDestroy {
if (this.fullscreenButtonSub) this.fullscreenButtonSub.unsubscribe();
if (this.backgroundEffectsButtonSub) this.backgroundEffectsButtonSub.unsubscribe();
if (this.leaveButtonSub) this.leaveButtonSub.unsubscribe();
if (this.recordingButtonSub) this.recordingButtonSub.unsubscribe();
if (this.participantsPanelButtonSub) this.participantsPanelButtonSub.unsubscribe();
if (this.chatPanelButtonSub) this.chatPanelButtonSub.unsubscribe();
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
@ -393,7 +426,10 @@ export class ToolbarComponent implements OnInit, OnDestroy {
await this.openviduService.publishAudio(!this.isAudioActive);
} catch (error) {
this.log.e('There was an error toggling microphone:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.TOGGLE_MICROPHONE'), error?.error || error?.message || error);
this.actionService.openDialog(
this.translateService.translate('ERRORS.TOGGLE_MICROPHONE'),
error?.error || error?.message || error
);
}
}
@ -427,7 +463,10 @@ export class ToolbarComponent implements OnInit, OnDestroy {
} catch (error) {
this.log.e('There was an error toggling screen share', error.code, error.message);
if (error && error.name === 'SCREEN_SHARING_NOT_SUPPORTED') {
this.actionService.openDialog(this.translateService.translate('ERRORS.SCREEN_SHARING'), this.translateService.translate('ERRORS.SCREEN_SUPPORT'));
this.actionService.openDialog(
this.translateService.translate('ERRORS.SCREEN_SHARING'),
this.translateService.translate('ERRORS.SCREEN_SUPPORT')
);
}
}
}
@ -442,19 +481,27 @@ export class ToolbarComponent implements OnInit, OnDestroy {
}
/**
* TODO: WIP
* @ignore
*/
toggleActivitiesPanel(expandPanel: string) {
// this.onActivitiesPanelButtonClicked.emit();
// this.panelService.togglePanel(PanelType.ACTIVITIES);
toggleRecording() {
if (this.recordingStatus === RecordingStatus.STARTED) {
this.log.d('Stopping recording');
this.onStopRecordingClicked.emit();
this.recordingService.updateStatus(RecordingStatus.STOPPING);
} else if (this.recordingStatus === RecordingStatus.STOPPED) {
this.onStartRecordingClicked.emit();
this.recordingService.updateStatus(RecordingStatus.STARTING);
if (this.showActivitiesPanelButton && !this.isActivitiesOpened) {
this.toggleActivitiesPanel('recording');
}
}
}
/**
* @ignore
*/
toggleBackgroundEffects() {
// this.onBackgroundEffectsButtonClicked.emit();
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
@ -483,6 +530,11 @@ export class ToolbarComponent implements OnInit, OnDestroy {
this.onFullscreenButtonClicked.emit();
}
private toggleActivitiesPanel(expandPanel: string) {
this.onActivitiesPanelButtonClicked.emit();
this.panelService.togglePanel(PanelType.ACTIVITIES, expandPanel);
}
protected subscribeToReconnection() {
this.session.on('reconnecting', () => {
if (this.panelService.isPanelOpened()) {
@ -495,14 +547,17 @@ export class ToolbarComponent implements OnInit, OnDestroy {
});
}
protected subscribeToMenuToggling() {
this.panelTogglingSubscription = this.panelService.panelOpenedObs.subscribe((ev: { opened: boolean; type?: PanelType }) => {
this.isChatOpened = ev.opened && ev.type === PanelType.CHAT;
this.isParticipantsOpened = ev.opened && ev.type === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.opened && ev.type === PanelType.ACTIVITIES;
if (this.isChatOpened) {
this.unreadMessages = 0;
this.panelTogglingSubscription = this.panelService.panelOpenedObs.subscribe(
(ev: { opened: boolean; type?: PanelType | string }) => {
this.isChatOpened = ev.opened && ev.type === PanelType.CHAT;
this.isParticipantsOpened = ev.opened && ev.type === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.opened && ev.type === PanelType.ACTIVITIES;
if (this.isChatOpened) {
this.unreadMessages = 0;
}
this.cd.markForCheck();
}
});
);
}
protected subscribeToChatMessages() {
@ -520,17 +575,20 @@ export class ToolbarComponent implements OnInit, OnDestroy {
this.isWebcamVideoActive = p.isCameraVideoActive();
this.isAudioActive = p.isCameraAudioActive() || p.isScreenAudioActive();
this.isScreenShareActive = p.isScreenActive();
this.isSessionCreator = p.getRole() === OpenViduRole.MODERATOR;
this.cd.markForCheck();
}
});
}
private subscribeToRecordingStatus() {
//TODO: WIP
// this.recordingSubscription = this.recordingService.recordingStatusObs.pipe(skip(1)).subscribe((info: RecordingInfo) => {
// this.isRecording = info.status === RecordingStatus.STARTED;
// this.cd.markForCheck();
// });
this.recordingSubscription = this.recordingService.recordingStatusObs
.pipe(skip(1))
.subscribe((ev: { info: RecordingInfo; time?: Date }) => {
this.recordingStatus = ev.info.status;
this.recordingTime = ev.time;
this.cd.markForCheck();
});
}
private subscribeToToolbarDirectives() {
@ -544,12 +602,19 @@ export class ToolbarComponent implements OnInit, OnDestroy {
});
this.fullscreenButtonSub = this.libService.fullscreenButtonObs.subscribe((value: boolean) => {
this.showFullscreenButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.leaveButtonSub = this.libService.leaveButtonObs.subscribe((value: boolean) => {
this.showLeaveButton = value;
this.cd.markForCheck();
});
this.recordingButtonSub = this.libService.recordingButton.subscribe((value: boolean) => {
this.showRecordingButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.chatPanelButtonSub = this.libService.chatPanelButtonObs.subscribe((value: boolean) => {
this.showChatPanelButton = value;
this.cd.markForCheck();
@ -558,12 +623,13 @@ export class ToolbarComponent implements OnInit, OnDestroy {
this.showParticipantsPanelButton = value;
this.cd.markForCheck();
});
// this.activitiesPanelButtonSub = this.libService.activitiesPanelButtonObs.subscribe((value: boolean) => {
// this.showActivitiesPanelButton = value;
// this.cd.markForCheck();
// });
this.activitiesPanelButtonSub = this.libService.activitiesPanelButtonObs.subscribe((value: boolean) => {
this.showActivitiesPanelButton = value;
this.cd.markForCheck();
});
this.backgroundEffectsButtonSub = this.libService.backgroundEffectsButton.subscribe((value: boolean) => {
this.showBackgroundEffectsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.displayLogoSub = this.libService.displayLogoObs.subscribe((value: boolean) => {
@ -575,4 +641,8 @@ export class ToolbarComponent implements OnInit, OnDestroy {
this.cd.markForCheck();
});
}
private checkDisplayMoreOptions() {
this.showMoreOptionsButton = this.showFullscreenButton || this.showBackgroundEffectsButton || this.showRecordingButton;
}
}

View File

@ -19,5 +19,5 @@
margin: auto;
text-align: -webkit-center;
text-align: -moz-center;
color: var(--ov-text-color);
color: var(--ov-panel-text-color);
}

View File

@ -1,20 +1,20 @@
<div id="call-container">
<div id="pre-join-container" *ngIf="showPrejoin && tokensReceived && participantReady && !joinSessionClicked">
<div id="pre-join-container" *ngIf="showPrejoin && tokensReceived && participantReady">
<ov-pre-join (onJoinButtonClicked)="_onJoinButtonClicked()"></ov-pre-join>
<!-- <ov-user-settings (onJoinClicked)="_onJoinClicked()" (onCloseClicked)="onLeaveSessionClicked()"></ov-user-settings> -->
</div>
<div id="spinner" *ngIf="(joinSessionClicked || !showPrejoin) && !participantReady && !error">
<div id="spinner" *ngIf="!participantReady && !streamPlaying && !error">
<mat-spinner [diameter]="50"></mat-spinner>
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
</div>
<div id="spinner" *ngIf="joinSessionClicked && !participantReady && error">
<div id="spinner" *ngIf="!participantReady && error">
<mat-icon class="error-icon">error</mat-icon>
<span>{{ errorMessage }}</span>
</div>
<div id="session-container" *ngIf="(joinSessionClicked || !showPrejoin) && participantReady && tokensReceived && !error">
<div id="session-container" *ngIf="showVideoconference || (!showPrejoin && participantReady && tokensReceived && !error)">
<ov-session (onSessionCreated)="_onSessionCreated($event)">
<ng-template #toolbar>
<ng-container *ngIf="openviduAngularToolbarTemplate">
@ -47,6 +47,9 @@
(onFullscreenButtonClicked)="onFullscreenButtonClicked()"
(onParticipantsPanelButtonClicked)="onParticipantsPanelButtonClicked()"
(onChatPanelButtonClicked)="onChatPanelButtonClicked()"
(onActivitiesPanelButtonClicked)="onActivitiesPanelButtonClicked()"
(onStartRecordingClicked)="onStartRecordingClicked('toolbar')"
(onStopRecordingClicked)="onStopRecordingClicked('toolbar')"
>
<ng-template #toolbarAdditionalButtons>
<ng-container *ngTemplateOutlet="openviduAngularToolbarAdditionalButtonsTemplate"></ng-container>
@ -87,11 +90,14 @@
</ng-template>
<ng-template #defaultActivitiesPanel>
<!-- <ov-activities-panel
<ov-activities-panel
id="default-activities-panel"
(onStartRecordingClicked)="onStartRecordingClicked()"
(onStopRecordingClicked)="onStopRecordingClicked()"
></ov-activities-panel> -->
(onStartRecordingClicked)="onStartRecordingClicked('panel')"
(onStopRecordingClicked)="onStopRecordingClicked('panel')"
(onDownloadRecordingClicked)="onDownloadRecordingClicked($event)"
(onDeleteRecordingClicked)="onDeleteRecordingClicked($event)"
(onPlayRecordingClicked)="onPlayRecordingClicked($event)"
></ov-activities-panel>
</ng-template>
<ng-template #defaultParticipantsPanel>

View File

@ -27,13 +27,14 @@ import {
ActivitiesPanelDirective
} from '../../directives/template/openvidu-angular.directive';
import { ILogger } from '../../models/logger.model';
import { OpenViduEdition } from '../../models/openvidu.model';
import { ParticipantAbstractModel, ParticipantProperties } from '../../models/participant.model';
import { TokenModel } from '../../models/token.model';
import { ActionService } from '../../services/action/action.service';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
import { DeviceService } from '../../services/device/device.service';
import { LoggerService } from '../../services/logger/logger.service';
import { OpenViduEdition, OpenViduService } from '../../services/openvidu/openvidu.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { StorageService } from '../../services/storage/storage.service';
import { TokenService } from '../../services/token/token.service';
@ -70,6 +71,7 @@ import { TranslateService } from '../../services/translate/translate.service';
* | **streamDisplayAudioDetection** | `boolean` | {@link StreamDisplayAudioDetectionDirective} |
* | **streamSettingsButton** | `boolean` | {@link StreamSettingsButtonDirective} |
* | **participantPanelItemMuteButton** | `boolean` | {@link ParticipantPanelItemMuteButtonDirective} |
* | **recordingActivityRecordingList** | `{@link RecordingInfo}[]` | {@link RecordingActivityRecordingListDirective} |
*
* <p class="component-link-text">
* <span class="italic">See all {@link ApiDirectiveModule API Directives}</span>
@ -325,6 +327,50 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
*/
@Output() onToolbarChatPanelButtonClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when activities panel button has been clicked.
*/
@Output() onToolbarActivitiesPanelButtonClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when start recording button is clicked {@link ToolbarComponent}.
* The recording should be stopped using the REST API.
*/
@Output() onToolbarStartRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when stop recording button is clicked from {@link ToolbarComponent}.
* The recording should be stopped using the REST API.
*/
@Output() onToolbarStopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when start recording button is clicked {@link ActivitiesPanelComponent}.
* The recording should be stopped using the REST API.
*/
@Output() onActivitiesPanelStartRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when stop recording button is clicked from {@link ActivitiesPanelComponent}.
* The recording should be stopped using the REST API.
*/
@Output() onActivitiesPanelStopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when download recording button is clicked from {@link ActivitiesPanelComponent}.
* The recording should be downloaded using the REST API.
*/
@Output() onActivitiesPanelDownloadRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when delete recording button is clicked from {@link ActivitiesPanelComponent}.
* The recording should be deleted using the REST API.
*/
@Output() onActivitiesPanelDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when play recording button is clicked from {@link ActivitiesPanelComponent}.
*/
@Output() onActivitiesPanelPlayRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when OpenVidu Session is created.
* See {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Session.html openvidu-browser Session}.
@ -339,7 +385,7 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
/**
* @internal
*/
joinSessionClicked: boolean = false;
showVideoconference: boolean = false;
/**
* @internal
*/
@ -360,6 +406,8 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
* @internal
*/
showPrejoin: boolean = true;
streamPlaying = false;
private externalParticipantName: string;
private prejoinSub: Subscription;
private participantNameSub: Subscription;
@ -404,12 +452,9 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
try {
const publisher = await this.openviduService.initDefaultPublisher(undefined);
if (publisher) {
publisher.once('accessDenied', (e: any) => {
this.handlePublisherError(e);
});
publisher.once('accessAllowed', () => {
this.participantReady = true;
});
publisher.once('accessDenied', (e: any) => this.handlePublisherError(e));
publisher.once('accessAllowed', () => (this.participantReady = true));
publisher.once('streamPlaying', () => (this.streamPlaying = true));
}
} catch (error) {
this.actionService.openDialog(error.name.replace(/_/g, ' '), error.message, true);
@ -477,14 +522,13 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
this.openviduAngularChatPanelTemplate = this.defaultChatPanelTemplate;
}
// TODO: WIP
// if (this.externalActivitiesPanel) {
// this.log.d('Setting EXTERNAL ACTIVITIES PANEL');
// this.openviduAngularActivitiesPanelTemplate = this.externalActivitiesPanel.template;
// } else {
// this.log.d('Setting DEFAULT ACTIVITIES PANEL');
// this.openviduAngularActivitiesPanelTemplate = this.defaultActivitiesPanelTemplate;
// }
if (this.externalActivitiesPanel) {
this.log.d('Setting EXTERNAL ACTIVITIES PANEL');
this.openviduAngularActivitiesPanelTemplate = this.externalActivitiesPanel.template;
} else {
this.log.d('Setting DEFAULT ACTIVITIES PANEL');
this.openviduAngularActivitiesPanelTemplate = this.defaultActivitiesPanelTemplate;
}
if (this.externalAdditionalPanels) {
this.log.d('Setting EXTERNAL ADDITIONAL PANELS');
@ -514,14 +558,15 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
* @internal
*/
_onJoinButtonClicked() {
this.joinSessionClicked = true;
this.showVideoconference = true;
this.showPrejoin = false;
this.onJoinButtonClicked.emit();
}
/**
* @internal
*/
onLeaveButtonClicked() {
this.joinSessionClicked = false;
this.showVideoconference = false;
this.participantReady = false;
this.onToolbarLeaveButtonClicked.emit();
}
@ -561,6 +606,56 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
onChatPanelButtonClicked() {
this.onToolbarChatPanelButtonClicked.emit();
}
/**
* @internal
*/
onActivitiesPanelButtonClicked() {
this.onToolbarActivitiesPanelButtonClicked.emit();
}
/**
* @internal
*/
onStartRecordingClicked(from: string) {
if (from === 'toolbar') {
this.onToolbarStartRecordingClicked.emit();
} else if (from === 'panel') {
this.onActivitiesPanelStartRecordingClicked.emit();
}
}
/**
* @internal
*/
onStopRecordingClicked(from: string) {
if (from === 'toolbar') {
this.onToolbarStopRecordingClicked.emit();
} else if (from === 'panel') {
this.onActivitiesPanelStopRecordingClicked.emit();
}
}
/**
* @internal
*/
onDownloadRecordingClicked(recordingId: string) {
this.onActivitiesPanelDownloadRecordingClicked.emit(recordingId);
}
/**
* @internal
*/
onDeleteRecordingClicked(recordingId: string) {
this.onActivitiesPanelDeleteRecordingClicked.emit(recordingId);
}
/**
* @internal
*/
onPlayRecordingClicked(recordingId: string) {
this.onActivitiesPanelPlayRecordingClicked.emit(recordingId);
}
/**
* @internal
*/

View File

@ -0,0 +1,52 @@
import { Directive, AfterViewInit, OnDestroy, Input, ElementRef } from '@angular/core';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
/**
* The **recordingActivity** directive allows show/hide the recording activity in {@link ActivitiesPanelComponent} activity panel component.
*
* Default: `true`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `activitiesPanel` component:
*
* @example
* <ov-videoconference [activitiesPanelRecordingActivity]="false"></ov-videoconference>
*
* \
* And it also can be used in the {@link ActivitiesPanelComponent}.
* @example
* <ov-activities-panel [recordingActivity]="false"></ov-activities-panel>
*/
@Directive({
selector: 'ov-videoconference[activitiesPanelRecordingActivity], ov-activities-panel[recordingActivity]'
})
export class ActivitiesPanelRecordingActivityDirective implements AfterViewInit, OnDestroy {
@Input() set activitiesPanelRecordingActivity(value: boolean) {
this.recordingActivityValue = value;
this.update(this.recordingActivityValue);
}
@Input() set recordingList(value: boolean) {
this.recordingActivityValue = value;
this.update(this.recordingActivityValue);
}
recordingActivityValue: boolean = true;
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.recordingActivityValue);
}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.recordingActivityValue = true;
this.update(true);
}
update(value: boolean) {
if (this.libService.recordingActivity.getValue() !== value) {
this.libService.recordingActivity.next(value);
}
}
}

View File

@ -1,6 +1,8 @@
import { NgModule } from '@angular/core';
import { ActivitiesPanelRecordingActivityDirective } from './activities-panel.directive';
import { LogoDirective } from './internals.directive';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import { RecordingActivityRecordingErrorDirective, RecordingActivityRecordingsListDirective } from './recording-activity.directive';
import {
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
@ -15,7 +17,8 @@ import {
ToolbarDisplaySessionNameDirective,
ToolbarDisplayLogoDirective,
ToolbarActivitiesPanelButtonDirective,
ToolbarBackgroundEffectsButtonDirective
ToolbarBackgroundEffectsButtonDirective,
ToolbarRecordingButtonDirective
} from './toolbar.directive';
import {
AudioMutedDirective,
@ -38,6 +41,7 @@ import {
ToolbarFullscreenButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarActivitiesPanelButtonDirective,
@ -48,7 +52,10 @@ import {
StreamSettingsButtonDirective,
LogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective
ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective,
RecordingActivityRecordingsListDirective,
RecordingActivityRecordingErrorDirective
],
exports: [
MinimalDirective,
@ -60,6 +67,7 @@ import {
ToolbarFullscreenButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarRecordingButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarActivitiesPanelButtonDirective,
@ -70,7 +78,10 @@ import {
StreamSettingsButtonDirective,
LogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective
ParticipantNameDirective,
ActivitiesPanelRecordingActivityDirective,
RecordingActivityRecordingsListDirective,
RecordingActivityRecordingErrorDirective
]
})
export class ApiDirectiveModule {}

View File

@ -0,0 +1,103 @@
import { Directive, AfterViewInit, OnDestroy, Input, ElementRef } from '@angular/core';
import { RecordingInfo } from '../../models/recording.model';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
/**
* The **recordingsList** directive allows show the recordings available for the session in {@link RecordingActivityComponent}.
*
* Default: `[]`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `recordingActivity` component:
*
* @example
* <ov-videoconference [recordingActivityRecordingsList]="list"></ov-videoconference>
*
* \
* And it also can be used in the {@link RecordingActivityComponent}.
* @example
* <ov-recording-activity [recordingsList]="list"></ov-recording-activity>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityRecordingsList], ov-recording-activity[recordingsList]'
})
export class RecordingActivityRecordingsListDirective implements AfterViewInit, OnDestroy {
@Input() set recordingActivityRecordingsList(value: RecordingInfo[]) {
this.recordingsValue = value;
this.update(this.recordingsValue);
}
@Input() set recordingsList(value: RecordingInfo[]) {
this.recordingsValue = value;
this.update(this.recordingsValue);
}
recordingsValue: RecordingInfo [] = [];
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.recordingsValue);
}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.recordingsValue = null;
this.update(null);
}
update(value: RecordingInfo[]) {
if (this.libService.recordingsList.getValue() !== value) {
this.libService.recordingsList.next(value);
}
}
}
/**
* The **recordingError** directive allows to show any possible error with the recording in the {@link RecordingActivityComponent}.
*
* Default: `[]`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `recordingActivity` component:
*
* @example
* <ov-videoconference [recordingActivityRecordingError]="error"></ov-videoconference>
*
* \
* And it also can be used in the {@link RecordingActivityComponent}.
* @example
* <ov-recording-activity [recordingError]="error"></ov-recording-activity>
*/
@Directive({
selector: 'ov-videoconference[recordingActivityRecordingError], ov-recording-activity[recordingError]'
})
export class RecordingActivityRecordingErrorDirective implements AfterViewInit, OnDestroy {
@Input() set recordingActivityRecordingError(value: any) {
this.recordingErrorValue = value;
this.update(this.recordingErrorValue);
}
@Input() set recordingError(value: any) {
this.recordingErrorValue = value;
this.update(this.recordingErrorValue);
}
recordingErrorValue: RecordingInfo [] = [];
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.recordingErrorValue);
}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.recordingErrorValue = null;
this.update(null);
}
update(value: any) {
if (this.libService.recordingError.getValue() !== value) {
this.libService.recordingError.next(value);
}
}
}

View File

@ -63,6 +63,67 @@ export class ToolbarScreenshareButtonDirective implements AfterViewInit, OnDestr
}
}
/**
* The **recordingButton** directive allows show/hide the start/stop recording toolbar button.
*
* Default: `true`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarRecordingButton]="false"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [recordingButton]="false"></ov-toolbar>
*
* @internal
*/
@Directive({
selector: 'ov-videoconference[toolbarRecordingButton], ov-toolbar[recordingButton]'
})
export class ToolbarRecordingButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarRecordingButton(value: boolean) {
this.recordingValue = value;
this.update(this.recordingValue);
}
/**
* @ignore
*/
@Input() set recordingButton(value: boolean) {
this.recordingValue = value;
this.update(this.recordingValue);
}
private recordingValue: boolean = true;
/**
* @ignore
*/
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.recordingValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.recordingValue = true;
this.update(true);
}
private update(value: boolean) {
if (this.libService.recordingButton.getValue() !== value) {
this.libService.recordingButton.next(value);
}
}
}
/**
* The **fullscreenButton** directive allows show/hide the fullscreen toolbar button.
*
@ -392,7 +453,7 @@ export class ToolbarActivitiesPanelButtonDirective implements AfterViewInit, OnD
/**
* @ignore
*/
@Input() set chatPanelButton(value: boolean) {
@Input() set activitiesPanelButton(value: boolean) {
this.toolbarActivitiesPanelValue = value;
this.update(this.toolbarActivitiesPanelValue);
}

View File

@ -60,7 +60,15 @@
"SUBTITLE": "为后人记录你的会议",
"CONTENT_TITLE": "记录你的视频通话",
"CONTENT_SUBTITLE": "当录音完成后,你将可以轻松地下载它",
"STARTING": "开始录音"
"STARTING": "开始录音",
"STOPPING": "停止录制",
"PLAY": "玩",
"DELETE": "删除",
"CANCEL": "取消",
"DELETE_QUESTION": "您确定要删除录音吗",
"DOWNLOAD": "下载",
"RECORDINGS": "录制",
"NO_MODERATOR": "只有主持人可以开始录音"
}
},
"ERRORS": {

View File

@ -60,7 +60,15 @@
"SUBTITLE": "Zeichnen Sie Ihr Meeting für die Nachwelt auf",
"CONTENT_TITLE": "Ihr Videogespräch aufzeichnen",
"CONTENT_SUBTITLE": "Wenn die Aufzeichnung beendet ist, können Sie sie ganz einfach herunterladen",
"STARTING": "Aufzeichnung starten"
"STARTING": "Aufzeichnung starten",
"STOPPING": "Aufnahme stoppen",
"PLAY": "Spielen",
"DELETE": "Löschen",
"CANCEL": "Absagen",
"DELETE_QUESTION": "Möchten Sie die Aufzeichnung wirklich löschen?",
"DOWNLOAD": "Download",
"RECORDINGS": "AUFZEICHNUNGEN",
"NO_MODERATOR": "Nur der MODERATOR kann die Aufzeichnung starten"
}
},
"ERRORS": {

View File

@ -59,8 +59,16 @@
"TITLE": "Recording",
"SUBTITLE": "Record your meeting for posterity",
"CONTENT_TITLE": "Record your video call",
"CONTENT_SUBTITLE": "When recording has finished you will can download it with ease",
"STARTING": "Starting recording"
"CONTENT_SUBTITLE": "When recording has finished you will be able to download it with ease",
"STARTING": "Starting recording",
"STOPPING": "Stopping recording",
"PLAY": "Play",
"DELETE": "Delete",
"CANCEL": "Cancel",
"DELETE_QUESTION": "Are you sure you want to delete the recording?",
"DOWNLOAD": "Download",
"RECORDINGS": "RECORDINGS",
"NO_MODERATOR": "Only the MODERATOR can start the recording"
}
},
"ERRORS": {

View File

@ -60,7 +60,26 @@
"SUBTITLE": "Graba tus llamadas para la posteridad",
"CONTENT_TITLE": "Graba tu video conferencia",
"CONTENT_SUBTITLE": "Cuando la grabación haya finalizado, podrás descargarla con facilidad",
"STARTING": "Iniciar grabación"
"STARTING": "Iniciando grabación",
"STOPPING": "Parando grabación",
"PLAY": "Reproducir",
"DELETE": "Borrar",
"CANCEL": "Cancelar",
"DELETE_QUESTION": "¿Estás seguro/a de que deseas borrar la grabación?",
"DOWNLOAD": "Descargar",
"RECORDINGS": "GRABACIONES",
"NO_MODERATOR": "Sólo el MODERADOR puede iniciar la grabación"
}
},
"ERRORS": {
"SESSION": "Hubo un error al conectar a la sesión",
"CONNECTION": "Sin conexión",
"RECONNECT": "Intentando reconectar a la sesión...",
"TOGGLE_CAMERA": "Hubo un error cambiando la cámara",
"TOGGLE_MICROPHONE": "Hubo un error cambiando el micrófono",
"SCREEN_SHARING": "Hubo un error compartiendo pantalla",
"SCREEN_SUPPORT": "Tu navegador no soporta la pantalla compartida",
"MEDIA_ACCESS": "No se ha podido acceder a tus dispositivos",
"DEVICE_NOT_FOUND": "No se han encontrado dispositivos de audio o video. Por favor, conecta al menos uno."
}
}

View File

@ -60,7 +60,16 @@
"SUBTITLE": "Enregistrez votre réunion pour la postérité",
"CONTENT_TITLE": "Enregistrez votre appel vidéo",
"CONTENT_SUBTITLE": "Une fois l'enregistrement terminé, vous pourrez le télécharger facilement",
"STARTING": "Début de l'enregistrement"
"STARTING": "Début de l'enregistrement",
"STOPPING": "Arrêt de l'enregistrement",
"PLAY": "Jouer",
"DELETE": "Effacer",
"CANCEL": "Annuler",
"DELETE_QUESTION": "Voulez-vous vraiment supprimer l'enregistrement ?",
"DOWNLOAD": "Télécharger",
"RECORDINGS": "ENREGISTREMENTS",
"NO_MODERATOR": "Seul le MODERATEUR peut lancer l'enregistrement"
}
},
"ERRORS": {

View File

@ -60,7 +60,15 @@
"SUBTITLE": "अपनी बैठक को भावी पीढ़ी के लिए रिकॉर्ड करें",
"CONTENT_TITLE": "अपना वीडियो कॉल रिकॉर्ड करें",
"CONTENT_SUBTITLE": "रिकॉर्डिंग समाप्त हो जाने पर आप इसे आसानी से डाउनलोड कर सकेंगे",
"STARTING": "रिकॉर्डिंग शुरू कर रहा है"
"STARTING": "रिकॉर्डिंग शुरू कर रहा है",
"STOPPING": "रिकॉर्डिंग बंद करना",
"PLAY": "खेलें",
"DELETE": "मिटाना",
"CANCEL": "रद्द करना",
"DELETE_QUESTION": "क्या आप वाकई रिकॉर्डिंग हटाना चाहते हैं",
"DOWNLOAD": "डाउनलोड",
"RECORDINGS": "रिकॉर्डिंग",
"NO_MODERATOR": "केवल मॉडरेटर ही रिकॉर्डिंग शुरू कर सकता है"
}
},
"ERRORS": {

View File

@ -60,7 +60,15 @@
"SUBTITLE": "Registra la tua riunione per i posteri",
"CONTENT_TITLE": "Registra la tua videochiamata",
"CONTENT_SUBTITLE": "Al termine della registrazione potrete scaricarla con facilità",
"STARTING": "Avvio della registrazione"
"STARTING": "Avvio della registrazione",
"STOPPING": "Interruzione della registrazione",
"PLAY": "Giocare a",
"DELETE": "Elimina",
"CANCEL": "Annulla",
"DELETE_QUESTION": "Sei sicuro di voler eliminare la registrazione?",
"DOWNLOAD": "Scarica",
"RECORDINGS": "REGISTRAZIONI",
"NO_MODERATOR": "Solo il MODERATORE può avviare la registrazione"
}
},
"ERRORS": {

View File

@ -60,7 +60,15 @@
"SUBTITLE": "会議を録音して後世に残す",
"CONTENT_TITLE": "ビデオ通話を録音する",
"CONTENT_SUBTITLE": "録音が完了したら、簡単にダウンロードできます",
"STARTING": "録画開始"
"STARTING": "録画開始",
"STOPPING": "録音を停止します",
"PLAY": "遊ぶ",
"DELETE": "消去",
"CANCEL": "キャンセル",
"DELETE_QUESTION": "録音を削除してもよろしいですか",
"DOWNLOAD": "ダウンロード",
"RECORDINGS": "録画",
"NO_MODERATOR": "録音を開始できるのは、モデレーターのみです"
}
},
"ERRORS": {

View File

@ -60,13 +60,21 @@
"SUBTITLE": "Neem uw vergadering op voor het nageslacht",
"CONTENT_TITLE": "Neem uw videogesprek op",
"CONTENT_SUBTITLE": "Als de opname klaar is kunt u deze met gemak downloaden",
"STARTING": "Beginnen met opnemen"
"STARTING": "Beginnen met opnemen",
"STOPPING": "Opname stoppen",
"PLAY": "Toneelstuk",
"DELETE": "Verwijderen",
"CANCEL": "Annuleren",
"DELETE_QUESTION": "Weet je zeker dat je de opname wilt verwijderen?",
"DOWNLOAD": "Downloaden",
"RECORDINGS": "OPNAME",
"NO_MODERATOR": "Alleen de MOEDERATOR kan de opname starten"
}
},
"ERRORS": {
"SESSION": "Er is een fout opgetreden bij het verbinden met de sessie",
"CONNECTION": "Verbinding verloren",
"RECONNECT": "Oeps! Proberen opnieuw verbinding te maken met de sessie...",
"RECONNECT": "Proberen opnieuw verbinding te maken met de sessie...",
"TOGGLE_CAMERA": "Er is een fout opgetreden bij het overschakelen naar een andere camera",
"TOGGLE_MICROPHONE": "Er is een fout opgetreden bij het overschakelen naar een microfoon",
"SCREEN_SHARING": "Fout bij het delen van het scherm",

View File

@ -60,7 +60,15 @@
"SUBTITLE": "Grave a sua reunião para a posteridade",
"CONTENT_TITLE": "Grave a sua videochamada",
"CONTENT_SUBTITLE": "Quando a gravação tiver terminado, poderá descarregá-la com facilidade",
"STARTING": "Começar a gravação"
"STARTING": "Começar a gravação",
"STOPPING": "Parando a gravação",
"PLAY": "Toque",
"DELETE": "Excluir",
"CANCEL": "Cancelar",
"DELETE_QUESTION": "Tem certeza de que deseja excluir a gravação?",
"DOWNLOAD": "Download",
"RECORDINGS": "GRAVAÇÕES",
"NO_MODERATOR": "Só o MODERADOR pode iniciar a gravação"
}
},
"ERRORS": {

View File

@ -1,9 +1,15 @@
/**
* @internal
*/
export enum EffectType {
NONE = 'NONE',
BLUR = 'BLUR',
IMAGE = 'IMAGE'
}
/**
* @internal
*/
export interface BackgroundEffect {
id: string;
type: EffectType;

View File

@ -0,0 +1,8 @@
/**
* @internal
*/
export enum OpenViduEdition {
CE = 'CE',
PRO = 'PRO',
ENTERPRISE = 'ENTERPRISE'
}

View File

@ -1,6 +1,14 @@
import { Publisher, StreamManager } from 'openvidu-browser';
import { VideoType } from './video-type.model';
/**
* @internal
*/
export enum OpenViduRole {
MODERATOR = 'MODERATOR',
PUBLISHER = 'PUBLISHER'
}
export interface StreamModel {
/**
* Whether the stream is available or not
@ -329,6 +337,13 @@ export abstract class ParticipantAbstractModel {
setMutedForcibly(muted: boolean) {
this.isMutedForcibly = muted;
}
/**
* @internal
*/
getRole(): OpenViduRole {
return <OpenViduRole>this.streams.get(VideoType.CAMERA)?.streamManager?.stream?.connection?.role;
}
}
/**

View File

@ -1,7 +1,22 @@
export enum RecordingStatus {
STARTING = 'starting',
STARTED = 'started',
STOPPING = 'stopping',
STOPPED = 'stopped',
FAILED = 'failed',
READY = 'ready'
};
/**
* @internal
*/
export enum RecordingStatus {
STARTED = 'STARTED',
STOPPED = 'STOPPED'
export interface RecordingInfo {
status: RecordingStatus;
id?: string;
name?: string;
reason?: string;
createdAt?: number;
duration?: number;
size?: string;
url?: string
}

View File

@ -1,6 +1,3 @@
/**
* @internal
*/
export interface TokenModel {
webcam: string;
screen?: string;

View File

@ -27,18 +27,20 @@ import { CommonModule } from '@angular/common';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { ToolbarComponent } from './components/toolbar/toolbar.component';
import { VideoComponent } from './components/video/video.component';
import { ChatPanelComponent } from './components/panel/chat-panel/chat-panel.component';
import { SessionComponent } from './components/session/session.component';
import { LayoutComponent } from './components/layout/layout.component';
import { StreamComponent } from './components/stream/stream.component';
import { DialogTemplateComponent } from './components/material/dialog.component';
import { DialogTemplateComponent } from './components/dialogs/dialog.component';
import { RecordingDialogComponent } from './components/dialogs/recording-dialog.component';
import { DeleteDialogComponent } from './components/dialogs/delete-recording.component';
import { LinkifyPipe } from './pipes/linkify.pipe';
import { TranslatePipe } from './pipes/translate.pipe';
import { StreamTypesEnabledPipe, ParticipantStreamsPipe } from './pipes/participant.pipe';
import { DurationFromSecondsPipe } from './pipes/recording.pipe';
import { OpenViduAngularConfig } from './config/openvidu-angular.config';
import { CdkOverlayContainer } from './config/custom-cdk-overlay';
@ -55,6 +57,8 @@ import { DocumentService } from './services/document/document.service';
import { LayoutService } from './services/layout/layout.service';
import { PanelService } from './services/panel/panel.service';
import { ParticipantService } from './services/participant/participant.service';
import { RecordingService } from './services/recording/recording.service';
import { ParticipantPanelItemComponent } from './components/panel/participants-panel/participant-panel-item/participant-panel-item.component';
import { ParticipantsPanelComponent } from './components/panel/participants-panel/participants-panel/participants-panel.component';
import { VideoconferenceComponent } from './components/videoconference/videoconference.component';
@ -78,8 +82,11 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
LayoutComponent,
StreamComponent,
DialogTemplateComponent,
RecordingDialogComponent,
DeleteDialogComponent,
LinkifyPipe,
ParticipantStreamsPipe,
DurationFromSecondsPipe,
StreamTypesEnabledPipe,
TranslatePipe,
ParticipantPanelItemComponent,
@ -137,7 +144,8 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
ParticipantService,
StorageService,
TokenService,
OpenViduService
OpenViduService,
RecordingService
],
exports: [
VideoconferenceComponent,
@ -155,12 +163,13 @@ import { RecordingActivityComponent } from './components/panel/activities-panel/
AudioWaveComponent,
PreJoinComponent,
ParticipantStreamsPipe,
DurationFromSecondsPipe,
StreamTypesEnabledPipe,
CommonModule,
OpenViduAngularDirectiveModule,
ApiDirectiveModule
],
entryComponents: [DialogTemplateComponent]
entryComponents: [DialogTemplateComponent, RecordingDialogComponent, DeleteDialogComponent]
})
export class OpenViduAngularModule {
static forRoot(config): ModuleWithProviders<OpenViduAngularModule> {

View File

@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
/**
* @internal
*/
@Pipe({
name: 'duration'
})
export class DurationFromSecondsPipe implements PipeTransform {
transform(durationInSeconds: number): string {
if (durationInSeconds < 60) {
return Math.floor(durationInSeconds) + 's';
} else if (durationInSeconds < 3600) {
return Math.floor(durationInSeconds / 60) + 'm ' + Math.floor(durationInSeconds % 60) + 's';
} else {
const hours = Math.floor(durationInSeconds / 3600);
const minutes = Math.floor((durationInSeconds - hours * 3600) / 60);
return hours + 'h ' + minutes + 'm';
}
}
}

View File

@ -1,8 +1,11 @@
import { Injectable } from '@angular/core';
import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { DialogTemplateComponent } from '../../components/material/dialog.component';
import { SafeResourceUrl } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import { DeleteDialogComponent } from '../../components/dialogs/delete-recording.component';
import { RecordingDialogComponent } from '../../components/dialogs/recording-dialog.component';
import { DialogTemplateComponent } from '../../components/dialogs/dialog.component';
import { INotificationOptions } from '../../models/notification-options.model';
/**
@ -12,8 +15,8 @@ import { INotificationOptions } from '../../models/notification-options.model';
providedIn: 'root'
})
export class ActionService {
private dialogRef: MatDialogRef<DialogTemplateComponent>;
private dialogRef: MatDialogRef<DialogTemplateComponent | RecordingDialogComponent | DeleteDialogComponent>;
private dialogSubscription: Subscription;
constructor(private snackBar: MatSnackBar, public dialog: MatDialog) {}
launchNotification(options: INotificationOptions, callback): void {
@ -36,9 +39,7 @@ export class ActionService {
openDialog(titleMessage: string, descriptionMessage: string, allowClose = true) {
try {
this.closeDialog();
} catch (error) {
} finally {
const config: MatDialogConfig = {
minWidth: '250px',
@ -46,13 +47,43 @@ export class ActionService {
disableClose: !allowClose
};
this.dialogRef = this.dialog.open(DialogTemplateComponent, config);
this.dialogRef.afterClosed().subscribe((result) => {
this.dialogSubscription = this.dialogRef.afterClosed().subscribe((result) => {
this.dialogRef = null;
});
}
}
openDeleteRecordingDialog(succsessCallback) {
try {
this.closeDialog();
} catch (error) {
} finally {
this.dialogRef = this.dialog.open(DeleteDialogComponent);
this.dialogSubscription = this.dialogRef.afterClosed().subscribe((result) => {
if (result) {
succsessCallback();
}
});
}
}
openRecordingPlayerDialog(src: SafeResourceUrl, type: string, allowClose = true) {
try {
this.closeDialog();
} catch (error) {
} finally {
const config: MatDialogConfig = {
minWidth: '250px',
data: { src, type, showActionButtons: allowClose },
disableClose: !allowClose
};
this.dialogRef = this.dialog.open(RecordingDialogComponent, config);
}
}
closeDialog() {
this.dialogRef.close();
if (this.dialogSubscription) this.dialogSubscription.unsubscribe();
}
}

View File

@ -1,6 +1,7 @@
import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { OpenViduAngularConfig, ParticipantFactoryFunction } from '../../config/openvidu-angular.config';
import { RecordingInfo } from '../../models/recording.model';
// import { version } from '../../../../package.json';
@ -54,6 +55,14 @@ export class OpenViduAngularConfigService {
participantItemMuteButtonObs: Observable<boolean>;
backgroundEffectsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
backgroundEffectsButtonObs: Observable<boolean>;
recordingsList = <BehaviorSubject<RecordingInfo[]>>new BehaviorSubject([]);
recordingsListObs: Observable<RecordingInfo[]>;
recordingButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
recordingButtonObs: Observable<boolean>;
recordingActivity = <BehaviorSubject<boolean>>new BehaviorSubject(true);
recordingActivityObs: Observable<boolean>;
recordingError = <BehaviorSubject<any>>new BehaviorSubject(null);
recordingErrorObs: Observable<any>;
constructor(@Inject('OPENVIDU_ANGULAR_CONFIG') config: OpenViduAngularConfig) {
this.configuration = config;
@ -74,12 +83,17 @@ export class OpenViduAngularConfigService {
this.activitiesPanelButtonObs = this.activitiesPanelButton.asObservable();
this.displaySessionNameObs = this.displaySessionName.asObservable();
this.displayLogoObs = this.displayLogo.asObservable();
this.recordingButtonObs = this.recordingButton.asObservable();
//Stream observables
this.displayParticipantNameObs = this.displayParticipantName.asObservable();
this.displayAudioDetectionObs = this.displayAudioDetection.asObservable();
this.settingsButtonObs = this.settingsButton.asObservable();
// Participant item observables
this.participantItemMuteButtonObs = this.participantItemMuteButton.asObservable();
// Recording activity observables
this.recordingActivityObs = this.recordingActivity.asObservable();
this.recordingsListObs = this.recordingsList.asObservable();
this.recordingErrorObs = this.recordingError.asObservable();
}
getConfig(): OpenViduAngularConfig {

View File

@ -12,12 +12,7 @@ import { CameraType } from '../../models/device.model';
import { ScreenType, VideoType } from '../../models/video-type.model';
import { ParticipantService } from '../participant/participant.service';
import { TokenService } from '../token/token.service';
export enum OpenViduEdition {
CE = 'CE',
PRO = 'PRO',
ENTERPRISE = 'ENTERPRISE'
}
import { OpenViduEdition } from '../../models/openvidu.model';
@Injectable({
providedIn: 'root'

View File

@ -15,9 +15,10 @@ export class PanelService {
protected log: ILogger;
protected isChatOpened: boolean = false;
protected isParticipantsOpened: boolean = false;
protected isActivitiesOpened: boolean = false;
private isExternalOpened: boolean = false;
private externalType: string;
protected _panelOpened = <BehaviorSubject<{ opened: boolean; type?: PanelType | string }>>new BehaviorSubject({ opened: false });
protected _panelOpened = <BehaviorSubject<{ opened: boolean; type?: PanelType | string, expand?: string }>>new BehaviorSubject({ opened: false });
/**
* @internal
@ -31,37 +32,46 @@ export class PanelService {
* Open or close the panel type received. Calling this method with the panel opened and the same type panel, will close the panel.
* If the type is differente, it will switch to the properly panel.
*/
togglePanel(type: PanelType | string) {
togglePanel(type: PanelType | string, expand?: string) {
this.log.d(`Toggling ${type} menu`);
let opened: boolean;
if (type === PanelType.CHAT) {
this.isChatOpened = !this.isChatOpened;
this.isParticipantsOpened = false;
this.isExternalOpened = false;
this.isActivitiesOpened = false
opened = this.isChatOpened;
} else if (type === PanelType.PARTICIPANTS) {
this.isParticipantsOpened = !this.isParticipantsOpened;
this.isChatOpened = false;
this.isExternalOpened = false;
this.isActivitiesOpened = false;
opened = this.isParticipantsOpened;
} else if (type === PanelType.ACTIVITIES) {
this.isActivitiesOpened = !this.isActivitiesOpened;
this.isChatOpened = false;
this.isExternalOpened = false;
this.isParticipantsOpened = false;
opened = this.isActivitiesOpened;
} else {
this.log.d('Toggling external panel');
this.isChatOpened = false;
this.isParticipantsOpened = false;
this.isActivitiesOpened = false;
// Open when is close or is opened with another type
this.isExternalOpened = !this.isExternalOpened || this.externalType !== type;
this.externalType = !this.isExternalOpened ? '' : type;
opened = this.isExternalOpened;
}
this._panelOpened.next({ opened, type });
this._panelOpened.next({ opened, type, expand });
}
/**
* @internal
*/
isPanelOpened(): boolean {
return this.isChatPanelOpened() || this.isParticipantsPanelOpened() || this.isExternalPanelOpened();
return this.isChatPanelOpened() || this.isParticipantsPanelOpened() || this.isActivitiesPanelOpened() || this.isExternalPanelOpened();
}
/**
@ -71,6 +81,7 @@ export class PanelService {
this.isParticipantsOpened = false;
this.isChatOpened = false;
this.isExternalOpened = false;
this.isActivitiesOpened = false;
this._panelOpened.next({ opened: false });
}
@ -88,6 +99,13 @@ export class PanelService {
return this.isParticipantsOpened;
}
/**
* Whether the activities panel is opened or not.
*/
isActivitiesPanelOpened(): boolean {
return this.isActivitiesOpened;
}
isExternalPanelOpened(): boolean {
return this.isExternalOpened;
}

View File

@ -150,6 +150,10 @@ export class ParticipantService {
return this.localParticipant.nickname;
}
getMyRole(): string {
return this.localParticipant.getRole();
}
/**
* @internal
*/

View File

@ -1,21 +1,10 @@
import { Injectable } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { RecordingEvent } from 'openvidu-browser';
import { BehaviorSubject, Observable } from 'rxjs';
import { RecordingStatus } from '../../models/recording.model';
import { RecordingInfo, RecordingStatus } from '../../models/recording.model';
import { ActionService } from '../action/action.service';
/**
* @internal
*/
export interface RecordingInfo {
status: RecordingStatus;
id: string;
name?: string;
reason?: string;
}
/**
* @internal
*/
@Injectable({
providedIn: 'root'
})
@ -23,30 +12,108 @@ export class RecordingService {
/**
* Recording status Observable which pushes the recording state in every update.
*/
recordingStatusObs: Observable<RecordingInfo>;
private recordingStatus = <BehaviorSubject<RecordingInfo>>new BehaviorSubject(null);
recordingStatusObs: Observable<{ info: RecordingInfo; time?: Date }>;
constructor() {
private recordingTime: Date;
private recordingTimeInterval: NodeJS.Timer;
private currentRecording: RecordingInfo = { status: RecordingStatus.STOPPED };
private recordingStatus = <BehaviorSubject<{ info: RecordingInfo; time?: Date }>>new BehaviorSubject(null);
/**
* @internal
* @param actionService
* @param sanitizer
*/
constructor(private actionService: ActionService, private sanitizer: DomSanitizer) {
this.recordingStatusObs = this.recordingStatus.asObservable();
}
/**
* @internal
* @param status
*/
updateStatus(status: RecordingStatus) {
this.currentRecording = {
status: status
};
this.recordingStatus.next({ info: this.currentRecording });
}
/**
* @internal
* @param event
*/
startRecording(event: RecordingEvent) {
const info: RecordingInfo = {
this.currentRecording = {
status: RecordingStatus.STARTED,
id: event.id,
name: event.name,
reason: event.reason
};
this.recordingStatus.next(info);
this.startRecordingTime();
this.recordingStatus.next({ info: this.currentRecording, time: this.recordingTime });
}
/**
* @internal
* @param event
*/
stopRecording(event: RecordingEvent) {
const info: RecordingInfo = {
status: RecordingStatus.STOPPED,
id: event.id,
name: event.name,
reason: event.reason
};
this.recordingStatus.next(info);
this.currentRecording.status = RecordingStatus.STOPPED;
this.currentRecording.reason = event.reason;
this.recordingStatus.next({ info: this.currentRecording, time: null });
this.stopRecordingTime();
}
/**
* Play the recording blob received as parameter. This parameter must be obtained from backend using the OpenVidu REST API
* @param blob
*/
playRecording(blob: Blob) {
const src = URL.createObjectURL(blob);
this.actionService.openRecordingPlayerDialog(this.sanitizer.bypassSecurityTrustResourceUrl(src), blob.type, true);
}
/**
* Download the the recording blob received as second parameter and renamed with the value of the firts parameter.
* This parameter must be obtained from backend using the OpenVidu REST API
* @param fileName
* @param blob
*/
downloadRecording(fileName: string, blob: Blob) {
const data = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = data;
link.download = `${fileName}.mp4`;
// this is necessary as link.click() does not work on the latest firefox
link.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
})
);
setTimeout(() => {
// For Firefox it is necessary to delay revoking the ObjectURL
window.URL.revokeObjectURL(data);
link.remove();
}, 100);
}
private startRecordingTime() {
this.recordingTime = new Date();
this.recordingTime.setHours(0, 0, 0, 0);
this.recordingTimeInterval = setInterval(() => {
this.recordingTime.setSeconds(this.recordingTime.getSeconds() + 1);
this.recordingTime = new Date(this.recordingTime.getTime());
this.recordingStatus.next({ info: this.currentRecording, time: this.recordingTime });
}, 1000);
}
private stopRecordingTime() {
clearInterval(this.recordingTimeInterval);
this.recordingTime = null;
}
}

View File

@ -11,6 +11,9 @@ import * as nl from '../../lang/nl.json';
import * as pt from '../../lang/pt.json';
import { StorageService } from '../storage/storage.service';
/**
* @internal
*/
@Injectable({
providedIn: 'root'
})

View File

@ -19,10 +19,10 @@ export * from './lib/services/layout/layout.service';
export * from './lib/services/panel/panel.service';
export * from './lib/services/cdk-overlay/cdk-overlay.service';
export * from './lib/services/storage/storage.service';
export * from './lib/services/recording/recording.service';
// Components
export * from './lib/components/videoconference/videoconference.component';
// export * from './lib/components/user-settings/user-settings.component';
export * from './lib/components/toolbar/toolbar.component';
export * from './lib/components/panel/panel.component';
export * from './lib/components/panel/chat-panel/chat-panel.component';
@ -46,9 +46,11 @@ export * from './lib/models/notification-options.model';
export * from './lib/models/token.model';
export * from './lib/models/signal.model';
export * from './lib/models/panel.model';
export * from './lib/models/recording.model';
// Pipes
export * from './lib/pipes/participant.pipe';
export * from './lib/pipes/recording.pipe';
// Directives
export * from './lib/directives/api/api.directive.module';
@ -59,3 +61,5 @@ export * from './lib/directives/api/toolbar.directive';
export * from './lib/directives/api/stream.directive';
export * from './lib/directives/api/videoconference.directive';
export * from './lib/directives/api/participant-panel-item.directive';
export * from './lib/directives/api/activities-panel.directive';
export * from './lib/directives/api/recording-activity.directive';

View File

@ -7,8 +7,10 @@
[videoMuted]="_videoMuted"
[audioMuted]="_audioMuted"
[toolbarScreenshareButton]="_toolbarScreenshareButton"
[toolbarRecordingButton]="_toolbarRecordingButton"
[toolbarFullscreenButton]="_toolbarFullscreenButton"
[toolbarLeaveButton]="_toolbarLeaveButton"
[toolbarActivitiesPanelButton]="_toolbarActivitiesPanelButton"
[toolbarChatPanelButton]="_toolbarChatPanelButton"
[toolbarParticipantsPanelButton]="_toolbarParticipantsPanelButton"
[toolbarDisplayLogo]="_toolbarDisplayLogo"
@ -17,6 +19,9 @@
[streamDisplayAudioDetection]="_streamDisplayAudioDetection"
[streamSettingsButton]="_streamSettingsButton"
[participantPanelItemMuteButton]="_participantPanelItemMuteButton"
[activitiesPanelRecordingActivity]="_activitiesPanelRecordingActivity"
[recordingActivityRecordingsList]="_recordingActivityRecordingsList"
[recordingActivityRecordingError]="_recordingActivityRecordingError"
(onJoinButtonClicked)="_onJoinButtonClicked()"
(onToolbarLeaveButtonClicked)="_onToolbarLeaveButtonClicked()"
(onToolbarCameraButtonClicked)="_onToolbarCameraButtonClicked()"
@ -24,7 +29,15 @@
(onToolbarScreenshareButtonClicked)="_onToolbarScreenshareButtonClicked()"
(onToolbarParticipantsPanelButtonClicked)="_onToolbarParticipantsPanelButtonClicked()"
(onToolbarChatPanelButtonClicked)="_onToolbarChatPanelButtonClicked()"
(onToolbarActivitiesPanelButtonClicked)="_onToolbarActivitiesPanelButtonClicked()"
(onToolbarFullscreenButtonClicked)="_onToolbarFullscreenButtonClicked()"
(onToolbarStartRecordingClicked)="onStartRecordingClicked('toolbar')"
(onToolbarStopRecordingClicked)="onStopRecordingClicked('toolbar')"
(onActivitiesPanelStartRecordingClicked)="onStartRecordingClicked('panel')"
(onActivitiesPanelStopRecordingClicked)="onStopRecordingClicked('panel')"
(onActivitiesPanelDownloadRecordingClicked)="_onActivitiesDownloadRecordingClicked($event)"
(onActivitiesPanelDeleteRecordingClicked)="_onActivitiesDeleteRecordingClicked($event)"
(onActivitiesPanelPlayRecordingClicked)="_onActivitiesPlayRecordingClicked($event)"
(onSessionCreated)="_onSessionCreated($event)"
(onParticipantCreated)="_onParticipantCreated($event)"
></ov-videoconference>

View File

@ -1,4 +1,3 @@
@use '@angular/material' as mat;
@import '~@angular/material/theming';
@ -20,13 +19,15 @@ $openvidu-components-warn: mat.define-palette(mat.$red-palette);
// Create the theme object. A theme consists of configurations for individual
// theming systems such as "color" or "typography".
$openvidu-components-theme: mat.define-light-theme((
color: (
primary: $openvidu-components-primary,
accent: $openvidu-components-accent,
warn: $openvidu-components-warn,
)
));
$openvidu-components-theme: mat.define-light-theme(
(
color: (
primary: $openvidu-components-primary,
accent: $openvidu-components-accent,
warn: $openvidu-components-warn
)
)
);
// Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component
@ -37,8 +38,8 @@ $openvidu-components-theme: mat.define-light-theme((
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/materialicons/v38/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');
}
src: url(https://fonts.gstatic.com/s/materialicons/v129/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons';
@ -56,13 +57,19 @@ $openvidu-components-theme: mat.define-light-theme((
-webkit-font-smoothing: antialiased;
}
html, body { height: 100%; overflow: hidden;}
body { margin: 0; font-family: 'Roboto','RobotoDraft',Helvetica,Arial,sans-serif;}
html,
body {
height: 100%;
overflow: hidden;
}
body {
margin: 0;
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
}
#poster-text {
padding: 0px !important;
}
ov-chat-panel .text-info {
margin: 3px auto !important;
}
}

View File

@ -1,12 +1,6 @@
import { Component, ElementRef, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { ILogger, LoggerService, OpenViduService } from 'openvidu-angular';
import { ILogger, LoggerService, OpenViduService, TokenModel, ParticipantAbstractModel, RecordingInfo } from 'openvidu-angular';
import { Session } from 'openvidu-browser';
import { ParticipantAbstractModel } from '../../../projects/openvidu-angular/src/lib/models/participant.model';
export interface TokenModel {
webcam: string;
screen: string;
}
/**
*
@ -45,6 +39,10 @@ export class OpenviduWebComponentComponent implements OnInit {
* @internal
*/
_toolbarScreenshareButton: boolean = true;
/**
* @internal
*/
_toolbarRecordingButton: boolean = true;
/**
* @internal
*/
@ -61,6 +59,10 @@ export class OpenviduWebComponentComponent implements OnInit {
* @internal
*/
_toolbarChatPanelButton: boolean = true;
/**
* @internal
*/
_toolbarActivitiesPanelButton: boolean = true;
/**
* @internal
*/
@ -89,6 +91,19 @@ export class OpenviduWebComponentComponent implements OnInit {
* @internal
*/
_participantPanelItemMuteButton: boolean = true;
/**
* @internal
*/
_recordingActivityRecordingError: any = null;
/**
* @internal
*/
_activitiesPanelRecordingActivity: boolean = true;
/**
* @internal
*/
_recordingActivityRecordingsList: RecordingInfo[] = [];
/**
* The **minimal** attribute applies a minimal UI hiding all controls except for cam and mic.
@ -167,6 +182,21 @@ export class OpenviduWebComponentComponent implements OnInit {
@Input() set toolbarScreenshareButton(value: string | boolean) {
this._toolbarScreenshareButton = this.castToBoolean(value);
}
/**
* The **toolbarRecordingButton** attribute allows show/hide the start/stop recording toolbar button.
*
* Default: `true`
*
* <div class="warn-container">
* <span>WARNING</span>: If you want to use this parameter to OpenVidu Web Component statically, you have to replace the <strong>camelCase</strong> with a <strong>hyphen between words</strong>.</div>
*
* @example
* <openvidu-webcomponent toolbar-recording-button="false"></openvidu-webcomponent>
*/
@Input() set toolbarRecordingButton(value: string | boolean) {
this._toolbarRecordingButton = this.castToBoolean(value);
}
/**
* The **toolbarFullscreenButton** attribute allows show/hide the fullscreen toolbar button.
*
@ -224,6 +254,21 @@ export class OpenviduWebComponentComponent implements OnInit {
@Input() set toolbarChatPanelButton(value: string | boolean) {
this._toolbarChatPanelButton = this.castToBoolean(value);
}
/**
* The **toolbarActivitiesPanelButton** attribute allows show/hide the activities panel toolbar button.
*
* Default: `true`
*
* <div class="warn-container">
* <span>WARNING</span>: If you want to use this parameter to OpenVidu Web Component statically, you have to replace the <strong>camelCase</strong> with a <strong>hyphen between words</strong>.</div>
*
* @example
* <openvidu-webcomponent toolbar-activities-panel-button="false"></openvidu-webcomponent>
*/
@Input() set toolbarActivitiesPanelButton(value: string | boolean) {
this._toolbarActivitiesPanelButton = this.castToBoolean(value);
}
/**
* The **toolbarParticipantsPanelButton** attribute allows show/hide the participants panel toolbar button.
*
@ -323,6 +368,42 @@ export class OpenviduWebComponentComponent implements OnInit {
this._participantPanelItemMuteButton = this.castToBoolean(value);
}
/**
* The **recordingActivityRecordingError** attribute allows to show any possible error with the recording in the {@link RecordingActivityComponent}.
*
* Default: `true`
*
* @example
* <openvidu-webcomponent recording-activity-recording-error="false"></openvidu-webcomponent>
*/
@Input() set recordingActivityRecordingError(value: any) {
this._recordingActivityRecordingError = value;
}
/**
* The **activitiesPanelRecordingActivity** attribute allows show/hide the recording activity in {@link ActivitiesPanelComponent}.
*
* Default: `true`
*
* @example
* <openvidu-webcomponent activity-panel-recording-activity="false"></openvidu-webcomponent>
*/
@Input() set activitiesPanelRecordingActivity(value: string | boolean) {
this._activitiesPanelRecordingActivity = this.castToBoolean(value);
}
/**
* The **recordingActivityRecordingList** attribute allows show to show the recordings available for the session in {@link RecordingActivityComponent}.
*
* Default: `[]`
*
* @example
* <openvidu-webcomponent recording-activity-recordings-list="recordingsList"></openvidu-webcomponent>
*/
@Input() set recordingActivityRecordingsList(value: RecordingInfo[]) {
this._recordingActivityRecordingsList = value;
}
/**
* Provides event notifications that fire when join button (in prejoin page) has been clicked.
*/
@ -363,6 +444,46 @@ export class OpenviduWebComponentComponent implements OnInit {
*/
@Output() onToolbarChatPanelButtonClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when activities panel button has been clicked.
*/
@Output() onToolbarActivitiesPanelButtonClicked: EventEmitter<void> = new EventEmitter<void>();
@Output() onToolbarStartRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when stop recording button is clicked from {@link ToolbarComponent}.
* The recording should be stopped using the REST API.
*/
@Output() onToolbarStopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when start recording button is clicked {@link ActivitiesPanelComponent}.
* The recording should be stopped using the REST API.
*/
@Output() onActivitiesPanelStartRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when stop recording button is clicked from {@link ActivitiesPanelComponent}.
* The recording should be stopped using the REST API.
*/
@Output() onActivitiesPanelStopRecordingClicked: EventEmitter<void> = new EventEmitter<void>();
/**
* Provides event notifications that fire when download recording button is clicked from {@link ActivitiesPanelComponent}.
* The recording should be downloaded using the REST API.
*/
@Output() onActivitiesPanelDownloadRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when delete recording button is clicked from {@link ActivitiesPanelComponent}.
* The recording should be deleted using the REST API.
*/
@Output() onActivitiesPanelDeleteRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when play recording button is clicked from {@link ActivitiesPanelComponent}.
*/
@Output() onActivitiesPanelPlayRecordingClicked: EventEmitter<string> = new EventEmitter<string>();
/**
* Provides event notifications that fire when OpenVidu Session is created.
* See {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Session.html openvidu-browser Session}.
@ -393,6 +514,7 @@ export class OpenviduWebComponentComponent implements OnInit {
/**
* @example
* <openvidu-webcomponent tokens='{"webcam":"TOKEN1", "screen":"TOKEN2"}'></openvidu-webcomponent>
* * <openvidu-webcomponent tokens='TOKEN'></openvidu-webcomponent>
*/
@Input('tokens')
set tokens(value: TokenModel | string) {
@ -455,12 +577,56 @@ export class OpenviduWebComponentComponent implements OnInit {
_onToolbarChatPanelButtonClicked() {
this.onToolbarChatPanelButtonClicked.emit();
}
_onToolbarActivitiesPanelButtonClicked() {
this.onToolbarActivitiesPanelButtonClicked.emit();
}
/**
* @internal
*/
_onToolbarFullscreenButtonClicked() {
this.onToolbarFullscreenButtonClicked.emit();
}
onStartRecordingClicked(from: string) {
if (from === 'toolbar') {
this.onToolbarStartRecordingClicked.emit();
} else if (from === 'panel') {
this.onActivitiesPanelStartRecordingClicked.emit();
}
}
/**
* @internal
*/
onStopRecordingClicked(from: string) {
if (from === 'toolbar') {
this.onToolbarStopRecordingClicked.emit();
} else if (from === 'panel') {
this.onActivitiesPanelStopRecordingClicked.emit();
}
}
/**
* @internal
*/
_onActivitiesDownloadRecordingClicked(recordingId: string) {
this.onActivitiesPanelDownloadRecordingClicked.emit(recordingId);
}
/**
* @internal
*/
_onActivitiesDeleteRecordingClicked(recordingId: string) {
this.onActivitiesPanelDeleteRecordingClicked.emit(recordingId);
}
/**
* @internal
*/
_onActivitiesPlayRecordingClicked(recordingId: string) {
this.onActivitiesPanelPlayRecordingClicked.emit(recordingId);
}
/**
* @internal
*/