297 lines
8.1 KiB
Swift
297 lines
8.1 KiB
Swift
// 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<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?
|
|
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<CVPixelBuffer>? {
|
|
return nil
|
|
}
|
|
}
|