1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00
jellyfin-web/src/plugins/htmlVideoPlayer/plugin.js

1939 lines
62 KiB
JavaScript
Raw Normal View History

2020-08-16 20:24:45 +02:00
import browser from '../../scripts/browser';
import { appHost } from '../../components/apphost';
2020-08-14 08:46:34 +02:00
import loading from '../../components/loading/loading';
2020-08-16 20:24:45 +02:00
import dom from '../../scripts/dom';
import { playbackManager } from '../../components/playback/playbackmanager';
import { appRouter } from '../../components/appRouter';
2020-07-26 14:18:34 +02:00
import {
bindEventsToHlsPlayer,
destroyHlsPlayer,
destroyFlvPlayer,
destroyCastPlayer,
getCrossOriginValue,
enableHlsJsPlayer,
applySrc,
2021-09-17 01:20:58 +03:00
resetSrc,
2020-07-26 14:18:34 +02:00
playWithPromise,
onEndedInternal,
saveVolume,
seekOnPlaybackStart,
onErrorInternal,
handleHlsJsMediaError,
getSavedVolume,
isValidDuration,
getBufferedRanges
2020-08-16 20:24:45 +02:00
} from '../../components/htmlMediaHelper';
import itemHelper from '../../components/itemHelper';
import Screenfull from 'screenfull';
import globalize from '../../scripts/globalize';
import ServerConnections from '../../components/ServerConnections';
import profileBuilder, { canPlaySecondaryAudio } from '../../scripts/browserDeviceProfile';
import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings';
import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/backdrop/backdrop';
import Events from '../../utils/events.ts';
import { includesAny } from '../../utils/container.ts';
/**
* Returns resolved URL.
* @param {string} url - URL.
* @returns {string} Resolved URL or `url` if resolving failed.
*/
function resolveUrl(url) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('HEAD', url, true);
xhr.onload = function () {
resolve(xhr.responseURL || url);
};
xhr.onerror = function (e) {
console.error(e);
resolve(url);
};
xhr.send(null);
});
}
/* eslint-disable indent */
function tryRemoveElement(elem) {
const parentNode = elem.parentNode;
if (parentNode) {
// Seeing crashes in edge webview
try {
parentNode.removeChild(elem);
} catch (err) {
2020-07-22 22:59:27 +02:00
console.error(`error removing dialog element: ${err}`);
}
}
}
2018-10-23 01:05:09 +03:00
function enableNativeTrackSupport(currentSrc, track) {
2022-10-03 14:22:02 -04:00
if (track?.DeliveryMethod === 'Embed') {
return true;
}
2022-10-03 14:22:02 -04:00
if (browser.firefox && (currentSrc || '').toLowerCase().includes('.m3u8')) {
return false;
}
if (browser.ps4) {
return false;
}
if (browser.web0s) {
return false;
}
// Edge is randomly not rendering subtitles
if (browser.edge) {
return false;
}
2022-10-03 14:22:02 -04:00
if (browser.iOS && (browser.iosVersion || 10) < 10) {
// works in the browser but not the native app
2022-10-03 14:22:02 -04:00
return false;
}
2018-10-23 01:05:09 +03:00
if (track) {
2020-07-21 22:22:16 +02:00
const format = (track.Codec || '').toLowerCase();
if (format === 'ssa' || format === 'ass') {
return false;
}
2018-10-23 01:05:09 +03:00
}
return true;
2018-10-23 01:05:09 +03:00
}
function requireHlsPlayer(callback) {
2020-08-16 20:24:45 +02:00
import('hls.js').then(({default: hls}) => {
window.Hls = hls;
callback();
});
2018-10-23 01:05:09 +03:00
}
function getMediaStreamAudioTracks(mediaSource) {
return mediaSource.MediaStreams.filter(function (s) {
return s.Type === 'Audio';
});
2018-10-23 01:05:09 +03:00
}
function getMediaStreamTextTracks(mediaSource) {
return mediaSource.MediaStreams.filter(function (s) {
return s.Type === 'Subtitle';
});
2018-10-23 01:05:09 +03:00
}
function zoomIn(elem) {
2020-07-27 19:58:57 +02:00
return new Promise(resolve => {
const duration = 240;
elem.style.animation = `htmlvideoplayer-zoomin ${duration}ms ease-in normal`;
dom.addEventListener(elem, dom.whichAnimationEvent(), resolve, {
once: true
});
});
2018-10-23 01:05:09 +03:00
}
function normalizeTrackEventText(text, useHtml) {
2020-07-21 22:22:16 +02:00
const result = text.replace(/\\N/gi, '\n').replace(/\r/gi, '');
return useHtml ? result.replace(/\n/gi, '<br>') : result;
2018-10-23 01:05:09 +03:00
}
function getTextTrackUrl(track, item, format) {
if (itemHelper.isLocalItem(item) && track.Path) {
return track.Path;
}
let url = playbackManager.getSubtitleUrl(track, item.ServerId);
if (format) {
url = url.replace('.vtt', format);
}
return url;
2018-10-23 01:05:09 +03:00
}
function getDefaultProfile() {
2020-10-18 13:53:12 +01:00
return profileBuilder({});
2018-10-23 01:05:09 +03:00
}
export class HtmlVideoPlayer {
/**
* @type {string}
*/
2020-08-01 04:07:00 +02:00
name;
/**
* @type {string}
*/
type = 'mediaplayer';
/**
* @type {string}
*/
id = 'htmlvideoplayer';
/**
* Let any players created by plugins take priority
*
* @type {number}
*/
priority = 1;
/**
* @type {boolean}
*/
isFetching = false;
/**
* @type {HTMLDivElement | null | undefined}
*/
#videoDialog;
/**
* @type {number | undefined}
*/
#subtitleTrackIndexToSetOnPlaying;
/**
* @type {number | null}
*/
#audioTrackIndexToSetOnPlaying;
/**
* @type {null | undefined}
*/
#currentClock;
/**
* @type {any | null | undefined}
*/
#currentSubtitlesOctopus;
/**
* @type {null | undefined}
*/
#currentAssRenderer;
/**
* @type {number | undefined}
*/
#customTrackIndex;
/**
* @type {boolean | undefined}
*/
#showTrackOffset;
/**
* @type {number | undefined}
*/
#currentTrackOffset;
/**
* @type {HTMLElement | null | undefined}
*/
#videoSubtitlesElem;
/**
* @type {any | null | undefined}
*/
#currentTrackEvents;
/**
* @type {string[] | undefined}
*/
#supportedFeatures;
/**
* @type {HTMLVideoElement | null | undefined}
*/
#mediaElement;
/**
* @type {number}
*/
#fetchQueue = 0;
/**
* @type {string | undefined}
*/
#currentSrc;
/**
* @type {boolean | undefined}
*/
#started;
/**
* @type {boolean | undefined}
*/
#timeUpdated;
/**
* @type {number | null | undefined}
*/
#currentTime;
/**
* @type {any | undefined}
*/
#flvPlayer;
/**
* @private (used in other files)
* @type {any | undefined}
*/
_hlsPlayer;
/**
* @private (used in other files)
* @type {any | null | undefined}
*/
_castPlayer;
/**
* @private (used in other files)
* @type {any | undefined}
*/
_currentPlayOptions;
/**
* @type {any | undefined}
*/
#lastProfile;
constructor() {
if (browser.edgeUwp) {
this.name = 'Windows Video Player';
} else {
this.name = 'Html Video Player';
}
}
currentSrc() {
return this.#currentSrc;
}
2019-09-12 21:24:16 +02:00
/**
* @private
*/
incrementFetchQueue() {
if (this.#fetchQueue <= 0) {
this.isFetching = true;
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'beginFetch');
2019-09-12 21:24:16 +02:00
}
this.#fetchQueue++;
2019-09-12 21:24:16 +02:00
}
/**
* @private
*/
decrementFetchQueue() {
this.#fetchQueue--;
2019-09-12 21:24:16 +02:00
if (this.#fetchQueue <= 0) {
this.isFetching = false;
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'endFetch');
2019-09-12 21:24:16 +02:00
}
}
/**
* @private
*/
updateVideoUrl(streamInfo) {
2020-07-21 22:22:16 +02:00
const isHls = streamInfo.url.toLowerCase().includes('.m3u8');
const mediaSource = streamInfo.mediaSource;
const item = streamInfo.item;
// Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts
// This will start the transcoding process before actually feeding the video url into the player
// Edit: Also seeing stalls from hls.js
if (mediaSource && item && !mediaSource.RunTimeTicks && isHls && streamInfo.playMethod === 'Transcode' && (browser.iOS || browser.osx)) {
2020-07-21 22:22:16 +02:00
const hlsPlaylistUrl = streamInfo.url.replace('master.m3u8', 'live.m3u8');
loading.show();
console.debug(`prefetching hls playlist: ${hlsPlaylistUrl}`);
return ServerConnections.getApiClient(item.ServerId).ajax({
type: 'GET',
2018-10-23 01:05:09 +03:00
url: hlsPlaylistUrl
}).then(function () {
console.debug(`completed prefetching hls playlist: ${hlsPlaylistUrl}`);
loading.hide();
streamInfo.url = hlsPlaylistUrl;
}, function () {
console.error(`error prefetching hls playlist: ${hlsPlaylistUrl}`);
loading.hide();
});
} else {
return Promise.resolve();
2018-10-23 01:05:09 +03:00
}
}
play(options) {
this.#started = false;
this.#timeUpdated = false;
this.#currentTime = null;
this.resetSubtitleOffset();
return this.createMediaElement(options).then(elem => {
return this.updateVideoUrl(options).then(() => {
return this.setCurrentSrc(elem, options);
});
});
}
/**
* @private
*/
setSrcWithFlvJs(elem, options, url) {
2020-08-16 20:24:45 +02:00
return import('flv.js').then(({default: flvjs}) => {
const flvPlayer = flvjs.createPlayer({
type: 'flv',
2018-10-23 01:05:09 +03:00
url: url
},
{
seekType: 'range',
lazyLoad: false
});
flvPlayer.attachMediaElement(elem);
flvPlayer.load();
this.#flvPlayer = flvPlayer;
// This is needed in setCurrentTrackElement
this.#currentSrc = url;
return flvPlayer.play();
});
2018-10-23 01:05:09 +03:00
}
/**
* @private
*/
setSrcWithHlsJs(elem, options, url) {
return new Promise((resolve, reject) => {
requireHlsPlayer(async () => {
let maxBufferLength = 30;
2020-12-12 16:44:57 +01:00
// Some browsers cannot handle huge fragments in high bitrate.
// This issue usually happens when using HWA encoders with a high bitrate setting.
// Limit the BufferLength to 6s, it works fine when playing 4k 120Mbps over HLS on chrome.
// https://github.com/video-dev/hls.js/issues/876
2020-12-12 16:44:57 +01:00
if ((browser.chrome || browser.edgeChromium || browser.firefox) && playbackManager.getMaxStreamingBitrate(this) >= 25000000) {
maxBufferLength = 6;
}
const includeCorsCredentials = await getIncludeCorsCredentials();
const hls = new Hls({
manifestLoadingTimeOut: 20000,
maxBufferLength: maxBufferLength,
xhrSetup(xhr) {
xhr.withCredentials = includeCorsCredentials;
}
2018-10-23 01:05:09 +03:00
});
hls.loadSource(url);
hls.attachMedia(elem);
2020-07-26 14:18:34 +02:00
bindEventsToHlsPlayer(this, hls, elem, this.onError, resolve, reject);
this._hlsPlayer = hls;
// This is needed in setCurrentTrackElement
this.#currentSrc = url;
});
});
}
/**
* @private
*/
async setCurrentSrc(elem, options) {
2020-05-27 00:38:01 +02:00
elem.removeEventListener('error', this.onError);
let val = options.url;
console.debug(`playing url: ${val}`);
// Convert to seconds
const seconds = (options.playerStartPositionTicks || 0) / 10000000;
if (seconds) {
val += `#t=${seconds}`;
}
2020-07-26 14:18:34 +02:00
destroyHlsPlayer(this);
destroyFlvPlayer(this);
destroyCastPlayer(this);
this.#subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex;
if (this.#subtitleTrackIndexToSetOnPlaying != null && this.#subtitleTrackIndexToSetOnPlaying >= 0) {
const initialSubtitleStream = options.mediaSource.MediaStreams[this.#subtitleTrackIndexToSetOnPlaying];
if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') {
this.#subtitleTrackIndexToSetOnPlaying = -1;
}
2018-10-23 01:05:09 +03:00
}
this.#audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex;
this._currentPlayOptions = options;
2020-07-26 14:18:34 +02:00
const crossOrigin = getCrossOriginValue(options.mediaSource);
if (crossOrigin) {
elem.crossOrigin = crossOrigin;
}
2018-10-23 01:05:09 +03:00
if (enableHlsJsPlayer(options.mediaSource.RunTimeTicks, 'Video') && val.includes('.m3u8')) {
return this.setSrcWithHlsJs(elem, options, val);
} else if (options.playMethod !== 'Transcode' && options.mediaSource.Container === 'flv') {
return this.setSrcWithFlvJs(elem, options, val);
} else {
elem.autoplay = true;
const includeCorsCredentials = await getIncludeCorsCredentials();
if (includeCorsCredentials) {
// Safari will not send cookies without this
elem.crossOrigin = 'use-credentials';
}
2020-07-26 14:18:34 +02:00
return applySrc(elem, val, options).then(() => {
this.#currentSrc = val;
2020-07-26 14:18:34 +02:00
return playWithPromise(elem, this.onError);
});
2018-10-23 01:05:09 +03:00
}
}
setSubtitleStreamIndex(index) {
this.setCurrentTrackElement(index);
}
2018-10-23 01:05:09 +03:00
resetSubtitleOffset() {
this.#currentTrackOffset = 0;
this.#showTrackOffset = false;
}
enableShowingSubtitleOffset() {
this.#showTrackOffset = true;
}
2019-04-08 20:21:16 +02:00
disableShowingSubtitleOffset() {
this.#showTrackOffset = false;
}
2019-04-08 20:21:16 +02:00
isShowingSubtitleOffsetEnabled() {
return this.#showTrackOffset;
}
2019-04-08 20:21:16 +02:00
/**
* @private
*/
getTextTrack() {
const videoElement = this.#mediaElement;
if (videoElement) {
return Array.from(videoElement.textTracks)
.find(function (trackElement) {
2019-12-09 21:37:05 +01:00
// get showing .vtt textTack
return trackElement.mode === 'showing';
});
} else {
return null;
}
}
/**
* @private
*/
setSubtitleOffset(offset) {
const offsetValue = parseFloat(offset);
2019-10-13 14:10:51 +02:00
// if .ass currently rendering
if (this.#currentSubtitlesOctopus) {
this.updateCurrentTrackOffset(offsetValue);
this.#currentSubtitlesOctopus.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + offsetValue;
2019-10-13 14:10:51 +02:00
} else {
const trackElement = this.getTextTrack();
// if .vtt currently rendering
if (trackElement) {
this.setTextTrackSubtitleOffset(trackElement, offsetValue);
} else if (this.#currentTrackEvents) {
this.setTrackEventsSubtitleOffset(this.#currentTrackEvents, offsetValue);
} else {
2020-05-04 12:44:12 +02:00
console.debug('No available track, cannot apply offset: ', offsetValue);
}
2019-10-13 14:10:51 +02:00
}
}
/**
* @private
*/
updateCurrentTrackOffset(offsetValue) {
let relativeOffset = offsetValue;
const newTrackOffset = offsetValue;
if (this.#currentTrackOffset) {
relativeOffset -= this.#currentTrackOffset;
}
this.#currentTrackOffset = newTrackOffset;
// relative to currentTrackOffset
return relativeOffset;
}
/**
* @private
*/
setTextTrackSubtitleOffset(currentTrack, offsetValue) {
if (currentTrack.cues) {
offsetValue = this.updateCurrentTrackOffset(offsetValue);
Array.from(currentTrack.cues)
.forEach(function (cue) {
cue.startTime -= offsetValue;
cue.endTime -= offsetValue;
});
}
}
/**
* @private
*/
setTrackEventsSubtitleOffset(trackEvents, offsetValue) {
if (Array.isArray(trackEvents)) {
offsetValue = this.updateCurrentTrackOffset(offsetValue) * 1e7; // ticks
trackEvents.forEach(function (trackEvent) {
trackEvent.StartPositionTicks -= offsetValue;
trackEvent.EndPositionTicks -= offsetValue;
});
}
}
getSubtitleOffset() {
return this.#currentTrackOffset;
}
2019-04-08 20:21:16 +02:00
/**
* @private
*/
isAudioStreamSupported(stream, deviceProfile, container) {
2020-07-21 22:22:16 +02:00
const codec = (stream.Codec || '').toLowerCase();
if (!codec) {
return true;
2018-10-23 01:05:09 +03:00
}
if (!deviceProfile) {
// This should never happen
return true;
2018-10-23 01:05:09 +03:00
}
const profiles = deviceProfile.DirectPlayProfiles || [];
return profiles.some(function (p) {
return p.Type === 'Video'
&& includesAny((p.Container || '').toLowerCase(), container)
&& includesAny((p.AudioCodec || '').toLowerCase(), codec);
});
2018-10-23 01:05:09 +03:00
}
/**
* @private
*/
getSupportedAudioStreams() {
const profile = this.#lastProfile;
const mediaSource = this._currentPlayOptions.mediaSource;
const container = mediaSource.Container.toLowerCase();
return getMediaStreamAudioTracks(mediaSource).filter((stream) => {
return this.isAudioStreamSupported(stream, profile, container);
});
2018-10-23 01:05:09 +03:00
}
setAudioStreamIndex(index) {
const streams = this.getSupportedAudioStreams();
if (streams.length < 2) {
// If there's only one supported stream then trust that the player will handle it on it's own
return;
2018-10-23 01:05:09 +03:00
}
let audioIndex = -1;
for (const stream of streams) {
audioIndex++;
if (stream.Index === index) {
break;
}
}
if (audioIndex === -1) {
return;
}
const elem = this.#mediaElement;
if (!elem) {
return;
}
2020-07-30 00:44:33 +02:00
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/audioTracks
2020-07-29 19:51:33 +02:00
/**
2020-07-30 00:41:11 +02:00
* @type {ArrayLike<any>|any[]}
2020-07-29 19:51:33 +02:00
*/
const elemAudioTracks = elem.audioTracks || [];
console.debug(`found ${elemAudioTracks.length} audio tracks`);
2020-07-29 19:51:33 +02:00
for (const [i, audioTrack] of Array.from(elemAudioTracks).entries()) {
if (audioIndex === i) {
console.debug(`setting audio track ${i} to enabled`);
audioTrack.enabled = true;
} else {
console.debug(`setting audio track ${i} to disabled`);
audioTrack.enabled = false;
}
}
}
stop(destroyPlayer) {
const elem = this.#mediaElement;
const src = this.#currentSrc;
if (elem) {
if (src) {
elem.pause();
}
2020-07-26 14:18:34 +02:00
onEndedInternal(this, elem, this.onError);
}
this.destroyCustomTrack(elem);
if (destroyPlayer) {
this.destroy();
}
return Promise.resolve();
}
destroy() {
2020-07-26 14:18:34 +02:00
destroyHlsPlayer(this);
destroyFlvPlayer(this);
setBackdropTransparency(TRANSPARENCY_LEVEL.None);
document.body.classList.remove('hide-scroll');
const videoElement = this.#mediaElement;
if (videoElement) {
this.#mediaElement = null;
this.destroyCustomTrack(videoElement);
2020-05-27 00:38:01 +02:00
videoElement.removeEventListener('timeupdate', this.onTimeUpdate);
videoElement.removeEventListener('ended', this.onEnded);
videoElement.removeEventListener('volumechange', this.onVolumeChange);
videoElement.removeEventListener('pause', this.onPause);
videoElement.removeEventListener('playing', this.onPlaying);
videoElement.removeEventListener('play', this.onPlay);
videoElement.removeEventListener('click', this.onClick);
videoElement.removeEventListener('dblclick', this.onDblClick);
videoElement.removeEventListener('waiting', this.onWaiting);
2021-09-12 01:28:56 +03:00
videoElement.removeEventListener('error', this.onError); // bound in htmlMediaHelper
2021-09-17 01:20:58 +03:00
resetSrc(videoElement);
videoElement.parentNode.removeChild(videoElement);
}
const dlg = this.#videoDialog;
if (dlg) {
this.#videoDialog = null;
dlg.parentNode.removeChild(dlg);
}
2020-08-16 20:24:45 +02:00
if (Screenfull.isEnabled) {
Screenfull.exit();
2020-07-29 09:28:06 -04:00
} else {
// iOS Safari
if (document.webkitIsFullScreen && document.webkitCancelFullscreen) {
document.webkitCancelFullscreen();
}
2020-04-26 16:16:48 +02:00
}
}
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
*/
2020-05-27 00:38:01 +02:00
onEnded = (e) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e.target;
this.destroyCustomTrack(elem);
2020-07-26 14:18:34 +02:00
onEndedInternal(this, elem, this.onError);
2020-08-01 04:07:00 +02:00
};
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
*/
2020-05-27 00:38:01 +02:00
onTimeUpdate = (e) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e.target;
2019-05-22 23:06:39 -07:00
// get the player position and the transcoding offset
2020-05-27 00:38:01 +02:00
const time = elem.currentTime;
if (time && !this.#timeUpdated) {
this.#timeUpdated = true;
2020-05-27 00:38:01 +02:00
this.ensureValidVideo(elem);
}
this.#currentTime = time;
const currentPlayOptions = this._currentPlayOptions;
// Not sure yet how this is coming up null since we never null it out, but it is causing app crashes
if (currentPlayOptions) {
let timeMs = time * 1000;
timeMs += ((currentPlayOptions.transcodingOffsetTicks || 0) / 10000);
this.updateSubtitleText(timeMs);
}
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'timeupdate');
2020-08-01 04:07:00 +02:00
};
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
*/
2020-05-27 00:38:01 +02:00
onVolumeChange = (e) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e.target;
2020-07-26 14:18:34 +02:00
saveVolume(elem.volume);
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'volumechange');
2020-08-01 04:07:00 +02:00
};
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onNavigatedToOsd = () => {
const dlg = this.#videoDialog;
if (dlg) {
dlg.classList.remove('videoPlayerContainer-onTop');
this.onStartedAndNavigatedToOsd();
}
2020-08-01 04:07:00 +02:00
};
/**
* @private
*/
onStartedAndNavigatedToOsd() {
// If this causes a failure during navigation we end up in an awkward UI state
this.setCurrentTrackElement(this.#subtitleTrackIndexToSetOnPlaying);
if (this.#audioTrackIndexToSetOnPlaying != null && this.canSetAudioStreamIndex()) {
this.setAudioStreamIndex(this.#audioTrackIndexToSetOnPlaying);
}
}
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
*/
2020-05-27 00:38:01 +02:00
onPlaying = (e) => {
/**
* @type {HTMLMediaElement}
*/
const elem = e.target;
if (!this.#started) {
this.#started = true;
2020-05-27 00:38:01 +02:00
elem.removeAttribute('controls');
loading.hide();
2020-07-26 14:18:34 +02:00
seekOnPlaybackStart(this, e.target, this._currentPlayOptions.playerStartPositionTicks, () => {
if (this.#currentSubtitlesOctopus) {
this.#currentSubtitlesOctopus.timeOffset = (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000 + this.#currentTrackOffset;
this.#currentSubtitlesOctopus.resize();
this.#currentSubtitlesOctopus.resetRenderAheadCache(false);
}
});
if (this._currentPlayOptions.fullscreen) {
2020-05-27 00:38:01 +02:00
appRouter.showVideoOsd().then(this.onNavigatedToOsd);
} else {
setBackdropTransparency(TRANSPARENCY_LEVEL.Backdrop);
this.#videoDialog.classList.remove('videoPlayerContainer-onTop');
this.onStartedAndNavigatedToOsd();
}
}
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'playing');
2020-08-01 04:07:00 +02:00
};
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onPlay = () => {
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'unpause');
2020-08-01 04:07:00 +02:00
};
/**
* @private
*/
ensureValidVideo(elem) {
if (elem !== this.#mediaElement) {
return;
}
if (elem.videoWidth === 0 && elem.videoHeight === 0) {
const mediaSource = (this._currentPlayOptions || {}).mediaSource;
// Only trigger this if there is media info
// Avoid triggering in situations where it might not actually have a video stream (audio only live tv channel)
if (!mediaSource || mediaSource.RunTimeTicks) {
2020-07-26 14:18:34 +02:00
onErrorInternal(this, 'mediadecodeerror');
}
}
}
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onClick = () => {
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'click');
2020-08-01 04:07:00 +02:00
};
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onDblClick = () => {
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'dblclick');
2020-08-01 04:07:00 +02:00
};
/**
* @private
*/
2020-05-27 00:38:01 +02:00
onPause = () => {
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'pause');
2020-08-01 04:07:00 +02:00
};
onWaiting() {
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'waiting');
2020-04-01 17:53:14 +02:00
}
/**
* @private
2020-05-27 00:38:01 +02:00
* @param e {Event} The event received from the `<video>` element
*/
2020-05-27 00:38:01 +02:00
onError = (e) => {
/**
* @type {HTMLMediaElement}
*/
2020-07-21 22:22:16 +02:00
const elem = e.target;
2020-05-27 00:38:01 +02:00
const errorCode = elem.error ? (elem.error.code || 0) : 0;
2020-07-21 22:22:16 +02:00
const errorMessage = elem.error ? (elem.error.message || '') : '';
console.error(`media element error: ${errorCode} ${errorMessage}`);
let type;
switch (errorCode) {
case 1:
// MEDIA_ERR_ABORTED
// This will trigger when changing media while something is playing
return;
case 2:
// MEDIA_ERR_NETWORK
type = 'network';
break;
case 3:
// MEDIA_ERR_DECODE
if (this._hlsPlayer) {
2020-07-26 14:18:34 +02:00
handleHlsJsMediaError(this);
return;
} else {
type = 'mediadecodeerror';
}
break;
case 4:
// MEDIA_ERR_SRC_NOT_SUPPORTED
type = 'medianotsupported';
break;
default:
// seeing cases where Edge is firing error events with no error code
// example is start playing something, then immediately change src to something else
return;
}
2020-07-26 14:18:34 +02:00
onErrorInternal(this, type);
2020-08-01 04:07:00 +02:00
};
/**
* @private
*/
destroyCustomTrack(videoElement) {
if (this.#videoSubtitlesElem) {
const subtitlesContainer = this.#videoSubtitlesElem.parentNode;
if (subtitlesContainer) {
tryRemoveElement(subtitlesContainer);
}
this.#videoSubtitlesElem = null;
}
this.#currentTrackEvents = null;
if (videoElement) {
const allTracks = videoElement.textTracks || []; // get list of tracks
for (const track of allTracks) {
if (track.label.includes('manualTrack')) {
track.mode = 'disabled';
}
}
}
this.#customTrackIndex = -1;
this.#currentClock = null;
this._currentAspectRatio = null;
const octopus = this.#currentSubtitlesOctopus;
if (octopus) {
octopus.dispose();
}
this.#currentSubtitlesOctopus = null;
const renderer = this.#currentAssRenderer;
if (renderer) {
renderer.setEnabled(false);
}
this.#currentAssRenderer = null;
}
/**
* @private
*/
2020-07-27 18:54:04 +02:00
fetchSubtitlesUwp(track) {
return Windows.Storage.StorageFile.getFileFromPathAsync(track.Path).then(function (storageFile) {
return Windows.Storage.FileIO.readTextAsync(storageFile);
}).then(function (text) {
return JSON.parse(text);
});
}
/**
* @private
*/
async fetchSubtitles(track, item) {
if (window.Windows && itemHelper.isLocalItem(item)) {
return this.fetchSubtitlesUwp(track, item);
}
this.incrementFetchQueue();
try {
const response = await fetch(getTextTrackUrl(track, item, '.js'));
if (!response.ok) {
throw new Error(response);
}
return response.json();
} finally {
this.decrementFetchQueue();
}
}
/**
* @private
*/
setTrackForDisplay(videoElement, track) {
if (!track) {
this.destroyCustomTrack(videoElement);
return;
}
2019-05-22 23:06:39 -07:00
// skip if already playing this track
if (this.#customTrackIndex === track.Index) {
return;
}
this.resetSubtitleOffset();
const item = this._currentPlayOptions.item;
this.destroyCustomTrack(videoElement);
this.#customTrackIndex = track.Index;
this.renderTracksEvents(videoElement, track, item);
}
2018-10-23 01:05:09 +03:00
/**
* @private
*/
renderSsaAss(videoElement, track, item) {
const supportedFonts = ['application/vnd.ms-opentype', 'application/x-truetype-font', 'font/otf', 'font/ttf', 'font/woff', 'font/woff2'];
2020-07-31 00:35:23 +08:00
const avaliableFonts = [];
const attachments = this._currentPlayOptions.mediaSource.MediaAttachments || [];
const apiClient = ServerConnections.getApiClient(item);
attachments.forEach(i => {
// we only require font files and ignore embedded media attachments like covers as there are cases where ffmpeg fails to extract those
if (supportedFonts.includes(i.MimeType)) {
// embedded font url
avaliableFonts.push(apiClient.getUrl(i.DeliveryUrl));
}
2020-07-24 21:55:33 +08:00
});
2020-07-31 00:30:05 +08:00
const fallbackFontList = apiClient.getUrl('/FallbackFont/Fonts', {
2020-07-24 21:55:33 +08:00
api_key: apiClient.accessToken()
});
const htmlVideoPlayer = this;
const options = {
video: videoElement,
subUrl: getTextTrackUrl(track, item),
2020-07-24 21:55:33 +08:00
fonts: avaliableFonts,
workerUrl: `${appRouter.baseUrl()}/libraries/subtitles-octopus-worker.js`,
legacyWorkerUrl: `${appRouter.baseUrl()}/libraries/subtitles-octopus-worker-legacy.js`,
onError() {
// HACK: Clear JavascriptSubtitlesOctopus: it gets disposed when an error occurs
htmlVideoPlayer.#currentSubtitlesOctopus = null;
// HACK: Give JavascriptSubtitlesOctopus time to dispose itself
setTimeout(() => {
onErrorInternal(htmlVideoPlayer, 'mediadecodeerror');
}, 0);
},
timeOffset: (this._currentPlayOptions.transcodingOffsetTicks || 0) / 10000000,
// new octopus options; override all, even defaults
renderMode: 'wasm-blend',
dropAllAnimations: false,
libassMemoryLimit: 40,
libassGlyphLimit: 40,
targetFps: 24,
prescaleFactor: 0.8,
prescaleHeightLimit: 1080,
maxRenderHeight: 2160,
resizeVariation: 0.2,
renderAhead: 90
};
import('@jellyfin/libass-wasm').then(({default: SubtitlesOctopus}) => {
Promise.all([
apiClient.getNamedConfiguration('encoding'),
// Worker in Tizen 5 doesn't resolve relative path with async request
resolveUrl(options.workerUrl),
resolveUrl(options.legacyWorkerUrl)
]).then(([config, workerUrl, legacyWorkerUrl]) => {
options.workerUrl = workerUrl;
options.legacyWorkerUrl = legacyWorkerUrl;
2020-07-24 21:55:33 +08:00
if (config.EnableFallbackFont) {
2020-11-19 17:21:24 -05:00
apiClient.getJSON(fallbackFontList).then((fontFiles = []) => {
2020-11-19 17:36:01 -05:00
fontFiles.forEach(font => {
2020-07-31 00:30:05 +08:00
const fontUrl = apiClient.getUrl(`/FallbackFont/Fonts/${font.Name}`, {
2020-07-29 03:04:13 +08:00
api_key: apiClient.accessToken()
});
2020-11-19 17:36:01 -05:00
avaliableFonts.push(fontUrl);
2020-07-29 03:04:13 +08:00
});
2020-07-31 00:30:05 +08:00
this.#currentSubtitlesOctopus = new SubtitlesOctopus(options);
2020-07-29 03:04:13 +08:00
});
} else {
2020-07-31 00:30:05 +08:00
this.#currentSubtitlesOctopus = new SubtitlesOctopus(options);
2020-07-24 21:55:33 +08:00
}
});
});
}
/**
* @private
*/
requiresCustomSubtitlesElement() {
// after a system update, ps4 isn't showing anything when creating a track element dynamically
// going to have to do it ourselves
if (browser.ps4) {
return true;
}
2022-09-12 13:38:59 -04:00
if (browser.web0s) {
return true;
}
if (browser.edge) {
return true;
}
if (browser.iOS) {
const userAgent = navigator.userAgent.toLowerCase();
// works in the browser but not the native app
if ((userAgent.includes('os 9') || userAgent.includes('os 8')) && !userAgent.includes('safari')) {
return true;
}
}
return false;
}
/**
* @private
*/
renderSubtitlesWithCustomElement(videoElement, track, item) {
this.fetchSubtitles(track, item).then((data) => {
if (!this.#videoSubtitlesElem) {
2020-07-21 22:22:16 +02:00
const subtitlesContainer = document.createElement('div');
subtitlesContainer.classList.add('videoSubtitles');
subtitlesContainer.innerHTML = '<div class="videoSubtitlesInner"></div>';
this.#videoSubtitlesElem = subtitlesContainer.querySelector('.videoSubtitlesInner');
this.setSubtitleAppearance(subtitlesContainer, this.#videoSubtitlesElem);
videoElement.parentNode.appendChild(subtitlesContainer);
this.#currentTrackEvents = data.TrackEvents;
}
});
}
/**
* @private
*/
setSubtitleAppearance(elem, innerElem) {
2020-08-16 20:24:45 +02:00
Promise.all([import('../../scripts/settings/userSettings'), import('../../components/subtitlesettings/subtitleappearancehelper')]).then(([userSettings, subtitleAppearanceHelper]) => {
subtitleAppearanceHelper.applyStyles({
text: innerElem,
window: elem
}, userSettings.getSubtitleAppearanceSettings());
});
2018-10-23 01:05:09 +03:00
}
/**
* @private
*/
getCueCss(appearance, selector) {
return `${selector}::cue {
${appearance.text.map((s) => s.value !== undefined && s.value !== '' ? `${s.name}:${s.value}!important;` : '').join('')}
}`;
}
/**
* @private
*/
setCueAppearance() {
2020-08-16 20:24:45 +02:00
Promise.all([import('../../scripts/settings/userSettings'), import('../../components/subtitlesettings/subtitleappearancehelper')]).then(([userSettings, subtitleAppearanceHelper]) => {
const elementId = `${this.id}-cuestyle`;
let styleElem = document.querySelector(`#${elementId}`);
if (!styleElem) {
styleElem = document.createElement('style');
styleElem.id = elementId;
document.getElementsByTagName('head')[0].appendChild(styleElem);
}
styleElem.innerHTML = this.getCueCss(subtitleAppearanceHelper.getStyles(userSettings.getSubtitleAppearanceSettings()), '.htmlvideoplayer');
});
}
/**
* @private
*/
renderTracksEvents(videoElement, track, item) {
if (!itemHelper.isLocalItem(item) || track.IsExternal) {
2020-07-21 22:22:16 +02:00
const format = (track.Codec || '').toLowerCase();
if (format === 'ssa' || format === 'ass') {
this.renderSsaAss(videoElement, track, item);
return;
}
if (this.requiresCustomSubtitlesElement()) {
this.renderSubtitlesWithCustomElement(videoElement, track, item);
return;
}
}
let trackElement = null;
2019-10-11 15:48:28 +02:00
if (videoElement.textTracks && videoElement.textTracks.length > 0) {
trackElement = videoElement.textTracks[0];
2019-10-11 15:48:28 +02:00
// This throws an error in IE, but is fine in chrome
// In IE it's not necessary anyway because changing the src seems to be enough
try {
trackElement.mode = 'showing';
while (trackElement.cues.length) {
trackElement.removeCue(trackElement.cues[0]);
}
} catch (e) {
2020-02-16 03:44:43 +01:00
console.error('error removing cue from textTrack');
}
2019-10-11 15:48:28 +02:00
trackElement.mode = 'disabled';
} else {
// There is a function addTextTrack but no function for removeTextTrack
// Therefore we add ONE element and replace its cue data
trackElement = videoElement.addTextTrack('subtitles', 'manualTrack', 'und');
}
2019-10-11 15:48:28 +02:00
// download the track json
this.fetchSubtitles(track, item).then(function (data) {
2020-08-16 20:24:45 +02:00
import('../../scripts/settings/userSettings').then((userSettings) => {
2020-07-07 01:06:47 +03:00
// show in ui
console.debug(`downloaded ${data.TrackEvents.length} track events`);
2020-07-07 01:06:47 +03:00
const subtitleAppearance = userSettings.getSubtitleAppearanceSettings();
2020-07-30 23:36:52 +03:00
const cueLine = parseInt(subtitleAppearance.verticalPosition, 10);
2020-07-07 01:06:47 +03:00
// add some cues to show the text
// in safari, the cues need to be added before setting the track mode to showing
for (const trackEvent of data.TrackEvents) {
const trackCueObject = window.VTTCue || window.TextTrackCue;
const cue = new trackCueObject(trackEvent.StartPositionTicks / 10000000, trackEvent.EndPositionTicks / 10000000, normalizeTrackEventText(trackEvent.Text, false));
2020-07-07 01:06:47 +03:00
if (cue.line === 'auto') {
cue.line = cueLine;
}
2020-07-07 01:06:47 +03:00
trackElement.addCue(cue);
}
2020-07-07 01:06:47 +03:00
trackElement.mode = 'showing';
});
2019-10-11 15:48:28 +02:00
});
}
/**
* @private
*/
updateSubtitleText(timeMs) {
const clock = this.#currentClock;
if (clock) {
try {
clock.seek(timeMs / 1000);
} catch (err) {
console.error(`error in libjass: ${err}`);
}
return;
}
const trackEvents = this.#currentTrackEvents;
const subtitleTextElement = this.#videoSubtitlesElem;
if (trackEvents && subtitleTextElement) {
const ticks = timeMs * 10000;
let selectedTrackEvent;
for (const trackEvent of trackEvents) {
if (trackEvent.StartPositionTicks <= ticks && trackEvent.EndPositionTicks >= ticks) {
selectedTrackEvent = trackEvent;
break;
}
}
if (selectedTrackEvent && selectedTrackEvent.Text) {
subtitleTextElement.innerHTML = normalizeTrackEventText(selectedTrackEvent.Text, true);
subtitleTextElement.classList.remove('hide');
} else {
subtitleTextElement.classList.add('hide');
}
}
}
/**
* @private
*/
setCurrentTrackElement(streamIndex) {
console.debug(`setting new text track index to: ${streamIndex}`);
const mediaStreamTextTracks = getMediaStreamTextTracks(this._currentPlayOptions.mediaSource);
let track = streamIndex === -1 ? null : mediaStreamTextTracks.filter(function (t) {
return t.Index === streamIndex;
})[0];
this.setTrackForDisplay(this.#mediaElement, track);
if (enableNativeTrackSupport(this.#currentSrc, track)) {
if (streamIndex !== -1) {
this.setCueAppearance();
}
} else {
// null these out to disable the player's native display (handled below)
streamIndex = -1;
track = null;
}
}
/**
* @private
*/
createMediaElement(options) {
2020-07-21 22:22:16 +02:00
const dlg = document.querySelector('.videoPlayerContainer');
if (!dlg) {
2021-01-26 15:48:00 -05:00
return import('./style.scss').then(() => {
loading.show();
const playerDlg = document.createElement('div');
playerDlg.setAttribute('dir', 'ltr');
playerDlg.classList.add('videoPlayerContainer');
if (options.fullscreen) {
playerDlg.classList.add('videoPlayerContainer-onTop');
}
2020-07-21 22:22:16 +02:00
let html = '';
const cssClass = 'htmlvideoplayer';
// Can't autoplay in these browsers so we need to use the full controls, at least until playback starts
if (!appHost.supports('htmlvideoautoplay')) {
html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" controls="controls" webkit-playsinline playsinline>';
} else if (browser.web0s) {
// in webOS, setting preload auto allows resuming videos
html += '<video class="' + cssClass + '" preload="auto" autoplay="autoplay" webkit-playsinline playsinline>';
} else {
// Chrome 35 won't play with preload none
html += '<video class="' + cssClass + '" preload="metadata" autoplay="autoplay" webkit-playsinline playsinline>';
}
html += '</video>';
playerDlg.innerHTML = html;
const videoElement = playerDlg.querySelector('video');
2020-07-26 14:18:34 +02:00
videoElement.volume = getSavedVolume();
videoElement.addEventListener('timeupdate', this.onTimeUpdate);
videoElement.addEventListener('ended', this.onEnded);
videoElement.addEventListener('volumechange', this.onVolumeChange);
videoElement.addEventListener('pause', this.onPause);
videoElement.addEventListener('playing', this.onPlaying);
videoElement.addEventListener('play', this.onPlay);
videoElement.addEventListener('click', this.onClick);
videoElement.addEventListener('dblclick', this.onDblClick);
videoElement.addEventListener('waiting', this.onWaiting);
2020-05-08 00:07:59 +02:00
if (options.backdropUrl) {
videoElement.poster = options.backdropUrl;
}
2018-10-23 01:05:09 +03:00
document.body.insertBefore(playerDlg, document.body.firstChild);
this.#videoDialog = playerDlg;
this.#mediaElement = videoElement;
2018-10-23 01:05:09 +03:00
delete this.forcedFullscreen;
2020-09-26 22:48:55 +03:00
if (options.fullscreen) {
2020-11-21 23:04:42 +03:00
// At this point, we must hide the scrollbar placeholder, so it's not being displayed while the item is being loaded
document.body.classList.add('hide-scroll');
2020-09-26 22:48:55 +03:00
// Enter fullscreen in the webOS browser to hide the top bar
if (!window.NativeShell && browser.web0s && Screenfull.isEnabled) {
Screenfull.request().then(() => {
this.forcedFullscreen = true;
});
return videoElement;
}
// don't animate on smart tv's, too slow
if (!browser.slow && browser.supportsCssAnimation()) {
return zoomIn(playerDlg).then(function () {
return videoElement;
});
}
}
return videoElement;
});
} else {
2020-09-26 22:48:55 +03:00
if (options.fullscreen) {
// we need to hide scrollbar when starting playback from page with animated background
2020-11-21 23:04:42 +03:00
document.body.classList.add('hide-scroll');
// Enter fullscreen in the webOS browser to hide the top bar
if (!this.forcedFullscreen && !window.NativeShell && browser.web0s && Screenfull.isEnabled) {
Screenfull.request().then(() => {
this.forcedFullscreen = true;
});
}
2020-09-26 22:48:55 +03:00
}
return Promise.resolve(dlg.querySelector('video'));
2018-10-23 01:05:09 +03:00
}
}
/**
* @private
*/
canPlayMediaType(mediaType) {
return (mediaType || '').toLowerCase() === 'video';
}
/**
* @private
*/
supportsPlayMethod(playMethod, item) {
if (appHost.supportsPlayMethod) {
return appHost.supportsPlayMethod(playMethod, item);
2018-10-23 01:05:09 +03:00
}
return true;
}
/**
* @private
*/
getDeviceProfile(item, options) {
return HtmlVideoPlayer.getDeviceProfileInternal(item, options).then((profile) => {
this.#lastProfile = profile;
return profile;
});
}
/**
* @private
*/
static getDeviceProfileInternal(item, options) {
if (appHost.getDeviceProfile) {
return appHost.getDeviceProfile(item, options);
2018-10-23 01:05:09 +03:00
}
return getDefaultProfile();
}
/**
* @private
*/
static getSupportedFeatures() {
const list = [];
2020-07-21 22:22:16 +02:00
const video = document.createElement('video');
2020-08-31 12:37:01 -04:00
if (
// Check non-standard Safari PiP support
typeof video.webkitSupportsPresentationMode === 'function' && video.webkitSupportsPresentationMode('picture-in-picture') && typeof video.webkitSetPresentationMode === 'function'
2022-10-04 17:31:48 -04:00
// Check non-standard Windows PiP support
|| (window.Windows
&& Windows.UI.ViewManagement.ApplicationView.getForCurrentView()
.isViewModeSupported(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay))
2020-08-31 12:37:01 -04:00
// Check standard PiP support
|| document.pictureInPictureEnabled
) {
list.push('PictureInPicture');
2018-10-23 01:05:09 +03:00
}
2020-01-10 11:31:03 -05:00
if (browser.safari || browser.iOS || browser.iPad) {
list.push('AirPlay');
2020-01-10 11:31:03 -05:00
}
2020-05-05 12:01:43 +02:00
if (typeof video.playbackRate === 'number') {
list.push('PlaybackRate');
2020-04-01 17:53:14 +02:00
}
list.push('SetBrightness');
2020-05-04 12:44:12 +02:00
list.push('SetAspectRatio');
2018-10-23 01:05:09 +03:00
return list;
2018-10-23 01:05:09 +03:00
}
supports(feature) {
if (!this.#supportedFeatures) {
this.#supportedFeatures = HtmlVideoPlayer.getSupportedFeatures();
}
return this.#supportedFeatures.includes(feature);
}
// Save this for when playback stops, because querying the time at that point might return 0
currentTime(val) {
const mediaElement = this.#mediaElement;
2018-10-23 01:05:09 +03:00
if (mediaElement) {
if (val != null) {
mediaElement.currentTime = val / 1000;
return;
}
const currentTime = this.#currentTime;
if (currentTime) {
return currentTime * 1000;
}
return (mediaElement.currentTime || 0) * 1000;
2018-10-23 01:05:09 +03:00
}
}
2020-07-27 18:54:04 +02:00
duration() {
const mediaElement = this.#mediaElement;
2018-10-23 01:05:09 +03:00
if (mediaElement) {
const duration = mediaElement.duration;
2020-07-26 14:18:34 +02:00
if (isValidDuration(duration)) {
return duration * 1000;
}
2018-10-23 01:05:09 +03:00
}
return null;
}
2020-07-27 18:54:04 +02:00
canSetAudioStreamIndex() {
const video = this.#mediaElement;
if (video) {
return canPlaySecondaryAudio(video);
}
return false;
}
static onPictureInPictureError(err) {
console.error(`Picture in picture error: ${err}`);
}
setPictureInPictureEnabled(isEnabled) {
const video = this.#mediaElement;
if (document.pictureInPictureEnabled) {
if (video) {
if (isEnabled) {
video.requestPictureInPicture().catch(HtmlVideoPlayer.onPictureInPictureError);
} else {
document.exitPictureInPicture().catch(HtmlVideoPlayer.onPictureInPictureError);
}
}
2019-05-22 23:06:39 -07:00
} else if (window.Windows) {
this.isPip = isEnabled;
if (isEnabled) {
Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay);
} else {
Windows.UI.ViewManagement.ApplicationView.getForCurrentView().tryEnterViewModeAsync(Windows.UI.ViewManagement.ApplicationViewMode.default);
}
2019-05-22 23:06:39 -07:00
} else {
2020-05-04 12:44:12 +02:00
if (video && video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function') {
video.webkitSetPresentationMode(isEnabled ? 'picture-in-picture' : 'inline');
}
}
}
isPictureInPictureEnabled() {
if (document.pictureInPictureEnabled) {
return !!document.pictureInPictureElement;
2019-05-22 23:06:39 -07:00
} else if (window.Windows) {
return this.isPip || false;
2019-05-22 23:06:39 -07:00
} else {
const video = this.#mediaElement;
if (video) {
2020-05-04 12:44:12 +02:00
return video.webkitPresentationMode === 'picture-in-picture';
}
}
return false;
}
2020-11-21 13:20:44 +08:00
isAirPlayEnabled() {
2020-01-10 11:31:03 -05:00
if (document.AirPlayEnabled) {
return !!document.AirplayElement;
2020-01-10 11:31:03 -05:00
}
return false;
}
2020-01-10 11:31:03 -05:00
setAirPlayEnabled(isEnabled) {
const video = this.#mediaElement;
2020-01-10 11:31:03 -05:00
if (document.AirPlayEnabled) {
if (video) {
if (isEnabled) {
2020-02-26 01:58:15 -05:00
video.requestAirPlay().catch(function(err) {
2020-05-04 12:44:12 +02:00
console.error('Error requesting AirPlay', err);
2020-02-26 01:58:15 -05:00
});
2020-01-10 11:31:03 -05:00
} else {
2020-02-26 01:58:15 -05:00
document.exitAirPLay().catch(function(err) {
2020-05-04 12:44:12 +02:00
console.error('Error exiting AirPlay', err);
2020-02-26 01:58:15 -05:00
});
2020-01-10 11:31:03 -05:00
}
}
} else {
video.webkitShowPlaybackTargetPicker();
}
}
2020-01-10 11:31:03 -05:00
setBrightness(val) {
const elem = this.#mediaElement;
2018-10-23 01:05:09 +03:00
if (elem) {
val = Math.max(0, val);
val = Math.min(100, val);
let rawValue = val;
2018-10-23 01:05:09 +03:00
rawValue = Math.max(20, rawValue);
2020-07-21 22:22:16 +02:00
const cssValue = rawValue >= 100 ? 'none' : (rawValue / 100);
elem.style['-webkit-filter'] = `brightness(${cssValue})`;
elem.style.filter = `brightness(${cssValue})`;
elem.brightnessValue = val;
2020-09-08 02:05:02 -04:00
Events.trigger(this, 'brightnesschange');
2018-10-23 01:05:09 +03:00
}
}
getBrightness() {
const elem = this.#mediaElement;
2018-10-23 01:05:09 +03:00
if (elem) {
const val = elem.brightnessValue;
return val == null ? 100 : val;
2018-10-23 01:05:09 +03:00
}
}
seekable() {
const mediaElement = this.#mediaElement;
2018-10-23 01:05:09 +03:00
if (mediaElement) {
const seekable = mediaElement.seekable;
2018-10-23 01:05:09 +03:00
if (seekable && seekable.length) {
let start = seekable.start(0);
let end = seekable.end(0);
2020-07-26 14:18:34 +02:00
if (!isValidDuration(start)) {
start = 0;
}
2020-07-26 14:18:34 +02:00
if (!isValidDuration(end)) {
end = 0;
}
return (end - start) > 0;
2018-10-23 01:05:09 +03:00
}
return false;
2018-10-23 01:05:09 +03:00
}
}
pause() {
const mediaElement = this.#mediaElement;
if (mediaElement) {
mediaElement.pause();
}
}
// This is a retry after error
resume() {
2022-10-05 15:31:15 -04:00
this.unpause();
}
unpause() {
const mediaElement = this.#mediaElement;
if (mediaElement) {
mediaElement.play();
}
}
paused() {
const mediaElement = this.#mediaElement;
if (mediaElement) {
return mediaElement.paused;
}
return false;
}
setPlaybackRate(value) {
const mediaElement = this.#mediaElement;
2020-04-01 17:53:14 +02:00
if (mediaElement) {
mediaElement.playbackRate = value;
}
2020-07-21 22:22:16 +02:00
}
2020-04-01 17:53:14 +02:00
getPlaybackRate() {
const mediaElement = this.#mediaElement;
2020-04-01 17:53:14 +02:00
if (mediaElement) {
return mediaElement.playbackRate;
}
return null;
2020-07-21 22:22:16 +02:00
}
2020-04-01 17:53:14 +02:00
getSupportedPlaybackRates() {
return [{
name: '0.5x',
id: 0.5
}, {
name: '0.75x',
id: 0.75
}, {
name: '1x',
id: 1.0
}, {
name: '1.25x',
id: 1.25
}, {
name: '1.5x',
id: 1.5
}, {
name: '1.75x',
id: 1.75
}, {
name: '2x',
id: 2.0
}];
}
setVolume(val) {
const mediaElement = this.#mediaElement;
if (mediaElement) {
2021-08-10 16:16:52 -07:00
mediaElement.volume = Math.pow(val / 100, 3);
}
}
getVolume() {
const mediaElement = this.#mediaElement;
if (mediaElement) {
2021-08-10 16:16:52 -07:00
return Math.min(Math.round(Math.pow(mediaElement.volume, 1 / 3) * 100), 100);
}
}
volumeUp() {
this.setVolume(Math.min(this.getVolume() + 2, 100));
}
volumeDown() {
this.setVolume(Math.max(this.getVolume() - 2, 0));
}
setMute(mute) {
const mediaElement = this.#mediaElement;
if (mediaElement) {
mediaElement.muted = mute;
}
}
isMuted() {
const mediaElement = this.#mediaElement;
if (mediaElement) {
return mediaElement.muted;
}
return false;
}
setAspectRatio(val) {
const mediaElement = this.#mediaElement;
if (mediaElement) {
2020-07-30 16:07:13 +02:00
if (val === 'auto') {
2020-05-04 12:44:12 +02:00
mediaElement.style.removeProperty('object-fit');
} else {
2020-05-04 12:44:12 +02:00
mediaElement.style['object-fit'] = val;
}
}
this._currentAspectRatio = val;
}
getAspectRatio() {
2020-05-04 12:44:12 +02:00
return this._currentAspectRatio || 'auto';
}
getSupportedAspectRatios() {
return [{
2020-11-17 22:55:48 +08:00
name: globalize.translate('Auto'),
2020-05-04 12:44:12 +02:00
id: 'auto'
}, {
2020-11-17 22:55:48 +08:00
name: globalize.translate('AspectRatioCover'),
2020-05-04 12:44:12 +02:00
id: 'cover'
}, {
2020-11-17 22:55:48 +08:00
name: globalize.translate('AspectRatioFill'),
2020-05-04 12:44:12 +02:00
id: 'fill'
}];
}
togglePictureInPicture() {
return this.setPictureInPictureEnabled(!this.isPictureInPictureEnabled());
}
toggleAirPlay() {
2020-01-10 11:31:03 -05:00
return this.setAirPlayEnabled(!this.isAirPlayEnabled());
}
2020-01-10 11:31:03 -05:00
getBufferedRanges() {
const mediaElement = this.#mediaElement;
if (mediaElement) {
2020-07-26 14:18:34 +02:00
return getBufferedRanges(this, mediaElement);
}
return [];
}
getStats() {
const mediaElement = this.#mediaElement;
const playOptions = this._currentPlayOptions || [];
const categories = [];
if (!mediaElement) {
return Promise.resolve({
categories: categories
});
}
const mediaCategory = {
2018-10-23 01:05:09 +03:00
stats: [],
type: 'media'
2018-10-23 01:05:09 +03:00
};
categories.push(mediaCategory);
if (playOptions.url) {
// create an anchor element (note: no need to append this element to the document)
2020-07-21 22:22:16 +02:00
let link = document.createElement('a');
// set href to any path
link.setAttribute('href', playOptions.url);
2020-07-21 22:22:16 +02:00
const protocol = (link.protocol || '').replace(':', '');
if (protocol) {
mediaCategory.stats.push({
2020-05-04 12:44:12 +02:00
label: globalize.translate('LabelProtocol'),
value: protocol
});
}
link = null;
}
2020-07-23 22:54:34 +02:00
if (this._hlsPlayer) {
mediaCategory.stats.push({
2020-05-04 12:44:12 +02:00
label: globalize.translate('LabelStreamType'),
value: 'HLS'
});
} else {
mediaCategory.stats.push({
2020-05-04 12:44:12 +02:00
label: globalize.translate('LabelStreamType'),
value: 'Video'
});
}
const videoCategory = {
2018-10-23 01:05:09 +03:00
stats: [],
type: 'video'
2018-10-23 01:05:09 +03:00
};
categories.push(videoCategory);
const devicePixelRatio = window.devicePixelRatio || 1;
const rect = mediaElement.getBoundingClientRect ? mediaElement.getBoundingClientRect() : {};
let height = Math.round(rect.height * devicePixelRatio);
let width = Math.round(rect.width * devicePixelRatio);
// Don't show player dimensions on smart TVs because the app UI could be lower resolution than the video and this causes users to think there is a problem
if (width && height && !browser.tv) {
videoCategory.stats.push({
2020-05-04 12:44:12 +02:00
label: globalize.translate('LabelPlayerDimensions'),
value: `${width}x${height}`
});
}
height = mediaElement.videoHeight;
width = mediaElement.videoWidth;
if (width && height) {
videoCategory.stats.push({
2020-05-04 12:44:12 +02:00
label: globalize.translate('LabelVideoResolution'),
value: `${width}x${height}`
});
}
if (mediaElement.getVideoPlaybackQuality) {
const playbackQuality = mediaElement.getVideoPlaybackQuality();
const droppedVideoFrames = playbackQuality.droppedVideoFrames || 0;
2018-10-23 01:05:09 +03:00
videoCategory.stats.push({
2020-05-04 12:44:12 +02:00
label: globalize.translate('LabelDroppedFrames'),
2018-10-23 01:05:09 +03:00
value: droppedVideoFrames
});
const corruptedVideoFrames = playbackQuality.corruptedVideoFrames || 0;
2018-10-23 01:05:09 +03:00
videoCategory.stats.push({
2020-05-04 12:44:12 +02:00
label: globalize.translate('LabelCorruptedFrames'),
2018-10-23 01:05:09 +03:00
value: corruptedVideoFrames
});
2018-10-23 01:05:09 +03:00
}
const audioCategory = {
2018-10-23 01:05:09 +03:00
stats: [],
type: 'audio'
2018-10-23 01:05:09 +03:00
};
categories.push(audioCategory);
const sinkId = mediaElement.sinkId;
if (sinkId) {
audioCategory.stats.push({
label: 'Sink Id:',
value: sinkId
});
}
return Promise.resolve({
2018-10-23 01:05:09 +03:00
categories: categories
});
}
}
/* eslint-enable indent */
export default HtmlVideoPlayer;