@npaw/forge-react-native 9.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +10 -0
  2. package/NPAWForgeReactNative.podspec +44 -0
  3. package/README.md +51 -0
  4. package/android/build.gradle +77 -0
  5. package/android/src/main/AndroidManifest.xml +14 -0
  6. package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +295 -0
  7. package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +61 -0
  8. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +735 -0
  9. package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgePackage.java +53 -0
  10. package/android/src/test/java/com/npaw/forge/reactnative/ForgeDtoMapperTest.java +133 -0
  11. package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +82 -0
  12. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/ForgeKernelFFI.h +17 -0
  13. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +873 -0
  14. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/module.modulemap +7 -0
  15. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
  16. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/ForgeKernelFFI.h +17 -0
  17. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +873 -0
  18. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
  19. package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
  20. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/ForgeKernelFFI.h +17 -0
  21. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +873 -0
  22. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/module.modulemap +7 -0
  23. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
  24. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/ForgeKernelFFI.h +17 -0
  25. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +873 -0
  26. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/module.modulemap +7 -0
  27. package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
  28. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +3931 -0
  29. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +873 -0
  30. package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.modulemap +7 -0
  31. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAccessLogFacts.swift +152 -0
  32. package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +877 -0
  33. package/ios/ForgeApple/Sources/NPAWForgeCapture/AttributionCapture.swift +158 -0
  34. package/ios/ForgeApple/Sources/NPAWForgeCapture/CapturePlanGate.swift +189 -0
  35. package/ios/ForgeApple/Sources/NPAWForgeCapture/CrashCapture.swift +120 -0
  36. package/ios/ForgeApple/Sources/NPAWForgeCapture/ForgeAutomaticCapture.swift +639 -0
  37. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsCapture.swift +1176 -0
  38. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsEngine.swift +674 -0
  39. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsMarkers.swift +105 -0
  40. package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsSwiftUI.swift +284 -0
  41. package/ios/ForgeApple/Sources/NPAWForgeCapture/SwiftUICapture.swift +44 -0
  42. package/ios/ForgeApple/Sources/NPAWForgeCore/AdAdapter.swift +199 -0
  43. package/ios/ForgeApple/Sources/NPAWForgeCore/AdSemantics.swift +136 -0
  44. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +596 -0
  45. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +297 -0
  46. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeClock.swift +29 -0
  47. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +109 -0
  48. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeOutcome.swift +89 -0
  49. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +291 -0
  50. package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeStorage.swift +70 -0
  51. package/ios/ForgeApple/Sources/NPAWForgeCore/ManualAdReporter.swift +180 -0
  52. package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +453 -0
  53. package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +594 -0
  54. package/ios/ForgeApple/Sources/NPAWForgeCore/NetworkRequestRecord.swift +191 -0
  55. package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +191 -0
  56. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +299 -0
  57. package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerSemantics.swift +266 -0
  58. package/ios/ForgeApple/Sources/NPAWForgeCore/PrivacySanitizer.swift +59 -0
  59. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +478 -0
  60. package/ios/ForgeApple/Sources/NPAWForgeCore/VideoOptions.swift +431 -0
  61. package/ios/ForgeApple/Sources/NPAWForgeNetworkCapture/ForgeNetworkCapture.swift +116 -0
  62. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/NPAWForgeNetworkSwizzler.m +293 -0
  63. package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/include/NPAWForgeNetworkSwizzler.h +33 -0
  64. package/ios/ForgeModuleImpl.swift +1131 -0
  65. package/ios/NPAWForgeTurboModule.mm +399 -0
  66. package/package.json +33 -0
  67. package/src/ForgeAdReporter.ts +101 -0
  68. package/src/ForgeVideo.ts +169 -0
  69. package/src/NPAWForge.ts +177 -0
  70. package/src/capture.ts +192 -0
  71. package/src/events.ts +40 -0
  72. package/src/index.ts +37 -0
  73. package/src/specs/NativeNPAWForge.ts +168 -0
  74. package/src/types.ts +114 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Copyright (C) NPAW - All Rights Reserved
