Skip to content

Commit 20138de

Browse files
committed
refactor(video_player): simplify audio track selection with index-based API
- Changed selectAudioTrack to use integer index instead of string ID for more efficient track selection - Added caching of audio selection options to avoid repeated lookups during track switching - Removed unnecessary format description mocking in tests to prevent Core Media crashes in test environment
1 parent 4b0a8ef commit 20138de

File tree

11 files changed

+151
-108
lines changed

11 files changed

+151
-108
lines changed

packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,13 +1058,9 @@ - (void)testGetAudioTracksWithRegularAssetTracks {
10581058
OCMStub([mockTrack2 languageCode]).andReturn(@"es");
10591059
OCMStub([mockTrack2 estimatedDataRate]).andReturn(96000.0f);
10601060

1061-
// Mock format descriptions for track 1
1062-
id mockFormatDesc1 = OCMClassMock([NSObject class]);
1063-
AudioStreamBasicDescription asbd1 = {0};
1064-
asbd1.mSampleRate = 48000.0;
1065-
asbd1.mChannelsPerFrame = 2;
1066-
1067-
OCMStub([mockTrack1 formatDescriptions]).andReturn(@[ mockFormatDesc1 ]);
1061+
// Mock empty format descriptions to avoid Core Media crashes in test environment
1062+
OCMStub([mockTrack1 formatDescriptions]).andReturn(@[]);
1063+
OCMStub([mockTrack2 formatDescriptions]).andReturn(@[]);
10681064

10691065
// Mock the asset to return our tracks
10701066
NSArray *mockTracks = @[ mockTrack1, mockTrack2 ];

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ static void FVPRemoveKeyValueObservers(NSObject *observer,
7373
@implementation FVPVideoPlayer {
7474
// Whether or not player and player item listeners have ever been registered.
7575
BOOL _listenersRegistered;
76+
// Cached media selection options for audio tracks (HLS streams)
77+
NSArray<AVMediaSelectionOption *> *_cachedAudioSelectionOptions;
7678
}
7779

7880
- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
@@ -152,6 +154,9 @@ - (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error {
152154
FVPRemoveKeyValueObservers(self, FVPGetPlayerObservations(), self.player);
153155
}
154156

157+
// Clear cached audio selection options
158+
_cachedAudioSelectionOptions = nil;
159+
155160
[self.player replaceCurrentItemWithPlayerItem:nil];
156161

157162
if (_onDisposed) {
@@ -478,13 +483,15 @@ - (nullable FVPNativeAudioTrackData *)getAudioTracks:(FlutterError *_Nullable *_
478483
AVMediaSelectionGroup *audioGroup =
479484
[asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible];
480485
if (audioGroup && audioGroup.options.count > 0) {
486+
// Cache the options array for later use in selectAudioTrack
487+
_cachedAudioSelectionOptions = audioGroup.options;
488+
481489
NSMutableArray<FVPMediaSelectionAudioTrackData *> *mediaSelectionTracks =
482490
[[NSMutableArray alloc] init];
483491
AVMediaSelectionOption *currentSelection = nil;
484-
if (@available(iOS 11.0, *)) {
485-
AVMediaSelection *currentMediaSelection = currentItem.currentMediaSelection;
486-
currentSelection =
487-
[currentMediaSelection selectedMediaOptionInMediaSelectionGroup:audioGroup];
492+
if (@available(iOS 11.0, macOS 10.13, *)) {
493+
AVMediaSelection *mediaSelection = currentItem.currentMediaSelection;
494+
currentSelection = [mediaSelection selectedMediaOptionInMediaSelectionGroup:audioGroup];
488495
} else {
489496
#pragma clang diagnostic push
490497
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -574,15 +581,9 @@ - (nullable FVPNativeAudioTrackData *)getAudioTracks:(FlutterError *_Nullable *_
574581
NSString *className = NSStringFromClass([formatDescObj class]);
575582

576583
// Only process objects that are clearly Core Media format descriptions
577-
// This works for both real CMFormatDescription objects and properly configured mock
578-
// objects
579584
if ([className hasPrefix:@"CMAudioFormatDescription"] ||
580585
[className hasPrefix:@"CMVideoFormatDescription"] ||
581-
[className hasPrefix:@"CMFormatDescription"] ||
582-
[formatDescObj
583-
isKindOfClass:[NSObject
584-
class]]) { // Allow mock objects that inherit from NSObject
585-
586+
[className hasPrefix:@"CMFormatDescription"]) {
586587
CMFormatDescriptionRef formatDesc = (__bridge CMFormatDescriptionRef)formatDescObj;
587588

588589
// Validate the format description reference before using Core Media APIs
@@ -653,7 +654,7 @@ - (nullable FVPNativeAudioTrackData *)getAudioTracks:(FlutterError *_Nullable *_
653654
return [FVPNativeAudioTrackData makeWithAssetTracks:assetTracks mediaSelectionTracks:nil];
654655
}
655656

656-
- (void)selectAudioTrack:(nonnull NSString *)trackId
657+
- (void)selectAudioTrack:(NSInteger)trackIndex
657658
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
658659
AVPlayerItem *currentItem = _player.currentItem;
659660
if (!currentItem || !currentItem.asset) {
@@ -662,19 +663,14 @@ - (void)selectAudioTrack:(nonnull NSString *)trackId
662663

663664
AVAsset *asset = currentItem.asset;
664665

665-
// Check if this is a media selection track (for HLS streams)
666-
if ([trackId hasPrefix:@"media_selection_"]) {
666+
// Validate that we have cached options and the index is valid
667+
if (_cachedAudioSelectionOptions && trackIndex >= 0 &&
668+
trackIndex < _cachedAudioSelectionOptions.count) {
669+
AVMediaSelectionOption *option = _cachedAudioSelectionOptions[trackIndex];
667670
AVMediaSelectionGroup *audioGroup =
668671
[asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible];
669-
if (audioGroup && audioGroup.options.count > 0) {
670-
// Parse the track ID to get the index
671-
NSString *indexString = [trackId substringFromIndex:[@"media_selection_" length]];
672-
NSInteger index = [indexString integerValue];
673-
674-
if (index >= 0 && index < audioGroup.options.count) {
675-
AVMediaSelectionOption *option = audioGroup.options[index];
676-
[currentItem selectMediaOption:option inMediaSelectionGroup:audioGroup];
677-
}
672+
if (audioGroup) {
673+
[currentItem selectMediaOption:option inMediaSelectionGroup:audioGroup];
678674
}
679675
}
680676
// For asset tracks, we don't have a direct way to select them in AVFoundation

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(
156156
- (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error;
157157
/// @return `nil` only when `error != nil`.
158158
- (nullable FVPNativeAudioTrackData *)getAudioTracks:(FlutterError *_Nullable *_Nonnull)error;
159-
- (void)selectAudioTrack:(NSString *)trackId error:(FlutterError *_Nullable *_Nonnull)error;
159+
- (void)selectAudioTrack:(NSInteger)trackIndex error:(FlutterError *_Nullable *_Nonnull)error;
160160
@end
161161

162162
extern void SetUpFVPVideoPlayerInstanceApi(id<FlutterBinaryMessenger> binaryMessenger,

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,9 +743,9 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id<FlutterBinaryMessenger> binaryM
743743
api);
744744
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
745745
NSArray<id> *args = message;
746-
NSString *arg_trackId = GetNullableObjectAtIndex(args, 0);
746+
NSInteger arg_trackIndex = [GetNullableObjectAtIndex(args, 0) integerValue];
747747
FlutterError *error;
748-
[api selectAudioTrack:arg_trackId error:&error];
748+
[api selectAudioTrack:arg_trackIndex error:&error];
749749
callback(wrapResult(nil, error));
750750
}];
751751
} else {

packages/video_player/video_player_avfoundation/example/lib/main.dart

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,16 @@ class _App extends StatelessWidget {
3434
body: TabBarView(
3535
children: <Widget>[
3636
_ViewTypeTabBar(
37-
builder:
38-
(VideoViewType viewType) => _BumbleBeeRemoteVideo(viewType),
37+
builder: (VideoViewType viewType) =>
38+
_BumbleBeeRemoteVideo(viewType),
3939
),
4040
_ViewTypeTabBar(
41-
builder:
42-
(VideoViewType viewType) =>
43-
_BumbleBeeEncryptedLiveStream(viewType),
41+
builder: (VideoViewType viewType) =>
42+
_BumbleBeeEncryptedLiveStream(viewType),
4443
),
4544
_ViewTypeTabBar(
46-
builder:
47-
(VideoViewType viewType) => _ButterFlyAssetVideo(viewType),
45+
builder: (VideoViewType viewType) =>
46+
_ButterFlyAssetVideo(viewType),
4847
),
4948
],
5049
),
@@ -270,13 +269,12 @@ class _BumbleBeeEncryptedLiveStreamState
270269
const Text('With remote encrypted m3u8'),
271270
Container(
272271
padding: const EdgeInsets.all(20),
273-
child:
274-
_controller.value.isInitialized
275-
? AspectRatio(
276-
aspectRatio: _controller.value.aspectRatio,
277-
child: VideoPlayer(_controller),
278-
)
279-
: const Text('loading...'),
272+
child: _controller.value.isInitialized
273+
? AspectRatio(
274+
aspectRatio: _controller.value.aspectRatio,
275+
child: VideoPlayer(_controller),
276+
)
277+
: const Text('loading...'),
280278
),
281279
],
282280
),
@@ -307,20 +305,19 @@ class _ControlsOverlay extends StatelessWidget {
307305
AnimatedSwitcher(
308306
duration: const Duration(milliseconds: 50),
309307
reverseDuration: const Duration(milliseconds: 200),
310-
child:
311-
controller.value.isPlaying
312-
? const SizedBox.shrink()
313-
: const ColoredBox(
314-
color: Colors.black26,
315-
child: Center(
316-
child: Icon(
317-
Icons.play_arrow,
318-
color: Colors.white,
319-
size: 100.0,
320-
semanticLabel: 'Play',
321-
),
308+
child: controller.value.isPlaying
309+
? const SizedBox.shrink()
310+
: const ColoredBox(
311+
color: Colors.black26,
312+
child: Center(
313+
child: Icon(
314+
Icons.play_arrow,
315+
color: Colors.white,
316+
size: 100.0,
317+
semanticLabel: 'Play',
322318
),
323319
),
320+
),
324321
),
325322
GestureDetector(
326323
onTap: () {

packages/video_player/video_player_avfoundation/example/pubspec.yaml

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

2121
dev_dependencies:
2222
flutter_test:

packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform {
188188
),
189189
'completed' => VideoEvent(eventType: VideoEventType.completed),
190190
'bufferingUpdate' => VideoEvent(
191-
buffered:
192-
(map['values'] as List<dynamic>)
193-
.map<DurationRange>(_toDurationRange)
194-
.toList(),
191+
buffered: (map['values'] as List<dynamic>)
192+
.map<DurationRange>(_toDurationRange)
193+
.toList(),
195194
eventType: VideoEventType.bufferingUpdate,
196195
),
197196
'bufferingStart' => VideoEvent(
@@ -214,16 +213,19 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform {
214213

215214
@override
216215
Future<List<VideoAudioTrack>> getAudioTracks(int playerId) async {
217-
final NativeAudioTrackData nativeData =
218-
await _playerWith(id: playerId).getAudioTracks();
216+
final NativeAudioTrackData nativeData = await _playerWith(
217+
id: playerId,
218+
).getAudioTracks();
219219
final List<VideoAudioTrack> tracks = <VideoAudioTrack>[];
220220

221221
// Convert asset tracks to VideoAudioTrack
222+
// Note: AVFoundation doesn't have track groups like ExoPlayer, so we use groupIndex=0
222223
if (nativeData.assetTracks != null) {
223224
for (final AssetAudioTrackData track in nativeData.assetTracks!) {
224225
tracks.add(
225226
VideoAudioTrack(
226-
id: track.trackId.toString(),
227+
groupIndex: 0,
228+
trackIndex: track.trackId,
227229
label: track.label,
228230
language: track.language,
229231
isSelected: track.isSelected,
@@ -237,14 +239,15 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform {
237239
}
238240

239241
// Convert media selection tracks to VideoAudioTrack (for HLS streams)
242+
// Note: AVFoundation doesn't have track groups like ExoPlayer, so we use groupIndex=0
240243
if (nativeData.mediaSelectionTracks != null) {
241244
for (final MediaSelectionAudioTrackData track
242245
in nativeData.mediaSelectionTracks!) {
243-
final String trackId = 'media_selection_${track.index}';
244246
final String? label = track.commonMetadataTitle ?? track.displayName;
245247
tracks.add(
246248
VideoAudioTrack(
247-
id: trackId,
249+
groupIndex: 0,
250+
trackIndex: track.index,
248251
label: label,
249252
language: track.languageCode,
250253
isSelected: track.isSelected,
@@ -257,8 +260,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform {
257260
}
258261

259262
@override
260-
Future<void> selectAudioTrack(int playerId, String trackId) {
261-
return _playerWith(id: playerId).selectAudioTrack(trackId);
263+
Future<void> selectAudioTrack(int playerId, VideoAudioTrack track) {
264+
// AVFoundation doesn't have track groups, so we only use trackIndex (groupIndex is ignored)
265+
return _playerWith(id: playerId).selectAudioTrack(track.trackIndex);
262266
}
263267

264268
@override
@@ -282,10 +286,9 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform {
282286
textureId: textureId,
283287
),
284288
VideoPlayerPlatformViewState() => _buildPlatformView(playerId),
285-
null =>
286-
throw Exception(
287-
'Could not find corresponding view type for playerId: $playerId',
288-
),
289+
null => throw Exception(
290+
'Could not find corresponding view type for playerId: $playerId',
291+
),
289292
};
290293
}
291294

packages/video_player/video_player_avfoundation/lib/src/messages.g.dart

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ class CreationOptions {
9292
result as List<Object?>;
9393
return CreationOptions(
9494
uri: result[0]! as String,
95-
httpHeaders:
96-
(result[1] as Map<Object?, Object?>?)!.cast<String, String>(),
95+
httpHeaders: (result[1] as Map<Object?, Object?>?)!
96+
.cast<String, String>(),
9797
);
9898
}
9999

@@ -393,8 +393,8 @@ class NativeAudioTrackData {
393393
result as List<Object?>;
394394
return NativeAudioTrackData(
395395
assetTracks: (result[0] as List<Object?>?)?.cast<AssetAudioTrackData>(),
396-
mediaSelectionTracks:
397-
(result[1] as List<Object?>?)?.cast<MediaSelectionAudioTrackData>(),
396+
mediaSelectionTracks: (result[1] as List<Object?>?)
397+
?.cast<MediaSelectionAudioTrackData>(),
398398
);
399399
}
400400

@@ -479,8 +479,9 @@ class AVFoundationVideoPlayerApi {
479479
BinaryMessenger? binaryMessenger,
480480
String messageChannelSuffix = '',
481481
}) : pigeonVar_binaryMessenger = binaryMessenger,
482-
pigeonVar_messageChannelSuffix =
483-
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
482+
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
483+
? '.$messageChannelSuffix'
484+
: '';
484485
final BinaryMessenger? pigeonVar_binaryMessenger;
485486

486487
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -641,8 +642,9 @@ class VideoPlayerInstanceApi {
641642
BinaryMessenger? binaryMessenger,
642643
String messageChannelSuffix = '',
643644
}) : pigeonVar_binaryMessenger = binaryMessenger,
644-
pigeonVar_messageChannelSuffix =
645-
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
645+
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
646+
? '.$messageChannelSuffix'
647+
: '';
646648
final BinaryMessenger? pigeonVar_binaryMessenger;
647649

648650
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
@@ -892,7 +894,7 @@ class VideoPlayerInstanceApi {
892894
}
893895
}
894896

895-
Future<void> selectAudioTrack(String trackId) async {
897+
Future<void> selectAudioTrack(int trackIndex) async {
896898
final String pigeonVar_channelName =
897899
'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix';
898900
final BasicMessageChannel<Object?> pigeonVar_channel =
@@ -902,7 +904,7 @@ class VideoPlayerInstanceApi {
902904
binaryMessenger: pigeonVar_binaryMessenger,
903905
);
904906
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
905-
<Object?>[trackId],
907+
<Object?>[trackIndex],
906908
);
907909
final List<Object?>? pigeonVar_replyList =
908910
await pigeonVar_sendFuture as List<Object?>?;

packages/video_player/video_player_avfoundation/pigeons/messages.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,5 @@ abstract class VideoPlayerInstanceApi {
149149
@ObjCSelector('getAudioTracks')
150150
NativeAudioTrackData getAudioTracks();
151151
@ObjCSelector('selectAudioTrack:')
152-
void selectAudioTrack(String trackId);
152+
void selectAudioTrack(int trackIndex);
153153
}

packages/video_player/video_player_avfoundation/pubspec.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ dev_dependencies:
3636
topics:
3737
- video
3838
- video-player
39+
# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
40+
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
41+
dependency_overrides:
42+
video_player_platform_interface:
43+
{ path: ../../../packages/video_player/video_player_platform_interface }

0 commit comments

Comments
 (0)