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)
*
* @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', []);

View File

@ -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, <VideoInsertMode>this.properties.insertMode);
this.insertVideo(this.targetElement, <VideoInsertMode>this.properties.insertMode);
resolve();
};

View File

@ -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, <VideoInsertMode>properties.insertMode);
stream.mediaManagers.forEach(mediaManager => {
mediaManager.insertVideo(subscriber.targetElement, <VideoInsertMode>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) {

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -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

View File

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

View File

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

View File

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