Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b544b45
feat(video_player_android): implement audio track selection API
nateshmbhat Nov 7, 2025
1fdb5d5
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Nov 10, 2025
a0a9a0d
Update packages/video_player/video_player_android/android/src/main/ja…
nateshmbhat Nov 19, 2025
061388d
refactor(video_player_android): improve audio track selection validat…
nateshmbhat Nov 19, 2025
8c776aa
Merge branch '28-oct-platform-android' of github.com:nateshmbhat/flut…
nateshmbhat Nov 19, 2025
40b1b3f
test(video_player_android): add audio track support availability test…
nateshmbhat Nov 19, 2025
f9d72fe
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Nov 19, 2025
f0a3f38
Update packages/video_player/video_player_android/android/src/main/ja…
nateshmbhat Nov 26, 2025
25fa206
test(video_player_android): change audio track validation from warnin…
nateshmbhat Nov 26, 2025
308a341
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Nov 26, 2025
2bcef5f
Merge branch '28-oct-platform-android' of github.com:nateshmbhat/flut…
nateshmbhat Nov 26, 2025
fc51b80
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Nov 27, 2025
15db47e
style(video_player_android): apply dart format and omit obvious local…
nateshmbhat Nov 27, 2025
ffdd3ac
Merge branch '28-oct-platform-android' of github.com:nateshmbhat/flut…
nateshmbhat Nov 27, 2025
c8a591f
docs(video_player_android): add TODO comments for ExoPlayer UnstableA…
nateshmbhat Dec 3, 2025
8538ab1
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Dec 3, 2025
9a9b64d
Remove extra line in CHANGELOG
camsim99 Dec 4, 2025
a6221b7
chore(video_player_android): format code and bump version to 2.9.0
nateshmbhat Dec 4, 2025
cfbdb9b
fix(video_player_android): fix Java formatting issues
nateshmbhat Dec 4, 2025
a8ed46c
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Dec 4, 2025
c80b01c
fix(video_player_android): add lint baseline for generated code errors
nateshmbhat Dec 4, 2025
8ab60f5
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Dec 6, 2025
5c4b8eb
fix: explicitly opt into Media3 UnstableApi usage in video player cre…
nateshmbhat Dec 9, 2025
30f2b5f
Merge branch '28-oct-platform-android' of github.com:nateshmbhat/flut…
nateshmbhat Dec 9, 2025
920b2e5
Merge branch 'main' into 28-oct-platform-android
nateshmbhat Dec 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.9.0

* Implements `getAudioTracks()` and `selectAudioTrack()` methods for Android using ExoPlayer.

## 2.8.17

* Moves video event processing logic to Dart, and fixes an issue where buffer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
package io.flutter.plugins.videoplayer;

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.Tracks;
import androidx.media3.exoplayer.ExoPlayer;

