Skip to content

Commit 4471eb2

Browse files
committed
feat(android): add audio track change notifications in video player
- Added onTracksChanged listener to notify when audio track selection changes - Implemented findSelectedAudioTrackId helper to get current audio track ID - Updated VideoPlayerCallbacks to include onAudioTrackChanged event - Added AudioTrackChangedEvent class to handle track change notifications - Modified selectAudioTrack to wait for track change confirmation with timeout - Refactored audio track data structures to use simpler construct
1 parent 2c7993f commit 4471eb2

File tree

10 files changed

+229
-74
lines changed

10 files changed

+229
-74
lines changed

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: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ public ExoPlayer getExoPlayer() {
141141

142142
@UnstableApi
143143
@Override
144-
public @NonNull Messages.NativeAudioTrackData getAudioTracks() {
145-
List<Messages.ExoPlayerAudioTrackData> audioTracks = new ArrayList<>();
144+
public @NonNull NativeAudioTrackData getAudioTracks() {
145+
List<ExoPlayerAudioTrackData> audioTracks = new ArrayList<>();
146146

147147
// Get the current tracks from ExoPlayer
148148
Tracks tracks = exoPlayer.getCurrentTracks();
@@ -157,27 +157,23 @@ public ExoPlayer getExoPlayer() {
157157
Format format = group.getTrackFormat(trackIndex);
158158
boolean isSelected = group.isTrackSelected(trackIndex);
159159

160-
// Create AudioTrackMessage with metadata
161-
Messages.ExoPlayerAudioTrackData audioTrack =
162-
new Messages.ExoPlayerAudioTrackData.Builder()
163-
.setTrackId(groupIndex + "_" + trackIndex)
164-
.setLabel(format.label)
165-
.setLanguage(format.language)
166-
.setIsSelected(isSelected)
167-
.setBitrate(format.bitrate != Format.NO_VALUE ? (long) format.bitrate : null)
168-
.setSampleRate(
169-
format.sampleRate != Format.NO_VALUE ? (long) format.sampleRate : null)
170-
.setChannelCount(
171-
format.channelCount != Format.NO_VALUE ? (long) format.channelCount : null)
172-
.setCodec(format.codecs != null ? format.codecs : null)
173-
.build();
160+
// Create audio track data with metadata
161+
ExoPlayerAudioTrackData audioTrack =
162+
new ExoPlayerAudioTrackData(
163+
groupIndex + "_" + trackIndex,
164+
format.label,
165+
format.language,
166+
isSelected,
167+
format.bitrate != Format.NO_VALUE ? (long) format.bitrate : null,
168+
format.sampleRate != Format.NO_VALUE ? (long) format.sampleRate : null,
169+
format.channelCount != Format.NO_VALUE ? (long) format.channelCount : null,
170+
format.codecs != null ? format.codecs : null);
174171

175172
audioTracks.add(audioTrack);
176173
}
177174
}
178175
}
179-
180-
return new Messages.NativeAudioTrackData.Builder().setExoPlayerTracks(audioTracks).build();
176+
return new NativeAudioTrackData(audioTracks);
181177
}
182178

183179
@UnstableApi

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/kotlin/io/flutter/plugins/videoplayer/Messages.kt

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,44 @@ data class IsPlayingStateEvent(val isPlaying: Boolean) : PlatformVideoEvent() {
225225
override fun hashCode(): Int = toList().hashCode()
226226
}
227227

