Skip to content

Commit 459e7f8

Browse files
committed
feat(video_player_avfoundation): implement audio track selection
- Added getAudioTracks() method supporting both HLS media selection and regular asset tracks - Added selectAudioTrack() method with support for both track selection mechanisms - Included comprehensive test coverage for audio track functionality with edge cases
1 parent 3caa48b commit 459e7f8

File tree

10 files changed

+1399
-2
lines changed

10 files changed

+1399
-2
lines changed

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.5
27

38
* Updates minimum supported version to iOS 13 and macOS 10.15.

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

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,4 +1024,362 @@ - (nonnull AVPlayerItem *)playerItemWithURL:(NSURL *)url {
10241024
return [AVPlayerItem playerItemWithAsset:[AVURLAsset URLAssetWithURL:url options:nil]];
10251025
}
10261026

1027+
#pragma mark - Audio Track Tests
1028+
1029+
- (void)testGetAudioTracksWithRegularAssetTracks {
1030+
// Create mocks
1031+
id mockPlayer = OCMClassMock([AVPlayer class]);
1032+
id mockPlayerItem = OCMClassMock([AVPlayerItem class]);
1033+
id mockAsset = OCMClassMock([AVAsset class]);
1034+
id mockAVFactory = OCMProtocolMock(@protocol(FVPAVFactory));
1035+
id mockViewProvider = OCMProtocolMock(@protocol(FVPViewProvider));
1036+
1037+
// Set up basic mock relationships
1038+
OCMStub([mockPlayer currentItem]).andReturn(mockPlayerItem);
1039+
OCMStub([mockPlayerItem asset]).andReturn(mockAsset);
1040+
OCMStub([mockAVFactory playerWithPlayerItem:OCMOCK_ANY]).andReturn(mockPlayer);
1041+
1042+
// Create player with mocks
1043+
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:mockPlayerItem
1044+
avFactory:mockAVFactory
1045+
viewProvider:mockViewProvider];
1046+
1047+
// Create mock asset tracks
1048+
id mockTrack1 = OCMClassMock([AVAssetTrack class]);
1049+
id mockTrack2 = OCMClassMock([AVAssetTrack class]);
1050+
1051+
// Configure track 1
1052+
OCMStub([mockTrack1 trackID]).andReturn(1);
1053+
OCMStub([mockTrack1 languageCode]).andReturn(@"en");
1054+
OCMStub([mockTrack1 estimatedDataRate]).andReturn(128000.0f);
1055+
1056+
// Configure track 2
1057+
OCMStub([mockTrack2 trackID]).andReturn(2);
1058+
OCMStub([mockTrack2 languageCode]).andReturn(@"es");
1059+
OCMStub([mockTrack2 estimatedDataRate]).andReturn(96000.0f);
1060+
1061+
// Mock empty format descriptions to avoid Core Media crashes in test environment
1062+
OCMStub([mockTrack1 formatDescriptions]).andReturn(@[]);
1063+
OCMStub([mockTrack2 formatDescriptions]).andReturn(@[]);
1064+
1065+
// Mock the asset to return our tracks
1066+
NSArray *mockTracks = @[ mockTrack1, mockTrack2 ];
1067+
OCMStub([mockAsset tracksWithMediaType:AVMediaTypeAudio]).andReturn(mockTracks);
1068+
1069+
// Mock no media selection group (regular asset)
1070+
OCMStub([mockAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible])
1071+
.andReturn(nil);
1072+
1073+
// Test the method
1074+
FlutterError *error = nil;
1075+
FVPNativeAudioTrackData *result = [player getAudioTracks:&error];
1076+
1077+
// Verify results
1078+
XCTAssertNil(error);
1079+
XCTAssertNotNil(result);
1080+
XCTAssertNotNil(result.assetTracks);
1081+
XCTAssertNil(result.mediaSelectionTracks);
1082+
XCTAssertEqual(result.assetTracks.count, 2);
1083+
1084+
// Verify first track
1085+
FVPAssetAudioTrackData *track1 = result.assetTracks[0];
1086+
XCTAssertEqual(track1.trackId, 1);
1087+
XCTAssertEqualObjects(track1.language, @"en");
1088+
XCTAssertTrue(track1.isSelected); // First track should be selected
1089+
XCTAssertEqualObjects(track1.bitrate, @128000);
1090+
1091+
// Verify second track
1092+
FVPAssetAudioTrackData *track2 = result.assetTracks[1];
1093+
XCTAssertEqual(track2.trackId, 2);
1094+
XCTAssertEqualObjects(track2.language, @"es");
1095+
XCTAssertFalse(track2.isSelected); // Second track should not be selected
1096+
XCTAssertEqualObjects(track2.bitrate, @96000);
1097+
1098+
[player disposeWithError:&error];
1099+
}
1100+
1101+
- (void)testGetAudioTracksWithMediaSelectionOptions {
1102+
// Create mocks
1103+
id mockPlayer = OCMClassMock([AVPlayer class]);
1104+
id mockPlayerItem = OCMClassMock([AVPlayerItem class]);
1105+
id mockAsset = OCMClassMock([AVAsset class]);
1106+
id mockAVFactory = OCMProtocolMock(@protocol(FVPAVFactory));
1107+
id mockViewProvider = OCMProtocolMock(@protocol(FVPViewProvider));
1108+
1109+
// Set up basic mock relationships
1110+
OCMStub([mockPlayer currentItem]).andReturn(mockPlayerItem);
1111+
OCMStub([mockPlayerItem asset]).andReturn(mockAsset);
1112+
OCMStub([mockAVFactory playerWithPlayerItem:OCMOCK_ANY]).andReturn(mockPlayer);
1113+
1114+
// Create player with mocks
1115+
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:mockPlayerItem
1116+
avFactory:mockAVFactory
1117+
viewProvider:mockViewProvider];
1118+
1119+
// Create mock media selection group and options
1120+
id mockMediaSelectionGroup = OCMClassMock([AVMediaSelectionGroup class]);
1121+
id mockOption1 = OCMClassMock([AVMediaSelectionOption class]);
1122+
id mockOption2 = OCMClassMock([AVMediaSelectionOption class]);
1123+
1124+
// Configure option 1
1125+
OCMStub([mockOption1 displayName]).andReturn(@"English");
1126+
id mockLocale1 = OCMClassMock([NSLocale class]);
1127+
OCMStub([mockLocale1 languageCode]).andReturn(@"en");
1128+
OCMStub([mockOption1 locale]).andReturn(mockLocale1);
1129+
1130+
// Configure option 2
1131+
OCMStub([mockOption2 displayName]).andReturn(@"Español");
1132+
id mockLocale2 = OCMClassMock([NSLocale class]);
1133+
OCMStub([mockLocale2 languageCode]).andReturn(@"es");
1134+
OCMStub([mockOption2 locale]).andReturn(mockLocale2);
1135+
1136+
// Mock metadata for option 1
1137+
id mockMetadataItem = OCMClassMock([AVMetadataItem class]);
1138+
OCMStub([mockMetadataItem commonKey]).andReturn(AVMetadataCommonKeyTitle);
1139+
OCMStub([mockMetadataItem stringValue]).andReturn(@"English Audio Track");
1140+
OCMStub([mockOption1 commonMetadata]).andReturn(@[ mockMetadataItem ]);
1141+
1142+
// Configure media selection group
1143+
NSArray *options = @[ mockOption1, mockOption2 ];
1144+
OCMStub([(AVMediaSelectionGroup *)mockMediaSelectionGroup options]).andReturn(options);
1145+
OCMStub([[(AVMediaSelectionGroup *)mockMediaSelectionGroup options] count]).andReturn(2);
1146+
1147+
// Mock the asset to return media selection group
1148+
OCMStub([mockAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible])
1149+
.andReturn(mockMediaSelectionGroup);
1150+
1151+
// Mock current selection for both iOS 11+ and older versions
1152+
id mockCurrentMediaSelection = OCMClassMock([AVMediaSelection class]);
1153+
OCMStub([mockPlayerItem currentMediaSelection]).andReturn(mockCurrentMediaSelection);
1154+
OCMStub(
1155+
[mockCurrentMediaSelection selectedMediaOptionInMediaSelectionGroup:mockMediaSelectionGroup])
1156+
.andReturn(mockOption1);
1157+
1158+
// Also mock the deprecated method for iOS < 11
1159+
OCMStub([mockPlayerItem selectedMediaOptionInMediaSelectionGroup:mockMediaSelectionGroup])
1160+
.andReturn(mockOption1);
1161+
1162+
// Test the method
1163+
FlutterError *error = nil;
1164+
FVPNativeAudioTrackData *result = [player getAudioTracks:&error];
1165+
1166+
// Verify results
1167+
XCTAssertNil(error);
1168+
XCTAssertNotNil(result);
1169+
XCTAssertNil(result.assetTracks);
1170+
XCTAssertNotNil(result.mediaSelectionTracks);
1171+
XCTAssertEqual(result.mediaSelectionTracks.count, 2);
1172+
1173+
// Verify first option
1174+
FVPMediaSelectionAudioTrackData *option1Data = result.mediaSelectionTracks[0];
1175+
XCTAssertEqual(option1Data.index, 0);
1176+
XCTAssertEqualObjects(option1Data.displayName, @"English");
1177+
XCTAssertEqualObjects(option1Data.languageCode, @"en");
1178+
XCTAssertTrue(option1Data.isSelected);
1179+
XCTAssertEqualObjects(option1Data.commonMetadataTitle, @"English Audio Track");
1180+
1181+
// Verify second option
1182+
FVPMediaSelectionAudioTrackData *option2Data = result.mediaSelectionTracks[1];
1183+
XCTAssertEqual(option2Data.index, 1);
1184+
XCTAssertEqualObjects(option2Data.displayName, @"Español");
1185+
XCTAssertEqualObjects(option2Data.languageCode, @"es");
1186+
XCTAssertFalse(option2Data.isSelected);
1187+
1188+
[player disposeWithError:&error];
1189+
}
1190+
1191+
- (void)testGetAudioTracksWithNoCurrentItem {
1192+
// Create mocks
1193+
id mockPlayer = OCMClassMock([AVPlayer class]);
1194+
id mockPlayerItem = OCMClassMock([AVPlayerItem class]);
1195+
id mockAVFactory = OCMProtocolMock(@protocol(FVPAVFactory));
1196+
id mockViewProvider = OCMProtocolMock(@protocol(FVPViewProvider));
1197+
1198+
// Set up basic mock relationships
1199+
OCMStub([mockAVFactory playerWithPlayerItem:OCMOCK_ANY]).andReturn(mockPlayer);
1200+
1201+
// Create player with mocks
1202+
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:mockPlayerItem
1203+
avFactory:mockAVFactory
1204+
viewProvider:mockViewProvider];
1205+
1206+
// Mock player with no current item
1207+
OCMStub([mockPlayer currentItem]).andReturn(nil);
1208+
1209+
// Test the method
1210+
FlutterError *error = nil;
1211+
FVPNativeAudioTrackData *result = [player getAudioTracks:&error];
1212+
1213+
// Verify results
1214+
XCTAssertNil(error);
1215+
XCTAssertNotNil(result);
1216+
XCTAssertNil(result.assetTracks);
1217+
XCTAssertNil(result.mediaSelectionTracks);
1218+
1219+
[player disposeWithError:&error];
1220+
}
1221+
1222+
- (void)testGetAudioTracksWithNoAsset {
1223+
// Create mocks
1224+
id mockPlayer = OCMClassMock([AVPlayer class]);
1225+
id mockPlayerItem = OCMClassMock([AVPlayerItem class]);
1226+
id mockAVFactory = OCMProtocolMock(@protocol(FVPAVFactory));
1227+
id mockViewProvider = OCMProtocolMock(@protocol(FVPViewProvider));
1228+
1229+
// Set up basic mock relationships
1230+
OCMStub([mockPlayer currentItem]).andReturn(mockPlayerItem);
1231+
OCMStub([mockAVFactory playerWithPlayerItem:OCMOCK_ANY]).andReturn(mockPlayer);
1232+
1233+
// Create player with mocks
1234+
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:mockPlayerItem
1235+
avFactory:mockAVFactory
1236+
viewProvider:mockViewProvider];
1237+
1238+
// Mock player item with no asset
1239+
OCMStub([mockPlayerItem asset]).andReturn(nil);
1240+
1241+
// Test the method
1242+
FlutterError *error = nil;
1243+
FVPNativeAudioTrackData *result = [player getAudioTracks:&error];
1244+
1245+
// Verify results
1246+
XCTAssertNil(error);
1247+
XCTAssertNotNil(result);
1248+
XCTAssertNil(result.assetTracks);
1249+
XCTAssertNil(result.mediaSelectionTracks);
1250+
1251+
[player disposeWithError:&error];
1252+
}
1253+
1254+
- (void)testGetAudioTracksCodecDetection {
1255+
// Create mocks
1256+
id mockPlayer = OCMClassMock([AVPlayer class]);
1257+
id mockPlayerItem = OCMClassMock([AVPlayerItem class]);
1258+
id mockAsset = OCMClassMock([AVAsset class]);
1259+
id mockAVFactory = OCMProtocolMock(@protocol(FVPAVFactory));
1260+
id mockViewProvider = OCMProtocolMock(@protocol(FVPViewProvider));
1261+
1262+
// Set up basic mock relationships
1263+
OCMStub([mockPlayer currentItem]).andReturn(mockPlayerItem);
1264+
OCMStub([mockPlayerItem asset]).andReturn(mockAsset);
1265+
OCMStub([mockAVFactory playerWithPlayerItem:OCMOCK_ANY]).andReturn(mockPlayer);
1266+
1267+
// Create player with mocks
1268+
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:mockPlayerItem
1269+
avFactory:mockAVFactory
1270+
viewProvider:mockViewProvider];
1271+
1272+
// Create mock asset track with format description
1273+
id mockTrack = OCMClassMock([AVAssetTrack class]);
1274+
OCMStub([mockTrack trackID]).andReturn(1);
1275+
OCMStub([mockTrack languageCode]).andReturn(@"en");
1276+
1277+
// Mock empty format descriptions to avoid Core Media crashes in test environment
1278+
OCMStub([mockTrack formatDescriptions]).andReturn(@[]);
1279+
1280+
// Mock the asset
1281+
OCMStub([mockAsset tracksWithMediaType:AVMediaTypeAudio]).andReturn(@[ mockTrack ]);
1282+
OCMStub([mockAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible])
1283+
.andReturn(nil);
1284+
1285+
// Test the method
1286+
FlutterError *error = nil;
1287+
FVPNativeAudioTrackData *result = [player getAudioTracks:&error];
1288+
1289+
// Verify results
1290+
XCTAssertNil(error);
1291+
XCTAssertNotNil(result);
1292+
XCTAssertNotNil(result.assetTracks);
1293+
XCTAssertEqual(result.assetTracks.count, 1);
1294+
1295+
FVPAssetAudioTrackData *track = result.assetTracks[0];
1296+
XCTAssertEqual(track.trackId, 1);
1297+
XCTAssertEqualObjects(track.language, @"en");
1298+
1299+
[player disposeWithError:&error];
1300+
}
1301+
1302+
- (void)testGetAudioTracksWithEmptyMediaSelectionOptions {
1303+
// Create mocks
1304+
id mockPlayer = OCMClassMock([AVPlayer class]);
1305+
id mockPlayerItem = OCMClassMock([AVPlayerItem class]);
1306+
id mockAsset = OCMClassMock([AVAsset class]);
1307+
id mockAVFactory = OCMProtocolMock(@protocol(FVPAVFactory));
1308+
id mockViewProvider = OCMProtocolMock(@protocol(FVPViewProvider));
1309+
1310+
// Set up basic mock relationships
1311+
OCMStub([mockPlayer currentItem]).andReturn(mockPlayerItem);
1312+
OCMStub([mockPlayerItem asset]).andReturn(mockAsset);
1313+
OCMStub([mockAVFactory playerWithPlayerItem:OCMOCK_ANY]).andReturn(mockPlayer);
1314+
1315+
// Create player with mocks
1316+
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:mockPlayerItem
1317+
avFactory:mockAVFactory
1318+
viewProvider:mockViewProvider];
1319+
1320+
// Create mock media selection group with no options
1321+
id mockMediaSelectionGroup = OCMClassMock([AVMediaSelectionGroup class]);
1322+
OCMStub([(AVMediaSelectionGroup *)mockMediaSelectionGroup options]).andReturn(@[]);
1323+
OCMStub([[(AVMediaSelectionGroup *)mockMediaSelectionGroup options] count]).andReturn(0);
1324+
1325+
// Mock the asset
1326+
OCMStub([mockAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible])
1327+
.andReturn(mockMediaSelectionGroup);
1328+
OCMStub([mockAsset tracksWithMediaType:AVMediaTypeAudio]).andReturn(@[]);
1329+
1330+
// Test the method
1331+
FlutterError *error = nil;
1332+
FVPNativeAudioTrackData *result = [player getAudioTracks:&error];
1333+
1334+
// Verify results - should fall back to asset tracks
1335+
XCTAssertNil(error);
1336+
XCTAssertNotNil(result);
1337+
XCTAssertNotNil(result.assetTracks);
1338+
XCTAssertNil(result.mediaSelectionTracks);
1339+
XCTAssertEqual(result.assetTracks.count, 0);
1340+
1341+
[player disposeWithError:&error];
1342+
}
1343+
1344+
- (void)testGetAudioTracksWithNilMediaSelectionOption {
1345+
// Create mocks
1346+
id mockPlayer = OCMClassMock([AVPlayer class]);
1347+
id mockPlayerItem = OCMClassMock([AVPlayerItem class]);
1348+
id mockAsset = OCMClassMock([AVAsset class]);
1349+
id mockAVFactory = OCMProtocolMock(@protocol(FVPAVFactory));
1350+
id mockViewProvider = OCMProtocolMock(@protocol(FVPViewProvider));
1351+
1352+
// Set up basic mock relationships
1353+
OCMStub([mockPlayer currentItem]).andReturn(mockPlayerItem);
1354+
OCMStub([mockPlayerItem asset]).andReturn(mockAsset);
1355+
OCMStub([mockAVFactory playerWithPlayerItem:OCMOCK_ANY]).andReturn(mockPlayer);
1356+
1357+
// Create player with mocks
1358+
FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:mockPlayerItem
1359+
avFactory:mockAVFactory
1360+
viewProvider:mockViewProvider];
1361+
1362+
// Create mock media selection group with nil option
1363+
id mockMediaSelectionGroup = OCMClassMock([AVMediaSelectionGroup class]);
1364+
NSArray *options = @[ [NSNull null] ]; // Simulate nil option
1365+
OCMStub([(AVMediaSelectionGroup *)mockMediaSelectionGroup options]).andReturn(options);
1366+
OCMStub([[(AVMediaSelectionGroup *)mockMediaSelectionGroup options] count]).andReturn(1);
1367+
1368+
// Mock the asset
1369+
OCMStub([mockAsset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible])
1370+
.andReturn(mockMediaSelectionGroup);
1371+
1372+
// Test the method
1373+
FlutterError *error = nil;
1374+
FVPNativeAudioTrackData *result = [player getAudioTracks:&error];
1375+
1376+
// Verify results - should handle nil option gracefully
1377+
XCTAssertNil(error);
1378+
XCTAssertNotNil(result);
1379+
XCTAssertNotNil(result.mediaSelectionTracks);
1380+
XCTAssertEqual(result.mediaSelectionTracks.count, 0); // Should skip nil options
1381+
1382+
[player disposeWithError:&error];
1383+
}
1384+
10271385
@end

0 commit comments

Comments
 (0)