Skip to content

[video_player_android] Add video track selection support - #11475

Merged
auto-submit[bot] merged 16 commits into
flutter:mainfrom
nateshmbhat:breakout/video-track-android
Jun 30, 2026
Merged

[video_player_android] Add video track selection support#11475
auto-submit[bot] merged 16 commits into
flutter:mainfrom
nateshmbhat:breakout/video-track-android

Conversation

@nateshmbhat

Copy link
Copy Markdown
Contributor

Summary

Android breakout PR for #10688.

  • Implements getVideoTracks() and selectVideoTrack() methods using ExoPlayer's TrackSelectionOverride
  • Adds onVideoTrackChanged event callback for track change notifications
  • Adds comprehensive Java and Dart unit tests

Dependency Chain

This PR is second in a series of breakout PRs:

  1. video_player_platform_interface ([video_player_platform_interface] Add video track selection support #11474) - pending
  2. video_player_android (this PR)
  3. video_player_avfoundation (pending)
  4. video_player + video_player_web (pending - original PR [video_player] : Add video track selection support for Android and iOS #10688 updated)

Note: This PR depends on video_player_platform_interface 6.7.0 being published first.

Test Plan

  • Java unit tests for getVideoTracks() and selectVideoTrack() in VideoPlayerTest.java
  • Java unit tests for onVideoTrackChanged callback in VideoPlayerEventCallbacksTest.java
  • Dart unit tests for AndroidVideoPlayer overrides

Implements getVideoTracks() and selectVideoTrack() methods for video
track (quality) selection using ExoPlayer.
Android breakout PR for flutter#10688.

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces video track selection capabilities to the Android video player plugin. Key changes include adding getVideoTracks(), selectVideoTrack(), and enableAutoVideoQuality() methods to the VideoPlayerInstanceApi and their implementations in VideoPlayer.java and android_video_player.dart. A workaround is implemented in selectVideoTrack to handle video dimension changes by temporarily disabling and re-enabling the video track type to force a renderer reset. New Pigeon messages and data structures are introduced to support video track information exchange, and ExoPlayerEventListener is updated to notify about video track changes. Comprehensive unit tests for the new video track functionalities have also been added. The review comment points out a potential crash risk in the postDelayed workaround within VideoPlayer.java if the player is disposed during the 150ms delay, as the current trackSelector == null guard is ineffective. It recommends using a cancellable callback mechanism or an isDisposed flag for robust handling.

Comment on lines +410 to +431
new android.os.Handler(android.os.Looper.getMainLooper())
.postDelayed(
() -> {
// Guard against player disposal during the delay
if (trackSelector == null) {
return;
}

trackSelector.setParameters(
trackSelector
.buildUponParameters()
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false)
.setOverrideForType(override)
.build());

// Restore playback state
exoPlayer.seekTo(currentPosition);
if (wasPlaying) {
exoPlayer.play();
}
},
150);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The postDelayed workaround for dimension changes introduces a potential crash risk. If the VideoPlayer is disposed (and the ExoPlayer released) during the 150ms delay, the calls to exoPlayer.seekTo() and exoPlayer.play() inside the delayed callback will throw an IllegalStateException because the player has been released.

The current guard if (trackSelector == null) (line 414) is likely ineffective because trackSelector is a final field in this class and is not nullified during the dispose() call in the current implementation of this plugin.

Recommendation:
Use a mechanism to cancel the pending callback or a robust way to check if the player has been disposed. For example, you could use a member Handler and call handler.removeCallbacksAndMessages(null) in dispose(), or maintain an isDisposed boolean flag that is set to true in dispose() and checked at the beginning of the delayed callback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address this.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7bdc032. Switched to a member Handler plus an isDisposed flag; dispose() now calls removeCallbacksAndMessages(null) and sets the flag before releasing the player, so the 150 ms dimension-change callback cannot run on a released ExoPlayer. Added a Robolectric regression test (testSelectVideoTrack_disposeDuringDimensionChangeDelayDoesNotCrash) that uses ShadowLooper.idleFor(200ms) after dispose() and asserts no seekTo/play is invoked.

@stuartmorgan-g

Copy link
Copy Markdown
Collaborator

This PR requires #11474 to land and be published.

@stuartmorgan-g
stuartmorgan-g marked this pull request as draft April 14, 2026 13:50
@nateshmbhat
nateshmbhatforce-pushed the breakout/video-track-android branch 2 times, most recently from 5ebe09b to 12597e9CompareMay 1, 2026 06:45
@nateshmbhat
nateshmbhat marked this pull request as ready for review May 1, 2026 06:45
@nateshmbhat

Copy link
Copy Markdown
ContributorAuthor

@stuartmorgan-g PR unblocked. good to merge ?

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements video track selection for the Android video player using ExoPlayer, including methods to retrieve available tracks, select a specific track, and enable auto-quality selection. The implementation includes a workaround for renderer resets when video dimensions change and updates the Pigeon-generated messaging layer. Feedback is provided regarding potential race conditions in the Dart selectVideoTrack implementation where concurrent calls could overwrite shared state, and concerns are raised about state inconsistency and potential crashes during the 150ms delay used for dimension-change renderer resets.

Comment on lines +440 to +491
Future<void> selectVideoTrack(VideoTrack? track) async {
// Create a completer to wait for the track selection to complete
_videoTrackSelectionCompleter = Completer<void>();

if (track == null) {
// Auto quality - use dedicated method
_expectedVideoTrackId = null;
try {
await _api.enableAutoVideoQuality();

// Wait for the onTracksChanged event from ExoPlayer with a timeout
await _videoTrackSelectionCompleter!.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
// If we timeout, just continue - the track may still have been selected
},
);
} finally {
_videoTrackSelectionCompleter = null;
_expectedVideoTrackId = null;
}
return;
}

