/* eslint-disable indent */ import appSettings from 'appSettings' ; import browser from 'browser'; import events from 'events'; export function getSavedVolume() { return appSettings.get('volume') || 1; } export function saveVolume(value) { if (value) { appSettings.set('volume', value); } } export function getCrossOriginValue(mediaSource) { if (mediaSource.IsRemote) { return null; } return 'anonymous'; } function canPlayNativeHls() { const media = document.createElement('video'); if (media.canPlayType('application/x-mpegURL').replace(/no/, '') || media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, '')) { return true; } return false; } export function enableHlsJsPlayer(runTimeTicks, mediaType) { if (window.MediaSource == null) { return false; } // hls.js is only in beta. needs more testing. if (browser.iOS) { return false; } // The native players on these devices support seeking live streams, no need to use hls.js here if (browser.tizen || browser.web0s) { return false; } if (canPlayNativeHls()) { // Having trouble with chrome's native support and transcoded music if (browser.android && mediaType === 'Audio') { return true; } if (browser.edge && mediaType === 'Video') { //return true; } // simple playback should use the native support if (runTimeTicks) { //if (!browser.edge) { return false; //} } //return false; } return true; } let recoverDecodingErrorDate; let recoverSwapAudioCodecDate; export function handleHlsJsMediaError(instance, reject) { const hlsPlayer = instance._hlsPlayer; if (!hlsPlayer) { return; } let now = Date.now(); if (window.performance && window.performance.now) { now = performance.now(); // eslint-disable-line compat/compat } if (!recoverDecodingErrorDate || (now - recoverDecodingErrorDate) > 3000) { recoverDecodingErrorDate = now; console.debug('try to recover media Error ...'); hlsPlayer.recoverMediaError(); } else { if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) { recoverSwapAudioCodecDate = now; console.debug('try to swap Audio Codec and recover media Error ...'); hlsPlayer.swapAudioCodec(); hlsPlayer.recoverMediaError(); } else { console.error('cannot recover, last media error recovery failed ...'); if (reject) { reject(); } else { onErrorInternal(instance, 'mediadecodeerror'); } } } } export function onErrorInternal(instance, type) { // Needed for video if (instance.destroyCustomTrack) { instance.destroyCustomTrack(instance._mediaElement); } events.trigger(instance, 'error', [ { type: type } ]); } export function isValidDuration(duration) { if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) { return true; } return false; } function setCurrentTimeIfNeeded(element, seconds) { if (Math.abs(element.currentTime || 0, seconds) <= 1) { element.currentTime = seconds; } } export function seekOnPlaybackStart(instance, element, ticks, onMediaReady) { const seconds = (ticks || 0) / 10000000; if (seconds) { // Appending #t=xxx to the query string doesn't seem to work with HLS // For plain video files, not all browsers support it either if (element.duration >= seconds) { // media is ready, seek immediately setCurrentTimeIfNeeded(element, seconds); if (onMediaReady) onMediaReady(); } else { // update video player position when media is ready to be sought const events = ['durationchange', 'loadeddata', 'play', 'loadedmetadata']; const onMediaChange = function(e) { if (element.currentTime === 0 && element.duration >= seconds) { // seek only when video position is exactly zero, // as this is true only if video hasn't started yet or // user rewound to the very beginning // (but rewinding cannot happen as the first event with media of non-empty duration) console.debug(`seeking to ${seconds} on ${e.type} event`); setCurrentTimeIfNeeded(element, seconds); events.map(function(name) { element.removeEventListener(name, onMediaChange); }); if (onMediaReady) onMediaReady(); } }; events.map(function (name) { return element.addEventListener(name, onMediaChange); }); } } } export function applySrc(elem, src, options) { if (window.Windows && options.mediaSource && options.mediaSource.IsLocal) { return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) { const playlist = new Windows.Media.Playback.MediaPlaybackList(); const source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file); const startTime = (options.playerStartPositionTicks || 0) / 10000; playlist.items.append(new Windows.Media.Playback.MediaPlaybackItem(source1, startTime)); elem.src = URL.createObjectURL(playlist, { oneTimeOnly: true }); return Promise.resolve(); }); } else { elem.src = src; } return Promise.resolve(); } function onSuccessfulPlay(elem, onErrorFn) { elem.addEventListener('error', onErrorFn); } export function playWithPromise(elem, onErrorFn) { try { const promise = elem.play(); if (promise && promise.then) { // Chrome now returns a promise return promise.catch(function (e) { const errorName = (e.name || '').toLowerCase(); // safari uses aborterror if (errorName === 'notallowederror' || errorName === 'aborterror') { // swallow this error because the user can still click the play button on the video element onSuccessfulPlay(elem, onErrorFn); return Promise.resolve(); } return Promise.reject(); }); } else { onSuccessfulPlay(elem, onErrorFn); return Promise.resolve(); } } catch (err) { console.error('error calling video.play: ' + err); return Promise.reject(); } } export function destroyCastPlayer(instance) { const player = instance._castPlayer; if (player) { try { player.unload(); } catch (err) { console.error(err); } instance._castPlayer = null; } } export function destroyHlsPlayer(instance) { const player = instance._hlsPlayer; if (player) { try { player.destroy(); } catch (err) { console.error(err); } instance._hlsPlayer = null; } } export function destroyFlvPlayer(instance) { const player = instance._flvPlayer; if (player) { try { player.unload(); player.detachMediaElement(); player.destroy(); } catch (err) { console.error(err); } instance._flvPlayer = null; } } export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) { hls.on(Hls.Events.MANIFEST_PARSED, function () { playWithPromise(elem, onErrorFn).then(resolve, function () { if (reject) { reject(); reject = null; } }); }); hls.on(Hls.Events.ERROR, function (event, data) { console.error('HLS Error: Type: ' + data.type + ' Details: ' + (data.details || '') + ' Fatal: ' + (data.fatal || false)); switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: // try to recover network error if (data.response && data.response.code && data.response.code >= 400) { console.debug('hls.js response error code: ' + data.response.code); // Trigger failure differently depending on whether this is prior to start of playback, or after hls.destroy(); if (reject) { reject('servererror'); reject = null; } else { onErrorInternal(instance, 'servererror'); } return; } break; default: break; } if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: if (data.response && data.response.code === 0) { // This could be a CORS error related to access control response headers console.debug('hls.js response error code: ' + data.response.code); // Trigger failure differently depending on whether this is prior to start of playback, or after hls.destroy(); if (reject) { reject('network'); reject = null; } else { onErrorInternal(instance, 'network'); } } else { console.debug('fatal network error encountered, try to recover'); hls.startLoad(); } break; case Hls.ErrorTypes.MEDIA_ERROR: console.debug('fatal media error encountered, try to recover'); handleHlsJsMediaError(instance, reject); reject = null; break; default: console.debug('Cannot recover from hls error - destroy and trigger error'); // cannot recover // Trigger failure differently depending on whether this is prior to start of playback, or after hls.destroy(); if (reject) { reject(); reject = null; } else { onErrorInternal(instance, 'mediadecodeerror'); } break; } } }); } export function onEndedInternal(instance, elem, onErrorFn) { elem.removeEventListener('error', onErrorFn); elem.src = ''; elem.innerHTML = ''; elem.removeAttribute('src'); destroyHlsPlayer(instance); destroyFlvPlayer(instance); destroyCastPlayer(instance); const stopInfo = { src: instance._currentSrc }; events.trigger(instance, 'stopped', [stopInfo]); instance._currentTime = null; instance._currentSrc = null; instance._currentPlayOptions = null; } export function getBufferedRanges(instance, elem) { const ranges = []; const seekable = elem.buffered || []; let offset; const currentPlayOptions = instance._currentPlayOptions; if (currentPlayOptions) { offset = currentPlayOptions.transcodingOffsetTicks; } offset = offset || 0; for (let i = 0, length = seekable.length; i < length; i++) { let start = seekable.start(i); let end = seekable.end(i); if (!isValidDuration(start)) { start = 0; } if (!isValidDuration(end)) { end = 0; continue; } ranges.push({ start: (start * 10000000) + offset, end: (end * 10000000) + offset }); } return ranges; } /* eslint-enable indent */