Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
459e7f8
feat(video_player_avfoundation): implement audio track selection
nateshmbhat Nov 7, 2025
272ff1d
chore(video_player): bump minimum OS versions for iOS and macOS
nateshmbhat Nov 7, 2025
6905f0a
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Nov 10, 2025
b253fa4
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Nov 19, 2025
1bfefef
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Nov 24, 2025
32ab95d
Update packages/video_player/video_player_avfoundation/darwin/video_p…
nateshmbhat Nov 26, 2025
9f13dc2
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Nov 26, 2025
dcb886d
refactor(ios): remove audio selection options caching
nateshmbhat Nov 26, 2025
cb061e8
refactor(ios): simplify audio track metadata extraction
nateshmbhat Nov 27, 2025
3c8b5d4
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Nov 27, 2025
c972c37
refactor(ios): improve error handling and code formatting in video pl…
nateshmbhat Nov 27, 2025
aee5360
Merge branch '28-oct-platform-avfoundation' of github.com:nateshmbhat…
nateshmbhat Nov 27, 2025
2071972
refactor(ios): replace error handling with assertions in audio track …
nateshmbhat Dec 3, 2025
4d78fb2
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Dec 3, 2025
b77c751
test(ios): remove redundant nil-check tests for getAudioTracks
nateshmbhat Dec 4, 2025
31858c4
chore(video_player_avfoundation): bump version to 2.9.0
nateshmbhat Dec 4, 2025
d2f6def
Merge branch '28-oct-platform-avfoundation' of github.com:nateshmbhat…
nateshmbhat Dec 4, 2025
f17d787
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Dec 9, 2025
c9e9d42
fixed lint warning
nateshmbhat Dec 10, 2025
4c7c534
Merge branch '28-oct-platform-avfoundation' of github.com:nateshmbhat…
nateshmbhat Dec 10, 2025
c60befb
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Dec 10, 2025
93989eb
Format avfoundation_video_player.dart
nateshmbhat Dec 10, 2025
731400a
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Dec 11, 2025
9d91faf
test: remove audio track test cases from `VideoPlayerTests.m`
nateshmbhat Dec 25, 2025
4c07e9e
Merge branch '28-oct-platform-avfoundation' of github.com:nateshmbhat…
nateshmbhat Dec 25, 2025
524223d
style: reformat `NSURL` initializations in tests and update Xcode pro…
nateshmbhat Dec 25, 2025
27d5b20
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Dec 25, 2025
b129b5e
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Jan 6, 2026
b15346e
feat: Simplify audio track selection by unifying asset and media sele…
nateshmbhat Jan 6, 2026
b577344
Merge branch '28-oct-platform-avfoundation' of github.com:nateshmbhat…
nateshmbhat Jan 6, 2026
02fe575
removed unnecessary files
nateshmbhat Jan 6, 2026
8d38697
fix(tests) : fix test compile errors
nateshmbhat Jan 9, 2026
0457a98
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Jan 10, 2026
6a64bd8
Merge branch 'main' into 28-oct-platform-avfoundation
nateshmbhat Jan 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<string>13.0</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion packages/video_player/video_player/example/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
1F784D8C27C8AC72541E3F4C /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -205,6 +206,23 @@
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
1F784D8C27C8AC72541E3F4C /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
Expand Down Expand Up @@ -335,7 +353,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -414,7 +432,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -465,7 +483,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player/example/macos/Podfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
platform :osx, '10.14'
platform :osx, '10.15'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
C0B5FBA873B9089B9B9062E0 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
Expand Down Expand Up @@ -306,6 +307,23 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
C0B5FBA873B9089B9B9062E0 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
D3E396DFBCC51886820113AA /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down Expand Up @@ -402,7 +420,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down Expand Up @@ -481,7 +499,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
Expand Down Expand Up @@ -528,7 +546,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.9.0

* Implements `getAudioTracks()` and `selectAudioTrack()` methods.
* Updates minimum supported SDK version to Flutter 3.29/Dart 3.7.

## 2.8.9

* Resolve `tracksWithMediaType:` deprecations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,149 @@ - (nonnull AVPlayerItem *)playerItemWithURL:(NSURL *)url {
return [AVPlayerItem playerItemWithAsset:[AVURLAsset URLAssetWithURL:url options:nil]];
}

#pragma mark - Audio Track Tests

// Tests getAudioTracks with a regular MP4 video file using real AVFoundation.
// Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array.
- (void)testGetAudioTracksWithRealMP4Video {
FVPVideoPlayer *player =
[[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL]
avFactory:[[FVPDefaultAVFactory alloc] init]
viewProvider:[[StubViewProvider alloc] initWithView:nil]];
XCTAssertNotNil(player);

XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"];
StubEventListener *listener =
[[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation];
player.eventListener = listener;
[self waitForExpectationsWithTimeout:30.0 handler:nil];

// Now test getAudioTracks
FlutterError *error = nil;
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];

XCTAssertNil(error);
XCTAssertNotNil(result);

// Regular MP4 files do not have media selection groups for audio.
// getAudioTracks only returns selectable audio tracks from HLS streams.
XCTAssertEqual(result.count, 0);

[player disposeWithError:&error];
}

