Skip to content

Commit 57725eb

Browse files
authored
feat(android): add audio track selection for ExoPlayer (#10312)
- Implemented getAudioTracks() and selectAudioTrack() methods for Android video player Android PR for : #9925 ## Pre-Review Checklist
1 parent 2cd921c commit 57725eb

File tree

19 files changed

+1635
-15
lines changed

19 files changed

+1635
-15
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.22
26

37
* Bumps kotlin_version to 2.2.21.

packages/video_player/video_player_android/android/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ android {
3232
minSdkVersion 24
3333
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
3434
}
35-
lintOptions {
35+
lint {
3636
checkAllWarnings = true
3737
warningsAsErrors = true
3838
disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency', 'NewerVersionAvailable'
39+
baseline = file("lint-baseline.xml")
3940
}
4041
compileOptions {
4142
sourceCompatibility = JavaVersion.VERSION_17
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<issues format="6" by="lint 8.11.0" type="baseline" client="gradle" dependencies="false" name="AGP (8.11.0)" variant="all" version="8.11.0">
3+
4+
<issue
5+
id="MemberExtensionConflict"
6+
message="`containsKey` is defined both as a member in class `kotlin.collections.Map` and an extension in package `kotlin.collections`. The defined behavior for this is to use the member, but since the extension is explicitly imported into this file, there&apos;s a chance that this was not expected. (One common way this happens is for members to be added to a class after code was already written to use an extension)."
7+
errorLine1=" a.all { (b as Map&lt;Any?, Any?>).containsKey(it.key) &amp;&amp; deepEquals(it.value, b[it.key]) }"
8+
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
9+
<location
10+
file="src/main/kotlin/io/flutter/plugins/videoplayer/Messages.kt"
11+
line="58"
12+
column="19"/>
13+
</issue>
14+
15+
</issues>

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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@
1111
import androidx.annotation.Nullable;
1212
import androidx.media3.common.AudioAttributes;
1313
import androidx.media3.common.C;
14+
import androidx.media3.common.Format;
1415
import androidx.media3.common.MediaItem;
1516
import androidx.media3.common.PlaybackParameters;
17+
import androidx.media3.common.TrackGroup;
18+
import androidx.media3.common.TrackSelectionOverride;
19+
import androidx.media3.common.Tracks;
20+
import androidx.media3.common.util.UnstableApi;
1621
import androidx.media3.exoplayer.ExoPlayer;
22+
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
1723
import io.flutter.view.TextureRegistry.SurfaceProducer;
24+
import java.util.ArrayList;
25+
import java.util.List;
1826

1927
/**
2028
* A class responsible for managing video playback using {@link ExoPlayer}.
@@ -26,6 +34,8 @@ public abstract class VideoPlayer implements VideoPlayerInstanceApi {
2634
@Nullable protected final SurfaceProducer surfaceProducer;
2735
@Nullable private DisposeHandler disposeHandler;
2836
@NonNull protected ExoPlayer exoPlayer;
37+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
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,8 @@ public interface DisposeHandler {
4353
void onDispose();
4454
}
4555

56+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
57+
@UnstableApi
4658
// Error thrown for this-escape warning on JDK 21+ due to https://bugs.openjdk.org/browse/JDK-8015831.
4759
// Keeping behavior as-is and addressing the warning could cause a regression: https://github.com/flutter/packages/pull/10193
4860
@SuppressWarnings("this-escape")
@@ -55,6 +67,12 @@ public VideoPlayer(
5567
this.videoPlayerEvents = events;
5668
this.surfaceProducer = surfaceProducer;
5769
exoPlayer = exoPlayerProvider.get();
70+
71+
// Try to get the track selector from the ExoPlayer if it was built with one
72+
if (exoPlayer.getTrackSelector() instanceof DefaultTrackSelector) {
73+
trackSelector = (DefaultTrackSelector) exoPlayer.getTrackSelector();
74+
}
75+
5876
exoPlayer.setMediaItem(mediaItem);
5977
exoPlayer.prepare();
6078
exoPlayer.addListener(createExoPlayerEventListener(exoPlayer, surfaceProducer));
@@ -125,6 +143,96 @@ public ExoPlayer getExoPlayer() {
125143
return exoPlayer;
126144
}
127145

146+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
147+
@UnstableApi
148+
@Override
149+
public @NonNull NativeAudioTrackData getAudioTracks() {
150+
List<ExoPlayerAudioTrackData> audioTracks = new ArrayList<>();
151+
152+
// Get the current tracks from ExoPlayer
153+
Tracks tracks = exoPlayer.getCurrentTracks();
154+
155+
// Iterate through all track groups
156+
for (int groupIndex = 0; groupIndex < tracks.getGroups().size(); groupIndex++) {
157+
Tracks.Group group = tracks.getGroups().get(groupIndex);
158+
159+
// Only process audio tracks
160+
if (group.getType() == C.TRACK_TYPE_AUDIO) {
161+
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
162+
Format format = group.getTrackFormat(trackIndex);
163+
boolean isSelected = group.isTrackSelected(trackIndex);
164+
165+
// Create audio track data with metadata
166+
ExoPlayerAudioTrackData audioTrack =
167+
new ExoPlayerAudioTrackData(
168+
(long) groupIndex,
169+
(long) trackIndex,
170+
format.label,
171+
format.language,
172+
isSelected,
173+
format.bitrate != Format.NO_VALUE ? (long) format.bitrate : null,
174+
format.sampleRate != Format.NO_VALUE ? (long) format.sampleRate : null,
175+
format.channelCount != Format.NO_VALUE ? (long) format.channelCount : null,
176+
format.codecs != null ? format.codecs : null);
177+
178+
audioTracks.add(audioTrack);
179+
}
180+
}
181+
}
182+
return new NativeAudioTrackData(audioTracks);
183+
}
184+
185+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
186+
@UnstableApi
187+
@Override
188+
public void selectAudioTrack(long groupIndex, long trackIndex) {
189+
if (trackSelector == null) {
190+
throw new IllegalStateException("Cannot select audio track: track selector is null");
191+
}
192+
193+
// Get current tracks
194+
Tracks tracks = exoPlayer.getCurrentTracks();
195+
196+
if (groupIndex < 0 || groupIndex >= tracks.getGroups().size()) {
197+
throw new IllegalArgumentException(
198+
"Cannot select audio track: groupIndex "
199+
+ groupIndex
200+
+ " is out of bounds (available groups: "
201+
+ tracks.getGroups().size()
202+
+ ")");
203+
}
204+
205+
Tracks.Group group = tracks.getGroups().get((int) groupIndex);
206+
207+
// Verify it's an audio track
208+
if (group.getType() != C.TRACK_TYPE_AUDIO) {
209+
throw new IllegalArgumentException(
210+
"Cannot select audio track: group at index "
211+
+ groupIndex
212+
+ " is not an audio track (type: "
213+
+ group.getType()
214+
+ ")");
215+
}
216+
217+
// Verify the track index is valid
218+
if (trackIndex < 0 || (int) trackIndex >= group.length) {
219+
throw new IllegalArgumentException(
220+
"Cannot select audio track: trackIndex "
221+
+ trackIndex
222+
+ " is out of bounds (available tracks in group: "
223+
+ group.length
224+
+ ")");
225+
}
226+
227+
// Get the track group and create a selection override
228+
TrackGroup trackGroup = group.getMediaTrackGroup();
229+
TrackSelectionOverride override = new TrackSelectionOverride(trackGroup, (int) trackIndex);
230+
231+
// Apply the track selection override
232+
trackSelector.setParameters(
233+
trackSelector.buildUponParameters().setOverrideForType(override).build());
234+
}
235+
128236
public void dispose() {
129237
if (disposeHandler != null) {
130238
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/VideoPlayerPlugin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import android.util.LongSparseArray;
99
import androidx.annotation.NonNull;
1010
import androidx.annotation.Nullable;
11+
import androidx.annotation.OptIn;
12+
import androidx.media3.common.util.UnstableApi;
1113
import io.flutter.FlutterInjector;
1214
import io.flutter.Log;
1315
import io.flutter.embedding.engine.plugins.FlutterPlugin;
@@ -78,6 +80,7 @@ public void initialize() {
7880
disposeAllPlayers();
7981
}
8082

83+
@OptIn(markerClass = UnstableApi.class)
8184
@Override
8285
public long createForPlatformView(@NonNull CreationOptions options) {
8386
final VideoAsset videoAsset = videoAssetWithOptions(options);
@@ -95,6 +98,7 @@ public long createForPlatformView(@NonNull CreationOptions options) {
9598
return id;
9699
}
97100

101+
@OptIn(markerClass = UnstableApi.class)
98102
@Override
99103
public @NonNull TexturePlayerIds createForTextureView(@NonNull CreationOptions options) {
100104
final VideoAsset videoAsset = videoAssetWithOptions(options);

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

Lines changed: 8 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,8 @@
2223
* displaying the video in the app.
2324
*/
2425
public class PlatformViewVideoPlayer extends VideoPlayer {
26+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
27+
@UnstableApi
2528
@VisibleForTesting
2629
public PlatformViewVideoPlayer(
2730
@NonNull VideoPlayerCallbacks events,
@@ -40,6 +43,8 @@ public PlatformViewVideoPlayer(
4043
* @param options options for playback.
4144
* @return a video player instance.
4245
*/
46+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
47+
@UnstableApi
4348
@NonNull
4449
public static PlatformViewVideoPlayer create(
4550
@NonNull Context context,
@@ -51,8 +56,11 @@ public static PlatformViewVideoPlayer create(
5156
asset.getMediaItem(),
5257
options,
5358
() -> {
59+
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
60+
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
5461
ExoPlayer.Builder builder =
5562
new ExoPlayer.Builder(context)
63+
.setTrackSelector(trackSelector)
5664
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
5765
return builder.build();
5866
});

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

Lines changed: 8 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,8 @@ public final class TextureVideoPlayer extends VideoPlayer implements SurfaceProd
3940
* @param options options for playback.
4041
* @return a video player instance.
4142
*/
43+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
44+
@UnstableApi
4245
@NonNull
4346
public static TextureVideoPlayer create(
4447
@NonNull Context context,
@@ -52,13 +55,18 @@ public static TextureVideoPlayer create(
5255
asset.getMediaItem(),
5356
options,
5457
() -> {
58+
androidx.media3.exoplayer.trackselection.DefaultTrackSelector trackSelector =
59+
new androidx.media3.exoplayer.trackselection.DefaultTrackSelector(context);
5560
ExoPlayer.Builder builder =
5661
new ExoPlayer.Builder(context)
62+
.setTrackSelector(trackSelector)
5763
.setMediaSourceFactory(asset.getMediaSourceFactory(context));
5864
return builder.build();
5965
});
6066
}
6167

68+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
69+
@UnstableApi
6270
@VisibleForTesting
6371
public TextureVideoPlayer(
6472
@NonNull VideoPlayerCallbacks events,

0 commit comments

Comments
 (0)