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*/