// 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_objc @testable 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? ) -> 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? private(set) var lastOutputSettings: [String: Any]? #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(outputSettings: [String: Any]) -> FVPPixelBufferSource { lastOutputSettings = outputSettings 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: DisplayLinkFactory { 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? { return nil } }