diff --git a/src/WebOsVideo/WebOsVideo.js b/src/WebOsVideo/WebOsVideo.js index b0a6803..6bfaa6a 100644 --- a/src/WebOsVideo/WebOsVideo.js +++ b/src/WebOsVideo/WebOsVideo.js @@ -1,25 +1,53 @@ +/* eslint-disable no-console */ var EventEmitter = require('eventemitter3'); var cloneDeep = require('lodash.clonedeep'); var deepFreeze = require('deep-freeze'); var ERROR = require('../error'); var getTracksData = require('../tracksData'); +/* ------------------------------------ + * Luna helper (robusto a permessi/assenza webOS) + * ------------------------------------ */ function luna(params, call, fail, method) { - if (call) params.onSuccess = call || function() {}; + params = params || {}; + if (call) params.onSuccess = call || function () {}; params.onFailure = function (result) { - // eslint-disable-next-line no-console - console.log('WebOS', (params.method || method) + ' [fail][' + result.errorCode + '] ' + result.errorText ); - - // eslint-disable-next-line no-console - console.log('fail result', JSON.stringify(result)); - - if (fail) fail(); + try { + console.log( + 'WebOS', + (params.method || method) + + ' [fail][' + + (result && result.errorCode) + + '] ' + + (result && result.errorText) + ); + console.log('fail result', JSON.stringify(result)); + } catch (_e) {} + if (fail) fail(result); }; - window.webOS.service.request(method || 'luna://com.webos.media', params); + try { + if ( + window && + window.webOS && + window.webOS.service && + typeof window.webOS.service.request === 'function' + ) { + window.webOS.service.request(method || 'luna://com.webos.media', params); + } else { + console.warn('webOS service not available; skipping luna call:', params.method || method); + if (fail) fail({ errorCode: -1, errorText: 'webOS.service.request not available' }); + } + } catch (e) { + console.warn('luna request threw:', e && e.message); + if (fail) fail({ errorCode: -2, errorText: 'Exception calling webOS.service.request' }); + } } +/* ------------------------------------ + * Colori sottotitoli: mapping Stremio -> WebOS + * ------------------------------------ */ var webOsColors = ['none', 'black', 'white', 'yellow', 'red', 'green', 'blue']; var stremioColors = { // rgba @@ -60,6 +88,9 @@ var stremioColors = { '#0000FF': 'blue' }; +/* ------------------------------------ + * Mapping posizioni/sizes sottotitoli + * ------------------------------------ */ function stremioSubOffsets(offset) { if (offset <= 0) { return -3; @@ -74,13 +105,12 @@ function stremioSubOffsets(offset) { } return false; } - function stremioSubSizes(size) { - // there is also: 0 (tiny) + // c'รจ anche 0 (tiny) if (size <= 100) { return 1; } else if (size <= 125) { - // not used because of 50% step + // non usato (step 50%) return 2; } else if (size <= 150) { return 3; @@ -90,49 +120,97 @@ function stremioSubSizes(size) { return false; } -var device = { +/* ------------------------------------ + * Base capabilities (verranno adattate per istanza) + * ------------------------------------ */ +var baseDeviceCaps = { unsupportedAudio: ['DTS', 'TRUEHD'], unsupportedSubs: ['HDMV/PGS', 'VOBSUB'] }; var fetchedDeviceInfo = false; -function retrieveDeviceInfo() { - if (fetchedDeviceInfo) { - return; - } - window.webOS.service.request('luna://com.webos.service.config', { - method: 'getConfigs', - parameters: { - 'configNames': [ - 'tv.model.edidType' - ] - }, - onSuccess: function (result) { - if (((result || {}).configs || {})['tv.model.edidType']) { - fetchedDeviceInfo = true; - var edidType = result.configs['tv.model.edidType'].toLowerCase(); - if (edidType.includes('dts')) { - device.unsupportedAudio = device.unsupportedAudio.filter(function(e) { - return e !== 'DTS'; - }); - } - if (edidType.includes('truehd')) { - device.unsupportedAudio = device.unsupportedAudio.filter(function(e) { - return e !== 'TRUEHD'; - }); +/* ------------------------------------ + * Normalizzazione label codec (evita falsi negativi) + * ------------------------------------ */ +function normCodec(name) { + var s = (name || '').toString().toUpperCase(); + if (!s) return ''; + if (s.indexOf('DTS') > -1) return 'DTS'; // include DTS, DTS-HD, DTS:X (gestito come famiglia) + if (s.indexOf('TRUEHD') > -1) return 'TRUEHD'; // include TrueHD/Atmos + if (s.indexOf('PGS') > -1) return 'HDMV/PGS'; + if (s.indexOf('VOBSUB') > -1) return 'VOBSUB'; + if (s.indexOf('AC-4') > -1 || s.indexOf('AC4') > -1) return 'AC4'; + if (s.indexOf('MPEG-H') > -1 || s.indexOf('MPEGH') > -1) return 'MPEG-H'; + return s; +} + +/* ------------------------------------ + * Rilevazione capability: EDID + deviceInfo (model/year) + * ------------------------------------ */ +function retrieveDeviceInfo(deviceCaps) { + if (fetchedDeviceInfo) return; + + // 1) EDID type (DTS / TRUEHD hint) + try { + window.webOS && + window.webOS.service && + window.webOS.service.request('luna://com.webos.service.config', { + method: 'getConfigs', + parameters: { configNames: ['tv.model.edidType'] }, + onSuccess: function (result) { + var edidType = + (((result || {}).configs || {})['tv.model.edidType'] || '').toLowerCase(); + if (edidType) { + fetchedDeviceInfo = true; + if (edidType.indexOf('dts') > -1) { + deviceCaps.unsupportedAudio = deviceCaps.unsupportedAudio.filter(function (e) { + return e !== 'DTS'; + }); + } + if (edidType.indexOf('truehd') > -1) { + deviceCaps.unsupportedAudio = deviceCaps.unsupportedAudio.filter(function (e) { + return e !== 'TRUEHD'; + }); + } + if (edidType.indexOf('ac-4') > -1 || edidType.indexOf('ac4') > -1) { + deviceCaps.unsupportedAudio = deviceCaps.unsupportedAudio.filter(function (e) { + return e !== 'AC4'; + }); + } + } + }, + onFailure: function (err) { + console.log('could not get deviceInfo (edidType)', err); } - } - }, - onFailure: function (err) { - // eslint-disable-next-line no-console - console.log('could not get deviceInfo', err); + }); + } catch (_e) { + console.log('EDID detection not available'); + } + + // 2) webOSTV.js deviceInfo: model/anno (best-effort) + try { + if (window.webOS && typeof window.webOS.deviceInfo === 'function') { + window.webOS.deviceInfo(function (dev) { + try { + var model = ((dev || {}).modelName || '').toUpperCase(); + // Esempio euristico: modelli 2024 noti (C4/G4/QNED di fascia alta) -> sblocca DTS + if (/C4|G4|M4|QNED9|QNED99|QNED95/.test(model)) { + deviceCaps.unsupportedAudio = deviceCaps.unsupportedAudio.filter(function (e) { + return e !== 'DTS'; + }); + } + } catch (_e) {} + }); } - }); + } catch (_e) { + console.log('webOS.deviceInfo not available'); + } } -function WebOsVideo(options) { +/* ========================================================================== */ +function WebOsVideo(options) { options = options || {}; var containerElement = options.containerElement; @@ -140,20 +218,24 @@ function WebOsVideo(options) { throw new Error('Container element required to be instance of HTMLElement'); } - var isLoaded = null; + // Opzione avanzata: abilita manualmente codec (edge-cases) + var forceEnable = new Set(options.forceEnableCodecs || []); + var deviceCaps = { + unsupportedAudio: baseDeviceCaps.unsupportedAudio.filter(function (c) { + return !forceEnable.has(c); + }), + unsupportedSubs: baseDeviceCaps.unsupportedSubs.filter(function (c) { + return !forceEnable.has(c); + }) + }; + var isLoaded = null; var subSize = 75; - var disabledSubs = true; - var currentSubTrack = false; - var currentAudioTrack = false; - var textTracks = []; - var audioTracks = []; - var _count_message = 0; var subStyles = { @@ -167,102 +249,123 @@ function WebOsVideo(options) { var toggleSubtitles = function (status) { if (!videoElement.mediaId) return; - disabledSubs = !status; - luna({ method: 'setSubtitleEnable', parameters: { - 'mediaId': videoElement.mediaId, - 'enable': status + mediaId: videoElement.mediaId, + enable: status } }); }; + // Stile base ::cue (per track WebVTT browser, non overlay nativo) var styleElement = document.createElement('style'); containerElement.appendChild(styleElement); - styleElement.sheet.insertRule('video::cue { font-size: 4vmin; color: rgb(255, 255, 255); background-color: rgba(0, 0, 0, 0); text-shadow: rgb(34, 34, 34) 1px 1px 0.1em; }'); + try { + styleElement.sheet.insertRule( + 'video::cue { font-size: 4vmin; color: rgb(255, 255, 255); background-color: rgba(0, 0, 0, 0); text-shadow: rgb(34, 34, 34) 1px 1px 0.1em; }' + ); + } catch (_e) {} + + // Elemento video var videoElement = document.createElement('video'); videoElement.style.width = '100%'; videoElement.style.height = '100%'; videoElement.style.backgroundColor = 'black'; - // videoElement.crossOrigin = 'anonymous'; videoElement.controls = false; - videoElement.onerror = function() { + + // Throttling per 'buffered' (riduce rumore eventi) + var lastBufferedEmit = 0; + var bufferedThrottleMs = 250; + function emitBufferedThrottled() { + var now = Date.now(); + if (now - lastBufferedEmit >= bufferedThrottleMs) { + lastBufferedEmit = now; + onPropChanged('buffered'); + } + } + + // Handler eventi HTML5 + videoElement.onerror = function () { onVideoError(); }; - videoElement.onended = function() { + videoElement.onended = function () { onEnded(); }; - videoElement.onpause = function() { + videoElement.onpause = function () { onPropChanged('paused'); }; - videoElement.onplay = function() { + videoElement.onplay = function () { onPropChanged('paused'); }; - videoElement.ontimeupdate = function() { + videoElement.ontimeupdate = function () { onPropChanged('time'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.ondurationchange = function() { + videoElement.ondurationchange = function () { onPropChanged('duration'); }; - videoElement.onwaiting = function() { + videoElement.onwaiting = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.onseeking = function() { + videoElement.onseeking = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.onseeked = function() { + videoElement.onseeked = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.onstalled = function() { + videoElement.onstalled = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.onplaying = function() { + videoElement.onplaying = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); if (!isLoaded) { isLoaded = true; onPropChanged('loaded'); } }; - videoElement.oncanplay = function() { + videoElement.oncanplay = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.canplaythrough = function() { + // BUGFIX: usare oncanplaythrough (non 'canplaythrough') + videoElement.oncanplaythrough = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.onloadeddata = function() { + videoElement.onloadeddata = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); }; - videoElement.onloadedmetadata = function() { + videoElement.onloadedmetadata = function () { onPropChanged('buffering'); - onPropChanged('buffered'); + emitBufferedThrottled(); setProp('time', startTime); }; - videoElement.onvolumechange = function() { + videoElement.onvolumechange = function () { onPropChanged('volume'); onPropChanged('muted'); }; - videoElement.onratechange = function() { + videoElement.onratechange = function () { onPropChanged('playbackSpeed'); }; - videoElement.textTracks.onchange = function() { - onPropChanged('subtitlesTracks'); - onPropChanged('selectedSubtitlesTrackId'); - onCueChange(); - Array.from(videoElement.textTracks).forEach(function(track) { - track.oncuechange = onCueChange; - }); - }; + + if (videoElement.textTracks) { + videoElement.textTracks.onchange = function () { + onPropChanged('subtitlesTracks'); + onPropChanged('selectedSubtitlesTrackId'); + onCueChange(); + Array.from(videoElement.textTracks).forEach(function (track) { + track.oncuechange = onCueChange; + }); + }; + } containerElement.appendChild(videoElement); var lastSubColor = null; @@ -303,7 +406,7 @@ function WebOsVideo(options) { function retrieveExtendedTracks() { if (!gotTraktData && stream !== null) { gotTraktData = true; - getTracksData(stream.url, function(resp) { + getTracksData(stream.url, function (resp) { var nrSubs = 0; var nrAudio = 0; textTracks = []; @@ -312,8 +415,9 @@ function WebOsVideo(options) { tracksData = resp; } if (((tracksData || {}).subs || []).length) { - tracksData.subs.forEach(function(track) { - if (device.unsupportedSubs.includes(track.codec || '')) { + tracksData.subs.forEach(function (track) { + var codec = normCodec(track.codec || ''); + if (deviceCaps.unsupportedSubs.indexOf(codec) > -1) { return; } var textTrackId = nrSubs; @@ -327,15 +431,17 @@ function WebOsVideo(options) { label: track.label || null, origin: 'EMBEDDED', embedded: true, - mode: textTrackId === currentSubTrack ? 'showing' : 'disabled', + codec: codec || null, + mode: textTrackId === currentSubTrack ? 'showing' : 'disabled' }); }); onPropChanged('subtitlesTracks'); onPropChanged('selectedSubtitlesTrackId'); } if (((tracksData || {}).audio || []).length) { - tracksData.audio.forEach(function(track) { - if (device.unsupportedAudio.includes(track.codec || '')) { + tracksData.audio.forEach(function (track) { + var codec = normCodec(track.codec || ''); + if (deviceCaps.unsupportedAudio.indexOf(codec) > -1) { return; } var audioTrackId = nrAudio; @@ -349,7 +455,8 @@ function WebOsVideo(options) { label: track.label || null, origin: 'EMBEDDED', embedded: true, - mode: audioTrackId === currentAudioTrack ? 'showing' : 'disabled', + codec: codec || null, + mode: audioTrackId === currentAudioTrack ? 'showing' : 'disabled' }); }); currentAudioTrack = 'EMBEDDED_0'; @@ -372,91 +479,82 @@ function WebOsVideo(options) { if (stream === null) { return null; } - return !!videoElement.paused; } case 'time': { - if (stream === null || videoElement.currentTime === null || !isFinite(videoElement.currentTime)) { + if ( + stream === null || + videoElement.currentTime === null || + !isFinite(videoElement.currentTime) + ) { return null; } - return Math.floor(videoElement.currentTime * 1000); } case 'duration': { - if (stream === null || videoElement.duration === null || !isFinite(videoElement.duration)) { + if ( + stream === null || + videoElement.duration === null || + !isFinite(videoElement.duration) + ) { return null; } - return Math.floor(videoElement.duration * 1000); } case 'buffering': { if (stream === null) { return null; } - return videoElement.readyState < videoElement.HAVE_FUTURE_DATA; } case 'buffered': { if (stream === null) { return null; } - - var time = videoElement.currentTime !== null && isFinite(videoElement.currentTime) ? videoElement.currentTime : 0; + var time = + videoElement.currentTime !== null && isFinite(videoElement.currentTime) + ? videoElement.currentTime + : 0; for (var i = 0; i < videoElement.buffered.length; i++) { - if (videoElement.buffered.start(i) <= time && time <= videoElement.buffered.end(i)) { + if ( + videoElement.buffered.start(i) <= time && + time <= videoElement.buffered.end(i) + ) { return Math.floor(videoElement.buffered.end(i) * 1000); } } - return Math.floor(time * 1000); } case 'subtitlesTracks': { if (stream === null) { return []; } - return textTracks; } case 'selectedSubtitlesTrackId': { if (stream === null || disabledSubs) { return null; } - return currentSubTrack; } case 'subtitlesOffset': { - if (destroyed) { - return null; - } - + if (destroyed) return null; return subtitlesOffset; } case 'subtitlesSize': { - if (destroyed) { - return null; - } - + if (destroyed) return null; return subSize; } case 'subtitlesTextColor': { - if (destroyed) { - return null; - } - + if (destroyed) return null; return lastSubColor || 'rgb(255, 255, 255)'; } case 'subtitlesBackgroundColor': { - if (destroyed) { - return null; - } - + if (destroyed) return null; return lastSubBgColor || 'rgba(0, 0, 0, 0)'; } case 'subtitlesOpacity': { - if (destroyed) { - return null; - } - + if (destroyed) return null; return subtitlesOpacity || 100; } case 'audioTracks': { @@ -469,21 +567,18 @@ function WebOsVideo(options) { if (destroyed || videoElement.volume === null || !isFinite(videoElement.volume)) { return null; } - return Math.floor(videoElement.volume * 100); } case 'muted': { if (destroyed) { return null; } - return !!videoElement.muted; } case 'playbackSpeed': { if (destroyed || lastPlaybackSpeed === null || !isFinite(lastPlaybackSpeed)) { return null; } - return lastPlaybackSpeed; } default: { @@ -491,45 +586,41 @@ function WebOsVideo(options) { } } } + function onCueChange() { - Array.from(videoElement.textTracks).forEach(function(track) { - Array.from(track.cues || []).forEach(function(cue) { + Array.from(videoElement.textTracks || []).forEach(function (track) { + Array.from(track.cues || []).forEach(function (cue) { cue.snapToLines = false; cue.line = 100 - subtitlesOffset; }); }); } - function onVideoError() { - if (destroyed) { - return; - } + function onVideoError() { + if (destroyed) return; var error; switch ((videoElement.error || {}).code) { - case 1: { + case 1: error = ERROR.HTML_VIDEO.MEDIA_ERR_ABORTED; break; - } - case 2: { + case 2: error = ERROR.HTML_VIDEO.MEDIA_ERR_NETWORK; break; - } - case 3: { + case 3: error = ERROR.HTML_VIDEO.MEDIA_ERR_DECODE; break; - } - case 4: { + case 4: error = ERROR.HTML_VIDEO.MEDIA_ERR_SRC_NOT_SUPPORTED; break; - } - default: { + default: error = ERROR.UNKNOWN_ERROR; - } } - onError(Object.assign({}, error, { - critical: true, - error: videoElement.error - })); + onError( + Object.assign({}, error, { + critical: true, + error: videoElement.error + }) + ); } function onError(error) { events.emit('error', error); @@ -546,31 +637,37 @@ function WebOsVideo(options) { } } function observeProp(propName) { - if (observedProps.hasOwnProperty(propName)) { + if (Object.prototype.hasOwnProperty.call(observedProps, propName)) { events.emit('propValue', propName, getProp(propName)); observedProps[propName] = true; } } + function setProp(propName, propValue) { switch (propName) { case 'paused': { if (stream !== null) { - propValue ? videoElement.pause() : videoElement.play(); + if (propValue) { + videoElement.pause(); + } else { + var p = videoElement.play(); + if (p && typeof p.catch === 'function') p.catch(function () {}); + } } - break; } case 'time': { - if (stream !== null && videoElement.readyState >= videoElement.HAVE_METADATA && propValue !== null && isFinite(propValue)) { + if ( + stream !== null && + videoElement.readyState >= videoElement.HAVE_METADATA && + propValue !== null && + isFinite(propValue) + ) { try { videoElement.currentTime = parseInt(propValue, 10) / 1000; onPropChanged('time'); - } catch(_e) { - // console.log('webos video change time error'); - // console.error(e); - } + } catch (_e) {} } - break; } case 'selectedSubtitlesTrackId': { @@ -587,7 +684,7 @@ function WebOsVideo(options) { 'setSubtitleFontSize', 'setSubtitleBackgroundOpacity', 'setSubtitleCharacterOpacity' - ].forEach(function(key) { + ].forEach(function (key) { luna({ method: key, parameters: { @@ -602,20 +699,24 @@ function WebOsVideo(options) { }); }); - // eslint-disable-next-line no-console - console.log('WebOS', 'change subtitles for id: ', videoElement.mediaId, ' index:', propValue); + console.log( + 'WebOS', + 'change subtitles for id: ', + videoElement.mediaId, + ' index:', + propValue + ); currentSubTrack = propValue; var trackIndex = parseInt(propValue.replace('EMBEDDED_', '')); - // eslint-disable-next-line no-console console.log('set subs to track idx: ' + trackIndex); - setTimeout(function() { - var successCb = function() { - var selectedSubtitlesTrack = getProp('subtitlesTracks') - .find(function(track) { - return track.id === propValue; - }); - textTracks = textTracks.map(function(track) { + + setTimeout(function () { + var successCb = function () { + var selectedSubtitlesTrack = getProp('subtitlesTracks').find(function (track) { + return track.id === propValue; + }); + textTracks = textTracks.map(function (track) { track.mode = track.id === currentSubTrack ? 'showing' : 'disabled'; return track; }); @@ -624,14 +725,18 @@ function WebOsVideo(options) { onPropChanged('selectedSubtitlesTrackId'); } }; - luna({ - method: 'selectTrack', - parameters: { - 'type': 'text', - 'mediaId': videoElement.mediaId, - 'index': trackIndex - } - }, successCb, successCb); + luna( + { + method: 'selectTrack', + parameters: { + type: 'text', + mediaId: videoElement.mediaId, + index: trackIndex + } + }, + successCb, + successCb + ); }, 500); } } @@ -641,59 +746,50 @@ function WebOsVideo(options) { onPropChanged('selectedSubtitlesTrackId'); toggleSubtitles(false); } - break; } case 'subtitlesOffset': { if (propValue !== null && isFinite(propValue)) { subtitlesOffset = propValue; - var nextOffset = stremioSubOffsets(Math.max(0, Math.min(100, parseInt(subtitlesOffset, 10)))); - if (nextOffset === false) { // use default - nextOffset = -2; - } + var nextOffset = stremioSubOffsets( + Math.max(0, Math.min(100, parseInt(subtitlesOffset, 10))) + ); + if (nextOffset === false) nextOffset = -2; subStyles.position = nextOffset; if (videoElement.mediaId) { luna({ method: 'setSubtitlePosition', parameters: { - 'mediaId': videoElement.mediaId, - 'position': nextOffset, + mediaId: videoElement.mediaId, + position: nextOffset } }); } - onPropChanged('subtitlesOffset'); } - break; } case 'subtitlesSize': { if (propValue !== null && isFinite(propValue)) { subSize = propValue; var nextSubSize = stremioSubSizes(Math.max(0, parseInt(subSize, 10))); - if (nextSubSize === false) { // use default - nextSubSize = 1; - } + if (nextSubSize === false) nextSubSize = 1; subStyles.font_size = nextSubSize; if (videoElement.mediaId) { luna({ method: 'setSubtitleFontSize', parameters: { - 'mediaId': videoElement.mediaId, - 'fontSize': nextSubSize, + mediaId: videoElement.mediaId, + fontSize: nextSubSize } }); } - onPropChanged('subtitlesSize'); } - break; } case 'subtitlesTextColor': { if (typeof propValue === 'string') { - // we use setSubtitleCharacterColor instead of setSubtitleColor - // because it has the same color options as the sub background var nextColor = 'white'; if (stremioColors[propValue] && webOsColors.indexOf(stremioColors[propValue]) > -1) { nextColor = stremioColors[propValue]; @@ -703,15 +799,14 @@ function WebOsVideo(options) { luna({ method: 'setSubtitleCharacterColor', parameters: { - 'mediaId': videoElement.mediaId, - 'charColor': nextColor, + mediaId: videoElement.mediaId, + charColor: nextColor } }); } lastSubColor = propValue; onPropChanged('subtitlesTextColor'); } - break; } case 'subtitlesBackgroundColor': { @@ -722,25 +817,19 @@ function WebOsVideo(options) { luna({ method: 'setSubtitleBackgroundColor', parameters: { - 'mediaId': videoElement.mediaId, - 'bgColor': stremioColors[propValue] === 'none' ? 'black' : stremioColors[propValue], + mediaId: videoElement.mediaId, + bgColor: stremioColors[propValue] === 'none' ? 'black' : stremioColors[propValue] } }); if (stremioColors[propValue] === 'none') { luna({ method: 'setSubtitleBackgroundOpacity', - parameters: { - 'mediaId': videoElement.mediaId, - 'bgOpacity': 0, - } + parameters: { mediaId: videoElement.mediaId, bgOpacity: 0 } }); } else { luna({ method: 'setSubtitleBackgroundOpacity', - parameters: { - 'mediaId': videoElement.mediaId, - 'bgOpacity': 255, - } + parameters: { mediaId: videoElement.mediaId, bgOpacity: 255 } }); } } @@ -748,70 +837,83 @@ function WebOsVideo(options) { lastSubBgColor = propValue; onPropChanged('subtitlesBackgroundColor'); } - break; } case 'subtitlesOpacity': { if (typeof propValue === 'number') { - var nextSubOpacity = Math.floor(propValue / 100 * 255); + var nextSubOpacity = Math.floor((propValue / 100) * 255); subStyles.char_opacity = nextSubOpacity; if (videoElement.mediaId) { luna({ method: 'setSubtitleCharacterOpacity', parameters: { - 'mediaId': videoElement.mediaId, - 'charOpacity': nextSubOpacity, + mediaId: videoElement.mediaId, + charOpacity: nextSubOpacity } }); } - subtitlesOpacity = propValue; onPropChanged('subtitlesOpacity'); } - break; } case 'selectedAudioTrackId': { if ((propValue || '').indexOf('EMBEDDED_') === 0) { currentAudioTrack = propValue; var trackIndex = parseInt(propValue.replace('EMBEDDED_', '')); + var targetTrack = audioTracks.find(function (t) { + return t.id === propValue; + }); + var targetCodec = normCodec((targetTrack && targetTrack.codec) || ''); if (videoElement.mediaId) { - luna({ - method: 'selectTrack', - parameters: { - 'type': 'audio', - 'mediaId': videoElement.mediaId, - 'index': trackIndex - } - }, function() { - var selectedAudioTrack = getProp('audioTracks') - .find(function(track) { + luna( + { + method: 'selectTrack', + parameters: { + type: 'audio', + mediaId: videoElement.mediaId, + index: trackIndex + } + }, + function () { + var selectedAudioTrack = getProp('audioTracks').find(function (track) { return track.id === propValue; }); - - audioTracks = audioTracks.map(function(track) { - track.mode = track.id === currentAudioTrack ? 'showing' : 'disabled'; - return track; - }); - - if (selectedAudioTrack) { - events.emit('audioTrackLoaded', selectedAudioTrack); - onPropChanged('selectedAudioTrackId'); + audioTracks = audioTracks.map(function (track) { + track.mode = track.id === currentAudioTrack ? 'showing' : 'disabled'; + return track; + }); + if (selectedAudioTrack) { + events.emit('audioTrackLoaded', selectedAudioTrack); + onPropChanged('selectedAudioTrackId'); + } + }, + function () { + // Fallimento: marca codec come unsupported e fallback + var nc = targetCodec || ''; + if (nc && deviceCaps.unsupportedAudio.indexOf(nc) === -1) { + deviceCaps.unsupportedAudio.push(nc); + } + var fallback = audioTracks.find(function (t) { + return deviceCaps.unsupportedAudio.indexOf(normCodec(t.codec || '')) === -1; + }); + if (fallback) { + setProp('selectedAudioTrackId', fallback.id); + } else { + console.log('No compatible audio track available after failure'); + } } - }); + ); } if (videoElement && videoElement.audioTracks) { for (var i = 0; i < videoElement.audioTracks.length; i++) { videoElement.audioTracks[i].enabled = false; } - - if(videoElement.audioTracks[trackIndex]) { + if (videoElement.audioTracks[trackIndex]) { videoElement.audioTracks[trackIndex].enabled = true; } } - } - break; } case 'volume': { @@ -819,7 +921,6 @@ function WebOsVideo(options) { videoElement.muted = false; videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue, 10))) / 100; } - break; } case 'muted': { @@ -829,34 +930,39 @@ function WebOsVideo(options) { case 'playbackSpeed': { if (propValue !== null && isFinite(propValue)) { lastPlaybackSpeed = parseFloat(propValue); - if (videoElement.mediaId) { + + // GATING: solo progressive (alcuni pipeline HLS non supportano rate != 1.0) + var isProgressive = + !!(stream && typeof stream.url === 'string') && + /^https?:.*\.(mp4|mkv|mov|m4v)(\?|#|$)/i.test(stream.url); + + if (videoElement.mediaId && isProgressive) { luna({ method: 'setPlayRate', parameters: { - 'mediaId': videoElement.mediaId, - 'playRate': lastPlaybackSpeed, - 'audioOutput': true, + mediaId: videoElement.mediaId, + playRate: lastPlaybackSpeed, + audioOutput: true } }); } onPropChanged('playbackSpeed'); } - break; } } } + function command(commandName, commandArgs) { switch (commandName) { case 'load': { - // not sure about this - // command('unload'); if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') { stream = commandArgs.stream; startTime = commandArgs.time; onPropChanged('stream'); - videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true; + videoElement.autoplay = + typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true; onPropChanged('loaded'); onPropChanged('paused'); @@ -870,22 +976,22 @@ function WebOsVideo(options) { onPropChanged('selectedAudioTrackId'); var count = 0; + var MAX_TRIES = 12; // ~3.6s (12 * 300ms) var initMediaId = function (cb) { function retrieveMediaId() { if (videoElement.mediaId) { clearInterval(timer); retrieveExtendedTracks(); - retrieveDeviceInfo(); + retrieveDeviceInfo(deviceCaps); cb(); return; } count++; - if (count > 4) { - // console.log('failed to get media id'); + if (count > MAX_TRIES) { clearInterval(timer); retrieveExtendedTracks(); - retrieveDeviceInfo(); + retrieveDeviceInfo(deviceCaps); cb(); } } @@ -893,52 +999,37 @@ function WebOsVideo(options) { }; var startVideo = function () { - // console.log('startVideo'); - // not needed? - // videoElement.src = stream.url; - try { videoElement.load(); - } catch(_e) { - // console.log('can\'t load video'); - // console.error(e); - } - + } catch (_e1) {} try { - // console.log('try play'); - videoElement.play(); - } catch(_e) { - // console.log('can\'t start video'); - // console.error(e); - } + var p = videoElement.play(); + if (p && typeof p.catch === 'function') p.catch(function () {}); + } catch (_e2) {} }; videoElement.src = stream.url; - initMediaId(startVideo); } else { - onError(Object.assign({}, ERROR.UNSUPPORTED_STREAM, { - critical: true, - stream: commandArgs ? commandArgs.stream : null - })); + onError( + Object.assign({}, ERROR.UNSUPPORTED_STREAM, { + critical: true, + stream: commandArgs ? commandArgs.stream : null + }) + ); } break; } case 'unload': { stream = null; startTime = null; - Array.from(videoElement.textTracks).forEach(function(track) { + Array.from(videoElement.textTracks || []).forEach(function (track) { track.oncuechange = null; }); videoElement.removeAttribute('src'); - videoElement.load(); - // not sure about this: - // try { - // videoElement.currentTime = 0; - // } catch(e) { - // console.log('webos video unload error'); - // console.error(e); - // } + try { + videoElement.load(); + } catch (_e) {} onPropChanged('stream'); onPropChanged('paused'); onPropChanged('time'); @@ -949,8 +1040,6 @@ function WebOsVideo(options) { onPropChanged('selectedSubtitlesTrackId'); onPropChanged('audioTracks'); onPropChanged('selectedAudioTrackId'); - // not sure about this: - // unload(function() {}); break; } case 'destroy': { @@ -977,30 +1066,26 @@ function WebOsVideo(options) { videoElement.onstalled = null; videoElement.onplaying = null; videoElement.oncanplay = null; - videoElement.canplaythrough = null; + videoElement.oncanplaythrough = null; videoElement.onloadeddata = null; videoElement.onloadedmetadata = null; videoElement.onvolumechange = null; videoElement.onratechange = null; - videoElement.textTracks.onchange = null; - containerElement.removeChild(videoElement); - containerElement.removeChild(styleElement); + if (videoElement.textTracks) videoElement.textTracks.onchange = null; + if (videoElement.parentNode === containerElement) containerElement.removeChild(videoElement); + if (styleElement.parentNode === containerElement) containerElement.removeChild(styleElement); break; } } } - this.on = function(eventName, listener) { - if (destroyed) { - throw new Error('Video is destroyed'); - } - + this.on = function (eventName, listener) { + if (destroyed) throw new Error('Video is destroyed'); events.on(eventName, listener); }; - this.dispatch = function(action) { - if (destroyed) { - throw new Error('Video is destroyed'); - } + + this.dispatch = function (action) { + if (destroyed) throw new Error('Video is destroyed'); if (action) { action = deepFreeze(cloneDeep(action)); @@ -1024,14 +1109,37 @@ function WebOsVideo(options) { }; } -WebOsVideo.canPlayStream = function() { // function(stream) +/* ------------------------------------ + * Static + * ------------------------------------ */ +WebOsVideo.canPlayStream = function () { return Promise.resolve(true); }; WebOsVideo.manifest = { name: 'WebOsVideo', external: false, - props: ['stream', 'loaded', 'paused', 'time', 'duration', 'buffering', 'buffered', 'audioTracks', 'selectedAudioTrackId', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesOffset', 'subtitlesSize', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOpacity', 'volume', 'muted', 'playbackSpeed'], + props: [ + 'stream', + 'loaded', + 'paused', + 'time', + 'duration', + 'buffering', + 'buffered', + 'audioTracks', + 'selectedAudioTrackId', + 'subtitlesTracks', + 'selectedSubtitlesTrackId', + 'subtitlesOffset', + 'subtitlesSize', + 'subtitlesTextColor', + 'subtitlesBackgroundColor', + 'subtitlesOpacity', + 'volume', + 'muted', + 'playbackSpeed' + ], commands: ['load', 'unload', 'destroy'], events: ['propValue', 'propChanged', 'ended', 'error', 'subtitlesTrackLoaded', 'audioTrackLoaded'] };