UNPKG

@skylineos/clsp-player

Version:

Skyline Technology Solutions' CLSP Video Player. Stream video in near-real-time in modern browsers.

334 lines (284 loc) 9.64 kB
import { timeout as PromiseTimeout, } from 'promise-timeout'; import Logger from '../../utils/Logger'; import utils from '../../utils/utils'; import RouterBaseManager from './RouterBaseManager'; import Router from './Router'; import iframeEventHandlers from './iframeEventHandlers'; import StreamConfiguration from '../../iov/StreamConfiguration'; const DEFAULT_ROUTER_CONNECTION_TIMEOUT = 120; // Setting this to half of the default value to help with SFS memory // management const DEFAULT_ROUTER_KEEP_ALIVE_INTERVAL = 30; // The number of seconds to wait for a "publish" message to be delivered const DEFAULT_ROUTER_PUBLISH_TIMEOUT = utils.DEFAULT_STREAM_TIMEOUT; export default class RouterIframeManager extends RouterBaseManager { /** * @static * * The events that this RouterStatsManager will emit. */ static events = { IFRAME_DESTROYED_EXTERNALLY: 'iframe-destroyed-externally', }; /** * @static * * The Router events that this Router Manager is responsible for */ static routerEvents = { CREATE_SUCCESS: RouterBaseManager.routerEvents.CREATE_SUCCESS, CREATE_FAILURE: RouterBaseManager.routerEvents.CREATE_FAILURE, }; static factory ( logId, clientId, streamConfiguration, containerElement, ) { return new RouterIframeManager( logId, clientId, streamConfiguration, containerElement, ); } /** * @private * * @param {String} logId * a string that identifies this router in log messages * @param {String} clientId * the guid to be used to construct the topic * @param {StreamConfiguration} streamConfiguration * The stream configuration to pull from the CLSP server / SFS * @param {Element} containerElement * The container of the video element and where the iframe will be inserted */ constructor ( logId, clientId, streamConfiguration, containerElement, ) { super( logId, clientId, ); if (!StreamConfiguration.isStreamConfiguration(streamConfiguration)) { throw new Error('invalid streamConfiguration passed to Router Iframe Manager constructor'); } if (!containerElement) { throw new Error('containerElement is required to construct a new Router Iframe Manager instance'); } // Passed state this.streamConfiguration = streamConfiguration; this.containerElement = containerElement; // Managed state this.iframe = null; // State flags this.isCreated = false; this.hasIframeDestroyedExternallyEventBeenEmitted = false; // These can be configured manually after construction this.ROUTER_CONNECTION_TIMEOUT = DEFAULT_ROUTER_CONNECTION_TIMEOUT; this.ROUTER_KEEP_ALIVE_INTERVAL = DEFAULT_ROUTER_KEEP_ALIVE_INTERVAL; this.ROUTER_PUBLISH_TIMEOUT = DEFAULT_ROUTER_PUBLISH_TIMEOUT; } /** * @async * * Create the iframe and the Router needed to get the stream from the server. * * @returns Promise * Resolves when the Router has been successfully created. * Rejects upon failure to create the Router. */ async create () { if (this.isDestroyed) { throw new Error('Tried to create a destroyed Router Iframe Manager!'); } if (this.isCreated) { this.logger.warn('Router Iframe Manager already created...'); return; } this.logger.debug('Initializing...'); try { await PromiseTimeout(this._create(), 2 * 1000); this.logger.info('Router created successfully'); this.isCreated = true; } catch (error) { this.logger.error('Failed to create the Iframe/Router!'); this.logger.error(error); } } _create () { return new Promise((resolve, reject) => { this._onRouterCreated = (error) => { if (error) { return reject(error); } resolve(); }; this.iframe = this._generateIframe(); // @todo - if the Iov were to create a wrapper around the video element // that it manages (rather than expecting one to already be there), each // video element and iframe could be contained in a single container, // rather than potentially having multiple video elements and multiple // iframes contained in a single parent. this.containerElement.appendChild(this.iframe); }); } _handleRouterCreatedEvent (eventType, event) { if (this.isDestroyed) { throw new Error('Tried to create a Router for a destroyed Router Iframe Manager!'); } // @todo - a better check may be `!this.isInitializing` if (!this._onRouterCreated) { throw new Error('Tried to create Router prior to initialization!'); } if (this.isCreated) { throw new Error('Tried to create Router after initialization!'); } switch (eventType) { case RouterIframeManager.routerEvents.CREATE_SUCCESS: { this._onRouterCreated(); break; } case RouterIframeManager.routerEvents.CREATE_FAILURE: { this._onRouterCreated(new Error(event.data.reason)); break; } default: { throw new Error(`Unknown eventType: ${eventType}`); } } } /** * @private * * Generate an iframe with an embedded CLSP Router. * * @returns Element */ _generateIframe () { this.logger.debug('Generating Iframe...'); const iframe = document.createElement('iframe'); iframe.setAttribute('id', this.clientId); // This iframe should be invisible iframe.width = 0; iframe.height = 0; iframe.setAttribute('style', 'display:none;'); iframe.srcdoc = ` <html> <head> <script type="text/javascript"> // Configure the CLSP properties window.clspRouterConfig = { logId: '${this.logId}', clientId: '${this.clientId}', host: '${this.streamConfiguration.host}', port: ${this.streamConfiguration.port}, useSSL: ${this.streamConfiguration.useSSL}, CONNECTION_TIMEOUT: ${this.ROUTER_CONNECTION_TIMEOUT}, KEEP_ALIVE_INTERVAL: ${this.ROUTER_KEEP_ALIVE_INTERVAL}, PUBLISH_TIMEOUT: ${this.ROUTER_PUBLISH_TIMEOUT}, Logger: (${Logger.toString()})("${utils.version}"), }; window.Router = ${Router.toString()}(window.parent.Paho); window.iframeEventHandlers = ${iframeEventHandlers.toString()}(); </script> </head> <body onload="window.router = window.iframeEventHandlers.onload( '${this.logId}', '${this.clientId}', window.Router, window.clspRouterConfig );" onunload="window.iframeEventHandlers.onunload( '${this.logId}', '${this.clientId}', window.router );" > <div id="message"></div> </body> </html> `; return iframe; } wasIframeDestroyedExternally () { return !this.iframe.contentWindow; } /** * @private * * Pass a Router command to the iframe. Should only ever be invoked in * response to a Router Manager command event. * * @param {Object} message * The message / event data to send with the command */ command (command, data) { this.logger.debug('Sending a message to the iframe...'); if (this.isDestroyComplete) { this.logger.warn('Cannot send message via destroyed iframe', command, data); return; } if (this.wasIframeDestroyedExternally()) { if (this.isDestroyed) { this.logger.info('Iframe was destroyed externally while in process of destroying'); return; } // @todo - maybe this should warn, but there are multiple commands that // still come through when the iframe is destroyed externally... this.logger.info('Cannot process command because the iframe is destroyed.'); if (this.hasIframeDestroyedExternallyEventBeenEmitted) { return; } this.hasIframeDestroyedExternallyEventBeenEmitted = true; // In the normal course of operation, sometimes other libraries or // implementations will delete the iframe or a parent component // rather than letting the CLSP Player manage it. In this instance, // we recognize that this happened, but do not show nor throw an error. this.logger.info('Iframe destroyed externally!'); this.emit(RouterIframeManager.events.IFRAME_DESTROYED_EXTERNALLY); return; } try { const message = { method: command, ...data, }; // @todo - we should not be dispatching to '*' - we should provide the SFS // host here instead // @see - https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage this.iframe.contentWindow.postMessage(message, '*'); } catch (error) { // @todo - can we do anything when this error is encountered this.logger.error(`Failed while trying to execute command ${error}`); this.logger.error(error); } } _destroyIframe () { if (this.isDestroyComplete) { return; } // The Router will be destroyed along with the iframe this.iframe.parentNode.removeChild(this.iframe); this.iframe.srcdoc = ''; this.iframe = null; // Calling this doesn't seem to work... // this.iframe.remove(); } async _destroy () { this._destroyIframe(); // The caller must destroy the streamConfiguration this.streamConfiguration = null; this.containerElement = null; await super._destroy(); } }