From 20ce6a73714c49e77773674e7fd37a14eb438d0d Mon Sep 17 00:00:00 2001 From: nateshmbhat Date: Thu, 9 Apr 2026 13:13:16 +0530 Subject: [PATCH] [video_player_avfoundation] Add video track selection support Implements getVideoTracks() and selectVideoTrack() methods for video track (quality) selection using AVFoundation. Video track selection requires iOS 15+ / macOS 12+ for HLS streams. AVFoundation breakout PR for flutter/packages#10688. --- .../video_player_avfoundation/CHANGELOG.md | 5 + .../darwin/RunnerTests/VideoPlayerTests.swift | 211 ++++++++ .../FVPVideoPlayer.m | 143 ++++++ .../video_player_avfoundation/messages.g.h | 130 +++-- .../video_player_avfoundation/messages.g.m | 484 +++++++++++------- .../example/pubspec.yaml | 2 +- .../lib/src/avfoundation_video_player.dart | 93 ++++ .../lib/src/messages.g.dart | 469 ++++++++++++----- .../pigeons/messages.dart | 66 +++ .../video_player_avfoundation/pubspec.yaml | 4 +- .../test/avfoundation_video_player_test.dart | 174 +++++++ .../avfoundation_video_player_test.mocks.dart | 35 ++ 12 files changed, 1465 insertions(+), 351 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 1508c0bbfe22..701f1228bd32 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.10.0 + +* Implements `getVideoTracks()` and `selectVideoTrack()` methods for video track (quality) selection using AVFoundation. +* Video track selection requires iOS 15+ / macOS 12+ for HLS streams. + ## 2.9.4 * Ensures that the display link does not continue requesting frames after a player is disposed. diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift index c965a70a8430..5856179f715b 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -802,6 +802,217 @@ private let hlsAudioTestURI = } } + // MARK: - Video Track Tests + + // Integration test for getVideoTracks with a non-HLS MP4 video over a live network request. + // Non-HLS MP4 files don't have adaptive bitrate variants, so we expect empty media selection + // tracks. + @Test func getVideoTracksWithRealMP4Video() async throws { + // TODO(stuartmorgan): Add more use of protocols in FVPVideoPlayer so that this test + // can use a fake item/asset instead of loading an actual remote asset. + let realObjectFactory = FVPDefaultAVFactory() + let testURL = try #require(URL(string: mp4TestURI)) + + let player = FVPVideoPlayer( + playerItem: playerItem(with: testURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) + player.eventListener = listener + } + + // Now test getVideoTracks + await withCheckedContinuation { continuation in + player.getVideoTracks { result, error in + #expect(error == nil) + #expect(result != nil) + // For regular MP4 files, media selection tracks should be nil (no HLS variants) + // The method returns empty data for non-HLS content + continuation.resume() + } + } + + var disposeError: FlutterError? + player.disposeWithError(&disposeError) + } + + // Integration test for getVideoTracks with an HLS stream over a live network request. + // HLS streams use AVAssetVariant API (iOS 15+) to enumerate available quality variants. + @Test func getVideoTracksWithRealHLSStream() async throws { + // TODO(stuartmorgan): Add more use of protocols in FVPVideoPlayer so that this test + // can use a fake item/asset instead of loading an actual remote asset. + let realObjectFactory = FVPDefaultAVFactory() + let hlsURL = try #require(URL(string: hlsTestURI)) + + let player = FVPVideoPlayer( + playerItem: playerItem(with: hlsURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) + player.eventListener = listener + } + + // Now test getVideoTracks + await withCheckedContinuation { continuation in + player.getVideoTracks { result, error in + #expect(error == nil) + #expect(result != nil) + + // For HLS streams on iOS 15+, we may have media selection tracks (variants) + if #available(iOS 15.0, macOS 12.0, *) { + // The bee.m3u8 stream may or may not have multiple video variants. + // We verify the method returns valid data without crashing. + if let mediaSelectionTracks = result?.mediaSelectionTracks { + // If media selection tracks exist, they should have valid structure + for track in mediaSelectionTracks { + #expect(track.variantIndex >= 0) + // Bitrate should be positive if present + if let bitrate = track.bitrate { + #expect(bitrate.intValue > 0) + } + } + } + } + continuation.resume() + } + } + + var disposeError: FlutterError? + player.disposeWithError(&disposeError) + } + + // Tests selectVideoTrack sets preferredPeakBitRate correctly. + @Test func selectVideoTrackSetsBitrate() async throws { + // TODO(stuartmorgan): Add more use of protocols in FVPVideoPlayer so that this test + // can use a fake item/asset instead of loading an actual remote asset. + let realObjectFactory = FVPDefaultAVFactory() + let testURL = try #require(URL(string: mp4TestURI)) + + let player = FVPVideoPlayer( + playerItem: playerItem(with: testURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) + player.eventListener = listener + } + + var error: FlutterError? + // Set a specific bitrate + player.selectVideoTrack(withBitrate: 5_000_000, error: &error) + #expect(error == nil) + #expect(player.player.currentItem?.preferredPeakBitRate == 5_000_000) + + player.disposeWithError(&error) + } + + // Tests selectVideoTrack with 0 bitrate enables auto quality selection. + @Test func selectVideoTrackAutoQuality() async throws { + // TODO(stuartmorgan): Add more use of protocols in FVPVideoPlayer so that this test + // can use a fake item/asset instead of loading an actual remote asset. + let realObjectFactory = FVPDefaultAVFactory() + let testURL = try #require(URL(string: mp4TestURI)) + + let player = FVPVideoPlayer( + playerItem: playerItem(with: testURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) + player.eventListener = listener + } + + var error: FlutterError? + // First set a specific bitrate + player.selectVideoTrack(withBitrate: 5_000_000, error: &error) + #expect(error == nil) + #expect(player.player.currentItem?.preferredPeakBitRate == 5_000_000) + + // Then set to auto quality (0) + player.selectVideoTrack(withBitrate: 0, error: &error) + #expect(error == nil) + #expect(player.player.currentItem?.preferredPeakBitRate == 0) + + player.disposeWithError(&error) + } + + // Tests that getVideoTracks works correctly through the plugin API with a real video. + @Test func getVideoTracksViaPluginWithRealVideo() async throws { + // TODO(stuartmorgan): Add more use of protocols in FVPVideoPlayer so that this test + // can use a fake item/asset instead of loading an actual remote asset. + let realObjectFactory = FVPDefaultAVFactory() + let testURL = try #require(URL(string: mp4TestURI)) + let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) + + let create = FVPCreationOptions.make( + withUri: mp4TestURI, + httpHeaders: [:]) + var error: FlutterError? + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer(with: create, error: &error)) + #expect(error == nil) + + let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer + #expect(player != nil) + + // Wait for player item to become ready + let item = try #require(player.player.currentItem) + await waitForPlayerItemStatus(item, state: .readyToPlay) + + // Now test getVideoTracks + await withCheckedContinuation { continuation in + player.getVideoTracks { result, error in + #expect(error == nil) + #expect(result != nil) + continuation.resume() + } + } + + player.disposeWithError(&error) + } + + // Tests selectVideoTrack via plugin API with HLS stream. + @Test func selectVideoTrackViaPluginWithHLSStream() async throws { + // TODO(stuartmorgan): Add more use of protocols in FVPVideoPlayer so that this test + // can use a fake item/asset instead of loading an actual remote asset. + let realObjectFactory = FVPDefaultAVFactory() + let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) + + var error: FlutterError? + // Use HLS stream which supports adaptive bitrate + let create = FVPCreationOptions.make( + withUri: hlsTestURI, + httpHeaders: [:]) + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer(with: create, error: &error)) + #expect(error == nil) + + let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer + #expect(player != nil) + + // Wait for player item to become ready + let item = try #require(player.player.currentItem) + await waitForPlayerItemStatus(item, state: .readyToPlay) + + // Test setting a specific bitrate + player.selectVideoTrack(withBitrate: 1_000_000, error: &error) + #expect(error == nil) + #expect(player.player.currentItem?.preferredPeakBitRate == 1_000_000) + + // Test setting auto quality + player.selectVideoTrack(withBitrate: 0, error: &error) + #expect(error == nil) + #expect(player.player.currentItem?.preferredPeakBitRate == 0) + + player.disposeWithError(&error) + } + // MARK: - Helper Methods /// Creates a plugin with the given dependencies, and default stubs for any that aren't provided, 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 2270120378d5..44bc8e2686ac 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 @@ -5,6 +5,7 @@ #import "./include/video_player_avfoundation/FVPVideoPlayer.h" #import "./include/video_player_avfoundation/FVPVideoPlayer_Internal.h" +#import #import #import "./include/video_player_avfoundation/AVAssetTrackUtils.h" @@ -14,6 +15,10 @@ static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; static void *rateContext = &rateContext; +/// The key name for loading AVURLAsset variants property asynchronously. +/// Note: Apple does not provide a constant for this key; it is documented in the AVURLAsset API. +static NSString *const kFVPAssetVariantsKey = @"variants"; + /// Registers KVO observers on 'object' for each entry in 'observations', which must be a /// dictionary mapping KVO keys to NSValue-wrapped context pointers. /// @@ -444,6 +449,144 @@ - (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable *_Nonnull) [self updatePlayingState]; } +- (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, + FlutterError *_Nullable))completion { + NSMutableArray *mediaSelectionTracks = [NSMutableArray array]; + + AVPlayerItem *currentItem = _player.currentItem; + if (!currentItem) { + completion(nil, nil); + return; + } + + AVURLAsset *urlAsset = (AVURLAsset *)currentItem.asset; + if (![urlAsset isKindOfClass:[AVURLAsset class]]) { + completion(nil, nil); + return; + } + + // Use AVAssetVariant API for iOS 15+ to get HLS variants + if (@available(iOS 15.0, macOS 12.0, *)) { + [urlAsset + loadValuesAsynchronouslyForKeys:@[ kFVPAssetVariantsKey ] + completionHandler:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + NSError *error = nil; + AVKeyValueStatus status = + [urlAsset statusOfValueForKey:kFVPAssetVariantsKey error:&error]; + + if (status == AVKeyValueStatusLoaded) { + NSArray *variants = urlAsset.variants; + double currentBitrate = MAX(currentItem.preferredPeakBitRate, 0); + + NSInteger variantIndex = 0; + for (AVAssetVariant *variant in variants) { + double peakBitRate = variant.peakBitRate; + CGSize videoSize = CGSizeZero; + double frameRate = 0; + NSString *codec = nil; + + // Get video attributes if available + AVAssetVariantVideoAttributes *videoAttrs = variant.videoAttributes; + if (videoAttrs) { + videoSize = videoAttrs.presentationSize; + frameRate = videoAttrs.nominalFrameRate; + // Get codec from media sub types + NSArray *codecTypes = videoAttrs.codecTypes; + if (codecTypes.count > 0) { + FourCharCode codecType = [codecTypes[0] unsignedIntValue]; + codec = [self codecStringFromFourCharCode:codecType]; + } + } + + // Determine if this variant is currently selected by comparing + // bitrates. Since AVPlayer doesn't expose the exact selected variant, + // we use a 10% tolerance to account for minor bitrate variations in + // adaptive streaming. + BOOL isSelected = + (currentBitrate > 0 && + fabs(peakBitRate - currentBitrate) < peakBitRate * 0.1); + + // Generate a human-readable resolution label (e.g., "1080p") + NSString *resolutionLabel = nil; + if (videoSize.height > 0) { + resolutionLabel = + [NSString stringWithFormat:@"%.0fp", videoSize.height]; + } + + FVPMediaSelectionVideoTrackData *trackData = + [self createVideoTrackDataWithIndex:variantIndex + label:resolutionLabel + peakBitRate:peakBitRate + videoSize:videoSize + frameRate:frameRate + codec:codec + isSelected:isSelected]; + [mediaSelectionTracks addObject:trackData]; + variantIndex++; + } + } + + FVPNativeVideoTrackData *result = + [FVPNativeVideoTrackData makeWithAssetTracks:nil + mediaSelectionTracks:mediaSelectionTracks]; + completion(result, nil); + }); + }]; + } else { + // For iOS < 15, AVAssetVariant API is not available. Return nil (not an error) + // since the absence of variant data is expected on older OS versions. + completion(nil, nil); + } +} + +/// Creates a video track data object with the given parameters, converting values to NSNumber +/// where appropriate and returning nil for invalid/zero values. +- (FVPMediaSelectionVideoTrackData *)createVideoTrackDataWithIndex:(NSInteger)index + label:(NSString *)label + peakBitRate:(double)peakBitRate + videoSize:(CGSize)videoSize + frameRate:(double)frameRate + codec:(NSString *)codec + isSelected:(BOOL)isSelected { + return [FVPMediaSelectionVideoTrackData + makeWithVariantIndex:index + label:label + bitrate:peakBitRate > 0 ? @((NSInteger)peakBitRate) : nil + width:videoSize.width > 0 ? @((NSInteger)videoSize.width) : nil + height:videoSize.height > 0 ? @((NSInteger)videoSize.height) : nil + frameRate:frameRate > 0 ? @(frameRate) : nil + codec:codec + isSelected:isSelected]; +} + +/// Converts a FourCharCode codec type to a human-readable string for display in the UI. +/// These codec names help users understand the video encoding format of each quality variant. +- (NSString *)codecStringFromFourCharCode:(FourCharCode)code { + switch (code) { + case kCMVideoCodecType_H264: + return @"avc1"; + case kCMVideoCodecType_HEVC: + return @"hevc"; + case kCMVideoCodecType_VP9: + return @"vp9"; + default: + return nil; + } +} + +- (void)selectVideoTrackWithBitrate:(NSInteger)bitrate + error:(FlutterError *_Nullable *_Nonnull)error { + AVPlayerItem *currentItem = _player.currentItem; + if (!currentItem) { + return; + } + + // Set preferredPeakBitRate to select the quality + // 0 means auto quality (adaptive streaming) + currentItem.preferredPeakBitRate = (double)bitrate; +} + - (nullable NSArray *)getAudioTracks: (FlutterError *_Nullable *_Nonnull)error { AVPlayerItem *currentItem = _player.currentItem; 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 3b2dd3952245..49fddcd711ac 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 @@ -17,46 +17,102 @@ NS_ASSUME_NONNULL_BEGIN @class FVPCreationOptions; @class FVPTexturePlayerIds; @class FVPMediaSelectionAudioTrackData; +@class FVPMediaSelectionVideoTrackData; +@class FVPAssetVideoTrackData; +@class FVPNativeVideoTrackData; /// Information passed to the platform view creation. @interface FVPPlatformVideoViewCreationParams : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId; -@property(nonatomic, assign) NSInteger playerId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId; +@property(nonatomic, assign) NSInteger playerId; @end @interface FVPCreationOptions : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders; -@property(nonatomic, copy) NSString *uri; -@property(nonatomic, copy) NSDictionary *httpHeaders; + httpHeaders:(NSDictionary *)httpHeaders; +@property(nonatomic, copy) NSString * uri; +@property(nonatomic, copy) NSDictionary * httpHeaders; @end @interface FVPTexturePlayerIds : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId; -@property(nonatomic, assign) NSInteger playerId; -@property(nonatomic, assign) NSInteger textureId; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId; +@property(nonatomic, assign) NSInteger playerId; +@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; ++ (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 + +/// Video track data from AVAssetVariant (HLS variants) for iOS 15+. +@interface FVPMediaSelectionVideoTrackData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithVariantIndex:(NSInteger )variantIndex + label:(nullable NSString *)label + bitrate:(nullable NSNumber *)bitrate + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected; +@property(nonatomic, assign) NSInteger variantIndex; +@property(nonatomic, copy, nullable) NSString * label; +@property(nonatomic, strong, nullable) NSNumber * bitrate; +@property(nonatomic, strong, nullable) NSNumber * width; +@property(nonatomic, strong, nullable) NSNumber * height; +@property(nonatomic, strong, nullable) NSNumber * frameRate; +@property(nonatomic, copy, nullable) NSString * codec; +@property(nonatomic, assign) BOOL isSelected; +@end + +/// Video track data from AVAssetTrack (regular videos). +@interface FVPAssetVideoTrackData : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithTrackId:(NSInteger )trackId + label:(nullable NSString *)label + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected; +@property(nonatomic, assign) NSInteger trackId; +@property(nonatomic, copy, nullable) NSString * label; +@property(nonatomic, strong, nullable) NSNumber * width; +@property(nonatomic, strong, nullable) NSNumber * height; +@property(nonatomic, strong, nullable) NSNumber * frameRate; +@property(nonatomic, copy, nullable) NSString * codec; +@property(nonatomic, assign) BOOL isSelected; +@end + +/// Container for video track data from iOS. +@interface FVPNativeVideoTrackData : NSObject ++ (instancetype)makeWithAssetTracks:(nullable NSArray *)assetTracks + mediaSelectionTracks:(nullable NSArray *)mediaSelectionTracks; +/// Asset-based tracks (for regular videos) +@property(nonatomic, copy, nullable) NSArray * assetTracks; +/// Media selection tracks (for HLS variants on iOS 15+) +@property(nonatomic, copy, nullable) NSArray * mediaSelectionTracks; @end /// The codec used by all APIs. @@ -65,25 +121,17 @@ NSObject *FVPGetMessagesCodec(void); @protocol FVPAVFoundationVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSNumber *)createPlatformViewPlayerWithOptions:(FVPCreationOptions *)params error:(FlutterError *_Nullable *_Nonnull)error; /// @return `nil` only when `error != nil`. -- (nullable FVPTexturePlayerIds *) - createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable FVPTexturePlayerIds *)createTexturePlayerWithOptions:(FVPCreationOptions *)creationOptions error:(FlutterError *_Nullable *_Nonnull)error; - (void)setMixWithOthers:(BOOL)mixWithOthers error:(FlutterError *_Nullable *_Nonnull)error; -- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset - package:(nullable NSString *)package - error:(FlutterError *_Nullable *_Nonnull)error; +- (nullable NSString *)fileURLForAssetWithName:(NSString *)asset package:(nullable NSString *)package error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPAVFoundationVideoPlayerApi( - id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *_Nullable api); + +extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); -extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - id binaryMessenger, - NSObject *_Nullable api, NSString *messageChannelSuffix); @protocol FVPVideoPlayerInstanceApi - (void)setLooping:(BOOL)looping error:(FlutterError *_Nullable *_Nonnull)error; @@ -96,17 +144,17 @@ extern void SetUpFVPAVFoundationVideoPlayerApiWithSuffix( - (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; +- (nullable NSArray *)getAudioTracks:(FlutterError *_Nullable *_Nonnull)error; +- (void)selectAudioTrackAtIndex:(NSInteger)trackIndex error:(FlutterError *_Nullable *_Nonnull)error; +/// Gets the available video tracks for the video. +- (void)getVideoTracks:(void (^)(FVPNativeVideoTrackData *_Nullable, FlutterError *_Nullable))completion; +/// Selects a video track by setting preferredPeakBitRate. +/// Pass 0 to enable auto quality selection. +- (void)selectVideoTrackWithBitrate:(NSInteger)bitrate error:(FlutterError *_Nullable *_Nonnull)error; @end -extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *_Nullable api); +extern void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *_Nullable api); -extern void SetUpFVPVideoPlayerInstanceApiWithSuffix( - id binaryMessenger, NSObject *_Nullable api, - NSString *messageChannelSuffix); +extern void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *_Nullable api, NSString *messageChannelSuffix); NS_ASSUME_NONNULL_END 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 abb8efbad50d..de645d982cfe 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 @@ -50,16 +50,32 @@ + (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)li - (NSArray *)toList; @end +@interface FVPMediaSelectionVideoTrackData () ++ (FVPMediaSelectionVideoTrackData *)fromList:(NSArray *)list; ++ (nullable FVPMediaSelectionVideoTrackData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPAssetVideoTrackData () ++ (FVPAssetVideoTrackData *)fromList:(NSArray *)list; ++ (nullable FVPAssetVideoTrackData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FVPNativeVideoTrackData () ++ (FVPNativeVideoTrackData *)fromList:(NSArray *)list; ++ (nullable FVPNativeVideoTrackData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @implementation FVPPlatformVideoViewCreationParams -+ (instancetype)makeWithPlayerId:(NSInteger)playerId { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId { + FVPPlatformVideoViewCreationParams* pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = playerId; return pigeonResult; } + (FVPPlatformVideoViewCreationParams *)fromList:(NSArray *)list { - FVPPlatformVideoViewCreationParams *pigeonResult = - [[FVPPlatformVideoViewCreationParams alloc] init]; + FVPPlatformVideoViewCreationParams *pigeonResult = [[FVPPlatformVideoViewCreationParams alloc] init]; pigeonResult.playerId = [GetNullableObjectAtIndex(list, 0) integerValue]; return pigeonResult; } @@ -75,8 +91,8 @@ + (nullable FVPPlatformVideoViewCreationParams *)nullableFromList:(NSArray * @implementation FVPCreationOptions + (instancetype)makeWithUri:(NSString *)uri - httpHeaders:(NSDictionary *)httpHeaders { - FVPCreationOptions *pigeonResult = [[FVPCreationOptions alloc] init]; + httpHeaders:(NSDictionary *)httpHeaders { + FVPCreationOptions* pigeonResult = [[FVPCreationOptions alloc] init]; pigeonResult.uri = uri; pigeonResult.httpHeaders = httpHeaders; return pigeonResult; @@ -99,8 +115,9 @@ + (nullable FVPCreationOptions *)nullableFromList:(NSArray *)list { @end @implementation FVPTexturePlayerIds -+ (instancetype)makeWithPlayerId:(NSInteger)playerId textureId:(NSInteger)textureId { - FVPTexturePlayerIds *pigeonResult = [[FVPTexturePlayerIds alloc] init]; ++ (instancetype)makeWithPlayerId:(NSInteger )playerId + textureId:(NSInteger )textureId { + FVPTexturePlayerIds* pigeonResult = [[FVPTexturePlayerIds alloc] init]; pigeonResult.playerId = playerId; pigeonResult.textureId = textureId; return pigeonResult; @@ -123,12 +140,12 @@ + (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]; ++ (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; @@ -159,19 +176,144 @@ + (nullable FVPMediaSelectionAudioTrackData *)nullableFromList:(NSArray *)li } @end +@implementation FVPMediaSelectionVideoTrackData ++ (instancetype)makeWithVariantIndex:(NSInteger )variantIndex + label:(nullable NSString *)label + bitrate:(nullable NSNumber *)bitrate + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected { + FVPMediaSelectionVideoTrackData* pigeonResult = [[FVPMediaSelectionVideoTrackData alloc] init]; + pigeonResult.variantIndex = variantIndex; + pigeonResult.label = label; + pigeonResult.bitrate = bitrate; + pigeonResult.width = width; + pigeonResult.height = height; + pigeonResult.frameRate = frameRate; + pigeonResult.codec = codec; + pigeonResult.isSelected = isSelected; + return pigeonResult; +} ++ (FVPMediaSelectionVideoTrackData *)fromList:(NSArray *)list { + FVPMediaSelectionVideoTrackData *pigeonResult = [[FVPMediaSelectionVideoTrackData alloc] init]; + pigeonResult.variantIndex = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.label = GetNullableObjectAtIndex(list, 1); + pigeonResult.bitrate = GetNullableObjectAtIndex(list, 2); + pigeonResult.width = GetNullableObjectAtIndex(list, 3); + pigeonResult.height = GetNullableObjectAtIndex(list, 4); + pigeonResult.frameRate = GetNullableObjectAtIndex(list, 5); + pigeonResult.codec = GetNullableObjectAtIndex(list, 6); + pigeonResult.isSelected = [GetNullableObjectAtIndex(list, 7) boolValue]; + return pigeonResult; +} ++ (nullable FVPMediaSelectionVideoTrackData *)nullableFromList:(NSArray *)list { + return (list) ? [FVPMediaSelectionVideoTrackData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.variantIndex), + self.label ?: [NSNull null], + self.bitrate ?: [NSNull null], + self.width ?: [NSNull null], + self.height ?: [NSNull null], + self.frameRate ?: [NSNull null], + self.codec ?: [NSNull null], + @(self.isSelected), + ]; +} +@end + +@implementation FVPAssetVideoTrackData ++ (instancetype)makeWithTrackId:(NSInteger )trackId + label:(nullable NSString *)label + width:(nullable NSNumber *)width + height:(nullable NSNumber *)height + frameRate:(nullable NSNumber *)frameRate + codec:(nullable NSString *)codec + isSelected:(BOOL )isSelected { + FVPAssetVideoTrackData* pigeonResult = [[FVPAssetVideoTrackData alloc] init]; + pigeonResult.trackId = trackId; + pigeonResult.label = label; + pigeonResult.width = width; + pigeonResult.height = height; + pigeonResult.frameRate = frameRate; + pigeonResult.codec = codec; + pigeonResult.isSelected = isSelected; + return pigeonResult; +} ++ (FVPAssetVideoTrackData *)fromList:(NSArray *)list { + FVPAssetVideoTrackData *pigeonResult = [[FVPAssetVideoTrackData alloc] init]; + pigeonResult.trackId = [GetNullableObjectAtIndex(list, 0) integerValue]; + pigeonResult.label = GetNullableObjectAtIndex(list, 1); + pigeonResult.width = GetNullableObjectAtIndex(list, 2); + pigeonResult.height = GetNullableObjectAtIndex(list, 3); + pigeonResult.frameRate = GetNullableObjectAtIndex(list, 4); + pigeonResult.codec = GetNullableObjectAtIndex(list, 5); + pigeonResult.isSelected = [GetNullableObjectAtIndex(list, 6) boolValue]; + return pigeonResult; +} ++ (nullable FVPAssetVideoTrackData *)nullableFromList:(NSArray *)list { + return (list) ? [FVPAssetVideoTrackData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + @(self.trackId), + self.label ?: [NSNull null], + self.width ?: [NSNull null], + self.height ?: [NSNull null], + self.frameRate ?: [NSNull null], + self.codec ?: [NSNull null], + @(self.isSelected), + ]; +} +@end + +@implementation FVPNativeVideoTrackData ++ (instancetype)makeWithAssetTracks:(nullable NSArray *)assetTracks + mediaSelectionTracks:(nullable NSArray *)mediaSelectionTracks { + FVPNativeVideoTrackData* pigeonResult = [[FVPNativeVideoTrackData alloc] init]; + pigeonResult.assetTracks = assetTracks; + pigeonResult.mediaSelectionTracks = mediaSelectionTracks; + return pigeonResult; +} ++ (FVPNativeVideoTrackData *)fromList:(NSArray *)list { + FVPNativeVideoTrackData *pigeonResult = [[FVPNativeVideoTrackData alloc] init]; + pigeonResult.assetTracks = GetNullableObjectAtIndex(list, 0); + pigeonResult.mediaSelectionTracks = GetNullableObjectAtIndex(list, 1); + return pigeonResult; +} ++ (nullable FVPNativeVideoTrackData *)nullableFromList:(NSArray *)list { + return (list) ? [FVPNativeVideoTrackData fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.assetTracks ?: [NSNull null], + self.mediaSelectionTracks ?: [NSNull null], + ]; +} +@end + @interface FVPMessagesPigeonCodecReader : FlutterStandardReader @end @implementation FVPMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 129: + case 129: return [FVPPlatformVideoViewCreationParams fromList:[self readValue]]; - case 130: + case 130: return [FVPCreationOptions fromList:[self readValue]]; - case 131: + case 131: return [FVPTexturePlayerIds fromList:[self readValue]]; - case 132: + case 132: return [FVPMediaSelectionAudioTrackData fromList:[self readValue]]; + case 133: + return [FVPMediaSelectionVideoTrackData fromList:[self readValue]]; + case 134: + return [FVPAssetVideoTrackData fromList:[self readValue]]; + case 135: + return [FVPNativeVideoTrackData fromList:[self readValue]]; default: return [super readValueOfType:type]; } @@ -194,6 +336,15 @@ - (void)writeValue:(id)value { } else if ([value isKindOfClass:[FVPMediaSelectionAudioTrackData class]]) { [self writeByte:132]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPMediaSelectionVideoTrackData class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPAssetVideoTrackData class]]) { + [self writeByte:134]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FVPNativeVideoTrackData class]]) { + [self writeByte:135]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -215,35 +366,25 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { static FlutterStandardMessageCodec *sSharedObject = nil; static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - FVPMessagesPigeonCodecReaderWriter *readerWriter = - [[FVPMessagesPigeonCodecReaderWriter alloc] init]; + FVPMessagesPigeonCodecReaderWriter *readerWriter = [[FVPMessagesPigeonCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } -void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPAVFoundationVideoPlayerApi(id binaryMessenger, NSObject *api) { SetUpFVPAVFoundationVideoPlayerApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.initialize", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initialize:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", - api); + NSCAssert([api respondsToSelector:@selector(initialize:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(initialize:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api initialize:&error]; @@ -254,19 +395,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForPlatformView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createPlatformViewPlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createPlatformViewPlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createPlatformViewPlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_params = GetNullableObjectAtIndex(args, 0); @@ -279,25 +414,18 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString - stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.createForTextureView", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(createTexturePlayerWithOptions:error:)", - api); + NSCAssert([api respondsToSelector:@selector(createTexturePlayerWithOptions:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(createTexturePlayerWithOptions:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FVPCreationOptions *arg_creationOptions = GetNullableObjectAtIndex(args, 0); FlutterError *error; - FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions - error:&error]; + FVPTexturePlayerIds *output = [api createTexturePlayerWithOptions:arg_creationOptions error:&error]; callback(wrapResult(output, error)); }]; } else { @@ -305,18 +433,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.setMixWithOthers", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(setMixWithOthers:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setMixWithOthers:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(setMixWithOthers:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_mixWithOthers = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -329,18 +452,13 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"AVFoundationVideoPlayerApi.getAssetUrl", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], - @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to " - @"@selector(fileURLForAssetWithName:package:error:)", - api); + NSCAssert([api respondsToSelector:@selector(fileURLForAssetWithName:package:error:)], @"FVPAVFoundationVideoPlayerApi api (%@) doesn't respond to @selector(fileURLForAssetWithName:package:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_asset = GetNullableObjectAtIndex(args, 0); @@ -354,30 +472,20 @@ void SetUpFVPAVFoundationVideoPlayerApiWithSuffix(id bin } } } -void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, - NSObject *api) { +void SetUpFVPVideoPlayerInstanceApi(id binaryMessenger, NSObject *api) { SetUpFVPVideoPlayerInstanceApiWithSuffix(binaryMessenger, api, @""); } -void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, - NSObject *api, - NSString *messageChannelSuffix) { - messageChannelSuffix = messageChannelSuffix.length > 0 - ? [NSString stringWithFormat:@".%@", messageChannelSuffix] - : @""; +void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryMessenger, NSObject *api, NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 ? [NSString stringWithFormat: @".%@", messageChannelSuffix] : @""; { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setLooping", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setLooping:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setLooping:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setLooping:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; BOOL arg_looping = [GetNullableObjectAtIndex(args, 0) boolValue]; @@ -390,18 +498,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setVolume", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(setVolume:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setVolume:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setVolume:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_volume = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -414,18 +517,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.setPlaybackSpeed", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " - @"@selector(setPlaybackSpeed:error:)", - api); + NSCAssert([api respondsToSelector:@selector(setPlaybackSpeed:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(setPlaybackSpeed:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; double arg_speed = [GetNullableObjectAtIndex(args, 0) doubleValue]; @@ -438,17 +536,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.play", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(playWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(playWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(playWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api playWithError:&error]; @@ -459,16 +553,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.getPosition", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(position:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); + NSCAssert([api respondsToSelector:@selector(position:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(position:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; NSNumber *output = [api position:&error]; @@ -479,42 +570,32 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.seekTo", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(seekTo:completion:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", - api); + NSCAssert([api respondsToSelector:@selector(seekTo:completion:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(seekTo:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSInteger arg_position = [GetNullableObjectAtIndex(args, 0) integerValue]; - [api seekTo:arg_position - completion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + [api seekTo:arg_position completion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.pause", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pauseWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(pauseWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(pauseWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api pauseWithError:&error]; @@ -525,18 +606,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.dispose", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert( - [api respondsToSelector:@selector(disposeWithError:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", - api); + NSCAssert([api respondsToSelector:@selector(disposeWithError:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(disposeWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; [api disposeWithError:&error]; @@ -547,17 +623,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.getAudioTracks", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getAudioTracks:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getAudioTracks:)", - 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]; @@ -568,18 +640,13 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM } } { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:[NSString stringWithFormat:@"%@%@", - @"dev.flutter.pigeon.video_player_avfoundation." - @"VideoPlayerInstanceApi.selectAudioTrack", - messageChannelSuffix] + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack", messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FVPGetMessagesCodec()]; + codec:FVPGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(selectAudioTrackAtIndex:error:)], - @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to " - @"@selector(selectAudioTrackAtIndex:error:)", - 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]; @@ -591,4 +658,43 @@ void SetUpFVPVideoPlayerInstanceApiWithSuffix(id binaryM [channel setMessageHandler:nil]; } } + /// Gets the available video tracks for the video. + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getVideoTracks", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(getVideoTracks:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(getVideoTracks:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + [api getVideoTracks:^(FVPNativeVideoTrackData *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } + /// Selects a video track by setting preferredPeakBitRate. + /// Pass 0 to enable auto quality selection. + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:[NSString stringWithFormat:@"%@%@", @"dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectVideoTrack", messageChannelSuffix] + binaryMessenger:binaryMessenger + codec:FVPGetMessagesCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(selectVideoTrackWithBitrate:error:)], @"FVPVideoPlayerInstanceApi api (%@) doesn't respond to @selector(selectVideoTrackWithBitrate:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSInteger arg_bitrate = [GetNullableObjectAtIndex(args, 0) integerValue]; + FlutterError *error; + [api selectVideoTrackWithBitrate:arg_bitrate error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index 4a9153c7d267..9ebd74df6edd 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.6.0 + video_player_platform_interface: ^6.7.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 6684d9c4c658..d9c8fc3491f8 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 @@ -212,6 +212,91 @@ class AVFoundationVideoPlayer extends VideoPlayerPlatform { return true; } + @override + Future> getVideoTracks(int playerId) async { + final NativeVideoTrackData nativeData = await _playerWith( + id: playerId, + ).getVideoTracks(); + final tracks = []; + + // Convert HLS variant tracks (iOS 15+) + if (nativeData.mediaSelectionTracks != null) { + for (final MediaSelectionVideoTrackData track + in nativeData.mediaSelectionTracks!) { + // Use bitrate as the track ID for HLS variants + final trackId = 'variant_${track.bitrate ?? track.variantIndex}'; + // Generate label from resolution if not provided + final String? label = + track.label ?? + (track.width != null && track.height != null + ? '${track.height}p' + : null); + tracks.add( + VideoTrack( + id: trackId, + isSelected: track.isSelected, + label: label, + bitrate: track.bitrate, + width: track.width, + height: track.height, + frameRate: track.frameRate, + codec: track.codec, + ), + ); + } + } + + // Convert asset tracks (for regular videos) + if (nativeData.assetTracks != null) { + for (final AssetVideoTrackData track in nativeData.assetTracks!) { + final trackId = 'asset_${track.trackId}'; + // Generate label from resolution if not provided + final String? label = + track.label ?? + (track.width != null && track.height != null + ? '${track.height}p' + : null); + tracks.add( + VideoTrack( + id: trackId, + isSelected: track.isSelected, + label: label, + width: track.width, + height: track.height, + frameRate: track.frameRate, + codec: track.codec, + ), + ); + } + } + + return tracks; + } + + @override + Future selectVideoTrack(int playerId, VideoTrack? track) async { + if (track == null) { + // Auto quality - pass 0 to clear preferredPeakBitRate + await _playerWith(id: playerId).selectVideoTrack(0); + return; + } + + // Use bitrate directly from the track for HLS quality selection + if (track.bitrate != null) { + await _playerWith(id: playerId).selectVideoTrack(track.bitrate!); + return; + } + + // For asset tracks without bitrate, we can't really select them differently + // Just ignore the selection for non-HLS content + } + + @override + bool isVideoTrackSupportAvailable() { + // iOS with AVFoundation supports video track selection + return true; + } + @override Widget buildView(int playerId) { return buildViewWithOptions(VideoViewOptions(playerId: playerId)); @@ -300,6 +385,14 @@ class _PlayerInstance { return _eventStreamController.stream; } + Future getVideoTracks() { + return _api.getVideoTracks(); + } + + Future selectVideoTrack(int bitrate) { + return _api.selectVideoTrack(bitrate); + } + Future dispose() async { await _eventSubscription?.cancel(); unawaited(_eventStreamController.close()); 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 24644d8f42d0..56ea80e6aada 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 @@ -17,49 +17,49 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && - a.indexed.every( - ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), - ); + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key]), - ); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + /// Information passed to the platform view creation. class PlatformVideoViewCreationParams { - PlatformVideoViewCreationParams({required this.playerId}); + PlatformVideoViewCreationParams({ + required this.playerId, + }); int playerId; List _toList() { - return [playerId]; + return [ + playerId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static PlatformVideoViewCreationParams decode(Object result) { result as List; - return PlatformVideoViewCreationParams(playerId: result[0]! as int); + return PlatformVideoViewCreationParams( + playerId: result[0]! as int, + ); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! PlatformVideoViewCreationParams || - other.runtimeType != runtimeType) { + if (other is! PlatformVideoViewCreationParams || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -70,30 +70,35 @@ class PlatformVideoViewCreationParams { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class CreationOptions { - CreationOptions({required this.uri, required this.httpHeaders}); + CreationOptions({ + required this.uri, + required this.httpHeaders, + }); String uri; Map httpHeaders; List _toList() { - return [uri, httpHeaders]; + return [ + uri, + httpHeaders, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static CreationOptions decode(Object result) { result as List; return CreationOptions( uri: result[0]! as String, - httpHeaders: (result[1] as Map?)! - .cast(), + httpHeaders: (result[1] as Map?)!.cast(), ); } @@ -111,23 +116,29 @@ class CreationOptions { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class TexturePlayerIds { - TexturePlayerIds({required this.playerId, required this.textureId}); + TexturePlayerIds({ + required this.playerId, + required this.textureId, + }); int playerId; int textureId; List _toList() { - return [playerId, textureId]; + return [ + playerId, + textureId, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static TexturePlayerIds decode(Object result) { result as List; @@ -151,7 +162,8 @@ class TexturePlayerIds { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } /// Raw audio track data from AVMediaSelectionOption (for HLS streams). @@ -185,8 +197,7 @@ class MediaSelectionAudioTrackData { } Object encode() { - return _toList(); - } + return _toList(); } static MediaSelectionAudioTrackData decode(Object result) { result as List; @@ -202,8 +213,84 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (other is! MediaSelectionAudioTrackData || - other.runtimeType != runtimeType) { + 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()) +; +} + +/// Video track data from AVAssetVariant (HLS variants) for iOS 15+. +class MediaSelectionVideoTrackData { + MediaSelectionVideoTrackData({ + required this.variantIndex, + this.label, + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int variantIndex; + + String? label; + + int? bitrate; + + int? width; + + int? height; + + double? frameRate; + + String? codec; + + bool isSelected; + + List _toList() { + return [ + variantIndex, + label, + bitrate, + width, + height, + frameRate, + codec, + isSelected, + ]; + } + + Object encode() { + return _toList(); } + + static MediaSelectionVideoTrackData decode(Object result) { + result as List; + return MediaSelectionVideoTrackData( + variantIndex: result[0]! as int, + label: result[1] as String?, + bitrate: result[2] as int?, + width: result[3] as int?, + height: result[4] as int?, + frameRate: result[5] as double?, + codec: result[6] as String?, + isSelected: result[7]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! MediaSelectionVideoTrackData || other.runtimeType != runtimeType) { return false; } if (identical(this, other)) { @@ -214,9 +301,132 @@ class MediaSelectionAudioTrackData { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } +/// Video track data from AVAssetTrack (regular videos). +class AssetVideoTrackData { + AssetVideoTrackData({ + required this.trackId, + this.label, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int trackId; + + String? label; + + int? width; + + int? height; + + double? frameRate; + + String? codec; + + bool isSelected; + + List _toList() { + return [ + trackId, + label, + width, + height, + frameRate, + codec, + isSelected, + ]; + } + + Object encode() { + return _toList(); } + + static AssetVideoTrackData decode(Object result) { + result as List; + return AssetVideoTrackData( + trackId: result[0]! as int, + label: result[1] as String?, + width: result[2] as int?, + height: result[3] as int?, + frameRate: result[4] as double?, + codec: result[5] as String?, + isSelected: result[6]! as bool, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! AssetVideoTrackData || 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()) +; +} + +/// Container for video track data from iOS. +class NativeVideoTrackData { + NativeVideoTrackData({ + this.assetTracks, + this.mediaSelectionTracks, + }); + + /// Asset-based tracks (for regular videos) + List? assetTracks; + + /// Media selection tracks (for HLS variants on iOS 15+) + List? mediaSelectionTracks; + + List _toList() { + return [ + assetTracks, + mediaSelectionTracks, + ]; + } + + Object encode() { + return _toList(); } + + static NativeVideoTrackData decode(Object result) { + result as List; + return NativeVideoTrackData( + assetTracks: (result[0] as List?)?.cast(), + mediaSelectionTracks: (result[1] as List?)?.cast(), + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! NativeVideoTrackData || 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 @@ -224,18 +434,27 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PlatformVideoViewCreationParams) { + } else if (value is PlatformVideoViewCreationParams) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is CreationOptions) { + } else if (value is CreationOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is TexturePlayerIds) { + } else if (value is TexturePlayerIds) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is MediaSelectionAudioTrackData) { + } else if (value is MediaSelectionAudioTrackData) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is MediaSelectionVideoTrackData) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is AssetVideoTrackData) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is NativeVideoTrackData) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -244,14 +463,20 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return PlatformVideoViewCreationParams.decode(readValue(buffer)!); - case 130: + case 130: return CreationOptions.decode(readValue(buffer)!); - case 131: + case 131: return TexturePlayerIds.decode(readValue(buffer)!); - case 132: + case 132: return MediaSelectionAudioTrackData.decode(readValue(buffer)!); + case 133: + return MediaSelectionVideoTrackData.decode(readValue(buffer)!); + case 134: + return AssetVideoTrackData.decode(readValue(buffer)!); + case 135: + return NativeVideoTrackData.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -262,13 +487,9 @@ class AVFoundationVideoPlayerApi { /// Constructor for [AVFoundationVideoPlayerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - AVFoundationVideoPlayerApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + AVFoundationVideoPlayerApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -276,8 +497,7 @@ class AVFoundationVideoPlayerApi { final String pigeonVar_messageChannelSuffix; Future initialize() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.initialize$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -299,16 +519,13 @@ class AVFoundationVideoPlayerApi { } Future createForPlatformView(CreationOptions params) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForPlatformView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [params], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([params]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -328,19 +545,14 @@ class AVFoundationVideoPlayerApi { } } - Future createForTextureView( - CreationOptions creationOptions, - ) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; + Future createForTextureView(CreationOptions creationOptions) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.createForTextureView$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [creationOptions], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([creationOptions]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -361,16 +573,13 @@ class AVFoundationVideoPlayerApi { } Future setMixWithOthers(bool mixWithOthers) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.setMixWithOthers$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [mixWithOthers], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([mixWithOthers]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -386,16 +595,13 @@ class AVFoundationVideoPlayerApi { } Future getAssetUrl(String asset, String? package) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.AVFoundationVideoPlayerApi.getAssetUrl$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [asset, package], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([asset, package]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -415,13 +621,9 @@ class VideoPlayerInstanceApi { /// Constructor for [VideoPlayerInstanceApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - VideoPlayerInstanceApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty - ? '.$messageChannelSuffix' - : ''; + VideoPlayerInstanceApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -429,16 +631,13 @@ class VideoPlayerInstanceApi { final String pigeonVar_messageChannelSuffix; Future setLooping(bool looping) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setLooping$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [looping], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([looping]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -454,16 +653,13 @@ class VideoPlayerInstanceApi { } Future setVolume(double volume) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setVolume$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [volume], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([volume]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -479,16 +675,13 @@ class VideoPlayerInstanceApi { } Future setPlaybackSpeed(double speed) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.setPlaybackSpeed$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [speed], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([speed]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -504,8 +697,7 @@ class VideoPlayerInstanceApi { } Future play() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.play$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -527,8 +719,7 @@ class VideoPlayerInstanceApi { } Future getPosition() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getPosition$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -555,16 +746,13 @@ class VideoPlayerInstanceApi { } Future seekTo(int position) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.seekTo$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [position], - ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([position]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); @@ -580,8 +768,7 @@ class VideoPlayerInstanceApi { } Future pause() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.pause$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -603,8 +790,7 @@ class VideoPlayerInstanceApi { } Future dispose() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.dispose$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -626,8 +812,7 @@ class VideoPlayerInstanceApi { } Future> getAudioTracks() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getAudioTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -649,22 +834,70 @@ class VideoPlayerInstanceApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as List?)! - .cast(); + return (pigeonVar_replyList[0] as List?)!.cast(); } } Future selectAudioTrack(int trackIndex) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectAudioTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([trackIndex]); + final 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; + } + } + + /// Gets the available video tracks for the video. + Future getVideoTracks() async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.getVideoTracks$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [trackIndex], + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final 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 NativeVideoTrackData?)!; + } + } + + /// Selects a video track by setting preferredPeakBitRate. + /// Pass 0 to enable auto quality selection. + Future selectVideoTrack(int bitrate) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.video_player_avfoundation.VideoPlayerInstanceApi.selectVideoTrack$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([bitrate]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index f49b46005307..773d3cd9f765 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -73,6 +73,61 @@ abstract class AVFoundationVideoPlayerApi { String? getAssetUrl(String asset, String? package); } +/// Video track data from AVAssetVariant (HLS variants) for iOS 15+. +class MediaSelectionVideoTrackData { + MediaSelectionVideoTrackData({ + required this.variantIndex, + this.label, + this.bitrate, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int variantIndex; + String? label; + int? bitrate; + int? width; + int? height; + double? frameRate; + String? codec; + bool isSelected; +} + +/// Video track data from AVAssetTrack (regular videos). +class AssetVideoTrackData { + AssetVideoTrackData({ + required this.trackId, + this.label, + this.width, + this.height, + this.frameRate, + this.codec, + required this.isSelected, + }); + + int trackId; + String? label; + int? width; + int? height; + double? frameRate; + String? codec; + bool isSelected; +} + +/// Container for video track data from iOS. +class NativeVideoTrackData { + NativeVideoTrackData({this.assetTracks, this.mediaSelectionTracks}); + + /// Asset-based tracks (for regular videos) + List? assetTracks; + + /// Media selection tracks (for HLS variants on iOS 15+) + List? mediaSelectionTracks; +} + @HostApi() abstract class VideoPlayerInstanceApi { @ObjCSelector('setLooping:') @@ -89,8 +144,19 @@ abstract class VideoPlayerInstanceApi { void seekTo(int position); void pause(); void dispose(); + @ObjCSelector('getAudioTracks') List getAudioTracks(); @ObjCSelector('selectAudioTrackAtIndex:') void selectAudioTrack(int trackIndex); + + /// Gets the available video tracks for the video. + @async + @ObjCSelector('getVideoTracks') + NativeVideoTrackData getVideoTracks(); + + /// Selects a video track by setting preferredPeakBitRate. + /// Pass 0 to enable auto quality selection. + @ObjCSelector('selectVideoTrackWithBitrate:') + void selectVideoTrack(int bitrate); } diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index f14fefb73326..7b677575486f 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.9.4 +version: 2.10.0 environment: sdk: ^3.10.0 @@ -24,7 +24,7 @@ flutter: dependencies: flutter: sdk: flutter - video_player_platform_interface: ^6.6.0 + video_player_platform_interface: ^6.7.0 dev_dependencies: build_runner: ^2.3.3 diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index c16d5bcb08e9..e9109cd0df85 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -690,5 +690,179 @@ void main() { ]), ); }); + + group('video tracks', () { + test('isVideoTrackSupportAvailable returns true', () { + final (AVFoundationVideoPlayer player, _, _) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + + expect(player.isVideoTrackSupportAvailable(), true); + }); + + test('getVideoTracks returns empty list when no tracks', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when( + api.getVideoTracks(), + ).thenAnswer((_) async => NativeVideoTrackData()); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks, isEmpty); + }); + + test( + 'getVideoTracks converts HLS variant tracks to VideoTrack', + () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + mediaSelectionTracks: [ + MediaSelectionVideoTrackData( + variantIndex: 0, + label: '1080p', + isSelected: true, + bitrate: 5000000, + width: 1920, + height: 1080, + frameRate: 30.0, + codec: 'avc1', + ), + MediaSelectionVideoTrackData( + variantIndex: 1, + label: '720p', + isSelected: false, + bitrate: 2500000, + width: 1280, + height: 720, + frameRate: 30.0, + codec: 'avc1', + ), + ], + ), + ); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks.length, 2); + + expect(tracks[0].id, 'variant_5000000'); + expect(tracks[0].label, '1080p'); + expect(tracks[0].isSelected, true); + expect(tracks[0].bitrate, 5000000); + expect(tracks[0].width, 1920); + expect(tracks[0].height, 1080); + expect(tracks[0].frameRate, 30.0); + expect(tracks[0].codec, 'avc1'); + + expect(tracks[1].id, 'variant_2500000'); + expect(tracks[1].label, '720p'); + expect(tracks[1].isSelected, false); + }, + ); + + test( + 'getVideoTracks generates label from resolution if not provided', + () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.getVideoTracks()).thenAnswer( + (_) async => NativeVideoTrackData( + mediaSelectionTracks: [ + MediaSelectionVideoTrackData( + variantIndex: 0, + isSelected: true, + bitrate: 5000000, + width: 1920, + height: 1080, + ), + ], + ), + ); + + final List tracks = await player.getVideoTracks(1); + + expect(tracks.length, 1); + expect(tracks[0].label, '1080p'); + }, + ); + + test( + 'selectVideoTrack with null sets auto quality (bitrate 0)', + () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.selectVideoTrack(0)).thenAnswer((_) async {}); + + await player.selectVideoTrack(1, null); + + verify(api.selectVideoTrack(0)); + }, + ); + + test('selectVideoTrack with track uses bitrate', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + when(api.selectVideoTrack(5000000)).thenAnswer((_) async {}); + + const track = VideoTrack( + id: 'variant_5000000', + isSelected: false, + bitrate: 5000000, + ); + await player.selectVideoTrack(1, track); + + verify(api.selectVideoTrack(5000000)); + }); + + test('selectVideoTrack ignores track without bitrate', () async { + final ( + AVFoundationVideoPlayer player, + _, + MockVideoPlayerInstanceApi api, + ) = setUpMockPlayer( + playerId: 1, + textureId: 101, + ); + + const track = VideoTrack(id: 'asset_123', isSelected: false); + await player.selectVideoTrack(1, track); + + verifyNever(api.selectVideoTrack(any)); + }); + }); }); } diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart index 8caf6ad8dc43..6afa3dd8dcb9 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.mocks.dart @@ -29,6 +29,12 @@ class _FakeTexturePlayerIds_0 extends _i1.SmartFake : super(parent, parentInvocation); } +class _FakeNativeVideoTrackData_1 extends _i1.SmartFake + implements _i2.NativeVideoTrackData { + _FakeNativeVideoTrackData_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + /// A class which mocks [AVFoundationVideoPlayerApi]. /// /// See the documentation for Mockito's code generation for more information. @@ -198,4 +204,33 @@ class MockVideoPlayerInstanceApi extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + + @override + _i4.Future<_i2.NativeVideoTrackData> getVideoTracks() => + (super.noSuchMethod( + Invocation.method(#getVideoTracks, []), + returnValue: _i4.Future<_i2.NativeVideoTrackData>.value( + _FakeNativeVideoTrackData_1( + this, + Invocation.method(#getVideoTracks, []), + ), + ), + returnValueForMissingStub: + _i4.Future<_i2.NativeVideoTrackData>.value( + _FakeNativeVideoTrackData_1( + this, + Invocation.method(#getVideoTracks, []), + ), + ), + ) + as _i4.Future<_i2.NativeVideoTrackData>); + + @override + _i4.Future selectVideoTrack(int? bitrate) => + (super.noSuchMethod( + Invocation.method(#selectVideoTrack, [bitrate]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) + as _i4.Future); }