فهرست منبع

fix: keep Jellyfin progress ring duration

Media3 reports an unknown duration for Jellyfin's progressive/transcoded stream, so position/duration was always zero. The native MediaItem now carries the queued track duration, the bridge state falls back to it, and the Dart media snapshot uses the queue duration when the native value is missing. Adds a regression test.
Krasimir Kazakov 5 روز پیش
والد
کامیت
2045a489c6

+ 10 - 1
android/app/src/main/kotlin/com/wasp/cardash_launcher/JellyfinPlaybackBridge.kt

@@ -181,10 +181,12 @@ class JellyfinPlaybackBridge(
                 )
                 )
                 putInt("playlistLength", rawItems.size)
                 putInt("playlistLength", rawItems.size)
             }
             }
+            val durationMs = (item["durationMs"] as? Number)?.toLong() ?: 0L
             val metadata = MediaMetadata.Builder()
             val metadata = MediaMetadata.Builder()
                 .setTitle(item["title"] as? String)
                 .setTitle(item["title"] as? String)
                 .setArtist(item["artist"] as? String)
                 .setArtist(item["artist"] as? String)
                 .setAlbumTitle(item["album"] as? String)
                 .setAlbumTitle(item["album"] as? String)
+                .setDurationMs(durationMs)
                 .setExtras(extras)
                 .setExtras(extras)
                 .apply {
                 .apply {
                     if (artwork != null && artwork.isNotEmpty()) {
                     if (artwork != null && artwork.isNotEmpty()) {
@@ -227,7 +229,14 @@ class JellyfinPlaybackBridge(
         val item = controller.currentMediaItem ?: return mapOf("available" to false)
         val item = controller.currentMediaItem ?: return mapOf("available" to false)
         val metadata = item.mediaMetadata
         val metadata = item.mediaMetadata
         val extras = metadata.extras
         val extras = metadata.extras
-        val duration = if (controller.duration > 0) controller.duration else 0L
+        // Jellyfin's progressive/transcoded audio often has no duration in
+        // ExoPlayer. Keep the queued track duration as the ring denominator.
+        val fallbackDuration = (metadata.durationMs ?: 0L).coerceAtLeast(0L)
+        val duration = if (controller.duration > 0) {
+            controller.duration
+        } else {
+            fallbackDuration
+        }
         return mapOf(
         return mapOf(
             "available" to true,
             "available" to true,
             "playing" to controller.isPlaying,
             "playing" to controller.isPlaying,

+ 9 - 2
lib/data/models/jellyfin_playback.dart

@@ -184,7 +184,14 @@ class JellyfinPlaybackSnapshot {
   }
   }
 
 
   /// Snapshot consumed by the Home volume card and the existing media widgets.
   /// Snapshot consumed by the Home volume card and the existing media widgets.
-  MediaPlayback toMediaPlayback() {
+  ///
+  /// A progressive/transcoded Jellyfin stream often has an unknown duration in
+  /// ExoPlayer. [fallbackDurationMs] comes from the queued track metadata so
+  /// the progress ring still has a denominator.
+  MediaPlayback toMediaPlayback({int fallbackDurationMs = 0}) {
+    final effectiveDuration = durationMs > 0
+        ? durationMs
+        : (fallbackDurationMs > 0 ? fallbackDurationMs : 0);
     return MediaPlayback(
     return MediaPlayback(
       hasAccess: available,
       hasAccess: available,
       hasSession: available && itemId != null,
       hasSession: available && itemId != null,
@@ -192,7 +199,7 @@ class JellyfinPlaybackSnapshot {
       title: title,
       title: title,
       artist: artist,
       artist: artist,
       packageName: appPackageName,
       packageName: appPackageName,
-      durationMs: durationMs,
+      durationMs: effectiveDuration,
       positionMs: positionMs,
       positionMs: positionMs,
     );
     );
   }
   }

+ 3 - 1
lib/features/jellyfin/jellyfin_playback_controller.dart

@@ -76,7 +76,9 @@ class JellyfinPlaybackController extends ChangeNotifier {
 
 
   MediaPlayback get mediaPlayback {
   MediaPlayback get mediaPlayback {
     if (!_snapshot.available) return const MediaPlayback();
     if (!_snapshot.available) return const MediaPlayback();
-    return _snapshot.toMediaPlayback();
+    return _snapshot.toMediaPlayback(
+      fallbackDurationMs: currentItem?.durationMs ?? 0,
+    );
   }
   }
 
 
   bool get canGoNext => _snapshot.hasNext;
   bool get canGoNext => _snapshot.hasNext;

+ 2 - 1
test/jellyfin_fakes.dart

@@ -354,6 +354,7 @@ class FakeJellyfinPlaybackPlatform implements JellyfinPlaybackPlatform {
   int setQueueCalls = 0;
   int setQueueCalls = 0;
   bool setQueueResult = true;
   bool setQueueResult = true;
   bool permissionGranted = false;
   bool permissionGranted = false;
+  bool zeroDuration = false;
 
 
   @override
   @override
   Future<bool> setQueue(JellyfinPlaybackQueue queue) async {
   Future<bool> setQueue(JellyfinPlaybackQueue queue) async {
@@ -384,7 +385,7 @@ class FakeJellyfinPlaybackPlatform implements JellyfinPlaybackPlatform {
       title: item.title,
       title: item.title,
       artist: item.artist,
       artist: item.artist,
       album: item.album,
       album: item.album,
-      durationMs: item.durationMs,
+      durationMs: zeroDuration ? 0 : item.durationMs,
       positionMs: positionMs,
       positionMs: positionMs,
       index: safeIndex,
       index: safeIndex,
       hasPrevious: safeIndex > 0,
       hasPrevious: safeIndex > 0,

+ 22 - 0
test/jellyfin_playback_controller_test.dart

@@ -175,6 +175,28 @@ void main() {
     expect(fixture.platform.permissionGranted, isTrue);
     expect(fixture.platform.permissionGranted, isTrue);
   });
   });
 
 
+  test('media progress falls back to the queued track duration', () async {
+    final fixture = await _fixture();
+    addTearDown(fixture.playback.dispose);
+    addTearDown(fixture.jellyfin.dispose);
+
+    await fixture.playback.playAlbum(album('album-1'));
+    fixture.platform.zeroDuration = true;
+    fixture.platform.state = fixture.platform.snapshotAt(
+      0,
+      playing: true,
+      positionMs: 30000,
+    );
+
+    await fixture.playback.toggle();
+
+    expect(fixture.playback.snapshot.durationMs, 0);
+    final media = fixture.playback.mediaPlayback;
+    expect(media.durationMs, fixture.playback.currentItem!.durationMs);
+    expect(media.positionMs, 30000);
+    expect(media.progress, greaterThan(0));
+  });
+
   test('a failed queue start surfaces an error', () async {
   test('a failed queue start surfaces an error', () async {
     final fixture = await _fixture();
     final fixture = await _fixture();
     addTearDown(fixture.playback.dispose);
     addTearDown(fixture.playback.dispose);