Remove mpv plugins.
This commit is contained in:
parent
de1d36f2dc
commit
6f5517015e
3 changed files with 0 additions and 1189 deletions
|
@ -1,377 +0,0 @@
|
|||
import { Events } from 'jellyfin-apiclient';
|
||||
import * as htmlMediaHelper from '../../components/htmlMediaHelper';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let fadeTimeout;
|
||||
function fade(instance, elem, startingVolume) {
|
||||
instance._isFadingOut = true;
|
||||
|
||||
// Need to record the starting volume on each pass rather than querying elem.volume
|
||||
// This is due to iOS safari not allowing volume changes and always returning the system volume value
|
||||
const newVolume = Math.max(0, startingVolume - 15);
|
||||
console.debug('fading volume to ' + newVolume);
|
||||
instance.api.player.setVolume(newVolume);
|
||||
|
||||
if (newVolume <= 0) {
|
||||
instance._isFadingOut = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
cancelFadeTimeout();
|
||||
fadeTimeout = setTimeout(function () {
|
||||
fade(instance, null, newVolume).then(resolve, reject);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
function cancelFadeTimeout() {
|
||||
const timeout = fadeTimeout;
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
fadeTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
class HtmlAudioPlayer {
|
||||
constructor() {
|
||||
const self = this;
|
||||
|
||||
self.name = 'Html Audio Player';
|
||||
self.type = 'mediaplayer';
|
||||
self.id = 'htmlaudioplayer';
|
||||
self.useServerPlaybackInfoForAudio = true;
|
||||
|
||||
self._duration = undefined;
|
||||
self._currentTime = undefined;
|
||||
self._paused = false;
|
||||
self._volume = htmlMediaHelper.getSavedVolume() * 100;
|
||||
self._playRate = 1;
|
||||
|
||||
self.api = undefined;
|
||||
|
||||
self.ensureApi = async () => {
|
||||
if (!self.api) {
|
||||
self.api = await getApi();
|
||||
}
|
||||
};
|
||||
|
||||
self.play = async (options) => {
|
||||
self._started = false;
|
||||
self._timeUpdated = false;
|
||||
self._currentTime = null;
|
||||
self._duration = undefined;
|
||||
|
||||
await self.ensureApi();
|
||||
const player = self.api.player;
|
||||
player.playing.connect(onPlaying);
|
||||
player.positionUpdate.connect(onTimeUpdate);
|
||||
player.finished.connect(onEnded);
|
||||
player.updateDuration.connect(onDuration);
|
||||
player.error.connect(onError);
|
||||
player.paused.connect(onPause);
|
||||
|
||||
return await setCurrentSrc(options);
|
||||
};
|
||||
|
||||
function setCurrentSrc(options) {
|
||||
return new Promise((resolve) => {
|
||||
const val = options.url;
|
||||
self._currentSrc = val;
|
||||
console.debug('playing url: ' + val);
|
||||
|
||||
// Convert to seconds
|
||||
const ms = (options.playerStartPositionTicks || 0) / 10000;
|
||||
self._currentPlayOptions = options;
|
||||
|
||||
self.api.player.load(val,
|
||||
{ startMilliseconds: ms, autoplay: true },
|
||||
{type: 'music', headers: {'User-Agent': 'JellyfinMediaPlayer'}, media: {}},
|
||||
'#1',
|
||||
'',
|
||||
resolve);
|
||||
});
|
||||
}
|
||||
|
||||
self.onEndedInternal = () => {
|
||||
const stopInfo = {
|
||||
src: self._currentSrc
|
||||
};
|
||||
|
||||
Events.trigger(self, 'stopped', [stopInfo]);
|
||||
|
||||
self._currentTime = null;
|
||||
self._currentSrc = null;
|
||||
self._currentPlayOptions = null;
|
||||
};
|
||||
|
||||
self.stop = async (destroyPlayer) => {
|
||||
cancelFadeTimeout();
|
||||
|
||||
const src = self._currentSrc;
|
||||
|
||||
if (src) {
|
||||
const originalVolume = self._volume;
|
||||
|
||||
await self.ensureApi();
|
||||
return await fade(self, null, self._volume).then(function () {
|
||||
self.pause();
|
||||
self.setVolume(originalVolume, false);
|
||||
|
||||
self.onEndedInternal();
|
||||
|
||||
if (destroyPlayer) {
|
||||
self.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
self.destroy = async () => {
|
||||
await self.ensureApi();
|
||||
self.api.player.stop();
|
||||
|
||||
const player = self.api.player;
|
||||
player.playing.disconnect(onPlaying);
|
||||
player.positionUpdate.disconnect(onTimeUpdate);
|
||||
player.finished.disconnect(onEnded);
|
||||
self._duration = undefined;
|
||||
player.updateDuration.disconnect(onDuration);
|
||||
player.error.disconnect(onError);
|
||||
player.paused.disconnect(onPause);
|
||||
};
|
||||
|
||||
function onDuration(duration) {
|
||||
self._duration = duration;
|
||||
}
|
||||
|
||||
function onEnded() {
|
||||
self.onEndedInternal();
|
||||
}
|
||||
|
||||
function onTimeUpdate(time) {
|
||||
// Don't trigger events after user stop
|
||||
if (!self._isFadingOut) {
|
||||
self._currentTime = time;
|
||||
Events.trigger(self, 'timeupdate');
|
||||
}
|
||||
}
|
||||
|
||||
function onPlaying() {
|
||||
if (!self._started) {
|
||||
self._started = true;
|
||||
}
|
||||
|
||||
self.setPlaybackRate(1);
|
||||
self.setMute(false);
|
||||
|
||||
if (self._paused) {
|
||||
self._paused = false;
|
||||
Events.trigger(self, 'unpause');
|
||||
}
|
||||
|
||||
Events.trigger(self, 'playing');
|
||||
}
|
||||
|
||||
function onPause() {
|
||||
self._paused = true;
|
||||
Events.trigger(self, 'pause');
|
||||
}
|
||||
|
||||
function onError(error) {
|
||||
console.error(`media element error: ${error}`);
|
||||
|
||||
htmlMediaHelper.onErrorInternal(self, 'mediadecodeerror');
|
||||
}
|
||||
}
|
||||
|
||||
currentSrc() {
|
||||
return this._currentSrc;
|
||||
}
|
||||
|
||||
canPlayMediaType(mediaType) {
|
||||
return (mediaType || '').toLowerCase() === 'audio';
|
||||
}
|
||||
|
||||
getDeviceProfile() {
|
||||
return Promise.resolve({
|
||||
'Name': 'Jellyfin Media Player',
|
||||
'MusicStreamingTranscodingBitrate': 1280000,
|
||||
'TimelineOffsetSeconds': 5,
|
||||
'TranscodingProfiles': [
|
||||
{'Type': 'Audio'}
|
||||
],
|
||||
'DirectPlayProfiles': [{'Type': 'Audio'}],
|
||||
'ResponseProfiles': [],
|
||||
'ContainerProfiles': [],
|
||||
'CodecProfiles': [],
|
||||
'SubtitleProfiles': []
|
||||
});
|
||||
}
|
||||
|
||||
currentTime(val) {
|
||||
if (val != null) {
|
||||
this.ensureApi().then(() => {
|
||||
this.api.player.seekTo(val);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return this._currentTime;
|
||||
}
|
||||
|
||||
async currentTimeAsync() {
|
||||
await this.ensureApi();
|
||||
return await new Promise((resolve) => {
|
||||
this.api.player.getPosition(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
duration() {
|
||||
if (this._duration) {
|
||||
return this._duration;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
seekable() {
|
||||
return Boolean(this._duration);
|
||||
}
|
||||
|
||||
getBufferedRanges() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async pause() {
|
||||
await this.ensureApi();
|
||||
this.api.player.pause();
|
||||
}
|
||||
|
||||
// This is a retry after error
|
||||
async resume() {
|
||||
await this.ensureApi();
|
||||
this._paused = false;
|
||||
this.api.player.play();
|
||||
}
|
||||
|
||||
async unpause() {
|
||||
await this.ensureApi();
|
||||
this.api.player.play();
|
||||
}
|
||||
|
||||
paused() {
|
||||
return this._paused;
|
||||
}
|
||||
|
||||
async setPlaybackRate(value) {
|
||||
this._playRate = value;
|
||||
await this.ensureApi();
|
||||
this.api.player.setPlaybackRate(value * 1000);
|
||||
}
|
||||
|
||||
getPlaybackRate() {
|
||||
return this._playRate;
|
||||
}
|
||||
|
||||
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
|
||||
}];
|
||||
}
|
||||
|
||||
async setVolume(val, save = true) {
|
||||
this._volume = val;
|
||||
if (save) {
|
||||
htmlMediaHelper.saveVolume((val || 100) / 100);
|
||||
Events.trigger(this, 'volumechange');
|
||||
}
|
||||
await this.ensureApi();
|
||||
this.api.player.setVolume(val);
|
||||
}
|
||||
|
||||
getVolume() {
|
||||
return this._volume;
|
||||
}
|
||||
|
||||
volumeUp() {
|
||||
this.setVolume(Math.min(this.getVolume() + 2, 100));
|
||||
}
|
||||
|
||||
volumeDown() {
|
||||
this.setVolume(Math.max(this.getVolume() - 2, 0));
|
||||
}
|
||||
|
||||
async setMute(mute) {
|
||||
this._muted = mute;
|
||||
await this.ensureApi();
|
||||
this.api.player.setMuted(mute);
|
||||
}
|
||||
|
||||
isMuted() {
|
||||
return this._muted;
|
||||
}
|
||||
|
||||
supports(feature) {
|
||||
if (!supportedFeatures) {
|
||||
supportedFeatures = getSupportedFeatures();
|
||||
}
|
||||
|
||||
return supportedFeatures.indexOf(feature) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
let supportedFeatures;
|
||||
|
||||
function getSupportedFeatures() {
|
||||
return ['PlaybackRate'];
|
||||
}
|
||||
|
||||
export default HtmlAudioPlayer;
|
|
@ -1,792 +0,0 @@
|
|||
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;
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
tryGetFramerate(options) {
|
||||
if (options.mediaSource && options.mediaSource.MediaStreams) {
|
||||
for (const stream of options.mediaSource.MediaStreams) {
|
||||
if (stream.Type == 'Video') {
|
||||
return stream.RealFrameRate || stream.AverageFrameRate || null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setCurrentSrc(elem, options) {
|
||||
return new Promise((resolve) => {
|
||||
const val = options.url;
|
||||
this.#currentSrc = val;
|
||||
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 streamdata = {type: 'video', headers: {'User-Agent': 'JellyfinMediaPlayer'}, media: {}};
|
||||
const fps = this.tryGetFramerate(options);
|
||||
if (fps) {
|
||||
streamdata.frameRate = fps;
|
||||
}
|
||||
|
||||
const player = this.#api.player;
|
||||
player.load(val,
|
||||
{ startMilliseconds: ms, autoplay: true },
|
||||
streamdata,
|
||||
(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 `<video>` element
|
||||
*/
|
||||
onError = (error) => {
|
||||
console.error(`media element error: ${error}`);
|
||||
|
||||
onErrorInternal(this, 'mediadecodeerror');
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
createMediaElement(options) {
|
||||
const dlg = document.querySelector('.videoPlayerContainer');
|
||||
|
||||
if (!dlg) {
|
||||
return import('./style.css').then(() => {
|
||||
loading.show();
|
||||
|
||||
const dlg = document.createElement('div');
|
||||
|
||||
dlg.classList.add('videoPlayerContainer');
|
||||
|
||||
if (options.fullscreen) {
|
||||
dlg.classList.add('videoPlayerContainer-onTop');
|
||||
}
|
||||
|
||||
const html = '';
|
||||
|
||||
dlg.innerHTML = html;
|
||||
|
||||
document.body.insertBefore(dlg, document.body.firstChild);
|
||||
this.#videoDialog = dlg;
|
||||
const player = this.#api.player;
|
||||
player.playing.connect(this.onPlaying);
|
||||
player.positionUpdate.connect(this.onTimeUpdate);
|
||||
player.finished.connect(this.onEnded);
|
||||
player.updateDuration.connect(this.onDuration);
|
||||
player.error.connect(this.onError);
|
||||
player.paused.connect(this.onPause);
|
||||
|
||||
if (options.fullscreen) {
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// we need to hide scrollbar when starting playback from page with animated background
|
||||
if (options.fullscreen) {
|
||||
document.body.classList.add('hide-scroll');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
canPlayMediaType(mediaType) {
|
||||
return (mediaType || '').toLowerCase() === 'video';
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
supportsPlayMethod() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getDeviceProfile() {
|
||||
return Promise.resolve({
|
||||
'Name': 'Jellyfin Media Player',
|
||||
'MusicStreamingTranscodingBitrate': 1280000,
|
||||
'TimelineOffsetSeconds': 5,
|
||||
'TranscodingProfiles': [
|
||||
{'Type': 'Audio'},
|
||||
{
|
||||
'Container': 'ts',
|
||||
'Type': 'Video',
|
||||
'Protocol': 'hls',
|
||||
'AudioCodec': 'aac,mp3,ac3,opus,flac,vorbis',
|
||||
'VideoCodec': 'h264,h265,hevc,mpeg4,mpeg2video',
|
||||
'MaxAudioChannels': '6'
|
||||
},
|
||||
{'Container': 'jpeg', 'Type': 'Photo'}
|
||||
],
|
||||
'DirectPlayProfiles': [{'Type': 'Video'}, {'Type': 'Audio'}, {'Type': 'Photo'}],
|
||||
'ResponseProfiles': [],
|
||||
'ContainerProfiles': [],
|
||||
'CodecProfiles': [],
|
||||
'SubtitleProfiles': [
|
||||
{'Format': 'srt', 'Method': 'External'},
|
||||
{'Format': 'srt', 'Method': 'Embed'},
|
||||
{'Format': 'ass', 'Method': 'External'},
|
||||
{'Format': 'ass', 'Method': 'Embed'},
|
||||
{'Format': 'sub', 'Method': 'Embed'},
|
||||
{'Format': 'sub', 'Method': 'External'},
|
||||
{'Format': 'ssa', 'Method': 'Embed'},
|
||||
{'Format': 'ssa', 'Method': 'External'},
|
||||
{'Format': 'smi', 'Method': 'Embed'},
|
||||
{'Format': 'smi', 'Method': 'External'},
|
||||
{'Format': 'pgssub', 'Method': 'Embed'},
|
||||
{'Format': 'dvdsub', 'Method': 'Embed'},
|
||||
{'Format': 'pgs', 'Method': 'Embed'}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
static getSupportedFeatures() {
|
||||
return ['PlaybackRate'];
|
||||
}
|
||||
|
||||
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) {
|
||||
if (val != null) {
|
||||
this.ensureApi().then(() => {
|
||||
this.#api.player.seekTo(val);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#currentTime;
|
||||
}
|
||||
|
||||
async currentTimeAsync() {
|
||||
await this.ensureApi();
|
||||
return await new Promise((resolve) => {
|
||||
this.#api.player.getPosition(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
onDuration = (duration) => {
|
||||
this.#duration = duration;
|
||||
};
|
||||
|
||||
duration() {
|
||||
if (this.#duration) {
|
||||
return this.#duration;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
canSetAudioStreamIndex() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static onPictureInPictureError(err) {
|
||||
console.error(`Picture in picture error: ${err}`);
|
||||
}
|
||||
|
||||
setPictureInPictureEnabled() {}
|
||||
|
||||
isPictureInPictureEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isAirPlayEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
setAirPlayEnabled() {}
|
||||
|
||||
setBrightness() {}
|
||||
|
||||
getBrightness() {
|
||||
return 100;
|
||||
}
|
||||
|
||||
seekable() {
|
||||
return Boolean(this.#duration);
|
||||
}
|
||||
|
||||
async pause() {
|
||||
await this.ensureApi();
|
||||
this.#api.player.pause();
|
||||
}
|
||||
|
||||
// This is a retry after error
|
||||
async resume() {
|
||||
await this.ensureApi();
|
||||
this.#paused = false;
|
||||
this.#api.player.play();
|
||||
}
|
||||
|
||||
async unpause() {
|
||||
await this.ensureApi();
|
||||
this.#api.player.play();
|
||||
}
|
||||
|
||||
paused() {
|
||||
return this.#paused;
|
||||
}
|
||||
|
||||
async setPlaybackRate(value) {
|
||||
await this.ensureApi();
|
||||
this.#playRate = value;
|
||||
this.#api.player.setPlaybackRate(value * 1000);
|
||||
}
|
||||
|
||||
getPlaybackRate() {
|
||||
return this.#playRate;
|
||||
}
|
||||
|
||||
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
|
||||
}];
|
||||
}
|
||||
|
||||
async setVolume(val, save = true) {
|
||||
this.#volume = val;
|
||||
if (save) {
|
||||
saveVolume((val || 100) / 100);
|
||||
Events.trigger(this, 'volumechange');
|
||||
}
|
||||
await this.ensureApi();
|
||||
this.#api.player.setVolume(val);
|
||||
}
|
||||
|
||||
getVolume() {
|
||||
return this.#volume;
|
||||
}
|
||||
|
||||
volumeUp() {
|
||||
this.setVolume(Math.min(this.getVolume() + 2, 100));
|
||||
}
|
||||
|
||||
volumeDown() {
|
||||
this.setVolume(Math.max(this.getVolume() - 2, 0));
|
||||
}
|
||||
|
||||
async setMute(mute) {
|
||||
await this.ensureApi();
|
||||
this.#muted = mute;
|
||||
this.#api.player.setMuted(mute);
|
||||
}
|
||||
|
||||
isMuted() {
|
||||
return this.#muted;
|
||||
}
|
||||
|
||||
setAspectRatio() {
|
||||
}
|
||||
|
||||
getAspectRatio() {
|
||||
return this._currentAspectRatio || 'auto';
|
||||
}
|
||||
|
||||
getSupportedAspectRatios() {
|
||||
return [{
|
||||
name: globalize.translate('Auto'),
|
||||
id: 'auto'
|
||||
}];
|
||||
}
|
||||
|
||||
togglePictureInPicture() {
|
||||
}
|
||||
|
||||
toggleAirPlay() {
|
||||
}
|
||||
|
||||
getBufferedRanges() {
|
||||
return [];
|
||||
}
|
||||
|
||||
getStats() {
|
||||
const playOptions = this._currentPlayOptions || [];
|
||||
const categories = [];
|
||||
|
||||
if (!this._currentPlayOptions) {
|
||||
return Promise.resolve({
|
||||
categories: categories
|
||||
});
|
||||
}
|
||||
|
||||
const mediaCategory = {
|
||||
stats: [],
|
||||
type: 'media'
|
||||
};
|
||||
categories.push(mediaCategory);
|
||||
|
||||
if (playOptions.url) {
|
||||
// create an anchor element (note: no need to append this element to the document)
|
||||
let link = document.createElement('a');
|
||||
// set href to any path
|
||||
link.setAttribute('href', playOptions.url);
|
||||
const protocol = (link.protocol || '').replace(':', '');
|
||||
|
||||
if (protocol) {
|
||||
mediaCategory.stats.push({
|
||||
label: globalize.translate('LabelProtocol'),
|
||||
value: protocol
|
||||
});
|
||||
}
|
||||
|
||||
link = null;
|
||||
}
|
||||
|
||||
mediaCategory.stats.push({
|
||||
label: globalize.translate('LabelStreamType'),
|
||||
value: 'Video'
|
||||
});
|
||||
|
||||
const videoCategory = {
|
||||
stats: [],
|
||||
type: 'video'
|
||||
};
|
||||
categories.push(videoCategory);
|
||||
|
||||
const audioCategory = {
|
||||
stats: [],
|
||||
type: 'audio'
|
||||
};
|
||||
categories.push(audioCategory);
|
||||
|
||||
return Promise.resolve({
|
||||
categories: categories
|
||||
});
|
||||
}
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
|
||||
export default HtmlVideoPlayer;
|
|
@ -1,20 +0,0 @@
|
|||
.videoPlayerContainer {
|
||||
position: fixed !important;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.videoPlayerContainer-onTop {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.htmlvideoplayer {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue