-
Notifications
You must be signed in to change notification settings - Fork 3.6k
[video_player] Convert iOS unit tests to Swift #10989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
auto-submit
merged 16 commits into
flutter:main
from
stuartmorgan-g:video-player-swift-tests
Feb 11, 2026
+1,260
−1,529
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
72b791f
Convert XCUITest to Swift
stuartmorgan-g 61d847c
Remove sketchy subclassing of AVAssetTrack
stuartmorgan-g 87c5071
Convert unit tests to Swift, mostly directly
stuartmorgan-g 58824d1
Consolidate test URIs
stuartmorgan-g 53d8397
Remove unnecesasry assertions
stuartmorgan-g 7e9edc7
Add a helper to reduce boilerplate
stuartmorgan-g 244ec5c
Inline creation arguments to reduce variable clutter
stuartmorgan-g ec4b403
Convert to Swift Testing
stuartmorgan-g b8b088f
Version bump
stuartmorgan-g e1a2365
Update macOS
stuartmorgan-g f168e1f
swift-format
stuartmorgan-g 60ba92d
Fix test path types in project
stuartmorgan-g 8edd46a
Adjust XCUITest setup to avoid warning
stuartmorgan-g ac3a09f
Review feedback
stuartmorgan-g 016c754
Restore the manual seek wrapper
stuartmorgan-g 4cd99e7
Use fileprivate unscoped constants
stuartmorgan-g File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,7 @@ | ||
| ## 2.9.2 | ||
|
|
||
| * Refactors for improved testability. | ||
|
|
||
| ## 2.9.1 | ||
|
|
||
| * Refactors native code for improved testability. | ||
|
|
||
293 changes: 293 additions & 0 deletions
293
packages/video_player/video_player_avfoundation/darwin/RunnerTests/TestClasses.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,293 @@ | ||
| // 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 Testing | ||
| 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 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, | ||
| 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) | ||
| } | ||
| } | ||
|
|
||
| final 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 ?? [] | ||
| } | ||
| } | ||
|
|
||
| final class StubPlayerItem: NSObject, FVPAVPlayerItem { | ||
| let asset: FVPAVAsset | ||
| var videoComposition: AVVideoComposition? | ||
|
|
||
| init(asset: FVPAVAsset = TestAsset()) { | ||
| self.asset = asset | ||
| super.init() | ||
| } | ||
| } | ||
|
|
||
| final 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) {} | ||
| } | ||
|
|
||
| final 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 { | ||
| Issue.record("Unregistering texture with wrong ID") | ||
| } | ||
| unregisteredTexture = true | ||
| } | ||
|
|
||
| func textureFrameAvailable(_ textureId: Int64) { | ||
| if textureId != 1 { | ||
| Issue.record("Texture frame available with wrong ID") | ||
| } | ||
| textureFrameAvailableCount += 1 | ||
| } | ||
| } | ||
|
|
||
| final 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 | ||
| } | ||
|
|
||
| final class StubAssetProvider: NSObject, FVPAssetProvider { | ||
| func lookupKey(forAsset asset: String) -> String? { | ||
| return asset | ||
| } | ||
|
|
||
| func lookupKey(forAsset asset: String, fromPackage package: String) -> String? { | ||
| return asset | ||
| } | ||
| } | ||
|
|
||
| final 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<CMTime>? | ||
| ) -> CVPixelBuffer? { | ||
| let buffer = pixelBuffer | ||
| // Ownership is transferred to the caller. | ||
| pixelBuffer = nil | ||
| return buffer | ||
| } | ||
| } | ||
|
|
||
| #if os(iOS) | ||
| final 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 | ||
|
|
||
| final 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 | ||
| } | ||
|
|
||
| final class StubFVPDisplayLink: NSObject, FVPDisplayLink { | ||
| var running: Bool = false | ||
| var duration: CFTimeInterval { | ||
| return 1.0 / 60.0 | ||
| } | ||
| } | ||
|
|
||
| final class StubFVPDisplayLinkFactory: NSObject, FVPDisplayLinkFactory { | ||
| let displayLink = StubFVPDisplayLink() | ||
| var fireDisplayLink: (() -> Void)? | ||
|
|
||
| func displayLink( | ||
| with viewProvider: FVPViewProvider, | ||
| callback: @escaping () -> Void | ||
| ) -> FVPDisplayLink { | ||
| fireDisplayLink = callback | ||
| return displayLink | ||
| } | ||
| } | ||
|
|
||
| final class StubEventListener: NSObject, FVPVideoEventListener { | ||
| var onInitialized: (() -> Void)? | ||
| private(set) var initializationDuration: Int64 = 0 | ||
| private(set) var initializationSize: CGSize = .zero | ||
|
|
||
| init(onInitialized: (() -> Void)? = nil) { | ||
| self.onInitialized = onInitialized | ||
| super.init() | ||
| } | ||
|
|
||
| func videoPlayerDidComplete() {} | ||
| func videoPlayerDidEndBuffering() {} | ||
| func videoPlayerDidError(withMessage errorMessage: String) {} | ||
| func videoPlayerDidInitialize(withDuration duration: Int64, size: CGSize) { | ||
| onInitialized?() | ||
| initializationDuration = duration | ||
| initializationSize = size | ||
| } | ||
| func videoPlayerDidSetPlaying(_ playing: Bool) {} | ||
| func videoPlayerDidStartBuffering() {} | ||
| func videoPlayerDidUpdateBufferRegions(_ regions: [[NSNumber]]!) {} | ||
| func videoPlayerWasDisposed() {} | ||
| } | ||
|
|
||
| final class StubTexture: NSObject, FlutterTexture { | ||
| func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? { | ||
| return nil | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.