From bcf636b5075950b542161665d4012a2fa0b51fc4 Mon Sep 17 00:00:00 2001 From: pabloFuente Date: Thu, 1 Mar 2018 11:25:25 +0100 Subject: [PATCH] openvidu-testapp local recording --- openvidu-testapp/src/app/app.module.ts | 14 +- .../openvidu-instance.component.ts | 238 +++++++++++++++++- .../test-apirest/test-apirest.component.ts | 2 +- .../local-recording-dialog.component.ts | 65 +++++ .../test-sessions/test-sessions.component.css | 2 +- .../app/services/mute-subscribers.service.ts | 21 ++ openvidu-testapp/src/styles.css | 77 +++++- 7 files changed, 400 insertions(+), 19 deletions(-) create mode 100644 openvidu-testapp/src/app/components/test-sessions/local-recording-dialog.component.ts create mode 100644 openvidu-testapp/src/app/services/mute-subscribers.service.ts diff --git a/openvidu-testapp/src/app/app.module.ts b/openvidu-testapp/src/app/app.module.ts index 588ad574..d1093a87 100644 --- a/openvidu-testapp/src/app/app.module.ts +++ b/openvidu-testapp/src/app/app.module.ts @@ -11,9 +11,12 @@ import { TestSessionsComponent } from './components/test-sessions/test-sessions. import { TestApirestComponent } from './components/test-apirest/test-apirest.component'; import { OpenviduInstanceComponent } from './components/openvidu-instance/openvidu-instance.component'; import { ExtensionDialogComponent } from './components/openvidu-instance/extension-dialog.component'; +import { LocalRecordingDialogComponent } from './components/test-sessions/local-recording-dialog.component'; + import { OpenviduRestService } from './services/openvidu-rest.service'; import { OpenviduParamsService } from './services/openvidu-params.service'; import { TestFeedService } from './services/test-feed.service'; +import { MuteSubscribersService } from './services/mute-subscribers.service'; @NgModule({ declarations: [ @@ -21,7 +24,8 @@ import { TestFeedService } from './services/test-feed.service'; OpenviduInstanceComponent, TestSessionsComponent, TestApirestComponent, - ExtensionDialogComponent + ExtensionDialogComponent, + LocalRecordingDialogComponent ], imports: [ BrowserModule, @@ -34,9 +38,13 @@ import { TestFeedService } from './services/test-feed.service'; providers: [ OpenviduRestService, OpenviduParamsService, - TestFeedService + TestFeedService, + MuteSubscribersService + ], + entryComponents: [ + ExtensionDialogComponent, + LocalRecordingDialogComponent ], - entryComponents: [ExtensionDialogComponent], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts index 6a30d9f5..08812cda 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts @@ -2,10 +2,14 @@ import { Component, Input, HostListener, ChangeDetectorRef, SimpleChanges, ElementRef, ViewChild, OnInit, OnDestroy, OnChanges } from '@angular/core'; -import { OpenVidu, Session, Subscriber, Publisher, Stream, Connection } from 'openvidu-browser'; -import { MatDialog } from '@angular/material'; +import { Subscription } from 'rxjs/Subscription'; + +import { OpenVidu, Session, Subscriber, Publisher, Stream, Connection, LocalRecorder } from 'openvidu-browser'; +import { MatDialog, MatDialogRef } from '@angular/material'; import { ExtensionDialogComponent } from './extension-dialog.component'; +import { LocalRecordingDialogComponent } from '../test-sessions/local-recording-dialog.component'; import { TestFeedService } from '../../services/test-feed.service'; +import { MuteSubscribersService } from '../../services/mute-subscribers.service'; declare var $: any; @@ -94,10 +98,17 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { openviduError: any; + private publisherRecorder: LocalRecorder; + private publisherRecording = false; + private publisherPaused = false; + private muteSubscribersSubscription: Subscription; + constructor( private changeDetector: ChangeDetectorRef, private extensionDialog: MatDialog, - private testFeedService: TestFeedService + private recordDialog: MatDialog, + private testFeedService: TestFeedService, + private muteSubscribersService: MuteSubscribersService, ) { this.generateSessionInfo(); } @@ -116,6 +127,13 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { if (this.sessionConf.startSession) { this.joinSession(); } + + this.muteSubscribersSubscription = this.muteSubscribersService.mutedEvent$.subscribe( + muteOrUnmute => { + Object.keys(this.subscribers).forEach((key) => { + this.subscribers[key].videoElement.muted = muteOrUnmute; + }); + }); } ngOnChanges(changes: SimpleChanges) { @@ -128,6 +146,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { } ngOnDestroy() { + if (!!this.muteSubscribersSubscription) { this.muteSubscribersSubscription.unsubscribe(); } this.leaveSession(); } @@ -259,18 +278,53 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { this.unpublished ? this.publishIcon = 'play_arrow' : this.publishIcon = 'stop'; } - private appendUserData(videoElement, connection): void { + private appendSubscriberData(videoElement: HTMLVideoElement, connection: Connection): void { const dataNode = document.createElement('div'); dataNode.className = 'data-node'; dataNode.id = 'data-' + this.session.connection.connectionId + '-' + connection.connectionId; dataNode.innerHTML = '

