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() {
try {
if (window.NativeShell) {
if (window.NativeShell?.AppHost?.exit) {
window.NativeShell.AppHost.exit();
} else if (browser.tizen) {
tizen.application.getCurrentApplication().exit();
@ -360,16 +360,20 @@ export const appHost = {
};
},
deviceName: function () {
return window.NativeShell ? window.NativeShell.AppHost.deviceName() : getDeviceName();
return window.NativeShell?.AppHost?.deviceName
? window.NativeShell.AppHost.deviceName() : getDeviceName();
},
deviceId: function () {
return window.NativeShell ? window.NativeShell.AppHost.deviceId() : getDeviceId();
return window.NativeShell?.AppHost?.deviceId
? window.NativeShell.AppHost.deviceId() : getDeviceId();
},
appName: function () {
return window.NativeShell ? window.NativeShell.AppHost.appName() : appName;
return window.NativeShell?.AppHost?.appName
? window.NativeShell.AppHost.appName() : appName;
},
appVersion: function () {
return window.NativeShell ? window.NativeShell.AppHost.appVersion() : appVersion;
return window.NativeShell?.AppHost?.appVersion
? window.NativeShell.AppHost.appVersion() : appVersion;
},
getPushTokenInfo: function () {
return {};

View file

@ -429,7 +429,7 @@ function getPlaybackInfo(player,
enableDirectStream,
allowVideoStreamCopy,
allowAudioStreamCopy) {
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio') {
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) {
return Promise.resolve({
MediaSources: [
{
@ -1692,7 +1692,7 @@ class PlaybackManager {
if (validatePlaybackInfoResult(self, result)) {
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.lastMediaInfoQuery = lastMediaInfoQuery;
@ -2272,7 +2272,7 @@ class PlaybackManager {
playOptions.items = null;
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;
@ -2311,7 +2311,7 @@ class PlaybackManager {
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 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 contentType;
let transcodingOffsetTicks = 0;
@ -2349,6 +2349,14 @@ class PlaybackManager {
const mediaSourceContainer = (mediaSource.Container || '').toLowerCase();
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') {
contentType = getMimeType(type.toLowerCase(), mediaSourceContainer);

View file

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

View file

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

View file

@ -167,14 +167,16 @@ class QueueCore {
* @param {string} origin The origin of the wait call, used for debug.
*/
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.');
const playerWrapper = this.manager.getPlayerWrapper();
playerWrapper.localPause();
const currentTime = new Date();
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 isPlaying = playerWrapper.isPlaying();

View file

@ -44,13 +44,15 @@ class PlayerFactory {
return this.getDefaultWrapper(syncPlayManager);
}
console.debug('SyncPlay WrapperFactory getWrapper:', player.id);
const Wrapper = this.wrappers[player.id];
const playerId = player.syncPlayWrapAs || player.id;
console.debug('SyncPlay WrapperFactory getWrapper:', playerId);
const Wrapper = this.wrappers[playerId];
if (Wrapper) {
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);
}

View file

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

View file

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

View file

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

View file

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

View file

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