From 14259a6b11ea2ef6b1f936061d5f9f9d1ec89b57 Mon Sep 17 00:00:00 2001 From: daole Date: Thu, 14 May 2026 16:23:03 +0700 Subject: [PATCH 1/7] [video_player_android] Avoid sending unset duration on initialization --- .../videoplayer/ExoPlayerEventListener.java | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index 33988786a78..461c6e4bb91 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -4,16 +4,30 @@ package io.flutter.plugins.videoplayer; +import android.os.Handler; +import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; +import androidx.media3.common.Timeline; import androidx.media3.common.Tracks; import androidx.media3.exoplayer.ExoPlayer; public abstract class ExoPlayerEventListener implements Player.Listener { + private static final long DURATION_UNSET_INITIALIZATION_TIMEOUT_MS = 500; private boolean isInitialized = false; + private boolean isWaitingForValidDuration = false; + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + private final Runnable initializationFallback = + () -> { + if (!isInitialized && isWaitingForValidDuration) { + isWaitingForValidDuration = false; + isInitialized = true; + sendInitialized(); + } + }; protected final ExoPlayer exoPlayer; protected final VideoPlayerCallbacks events; @@ -51,6 +65,32 @@ public ExoPlayerEventListener( protected abstract void sendInitialized(); + private boolean hasValidDuration() { + return exoPlayer.getDuration() != C.TIME_UNSET; + } + + private boolean shouldWaitForValidDuration() { + return !exoPlayer.isCurrentMediaItemLive() && !exoPlayer.isCurrentMediaItemDynamic(); + } + + private void maybeSendInitialized() { + if (isInitialized) { + return; + } + + if (!hasValidDuration() && shouldWaitForValidDuration()) { + isWaitingForValidDuration = true; + mainHandler.removeCallbacks(initializationFallback); + mainHandler.postDelayed(initializationFallback, DURATION_UNSET_INITIALIZATION_TIMEOUT_MS); + return; + } + + isWaitingForValidDuration = false; + isInitialized = true; + mainHandler.removeCallbacks(initializationFallback); + sendInitialized(); + } + @Override public void onPlaybackStateChanged(final int playbackState) { PlatformPlaybackState platformState = PlatformPlaybackState.UNKNOWN; @@ -60,10 +100,7 @@ public void onPlaybackStateChanged(final int playbackState) { break; case Player.STATE_READY: platformState = PlatformPlaybackState.READY; - if (!isInitialized) { - isInitialized = true; - sendInitialized(); - } + maybeSendInitialized(); break; case Player.STATE_ENDED: platformState = PlatformPlaybackState.ENDED; @@ -75,6 +112,13 @@ public void onPlaybackStateChanged(final int playbackState) { events.onPlaybackStateChanged(platformState); } + @Override + public void onTimelineChanged(@NonNull Timeline timeline, int reason) { + if (isWaitingForValidDuration && exoPlayer.getPlaybackState() == Player.STATE_READY) { + maybeSendInitialized(); + } + } + @Override public void onPlayerError(@NonNull final PlaybackException error) { if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) { From 9ebceb4b6721c1e3ec1cc0a30478ce62c59a15da Mon Sep 17 00:00:00 2001 From: daole Date: Thu, 14 May 2026 17:03:49 +0700 Subject: [PATCH 2/7] Cancel pending initialization fallback on video player dispose --- .../videoplayer/ExoPlayerEventListener.java | 13 +++- .../plugins/videoplayer/VideoPlayer.java | 8 ++- .../ExoPlayerEventListenerTest.java | 64 +++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index 461c6e4bb91..354f64b296b 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -65,6 +65,12 @@ public ExoPlayerEventListener( protected abstract void sendInitialized(); + /** Cancels pending initialization callbacks when the player is disposed. */ + public void dispose() { + isWaitingForValidDuration = false; + mainHandler.removeCallbacks(initializationFallback); + } + private boolean hasValidDuration() { return exoPlayer.getDuration() != C.TIME_UNSET; } @@ -79,9 +85,10 @@ private void maybeSendInitialized() { } if (!hasValidDuration() && shouldWaitForValidDuration()) { - isWaitingForValidDuration = true; - mainHandler.removeCallbacks(initializationFallback); - mainHandler.postDelayed(initializationFallback, DURATION_UNSET_INITIALIZATION_TIMEOUT_MS); + if (!isWaitingForValidDuration) { + isWaitingForValidDuration = true; + mainHandler.postDelayed(initializationFallback, DURATION_UNSET_INITIALIZATION_TIMEOUT_MS); + } return; } diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 7cfb5c1c13b..3d772e59eac 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -33,6 +33,7 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi { @NonNull protected final VideoPlayerCallbacks videoPlayerEvents; @Nullable protected final SurfaceProducer surfaceProducer; @Nullable private DisposeHandler disposeHandler; + @Nullable private ExoPlayerEventListener exoPlayerEventListener; @NonNull protected ExoPlayer exoPlayer; // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. @UnstableApi @Nullable protected DefaultTrackSelector trackSelector; @@ -75,7 +76,8 @@ public VideoPlayer( exoPlayer.setMediaItem(mediaItem); exoPlayer.prepare(); - exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer)); + exoPlayerEventListener = createExoPlayerEventListener(exoPlayer, surfaceProducer); + exoPlayer.addListener(exoPlayerEventListener); setAudioAttributes(exoPlayer, options.mixWithOthers); } @@ -237,6 +239,10 @@ public void dispose() { if (disposeHandler != null) { disposeHandler.onDispose(); } + if (exoPlayerEventListener != null) { + exoPlayerEventListener.dispose(); + exoPlayerEventListener = null; + } exoPlayer.release(); } } diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java index 529e8d5a6af..2456b74d27a 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java @@ -4,14 +4,21 @@ package io.flutter.plugins.videoplayer; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import android.os.Looper; +import androidx.media3.common.C; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; +import androidx.media3.common.Timeline; import androidx.media3.exoplayer.ExoPlayer; +import java.time.Duration; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -20,6 +27,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; +import org.robolectric.Shadows; /** * Unit tests for {@link ExoPlayerEventListener}. @@ -87,6 +95,8 @@ public void onPlaybackStateChangedIdleSendsIdle() { @Test public void onPlaybackStateChangedReadySendsInitializedAndReady() { + when(mockExoPlayer.getDuration()).thenReturn(10L); + eventListener.onPlaybackStateChanged(Player.STATE_READY); verify(mockCallbacks).onPlaybackStateChanged(PlatformPlaybackState.READY); @@ -94,6 +104,60 @@ public void onPlaybackStateChangedReadySendsInitializedAndReady() { assertTrue(eventListener.calledSendInitialized()); } + @Test + public void onPlaybackStateChangedReadyDelaysInitializationWhenDurationUnsetForNonLiveMedia() { + when(mockExoPlayer.getDuration()).thenReturn(C.TIME_UNSET); + when(mockExoPlayer.isCurrentMediaItemLive()).thenReturn(false); + when(mockExoPlayer.isCurrentMediaItemDynamic()).thenReturn(false); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + + verify(mockCallbacks).onPlaybackStateChanged(PlatformPlaybackState.READY); + verifyNoMoreInteractions(mockCallbacks); + assertFalse(eventListener.calledSendInitialized()); + } + + @Test + public void onPlaybackStateChangedReadySendsInitializedImmediatelyWhenDurationAvailable() { + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + + assertTrue(eventListener.calledSendInitialized()); + } + + @Test + public void onPlaybackStateChangedReadyFallsBackWhenDurationRemainsUnset() { + when(mockExoPlayer.getDuration()).thenReturn(C.TIME_UNSET); + when(mockExoPlayer.isCurrentMediaItemLive()).thenReturn(false); + when(mockExoPlayer.isCurrentMediaItemDynamic()).thenReturn(false); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + assertFalse(eventListener.calledSendInitialized()); + + Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500)); + + assertTrue(eventListener.calledSendInitialized()); + } + + @Test + public void onTimelineChangedSendsInitializedWhenDurationBecomesAvailable() { + when(mockExoPlayer.getDuration()).thenReturn(C.TIME_UNSET); + when(mockExoPlayer.isCurrentMediaItemLive()).thenReturn(false); + when(mockExoPlayer.isCurrentMediaItemDynamic()).thenReturn(false); + + eventListener.onPlaybackStateChanged(Player.STATE_READY); + assertFalse(eventListener.calledSendInitialized()); + + when(mockExoPlayer.getPlaybackState()).thenReturn(Player.STATE_READY); + when(mockExoPlayer.getDuration()).thenReturn(10L); + + eventListener.onTimelineChanged( + mock(Timeline.class), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + + assertTrue(eventListener.calledSendInitialized()); + } + @Test public void onErrorBehindLiveWindowSeekToDefaultAndPrepare() { eventListener.onPlayerError( From d8c0f25d93f395622a3cbfea4898c5e0186dbdf7 Mon Sep 17 00:00:00 2001 From: daole Date: Fri, 15 May 2026 14:28:34 +0700 Subject: [PATCH 3/7] Increase duration unset initialization timeout from 500ms to 2000ms --- .../io/flutter/plugins/videoplayer/ExoPlayerEventListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index 354f64b296b..9bf7df8ab75 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -16,7 +16,7 @@ import androidx.media3.exoplayer.ExoPlayer; public abstract class ExoPlayerEventListener implements Player.Listener { - private static final long DURATION_UNSET_INITIALIZATION_TIMEOUT_MS = 500; + private static final long DURATION_UNSET_INITIALIZATION_TIMEOUT_MS = 2000; private boolean isInitialized = false; private boolean isWaitingForValidDuration = false; private final Handler mainHandler = new Handler(Looper.getMainLooper()); From 4714b44e07cfc3e44ab1db1940d7dfdf41cf9334 Mon Sep 17 00:00:00 2001 From: daole Date: Thu, 21 May 2026 10:52:16 +0700 Subject: [PATCH 4/7] [video_player_android] Version bump --- packages/video_player/video_player_android/CHANGELOG.md | 4 ++++ packages/video_player/video_player_android/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 85f40631a27..340523225be 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.9.6 + +* Fixes a [bug](https://github.com/flutter/flutter/issues/176575) where some videos report an incorrect duration when initialized without a video duration. + ## 2.9.5 * Updates build files from Groovy to Kotlin. diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 4461ddd73d1..d68d7507bae 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.9.5 +version: 2.9.6 environment: sdk: ^3.9.0 From 531b955c8e91e570b16356d1a735410da3d79c3c Mon Sep 17 00:00:00 2001 From: daole Date: Wed, 3 Jun 2026 10:17:03 +0700 Subject: [PATCH 5/7] [video_android_player] make DURATION_UNSET_INITIALIZATION_TIMEOUT_MS static in ExoPlayerEventListener and update test to use this constant --- .../io/flutter/plugins/videoplayer/ExoPlayerEventListener.java | 2 +- .../plugins/videoplayer/ExoPlayerEventListenerTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java index 9bf7df8ab75..fb8ec03cea5 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java @@ -16,7 +16,7 @@ import androidx.media3.exoplayer.ExoPlayer; public abstract class ExoPlayerEventListener implements Player.Listener { - private static final long DURATION_UNSET_INITIALIZATION_TIMEOUT_MS = 2000; + static final long DURATION_UNSET_INITIALIZATION_TIMEOUT_MS = 2000; private boolean isInitialized = false; private boolean isWaitingForValidDuration = false; private final Handler mainHandler = new Handler(Looper.getMainLooper()); diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java index 2456b74d27a..78fc4b1c11f 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java @@ -135,7 +135,8 @@ public void onPlaybackStateChangedReadyFallsBackWhenDurationRemainsUnset() { eventListener.onPlaybackStateChanged(Player.STATE_READY); assertFalse(eventListener.calledSendInitialized()); - Shadows.shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500)); + Shadows.shadowOf(Looper.getMainLooper()) + .idleFor(Duration.ofMillis(ExoPlayerEventListener.DURATION_UNSET_INITIALIZATION_TIMEOUT_MS)); assertTrue(eventListener.calledSendInitialized()); } From 700aaf13c585ab0a8244b54870d94fb2ef8e1052 Mon Sep 17 00:00:00 2001 From: daole Date: Mon, 13 Jul 2026 10:59:13 +0700 Subject: [PATCH 6/7] [video_player_android] Version bump --- packages/video_player/video_player_android/CHANGELOG.md | 5 ++++- packages/video_player/video_player_android/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 597fc2c568d..26f60a5c2b7 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.12.0 + +* Fixes a [bug](https://github.com/flutter/flutter/issues/176575) where some videos report an incorrect duration when initialized without a video duration. + ## 2.11.0 * Adds `backBufferDurationMs` to `CreationOptions` to configure ExoPlayer `DefaultLoadControl` back buffer duration. @@ -12,7 +16,6 @@ ## 2.9.6 -* Fixes a [bug](https://github.com/flutter/flutter/issues/176575) where some videos report an incorrect duration when initialized without a video duration. * Migrates to Built-in Kotlin to support AGP 9. * Updates minimum supported SDK version to Flutter 3.44/Dart 3.12. diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 11e0efc1814..896c612d45e 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_android description: Android implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.11.0 +version: 2.12.0 environment: sdk: ^3.12.0 From 6602774af532765557fa130353f366f51e0af0d0 Mon Sep 17 00:00:00 2001 From: Camille Simon Date: Mon, 20 Jul 2026 12:16:04 -0700 Subject: [PATCH 7/7] fix formatting --- .../plugins/videoplayer/ExoPlayerEventListenerTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java index 78fc4b1c11f..63b8d032783 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/ExoPlayerEventListenerTest.java @@ -136,7 +136,8 @@ public void onPlaybackStateChangedReadyFallsBackWhenDurationRemainsUnset() { assertFalse(eventListener.calledSendInitialized()); Shadows.shadowOf(Looper.getMainLooper()) - .idleFor(Duration.ofMillis(ExoPlayerEventListener.DURATION_UNSET_INITIALIZATION_TIMEOUT_MS)); + .idleFor( + Duration.ofMillis(ExoPlayerEventListener.DURATION_UNSET_INITIALIZATION_TIMEOUT_MS)); assertTrue(eventListener.calledSendInitialized()); } @@ -154,7 +155,7 @@ public void onTimelineChangedSendsInitializedWhenDurationBecomesAvailable() { when(mockExoPlayer.getDuration()).thenReturn(10L); eventListener.onTimelineChanged( - mock(Timeline.class), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); + mock(Timeline.class), Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); assertTrue(eventListener.calledSendInitialized()); }