diff --git a/ios/core/Sources/Player/HeadlessPlayer.swift b/ios/core/Sources/Player/HeadlessPlayer.swift index ae7d57e8b..627aa74ad 100644 --- a/ios/core/Sources/Player/HeadlessPlayer.swift +++ b/ios/core/Sources/Player/HeadlessPlayer.swift @@ -129,6 +129,9 @@ public protocol HeadlessPlayer { associatedtype RegistryType: PlayerRegistry /// Registry for Assets var assetRegistry: RegistryType { get } + + var pluginManager: PluginManager { get } + /// The current state of Player var state: BaseFlowState? { get } /// A reference to the core player in the JSContext @@ -293,18 +296,6 @@ public extension HeadlessPlayer { logger.e("JavaScriptCore Exception: \(String(describing: value)) \(moreInfo)") } } - - func findPlugin(_ plugin: Plugin.Type) -> JSValue? { - return jsPlayerReference? - .invokeMethod("findPlugin", withArguments: [ - jsPlayerReference?.context.getSymbol(plugin.symbol) as Any - ]) - } - - func applyTo(_ plugin: Plugin.Type, apply: @escaping (JSValue) -> Void) { - guard let plugin = findPlugin(plugin) else { return } - apply(plugin) - } } public protocol WithSymbol { diff --git a/ios/core/Sources/PluginManager.swift b/ios/core/Sources/PluginManager.swift new file mode 100644 index 000000000..385013333 --- /dev/null +++ b/ios/core/Sources/PluginManager.swift @@ -0,0 +1,54 @@ +// +// PluginManager.swift +// PlayerUI +// +// Created by Zhao Xia Wu on 2025-02-25. +// + +import Foundation +import SwiftHooks + +public class PluginManager { + private var plugins: [NativePlugin] = [] + + public var hooks = PluginManagerHooks() + + public init() { + setupHooks() + } + + private func setupHooks() { + // Registering the plugin + hooks.registerPlugin.tap(name: "RegisterPluginAppend") { plugin in + if !self.plugins.contains(where: { $0.pluginName == plugin.pluginName }) { + self.plugins.append(plugin) + } + } + + // Finding the plugin + hooks.findPlugin.tap(name: "FindPlugin") { pluginType in + guard let match = self.plugins.first(where: { type(of: $0) == pluginType }) else { + return .skip + } + return .bail(match) + } + } + + // Method to add a plugin to the Manager where pluginNames are unique + public func registerPlugin(_ plugin: NativePlugin) { + self.hooks.registerPlugin.call(plugin) + } + + // Method to retrieve a plugin by type + public func findPlugin(ofType type: T.Type) -> T? { + return self.hooks.findPlugin.call(type) as? T + } +} + +public struct PluginManagerHooks { + // Hook for registering a new plugin + public let registerPlugin = SyncHook() + + // Hook for finding plugins + public let findPlugin = SyncBailHook() +} diff --git a/ios/swiftui/Sources/SwiftUIPlayer.swift b/ios/swiftui/Sources/SwiftUIPlayer.swift index 13bb62ee0..a93a51aeb 100644 --- a/ios/swiftui/Sources/SwiftUIPlayer.swift +++ b/ios/swiftui/Sources/SwiftUIPlayer.swift @@ -38,6 +38,8 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { fileprivate(set) var hooks: SwiftUIPlayerHooks? fileprivate let registry = SwiftUIRegistry() + fileprivate let pluginManager = PluginManager() + @Published fileprivate var result: Result? /// Returns true iff there is a non-nil player. @@ -76,7 +78,15 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { self.hooks = hooks DispatchQueue.main.async { self.result = nil } - for plugin in allPlugins { plugin.apply(player: player) } + // To ensure plugin.apply gets called even if plugin were to get registered after player.start + pluginManager.hooks.registerPlugin.tap(name: "RegisterPluginApply") { plugin in + plugin.apply(player: player) + } + + for plugin in allPlugins { + pluginManager.registerPlugin(plugin) + } + registry.partialMatchRegistry = partialMatchPlugin hooks.viewController.tap { [weak self] controller in @@ -178,6 +188,8 @@ public struct SwiftUIPlayer: View, HeadlessPlayer { /// The registry for registering assets to be used for rendering public var assetRegistry: SwiftUIRegistry { context.registry } + public var pluginManager: PluginManager { context.pluginManager } + // For ViewInspector testing internal let inspection = Inspection() diff --git a/ios/test-utils-core/Sources/utilities/HeadlessPlayerImpl.swift b/ios/test-utils-core/Sources/utilities/HeadlessPlayerImpl.swift index 8c870b82c..92f9cd0d1 100644 --- a/ios/test-utils-core/Sources/utilities/HeadlessPlayerImpl.swift +++ b/ios/test-utils-core/Sources/utilities/HeadlessPlayerImpl.swift @@ -10,6 +10,8 @@ open class HeadlessPlayerImpl: HeadlessPlayer { public var hooks: HeadlessHooks? public var logger = TapableLogger() + public var pluginManager = PluginManager() + public var jsPlayerReference: JSValue? let match = PartialMatchFingerprintPlugin() diff --git a/ios/test-utils-core/Sources/utilities/TestPlayer.swift b/ios/test-utils-core/Sources/utilities/TestPlayer.swift index 80b4da123..70ee53192 100644 --- a/ios/test-utils-core/Sources/utilities/TestPlayer.swift +++ b/ios/test-utils-core/Sources/utilities/TestPlayer.swift @@ -23,6 +23,7 @@ public class TestPlayer(player: P) where P: HeadlessPlayer { requestTimeWebPlugin.context = player.jsPlayerReference?.context - player.applyTo(MetricsPlugin.self) { [weak self] plugin in - self?.requestTimeWebPlugin.pluginRef?.invokeMethod("apply", withArguments: [plugin]) + + guard let metricsPlugin = player.pluginManager.findPlugin(ofType: MetricsPlugin.self) else { + return } + + self.requestTimeWebPlugin.apply(to: metricsPlugin) } } @@ -47,6 +50,12 @@ class RequestTimeWebPlugin: JSBasePlugin { return [JSValue(object: handler, in: context) as Any] } + fileprivate func apply(to metricsPlugin: MetricsPlugin) { + guard let context = metricsPlugin.context else { return } + self.setup(context: context) + self.pluginRef?.invokeMethod("apply", withArguments: [metricsPlugin.pluginRef]) + } + override open func getUrlForFile(fileName: String) -> URL? { #if SWIFT_PACKAGE ResourceUtilities.urlForFile(name: fileName, ext: "js", bundle: Bundle.module)