Fix BIF scrub preview aspect ratio and raise desktop size limit

This commit is contained in:
Micah Morrison
2026-05-28 15:46:08 -04:00
parent 4217da1a8f
commit 9cc32e125b
5 changed files with 17 additions and 7 deletions
+4
View File
@@ -25,6 +25,9 @@ class MediaSourceInfo {
/// null here and uses [partId] + the BIF service instead. /// null here and uses [partId] + the BIF service instead.
final Map<int, TrickplayInfo>? trickplayByWidth; final Map<int, TrickplayInfo>? trickplayByWidth;
/// Display aspect ratio of the video stream (width / height).
final double? videoAspectRatio;
MediaSourceInfo({ MediaSourceInfo({
required this.videoUrl, required this.videoUrl,
required this.audioTracks, required this.audioTracks,
@@ -36,6 +39,7 @@ class MediaSourceInfo {
this.defaultAudioStreamIndex, this.defaultAudioStreamIndex,
this.defaultSubtitleStreamIndex, this.defaultSubtitleStreamIndex,
this.trickplayByWidth, this.trickplayByWidth,
this.videoAspectRatio,
}); });
int? getPartId() => partId; int? getPartId() => partId;
} }
+10 -3
View File
@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'plex_client.dart'; import 'plex_client.dart';
import 'scrub_preview_source.dart'; import 'scrub_preview_source.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/platform_detector.dart';
/// A single BIF thumbnail entry: timestamp in milliseconds + JPEG bytes. /// A single BIF thumbnail entry: timestamp in milliseconds + JPEG bytes.
typedef BifEntry = ({int timestampMs, Uint8List imageBytes}); typedef BifEntry = ({int timestampMs, Uint8List imageBytes});
@@ -66,14 +67,20 @@ List<BifEntry> _parseBifBytes(Uint8List bytes) {
class BifThumbnailService implements ScrubPreviewSource { class BifThumbnailService implements ScrubPreviewSource {
List<BifEntry>? _entries; List<BifEntry>? _entries;
double? _aspectRatio;
/// Download and parse the BIF file for [partId]. /// Download and parse the BIF file for [partId].
/// Returns silently on failure (thumbnails simply won't be available). /// Returns silently on failure (thumbnails simply won't be available).
Future<void> load(PlexClient client, int partId) async { Future<void> load(PlexClient client, int partId, {double? aspectRatio}) async {
_aspectRatio = aspectRatio;
_entries = null; _entries = null;
try { try {
final bytes = await client.downloadBifFile(partId); final bytes = await client.downloadBifFile(partId);
if (bytes == null || bytes.isEmpty) return; if (bytes == null || bytes.isEmpty) return;
if (bytes.length > 50 * 1024 * 1024) { const fiftyMb = 50 * 1024 * 1024;
const twoHundredMb = 200 * 1024 * 1024;
final maxBytes = PlatformDetector.isDesktopOS() ? twoHundredMb : fiftyMb;
if (bytes.length > maxBytes) {
appLogger.w('BIF file too large (${bytes.length} bytes), skipping'); appLogger.w('BIF file too large (${bytes.length} bytes), skipping');
return; return;
} }
@@ -91,7 +98,7 @@ class BifThumbnailService implements ScrubPreviewSource {
ScrubFrame? getFrame(Duration time) { ScrubFrame? getFrame(Duration time) {
final bytes = getThumbnail(time); final bytes = getThumbnail(time);
if (bytes == null) return null; if (bytes == null) return null;
return BytesScrubFrame(bytes); return BytesScrubFrame(bytes, aspectRatio: _aspectRatio ?? 16 / 9);
} }
/// Return the JPEG bytes for the thumbnail nearest to [time]. /// Return the JPEG bytes for the thumbnail nearest to [time].
+1 -1
View File
@@ -3505,7 +3505,7 @@ class PlexClient
if (partId == null) return null; if (partId == null) return null;
final service = BifThumbnailService(); final service = BifThumbnailService();
try { try {
await service.load(this, partId); await service.load(this, partId, aspectRatio: mediaSource.videoAspectRatio);
return service; return service;
} catch (e, st) { } catch (e, st) {
appLogger.w('BIF thumbnail load failed for part $partId', error: e, stackTrace: st); appLogger.w('BIF thumbnail load failed for part $partId', error: e, stackTrace: st);
+1
View File
@@ -59,6 +59,7 @@ PlexVideoPlaybackData parsePlexVideoPlaybackDataFromJson(
chapters: chapters, chapters: chapters,
partId: part['id'] as int?, partId: part['id'] as int?,
displayCriteria: PlexMappers.displayCriteriaFromJson(media as Map<String, dynamic>?, streams.videoStream), displayCriteria: PlexMappers.displayCriteriaFromJson(media as Map<String, dynamic>?, streams.videoStream),
videoAspectRatio: ((media as Map?)?['aspectRatio'] as num?)?.toDouble(),
); );
} }
} }
+1 -3
View File
@@ -17,9 +17,7 @@ sealed class ScrubFrame {
double get aspectRatio; double get aspectRatio;
} }
/// Plex BIF: standalone JPEG bytes ready for [Image.memory]. Plex doesn't /// Plex BIF: standalone JPEG bytes ready for [Image.memory].
/// expose per-BIF dimensions out-of-band; servers virtually always use
/// 16:9, which matches the default tooltip box.
class BytesScrubFrame extends ScrubFrame { class BytesScrubFrame extends ScrubFrame {
final Uint8List bytes; final Uint8List bytes;
@override @override