From 0fb38c6894d3ef405b69376417dbb0a2f7c26e64 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Sat, 12 Oct 2024 15:43:40 +0300 Subject: [PATCH 01/11] Add 'prompt to skip' to media segments --- .../playback/constants/mediaSegmentAction.ts | 1 + .../playback/utils/mediaSegmentManager.ts | 2 + .../features/playback/utils/mediaSegments.ts | 2 +- src/components/playback/playbackmanager.js | 15 ++ src/components/playback/skipbutton.scss | 32 ++++ src/components/playback/skipsegment.ts | 162 ++++++++++++++++++ src/strings/en-us.json | 3 + 7 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/components/playback/skipbutton.scss create mode 100644 src/components/playback/skipsegment.ts diff --git a/src/apps/stable/features/playback/constants/mediaSegmentAction.ts b/src/apps/stable/features/playback/constants/mediaSegmentAction.ts index c1eb9652f0..87b54f4bce 100644 --- a/src/apps/stable/features/playback/constants/mediaSegmentAction.ts +++ b/src/apps/stable/features/playback/constants/mediaSegmentAction.ts @@ -3,5 +3,6 @@ */ export enum MediaSegmentAction { None = 'None', + PromptToSkip = 'PromptToSkip', Skip = 'Skip' } diff --git a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts index 7957ff2334..9618652397 100644 --- a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts +++ b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts @@ -66,6 +66,8 @@ class MediaSegmentManager extends PlaybackSubscriber { console.debug('[MediaSegmentManager] skipping to next item in queue'); this.playbackManager.nextTrack(this.player); } + } else if (action === MediaSegmentAction.PromptToSkip) { + this.playbackManager.promptToSkip(mediaSegment); } } diff --git a/src/apps/stable/features/playback/utils/mediaSegments.ts b/src/apps/stable/features/playback/utils/mediaSegments.ts index 9ea8e1bdf7..4a782453f1 100644 --- a/src/apps/stable/features/playback/utils/mediaSegments.ts +++ b/src/apps/stable/features/playback/utils/mediaSegments.ts @@ -13,7 +13,7 @@ const isBeforeSegment = (segment: MediaSegmentDto, time: number, direction: numb ); }; -const isInSegment = (segment: MediaSegmentDto, time: number) => ( +export const isInSegment = (segment: MediaSegmentDto, time: number) => ( typeof segment.StartTicks !== 'undefined' && segment.StartTicks <= time && (typeof segment.EndTicks === 'undefined' || segment.EndTicks > time) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index bddc34930f..a7a5ca4bb9 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -26,6 +26,8 @@ import { MediaError } from 'types/mediaError'; import { getMediaError } from 'utils/mediaError'; import { toApi } from 'utils/jellyfin-apiclient/compat'; import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind.js'; +import browser from 'scripts/browser.js'; +import { bindSkipSegment } from './skipsegment.ts'; const UNLIMITED_ITEMS = -1; @@ -933,6 +935,12 @@ export class PlaybackManager { return Promise.resolve(self._playQueueManager.getPlaylist()); }; + self.promptToSkip = function (mediaSegment) { + if (mediaSegment && this._skipSegment) { + this._skipSegment.onPromptSkip(mediaSegment); + } + }; + function removeCurrentPlayer(player) { const previousPlayer = self._currentPlayer; @@ -3666,6 +3674,9 @@ export class PlaybackManager { } bindMediaSegmentManager(self); + if (!browser.tv && !browser.xboxOne && !browser.ps4) { + this._skipSegment = bindSkipSegment(self); + } } getCurrentPlayer() { @@ -3680,6 +3691,10 @@ export class PlaybackManager { return this.getCurrentTicks(player) / 10000; } + getNextItem() { + return this._playQueueManager.getNextItemInfo(); + } + nextItem(player = this._currentPlayer) { if (player && !enableLocalPlaylistManagement(player)) { return player.nextItem(); diff --git a/src/components/playback/skipbutton.scss b/src/components/playback/skipbutton.scss new file mode 100644 index 0000000000..e3a9e0dc0b --- /dev/null +++ b/src/components/playback/skipbutton.scss @@ -0,0 +1,32 @@ +.skip-button { + display: flex; + align-items: center; + position: fixed; + bottom: 18%; + right: 16%; + z-index: 10000; + padding: 12px 20px; + color: black; + border: none; + border-radius: 100px; + font-weight: bold; + font-size: 1.2em; + transition: opacity 200ms ease-out; + gap: 3px; + box-shadow: 7px 6px 15px -14px rgba(0, 0, 0, 0.65); + cursor: pointer; +} + +@media (orientation: landscape) and (max-height: 500px) { + .skip-button { + bottom: 27%; + } +} + +.no-transition { + transition: none; +} + +.skip-button-hidden { + opacity: 0; +} diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts new file mode 100644 index 0000000000..d4210350a4 --- /dev/null +++ b/src/components/playback/skipsegment.ts @@ -0,0 +1,162 @@ +import { PlaybackManager } from './playbackmanager'; +import { TICKS_PER_MILLISECOND } from 'constants/time'; +import { MediaSegmentDto, MediaSegmentType } from '@jellyfin/sdk/lib/generated-client'; +import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber'; +import { isInSegment } from 'apps/stable/features/playback/utils/mediaSegments'; +import Events, { type Event } from '../../utils/events'; +import { EventType } from 'types/eventType'; +import './skipbutton.scss'; +import dom from 'scripts/dom'; +import globalize from 'lib/globalize'; + +interface ShowOptions { + animate?: boolean; + keep?: boolean; +} + +class SkipSegment extends PlaybackSubscriber { + private skipElement: HTMLButtonElement | undefined; + private currentSegment: MediaSegmentDto | null | undefined; + private hideTimeout: ReturnType | null | undefined; + + constructor(playbackManager: PlaybackManager) { + super(playbackManager); + + this.onOsdChanged = this.onOsdChanged.bind(this); + + Events.on(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged); + } + + onOsdChanged(_e: Event, isOpen: boolean) { + if (this.currentSegment) { + if (isOpen) { + this.showSkipButton({ + animate: false, + keep: true + }); + } else if (!this.hideTimeout) { + this.hideSkipButton(); + } + } + } + + onHideComplete() { + if (this.skipElement) { + this.skipElement.classList.add('hide'); + } + } + + createSkipElement() { + if (!this.skipElement && this.currentSegment) { + const elem = document.createElement('button'); + elem.classList.add('skip-button'); + elem.classList.add('hide'); + elem.classList.add('skip-button-hidden'); + + elem.addEventListener('click', () => { + if (this.currentSegment) { + this.playbackManager.seek(this.currentSegment.EndTicks); + } + }); + + document.body.appendChild(elem); + this.skipElement = elem; + } + } + + setButtonText() { + if (this.skipElement && this.currentSegment) { + if (this.player && this.currentSegment.EndTicks + && this.currentSegment.Type === MediaSegmentType.Outro + && this.currentSegment.EndTicks >= this.playbackManager.currentItem(this.player).RunTimeTicks + && this.playbackManager.getNextItem() + ) { + // Display "Next Episode" if it's an outro segment, exceeds or is equal to the runtime, and if there is a next track. + this.skipElement.innerHTML += globalize.translate('MediaSegmentNextEpisode'); + } else { + this.skipElement.innerHTML = globalize.translate('MediaSegmentSkipPrompt', globalize.translate(`MediaSegmentType.${this.currentSegment.Type}`)); + } + this.skipElement.innerHTML += ''; + } + } + + showSkipButton(options: ShowOptions) { + const elem = this.skipElement; + if (elem) { + this.clearHideTimeout(); + dom.removeEventListener(elem, dom.whichTransitionEvent(), this.onHideComplete, { + once: true + }); + elem.classList.remove('hide'); + if (!options.animate) { + elem.classList.add('no-transition'); + } else { + elem.classList.remove('no-transition'); + } + + void elem.offsetWidth; + + requestAnimationFrame(() => { + elem.classList.remove('skip-button-hidden'); + + if (!options.keep) { + this.hideTimeout = setTimeout(this.hideSkipButton.bind(this), 6000); + } + }); + } + } + + hideSkipButton() { + const elem = this.skipElement; + if (elem) { + elem.classList.remove('no-transition'); + void elem.offsetWidth; + + requestAnimationFrame(() => { + elem.classList.add('skip-button-hidden'); + + dom.addEventListener(elem, dom.whichTransitionEvent(), this.onHideComplete, { + once: true + }); + }); + } + } + + clearHideTimeout() { + if (this.hideTimeout) { + clearTimeout(this.hideTimeout); + this.hideTimeout = null; + } + } + + onPromptSkip(segment: MediaSegmentDto) { + if (!this.currentSegment) { + this.currentSegment = segment; + + this.createSkipElement(); + + this.setButtonText(); + + this.showSkipButton({ animate: true }); + } + } + + onPlayerTimeUpdate() { + if (this.currentSegment) { + const time = this.playbackManager.currentTime(this.player) * TICKS_PER_MILLISECOND; + + if (!isInSegment(this.currentSegment, time)) { + this.currentSegment = null; + this.hideSkipButton(); + } + } + } + + onPlaybackStop() { + this.currentSegment = null; + this.hideSkipButton(); + Events.off(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged); + } +} + +export const bindSkipSegment = (playbackManager: PlaybackManager) => new SkipSegment(playbackManager); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 1adc9794c8..7ec6d03886 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1072,7 +1072,10 @@ "MediaInfoVideoRange": "Video range", "MediaIsBeingConverted": "The media is being converted into a format that is compatible with the device that is playing the media.", "MediaSegmentAction.None": "None", + "MediaSegmentAction.PromptToSkip": "Prompt To Skip", "MediaSegmentAction.Skip": "Skip", + "MediaSegmentNextEpisode": "Next Episode", + "MediaSegmentSkipPrompt": "Skip {0}", "MediaSegmentType.Commercial": "Commercial", "MediaSegmentType.Intro": "Intro", "MediaSegmentType.Outro": "Outro", From dfff2efefc66856de6d90797272511228888a740 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:04:10 +0300 Subject: [PATCH 02/11] Don't show prompt when segment <3s --- .../playback/utils/mediaSegmentManager.ts | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts index 9618652397..d3c28bd04e 100644 --- a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts +++ b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts @@ -37,6 +37,38 @@ class MediaSegmentManager extends PlaybackSubscriber { } } + skipSegment(mediaSegment: MediaSegmentDto) { + // Ignore segment if playback progress has passed the segment's start time + if (mediaSegment.StartTicks !== undefined && this.lastTime > mediaSegment.StartTicks) { + console.info('[MediaSegmentManager] ignoring skipping segment that has been seeked back into', mediaSegment); + this.isLastSegmentIgnored = true; + } else if (mediaSegment.EndTicks) { + // If there is an end time, seek to it + // Do not skip if duration < 1s to avoid slow stream changes + if (mediaSegment.StartTicks && mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND) { + console.info('[MediaSegmentManager] ignoring skipping segment with duration <1s', mediaSegment); + this.isLastSegmentIgnored = true; + return; + } + console.debug('[MediaSegmentManager] skipping to %s ms', mediaSegment.EndTicks / TICKS_PER_MILLISECOND); + this.playbackManager.seek(mediaSegment.EndTicks, this.player); + } else { + // If there is no end time, skip to the next track + console.debug('[MediaSegmentManager] skipping to next item in queue'); + this.playbackManager.nextTrack(this.player); + } + } + + promptToSkip(mediaSegment: MediaSegmentDto) { + if (mediaSegment.StartTicks && mediaSegment.EndTicks + && mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND * 3) { + console.info('[MediaSegmentManager] ignoring skipping segment with duration <3s', mediaSegment); + this.isLastSegmentIgnored = true; + return; + } + this.playbackManager.promptToSkip(mediaSegment); + } + private performAction(mediaSegment: MediaSegmentDto) { if (!this.mediaSegmentTypeActions || !mediaSegment.Type || !this.mediaSegmentTypeActions[mediaSegment.Type]) { console.error('[MediaSegmentManager] segment type missing from action map', mediaSegment, this.mediaSegmentTypeActions); @@ -45,29 +77,9 @@ class MediaSegmentManager extends PlaybackSubscriber { const action = this.mediaSegmentTypeActions[mediaSegment.Type]; if (action === MediaSegmentAction.Skip) { - // Ignore segment if playback progress has passed the segment's start time - if (mediaSegment.StartTicks !== undefined && this.lastTime > mediaSegment.StartTicks) { - console.info('[MediaSegmentManager] ignoring skipping segment that has been seeked back into', mediaSegment); - this.isLastSegmentIgnored = true; - return; - } else if (mediaSegment.EndTicks) { - // If there is an end time, seek to it - // Do not skip if duration < 1s to avoid slow stream changes - if (mediaSegment.StartTicks && mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND) { - console.info('[MediaSegmentManager] ignoring skipping segment with duration <1s', mediaSegment); - this.isLastSegmentIgnored = true; - return; - } - - console.debug('[MediaSegmentManager] skipping to %s ms', mediaSegment.EndTicks / TICKS_PER_MILLISECOND); - this.playbackManager.seek(mediaSegment.EndTicks, this.player); - } else { - // If there is no end time, skip to the next track - console.debug('[MediaSegmentManager] skipping to next item in queue'); - this.playbackManager.nextTrack(this.player); - } + this.skipSegment(mediaSegment); } else if (action === MediaSegmentAction.PromptToSkip) { - this.playbackManager.promptToSkip(mediaSegment); + this.promptToSkip(mediaSegment); } } From 0008f89267557f3d656854284c56c8e75d3eb49e Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:52:05 +0300 Subject: [PATCH 03/11] Fix OSD event after player reset --- src/components/playback/skipsegment.ts | 35 +++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index d4210350a4..fa8762dd48 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -23,21 +23,6 @@ class SkipSegment extends PlaybackSubscriber { super(playbackManager); this.onOsdChanged = this.onOsdChanged.bind(this); - - Events.on(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged); - } - - onOsdChanged(_e: Event, isOpen: boolean) { - if (this.currentSegment) { - if (isOpen) { - this.showSkipButton({ - animate: false, - keep: true - }); - } else if (!this.hideTimeout) { - this.hideSkipButton(); - } - } } onHideComplete() { @@ -129,6 +114,19 @@ class SkipSegment extends PlaybackSubscriber { } } + onOsdChanged(_e: Event, isOpen: boolean) { + if (this.currentSegment) { + if (isOpen) { + this.showSkipButton({ + animate: false, + keep: true + }); + } else if (!this.hideTimeout) { + this.hideSkipButton(); + } + } + } + onPromptSkip(segment: MediaSegmentDto) { if (!this.currentSegment) { this.currentSegment = segment; @@ -152,6 +150,13 @@ class SkipSegment extends PlaybackSubscriber { } } + onPlayerChange(): void { + if (this.playbackManager.getCurrentPlayer()) { + Events.off(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged); + Events.on(document, EventType.SHOW_VIDEO_OSD, this.onOsdChanged); + } + } + onPlaybackStop() { this.currentSegment = null; this.hideSkipButton(); From 198d4b157c1fd4bd83dd94c91744d4c54c1af6e3 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:09:54 +0300 Subject: [PATCH 04/11] Rename to ask to skip --- .../stable/features/playback/constants/mediaSegmentAction.ts | 2 +- src/apps/stable/features/playback/utils/mediaSegmentManager.ts | 2 +- src/strings/en-us.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apps/stable/features/playback/constants/mediaSegmentAction.ts b/src/apps/stable/features/playback/constants/mediaSegmentAction.ts index 87b54f4bce..86a6a759f4 100644 --- a/src/apps/stable/features/playback/constants/mediaSegmentAction.ts +++ b/src/apps/stable/features/playback/constants/mediaSegmentAction.ts @@ -3,6 +3,6 @@ */ export enum MediaSegmentAction { None = 'None', - PromptToSkip = 'PromptToSkip', + AskToSkip = 'AskToSkip', Skip = 'Skip' } diff --git a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts index d3c28bd04e..faf75f9c7f 100644 --- a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts +++ b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts @@ -78,7 +78,7 @@ class MediaSegmentManager extends PlaybackSubscriber { const action = this.mediaSegmentTypeActions[mediaSegment.Type]; if (action === MediaSegmentAction.Skip) { this.skipSegment(mediaSegment); - } else if (action === MediaSegmentAction.PromptToSkip) { + } else if (action === MediaSegmentAction.AskToSkip) { this.promptToSkip(mediaSegment); } } diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 7ec6d03886..d06e779771 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1072,7 +1072,7 @@ "MediaInfoVideoRange": "Video range", "MediaIsBeingConverted": "The media is being converted into a format that is compatible with the device that is playing the media.", "MediaSegmentAction.None": "None", - "MediaSegmentAction.PromptToSkip": "Prompt To Skip", + "MediaSegmentAction.AskToSkip": "Ask To Skip", "MediaSegmentAction.Skip": "Skip", "MediaSegmentNextEpisode": "Next Episode", "MediaSegmentSkipPrompt": "Skip {0}", From 64b9ab2a19d0ce14dd9c14e02c212b0598e04931 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:10:13 +0300 Subject: [PATCH 05/11] Keep button for 8 seconds --- src/components/playback/skipsegment.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index fa8762dd48..b3c50daf9a 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -85,7 +85,7 @@ class SkipSegment extends PlaybackSubscriber { elem.classList.remove('skip-button-hidden'); if (!options.keep) { - this.hideTimeout = setTimeout(this.hideSkipButton.bind(this), 6000); + this.hideTimeout = setTimeout(this.hideSkipButton.bind(this), 8000); } }); } From 7ad7f07b7c793ab0c7b45d83fadced17eee1bb24 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:20:39 +0300 Subject: [PATCH 06/11] Do nothing if there is 1 second left --- src/components/playback/skipsegment.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index b3c50daf9a..10d9e32248 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -1,5 +1,5 @@ import { PlaybackManager } from './playbackmanager'; -import { TICKS_PER_MILLISECOND } from 'constants/time'; +import { TICKS_PER_MILLISECOND, TICKS_PER_SECOND } from 'constants/time'; import { MediaSegmentDto, MediaSegmentType } from '@jellyfin/sdk/lib/generated-client'; import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber'; import { isInSegment } from 'apps/stable/features/playback/utils/mediaSegments'; @@ -39,8 +39,13 @@ class SkipSegment extends PlaybackSubscriber { elem.classList.add('skip-button-hidden'); elem.addEventListener('click', () => { - if (this.currentSegment) { - this.playbackManager.seek(this.currentSegment.EndTicks); + const time = this.playbackManager.currentTime() * TICKS_PER_MILLISECOND; + if (this.currentSegment?.EndTicks) { + if (time < this.currentSegment.EndTicks - TICKS_PER_SECOND) { + this.playbackManager.seek(this.currentSegment.EndTicks); + } else { + this.hideSkipButton(); + } } }); From 9b269296fe565c6a6590a1ab01ac05fefcd567ee Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:08:27 +0300 Subject: [PATCH 07/11] Use UpNextDialog for outro segments --- .../playback/constants/playerEvent.ts | 1 + .../playback/utils/mediaSegmentManager.ts | 2 +- .../playback/utils/playbackSubscriber.ts | 3 +++ src/components/playback/playbackmanager.js | 6 ++++-- src/components/playback/skipsegment.ts | 21 +++++++++---------- src/controllers/playback/video/index.js | 18 +++++++++++++--- src/strings/en-us.json | 1 - 7 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/apps/stable/features/playback/constants/playerEvent.ts b/src/apps/stable/features/playback/constants/playerEvent.ts index c236475383..ad9d558a13 100644 --- a/src/apps/stable/features/playback/constants/playerEvent.ts +++ b/src/apps/stable/features/playback/constants/playerEvent.ts @@ -14,6 +14,7 @@ export enum PlayerEvent { PlaylistItemAdd = 'playlistitemadd', PlaylistItemMove = 'playlistitemmove', PlaylistItemRemove = 'playlistitemremove', + PromptSkip = 'promptskip', RepeatModeChange = 'repeatmodechange', ShuffleModeChange = 'shufflequeuemodechange', Stopped = 'stopped', diff --git a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts index faf75f9c7f..6a79838aa8 100644 --- a/src/apps/stable/features/playback/utils/mediaSegmentManager.ts +++ b/src/apps/stable/features/playback/utils/mediaSegmentManager.ts @@ -62,7 +62,7 @@ class MediaSegmentManager extends PlaybackSubscriber { promptToSkip(mediaSegment: MediaSegmentDto) { if (mediaSegment.StartTicks && mediaSegment.EndTicks && mediaSegment.EndTicks - mediaSegment.StartTicks < TICKS_PER_SECOND * 3) { - console.info('[MediaSegmentManager] ignoring skipping segment with duration <3s', mediaSegment); + console.info('[MediaSegmentManager] ignoring segment prompt with duration <3s', mediaSegment); this.isLastSegmentIgnored = true; return; } diff --git a/src/apps/stable/features/playback/utils/playbackSubscriber.ts b/src/apps/stable/features/playback/utils/playbackSubscriber.ts index d492469f71..ccbbfb2c00 100644 --- a/src/apps/stable/features/playback/utils/playbackSubscriber.ts +++ b/src/apps/stable/features/playback/utils/playbackSubscriber.ts @@ -11,6 +11,7 @@ import Events, { type Event } from 'utils/events'; import { PlaybackManagerEvent } from '../constants/playbackManagerEvent'; import { PlayerEvent } from '../constants/playerEvent'; import type { ManagedPlayerStopInfo, MovedItem, PlayerError, PlayerErrorCode, PlayerStopInfo, RemovedItems } from '../types/callbacks'; +import { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client'; export interface PlaybackSubscriber { onPlaybackCancelled?(e: Event): void @@ -18,6 +19,7 @@ export interface PlaybackSubscriber { onPlaybackStart?(e: Event, player: Plugin, state: PlayerState): void onPlaybackStop?(e: Event, info: PlaybackStopInfo): void onPlayerChange?(e: Event, player: Plugin, target: PlayTarget, previousPlayer: Plugin): void + onPromptSkip?(e: Event, mediaSegment: MediaSegmentDto): void onPlayerError?(e: Event, error: PlayerError): void onPlayerFullscreenChange?(e: Event): void onPlayerItemStarted?(e: Event, item?: BaseItemDto, mediaSource?: MediaSourceInfo): void @@ -62,6 +64,7 @@ export abstract class PlaybackSubscriber { [PlayerEvent.PlaylistItemAdd]: this.onPlayerPlaylistItemAdd?.bind(this), [PlayerEvent.PlaylistItemMove]: this.onPlayerPlaylistItemMove?.bind(this), [PlayerEvent.PlaylistItemRemove]: this.onPlayerPlaylistItemRemove?.bind(this), + [PlayerEvent.PromptSkip]: this.onPromptSkip?.bind(this), [PlayerEvent.RepeatModeChange]: this.onPlayerRepeatModeChange?.bind(this), [PlayerEvent.ShuffleModeChange]: this.onPlayerShuffleModeChange?.bind(this), [PlayerEvent.Stopped]: this.onPlayerStopped?.bind(this), diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index a7a5ca4bb9..35001ad9da 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -935,9 +935,11 @@ export class PlaybackManager { return Promise.resolve(self._playQueueManager.getPlaylist()); }; - self.promptToSkip = function (mediaSegment) { + self.promptToSkip = function (mediaSegment, player) { + player = player || self._currentPlayer; + if (mediaSegment && this._skipSegment) { - this._skipSegment.onPromptSkip(mediaSegment); + Events.trigger(player, 'promptskip', [mediaSegment]); } }; diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index 10d9e32248..9169e81b8c 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -56,16 +56,7 @@ class SkipSegment extends PlaybackSubscriber { setButtonText() { if (this.skipElement && this.currentSegment) { - if (this.player && this.currentSegment.EndTicks - && this.currentSegment.Type === MediaSegmentType.Outro - && this.currentSegment.EndTicks >= this.playbackManager.currentItem(this.player).RunTimeTicks - && this.playbackManager.getNextItem() - ) { - // Display "Next Episode" if it's an outro segment, exceeds or is equal to the runtime, and if there is a next track. - this.skipElement.innerHTML += globalize.translate('MediaSegmentNextEpisode'); - } else { - this.skipElement.innerHTML = globalize.translate('MediaSegmentSkipPrompt', globalize.translate(`MediaSegmentType.${this.currentSegment.Type}`)); - } + this.skipElement.innerHTML = globalize.translate('MediaSegmentSkipPrompt', globalize.translate(`MediaSegmentType.${this.currentSegment.Type}`)); this.skipElement.innerHTML += ''; } } @@ -132,7 +123,15 @@ class SkipSegment extends PlaybackSubscriber { } } - onPromptSkip(segment: MediaSegmentDto) { + onPromptSkip(e: Event, segment: MediaSegmentDto) { + if (this.player && segment.EndTicks != null + && segment.Type === MediaSegmentType.Outro + && segment.EndTicks >= this.playbackManager.currentItem(this.player).RunTimeTicks + && this.playbackManager.getNextItem() + ) { + // Don't display button when UpNextDialog is expected. + return; + } if (!this.currentSegment) { this.currentSegment = segment; diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 779b68f9f6..fb7cad1f9a 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -29,9 +29,8 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components import { pluginManager } from '../../../components/pluginManager'; import { PluginType } from '../../../types/plugin.ts'; import { EventType } from 'types/eventType'; - -const TICKS_PER_MINUTE = 600000000; -const TICKS_PER_SECOND = 10000000; +import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client'; +import { TICKS_PER_MINUTE, TICKS_PER_SECOND } from 'constants/time'; function getOpenedDialog() { return document.querySelector('.dialogContainer .dialog.opened'); @@ -579,6 +578,7 @@ export default function (view) { }, state); Events.on(player, 'playbackstart', onPlaybackStart); Events.on(player, 'playbackstop', onPlaybackStopped); + Events.on(player, 'promptskip', onPromptSkip); Events.on(player, 'volumechange', onVolumeChanged); Events.on(player, 'pause', onPlayPauseStateChanged); Events.on(player, 'unpause', onPlayPauseStateChanged); @@ -603,6 +603,7 @@ export default function (view) { if (player) { Events.off(player, 'playbackstart', onPlaybackStart); Events.off(player, 'playbackstop', onPlaybackStopped); + Events.off(player, 'promptskip', onPromptSkip); Events.off(player, 'volumechange', onVolumeChanged); Events.off(player, 'pause', onPlayPauseStateChanged); Events.off(player, 'unpause', onPlayPauseStateChanged); @@ -631,6 +632,17 @@ export default function (view) { } } + function onPromptSkip(e, mediaSegment) { + const player = this; + if (mediaSegment && player && mediaSegment.EndTicks != null + && mediaSegment.Type === MediaSegmentType.Outro + && mediaSegment.EndTicks >= playbackManager.duration(player) + && playbackManager.getNextItem() + ) { + showComingUpNext(player); + } + } + function showComingUpNextIfNeeded(player, currentItem, currentTimeTicks, runtimeTicks) { if (runtimeTicks && currentTimeTicks && !comingUpNextDisplayed && !currentVisibleMenu && currentItem.Type === 'Episode' && userSettings.enableNextVideoInfoOverlay()) { let showAtSecondsLeft = 30; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index d06e779771..c58225bc23 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1074,7 +1074,6 @@ "MediaSegmentAction.None": "None", "MediaSegmentAction.AskToSkip": "Ask To Skip", "MediaSegmentAction.Skip": "Skip", - "MediaSegmentNextEpisode": "Next Episode", "MediaSegmentSkipPrompt": "Skip {0}", "MediaSegmentType.Commercial": "Commercial", "MediaSegmentType.Intro": "Intro", From 87f2acaf4016f2f083b7042dbeec1c261994adfd Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:34:52 +0300 Subject: [PATCH 08/11] Fix import --- src/apps/stable/features/playback/utils/playbackSubscriber.ts | 2 +- src/components/playback/skipsegment.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/apps/stable/features/playback/utils/playbackSubscriber.ts b/src/apps/stable/features/playback/utils/playbackSubscriber.ts index ccbbfb2c00..830d3bc8ee 100644 --- a/src/apps/stable/features/playback/utils/playbackSubscriber.ts +++ b/src/apps/stable/features/playback/utils/playbackSubscriber.ts @@ -11,7 +11,7 @@ import Events, { type Event } from 'utils/events'; import { PlaybackManagerEvent } from '../constants/playbackManagerEvent'; import { PlayerEvent } from '../constants/playerEvent'; import type { ManagedPlayerStopInfo, MovedItem, PlayerError, PlayerErrorCode, PlayerStopInfo, RemovedItems } from '../types/callbacks'; -import { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client'; +import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto'; export interface PlaybackSubscriber { onPlaybackCancelled?(e: Event): void diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index 9169e81b8c..c6b78af9d3 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -1,6 +1,7 @@ import { PlaybackManager } from './playbackmanager'; import { TICKS_PER_MILLISECOND, TICKS_PER_SECOND } from 'constants/time'; -import { MediaSegmentDto, MediaSegmentType } from '@jellyfin/sdk/lib/generated-client'; +import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client/models/media-segment-type'; +import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto'; import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber'; import { isInSegment } from 'apps/stable/features/playback/utils/mediaSegments'; import Events, { type Event } from '../../utils/events'; From 11e4549b487b2fb643b877672c7255de2174b550 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:37:38 +0300 Subject: [PATCH 09/11] Use PlayerEvent constant --- src/components/playback/playbackmanager.js | 3 ++- src/controllers/playback/video/index.js | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index b50e3fa04e..25b3e32a73 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -22,6 +22,7 @@ import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts'; import { getItemBackdropImageUrl } from '../../utils/jellyfin-apiclient/backdropImage'; import { bindMediaSegmentManager } from 'apps/stable/features/playback/utils/mediaSegmentManager'; +import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent'; import { MediaError } from 'types/mediaError'; import { getMediaError } from 'utils/mediaError'; import { toApi } from 'utils/jellyfin-apiclient/compat'; @@ -939,7 +940,7 @@ export class PlaybackManager { player = player || self._currentPlayer; if (mediaSegment && this._skipSegment) { - Events.trigger(player, 'promptskip', [mediaSegment]); + Events.trigger(player, PlayerEvent.PromptSkip, [mediaSegment]); } }; diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 740b80afb0..0d230a5c32 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -31,6 +31,7 @@ import { PluginType } from '../../../types/plugin.ts'; import { EventType } from 'types/eventType'; import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client'; import { TICKS_PER_MINUTE, TICKS_PER_SECOND } from 'constants/time'; +import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent'; function getOpenedDialog() { return document.querySelector('.dialogContainer .dialog.opened'); @@ -578,7 +579,7 @@ export default function (view) { }, state); Events.on(player, 'playbackstart', onPlaybackStart); Events.on(player, 'playbackstop', onPlaybackStopped); - Events.on(player, 'promptskip', onPromptSkip); + Events.on(player, PlayerEvent.PromptSkip, onPromptSkip); Events.on(player, 'volumechange', onVolumeChanged); Events.on(player, 'pause', onPlayPauseStateChanged); Events.on(player, 'unpause', onPlayPauseStateChanged); @@ -603,7 +604,7 @@ export default function (view) { if (player) { Events.off(player, 'playbackstart', onPlaybackStart); Events.off(player, 'playbackstop', onPlaybackStopped); - Events.off(player, 'promptskip', onPromptSkip); + Events.off(player, PlayerEvent.PromptSkip, onPromptSkip); Events.off(player, 'volumechange', onVolumeChanged); Events.off(player, 'pause', onPlayPauseStateChanged); Events.off(player, 'unpause', onPlayPauseStateChanged); From d8db0ba226971073296c7af066723a87afabb0e3 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Fri, 25 Oct 2024 20:58:37 +0300 Subject: [PATCH 10/11] Remove check for outro --- src/components/playback/skipsegment.ts | 1 - src/controllers/playback/video/index.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index c6b78af9d3..e0f2ae81e1 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -126,7 +126,6 @@ class SkipSegment extends PlaybackSubscriber { onPromptSkip(e: Event, segment: MediaSegmentDto) { if (this.player && segment.EndTicks != null - && segment.Type === MediaSegmentType.Outro && segment.EndTicks >= this.playbackManager.currentItem(this.player).RunTimeTicks && this.playbackManager.getNextItem() ) { diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 0d230a5c32..0601e504ed 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -636,7 +636,6 @@ export default function (view) { function onPromptSkip(e, mediaSegment) { const player = this; if (mediaSegment && player && mediaSegment.EndTicks != null - && mediaSegment.Type === MediaSegmentType.Outro && mediaSegment.EndTicks >= playbackManager.duration(player) && playbackManager.getNextItem() ) { From 8cc23f28826f72cd0f40c8cab14cb3f0ae197208 Mon Sep 17 00:00:00 2001 From: viown <48097677+viown@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:01:02 +0300 Subject: [PATCH 11/11] Remove undefined MediaSegmentType --- src/components/playback/skipsegment.ts | 1 - src/controllers/playback/video/index.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/playback/skipsegment.ts b/src/components/playback/skipsegment.ts index e0f2ae81e1..458297d682 100644 --- a/src/components/playback/skipsegment.ts +++ b/src/components/playback/skipsegment.ts @@ -1,6 +1,5 @@ import { PlaybackManager } from './playbackmanager'; import { TICKS_PER_MILLISECOND, TICKS_PER_SECOND } from 'constants/time'; -import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client/models/media-segment-type'; import type { MediaSegmentDto } from '@jellyfin/sdk/lib/generated-client/models/media-segment-dto'; import { PlaybackSubscriber } from 'apps/stable/features/playback/utils/playbackSubscriber'; import { isInSegment } from 'apps/stable/features/playback/utils/mediaSegments'; diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 0601e504ed..df0dde9bb8 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -29,7 +29,6 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components import { pluginManager } from '../../../components/pluginManager'; import { PluginType } from '../../../types/plugin.ts'; import { EventType } from 'types/eventType'; -import { MediaSegmentType } from '@jellyfin/sdk/lib/generated-client'; import { TICKS_PER_MINUTE, TICKS_PER_SECOND } from 'constants/time'; import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent';