diff --git a/src/components/apphost.js b/src/components/apphost.js index 36feb896f3..39cfabbe4c 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -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 {}; diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 6defed364a..41d97f506d 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -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); diff --git a/src/components/pluginManager.js b/src/components/pluginManager.js index 985b76725f..5281e4d46f 100644 --- a/src/components/pluginManager.js +++ b/src/components/pluginManager.js @@ -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}`); diff --git a/src/components/syncPlay/core/PlaybackCore.js b/src/components/syncPlay/core/PlaybackCore.js index 12e0c67abb..2cab13e784 100644 --- a/src/components/syncPlay/core/PlaybackCore.js +++ b/src/components/syncPlay/core/PlaybackCore.js @@ -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; diff --git a/src/components/syncPlay/core/QueueCore.js b/src/components/syncPlay/core/QueueCore.js index 30fddf5f26..81feac0aca 100644 --- a/src/components/syncPlay/core/QueueCore.js +++ b/src/components/syncPlay/core/QueueCore.js @@ -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(); diff --git a/src/components/syncPlay/core/players/PlayerFactory.js b/src/components/syncPlay/core/players/PlayerFactory.js index 709680cf63..1de9aa5126 100644 --- a/src/components/syncPlay/core/players/PlayerFactory.js +++ b/src/components/syncPlay/core/players/PlayerFactory.js @@ -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); } diff --git a/src/components/syncPlay/ui/playbackPermissionManager.js b/src/components/syncPlay/ui/playbackPermissionManager.js index e2d7ef2f4c..41c2dc84d3 100644 --- a/src/components/syncPlay/ui/playbackPermissionManager.js +++ b/src/components/syncPlay/ui/playbackPermissionManager.js @@ -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(() => { diff --git a/src/components/syncPlay/ui/players/HtmlVideoPlayer.js b/src/components/syncPlay/ui/players/HtmlVideoPlayer.js index cc045d4954..16d26cd1c2 100644 --- a/src/components/syncPlay/ui/players/HtmlVideoPlayer.js +++ b/src/components/syncPlay/ui/players/HtmlVideoPlayer.js @@ -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} The player position, in milliseconds. + */ + this.currentTimeAsync = () => { + return this.player.currentTimeAsync(); + }; + } } /** diff --git a/src/components/upnextdialog/upnextdialog.js b/src/components/upnextdialog/upnextdialog.js index cb40cfcbc8..2284975f13 100644 --- a/src/components/upnextdialog/upnextdialog.js +++ b/src/components/upnextdialog/upnextdialog.js @@ -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); diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index d7e8fd5bf2..6744d26d51 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -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) { diff --git a/src/scripts/mouseManager.js b/src/scripts/mouseManager.js index 3f37dfbd9e..1ad3389c04 100644 --- a/src/scripts/mouseManager.js +++ b/src/scripts/mouseManager.js @@ -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') { diff --git a/src/scripts/shell.js b/src/scripts/shell.js index 006d0433ff..aba8698cdb 100644 --- a/src/scripts/shell.js +++ b/src/scripts/shell.js @@ -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; }