3
+ *
4
+ * This source code is protected under international copyright law. All rights
5
+ * reserved and protected by the copyright holders.
6
+ * This file is confidential and only available to authorized individuals with the
7
+ * permission of the copyright holders. If you encounter this file and do not have
8
+ * permission, please contact the copyright holders and delete this file.
9
+ *
10
+ *
11
+ * @author: Victor Soto <victor.soto@nicepeopleatwork.com>
12
+ * @description: Apple network request observation model forwarded to the Rust runtime.
13
+ */
14
+
15
+ import Foundation
16
+
17
+ public enum NetworkRequestOutcome: String {
18
+ case success
19
+ case httpError = "http_error"
20
+ case networkError = "network_error"
21
+ case cancelled
22
+ case timeout
23
+ case blocked
24
+ case unknown
25
+ }
26
+
27
+ public enum NetworkRequestSource: String {
28
+ case automaticURLSession = "automatic_urlsession"
29
+ case automaticNSURLConnection = "automatic_nsurlconnection"
30
+ case manual
31
+ }
32
+
33
+ public struct NetworkRequestRecord: Equatable {
34
+ public var eventId: String
35
+ public var source: NetworkRequestSource
36
+ public var targetUrl: String
37
+ public var method: String
38
+ public var timestampMs: Int64
39
+ public var durationMs: Int64
40
+ public var responseStatusCode: Int
41
+ public var outcome: NetworkRequestOutcome
42
+ public var responseStatusText: String?
43
+ public var errorMessage: String?
44
+ public var traceparent: String?
45
+ public var dnsDurationMs: Int64?
46
+ public var connectDurationMs: Int64?
47
+ public var sslDurationMs: Int64?
48
+ public var requestDurationMs: Int64?
49
+ public var timeToFirstByteMs: Int64?
50
+ public var responseDownloadDurationMs: Int64?
51
+ public var redirectDurationMs: Int64?
52
+ public var redirectCount: Int64?
53
+ public var networkProtocol: String?
54
+ /// Request body size in bytes (measured uploaded byte count, request
55
+ /// `Content-Length` fallback). Body content is never captured; only this
56
+ /// size is reported.
57
+ public var requestBytes: Int64?
58
+ /// Response body size in bytes (measured downloaded byte count, response
59
+ /// `Content-Length` fallback). Body content is never captured; only this
60
+ /// size is reported.
61
+ public var responseBytes: Int64?
62
+ public var requestQuery: [String: String]
63
+ public var requestHeaders: [String: String]
64
+ public var responseHeaders: [String: String]
65
+
66
+ public init(
67
+ targetUrl: String,
68
+ method: String,
69
+ responseStatusCode: Int,
70
+ durationMs: Int64,
71
+ outcome: NetworkRequestOutcome,
72
+ eventId: String = UUID().uuidString,
73
+ source: NetworkRequestSource = .manual,
74
+ timestampMs: Int64 = 0
75
+ ) {
76
+ self.eventId = eventId
77
+ self.source = source
78
+ self.targetUrl = targetUrl
79
+ self.method = method
80
+ self.timestampMs = timestampMs
81
+ self.durationMs = max(0, durationMs)
82
+ self.responseStatusCode = max(0, responseStatusCode)
83
+ self.outcome = outcome
84
+ self.requestQuery = [:]
85
+ self.requestHeaders = [:]
86
+ self.responseHeaders = [:]
87
+ }
88
+
89
+ public func sanitized(configuration: NetworkPrivacyConfiguration) -> NetworkRequestRecord {
90
+ var copy = self
91
+ copy.targetUrl = configuration.sanitizeUrl(targetUrl)
92
+ copy.requestQuery = configuration.filterMap(requestQuery, allowlist: configuration.queryAllowlist)
93
+ copy.requestHeaders = configuration.filterMap(requestHeaders, allowlist: configuration.requestHeaderAllowlist)
94
+ copy.responseHeaders = configuration.filterMap(responseHeaders, allowlist: configuration.responseHeaderAllowlist)
95
+ copy.errorMessage = errorMessage.map(configuration.sanitizeText)
96
+ return copy
97
+ }
98
+ }
99
+
100
+ public struct NetworkPrivacyConfiguration: Equatable {
101
+ public var queryAllowlist: Set<String>
102
+ public var requestHeaderAllowlist: Set<String>
103
+ public var responseHeaderAllowlist: Set<String>
104
+ public var redactionKeys: Set<String>
105
+ public var maxStringLength: Int
106
+
107
+ public init(
108
+ queryAllowlist: Set<String> = [],
109
+ requestHeaderAllowlist: Set<String> = [],
110
+ responseHeaderAllowlist: Set<String> = [],
111
+ redactionKeys: Set<String> = Self.defaultRedactionKeys,
112
+ maxStringLength: Int = 512
113
+ ) {
114
+ self.queryAllowlist = queryAllowlist.map(Self.normalizeKey).asSet
115
+ self.requestHeaderAllowlist = requestHeaderAllowlist.map(Self.normalizeKey).asSet
116
+ self.responseHeaderAllowlist = responseHeaderAllowlist.map(Self.normalizeKey).asSet
117
+ self.redactionKeys = redactionKeys.map(Self.normalizeKey).asSet
118
+ self.maxStringLength = max(1, maxStringLength)
119
+ }
120
+
121
+ public static let defaultRedactionKeys: Set<String> = [
122
+ "authorization",
123
+ "proxy-authorization",
124
+ "cookie",
125
+ "set-cookie",
126
+ "api-key",
127
+ "x-api-key",
128
+ "token",
129
+ "access-token",
130
+ "refresh-token",
131
+ "password",
132
+ "passwd",
133
+ "credential",
134
+ "secret",
135
+ "email",
136
+ "phone",
137
+ "payment",
138
+ "card",
139
+ ]
140
+
141
+ public func sanitizeUrl(_ value: String) -> String {
142
+ guard var components = URLComponents(string: value) else {
143
+ return String(value.split(separator: "?").first.map(String.init) ?? value).prefixString(maxStringLength)
144
+ }
145
+ components.user = nil
146
+ components.password = nil
147
+ components.query = nil
148
+ components.fragment = nil
149
+ return (components.string ?? value).prefixString(maxStringLength)
150
+ }
151
+
152
+ public func filterMap(_ value: [String: String], allowlist: Set<String>) -> [String: String] {
153
+ guard !allowlist.isEmpty else { return [:] }
154
+ var output: [String: String] = [:]
155
+ for (key, rawValue) in value {
156
+ guard output.count < 32 else { break }
157
+ let normalized = Self.normalizeKey(key)
158
+ guard allowlist.contains(normalized), !key.isEmpty, !rawValue.isEmpty else { continue }
159
+ output[key] = isSensitive(normalized) ? "[REDACTED]" : rawValue.prefixString(maxStringLength)
160
+ }
161
+ return output
162
+ }
163
+
164
+ public func sanitizeText(_ value: String) -> String {
165
+ let normalized = Self.normalizeKey(value)
166
+ if redactionKeys.contains(where: { normalized.contains($0) }) {
167
+ return "[REDACTED]"
168
+ }
169
+ return value.prefixString(maxStringLength)
170
+ }
171
+
172
+ private func isSensitive(_ key: String) -> Bool {
173
+ redactionKeys.contains(where: { key.contains($0) })
174
+ }
175
+
176
+ private static func normalizeKey(_ value: String) -> String {
177
+ value.trimmingCharacters(in: .whitespacesAndNewlines)
178
+ .lowercased()
179
+ .replacingOccurrences(of: "_", with: "-")
180
+ }
181
+ }
182
+
183
+ private extension Array where Element == String {
184
+ var asSet: Set<String> { Set(self) }
185
+ }
186
+
187
+ private extension String {
188
+ func prefixString(_ length: Int) -> String {
189
+ String(prefix(length))
190
+ }
191
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Copyright (C) NPAW - All Rights Reserved
3
+ *
4
+ * This source code is protected under international copyright law. All rights
5
+ * reserved and protected by the copyright holders.
6
+ * This file is confidential and only available to authorized individuals with the
7
+ * permission of the copyright holders. If you encounter this file and do not have
8
+ * permission, please contact the copyright holders and delete this file.
9
+ *
10
+ *
11
+ * @author: Victor Soto <victor.soto@nicepeopleatwork.com>
12
+ * @description: Objective-C compatible wrappers for core Apple binding use cases.
13
+ */
14
+
15
+ import Foundation
16
+
17
+ @objc(NPAWForgeObjCBuilder)
18
+ public final class NPAWForgeObjCBuilder: NSObject {
19
+ private let builder: NPAWForge.Builder
20
+
21
+ @objc public init(accountCode: String) {
22
+ self.builder = NPAWForge.Builder(accountCode: accountCode)
23
+ }
24
+
25
+ @discardableResult
26
+ @objc(setAppName:)
27
+ public func setAppName(_ value: String?) -> NPAWForgeObjCBuilder {
28
+ builder.setAppName(value)
29
+ return self
30
+ }
31
+
32
+ @discardableResult
33
+ @objc(setAppReleaseVersion:)
34
+ public func setAppReleaseVersion(_ value: String?) -> NPAWForgeObjCBuilder {
35
+ builder.setAppReleaseVersion(value)
36
+ return self
37
+ }
38
+
39
+ @discardableResult
40
+ @objc(setLmaEndpoint:)
41
+ public func setLmaEndpoint(_ value: String?) -> NPAWForgeObjCBuilder {
42
+ builder.setLmaEndpoint(value)
43
+ return self
44
+ }
45
+
46
+ @discardableResult
47
+ @objc(setLogLevelName:)
48
+ public func setLogLevelName(_ value: String?) -> NPAWForgeObjCBuilder {
49
+ builder.setLogLevel(value)
50
+ return self
51
+ }
52
+
53
+ @discardableResult
54
+ @objc(setStartSessionOnInitialize:)
55
+ public func setStartSessionOnInitialize(_ enabled: Bool) -> NPAWForgeObjCBuilder {
56
+ builder.setStartSessionOnInitialize(enabled)
57
+ return self
58
+ }
59
+
60
+ @objc public func build() -> NPAWForgeObjC {
61
+ NPAWForgeObjC(swiftForge: builder.build())
62
+ }
63
+ }
64
+
65
+ @objc(NPAWForgeObjC)
66
+ public final class NPAWForgeObjC: NSObject {
67
+ public let swiftForge: NPAWForge
68
+
69
+ public init(swiftForge: NPAWForge) {
70
+ self.swiftForge = swiftForge
71
+ }
72
+
73
+ @objc public func startSession() -> ForgeOutcome {
74
+ swiftForge.startSession()
75
+ }
76
+
77
+ @objc(startSessionWithMetadata:)
78
+ public func startSession(metadata: NSDictionary?) -> ForgeOutcome {
79
+ swiftForge.startSession(metadata.toStringAnyMap())
80
+ }
81
+
82
+ @objc public func endSession() -> ForgeOutcome {
83
+ swiftForge.endSession()
84
+ }
85
+
86
+ @objc(endSessionWithMetadata:)
87
+ public func endSession(metadata: NSDictionary?) -> ForgeOutcome {
88
+ swiftForge.endSession(metadata.toStringAnyMap())
89
+ }
90
+
91
+ @objc(trackSessionEventWithName:metadata:)
92
+ public func trackSessionEvent(name: String, metadata: NSDictionary?) -> ForgeOutcome {
93
+ swiftForge.trackSessionEvent(name: name, metadata: metadata.toStringAnyMap())
94
+ }
95
+
96
+ @objc(trackSessionErrorWithCode:message:metadata:)
97
+ public func trackSessionError(code: String, message: String, metadata: NSDictionary?) -> ForgeOutcome {
98
+ swiftForge.trackSessionError(errorCode: code, errorMessage: message, metadata: metadata.toStringAnyMap())
99
+ }
100
+
101
+ @objc(trackRawEventWithName:params:)
102
+ public func trackRawEvent(name: String, params: NSDictionary?) -> ForgeOutcome {
103
+ swiftForge.trackRawEvent(name: name, params: params.toStringAnyMap())
104
+ }
105
+
106
+ @objc(trackSearchWithQuery:resultCount:)
107
+ public func trackSearch(query: String, resultCount: NSNumber?) -> ForgeOutcome {
108
+ swiftForge.trackSearch(query: query, resultCount: resultCount?.intValue)
109
+ }
110
+
111
+ @objc(trackSearchResultClickWithQuery:contentId:position:)
112
+ public func trackSearchResultClick(query: String, contentId: String?, position: NSNumber?) -> ForgeOutcome {
113
+ swiftForge.trackSearchResultClick(
114
+ query: query,
115
+ options: ForgeSearchResultClickOptions(contentId: contentId, position: position?.intValue)
116
+ )
117
+ }
118
+
119
+ @objc(trackBusinessEventWithName:attributes:)
120
+ public func trackBusinessEvent(name: String, attributes: NSDictionary?) -> ForgeOutcome {
121
+ swiftForge.trackBusinessEvent(name: name, attributes: attributes.toStringAnyMap())
122
+ }
123
+
124
+ @objc(trackAttributionWithOptions:)
125
+ public func trackAttribution(options: NSDictionary?) -> ForgeOutcome {
126
+ let values = options.toStringStringMap()
127
+ return swiftForge.trackAttribution(options: ForgeAttributionOptions(
128
+ utmSource: values["utmSource"],
129
+ utmMedium: values["utmMedium"],
130
+ utmCampaign: values["utmCampaign"],
131
+ utmTerm: values["utmTerm"],
132
+ utmContent: values["utmContent"],
133
+ utmUrl: values["utmUrl"]
134
+ ))
135
+ }
136
+
137
+ @objc(loginSuccessWithUserId:options:)
138
+ public func loginSuccess(userId: String, options: NSDictionary?) -> ForgeOutcome {
139
+ swiftForge.loginSuccess(userId: userId, options: options.toStringStringMap())
140
+ }
141
+
142
+ @objc(loginFailedWithOptions:)
143
+ public func loginFailed(options: NSDictionary?) -> ForgeOutcome {
144
+ swiftForge.loginFailed(options: options.toStringStringMap())
145
+ }
146
+
147
+ @objc(logoutWithOptions:)
148
+ public func logout(options: NSDictionary?) -> ForgeOutcome {
149
+ swiftForge.logout(options: options.toStringStringMap())
150
+ }
151
+
152
+ @objc(profileCreatedWithProfileId:options:)
153
+ public func profileCreated(profileId: String, options: NSDictionary?) -> ForgeOutcome {
154
+ swiftForge.profileCreated(profileId: profileId, options: options.toStringStringMap())
155
+ }
156
+
157
+ @objc(profileSelectedWithProfileId:options:)
158
+ public func profileSelected(profileId: String, options: NSDictionary?) -> ForgeOutcome {
159
+ swiftForge.profileSelected(profileId: profileId, options: options.toStringStringMap())
160
+ }
161
+
162
+ @objc(profileDeletedWithProfileId:)
163
+ public func profileDeleted(profileId: String) -> ForgeOutcome {
164
+ swiftForge.profileDeleted(profileId: profileId)
165
+ }
166
+
167
+ @objc(setLogLevelName:)
168
+ public func setLogLevelName(_ value: String?) -> ForgeOutcome {
169
+ swiftForge.setLogLevel(value)
170
+ }
171
+
172
+ @objc public func destroy() -> ForgeOutcome {
173
+ swiftForge.destroy()
174
+ }
175
+ }
176
+
177
+ private extension Optional where Wrapped == NSDictionary {
178
+ func toStringAnyMap() -> [String: Any] {
179
+ guard let dictionary = self else { return [:] }
180
+ var output: [String: Any] = [:]
181
+ for (key, value) in dictionary {
182
+ guard let key = key as? String else { continue }
183
+ output[key] = value
184
+ }
185
+ return output
186
+ }
187
+
188
+ func toStringStringMap() -> [String: String] {
189
+ toStringAnyMap().mapValues { String(describing: $0) }
190
+ }
191
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Copyright (C) NPAW - All Rights Reserved
3
+ *
4
+ * This source code is protected under international copyright law. All rights
5
+ * reserved and protected by the copyright holders.
6
+ * This file is confidential and only available to authorized individuals with the
7
+ * permission of the copyright holders. If you encounter this file and do not have
8
+ * permission, please contact the copyright holders and delete this file.
9
+ *
10
+ *
11
+ * @author: Victor Soto <victor.soto@nicepeopleatwork.com>
12
+ * @description: Apple player adapter base class and public manual fire API.
13
+ */
14
+
15
+ import Foundation
16
+
17
+ open class PlayerAdapter: NSObject {
18
+ public let adapterInstanceId = UUID()
19
+ open var descriptor = AdapterDescriptor(
20
+ adapterId: "generic-player",
21
+ category: .player,
22
+ playerFamily: "Generic Player",
23
+ version: "unknown"
24
+ )
25
+
26
+ /// Adapter-level default for the kernel `waitForMetadata` start gate.
27
+ /// Adapters whose players report duration/liveness/rendition only after
28
+ /// the first start signal (for example AVPlayer before the HLS playlist
29
+ /// is parsed) override this to `true` so `/start` waits for those facts.
30
+ /// An explicit `VideoOptions.waitForMetadata` value always wins over this
31
+ /// default, and the kernel default stays `false`.
32
+ open var defaultWaitForMetadata: Bool { false }
33
+ open var playerObject: AnyObject? {
34
+ didSet {
35
+ if oldValue === playerObject { return }
36
+ if isBound { unregisterListeners() }
37
+ if isBound, playerObject != nil { registerListeners() }
38
+ }
39
+ }
40
+ private var isBound = false
41
+ public private(set) weak var videoAdapter: VideoAdapter?
42
+
43
+ /// True between an accepted pause and the accepted resume/stop/error/
44
+ /// destroy that clears it (mirrors the Android base adapter contract).
45
+ public private(set) var isPaused = false
46
+ private var adPlaybackActive = false
47
+
48
+ public init(player: AnyObject? = nil) {
49
+ self.playerObject = player
50
+ super.init()
51
+ }
52
+
53
+ /// Marks whether ad playback owns the player right now; called by
54
+ /// companion ad adapters around break start/stop (mirrors the Android
55
+ /// base adapter contract).
56
+ public func setIsPlayingAd(_ playingAd: Bool) {
57
+ adPlaybackActive = playingAd
58
+ }
59
+
60
+ public func isPlayingAd() -> Bool { adPlaybackActive }
61
+
62
+ func bindToVideoAdapter(_ videoAdapter: VideoAdapter) {
63
+ self.videoAdapter = videoAdapter
64
+ isBound = true
65
+ if playerObject != nil { registerListeners() }
66
+ }
67
+
68
+ func unbindFromVideoAdapter() {
69
+ if isBound { unregisterListeners() }
70
+ isBound = false
71
+ videoAdapter = nil
72
+ }
73
+
74
+ open func registerListeners() {}
75
+ open func unregisterListeners() {}
76
+
77
+ /// Remote behavior hints from the latest kernel capture plan (`bh.v`), or
78
+ /// `nil` before the first plan or while unbound. Adapters read this once
79
+ /// at view start (`registerListeners`); hints are applied at the next
80
+ /// view start only, never hot-swapped mid-view.
81
+ public var capturePlanBehaviorHints: ForgeCapturePlan.Behavior? {
82
+ videoAdapter?.capturePlanBehavior()
83
+ }
84
+
85
+ open func metadata() -> [String: String] {
86
+ [
87
+ "adapterId": descriptor.adapterId,
88
+ "category": descriptor.category.rawValue,
89
+ "playerFamily": descriptor.playerFamily,
90
+ "version": descriptor.version,
91
+ ]
92
+ }
93
+
94
+ open func snapshotMetadata(
95
+ timemarkMs: Int64 = ForgeClock.nowMs(),
96
+ elapsedRealtimeMs: Int64 = ForgeClock.elapsedRealtimeMs()
97
+ ) -> PlayerMetadataSnapshot {
98
+ defaultMetadata(nil)
99
+ }
100
+
101
+ /// Optional QoS sample (throughput, dropped frames, total bytes, latency,
102
+ /// frame rate). Adapters that can read these from their player override
103
+ /// this; the default returns nil so the runtime omits QoS for adapters
104
+ /// that do not measure it (mirrors the Android base adapter contract).
105
+ open func snapshotQos(
106
+ timemarkMs: Int64 = ForgeClock.nowMs(),
107
+ elapsedRealtimeMs: Int64 = ForgeClock.elapsedRealtimeMs()
108
+ ) -> QosSnapshot? {
109
+ nil
110
+ }
111
+
112
+ open func onPlayerEvent(
113
+ _ event: PlayerEvent,
114
+ timestampMs: Int64 = 0,
115
+ metadata: PlayerMetadataSnapshot? = nil
116
+ ) -> ForgeOutcome {
117
+ let resolved = metadata ?? PlayerMetadataSnapshot(playerName: descriptor.playerFamily)
118
+ switch event {
119
+ case .initEvent: return fireInit(metadata: resolved, timestampMs: timestampMs)
120
+ case .start: return fireStart(metadata: resolved, timestampMs: timestampMs)
121
+ case .join: return fireJoin(metadata: resolved, timestampMs: timestampMs)
122
+ case .pause: return firePause(metadata: resolved, timestampMs: timestampMs)
123
+ case .resume: return fireResume(metadata: resolved, timestampMs: timestampMs)
124
+ case .seekBegin: return fireSeekBegin(metadata: resolved, timestampMs: timestampMs)
125
+ case .seekEnd: return fireSeekEnd(metadata: resolved, timestampMs: timestampMs)
126
+ case .bufferBegin: return fireBufferBegin(metadata: resolved, timestampMs: timestampMs)
127
+ case .bufferEnd: return fireBufferEnd(metadata: resolved, timestampMs: timestampMs)
128
+ case .stop: return fireStop(metadata: resolved, timestampMs: timestampMs)
129
+ case .error: return fireError(errorCode: resolved.errorCode, errorMessage: resolved.errorMessage, errorMetadata: resolved.errorMetadata, timestampMs: timestampMs)
130
+ case .fatalError: return fireFatalError(errorCode: resolved.errorCode, errorMessage: resolved.errorMessage, errorMetadata: resolved.errorMetadata, timestampMs: timestampMs)
131
+ case .destroy: return fireDestroy(timestampMs: timestampMs)
132
+ case .customVideoEvent: return fireEvent(timestampMs: timestampMs)
133
+ case .customVideoEventEnd: return fireEventEnd(timestampMs: timestampMs)
134
+ case .metadataUpdate: return fireMetadataUpdate(metadata: resolved, timestampMs: timestampMs)
135
+ }
136
+ }
137
+
138
+ open func fireInit(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
139
+ record(.initEvent, metadata ?? defaultMetadata(params), timestampMs)
140
+ }
141
+
142
+ open func fireStart(params: [String: String]? = [:], forceInit: Bool? = false, metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
143
+ var snapshot = metadata ?? defaultMetadata(params)
144
+ snapshot.forceInit = forceInit ?? false
145
+ return record(.start, snapshot, timestampMs)
146
+ }
147
+
148
+ open func fireJoin(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
149
+ record(.join, metadata ?? defaultMetadata(params), timestampMs)
150
+ }
151
+
152
+ open func firePause(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
153
+ record(.pause, metadata ?? defaultMetadata(params), timestampMs)
154
+ }
155
+
156
+ open func fireResume(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
157
+ record(.resume, metadata ?? defaultMetadata(params), timestampMs)
158
+ }
159
+
160
+ open func fireSeekBegin(convertFromBuffer: Bool? = true, params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
161
+ var snapshot = metadata ?? defaultMetadata(params)
162
+ snapshot.convertFromBuffer = convertFromBuffer ?? true
163
+ return record(.seekBegin, snapshot, timestampMs)
164
+ }
165
+
166
+ open func fireSeekEnd(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
167
+ record(.seekEnd, metadata ?? defaultMetadata(params), timestampMs)
168
+ }
169
+
170
+ open func fireBufferBegin(convertFromSeek: Bool? = false, params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
171
+ var snapshot = metadata ?? defaultMetadata(params)
172
+ snapshot.convertFromSeek = convertFromSeek ?? false
173
+ return record(.bufferBegin, snapshot, timestampMs)
174
+ }
175
+
176
+ open func fireBufferEnd(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
177
+ record(.bufferEnd, metadata ?? defaultMetadata(params), timestampMs)
178
+ }
179
+
180
+ open func fireStop(params: [String: String]? = [:], metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
181
+ record(.stop, metadata ?? defaultMetadata(params), timestampMs)
182
+ }
183
+
184
+ open func fireError(errorCode: String? = nil, errorMessage: String? = nil, errorMetadata: String? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
185
+ var metadata = defaultMetadata(nil)
186
+ metadata.errorCode = errorCode
187
+ metadata.errorMessage = errorMessage
188
+ metadata.errorMetadata = errorMetadata
189
+ return record(.error, metadata, timestampMs)
190
+ }
191
+
192
+ open func fireFatalError(errorCode: String? = nil, errorMessage: String? = nil, errorMetadata: String? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
193
+ var metadata = defaultMetadata(nil)
194
+ metadata.errorCode = errorCode
195
+ metadata.errorMessage = errorMessage
196
+ metadata.errorMetadata = errorMetadata
197
+ metadata.errorLevel = "fatal"
198
+ return record(.fatalError, metadata, timestampMs)
199
+ }
200
+
201
+ open func fireDestroy(timestampMs: Int64 = 0) -> ForgeOutcome {
202
+ record(.destroy, defaultMetadata(nil), timestampMs)
203
+ }
204
+
205
+ open func fireEvent(
206
+ eventName: String = "",
207
+ dimensions: [String: String] = [:],
208
+ values: [String: Double] = [:],
209
+ topLevelDimensions: [String: String] = [:],
210
+ hasEndDatetime: Bool = false,
211
+ timestampMs: Int64 = 0
212
+ ) -> ForgeOutcome {
213
+ var metadata = defaultMetadata(nil)
214
+ metadata.customEvent = CustomVideoEventPayload(
215
+ name: eventName,
216
+ dimensions: dimensions,
217
+ values: values,
218
+ topLevel: topLevelDimensions,
219
+ hasEndDatetime: hasEndDatetime
220
+ )
221
+ return record(.customVideoEvent, metadata, timestampMs)
222
+ }
223
+
224
+ open func fireEventEnd(
225
+ eventName: String = "",
226
+ dimensions: [String: String] = [:],
227
+ values: [String: Double] = [:],
228
+ topLevelDimensions: [String: String] = [:],
229
+ timestampMs: Int64 = 0
230
+ ) -> ForgeOutcome {
231
+ var metadata = defaultMetadata(nil)
232
+ metadata.customEvent = CustomVideoEventPayload(
233
+ name: eventName,
234
+ dimensions: dimensions,
235
+ values: values,
236
+ topLevel: topLevelDimensions
237
+ )
238
+ return record(.customVideoEventEnd, metadata, timestampMs)
239
+ }
240
+
241
+ open func fireMetadataUpdate(metadata: PlayerMetadataSnapshot? = nil, timestampMs: Int64 = 0) -> ForgeOutcome {
242
+ record(.metadataUpdate, metadata ?? defaultMetadata(nil), timestampMs)
243
+ }
244
+
245
+ open func defaultMetadata(_ params: [String: String]?) -> PlayerMetadataSnapshot {
246
+ PlayerMetadataSnapshot(
247
+ playerName: params?["player"] ?? descriptor.playerFamily,
248
+ playerVersion: params?["playerVersion"] ?? descriptor.version,
249
+ adapterVersion: descriptor.version,
250
+ resourceUrl: params?["resource"],
251
+ parsedResource: params?["parsedResource"] ?? params?["urlToParse"],
252
+ title: params?["title"],
253
+ contentType: (params?["contentType"] ?? params?["content_type"]).flatMap(ContentType.from) ?? .unknown,
254
+ playheadMs: params?["playhead"].flatMap(Self.secondsToMillis),
255
+ durationMs: params?["mediaDuration"].flatMap(Self.secondsToMillis),
256
+ bitrate: params?["bitrate"].flatMap(Int64.init),
257
+ rendition: params?["rendition"],
258
+ videoCodec: params?["videoCodec"],
259
+ audioCodec: params?["audioCodec"],
260
+ codecProfile: params?["codecProfile"],
261
+ containerFormat: params?["containerFormat"],
262
+ errorMetadata: params?["errorMetadata"],
263
+ errorLevel: params?["errorLevel"],
264
+ program: params?["program"]
265
+ )
266
+ }
267
+
268
+ func record(_ event: PlayerEvent, _ metadata: PlayerMetadataSnapshot, _ timestampMs: Int64) -> ForgeOutcome {
269
+ let outcome = videoAdapter?.recordPlayerEvent(event: event, timestampMs: timestampMs, metadata: metadata) ?? .ignoredInvalidState
270
+ if outcome.isAccepted { updatePlaybackState(event) }
271
+ return outcome
272
+ }
273
+
274
+ private func updatePlaybackState(_ event: PlayerEvent) {
275
+ switch event {
276
+ case .pause:
277
+ isPaused = true
278
+ case .resume, .stop, .error, .fatalError, .destroy:
279
+ isPaused = false
280
+ default:
281
+ break
282
+ }
283
+ }
284
+
285
+ private static func secondsToMillis(_ value: String) -> Int64? {
286
+ Double(value).map { Int64($0 * 1000.0) }
287
+ }
288
+ }
289
+
290
+ private extension ContentType {
291
+ static func from(_ value: String) -> ContentType {
292
+ switch value.lowercased() {
293
+ case "live": return .live
294
+ case "vod": return .vod
295
+ case "audio": return .audio
296
+ default: return .unknown
297
+ }
298
+ }
299
+ }