Skip to content

Commit 93c9395

Browse files
authored
feat(ios): add audio track selection support for AVFoundation (#10313)
- Implemented getAudioTracks() method to retrieve available audio tracks from both HLS streams and regular video files AVFoundation Platform PR for : #9925 ## Pre-Review Checklist
1 parent cc37b19 commit 93c9395

File tree

18 files changed

+582
-13
lines changed

18 files changed

+582
-13
lines changed

packages/video_player/video_player/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
versions of the endorsed platform implementations.
66
* Applications built with older versions of Flutter will continue to
77
use compatible versions of the platform implementations.
8+
* Updates example app minimum platform versions.
89

910
## 2.10.1
1011

packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
<key>CFBundleVersion</key>
2222
<string>1.0</string>
2323
<key>MinimumOSVersion</key>
24-
<string>12.0</string>
24+
<string>13.0</string>
2525
</dict>
2626
</plist>

packages/video_player/video_player/example/ios/Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Uncomment this line to define a global platform for your project
2-
# platform :ios, '12.0'
2+
# platform :ios, '13.0'
33

44
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
55
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
97C146EC1CF9000F007C117D /* Resources */,
141141
9705A1C41CF9048500538489 /* Embed Frameworks */,
142142
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
143+
1F784D8C27C8AC72541E3F4C /* [CP] Embed Pods Frameworks */,
143144
);
144145
buildRules = (
145146
);
@@ -205,6 +206,23 @@
205206
/* End PBXResourcesBuildPhase section */
206207

207208
/* Begin PBXShellScriptBuildPhase section */
209+
1F784D8C27C8AC72541E3F4C /* [CP] Embed Pods Frameworks */ = {
210+
isa = PBXShellScriptBuildPhase;
211+
buildActionMask = 2147483647;
212+
files = (
213+
);
214+
inputFileListPaths = (
215+
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
216+
);
217+
name = "[CP] Embed Pods Frameworks";
218+
outputFileListPaths = (
219+
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
220+
);
221+
runOnlyForDeploymentPostprocessing = 0;
222+
shellPath = /bin/sh;
223+
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
224+
showEnvVarsInLog = 0;
225+
};
208226
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
209227
isa = PBXShellScriptBuildPhase;
210228
alwaysOutOfDate = 1;
@@ -335,7 +353,7 @@
335353
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
336354
GCC_WARN_UNUSED_FUNCTION = YES;
337355
GCC_WARN_UNUSED_VARIABLE = YES;
338-
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
356+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
339357
MTL_ENABLE_DEBUG_INFO = NO;
340358
SDKROOT = iphoneos;
341359
SUPPORTED_PLATFORMS = iphoneos;
@@ -414,7 +432,7 @@
414432
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
415433
GCC_WARN_UNUSED_FUNCTION = YES;
416434
GCC_WARN_UNUSED_VARIABLE = YES;
417-
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
435+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
418436
MTL_ENABLE_DEBUG_INFO = YES;
419437
ONLY_ACTIVE_ARCH = YES;
420438
SDKROOT = iphoneos;
@@ -465,7 +483,7 @@
465483
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
466484
GCC_WARN_UNUSED_FUNCTION = YES;
467485
GCC_WARN_UNUSED_VARIABLE = YES;
468-
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
486+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
469487
MTL_ENABLE_DEBUG_INFO = NO;
470488
SDKROOT = iphoneos;
471489
SUPPORTED_PLATFORMS = iphoneos;

packages/video_player/video_player/example/macos/Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
platform :osx, '10.14'
1+
platform :osx, '10.15'
22

