diff --git a/lib/media/media_source_info.dart b/lib/media/media_source_info.dart index 67583e0d..36d990d2 100644 --- a/lib/media/media_source_info.dart +++ b/lib/media/media_source_info.dart @@ -25,6 +25,9 @@ class MediaSourceInfo { /// null here and uses [partId] + the BIF service instead. final Map? trickplayByWidth; + /// Display aspect ratio of the video stream (width / height). + final double? videoAspectRatio; + MediaSourceInfo({ required this.videoUrl, required this.audioTracks, @@ -36,6 +39,7 @@ class MediaSourceInfo { this.defaultAudioStreamIndex, this.defaultSubtitleStreamIndex, this.trickplayByWidth, + this.videoAspectRatio, }); int? getPartId() => partId; } diff --git a/lib/services/bif_thumbnail_service.dart b/lib/services/bif_thumbnail_service.dart index f80720b0..92cf1692 100644 --- a/lib/services/bif_thumbnail_service.dart +++ b/lib/services/bif_thumbnail_service.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'plex_client.dart'; import 'scrub_preview_source.dart'; import '../utils/app_logger.dart'; +import '../utils/platform_detector.dart'; /// A single BIF thumbnail entry: timestamp in milliseconds + JPEG bytes. typedef BifEntry = ({int timestampMs, Uint8List imageBytes}); @@ -66,14 +67,20 @@ List _parseBifBytes(Uint8List bytes) { class BifThumbnailService implements ScrubPreviewSource { List? _entries; + double? _aspectRatio; + /// Download and parse the BIF file for [partId]. /// Returns silently on failure (thumbnails simply won't be available). - Future load(PlexClient client, int partId) async { + Future load(PlexClient client, int partId, {double? aspectRatio}) async { + _aspectRatio = aspectRatio; _entries = null; try { final bytes = await client.downloadBifFile(partId); 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'); return; } @@ -91,7 +98,7 @@ class BifThumbnailService implements ScrubPreviewSource { ScrubFrame? getFrame(Duration time) { final bytes = getThumbnail(time); 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]. diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 99df1bbd..7ed2926d 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -3505,7 +3505,7 @@ class PlexClient if (partId == null) return null; final service = BifThumbnailService(); try { - await service.load(this, partId); + await service.load(this, partId, aspectRatio: mediaSource.videoAspectRatio); return service; } catch (e, st) { appLogger.w('BIF thumbnail load failed for part $partId', error: e, stackTrace: st); diff --git a/lib/services/plex_playback_mapper.dart b/lib/services/plex_playback_mapper.dart index a3c0b8d3..d6888038 100644 --- a/lib/services/plex_playback_mapper.dart +++ b/lib/services/plex_playback_mapper.dart @@ -59,6 +59,7 @@ PlexVideoPlaybackData parsePlexVideoPlaybackDataFromJson( chapters: chapters, partId: part['id'] as int?, displayCriteria: PlexMappers.displayCriteriaFromJson(media as Map?, streams.videoStream), + videoAspectRatio: ((media as Map?)?['aspectRatio'] as num?)?.toDouble(), ); } } diff --git a/lib/services/scrub_preview_source.dart b/lib/services/scrub_preview_source.dart index 05a70b0c..f18758bc 100644 --- a/lib/services/scrub_preview_source.dart +++ b/lib/services/scrub_preview_source.dart @@ -17,9 +17,7 @@ sealed class ScrubFrame { double get aspectRatio; } -/// Plex BIF: standalone JPEG bytes ready for [Image.memory]. Plex doesn't -/// expose per-BIF dimensions out-of-band; servers virtually always use -/// 16:9, which matches the default tooltip box. +/// Plex BIF: standalone JPEG bytes ready for [Image.memory]. class BytesScrubFrame extends ScrubFrame { final Uint8List bytes; @override