// Extract groupIndex and trackIndex from the track id
final List<String> parts = track.id.split('_');
if (parts.length != 2) {
throw ArgumentError(
'Invalid track id format: "${track.id}". Expected format: "groupIndex_trackIndex"',
);
}

final int groupIndex = int.parse(parts[0]);
final int trackIndex = int.parse(parts[1]);

_expectedVideoTrackId = track.id;

try {
await _api.selectVideoTrack(groupIndex, trackIndex);

// Wait for the onTracksChanged event from ExoPlayer with a timeout
await _videoTrackSelectionCompleter!.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
// If we timeout, just continue - the track may still have been selected
},
);
} finally {
_videoTrackSelectionCompleter = null;
_expectedVideoTrackId = null;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The selectVideoTrack implementation is vulnerable to race conditions when multiple calls occur concurrently. Since _videoTrackSelectionCompleter and _expectedVideoTrackId are shared fields, a second call will overwrite the state of an ongoing first call. Additionally, the finally block of the first call to complete will clear these fields, which can cause subsequent calls to timeout or resolve prematurely with incorrect state. Using local variables to guard the cleanup ensures that each call only clears the fields if it is still the 'current' active request.

Future<void> selectVideoTrack(VideoTrack? track) async {
finalCompleter<void> completer =Completer<void>();
_videoTrackSelectionCompleter = completer;
finalString? expectedId = track?.id;
_expectedVideoTrackId = expectedId;
try {
if (track ==null) {
// Auto quality - use dedicated methodawait _api.enableAutoVideoQuality();
} else {
// Extract groupIndex and trackIndex from the track idfinalList<String> parts = track.id.split('_');
if (parts.length !=2) {
throwArgumentError(
'Invalid track id format: "${track.id}". Expected format: "groupIndex_trackIndex"',
);
}
finalint groupIndex =int.parse(parts[0]);
finalint trackIndex =int.parse(parts[1]);
await _api.selectVideoTrack(groupIndex, trackIndex);
}
// Wait for the onTracksChanged event from ExoPlayer with a timeoutawait completer.future.timeout(
constDuration(seconds:5),
onTimeout: () {
// If we timeout, just continue - the track may still have been selected
},
);
} finally {
if (_videoTrackSelectionCompleter == completer) {
_videoTrackSelectionCompleter =null;
}
if (_expectedVideoTrackId == expectedId) {
_expectedVideoTrackId =null;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7bdc032. Captured the completer and expected id in locals (completer, expectedId) and guarded the finally cleanup with identical(_videoTrackSelectionCompleter, completer) / _expectedVideoTrackId == expectedId, so an older overlapping selectVideoTrack call can no longer null out a newer call's state. Also merged the null/non-null track branches to remove the duplicated wait/cleanup logic. Added a regression test (concurrent selectVideoTrack calls do not clobber each other) that fails fast on call 1 and verifies call 2 still completes on its matching VideoTrackChangedEvent.

@stuartmorgan-g

Copy link
Copy Markdown
Collaborator

@stuartmorgan-g PR unblocked. good to merge ?

No PRs can me merged without review. This needs final re-review by the folks who looked at the original combination PR.

@stuartmorgan-gstuartmorgan-g added triage-android Should be looked at in Android triage federated: partial_changes PR that contains changes for only a single package of a federated plugin change labels May 1, 2026
@jesswrd
jesswrd requested review from mboetger and removed request for camsim99May 5, 2026 21:18
…deoTrack race
- VideoPlayer.java: replace throwaway Handler + ineffective trackSelector
null-check with a member mainHandler and isDisposed flag; dispose() now
cancels the 150ms dimension-change callback so it cannot run on a
released ExoPlayer.
- android_video_player.dart: capture completer/expectedId in locals and
guard cleanup with identical()/== so an older concurrent
selectVideoTrack call no longer clears a newer call's state.
- Add Java regression test (Robolectric ShadowLooper) for dispose during
the dimension-change delay, and a Dart concurrent-call regression test.
Comment on lines +463 to +464
final int groupIndex = int.parse(parts[0]);
final int trackIndex = int.parse(parts[1]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible these could end up not being ints? int.parse will crash if so.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No not possible

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this path to parse Android track IDs with int.tryParse and throw ArgumentError if the numeric contract is violated, rather than letting a FormatException bubble out. Added a regression test for non-numeric track IDs as well.

await completer.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
// If we timeout, just continue - the track may still have been selected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth logging something for debugging purposes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added debug logging on the 5-second timeout path so we emit whether the stalled selection was for a specific track or auto quality selection. There is also a test that advances the timeout and asserts that the log is produced.

Auto/adaptive quality selection (selectVideoTrack(null)) no longer
registers a completer or waits for a VideoTrackChangedEvent. Because the
native side reports a concrete track id even for adaptive playback, the
previous 'expecting null matches any event' logic could complete the
auto-selection future on a VideoTrackChangedEvent belonging to a prior
selectVideoTrack(track) call.
Clearing the override now resolves on its own, and the event handler
matches the reported track id exactly, so an unrelated/earlier
selection can no longer complete an in-flight selection.
@nateshmbhat
nateshmbhat requested a review from mboetgerJune 7, 2026 09:00
@nateshmbhat

Copy link
Copy Markdown
ContributorAuthor

@mboetger have addressed your concern and pushed fix.

// to resource conflicts and rendering artifacts. The 150ms value was determined through
// empirical testing across various Android devices and provides a reliable balance
// between responsiveness and ensuring complete resource cleanup. Shorter delays (e.g.,
// 50-100ms) were found to still cause glitches on some devices, while longer delays

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My biggest concern with the PR is this delay. It seems to be device dependent too - so on lower end devices, this probably just won't work. That being said, I recognize this is new functionality and their is no exoplayer api to alleviate this. I'll give it approval, but not a big fan.

@nateshmbhat

nateshmbhat commented Jun 17, 2026

Copy link
Copy Markdown
ContributorAuthor

@tarrinneal good to merge?

@stuartmorgan-g

Copy link
Copy Markdown
Collaborator

@nateshmbhat The PR needs two approvals to land, so no. I'm not a reviewer for this PR, so I'm not sure why you are pinging me.

@tarrinnealtarrinneal added CICD Run CI/CD and removed federated: partial_changes PR that contains changes for only a single package of a federated plugin change labels Jun 17, 2026

@tarrinnealtarrinneal left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lg two nits

Comment threadpackages/video_player/video_player_android/CHANGELOG.md
@github-actionsgithub-actionsBot removed the CICD Run CI/CD label Jun 18, 2026
auto-submitBot pushed a commit that referenced this pull request Jun 26, 2026
## Summary
AVFoundation breakout PR for #10688.
- Implements `getVideoTracks()` and `selectVideoTrack()` methods using AVFoundation
- Video track selection requires iOS 15+ / macOS 12+ for HLS streams
- Adds comprehensive Swift and Dart unit tests
## Dependency Chain
This PR is **third** in a series of breakout PRs:
1. `video_player_platform_interface` (#11474) - pending
2. `video_player_android` (#11475) - pending
3. `video_player_avfoundation` (this PR)
4. `video_player` + `video_player_web` (pending - original PR #10688 updated)
**Note:** This PR depends on `video_player_platform_interface` 6.7.0 being published first.
## Test Plan
@stuartmorgan-gstuartmorgan-g added the CICD Run CI/CD label Jun 26, 2026
@stuartmorgan-g

Copy link
Copy Markdown
Collaborator

@tarrinneal Anything missing for this to be autosubmitted?

@tarrinneal

Copy link
Copy Markdown
Contributor

@tarrinneal Anything missing for this to be autosubmitted?

formatting mostly

@flutter-dashboardflutter-dashboardBot removed the CICD Run CI/CD label Jun 27, 2026
@nateshmbhat

Copy link
Copy Markdown
ContributorAuthor

formatting fixed @stuartmorgan-g@tarrinneal

@stuartmorgan-gstuartmorgan-g added the CICD Run CI/CD label Jun 29, 2026
@tarrinnealtarrinneal added the autosubmit Merge PR when tree becomes green via auto submit App label Jun 30, 2026
@auto-submit
auto-submitBot merged commit 274ed3e into flutter:mainJun 30, 2026
88 checks passed
pullBot pushed a commit to safarmer/flutter that referenced this pull request Jun 30, 2026
…er#188792)
flutter/packages@656ccaa...274ed3e
2026-06-30 nateshmbhat1@gmail.com [video_player_android] Add video track
selection support (flutter/packages#11475)
2026-06-30 engine-flutter-autoroll@skia.org Manual roll Flutter from
b081f33 to 0c80830 (1 revision) (flutter/packages#12058)
2026-06-30 36861262+QuncCccccc@users.noreply.github.com [material_ui]
Remove `widgets` import from `material_test.dart`
(flutter/packages#12056)
2026-06-29 36861262+QuncCccccc@users.noreply.github.com [material_ui]
Remove `widgets` imports from `card_test.dart`,
`checkbox_list_tile_test.dart` (flutter/packages#12054)
2026-06-29 36861262+QuncCccccc@users.noreply.github.com [material_ui]
Remove `widgets/clipboard_utils.dart`,
`widgets/text_selection_toolbar_utils.dart` imports from
`adaptive_text_selection_toolbar_test.dart` (flutter/packages#12053)
2026-06-29 engine-flutter-autoroll@skia.org Manual roll Flutter from
11e339e to b081f33 (1 revision) (flutter/packages#12050)
2026-06-29 36861262+QuncCccccc@users.noreply.github.com [material_ui]
Remove `widgets/clipboard_utils.dart` imports from
`date_picker_test.dart`, `input_date_picker_form_field_test.dart`,
`search_test.dart`, `selectable_text_test.dart`,
`text_form_field_test.dart`, `text_selection_test.dart`
(flutter/packages#12030)
2026-06-29 engine-flutter-autoroll@skia.org Manual roll Flutter from
87224e0 to 11e339e (4 revisions) (flutter/packages#12041)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui]
Enable `text_field_test` (flutter/packages#12022)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui] Port
PR (flutter#184807) from flutter/flutter to material_ui
(flutter/packages#11972)
2026-06-29 rmolivares@renzo-olivares.dev [cupertino_ui] Migrate
`button_test.dart` to `SemanticsHandle` (flutter/packages#11992)
2026-06-29 rmolivares@renzo-olivares.dev [cupertino_ui] Migrate
`radio_test.dart` to `SemanticsHandle` (flutter/packages#11981)
2026-06-29 rmolivares@renzo-olivares.dev [cupertino_ui] Migrate
`picker_test.dart` to `SemanticsHandle` (flutter/packages#12008)
2026-06-29 36861262+QuncCccccc@users.noreply.github.com [cupertino_ui]
Create util files. Remove widgets import in
`adaptive_text_selection_toolbar_test.dart` and
`text_selection_test.dart` (flutter/packages#12023)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui]
Enable `floating_action_button_test` (flutter/packages#12014)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui]
Enable `dropdown_test` (flutter/packages#12011)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui]
Enable `chip_test` (flutter/packages#12009)
2026-06-29 36861262+QuncCccccc@users.noreply.github.com [material_ui]
Remove widgets import in `data_table_test.dart`, `switch_test.dart` and
`tooltip_theme_test.dart` (flutter/packages#12031)
2026-06-29 burak.karahan@mail.ru [material_ui] Port flutter/flutter
flutter#186670 "Use local semantics tester in Material selection tests"
(flutter/packages#11983)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui]
Enable `switch_list_tile_test` (flutter/packages#12020)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui]
Enable `popup_menu_test` (flutter/packages#12018)
2026-06-29 21270878+elliette@users.noreply.github.com [material_ui]
Enable `date_range_picker_test` (flutter/packages#12010)
2026-06-29 stuartmorgan@google.com [google_sign_in] Simplify Android
user ID extraction (flutter/packages#12025)
If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC flutter-ecosystem@google.com on the revert to ensure that a
human
is aware of the problem.
To file a bug in Flutter:
https://github.com/flutter/flutter/issues/new/choose
To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622
Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmitMerge PR when tree becomes green via auto submit AppCICDRun CI/CDp: video_playerplatform-androidtriage-androidShould be looked at in Android triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@nateshmbhat@stuartmorgan-g@tarrinneal@mboetger