// Tests getAudioTracks with an HLS stream using real AVFoundation.
// HLS streams use media selection groups for audio track selection.
- (void)testGetAudioTracksWithRealHLSStream {
NSURL *hlsURL = [NSURL
URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"];
XCTAssertNotNil(hlsURL);

FVPVideoPlayer *player =
[[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:hlsURL]
avFactory:[[FVPDefaultAVFactory alloc] init]
viewProvider:[[StubViewProvider alloc] initWithView:nil]];
XCTAssertNotNil(player);

XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"];
StubEventListener *listener =
[[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation];
player.eventListener = listener;
[self waitForExpectationsWithTimeout:30.0 handler:nil];

// Now test getAudioTracks
FlutterError *error = nil;
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];

XCTAssertNil(error);
XCTAssertNotNil(result);

// For HLS streams with multiple audio options, we get media selection tracks.
// The bee.m3u8 stream may or may not have multiple audio tracks.
// We verify the method returns valid data without crashing.
for (FVPMediaSelectionAudioTrackData *track in result) {
XCTAssertNotNil(track.displayName);
XCTAssertGreaterThanOrEqual(track.index, 0);
}

[player disposeWithError:&error];
}

// Tests that getAudioTracks returns valid data for audio-only files.
// Regular audio files do not have media selection groups, so getAudioTracks returns an empty array.
- (void)testGetAudioTracksWithRealAudioFile {
NSURL *audioURL = [NSURL
URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3"];
XCTAssertNotNil(audioURL);

FVPVideoPlayer *player =
[[FVPVideoPlayer alloc] initWithPlayerItem:[self playerItemWithURL:audioURL]
avFactory:[[FVPDefaultAVFactory alloc] init]
viewProvider:[[StubViewProvider alloc] initWithView:nil]];
XCTAssertNotNil(player);

XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"];
StubEventListener *listener =
[[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation];
player.eventListener = listener;
[self waitForExpectationsWithTimeout:30.0 handler:nil];

// Now test getAudioTracks
FlutterError *error = nil;
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];

XCTAssertNil(error);
XCTAssertNotNil(result);

// Regular audio files do not have media selection groups.
// getAudioTracks only returns selectable audio tracks from HLS streams.
XCTAssertEqual(result.count, 0);

[player disposeWithError:&error];
}

// Tests that getAudioTracks works correctly through the plugin API with a real video.
// Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array.
- (void)testGetAudioTracksViaPluginWithRealVideo {
NSObject<FlutterPluginRegistrar> *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar));
FVPVideoPlayerPlugin *videoPlayerPlugin =
[[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar];

FlutterError *error;
[videoPlayerPlugin initialize:&error];
XCTAssertNil(error);

FVPCreationOptions *create = [FVPCreationOptions
makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
httpHeaders:@{}];
FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create
error:&error];
XCTAssertNil(error);
XCTAssertNotNil(identifiers);

FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)];
XCTAssertNotNil(player);

// Wait for player item to become ready
AVPlayerItem *item = player.player.currentItem;
[self keyValueObservingExpectationForObject:(id)item
keyPath:@"status"
expectedValue:@(AVPlayerItemStatusReadyToPlay)];
[self waitForExpectationsWithTimeout:30.0 handler:nil];

// Now test getAudioTracks
NSArray<FVPMediaSelectionAudioTrackData *> *result = [player getAudioTracks:&error];

XCTAssertNil(error);
XCTAssertNotNil(result);

// Regular MP4 files do not have media selection groups.
// getAudioTracks only returns selectable audio tracks from HLS streams.
XCTAssertEqual(result.count, 0);

[player disposeWithError:&error];
}

- (void)testLoadTracksWithMediaTypeIsCalledOnNewerOS {
if (@available(iOS 15.0, macOS 12.0, *)) {
AVAsset *mockAsset = OCMClassMock([AVAsset class]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,70 @@ - (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable *_Nonnull)
[self updatePlayingState];
}

- (nullable NSArray<FVPMediaSelectionAudioTrackData *> *)getAudioTracks:
(FlutterError *_Nullable *_Nonnull)error {
AVPlayerItem *currentItem = _player.currentItem;
NSAssert(currentItem, @"currentItem should not be nil");
AVAsset *asset = currentItem.asset;

// Get tracks from media selection (for HLS streams)
AVMediaSelectionGroup *audioGroup =
[asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible];

NSMutableArray<FVPMediaSelectionAudioTrackData *> *mediaSelectionTracks =
[[NSMutableArray alloc] init];

if (audioGroup.options.count > 0) {
AVMediaSelection *mediaSelection = currentItem.currentMediaSelection;
AVMediaSelectionOption *currentSelection =
[mediaSelection selectedMediaOptionInMediaSelectionGroup:audioGroup];

for (NSInteger i = 0; i < audioGroup.options.count; i++) {
AVMediaSelectionOption *option = audioGroup.options[i];
NSString *displayName = option.displayName;

NSString *languageCode = nil;
if (option.locale) {
languageCode = option.locale.languageCode;
}

NSArray<AVMetadataItem *> *titleItems =
[AVMetadataItem metadataItemsFromArray:option.commonMetadata
withKey:AVMetadataCommonKeyTitle
keySpace:AVMetadataKeySpaceCommon];
NSString *commonMetadataTitle = titleItems.firstObject.stringValue;

BOOL isSelected = [currentSelection isEqual:option];

FVPMediaSelectionAudioTrackData *trackData =
[FVPMediaSelectionAudioTrackData makeWithIndex:i
displayName:displayName
languageCode:languageCode
isSelected:isSelected
commonMetadataTitle:commonMetadataTitle];

[mediaSelectionTracks addObject:trackData];
}
}

return mediaSelectionTracks;
}

- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
AVPlayerItem *currentItem = _player.currentItem;
NSAssert(currentItem, @"currentItem should not be nil");
AVAsset *asset = currentItem.asset;

AVMediaSelectionGroup *audioGroup =
[asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicAudible];

if (audioGroup && trackIndex >= 0 && trackIndex < (NSInteger)audioGroup.options.count) {
AVMediaSelectionOption *option = audioGroup.options[trackIndex];
[currentItem selectMediaOption:option inMediaSelectionGroup:audioGroup];
}
}

#pragma mark - Private

- (int64_t)duration {
Expand Down
Loading