UNPKG

@lightningjs/sdk

Version:

The Lightning-SDK helps you build great Lightning-based TV apps!

537 lines (465 loc) 15.2 kB
/* * If not stated otherwise in this file or this component's LICENSE file the * following copyright and licenses apply: * * Copyright 2020 Metrological * * 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 Lightning from '../Lightning' import Metrics from '../Metrics' import Settings from '../Settings' import Log from '../Log' const events = [ 'timeupdate', 'error', 'ended', 'loadeddata', 'canplay', 'play', 'playing', 'pause', 'loadstart', 'seeking', 'seeked', 'encrypted', ] let mediaUrl = url => url export const initMediaPlayer = config => { if (config.mediaUrl) { mediaUrl = config.mediaUrl } } export default class Mediaplayer extends Lightning.Component { _construct() { this._skipRenderToTexture = false this._metrics = null this._textureMode = Settings.get('platform', 'textureMode') || false Log.info('Texture mode: ' + this._textureMode) console.warn( [ "The 'MediaPlayer'-plugin in the Lightning-SDK is deprecated and will be removed in future releases.", "Please consider using the new 'VideoPlayer'-plugin instead.", 'https://rdkcentral.github.io/Lightning-SDK/#/plugins/videoplayer', ].join('\n\n') ) } static _template() { return { Video: { VideoWrap: { VideoTexture: { visible: false, pivot: 0.5, texture: { type: Lightning.textures.StaticTexture, options: {} }, }, }, }, } } set skipRenderToTexture(v) { this._skipRenderToTexture = v } get textureMode() { return this._textureMode } get videoView() { return this.tag('Video') } _init() { //re-use videotag if already there const videoEls = document.getElementsByTagName('video') if (videoEls && videoEls.length > 0) this.videoEl = videoEls[0] else { this.videoEl = document.createElement('video') this.videoEl.setAttribute('id', 'video-player') this.videoEl.style.position = 'absolute' this.videoEl.style.zIndex = '1' this.videoEl.style.display = 'none' this.videoEl.setAttribute('width', '100%') this.videoEl.setAttribute('height', '100%') this.videoEl.style.visibility = this.textureMode ? 'hidden' : 'visible' document.body.appendChild(this.videoEl) } if (this.textureMode && !this._skipRenderToTexture) { this._createVideoTexture() } this.eventHandlers = [] } _registerListeners() { events.forEach(event => { const handler = e => { if (this._metrics && this._metrics[event] && typeof this._metrics[event] === 'function') { this._metrics[event]({ currentTime: this.videoEl.currentTime }) } this.fire(event, { videoElement: this.videoEl, event: e }) } this.eventHandlers.push(handler) this.videoEl.addEventListener(event, handler) }) } _deregisterListeners() { Log.info('Deregistering event listeners MediaPlayer') events.forEach((event, index) => { this.videoEl.removeEventListener(event, this.eventHandlers[index]) }) this.eventHandlers = [] } _attach() { this._registerListeners() } _detach() { this._deregisterListeners() this.close() } _createVideoTexture() { const stage = this.stage const gl = stage.gl const glTexture = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, glTexture) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) this.videoTexture.options = { source: glTexture, w: this.videoEl.width, h: this.videoEl.height } } _startUpdatingVideoTexture() { if (this.textureMode && !this._skipRenderToTexture) { const stage = this.stage if (!this._updateVideoTexture) { this._updateVideoTexture = () => { if (this.videoTexture.options.source && this.videoEl.videoWidth && this.active) { const gl = stage.gl const currentTime = new Date().getTime() // When BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DEBUGUTILS is not set in WPE, webkitDecodedFrameCount will not be available. // We'll fallback to fixed 30fps in this case. const frameCount = this.videoEl.webkitDecodedFrameCount const mustUpdate = frameCount ? this._lastFrame !== frameCount : this._lastTime < currentTime - 30 if (mustUpdate) { this._lastTime = currentTime this._lastFrame = frameCount try { gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source) gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.videoEl) this._lastFrame = this.videoEl.webkitDecodedFrameCount this.videoTextureView.visible = true this.videoTexture.options.w = this.videoEl.videoWidth this.videoTexture.options.h = this.videoEl.videoHeight const expectedAspectRatio = this.videoTextureView.w / this.videoTextureView.h const realAspectRatio = this.videoEl.videoWidth / this.videoEl.videoHeight if (expectedAspectRatio > realAspectRatio) { this.videoTextureView.scaleX = realAspectRatio / expectedAspectRatio this.videoTextureView.scaleY = 1 } else { this.videoTextureView.scaleY = expectedAspectRatio / realAspectRatio this.videoTextureView.scaleX = 1 } } catch (e) { Log.error('texImage2d video', e) this._stopUpdatingVideoTexture() this.videoTextureView.visible = false } this.videoTexture.source.forceRenderUpdate() } } } } if (!this._updatingVideoTexture) { stage.on('frameStart', this._updateVideoTexture) this._updatingVideoTexture = true } } } _stopUpdatingVideoTexture() { if (this.textureMode) { const stage = this.stage stage.removeListener('frameStart', this._updateVideoTexture) this._updatingVideoTexture = false this.videoTextureView.visible = false if (this.videoTexture.options.source) { const gl = stage.gl gl.bindTexture(gl.TEXTURE_2D, this.videoTexture.options.source) gl.clearColor(0, 0, 0, 1) gl.clear(gl.COLOR_BUFFER_BIT) } } } updateSettings(settings = {}) { // The Component that 'consumes' the media player. this._consumer = settings.consumer if (this._consumer && this._consumer.getMediaplayerSettings) { // Allow consumer to add settings. settings = Object.assign(settings, this._consumer.getMediaplayerSettings()) } if (!Lightning.Utils.equalValues(this._stream, settings.stream)) { if (settings.stream && settings.stream.keySystem) { navigator .requestMediaKeySystemAccess( settings.stream.keySystem.id, settings.stream.keySystem.config ) .then(keySystemAccess => { return keySystemAccess.createMediaKeys() }) .then(createdMediaKeys => { return this.videoEl.setMediaKeys(createdMediaKeys) }) .then(() => { if (settings.stream && settings.stream.src) this.open(settings.stream.src) }) .catch(() => { console.error('Failed to set up MediaKeys') }) } else if (settings.stream && settings.stream.src) { // This is here to be backwards compatible, will be removed // in future sdk release if (Settings.get('app', 'hls')) { if (!window.Hls) { window.Hls = class Hls { static isSupported() { console.warn('hls-light not included') return false } } } if (window.Hls.isSupported()) { if (!this._hls) this._hls = new window.Hls({ liveDurationInfinity: true }) this._hls.loadSource(settings.stream.src) this._hls.attachMedia(this.videoEl) this.videoEl.style.display = 'block' } } else { this.open(settings.stream.src) } } else { this.close() } this._stream = settings.stream } this._setHide(settings.hide) this._setVideoArea(settings.videoPos) } _setHide(hide) { if (this.textureMode) { this.tag('Video').setSmooth('alpha', hide ? 0 : 1) } else { this.videoEl.style.visibility = hide ? 'hidden' : 'visible' } } open(url, settings = { hide: false, videoPosition: null }) { // prep the media url to play depending on platform (mediaPlayerplugin) url = mediaUrl(url) this._metrics = Metrics.media(url) Log.info('Playing stream', url) if (this.application.noVideo) { Log.info('noVideo option set, so ignoring: ' + url) return } // close the video when opening same url as current (effectively reloading) if (this.videoEl.getAttribute('src') === url) { this.close() } this.videoEl.setAttribute('src', url) // force hide, then force show (in next tick!) // (fixes comcast playback rollover issue) this.videoEl.style.visibility = 'hidden' this.videoEl.style.display = 'none' setTimeout(() => { this.videoEl.style.display = 'block' this.videoEl.style.visibility = 'visible' }) this._setHide(settings.hide) this._setVideoArea(settings.videoPosition || [0, 0, 1920, 1080]) } close() { // We need to pause first in order to stop sound. this.videoEl.pause() this.videoEl.removeAttribute('src') // force load to reset everything without errors this.videoEl.load() this._clearSrc() this.videoEl.style.display = 'none' } playPause() { if (this.isPlaying()) { this.doPause() } else { this.doPlay() } } get muted() { return this.videoEl.muted } set muted(v) { this.videoEl.muted = v } get loop() { return this.videoEl.loop } set loop(v) { this.videoEl.loop = v } isPlaying() { return this._getState() === 'Playing' } doPlay() { this.videoEl.play() } doPause() { this.videoEl.pause() } reload() { var url = this.videoEl.getAttribute('src') this.close() this.videoEl.src = url } getPosition() { return Promise.resolve(this.videoEl.currentTime) } setPosition(pos) { this.videoEl.currentTime = pos } getDuration() { return Promise.resolve(this.videoEl.duration) } seek(time, absolute = false) { if (absolute) { this.videoEl.currentTime = time } else { this.videoEl.currentTime += time } } get videoTextureView() { return this.tag('Video').tag('VideoTexture') } get videoTexture() { return this.videoTextureView.texture } _setVideoArea(videoPos) { if (Lightning.Utils.equalValues(this._videoPos, videoPos)) { return } this._videoPos = videoPos if (this.textureMode) { this.videoTextureView.patch({ smooth: { x: videoPos[0], y: videoPos[1], w: videoPos[2] - videoPos[0], h: videoPos[3] - videoPos[1], }, }) } else { const precision = this.stage.getRenderPrecision() this.videoEl.style.left = Math.round(videoPos[0] * precision) + 'px' this.videoEl.style.top = Math.round(videoPos[1] * precision) + 'px' this.videoEl.style.width = Math.round((videoPos[2] - videoPos[0]) * precision) + 'px' this.videoEl.style.height = Math.round((videoPos[3] - videoPos[1]) * precision) + 'px' } } _fireConsumer(event, args) { if (this._consumer) { this._consumer.fire(event, args) } } _equalInitData(buf1, buf2) { if (!buf1 || !buf2) return false if (buf1.byteLength != buf2.byteLength) return false const dv1 = new Int8Array(buf1) const dv2 = new Int8Array(buf2) for (let i = 0; i != buf1.byteLength; i++) if (dv1[i] != dv2[i]) return false return true } error(args) { this._fireConsumer('$mediaplayerError', args) this._setState('') return '' } loadeddata(args) { this._fireConsumer('$mediaplayerLoadedData', args) } play(args) { this._fireConsumer('$mediaplayerPlay', args) } playing(args) { this._fireConsumer('$mediaplayerPlaying', args) this._setState('Playing') } canplay(args) { this.videoEl.play() this._fireConsumer('$mediaplayerStart', args) } loadstart(args) { this._fireConsumer('$mediaplayerLoad', args) } seeked() { this._fireConsumer('$mediaplayerSeeked', { currentTime: this.videoEl.currentTime, duration: this.videoEl.duration || 1, }) } seeking() { this._fireConsumer('$mediaplayerSeeking', { currentTime: this.videoEl.currentTime, duration: this.videoEl.duration || 1, }) } durationchange(args) { this._fireConsumer('$mediaplayerDurationChange', args) } encrypted(args) { const video = args.videoElement const event = args.event // FIXME: Double encrypted events need to be properly filtered by Gstreamer if (video.mediaKeys && !this._equalInitData(this._previousInitData, event.initData)) { this._previousInitData = event.initData this._fireConsumer('$mediaplayerEncrypted', args) } } static _states() { return [ class Playing extends this { $enter() { this._startUpdatingVideoTexture() } $exit() { this._stopUpdatingVideoTexture() } timeupdate() { this._fireConsumer('$mediaplayerProgress', { currentTime: this.videoEl.currentTime, duration: this.videoEl.duration || 1, }) } ended(args) { this._fireConsumer('$mediaplayerEnded', args) this._setState('') } pause(args) { this._fireConsumer('$mediaplayerPause', args) this._setState('Playing.Paused') } _clearSrc() { this._fireConsumer('$mediaplayerStop', {}) this._setState('') } static _states() { return [class Paused extends this {}] } }, ] } }