From 72b791f6612fc3f2fd7b9b1e4bf6cf2e4ed2ffdf Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 6 Feb 2026 10:47:52 -0500 Subject: [PATCH 01/16] Convert XCUITest to Swift --- .../ios/Runner.xcodeproj/project.pbxproj | 24 +++--- .../xcshareddata/xcschemes/Runner.xcscheme | 2 + .../ios/RunnerUITests/VideoPlayerUITests.m | 73 ------------------- .../RunnerUITests/VideoPlayerUITests.swift | 68 +++++++++++++++++ .../macos/Runner.xcodeproj/project.pbxproj | 12 +-- 5 files changed, 86 insertions(+), 93 deletions(-) delete mode 100644 packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.m create mode 100644 packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.swift diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index f4ad957fd95d..4bd39799f457 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,11 +3,12 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331585C52F36433100FACB51 /* VideoPlayerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331585C42F36433100FACB51 /* VideoPlayerUITests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -17,7 +18,6 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B0F5C77B94E32FB72444AE9F /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20721C28387E1F78689EC502 /* libPods-Runner.a */; }; D182ECB59C06DBC7E2D5D913 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BD232FD3BD3343A5F52AF50 /* libPods-RunnerTests.a */; }; - F7151F2F26603EBD0028CB91 /* VideoPlayerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */; }; F7151F3D26603ECA0028CB91 /* VideoPlayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */; }; /* End PBXBuildFile section */ @@ -56,6 +56,7 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 20721C28387E1F78689EC502 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2A2EA522BDC492279A91AB75 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 331585C42F36433100FACB51 /* VideoPlayerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerUITests.swift; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 6CDC4DA5940705A6E7671616 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -73,7 +74,6 @@ B15EC39F4617FE1082B18834 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; C18C242FF01156F58C0DAF1C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VideoPlayerUITests.m; sourceTree = ""; }; F7151F3026603EBD0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = VideoPlayerTests.m; path = ../../../darwin/RunnerTests/VideoPlayerTests.m; sourceTree = ""; }; @@ -189,7 +189,7 @@ F7151F2D26603EBD0028CB91 /* RunnerUITests */ = { isa = PBXGroup; children = ( - F7151F2E26603EBD0028CB91 /* VideoPlayerUITests.m */, + 331585C42F36433100FACB51 /* VideoPlayerUITests.swift */, F7151F3026603EBD0028CB91 /* Info.plist */, ); path = RunnerUITests; @@ -265,8 +265,6 @@ F7151F4026603ECA0028CB91 /* PBXTargetDependency */, ); name = RunnerTests; - packageProductDependencies = ( - ); productName = RunnerTests; productReference = F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -285,6 +283,7 @@ }; F7151F2B26603EBD0028CB91 = { CreatedOnToolsVersion = 12.5; + LastSwiftMigration = 2620; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -305,7 +304,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -354,12 +353,10 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation/path_provider_foundation_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/video_player_avfoundation/video_player_avfoundation_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/path_provider_foundation_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/video_player_avfoundation_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; @@ -455,7 +452,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F2F26603EBD0028CB91 /* VideoPlayerUITests.m in Sources */, + 331585C52F36433100FACB51 /* VideoPlayerUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -659,6 +656,7 @@ F7151F3326603EBD0028CB91 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -669,6 +667,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; TEST_TARGET_NAME = Runner; }; name = Debug; @@ -676,6 +676,7 @@ F7151F3426603EBD0028CB91 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -686,6 +687,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TEST_TARGET_NAME = Runner; }; name = Release; @@ -770,7 +772,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d7730d34dabc..6ef3fa75e7f3 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Date: Fri, 6 Feb 2026 13:30:29 -0500 Subject: [PATCH 02/16] Remove sketchy subclassing of AVAssetTrack --- .../darwin/RunnerTests/VideoPlayerTests.m | 160 +++++++----------- .../AVAssetTrackUtils.m | 4 +- .../FVPVideoPlayer.m | 3 +- .../AVAssetTrackUtils.h | 6 +- 4 files changed, 71 insertions(+), 102 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index 19e3a70d27fc..09952af5588d 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -6,49 +6,6 @@ @import video_player_avfoundation; @import XCTest; -#if TARGET_OS_IOS -@interface FakeAVAssetTrack : AVAssetTrack -@property(readonly, nonatomic) CGAffineTransform preferredTransform; -@property(readonly, nonatomic) CGSize naturalSize; -@property(readonly, nonatomic) UIImageOrientation orientation; -- (instancetype)initWithOrientation:(UIImageOrientation)orientation; -@end - -@implementation FakeAVAssetTrack - -- (instancetype)initWithOrientation:(UIImageOrientation)orientation { - _orientation = orientation; - _naturalSize = CGSizeMake(800, 600); - return self; -} - -- (CGAffineTransform)preferredTransform { - switch (_orientation) { - case UIImageOrientationUp: - return CGAffineTransformMake(1, 0, 0, 1, 0, 0); - case UIImageOrientationDown: - return CGAffineTransformMake(-1, 0, 0, -1, 0, 0); - case UIImageOrientationLeft: - return CGAffineTransformMake(0, -1, 1, 0, 0, 0); - case UIImageOrientationRight: - return CGAffineTransformMake(0, 1, -1, 0, 0, 0); - case UIImageOrientationUpMirrored: - return CGAffineTransformMake(-1, 0, 0, 1, 0, 0); - case UIImageOrientationDownMirrored: - return CGAffineTransformMake(1, 0, 0, -1, 0, 0); - case UIImageOrientationLeftMirrored: - return CGAffineTransformMake(0, -1, -1, 0, 0, 0); - case UIImageOrientationRightMirrored: - return CGAffineTransformMake(0, 1, 1, 0, 0, 0); - } -} - -@end -#endif - -@interface VideoPlayerTests : XCTestCase -@end - /// An AVPlayer subclass that records method call parameters for inspection. // TODO(stuartmorgan): Replace with a protocol like the other classes. @interface InspectableAVPlayer : AVPlayer @@ -432,6 +389,9 @@ - (void)videoPlayerWasDisposed { #pragma mark - +@interface VideoPlayerTests : XCTestCase +@end + @implementation VideoPlayerTests - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { @@ -801,15 +761,68 @@ - (void)testAudioOnlyHLSControls { } #if TARGET_OS_IOS -- (void)testTransformFix { - [self validateTransformFixForOrientation:UIImageOrientationUp]; - [self validateTransformFixForOrientation:UIImageOrientationDown]; - [self validateTransformFixForOrientation:UIImageOrientationLeft]; - [self validateTransformFixForOrientation:UIImageOrientationRight]; - [self validateTransformFixForOrientation:UIImageOrientationUpMirrored]; - [self validateTransformFixForOrientation:UIImageOrientationDownMirrored]; - [self validateTransformFixForOrientation:UIImageOrientationLeftMirrored]; - [self validateTransformFixForOrientation:UIImageOrientationRightMirrored]; +- (void)testTransformFixOrientationUp { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(1, 0, 0, 1, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, 0); + XCTAssertEqual(t.ty, 0); +} + +- (void)testTransformFixOrientationDown { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(-1, 0, 0, -1, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, size.width); + XCTAssertEqual(t.ty, size.height); +} + +- (void)testTransformFixOrientationLeft { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(0, -1, 1, 0, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, 0); + XCTAssertEqual(t.ty, size.width); +} + +- (void)testTransformFixOrientationRight { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(0, 1, -1, 0, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, size.height); + XCTAssertEqual(t.ty, 0); +} + +- (void)testTransformFixOrientationUpMirrored { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(-1, 0, 0, 1, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, size.width); + XCTAssertEqual(t.ty, 0); +} + +- (void)testTransformFixOrientationDownMirrored { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(1, 0, 0, -1, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, 0); + XCTAssertEqual(t.ty, size.height); +} + +- (void)testTransformFixOrientationLeftMirrored { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(0, -1, -1, 0, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, size.height); + XCTAssertEqual(t.ty, size.width); +} + +- (void)testTransformFixOrientationRightMirrored { + CGSize size = CGSizeMake(800, 600); + CGAffineTransform naturalTransform = CGAffineTransformMake(0, 1, 1, 0, 0, 0); + CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); + XCTAssertEqual(t.tx, 0); + XCTAssertEqual(t.ty, 0); } #endif @@ -1204,49 +1217,6 @@ - (void)testSetMixWithOthersShouldNoOpWhenNoChangesAreRequired { XCTAssertFalse(audioSession.setCategoryCalled); } - -- (void)validateTransformFixForOrientation:(UIImageOrientation)orientation { - AVAssetTrack *track = [[FakeAVAssetTrack alloc] initWithOrientation:orientation]; - CGAffineTransform t = FVPGetStandardizedTransformForTrack(track); - CGSize size = track.naturalSize; - CGFloat expectX, expectY; - switch (orientation) { - case UIImageOrientationUp: - expectX = 0; - expectY = 0; - break; - case UIImageOrientationDown: - expectX = size.width; - expectY = size.height; - break; - case UIImageOrientationLeft: - expectX = 0; - expectY = size.width; - break; - case UIImageOrientationRight: - expectX = size.height; - expectY = 0; - break; - case UIImageOrientationUpMirrored: - expectX = size.width; - expectY = 0; - break; - case UIImageOrientationDownMirrored: - expectX = 0; - expectY = size.height; - break; - case UIImageOrientationLeftMirrored: - expectX = size.height; - expectY = size.width; - break; - case UIImageOrientationRightMirrored: - expectX = 0; - expectY = 0; - break; - } - XCTAssertEqual(t.tx, expectX); - XCTAssertEqual(t.ty, expectY); -} #endif /// Returns a test URL for creating a player from a network source. diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/AVAssetTrackUtils.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/AVAssetTrackUtils.m index 11c1ff285376..d104ea422b56 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/AVAssetTrackUtils.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/AVAssetTrackUtils.m @@ -4,9 +4,7 @@ @import AVFoundation; -CGAffineTransform FVPGetStandardizedTransformForTrack(AVAssetTrack *track) { - CGAffineTransform t = track.preferredTransform; - CGSize size = track.naturalSize; +CGAffineTransform FVPGetStandardizedTrackTransform(CGAffineTransform t, CGSize size) { // Each case of control flows corresponds to a specific // `UIImageOrientation`, with 8 cases in total. if (t.a == 1 && t.b == 0 && t.c == 0 && t.d == 1) { 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 f57489cfc2b5..2270120378d5 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 @@ -90,7 +90,8 @@ - (instancetype)initWithPlayerItem:(NSObject *)item if ([videoTrack statusOfValueForKey:@"preferredTransform" error:nil] == AVKeyValueStatusLoaded) { // Rotate the video by using a videoComposition and the preferredTransform - self->_preferredTransform = FVPGetStandardizedTransformForTrack(videoTrack); + self->_preferredTransform = FVPGetStandardizedTrackTransform( + videoTrack.preferredTransform, videoTrack.naturalSize); // Do not use video composition when it is not needed. if (CGAffineTransformIsIdentity(self->_preferredTransform)) { return; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/AVAssetTrackUtils.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/AVAssetTrackUtils.h index 19086b10e430..703a24abce2a 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/AVAssetTrackUtils.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/AVAssetTrackUtils.h @@ -4,9 +4,9 @@ @import AVFoundation; -/// Returns a standardized transform -/// according to the orientation of the track. +/// Returns a standardized transform according to the orientation of a track with the given +/// information. /// /// Note: https://stackoverflow.com/questions/64161544 /// `AVAssetTrack.preferredTransform` can have wrong `tx` and `ty`. -CGAffineTransform FVPGetStandardizedTransformForTrack(AVAssetTrack *track); +CGAffineTransform FVPGetStandardizedTrackTransform(CGAffineTransform t, CGSize size); From 87c50711a88b37fe5c049bfc8fec28bc3069cacc Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 6 Feb 2026 13:31:02 -0500 Subject: [PATCH 03/16] Convert unit tests to Swift, mostly directly --- .../darwin/RunnerTests/TestClasses.swift | 286 ++++ .../darwin/RunnerTests/VideoPlayerTests.m | 1396 ----------------- .../darwin/RunnerTests/VideoPlayerTests.swift | 952 +++++++++++ .../FVPTextureBasedVideoPlayer_Test.h | 2 +- .../FVPVideoPlayerPlugin.h | 4 + .../FVPVideoPlayerPlugin_Test.h | 4 + .../ios/Runner.xcodeproj/project.pbxproj | 24 +- 7 files changed, 1264 insertions(+), 1404 deletions(-) create mode 100644 packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift delete mode 100644 packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m create mode 100644 packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift new file mode 100644 index 000000000000..d28dabf59d6a --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift @@ -0,0 +1,286 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import XCTest +import video_player_avfoundation + +#if os(iOS) + import Flutter + import UIKit +#else + import FlutterMacOS +#endif + +/// An AVPlayer subclass that records method call parameters for inspection. +// TODO(stuartmorgan): Replace with a protocol like the other classes. +@MainActor class InspectableAVPlayer: AVPlayer { + private(set) nonisolated(unsafe) var beforeTolerance: NSNumber? + private(set) nonisolated(unsafe) var afterTolerance: NSNumber? + private(set) nonisolated(unsafe) var lastSeekTime: CMTime = .invalid + + override func seek( + to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime, + completionHandler: @escaping @Sendable (Bool) -> Void + ) { + beforeTolerance = NSNumber(value: toleranceBefore.value) + afterTolerance = NSNumber(value: toleranceAfter.value) + lastSeekTime = time + super.seek( + to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter, + completionHandler: completionHandler) + } +} + +class TestAsset: NSObject, FVPAVAsset { + let duration: CMTime + let tracks: [AVAssetTrack]? + + var loadedTracksAsynchronously = false + + init(duration: CMTime = CMTime.zero, tracks: [AVAssetTrack]? = nil) { + self.duration = duration + self.tracks = tracks + super.init() + } + + func statusOfValue(forKey key: String, error outError: NSErrorPointer) -> AVKeyValueStatus { + return tracks == nil ? .loading : .loaded + } + + func loadValuesAsynchronously(forKeys keys: [String], completionHandler handler: (() -> Void)?) { + handler?() + } + + @available(macOS 12.0, iOS 15.0, *) + func loadTracks( + withMediaType mediaType: AVMediaType, + completionHandler: @escaping ([AVAssetTrack]?, Error?) -> Void + ) { + loadedTracksAsynchronously = true + completionHandler(tracks, nil) + } + + func tracks(withMediaType mediaType: AVMediaType) -> [AVAssetTrack] { + return tracks ?? [] + } +} + +class StubPlayerItem: NSObject, FVPAVPlayerItem { + let asset: FVPAVAsset + var videoComposition: AVVideoComposition? + + init(asset: FVPAVAsset = TestAsset()) { + self.asset = asset + super.init() + } +} + +class StubBinaryMessenger: NSObject, FlutterBinaryMessenger { + func send(onChannel channel: String, message: Data?) {} + func send( + onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil + ) {} + func setMessageHandlerOnChannel( + _ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil + ) -> FlutterBinaryMessengerConnection { + return 0 + } + func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} +} + +class TestTextureRegistry: NSObject, FlutterTextureRegistry { + private(set) var registeredTexture = false + private(set) var unregisteredTexture = false + private(set) var textureFrameAvailableCount = 0 + + func register(_ texture: FlutterTexture) -> Int64 { + registeredTexture = true + return 1 + } + + func unregisterTexture(_ textureId: Int64) { + if textureId != 1 { + XCTFail("Unregistering texture with wrong ID") + } + unregisteredTexture = true + } + + func textureFrameAvailable(_ textureId: Int64) { + if textureId != 1 { + XCTFail("Texture frame available with wrong ID") + } + textureFrameAvailableCount += 1 + } +} + +class StubViewProvider: NSObject, FVPViewProvider { + #if os(iOS) + var viewController: UIViewController? + init(viewController: UIViewController? = nil) { + self.viewController = viewController + super.init() + } + #else + var view: NSView? + init(view: NSView? = nil) { + self.view = view + super.init() + } + #endif +} + +class StubAssetProvider: NSObject, FVPAssetProvider { + func lookupKey(forAsset asset: String) -> String? { + return asset + } + + func lookupKey(forAsset asset: String, fromPackage package: String) -> String? { + return asset + } +} + +class TestPixelBufferSource: NSObject, FVPPixelBufferSource { + var pixelBuffer: CVPixelBuffer? + let videoOutput: AVPlayerItemVideoOutput + + override init() { + videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, + kCVPixelBufferIOSurfacePropertiesKey as String: [:] as [String: String], + ]) + super.init() + } + + func itemTime(forHostTime hostTimeInSeconds: CFTimeInterval) -> CMTime { + return CMTimeMakeWithSeconds(hostTimeInSeconds, preferredTimescale: 1000) + } + + func hasNewPixelBuffer(forItemTime itemTime: CMTime) -> Bool { + return pixelBuffer != nil + } + + func copyPixelBuffer( + forItemTime itemTime: CMTime, itemTimeForDisplay: UnsafeMutablePointer? + ) -> CVPixelBuffer? { + let buffer = pixelBuffer + // Ownership is transferred to the caller. + pixelBuffer = nil + return buffer + } +} + +#if os(iOS) + class TestAudioSession: NSObject, FVPAVAudioSession { + var category: AVAudioSession.Category = .ambient + var categoryOptions: AVAudioSession.CategoryOptions = [] + private(set) var setCategoryCalled = false + + func setCategory( + _ category: AVAudioSession.Category, + with options: AVAudioSession.CategoryOptions + ) throws { + setCategoryCalled = true + self.category = category + self.categoryOptions = options + } + } +#endif + +class StubFVPAVFactory: NSObject, FVPAVFactory { + let player: AVPlayer + let playerItem: FVPAVPlayerItem + let pixelBufferSource: FVPPixelBufferSource? + #if os(iOS) + var audioSession: FVPAVAudioSession + #endif + + init( + player: AVPlayer? = nil, + playerItem: FVPAVPlayerItem? = nil, + pixelBufferSource: FVPPixelBufferSource? = nil + ) { + let dummyURL = URL(string: "https://flutter.dev")! + self.player = + player + ?? AVPlayer(playerItem: AVPlayerItem(url: dummyURL)) + self.playerItem = playerItem ?? StubPlayerItem() + self.pixelBufferSource = pixelBufferSource + #if os(iOS) + self.audioSession = TestAudioSession() + #endif + super.init() + } + + func urlAsset(with url: URL, options: [String: Any]?) -> FVPAVAsset { + return playerItem.asset + } + + func playerItem(with asset: FVPAVAsset) -> FVPAVPlayerItem { + return playerItem + } + + func player(with playerItem: FVPAVPlayerItem) -> AVPlayer { + return self.player + } + + func videoOutput(pixelBufferAttributes attributes: [String: Any]) -> FVPPixelBufferSource { + return pixelBufferSource ?? TestPixelBufferSource() + } + + #if os(iOS) + func sharedAudioSession() -> FVPAVAudioSession { + return audioSession + } + #endif +} + +class StubFVPDisplayLink: NSObject, FVPDisplayLink { + var running: Bool = false + var duration: CFTimeInterval { + return 1.0 / 60.0 + } +} + +class StubFVPDisplayLinkFactory: NSObject, FVPDisplayLinkFactory { + let displayLink = StubFVPDisplayLink() + var fireDisplayLink: (() -> Void)? + + func displayLink(with viewProvider: FVPViewProvider, callback: @escaping () -> Void) + -> FVPDisplayLink + { + fireDisplayLink = callback + return displayLink + } +} + +class StubEventListener: NSObject, FVPVideoEventListener { + var initializationExpectation: XCTestExpectation? + private(set) var initializationDuration: Int64 = 0 + private(set) var initializationSize: CGSize = .zero + + init(initializationExpectation: XCTestExpectation? = nil) { + self.initializationExpectation = initializationExpectation + super.init() + } + + func videoPlayerDidComplete() {} + func videoPlayerDidEndBuffering() {} + func videoPlayerDidError(withMessage errorMessage: String) {} + func videoPlayerDidInitialize(withDuration duration: Int64, size: CGSize) { + initializationExpectation?.fulfill() + initializationDuration = duration + initializationSize = size + } + func videoPlayerDidSetPlaying(_ playing: Bool) {} + func videoPlayerDidStartBuffering() {} + func videoPlayerDidUpdateBufferRegions(_ regions: [[NSNumber]]!) {} + func videoPlayerWasDisposed() {} +} + +class StubTexture: NSObject, FlutterTexture { + func copyPixelBuffer() -> Unmanaged? { + return nil + } +} diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m deleted file mode 100644 index 09952af5588d..000000000000 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ /dev/null @@ -1,1396 +0,0 @@ -// Copyright 2013 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import AVFoundation; -@import video_player_avfoundation; -@import XCTest; - -/// An AVPlayer subclass that records method call parameters for inspection. -// TODO(stuartmorgan): Replace with a protocol like the other classes. -@interface InspectableAVPlayer : AVPlayer -@property(readonly, nonatomic) NSNumber *beforeTolerance; -@property(readonly, nonatomic) NSNumber *afterTolerance; -@property(readonly, assign) CMTime lastSeekTime; -@end - -@implementation InspectableAVPlayer - -- (void)seekToTime:(CMTime)time - toleranceBefore:(CMTime)toleranceBefore - toleranceAfter:(CMTime)toleranceAfter - completionHandler:(void (^)(BOOL finished))completionHandler { - _beforeTolerance = [NSNumber numberWithLong:toleranceBefore.value]; - _afterTolerance = [NSNumber numberWithLong:toleranceAfter.value]; - _lastSeekTime = time; - [super seekToTime:time - toleranceBefore:toleranceBefore - toleranceAfter:toleranceAfter - completionHandler:completionHandler]; -} - -@end - -@interface TestAsset : NSObject -@property(nonatomic, readonly) CMTime duration; -@property(nonatomic, nullable, readonly) NSArray *tracks; - -@property(nonatomic, assign) BOOL loadedTracksAsynchronously; -@end - -@implementation TestAsset -- (instancetype)init { - return [self initWithDuration:kCMTimeZero tracks:nil]; -} - -- (instancetype)initWithDuration:(CMTime)duration - tracks:(nullable NSArray *)tracks { - self = [super init]; - _duration = duration; - _tracks = tracks; - return self; -} - -- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key - error:(NSError *_Nullable *_Nullable)outError { - return self.tracks == nil ? AVKeyValueStatusLoading : AVKeyValueStatusLoaded; -} - -- (void)loadValuesAsynchronouslyForKeys:(NSArray *)keys - completionHandler:(nullable void (^NS_SWIFT_SENDABLE)(void))handler { - if (handler) { - handler(); - } -} - -- (void)loadTracksWithMediaType:(AVMediaType)mediaType - completionHandler:(void (^NS_SWIFT_SENDABLE)(NSArray *_Nullable, - NSError *_Nullable))completionHandler - API_AVAILABLE(macos(12.0), ios(15.0)) { - self.loadedTracksAsynchronously = YES; - completionHandler(_tracks, nil); -} - -- (NSArray *)tracksWithMediaType:(AVMediaType)mediaType - API_DEPRECATED("Use loadTracksWithMediaType:completionHandler: instead", macos(10.7, 15.0), - ios(4.0, 18.0)) { - return _tracks ?: @[]; -} -@end - -@interface StubPlayerItem : NSObject -@property(nonatomic, readonly) NSObject *asset; -@property(nonatomic, copy, nullable) AVVideoComposition *videoComposition; -@end - -@implementation StubPlayerItem -- (instancetype)init { - return [self initWithAsset:[[TestAsset alloc] init]]; -} - -- (instancetype)initWithAsset:(NSObject *)asset { - self = [super init]; - _asset = asset; - return self; -} -@end - -@interface StubBinaryMessenger : NSObject -@end - -@implementation StubBinaryMessenger - -- (void)sendOnChannel:(NSString *)channel message:(NSData *_Nullable)message { -} -- (void)sendOnChannel:(NSString *)channel - message:(NSData *_Nullable)message - binaryReply:(FlutterBinaryReply _Nullable)callback { -} -- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString *)channel - binaryMessageHandler: - (FlutterBinaryMessageHandler _Nullable)handler { - return 0; -} -- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { -} -@end - -@interface TestTextureRegistry : NSObject -@property(nonatomic, assign) BOOL registeredTexture; -@property(nonatomic, assign) BOOL unregisteredTexture; -@property(nonatomic, assign) int textureFrameAvailableCount; -@end - -@implementation TestTextureRegistry -- (int64_t)registerTexture:(NSObject *)texture { - self.registeredTexture = true; - return 1; -} - -- (void)unregisterTexture:(int64_t)textureId { - if (textureId != 1) { - XCTFail(@"Unregistering texture with wrong ID"); - } - self.unregisteredTexture = true; -} - -- (void)textureFrameAvailable:(int64_t)textureId { - if (textureId != 1) { - XCTFail(@"Texture frame available with wrong ID"); - } - self.textureFrameAvailableCount++; -} -@end - -@interface StubViewProvider : NSObject -#if TARGET_OS_IOS -- (instancetype)initWithViewController:(UIViewController *)viewController; -@property(nonatomic, nullable) UIViewController *viewController; -#else -- (instancetype)initWithView:(NSView *)view; -@property(nonatomic, nullable) NSView *view; -#endif -@end - -@implementation StubViewProvider -#if TARGET_OS_IOS -- (instancetype)initWithViewController:(UIViewController *)viewController { - self = [super init]; - _viewController = viewController; - return self; -} -#else -- (instancetype)initWithView:(NSView *)view { - self = [super init]; - _view = view; - return self; -} -#endif -@end - -@interface StubAssetProvider : NSObject -@end - -@implementation StubAssetProvider -- (NSString *)lookupKeyForAsset:(NSString *)asset { - return asset; -} - -- (NSString *)lookupKeyForAsset:(NSString *)asset fromPackage:(NSString *)package { - return asset; -} -@end - -@interface TestPixelBufferSource : NSObject -@property(nonatomic) CVPixelBufferRef pixelBuffer; -@property(nonatomic, readonly) AVPlayerItemVideoOutput *videoOutput; -@end - -@implementation TestPixelBufferSource -- (instancetype)init { - self = [super init]; - // Create an arbitrary video output to for attaching to actual AVFoundation - // objects. The attributes don't matter since this isn't used to implement - // the methods called by the plugin. - _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:@{ - (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), - (id)kCVPixelBufferIOSurfacePropertiesKey : @{} - }]; - return self; -} - -- (void)dealloc { - CVPixelBufferRelease(_pixelBuffer); -} - -- (void)setPixelBuffer:(CVPixelBufferRef)pixelBuffer { - CVPixelBufferRelease(_pixelBuffer); - _pixelBuffer = CVPixelBufferRetain(pixelBuffer); -} - -- (CMTime)itemTimeForHostTime:(CFTimeInterval)hostTimeInSeconds { - return CMTimeMakeWithSeconds(hostTimeInSeconds, 1000); -} - -- (BOOL)hasNewPixelBufferForItemTime:(CMTime)itemTime { - return _pixelBuffer != NULL; -} - -- (nullable CVPixelBufferRef)copyPixelBufferForItemTime:(CMTime)itemTime - itemTimeForDisplay:(nullable CMTime *)outItemTimeForDisplay { - CVPixelBufferRef pixelBuffer = _pixelBuffer; - // Ownership is transferred to the caller. - _pixelBuffer = NULL; - return pixelBuffer; -} -@end - -#if TARGET_OS_IOS -@interface TestAudioSession : NSObject -@property(nonatomic, readwrite) AVAudioSessionCategory category; -@property(nonatomic, assign) AVAudioSessionCategoryOptions categoryOptions; - -/// Tracks whether setCategory:withOptions:error: has been called. -@property(nonatomic, assign) BOOL setCategoryCalled; -@end - -@implementation TestAudioSession -- (BOOL)setCategory:(AVAudioSessionCategory)category - withOptions:(AVAudioSessionCategoryOptions)options - error:(NSError **)outError { - self.setCategoryCalled = YES; - self.category = category; - self.categoryOptions = options; - return YES; -} -@end -#endif - -@interface StubFVPAVFactory : NSObject - -@property(nonatomic, strong) AVPlayer *player; -@property(nonatomic, strong) NSObject *playerItem; -@property(nonatomic, strong) NSObject *pixelBufferSource; -#if TARGET_OS_IOS -@property(nonatomic, strong) NSObject *audioSession; -#endif - -@end - -@implementation StubFVPAVFactory - -// Creates a factory that returns the given items. Any items that are nil will instead return -// a real object just as the non-test implementation would. -- (instancetype)initWithPlayer:(nullable AVPlayer *)player - playerItem:(nullable NSObject *)playerItem - pixelBufferSource:(nullable NSObject *)pixelBufferSource { - self = [super init]; - // Create a player with a dummy item so that the player is valid, since most tests won't work - // without a valid player. - // TODO(stuartmorgan): Introduce a protocol for AVPlayer and use a stub here instead. - NSURL *dummyURL = [NSURL URLWithString:@""]; - _player = - player ?: [[AVPlayer alloc] initWithPlayerItem:[AVPlayerItem playerItemWithURL:dummyURL]]; - _playerItem = playerItem ?: [[StubPlayerItem alloc] init]; - _pixelBufferSource = pixelBufferSource; -#if TARGET_OS_IOS - _audioSession = [[TestAudioSession alloc] init]; -#endif - return self; -} - -- (NSObject *)URLAssetWithURL:(NSURL *)URL - options:(nullable NSDictionary *)options { - return self.playerItem.asset; -} - -- (NSObject *)playerItemWithAsset:(NSObject *)asset { - return self.playerItem; -} - -- (AVPlayer *)playerWithPlayerItem:(NSObject *)playerItem { - return self.player; -} - -- (NSObject *)videoOutputWithPixelBufferAttributes: - (NSDictionary *)attributes { - return self.pixelBufferSource ?: [[TestPixelBufferSource alloc] init]; -} - -#if TARGET_OS_IOS -- (NSObject *)sharedAudioSession { - return self.audioSession; -} -#endif - -@end - -#pragma mark - - -@interface StubFVPDisplayLink : NSObject -@property(nonatomic, assign) BOOL running; -@end - -@implementation StubFVPDisplayLink -- (CFTimeInterval)duration { - return 1.0 / 60.0; -} -@end - -/** Test implementation of FVPDisplayLinkFactory that returns a stub display link instance. */ -@interface StubFVPDisplayLinkFactory : NSObject -/** This display link to return. */ -@property(nonatomic, strong) StubFVPDisplayLink *displayLink; -@property(nonatomic, copy) void (^fireDisplayLink)(void); -@end - -@implementation StubFVPDisplayLinkFactory -- (instancetype)init { - self = [super init]; - _displayLink = [[StubFVPDisplayLink alloc] init]; - return self; -} -- (NSObject *)displayLinkWithViewProvider:(NSObject *)viewProvider - callback:(void (^)(void))callback { - self.fireDisplayLink = callback; - return self.displayLink; -} - -@end - -#pragma mark - - -@interface StubEventListener : NSObject - -@property(nonatomic) XCTestExpectation *initializationExpectation; -@property(nonatomic) int64_t initializationDuration; -@property(nonatomic) CGSize initializationSize; - -- (instancetype)initWithInitializationExpectation:(XCTestExpectation *)expectation; - -@end - -@implementation StubEventListener - -- (instancetype)initWithInitializationExpectation:(XCTestExpectation *)expectation { - self = [super init]; - _initializationExpectation = expectation; - return self; -} - -- (void)videoPlayerDidComplete { -} - -- (void)videoPlayerDidEndBuffering { -} - -- (void)videoPlayerDidErrorWithMessage:(NSString *)errorMessage { -} - -- (void)videoPlayerDidInitializeWithDuration:(int64_t)duration size:(CGSize)size { - [self.initializationExpectation fulfill]; - self.initializationDuration = duration; - self.initializationSize = size; -} - -- (void)videoPlayerDidSetPlaying:(BOOL)playing { -} - -- (void)videoPlayerDidStartBuffering { -} - -- (void)videoPlayerDidUpdateBufferRegions:(NSArray *> *)regions { -} - -- (void)videoPlayerWasDisposed { -} - -@end - -#pragma mark - - -@interface VideoPlayerTests : XCTestCase -@end - -@implementation VideoPlayerTests - -- (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { - // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 - // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some - // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An - // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams - // for issue #1, and restore the correct width and height for issue #2. -#if TARGET_OS_OSX - NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 10, 10)]; - view.wantsLayer = true; - id viewProvider = [[StubViewProvider alloc] initWithView:view]; -#else - UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; - UIViewController *viewController = [[UIViewController alloc] init]; - viewController.view = view; - id viewProvider = - [[StubViewProvider alloc] initWithViewController:viewController]; -#endif - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:nil] - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:viewProvider - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - httpHeaders:@{}]; - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - XCTAssertNil(error); - XCTAssertNotNil(identifiers); - FVPTextureBasedVideoPlayer *player = - (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - XCTAssertNotNil(player); - - XCTAssertNotNil(player.playerLayer, @"AVPlayerLayer should be present."); - XCTAssertEqual(player.playerLayer.superlayer, view.layer, - @"AVPlayerLayer should be added on screen."); -} - -- (void)testPlayerForPlatformViewDoesNotRegisterTexture { - TestTextureRegistry *textureRegistry = [[TestTextureRegistry alloc] init]; - StubFVPDisplayLinkFactory *stubDisplayLinkFactory = [[StubFVPDisplayLinkFactory alloc] init]; - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:nil] - displayLinkFactory:stubDisplayLinkFactory - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:textureRegistry - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *initializationError; - [videoPlayerPlugin initialize:&initializationError]; - XCTAssertNil(initializationError); - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" - httpHeaders:@{}]; - FlutterError *createError; - [videoPlayerPlugin createPlatformViewPlayerWithOptions:create error:&createError]; - - XCTAssertFalse(textureRegistry.registeredTexture); -} - -- (void)testSeekToWhilePausedStartsDisplayLinkTemporarily { - StubFVPDisplayLinkFactory *stubDisplayLinkFactory = [[StubFVPDisplayLinkFactory alloc] init]; - TestPixelBufferSource *mockVideoOutput = [[TestPixelBufferSource alloc] init]; - // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create - // the player via the plugin instead of directly to include that logic in the test. - FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] - initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:mockVideoOutput] - displayLinkFactory:stubDisplayLinkFactory - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *initializationError; - [videoPlayerPlugin initialize:&initializationError]; - XCTAssertNil(initializationError); - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" - httpHeaders:@{}]; - FlutterError *createError; - FVPTexturePlayerIds *identifiers = - [videoPlayerPlugin createTexturePlayerWithOptions:create error:&createError]; - FVPTextureBasedVideoPlayer *player = - (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - - // Ensure that the video playback is paused before seeking. - FlutterError *pauseError; - [player pauseWithError:&pauseError]; - - XCTestExpectation *seekExpectation = [self expectationWithDescription:@"seekTo completes"]; - [player seekTo:1234 - completion:^(FlutterError *_Nullable error) { - [seekExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - - // Seeking to a new position should start the display link temporarily. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running); - - // Simulate a buffer being available. - CVPixelBufferRef bufferRef; - CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef); - mockVideoOutput.pixelBuffer = bufferRef; - CVPixelBufferRelease(bufferRef); - // Simulate a callback from the engine to request a new frame. - stubDisplayLinkFactory.fireDisplayLink(); - CFRelease([player copyPixelBuffer]); - // Since a frame was found, and the video is paused, the display link should be paused again. - XCTAssertFalse(stubDisplayLinkFactory.displayLink.running); -} - -- (void)testInitStartsDisplayLinkTemporarily { - StubFVPDisplayLinkFactory *stubDisplayLinkFactory = [[StubFVPDisplayLinkFactory alloc] init]; - TestPixelBufferSource *mockVideoOutput = [[TestPixelBufferSource alloc] init]; - FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] - initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:mockVideoOutput] - displayLinkFactory:stubDisplayLinkFactory - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *initializationError; - [videoPlayerPlugin initialize:&initializationError]; - XCTAssertNil(initializationError); - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" - httpHeaders:@{}]; - FlutterError *createError; - FVPTexturePlayerIds *identifiers = - [videoPlayerPlugin createTexturePlayerWithOptions:create error:&createError]; - - // Init should start the display link temporarily. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running); - - // Simulate a buffer being available. - CVPixelBufferRef bufferRef; - CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef); - mockVideoOutput.pixelBuffer = bufferRef; - CVPixelBufferRelease(bufferRef); - // Simulate a callback from the engine to request a new frame. - FVPTextureBasedVideoPlayer *player = - (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - stubDisplayLinkFactory.fireDisplayLink(); - CFRelease([player copyPixelBuffer]); - // Since a frame was found, and the video is paused, the display link should be paused again. - XCTAssertFalse(stubDisplayLinkFactory.displayLink.running); -} - -- (void)testSeekToWhilePlayingDoesNotStopDisplayLink { - StubFVPDisplayLinkFactory *stubDisplayLinkFactory = [[StubFVPDisplayLinkFactory alloc] init]; - TestPixelBufferSource *mockVideoOutput = [[TestPixelBufferSource alloc] init]; - // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create - // the player via the plugin instead of directly to include that logic in the test. - FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] - initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:mockVideoOutput] - displayLinkFactory:stubDisplayLinkFactory - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *initializationError; - [videoPlayerPlugin initialize:&initializationError]; - XCTAssertNil(initializationError); - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" - httpHeaders:@{}]; - FlutterError *createError; - FVPTexturePlayerIds *identifiers = - [videoPlayerPlugin createTexturePlayerWithOptions:create error:&createError]; - FVPTextureBasedVideoPlayer *player = - (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - - // Ensure that the video is playing before seeking. - FlutterError *playError; - [player playWithError:&playError]; - - XCTestExpectation *seekExpectation = [self expectationWithDescription:@"seekTo completes"]; - [player seekTo:1234 - completion:^(FlutterError *_Nullable error) { - [seekExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running); - - // Simulate a buffer being available. - CVPixelBufferRef bufferRef; - CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef); - mockVideoOutput.pixelBuffer = bufferRef; - CVPixelBufferRelease(bufferRef); - // Simulate a callback from the engine to request a new frame. - stubDisplayLinkFactory.fireDisplayLink(); - CFRelease([player copyPixelBuffer]); - // Since the video was playing, the display link should not be paused after getting a buffer. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running); -} - -- (void)testPauseWhileWaitingForFrameDoesNotStopDisplayLink { - StubFVPDisplayLinkFactory *stubDisplayLinkFactory = [[StubFVPDisplayLinkFactory alloc] init]; - // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create - // the player via the plugin instead of directly to include that logic in the test. - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:nil] - displayLinkFactory:stubDisplayLinkFactory - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *initializationError; - [videoPlayerPlugin initialize:&initializationError]; - XCTAssertNil(initializationError); - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" - httpHeaders:@{}]; - FlutterError *createError; - FVPTexturePlayerIds *identifiers = - [videoPlayerPlugin createTexturePlayerWithOptions:create error:&createError]; - FVPTextureBasedVideoPlayer *player = - (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - - // Run a play/pause cycle to force the pause codepath to run completely. - FlutterError *playPauseError; - [player playWithError:&playPauseError]; - [player pauseWithError:&playPauseError]; - - // Since a buffer hasn't been available yet, the pause should not have stopped the display link. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running); -} - -- (void)testDeregistersFromPlayer { - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:nil] - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - httpHeaders:@{}]; - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - XCTAssertNil(error); - XCTAssertNotNil(identifiers); - FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - XCTAssertNotNil(player); - - [player disposeWithError:&error]; - XCTAssertEqual(videoPlayerPlugin.playersByIdentifier.count, 0); - XCTAssertNil(error); -} - -- (void)testBufferingStateFromPlayer { - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:realObjectFactory - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - httpHeaders:@{}]; - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - XCTAssertNil(error); - XCTAssertNotNil(identifiers); - FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - XCTAssertNotNil(player); - AVPlayer *avPlayer = player.player; - [avPlayer play]; - - // TODO(stuartmorgan): Update this test to instead use a mock listener, and add separate unit - // tests of FVPEventBridge. - [(NSObject *)player.eventListener - onListenWithArguments:nil - eventSink:^(NSDictionary *event) { - if ([event[@"event"] isEqualToString:@"bufferingEnd"]) { - XCTAssertTrue(avPlayer.currentItem.isPlaybackLikelyToKeepUp); - } - - if ([event[@"event"] isEqualToString:@"bufferingStart"]) { - XCTAssertFalse(avPlayer.currentItem.isPlaybackLikelyToKeepUp); - } - }]; - XCTestExpectation *bufferingStateExpectation = - [self expectationWithDescription:@"bufferingState"]; - NSTimeInterval timeout = 10; - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC); - dispatch_after(delay, dispatch_get_main_queue(), ^{ - [bufferingStateExpectation fulfill]; - }); - [self waitForExpectationsWithTimeout:timeout + 1 handler:nil]; -} - -- (void)testVideoControls { - StubEventListener *eventListener = - [self sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"]; - XCTAssertEqual(eventListener.initializationSize.height, 720); - XCTAssertEqual(eventListener.initializationSize.width, 1280); - XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 4000, 200); -} - -- (void)testAudioControls { - StubEventListener *eventListener = [self - sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3"]; - XCTAssertEqual(eventListener.initializationSize.height, 0); - XCTAssertEqual(eventListener.initializationSize.width, 0); - // Perfect precision not guaranteed. - XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 5400, 200); -} - -- (void)testHLSControls { - StubEventListener *eventListener = [self - sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"]; - XCTAssertEqual(eventListener.initializationSize.height, 720); - XCTAssertEqual(eventListener.initializationSize.width, 1280); - XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 4000, 200); -} - -- (void)testAudioOnlyHLSControls { - XCTSkip(@"Flaky; see https://github.com/flutter/flutter/issues/164381"); - - StubEventListener *eventListener = - [self sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/" - @"bee_audio_only.m3u8"]; - XCTAssertEqual(eventListener.initializationSize.height, 0); - XCTAssertEqual(eventListener.initializationSize.width, 0); - XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 4000, 200); -} - -#if TARGET_OS_IOS -- (void)testTransformFixOrientationUp { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(1, 0, 0, 1, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, 0); - XCTAssertEqual(t.ty, 0); -} - -- (void)testTransformFixOrientationDown { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(-1, 0, 0, -1, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, size.width); - XCTAssertEqual(t.ty, size.height); -} - -- (void)testTransformFixOrientationLeft { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(0, -1, 1, 0, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, 0); - XCTAssertEqual(t.ty, size.width); -} - -- (void)testTransformFixOrientationRight { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(0, 1, -1, 0, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, size.height); - XCTAssertEqual(t.ty, 0); -} - -- (void)testTransformFixOrientationUpMirrored { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(-1, 0, 0, 1, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, size.width); - XCTAssertEqual(t.ty, 0); -} - -- (void)testTransformFixOrientationDownMirrored { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(1, 0, 0, -1, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, 0); - XCTAssertEqual(t.ty, size.height); -} - -- (void)testTransformFixOrientationLeftMirrored { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(0, -1, -1, 0, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, size.height); - XCTAssertEqual(t.ty, size.width); -} - -- (void)testTransformFixOrientationRightMirrored { - CGSize size = CGSizeMake(800, 600); - CGAffineTransform naturalTransform = CGAffineTransformMake(0, 1, 1, 0, 0, 0); - CGAffineTransform t = FVPGetStandardizedTrackTransform(naturalTransform, size); - XCTAssertEqual(t.tx, 0); - XCTAssertEqual(t.ty, 0); -} -#endif - -- (void)testSeekToleranceWhenNotSeekingToEnd { - InspectableAVPlayer *inspectableAVPlayer = [[InspectableAVPlayer alloc] init]; - StubFVPAVFactory *stubAVFactory = [[StubFVPAVFactory alloc] initWithPlayer:inspectableAVPlayer - playerItem:nil - pixelBufferSource:nil]; - FVPVideoPlayer *player = - [[FVPVideoPlayer alloc] initWithPlayerItem:[[StubPlayerItem alloc] init] - avFactory:stubAVFactory - viewProvider:[[StubViewProvider alloc] init]]; - NSObject *listener = [[StubEventListener alloc] init]; - player.eventListener = listener; - - XCTestExpectation *seekExpectation = - [self expectationWithDescription:@"seekTo has zero tolerance when seeking not to end"]; - [player seekTo:1234 - completion:^(FlutterError *_Nullable error) { - [seekExpectation fulfill]; - }]; - - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - XCTAssertEqual([inspectableAVPlayer.beforeTolerance intValue], 0); - XCTAssertEqual([inspectableAVPlayer.afterTolerance intValue], 0); -} - -- (void)testSeekToleranceWhenSeekingToEnd { - InspectableAVPlayer *inspectableAVPlayer = [[InspectableAVPlayer alloc] init]; - StubFVPAVFactory *stubAVFactory = [[StubFVPAVFactory alloc] initWithPlayer:inspectableAVPlayer - playerItem:nil - pixelBufferSource:nil]; - FVPVideoPlayer *player = - [[FVPVideoPlayer alloc] initWithPlayerItem:[[StubPlayerItem alloc] init] - avFactory:stubAVFactory - viewProvider:[[StubViewProvider alloc] init]]; - NSObject *listener = [[StubEventListener alloc] init]; - player.eventListener = listener; - - XCTestExpectation *seekExpectation = - [self expectationWithDescription:@"seekTo has non-zero tolerance when seeking to end"]; - // The duration of this video is "0" due to the non standard initiliatazion process. - [player seekTo:0 - completion:^(FlutterError *_Nullable error) { - [seekExpectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - XCTAssertGreaterThan([inspectableAVPlayer.beforeTolerance intValue], 0); - XCTAssertGreaterThan([inspectableAVPlayer.afterTolerance intValue], 0); -} - -/// Sanity checks a video player playing the given URL with the actual AVPlayer. This is essentially -/// a mini integration test of the player component. -/// -/// Returns the stub event listener to allow tests to inspect the call state. -- (StubEventListener *)sanityTestURI:(NSString *)testURI { - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - NSURL *testURL = [NSURL URLWithString:testURI]; - XCTAssertNotNil(testURL); - FVPVideoPlayer *player = [[FVPVideoPlayer alloc] - initWithPlayerItem:[self playerItemWithURL:testURL factory:realObjectFactory] - avFactory:realObjectFactory - viewProvider:[[StubViewProvider alloc] init]]; - XCTAssertNotNil(player); - - XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; - StubEventListener *listener = - [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; - player.eventListener = listener; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - - // Starts paused. - AVPlayer *avPlayer = player.player; - XCTAssertEqual(avPlayer.rate, 0); - XCTAssertEqual(avPlayer.volume, 1); - XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusPaused); - - // Change playback speed. - FlutterError *error; - [player setPlaybackSpeed:2 error:&error]; - XCTAssertNil(error); - [player playWithError:&error]; - XCTAssertNil(error); - XCTAssertEqual(avPlayer.rate, 2); - XCTAssertEqual(avPlayer.timeControlStatus, AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate); - - // Volume - [player setVolume:0.1 error:&error]; - XCTAssertNil(error); - XCTAssertEqual(avPlayer.volume, 0.1f); - - return listener; -} - -// Checks whether [AVPlayer rate] KVO observations are correctly detached. -// - https://github.com/flutter/flutter/issues/124937 -// -// Failing to de-register results in a crash in [AVPlayer willChangeValueForKey:]. -- (void)testDoesNotCrashOnRateObservationAfterDisposal { - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - - AVPlayer *avPlayer = nil; - __weak FVPVideoPlayer *weakPlayer = nil; - - // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. - @autoreleasepool { - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:realObjectFactory - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - httpHeaders:@{}]; - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - XCTAssertNil(error); - XCTAssertNotNil(identifiers); - - FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - XCTAssertNotNil(player); - weakPlayer = player; - avPlayer = player.player; - - [player disposeWithError:&error]; - XCTAssertNil(error); - } - - // [FVPVideoPlayerPlugin dispose:error:] selector is dispatching the [FVPVideoPlayer dispose] call - // with a 1-second delay keeping a strong reference to the player. The polling ensures the player - // was truly deallocated. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" - [self expectationForPredicate:[NSPredicate predicateWithFormat:@"self != nil"] - evaluatedWithObject:weakPlayer - handler:nil]; -#pragma clang diagnostic pop - [self waitForExpectationsWithTimeout:10.0 handler:nil]; - - [avPlayer willChangeValueForKey:@"rate"]; // No assertions needed. Lack of crash is a success. -} - -// During the hot reload: -// 1. `[FVPVideoPlayer onTextureUnregistered:]` gets called. -// 2. `[FVPVideoPlayerPlugin initialize:]` gets called. -// -// Both of these methods dispatch [FVPVideoPlayer dispose] on the main thread -// leading to a possible crash when de-registering observers twice. -- (void)testHotReloadDoesNotCrash { - __weak FVPVideoPlayer *weakPlayer = nil; - - // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. - @autoreleasepool { - FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] - initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:nil] - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - httpHeaders:@{}]; - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - XCTAssertNil(error); - XCTAssertNotNil(identifiers); - - FVPTextureBasedVideoPlayer *player = - (FVPTextureBasedVideoPlayer *) - videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - XCTAssertNotNil(player); - weakPlayer = player; - - [player onTextureUnregistered:nil]; - XCTAssertNil(error); - - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - } - - // [FVPVideoPlayerPlugin dispose:error:] selector is dispatching the [FVPVideoPlayer dispose] call - // with a 1-second delay keeping a strong reference to the player. The polling ensures the player - // was truly deallocated. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" - [self expectationForPredicate:[NSPredicate predicateWithFormat:@"self != nil"] - evaluatedWithObject:weakPlayer - handler:nil]; -#pragma clang diagnostic pop - [self waitForExpectationsWithTimeout:10.0 - handler:nil]; // No assertions needed. Lack of crash is a success. -} - -- (void)testFailedToLoadVideoEventShouldBeAlwaysSent { - // Use real objects to test a real failure flow. - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:realObjectFactory - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - FlutterError *error; - - [videoPlayerPlugin initialize:&error]; - - FVPCreationOptions *create = [FVPCreationOptions makeWithUri:@"" httpHeaders:@{}]; - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - XCTAssertNotNil(player); - - [self keyValueObservingExpectationForObject:(id)player.player.currentItem - keyPath:@"status" - expectedValue:@(AVPlayerItemStatusFailed)]; - [self waitForExpectationsWithTimeout:10.0 handler:nil]; - - XCTestExpectation *failedExpectation = [self expectationWithDescription:@"failed"]; - // TODO(stuartmorgan): Update this test to instead use a mock listener, and add separate unit - // tests of FVPEventBridge. - [(NSObject *)player.eventListener - onListenWithArguments:nil - eventSink:^(FlutterError *event) { - if ([event isKindOfClass:FlutterError.class]) { - [failedExpectation fulfill]; - } - }]; - [self waitForExpectationsWithTimeout:10.0 handler:nil]; -} - -- (void)testUpdatePlayingStateShouldNotResetRate { - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - FVPVideoPlayer *player = [[FVPVideoPlayer alloc] - initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL factory:realObjectFactory] - avFactory:realObjectFactory - viewProvider:[[StubViewProvider alloc] init]]; - - XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; - StubEventListener *listener = - [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; - player.eventListener = listener; - [self waitForExpectationsWithTimeout:10 handler:nil]; - - FlutterError *error; - [player setPlaybackSpeed:2 error:&error]; - [player playWithError:&error]; - XCTAssertEqual(player.player.rate, 2); -} - -- (void)testPlayerShouldNotDropEverySecondFrame { - TestTextureRegistry *textureRegistry = [[TestTextureRegistry alloc] init]; - - StubFVPDisplayLinkFactory *stubDisplayLinkFactory = [[StubFVPDisplayLinkFactory alloc] init]; - TestPixelBufferSource *mockVideoOutput = [[TestPixelBufferSource alloc] init]; - FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] - initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:mockVideoOutput] - displayLinkFactory:stubDisplayLinkFactory - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:textureRegistry - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - httpHeaders:@{}]; - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - NSInteger playerIdentifier = identifiers.playerId; - FVPTextureBasedVideoPlayer *player = - (FVPTextureBasedVideoPlayer *)videoPlayerPlugin.playersByIdentifier[@(playerIdentifier)]; - - void (^addFrame)(void) = ^{ - CVPixelBufferRef bufferRef; - CVPixelBufferCreate(NULL, 1, 1, kCVPixelFormatType_32BGRA, NULL, &bufferRef); - mockVideoOutput.pixelBuffer = bufferRef; - CVPixelBufferRelease(bufferRef); - }; - - addFrame(); - stubDisplayLinkFactory.fireDisplayLink(); - CFRelease([player copyPixelBuffer]); - XCTAssertEqual(textureRegistry.textureFrameAvailableCount, 1); - - addFrame(); - stubDisplayLinkFactory.fireDisplayLink(); - CFRelease([player copyPixelBuffer]); - XCTAssertEqual(textureRegistry.textureFrameAvailableCount, 2); -} - -- (void)testVideoOutputIsAddedWhenAVPlayerItemBecomesReady { - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:realObjectFactory - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - XCTAssertNil(error); - FVPCreationOptions *create = [FVPCreationOptions - makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - httpHeaders:@{}]; - - FVPTexturePlayerIds *identifiers = [videoPlayerPlugin createTexturePlayerWithOptions:create - error:&error]; - XCTAssertNil(error); - XCTAssertNotNil(identifiers); - FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[@(identifiers.playerId)]; - XCTAssertNotNil(player); - - AVPlayerItem *item = player.player.currentItem; - [self keyValueObservingExpectationForObject:(id)item - keyPath:@"status" - expectedValue:@(AVPlayerItemStatusReadyToPlay)]; - [self waitForExpectationsWithTimeout:10.0 handler:nil]; - // Video output is added as soon as the status becomes ready to play. - XCTAssertEqual(item.outputs.count, 1); -} - -#if TARGET_OS_IOS -- (void)testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker { - StubFVPAVFactory *stubFactory = [[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:nil]; - TestAudioSession *audioSession = [[TestAudioSession alloc] init]; - stubFactory.audioSession = audioSession; - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:stubFactory - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - audioSession.category = AVAudioSessionCategoryPlayAndRecord; - audioSession.categoryOptions = AVAudioSessionCategoryOptionDefaultToSpeaker; - - FlutterError *error; - [videoPlayerPlugin initialize:&error]; - [videoPlayerPlugin setMixWithOthers:true error:&error]; - XCTAssert(audioSession.category == AVAudioSessionCategoryPlayAndRecord, - @"Category should be PlayAndRecord."); - XCTAssert(audioSession.categoryOptions & AVAudioSessionCategoryOptionDefaultToSpeaker, - @"Flag DefaultToSpeaker was removed."); - XCTAssert(audioSession.categoryOptions & AVAudioSessionCategoryOptionMixWithOthers, - @"Flag MixWithOthers should be set."); -} - -- (void)testSetMixWithOthersShouldNoOpWhenNoChangesAreRequired { - StubFVPAVFactory *stubFactory = [[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:nil - pixelBufferSource:nil]; - TestAudioSession *audioSession = [[TestAudioSession alloc] init]; - stubFactory.audioSession = audioSession; - FVPVideoPlayerPlugin *videoPlayerPlugin = - [[FVPVideoPlayerPlugin alloc] initWithAVFactory:stubFactory - displayLinkFactory:nil - binaryMessenger:[[StubBinaryMessenger alloc] init] - textureRegistry:[[TestTextureRegistry alloc] init] - viewProvider:[[StubViewProvider alloc] init] - assetProvider:[[StubAssetProvider alloc] init]]; - - audioSession.category = AVAudioSessionCategoryPlayAndRecord; - audioSession.categoryOptions = - AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDefaultToSpeaker; - - FlutterError *error; - [videoPlayerPlugin setMixWithOthers:true error:&error]; - - XCTAssertFalse(audioSession.setCategoryCalled); -} -#endif - -/// Returns a test URL for creating a player from a network source. -- (nonnull NSURL *)mp4TestURL { - return (NSURL *_Nonnull)[NSURL - URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"]; -} - -- (nonnull NSObject *)playerItemWithURL:(NSURL *)url - factory:(NSObject *)factory { - return [factory playerItemWithAsset:[factory URLAssetWithURL:url options:nil]]; -} - -#pragma mark - Audio Track Tests - -// Tests getAudioTracks with a regular MP4 video file using real AVFoundation. -// Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. -- (void)testGetAudioTracksWithRealMP4Video { - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - FVPVideoPlayer *player = [[FVPVideoPlayer alloc] - initWithPlayerItem:[self playerItemWithURL:self.mp4TestURL factory:realObjectFactory] - avFactory:realObjectFactory - viewProvider:[[StubViewProvider alloc] init]]; - XCTAssertNotNil(player); - - XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; - StubEventListener *listener = - [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; - player.eventListener = listener; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - - // Now test getAudioTracks - FlutterError *error = nil; - NSArray *result = [player getAudioTracks:&error]; - - XCTAssertNil(error); - XCTAssertNotNil(result); - - // Regular MP4 files do not have media selection groups for audio. - // getAudioTracks only returns selectable audio tracks from HLS streams. - XCTAssertEqual(result.count, 0); - - [player disposeWithError:&error]; -} - -// Tests getAudioTracks with an HLS stream using real AVFoundation. -// HLS streams use media selection groups for audio track selection. -- (void)testGetAudioTracksWithRealHLSStream { - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - NSURL *hlsURL = [NSURL - URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"]; - XCTAssertNotNil(hlsURL); - - FVPVideoPlayer *player = [[FVPVideoPlayer alloc] - initWithPlayerItem:[self playerItemWithURL:hlsURL factory:realObjectFactory] - avFactory:realObjectFactory - viewProvider:[[StubViewProvider alloc] init]]; - XCTAssertNotNil(player); - - XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; - StubEventListener *listener = - [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; - player.eventListener = listener; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - - // Now test getAudioTracks - FlutterError *error = nil; - NSArray *result = [player getAudioTracks:&error]; - - XCTAssertNil(error); - XCTAssertNotNil(result); - - // For HLS streams with multiple audio options, we get media selection tracks. - // The bee.m3u8 stream may or may not have multiple audio tracks. - // We verify the method returns valid data without crashing. - for (FVPMediaSelectionAudioTrackData *track in result) { - XCTAssertNotNil(track.displayName); - XCTAssertGreaterThanOrEqual(track.index, 0); - } - - [player disposeWithError:&error]; -} - -// Tests that getAudioTracks returns valid data for audio-only files. -// Regular audio files do not have media selection groups, so getAudioTracks returns an empty array. -- (void)testGetAudioTracksWithRealAudioFile { - // 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. - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - NSURL *audioURL = [NSURL - URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3"]; - XCTAssertNotNil(audioURL); - - FVPVideoPlayer *player = [[FVPVideoPlayer alloc] - initWithPlayerItem:[self playerItemWithURL:audioURL factory:realObjectFactory] - avFactory:realObjectFactory - viewProvider:[[StubViewProvider alloc] init]]; - XCTAssertNotNil(player); - - XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; - StubEventListener *listener = - [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; - player.eventListener = listener; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - - // Now test getAudioTracks - FlutterError *error = nil; - NSArray *result = [player getAudioTracks:&error]; - - XCTAssertNil(error); - XCTAssertNotNil(result); - - // Regular audio files do not have media selection groups. - // getAudioTracks only returns selectable audio tracks from HLS streams. - XCTAssertEqual(result.count, 0); - - [player disposeWithError:&error]; -} - -// Tests that getAudioTracks works correctly through the plugin API with a real video. -// Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. -- (void)testGetAudioTracksViaPluginWithRealVideo { - // 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. - NSObject *realObjectFactory = [[FVPDefaultAVFactory alloc] init]; - NSURL *testURL = - [NSURL URLWithString:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"]; - XCTAssertNotNil(testURL); - FVPVideoPlayer *player = [[FVPVideoPlayer alloc] - initWithPlayerItem:[self playerItemWithURL:testURL factory:realObjectFactory] - avFactory:realObjectFactory - viewProvider:[[StubViewProvider alloc] init]]; - - // Wait for player item to become ready - AVPlayerItem *item = player.player.currentItem; - [self keyValueObservingExpectationForObject:(id)item - keyPath:@"status" - expectedValue:@(AVPlayerItemStatusReadyToPlay)]; - [self waitForExpectationsWithTimeout:30.0 handler:nil]; - - // Now test getAudioTracks - FlutterError *error; - NSArray *result = [player getAudioTracks:&error]; - - XCTAssertNil(error); - XCTAssertNotNil(result); - - // Regular MP4 files do not have media selection groups. - // getAudioTracks only returns selectable audio tracks from HLS streams. - XCTAssertEqual(result.count, 0); - - [player disposeWithError:&error]; -} - -- (void)testLoadTracksWithMediaTypeIsCalledOnNewerOS { - if (@available(iOS 15.0, macOS 12.0, *)) { - TestAsset *mockAsset = [[TestAsset alloc] initWithDuration:CMTimeMake(1, 1) tracks:@[]]; - NSObject *item = [[StubPlayerItem alloc] initWithAsset:mockAsset]; - - StubFVPAVFactory *stubAVFactory = [[StubFVPAVFactory alloc] initWithPlayer:nil - playerItem:item - pixelBufferSource:nil]; - StubViewProvider *stubViewProvider = -#if TARGET_OS_OSX - [[StubViewProvider alloc] initWithView:nil]; -#else - [[StubViewProvider alloc] initWithViewController:nil]; -#endif - FVPVideoPlayer *player = [[FVPVideoPlayer alloc] initWithPlayerItem:item - avFactory:stubAVFactory - viewProvider:stubViewProvider]; - XCTAssertNotNil(player); - XCTAssertTrue(mockAsset.loadedTracksAsynchronously); - } -} - -@end diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift new file mode 100644 index 000000000000..f2744ff89bd5 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -0,0 +1,952 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import AVFoundation +import Flutter +import XCTest +@preconcurrency import video_player_avfoundation + +@MainActor class VideoPlayerTests: XCTestCase { + + let mp4TestURL = URL( + string: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")! + let hlsTestURL = URL( + string: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8")! + + func testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream() { + // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 + // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some + // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An + // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams + // for issue #1, and restore the correct width and height for issue #2. + #if os(iOS) + let view = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10)) + let viewController = UIViewController() + viewController.view = view + let viewProvider = StubViewProvider(viewController: viewController) + #else + let view = NSView(frame: NSRect(x: 0, y: 0, width: 10, height: 10)) + view.wantsLayer = true + let viewProvider = StubViewProvider(view: view) + #endif + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(), + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: viewProvider, + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + XCTAssertNotNil(identifiers) + let player = + videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + + XCTAssertNotNil(player.playerLayer, "AVPlayerLayer should be present.") + XCTAssertEqual( + player.playerLayer.superlayer, view.layer, "AVPlayerLayer should be added on screen.") + } + + func testPlayerForPlatformViewDoesNotRegisterTexture() { + let textureRegistry = TestTextureRegistry() + let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(), + displayLinkFactory: stubDisplayLinkFactory, + binaryMessenger: StubBinaryMessenger(), + textureRegistry: textureRegistry, + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", + httpHeaders: [:]) + videoPlayerPlugin.createPlatformViewPlayer(with: create, error: &error) + XCTAssertNil(error) + + XCTAssertFalse(textureRegistry.registeredTexture) + } + + func testSeekToWhilePausedStartsDisplayLinkTemporarily() { + let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() + let mockVideoOutput = TestPixelBufferSource() + // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create + // the player via the plugin instead of directly to include that logic in the test. + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), + displayLinkFactory: stubDisplayLinkFactory, + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + XCTAssertNotNil(identifiers) + let player = + videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + + // Ensure that the video playback is paused before seeking. + player.pauseWithError(&error) + XCTAssertNil(error) + + let seekExpectation = expectation(description: "seekTo completes") + player.seek(to: 1234) { error in + seekExpectation.fulfill() + } + waitForExpectations(timeout: 30.0) + + // Seeking to a new position should start the display link temporarily. + XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + + // Simulate a buffer being available. + var bufferRef: CVPixelBuffer? + CVPixelBufferCreate(nil, 1, 1, kCVPixelFormatType_32BGRA, nil, &bufferRef) + mockVideoOutput.pixelBuffer = bufferRef + // Simulate a callback from the engine to request a new frame. + stubDisplayLinkFactory.fireDisplayLink?() + player.copyPixelBuffer() + // Since a frame was found, and the video is paused, the display link should be paused again. + XCTAssertFalse(stubDisplayLinkFactory.displayLink.running) + } + + func testInitStartsDisplayLinkTemporarily() { + let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() + let mockVideoOutput = TestPixelBufferSource() + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), + displayLinkFactory: stubDisplayLinkFactory, + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + + // Init should start the display link temporarily. + XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + + // Simulate a buffer being available. + var bufferRef: CVPixelBuffer? + CVPixelBufferCreate(nil, 1, 1, kCVPixelFormatType_32BGRA, nil, &bufferRef) + mockVideoOutput.pixelBuffer = bufferRef + // Simulate a callback from the engine to request a new frame. + let player = + videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + stubDisplayLinkFactory.fireDisplayLink?() + player.copyPixelBuffer() + // Since a frame was found, and the video is paused, the display link should be paused again. + XCTAssertFalse(stubDisplayLinkFactory.displayLink.running) + } + + func testSeekToWhilePlayingDoesNotStopDisplayLink() { + let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() + let mockVideoOutput = TestPixelBufferSource() + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), + displayLinkFactory: stubDisplayLinkFactory, + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + let player = + videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + + // Ensure that the video is playing before seeking. + player.playWithError(&error) + XCTAssertNil(error) + + let seekExpectation = expectation(description: "seekTo completes") + player.seek(to: 1234) { error in + seekExpectation.fulfill() + } + waitForExpectations(timeout: 30.0) + XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + + // Simulate a buffer being available. + var bufferRef: CVPixelBuffer? + CVPixelBufferCreate(nil, 1, 1, kCVPixelFormatType_32BGRA, nil, &bufferRef) + mockVideoOutput.pixelBuffer = bufferRef + // Simulate a callback from the engine to request a new frame. + stubDisplayLinkFactory.fireDisplayLink?() + // Since the video was playing, the display link should not be paused after getting a buffer. + XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + } + + func testPauseWhileWaitingForFrameDoesNotStopDisplayLink() { + let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() + // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create + // the player via the plugin instead of directly to include that logic in the test. + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(), + displayLinkFactory: stubDisplayLinkFactory, + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + let player = + videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + + // Run a play/pause cycle to force the pause codepath to run completely. + player.playWithError(&error) + XCTAssertNil(error) + player.pauseWithError(&error) + XCTAssertNil(error) + + // Since a buffer hasn't been available yet, the pause should not have stopped the display link. + XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + } + + func testDeregistersFromPlayer() { + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(), + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + XCTAssertNotNil(identifiers) + let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + XCTAssertNotNil(player) + + player.disposeWithError(&error) + XCTAssertNil(error) + XCTAssertEqual(videoPlayerPlugin.playersByIdentifier.count, 0) + } + + func testBufferingStateFromPlayer() { + let realObjectFactory = FVPDefaultAVFactory() + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: realObjectFactory, + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + XCTAssertNotNil(identifiers) + let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + XCTAssertNotNil(player) + let avPlayer = player.player + avPlayer.play() + + let bufferingStateExpectation = expectation(description: "bufferingState") + let eventSink: FlutterEventSink = { event in + guard let event = event as? [String: Any], let eventType = event["event"] as? String else { + return + } + if eventType == "bufferingEnd" { + XCTAssertTrue(avPlayer.currentItem!.isPlaybackLikelyToKeepUp) + } + if eventType == "bufferingStart" { + XCTAssertFalse(avPlayer.currentItem!.isPlaybackLikelyToKeepUp) + } + } + (player.eventListener as? FlutterStreamHandler)?.onListen( + withArguments: nil, eventSink: eventSink) + + let timeout: TimeInterval = 10 + DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { + bufferingStateExpectation.fulfill() + } + waitForExpectations(timeout: timeout + 1) + } + + func testVideoControls() { + let eventListener = sanityTestURI( + "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4") + XCTAssertEqual(eventListener.initializationSize.height, 720) + XCTAssertEqual(eventListener.initializationSize.width, 1280) + XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) + } + + func testAudioControls() { + let eventListener = sanityTestURI( + "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3") + XCTAssertEqual(eventListener.initializationSize.height, 0) + XCTAssertEqual(eventListener.initializationSize.width, 0) + XCTAssertEqual(Double(eventListener.initializationDuration), 5400, accuracy: 200) + } + + func testHLSControls() { + let eventListener = sanityTestURI( + "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8") + XCTAssertEqual(eventListener.initializationSize.height, 720) + XCTAssertEqual(eventListener.initializationSize.width, 1280) + XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) + } + + func testAudioOnlyHLSControls() throws { + throw XCTSkip("Flaky; see https://github.com/flutter/flutter/issues/164381") + + let eventListener = sanityTestURI( + "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee_audio_only.m3u8") + XCTAssertEqual(eventListener.initializationSize.height, 0) + XCTAssertEqual(eventListener.initializationSize.width, 0) + XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) + } + + #if os(iOS) + func testTransformFixOrientationUp() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform.identity + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, 0) + XCTAssertEqual(t.ty, 0) + } + + func testTransformFixOrientationDown() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform(a: -1, b: 0, c: 0, d: -1, tx: 0, ty: 0) + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, size.width) + XCTAssertEqual(t.ty, size.height) + } + + func testTransformFixOrientationLeft() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform(a: 0, b: -1, c: 1, d: 0, tx: 0, ty: 0) + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, 0) + XCTAssertEqual(t.ty, size.width) + } + + func testTransformFixOrientationRight() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform(a: 0, b: 1, c: -1, d: 0, tx: 0, ty: 0) + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, size.height) + XCTAssertEqual(t.ty, 0) + } + + func testTransformFixOrientationUpMirrored() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform(a: -1, b: 0, c: 0, d: 1, tx: 0, ty: 0) + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, size.width) + XCTAssertEqual(t.ty, 0) + } + + func testTransformFixOrientationDownMirrored() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, 0) + XCTAssertEqual(t.ty, size.height) + } + + func testTransformFixOrientationLeftMirrored() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform(a: 0, b: -1, c: -1, d: 0, tx: 0, ty: 0) + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, size.height) + XCTAssertEqual(t.ty, size.width) + } + + func testTransformFixOrientationRightMirrored() { + let size = CGSize(width: 800, height: 600) + let naturalTransform = CGAffineTransform(a: 0, b: 1, c: 1, d: 0, tx: 0, ty: 0) + let t = FVPGetStandardizedTrackTransform(naturalTransform, size) + XCTAssertEqual(t.tx, 0) + XCTAssertEqual(t.ty, 0) + } + #endif + + func testSeekToleranceWhenNotSeekingToEnd() { + let inspectableAVPlayer = InspectableAVPlayer() + let stubAVFactory = StubFVPAVFactory(player: inspectableAVPlayer) + let player = FVPVideoPlayer( + playerItem: StubPlayerItem(), + avFactory: stubAVFactory, + viewProvider: StubViewProvider()) + let listener = StubEventListener() + player.eventListener = listener + + let seekExpectation = expectation( + description: "seekTo has zero tolerance when seeking not to end") + player.seek(to: 1234) { error in + seekExpectation.fulfill() + } + + waitForExpectations(timeout: 30.0) + XCTAssertEqual(inspectableAVPlayer.beforeTolerance?.intValue, 0) + XCTAssertEqual(inspectableAVPlayer.afterTolerance?.intValue, 0) + } + + func testSeekToleranceWhenSeekingToEnd() { + let inspectableAVPlayer = InspectableAVPlayer() + let stubAVFactory = StubFVPAVFactory(player: inspectableAVPlayer) + let player = FVPVideoPlayer( + playerItem: StubPlayerItem(), + avFactory: stubAVFactory, + viewProvider: StubViewProvider()) + let listener = StubEventListener() + player.eventListener = listener + + let seekExpectation = expectation( + description: "seekTo has non-zero tolerance when seeking to end") + // The duration of this video is "0" due to the non standard initiliatazion process. + player.seek(to: 0) { error in + seekExpectation.fulfill() + } + waitForExpectations(timeout: 30.0) + XCTAssertGreaterThan(inspectableAVPlayer.beforeTolerance?.intValue ?? 0, 0) + XCTAssertGreaterThan(inspectableAVPlayer.afterTolerance?.intValue ?? 0, 0) + } + + /// Sanity checks a video player playing the given URL with the actual AVPlayer. This is essentially + /// a mini integration test of the player component. + /// + /// Returns the stub event listener to allow tests to inspect the call state. + func sanityTestURI(_ testURI: String) -> StubEventListener { + let realObjectFactory = FVPDefaultAVFactory() + guard let testURL = URL(string: testURI) else { + XCTFail("Failed to create URL") + return StubEventListener() + } + let player = FVPVideoPlayer( + playerItem: playerItem(with: testURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + XCTAssertNotNil(player) + + let initializedExpectation = expectation(description: "initialized") + let listener = StubEventListener(initializationExpectation: initializedExpectation) + player.eventListener = listener + waitForExpectations(timeout: 30.0) + + // Starts paused. + let avPlayer = player.player + XCTAssertEqual(avPlayer.rate, 0) + XCTAssertEqual(avPlayer.volume, 1) + XCTAssertEqual(avPlayer.timeControlStatus, .paused) + + // Change playback speed. + var error: FlutterError? + player.setPlaybackSpeed(2, error: &error) + XCTAssertNil(error) + player.playWithError(&error) + XCTAssertNil(error) + XCTAssertEqual(avPlayer.rate, 2) + XCTAssertEqual(avPlayer.timeControlStatus, .waitingToPlayAtSpecifiedRate) + + // Volume + player.setVolume(0.1, error: &error) + XCTAssertNil(error) + XCTAssertEqual(avPlayer.volume, 0.1) + + return listener + } + + // Checks whether [AVPlayer rate] KVO observations are correctly detached. + // - https://github.com/flutter/flutter/issues/124937 + // + // Failing to de-register results in a crash in [AVPlayer willChangeValueForKey:]. + func testDoesNotCrashOnRateObservationAfterDisposal() { + let realObjectFactory = FVPDefaultAVFactory() + + var avPlayer: AVPlayer? = nil + weak var weakPlayer: FVPVideoPlayer? = nil + + // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. + autoreleasepool { + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: realObjectFactory, + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + XCTAssertNotNil(identifiers) + + let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + XCTAssertNotNil(player) + weakPlayer = player + avPlayer = player.player + + player.disposeWithError(&error) + XCTAssertNil(error) + } + + let expectation = XCTestExpectation(description: "Object deallocated") + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak weakPlayer] timer in + if weakPlayer == nil { + timer.invalidate() + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 10.0) + + avPlayer?.willChangeValue(forKey: "rate") // No assertions needed. Lack of crash is a success. + avPlayer?.didChangeValue(forKey: "rate") + } + + // During the hot reload: + // 1. `[FVPVideoPlayer onTextureUnregistered:]` gets called. + // 2. `[FVPVideoPlayerPlugin initialize:]` gets called. + // + // Both of these methods dispatch [FVPVideoPlayer dispose] on the main thread + // leading to a possible crash when de-registering observers twice. + func testHotReloadDoesNotCrash() { + weak var weakPlayer: FVPVideoPlayer? = nil + + // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. + autoreleasepool { + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(), + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + XCTAssertNotNil(identifiers) + + let player = + videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + weakPlayer = player + + player.onTextureUnregistered(StubTexture()) + + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + } + + let expectation = XCTestExpectation(description: "Object deallocated") + Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak weakPlayer] timer in + if weakPlayer == nil { + timer.invalidate() + expectation.fulfill() + } + } + wait(for: [expectation], timeout: 10.0) // No assertions needed. Lack of crash is a success. + } + + func testFailedToLoadVideoEventShouldBeAlwaysSent() { + // Use real objects to test a real failure flow. + let realObjectFactory = FVPDefaultAVFactory() + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: realObjectFactory, + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make(withUri: "", httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + XCTAssertNotNil(player) + + let item = player.player.currentItem! + keyValueObservingExpectation(for: item, keyPath: "status") { _, change in + return item.status == .failed + } + waitForExpectations(timeout: 10.0) + + let failedExpectation = expectation(description: "failed") + // TODO(stuartmorgan): Update this test to instead use a mock listener, and add separate unit + // tests of FVPEventBridge. + let eventSink: FlutterEventSink = { event in + if event is FlutterError { + failedExpectation.fulfill() + } + } + (player.eventListener as? FlutterStreamHandler)?.onListen( + withArguments: nil, eventSink: eventSink) + waitForExpectations(timeout: 10.0) + } + + func testUpdatePlayingStateShouldNotResetRate() { + let realObjectFactory = FVPDefaultAVFactory() + let player = FVPVideoPlayer( + playerItem: playerItem(with: mp4TestURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + + let initializedExpectation = expectation(description: "initialized") + let listener = StubEventListener(initializationExpectation: initializedExpectation) + player.eventListener = listener + waitForExpectations(timeout: 10) + + var error: FlutterError? + player.setPlaybackSpeed(2, error: &error) + XCTAssertNil(error) + player.playWithError(&error) + XCTAssertNil(error) + XCTAssertEqual(player.player.rate, 2) + } + + func testPlayerShouldNotDropEverySecondFrame() { + let textureRegistry = TestTextureRegistry() + let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() + let mockVideoOutput = TestPixelBufferSource() + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), + displayLinkFactory: stubDisplayLinkFactory, + binaryMessenger: StubBinaryMessenger(), + textureRegistry: textureRegistry, + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + httpHeaders: [:]) + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + let playerIdentifier = identifiers!.playerId + let player = + videoPlayerPlugin.playersByIdentifier[playerIdentifier] as! FVPTextureBasedVideoPlayer + + func addFrame() { + var bufferRef: CVPixelBuffer? + CVPixelBufferCreate(nil, 1, 1, kCVPixelFormatType_32BGRA, nil, &bufferRef) + mockVideoOutput.pixelBuffer = bufferRef + } + + addFrame() + stubDisplayLinkFactory.fireDisplayLink?() + player.copyPixelBuffer() + XCTAssertEqual(textureRegistry.textureFrameAvailableCount, 1) + + addFrame() + stubDisplayLinkFactory.fireDisplayLink?() + player.copyPixelBuffer() + XCTAssertEqual(textureRegistry.textureFrameAvailableCount, 2) + } + + func testVideoOutputIsAddedWhenAVPlayerItemBecomesReady() { + let realObjectFactory = FVPDefaultAVFactory() + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: realObjectFactory, + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + + let create = FVPCreationOptions.make( + withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", + httpHeaders: [:]) + + let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + XCTAssertNil(error) + XCTAssertNotNil(identifiers) + let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + XCTAssertNotNil(player) + + let item = player.player.currentItem! + keyValueObservingExpectation(for: item, keyPath: "status") { _, change in + return item.status == .readyToPlay + } + waitForExpectations(timeout: 10.0) + // Video output is added as soon as the status becomes ready to play. + XCTAssertEqual(item.outputs.count, 1) + } + + #if os(iOS) + func testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker() { + let stubFactory = StubFVPAVFactory() + let audioSession = TestAudioSession() + stubFactory.audioSession = audioSession + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: stubFactory, + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + audioSession.category = .playAndRecord + audioSession.categoryOptions = .defaultToSpeaker + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + videoPlayerPlugin.setMixWithOthers(true, error: &error) + XCTAssertNil(error) + XCTAssertEqual(audioSession.category, .playAndRecord, "Category should be PlayAndRecord.") + XCTAssertTrue( + audioSession.categoryOptions.contains(.defaultToSpeaker), + "Flag DefaultToSpeaker was removed.") + XCTAssertTrue( + audioSession.categoryOptions.contains(.mixWithOthers), "Flag MixWithOthers should be set.") + } + + func testSetMixWithOthersShouldNoOpWhenNoChangesAreRequired() { + let stubFactory = StubFVPAVFactory() + let audioSession = TestAudioSession() + stubFactory.audioSession = audioSession + let videoPlayerPlugin = FVPVideoPlayerPlugin( + avFactory: stubFactory, + displayLinkFactory: StubFVPDisplayLinkFactory(), + binaryMessenger: StubBinaryMessenger(), + textureRegistry: TestTextureRegistry(), + viewProvider: StubViewProvider(), + assetProvider: StubAssetProvider()) + + audioSession.category = .playAndRecord + audioSession.categoryOptions = [.mixWithOthers, .defaultToSpeaker] + + var error: FlutterError? + videoPlayerPlugin.initialize(&error) + XCTAssertNil(error) + videoPlayerPlugin.setMixWithOthers(true, error: &error) + XCTAssertNil(error) + XCTAssertFalse(audioSession.setCategoryCalled) + } + #endif + + // MARK: - Audio Track Tests + + // Tests getAudioTracks with a regular MP4 video file using real AVFoundation. + // Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. + func testGetAudioTracksWithRealMP4Video() { + let realObjectFactory = FVPDefaultAVFactory() + let player = FVPVideoPlayer( + playerItem: playerItem(with: mp4TestURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + XCTAssertNotNil(player) + + let initializedExpectation = expectation(description: "initialized") + let listener = StubEventListener(initializationExpectation: initializedExpectation) + player.eventListener = listener + waitForExpectations(timeout: 30.0) + + // Now test getAudioTracks + var error: FlutterError? + let result = player.getAudioTracks(&error) + + XCTAssertNil(error) + XCTAssertNotNil(result) + + // Regular MP4 files do not have media selection groups for audio. + // getAudioTracks only returns selectable audio tracks from HLS streams. + XCTAssertEqual(result?.count, 0) + + player.disposeWithError(&error) + } + + // Tests getAudioTracks with an HLS stream using real AVFoundation. + // HLS streams use media selection groups for audio track selection. + func testGetAudioTracksWithRealHLSStream() { + let realObjectFactory = FVPDefaultAVFactory() + let hlsURL = URL( + string: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8")! + + let player = FVPVideoPlayer( + playerItem: playerItem(with: hlsURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + XCTAssertNotNil(player) + + let initializedExpectation = expectation(description: "initialized") + let listener = StubEventListener(initializationExpectation: initializedExpectation) + player.eventListener = listener + waitForExpectations(timeout: 30.0) + + // Now test getAudioTracks + var error: FlutterError? + let result = player.getAudioTracks(&error) + + XCTAssertNil(error) + XCTAssertNotNil(result) + + // For HLS streams with multiple audio options, we get media selection tracks. + // The bee.m3u8 stream may or may not have multiple audio tracks. + // We verify the method returns valid data without crashing. + for track in result ?? [] { + XCTAssertNotNil(track.displayName) + XCTAssertGreaterThanOrEqual(track.index, 0) + } + + player.disposeWithError(&error) + } + + // Tests that getAudioTracks returns valid data for audio-only files. + // Regular audio files do not have media selection groups, so getAudioTracks returns an empty array. + func testGetAudioTracksWithRealAudioFile() { + // 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 audioURL = URL( + string: "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3")! + + let player = FVPVideoPlayer( + playerItem: playerItem(with: audioURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + XCTAssertNotNil(player) + + let initializedExpectation = expectation(description: "initialized") + let listener = StubEventListener(initializationExpectation: initializedExpectation) + player.eventListener = listener + waitForExpectations(timeout: 30.0) + + // Now test getAudioTracks + var error: FlutterError? + let result = player.getAudioTracks(&error) + + XCTAssertNil(error) + XCTAssertNotNil(result) + + // Regular audio files do not have media selection groups. + // getAudioTracks only returns selectable audio tracks from HLS streams. + XCTAssertEqual(result?.count, 0) + + player.disposeWithError(&error) + } + + // Tests that getAudioTracks works correctly through the plugin API with a real video. + // Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. + func testGetAudioTracksViaPluginWithRealVideo() { + // 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 = URL( + string: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")! + let player = FVPVideoPlayer( + playerItem: playerItem(with: testURL, factory: realObjectFactory), + avFactory: realObjectFactory, + viewProvider: StubViewProvider()) + + // Wait for player item to become ready + let item = player.player.currentItem! + keyValueObservingExpectation(for: item, keyPath: "status") { _, _ in + return item.status == .readyToPlay + } + waitForExpectations(timeout: 30.0) + + // Now test getAudioTracks + var error: FlutterError? + let result = player.getAudioTracks(&error) + + XCTAssertNil(error) + XCTAssertNotNil(result) + + // Regular MP4 files do not have media selection groups. + // getAudioTracks only returns selectable audio tracks from HLS streams. + XCTAssertEqual(result?.count, 0) + + player.disposeWithError(&error) + } + + func testLoadTracksWithMediaTypeIsCalledOnNewerOS() { + if #available(iOS 15.0, macOS 12.0, *) { + let mockAsset = TestAsset(duration: CMTimeMake(value: 1, timescale: 1), tracks: []) + let item = StubPlayerItem(asset: mockAsset) + + let stubAVFactory = StubFVPAVFactory(player: nil, playerItem: item, pixelBufferSource: nil) + let stubViewProvider = StubViewProvider() + let player = FVPVideoPlayer( + playerItem: item, avFactory: stubAVFactory, viewProvider: stubViewProvider) + XCTAssertNotNil(player) + XCTAssertTrue(mockAsset.loadedTracksAsynchronously) + } + } + // MARK: - Helper Methods + + private func playerItem(with url: URL, factory: FVPAVFactory) -> FVPAVPlayerItem { + let asset = factory.urlAsset(with: url, options: nil) + return factory.playerItem(with: asset) + } +} diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h index 2a83455cef65..cb51d28b542e 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPTextureBasedVideoPlayer_Test.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN /// Called when the texture is unregistered. /// This method is used to clean up resources associated with the texture. -- (void)onTextureUnregistered:(nullable NSObject *)texture; +- (void)onTextureUnregistered:(NSObject *)texture; @end NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin.h index 9f15d53b0821..447d38731380 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin.h @@ -8,6 +8,10 @@ @import Flutter; #endif +NS_ASSUME_NONNULL_BEGIN + @interface FVPVideoPlayerPlugin : NSObject - (instancetype)initWithRegistrar:(NSObject *)registrar; @end + +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h index 4da66be36023..b9ba034edc8d 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayerPlugin_Test.h @@ -17,6 +17,8 @@ #import "FVPViewProvider.h" #import "messages.g.h" +NS_ASSUME_NONNULL_BEGIN + // Protocol for an AVPlayer instance factory. Used for injecting display links in tests. @protocol FVPDisplayLinkFactory - (NSObject *)displayLinkWithViewProvider:(NSObject *)viewProvider @@ -38,3 +40,5 @@ assetProvider:(NSObject *)assetProvider; @end + +NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 4bd39799f457..2990cd2365ab 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,14 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331585C52F36433100FACB51 /* VideoPlayerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331585C42F36433100FACB51 /* VideoPlayerUITests.swift */; }; + 335542AD2F364D080081D0DC /* VideoPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335542AC2F364D080081D0DC /* VideoPlayerTests.swift */; }; + 335542B02F366B4F0081D0DC /* TestClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335542AF2F366B4F0081D0DC /* TestClasses.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -18,7 +20,6 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; B0F5C77B94E32FB72444AE9F /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 20721C28387E1F78689EC502 /* libPods-Runner.a */; }; D182ECB59C06DBC7E2D5D913 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BD232FD3BD3343A5F52AF50 /* libPods-RunnerTests.a */; }; - F7151F3D26603ECA0028CB91 /* VideoPlayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +58,8 @@ 20721C28387E1F78689EC502 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2A2EA522BDC492279A91AB75 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331585C42F36433100FACB51 /* VideoPlayerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerUITests.swift; sourceTree = ""; }; + 335542AC2F364D080081D0DC /* VideoPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerTests.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift; sourceTree = ""; }; + 335542AF2F366B4F0081D0DC /* TestClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClasses.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 6CDC4DA5940705A6E7671616 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -76,7 +79,6 @@ F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F7151F3026603EBD0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = VideoPlayerTests.m; path = ../../../darwin/RunnerTests/VideoPlayerTests.m; sourceTree = ""; }; F7151F3E26603ECA0028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -198,7 +200,8 @@ F7151F3B26603ECA0028CB91 /* RunnerTests */ = { isa = PBXGroup; children = ( - F7151F3C26603ECA0028CB91 /* VideoPlayerTests.m */, + 335542AC2F364D080081D0DC /* VideoPlayerTests.swift */, + 335542AF2F366B4F0081D0DC /* TestClasses.swift */, F7151F3E26603ECA0028CB91 /* Info.plist */, ); path = RunnerTests; @@ -289,6 +292,7 @@ }; F7151F3926603ECA0028CB91 = { CreatedOnToolsVersion = 12.5; + LastSwiftMigration = 2620; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -304,7 +308,7 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; @@ -460,7 +464,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - F7151F3D26603ECA0028CB91 /* VideoPlayerTests.m in Sources */, + 335542AD2F364D080081D0DC /* VideoPlayerTests.swift in Sources */, + 335542B02F366B4F0081D0DC /* TestClasses.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -697,6 +702,7 @@ baseConfigurationReference = 6CDC4DA5940705A6E7671616 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -707,6 +713,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Debug; @@ -716,6 +724,7 @@ baseConfigurationReference = 2A2EA522BDC492279A91AB75 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -726,6 +735,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 6.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Release; @@ -772,7 +782,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; From 58824d18c64e38128dcf94fe1cc4d5aab9e0fe43 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 6 Feb 2026 16:17:45 -0500 Subject: [PATCH 04/16] Consolidate test URIs --- .../darwin/RunnerTests/VideoPlayerTests.swift | 83 ++++++------------- 1 file changed, 26 insertions(+), 57 deletions(-) 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 f2744ff89bd5..ed217ecdd38b 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -9,10 +9,11 @@ import XCTest @MainActor class VideoPlayerTests: XCTestCase { - let mp4TestURL = URL( - string: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")! - let hlsTestURL = URL( - string: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8")! + let mp4TestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + let hlsTestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" + let mp3AudioTestURI = "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3" + let hlsAudioTestURI = + "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee_audio_only.m3u8" func testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream() { // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 @@ -42,9 +43,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -71,9 +70,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) videoPlayerPlugin.createPlatformViewPlayer(with: create, error: &error) XCTAssertNil(error) @@ -97,9 +94,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -145,9 +140,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) @@ -182,9 +175,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let player = @@ -227,9 +218,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let player = @@ -258,9 +247,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -286,9 +273,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -320,24 +305,21 @@ import XCTest } func testVideoControls() { - let eventListener = sanityTestURI( - "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4") + let eventListener = sanityTestURI(mp4TestURI) XCTAssertEqual(eventListener.initializationSize.height, 720) XCTAssertEqual(eventListener.initializationSize.width, 1280) XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) } func testAudioControls() { - let eventListener = sanityTestURI( - "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3") + let eventListener = sanityTestURI(mp3AudioTestURI) XCTAssertEqual(eventListener.initializationSize.height, 0) XCTAssertEqual(eventListener.initializationSize.width, 0) XCTAssertEqual(Double(eventListener.initializationDuration), 5400, accuracy: 200) } func testHLSControls() { - let eventListener = sanityTestURI( - "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8") + let eventListener = sanityTestURI(hlsTestURI) XCTAssertEqual(eventListener.initializationSize.height, 720) XCTAssertEqual(eventListener.initializationSize.width, 1280) XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) @@ -346,8 +328,7 @@ import XCTest func testAudioOnlyHLSControls() throws { throw XCTSkip("Flaky; see https://github.com/flutter/flutter/issues/164381") - let eventListener = sanityTestURI( - "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee_audio_only.m3u8") + let eventListener = sanityTestURI(hlsAudioTestURI) XCTAssertEqual(eventListener.initializationSize.height, 0) XCTAssertEqual(eventListener.initializationSize.width, 0) XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) @@ -529,9 +510,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -581,9 +560,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -651,7 +628,7 @@ import XCTest func testUpdatePlayingStateShouldNotResetRate() { let realObjectFactory = FVPDefaultAVFactory() let player = FVPVideoPlayer( - playerItem: playerItem(with: mp4TestURL, factory: realObjectFactory), + playerItem: playerItem(with: URL(string: mp4TestURI)!, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) @@ -684,9 +661,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", - httpHeaders: [:]) + let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let playerIdentifier = identifiers!.playerId @@ -723,10 +698,7 @@ import XCTest videoPlayerPlugin.initialize(&error) XCTAssertNil(error) - let create = FVPCreationOptions.make( - withUri: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4", - httpHeaders: [:]) - + let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -802,7 +774,7 @@ import XCTest func testGetAudioTracksWithRealMP4Video() { let realObjectFactory = FVPDefaultAVFactory() let player = FVPVideoPlayer( - playerItem: playerItem(with: mp4TestURL, factory: realObjectFactory), + playerItem: playerItem(with: URL(string: mp4TestURI)!, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) XCTAssertNotNil(player) @@ -830,8 +802,7 @@ import XCTest // HLS streams use media selection groups for audio track selection. func testGetAudioTracksWithRealHLSStream() { let realObjectFactory = FVPDefaultAVFactory() - let hlsURL = URL( - string: "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8")! + let hlsURL = URL(string: hlsTestURI)! let player = FVPVideoPlayer( playerItem: playerItem(with: hlsURL, factory: realObjectFactory), @@ -868,8 +839,7 @@ import XCTest // 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 audioURL = URL( - string: "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3")! + let audioURL = URL(string: mp3AudioTestURI)! let player = FVPVideoPlayer( playerItem: playerItem(with: audioURL, factory: realObjectFactory), @@ -902,8 +872,7 @@ import XCTest // 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 = URL( - string: "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4")! + let testURL = URL(string: mp4TestURI)! let player = FVPVideoPlayer( playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, From 53d8397e6d10491dc5ce5a41056da4830b9f5562 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 6 Feb 2026 16:19:00 -0500 Subject: [PATCH 05/16] Remove unnecesasry assertions --- .../darwin/RunnerTests/VideoPlayerTests.swift | 10 ---------- 1 file changed, 10 deletions(-) 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 ed217ecdd38b..01bed3974adf 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -252,7 +252,6 @@ import XCTest XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer - XCTAssertNotNil(player) player.disposeWithError(&error) XCTAssertNil(error) @@ -278,7 +277,6 @@ import XCTest XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer - XCTAssertNotNil(player) let avPlayer = player.player avPlayer.play() @@ -456,7 +454,6 @@ import XCTest playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - XCTAssertNotNil(player) let initializedExpectation = expectation(description: "initialized") let listener = StubEventListener(initializationExpectation: initializedExpectation) @@ -516,7 +513,6 @@ import XCTest XCTAssertNotNil(identifiers) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer - XCTAssertNotNil(player) weakPlayer = player avPlayer = player.player @@ -604,7 +600,6 @@ import XCTest let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer - XCTAssertNotNil(player) let item = player.player.currentItem! keyValueObservingExpectation(for: item, keyPath: "status") { _, change in @@ -703,7 +698,6 @@ import XCTest XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer - XCTAssertNotNil(player) let item = player.player.currentItem! keyValueObservingExpectation(for: item, keyPath: "status") { _, change in @@ -777,7 +771,6 @@ import XCTest playerItem: playerItem(with: URL(string: mp4TestURI)!, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - XCTAssertNotNil(player) let initializedExpectation = expectation(description: "initialized") let listener = StubEventListener(initializationExpectation: initializedExpectation) @@ -808,7 +801,6 @@ import XCTest playerItem: playerItem(with: hlsURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - XCTAssertNotNil(player) let initializedExpectation = expectation(description: "initialized") let listener = StubEventListener(initializationExpectation: initializedExpectation) @@ -845,7 +837,6 @@ import XCTest playerItem: playerItem(with: audioURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - XCTAssertNotNil(player) let initializedExpectation = expectation(description: "initialized") let listener = StubEventListener(initializationExpectation: initializedExpectation) @@ -908,7 +899,6 @@ import XCTest let stubViewProvider = StubViewProvider() let player = FVPVideoPlayer( playerItem: item, avFactory: stubAVFactory, viewProvider: stubViewProvider) - XCTAssertNotNil(player) XCTAssertTrue(mockAsset.loadedTracksAsynchronously) } } From 7e9edc7b3b6bed4f0510cb358276c1f5e1ea6cf6 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 6 Feb 2026 16:32:03 -0500 Subject: [PATCH 06/16] Add a helper to reduce boilerplate --- .../darwin/RunnerTests/VideoPlayerTests.swift | 214 +++++------------- 1 file changed, 58 insertions(+), 156 deletions(-) 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 01bed3974adf..ef64943ad658 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -31,19 +31,10 @@ import XCTest view.wantsLayer = true let viewProvider = StubViewProvider(view: view) #endif - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: StubFVPAVFactory(), - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: viewProvider, - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin(viewProvider: viewProvider) let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -58,19 +49,12 @@ import XCTest func testPlayerForPlatformViewDoesNotRegisterTexture() { let textureRegistry = TestTextureRegistry() let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: StubFVPAVFactory(), + let videoPlayerPlugin = createInitializedPlugin( displayLinkFactory: stubDisplayLinkFactory, - binaryMessenger: StubBinaryMessenger(), - textureRegistry: textureRegistry, - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + textureRegistry: textureRegistry) let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) + var error: FlutterError? videoPlayerPlugin.createPlatformViewPlayer(with: create, error: &error) XCTAssertNil(error) @@ -82,19 +66,12 @@ import XCTest let mockVideoOutput = TestPixelBufferSource() // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create // the player via the plugin instead of directly to include that logic in the test. - let videoPlayerPlugin = FVPVideoPlayerPlugin( + let videoPlayerPlugin = createInitializedPlugin( avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), - displayLinkFactory: stubDisplayLinkFactory, - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + displayLinkFactory: stubDisplayLinkFactory) let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -128,19 +105,12 @@ import XCTest func testInitStartsDisplayLinkTemporarily() { let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() - let videoPlayerPlugin = FVPVideoPlayerPlugin( + let videoPlayerPlugin = createInitializedPlugin( avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), - displayLinkFactory: stubDisplayLinkFactory, - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + displayLinkFactory: stubDisplayLinkFactory) let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) @@ -163,19 +133,12 @@ import XCTest func testSeekToWhilePlayingDoesNotStopDisplayLink() { let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() - let videoPlayerPlugin = FVPVideoPlayerPlugin( + let videoPlayerPlugin = createInitializedPlugin( avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), - displayLinkFactory: stubDisplayLinkFactory, - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + displayLinkFactory: stubDisplayLinkFactory) let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let player = @@ -206,19 +169,10 @@ import XCTest let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create // the player via the plugin instead of directly to include that logic in the test. - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: StubFVPAVFactory(), - displayLinkFactory: stubDisplayLinkFactory, - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin(displayLinkFactory: stubDisplayLinkFactory) let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let player = @@ -235,19 +189,10 @@ import XCTest } func testDeregistersFromPlayer() { - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: StubFVPAVFactory(), - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin() let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -260,19 +205,10 @@ import XCTest func testBufferingStateFromPlayer() { let realObjectFactory = FVPDefaultAVFactory() - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: realObjectFactory, - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -495,19 +431,10 @@ import XCTest // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. autoreleasepool { - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: realObjectFactory, - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -544,19 +471,10 @@ import XCTest // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. autoreleasepool { - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: StubFVPAVFactory(), - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin(avFactory: StubFVPAVFactory()) let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -584,19 +502,10 @@ import XCTest func testFailedToLoadVideoEventShouldBeAlwaysSent() { // Use real objects to test a real failure flow. let realObjectFactory = FVPDefaultAVFactory() - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: realObjectFactory, - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) let create = FVPCreationOptions.make(withUri: "", httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer @@ -644,19 +553,13 @@ import XCTest let textureRegistry = TestTextureRegistry() let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() - let videoPlayerPlugin = FVPVideoPlayerPlugin( + let videoPlayerPlugin = createInitializedPlugin( avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), displayLinkFactory: stubDisplayLinkFactory, - binaryMessenger: StubBinaryMessenger(), - textureRegistry: textureRegistry, - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + textureRegistry: textureRegistry) let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) let playerIdentifier = identifiers!.playerId @@ -682,18 +585,10 @@ import XCTest func testVideoOutputIsAddedWhenAVPlayerItemBecomesReady() { let realObjectFactory = FVPDefaultAVFactory() - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: realObjectFactory, - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) + var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -713,20 +608,11 @@ import XCTest let stubFactory = StubFVPAVFactory() let audioSession = TestAudioSession() stubFactory.audioSession = audioSession - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: stubFactory, - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - audioSession.category = .playAndRecord audioSession.categoryOptions = .defaultToSpeaker + let videoPlayerPlugin = createInitializedPlugin(avFactory: stubFactory) var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) videoPlayerPlugin.setMixWithOthers(true, error: &error) XCTAssertNil(error) XCTAssertEqual(audioSession.category, .playAndRecord, "Category should be PlayAndRecord.") @@ -741,20 +627,11 @@ import XCTest let stubFactory = StubFVPAVFactory() let audioSession = TestAudioSession() stubFactory.audioSession = audioSession - let videoPlayerPlugin = FVPVideoPlayerPlugin( - avFactory: stubFactory, - displayLinkFactory: StubFVPDisplayLinkFactory(), - binaryMessenger: StubBinaryMessenger(), - textureRegistry: TestTextureRegistry(), - viewProvider: StubViewProvider(), - assetProvider: StubAssetProvider()) - audioSession.category = .playAndRecord audioSession.categoryOptions = [.mixWithOthers, .defaultToSpeaker] + let videoPlayerPlugin = createInitializedPlugin(avFactory: stubFactory) var error: FlutterError? - videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) videoPlayerPlugin.setMixWithOthers(true, error: &error) XCTAssertNil(error) XCTAssertFalse(audioSession.setCategoryCalled) @@ -899,11 +776,36 @@ import XCTest let stubViewProvider = StubViewProvider() let player = FVPVideoPlayer( playerItem: item, avFactory: stubAVFactory, viewProvider: stubViewProvider) + XCTAssertNotNil(player) XCTAssertTrue(mockAsset.loadedTracksAsynchronously) } } + // MARK: - Helper Methods + /// Creates a plugin with the given dependencies, and default stubs for any that aren't provided, + /// then initializes it. + private func createInitializedPlugin( + avFactory: FVPAVFactory = StubFVPAVFactory(), + displayLinkFactory: FVPDisplayLinkFactory = StubFVPDisplayLinkFactory(), + binaryMessenger: FlutterBinaryMessenger = StubBinaryMessenger(), + textureRegistry: FlutterTextureRegistry = TestTextureRegistry(), + viewProvider: FVPViewProvider = StubViewProvider(), + assetProvider: FVPAssetProvider = StubAssetProvider() + ) -> FVPVideoPlayerPlugin { + let plugin = FVPVideoPlayerPlugin( + avFactory: avFactory, + displayLinkFactory: displayLinkFactory, + binaryMessenger: binaryMessenger, + textureRegistry: textureRegistry, + viewProvider: viewProvider, + assetProvider: assetProvider) + var error: FlutterError? + plugin.initialize(&error) + XCTAssertNil(error) + return plugin + } + private func playerItem(with url: URL, factory: FVPAVFactory) -> FVPAVPlayerItem { let asset = factory.urlAsset(with: url, options: nil) return factory.playerItem(with: asset) From 244ec5c7ef97d911996f51ef1f2bf0a0efe06729 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 6 Feb 2026 16:35:08 -0500 Subject: [PATCH 07/16] Inline creation arguments to reduce variable clutter --- .../darwin/RunnerTests/VideoPlayerTests.swift | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) 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 ef64943ad658..eb4ef14ee892 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -33,9 +33,9 @@ import XCTest #endif let videoPlayerPlugin = createInitializedPlugin(viewProvider: viewProvider) - let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = @@ -53,9 +53,9 @@ import XCTest displayLinkFactory: stubDisplayLinkFactory, textureRegistry: textureRegistry) - let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) var error: FlutterError? - videoPlayerPlugin.createPlatformViewPlayer(with: create, error: &error) + videoPlayerPlugin.createPlatformViewPlayer( + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertFalse(textureRegistry.registeredTexture) @@ -70,9 +70,9 @@ import XCTest avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), displayLinkFactory: stubDisplayLinkFactory) - let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = @@ -109,9 +109,9 @@ import XCTest avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), displayLinkFactory: stubDisplayLinkFactory) - let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) // Init should start the display link temporarily. @@ -137,9 +137,9 @@ import XCTest avFactory: StubFVPAVFactory(pixelBufferSource: mockVideoOutput), displayLinkFactory: stubDisplayLinkFactory) - let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer @@ -171,9 +171,9 @@ import XCTest // the player via the plugin instead of directly to include that logic in the test. let videoPlayerPlugin = createInitializedPlugin(displayLinkFactory: stubDisplayLinkFactory) - let create = FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer @@ -191,9 +191,9 @@ import XCTest func testDeregistersFromPlayer() { let videoPlayerPlugin = createInitializedPlugin() - let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer @@ -207,9 +207,9 @@ import XCTest let realObjectFactory = FVPDefaultAVFactory() let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) - let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer @@ -433,9 +433,9 @@ import XCTest autoreleasepool { let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) - let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -473,9 +473,9 @@ import XCTest autoreleasepool { let videoPlayerPlugin = createInitializedPlugin(avFactory: StubFVPAVFactory()) - let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) @@ -504,9 +504,9 @@ import XCTest let realObjectFactory = FVPDefaultAVFactory() let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) - let create = FVPCreationOptions.make(withUri: "", httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: "", httpHeaders: [:]), error: &error) XCTAssertNil(error) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer @@ -558,9 +558,9 @@ import XCTest displayLinkFactory: stubDisplayLinkFactory, textureRegistry: textureRegistry) - let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) let playerIdentifier = identifiers!.playerId let player = @@ -587,9 +587,9 @@ import XCTest let realObjectFactory = FVPDefaultAVFactory() let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) - let create = FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer(with: create, error: &error) + let identifiers = videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) XCTAssertNil(error) XCTAssertNotNil(identifiers) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer From ec4b403c8aa0b116c55dd02eef90eea92cfa2100 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Feb 2026 14:33:42 -0500 Subject: [PATCH 08/16] Convert to Swift Testing --- .../darwin/RunnerTests/TestClasses.swift | 14 +- .../darwin/RunnerTests/VideoPlayerTests.swift | 559 +++++++++--------- 2 files changed, 288 insertions(+), 285 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift index d28dabf59d6a..a151ca49587b 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import AVFoundation -import XCTest +import Testing import video_player_avfoundation #if os(iOS) @@ -102,14 +102,14 @@ class TestTextureRegistry: NSObject, FlutterTextureRegistry { func unregisterTexture(_ textureId: Int64) { if textureId != 1 { - XCTFail("Unregistering texture with wrong ID") + Issue.record("Unregistering texture with wrong ID") } unregisteredTexture = true } func textureFrameAvailable(_ textureId: Int64) { if textureId != 1 { - XCTFail("Texture frame available with wrong ID") + Issue.record("Texture frame available with wrong ID") } textureFrameAvailableCount += 1 } @@ -256,12 +256,12 @@ class StubFVPDisplayLinkFactory: NSObject, FVPDisplayLinkFactory { } class StubEventListener: NSObject, FVPVideoEventListener { - var initializationExpectation: XCTestExpectation? + var initializationContinuation: CheckedContinuation? private(set) var initializationDuration: Int64 = 0 private(set) var initializationSize: CGSize = .zero - init(initializationExpectation: XCTestExpectation? = nil) { - self.initializationExpectation = initializationExpectation + init(initializationContinuation: CheckedContinuation? = nil) { + self.initializationContinuation = initializationContinuation super.init() } @@ -269,7 +269,7 @@ class StubEventListener: NSObject, FVPVideoEventListener { func videoPlayerDidEndBuffering() {} func videoPlayerDidError(withMessage errorMessage: String) {} func videoPlayerDidInitialize(withDuration duration: Int64, size: CGSize) { - initializationExpectation?.fulfill() + initializationContinuation?.resume() initializationDuration = duration initializationSize = size } 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 eb4ef14ee892..4613f9d62af5 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -4,10 +4,10 @@ import AVFoundation import Flutter -import XCTest +import Testing @preconcurrency import video_player_avfoundation -@MainActor class VideoPlayerTests: XCTestCase { +@MainActor struct VideoPlayerTests { let mp4TestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" let hlsTestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" @@ -15,7 +15,9 @@ import XCTest let hlsAudioTestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee_audio_only.m3u8" - func testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream() { + @Test func blankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream() + throws + { // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 // (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some // video streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). An @@ -34,19 +36,17 @@ import XCTest let videoPlayerPlugin = createInitializedPlugin(viewProvider: viewProvider) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) - XCTAssertNotNil(identifiers) + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + #expect(error == nil) let player = - videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPTextureBasedVideoPlayer - XCTAssertNotNil(player.playerLayer, "AVPlayerLayer should be present.") - XCTAssertEqual( - player.playerLayer.superlayer, view.layer, "AVPlayerLayer should be added on screen.") + #expect(player.playerLayer.superlayer == view.layer) } - func testPlayerForPlatformViewDoesNotRegisterTexture() { + @Test func playerForPlatformViewDoesNotRegisterTexture() { let textureRegistry = TestTextureRegistry() let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let videoPlayerPlugin = createInitializedPlugin( @@ -56,12 +56,12 @@ import XCTest var error: FlutterError? videoPlayerPlugin.createPlatformViewPlayer( with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) + #expect(error == nil) - XCTAssertFalse(textureRegistry.registeredTexture) + #expect(!textureRegistry.registeredTexture) } - func testSeekToWhilePausedStartsDisplayLinkTemporarily() { + @Test func seekToWhilePausedStartsDisplayLinkTemporarily() async throws { let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create @@ -71,25 +71,21 @@ import XCTest displayLinkFactory: stubDisplayLinkFactory) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) - XCTAssertNotNil(identifiers) + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error)) + #expect(error == nil) let player = - videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPTextureBasedVideoPlayer // Ensure that the video playback is paused before seeking. player.pauseWithError(&error) - XCTAssertNil(error) + #expect(error == nil) - let seekExpectation = expectation(description: "seekTo completes") - player.seek(to: 1234) { error in - seekExpectation.fulfill() - } - waitForExpectations(timeout: 30.0) + await asyncSeekTo(player: player, time: 1234) // Seeking to a new position should start the display link temporarily. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + #expect(stubDisplayLinkFactory.displayLink.running) // Simulate a buffer being available. var bufferRef: CVPixelBuffer? @@ -99,10 +95,10 @@ import XCTest stubDisplayLinkFactory.fireDisplayLink?() player.copyPixelBuffer() // Since a frame was found, and the video is paused, the display link should be paused again. - XCTAssertFalse(stubDisplayLinkFactory.displayLink.running) + #expect(!stubDisplayLinkFactory.displayLink.running) } - func testInitStartsDisplayLinkTemporarily() { + @Test func initStartsDisplayLinkTemporarily() { let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() let videoPlayerPlugin = createInitializedPlugin( @@ -112,10 +108,10 @@ import XCTest var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) + #expect(error == nil) // Init should start the display link temporarily. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + #expect(stubDisplayLinkFactory.displayLink.running) // Simulate a buffer being available. var bufferRef: CVPixelBuffer? @@ -127,10 +123,10 @@ import XCTest stubDisplayLinkFactory.fireDisplayLink?() player.copyPixelBuffer() // Since a frame was found, and the video is paused, the display link should be paused again. - XCTAssertFalse(stubDisplayLinkFactory.displayLink.running) + #expect(!stubDisplayLinkFactory.displayLink.running) } - func testSeekToWhilePlayingDoesNotStopDisplayLink() { + @Test func seekToWhilePlayingDoesNotStopDisplayLink() async { let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() let videoPlayerPlugin = createInitializedPlugin( @@ -140,20 +136,17 @@ import XCTest var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) + #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer // Ensure that the video is playing before seeking. player.playWithError(&error) - XCTAssertNil(error) + #expect(error == nil) - let seekExpectation = expectation(description: "seekTo completes") - player.seek(to: 1234) { error in - seekExpectation.fulfill() - } - waitForExpectations(timeout: 30.0) - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + await asyncSeekTo(player: player, time: 1234) + + #expect(stubDisplayLinkFactory.displayLink.running) // Simulate a buffer being available. var bufferRef: CVPixelBuffer? @@ -162,10 +155,10 @@ import XCTest // Simulate a callback from the engine to request a new frame. stubDisplayLinkFactory.fireDisplayLink?() // Since the video was playing, the display link should not be paused after getting a buffer. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + #expect(stubDisplayLinkFactory.displayLink.running) } - func testPauseWhileWaitingForFrameDoesNotStopDisplayLink() { + @Test func pauseWhileWaitingForFrameDoesNotStopDisplayLink() { let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() // Display link and frame updater wire-up is currently done in FVPVideoPlayerPlugin, so create // the player via the plugin instead of directly to include that logic in the test. @@ -174,167 +167,170 @@ import XCTest var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) + #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer // Run a play/pause cycle to force the pause codepath to run completely. player.playWithError(&error) - XCTAssertNil(error) + #expect(error == nil) player.pauseWithError(&error) - XCTAssertNil(error) + #expect(error == nil) // Since a buffer hasn't been available yet, the pause should not have stopped the display link. - XCTAssertTrue(stubDisplayLinkFactory.displayLink.running) + #expect(stubDisplayLinkFactory.displayLink.running) } - func testDeregistersFromPlayer() { + @Test func deregistersFromPlayer() throws { let videoPlayerPlugin = createInitializedPlugin() var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) - XCTAssertNotNil(identifiers) - let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + #expect(error == nil) + let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer player.disposeWithError(&error) - XCTAssertNil(error) - XCTAssertEqual(videoPlayerPlugin.playersByIdentifier.count, 0) + #expect(error == nil) + #expect(videoPlayerPlugin.playersByIdentifier.count == 0) } - func testBufferingStateFromPlayer() { + @Test func bufferingStateFromPlayer() async throws { + // TODO(stuartmorgan): Rewrite this test to use stubs, instead of running for 10 + // seconds with a real player and hoping to get buffer status updates. let realObjectFactory = FVPDefaultAVFactory() let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) - XCTAssertNotNil(identifiers) - let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + #expect(error == nil) + let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer let avPlayer = player.player avPlayer.play() - let bufferingStateExpectation = expectation(description: "bufferingState") let eventSink: FlutterEventSink = { event in guard let event = event as? [String: Any], let eventType = event["event"] as? String else { return } if eventType == "bufferingEnd" { - XCTAssertTrue(avPlayer.currentItem!.isPlaybackLikelyToKeepUp) + #expect(avPlayer.currentItem!.isPlaybackLikelyToKeepUp) } if eventType == "bufferingStart" { - XCTAssertFalse(avPlayer.currentItem!.isPlaybackLikelyToKeepUp) + #expect(!avPlayer.currentItem!.isPlaybackLikelyToKeepUp) } } (player.eventListener as? FlutterStreamHandler)?.onListen( withArguments: nil, eventSink: eventSink) - let timeout: TimeInterval = 10 - DispatchQueue.main.asyncAfter(deadline: .now() + timeout) { - bufferingStateExpectation.fulfill() - } - waitForExpectations(timeout: timeout + 1) + // Load for a while to let some buffer events happen. + try await Task.sleep(nanoseconds: 10 * 1_000_000_000) } - func testVideoControls() { - let eventListener = sanityTestURI(mp4TestURI) - XCTAssertEqual(eventListener.initializationSize.height, 720) - XCTAssertEqual(eventListener.initializationSize.width, 1280) - XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) + private func durationApproximatelyEquals(_ actual: Int64, _ expected: Int64, tolerance: Int64) + -> Bool + { + return abs(actual - expected) < tolerance } - func testAudioControls() { - let eventListener = sanityTestURI(mp3AudioTestURI) - XCTAssertEqual(eventListener.initializationSize.height, 0) - XCTAssertEqual(eventListener.initializationSize.width, 0) - XCTAssertEqual(Double(eventListener.initializationDuration), 5400, accuracy: 200) + @Test func videoControls() async throws { + let eventListener = try await sanityTestURI(mp4TestURI) + #expect(eventListener.initializationSize.height == 720) + #expect(eventListener.initializationSize.width == 1280) + #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) } - func testHLSControls() { - let eventListener = sanityTestURI(hlsTestURI) - XCTAssertEqual(eventListener.initializationSize.height, 720) - XCTAssertEqual(eventListener.initializationSize.width, 1280) - XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) + @Test func audioControls() async throws { + let eventListener = try await sanityTestURI(mp3AudioTestURI) + #expect(eventListener.initializationSize.height == 0) + #expect(eventListener.initializationSize.width == 0) + #expect(durationApproximatelyEquals(eventListener.initializationDuration, 5400, tolerance: 200)) } - func testAudioOnlyHLSControls() throws { - throw XCTSkip("Flaky; see https://github.com/flutter/flutter/issues/164381") + @Test func hLSControls() async throws { + let eventListener = try await sanityTestURI(hlsTestURI) + #expect(eventListener.initializationSize.height == 720) + #expect(eventListener.initializationSize.width == 1280) + #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) + } - let eventListener = sanityTestURI(hlsAudioTestURI) - XCTAssertEqual(eventListener.initializationSize.height, 0) - XCTAssertEqual(eventListener.initializationSize.width, 0) - XCTAssertEqual(Double(eventListener.initializationDuration), 4000, accuracy: 200) + @Test(.disabled("Flaky"), .bug("https://github.com/flutter/flutter/issues/164381")) + func audioOnlyHLSControls() async throws { + let eventListener = try await sanityTestURI(hlsAudioTestURI) + #expect(eventListener.initializationSize.height == 0) + #expect(eventListener.initializationSize.width == 0) + #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) } #if os(iOS) - func testTransformFixOrientationUp() { + @Test func transformFixOrientationUp() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform.identity let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, 0) - XCTAssertEqual(t.ty, 0) + #expect(t.tx == 0) + #expect(t.ty == 0) } - func testTransformFixOrientationDown() { + @Test func transformFixOrientationDown() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform(a: -1, b: 0, c: 0, d: -1, tx: 0, ty: 0) let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, size.width) - XCTAssertEqual(t.ty, size.height) + #expect(t.tx == size.width) + #expect(t.ty == size.height) } - func testTransformFixOrientationLeft() { + @Test func transformFixOrientationLeft() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform(a: 0, b: -1, c: 1, d: 0, tx: 0, ty: 0) let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, 0) - XCTAssertEqual(t.ty, size.width) + #expect(t.tx == 0) + #expect(t.ty == size.width) } - func testTransformFixOrientationRight() { + @Test func transformFixOrientationRight() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform(a: 0, b: 1, c: -1, d: 0, tx: 0, ty: 0) let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, size.height) - XCTAssertEqual(t.ty, 0) + #expect(t.tx == size.height) + #expect(t.ty == 0) } - func testTransformFixOrientationUpMirrored() { + @Test func transformFixOrientationUpMirrored() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform(a: -1, b: 0, c: 0, d: 1, tx: 0, ty: 0) let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, size.width) - XCTAssertEqual(t.ty, 0) + #expect(t.tx == size.width) + #expect(t.ty == 0) } - func testTransformFixOrientationDownMirrored() { + @Test func transformFixOrientationDownMirrored() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: 0) let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, 0) - XCTAssertEqual(t.ty, size.height) + #expect(t.tx == 0) + #expect(t.ty == size.height) } - func testTransformFixOrientationLeftMirrored() { + @Test func transformFixOrientationLeftMirrored() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform(a: 0, b: -1, c: -1, d: 0, tx: 0, ty: 0) let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, size.height) - XCTAssertEqual(t.ty, size.width) + #expect(t.tx == size.height) + #expect(t.ty == size.width) } - func testTransformFixOrientationRightMirrored() { + @Test func transformFixOrientationRightMirrored() { let size = CGSize(width: 800, height: 600) let naturalTransform = CGAffineTransform(a: 0, b: 1, c: 1, d: 0, tx: 0, ty: 0) let t = FVPGetStandardizedTrackTransform(naturalTransform, size) - XCTAssertEqual(t.tx, 0) - XCTAssertEqual(t.ty, 0) + #expect(t.tx == 0) + #expect(t.ty == 0) } #endif - func testSeekToleranceWhenNotSeekingToEnd() { + @Test func seekToleranceWhenNotSeekingToEnd() async { let inspectableAVPlayer = InspectableAVPlayer() let stubAVFactory = StubFVPAVFactory(player: inspectableAVPlayer) let player = FVPVideoPlayer( @@ -344,18 +340,13 @@ import XCTest let listener = StubEventListener() player.eventListener = listener - let seekExpectation = expectation( - description: "seekTo has zero tolerance when seeking not to end") - player.seek(to: 1234) { error in - seekExpectation.fulfill() - } + await asyncSeekTo(player: player, time: 1234) - waitForExpectations(timeout: 30.0) - XCTAssertEqual(inspectableAVPlayer.beforeTolerance?.intValue, 0) - XCTAssertEqual(inspectableAVPlayer.afterTolerance?.intValue, 0) + #expect(inspectableAVPlayer.beforeTolerance?.intValue == 0) + #expect(inspectableAVPlayer.afterTolerance?.intValue == 0) } - func testSeekToleranceWhenSeekingToEnd() { + @Test func seekToleranceWhenSeekingToEnd() async { let inspectableAVPlayer = InspectableAVPlayer() let stubAVFactory = StubFVPAVFactory(player: inspectableAVPlayer) let player = FVPVideoPlayer( @@ -365,56 +356,49 @@ import XCTest let listener = StubEventListener() player.eventListener = listener - let seekExpectation = expectation( - description: "seekTo has non-zero tolerance when seeking to end") - // The duration of this video is "0" due to the non standard initiliatazion process. - player.seek(to: 0) { error in - seekExpectation.fulfill() - } - waitForExpectations(timeout: 30.0) - XCTAssertGreaterThan(inspectableAVPlayer.beforeTolerance?.intValue ?? 0, 0) - XCTAssertGreaterThan(inspectableAVPlayer.afterTolerance?.intValue ?? 0, 0) + await asyncSeekTo(player: player, time: 0) + + #expect((inspectableAVPlayer.beforeTolerance?.intValue ?? 0) > 0) + #expect((inspectableAVPlayer.afterTolerance?.intValue ?? 0) > 0) } /// Sanity checks a video player playing the given URL with the actual AVPlayer. This is essentially /// a mini integration test of the player component. /// /// Returns the stub event listener to allow tests to inspect the call state. - func sanityTestURI(_ testURI: String) -> StubEventListener { + func sanityTestURI(_ testURI: String) async throws -> StubEventListener { let realObjectFactory = FVPDefaultAVFactory() - guard let testURL = URL(string: testURI) else { - XCTFail("Failed to create URL") - return StubEventListener() - } + let testURL = try #require(URL(string: testURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - let initializedExpectation = expectation(description: "initialized") - let listener = StubEventListener(initializationExpectation: initializedExpectation) - player.eventListener = listener - waitForExpectations(timeout: 30.0) + let listener = StubEventListener() + await withCheckedContinuation { initialized in + listener.initializationContinuation = initialized + player.eventListener = listener + } // Starts paused. let avPlayer = player.player - XCTAssertEqual(avPlayer.rate, 0) - XCTAssertEqual(avPlayer.volume, 1) - XCTAssertEqual(avPlayer.timeControlStatus, .paused) + #expect(avPlayer.rate == 0) + #expect(avPlayer.volume == 1) + #expect(avPlayer.timeControlStatus == .paused) // Change playback speed. var error: FlutterError? player.setPlaybackSpeed(2, error: &error) - XCTAssertNil(error) + #expect(error == nil) player.playWithError(&error) - XCTAssertNil(error) - XCTAssertEqual(avPlayer.rate, 2) - XCTAssertEqual(avPlayer.timeControlStatus, .waitingToPlayAtSpecifiedRate) + #expect(error == nil) + #expect(avPlayer.rate == 2) + #expect(avPlayer.timeControlStatus == .waitingToPlayAtSpecifiedRate) // Volume player.setVolume(0.1, error: &error) - XCTAssertNil(error) - XCTAssertEqual(avPlayer.volume, 0.1) + #expect(error == nil) + #expect(avPlayer.volume == 0.1) return listener } @@ -423,41 +407,45 @@ import XCTest // - https://github.com/flutter/flutter/issues/124937 // // Failing to de-register results in a crash in [AVPlayer willChangeValueForKey:]. - func testDoesNotCrashOnRateObservationAfterDisposal() { + @Test func doesNotCrashOnRateObservationAfterDisposal() async throws { let realObjectFactory = FVPDefaultAVFactory() var avPlayer: AVPlayer? = nil weak var weakPlayer: FVPVideoPlayer? = nil // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. - autoreleasepool { + try autoreleasepool { let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) - XCTAssertNotNil(identifiers) + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + #expect(error == nil) - let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer weakPlayer = player avPlayer = player.player player.disposeWithError(&error) - XCTAssertNil(error) + #expect(error == nil) } - let expectation = XCTestExpectation(description: "Object deallocated") - Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak weakPlayer] timer in + // Wait for the weak pointer to be invalidated, indicating that the player has been deallocated. + let checkInterval = 0.1 + let maxTries = Int64(30 / checkInterval) + for _ in 1...maxTries { if weakPlayer == nil { - timer.invalidate() - expectation.fulfill() + break } + try await Task.sleep(nanoseconds: UInt64(checkInterval * 1_000_000_000)) } - wait(for: [expectation], timeout: 10.0) - avPlayer?.willChangeValue(forKey: "rate") // No assertions needed. Lack of crash is a success. - avPlayer?.didChangeValue(forKey: "rate") + await MainActor.run { + avPlayer?.willChangeValue(forKey: "rate") + avPlayer?.didChangeValue(forKey: "rate") + } + // No assertions needed. Lack of crash is a success. } // During the hot reload: @@ -466,40 +454,42 @@ import XCTest // // Both of these methods dispatch [FVPVideoPlayer dispose] on the main thread // leading to a possible crash when de-registering observers twice. - func testHotReloadDoesNotCrash() { + @Test func hotReloadDoesNotCrash() async throws { weak var weakPlayer: FVPVideoPlayer? = nil // Autoreleasepool is needed to simulate conditions of FVPVideoPlayer deallocation. - autoreleasepool { + try autoreleasepool { let videoPlayerPlugin = createInitializedPlugin(avFactory: StubFVPAVFactory()) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) - XCTAssertNotNil(identifiers) + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + #expect(error == nil) let player = - videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPTextureBasedVideoPlayer weakPlayer = player player.onTextureUnregistered(StubTexture()) videoPlayerPlugin.initialize(&error) - XCTAssertNil(error) + #expect(error == nil) } - let expectation = XCTestExpectation(description: "Object deallocated") - Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak weakPlayer] timer in + // Wait for the weak pointer to be invalidated, indicating that the player has been deallocated. + let checkInterval = 0.1 + let maxTries = Int64(30 / checkInterval) + for _ in 1...maxTries { if weakPlayer == nil { - timer.invalidate() - expectation.fulfill() + break } + try await Task.sleep(nanoseconds: UInt64(checkInterval * 1_000_000_000)) } - wait(for: [expectation], timeout: 10.0) // No assertions needed. Lack of crash is a success. + // No assertions needed. Lack of crash is a success. } - func testFailedToLoadVideoEventShouldBeAlwaysSent() { + @Test func failedToLoadVideoEventShouldBeAlwaysSent() async { // Use real objects to test a real failure flow. let realObjectFactory = FVPDefaultAVFactory() let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) @@ -507,49 +497,43 @@ import XCTest var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( with: FVPCreationOptions.make(withUri: "", httpHeaders: [:]), error: &error) - XCTAssertNil(error) + #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer - let item = player.player.currentItem! - keyValueObservingExpectation(for: item, keyPath: "status") { _, change in - return item.status == .failed - } - waitForExpectations(timeout: 10.0) - - let failedExpectation = expectation(description: "failed") - // TODO(stuartmorgan): Update this test to instead use a mock listener, and add separate unit - // tests of FVPEventBridge. - let eventSink: FlutterEventSink = { event in - if event is FlutterError { - failedExpectation.fulfill() + await withCheckedContinuation { continuation in + // TODO(stuartmorgan): Update this test to instead use a mock listener, and add separate unit + // tests of FVPEventBridge. + let eventSink: FlutterEventSink = { event in + if event is FlutterError { + continuation.resume() + } } + (player.eventListener as? FlutterStreamHandler)?.onListen( + withArguments: nil, eventSink: eventSink) } - (player.eventListener as? FlutterStreamHandler)?.onListen( - withArguments: nil, eventSink: eventSink) - waitForExpectations(timeout: 10.0) } - func testUpdatePlayingStateShouldNotResetRate() { + @Test func updatePlayingStateShouldNotResetRate() async { let realObjectFactory = FVPDefaultAVFactory() let player = FVPVideoPlayer( playerItem: playerItem(with: URL(string: mp4TestURI)!, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - let initializedExpectation = expectation(description: "initialized") - let listener = StubEventListener(initializationExpectation: initializedExpectation) - player.eventListener = listener - waitForExpectations(timeout: 10) + await withCheckedContinuation { initialized in + let listener = StubEventListener(initializationContinuation: initialized) + player.eventListener = listener + } var error: FlutterError? player.setPlaybackSpeed(2, error: &error) - XCTAssertNil(error) + #expect(error == nil) player.playWithError(&error) - XCTAssertNil(error) - XCTAssertEqual(player.player.rate, 2) + #expect(error == nil) + #expect(player.player.rate == 2) } - func testPlayerShouldNotDropEverySecondFrame() { + @Test func playerShouldNotDropEverySecondFrame() { let textureRegistry = TestTextureRegistry() let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() @@ -561,7 +545,7 @@ import XCTest var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) + #expect(error == nil) let playerIdentifier = identifiers!.playerId let player = videoPlayerPlugin.playersByIdentifier[playerIdentifier] as! FVPTextureBasedVideoPlayer @@ -575,36 +559,38 @@ import XCTest addFrame() stubDisplayLinkFactory.fireDisplayLink?() player.copyPixelBuffer() - XCTAssertEqual(textureRegistry.textureFrameAvailableCount, 1) + #expect(textureRegistry.textureFrameAvailableCount == 1) addFrame() stubDisplayLinkFactory.fireDisplayLink?() player.copyPixelBuffer() - XCTAssertEqual(textureRegistry.textureFrameAvailableCount, 2) + #expect(textureRegistry.textureFrameAvailableCount == 2) } - func testVideoOutputIsAddedWhenAVPlayerItemBecomesReady() { + @Test func videoOutputIsAddedWhenAVPlayerIsInitialized() async throws { let realObjectFactory = FVPDefaultAVFactory() let videoPlayerPlugin = createInitializedPlugin(avFactory: realObjectFactory) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) - XCTAssertNil(error) - XCTAssertNotNil(identifiers) - let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPVideoPlayer + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + #expect(error == nil) + let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer - let item = player.player.currentItem! - keyValueObservingExpectation(for: item, keyPath: "status") { _, change in - return item.status == .readyToPlay + let listener = StubEventListener() + await withCheckedContinuation { initialized in + listener.initializationContinuation = initialized + player.eventListener = listener } - waitForExpectations(timeout: 10.0) + + let item = player.player.currentItem! // Video output is added as soon as the status becomes ready to play. - XCTAssertEqual(item.outputs.count, 1) + #expect(item.outputs.count == 1) } #if os(iOS) - func testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker() { + @Test func videoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker() { let stubFactory = StubFVPAVFactory() let audioSession = TestAudioSession() stubFactory.audioSession = audioSession @@ -614,16 +600,13 @@ import XCTest var error: FlutterError? videoPlayerPlugin.setMixWithOthers(true, error: &error) - XCTAssertNil(error) - XCTAssertEqual(audioSession.category, .playAndRecord, "Category should be PlayAndRecord.") - XCTAssertTrue( - audioSession.categoryOptions.contains(.defaultToSpeaker), - "Flag DefaultToSpeaker was removed.") - XCTAssertTrue( - audioSession.categoryOptions.contains(.mixWithOthers), "Flag MixWithOthers should be set.") + #expect(error == nil) + #expect(audioSession.category == .playAndRecord) + #expect(audioSession.categoryOptions.contains(.defaultToSpeaker)) + #expect(audioSession.categoryOptions.contains(.mixWithOthers)) } - func testSetMixWithOthersShouldNoOpWhenNoChangesAreRequired() { + @Test func setMixWithOthersShouldNoOpWhenNoChangesAreRequired() { let stubFactory = StubFVPAVFactory() let audioSession = TestAudioSession() stubFactory.audioSession = audioSession @@ -633,8 +616,8 @@ import XCTest var error: FlutterError? videoPlayerPlugin.setMixWithOthers(true, error: &error) - XCTAssertNil(error) - XCTAssertFalse(audioSession.setCategoryCalled) + #expect(error == nil) + #expect(!audioSession.setCategoryCalled) } #endif @@ -642,35 +625,33 @@ import XCTest // Tests getAudioTracks with a regular MP4 video file using real AVFoundation. // Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. - func testGetAudioTracksWithRealMP4Video() { + @Test func getAudioTracksWithRealMP4Video() async throws { let realObjectFactory = FVPDefaultAVFactory() let player = FVPVideoPlayer( playerItem: playerItem(with: URL(string: mp4TestURI)!, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - let initializedExpectation = expectation(description: "initialized") - let listener = StubEventListener(initializationExpectation: initializedExpectation) - player.eventListener = listener - waitForExpectations(timeout: 30.0) + await withCheckedContinuation { initialized in + let listener = StubEventListener(initializationContinuation: initialized) + player.eventListener = listener + } // Now test getAudioTracks var error: FlutterError? - let result = player.getAudioTracks(&error) - - XCTAssertNil(error) - XCTAssertNotNil(result) + let result = try #require(player.getAudioTracks(&error)) + #expect(error == nil) // Regular MP4 files do not have media selection groups for audio. // getAudioTracks only returns selectable audio tracks from HLS streams. - XCTAssertEqual(result?.count, 0) + #expect(result.count == 0) player.disposeWithError(&error) } // Tests getAudioTracks with an HLS stream using real AVFoundation. // HLS streams use media selection groups for audio track selection. - func testGetAudioTracksWithRealHLSStream() { + @Test func getAudioTracksWithRealHLSStream() async throws { let realObjectFactory = FVPDefaultAVFactory() let hlsURL = URL(string: hlsTestURI)! @@ -679,24 +660,22 @@ import XCTest avFactory: realObjectFactory, viewProvider: StubViewProvider()) - let initializedExpectation = expectation(description: "initialized") - let listener = StubEventListener(initializationExpectation: initializedExpectation) - player.eventListener = listener - waitForExpectations(timeout: 30.0) + await withCheckedContinuation { initialized in + let listener = StubEventListener(initializationContinuation: initialized) + player.eventListener = listener + } // Now test getAudioTracks var error: FlutterError? - let result = player.getAudioTracks(&error) - - XCTAssertNil(error) - XCTAssertNotNil(result) + let result = try #require(player.getAudioTracks(&error)) + #expect(error == nil) // For HLS streams with multiple audio options, we get media selection tracks. // The bee.m3u8 stream may or may not have multiple audio tracks. // We verify the method returns valid data without crashing. - for track in result ?? [] { - XCTAssertNotNil(track.displayName) - XCTAssertGreaterThanOrEqual(track.index, 0) + for track in result { + #expect(track.displayName != nil) + #expect(track.index >= 0) } player.disposeWithError(&error) @@ -704,7 +683,7 @@ import XCTest // Tests that getAudioTracks returns valid data for audio-only files. // Regular audio files do not have media selection groups, so getAudioTracks returns an empty array. - func testGetAudioTracksWithRealAudioFile() { + @Test func getAudioTracksWithRealAudioFile() 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() @@ -715,28 +694,26 @@ import XCTest avFactory: realObjectFactory, viewProvider: StubViewProvider()) - let initializedExpectation = expectation(description: "initialized") - let listener = StubEventListener(initializationExpectation: initializedExpectation) - player.eventListener = listener - waitForExpectations(timeout: 30.0) + await withCheckedContinuation { initialized in + let listener = StubEventListener(initializationContinuation: initialized) + player.eventListener = listener + } // Now test getAudioTracks var error: FlutterError? - let result = player.getAudioTracks(&error) - - XCTAssertNil(error) - XCTAssertNotNil(result) + let result = try #require(player.getAudioTracks(&error)) + #expect(error == nil) // Regular audio files do not have media selection groups. // getAudioTracks only returns selectable audio tracks from HLS streams. - XCTAssertEqual(result?.count, 0) + #expect(result.count == 0) player.disposeWithError(&error) } // Tests that getAudioTracks works correctly through the plugin API with a real video. // Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. - func testGetAudioTracksViaPluginWithRealVideo() { + @Test func getAudioTracksViaPluginWithRealVideo() 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() @@ -746,38 +723,35 @@ import XCTest avFactory: realObjectFactory, viewProvider: StubViewProvider()) - // Wait for player item to become ready - let item = player.player.currentItem! - keyValueObservingExpectation(for: item, keyPath: "status") { _, _ in - return item.status == .readyToPlay + // Wait for player to become ready + let listener = StubEventListener() + await withCheckedContinuation { initialized in + listener.initializationContinuation = initialized + player.eventListener = listener } - waitForExpectations(timeout: 30.0) // Now test getAudioTracks var error: FlutterError? - let result = player.getAudioTracks(&error) - - XCTAssertNil(error) - XCTAssertNotNil(result) + let result = try #require(player.getAudioTracks(&error)) + #expect(error == nil) // Regular MP4 files do not have media selection groups. // getAudioTracks only returns selectable audio tracks from HLS streams. - XCTAssertEqual(result?.count, 0) + #expect(result.count == 0) player.disposeWithError(&error) } - func testLoadTracksWithMediaTypeIsCalledOnNewerOS() { + @Test func loadTracksWithMediaTypeIsCalledOnNewerOS() { if #available(iOS 15.0, macOS 12.0, *) { let mockAsset = TestAsset(duration: CMTimeMake(value: 1, timescale: 1), tracks: []) let item = StubPlayerItem(asset: mockAsset) let stubAVFactory = StubFVPAVFactory(player: nil, playerItem: item, pixelBufferSource: nil) let stubViewProvider = StubViewProvider() - let player = FVPVideoPlayer( + let _ = FVPVideoPlayer( playerItem: item, avFactory: stubAVFactory, viewProvider: stubViewProvider) - XCTAssertNotNil(player) - XCTAssertTrue(mockAsset.loadedTracksAsynchronously) + #expect(mockAsset.loadedTracksAsynchronously) } } @@ -802,7 +776,7 @@ import XCTest assetProvider: assetProvider) var error: FlutterError? plugin.initialize(&error) - XCTAssertNil(error) + #expect(error == nil) return plugin } @@ -810,4 +784,33 @@ import XCTest let asset = factory.urlAsset(with: url, options: nil) return factory.playerItem(with: asset) } + + private func waitForPlayerItemStatus(_ item: AVPlayerItem, state: AVPlayerItem.Status) async { + await withCheckedContinuation { continuation in + // Check whether it already has the desired status. + if item.status == state { + continuation.resume() + return + } + // If not, wait for that status. + var observation: NSKeyValueObservation? + observation = item.observe(\.status, options: [.initial, .new]) { + [observation = observation] _, change in + if change.newValue == state { + observation?.invalidate() + continuation.resume() + } + } + } + } + + // Temporary test adapter until the player implementation is converted to use async. + private func asyncSeekTo(player: FVPVideoPlayer, time: Int) async { + await withCheckedContinuation { continuation in + player.seek(to: time) { error in + #expect(error == nil) + continuation.resume() + } + } + } } From b8b088fa4f0deeb519f227ce3f27c56a88bb1ce2 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Feb 2026 14:34:29 -0500 Subject: [PATCH 09/16] Version bump --- packages/video_player/video_player_avfoundation/CHANGELOG.md | 4 ++++ packages/video_player/video_player_avfoundation/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 72fb28c16fdb..e8dcfaca7b98 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.9.2 + +* Refactors for improved testability. + ## 2.9.1 * Refactors native code for improved testability. diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index f08f42f54282..a8ecbe43c041 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.1 +version: 2.9.2 environment: sdk: ^3.10.0 From e1a2365405a1c496329e3958ce7e44ab664c1056 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 9 Feb 2026 14:42:34 -0500 Subject: [PATCH 10/16] Update macOS --- .../darwin/RunnerTests/VideoPlayerTests.swift | 8 +++++++- .../example/macos/Runner.xcodeproj/project.pbxproj | 14 +++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) 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 4613f9d62af5..989320ca11af 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -3,10 +3,16 @@ // found in the LICENSE file. import AVFoundation -import Flutter import Testing @preconcurrency import video_player_avfoundation +#if os(iOS) + import Flutter +#else + import FlutterMacOS +#endif + + @MainActor struct VideoPlayerTests { let mp4TestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" diff --git a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj index 1f82aadcd22a..4f7aa78b7520 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -23,7 +23,8 @@ /* Begin PBXBuildFile section */ 18AD5E2A5B24DAFCF3749529 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FA146FB7E9341C7D2257338 /* Pods_RunnerTests.framework */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33683FF12ABCAC94007305E4 /* VideoPlayerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33683FF02ABCAC94007305E4 /* VideoPlayerTests.m */; }; + 33C53DEF2F3A6F65008AFBB3 /* TestClasses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33C53DED2F3A6F65008AFBB3 /* TestClasses.swift */; }; + 33C53DF02F3A6F65008AFBB3 /* VideoPlayerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33C53DEE2F3A6F65008AFBB3 /* VideoPlayerTests.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; @@ -68,7 +69,8 @@ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33683FF02ABCAC94007305E4 /* VideoPlayerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VideoPlayerTests.m; path = ../../darwin/RunnerTests/VideoPlayerTests.m; sourceTree = SOURCE_ROOT; }; + 33C53DED2F3A6F65008AFBB3 /* TestClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClasses.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift; sourceTree = ""; }; + 33C53DEE2F3A6F65008AFBB3 /* VideoPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerTests.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* video_player_avfoundation_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = video_player_avfoundation_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; @@ -115,7 +117,8 @@ 330B3F8E2B1F9C6A00E6DC3F /* RunnerTests */ = { isa = PBXGroup; children = ( - 33683FF02ABCAC94007305E4 /* VideoPlayerTests.m */, + 33C53DEE2F3A6F65008AFBB3 /* VideoPlayerTests.swift */, + 33C53DED2F3A6F65008AFBB3 /* TestClasses.swift */, ); name = RunnerTests; sourceTree = ""; @@ -268,7 +271,7 @@ TargetAttributes = { 331C80D4294CF70F00263BE5 = { CreatedOnToolsVersion = 14.0; - LastSwiftMigration = 1500; + LastSwiftMigration = 2620; TestTargetID = 33CC10EC2044A3C60003C045; }; 33CC10EC2044A3C60003C045 = { @@ -436,7 +439,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 33683FF12ABCAC94007305E4 /* VideoPlayerTests.m in Sources */, + 33C53DEF2F3A6F65008AFBB3 /* TestClasses.swift in Sources */, + 33C53DF02F3A6F65008AFBB3 /* VideoPlayerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From f168e1fea9683eb70b281c39c525638fbebeba41 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 10 Feb 2026 14:51:30 -0500 Subject: [PATCH 11/16] swift-format --- .../darwin/RunnerTests/VideoPlayerTests.swift | 1 - 1 file changed, 1 deletion(-) 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 989320ca11af..5c455761e989 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -12,7 +12,6 @@ import Testing import FlutterMacOS #endif - @MainActor struct VideoPlayerTests { let mp4TestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" From 60ba92d863bfa04856c528d23d40c01d4ce66d16 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 10 Feb 2026 14:54:32 -0500 Subject: [PATCH 12/16] Fix test path types in project --- .../example/ios/Runner.xcodeproj/project.pbxproj | 4 ++-- .../example/macos/Runner.xcodeproj/project.pbxproj | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj index 2990cd2365ab..54ab55a992ce 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -58,8 +58,8 @@ 20721C28387E1F78689EC502 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 2A2EA522BDC492279A91AB75 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331585C42F36433100FACB51 /* VideoPlayerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerUITests.swift; sourceTree = ""; }; - 335542AC2F364D080081D0DC /* VideoPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerTests.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift; sourceTree = ""; }; - 335542AF2F366B4F0081D0DC /* TestClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClasses.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift; sourceTree = ""; }; + 335542AC2F364D080081D0DC /* VideoPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerTests.swift; path = ../../darwin/RunnerTests/VideoPlayerTests.swift; sourceTree = SOURCE_ROOT; }; + 335542AF2F366B4F0081D0DC /* TestClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClasses.swift; path = ../../darwin/RunnerTests/TestClasses.swift; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 6CDC4DA5940705A6E7671616 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; diff --git a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj index 4f7aa78b7520..c5031043947e 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXAggregateTarget section */ @@ -69,8 +69,8 @@ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33C53DED2F3A6F65008AFBB3 /* TestClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClasses.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift; sourceTree = ""; }; - 33C53DEE2F3A6F65008AFBB3 /* VideoPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerTests.swift; path = /Users/stuartmorgan/src/packages/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift; sourceTree = ""; }; + 33C53DED2F3A6F65008AFBB3 /* TestClasses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TestClasses.swift; path = ../../darwin/RunnerTests/TestClasses.swift; sourceTree = SOURCE_ROOT; }; + 33C53DEE2F3A6F65008AFBB3 /* VideoPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerTests.swift; path = ../../darwin/RunnerTests/VideoPlayerTests.swift; sourceTree = SOURCE_ROOT; }; 33CC10ED2044A3C60003C045 /* video_player_avfoundation_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = video_player_avfoundation_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; @@ -300,7 +300,7 @@ ); mainGroup = 33CC10E42044A3C60003C045; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; @@ -379,10 +379,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -804,7 +808,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; From 8edd46a0be1a30979ec838dd6d3c198b412f1f63 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 10 Feb 2026 16:45:48 -0500 Subject: [PATCH 13/16] Adjust XCUITest setup to avoid warning --- .../ios/RunnerUITests/VideoPlayerUITests.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.swift b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.swift index 39a26bd1fd24..dba15f69c09d 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.swift +++ b/packages/video_player/video_player_avfoundation/example/ios/RunnerUITests/VideoPlayerUITests.swift @@ -6,18 +6,16 @@ import XCTest @MainActor class VideoPlayerUITests: XCTestCase { - var app: XCUIApplication! - override func setUp() async throws { - try await super.setUp() + override func setUp() { + super.setUp() continueAfterFailure = false - - app = XCUIApplication() - app.launch() } func testPlayVideo() { - let app = self.app! + let app = XCUIApplication() + app.launch() + let remoteTab = app.otherElements.matching(NSPredicate(format: "selected == YES")).element( boundBy: 0) XCTAssertTrue(remoteTab.waitForExistence(timeout: 30.0)) From ac3a09f5ec02f7862e1ccfcf76568f0c47542609 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 11 Feb 2026 11:26:16 -0500 Subject: [PATCH 14/16] Review feedback --- .../darwin/RunnerTests/TestClasses.swift | 57 ++++---- .../darwin/RunnerTests/VideoPlayerTests.swift | 131 ++++++++++-------- .../example/ios/Flutter/Debug.xcconfig | 2 +- .../example/ios/Flutter/Release.xcconfig | 2 +- 4 files changed, 104 insertions(+), 88 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift index a151ca49587b..3856995b855c 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift @@ -15,13 +15,15 @@ import video_player_avfoundation /// An AVPlayer subclass that records method call parameters for inspection. // TODO(stuartmorgan): Replace with a protocol like the other classes. -@MainActor class InspectableAVPlayer: AVPlayer { +@MainActor final class InspectableAVPlayer: AVPlayer { private(set) nonisolated(unsafe) var beforeTolerance: NSNumber? private(set) nonisolated(unsafe) var afterTolerance: NSNumber? private(set) nonisolated(unsafe) var lastSeekTime: CMTime = .invalid override func seek( - to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime, + to time: CMTime, + toleranceBefore: CMTime, + toleranceAfter: CMTime, completionHandler: @escaping @Sendable (Bool) -> Void ) { beforeTolerance = NSNumber(value: toleranceBefore.value) @@ -33,7 +35,7 @@ import video_player_avfoundation } } -class TestAsset: NSObject, FVPAVAsset { +final class TestAsset: NSObject, FVPAVAsset { let duration: CMTime let tracks: [AVAssetTrack]? @@ -67,7 +69,7 @@ class TestAsset: NSObject, FVPAVAsset { } } -class StubPlayerItem: NSObject, FVPAVPlayerItem { +final class StubPlayerItem: NSObject, FVPAVPlayerItem { let asset: FVPAVAsset var videoComposition: AVVideoComposition? @@ -77,20 +79,23 @@ class StubPlayerItem: NSObject, FVPAVPlayerItem { } } -class StubBinaryMessenger: NSObject, FlutterBinaryMessenger { +final class StubBinaryMessenger: NSObject, FlutterBinaryMessenger { func send(onChannel channel: String, message: Data?) {} func send( - onChannel channel: String, message: Data?, binaryReply callback: FlutterBinaryReply? = nil + onChannel channel: String, + message: Data?, + binaryReply callback: FlutterBinaryReply? = nil ) {} func setMessageHandlerOnChannel( - _ channel: String, binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil + _ channel: String, + binaryMessageHandler handler: FlutterBinaryMessageHandler? = nil ) -> FlutterBinaryMessengerConnection { return 0 } func cleanUpConnection(_ connection: FlutterBinaryMessengerConnection) {} } -class TestTextureRegistry: NSObject, FlutterTextureRegistry { +final class TestTextureRegistry: NSObject, FlutterTextureRegistry { private(set) var registeredTexture = false private(set) var unregisteredTexture = false private(set) var textureFrameAvailableCount = 0 @@ -115,7 +120,7 @@ class TestTextureRegistry: NSObject, FlutterTextureRegistry { } } -class StubViewProvider: NSObject, FVPViewProvider { +final class StubViewProvider: NSObject, FVPViewProvider { #if os(iOS) var viewController: UIViewController? init(viewController: UIViewController? = nil) { @@ -131,7 +136,7 @@ class StubViewProvider: NSObject, FVPViewProvider { #endif } -class StubAssetProvider: NSObject, FVPAssetProvider { +final class StubAssetProvider: NSObject, FVPAssetProvider { func lookupKey(forAsset asset: String) -> String? { return asset } @@ -141,7 +146,7 @@ class StubAssetProvider: NSObject, FVPAssetProvider { } } -class TestPixelBufferSource: NSObject, FVPPixelBufferSource { +final class TestPixelBufferSource: NSObject, FVPPixelBufferSource { var pixelBuffer: CVPixelBuffer? let videoOutput: AVPlayerItemVideoOutput @@ -162,7 +167,8 @@ class TestPixelBufferSource: NSObject, FVPPixelBufferSource { } func copyPixelBuffer( - forItemTime itemTime: CMTime, itemTimeForDisplay: UnsafeMutablePointer? + forItemTime itemTime: CMTime, + itemTimeForDisplay: UnsafeMutablePointer? ) -> CVPixelBuffer? { let buffer = pixelBuffer // Ownership is transferred to the caller. @@ -172,7 +178,7 @@ class TestPixelBufferSource: NSObject, FVPPixelBufferSource { } #if os(iOS) - class TestAudioSession: NSObject, FVPAVAudioSession { + final class TestAudioSession: NSObject, FVPAVAudioSession { var category: AVAudioSession.Category = .ambient var categoryOptions: AVAudioSession.CategoryOptions = [] private(set) var setCategoryCalled = false @@ -188,7 +194,7 @@ class TestPixelBufferSource: NSObject, FVPPixelBufferSource { } #endif -class StubFVPAVFactory: NSObject, FVPAVFactory { +final class StubFVPAVFactory: NSObject, FVPAVFactory { let player: AVPlayer let playerItem: FVPAVPlayerItem let pixelBufferSource: FVPPixelBufferSource? @@ -236,32 +242,33 @@ class StubFVPAVFactory: NSObject, FVPAVFactory { #endif } -class StubFVPDisplayLink: NSObject, FVPDisplayLink { +final class StubFVPDisplayLink: NSObject, FVPDisplayLink { var running: Bool = false var duration: CFTimeInterval { return 1.0 / 60.0 } } -class StubFVPDisplayLinkFactory: NSObject, FVPDisplayLinkFactory { +final class StubFVPDisplayLinkFactory: NSObject, FVPDisplayLinkFactory { let displayLink = StubFVPDisplayLink() var fireDisplayLink: (() -> Void)? - func displayLink(with viewProvider: FVPViewProvider, callback: @escaping () -> Void) - -> FVPDisplayLink - { + func displayLink( + with viewProvider: FVPViewProvider, + callback: @escaping () -> Void + ) -> FVPDisplayLink { fireDisplayLink = callback return displayLink } } -class StubEventListener: NSObject, FVPVideoEventListener { - var initializationContinuation: CheckedContinuation? +final class StubEventListener: NSObject, FVPVideoEventListener { + var onInitialized: (() -> Void)? private(set) var initializationDuration: Int64 = 0 private(set) var initializationSize: CGSize = .zero - init(initializationContinuation: CheckedContinuation? = nil) { - self.initializationContinuation = initializationContinuation + init(onInitialized: (() -> Void)? = nil) { + self.onInitialized = onInitialized super.init() } @@ -269,7 +276,7 @@ class StubEventListener: NSObject, FVPVideoEventListener { func videoPlayerDidEndBuffering() {} func videoPlayerDidError(withMessage errorMessage: String) {} func videoPlayerDidInitialize(withDuration duration: Int64, size: CGSize) { - initializationContinuation?.resume() + onInitialized?() initializationDuration = duration initializationSize = size } @@ -279,7 +286,7 @@ class StubEventListener: NSObject, FVPVideoEventListener { func videoPlayerWasDisposed() {} } -class StubTexture: NSObject, FlutterTexture { +final class StubTexture: NSObject, FlutterTexture { func copyPixelBuffer() -> Unmanaged? { return nil } 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 5c455761e989..4ba3eede99d8 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -14,10 +14,13 @@ import Testing @MainActor struct VideoPlayerTests { - let mp4TestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - let hlsTestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" - let mp3AudioTestURI = "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3" - let hlsAudioTestURI = + private static let mp4TestURI = + "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + private static let hlsTestURI = + "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" + private static let mp3AudioTestURI = + "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3" + private static let hlsAudioTestURI = "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee_audio_only.m3u8" @Test func blankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream() @@ -43,7 +46,8 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPTextureBasedVideoPlayer @@ -60,7 +64,8 @@ import Testing var error: FlutterError? videoPlayerPlugin.createPlatformViewPlayer( - with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + error: &error) #expect(error == nil) #expect(!textureRegistry.registeredTexture) @@ -78,7 +83,8 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error)) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPTextureBasedVideoPlayer @@ -87,7 +93,7 @@ import Testing player.pauseWithError(&error) #expect(error == nil) - await asyncSeekTo(player: player, time: 1234) + error = await player.seek(to: 1234) // Seeking to a new position should start the display link temporarily. #expect(stubDisplayLinkFactory.displayLink.running) @@ -103,7 +109,7 @@ import Testing #expect(!stubDisplayLinkFactory.displayLink.running) } - @Test func initStartsDisplayLinkTemporarily() { + @Test func initStartsDisplayLinkTemporarily() throws { let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() let videoPlayerPlugin = createInitializedPlugin( @@ -111,8 +117,10 @@ import Testing displayLinkFactory: stubDisplayLinkFactory) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) // Init should start the display link temporarily. @@ -124,7 +132,7 @@ import Testing mockVideoOutput.pixelBuffer = bufferRef // Simulate a callback from the engine to request a new frame. let player = - videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer + videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPTextureBasedVideoPlayer stubDisplayLinkFactory.fireDisplayLink?() player.copyPixelBuffer() // Since a frame was found, and the video is paused, the display link should be paused again. @@ -140,7 +148,8 @@ import Testing var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + error: &error) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer @@ -149,7 +158,7 @@ import Testing player.playWithError(&error) #expect(error == nil) - await asyncSeekTo(player: player, time: 1234) + error = await player.seek(to: 1234) #expect(stubDisplayLinkFactory.displayLink.running) @@ -171,7 +180,8 @@ import Testing var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + error: &error) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers!.playerId] as! FVPTextureBasedVideoPlayer @@ -192,7 +202,8 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer @@ -210,7 +221,8 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer let avPlayer = player.player @@ -241,21 +253,21 @@ import Testing } @Test func videoControls() async throws { - let eventListener = try await sanityTestURI(mp4TestURI) + let eventListener = try await sanityTestURI(VideoPlayerTests.mp4TestURI) #expect(eventListener.initializationSize.height == 720) #expect(eventListener.initializationSize.width == 1280) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) } @Test func audioControls() async throws { - let eventListener = try await sanityTestURI(mp3AudioTestURI) + let eventListener = try await sanityTestURI(VideoPlayerTests.mp3AudioTestURI) #expect(eventListener.initializationSize.height == 0) #expect(eventListener.initializationSize.width == 0) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 5400, tolerance: 200)) } @Test func hLSControls() async throws { - let eventListener = try await sanityTestURI(hlsTestURI) + let eventListener = try await sanityTestURI(VideoPlayerTests.hlsTestURI) #expect(eventListener.initializationSize.height == 720) #expect(eventListener.initializationSize.width == 1280) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) @@ -263,7 +275,7 @@ import Testing @Test(.disabled("Flaky"), .bug("https://github.com/flutter/flutter/issues/164381")) func audioOnlyHLSControls() async throws { - let eventListener = try await sanityTestURI(hlsAudioTestURI) + let eventListener = try await sanityTestURI(VideoPlayerTests.hlsAudioTestURI) #expect(eventListener.initializationSize.height == 0) #expect(eventListener.initializationSize.width == 0) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) @@ -345,7 +357,7 @@ import Testing let listener = StubEventListener() player.eventListener = listener - await asyncSeekTo(player: player, time: 1234) + await player.seek(to: 1234) #expect(inspectableAVPlayer.beforeTolerance?.intValue == 0) #expect(inspectableAVPlayer.afterTolerance?.intValue == 0) @@ -361,7 +373,7 @@ import Testing let listener = StubEventListener() player.eventListener = listener - await asyncSeekTo(player: player, time: 0) + await player.seek(to: 0) #expect((inspectableAVPlayer.beforeTolerance?.intValue ?? 0) > 0) #expect((inspectableAVPlayer.afterTolerance?.intValue ?? 0) > 0) @@ -380,8 +392,8 @@ import Testing viewProvider: StubViewProvider()) let listener = StubEventListener() - await withCheckedContinuation { initialized in - listener.initializationContinuation = initialized + await withCheckedContinuation { continuation in + listener.onInitialized = { continuation.resume() } player.eventListener = listener } @@ -425,7 +437,8 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer @@ -469,7 +482,8 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) let player = @@ -518,15 +532,16 @@ import Testing } } - @Test func updatePlayingStateShouldNotResetRate() async { + @Test func updatePlayingStateShouldNotResetRate() async throws { let realObjectFactory = FVPDefaultAVFactory() + let testURL = try #require(URL(string: VideoPlayerTests.mp4TestURI)) let player = FVPVideoPlayer( - playerItem: playerItem(with: URL(string: mp4TestURI)!, factory: realObjectFactory), + playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - await withCheckedContinuation { initialized in - let listener = StubEventListener(initializationContinuation: initialized) + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) player.eventListener = listener } @@ -538,7 +553,7 @@ import Testing #expect(player.player.rate == 2) } - @Test func playerShouldNotDropEverySecondFrame() { + @Test func playerShouldNotDropEverySecondFrame() throws { let textureRegistry = TestTextureRegistry() let stubDisplayLinkFactory = StubFVPDisplayLinkFactory() let mockVideoOutput = TestPixelBufferSource() @@ -548,10 +563,12 @@ import Testing textureRegistry: textureRegistry) var error: FlutterError? - let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error) + let identifiers = try #require( + videoPlayerPlugin.createTexturePlayer( + with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) - let playerIdentifier = identifiers!.playerId + let playerIdentifier = identifiers.playerId let player = videoPlayerPlugin.playersByIdentifier[playerIdentifier] as! FVPTextureBasedVideoPlayer @@ -579,17 +596,18 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) + with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer let listener = StubEventListener() - await withCheckedContinuation { initialized in - listener.initializationContinuation = initialized + await withCheckedContinuation { continuation in + listener.onInitialized = { continuation.resume() } player.eventListener = listener } - let item = player.player.currentItem! + let item = try #require(player.player.currentItem) // Video output is added as soon as the status becomes ready to play. #expect(item.outputs.count == 1) } @@ -632,13 +650,14 @@ import Testing // Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. @Test func getAudioTracksWithRealMP4Video() async throws { let realObjectFactory = FVPDefaultAVFactory() + let testURL = try #require(URL(string: VideoPlayerTests.mp4TestURI)) let player = FVPVideoPlayer( - playerItem: playerItem(with: URL(string: mp4TestURI)!, factory: realObjectFactory), + playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - await withCheckedContinuation { initialized in - let listener = StubEventListener(initializationContinuation: initialized) + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) player.eventListener = listener } @@ -658,15 +677,15 @@ import Testing // HLS streams use media selection groups for audio track selection. @Test func getAudioTracksWithRealHLSStream() async throws { let realObjectFactory = FVPDefaultAVFactory() - let hlsURL = URL(string: hlsTestURI)! + let hlsURL = try #require(URL(string: VideoPlayerTests.hlsTestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: hlsURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - await withCheckedContinuation { initialized in - let listener = StubEventListener(initializationContinuation: initialized) + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) player.eventListener = listener } @@ -692,15 +711,15 @@ import Testing // 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 audioURL = URL(string: mp3AudioTestURI)! + let audioURL = try #require(URL(string: VideoPlayerTests.mp3AudioTestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: audioURL, factory: realObjectFactory), avFactory: realObjectFactory, viewProvider: StubViewProvider()) - await withCheckedContinuation { initialized in - let listener = StubEventListener(initializationContinuation: initialized) + await withCheckedContinuation { continuation in + let listener = StubEventListener(onInitialized: { continuation.resume() }) player.eventListener = listener } @@ -722,7 +741,7 @@ import Testing // 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 = URL(string: mp4TestURI)! + let testURL = try #require(URL(string: VideoPlayerTests.mp4TestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, @@ -730,8 +749,8 @@ import Testing // Wait for player to become ready let listener = StubEventListener() - await withCheckedContinuation { initialized in - listener.initializationContinuation = initialized + await withCheckedContinuation { continuation in + listener.onInitialized = { continuation.resume() } player.eventListener = listener } @@ -808,14 +827,4 @@ import Testing } } } - - // Temporary test adapter until the player implementation is converted to use async. - private func asyncSeekTo(player: FVPVideoPlayer, time: Int) async { - await withCheckedContinuation { continuation in - player.seek(to: time) { error in - #expect(error == nil) - continuation.resume() - } - } - } } diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig index e8efba114687..ec97fc6f3021 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig index 399e9340e6f6..c4855bfe2000 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig +++ b/packages/video_player/video_player_avfoundation/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" From 016c7544a7a5081921079b64d43f2c2a103716f7 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 11 Feb 2026 12:49:30 -0500 Subject: [PATCH 15/16] Restore the manual seek wrapper --- .../darwin/RunnerTests/VideoPlayerTests.swift | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) 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 4ba3eede99d8..9be7a4ee8cc0 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -93,7 +93,7 @@ import Testing player.pauseWithError(&error) #expect(error == nil) - error = await player.seek(to: 1234) + await asyncSeekTo(player: player, time: 1234) // Seeking to a new position should start the display link temporarily. #expect(stubDisplayLinkFactory.displayLink.running) @@ -158,7 +158,7 @@ import Testing player.playWithError(&error) #expect(error == nil) - error = await player.seek(to: 1234) + await asyncSeekTo(player: player, time: 1234) #expect(stubDisplayLinkFactory.displayLink.running) @@ -357,7 +357,7 @@ import Testing let listener = StubEventListener() player.eventListener = listener - await player.seek(to: 1234) + await asyncSeekTo(player: player, time: 1234) #expect(inspectableAVPlayer.beforeTolerance?.intValue == 0) #expect(inspectableAVPlayer.afterTolerance?.intValue == 0) @@ -373,7 +373,7 @@ import Testing let listener = StubEventListener() player.eventListener = listener - await player.seek(to: 0) + await asyncSeekTo(player: player, time: 0) #expect((inspectableAVPlayer.beforeTolerance?.intValue ?? 0) > 0) #expect((inspectableAVPlayer.afterTolerance?.intValue ?? 0) > 0) @@ -827,4 +827,18 @@ import Testing } } } + + // TODO(stuartmorgan): Remove this in favor of just `await player.seek(...)` once + // Pigeon is generating Swift 6-friendly output. Currently using the automatic async + // conversion generates warnings due to the lack of concurrency annotations + // ("non-sendable type 'FlutterError?' returned by implicitly asynchronous call to + // nonisolated function cannot cross actor boundary"). + private func asyncSeekTo(player: FVPVideoPlayer, time: Int) async { + await withCheckedContinuation { continuation in + player.seek(to: time) { error in + #expect(error == nil) + continuation.resume() + } + } + } } From 4cd99e795548ceea28388ccae3f893334e369f94 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 11 Feb 2026 12:51:45 -0500 Subject: [PATCH 16/16] Use fileprivate unscoped constants --- .../darwin/RunnerTests/VideoPlayerTests.swift | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) 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 9be7a4ee8cc0..14d46abb2c5a 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.swift @@ -12,16 +12,16 @@ import Testing import FlutterMacOS #endif -@MainActor struct VideoPlayerTests { +private let mp4TestURI = + "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" +private let hlsTestURI = + "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" +private let mp3AudioTestURI = + "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3" +private let hlsAudioTestURI = + "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee_audio_only.m3u8" - private static let mp4TestURI = - "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" - private static let hlsTestURI = - "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" - private static let mp3AudioTestURI = - "https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3" - private static let hlsAudioTestURI = - "https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee_audio_only.m3u8" +@MainActor struct VideoPlayerTests { @Test func blankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream() throws @@ -46,7 +46,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) let player = @@ -64,7 +64,7 @@ import Testing var error: FlutterError? videoPlayerPlugin.createPlatformViewPlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) #expect(error == nil) @@ -83,7 +83,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) let player = @@ -119,7 +119,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) @@ -148,7 +148,7 @@ import Testing var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) #expect(error == nil) let player = @@ -180,7 +180,7 @@ import Testing var error: FlutterError? let identifiers = videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.hlsTestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: hlsTestURI, httpHeaders: [:]), error: &error) #expect(error == nil) let player = @@ -202,7 +202,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer @@ -221,7 +221,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer @@ -253,21 +253,21 @@ import Testing } @Test func videoControls() async throws { - let eventListener = try await sanityTestURI(VideoPlayerTests.mp4TestURI) + let eventListener = try await sanityTestURI(mp4TestURI) #expect(eventListener.initializationSize.height == 720) #expect(eventListener.initializationSize.width == 1280) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) } @Test func audioControls() async throws { - let eventListener = try await sanityTestURI(VideoPlayerTests.mp3AudioTestURI) + let eventListener = try await sanityTestURI(mp3AudioTestURI) #expect(eventListener.initializationSize.height == 0) #expect(eventListener.initializationSize.width == 0) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 5400, tolerance: 200)) } @Test func hLSControls() async throws { - let eventListener = try await sanityTestURI(VideoPlayerTests.hlsTestURI) + let eventListener = try await sanityTestURI(hlsTestURI) #expect(eventListener.initializationSize.height == 720) #expect(eventListener.initializationSize.width == 1280) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) @@ -275,7 +275,7 @@ import Testing @Test(.disabled("Flaky"), .bug("https://github.com/flutter/flutter/issues/164381")) func audioOnlyHLSControls() async throws { - let eventListener = try await sanityTestURI(VideoPlayerTests.hlsAudioTestURI) + let eventListener = try await sanityTestURI(hlsAudioTestURI) #expect(eventListener.initializationSize.height == 0) #expect(eventListener.initializationSize.width == 0) #expect(durationApproximatelyEquals(eventListener.initializationDuration, 4000, tolerance: 200)) @@ -437,7 +437,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) @@ -482,7 +482,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) @@ -534,7 +534,7 @@ import Testing @Test func updatePlayingStateShouldNotResetRate() async throws { let realObjectFactory = FVPDefaultAVFactory() - let testURL = try #require(URL(string: VideoPlayerTests.mp4TestURI)) + let testURL = try #require(URL(string: mp4TestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, @@ -565,7 +565,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) let playerIdentifier = identifiers.playerId @@ -596,7 +596,7 @@ import Testing var error: FlutterError? let identifiers = try #require( videoPlayerPlugin.createTexturePlayer( - with: FVPCreationOptions.make(withUri: VideoPlayerTests.mp4TestURI, httpHeaders: [:]), + with: FVPCreationOptions.make(withUri: mp4TestURI, httpHeaders: [:]), error: &error)) #expect(error == nil) let player = videoPlayerPlugin.playersByIdentifier[identifiers.playerId] as! FVPVideoPlayer @@ -650,7 +650,7 @@ import Testing // Regular MP4 files do not have media selection groups, so getAudioTracks returns an empty array. @Test func getAudioTracksWithRealMP4Video() async throws { let realObjectFactory = FVPDefaultAVFactory() - let testURL = try #require(URL(string: VideoPlayerTests.mp4TestURI)) + let testURL = try #require(URL(string: mp4TestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory, @@ -677,7 +677,7 @@ import Testing // HLS streams use media selection groups for audio track selection. @Test func getAudioTracksWithRealHLSStream() async throws { let realObjectFactory = FVPDefaultAVFactory() - let hlsURL = try #require(URL(string: VideoPlayerTests.hlsTestURI)) + let hlsURL = try #require(URL(string: hlsTestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: hlsURL, factory: realObjectFactory), @@ -711,7 +711,7 @@ import Testing // 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 audioURL = try #require(URL(string: VideoPlayerTests.mp3AudioTestURI)) + let audioURL = try #require(URL(string: mp3AudioTestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: audioURL, factory: realObjectFactory), @@ -741,7 +741,7 @@ import Testing // 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: VideoPlayerTests.mp4TestURI)) + let testURL = try #require(URL(string: mp4TestURI)) let player = FVPVideoPlayer( playerItem: playerItem(with: testURL, factory: realObjectFactory), avFactory: realObjectFactory,