mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-20 04:18:19 -07:00
485 lines
15 KiB
JavaScript
485 lines
15 KiB
JavaScript
define(['appSettings', 'browser', 'events'], function (appSettings, browser, events) {
|
|
'use strict';
|
|
|
|
function getSavedVolume() {
|
|
return appSettings.get('volume') || 1;
|
|
}
|
|
|
|
function saveVolume(value) {
|
|
if (value) {
|
|
appSettings.set('volume', value);
|
|
}
|
|
}
|
|
|
|
function getCrossOriginValue(mediaSource) {
|
|
if (mediaSource.IsRemote) {
|
|
return null;
|
|
}
|
|
|
|
return 'anonymous';
|
|
}
|
|
|
|
function canPlayNativeHls() {
|
|
var media = document.createElement('video');
|
|
|
|
if (media.canPlayType('application/x-mpegURL').replace(/no/, '') ||
|
|
media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, '')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function enableHlsShakaPlayer(item, mediaSource, mediaType) {
|
|
/* eslint-disable-next-line compat/compat */
|
|
if (!!window.MediaSource && !!MediaSource.isTypeSupported) {
|
|
|
|
if (canPlayNativeHls()) {
|
|
|
|
if (browser.edge && mediaType === 'Video') {
|
|
return true;
|
|
}
|
|
|
|
// simple playback should use the native support
|
|
if (mediaSource.RunTimeTicks) {
|
|
//if (!browser.edge) {
|
|
//return false;
|
|
//}
|
|
}
|
|
|
|
//return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
var recoverDecodingErrorDate;
|
|
var recoverSwapAudioCodecDate;
|
|
function handleHlsJsMediaError(instance, reject) {
|
|
|
|
var hlsPlayer = instance._hlsPlayer;
|
|
|
|
if (!hlsPlayer) {
|
|
return;
|
|
}
|
|
|
|
var 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');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function onErrorInternal(instance, type) {
|
|
|
|
// Needed for video
|
|
if (instance.destroyCustomTrack) {
|
|
instance.destroyCustomTrack(instance._mediaElement);
|
|
}
|
|
|
|
events.trigger(instance, 'error', [
|
|
{
|
|
type: type
|
|
}
|
|
]);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
function seekOnPlaybackStart(instance, element, ticks, onMediaReady) {
|
|
|
|
var seconds = (ticks || 0) / 10000000;
|
|
|
|
if (seconds) {
|
|
var src = (instance.currentSrc() || '').toLowerCase();
|
|
|
|
// 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
|
|
var events = ['durationchange', 'loadeddata', 'play', 'loadedmetadata'];
|
|
var 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);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function applySrc(elem, src, options) {
|
|
|
|
if (window.Windows && options.mediaSource && options.mediaSource.IsLocal) {
|
|
|
|
return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) {
|
|
|
|
var playlist = new Windows.Media.Playback.MediaPlaybackList();
|
|
|
|
var source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file);
|
|
var 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);
|
|
}
|
|
|
|
function playWithPromise(elem, onErrorFn) {
|
|
|
|
try {
|
|
var promise = elem.play();
|
|
if (promise && promise.then) {
|
|
// Chrome now returns a promise
|
|
return promise.catch(function (e) {
|
|
|
|
var 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();
|
|
}
|
|
}
|
|
|
|
function destroyCastPlayer(instance) {
|
|
|
|
var player = instance._castPlayer;
|
|
if (player) {
|
|
try {
|
|
player.unload();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
instance._castPlayer = null;
|
|
}
|
|
}
|
|
|
|
function destroyShakaPlayer(instance) {
|
|
var player = instance._shakaPlayer;
|
|
if (player) {
|
|
try {
|
|
player.destroy();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
instance._shakaPlayer = null;
|
|
}
|
|
}
|
|
|
|
function destroyHlsPlayer(instance) {
|
|
var player = instance._hlsPlayer;
|
|
if (player) {
|
|
try {
|
|
player.destroy();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
instance._hlsPlayer = null;
|
|
}
|
|
}
|
|
|
|
function destroyFlvPlayer(instance) {
|
|
var player = instance._flvPlayer;
|
|
if (player) {
|
|
try {
|
|
player.unload();
|
|
player.detachMediaElement();
|
|
player.destroy();
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
|
|
instance._flvPlayer = null;
|
|
}
|
|
}
|
|
|
|
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');
|
|
var currentReject = reject;
|
|
reject = null;
|
|
handleHlsJsMediaError(instance, currentReject);
|
|
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;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function onEndedInternal(instance, elem, onErrorFn) {
|
|
|
|
elem.removeEventListener('error', onErrorFn);
|
|
|
|
elem.src = '';
|
|
elem.innerHTML = '';
|
|
elem.removeAttribute('src');
|
|
|
|
destroyHlsPlayer(instance);
|
|
destroyFlvPlayer(instance);
|
|
destroyShakaPlayer(instance);
|
|
destroyCastPlayer(instance);
|
|
|
|
var stopInfo = {
|
|
src: instance._currentSrc
|
|
};
|
|
|
|
events.trigger(instance, 'stopped', [stopInfo]);
|
|
|
|
instance._currentTime = null;
|
|
instance._currentSrc = null;
|
|
instance._currentPlayOptions = null;
|
|
}
|
|
|
|
function getBufferedRanges(instance, elem) {
|
|
|
|
var ranges = [];
|
|
var seekable = elem.buffered || [];
|
|
|
|
var offset;
|
|
var currentPlayOptions = instance._currentPlayOptions;
|
|
if (currentPlayOptions) {
|
|
offset = currentPlayOptions.transcodingOffsetTicks;
|
|
}
|
|
|
|
offset = offset || 0;
|
|
|
|
for (var i = 0, length = seekable.length; i < length; i++) {
|
|
|
|
var start = seekable.start(i);
|
|
var 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;
|
|
}
|
|
|
|
return {
|
|
getSavedVolume: getSavedVolume,
|
|
saveVolume: saveVolume,
|
|
enableHlsJsPlayer: enableHlsJsPlayer,
|
|
enableHlsShakaPlayer: enableHlsShakaPlayer,
|
|
handleHlsJsMediaError: handleHlsJsMediaError,
|
|
isValidDuration: isValidDuration,
|
|
onErrorInternal: onErrorInternal,
|
|
seekOnPlaybackStart: seekOnPlaybackStart,
|
|
applySrc: applySrc,
|
|
playWithPromise: playWithPromise,
|
|
destroyHlsPlayer: destroyHlsPlayer,
|
|
destroyFlvPlayer: destroyFlvPlayer,
|
|
destroyCastPlayer: destroyCastPlayer,
|
|
bindEventsToHlsPlayer: bindEventsToHlsPlayer,
|
|
onEndedInternal: onEndedInternal,
|
|
getCrossOriginValue: getCrossOriginValue,
|
|
getBufferedRanges: getBufferedRanges
|
|
};
|
|
});
|