From 3c4cef0f2fedd7417333ba0584c86637a66bd7ab Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sat, 8 Nov 2025 19:05:43 +0100 Subject: [PATCH 01/11] add scrub controls --- src/controllers/playback/video/index.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index e3f3d3733aa3..cb81b84d9c20 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -1310,6 +1310,18 @@ export default function (view) { showOsd(btnFastForward); } break; + case ',': + if (!e.shiftKey) { + e.preventDefault(); + playbackManager.seekRelative(currentPlayer, 1); + } + break; + case '.': + if (!e.shiftKey) { + e.preventDefault(); + playbackManager.seekRelative(currentPlayer, -1); + } + break; case 'j': case 'J': case 'ArrowLeft': From b65f95036745ef61707a2726ef1f03df265fed46 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sat, 8 Nov 2025 19:08:02 +0100 Subject: [PATCH 02/11] fix direction --- src/controllers/playback/video/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index cb81b84d9c20..85d43cb3526e 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -1313,13 +1313,13 @@ export default function (view) { case ',': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(currentPlayer, 1); + playbackManager.seekRelative(currentPlayer, -1); } break; case '.': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(currentPlayer, -1); + playbackManager.seekRelative(currentPlayer, 1); } break; case 'j': From bedceb362c09e4a3677f2abcd75ff221b4511507 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sat, 8 Nov 2025 19:18:24 +0100 Subject: [PATCH 03/11] fix order --- src/controllers/playback/video/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 85d43cb3526e..3afb4d8f1c7f 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -1313,13 +1313,13 @@ export default function (view) { case ',': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(currentPlayer, -1); + playbackManager.seekRelative(-1, currentPlayer); } break; case '.': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(currentPlayer, 1); + playbackManager.seekRelative(1, currentPlayer); } break; case 'j': From 6023a198d0317e066a0339c985cf480b8ab5e071 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sat, 8 Nov 2025 20:09:02 +0100 Subject: [PATCH 04/11] hardcode 25fps for now --- src/controllers/playback/video/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 3afb4d8f1c7f..4a841d10c1d5 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -1313,13 +1313,13 @@ export default function (view) { case ',': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(-1, currentPlayer); + playbackManager.seekRelative(-4e4, currentPlayer); } break; case '.': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(1, currentPlayer); + playbackManager.seekRelative(4e4, currentPlayer); } break; case 'j': From 373f30d79d9e708802f1e4df7ccb00334965ccba Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 9 Nov 2025 14:56:52 +0100 Subject: [PATCH 05/11] get fps --- src/components/playback/playbackmanager.js | 18 ++++++++++++++++++ src/controllers/playback/video/index.js | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index f9c1f2512f69..9bd4ef1f594e 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1345,6 +1345,10 @@ export class PlaybackManager { } }; + self.getFps = function (player) { + self.currentMediaSource(player).MediaStreams.find(s => s.Type === "Video")?.RealFrameRate + } + function getSavedMaxStreamingBitrate(apiClient, mediaType) { if (!apiClient) { // This should hopefully never happen @@ -3885,6 +3889,20 @@ export class PlaybackManager { this.seekRelative(offsetTicks, player); } + nextFrame(player = this._currentPlayer) { + const fps = this.getFps(player); + if (fps != null) { + this.seekRelative(1e6 / fps, player); + } + } + + previousFrame(player = this._currentPlayer) { + const fps = this.getFps(player); + if (fps != null) { + this.seekRelative(-1e6 / fps, player); + } + } + seekPercent(percent, player = this._currentPlayer) { let ticks = this.duration(player) || 0; diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 4a841d10c1d5..2f52aed1c326 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -1313,13 +1313,13 @@ export default function (view) { case ',': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(-4e4, currentPlayer); + playbackManager.previousFrame(currentPlayer); } break; case '.': if (!e.shiftKey) { e.preventDefault(); - playbackManager.seekRelative(4e4, currentPlayer); + playbackManager.nextFrame(currentPlayer); } break; case 'j': From 60469d524d9bef458ec7f97b38c5f684a0b9a97b Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 9 Nov 2025 15:03:11 +0100 Subject: [PATCH 06/11] style --- src/components/playback/playbackmanager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 9bd4ef1f594e..de425de02079 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1346,8 +1346,8 @@ export class PlaybackManager { }; self.getFps = function (player) { - self.currentMediaSource(player).MediaStreams.find(s => s.Type === "Video")?.RealFrameRate - } + return self.currentMediaSource(player).MediaStreams.find(s => s.Type === 'Video')?.RealFrameRate; + }; function getSavedMaxStreamingBitrate(apiClient, mediaType) { if (!apiClient) { From a4d1bac7a38e19fc4b63e45737b53ff2ab1ac11e Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 9 Nov 2025 15:17:22 +0100 Subject: [PATCH 07/11] fix exponent --- src/components/playback/playbackmanager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index de425de02079..0dbbf0c2e950 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -3892,14 +3892,14 @@ export class PlaybackManager { nextFrame(player = this._currentPlayer) { const fps = this.getFps(player); if (fps != null) { - this.seekRelative(1e6 / fps, player); + this.seekRelative(1e5 / fps, player); } } previousFrame(player = this._currentPlayer) { const fps = this.getFps(player); if (fps != null) { - this.seekRelative(-1e6 / fps, player); + this.seekRelative(-1e5 / fps, player); } } From 62e82fc4ecdd13d9af3c798bf0c2a59ebc549636 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 9 Nov 2025 15:29:57 +0100 Subject: [PATCH 08/11] oops --- src/components/playback/playbackmanager.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 0dbbf0c2e950..46f49be09fe6 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -3889,17 +3889,18 @@ export class PlaybackManager { this.seekRelative(offsetTicks, player); } + // tick = ⅒ns, so ticks/frame = 1e10 ns / 1e5 * nextFrame(player = this._currentPlayer) { const fps = this.getFps(player); if (fps != null) { - this.seekRelative(1e5 / fps, player); + this.seekRelative(1e7 / fps, player); } } previousFrame(player = this._currentPlayer) { const fps = this.getFps(player); if (fps != null) { - this.seekRelative(-1e5 / fps, player); + this.seekRelative(-1e7 / fps, player); } } From 2198a66007fadb83d6e48410b4f1d65a332e9131 Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 9 Nov 2025 15:33:56 +0100 Subject: [PATCH 09/11] remove comment --- src/components/playback/playbackmanager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 46f49be09fe6..96356d946a67 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -3889,7 +3889,6 @@ export class PlaybackManager { this.seekRelative(offsetTicks, player); } - // tick = ⅒ns, so ticks/frame = 1e10 ns / 1e5 * nextFrame(player = this._currentPlayer) { const fps = this.getFps(player); if (fps != null) { From 1b70b814626ac4c1f497f25c4c6c389e7c7bcd0a Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sun, 9 Nov 2025 15:58:03 +0100 Subject: [PATCH 10/11] use constant --- src/components/playback/playbackmanager.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 96356d946a67..4f5f6b3450a8 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -27,6 +27,7 @@ import { PlayerEvent } from 'apps/stable/features/playback/constants/playerEvent import { bindMediaSegmentManager } from 'apps/stable/features/playback/utils/mediaSegmentManager'; import { bindMediaSessionSubscriber } from 'apps/stable/features/playback/utils/mediaSessionSubscriber'; import { AppFeature } from 'constants/appFeature'; +import { TICKS_PER_SECOND } from 'constants/time'; import { ServerConnections } from 'lib/jellyfin-apiclient'; import { MediaError } from 'types/mediaError'; import { getMediaError } from 'utils/mediaError'; @@ -3892,14 +3893,14 @@ export class PlaybackManager { nextFrame(player = this._currentPlayer) { const fps = this.getFps(player); if (fps != null) { - this.seekRelative(1e7 / fps, player); + this.seekRelative(TICKS_PER_SECOND / fps, player); } } previousFrame(player = this._currentPlayer) { const fps = this.getFps(player); if (fps != null) { - this.seekRelative(-1e7 / fps, player); + this.seekRelative(-TICKS_PER_SECOND / fps, player); } } From 148bcd7138fb23f15038e7ac11e53b26c626300b Mon Sep 17 00:00:00 2001 From: "Philipp A." Date: Sat, 6 Dec 2025 14:10:35 +0100 Subject: [PATCH 11/11] Apply suggestions from code review Co-authored-by: Bill Thornton --- src/components/playback/playbackmanager.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 0ac75f2057bf..1cbdf1d3a345 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1347,7 +1347,7 @@ export class PlaybackManager { }; self.getFps = function (player) { - return self.currentMediaSource(player).MediaStreams.find(s => s.Type === 'Video')?.RealFrameRate; + return self.currentMediaSource(player).MediaStreams.find(s => s.Type === 'Video')?.ReferenceFrameRate; }; function getSavedMaxStreamingBitrate(apiClient, mediaType) { @@ -3891,14 +3891,14 @@ export class PlaybackManager { nextFrame(player = this._currentPlayer) { const fps = this.getFps(player); - if (fps != null) { + if (fps) { this.seekRelative(TICKS_PER_SECOND / fps, player); } } previousFrame(player = this._currentPlayer) { const fps = this.getFps(player); - if (fps != null) { + if (fps) { this.seekRelative(-TICKS_PER_SECOND / fps, player); } }