' + connection.data + '

' + - ''; + '' + + '' + + ''; videoElement.parentNode.insertBefore(dataNode, videoElement.nextSibling); document.getElementById('sub-btn-' + this.session.connection.connectionId + '-' + connection.connectionId).addEventListener('click', this.subUnsubFromSubscriber.bind(this, connection.connectionId)); + document.getElementById('record-btn-' + this.session.connection.connectionId + '-' + connection.connectionId).addEventListener('click', + this.recordSubscriber.bind(this, connection.connectionId)); + document.getElementById('pause-btn-' + this.session.connection.connectionId + '-' + connection.connectionId).addEventListener('click', + this.pauseSubscriber.bind(this, connection.connectionId)); + } + + private appendPublisherData(videoElement: HTMLVideoElement): void { + const dataNode = document.createElement('div'); + dataNode.className = 'data-node'; + dataNode.id = 'data-' + this.session.connection.connectionId + '-' + this.session.connection.connectionId; + dataNode.innerHTML = + '' + + ''; + videoElement.parentNode.insertBefore(dataNode, videoElement.nextSibling); + document.getElementById('local-record-btn-' + this.session.connection.connectionId).addEventListener('click', + this.recordPublisher.bind(this)); + document.getElementById('local-pause-btn-' + this.session.connection.connectionId).addEventListener('click', + this.pausePublisher.bind(this)); } private removeUserData(connectionId: string): void { @@ -369,11 +423,108 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { }); } + recordPublisher(): void { + if (!this.publisherRecording) { + this.publisherRecorder = this.OV.initLocalRecorder(this.publisher.stream); + this.publisherRecorder.record(); + this.publisherRecording = true; + document.getElementById('local-record-icon-' + this.session.connection.connectionId).innerHTML = 'stop'; + document.getElementById('local-pause-btn-' + this.session.connection.connectionId).style.display = 'block'; + } else { + this.publisherRecorder.stop() + .then(() => { + let dialogRef: MatDialogRef; + dialogRef = this.recordDialog.open(LocalRecordingDialogComponent, { + disableClose: true, + data: { + recorder: this.publisherRecorder + } + }); + dialogRef.componentInstance.myReference = dialogRef; + + dialogRef.afterOpen().subscribe(() => { + this.afterOpenPreview(this.publisherRecorder); + }); + dialogRef.afterClosed().subscribe(() => { + this.afterClosePreview(this.publisherRecorder); + }); + }) + .catch((error) => { + console.error('Error stopping LocalRecorder: ' + error); + }); + this.restartPublisherRecord(); + } + } + + pausePublisher(): void { + if (!this.publisherPaused) { + this.publisherRecorder.pause(); + document.getElementById('local-pause-icon-' + this.session.connection.connectionId).innerHTML = 'play_arrow'; + } else { + this.publisherRecorder.resume(); + document.getElementById('local-pause-icon-' + this.session.connection.connectionId).innerHTML = 'pause'; + } + this.publisherPaused = !this.publisherPaused; + } + + recordSubscriber(connectionId: string): void { + const subscriber: Subscriber = this.subscribers[connectionId].subscriber; + const recording = this.subscribers[connectionId].recording; + if (!recording) { + this.subscribers[connectionId].recorder = this.OV.initLocalRecorder(subscriber.stream); + this.subscribers[connectionId].recorder.record(); + this.subscribers[connectionId].recording = true; + document.getElementById('record-icon-' + this.session.connection.connectionId + '-' + connectionId).innerHTML = 'stop'; + document.getElementById('pause-btn-' + this.session.connection.connectionId + '-' + connectionId).style.display = 'block'; + } else { + this.subscribers[connectionId].recorder.stop() + .then(() => { + let dialogRef: MatDialogRef; + dialogRef = this.recordDialog.open(LocalRecordingDialogComponent, { + disableClose: true, + data: { + recorder: this.subscribers[connectionId].recorder + } + }); + dialogRef.componentInstance.myReference = dialogRef; + + dialogRef.afterOpen().subscribe(() => { + this.afterOpenPreview(this.subscribers[connectionId].recorder); + }); + dialogRef.afterClosed().subscribe(() => { + this.afterClosePreview(this.subscribers[connectionId].recorder); + }); + }) + .catch((error) => { + console.error('Error stopping LocalRecorder: ' + error); + }); + this.restartSubscriberRecord(connectionId); + } + } + + pauseSubscriber(connectionId: string): void { + const subscriber: Subscriber = this.subscribers[connectionId].subscriber; + const subscriberPaused = this.subscribers[connectionId].paused; + if (!subscriberPaused) { + this.subscribers[connectionId].recorder.pause(); + document.getElementById('pause-icon-' + this.session.connection.connectionId + '-' + connectionId).innerHTML = 'play_arrow'; + } else { + this.subscribers[connectionId].recorder.resume(); + document.getElementById('pause-icon-' + this.session.connection.connectionId + '-' + connectionId).innerHTML = 'pause'; + } + this.subscribers[connectionId].paused = !this.subscribers[connectionId].paused; + } + publishUnpublish(): void { if (this.unpublished) { this.session.publish(this.publisher); } else { + if (!!this.publisherRecorder && this.publisherRecording) { + this.publisherRecorder.clean(); + } this.session.unpublish(this.publisher); + this.removeUserData(this.session.connection.connectionId); + this.restartPublisherRecord(); } this.unpublished = !this.unpublished; this.updatePublishIcon(); @@ -383,6 +534,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { if (!this.unpublished) { this.session.unpublish(this.publisher); + this.removeUserData(this.session.connection.connectionId); } let screenChange; @@ -448,9 +600,15 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { subUnsubFromSubscriber(connectionId: string) { let subscriber: Subscriber = this.subscribers[connectionId].subscriber; if (this.subscribers[connectionId].subbed) { + if (!!this.subscribers[connectionId].recorder && this.subscribers[connectionId].recording) { + this.subscribers[connectionId].recorder.clean(); + } this.session.unsubscribe(subscriber); + this.restartSubscriberRecord(connectionId); document.getElementById('data-' + this.session.connection.connectionId + '-' + connectionId).style.marginLeft = '0'; document.getElementById('icon-' + this.session.connection.connectionId + '-' + connectionId).innerHTML = 'notifications_off'; + document.getElementById('record-btn-' + this.session.connection.connectionId + '-' + connectionId).remove(); + document.getElementById('pause-btn-' + this.session.connection.connectionId + '-' + connectionId).remove(); } else { subscriber = this.session.subscribe(subscriber.stream, 'remote-vid-' + this.session.connection.connectionId); this.subscribers[connectionId].subscriber = subscriber; @@ -459,11 +617,12 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { $(e.element).css({ 'background-color': '#4d4d4d' }); $(e.element).attr('poster', 'assets/images/volume.png'); } - this.removeUserData(connectionId); - this.appendUserData(e.element, subscriber.stream.connection); + this.subscribers[connectionId].videoElement = e.element; this.updateEventList('videoElementCreated', e.element.id); }); subscriber.on('videoPlaying', (e) => { + this.removeUserData(connectionId); + this.appendSubscriberData(e.element, subscriber.stream.connection); this.updateEventList('videoPlaying', e.element.id); }); } @@ -477,21 +636,29 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { if (this.subscribeTo) { const subscriber: Subscriber = session.subscribe(event.stream, 'remote-vid-' + session.connection.connectionId); + this.subscribers[subscriber.stream.connection.connectionId] = { + 'subscriber': subscriber, + 'subbed': true, + 'recorder': undefined, + 'recording': false, + 'paused': false, + 'videoElement': undefined + }; subscriber.on('videoElementCreated', (e) => { if (!event.stream.getRecvVideo()) { $(e.element).css({ 'background-color': '#4d4d4d' }); $(e.element).attr('poster', 'assets/images/volume.png'); } - this.appendUserData(e.element, subscriber.stream.connection); + this.subscribers[subscriber.stream.connection.connectionId].videoElement = e.element; this.updateEventList('videoElementCreated', e.element.id); }); subscriber.on('videoPlaying', (e) => { + this.appendSubscriberData(e.element, subscriber.stream.connection); this.updateEventList('videoPlaying', e.element.id); }); subscriber.on('videoElementDestroyed', (e) => { this.updateEventList('videoElementDestroyed', '(Subscriber)'); }); - this.subscribers[subscriber.stream.connection.connectionId] = { 'subscriber': subscriber, 'subbed': true }; } this.updateEventList('streamCreated', event.stream.connection.connectionId); }); @@ -546,12 +713,13 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { this.updateEventList('accessDenied', ''); }); - publisher.on('videoPlaying', (e) => { + this.appendPublisherData(e.element); this.updateEventList('videoPlaying', e.element.id); }); publisher.on('remoteVideoPlaying', (e) => { + this.appendPublisherData(e.element); this.updateEventList('remoteVideoPlaying', e.element.id); }); @@ -568,4 +736,48 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { }); } + private afterOpenPreview(recorder: LocalRecorder): void { + this.muteSubscribersService.updateMuted(true); + recorder.preview('local-recorder-preview').controls = true; + } + + private afterClosePreview(recorder: LocalRecorder): void { + this.muteSubscribersService.updateMuted(false); + recorder.clean(); + } + + private restartPublisherRecord(): void { + let el: HTMLElement = document.getElementById('local-record-icon-' + this.session.connection.connectionId); + if (!!el) { + el.innerHTML = 'fiber_manual_record'; + } + el = document.getElementById('local-pause-icon-' + this.session.connection.connectionId); + if (!!el) { + el.innerHTML = 'pause'; + } + el = document.getElementById('local-pause-btn-' + this.session.connection.connectionId); + if (!!el) { + el.style.display = 'none'; + } + this.publisherPaused = false; + this.publisherRecording = false; + } + + private restartSubscriberRecord(connectionId: string): void { + let el: HTMLElement = document.getElementById('record-icon-' + this.session.connection.connectionId + '-' + connectionId); + if (!!el) { + el.innerHTML = 'fiber_manual_record'; + } + el = document.getElementById('pause-icon-' + this.session.connection.connectionId + '-' + connectionId); + if (!!el) { + el.innerHTML = 'pause'; + } + el = document.getElementById('pause-btn-' + this.session.connection.connectionId + '-' + connectionId); + if (!!el) { + el.style.display = 'none'; + } + this.subscribers[connectionId].recording = false; + this.subscribers[connectionId].paused = false; + } + } diff --git a/openvidu-testapp/src/app/components/test-apirest/test-apirest.component.ts b/openvidu-testapp/src/app/components/test-apirest/test-apirest.component.ts index 1f7269ea..e7465080 100644 --- a/openvidu-testapp/src/app/components/test-apirest/test-apirest.component.ts +++ b/openvidu-testapp/src/app/components/test-apirest/test-apirest.component.ts @@ -53,7 +53,7 @@ export class TestApirestComponent implements OnInit, OnDestroy { } ngOnDestroy() { - this.paramsSubscription.unsubscribe(); + if (!!this.paramsSubscription) { this.paramsSubscription.unsubscribe(); } } private getSessionId() { diff --git a/openvidu-testapp/src/app/components/test-sessions/local-recording-dialog.component.ts b/openvidu-testapp/src/app/components/test-sessions/local-recording-dialog.component.ts new file mode 100644 index 00000000..92e2a77a --- /dev/null +++ b/openvidu-testapp/src/app/components/test-sessions/local-recording-dialog.component.ts @@ -0,0 +1,65 @@ +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import { LocalRecorder } from 'openvidu-browser'; + +@Component({ + selector: 'app-local-recording-dialog', + template: ` +
+
+
+
+ + + + + + + {{uploadIcon}} +
+ `, + styles: [` + #quality-div { + margin-top: 20px; + } + `], +}) +export class LocalRecordingDialogComponent { + + public myReference: MatDialogRef; + + private recorder: LocalRecorder; + + private uploading = false; + private endpoint = ''; + private uploadIcon: string; + private iconColor: string; + private iconClass = ''; + + constructor(@Inject(MAT_DIALOG_DATA) public data: any) { + this.recorder = data.recorder; + } + + close() { + this.myReference.close(); + } + + uploadFile() { + this.iconColor = 'black'; + this.iconClass = 'rotating'; + this.uploadIcon = 'cached'; + this.uploading = true; + this.recorder.uploadAsBinary(this.endpoint) + .then(() => { + this.iconColor = 'green'; + this.uploadIcon = 'done'; + this.iconClass = ''; + }) + .catch((e) => { + this.iconColor = 'red'; + this.uploadIcon = 'clear'; + this.iconClass = ''; + }); + } +} diff --git a/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.css b/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.css index fda797b8..dc9aaed1 100644 --- a/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.css +++ b/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.css @@ -26,4 +26,4 @@ mat-form-field { .auto-join-check { margin-right: 10px; -} \ No newline at end of file +} diff --git a/openvidu-testapp/src/app/services/mute-subscribers.service.ts b/openvidu-testapp/src/app/services/mute-subscribers.service.ts new file mode 100644 index 00000000..aa0f97df --- /dev/null +++ b/openvidu-testapp/src/app/services/mute-subscribers.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs/Subject'; + +@Injectable() +export class MuteSubscribersService { + + muted = false; + mutedEvent$ = new Subject(); + + constructor() { } + + getMuted() { + return this.muted; + } + + updateMuted(muted: boolean) { + this.muted = muted; + this.mutedEvent$.next(this.muted); + } + +} diff --git a/openvidu-testapp/src/styles.css b/openvidu-testapp/src/styles.css index a6dc82f6..84fe709b 100644 --- a/openvidu-testapp/src/styles.css +++ b/openvidu-testapp/src/styles.css @@ -66,7 +66,7 @@ button { .video-container div.data-node .sub-btn { outline: 0; border: none; - background: white; + background: rgba(255, 255, 255, 0.75); cursor: pointer; padding: 0; margin-top: 40px; @@ -77,10 +77,26 @@ button { color: #4d4d4d; } +.video-container div.data-node .rec-btn { + float: right; + border-top-right-radius: 0; + border-top-left-radius: 2px; + color: #ac0000; +} + +.video-container div.data-node .rec-btn:hover { + color: #ac000082; +} + +.video-container div.data-node .rec-btn.publisher-rec-btn { + margin-top: 70px; +} + .video-container div.data-node .material-icons { font-size: 17px; width: 17px; height: 17px; + line-height: 20px; } .mat-expansion-panel-body { @@ -105,3 +121,62 @@ button { .mat-expansion-panel-spacing { margin: 3px 0 !important; } + +#local-recorder-preview video { + width: 500px; + height: inherit; +} + +/*Rotating animation*/ + +@-webkit-keyframes rotating +/* Safari and Chrome */ + + { + from { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } + to { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } +} + +@keyframes rotating { + from { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } + to { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } +} + +.rotating { + -webkit-animation: rotating 1s linear infinite; + -moz-animation: rotating 1s linear infinite; + -ms-animation: rotating 1s linear infinite; + -o-animation: rotating 1s linear infinite; + animation: rotating 1s linear infinite; + cursor: default !important; +} + +.rotating:hover { + color: inherit !important; +} + +/*End rotating animation*/