UNPKG

zombiebox-platform-lg

Version:

LG NetCast Smart TV support abstraction layer for ZombieBox framework.

525 lines (442 loc) 9.23 kB
/* * This file is part of the ZombieBox package. * * Copyright © 2014-2020, Interfaced * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import {div, remove} from 'zb/html'; import Rect from 'zb/geometry/rect'; import {State} from 'zb/device/interfaces/i-video'; import {error, debug} from 'zb/console/console'; import AbstractVideo from 'zb/device/abstract-video'; import UnsupportedFeature from 'zb/device/errors/unsupported-feature'; import {ResolutionInfo, findLargest} from 'zb/device/resolutions'; import ViewPort from './view-port'; /** */ export class Video extends AbstractVideo { /** * @param {Rect} rect */ constructor(rect) { super(rect); debug('Video: creating'); /** * @type {ViewPort} * @protected */ this._viewport; /** * @type {number} * @protected */ this._pollingTime = 1000; /** * @type {number} Timeout ID * @protected */ this._pollingTimeout; /** * @type {{ * position: number, * durationDetected: boolean, * metadataDetected: boolean * }} * @protected */ this._pollingData = { position: 0, durationDetected: false, metadataDetected: false }; /** * @type {State} * @protected */ this._state = State.INITED; /** * @type {number} * @protected */ this._startPosition = NaN; /** * @type {HTMLDivElement} * @protected */ this._container = this._createContainer(); /** * @type {LGVideoObject} * @protected */ this._object = this._createVideoObject(); this._initViewPort(); this._onStateChange = this._onStateChange.bind(this); this._pollingTick = this._pollingTick.bind(this); this._initVideoObject(''); debug('Video: done creating'); } /** * @override */ play(url, startFrom) { debug(`Video: playing ${url}`); this._initVideoObject(url); if (startFrom) { this._startPosition = startFrom; debug(`Video: playback started from ${startFrom}ms`); } else { this._startPosition = NaN; debug('Video: playback started'); } this._object.play(); } /** * @override */ pause() { if (this._object) { this._object.play(0); } } /** * @override */ resume() { if (this._object) { this._object.play(); } } /** * @override */ stop() { if (this._object) { this._object.stop(); } this._state = State.STOPPED; this._fireEvent(this.EVENT_STOP); this._pollingStop(); } /** * @override */ setVolume(value) { throw new UnsupportedFeature('Volume setting'); } /** * @override */ getVolume() { throw new UnsupportedFeature('Volume getting'); } /** * @override */ getMuted() { throw new UnsupportedFeature('Mute state getting'); } /** * @override */ setMuted(value) { throw new UnsupportedFeature('Mute state setting'); } /** * @override */ destroy() { try { // Stop position polling interval this._pollingStop(); // Remove from DOM this._removeVideoObject(); this._removeContainer(); } catch (err) { error('Video::destroy', err); } } /** * @override */ forward() { if (!this._object) { return false; } this._object.play(16); return true; } /** * @override */ rewind() { if (!this._object) { return false; } this._object.play(-16); return true; } /** * @override */ getPlaybackRate() { if (!this._object) { return 1; } return this._object.speed; } /** * @override */ setPlaybackRate(rate) { if (this._object) { this._object.play(rate); } } /** * @override */ getDuration() { if (!this._object) { return 0; } return parseInt(this._object.playTime, 10); } /** * @override */ getState() { if (!this._object) { return this._state; } switch (this._object.playState) { case PlayState.STOPPED: this._state = State.STOPPED; break; case PlayState.BUFFERING: this._state = State.BUFFERING; break; case PlayState.PLAYING: this._state = State.PLAYING; break; case PlayState.PAUSED: this._state = State.PAUSED; break; case PlayState.CONNECTING: this._state = State.LOADING; break; case PlayState.FINISHED: this._state = State.STOPPED; break; case PlayState.ERROR: this._state = State.ERROR; break; } return this._state; } /** * @override */ getUrl() { if (this._object && this._object.data) { return this._object.data; } return ''; } /** * @override */ getPosition() { if (!this._object) { return 0; } return parseInt(this._object.playPosition, 10); } /** * @override */ setPosition(value) { if (this._object) { this._object.seek(value); } } /** * @param {number=} time */ setPollingTime(time = 1000) { this._pollingTime = time; this._pollingRestart(); } /** * @override * @return {ViewPort} */ _createViewPort(containerRect) { const panelResolution = ResolutionInfo[findLargest(containerRect)]; const appResolution = panelResolution; return new ViewPort(panelResolution, appResolution, this._object); } /** * @return {HTMLDivElement} * @protected */ _createContainer() { const container = div('video-container'); container.style.backgroundColor = 'black'; container.style.position = 'absolute'; container.style.overflow = 'hidden'; document.body.insertBefore(container, document.body.firstChild); return container; } /** * @return {LGVideoObject} * @protected */ _createVideoObject() { const videoObject = /** @type {LGVideoObject} */ (document.createElement('object')); videoObject.setAttribute('type', 'application/x-netcast-av'); videoObject.setAttribute('autoStart', 'true'); videoObject.setAttribute('downloadable', 'false'); return videoObject; } /** * @protected */ _removeVideoObject() { if (this._object && this._object.parentNode) { remove(this._object); } this._object = null; } /** * @protected */ _removeContainer() { if (this._container) { remove(this._container); } this._container = null; } /** * @param {string} url * @protected */ _initVideoObject(url) { this._removeVideoObject(); this._object = this._createVideoObject(); this._object.setAttribute('data', url); this._object.onPlayStateChange = this._onStateChange.bind(this); debug('Video: object created'); this._container.appendChild(this._object); debug('Video: object appended'); this._viewport.setVideoObject(this._object); this._viewport.updateViewPort(); this._pollingRestart(); } /** * @protected */ _onStateChange() { let oldState = this._state; const state = this.getState(); if (state === oldState) { return; } if (!isNaN(this._startPosition)) { if (state === State.BUFFERING) { // Substitution of old event to skip seek events oldState = State.INITED; this.setPosition(this._startPosition); } else if (state === State.PLAYING) { this._state = State.BUFFERING; this._startPosition = NaN; return; } } debug(`Video: state changed from ${oldState} to ${state}`); this._fireEvent(this.EVENT_STATE_CHANGE, state, oldState); let event = this._matchStateToEvent(state); if (this._object.playState === PlayState.FINISHED) { event = this.EVENT_ENDED; } if (event) { this._fireEvent(event); } } /** * Get event name that should be fired when video changes it's state to *state* * @param {State} state * @return {string|undefined} * @protected */ _matchStateToEvent(state) { const matchedStates = { [State.PLAYING]: this.EVENT_PLAY, [State.PAUSED]: this.EVENT_PAUSE, [State.STOPPED]: this.EVENT_STOP, [State.BUFFERING]: this.EVENT_BUFFERING, [State.ERROR]: this.EVENT_ERROR }; return matchedStates[state]; } /** * @protected */ _pollingRestart() { this._pollingStop(); this._pollingData = { position: 0, durationDetected: false, metadataDetected: false }; this._pollingTimeout = setTimeout(this._pollingTick, this._pollingTime); } /** * @protected */ _pollingStop() { if (!isNaN(this._pollingTimeout)) { clearTimeout(this._pollingTimeout); } this._pollingTimeout = NaN; } /** * @protected */ _pollingTick() { let duration = NaN; if (!this._pollingData.durationDetected) { duration = this.getDuration(); if (duration) { this._pollingData.durationDetected = true; this._fireEvent(this.EVENT_DURATION_CHANGE, duration); } } const position = this.getPosition(); if (this._pollingData.position !== position) { this._pollingData.position = position; this._fireEvent(this.EVENT_TIME_UPDATE, position); } if (!this._pollingData.metadataDetected) { if (duration || position || this.getState() === State.PLAYING) { this._pollingData.metadataDetected = true; this._fireEvent(this.EVENT_LOADED_META_DATA); } } // Schedule next poll this._pollingTimeout = setTimeout(this._pollingTick, this._pollingTime); } } /** * @enum {number} */ export const PlayState = { STOPPED: 0, PLAYING: 1, PAUSED: 2, CONNECTING: 3, BUFFERING: 4, FINISHED: 5, ERROR: 6 };