fix: correct jellyfin trickplay sheet geometry

close #1417
This commit is contained in:
edde746
2026-06-26 00:26:26 +02:00
parent f04691d321
commit f29030c3a0
2 changed files with 15 additions and 21 deletions
+7 -14
View File
@@ -1,4 +1,3 @@
import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:cached_network_image_ce/cached_network_image.dart';
@@ -77,9 +76,8 @@ class JellyfinTrickplayService implements ScrubPreviewSource {
} }
/// Pure math helper exposed for unit tests: maps a timestamp to the /// Pure math helper exposed for unit tests: maps a timestamp to the
/// sheet index, tile coordinates within the sheet, and the sheet's /// sheet index, tile coordinates within the sheet, and the sheet's full
/// (possibly partial) row/column count. Returns `null` only when the /// row/column count. Returns `null` only when the manifest is degenerate.
/// manifest is degenerate.
TrickplayTileLocation? tileLocationFor(Duration time) { TrickplayTileLocation? tileLocationFor(Duration time) {
if (_disposed) return null; if (_disposed) return null;
final info = _info; final info = _info;
@@ -94,17 +92,12 @@ class JellyfinTrickplayService implements ScrubPreviewSource {
final tileColumn = tileInSheet % info.tileWidth; final tileColumn = tileInSheet % info.tileWidth;
final tileRow = tileInSheet ~/ info.tileWidth; final tileRow = tileInSheet ~/ info.tileWidth;
final firstThumbInSheet = sheetIndex * tilesPerSheet;
final thumbsInSheet = math.min(tilesPerSheet, info.thumbnailCount - firstThumbInSheet);
final sheetColumns = thumbsInSheet >= info.tileWidth ? info.tileWidth : thumbsInSheet;
final sheetRows = (thumbsInSheet + info.tileWidth - 1) ~/ info.tileWidth;
return TrickplayTileLocation( return TrickplayTileLocation(
sheetIndex: sheetIndex, sheetIndex: sheetIndex,
tileColumn: tileColumn, tileColumn: tileColumn,
tileRow: tileRow, tileRow: tileRow,
sheetColumns: sheetColumns, sheetColumns: info.tileWidth,
sheetRows: sheetRows, sheetRows: info.tileHeight,
sourceTileSize: Size(info.width.toDouble(), info.height.toDouble()), sourceTileSize: Size(info.width.toDouble(), info.height.toDouble()),
); );
} }
@@ -176,9 +169,9 @@ class JellyfinTrickplayService implements ScrubPreviewSource {
} }
/// Pure data: which sheet to fetch, which tile within it to display, the /// Pure data: which sheet to fetch, which tile within it to display, the
/// sheet's (possibly partial) row/column count, and the source tile's /// sheet's row/column count, and the source tile's pixel dimensions for
/// pixel dimensions for aspect-correct scaling at render time. Returned /// aspect-correct scaling at render time. Returned by
/// by [JellyfinTrickplayService.tileLocationFor] for unit tests. /// [JellyfinTrickplayService.tileLocationFor] for unit tests.
class TrickplayTileLocation { class TrickplayTileLocation {
final int sheetIndex; final int sheetIndex;
final int tileColumn; final int tileColumn;
@@ -95,7 +95,7 @@ void main() {
// 250 thumbnails, 1s apart, 10×10 tiles per sheet ⇒ 3 sheets. // 250 thumbnails, 1s apart, 10×10 tiles per sheet ⇒ 3 sheets.
// sheet 0: indices 0..99 // sheet 0: indices 0..99
// sheet 1: indices 100..199 (full) // sheet 1: indices 100..199 (full)
// sheet 2: indices 200..249 (last sheet, only 50 thumbs ⇒ 5 rows) // sheet 2: indices 200..249 (last sheet, padded to a full 10x10 grid)
setUp(() async { setUp(() async {
client = await JellyfinClient.create(_conn()); client = await JellyfinClient.create(_conn());
@@ -143,9 +143,10 @@ void main() {
expect(loc.sheetIndex, 2); expect(loc.sheetIndex, 2);
expect(loc.tileColumn, 9); expect(loc.tileColumn, 9);
expect(loc.tileRow, 4); expect(loc.tileRow, 4);
// Sheet 2 has 50 thumbs ⇒ 5 rows, full 10 cols // Jellyfin pads the final tile image to the full configured grid, so
// the renderer must keep full sheet geometry even for partial sheets.
expect(loc.sheetColumns, 10); expect(loc.sheetColumns, 10);
expect(loc.sheetRows, 5); expect(loc.sheetRows, 10);
}); });
test('clamps past the end to the last available thumbnail', () { test('clamps past the end to the last available thumbnail', () {
@@ -168,7 +169,7 @@ void main() {
}); });
}); });
group('JellyfinTrickplayService — partial last sheet sizing', () { group('JellyfinTrickplayService — padded last sheet sizing', () {
late JellyfinClient client; late JellyfinClient client;
setUp(() async { setUp(() async {
@@ -177,7 +178,7 @@ void main() {
tearDown(() => client.close()); tearDown(() => client.close());
test('last sheet with 1 thumbnail reports 1 col × 1 row', () { test('last sheet with 1 thumbnail still reports the full sheet grid', () {
// 17 thumbs, 4×4 sheet ⇒ sheet 0 full (16), sheet 1 has 1 thumb only. // 17 thumbs, 4×4 sheet ⇒ sheet 0 full (16), sheet 1 has 1 thumb only.
final svc = JellyfinTrickplayService.create( final svc = JellyfinTrickplayService.create(
client: client, client: client,
@@ -192,8 +193,8 @@ void main() {
expect(loc.sheetIndex, 1); expect(loc.sheetIndex, 1);
expect(loc.tileColumn, 0); expect(loc.tileColumn, 0);
expect(loc.tileRow, 0); expect(loc.tileRow, 0);
expect(loc.sheetColumns, 1); expect(loc.sheetColumns, 4);
expect(loc.sheetRows, 1); expect(loc.sheetRows, 4);
}); });
}); });