33
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
44
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@
193193
33CC10EB2044A3C60003C045 /* Resources */,
194194
33CC110E2044A8840003C045 /* Bundle Framework */,
195195
3399D490228B24CF009A79C7 /* ShellScript */,
196+
C0B5FBA873B9089B9B9062E0 /* [CP] Embed Pods Frameworks */,
196197
);
197198
buildRules = (
198199
);
@@ -306,6 +307,23 @@
306307
shellPath = /bin/sh;
307308
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
308309
};
310+
C0B5FBA873B9089B9B9062E0 /* [CP] Embed Pods Frameworks */ = {
311+
isa = PBXShellScriptBuildPhase;
312+
buildActionMask = 2147483647;
313+
files = (
314+
);
315+
inputFileListPaths = (
316+
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
317+
);
318+
name = "[CP] Embed Pods Frameworks";
319+
outputFileListPaths = (
320+
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
321+
);
322+
runOnlyForDeploymentPostprocessing = 0;
323+
shellPath = /bin/sh;
324+
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
325+
showEnvVarsInLog = 0;
326+
};
309327
D3E396DFBCC51886820113AA /* [CP] Check Pods Manifest.lock */ = {
310328
isa = PBXShellScriptBuildPhase;
311329
buildActionMask = 2147483647;
@@ -402,7 +420,7 @@
402420
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
403421
GCC_WARN_UNUSED_FUNCTION = YES;
404422
GCC_WARN_UNUSED_VARIABLE = YES;
405-
MACOSX_DEPLOYMENT_TARGET = 10.14;
423+
MACOSX_DEPLOYMENT_TARGET = 10.15;
406424
MTL_ENABLE_DEBUG_INFO = NO;
407425
SDKROOT = macosx;
408426
SWIFT_COMPILATION_MODE = wholemodule;
@@ -481,7 +499,7 @@
481499
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
482500
GCC_WARN_UNUSED_FUNCTION = YES;
483501
GCC_WARN_UNUSED_VARIABLE = YES;
484-
MACOSX_DEPLOYMENT_TARGET = 10.14;
502+
MACOSX_DEPLOYMENT_TARGET = 10.15;
485503
MTL_ENABLE_DEBUG_INFO = YES;
486504
ONLY_ACTIVE_ARCH = YES;
487505
SDKROOT = macosx;
@@ -528,7 +546,7 @@
528546
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
529547
GCC_WARN_UNUSED_FUNCTION = YES;
530548
GCC_WARN_UNUSED_VARIABLE = YES;
531-
MACOSX_DEPLOYMENT_TARGET = 10.14;
549+
MACOSX_DEPLOYMENT_TARGET = 10.15;
532550
MTL_ENABLE_DEBUG_INFO = NO;
533551
SDKROOT = macosx;
534552
SWIFT_COMPILATION_MODE = wholemodule;

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.9.0
2+
3+
* Implements `getAudioTracks()` and `selectAudioTrack()` methods.
4+
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.
5+
16
## 2.8.10
27

38
* Improves compatibility with `UIScene`.

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

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,149 @@ - (nonnull AVPlayerItem *)playerItemWithURL:(NSURL *)url {
10621062
return [AVPlayerItem playerItemWithAsset:[AVURLAsset URLAssetWithURL:url options:nil]];
10631063
}
10641064

1065+
#pragma mark - Audio Track Tests
1066+
1067+
// Tests getAudioTracks with a regular MP4 video file using real AVFoundation.
1068+
// Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array.
1069+
- (void)testGetAudioTracksWithRealMP4Video {
1070+
FVPVideoPlayer *player =
1071+
[[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL]
1072+
avFactory:[[FVPDefaultAVFactory alloc] init]
1073+
viewProvider:[[StubViewProvider alloc] init]];
1074+
XCTAssertNotNil(player);
1075+
1076+
XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"];
1077+
StubEventListener *listener =
1078+
[[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation];
1079+
player.eventListener = listener;
1080+
[self waitForExpectationsWithTimeout:30.0 handler:nil];
1081+
1082+
// Now test getAudioTracks
1083+
FlutterError *error = nil;
1084+
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];
1085+
1086+
XCTAssertNil(error);
1087+
XCTAssertNotNil(result);
1088+
1089+
// Regular MP4 files do not have media selection groups for audio.
1090+
// getAudioTracks only returns selectable audio tracks from HLS streams.
1091+
XCTAssertEqual(result.count, 0);
1092+
1093+
[player disposeWithError:&error];
1094+
}
1095+
1096+
// Tests getAudioTracks with an HLS stream using real AVFoundation.
1097+
// HLS streams use media selection groups for audio track selection.
1098+
- (void)testGetAudioTracksWithRealHLSStream {
1099+
NSURL *hlsURL = [NSURL
1100+
URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"];
1101+
XCTAssertNotNil(hlsURL);
1102+
1103+
FVPVideoPlayer *player =
1104+
[[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:hlsURL]
1105+
avFactory:[[FVPDefaultAVFactory alloc] init]
1106+
viewProvider:[[StubViewProvider alloc] init]];
1107+
XCTAssertNotNil(player);
1108+
1109+
XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"];
1110+
StubEventListener *listener =
1111+
[[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation];
1112+
player.eventListener = listener;
1113+
[self waitForExpectationsWithTimeout:30.0 handler:nil];
1114+
1115+
// Now test getAudioTracks
1116+
FlutterError *error = nil;
1117+
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];
1118+
1119+
XCTAssertNil(error);
1120+
XCTAssertNotNil(result);
1121+
1122+
// For HLS streams with multiple audio options, we get media selection tracks.
1123+
// The bee.m3u8 stream may or may not have multiple audio tracks.
1124+
// We verify the method returns valid data without crashing.
1125+
for (FVPMediaSelectionAudioTrackData *track in result) {
1126+
XCTAssertNotNil(track.displayName);
1127+
XCTAssertGreaterThanOrEqual(track.index, 0);
1128+
}
1129+
1130+
[player disposeWithError:&error];
1131+
}
1132+
1133+
// Tests that getAudioTracks returns valid data for audio-only files.
1134+
// Regular audio files do not have media selection groups, so getAudioTracks returns an empty array.
1135+
- (void)testGetAudioTracksWithRealAudioFile {
1136+
NSURL *audioURL = [NSURL
1137+
URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3"];
1138+
XCTAssertNotNil(audioURL);
1139+
1140+
FVPVideoPlayer *player =
1141+
[[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:audioURL]
1142+
avFactory:[[FVPDefaultAVFactory alloc] init]
1143+
viewProvider:[[StubViewProvider alloc] init]];
1144+
XCTAssertNotNil(player);
1145+
1146+
XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"];
1147+
StubEventListener *listener =
1148+
[[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation];
1149+
player.eventListener = listener;
1150+
[self waitForExpectationsWithTimeout:30.0 handler:nil];
1151+
1152+
// Now test getAudioTracks
1153+
FlutterError *error = nil;
1154+
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];
1155+
1156+
XCTAssertNil(error);
1157+
XCTAssertNotNil(result);
1158+
1159+
// Regular audio files do not have media selection groups.
1160+
// getAudioTracks only returns selectable audio tracks from HLS streams.
1161+
XCTAssertEqual(result.count, 0);
1162+
1163+
[player disposeWithError:&error];
1164+
}
1165+
1166+
// Tests that getAudioTracks works correctly through the plugin API with a real video.
1167+
// Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array.
1168+
- (void)testGetAudioTracksViaPluginWithRealVideo {
1169+
NSObject<FlutterPluginRegistrar> *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar));
1170+
FVPVideoPlayerPlugin *videoPlayerPlugin =
1171+
[[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar];
1172+
1173+
FlutterError *error;
1174+
[videoPlayerPlugin initialize:&error];
1175+
XCTAssertNil(error);
1176+
1177+
FVPCreationOptions *create = [FVPCreationOptions
1178+
makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
1179+
httpHeaders:@{}];
1180+
FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create
1181+
error:&error];
1182+
XCTAssertNil(error);
1183+
XCTAssertNotNil(identifiers);
1184+
1185+
FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)];
1186+
XCTAssertNotNil(player);
1187+
1188+
// Wait for player item to become ready
1189+
AVPlayerItem *item = player.player.currentItem;
1190+
[self keyValueObservingExpectationForObject:(id)item
1191+
keyPath:@"status"
1192+
expectedValue:@(AVPlayerItemStatusReadyToPlay)];
1193+
[self waitForExpectationsWithTimeout:30.0 handler:nil];
1194+
1195+
// Now test getAudioTracks
1196+
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];
1197+
1198+
XCTAssertNil(error);
1199+
XCTAssertNotNil(result);
1200+
1201+
// Regular MP4 files do not have media selection groups.
1202+
// getAudioTracks only returns selectable audio tracks from HLS streams.
1203+
XCTAssertEqual(result.count, 0);
1204+
1205+
[player disposeWithError:&error];
1206+
}
1207+
10651208
- (void)testLoadTracksWithMediaTypeIsCalledOnNewerOS {
10661209
if (@available(iOS 15.0, macOS 12.0, *)) {
10671210
AVAsset *mockAsset = OCMClassMock([AVAsset class]);

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,70 @@ - (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable *_Nonnull)
443443
[self updatePlayingState];
444444
}
445445

446+
- (nullable NSArray<FVPMediaSelectionAudioTrackData *> *)getAudioTracks:
447+
(FlutterError *_Nullable *_Nonnull)error {
448+
AVPlayerItem *currentItem = _player.currentItem;
449+
NSAssert(currentItem, @"currentItem should not be nil");
450+
AVAsset *asset = currentItem.asset;
451+
452+
// Get tracks from media selection (for HLS streams)
453+
AVMediaSelectionGroup *audioGroup =
454+
[asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible];
455+
456+
NSMutableArray<FVPMediaSelectionAudioTrackData *> *mediaSelectionTracks =
457+
[[NSMutableArray alloc] init];
458+
459+
if (audioGroup.options.count > 0) {
460+
AVMediaSelection *mediaSelection = currentItem.currentMediaSelection;
461+
AVMediaSelectionOption *currentSelection =
462+
[mediaSelection selectedMediaOptionInMediaSelectionGroup:audioGroup];
463+
464+
for (NSInteger i = 0; i < audioGroup.options.count; i++) {
465+
AVMediaSelectionOption *option = audioGroup.options[i];
466+
NSString *displayName = option.displayName;
467+
468+
NSString *languageCode = nil;
469+
if (option.locale) {
470+
languageCode = option.locale.languageCode;
471+
}
472+
473+
NSArray<AVMetadataItem *> *titleItems =
474+
[AVMetadataItem metadataItemsFromArray:option.commonMetadata
475+
withKey:AVMetadataCommonKeyTitle
476+
keySpace:AVMetadataKeySpaceCommon];
477+
NSString *commonMetadataTitle = titleItems.firstObject.stringValue;
478+
479+
BOOL isSelected = [currentSelection isEqual:option];
480+
481+
FVPMediaSelectionAudioTrackData *trackData =
482+
[FVPMediaSelectionAudioTrackData makeWithIndex:i
483+
displayName:displayName
484+
languageCode:languageCode
485+
isSelected:isSelected
486+
commonMetadataTitle:commonMetadataTitle];
487+
488+
[mediaSelectionTracks addObject:trackData];
489+
}
490+
}
491+
492+
return mediaSelectionTracks;
493+
}
494+
495+
- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex
496+
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
497+
AVPlayerItem *currentItem = _player.currentItem;
498+
NSAssert(currentItem, @"currentItem should not be nil");
499+
AVAsset *asset = currentItem.asset;
500+
501+
AVMediaSelectionGroup *audioGroup =
502+
[asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible];
503+
504+
if (audioGroup && trackIndex >= 0 && trackIndex < (NSInteger)audioGroup.options.count) {
505+
AVMediaSelectionOption *option = audioGroup.options[trackIndex];
506+
[currentItem selectMediaOption:option inMediaSelectionGroup:audioGroup];
507+
}
508+
}
509+
446510
#pragma mark - Private
447511

448512
- (int64_t)duration {

0 commit comments

Comments
 (0)