openvidu-browser: MediaManager class parent of Publisher and Subscriber

pull/73/head
pabloFuente 2018-05-23 15:01:40 +02:00
parent 0dcc77b0c5
commit d5e4482e37
12 changed files with 424 additions and 437 deletions

View File

@ -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)';
}
}

View File

@ -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) * 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 * @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. * @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 * `completionHandler` function is called before the Publisher dispatches an `accessAllowed` or an `accessDenied` event
*/ */
@ -145,7 +145,7 @@ export class OpenVidu {
} }
publisher.emitEvent('accessAllowed', []); publisher.emitEvent('accessAllowed', []);
}).catch((error) => { }).catch((error) => {
if (!!completionHandler !== undefined) { if (completionHandler !== undefined) {
completionHandler(error); completionHandler(error);
} }
publisher.emitEvent('accessDenied', []); publisher.emitEvent('accessDenied', []);

View File

@ -15,6 +15,7 @@
* *
*/ */
import { MediaManager } from './MediaManager';
import { OpenVidu } from './OpenVidu'; import { OpenVidu } from './OpenVidu';
import { Session } from './Session'; import { Session } from './Session';
import { Stream } from './Stream'; import { Stream } from './Stream';
@ -22,78 +23,49 @@ import { EventDispatcher } from '../OpenViduInternal/Interfaces/Public/EventDisp
import { PublisherProperties } from '../OpenViduInternal/Interfaces/Public/PublisherProperties'; import { PublisherProperties } from '../OpenViduInternal/Interfaces/Public/PublisherProperties';
import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions';
import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions'; import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions';
import { Event } from '../OpenViduInternal/Events/Event';
import { StreamEvent } from '../OpenViduInternal/Events/StreamEvent'; import { StreamEvent } from '../OpenViduInternal/Events/StreamEvent';
import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent'; import { VideoElementEvent } from '../OpenViduInternal/Events/VideoElementEvent';
import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError'; import { OpenViduError, OpenViduErrorName } from '../OpenViduInternal/Enums/OpenViduError';
import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode'; 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 * 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 * Whether the Publisher has been granted access to the requested input devices or not
*/ */
accessAllowed = false; 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 * The [[Session]] to which the Publisher belongs
*/ */
session: Session; // Initialized by Session.publish(Publisher) session: Session; // Initialized by Session.publish(Publisher)
/** /**
* The [[Stream]] that you are publishing * @hidden
*/ */
stream: Stream; accessDenied = false;
private ee = new EventEmitter();
private element?: HTMLElement;
private properties: PublisherProperties; private properties: PublisherProperties;
private permissionDialogTimeout: NodeJS.Timer; private permissionDialogTimeout: NodeJS.Timer;
/** /**
* @hidden * @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.properties = properties;
this.stream = new Stream(this.session, { publisherProperties: properties, mediaConstraints: {} });
this.stream.on('video-removed', (element: HTMLVideoElement) => { this.stream.on('local-stream-destroyed-by-disconnect', (reason: string) => {
this.ee.emitEvent('videoElementDestroyed', [new VideoElementEvent(element, this, 'videoElementDestroyed')]);
});
this.stream.on('stream-destroyed-by-disconnect', (reason: string) => {
const streamEvent = new StreamEvent(true, this, 'streamDestroyed', this.stream, reason); const streamEvent = new StreamEvent(true, this, 'streamDestroyed', this.stream, reason);
this.ee.emitEvent('streamDestroyed', [streamEvent]); this.ee.emitEvent('streamDestroyed', [streamEvent]);
streamEvent.callDefaultBehaviour(); 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]] * See [[EventDispatcher.on]]
*/ */
on(type: string, handler: (event: StreamEvent | VideoElementEvent) => void): EventDispatcher { on(type: string, handler: (event: Event) => void): EventDispatcher {
this.ee.on(type, event => { super.on(type, handler);
if (event) {
console.info("Event '" + type + "' triggered by 'Publisher'", event);
} else {
console.info("Event '" + type + "' triggered by 'Publisher'");
}
handler(event);
});
if (type === 'streamCreated') { 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, '')]); this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', this.stream, '')]);
} else { } else {
this.stream.on('stream-created-by-publisher', () => { 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') { if (type === 'remoteVideoPlaying') {
const video = this.stream.getVideoElement(); if (this.stream.displayMyRemote() && this.video &&
if (this.stream.displayMyRemote() && video && this.video.currentTime > 0 &&
video.currentTime > 0 && this.video.paused === false &&
video.paused === false && this.video.ended === false &&
video.ended === false && this.video.readyState === 4) {
video.readyState === 4) { this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.video, this, 'remoteVideoPlaying')]);
this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'remoteVideoPlaying')]);
} else { } else {
this.stream.on('remote-video-is-playing', (element) => { this.stream.on('remote-video-is-playing', (element) => {
this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(element.element, this, 'remoteVideoPlaying')]); this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(element.element, this, 'remoteVideoPlaying')]);
@ -183,16 +122,15 @@ export class Publisher implements EventDispatcher {
} }
} }
if (type === 'accessAllowed') { if (type === 'accessAllowed') {
if (this.stream.accessIsAllowed) { if (this.accessAllowed) {
this.ee.emitEvent('accessAllowed'); this.ee.emitEvent('accessAllowed');
} }
} }
if (type === 'accessDenied') { if (type === 'accessDenied') {
if (this.stream.accessIsDenied) { if (this.accessDenied) {
this.ee.emitEvent('accessDenied'); this.ee.emitEvent('accessDenied');
} }
} }
return this; return this;
} }
@ -200,18 +138,10 @@ export class Publisher implements EventDispatcher {
/** /**
* See [[EventDispatcher.once]] * See [[EventDispatcher.once]]
*/ */
once(type: string, handler: (event: StreamEvent | VideoElementEvent) => void): Publisher { once(type: string, handler: (event: Event) => void): Publisher {
this.ee.once(type, event => { super.once(type, handler);
if (event) {
console.info("Event '" + type + "' triggered by 'Publisher'", event);
} else {
console.info("Event '" + type + "' triggered by 'Publisher'");
}
handler(event);
});
if (type === 'streamCreated') { 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, '')]); this.ee.emitEvent('streamCreated', [new StreamEvent(false, this, 'streamCreated', this.stream, '')]);
} else { } else {
this.stream.once('stream-created-by-publisher', () => { 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') { if (type === 'remoteVideoPlaying') {
const video = this.stream.getVideoElement(); if (this.stream.displayMyRemote() && this.video &&
if (this.stream.displayMyRemote() && video && this.video.currentTime > 0 &&
video.currentTime > 0 && this.video.paused === false &&
video.paused === false && this.video.ended === false &&
video.ended === false && this.video.readyState === 4) {
video.readyState === 4) { this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.video, this, 'remoteVideoPlaying')]);
this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(this.stream.getVideoElement(), this, 'remoteVideoPlaying')]);
} else { } else {
this.stream.once('remote-video-is-playing', (element) => { this.stream.once('remote-video-is-playing', (element) => {
this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(element.element, this, 'remoteVideoPlaying')]); this.ee.emitEvent('remoteVideoPlaying', [new VideoElementEvent(element.element, this, 'remoteVideoPlaying')]);
@ -258,29 +163,15 @@ export class Publisher implements EventDispatcher {
} }
} }
if (type === 'accessAllowed') { if (type === 'accessAllowed') {
if (this.stream.accessIsAllowed) { if (this.accessAllowed) {
this.ee.emitEvent('accessAllowed'); this.ee.emitEvent('accessAllowed');
} }
} }
if (type === 'accessDenied') { if (type === 'accessDenied') {
if (this.stream.accessIsDenied) { if (this.accessDenied) {
this.ee.emitEvent('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; return this;
} }
@ -294,14 +185,14 @@ export class Publisher implements EventDispatcher {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const errorCallback = (openViduError: OpenViduError) => { const errorCallback = (openViduError: OpenViduError) => {
this.stream.accessIsDenied = true; this.accessDenied = true;
this.stream.accessIsAllowed = false; this.accessAllowed = false;
reject(openViduError); reject(openViduError);
}; };
const successCallback = (mediaStream: MediaStream) => { const successCallback = (mediaStream: MediaStream) => {
this.stream.accessIsAllowed = true; this.accessAllowed = true;
this.stream.accessIsDenied = false; this.accessDenied = false;
if (this.openvidu.isMediaStreamTrack(this.properties.audioSource)) { if (this.openvidu.isMediaStreamTrack(this.properties.audioSource)) {
mediaStream.removeTrack(mediaStream.getAudioTracks()[0]); mediaStream.removeTrack(mediaStream.getAudioTracks()[0]);
@ -322,7 +213,7 @@ export class Publisher implements EventDispatcher {
} }
this.stream.setMediaStream(mediaStream); this.stream.setMediaStream(mediaStream);
this.stream.insertVideo(this.element, <VideoInsertMode>this.properties.insertMode); this.insertVideo(this.targetElement, <VideoInsertMode>this.properties.insertMode);
resolve(); resolve();
}; };

View File

@ -185,7 +185,7 @@ export class Session implements EventDispatcher {
* *
* @param stream Stream object to subscribe to * @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 * @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. * @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 { 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); const subscriber = new Subscriber(stream, targetElement, properties);
stream.insertVideo(subscriber.element, <VideoInsertMode>properties.insertMode); stream.mediaManagers.forEach(mediaManager => {
mediaManager.insertVideo(subscriber.targetElement, <VideoInsertMode>properties.insertMode);
});
return subscriber; return subscriber;
} }
@ -272,9 +274,9 @@ export class Session implements EventDispatcher {
console.info('Unsubscribing from ' + connectionId); console.info('Unsubscribing from ' + connectionId);
this.openvidu.sendRequest('unsubscribeFromVideo', { this.openvidu.sendRequest(
sender: subscriber.stream.connection.connectionId 'unsubscribeFromVideo',
}, { sender: subscriber.stream.connection.connectionId },
(error, response) => { (error, response) => {
if (error) { if (error) {
console.error('Error unsubscribing from ' + connectionId, error); console.error('Error unsubscribing from ' + connectionId, error);
@ -283,8 +285,9 @@ export class Session implements EventDispatcher {
} }
subscriber.stream.disposeWebRtcPeer(); subscriber.stream.disposeWebRtcPeer();
subscriber.stream.disposeMediaStream(); subscriber.stream.disposeMediaStream();
}); }
subscriber.stream.removeVideo(); );
subscriber.stream.removeVideos();
} }
@ -307,7 +310,7 @@ export class Session implements EventDispatcher {
publisher.session = this; publisher.session = this;
publisher.stream.session = this; publisher.stream.session = this;
if (!publisher.stream.isPublisherPublished) { if (!publisher.stream.isLocalStreamPublished) {
// 'Session.unpublish(Publisher)' has NOT been called // 'Session.unpublish(Publisher)' has NOT been called
this.connection.addStream(publisher.stream); this.connection.addStream(publisher.stream);
publisher.stream.publish() publisher.stream.publish()
@ -788,7 +791,7 @@ export class Session implements EventDispatcher {
if (!!this.connection.stream) { if (!!this.connection.stream) {
// Make Publisher object dispatch 'streamDestroyed' event (if there's a local stream) // Make Publisher object dispatch 'streamDestroyed' event (if there's a local stream)
this.connection.stream.disposeWebRtcPeer(); 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) { if (!this.connection.disposed) {

View File

@ -16,15 +16,17 @@
*/ */
import { Connection } from './Connection'; import { Connection } from './Connection';
import { MediaManager } from './MediaManager';
import { Session } from './Session'; import { Session } from './Session';
import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions'; import { InboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/InboundStreamOptions';
import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions'; import { OutboundStreamOptions } from '../OpenViduInternal/Interfaces/Private/OutboundStreamOptions';
import { WebRtcStats } from '../OpenViduInternal/WebRtcStats/WebRtcStats'; import { WebRtcStats } from '../OpenViduInternal/WebRtcStats/WebRtcStats';
import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent'; import { PublisherSpeakingEvent } from '../OpenViduInternal/Events/PublisherSpeakingEvent';
import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode';
import EventEmitter = require('wolfy87-eventemitter'); import EventEmitter = require('wolfy87-eventemitter');
import * as kurentoUtils from '../OpenViduInternal/KurentoUtils/kurento-utils-js'; import * as kurentoUtils from '../OpenViduInternal/KurentoUtils/kurento-utils-js';
import { VideoInsertMode } from '../OpenViduInternal/Enums/VideoInsertMode';
/** /**
@ -63,13 +65,15 @@ export class Stream {
*/ */
typeOfVideo?: string; typeOfVideo?: string;
/**
* Array of [[MediaManager]] objects displaying this stream in the DOM
*/
mediaManagers: MediaManager[] = [];
private ee = new EventEmitter(); private ee = new EventEmitter();
private webRtcPeer: any; private webRtcPeer: any;
private mediaStream: MediaStream; private mediaStream: MediaStream;
private video: HTMLVideoElement;
private targetElement: HTMLElement;
private parentId: string;
private webRtcStats: WebRtcStats; private webRtcStats: WebRtcStats;
private isSubscribeToRemote = false; private isSubscribeToRemote = false;
@ -77,23 +81,11 @@ export class Stream {
/** /**
* @hidden * @hidden
*/ */
isReadyToPublish = false; isLocalStreamReadyToPublish = false;
/** /**
* @hidden * @hidden
*/ */
isPublisherPublished = false; isLocalStreamPublished = false;
/**
* @hidden
*/
isVideoELementCreated = false;
/**
* @hidden
*/
accessIsAllowed = false;
/**
* @hidden
*/
accessIsDenied = false;
/** /**
* @hidden * @hidden
*/ */
@ -149,11 +141,30 @@ export class Stream {
} }
this.on('mediastream-updated', () => { 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 + ']'); 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 */ /* Hidden methods */
@ -186,13 +197,6 @@ export class Stream {
return this.webRtcPeer.peerConnection; return this.webRtcPeer.peerConnection;
} }
/**
* @hidden
*/
getVideoElement(): HTMLVideoElement {
return this.video;
}
/** /**
* @hidden * @hidden
*/ */
@ -227,7 +231,7 @@ export class Stream {
*/ */
publish(): Promise<any> { publish(): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.isReadyToPublish) { if (this.isLocalStreamReadyToPublish) {
this.initWebRtcPeerSend() this.initWebRtcPeerSend()
.then(() => { .then(() => {
resolve(); resolve();
@ -301,68 +305,6 @@ export class Stream {
this.ee.once(eventName, listener); 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 * @hidden
*/ */
@ -445,6 +387,15 @@ export class Stream {
this.speechEvent = undefined; this.speechEvent = undefined;
} }
/**
* @hidden
*/
removeVideos(): void {
this.mediaManagers.forEach(mediaManager => {
mediaManager.removeVideo();
});
}
/* Private methods */ /* Private methods */
@ -510,7 +461,7 @@ export class Stream {
this.webRtcPeer.generateOffer(successCallback); this.webRtcPeer.generateOffer(successCallback);
}); });
} }
this.isPublisherPublished = true; this.isLocalStreamPublished = true;
}); });
} }
@ -588,24 +539,9 @@ export class Stream {
} }
} }
if (!!this.video) { this.mediaManagers.forEach(mediaManager => {
// let thumbnailId = this.video.thumb; mediaManager.addOnCanPlayEvent();
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.session.emitEvent('stream-subscribed', [{ this.session.emitEvent('stream-subscribed', [{
stream: this stream: this
}]); }]);
@ -631,38 +567,12 @@ export class Stream {
} }
} }
private isLocal(): boolean { /**
* @hidden
*/
isLocal(): boolean {
// inbound options undefined and outbound options defined // inbound options undefined and outbound options defined
return (!this.inboundStreamOpts && !!this.outboundStreamOpts); 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)';
}
} }

View File

@ -15,56 +15,27 @@
* *
*/ */
import { MediaManager } from './MediaManager';
import { Stream } from './Stream'; import { Stream } from './Stream';
import { SubscriberProperties } from '../OpenViduInternal/Interfaces/Public/SubscriberProperties'; 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 * Packs remote media streams. Participants automatically receive them when others publish their streams. Initialized with [[Session.subscribe]] method
*/ */
export class Subscriber implements EventDispatcher { export class Subscriber extends MediaManager {
/**
* 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();
private element?: HTMLElement;
private properties: SubscriberProperties; private properties: SubscriberProperties;
/** /**
* @hidden * @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.stream = stream;
this.properties = properties; 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; 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;
}
} }

View File

@ -15,9 +15,8 @@
* *
*/ */
import { Publisher } from '../../OpenVidu/Publisher'; import { MediaManager } from '../../OpenVidu/MediaManager';
import { Session } from '../../OpenVidu/Session'; import { Session } from '../../OpenVidu/Session';
import { Subscriber } from '../../OpenVidu/Subscriber';
export abstract class Event { export abstract class Event {
@ -29,7 +28,7 @@ export abstract class Event {
/** /**
* The object that dispatched the 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 * 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 * @hidden
*/ */
constructor(cancelable, target, type) { constructor(cancelable: boolean, target: Session | MediaManager, type: string) {
this.cancelable = cancelable; this.cancelable = cancelable;
this.target = target; this.target = target;
this.type = type; this.type = type;

View File

@ -52,7 +52,7 @@ export class SessionDisconnectedEvent extends Event {
if (!!session.remoteConnections[connectionId].stream) { if (!!session.remoteConnections[connectionId].stream) {
session.remoteConnections[connectionId].stream.disposeWebRtcPeer(); session.remoteConnections[connectionId].stream.disposeWebRtcPeer();
session.remoteConnections[connectionId].stream.disposeMediaStream(); session.remoteConnections[connectionId].stream.disposeMediaStream();
session.remoteConnections[connectionId].stream.removeVideo(); session.remoteConnections[connectionId].stream.removeVideos();
delete session.remoteStreamsCreated[session.remoteConnections[connectionId].stream.streamId]; delete session.remoteStreamsCreated[session.remoteConnections[connectionId].stream.streamId];
session.remoteConnections[connectionId].dispose(); session.remoteConnections[connectionId].dispose();
} }

View File

@ -65,7 +65,7 @@ export class StreamEvent extends Event {
// Remote Stream // Remote Stream
this.stream.disposeWebRtcPeer(); this.stream.disposeWebRtcPeer();
this.stream.disposeMediaStream(); this.stream.disposeMediaStream();
this.stream.removeVideo(); this.stream.removeVideos();
} else if (this.target instanceof Publisher) { } else if (this.target instanceof Publisher) {
@ -73,8 +73,8 @@ export class StreamEvent extends Event {
// Local Stream // Local Stream
this.stream.disposeMediaStream(); this.stream.disposeMediaStream();
this.stream.removeVideo(); this.stream.removeVideos();
this.stream.isReadyToPublish = false; this.stream.isLocalStreamReadyToPublish = false;
} }
// Delete stream from Session.remoteStreamsCreated map // Delete stream from Session.remoteStreamsCreated map

View File

@ -16,6 +16,7 @@
*/ */
import { Event } from './Event'; import { Event } from './Event';
import { MediaManager } from '../../OpenVidu/MediaManager';
import { Publisher } from '../../OpenVidu/Publisher'; import { Publisher } from '../../OpenVidu/Publisher';
import { Subscriber } from '../../OpenVidu/Subscriber'; import { Subscriber } from '../../OpenVidu/Subscriber';
@ -37,7 +38,7 @@ export class VideoElementEvent extends Event {
/** /**
* @hidden * @hidden
*/ */
constructor(element: HTMLVideoElement, target: Publisher | Subscriber, type: string) { constructor(element: HTMLVideoElement, target: MediaManager, type: string) {
super(false, target, type); super(false, target, type);
this.element = element; this.element = element;
} }

View File

@ -21,17 +21,23 @@ export interface EventDispatcher {
/** /**
* Adds function `handler` to handle event `type` * Adds function `handler` to handle event `type`
*
* @returns The EventDispatcher object
*/ */
on(type: string, handler: (event: Event) => void): EventDispatcher; 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 * 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 * 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;
} }

View File

@ -2,6 +2,7 @@ export { OpenVidu } from './OpenVidu/OpenVidu';
export { Session } from './OpenVidu/Session'; export { Session } from './OpenVidu/Session';
export { Publisher } from './OpenVidu/Publisher'; export { Publisher } from './OpenVidu/Publisher';
export { Subscriber } from './OpenVidu/Subscriber'; export { Subscriber } from './OpenVidu/Subscriber';
export { MediaManager } from './OpenVidu/MediaManager';
export { Stream } from './OpenVidu/Stream'; export { Stream } from './OpenVidu/Stream';
export { Connection } from './OpenVidu/Connection'; export { Connection } from './OpenVidu/Connection';
export { LocalRecorder } from './OpenVidu/LocalRecorder'; export { LocalRecorder } from './OpenVidu/LocalRecorder';