228+
/**
229+
* Sent when audio tracks change.
230+
*
231+
* This includes when the selected audio track changes after calling selectAudioTrack. Corresponds
232+
* to ExoPlayer's onTracksChanged.
233+
*
234+
* Generated class from Pigeon that represents data sent in messages.
235+
*/
236+
data class AudioTrackChangedEvent(
237+
/** The ID of the newly selected audio track, if any. */
238+
val selectedTrackId: String? = null
239+
) : PlatformVideoEvent() {
240+
companion object {
241+
fun fromList(pigeonVar_list: List<Any?>): AudioTrackChangedEvent {
242+
val selectedTrackId = pigeonVar_list[0] as String?
243+
return AudioTrackChangedEvent(selectedTrackId)
244+
}
245+
}
246+
247+
fun toList(): List<Any?> {
248+
return listOf(
249+
selectedTrackId,
250+
)
251+
}
252+
253+
override fun equals(other: Any?): Boolean {
254+
if (other !is AudioTrackChangedEvent) {
255+
return false
256+
}
257+
if (this === other) {
258+
return true
259+
}
260+
return MessagesPigeonUtils.deepEquals(toList(), other.toList())
261+
}
262+
263+
override fun hashCode(): Int = toList().hashCode()
264+
}
265+
228266
/**
229267
* Information passed to the platform view creation.
230268
*
@@ -527,26 +565,29 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
527565
return (readValue(buffer) as? List<Any?>)?.let { IsPlayingStateEvent.fromList(it) }
528566
}
529567
134.toByte() -> {
568+
return (readValue(buffer) as? List<Any?>)?.let { AudioTrackChangedEvent.fromList(it) }
569+
}
570+
135.toByte() -> {
530571
return (readValue(buffer) as? List<Any?>)?.let {
531572
PlatformVideoViewCreationParams.fromList(it)
532573
}
533574
}
534-
135.toByte() -> {
575+
136.toByte() -> {
535576
return (readValue(buffer) as? List<Any?>)?.let { CreationOptions.fromList(it) }
536577
}
537-
136.toByte() -> {
578+
137.toByte() -> {
538579
return (readValue(buffer) as? List<Any?>)?.let { TexturePlayerIds.fromList(it) }
539580
}
540-
137.toByte() -> {
581+
138.toByte() -> {
541582
return (readValue(buffer) as? List<Any?>)?.let { PlaybackState.fromList(it) }
542583
}
543-
138.toByte() -> {
584+
139.toByte() -> {
544585
return (readValue(buffer) as? List<Any?>)?.let { AudioTrackMessage.fromList(it) }
545586
}
546-
139.toByte() -> {
587+
140.toByte() -> {
547588
return (readValue(buffer) as? List<Any?>)?.let { ExoPlayerAudioTrackData.fromList(it) }
548589
}
549-
140.toByte() -> {
590+
141.toByte() -> {
550591
return (readValue(buffer) as? List<Any?>)?.let { NativeAudioTrackData.fromList(it) }
551592
}
552593
else -> super.readValueOfType(type, buffer)
@@ -575,34 +616,38 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
575616
stream.write(133)
576617
writeValue(stream, value.toList())
577618
}
578-
is PlatformVideoViewCreationParams -> {
619+
is AudioTrackChangedEvent -> {
579620
stream.write(134)
580621
writeValue(stream, value.toList())
581622
}
582-
is CreationOptions -> {
623+
is PlatformVideoViewCreationParams -> {
583624
stream.write(135)
584625
writeValue(stream, value.toList())
585626
}
586-
is TexturePlayerIds -> {
627+
is CreationOptions -> {
587628
stream.write(136)
588629
writeValue(stream, value.toList())
589630
}
590-
is PlaybackState -> {
631+
is TexturePlayerIds -> {
591632
stream.write(137)
592633
writeValue(stream, value.toList())
593634
}
594-
is AudioTrackMessage -> {
635+
is PlaybackState -> {
595636
stream.write(138)
596637
writeValue(stream, value.toList())
597638
}
598-
is ExoPlayerAudioTrackData -> {
639+
is AudioTrackMessage -> {
599640
stream.write(139)
600641
writeValue(stream, value.toList())
601642
}
602-
is NativeAudioTrackData -> {
643+
is ExoPlayerAudioTrackData -> {
603644
stream.write(140)
604645
writeValue(stream, value.toList())
605646
}
647+
is NativeAudioTrackData -> {
648+
stream.write(141)
649+
writeValue(stream, value.toList())
650+
}
606651
else -> super.writeValue(stream, value)
607652
}
608653
}

packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/AudioTracksTest.java

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -116,30 +116,30 @@ public void testGetAudioTracks_withMultipleAudioTracks() {
116116
when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks);
117117

118118
// Test the method
119-
Messages.NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
120-
List<Messages.ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
119+
NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
120+
List<ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
121121

122122
// Verify results
123123
assertNotNull(result);
124124
assertEquals(2, result.size());
125125

126126
// Verify first track
127-
Messages.ExoPlayerAudioTrackData track1 = result.get(0);
127+
ExoPlayerAudioTrackData track1 = result.get(0);
128128
assertEquals("0_0", track1.getTrackId());
129129
assertEquals("English", track1.getLabel());
130130
assertEquals("en", track1.getLanguage());
131-
assertTrue(track1.getIsSelected());
131+
assertTrue(track1.isSelected());
132132
assertEquals(Long.valueOf(128000), track1.getBitrate());
133133
assertEquals(Long.valueOf(48000), track1.getSampleRate());
134134
assertEquals(Long.valueOf(2), track1.getChannelCount());
135135
assertEquals("mp4a.40.2", track1.getCodec());
136136

137137
// Verify second track
138-
Messages.ExoPlayerAudioTrackData track2 = result.get(1);
138+
ExoPlayerAudioTrackData track2 = result.get(1);
139139
assertEquals("1_0", track2.getTrackId());
140140
assertEquals("Español", track2.getLabel());
141141
assertEquals("es", track2.getLanguage());
142-
assertFalse(track2.getIsSelected());
142+
assertFalse(track2.isSelected());
143143
assertEquals(Long.valueOf(96000), track2.getBitrate());
144144
assertEquals(Long.valueOf(44100), track2.getSampleRate());
145145
assertEquals(Long.valueOf(2), track2.getChannelCount());
@@ -156,8 +156,8 @@ public void testGetAudioTracks_withNoAudioTracks() {
156156
when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks);
157157

158158
// Test the method
159-
Messages.NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
160-
List<Messages.ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
159+
NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
160+
List<ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
161161

162162
// Verify results
163163
assertNotNull(result);
@@ -189,18 +189,18 @@ public void testGetAudioTracks_withNullValues() {
189189
when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks);
190190

191191
// Test the method
192-
Messages.NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
193-
List<Messages.ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
192+
NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
193+
List<ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
194194

195195
// Verify results
196196
assertNotNull(result);
197197
assertEquals(1, result.size());
198198

199-
Messages.ExoPlayerAudioTrackData track = result.get(0);
199+
ExoPlayerAudioTrackData track = result.get(0);
200200
assertEquals("0_0", track.getTrackId());
201201
assertNull(track.getLabel()); // Null values should be preserved
202202
assertNull(track.getLanguage()); // Null values should be preserved
203-
assertFalse(track.getIsSelected());
203+
assertFalse(track.isSelected());
204204
assertNull(track.getBitrate());
205205
assertNull(track.getSampleRate());
206206
assertNull(track.getChannelCount());
@@ -239,16 +239,16 @@ public void testGetAudioTracks_withMultipleTracksInSameGroup() {
239239
when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks);
240240

241241
// Test the method
242-
Messages.NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
243-
List<Messages.ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
242+
NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
243+
List<ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
244244

245245
// Verify results
246246
assertNotNull(result);
247247
assertEquals(2, result.size());
248248

249249
// Verify track IDs are unique
250-
Messages.ExoPlayerAudioTrackData track1 = result.get(0);
251-
Messages.ExoPlayerAudioTrackData track2 = result.get(1);
250+
ExoPlayerAudioTrackData track1 = result.get(0);
251+
ExoPlayerAudioTrackData track2 = result.get(1);
252252
assertEquals("0_0", track1.getTrackId());
253253
assertEquals("0_1", track2.getTrackId());
254254
assertNotEquals(track1.getTrackId(), track2.getTrackId());
@@ -276,8 +276,8 @@ public void testGetAudioTracks_withDifferentCodecs() {
276276
when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks);
277277

278278
// Test the method
279-
Messages.NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
280-
List<Messages.ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
279+
NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
280+
List<ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
281281

282282
// Verify results
283283
assertNotNull(result);
@@ -311,14 +311,14 @@ public void testGetAudioTracks_withHighBitrateValues() {
311311
when(mockExoPlayer.getCurrentTracks()).thenReturn(mockTracks);
312312

313313
// Test the method
314-
Messages.NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
315-
List<Messages.ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
314+
NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
315+
List<ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
316316

317317
// Verify results
318318
assertNotNull(result);
319319
assertEquals(1, result.size());
320320

321-
Messages.ExoPlayerAudioTrackData track = result.get(0);
321+
ExoPlayerAudioTrackData track = result.get(0);
322322
assertEquals(Long.valueOf(1536000), track.getBitrate());
323323
assertEquals(Long.valueOf(96000), track.getSampleRate());
324324
assertEquals(Long.valueOf(8), track.getChannelCount());
@@ -347,8 +347,8 @@ public void testGetAudioTracks_performanceWithManyTracks() {
347347

348348
// Measure performance
349349
long startTime = System.currentTimeMillis();
350-
Messages.NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
351-
List<Messages.ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
350+
NativeAudioTrackData nativeData = videoPlayer.getAudioTracks();
351+
List<ExoPlayerAudioTrackData> result = nativeData.getExoPlayerTracks();
352352
long endTime = System.currentTimeMillis();
353353

354354
// Verify results

packages/video_player/video_player_android/example/pubspec.yaml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies:
1818
# The example app is bundled with the plugin so we use a path dependency on
1919
# the parent directory to use the current plugin's version.
2020
path: ../
21-
video_player_platform_interface: ^6.3.0
21+
video_player_platform_interface: ^6.6.0
2222

2323
dev_dependencies:
2424
espresso: ^0.4.0
@@ -34,7 +34,3 @@ flutter:
3434
assets:
3535
- assets/flutter-mark-square-64.png
3636
- assets/Butterfly-209.mp4
37-
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
38-
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
39-
dependency_overrides:
40-
video_player_platform_interface: {path: ../../../../packages/video_player/video_player_platform_interface}

0 commit comments

Comments
 (0)