Skip to content

Commit

Permalink
Add 'prompt to skip' to media segments
Browse files Browse the repository at this point in the history
  • Loading branch information
viown committed Oct 15, 2024
1 parent 82d963b commit 4c97e9c
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
*/
export enum MediaSegmentAction {
None = 'None',
PromptToSkip = 'PromptToSkip',
Skip = 'Skip'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/apps/stable/features/playback/utils/mediaSegments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions src/components/playback/playbackmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -3666,6 +3674,9 @@ export class PlaybackManager {
}

bindMediaSegmentManager(self);
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
this._skipSegment = bindSkipSegment(self);
}
}

getCurrentPlayer() {
Expand All @@ -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();
Expand Down
32 changes: 32 additions & 0 deletions src/components/playback/skipbutton.scss
Original file line number Diff line number Diff line change
@@ -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;
}
162 changes: 162 additions & 0 deletions src/components/playback/skipsegment.ts
Original file line number Diff line number Diff line change
@@ -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<typeof setTimeout> | 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 += '<span class="material-icons skip_next" aria-hidden="true"></span>';
}
}

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);
3 changes: 3 additions & 0 deletions src/strings/en-us.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 4c97e9c

Please sign in to comment.