Skip to content

Commit b544b45

Browse files
committed
feat(video_player_android): implement audio track selection API
- Added getAudioTracks() method to retrieve available audio tracks with metadata (bitrate, sample rate, channel count, codec) - Added selectAudioTrack() method to switch between audio tracks using ExoPlayer's track selector - Implemented onTracksChanged listener to notify when audio track selection changes
1 parent 3caa48b commit b544b45

File tree

14 files changed

+1406
-13
lines changed

14 files changed

+1406
-13
lines changed

packages/video_player/video_player_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.9.0
2+
3+
* Implements `getAudioTracks()` and `selectAudioTrack()` methods for Android using ExoPlayer.
4+
15
## 2.8.17
26

37
* Moves video event processing logic to Dart, and fixes an issue where buffer

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
package io.flutter.plugins.videoplayer;
66

77
import androidx.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
import androidx.media3.common.C;
810
import androidx.media3.common.PlaybackException;
911
import androidx.media3.common.Player;
12+
import androidx.media3.common.Tracks;
1013
import androidx.media3.exoplayer.ExoPlayer;
1114

1215
public abstract class ExoPlayerEventListener implements Player.Listener {
@@ -88,4 +91,34 @@ public void onPlayerError(@NonNull final PlaybackException error) {
8891
public void onIsPlayingChanged(boolean isPlaying) {
8992
events.onIsPlayingStateUpdate(isPlaying);
9093
}
94+
95+
@Override
96+
public void onTracksChanged(@NonNull Tracks tracks) {
97+
// Find the currently selected audio track and notify
98+
String selectedTrackId = findSelectedAudioTrackId(tracks);
99+
events.onAudioTrackChanged(selectedTrackId);
100+
}
101+
102+
/**
103+
* Finds the ID of the currently selected audio track.
104+
*
105+
* @param tracks The current tracks
106+
* @return The track ID in format "groupIndex_trackIndex", or null if no audio track is selected
107+
*/
108+
@Nullable
109+
private String findSelectedAudioTrackId(@NonNull Tracks tracks) {
110+
int groupIndex = 0;
111+
for (Tracks.Group group : tracks.getGroups()) {
112+
if (group.getType() == C.TRACK_TYPE_AUDIO && group.isSelected()) {
113+
// Find the selected track within this group
114+
for (int i = 0; i < group.length; i++) {
115+
if (group.isTrackSelected(i)) {
116+
return groupIndex + "_" + i;
117+
}
118+
}
119+
}
120+
groupIndex++;
121+
}
122+
return null;
123+
}
91124
}

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,23 @@
77
import static androidx.media3.common.Player.REPEAT_MODE_ALL;
88
import static androidx.media3.common.Player.REPEAT_MODE_OFF;
99

10+
import android.util.Log;
1011
import androidx.annotation.NonNull;
1112
import androidx.annotation.Nullable;
1213
import androidx.media3.common.AudioAttributes;
1314
import androidx.media3.common.C;
15+
import androidx.media3.common.Format;
1416
import androidx.media3.common.MediaItem;
1517
import androidx.media3.common.PlaybackParameters;
18+
import androidx.media3.common.TrackGroup;
19+
import androidx.media3.common.TrackSelectionOverride;
20+
import androidx.media3.common.Tracks;
21+
import androidx.media3.common.util.UnstableApi;
1622
import androidx.media3.exoplayer.ExoPlayer;
23+
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
1724
import io.flutter.view.TextureRegistry.SurfaceProducer;
25+
import java.util.ArrayList;
26+
import java.util.List;
1827

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

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

56+
@UnstableApi
4657
public VideoPlayer(
4758
@NonNull VideoPlayerCallbacks events,
4859
@NonNull MediaItem mediaItem,
@@ -52,6 +63,12 @@ public VideoPlayer(
5263
this.videoPlayerEvents = events;
5364
this.surfaceProducer = surfaceProducer;
5465
exoPlayer = exoPlayerProvider.get();
66+
67+
// Try to get the track selector from the ExoPlayer if it was built with one
68+
if (exoPlayer.getTrackSelector() instanceof DefaultTrackSelector) {
69+
trackSelector = (DefaultTrackSelector) exoPlayer.getTrackSelector();
70+
}
71+
5572
exoPlayer.setMediaItem(mediaItem);
5673
exoPlayer.prepare();
5774
exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer));
@@ -122,6 +139,112 @@ public ExoPlayer getExoPlayer() {
122139
return exoPlayer;
123140
}
124141

142+
@UnstableApi
143+
@Override
144+
public @NonNull NativeAudioTrackData getAudioTracks() {
145+
List<ExoPlayerAudioTrackData> audioTracks = new ArrayList<>();
146+
147+
// Get the current tracks from ExoPlayer
148+
Tracks tracks = exoPlayer.getCurrentTracks();
149+
150+
// Iterate through all track groups
151+
for (int groupIndex = 0; groupIndex < tracks.getGroups().size(); groupIndex++) {
152+
Tracks.Group group = tracks.getGroups().get(groupIndex);
153+
154+
// Only process audio tracks
155+
if (group.getType() == C.TRACK_TYPE_AUDIO) {
156+
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
157+
Format format = group.getTrackFormat(trackIndex);
158+
boolean isSelected = group.isTrackSelected(trackIndex);
159+
160+
// Create audio track data with metadata
161+
ExoPlayerAudioTrackData audioTrack =
162+
new ExoPlayerAudioTrackData(
163+
(long) groupIndex,
164+
(long) trackIndex,
165+
format.label,
166+
format.language,
167+
isSelected,
168+
format.bitrate != Format.NO_VALUE ? (long) format.bitrate : null,
169+
format.sampleRate != Format.NO_VALUE ? (long) format.sampleRate : null,
170+
format.channelCount != Format.NO_VALUE ? (long) format.channelCount : null,
171+
format.codecs != null ? format.codecs : null);
172+
173+
audioTracks.add(audioTrack);
174+
}
175+
}
176+
}
177+
return new NativeAudioTrackData(audioTracks);
178+
}
179+
180+
@UnstableApi
181+
@Override
182+
public void selectAudioTrack(long groupIndex, long trackIndex) {
183+
if (trackSelector == null) {
184+
Log.w("VideoPlayer", "Cannot select audio track: track selector is null");
185+
return;
186+
}
187+
188+
try {
189+
190+
// Get current tracks
191+
Tracks tracks = exoPlayer.getCurrentTracks();
192+
193+
if (groupIndex >= tracks.getGroups().size()) {
194+
Log.w(
195+
"VideoPlayer",
196+
"Cannot select audio track: groupIndex "
197+
+ groupIndex
198+
+ " is out of bounds (available groups: "
199+
+ tracks.getGroups().size()
200+
+ ")");
201+
return;
202+
}
203+
204+
Tracks.Group group = tracks.getGroups().get((int) groupIndex);
205+
206+
// Verify it's an audio track and the track index is valid
207+
if (group.getType() != C.TRACK_TYPE_AUDIO || (int) trackIndex >= group.length) {
208+
if (group.getType() != C.TRACK_TYPE_AUDIO) {
209+
Log.w(
210+
"VideoPlayer",
211+
"Cannot select audio track: group at index "
212+
+ groupIndex
213+
+ " is not an audio track (type: "
214+
+ group.getType()
215+
+ ")");
216+
} else {
217+
Log.w(
218+
"VideoPlayer",
219+
"Cannot select audio track: trackIndex "
220+
+ trackIndex
221+
+ " is out of bounds (available tracks in group: "
222+
+ group.length
223+
+ ")");
224+
}
225+
return;
226+
}
227+
228+
// Get the track group and create a selection override
229+
TrackGroup trackGroup = group.getMediaTrackGroup();
230+
TrackSelectionOverride override = new TrackSelectionOverride(trackGroup, (int) trackIndex);
231+
232+
// Apply the track selection override
233+
trackSelector.setParameters(
234+
trackSelector.buildUponParameters().setOverrideForType(override).build());
235+
236+
} catch (ArrayIndexOutOfBoundsException e) {
237+
Log.w(
238+
"VideoPlayer",
239+
"Cannot select audio track: invalid indices (groupIndex: "
240+
+ groupIndex
241+
+ ", trackIndex: "
242+
+ trackIndex
243+
+ "). "
244+
+ e.getMessage());
245+
}
246+
}
247+
125248
public void dispose() {
126249
if (disposeHandler != null) {
127250
disposeHandler.onDispose();

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ public interface VideoPlayerCallbacks {
2424
void onError(@NonNull String code, @Nullable String message, @Nullable Object details);
2525

2626
void onIsPlayingStateUpdate(boolean isPlaying);
27+
28+
void onAudioTrackChanged(@Nullable String selectedTrackId);
2729
}

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,9 @@ public void onError(@NonNull String code, @Nullable String message, @Nullable Ob
6363
public void onIsPlayingStateUpdate(boolean isPlaying) {
6464
eventSink.success(new IsPlayingStateEvent(isPlaying));
6565
}
66+
67+
@Override
68+
public void onAudioTrackChanged(@Nullable String selectedTrackId) {
69+
eventSink.success(new AudioTrackChangedEvent(selectedTrackId));
70+
}
6671
}

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/platformview/PlatformViewVideoPlayer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import androidx.annotation.Nullable;
1010
import androidx.annotation.VisibleForTesting;
1111
import androidx.media3.common.MediaItem;
12+
import androidx.media3.common.util.UnstableApi;
1213
import androidx.media3.exoplayer.ExoPlayer;
1314
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
1415
import io.flutter.plugins.videoplayer.VideoAsset;
@@ -22,6 +23,7 @@
2223
* displaying the video in the app.
2324
*/
2425
public class PlatformViewVideoPlayer extends VideoPlayer {
26+
@UnstableApi
2527
@VisibleForTesting
2628
public PlatformViewVideoPlayer(
2729
@NonNull VideoPlayerCallbacks events,
@@ -40,6 +42,7 @@ public PlatformViewVideoPlayer(
4042
* @param options options for playback.
4143
* @return a video player instance.
4244
*/
45+
@UnstableApi
4346
@NonNull
4447
public static PlatformViewVideoPlayer create(
4548
@NonNull Context context,
@@ -51,8 +54,11 @@ public static PlatformViewVideoPlayer create(
5154
asset.getMediaItem(),
5255
options,
5356
() -> {
57+
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
58+
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
5459
ExoPlayer.Builder builder =
5560
new ExoPlayer.Builder(context)
61+
.setTrackSelector(trackSelector)
5662
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
5763
return builder.build();
5864
});

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/texture/TextureVideoPlayer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import androidx.annotation.RestrictTo;
1212
import androidx.annotation.VisibleForTesting;
1313
import androidx.media3.common.MediaItem;
14+
import androidx.media3.common.util.UnstableApi;
1415
import androidx.media3.exoplayer.ExoPlayer;
1516
import io.flutter.plugins.videoplayer.ExoPlayerEventListener;
1617
import io.flutter.plugins.videoplayer.VideoAsset;
@@ -39,6 +40,7 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProd
3940
* @param options options for playback.
4041
* @return a video player instance.
4142
*/
43+
@UnstableApi
4244
@NonNull
4345
public static TextureVideoPlayer create(
4446
@NonNull Context context,
@@ -52,13 +54,17 @@ public static TextureVideoPlayer create(
5254
asset.getMediaItem(),
5355
options,
5456
() -> {
57+
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
58+
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
5559
ExoPlayer.Builder builder =
5660
new ExoPlayer.Builder(context)
61+
.setTrackSelector(trackSelector)
5762
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
5863
return builder.build();
5964
});
6065
}
6166

67+
@UnstableApi
6268
@VisibleForTesting
6369
public TextureVideoPlayer(
6470
@NonNull VideoPlayerCallbacks events,

0 commit comments

Comments
 (0)