1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00
jellyfin-web/src/components/htmlMediaHelper.js
2023-03-29 00:38:22 -04:00

395 lines
13 KiB
JavaScript

/* eslint-disable indent */
import appSettings from '../scripts/settings/appSettings' ;
import browser from '../scripts/browser';
import Events from '../utils/events.ts';
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');
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '')
|| media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, ''));
}
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) {
return duration
&& !isNaN(duration)
&& duration !== Number.POSITIVE_INFINITY
&& duration !== Number.NEGATIVE_INFINITY;
}
function setCurrentTimeIfNeeded(element, seconds) {
// If it's worth skipping (1 sec or less of a difference)
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.forEach(name => {
element.removeEventListener(name, onMediaChange);
});
if (onMediaReady) onMediaReady();
}
};
events.forEach(name => {
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();
}
export function resetSrc(elem) {
elem.src = '';
elem.innerHTML = '';
elem.removeAttribute('src');
}
function onSuccessfulPlay(elem, onErrorFn) {
elem.addEventListener('error', onErrorFn);
}
export function playWithPromise(elem, onErrorFn) {
try {
return elem.play()
.catch((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
return Promise.resolve();
}
return Promise.reject();
})
.then(() => {
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));
// try to recover network error
if (data.type === Hls.ErrorTypes.NETWORK_ERROR
&& 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;
}
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);
resetSrc(elem);
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 */