import browser from '../../scripts/browser'; import { Events } from 'jellyfin-apiclient'; import loading from '../../components/loading/loading'; import { appRouter } from '../../components/appRouter'; import { saveVolume, getSavedVolume, onErrorInternal } from '../../components/htmlMediaHelper'; import Screenfull from 'screenfull'; import globalize from '../../scripts/globalize'; function loadScript(src) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); } async function createApi() { await loadScript('qrc:///qtwebchannel/qwebchannel.js'); const channel = await new Promise((resolve) => { /*global QWebChannel */ new QWebChannel(window.qt.webChannelTransport, resolve); }); return channel.objects; } async function getApi() { if (window.apiPromise) { return await window.apiPromise; } window.apiPromise = createApi(); return await window.apiPromise; } /* eslint-disable indent */ function getMediaStreamAudioTracks(mediaSource) { return mediaSource.MediaStreams.filter(function (s) { return s.Type === 'Audio'; }); } export class HtmlVideoPlayer { /** * @type {string} */ name; /** * @type {string} */ type = 'mediaplayer'; /** * @type {string} */ id = 'htmlvideoplayer'; useFullSubtitleUrls = true; mustDestroy = true; /** * @type {boolean} */ isFetching = false; /** * @type {HTMLDivElement | null | undefined} */ #videoDialog; /** * @type {number | undefined} */ #subtitleTrackIndexToSetOnPlaying; /** * @type {number | null} */ #audioTrackIndexToSetOnPlaying; /** * @type {boolean | undefined} */ #showTrackOffset; /** * @type {number | undefined} */ #currentTrackOffset; /** * @type {string[] | undefined} */ #supportedFeatures; /** * @type {string | undefined} */ #currentSrc; /** * @type {boolean | undefined} */ #started; /** * @type {boolean | undefined} */ #timeUpdated; /** * @type {number | null | undefined} */ #currentTime; /** * @private (used in other files) * @type {any | undefined} */ _currentPlayOptions; /** * @type {any | undefined} */ #lastProfile; /** * @type {number | undefined} */ #duration; /** * @type {boolean} */ #paused = false; /** * @type {int} */ #volume = 100; /** * @type {boolean} */ #muted = false; /** * @type {float} */ #playRate = 1; #api = undefined; constructor() { if (browser.edgeUwp) { this.name = 'Windows Video Player'; } else { this.name = 'Html Video Player'; } } currentSrc() { return this.#currentSrc; } async ensureApi() { if (!this.#api) { this.#api = await getApi(); } } async play(options) { this.#started = false; this.#timeUpdated = false; this.#currentTime = null; this.resetSubtitleOffset(); loading.show(); await this.ensureApi(); this.#api.power.setScreensaverEnabled(false); const elem = await this.createMediaElement(options); return await this.setCurrentSrc(elem, options); } /** * @private */ getSubtitleParam() { const options = this._currentPlayOptions; if (this.#subtitleTrackIndexToSetOnPlaying != null && this.#subtitleTrackIndexToSetOnPlaying >= 0) { const initialSubtitleStream = options.mediaSource.MediaStreams[this.#subtitleTrackIndexToSetOnPlaying]; if (!initialSubtitleStream || initialSubtitleStream.DeliveryMethod === 'Encode') { this.#subtitleTrackIndexToSetOnPlaying = -1; } else if (initialSubtitleStream.DeliveryMethod === 'External') { return '#,' + initialSubtitleStream.DeliveryUrl; } } if (this.#subtitleTrackIndexToSetOnPlaying == -1 || this.#subtitleTrackIndexToSetOnPlaying == null) { return ''; } return '#' + this.#subtitleTrackIndexToSetOnPlaying; } /** * @private */ setCurrentSrc(elem, options) { return new Promise((resolve) => { const val = options.url; console.debug(`playing url: ${val}`); // Convert to seconds const ms = (options.playerStartPositionTicks || 0) / 10000; this._currentPlayOptions = options; this.#subtitleTrackIndexToSetOnPlaying = options.mediaSource.DefaultSubtitleStreamIndex == null ? -1 : options.mediaSource.DefaultSubtitleStreamIndex; this.#audioTrackIndexToSetOnPlaying = options.playMethod === 'Transcode' ? null : options.mediaSource.DefaultAudioStreamIndex; const player = this.#api.player; player.load(val, { startMilliseconds: ms, autoplay: true }, {type: 'video', headers: {'User-Agent': 'JellyfinMediaPlayer'}, frameRate: 0, media: {}}, (this.#audioTrackIndexToSetOnPlaying != null) ? '#' + this.#audioTrackIndexToSetOnPlaying : '#1', this.getSubtitleParam(), resolve); }); } async setSubtitleStreamIndex(index) { await this.ensureApi(); this.#subtitleTrackIndexToSetOnPlaying = index; this.#api.player.setSubtitleStream(this.getSubtitleParam()); } async resetSubtitleOffset() { await this.ensureApi(); this.#currentTrackOffset = 0; this.#showTrackOffset = false; this.#api.player.setSubtitleDelay(0); } enableShowingSubtitleOffset() { this.#showTrackOffset = true; } disableShowingSubtitleOffset() { this.#showTrackOffset = false; } isShowingSubtitleOffsetEnabled() { return this.#showTrackOffset; } async setSubtitleOffset(offset) { await this.ensureApi(); const offsetValue = parseFloat(offset); this.#currentTrackOffset = offsetValue; this.#api.player.setSubtitleDelay(offset); } getSubtitleOffset() { return this.#currentTrackOffset; } /** * @private */ isAudioStreamSupported() { return true; } /** * @private */ getSupportedAudioStreams() { const profile = this.#lastProfile; return getMediaStreamAudioTracks(this._currentPlayOptions.mediaSource).filter((stream) => { return this.isAudioStreamSupported(stream, profile); }); } async setAudioStreamIndex(index) { await this.ensureApi(); this.#audioTrackIndexToSetOnPlaying = 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; } this.#api.player.setAudioStream(index != -1 ? '#' + index : ''); } onEndedInternal() { const stopInfo = { src: this._currentSrc }; Events.trigger(this, 'stopped', [stopInfo]); this._currentTime = null; this._currentSrc = null; this._currentPlayOptions = null; } async stop(destroyPlayer) { await this.ensureApi(); this.#api.player.stop(); this.#api.power.setScreensaverEnabled(true); this.onEndedInternal(); if (destroyPlayer) { this.destroy(); } return; } async destroy() { await this.ensureApi(); this.#api.player.stop(); this.#api.power.setScreensaverEnabled(true); appRouter.setTransparency('none'); document.body.classList.remove('hide-scroll'); const player = this.#api.player; player.playing.disconnect(this.onPlaying); player.positionUpdate.disconnect(this.onTimeUpdate); player.finished.disconnect(this.onEnded); this.#duration = undefined; player.updateDuration.disconnect(this.onDuration); player.error.disconnect(this.onError); player.paused.disconnect(this.onPause); const dlg = this.#videoDialog; if (dlg) { this.#videoDialog = null; dlg.parentNode.removeChild(dlg); } if (Screenfull.isEnabled) { Screenfull.exit(); } else { // iOS Safari if (document.webkitIsFullScreen && document.webkitCancelFullscreen) { document.webkitCancelFullscreen(); } } } /** * @private */ onEnded = () => { this.onEndedInternal(); }; /** * @private */ onTimeUpdate = (time) => { if (time && !this.#timeUpdated) { this.#timeUpdated = true; } this.#currentTime = time; Events.trigger(this, 'timeupdate'); }; /** * @private */ onNavigatedToOsd = () => { const dlg = this.#videoDialog; if (dlg) { dlg.classList.remove('videoPlayerContainer-onTop'); } }; /** * @private */ onPlaying = () => { if (!this.#started) { this.#started = true; loading.hide(); const volume = getSavedVolume() * 100; if (volume != this.#volume) { this.setVolume(volume, false); } this.setPlaybackRate(1); this.setMute(false); if (this._currentPlayOptions.fullscreen) { appRouter.showVideoOsd().then(this.onNavigatedToOsd); } else { appRouter.setTransparency('backdrop'); this.#videoDialog.classList.remove('videoPlayerContainer-onTop'); } } if (this.#paused) { this.#paused = false; Events.trigger(this, 'unpause'); } Events.trigger(this, 'playing'); }; /** * @private */ onPause = () => { this.#paused = true; // For Syncplay ready notification Events.trigger(this, 'pause'); }; onWaiting = () => { Events.trigger(this, 'waiting'); }; /** * @private * @param e {Event} The event received from the `