diff --git a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist index 7c569640062..1dc6cf7652b 100644 --- a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/packages/video_player/video_player/example/ios/Podfile b/packages/video_player/video_player/example/ios/Podfile index 01d4aa611bb..17adeb14132 100644 --- a/packages/video_player/video_player/example/ios/Podfile +++ b/packages/video_player/video_player/example/ios/Podfile @@ -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' diff --git a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj index 2ab10fb9081..a003785afc3 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 1F784D8C27C8AC72541E3F4C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/packages/video_player/video_player/example/macos/Podfile b/packages/video_player/video_player/example/macos/Podfile index ae77cc1d426..66f6172bbb3 100644 --- a/packages/video_player/video_player/example/macos/Podfile +++ b/packages/video_player/video_player/example/macos/Podfile @@ -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' diff --git a/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj index e6fa40d2ed6..9869c74bb38 100644 --- a/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player/example/macos/Runner.xcodeproj/project.pbxproj @@ -193,6 +193,7 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + C0B5FBA873B9089B9B9062E0 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 593df056f9c..b72d84b232c 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -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. diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index afcdfed3bc9..26bc4a50f41 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -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 *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 *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 *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 *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 *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]); diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index 3c45daec018..5b79d4291c2 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -443,6 +443,70 @@ - (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable *_Nonnull) [self updatePlayingState]; } +- (nullable NSArray *)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 *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 *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 { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h index d06c3fd0179..01f584187b1 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/messages.g.h @@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @class FVPPlatformVideoViewCreationParams; @class FVPCreationOptions; @class FVPTexturePlayerIds; +@class FVPMediaSelectionAudioTrackData; /// Information passed to the platform view creation. @interface FVPPlatformVideoViewCreationParams : NSObject @@ -42,6 +43,22 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) NSInteger textureId; @end +/// Raw audio track data from AVMediaSelectionOption (for HLS streams). +@interface FVPMediaSelectionAudioTrackData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithIndex:(NSInteger)index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL)isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle; +@property(nonatomic, assign) NSInteger index; +@property(nonatomic, copy, nullable) NSString *displayName; +@property(nonatomic, copy, nullable) NSString *languageCode; +@property(nonatomic, assign) BOOL isSelected; +@property(nonatomic, copy, nullable) NSString *commonMetadataTitle; +@end + /// The codec used by all APIs. NSObject *FVPGetMessagesCodec(void); @@ -78,6 +95,11 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - (void)seekTo:(NSInteger)position completion:(void (^)(FlutterError *_Nullable))completion; - (void)pauseWithError:(FlutterError *_Nullable *_Nonnull)error; - (void)disposeWithError:(FlutterError *_Nullable *_Nonnull)error; +/// @return `nil` only when `error != nil`. +- (nullable NSArray *)getAudioTracks: + (FlutterError *_Nullable *_Nonnull)error; +- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex + error:(FlutterError *_Nullable *_Nonnull)error; @end extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m index 155ac2bacad..c421576564c 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/messages.g.m @@ -48,6 +48,12 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FVPMediaSelectionAudioTrackData () ++ (FVPMediaSelectionAudioTrackData *)fromList:(NSArray *)list; ++ (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FVPPlatformVideoViewCreationParams + (instancetype)makeWithPlayerId:(NSInteger)playerId { FVPPlatformVideoViewCreationParams *pigeonResult = @@ -120,6 +126,43 @@ + (nullable FVPTexturePlayerIds *)nullableFromList:(NSArray *)list { } @end +@implementation FVPMediaSelectionAudioTrackData ++ (instancetype)makeWithIndex:(NSInteger)index + displayName:(nullable NSString *)displayName + languageCode:(nullable NSString *)languageCode + isSelected:(BOOL)isSelected + commonMetadataTitle:(nullable NSString *)commonMetadataTitle { + FVPMediaSelectionAudioTrackData *pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; + pigeonResult.index = index; + pigeonResult.displayName = displayName; + pigeonResult.languageCode = languageCode; + pigeonResult.isSelected = isSelected; + pigeonResult.commonMetadataTitle = commonMetadataTitle; + return pigeonResult; +} ++ (FVPMediaSelectionAudioTrackData *)fromList:(NSArray *)list { + FVPMediaSelectionAudioTrackData *pigeonResult = [[FVPMediaSelectionAudioTrackData alloc] init]; + pigeonResult.index = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.displayName = GetNullableObjectAtIndex(list, 1); + pigeonResult.languageCode = GetNullableObjectAtIndex(list, 2); + pigeonResult.isSelected = [GetNullableObjectAtIndex(list, 3) boolValue]; + pigeonResult.commonMetadataTitle = GetNullableObjectAtIndex(list, 4); + return pigeonResult; +} ++ (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)list { + return (list) ? [FVPMediaSelectionAudioTrackData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.index), + self.displayName ?: [NSNull null], + self.languageCode ?: [NSNull null], + @(self.isSelected), + self.commonMetadataTitle ?: [NSNull null], + ]; +} +@end + @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FVPMessagesPigeonCodecReader @@ -131,6 +174,8 @@ - (nullable id)readValueOfType:(UInt8)type { return [FVPCreationOptions fromList:[self readValue]]; case 131: return [FVPTexturePlayerIds fromList:[self readValue]]; + case 132: + return [FVPMediaSelectionAudioTrackData fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -150,6 +195,9 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FVPTexturePlayerIds class]]) { [self writeByte:131]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPMediaSelectionAudioTrackData class]]) { + [self writeByte:132]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -502,4 +550,49 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.getAudioTracks", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + FlutterError *error; + NSArray *output = [api getAudioTracks:&error]; + callback(wrapResult(output, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.video_player_avfoundation." + @"VideoPlayerInstanceApi.selectAudioTrack", + messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], + @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " + @"@selector(selectAudioTrackAtIndex:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_trackIndex = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api selectAudioTrackAtIndex:arg_trackIndex error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/video_player/video_player_avfoundation/example/ios/Podfile b/packages/video_player/video_player_avfoundation/example/ios/Podfile index c9339a034eb..6eafd7e2e95 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Podfile +++ b/packages/video_player/video_player_avfoundation/example/ios/Podfile @@ -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' diff --git a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj index 44df4b4d978..41178cae189 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -246,6 +246,7 @@ 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 43465698DA6E8053DBCCE1D3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -373,6 +374,23 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 43465698DA6E8053DBCCE1D3 /* [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; + }; 5121AE1943D8EE14C90ED8B7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index cc176e75c3f..902bf087303 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - video_player_platform_interface: ^6.3.0 + video_player_platform_interface: ^6.6.0 dev_dependencies: flutter_test: diff --git a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart index 834b36ed6b0..6684d9c4c65 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/avfoundation_video_player.dart @@ -178,6 +178,40 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return _api.setMixWithOthers(mixWithOthers); } + @override + Future> getAudioTracks(int playerId) async { + final List nativeData = await _playerWith( + id: playerId, + ).getAudioTracks(); + final tracks = []; + + for (final track in nativeData) { + final String? label = track.commonMetadataTitle ?? track.displayName; + tracks.add( + VideoAudioTrack( + id: track.index.toString(), + label: label, + language: track.languageCode, + isSelected: track.isSelected, + ), + ); + } + + return tracks; + } + + @override + Future selectAudioTrack(int playerId, String trackId) { + final int trackIndex = int.parse(trackId); + return _playerWith(id: playerId).selectAudioTrack(trackIndex); + } + + @override + bool isAudioTrackSupportAvailable() { + // iOS/macOS with AVFoundation supports audio track selection + return true; + } + @override Widget buildView(int playerId) { return buildViewWithOptions(VideoViewOptions(playerId: playerId)); @@ -249,6 +283,12 @@ class _PlayerInstance { return Duration(milliseconds: await _api.getPosition()); } + Future> getAudioTracks() => + _api.getAudioTracks(); + + Future selectAudioTrack(int trackIndex) => + _api.selectAudioTrack(trackIndex); + Stream get videoEvents { _eventSubscription ??= _eventChannel.receiveBroadcastStream().listen( _onStreamEvent, diff --git a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart index 9072c153f95..76b66c3ca4b 100644 --- a/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart +++ b/packages/video_player/video_player_avfoundation/lib/src/messages.g.dart @@ -154,6 +154,69 @@ class TexturePlayerIds { int get hashCode => Object.hashAll(_toList()); } +/// Raw audio track data from AVMediaSelectionOption (for HLS streams). +class MediaSelectionAudioTrackData { + MediaSelectionAudioTrackData({ + required this.index, + this.displayName, + this.languageCode, + required this.isSelected, + this.commonMetadataTitle, + }); + + int index; + + String? displayName; + + String? languageCode; + + bool isSelected; + + String? commonMetadataTitle; + + List _toList() { + return [ + index, + displayName, + languageCode, + isSelected, + commonMetadataTitle, + ]; + } + + Object encode() { + return _toList(); + } + + static MediaSelectionAudioTrackData decode(Object result) { + result as List; + return MediaSelectionAudioTrackData( + index: result[0]! as int, + displayName: result[1] as String?, + languageCode: result[2] as String?, + isSelected: result[3]! as bool, + commonMetadataTitle: result[4] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! MediaSelectionAudioTrackData || + other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -170,6 +233,9 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is TexturePlayerIds) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is MediaSelectionAudioTrackData) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -184,6 +250,8 @@ class _PigeonCodec extends StandardMessageCodec { return CreationOptions.decode(readValue(buffer)!); case 131: return TexturePlayerIds.decode(readValue(buffer)!); + case 132: + return MediaSelectionAudioTrackData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -582,4 +650,62 @@ class VideoPlayerInstanceApi { return; } } + + Future> getAudioTracks() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as List?)! + .cast(); + } + } + + Future selectAudioTrack(int trackIndex) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [trackIndex], + ); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index 6e872dec145..f49b4600530 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -39,6 +39,23 @@ class TexturePlayerIds { final int textureId; } +/// Raw audio track data from AVMediaSelectionOption (for HLS streams). +class MediaSelectionAudioTrackData { + MediaSelectionAudioTrackData({ + required this.index, + this.displayName, + this.languageCode, + required this.isSelected, + this.commonMetadataTitle, + }); + + int index; + String? displayName; + String? languageCode; + bool isSelected; + String? commonMetadataTitle; +} + @HostApi() abstract class AVFoundationVideoPlayerApi { @ObjCSelector('initialize') @@ -72,4 +89,8 @@ abstract class VideoPlayerInstanceApi { void seekTo(int position); void pause(); void dispose(); + @ObjCSelector('getAudioTracks') + List getAudioTracks(); + @ObjCSelector('selectAudioTrackAtIndex:') + void selectAudioTrack(int trackIndex); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 532cc426ecd..3a428e6cc82 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS implementation of the video_player plugin. repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.8.9 +version: 2.9.0 environment: sdk: ^3.9.0 @@ -24,7 +24,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.3.0 + video_player_platform_interface: ^6.6.0 dev_dependencies: build_runner: ^2.3.3