public abstract class ExoPlayerEventListener implements Player.Listener {
Expand Down Expand Up @@ -88,4 +91,34 @@ public void onPlayerError(@NonNull final PlaybackException error) {
public void onIsPlayingChanged(boolean isPlaying) {
events.onIsPlayingStateUpdate(isPlaying);
}

@Override
public void onTracksChanged(@NonNull Tracks tracks) {
// Find the currently selected audio track and notify
String selectedTrackId = findSelectedAudioTrackId(tracks);
events.onAudioTrackChanged(selectedTrackId);
}

/**
* Finds the ID of the currently selected audio track.
*
* @param tracks The current tracks
* @return The track ID in format "groupIndex_trackIndex", or null if no audio track is selected
*/
@Nullable
private String findSelectedAudioTrackId(@NonNull Tracks tracks) {
int groupIndex = 0;
for (Tracks.Group group : tracks.getGroups()) {
if (group.getType() == C.TRACK_TYPE_AUDIO && group.isSelected()) {
// Find the selected track within this group
for (int i = 0; i < group.length; i++) {
if (group.isTrackSelected(i)) {
return groupIndex + "_" + i;
}
}
}
groupIndex++;
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,23 @@
import static androidx.media3.common.Player.REPEAT_MODE_ALL;
import static androidx.media3.common.Player.REPEAT_MODE_OFF;

import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import io.flutter.view.TextureRegistry.SurfaceProducer;
import java.util.ArrayList;
import java.util.List;

/**
* A class responsible for managing video playback using {@link ExoPlayer}.
Expand All @@ -26,6 +35,7 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi {
@Nullable protected final SurfaceProducer surfaceProducer;
@Nullable private DisposeHandler disposeHandler;
@NonNull protected ExoPlayer exoPlayer;
@UnstableApi @Nullable protected DefaultTrackSelector trackSelector;

/** A closure-compatible signature since {@link java.util.function.Supplier} is API level 24. */
public interface ExoPlayerProvider {
Expand All @@ -43,6 +53,7 @@ public interface DisposeHandler {
void onDispose();
}

@UnstableApi
public VideoPlayer(
@NonNull VideoPlayerCallbacks events,
@NonNull MediaItem mediaItem,
Expand All @@ -52,6 +63,12 @@ public VideoPlayer(
this.videoPlayerEvents = events;
this.surfaceProducer = surfaceProducer;
exoPlayer = exoPlayerProvider.get();

// Try to get the track selector from the ExoPlayer if it was built with one
if (exoPlayer.getTrackSelector() instanceof DefaultTrackSelector) {
trackSelector = (DefaultTrackSelector) exoPlayer.getTrackSelector();
}

exoPlayer.setMediaItem(mediaItem);
exoPlayer.prepare();
exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer));
Expand Down Expand Up @@ -122,6 +139,112 @@ public ExoPlayer getExoPlayer() {
return exoPlayer;
}

@UnstableApi
@Override
public @NonNull NativeAudioTrackData getAudioTracks() {
List<ExoPlayerAudioTrackData> audioTracks = new ArrayList<>();

// Get the current tracks from ExoPlayer
Tracks tracks = exoPlayer.getCurrentTracks();

// Iterate through all track groups
for (int groupIndex = 0; groupIndex < tracks.getGroups().size(); groupIndex++) {
Tracks.Group group = tracks.getGroups().get(groupIndex);

// Only process audio tracks
if (group.getType() == C.TRACK_TYPE_AUDIO) {
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
Format format = group.getTrackFormat(trackIndex);
boolean isSelected = group.isTrackSelected(trackIndex);

// Create audio track data with metadata
ExoPlayerAudioTrackData audioTrack =
new ExoPlayerAudioTrackData(
(long) groupIndex,
(long) trackIndex,
format.label,
format.language,
isSelected,
format.bitrate != Format.NO_VALUE ? (long) format.bitrate : null,
format.sampleRate != Format.NO_VALUE ? (long) format.sampleRate : null,
format.channelCount != Format.NO_VALUE ? (long) format.channelCount : null,
format.codecs != null ? format.codecs : null);

audioTracks.add(audioTrack);
}
}
}
return new NativeAudioTrackData(audioTracks);
}

@UnstableApi
@Override
public void selectAudioTrack(long groupIndex, long trackIndex) {
if (trackSelector == null) {
Log.w("VideoPlayer", "Cannot select audio track: track selector is null");
return;
}

try {

// Get current tracks
Tracks tracks = exoPlayer.getCurrentTracks();

if (groupIndex >= tracks.getGroups().size()) {
Log.w(
"VideoPlayer",
"Cannot select audio track: groupIndex "
+ groupIndex
+ " is out of bounds (available groups: "
+ tracks.getGroups().size()
+ ")");
return;
}

Tracks.Group group = tracks.getGroups().get((int) groupIndex);

// Verify it's an audio track and the track index is valid
if (group.getType() != C.TRACK_TYPE_AUDIO || (int) trackIndex >= group.length) {
if (group.getType() != C.TRACK_TYPE_AUDIO) {
Log.w(
"VideoPlayer",
"Cannot select audio track: group at index "
+ groupIndex
+ " is not an audio track (type: "
+ group.getType()
+ ")");
} else {
Log.w(
"VideoPlayer",
"Cannot select audio track: trackIndex "
+ trackIndex
+ " is out of bounds (available tracks in group: "
+ group.length
+ ")");
}
return;
}

// Get the track group and create a selection override
TrackGroup trackGroup = group.getMediaTrackGroup();
TrackSelectionOverride override = new TrackSelectionOverride(trackGroup, (int) trackIndex);

// Apply the track selection override
trackSelector.setParameters(
trackSelector.buildUponParameters().setOverrideForType(override).build());

Choose a reason for hiding this comment

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

medium

The setParameters method can throw exceptions if the provided parameters are invalid or if there is an issue with the underlying player. Consider adding a try-catch block around this call to handle potential exceptions and prevent crashes. Log the exception for debugging purposes.


} catch (ArrayIndexOutOfBoundsException e) {
Log.w(
"VideoPlayer",
"Cannot select audio track: invalid indices (groupIndex: "
+ groupIndex
+ ", trackIndex: "
+ trackIndex
+ "). "
+ e.getMessage());
}
}

public void dispose() {
if (disposeHandler != null) {
disposeHandler.onDispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ public interface VideoPlayerCallbacks {
void onError(@NonNull String code, @Nullable String message, @Nullable Object details);

void onIsPlayingStateUpdate(boolean isPlaying);

void onAudioTrackChanged(@Nullable String selectedTrackId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,9 @@ public void onError(@NonNull String code, @Nullable String message, @Nullable Ob
public void onIsPlayingStateUpdate(boolean isPlaying) {
eventSink.success(new IsPlayingStateEvent(isPlaying));
}

@Override
public void onAudioTrackChanged(@Nullable String selectedTrackId) {
eventSink.success(new AudioTrackChangedEvent(selectedTrackId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
import io.flutter.plugins.videoplayer.VideoAsset;
Expand All @@ -22,6 +23,7 @@
* displaying the video in the app.
*/
public class PlatformViewVideoPlayer extends VideoPlayer {
@UnstableApi
@VisibleForTesting
public PlatformViewVideoPlayer(
@NonNull VideoPlayerCallbacks events,
Expand All @@ -40,6 +42,7 @@ public PlatformViewVideoPlayer(
* @param options options for playback.
* @return a video player instance.
*/
@UnstableApi
@NonNull
public static PlatformViewVideoPlayer create(
@NonNull Context context,
Expand All @@ -51,8 +54,11 @@ public static PlatformViewVideoPlayer create(
asset.getMediaItem(),
options,
() -> {
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
ExoPlayer.Builder builder =
new ExoPlayer.Builder(context)
.setTrackSelector(trackSelector)
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
return builder.build();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.ExoPlayer;
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
import io.flutter.plugins.videoplayer.VideoAsset;
Expand Down Expand Up @@ -39,6 +40,7 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProd
* @param options options for playback.
* @return a video player instance.
*/
@UnstableApi
@NonNull
public static TextureVideoPlayer create(
@NonNull Context context,
Expand All @@ -52,13 +54,17 @@ public static TextureVideoPlayer create(
asset.getMediaItem(),
options,
() -> {
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
ExoPlayer.Builder builder =
new ExoPlayer.Builder(context)
.setTrackSelector(trackSelector)
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
return builder.build();
});
}

@UnstableApi
@VisibleForTesting
public TextureVideoPlayer(
@NonNull VideoPlayerCallbacks events,
Expand Down
Loading