diff --git a/openvidu-browser/src/OpenVidu/MediaManager.ts b/openvidu-browser/src/OpenVidu/MediaManager.ts new file mode 100644 index 00000000..22c210d1 --- /dev/null +++ b/openvidu-browser/src/OpenVidu/MediaManager.ts @@ -0,0 +1,302 @@ +/* + * (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Stream } from './Stream'; +import { EventDispatcher } from '../OpenViduInternal/Interfaces/Public/EventDispatcher'; +import { Event } from '../OpenViduInternal/Events/Event'; +import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent'; +import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; + +import EventEmitter = require('wolfy87-eventemitter'); + + +/** + * Interface in charge of displaying the media streams in the HTML DOM. This wraps any Publisher and Subscriber object, as well as + * any extra representation in the DOM you assign to some Stream by calling [[Stream.addVideoElement]]. + * + * The use of this interface is useful when you don't need to differentiate between streams and just want to directly manage videos + */ +export class MediaManager implements EventDispatcher { + + /** + * The Stream represented in the DOM by the MediaManager + */ + stream: Stream; + + /** + * Whether the MediaManager is representing in the DOM a local Stream ([[Publisher]]) or a remote Stream ([[Subscriber]]) + */ + remote: boolean; + + /** + * The DOM HTMLElement assigned as target element when initializing the MediaManager. This property is defined when [[OpenVidu.initPublisher]] + * or [[Session.subscribe]] methods have been called passing a valid `targetElement` parameter. It is undefined when [[OpenVidu.initPublisher]] + * or [[Session.subscribe]] methods have been called passing *null* or *undefined* as `targetElement` parameter or when the MediaManager hass been + * created by calling [[Stream.addVideoElement]] + */ + targetElement?: HTMLElement; + + /** + * The DOM HTMLVideoElement displaying the MediaManager's stream + */ + video: HTMLVideoElement; + + /** + * `id` attribute of the DOM HTMLVideoElement displaying the MediaManager's stream + */ + id: string; + + /** + * @hidden + */ + isVideoElementCreated = false; + + protected ee = new EventEmitter(); + protected customEe = new EventEmitter(); + + + /** + * @hidden + */ + constructor(stream: Stream, targetElement?: HTMLElement | string) { + this.stream = stream; + this.stream.mediaManagers.push(this); + if (typeof targetElement === 'string') { + const e = document.getElementById(targetElement); + if (!!e) { + this.targetElement = e; + } + } else if (targetElement instanceof HTMLElement) { + this.targetElement = targetElement; + } else if (!!this.targetElement) { + console.warn("The provided 'targetElement' couldn't be resolved to any HTML element: " + targetElement); + } + + this.customEe.on('video-removed', (element: HTMLVideoElement) => { + this.ee.emitEvent('videoElementDestroyed', [new VideoElementEvent(element, this, 'videoElementDestroyed')]); + }); + } + + /** + * See [[EventDispatcher.on]] + */ + on(type: string, handler: (event: Event) => void): EventDispatcher { + this.ee.on(type, event => { + if (event) { + console.info("Event '" + type + "' triggered", event); + } else { + console.info("Event '" + type + "' triggered"); + } + handler(event); + }); + if (type === 'videoElementCreated') { + if (!!this.stream && this.isVideoElementCreated) { + this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(this.video, this, 'videoElementCreated')]); + } else { + this.customEe.on('video-element-created', element => { + this.id = element.id; + this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(element.element, this, 'videoElementCreated')]); + }); + } + } + if (type === 'videoPlaying') { + if (!this.stream.displayMyRemote() && !!this.video && + this.video.currentTime > 0 && + this.video.paused === false && + this.video.ended === false && + this.video.readyState === 4) { + this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.video, this, 'videoPlaying')]); + } else { + this.customEe.once('video-is-playing', (element) => { + this.ee.emitEvent('videoPlaying', [new VideoElementEvent(element.element, this, 'videoPlaying')]); + }); + } + } + return this; + } + + /** + * See [[EventDispatcher.once]] + */ + once(type: string, handler: (event: Event) => void): MediaManager { + this.ee.once(type, event => { + if (event) { + console.info("Event '" + type + "' triggered once", event); + } else { + console.info("Event '" + type + "' triggered once"); + } + handler(event); + }); + if (type === 'videoElementCreated') { + if (!!this.stream && this.isVideoElementCreated) { + this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(this.video, this, 'videoElementCreated')]); + } else { + this.customEe.once('video-element-created', element => { + this.id = element.id; + this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(element.element, this, 'videoElementCreated')]); + }); + } + } + if (type === 'videoPlaying') { + if (!this.stream.displayMyRemote() && this.video && + this.video.currentTime > 0 && + this.video.paused === false && + this.video.ended === false && + this.video.readyState === 4) { + this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.video, this, 'videoPlaying')]); + } else { + this.customEe.once('video-is-playing', (element) => { + this.ee.emitEvent('videoPlaying', [new VideoElementEvent(element.element, this, 'videoPlaying')]); + }); + } + } + return this; + } + + /** + * See [[EventDispatcher.off]] + */ + off(type: string, handler?: (event: Event) => void): MediaManager { + if (!handler) { + this.ee.removeAllListeners(type); + } else { + this.ee.off(type, handler); + } + return this; + } + + + /** + * @hidden + */ + insertVideo(targetElement?: HTMLElement, insertMode?: VideoInsertMode): HTMLVideoElement { + if (!!targetElement) { + + this.video = document.createElement('video'); + + this.video.id = (this.stream.isLocal() ? 'local-' : 'remote-') + 'video-' + this.stream.streamId; + this.video.autoplay = true; + this.video.controls = false; + this.video.srcObject = this.stream.getMediaStream(); + + if (this.stream.isLocal() && !this.stream.displayMyRemote()) { + this.video.muted = true; + + if (this.stream.outboundStreamOpts.publisherProperties.mirror) { + this.mirrorVideo(); + } + + this.video.oncanplay = () => { + console.info("Local 'Stream' with id [" + this.stream.streamId + '] video is now playing'); + this.customEe.emitEvent('video-is-playing', [{ + element: this.video + }]); + }; + } else { + this.video.title = this.stream.streamId; + } + + this.targetElement = targetElement; + + const insMode = !!insertMode ? insertMode : VideoInsertMode.APPEND; + + this.insertVideoWithMode(insMode); + + this.customEe.emitEvent('video-element-created', [{ + element: this.video + }]); + + this.isVideoElementCreated = true; + } + + if (this.stream.isLocal()) { + this.stream.isLocalStreamReadyToPublish = true; + this.stream.emitEvent('stream-ready-to-publish', []); + } + + return this.video; + } + + /** + * @hidden + */ + insertVideoWithMode(insertMode: VideoInsertMode): void { + if (!!this.targetElement) { + switch (insertMode) { + case VideoInsertMode.AFTER: + this.targetElement.parentNode!!.insertBefore(this.video, this.targetElement.nextSibling); + break; + case VideoInsertMode.APPEND: + this.targetElement.appendChild(this.video); + break; + case VideoInsertMode.BEFORE: + this.targetElement.parentNode!!.insertBefore(this.video, this.targetElement); + break; + case VideoInsertMode.PREPEND: + this.targetElement.insertBefore(this.video, this.targetElement.childNodes[0]); + break; + case VideoInsertMode.REPLACE: + this.targetElement.parentNode!!.replaceChild(this.video, this.targetElement); + break; + default: + this.insertVideoWithMode(VideoInsertMode.APPEND); + } + } + } + + /** + * @hidden + */ + removeVideo(): void { + if (!!this.video) { + this.video.parentNode!.removeChild(this.video); + this.customEe.emitEvent('video-removed', [this.video]); + delete this.video; + } + } + + /** + * @hidden + */ + addOnCanPlayEvent() { + if (!!this.video) { + // let thumbnailId = this.video.thumb; + this.video.oncanplay = () => { + if (this.stream.isLocal() && this.stream.displayMyRemote()) { + console.info("Your own remote 'Stream' with id [" + this.stream.streamId + '] video is now playing'); + this.customEe.emitEvent('remote-video-is-playing', [{ + element: this.video + }]); + } else if (!this.stream.isLocal() && !this.stream.displayMyRemote()) { + console.info("Remote 'Stream' with id [" + this.stream.streamId + '] video is now playing'); + this.customEe.emitEvent('video-is-playing', [{ + element: this.video + }]); + } + // show(thumbnailId); + // this.hideSpinner(this.streamId); + }; + } + } + + + private mirrorVideo(): void { + this.video.style.transform = 'rotateY(180deg)'; + this.video.style.webkitTransform = 'rotateY(180deg)'; + } + +} \ No newline at end of file diff --git a/openvidu-browser/src/OpenVidu/OpenVidu.ts b/openvidu-browser/src/OpenVidu/OpenVidu.ts index 46b0ab2a..e87b62ae 100644 --- a/openvidu-browser/src/OpenVidu/OpenVidu.ts +++ b/openvidu-browser/src/OpenVidu/OpenVidu.ts @@ -92,7 +92,7 @@ export class OpenVidu { * The [[Publisher]] object will dispatch a `videoPlaying` event once the local video starts playing (only if `videoElementCreated` event has been previously dispatched) * * @param targetElement HTML DOM element (or its `id` attribute) in which the video element of the Publisher will be inserted (see [[PublisherProperties.insertMode]]). If null or undefined no default video will be created for this Publisher - * (you can always access the native MediaStream object by calling _Publisher.stream.getMediaStream()_ and use it as _srcObject_ of any HTML video element) + * (you can always call method [[Stream.addVideoElement]] for the object [[Publisher.stream]] to manage the video elements on your own) * @param completionHandler `error` parameter is null if `initPublisher` succeeds, and is defined if it fails. * `completionHandler` function is called before the Publisher dispatches an `accessAllowed` or an `accessDenied` event */ @@ -145,7 +145,7 @@ export class OpenVidu { } publisher.emitEvent('accessAllowed', []); }).catch((error) => { - if (!!completionHandler !== undefined) { + if (completionHandler !== undefined) { completionHandler(error); } publisher.emitEvent('accessDenied', []); diff --git a/openvidu-browser/src/OpenVidu/Publisher.ts b/openvidu-browser/src/OpenVidu/Publisher.ts index c1abd62c..40cb5825 100644 --- a/openvidu-browser/src/OpenVidu/Publisher.ts +++ b/openvidu-browser/src/OpenVidu/Publisher.ts @@ -15,6 +15,7 @@ * */ +import { MediaManager } from './MediaManager'; import { OpenVidu } from './OpenVidu'; import { Session } from './Session'; import { Stream } from './Stream'; @@ -22,78 +23,49 @@ import { EventDispatcher } from '../OpenViduInternal/Interfaces/Public/EventDisp import { PublisherProperties } from '../OpenViduInternal/Interfaces/Public/PublisherProperties'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions'; +import { Event } from '../OpenViduInternal/Events/Event'; import { StreamEvent } from '../OpenViduInternal/Events/StreamEvent'; import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; -import EventEmitter = require('wolfy87-eventemitter'); - /** * Packs local media streams. Participants can publish it to a session. Initialized with [[OpenVidu.initPublisher]] method */ -export class Publisher implements EventDispatcher { +export class Publisher extends MediaManager { /** * Whether the Publisher has been granted access to the requested input devices or not */ accessAllowed = false; - /** - * HTML DOM element in which the Publisher's video has been inserted - */ - element: HTMLElement; - - /** - * DOM id of the Publisher's video element - */ - id: string; - /** * The [[Session]] to which the Publisher belongs */ session: Session; // Initialized by Session.publish(Publisher) /** - * The [[Stream]] that you are publishing + * @hidden */ - stream: Stream; - - private ee = new EventEmitter(); + accessDenied = false; + private element?: HTMLElement; private properties: PublisherProperties; private permissionDialogTimeout: NodeJS.Timer; /** * @hidden */ - constructor(targetElement: string | HTMLElement, properties: PublisherProperties, private openvidu: OpenVidu) { + constructor(targEl: string | HTMLElement, properties: PublisherProperties, private openvidu: OpenVidu) { + super(new Stream(new Session(openvidu), { publisherProperties: properties, mediaConstraints: {} }), targEl); this.properties = properties; - this.stream = new Stream(this.session, { publisherProperties: properties, mediaConstraints: {} }); - this.stream.on('video-removed', (element: HTMLVideoElement) => { - this.ee.emitEvent('videoElementDestroyed', [new VideoElementEvent(element, this, 'videoElementDestroyed')]); - }); - - this.stream.on('stream-destroyed-by-disconnect', (reason: string) => { + this.stream.on('local-stream-destroyed-by-disconnect', (reason: string) => { const streamEvent = new StreamEvent(true, this, 'streamDestroyed', this.stream, reason); this.ee.emitEvent('streamDestroyed', [streamEvent]); streamEvent.callDefaultBehaviour(); }); - - if (typeof targetElement === 'string') { - const e = document.getElementById(targetElement); - if (!!e) { - this.element = e; - } - } else if (targetElement instanceof HTMLElement) { - this.element = targetElement; - } - - if (!this.element) { - console.warn("The provided 'targetElement' for the Publisher couldn't be resolved to any HTML element: " + targetElement); - } } /** @@ -125,18 +97,10 @@ export class Publisher implements EventDispatcher { /** * See [[EventDispatcher.on]] */ - on(type: string, handler: (event: StreamEvent | VideoElementEvent) => void): EventDispatcher { - this.ee.on(type, event => { - if (event) { - console.info("Event '" + type + "' triggered by 'Publisher'", event); - } else { - console.info("Event '" + type + "' triggered by 'Publisher'"); - } - handler(event); - }); - + on(type: string, handler: (event: Event) => void): EventDispatcher { + super.on(type, handler); if (type === 'streamCreated') { - if (!!this.stream && this.stream.isPublisherPublished) { + if (!!this.stream && this.stream.isLocalStreamPublished) { this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', this.stream, '')]); } else { this.stream.on('stream-created-by-publisher', () => { @@ -144,38 +108,13 @@ export class Publisher implements EventDispatcher { }); } } - if (type === 'videoElementCreated') { - if (!!this.stream && this.stream.isVideoELementCreated) { - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoElementCreated')]); - } else { - this.stream.on('video-element-created-by-stream', (element) => { - this.id = element.id; - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(element.element, this, 'videoElementCreated')]); - }); - } - } - if (type === 'videoPlaying') { - const video = this.stream.getVideoElement(); - if (!this.stream.displayMyRemote() && video && - video.currentTime > 0 && - video.paused === false && - video.ended === false && - video.readyState === 4) { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoPlaying')]); - } else { - this.stream.on('video-is-playing', (element) => { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(element.element, this, 'videoPlaying')]); - }); - } - } if (type === 'remoteVideoPlaying') { - const video = this.stream.getVideoElement(); - if (this.stream.displayMyRemote() && video && - video.currentTime > 0 && - video.paused === false && - video.ended === false && - video.readyState === 4) { - this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'remoteVideoPlaying')]); + if (this.stream.displayMyRemote() && this.video && + this.video.currentTime > 0 && + this.video.paused === false && + this.video.ended === false && + this.video.readyState === 4) { + this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.video, this, 'remoteVideoPlaying')]); } else { this.stream.on('remote-video-is-playing', (element) => { this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(element.element, this, 'remoteVideoPlaying')]); @@ -183,16 +122,15 @@ export class Publisher implements EventDispatcher { } } if (type === 'accessAllowed') { - if (this.stream.accessIsAllowed) { + if (this.accessAllowed) { this.ee.emitEvent('accessAllowed'); } } if (type === 'accessDenied') { - if (this.stream.accessIsDenied) { + if (this.accessDenied) { this.ee.emitEvent('accessDenied'); } } - return this; } @@ -200,18 +138,10 @@ export class Publisher implements EventDispatcher { /** * See [[EventDispatcher.once]] */ - once(type: string, handler: (event: StreamEvent | VideoElementEvent) => void): Publisher { - this.ee.once(type, event => { - if (event) { - console.info("Event '" + type + "' triggered by 'Publisher'", event); - } else { - console.info("Event '" + type + "' triggered by 'Publisher'"); - } - handler(event); - }); - + once(type: string, handler: (event: Event) => void): Publisher { + super.once(type, handler); if (type === 'streamCreated') { - if (!!this.stream && this.stream.isPublisherPublished) { + if (!!this.stream && this.stream.isLocalStreamPublished) { this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', this.stream, '')]); } else { this.stream.once('stream-created-by-publisher', () => { @@ -219,38 +149,13 @@ export class Publisher implements EventDispatcher { }); } } - if (type === 'videoElementCreated') { - if (!!this.stream && this.stream.isVideoELementCreated) { - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoElementCreated')]); - } else { - this.stream.once('video-element-created-by-stream', (element) => { - this.id = element.id; - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(element.element, this, 'videoElementCreated')]); - }); - } - } - if (type === 'videoPlaying') { - const video = this.stream.getVideoElement(); - if (!this.stream.displayMyRemote() && video && - video.currentTime > 0 && - video.paused === false && - video.ended === false && - video.readyState === 4) { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoPlaying')]); - } else { - this.stream.once('video-is-playing', (element) => { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(element.element, this, 'videoPlaying')]); - }); - } - } if (type === 'remoteVideoPlaying') { - const video = this.stream.getVideoElement(); - if (this.stream.displayMyRemote() && video && - video.currentTime > 0 && - video.paused === false && - video.ended === false && - video.readyState === 4) { - this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'remoteVideoPlaying')]); + if (this.stream.displayMyRemote() && this.video && + this.video.currentTime > 0 && + this.video.paused === false && + this.video.ended === false && + this.video.readyState === 4) { + this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.video, this, 'remoteVideoPlaying')]); } else { this.stream.once('remote-video-is-playing', (element) => { this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(element.element, this, 'remoteVideoPlaying')]); @@ -258,29 +163,15 @@ export class Publisher implements EventDispatcher { } } if (type === 'accessAllowed') { - if (this.stream.accessIsAllowed) { + if (this.accessAllowed) { this.ee.emitEvent('accessAllowed'); } } if (type === 'accessDenied') { - if (this.stream.accessIsDenied) { + if (this.accessDenied) { this.ee.emitEvent('accessDenied'); } } - - return this; - } - - - /** - * See [[EventDispatcher.off]] - */ - off(type: string, handler?: (event: StreamEvent | VideoElementEvent) => void): Publisher { - if (!handler) { - this.ee.removeAllListeners(type); - } else { - this.ee.off(type, handler); - } return this; } @@ -294,14 +185,14 @@ export class Publisher implements EventDispatcher { return new Promise((resolve, reject) => { const errorCallback = (openViduError: OpenViduError) => { - this.stream.accessIsDenied = true; - this.stream.accessIsAllowed = false; + this.accessDenied = true; + this.accessAllowed = false; reject(openViduError); }; const successCallback = (mediaStream: MediaStream) => { - this.stream.accessIsAllowed = true; - this.stream.accessIsDenied = false; + this.accessAllowed = true; + this.accessDenied = false; if (this.openvidu.isMediaStreamTrack(this.properties.audioSource)) { mediaStream.removeTrack(mediaStream.getAudioTracks()[0]); @@ -322,7 +213,7 @@ export class Publisher implements EventDispatcher { } this.stream.setMediaStream(mediaStream); - this.stream.insertVideo(this.element, this.properties.insertMode); + this.insertVideo(this.targetElement, this.properties.insertMode); resolve(); }; diff --git a/openvidu-browser/src/OpenVidu/Session.ts b/openvidu-browser/src/OpenVidu/Session.ts index 8acf9f51..1304cedd 100644 --- a/openvidu-browser/src/OpenVidu/Session.ts +++ b/openvidu-browser/src/OpenVidu/Session.ts @@ -185,7 +185,7 @@ export class Session implements EventDispatcher { * * @param stream Stream object to subscribe to * @param targetElement HTML DOM element (or its `id` attribute) in which the video element of the Subscriber will be inserted (see [[SubscriberProperties.insertMode]]). If null or undefined no default video will be created for this Subscriber - * (you can always access the native MediaStream object by calling _Subscriber.stream.getMediaStream()_ and use it as _srcObject_ of any HTML video element) + * (you can always call method [[Stream.addVideoElement]] for the object [[Subscriber.stream]] to manage the video elements on your own) * @param completionHandler `error` parameter is null if `subscribe` succeeds, and is defined if it fails. */ subscribe(stream: Stream, targetElement: string | HTMLElement, param3?: ((error: Error | undefined) => void) | SubscriberProperties, param4?: ((error: Error | undefined) => void)): Subscriber { @@ -226,7 +226,9 @@ export class Session implements EventDispatcher { } }); const subscriber = new Subscriber(stream, targetElement, properties); - stream.insertVideo(subscriber.element, properties.insertMode); + stream.mediaManagers.forEach(mediaManager => { + mediaManager.insertVideo(subscriber.targetElement, properties.insertMode); + }); return subscriber; } @@ -272,9 +274,9 @@ export class Session implements EventDispatcher { console.info('Unsubscribing from ' + connectionId); - this.openvidu.sendRequest('unsubscribeFromVideo', { - sender: subscriber.stream.connection.connectionId - }, + this.openvidu.sendRequest( + 'unsubscribeFromVideo', + { sender: subscriber.stream.connection.connectionId }, (error, response) => { if (error) { console.error('Error unsubscribing from ' + connectionId, error); @@ -283,8 +285,9 @@ export class Session implements EventDispatcher { } subscriber.stream.disposeWebRtcPeer(); subscriber.stream.disposeMediaStream(); - }); - subscriber.stream.removeVideo(); + } + ); + subscriber.stream.removeVideos(); } @@ -307,7 +310,7 @@ export class Session implements EventDispatcher { publisher.session = this; publisher.stream.session = this; - if (!publisher.stream.isPublisherPublished) { + if (!publisher.stream.isLocalStreamPublished) { // 'Session.unpublish(Publisher)' has NOT been called this.connection.addStream(publisher.stream); publisher.stream.publish() @@ -788,7 +791,7 @@ export class Session implements EventDispatcher { if (!!this.connection.stream) { // Make Publisher object dispatch 'streamDestroyed' event (if there's a local stream) this.connection.stream.disposeWebRtcPeer(); - this.connection.stream.emitEvent('stream-destroyed-by-disconnect', [reason]); + this.connection.stream.emitEvent('local-stream-destroyed-by-disconnect', [reason]); } if (!this.connection.disposed) { diff --git a/openvidu-browser/src/OpenVidu/Stream.ts b/openvidu-browser/src/OpenVidu/Stream.ts index 5a7a5411..fef4b262 100644 --- a/openvidu-browser/src/OpenVidu/Stream.ts +++ b/openvidu-browser/src/OpenVidu/Stream.ts @@ -16,15 +16,17 @@ */ import { Connection } from './Connection'; +import { MediaManager } from './MediaManager'; import { Session } from './Session'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions'; import { WebRtcStats } from '../OpenViduInternal/WebRtcStats/WebRtcStats'; import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent'; +import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; + import EventEmitter = require('wolfy87-eventemitter'); import * as kurentoUtils from '../OpenViduInternal/KurentoUtils/kurento-utils-js'; -import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; /** @@ -63,13 +65,15 @@ export class Stream { */ typeOfVideo?: string; + /** + * Array of [[MediaManager]] objects displaying this stream in the DOM + */ + mediaManagers: MediaManager[] = []; + private ee = new EventEmitter(); private webRtcPeer: any; private mediaStream: MediaStream; - private video: HTMLVideoElement; - private targetElement: HTMLElement; - private parentId: string; private webRtcStats: WebRtcStats; private isSubscribeToRemote = false; @@ -77,23 +81,11 @@ export class Stream { /** * @hidden */ - isReadyToPublish = false; + isLocalStreamReadyToPublish = false; /** * @hidden */ - isPublisherPublished = false; - /** - * @hidden - */ - isVideoELementCreated = false; - /** - * @hidden - */ - accessIsAllowed = false; - /** - * @hidden - */ - accessIsDenied = false; + isLocalStreamPublished = false; /** * @hidden */ @@ -149,11 +141,30 @@ export class Stream { } this.on('mediastream-updated', () => { - if (this.video) this.video.srcObject = this.mediaStream; + this.mediaManagers.forEach(mediaManager => { + if (!!mediaManager.video) { + mediaManager.video.srcObject = this.mediaStream; + } + }); console.debug('Video srcObject [' + this.mediaStream + '] updated in stream [' + this.streamId + ']'); }); } + /** + * Makes `video` element parameter display this Stream. This is useful when you are managing the video elements on your own + * (parameter `targetElement` of methods [[OpenVidu.initPublisher]] or [[Session.subscribe]] is set to *null* or *undefined*) + * or if you want to have multiple video elements display the same media stream + */ + addVideoElement(video: HTMLVideoElement): MediaManager { + video.srcObject = this.mediaStream; + const mediaManager = new MediaManager(this); + mediaManager.video = video; + mediaManager.id = video.id; + mediaManager.isVideoElementCreated = true; + mediaManager.remote = !this.isLocal(); + return mediaManager; + } + /* Hidden methods */ @@ -186,13 +197,6 @@ export class Stream { return this.webRtcPeer.peerConnection; } - /** - * @hidden - */ - getVideoElement(): HTMLVideoElement { - return this.video; - } - /** * @hidden */ @@ -227,7 +231,7 @@ export class Stream { */ publish(): Promise { return new Promise((resolve, reject) => { - if (this.isReadyToPublish) { + if (this.isLocalStreamReadyToPublish) { this.initWebRtcPeerSend() .then(() => { resolve(); @@ -301,68 +305,6 @@ export class Stream { this.ee.once(eventName, listener); } - /** - * @hidden - */ - insertVideo(targetElement?: HTMLElement, insertMode?: VideoInsertMode): HTMLVideoElement { - if (!!targetElement) { - - this.video = document.createElement('video'); - - this.video.id = (this.isLocal() ? 'local-' : 'remote-') + 'video-' + this.streamId; - this.video.autoplay = true; - this.video.controls = false; - this.video.srcObject = this.mediaStream; - - if (this.isLocal() && !this.displayMyRemote()) { - this.video.muted = true; - - if (this.outboundStreamOpts.publisherProperties.mirror) { - this.mirrorVideo(this.video); - } - - this.video.oncanplay = () => { - console.info("Local 'Stream' with id [" + this.streamId + '] video is now playing'); - this.ee.emitEvent('video-is-playing', [{ - element: this.video - }]); - }; - } else { - this.video.title = this.streamId; - } - - this.targetElement = targetElement; - this.parentId = targetElement.id; - - const insMode = !!insertMode ? insertMode : VideoInsertMode.APPEND; - this.insertElementWithMode(this.video, insMode); - - this.ee.emitEvent('video-element-created-by-stream', [{ - element: this.video - }]); - - this.isVideoELementCreated = true; - } - - this.isReadyToPublish = true; - this.ee.emitEvent('stream-ready-to-publish'); - - return this.video; - } - - /** - * @hidden - */ - removeVideo(): void { - if (this.video) { - if (document.getElementById(this.parentId)) { - document.getElementById(this.parentId)!.removeChild(this.video); - this.ee.emitEvent('video-removed', [this.video]); - } - delete this.video; - } - } - /** * @hidden */ @@ -445,6 +387,15 @@ export class Stream { this.speechEvent = undefined; } + /** + * @hidden + */ + removeVideos(): void { + this.mediaManagers.forEach(mediaManager => { + mediaManager.removeVideo(); + }); + } + /* Private methods */ @@ -510,7 +461,7 @@ export class Stream { this.webRtcPeer.generateOffer(successCallback); }); } - this.isPublisherPublished = true; + this.isLocalStreamPublished = true; }); } @@ -588,24 +539,9 @@ export class Stream { } } - if (!!this.video) { - // let thumbnailId = this.video.thumb; - this.video.oncanplay = () => { - if (this.isLocal() && this.displayMyRemote()) { - console.info("Your own remote 'Stream' with id [" + this.streamId + '] video is now playing'); - this.ee.emitEvent('remote-video-is-playing', [{ - element: this.video - }]); - } else if (!this.isLocal() && !this.displayMyRemote()) { - console.info("Remote 'Stream' with id [" + this.streamId + '] video is now playing'); - this.ee.emitEvent('video-is-playing', [{ - element: this.video - }]); - } - // show(thumbnailId); - // this.hideSpinner(this.streamId); - }; - } + this.mediaManagers.forEach(mediaManager => { + mediaManager.addOnCanPlayEvent(); + }); this.session.emitEvent('stream-subscribed', [{ stream: this }]); @@ -631,38 +567,12 @@ export class Stream { } } - private isLocal(): boolean { + /** + * @hidden + */ + isLocal(): boolean { // inbound options undefined and outbound options defined return (!this.inboundStreamOpts && !!this.outboundStreamOpts); } - private insertElementWithMode(element: HTMLElement, insertMode: VideoInsertMode): void { - if (!!this.targetElement) { - switch (insertMode) { - case VideoInsertMode.AFTER: - this.targetElement.parentNode!!.insertBefore(element, this.targetElement.nextSibling); - break; - case VideoInsertMode.APPEND: - this.targetElement.appendChild(element); - break; - case VideoInsertMode.BEFORE: - this.targetElement.parentNode!!.insertBefore(element, this.targetElement); - break; - case VideoInsertMode.PREPEND: - this.targetElement.insertBefore(element, this.targetElement.childNodes[0]); - break; - case VideoInsertMode.REPLACE: - this.targetElement.parentNode!!.replaceChild(element, this.targetElement); - break; - default: - this.insertElementWithMode(element, VideoInsertMode.APPEND); - } - } - } - - private mirrorVideo(video: HTMLVideoElement): void { - video.style.transform = 'rotateY(180deg)'; - video.style.webkitTransform = 'rotateY(180deg)'; - } - } \ No newline at end of file diff --git a/openvidu-browser/src/OpenVidu/Subscriber.ts b/openvidu-browser/src/OpenVidu/Subscriber.ts index cc94dee5..63c9e4f5 100644 --- a/openvidu-browser/src/OpenVidu/Subscriber.ts +++ b/openvidu-browser/src/OpenVidu/Subscriber.ts @@ -15,56 +15,27 @@ * */ +import { MediaManager } from './MediaManager'; import { Stream } from './Stream'; import { SubscriberProperties } from '../OpenViduInternal/Interfaces/Public/SubscriberProperties'; -import { EventDispatcher } from '../OpenViduInternal/Interfaces/Public/EventDispatcher'; -import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent'; -import EventEmitter = require('wolfy87-eventemitter'); /** * Packs remote media streams. Participants automatically receive them when others publish their streams. Initialized with [[Session.subscribe]] method */ -export class Subscriber implements EventDispatcher { - - /** - * HTML DOM element in which the Subscriber's video has been inserted - */ - element: HTMLElement; - - /** - * DOM id of the Subscriber's video element - */ - id: string; - - /** - * The [[Stream]] to which you are subscribing - */ - stream: Stream; - - private ee = new EventEmitter(); +export class Subscriber extends MediaManager { + private element?: HTMLElement; private properties: SubscriberProperties; /** * @hidden */ - constructor(stream: Stream, targetElement: string | HTMLElement, properties: SubscriberProperties) { + constructor(stream: Stream, targEl: string | HTMLElement, properties: SubscriberProperties) { + super(stream, targEl); + this.element = this.targetElement; this.stream = stream; this.properties = properties; - - if (typeof targetElement === 'string') { - const e = document.getElementById(targetElement); - if (!!e) { - this.element = e; - } - } else if (targetElement instanceof HTMLElement) { - this.element = targetElement; - } - - this.stream.once('video-removed', (element: HTMLVideoElement) => { - this.ee.emitEvent('videoElementDestroyed', [new VideoElementEvent(element, this, 'videoElementDestroyed')]); - }); } /** @@ -91,101 +62,4 @@ export class Subscriber implements EventDispatcher { return this; } - - /** - * See [[EventDispatcher.on]] - */ - on(type: string, handler: (event: VideoElementEvent) => void): EventDispatcher { - this.ee.on(type, event => { - if (event) { - console.info("Event '" + type + "' triggered by 'Subscriber'", event); - } else { - console.info("Event '" + type + "' triggered by 'Subscriber'"); - } - handler(event); - }); - - if (type === 'videoElementCreated') { - if (this.stream.isVideoELementCreated) { - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoElementCreated')]); - } else { - this.stream.once('video-element-created-by-stream', element => { - this.id = element.id; - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(element, this, 'videoElementCreated')]); - }); - } - } - if (type === 'videoPlaying') { - const video = this.stream.getVideoElement(); - if (!this.stream.displayMyRemote() && video && - video.currentTime > 0 && - video.paused === false && - video.ended === false && - video.readyState === 4) { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoPlaying')]); - } else { - this.stream.once('video-is-playing', (element) => { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(element.element, this, 'videoPlaying')]); - }); - } - } - - return this; - } - - - /** - * See [[EventDispatcher.once]] - */ - once(type: string, handler: (event: VideoElementEvent) => void): Subscriber { - this.ee.once(type, event => { - if (event) { - console.info("Event '" + type + "' triggered once by 'Subscriber'", event); - } else { - console.info("Event '" + type + "' triggered once by 'Subscriber'"); - } - handler(event); - }); - - if (type === 'videoElementCreated') { - if (this.stream.isVideoELementCreated) { - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoElementCreated')]); - } else { - this.stream.once('video-element-created-by-stream', element => { - this.id = element.id; - this.ee.emitEvent('videoElementCreated', [new VideoElementEvent(element, this, 'videoElementCreated')]); - }); - } - } - if (type === 'videoPlaying') { - const video = this.stream.getVideoElement(); - if (!this.stream.displayMyRemote() && video && - video.currentTime > 0 && - video.paused === false && - video.ended === false && - video.readyState === 4) { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'videoPlaying')]); - } else { - this.stream.once('video-is-playing', (element) => { - this.ee.emitEvent('videoPlaying', [new VideoElementEvent(element.element, this, 'videoPlaying')]); - }); - } - } - - return this; - } - - - /** - * See [[EventDispatcher.off]] - */ - off(type: string, handler?: (event: VideoElementEvent) => void): Subscriber { - if (!handler) { - this.ee.removeAllListeners(type); - } else { - this.ee.off(type, handler); - } - return this; - } - } \ No newline at end of file diff --git a/openvidu-browser/src/OpenViduInternal/Events/Event.ts b/openvidu-browser/src/OpenViduInternal/Events/Event.ts index f1d96f1d..2eacc343 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/Event.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/Event.ts @@ -15,9 +15,8 @@ * */ -import { Publisher } from '../../OpenVidu/Publisher'; +import { MediaManager } from '../../OpenVidu/MediaManager'; import { Session } from '../../OpenVidu/Session'; -import { Subscriber } from '../../OpenVidu/Subscriber'; export abstract class Event { @@ -29,7 +28,7 @@ export abstract class Event { /** * The object that dispatched the event */ - target: Session | Subscriber | Publisher; + target: Session | MediaManager; /** * The type of event. This is the same string you pass as first parameter when calling method `on()` of any object implementing [[EventDispatcher]] interface @@ -41,7 +40,7 @@ export abstract class Event { /** * @hidden */ - constructor(cancelable, target, type) { + constructor(cancelable: boolean, target: Session | MediaManager, type: string) { this.cancelable = cancelable; this.target = target; this.type = type; diff --git a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts index 4ff256f8..0a9e532c 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/SessionDisconnectedEvent.ts @@ -52,7 +52,7 @@ export class SessionDisconnectedEvent extends Event { if (!!session.remoteConnections[connectionId].stream) { session.remoteConnections[connectionId].stream.disposeWebRtcPeer(); session.remoteConnections[connectionId].stream.disposeMediaStream(); - session.remoteConnections[connectionId].stream.removeVideo(); + session.remoteConnections[connectionId].stream.removeVideos(); delete session.remoteStreamsCreated[session.remoteConnections[connectionId].stream.streamId]; session.remoteConnections[connectionId].dispose(); } diff --git a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts index 556c18c5..c2873478 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/StreamEvent.ts @@ -65,7 +65,7 @@ export class StreamEvent extends Event { // Remote Stream this.stream.disposeWebRtcPeer(); this.stream.disposeMediaStream(); - this.stream.removeVideo(); + this.stream.removeVideos(); } else if (this.target instanceof Publisher) { @@ -73,8 +73,8 @@ export class StreamEvent extends Event { // Local Stream this.stream.disposeMediaStream(); - this.stream.removeVideo(); - this.stream.isReadyToPublish = false; + this.stream.removeVideos(); + this.stream.isLocalStreamReadyToPublish = false; } // Delete stream from Session.remoteStreamsCreated map diff --git a/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts b/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts index 5ce927fc..5ddc84a6 100644 --- a/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts +++ b/openvidu-browser/src/OpenViduInternal/Events/VideoElementEvent.ts @@ -16,6 +16,7 @@ */ import { Event } from './Event'; +import { MediaManager } from '../../OpenVidu/MediaManager'; import { Publisher } from '../../OpenVidu/Publisher'; import { Subscriber } from '../../OpenVidu/Subscriber'; @@ -37,7 +38,7 @@ export class VideoElementEvent extends Event { /** * @hidden */ - constructor(element: HTMLVideoElement, target: Publisher | Subscriber, type: string) { + constructor(element: HTMLVideoElement, target: MediaManager, type: string) { super(false, target, type); this.element = element; } diff --git a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/EventDispatcher.ts b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/EventDispatcher.ts index 28bedc8c..96e9cc3b 100644 --- a/openvidu-browser/src/OpenViduInternal/Interfaces/Public/EventDispatcher.ts +++ b/openvidu-browser/src/OpenViduInternal/Interfaces/Public/EventDispatcher.ts @@ -21,17 +21,23 @@ export interface EventDispatcher { /** * Adds function `handler` to handle event `type` + * + * @returns The EventDispatcher object */ on(type: string, handler: (event: Event) => void): EventDispatcher; /** * Adds function `handler` to handle event `type` just once. The handler will be automatically removed after first execution + * + * @returns The object that dispatched the event */ - once(type: string, handler: (event: Event) => void): any; + once(type: string, handler: (event: Event) => void): Object; /** * Removes a `handler` from event `type`. If no handler is provided, all handlers will be removed from the event + * + * @returns The object that dispatched the event */ - off(type: string, handler?: (event: Event) => void): any; + off(type: string, handler?: (event: Event) => void): Object; } \ No newline at end of file diff --git a/openvidu-browser/src/index.ts b/openvidu-browser/src/index.ts index d6635232..5b654f7a 100644 --- a/openvidu-browser/src/index.ts +++ b/openvidu-browser/src/index.ts @@ -2,6 +2,7 @@ export { OpenVidu } from './OpenVidu/OpenVidu'; export { Session } from './OpenVidu/Session'; export { Publisher } from './OpenVidu/Publisher'; export { Subscriber } from './OpenVidu/Subscriber'; +export { MediaManager } from './OpenVidu/MediaManager'; export { Stream } from './OpenVidu/Stream'; export { Connection } from './OpenVidu/Connection'; export { LocalRecorder } from './OpenVidu/LocalRecorder';