From 0315a75187725b69da89f601f14ae5d37297dc69 Mon Sep 17 00:00:00 2001 From: pabloFuente Date: Tue, 29 May 2018 18:32:49 +0200 Subject: [PATCH] openvidu-testapp refactoring to StreamManager openvidu-brwoser API --- openvidu-testapp/.angular-cli.json | 3 +- openvidu-testapp/src/app/app.module.ts | 7 + .../dialogs/events-dialog.component.ts | 47 + .../local-recording-dialog.component.ts | 12 +- .../dialogs/session-api-dialog.component.ts | 2 +- .../openvidu-instance.component.css | 22 +- .../openvidu-instance.component.html | 46 +- .../openvidu-instance.component.ts | 811 +++++------------- .../test-sessions.component.html | 2 +- .../components/video/ov-video.component.ts | 28 + .../app/components/video/video.component.css | 98 +++ .../app/components/video/video.component.html | 58 ++ .../app/components/video/video.component.ts | 569 ++++++++++++ .../fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 | Bin 0 -> 49028 bytes openvidu-testapp/src/index.html | 5 - openvidu-testapp/src/material-icons.css | 23 + openvidu-testapp/src/styles.css | 61 -- 17 files changed, 1074 insertions(+), 720 deletions(-) create mode 100644 openvidu-testapp/src/app/components/dialogs/events-dialog.component.ts create mode 100644 openvidu-testapp/src/app/components/video/ov-video.component.ts create mode 100644 openvidu-testapp/src/app/components/video/video.component.css create mode 100644 openvidu-testapp/src/app/components/video/video.component.html create mode 100644 openvidu-testapp/src/app/components/video/video.component.ts create mode 100644 openvidu-testapp/src/assets/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 create mode 100644 openvidu-testapp/src/material-icons.css diff --git a/openvidu-testapp/.angular-cli.json b/openvidu-testapp/.angular-cli.json index abb53e19..6bc4c127 100644 --- a/openvidu-testapp/.angular-cli.json +++ b/openvidu-testapp/.angular-cli.json @@ -20,7 +20,8 @@ "prefix": "app", "styles": [ "styles.css", - "openvidu-theme.scss" + "openvidu-theme.scss", + "material-icons.css" ], "scripts": [], "environmentSource": "environments/environment.ts", diff --git a/openvidu-testapp/src/app/app.module.ts b/openvidu-testapp/src/app/app.module.ts index ed4f1a80..4cad47d0 100644 --- a/openvidu-testapp/src/app/app.module.ts +++ b/openvidu-testapp/src/app/app.module.ts @@ -10,6 +10,8 @@ import { AppComponent } from './app.component'; import { TestSessionsComponent } from './components/test-sessions/test-sessions.component'; import { TestApirestComponent } from './components/test-apirest/test-apirest.component'; import { OpenviduInstanceComponent } from './components/openvidu-instance/openvidu-instance.component'; +import { VideoComponent } from './components/video/video.component'; +import { OpenViduVideoComponent } from './components/video/ov-video.component'; import { ExtensionDialogComponent } from './components/dialogs/extension-dialog.component'; import { LocalRecordingDialogComponent } from './components/dialogs/local-recording-dialog.component'; @@ -19,16 +21,20 @@ import { TestFeedService } from './services/test-feed.service'; import { MuteSubscribersService } from './services/mute-subscribers.service'; import { SessionPropertiesDialogComponent } from './components/dialogs/session-properties-dialog.component'; import { SessionApiDialogComponent } from './components/dialogs/session-api-dialog.component'; +import { EventsDialogComponent } from './components/dialogs/events-dialog.component'; @NgModule({ declarations: [ AppComponent, OpenviduInstanceComponent, + VideoComponent, + OpenViduVideoComponent, TestSessionsComponent, TestApirestComponent, ExtensionDialogComponent, SessionPropertiesDialogComponent, SessionApiDialogComponent, + EventsDialogComponent, LocalRecordingDialogComponent ], imports: [ @@ -49,6 +55,7 @@ import { SessionApiDialogComponent } from './components/dialogs/session-api-dial ExtensionDialogComponent, SessionPropertiesDialogComponent, SessionApiDialogComponent, + EventsDialogComponent, LocalRecordingDialogComponent ], bootstrap: [AppComponent] diff --git a/openvidu-testapp/src/app/components/dialogs/events-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/events-dialog.component.ts new file mode 100644 index 00000000..24438242 --- /dev/null +++ b/openvidu-testapp/src/app/components/dialogs/events-dialog.component.ts @@ -0,0 +1,47 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; + +@Component({ + selector: 'app-events-dialog', + template: ` +

{{target}} events

+ + ALL + + {{event}} + + + + + + `, + styles: [ + 'mat-dialog-content { display: inline; }', + 'mat-divider { margin-top: 5px; margin-bottom: 5px }' + ] +}) +export class EventsDialogComponent { + + target = ''; + checkAll = true; + eventCollection: any = {}; + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data) { + this.target = data.target; + this.eventCollection = data.eventCollection; + } + + updateAll() { + Object.keys(this.eventCollection).forEach(key => { + this.eventCollection[key] = this.checkAll; + }); + } + + eventNamesArray(): String[] { + return Object.keys(this.eventCollection); + } + +} diff --git a/openvidu-testapp/src/app/components/dialogs/local-recording-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/local-recording-dialog.component.ts index 879c326d..cecfeace 100644 --- a/openvidu-testapp/src/app/components/dialogs/local-recording-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/local-recording-dialog.component.ts @@ -29,13 +29,13 @@ export class LocalRecordingDialogComponent { public myReference: MatDialogRef; - private recorder: LocalRecorder; + recorder: LocalRecorder; - private uploading = false; - private endpoint = ''; - private uploadIcon: string; - private iconColor: string; - private iconClass = ''; + uploading = false; + endpoint = ''; + uploadIcon: string; + iconColor: string; + iconClass = ''; constructor(@Inject(MAT_DIALOG_DATA) public data: any) { this.recorder = data.recorder; diff --git a/openvidu-testapp/src/app/components/dialogs/session-api-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/session-api-dialog.component.ts index 1ab951d3..6925e9e4 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-api-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/session-api-dialog.component.ts @@ -5,7 +5,7 @@ import { Session } from 'openvidu-browser'; import { OpenVidu as OpenViduAPI } from 'openvidu-node-client'; @Component({ - selector: 'app-session-properties-dialog', + selector: 'app-session-api-dialog', template: `

API REST

diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.css b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.css index fad4dfe6..97bbf69e 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.css +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.css @@ -34,7 +34,7 @@ mat-card.session-card { .inner-card { border: 1px solid #e1e1e1; - padding: 10px 15px 10px 15px; + padding: 10px 3px 10px 10px; background: #ffffff; margin-top: 5px; } @@ -138,19 +138,25 @@ mat-expansion-panel-header { } .mat-icon-custom { - width: 29px; - height: 29px; - line-height: 18px; + width: 24px; + height: 24px; + line-height: 17px; + margin-bottom: -4px; } .mat-icon-custom-ic { - width: 20px; - height: 20px; - font-size: 20px; - line-height: 20px; + width: 18px; + height: 18px; + font-size: 18px; + line-height: 18px; } .session-btns-div { margin-top: -14px; margin-right: -14px; + margin-left: 5px; +} + +.publisher-btns-div { + margin-top: -7px; } \ No newline at end of file diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html index f9fce22a..ef37cd93 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.html @@ -8,20 +8,25 @@
- + - +
- - +
@@ -36,7 +41,7 @@
-
+

Send

@@ -57,7 +62,7 @@
-
+
Video @@ -71,6 +76,19 @@
to remote
+
+
+ + +
+
+
@@ -85,21 +103,9 @@
{{sessionName}}
- - - - @@ -123,6 +129,10 @@
+ + + +
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 3e36ce5c..5ede01b0 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 @@ -7,7 +7,7 @@ import { Subscription } from 'rxjs/Subscription'; import { OpenVidu, Session, Subscriber, Publisher, Stream, Connection, LocalRecorder, VideoInsertMode, StreamEvent, ConnectionEvent, - SessionDisconnectedEvent, SignalEvent, RecordingEvent, VideoElementEvent + SessionDisconnectedEvent, SignalEvent, RecordingEvent, VideoElementEvent, PublisherSpeakingEvent, StreamManagerEvent, StreamManager } from 'openvidu-browser'; import { OpenVidu as OpenViduAPI, @@ -22,10 +22,10 @@ import { ExtensionDialogComponent } from '../dialogs/extension-dialog.component' import { LocalRecordingDialogComponent } from '../dialogs/local-recording-dialog.component'; import { TestFeedService } from '../../services/test-feed.service'; import { MuteSubscribersService } from '../../services/mute-subscribers.service'; +import { EventsDialogComponent } from '../dialogs/events-dialog.component'; import { SessionPropertiesDialogComponent } from '../dialogs/session-properties-dialog.component'; import { SessionApiDialogComponent } from '../dialogs/session-api-dialog.component'; -declare var $: any; export interface SessionConf { subscribeTo: boolean; @@ -56,6 +56,9 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { @Input() sessionConf: SessionConf; + @Input() + index: number; + // Session join data clientData: string; sessionName: string; @@ -91,7 +94,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { OV: OpenVidu; session: Session; publisher: Publisher; - subscribers = {}; + subscribers: Subscriber[] = []; // OpenVidu Node Client objects sessionProperties: SessionPropertiesAPI = { @@ -102,17 +105,18 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { customSessionId: '' }; - // Session audio and video status - audioMuted = false; - videoMuted = false; - unpublished = false; - publisherChanged = false; - audioIcon = 'mic'; - videoIcon = 'videocam'; - publishIcon = 'stop'; - - sendAudioChange: boolean; - sendVideoChange: boolean; + sessionEvents = { + connectionCreated: true, + connectionDestroyed: true, + sessionDisconnected: true, + streamCreated: true, + streamDestroyed: true, + recordingStarted: true, + recordingStopped: true, + signal: true, + publisherStartSpeaking: false, + publisherStopSpeaking: false + }; events: OpenViduEvent[] = []; @@ -127,8 +131,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { private changeDetector: ChangeDetectorRef, private dialog: MatDialog, private recordDialog: MatDialog, - private testFeedService: TestFeedService, - private muteSubscribersService: MuteSubscribersService, + private testFeedService: TestFeedService ) { this.generateSessionInfo(); } @@ -147,13 +150,6 @@ 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) { @@ -166,7 +162,6 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { } ngOnDestroy() { - if (!!this.muteSubscribersSubscription) { this.muteSubscribersSubscription.unsubscribe(); } this.leaveSession(); } @@ -199,24 +194,24 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { this.session = this.OV.initSession(); - this.addSessionEvents(this.session); + this.updateSessionEvents({ + connectionCreated: false, + connectionDestroyed: false, + sessionDisconnected: false, + streamCreated: false, + streamDestroyed: false, + recordingStarted: false, + recordingStopped: false, + signal: false, + publisherStartSpeaking: true, + publisherStopSpeaking: true + }, true); this.session.connect(token, this.clientData) .then(() => { this.changeDetector.detectChanges(); if (this.publishTo) { - - this.audioMuted = !this.activeAudio; - this.videoMuted = !this.activeVideo; - this.unpublished = false; - this.updateAudioIcon(); - this.updateVideoIcon(); - this.updatePublishIcon(); - - this.sendAudioChange = this.sendAudio; - this.sendVideoChange = this.sendVideo; - // this.asyncInitPublisher(); this.syncInitPublisher(); } @@ -226,113 +221,14 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { }); } - private leaveSession(): void { - if (!!this.publisherRecorder) { - this.restartPublisherRecord(); - } - Object.keys(this.subscribers).forEach((key) => { - if (!!this.subscribers[key].recorder) { - this.restartSubscriberRecord(key); - } - }); if (this.session) { this.session.disconnect(); } - this.session = null; - this.OV = null; - } - - private toggleAudio() { - this.publisher.publishAudio(this.audioMuted); - this.audioMuted = !this.audioMuted; - this.updateAudioIcon(); - } - - private toggleVideo() { - this.publisher.publishVideo(this.videoMuted); - this.videoMuted = !this.videoMuted; - this.updateVideoIcon(); - } - - private updateAudioIcon() { - this.audioMuted ? this.audioIcon = 'mic_off' : this.audioIcon = 'mic'; - } - - private updateVideoIcon() { - this.videoMuted ? this.videoIcon = 'videocam_off' : this.videoIcon = 'videocam'; - } - - private updatePublishIcon() { - this.unpublished ? this.publishIcon = 'play_arrow' : this.publishIcon = 'stop'; - } - - 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('sub-video-btn-' + this.session.connection.connectionId + '-' + connection.connectionId) - .addEventListener('click', this.subUnsubFromSubscriberVideo.bind(this, connection.connectionId)); - document.getElementById('sub-audio-btn-' + this.session.connection.connectionId + '-' + connection.connectionId) - .addEventListener('click', this.subUnsubFromSubscriberAudio.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 { - $('#remote-vid-' + this.session.connection.connectionId) - .find('#data-' + this.session.connection.connectionId + '-' + connectionId).remove(); + delete this.session; + delete this.OV; + delete this.publisher; + this.subscribers = []; } private updateEventList(event: string, content: string) { @@ -433,432 +329,122 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { // this.initGrayVideo(); } - 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; + updateSessionEvents(oldValues, firstTime) { - dialogRef.afterOpen().subscribe(() => { - this.afterOpenPreview(this.publisherRecorder); - }); - dialogRef.afterClosed().subscribe(() => { - this.afterClosePreview(); - }); - }) - .catch((error) => { - console.error('Error stopping LocalRecorder: ' + error); - }); - } - } - - 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(connectionId); - }); - }) - .catch((error) => { - console.error('Error stopping LocalRecorder: ' + error); - }); - } - } - - 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) - .then(() => { - console.log(this.publisher); - }) - .catch(e => { - console.error(e); - }); - } else { - this.session.unpublish(this.publisher); - this.removeUserData(this.session.connection.connectionId); - this.restartPublisherRecord(); - } - this.unpublished = !this.unpublished; - this.updatePublishIcon(); - } - - changePublisher() { - let screenChange; - if (!this.publisherChanged) { - if (this.sendAudio && !this.sendVideo) { - this.sendAudioChange = false; - this.sendVideoChange = true; - screenChange = false; - } else if (!this.sendAudio && this.sendVideo) { - this.sendAudioChange = true; - this.sendVideoChange = false; - } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'video') { - this.sendAudioChange = false; - this.sendVideoChange = true; - screenChange = true; - } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'screen') { - this.sendAudioChange = false; - this.sendVideoChange = true; - screenChange = false; - } - } else { - this.sendAudioChange = this.sendAudio; - this.sendVideoChange = this.sendVideo; - screenChange = this.optionsVideo === 'screen' ? true : false; - } - - this.audioMuted = false; - this.videoMuted = false; - this.unpublished = false; - this.updateAudioIcon(); - this.updateVideoIcon(); - this.updatePublishIcon(); - - const otherPublisher = this.OV.initPublisher( - 'local-vid-' + this.session.connection.connectionId, - { - audioSource: this.sendAudioChange ? undefined : false, - videoSource: this.sendVideoChange ? (screenChange ? 'screen' : undefined) : false, - publishAudio: (!this.publisherChanged) ? true : !this.audioMuted, - publishVideo: (!this.publisherChanged) ? true : !this.videoMuted, - resolution: '640x480', - frameRate: 30, - insertMode: VideoInsertMode.APPEND - }, - (err) => { - if (err) { - console.warn(err); - this.openviduError = err; - if (err.name === 'SCREEN_EXTENSION_NOT_INSTALLED') { - this.dialog.open(ExtensionDialogComponent, { - data: { url: err.message }, - disableClose: true, - width: '250px' - }); + if (this.sessionEvents.streamCreated !== oldValues.streamCreated || firstTime) { + this.session.off('streamCreated'); + if (this.sessionEvents.streamCreated) { + this.session.on('streamCreated', (event: StreamEvent) => { + this.changeDetector.detectChanges(); + if (this.subscribeTo) { + this.syncSubscribe(this.session, event); } - } - }); - this.addPublisherEvents(otherPublisher); - - otherPublisher.once('accessAllowed', () => { - if (!this.unpublished) { - this.session.unpublish(this.publisher); - this.publisher = otherPublisher; - this.removeUserData(this.session.connection.connectionId); - this.restartPublisherRecord(); - } - this.session.publish(otherPublisher); - }); - - this.publisherChanged = !this.publisherChanged; - } - - subUnsubFromSubscriber(connectionId: string) { - let subscriber: Subscriber = this.subscribers[connectionId].subscriber; - if (this.subscribers[connectionId].subbed) { - this.session.unsubscribe(subscriber); - this.restartSubscriberRecord(connectionId); - document.getElementById('data-' + this.session.connection.connectionId + '-' + connectionId).style.marginLeft = '0'; - document.getElementById('sub-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(); - document.getElementById('sub-video-btn-' + this.session.connection.connectionId + '-' + connectionId).remove(); - document.getElementById('sub-audio-btn-' + this.session.connection.connectionId + '-' + connectionId).remove(); - this.subscribers[connectionId].subbedVideo = false; - this.subscribers[connectionId].subbedAudio = false; - } else { - - - this.session.subscribeAsync(subscriber.stream, 'remote-vid-' + this.session.connection.connectionId) - .then(sub => { - subscriber = sub; - this.subscribers[connectionId].subscriber = subscriber; - subscriber.on('videoElementCreated', (e: VideoElementEvent) => { - if (!subscriber.stream.hasVideo) { - $(e.element).css({ 'background-color': '#4d4d4d' }); - $(e.element).attr('poster', 'assets/images/volume.png'); - } - this.subscribers[connectionId].videoElement = e.element; - this.updateEventList('videoElementCreated', e.element.id); - }); - subscriber.on('videoPlaying', (e: VideoElementEvent) => { - this.removeUserData(connectionId); - this.appendSubscriberData(e.element, subscriber.stream.connection); - this.updateEventList('videoPlaying', e.element.id); - }); - }) - .catch(err => { - console.error(err); + this.updateEventList('streamCreated', event.stream.streamId); }); - - - /*subscriber = this.session.subscribe(subscriber.stream, 'remote-vid-' + this.session.connection.connectionId); - this.subscribers[connectionId].subscriber = subscriber; - subscriber.on('videoElementCreated', (e) => { - if (!subscriber.stream.hasVideo) { - $(e.element).css({ 'background-color': '#4d4d4d' }); - $(e.element).attr('poster', 'assets/images/volume.png'); - } - 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); - });*/ - - - } - this.subscribers[connectionId].subbed = !this.subscribers[connectionId].subbed; - } - - subUnsubFromSubscriberVideo(connectionId: string) { - this.subscribers[connectionId].subscriber.subscribeToVideo(!!this.subscribers[connectionId].subbedVideo); - this.subscribers[connectionId].subbedVideo = !this.subscribers[connectionId].subbedVideo; - document.getElementById('sub-video-icon-' + this.session.connection.connectionId + '-' + connectionId).innerHTML = - this.subscribers[connectionId].subbedVideo ? 'videocam_off' : 'videocam'; - } - - subUnsubFromSubscriberAudio(connectionId: string) { - this.subscribers[connectionId].subscriber.subscribeToAudio(!!this.subscribers[connectionId].subbedAudio); - this.subscribers[connectionId].subbedAudio = !this.subscribers[connectionId].subbedAudio; - document.getElementById('sub-audio-icon-' + this.session.connection.connectionId + '-' + connectionId).innerHTML = - this.subscribers[connectionId].subbedAudio ? 'mic_off' : 'mic'; - } - - addSessionEvents(session: Session) { - session.on('streamCreated', (event: StreamEvent) => { - - this.changeDetector.detectChanges(); - - if (this.subscribeTo) { - // this.syncSubscribe(session, event); - this.asyncSubscribe(session, event); - } - this.updateEventList('streamCreated', event.stream.connection.connectionId); - }); - - session.on('streamDestroyed', (event: StreamEvent) => { - this.removeUserData(event.stream.connection.connectionId); - this.updateEventList('streamDestroyed', event.stream.connection.connectionId); - }); - session.on('connectionCreated', (event: ConnectionEvent) => { - this.updateEventList('connectionCreated', event.connection.connectionId); - }); - session.on('connectionDestroyed', (event: ConnectionEvent) => { - this.updateEventList('connectionDestroyed', event.connection.connectionId); - }); - session.on('sessionDisconnected', (event: SessionDisconnectedEvent) => { - this.updateEventList('sessionDisconnected', 'No data'); - if (event.reason === 'networkDisconnect') { - this.session = null; - this.OV = null; - } - }); - session.on('signal', (event: SignalEvent) => { - this.updateEventList('signal', event.from.connectionId + '-' + event.data); - }); - - session.on('recordingStarted', (event: RecordingEvent) => { - this.updateEventList('recordingStarted', event.id); - }); - - session.on('recordingStopped', (event: RecordingEvent) => { - this.updateEventList('recordingStopped', event.id); - }); - - /*session.on('publisherStartSpeaking', (event) => { - console.log('Publisher start speaking'); - }); - - session.on('publisherStopSpeaking', (event) => { - console.log('Publisher stop speaking'); - });*/ - } - - addPublisherEvents(publisher: Publisher) { - publisher.on('videoElementCreated', (event: VideoElementEvent) => { - if (this.publishTo && - (!this.sendVideoChange || - this.sendVideoChange && - !(this.optionsVideo !== 'screen') && - this.openviduError && - this.openviduError.name === 'NO_VIDEO_DEVICE')) { - $(event.element).css({ 'background-color': '#4d4d4d' }); - $(event.element).attr('poster', 'assets/images/volume.png'); - } - this.updateEventList('videoElementCreated', event.element.id); - }); - - publisher.on('accessAllowed', (e) => { - this.updateEventList('accessAllowed', ''); - }); - - publisher.on('accessDenied', (e) => { - this.updateEventList('accessDenied', ''); - }); - - publisher.on('accessDialogOpened', (e) => { - this.updateEventList('accessDialogOpened', ''); - }); - - publisher.on('accessDialogClosed', (e) => { - this.updateEventList('accessDialogClosed', ''); - }); - - publisher.on('videoPlaying', (e: VideoElementEvent) => { - this.appendPublisherData(e.element); - this.updateEventList('videoPlaying', e.element.id); - }); - - publisher.on('remoteVideoPlaying', (e: VideoElementEvent) => { - this.appendPublisherData(e.element); - this.updateEventList('remoteVideoPlaying', e.element.id); - }); - - publisher.on('streamCreated', (e: StreamEvent) => { - this.updateEventList('streamCreated', e.stream.connection.connectionId); - }); - - publisher.on('streamDestroyed', (e: StreamEvent) => { - this.updateEventList('streamDestroyed', e.stream.connection.connectionId); - }); - - publisher.on('videoElementDestroyed', (e: VideoElementEvent) => { - this.updateEventList('videoElementDestroyed', '(Publisher)'); - }); - } - - private afterOpenPreview(recorder: LocalRecorder): void { - this.muteSubscribersService.updateMuted(true); - recorder.preview('recorder-preview').controls = true; - } - - private afterClosePreview(connectionId?: string): void { - this.muteSubscribersService.updateMuted(false); - if (!!connectionId) { - this.restartSubscriberRecord(connectionId); - } else { - this.restartPublisherRecord(); - } - } - - private restartPublisherRecord(): void { - if (!!this.session) { - 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; - if (!!this.publisherRecorder) { - this.publisherRecorder.clean(); - } - } - private restartSubscriberRecord(connectionId: string): void { - if (!!this.session) { - 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'; + if (this.sessionEvents.streamDestroyed !== oldValues.streamDestroyed || firstTime) { + this.session.off('streamDestroyed'); + if (this.sessionEvents.streamDestroyed) { + this.session.on('streamDestroyed', (event: StreamEvent) => { + const index = this.subscribers.indexOf(event.stream.streamManager); + if (index > -1) { + this.subscribers.splice(index, 1); + } + this.updateEventList('streamDestroyed', event.stream.streamId); + }); } } - this.subscribers[connectionId].recording = false; - this.subscribers[connectionId].paused = false; - if (!!this.subscribers[connectionId].recorder) { - this.subscribers[connectionId].recorder.clean(); + if (this.sessionEvents.connectionCreated !== oldValues.connectionCreated || firstTime) { + this.session.off('connectionCreated'); + if (this.sessionEvents.connectionCreated) { + this.session.on('connectionCreated', (event: ConnectionEvent) => { + this.updateEventList('connectionCreated', event.connection.connectionId); + }); + } + } + + if (this.sessionEvents.connectionDestroyed !== oldValues.connectionDestroyed || firstTime) { + this.session.off('connectionDestroyed'); + if (this.sessionEvents.connectionDestroyed) { + this.session.on('connectionDestroyed', (event: ConnectionEvent) => { + delete this.subscribers[event.connection.connectionId]; + this.updateEventList('connectionDestroyed', event.connection.connectionId); + }); + } + } + + if (this.sessionEvents.sessionDisconnected !== oldValues.sessionDisconnected || firstTime) { + this.session.off('sessionDisconnected'); + if (this.sessionEvents.sessionDisconnected) { + this.session.on('sessionDisconnected', (event: SessionDisconnectedEvent) => { + this.updateEventList('sessionDisconnected', 'No data'); + if (event.reason === 'networkDisconnect') { + this.session = null; + this.OV = null; + } + }); + } + } + + if (this.sessionEvents.signal !== oldValues.signal || firstTime) { + this.session.off('signal'); + if (this.sessionEvents.signal) { + this.session.on('signal', (event: SignalEvent) => { + this.updateEventList('signal', event.from.connectionId + '-' + event.data); + }); + } + } + + if (this.sessionEvents.recordingStarted !== oldValues.recordingStarted || firstTime) { + this.session.off('recordingStarted'); + if (this.sessionEvents.recordingStarted) { + this.session.on('recordingStarted', (event: RecordingEvent) => { + this.updateEventList('recordingStarted', event.id); + }); + } + } + + if (this.sessionEvents.recordingStopped !== oldValues.recordingStopped || firstTime) { + this.session.off('recordingStopped'); + if (this.sessionEvents.recordingStopped) { + this.session.on('recordingStopped', (event: RecordingEvent) => { + this.updateEventList('recordingStopped', event.id); + }); + } + } + + if (this.sessionEvents.publisherStartSpeaking !== oldValues.publisherStartSpeaking || firstTime) { + this.session.off('publisherStartSpeaking'); + if (this.sessionEvents.publisherStartSpeaking) { + this.session.on('publisherStartSpeaking', (event: PublisherSpeakingEvent) => { + this.updateEventList('publisherStartSpeaking', event.connection.connectionId); + }); + } + } + + if (this.sessionEvents.publisherStopSpeaking !== oldValues.publisherStopSpeaking || firstTime) { + this.session.off('publisherStopSpeaking'); + if (this.sessionEvents.publisherStopSpeaking) { + this.session.on('publisherStopSpeaking', (event: PublisherSpeakingEvent) => { + this.updateEventList('publisherStopSpeaking', event.connection.connectionId); + }); + } } } syncInitPublisher() { this.publisher = this.OV.initPublisher( - 'local-vid-' + this.session.connection.connectionId, + undefined, { audioSource: this.sendAudio ? undefined : false, videoSource: this.sendVideo ? (this.optionsVideo === 'screen' ? 'screen' : undefined) : false, publishAudio: this.activeAudio, publishVideo: this.activeVideo, resolution: '640x480', - frameRate: 30, - insertMode: VideoInsertMode.APPEND + frameRate: 30 }, (err) => { if (err) { @@ -874,8 +460,6 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { } }); - this.addPublisherEvents(this.publisher); - if (this.subscribeToRemote) { this.publisher.subscribeToRemote(); } @@ -897,7 +481,6 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { }) .then(publisher => { this.publisher = publisher; - this.addPublisherEvents(this.publisher); if (this.subscribeToRemote) { this.publisher.subscribeToRemote(); } @@ -925,75 +508,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { } syncSubscribe(session: Session, event) { - 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: VideoElementEvent) => { - if (!event.stream.hasVideo) { - $(e.element).css({ 'background-color': '#4d4d4d' }); - $(e.element).attr('poster', 'assets/images/volume.png'); - } - this.subscribers[subscriber.stream.connection.connectionId].videoElement = e.element; - this.updateEventList('videoElementCreated', e.element.id); - }); - subscriber.on('videoPlaying', (e: VideoElementEvent) => { - this.appendSubscriberData(e.element, subscriber.stream.connection); - this.updateEventList('videoPlaying', e.element.id); - }); - subscriber.on('videoElementDestroyed', (e) => { - this.updateEventList('videoElementDestroyed', '(Subscriber)'); - }); - } - - asyncSubscribe(session: Session, event) { - session.subscribeAsync(event.stream, 'remote-vid-' + session.connection.connectionId) - .then(subscriber => { - this.subscribers[subscriber.stream.connection.connectionId] = { - 'subscriber': subscriber, - 'subbed': true, - 'recorder': undefined, - 'recording': false, - 'paused': false, - 'videoElement': undefined - }; - subscriber.on('videoElementCreated', (e: VideoElementEvent) => { - if (!event.stream.hasVideo) { - $(e.element).css({ 'background-color': '#4d4d4d' }); - $(e.element).attr('poster', 'assets/images/volume.png'); - } - this.subscribers[subscriber.stream.connection.connectionId].videoElement = e.element; - this.updateEventList('videoElementCreated', e.element.id); - }); - subscriber.on('videoPlaying', (e: VideoElementEvent) => { - this.appendSubscriberData(e.element, subscriber.stream.connection); - this.updateEventList('videoPlaying', e.element.id); - }); - subscriber.on('videoElementDestroyed', (e) => { - this.updateEventList('videoElementDestroyed', '(Subscriber)'); - }); - }) - .catch(err => { - console.error(err); - }); - } - - enableSpeakingEvents() { - this.session.on('publisherStartSpeaking', (event) => { - }); - - this.session.on('publisherStopSpeaking', (event) => { - }); - } - - disableSpeakingEvents() { - this.session.off('publisherStartSpeaking'); - this.session.off('publisherStopSpeaking'); + this.subscribers.push(session.subscribe(event.stream, undefined)); } initGrayVideo(): void { @@ -1050,7 +565,7 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { this.sessionName = this.sessionProperties.customSessionId; } } - document.getElementById('session-settings-btn').classList.remove('cdk-program-focused'); + document.getElementById('session-settings-btn-' + this.index).classList.remove('cdk-program-focused'); }); } @@ -1064,7 +579,54 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { }); dialogRef.afterClosed().subscribe((result: string) => { - document.getElementById('session-api-btn').classList.remove('cdk-program-focused'); + document.getElementById('session-api-btn-' + this.index).classList.remove('cdk-program-focused'); + }); + } + + openSessionEventsDialog() { + + const oldValues = { + connectionCreated: this.sessionEvents.connectionCreated, + connectionDestroyed: this.sessionEvents.connectionDestroyed, + sessionDisconnected: this.sessionEvents.sessionDisconnected, + streamCreated: this.sessionEvents.streamCreated, + streamDestroyed: this.sessionEvents.streamDestroyed, + recordingStarted: this.sessionEvents.recordingStarted, + recordingStopped: this.sessionEvents.recordingStopped, + signal: this.sessionEvents.signal, + publisherStartSpeaking: this.sessionEvents.publisherStartSpeaking, + publisherStopSpeaking: this.sessionEvents.publisherStopSpeaking + }; + + const dialogRef = this.dialog.open(EventsDialogComponent, { + data: { + eventCollection: this.sessionEvents, + target: 'Session' + }, + width: '280px', + autoFocus: false, + disableClose: true + }); + + dialogRef.afterClosed().subscribe((result) => { + + if (!!this.session && JSON.stringify(this.sessionEvents) !== JSON.stringify(oldValues)) { + this.updateSessionEvents(oldValues, false); + } + + this.sessionEvents = { + connectionCreated: result.connectionCreated, + connectionDestroyed: result.connectionDestroyed, + sessionDisconnected: result.sessionDisconnected, + streamCreated: result.streamCreated, + streamDestroyed: result.streamDestroyed, + recordingStarted: result.recordingStarted, + recordingStopped: result.recordingStopped, + signal: result.signal, + publisherStartSpeaking: result.publisherStartSpeaking, + publisherStopSpeaking: result.publisherStopSpeaking + }; + document.getElementById('session-events-btn-' + this.index).classList.remove('cdk-program-focused'); }); } @@ -1079,4 +641,15 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { }); } + udpateEventFromChild(event) { + this.updateEventList(event.event, event.content); + } + + updateSubscriberFromChild(newSubscriber: Subscriber) { + const oldSubscriber = this.subscribers.filter(sub => { + return sub.stream.streamId === newSubscriber.stream.streamId; + })[0]; + this.subscribers[this.subscribers.indexOf(oldSubscriber)] = newSubscriber; + } + } diff --git a/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.html b/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.html index 00ef80a9..f8eacfa5 100644 --- a/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.html +++ b/openvidu-testapp/src/app/components/test-sessions/test-sessions.component.html @@ -16,7 +16,7 @@
- +
diff --git a/openvidu-testapp/src/app/components/video/ov-video.component.ts b/openvidu-testapp/src/app/components/video/ov-video.component.ts new file mode 100644 index 00000000..80b17a50 --- /dev/null +++ b/openvidu-testapp/src/app/components/video/ov-video.component.ts @@ -0,0 +1,28 @@ +import { Component, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; +import { StreamManager } from 'openvidu-browser'; + +@Component({ + selector: 'app-ov-video', + template: '' +}) +export class OpenViduVideoComponent implements AfterViewInit { + + @ViewChild('videoElement') elementRef: ElementRef; + + @Input() poster = ''; + + _streamManager: StreamManager; + + ngAfterViewInit() { + this._streamManager.addVideoElement(this.elementRef.nativeElement); + } + + @Input() + set streamManager(streamManager: StreamManager) { + this._streamManager = streamManager; + if (!!this.elementRef) { + this._streamManager.addVideoElement(this.elementRef.nativeElement); + } + } + +} diff --git a/openvidu-testapp/src/app/components/video/video.component.css b/openvidu-testapp/src/app/components/video/video.component.css new file mode 100644 index 00000000..0eed51fb --- /dev/null +++ b/openvidu-testapp/src/app/components/video/video.component.css @@ -0,0 +1,98 @@ +app-ov-video { + float: left; + height: 90px; +} + +.data-node { + width: 120px; + height: 90px; + float: left; + position: relative; + margin-left: -120px; + margin-top: 0; +} + +p { + display: inline-flex; + vertical-align: top; + margin: 0; + background: #ffffff; + padding-left: 5px; + padding-right: 5px; + color: #797979; + font-weight: 100; + font-size: 14px; +} + +.material-icons { + font-size: 17px; + width: 17px; + height: 17px; + line-height: 20px; +} + +.video-btn { + border: none; + background: rgba(255, 255, 255, 0.75); + cursor: pointer; + padding: 0; + height: 20px; + float: left; +} + +.video-btn:hover { + color: #4d4d4d; +} + +.video-btn.top-row { + margin-top: 0; + display: inline-flex; + float: right; +} + +.rec-btn { + float: right; + color: #ac0000; +} + +.rec-btn:hover { + color: #ac000082; +} + +.events-btn { + float: right; +} + +.top-left-rounded { + border-top-left-radius: 2px; +} + +.top-right-rounded { + border-top-right-radius: 2px; +} + +.bottom-left-rounded { + border-bottom-left-radius: 2px; +} + +.bottom-right-rounded { + border-bottom-right-radius: 2px; +} + +.grey-background { + background-color: #4d4d4d +} + +.top-div { + position: absolute; + top: 0; + right: 0; + width: 100%; +} + +.bottom-div { + position: absolute; + bottom: 0; + height: 20px; + width: 100%; +} \ No newline at end of file diff --git a/openvidu-testapp/src/app/components/video/video.component.html b/openvidu-testapp/src/app/components/video/video.component.html new file mode 100644 index 00000000..60d07f11 --- /dev/null +++ b/openvidu-testapp/src/app/components/video/video.component.html @@ -0,0 +1,58 @@ +
+ +
+
+ +
+
+ + + + + + +
+
+
+
+

{{streamManager.stream.connection.data}}

+ +
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/openvidu-testapp/src/app/components/video/video.component.ts b/openvidu-testapp/src/app/components/video/video.component.ts new file mode 100644 index 00000000..c0056ab9 --- /dev/null +++ b/openvidu-testapp/src/app/components/video/video.component.ts @@ -0,0 +1,569 @@ +import { Component, Input, OnInit, ViewChild, ElementRef, Output, EventEmitter, OnDestroy } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material'; + +import { + StreamManager, + StreamManagerEvent, + VideoElementEvent, + Subscriber, + Session, + LocalRecorder, + OpenVidu, + Publisher, + StreamEvent, + VideoInsertMode +} from 'openvidu-browser'; + +import { EventsDialogComponent } from '../dialogs/events-dialog.component'; +import { MuteSubscribersService } from '../../services/mute-subscribers.service'; +import { Subscription } from 'rxjs/Subscription'; +import { LocalRecordingDialogComponent } from '../dialogs/local-recording-dialog.component'; +import { ExtensionDialogComponent } from '../dialogs/extension-dialog.component'; +import { OpenViduVideoComponent } from './ov-video.component'; + +@Component({ + selector: 'app-video', + templateUrl: './video.component.html', + styleUrls: ['./video.component.css'] +}) +export class VideoComponent implements OnInit, OnDestroy { + + @Input() streamManager: StreamManager; + @Input() OV: OpenVidu; + @Input() eventCollection: any; + + @Output() updateEventListInParent = new EventEmitter(); + @Output() reSubbed = new EventEmitter(); + + subbed = true; + subbedVideo = true; + subbedAudio = true; + recorder = undefined; + recording = false; + recordingPaused = false; + videoElement = undefined; + showButtons = false; + videoClasses = ''; + videoPoster = ''; + + unpublished = false; + publisherChanged = false; + audioMuted = false; + videoMuted = false; + sendAudio = true; + sendVideo = true; + sendAudioChange = false; + sendVideoChange = false; + optionsVideo = ''; + + private muteSubscribersSubscription: Subscription; + + // Icons + pubSubIcon = 'stop'; + pubSubVideoIcon = 'videocam'; + pubSubAudioIcon = 'mic'; + recordIcon = 'fiber_manual_record'; + pauseRecordIcon = ''; + + constructor(private dialog: MatDialog, private muteSubscribersService: MuteSubscribersService + ) { } + + ngOnInit() { + + if (this.streamManager.remote) { + // Init subscriber events + this.eventCollection = { + videoElementCreated: true, + videoElementDestroyed: true, + streamPlaying: true + }; + this.updateSubscriberEvents({ + videoElementCreated: false, + videoElementDestroyed: false, + streamPlaying: false + }); + + } else { + // Init publisher events + this.eventCollection = { + videoElementCreated: true, + videoElementDestroyed: true, + streamPlaying: true, + accessAllowed: true, + accessDenied: true, + accessDialogOpened: true, + accessDialogClosed: true, + streamCreated: true, + streamDestroyed: true + }; + this.updatePublisherEvents( + this.streamManager, + { + videoElementCreated: false, + videoElementDestroyed: false, + streamPlaying: false, + accessAllowed: false, + accessDenied: false, + accessDialogOpened: false, + accessDialogClosed: false, + streamCreated: false, + streamDestroyed: false + }); + this.sendAudio = this.streamManager.stream.hasAudio; + this.sendVideo = this.streamManager.stream.hasVideo; + this.optionsVideo = this.streamManager.stream.typeOfVideo; + } + + this.muteSubscribersSubscription = this.muteSubscribersService.mutedEvent$.subscribe(muteOrUnmute => { + this.streamManager.videos.forEach(v => { + v.video.muted = muteOrUnmute; + }); + }); + } + + ngOnDestroy() { + if (!!this.recorder) { + this.recorder.clean(); + } + if (!!this.muteSubscribersSubscription) { this.muteSubscribersSubscription.unsubscribe(); } + } + + subUnsub() { + const subscriber: Subscriber = this.streamManager; + if (this.subbed) { + this.streamManager.stream.session.unsubscribe(subscriber); + this.restartRecorder(); + + this.pubSubVideoIcon = ''; + this.pubSubAudioIcon = ''; + this.recordIcon = ''; + this.pauseRecordIcon = ''; + this.pubSubIcon = 'play_arrow'; + this.subbedVideo = false; + this.subbedAudio = false; + } else { + const oldValues = { + videoElementCreated: this.eventCollection.videoElementCreated, + videoElementDestroyed: this.eventCollection.videoElementDestroyed, + streamPlaying: this.eventCollection.streamPlaying + }; + this.streamManager = this.streamManager.stream.session.subscribe(subscriber.stream, undefined); + this.reSubbed.emit(this.streamManager); + + this.pubSubVideoIcon = 'videocam'; + this.pubSubAudioIcon = 'mic'; + this.recordIcon = 'fiber_manual_record'; + this.pauseRecordIcon = ''; + this.pubSubIcon = 'stop'; + this.subbedVideo = true; + this.subbedAudio = true; + + this.updateSubscriberEvents(oldValues); + } + this.subbed = !this.subbed; + } + + subUnsubVideo(connectionId: string) { + const subscriber: Subscriber = this.streamManager; + this.subbedVideo = !this.subbedVideo; + subscriber.subscribeToVideo(this.subbedVideo); + this.pubSubVideoIcon = this.subbedVideo ? 'videocam' : 'videocam_off'; + } + + subUnsubAudio(connectionId: string) { + const subscriber: Subscriber = this.streamManager; + this.subbedAudio = !this.subbedAudio; + subscriber.subscribeToAudio(this.subbedAudio); + this.pubSubAudioIcon = this.subbedAudio ? 'mic' : 'mic_off'; + } + + pubUnpub() { + const publisher: Publisher = this.streamManager; + if (this.unpublished) { + this.streamManager.stream.session.publish(publisher) + .then(() => { + console.log(publisher); + }) + .catch(e => { + console.error(e); + }); + } else { + this.streamManager.stream.session.unpublish(publisher); + } + this.unpublished = !this.unpublished; + this.unpublished ? this.pubSubIcon = 'play_arrow' : this.pubSubIcon = 'stop'; + } + + pubUnpubVideo() { + const publisher: Publisher = this.streamManager; + publisher.publishVideo(this.videoMuted); + this.videoMuted = !this.videoMuted; + this.pubSubVideoIcon = this.videoMuted ? 'videocam_off' : 'videocam'; + } + + pubUnpubAudio() { + const publisher: Publisher = this.streamManager; + publisher.publishAudio(this.audioMuted); + this.audioMuted = !this.audioMuted; + this.pubSubAudioIcon = this.audioMuted ? 'mic_off' : 'mic'; + } + + changePub() { + let screenChange; + if (!this.publisherChanged) { + if (this.sendAudio && !this.sendVideo) { + this.sendAudioChange = false; + this.sendVideoChange = true; + screenChange = false; + } else if (!this.sendAudio && this.sendVideo) { + this.sendAudioChange = true; + this.sendVideoChange = false; + } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'CAMERA') { + this.sendAudioChange = false; + this.sendVideoChange = true; + screenChange = true; + } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'SCREEN') { + this.sendAudioChange = false; + this.sendVideoChange = true; + screenChange = false; + } + } else { + this.sendAudioChange = this.sendAudio; + this.sendVideoChange = this.sendVideo; + screenChange = this.optionsVideo === 'SCREEN' ? true : false; + } + + this.audioMuted = false; + this.videoMuted = false; + this.unpublished = false; + + const otherPublisher = this.OV.initPublisher( + undefined, + { + audioSource: this.sendAudioChange ? undefined : false, + videoSource: this.sendVideoChange ? (screenChange ? 'screen' : undefined) : false, + publishAudio: (!this.publisherChanged) ? true : !this.audioMuted, + publishVideo: (!this.publisherChanged) ? true : !this.videoMuted, + resolution: '640x480', + frameRate: 30, + insertMode: VideoInsertMode.APPEND + }, + (err) => { + if (err) { + console.warn(err); + if (err.name === 'SCREEN_EXTENSION_NOT_INSTALLED') { + this.dialog.open(ExtensionDialogComponent, { + data: { url: err.message }, + disableClose: true, + width: '250px' + }); + } + } + }); + this.updatePublisherEvents(otherPublisher, { + videoElementCreated: !this.eventCollection.videoElementCreated, + videoElementDestroyed: !this.eventCollection.videoElementDestroyed, + streamPlaying: !this.eventCollection.streamPlaying, + accessAllowed: !this.eventCollection.accessAllowed, + accessDenied: !this.eventCollection.accessDenied, + accessDialogOpened: !this.eventCollection.accessDialogOpened, + accessDialogClosed: !this.eventCollection.accessDialogClosed, + streamCreated: !this.eventCollection.streamCreated, + streamDestroyed: !this.eventCollection.streamDestroyed + }); + + otherPublisher.once('accessAllowed', () => { + if (!this.unpublished) { + this.streamManager.stream.session.unpublish(this.streamManager); + this.streamManager = otherPublisher; + } + this.streamManager.stream.session.publish(otherPublisher).then(() => { + console.log(this.streamManager); + }); + }); + + this.publisherChanged = !this.publisherChanged; + } + + updateSubscriberEvents(oldValues) { + const sub: Subscriber = this.streamManager; + + if (this.eventCollection.videoElementCreated) { + if (!oldValues.videoElementCreated) { + sub.on('videoElementCreated', (event: VideoElementEvent) => { + if (!sub.stream.hasVideo) { + this.videoClasses = 'grey-background'; + this.videoPoster = 'assets/images/volume.png'; + } else { + this.videoClasses = ''; + this.videoPoster = ''; + } + this.updateEventListInParent.emit({ + event: 'videoElementCreated', + content: event.element.id + }); + }); + } + } else { + sub.off('videoElementCreated'); + } + + if (this.eventCollection.videoElementDestroyed) { + if (!oldValues.videoElementDestroyed) { + sub.on('videoElementDestroyed', (event: VideoElementEvent) => { + this.showButtons = false; + this.updateEventListInParent.emit({ + event: 'videoElementDestroyed', + content: event.element.id + }); + }); + } + } else { + sub.off('videoElementDestroyed'); + } + + if (this.eventCollection.streamPlaying) { + if (!oldValues.streamPlaying) { + sub.on('streamPlaying', (event: StreamManagerEvent) => { + this.showButtons = true; + this.updateEventListInParent.emit({ + event: 'streamPlaying', + content: this.streamManager.stream.streamId + }); + }); + } + } else { + sub.off('streamPlaying'); + } + } + + updatePublisherEvents(pub: Publisher, oldValues: any) { + if (this.eventCollection.videoElementCreated) { + if (!oldValues.videoElementCreated) { + pub.on('videoElementCreated', (event: VideoElementEvent) => { + if (!pub.stream.hasVideo) { + this.videoClasses = 'grey-background'; + this.videoPoster = 'assets/images/volume.png'; + } else { + this.videoClasses = ''; + this.videoPoster = ''; + } + this.updateEventListInParent.emit({ + event: 'videoElementCreated', + content: event.element.id + }); + }); + } + } else { + pub.off('videoElementCreated'); + } + + if (this.eventCollection.accessAllowed) { + if (!oldValues.accessAllowed) { + pub.on('accessAllowed', (e) => { + this.updateEventListInParent.emit({ + event: 'accessAllowed', + content: '' + }); + }); + } + } else { + pub.off('accessAllowed'); + } + + if (this.eventCollection.accessDenied) { + if (!oldValues.accessDenied) { + pub.on('accessDenied', (e) => { + this.updateEventListInParent.emit({ + event: 'accessDenied', + content: '' + }); + }); + } + } else { + pub.off('accessDenied'); + } + + if (this.eventCollection.accessDialogOpened) { + if (!oldValues.accessDialogOpened) { + pub.on('accessDialogOpened', (e) => { + this.updateEventListInParent.emit({ + event: 'accessDialogOpened', + content: '' + }); + }); + } + } else { + pub.off('accessDialogOpened'); + } + + if (this.eventCollection.accessDialogClosed) { + if (!oldValues.accessDialogClosed) { + pub.on('accessDialogClosed', (e) => { + this.updateEventListInParent.emit({ + event: 'accessDialogClosed', + content: '' + }); + }); + } + } else { + pub.off('accessDialogClosed'); + } + + if (this.eventCollection.streamCreated) { + if (!oldValues.streamCreated) { + pub.on('streamCreated', (e: StreamEvent) => { + this.updateEventListInParent.emit({ + event: 'streamCreated', + content: e.stream.streamId + }); + }); + } + } else { + pub.off('streamCreated'); + } + + if (this.eventCollection.streamDestroyed) { + if (!oldValues.streamDestroyed) { + pub.on('streamDestroyed', (e: StreamEvent) => { + this.updateEventListInParent.emit({ + event: 'streamDestroyed', + content: e.stream.streamId + }); + }); + } + } else { + pub.off('streamDestroyed'); + } + + if (this.eventCollection.videoElementDestroyed) { + if (!oldValues.videoElementDestroyed) { + pub.on('videoElementDestroyed', (e: VideoElementEvent) => { + this.updateEventListInParent.emit({ + event: 'videoElementDestroyed', + content: '(Publisher)' + }); + }); + } + } else { + pub.off('videoElementDestroyed'); + } + + if (this.eventCollection.streamPlaying) { + if (!oldValues.streamPlaying) { + pub.on('streamPlaying', (event: StreamManagerEvent) => { + this.showButtons = true; + this.updateEventListInParent.emit({ + event: 'streamPlaying', + content: this.streamManager.stream.streamId + }); + }); + } + } else { + pub.off('streamPlaying'); + } + } + + openSubscriberEventsDialog() { + const oldValues = { + videoElementCreated: this.eventCollection.videoElementCreated, + videoElementDestroyed: this.eventCollection.videoElementDestroyed, + streamPlaying: this.eventCollection.streamPlaying + }; + const dialogRef = this.dialog.open(EventsDialogComponent, { + data: { + eventCollection: this.eventCollection, + target: 'Subscriber' + }, + width: '280px', + autoFocus: false, + disableClose: true + }); + dialogRef.afterClosed().subscribe((result) => { + this.updateSubscriberEvents(oldValues); + }); + } + + + openPublisherEventsDialog() { + const oldValues = { + videoElementCreated: this.eventCollection.videoElementCreated, + videoElementDestroyed: this.eventCollection.videoElementDestroyed, + streamPlaying: this.eventCollection.streamPlaying, + accessAllowed: this.eventCollection.accessAllowed, + accessDenied: this.eventCollection.accessDenied, + accessDialogOpened: this.eventCollection.accessDialogOpened, + accessDialogClosed: this.eventCollection.accessDialogClosed, + streamCreated: this.eventCollection.streamCreated, + streamDestroyed: this.eventCollection.streamDestroyed + }; + const dialogRef = this.dialog.open(EventsDialogComponent, { + data: { + eventCollection: this.eventCollection, + target: 'Publisher' + }, + width: '280px', + autoFocus: false, + disableClose: true + }); + dialogRef.afterClosed().subscribe((result) => { + this.updatePublisherEvents(this.streamManager, oldValues); + }); + } + + record(): void { + if (!this.recording) { + this.recorder = this.OV.initLocalRecorder(this.streamManager.stream); + this.recorder.record(); + this.recording = true; + this.recordIcon = 'stop'; + this.pauseRecordIcon = 'pause'; + } else { + this.recorder.stop() + .then(() => { + let dialogRef: MatDialogRef; + dialogRef = this.dialog.open(LocalRecordingDialogComponent, { + disableClose: true, + data: { + recorder: this.recorder + } + }); + dialogRef.componentInstance.myReference = dialogRef; + + dialogRef.afterOpen().subscribe(() => { + this.muteSubscribersService.updateMuted(true); + this.recorder.preview('recorder-preview').controls = true; + }); + dialogRef.afterClosed().subscribe(() => { + this.muteSubscribersService.updateMuted(false); + this.restartRecorder(); + }); + }) + .catch((error) => { + console.error('Error stopping LocalRecorder: ' + error); + }); + } + } + + pauseRecord(): void { + if (!this.recordingPaused) { + this.recorder.pause(); + this.pauseRecordIcon = 'play_arrow'; + } else { + this.recorder.resume(); + this.pauseRecordIcon = 'pause'; + } + this.recordingPaused = !this.recordingPaused; + } + + private restartRecorder() { + this.recording = false; + this.recordingPaused = false; + this.recordIcon = 'fiber_manual_record'; + this.pauseRecordIcon = ''; + if (!!this.recorder) { + this.recorder.clean(); + } + } + +} diff --git a/openvidu-testapp/src/assets/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 b/openvidu-testapp/src/assets/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..af37c6a9821486b5809672545c9e8475fdd6309e GIT binary patch literal 49028 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⪼b;rsf?{Thz{2<`^60_M{Y4AT2yC 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

fh7x1r8d`#28ltG+ynxoQW9GED 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 literal 0 HcmV?d00001 diff --git a/openvidu-testapp/src/index.html b/openvidu-testapp/src/index.html index 27abaf4d..4bb6d2cc 100644 --- a/openvidu-testapp/src/index.html +++ b/openvidu-testapp/src/index.html @@ -9,11 +9,6 @@ - - - - diff --git a/openvidu-testapp/src/material-icons.css b/openvidu-testapp/src/material-icons.css new file mode 100644 index 00000000..8cbb0e3d --- /dev/null +++ b/openvidu-testapp/src/material-icons.css @@ -0,0 +1,23 @@ +/* fallback */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: url(/assets/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2'); + } + + .material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; + } \ No newline at end of file diff --git a/openvidu-testapp/src/styles.css b/openvidu-testapp/src/styles.css index 74c44f00..46f772ae 100644 --- a/openvidu-testapp/src/styles.css +++ b/openvidu-testapp/src/styles.css @@ -38,67 +38,6 @@ button { line-height: 15px !important; } -.video-container video { - float: left; -} - -.video-container div.data-node { - width: 120px; - height: 90px; - float: left; - position: relative; - margin-left: -120px; - margin-top: 0; -} - -.video-container p { - margin-top: 0; - width: fit-content; - background: #ffffff; - padding-left: 5px; - padding-right: 5px; - color: #797979; - font-weight: 100; - font-size: 14px; - border-bottom-right-radius: 2px; -} - -.video-container div.data-node .sub-btn { - outline: 0; - border: none; - background: rgba(255, 255, 255, 0.75); - cursor: pointer; - padding: 0; - margin-top: 40px; - border-top-right-radius: 2px; -} - -.video-container div.data-node .sub-btn:hover { - 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 { font-size: 9.5px !important; padding: 0 9px 0px !important;