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

Merge pull request #2557 from iwalton3/jpm-plugin

NativeShell enhancements for Jellyfin Media Player
This commit is contained in:
Bill Thornton 2021-07-10 00:51:37 -04:00 committed by GitHub
commit f5cf6879f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 108 additions and 45 deletions

View file

@ -275,7 +275,7 @@ const supportedFeatures = function () {
*/ */
function doExit() { function doExit() {
try { try {
if (window.NativeShell) { if (window.NativeShell?.AppHost?.exit) {
window.NativeShell.AppHost.exit(); window.NativeShell.AppHost.exit();
} else if (browser.tizen) { } else if (browser.tizen) {
tizen.application.getCurrentApplication().exit(); tizen.application.getCurrentApplication().exit();
@ -360,16 +360,20 @@ export const appHost = {
}; };
}, },
deviceName: function () { deviceName: function () {
return window.NativeShell ? window.NativeShell.AppHost.deviceName() : getDeviceName(); return window.NativeShell?.AppHost?.deviceName
? window.NativeShell.AppHost.deviceName() : getDeviceName();
}, },
deviceId: function () { deviceId: function () {
return window.NativeShell ? window.NativeShell.AppHost.deviceId() : getDeviceId(); return window.NativeShell?.AppHost?.deviceId
? window.NativeShell.AppHost.deviceId() : getDeviceId();
}, },
appName: function () { appName: function () {
return window.NativeShell ? window.NativeShell.AppHost.appName() : appName; return window.NativeShell?.AppHost?.appName
? window.NativeShell.AppHost.appName() : appName;
}, },
appVersion: function () { appVersion: function () {
return window.NativeShell ? window.NativeShell.AppHost.appVersion() : appVersion; return window.NativeShell?.AppHost?.appVersion
? window.NativeShell.AppHost.appVersion() : appVersion;
}, },
getPushTokenInfo: function () { getPushTokenInfo: function () {
return {}; return {};

View file

@ -429,7 +429,7 @@ function getPlaybackInfo(player,
enableDirectStream, enableDirectStream,
allowVideoStreamCopy, allowVideoStreamCopy,
allowAudioStreamCopy) { allowAudioStreamCopy) {
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio') { if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) {
return Promise.resolve({ return Promise.resolve({
MediaSources: [ MediaSources: [
{ {
@ -1692,7 +1692,7 @@ class PlaybackManager {
if (validatePlaybackInfoResult(self, result)) { if (validatePlaybackInfoResult(self, result)) {
currentMediaSource = result.MediaSources[0]; currentMediaSource = result.MediaSources[0];
const streamInfo = createStreamInfo(apiClient, currentItem.MediaType, currentItem, currentMediaSource, ticks); const streamInfo = createStreamInfo(apiClient, currentItem.MediaType, currentItem, currentMediaSource, ticks, player);
streamInfo.fullscreen = currentPlayOptions.fullscreen; streamInfo.fullscreen = currentPlayOptions.fullscreen;
streamInfo.lastMediaInfoQuery = lastMediaInfoQuery; streamInfo.lastMediaInfoQuery = lastMediaInfoQuery;
@ -2272,7 +2272,7 @@ class PlaybackManager {
playOptions.items = null; playOptions.items = null;
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex).then(function (mediaSource) { return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex).then(function (mediaSource) {
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition); const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
streamInfo.fullscreen = playOptions.fullscreen; streamInfo.fullscreen = playOptions.fullscreen;
@ -2311,7 +2311,7 @@ class PlaybackManager {
return player.getDeviceProfile(item).then(function (deviceProfile) { return player.getDeviceProfile(item).then(function (deviceProfile) {
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, options.mediaSourceId, options.audioStreamIndex, options.subtitleStreamIndex).then(function (mediaSource) { return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, options.mediaSourceId, options.audioStreamIndex, options.subtitleStreamIndex).then(function (mediaSource) {
return createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition); return createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player);
}); });
}); });
}); });
@ -2337,7 +2337,7 @@ class PlaybackManager {
}); });
}; };
function createStreamInfo(apiClient, type, item, mediaSource, startPosition) { function createStreamInfo(apiClient, type, item, mediaSource, startPosition, player) {
let mediaUrl; let mediaUrl;
let contentType; let contentType;
let transcodingOffsetTicks = 0; let transcodingOffsetTicks = 0;
@ -2349,6 +2349,14 @@ class PlaybackManager {
const mediaSourceContainer = (mediaSource.Container || '').toLowerCase(); const mediaSourceContainer = (mediaSource.Container || '').toLowerCase();
let directOptions; let directOptions;
if (mediaSource.MediaStreams && player.useFullSubtitleUrls) {
mediaSource.MediaStreams.forEach(stream => {
if (stream.DeliveryUrl && stream.DeliveryUrl.startsWith('/')) {
stream.DeliveryUrl = apiClient.getUrl(stream.DeliveryUrl);
}
});
}
if (type === 'Video' || type === 'Audio') { if (type === 'Video' || type === 'Audio') {
contentType = getMimeType(type.toLowerCase(), mediaSourceContainer); contentType = getMimeType(type.toLowerCase(), mediaSourceContainer);

View file

@ -3,6 +3,11 @@ import globalize from '../scripts/globalize';
import loading from './loading/loading'; import loading from './loading/loading';
import appSettings from '../scripts/settings/appSettings'; import appSettings from '../scripts/settings/appSettings';
import { playbackManager } from './playback/playbackmanager'; import { playbackManager } from './playback/playbackmanager';
import { appHost } from '../components/apphost';
import { appRouter } from '../components/appRouter';
import * as inputManager from '../scripts/inputManager';
import toast from '../components/toast/toast';
import confirm from '../components/confirm/confirm';
/* eslint-disable indent */ /* eslint-disable indent */
@ -90,7 +95,13 @@ import { playbackManager } from './playback/playbackmanager';
events: Events, events: Events,
loading, loading,
appSettings, appSettings,
playbackManager playbackManager,
globalize,
appHost,
appRouter,
inputManager,
toast,
confirm
}); });
} else { } else {
console.debug(`Loading plugin (via dynamic import): ${pluginSpec}`); console.debug(`Loading plugin (via dynamic import): ${pluginSpec}`);

View file

@ -118,9 +118,11 @@ class PlaybackCore {
* Sends a buffering request to the server. * Sends a buffering request to the server.
* @param {boolean} isBuffering Whether this client is buffering or not. * @param {boolean} isBuffering Whether this client is buffering or not.
*/ */
sendBufferingRequest(isBuffering = true) { async sendBufferingRequest(isBuffering = true) {
const playerWrapper = this.manager.getPlayerWrapper(); const playerWrapper = this.manager.getPlayerWrapper();
const currentPosition = playerWrapper.currentTime(); const currentPosition = (playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime());
const currentPositionTicks = Math.round(currentPosition * Helper.TicksPerMillisecond); const currentPositionTicks = Math.round(currentPosition * Helper.TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying(); const isPlaying = playerWrapper.isPlaying();
@ -155,7 +157,7 @@ class PlaybackCore {
* Applies a command and checks the playback state if a duplicate command is received. * Applies a command and checks the playback state if a duplicate command is received.
* @param {Object} command The playback command. * @param {Object} command The playback command.
*/ */
applyCommand(command) { async applyCommand(command) {
// Check if duplicate. // Check if duplicate.
if (this.lastCommand && if (this.lastCommand &&
this.lastCommand.When.getTime() === command.When.getTime() && this.lastCommand.When.getTime() === command.When.getTime() &&
@ -177,7 +179,9 @@ class PlaybackCore {
} else { } else {
// Check if playback state matches requested command. // Check if playback state matches requested command.
const playerWrapper = this.manager.getPlayerWrapper(); const playerWrapper = this.manager.getPlayerWrapper();
const currentPositionTicks = Math.round(playerWrapper.currentTime() * Helper.TicksPerMillisecond); const currentPositionTicks = Math.round((playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime()) * Helper.TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying(); const isPlaying = playerWrapper.isPlaying();
switch (command.Command) { switch (command.Command) {
@ -255,14 +259,16 @@ class PlaybackCore {
* @param {Date} playAtTime The server's UTC time at which to resume playback. * @param {Date} playAtTime The server's UTC time at which to resume playback.
* @param {number} positionTicks The PositionTicks from where to resume. * @param {number} positionTicks The PositionTicks from where to resume.
*/ */
scheduleUnpause(playAtTime, positionTicks) { async scheduleUnpause(playAtTime, positionTicks) {
this.clearScheduledCommand(); this.clearScheduledCommand();
const enableSyncTimeout = this.maxDelaySpeedToSync / 2.0; const enableSyncTimeout = this.maxDelaySpeedToSync / 2.0;
const currentTime = new Date(); const currentTime = new Date();
const playAtTimeLocal = this.timeSyncCore.remoteDateToLocal(playAtTime); const playAtTimeLocal = this.timeSyncCore.remoteDateToLocal(playAtTime);
const playerWrapper = this.manager.getPlayerWrapper(); const playerWrapper = this.manager.getPlayerWrapper();
const currentPositionTicks = playerWrapper.currentTime() * Helper.TicksPerMillisecond; const currentPositionTicks = (playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime()) * Helper.TicksPerMillisecond;
if (playAtTimeLocal > currentTime) { if (playAtTimeLocal > currentTime) {
const playTimeout = playAtTimeLocal - currentTime; const playTimeout = playAtTimeLocal - currentTime;

View file

@ -167,14 +167,16 @@ class QueueCore {
* @param {string} origin The origin of the wait call, used for debug. * @param {string} origin The origin of the wait call, used for debug.
*/ */
scheduleReadyRequestOnPlaybackStart(apiClient, origin) { scheduleReadyRequestOnPlaybackStart(apiClient, origin) {
Helper.waitForEventOnce(this.manager, 'playbackstart', Helper.WaitForEventDefaultTimeout, ['playbackerror']).then(() => { Helper.waitForEventOnce(this.manager, 'playbackstart', Helper.WaitForEventDefaultTimeout, ['playbackerror']).then(async () => {
console.debug('SyncPlay scheduleReadyRequestOnPlaybackStart: local pause and notify server.'); console.debug('SyncPlay scheduleReadyRequestOnPlaybackStart: local pause and notify server.');
const playerWrapper = this.manager.getPlayerWrapper(); const playerWrapper = this.manager.getPlayerWrapper();
playerWrapper.localPause(); playerWrapper.localPause();
const currentTime = new Date(); const currentTime = new Date();
const now = this.manager.timeSyncCore.localDateToRemote(currentTime); const now = this.manager.timeSyncCore.localDateToRemote(currentTime);
const currentPosition = playerWrapper.currentTime(); const currentPosition = (playerWrapper.currentTimeAsync
? await playerWrapper.currentTimeAsync()
: playerWrapper.currentTime());
const currentPositionTicks = Math.round(currentPosition * Helper.TicksPerMillisecond); const currentPositionTicks = Math.round(currentPosition * Helper.TicksPerMillisecond);
const isPlaying = playerWrapper.isPlaying(); const isPlaying = playerWrapper.isPlaying();

View file

@ -44,13 +44,15 @@ class PlayerFactory {
return this.getDefaultWrapper(syncPlayManager); return this.getDefaultWrapper(syncPlayManager);
} }
console.debug('SyncPlay WrapperFactory getWrapper:', player.id); const playerId = player.syncPlayWrapAs || player.id;
const Wrapper = this.wrappers[player.id];
console.debug('SyncPlay WrapperFactory getWrapper:', playerId);
const Wrapper = this.wrappers[playerId];
if (Wrapper) { if (Wrapper) {
return new Wrapper(player, syncPlayManager); return new Wrapper(player, syncPlayManager);
} }
console.debug(`SyncPlay WrapperFactory getWrapper: unknown player ${player.id}, using default wrapper.`); console.debug(`SyncPlay WrapperFactory getWrapper: unknown player ${playerId}, using default wrapper.`);
return this.getDefaultWrapper(syncPlayManager); return this.getDefaultWrapper(syncPlayManager);
} }

View file

@ -1,3 +1,5 @@
import { appHost } from '../../apphost';
/** /**
* Creates an audio element that plays a silent sound. * Creates an audio element that plays a silent sound.
* @returns {HTMLMediaElement} The audio element. * @returns {HTMLMediaElement} The audio element.
@ -33,6 +35,10 @@ class PlaybackPermissionManager {
* @returns {Promise} Promise that resolves succesfully if playback permission is allowed. * @returns {Promise} Promise that resolves succesfully if playback permission is allowed.
*/ */
check () { check () {
if (appHost.supports('htmlaudioautoplay')) {
return Promise.resolve(true);
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const media = createTestMediaElement(); const media = createTestMediaElement();
media.play().then(() => { media.play().then(() => {

View file

@ -17,6 +17,16 @@ class HtmlVideoPlayer extends NoActivePlayer {
this.isPlayerActive = false; this.isPlayerActive = false;
this.savedPlaybackRate = 1.0; this.savedPlaybackRate = 1.0;
this.minBufferingThresholdMillis = 3000; this.minBufferingThresholdMillis = 3000;
if (player.currentTimeAsync) {
/**
* Gets current playback position.
* @returns {Promise<number>} The player position, in milliseconds.
*/
this.currentTimeAsync = () => {
return this.player.currentTimeAsync();
};
}
} }
/** /**

View file

@ -94,13 +94,13 @@ import '../../assets/css/flexstyles.scss';
} }
} }
function onStartNowClick() { async function onStartNowClick() {
const options = this.options; const options = this.options;
if (options) { if (options) {
const player = options.player; const player = options.player;
this.hide(); await this.hide();
playbackManager.nextTrack(player); playbackManager.nextTrack(player);
} }
@ -139,7 +139,7 @@ import '../../assets/css/flexstyles.scss';
Events.trigger(instance, 'hide'); Events.trigger(instance, 'hide');
} }
function hideComingUpNext() { async function hideComingUpNext() {
const instance = this; const instance = this;
clearCountdownTextTimeout(this); clearCountdownTextTimeout(this);
@ -159,17 +159,21 @@ import '../../assets/css/flexstyles.scss';
return; return;
} }
const fn = onHideAnimationComplete.bind(instance);
instance._onHideAnimationComplete = fn;
const transitionEvent = await new Promise((resolve) => {
dom.addEventListener(elem, transitionEndEventName, resolve, {
once: true
});
// trigger a reflow to force it to animate again // trigger a reflow to force it to animate again
void elem.offsetWidth; void elem.offsetWidth;
elem.classList.add('upNextDialog-hidden'); elem.classList.add('upNextDialog-hidden');
const fn = onHideAnimationComplete.bind(instance);
instance._onHideAnimationComplete = fn;
dom.addEventListener(elem, transitionEndEventName, fn, {
once: true
}); });
instance._onHideAnimationComplete(transitionEvent);
} }
function getTimeRemainingMs(instance) { function getTimeRemainingMs(instance) {
@ -226,8 +230,8 @@ class UpNextDialog {
startComingUpNextHideTimer(this); startComingUpNextHideTimer(this);
} }
hide() { async hide() {
hideComingUpNext.call(this); await hideComingUpNext.bind(this)();
} }
destroy() { destroy() {
hideComingUpNext.call(this); hideComingUpNext.call(this);

View file

@ -313,8 +313,8 @@ import { appRouter } from '../../../components/appRouter';
function onPointerMove(e) { function onPointerMove(e) {
if ((e.pointerType || (layoutManager.mobile ? 'touch' : 'mouse')) === 'mouse') { if ((e.pointerType || (layoutManager.mobile ? 'touch' : 'mouse')) === 'mouse') {
const eventX = e.screenX || 0; const eventX = e.screenX || e.clientX || 0;
const eventY = e.screenY || 0; const eventY = e.screenY || e.clientY || 0;
const obj = lastPointerMoveData; const obj = lastPointerMoveData;
if (!obj) { if (!obj) {

View file

@ -54,8 +54,8 @@ import dom from '../scripts/dom';
let lastPointerMoveData; let lastPointerMoveData;
function onPointerMove(e) { function onPointerMove(e) {
const eventX = e.screenX; const eventX = e.screenX || e.clientX;
const eventY = e.screenY; const eventY = e.screenY || e.clientY;
// if coord don't exist how could it move // if coord don't exist how could it move
if (typeof eventX === 'undefined' && typeof eventY === 'undefined') { if (typeof eventX === 'undefined' && typeof eventY === 'undefined') {

View file

@ -1,30 +1,40 @@
// TODO: This seems like a good candidate for deprecation // TODO: This seems like a good candidate for deprecation
export default { export default {
enableFullscreen: function() { enableFullscreen: function() {
window.NativeShell?.enableFullscreen(); if (window.NativeShell?.enableFullscreen) {
window.NativeShell.enableFullscreen();
}
}, },
disableFullscreen: function() { disableFullscreen: function() {
window.NativeShell?.disableFullscreen(); if (window.NativeShell?.disableFullscreen) {
window.NativeShell.disableFullscreen();
}
}, },
openUrl: function(url, target) { openUrl: function(url, target) {
if (window.NativeShell) { if (window.NativeShell?.openUrl) {
window.NativeShell.openUrl(url, target); window.NativeShell.openUrl(url, target);
} else { } else {
window.open(url, target || '_blank'); window.open(url, target || '_blank');
} }
}, },
updateMediaSession(mediaInfo) { updateMediaSession(mediaInfo) {
window.NativeShell?.updateMediaSession(mediaInfo); if (window.NativeShell?.updateMediaSession) {
window.NativeShell.updateMediaSession(mediaInfo);
}
}, },
hideMediaSession() { hideMediaSession() {
window.NativeShell?.hideMediaSession(); if (window.NativeShell?.hideMediaSession) {
window.NativeShell.hideMediaSession();
}
}, },
/** /**
* Notify the NativeShell about volume level changes. * Notify the NativeShell about volume level changes.
* Useful for e.g. remote playback. * Useful for e.g. remote playback.
*/ */
updateVolumeLevel(volume) { updateVolumeLevel(volume) {
window.NativeShell?.updateVolumeLevel(volume); if (window.NativeShell?.updateVolumeLevel) {
window.NativeShell.updateVolumeLevel(volume);
}
}, },
/** /**
* Download specified files with NativeShell if possible * Download specified files with NativeShell if possible
@ -32,7 +42,7 @@ export default {
* @returns true on success * @returns true on success
*/ */
downloadFiles(items) { downloadFiles(items) {
if (window.NativeShell) { if (window.NativeShell?.downloadFile) {
items.forEach(item => window.NativeShell.downloadFile(item)); items.forEach(item => window.NativeShell.downloadFile(item));
return true; return true;
} }