@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.
- package/LICENSE +10 -0
- package/NPAWForgeReactNative.podspec +44 -0
- package/README.md +51 -0
- package/android/build.gradle +77 -0
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +295 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +61 -0
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +735 -0
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgePackage.java +53 -0
- package/android/src/test/java/com/npaw/forge/reactnative/ForgeDtoMapperTest.java +133 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +82 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/ForgeKernelFFI.h +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/module.modulemap +7 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +3931 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +873 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.modulemap +7 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAccessLogFacts.swift +152 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +877 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/AttributionCapture.swift +158 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/CapturePlanGate.swift +189 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/CrashCapture.swift +120 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/ForgeAutomaticCapture.swift +639 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsCapture.swift +1176 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsEngine.swift +674 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsMarkers.swift +105 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SectionsSwiftUI.swift +284 -0
- package/ios/ForgeApple/Sources/NPAWForgeCapture/SwiftUICapture.swift +44 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/AdAdapter.swift +199 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/AdSemantics.swift +136 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +596 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +297 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeClock.swift +29 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +109 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeOutcome.swift +89 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +291 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeStorage.swift +70 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ManualAdReporter.swift +180 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +453 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +594 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NetworkRequestRecord.swift +191 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ObjectiveCWrappers.swift +191 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +299 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerSemantics.swift +266 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PrivacySanitizer.swift +59 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoAdapter.swift +478 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/VideoOptions.swift +431 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCapture/ForgeNetworkCapture.swift +116 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/NPAWForgeNetworkSwizzler.m +293 -0
- package/ios/ForgeApple/Sources/NPAWForgeNetworkCaptureObjC/include/NPAWForgeNetworkSwizzler.h +33 -0
- package/ios/ForgeModuleImpl.swift +1131 -0
- package/ios/NPAWForgeTurboModule.mm +399 -0
- package/package.json +33 -0
- package/src/ForgeAdReporter.ts +101 -0
- package/src/ForgeVideo.ts +169 -0
- package/src/NPAWForge.ts +177 -0
- package/src/capture.ts +192 -0
- package/src/events.ts +40 -0
- package/src/index.ts +37 -0
- package/src/specs/NativeNPAWForge.ts +168 -0
- package/src/types.ts +114 -0
|
@@ -0,0 +1,594 @@
|
|
|
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: Production Swift runtime adapter backed by generated UniFFI bindings.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
#if FORGE_HAS_UNIFFI
|
|
18
|
+
#if canImport(ForgeKernelUniFFI)
|
|
19
|
+
import ForgeKernelUniFFI
|
|
20
|
+
#endif
|
|
21
|
+
|
|
22
|
+
final class UniFFIForgeRuntime: ForgeRuntime {
|
|
23
|
+
private let bridge: ForgeRuntimeBridge
|
|
24
|
+
private let callbackLock = NSLock()
|
|
25
|
+
private var contentMetadataProviders: [Int64: UniFFIContentMetadataProvider] = [:]
|
|
26
|
+
private var videoPreSendHooks: [Int64: UniFFIVideoPreSendHook] = [:]
|
|
27
|
+
private var sessionPreSendHook: UniFFISessionPreSendHook?
|
|
28
|
+
private var capturePlanListener: UniFFICapturePlanListener?
|
|
29
|
+
|
|
30
|
+
init(config: ForgeAppleConfig) {
|
|
31
|
+
bridge = ForgeRuntimeBridge(config: AppleRuntimeConfig(
|
|
32
|
+
accountCode: config.accountCode,
|
|
33
|
+
pluginVersion: config.pluginVersion,
|
|
34
|
+
appName: config.appName,
|
|
35
|
+
buildVariant: config.buildVariant,
|
|
36
|
+
appReleaseVersion: config.appReleaseVersion,
|
|
37
|
+
lmaEndpoint: config.lmaEndpoint,
|
|
38
|
+
analyticsUserAgent: config.analyticsUserAgent,
|
|
39
|
+
allowLocalHttp: config.allowLocalHttp,
|
|
40
|
+
logLevel: config.logLevel.wireName,
|
|
41
|
+
preSessionEventPolicy: ApplePreSessionEventPolicy(
|
|
42
|
+
processEventsBeforeSessionStart: config.processEventsBeforeSessionStart,
|
|
43
|
+
queueLimitEvents: UInt64(max(0, config.preSessionEventQueueLimitEvents)),
|
|
44
|
+
maxAgeMs: u64(config.preSessionEventQueueMaxAgeMs)
|
|
45
|
+
)
|
|
46
|
+
))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func start() -> ForgeOutcome {
|
|
50
|
+
outcome(bridge.start(nowMs: 0))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func setLogLevel(_ level: ForgeLogLevel) -> ForgeOutcome {
|
|
54
|
+
outcome(bridge.setLogLevel(logLevel: level.wireName))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView {
|
|
58
|
+
RuntimeVideoView(id: Int64(clamping: bridge.createVideoView(
|
|
59
|
+
nowMs: u64(timestampMs),
|
|
60
|
+
options: AppleVideoOptions(
|
|
61
|
+
joinTimeCalculationMode: options.joinTimeCalculationMode?.rawValue,
|
|
62
|
+
adJoinFromAdBreakStart: options.adJoinFromAdBreakStart,
|
|
63
|
+
adConnectedRequiresAdJoin: options.adConnectedRequiresAdJoin,
|
|
64
|
+
contentIsLiveNoSeek: options.contentIsLiveNoSeek,
|
|
65
|
+
ignorePauseSmallEvents: options.ignorePauseSmallEvents
|
|
66
|
+
)
|
|
67
|
+
)))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func recordEvent(_ event: ForgeRecordedEvent) -> ForgeOutcome {
|
|
71
|
+
outcome(bridge.recordSessionEvent(
|
|
72
|
+
eventName: event.eventName,
|
|
73
|
+
timemarkMs: u64(event.timestampMs),
|
|
74
|
+
params: event.payload
|
|
75
|
+
))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func recordIdentityEvent(kind: String, timemarkMs: Int64, fields: [String: String]) -> ForgeOutcome {
|
|
79
|
+
outcome(bridge.recordIdentityEvent(
|
|
80
|
+
kind: kind,
|
|
81
|
+
timemarkMs: u64(timemarkMs),
|
|
82
|
+
fields: fields
|
|
83
|
+
))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func recordApplicationForeground(_ foreground: Bool, timestampMs: Int64) -> ForgeOutcome {
|
|
87
|
+
outcome(bridge.recordApplicationForeground(foreground: foreground, timemarkMs: u64(timestampMs)))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func recordNetworkEvent(_ record: NetworkRequestRecord) -> ForgeOutcome {
|
|
91
|
+
outcome(bridge.recordNetworkEvent(observation: AppleNetworkObservation(
|
|
92
|
+
eventId: record.eventId,
|
|
93
|
+
source: record.source.rawValue,
|
|
94
|
+
targetUrl: record.targetUrl,
|
|
95
|
+
method: record.method,
|
|
96
|
+
timemarkMs: u64(record.timestampMs),
|
|
97
|
+
durationMs: u64(record.durationMs),
|
|
98
|
+
responseStatusCode: UInt16(clamping: record.responseStatusCode),
|
|
99
|
+
outcome: record.outcome.rawValue,
|
|
100
|
+
responseStatusText: record.responseStatusText,
|
|
101
|
+
errorMessage: record.errorMessage,
|
|
102
|
+
traceparent: record.traceparent,
|
|
103
|
+
dnsDurationMs: record.dnsDurationMs.map(u64),
|
|
104
|
+
connectDurationMs: record.connectDurationMs.map(u64),
|
|
105
|
+
sslDurationMs: record.sslDurationMs.map(u64),
|
|
106
|
+
requestDurationMs: record.requestDurationMs.map(u64),
|
|
107
|
+
timeToFirstByteMs: record.timeToFirstByteMs.map(u64),
|
|
108
|
+
responseDownloadDurationMs: record.responseDownloadDurationMs.map(u64),
|
|
109
|
+
redirectDurationMs: record.redirectDurationMs.map(u64),
|
|
110
|
+
redirectCount: record.redirectCount.map(u64),
|
|
111
|
+
protocol: record.networkProtocol,
|
|
112
|
+
requestBytes: record.requestBytes.map(u64),
|
|
113
|
+
responseBytes: record.responseBytes.map(u64),
|
|
114
|
+
requestQuery: record.requestQuery.isEmpty ? nil : record.requestQuery,
|
|
115
|
+
requestHeaders: record.requestHeaders.isEmpty ? nil : record.requestHeaders,
|
|
116
|
+
responseHeaders: record.responseHeaders.isEmpty ? nil : record.responseHeaders
|
|
117
|
+
)))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func recordPlayerEvent(
|
|
121
|
+
view: RuntimeVideoView,
|
|
122
|
+
event: PlayerEvent,
|
|
123
|
+
timemarkMs: Int64,
|
|
124
|
+
elapsedRealtimeMs: Int64,
|
|
125
|
+
options: VideoOptions,
|
|
126
|
+
metadata: PlayerMetadataSnapshot
|
|
127
|
+
) -> ForgeOutcome {
|
|
128
|
+
outcome(bridge.recordPlayerEvent(
|
|
129
|
+
viewId: u64(view.id),
|
|
130
|
+
event: appleVideoEvent(event),
|
|
131
|
+
timestamp: AppleSemanticTimestamp(timemarkMs: u64(timemarkMs), elapsedRealtimeMs: u64(elapsedRealtimeMs)),
|
|
132
|
+
overridesJson: options.overridesJson,
|
|
133
|
+
metadata: appleContentMetadata(metadata)
|
|
134
|
+
))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func recordAdEvent(
|
|
138
|
+
view: RuntimeVideoView,
|
|
139
|
+
event: AdEvent,
|
|
140
|
+
timemarkMs: Int64,
|
|
141
|
+
elapsedRealtimeMs: Int64,
|
|
142
|
+
options: VideoOptions,
|
|
143
|
+
metadata: AdMetadataSnapshot,
|
|
144
|
+
adapterVersion: String?
|
|
145
|
+
) -> ForgeOutcome {
|
|
146
|
+
outcome(bridge.recordAdEvent(
|
|
147
|
+
viewId: u64(view.id),
|
|
148
|
+
event: appleAdEvent(event),
|
|
149
|
+
timestamp: AppleSemanticTimestamp(timemarkMs: u64(timemarkMs), elapsedRealtimeMs: u64(elapsedRealtimeMs)),
|
|
150
|
+
overridesJson: options.overridesJson,
|
|
151
|
+
metadata: appleAdMetadata(metadata),
|
|
152
|
+
adapterVersion: adapterVersion
|
|
153
|
+
))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func registerContentMetadataProvider(view: RuntimeVideoView, provider: RuntimeContentMetadataProvider?) -> ForgeOutcome {
|
|
157
|
+
if let provider {
|
|
158
|
+
let callback = UniFFIContentMetadataProvider(provider: provider)
|
|
159
|
+
let result = outcome(bridge.setContentMetadataProvider(viewId: u64(view.id), provider: callback))
|
|
160
|
+
if result.isAccepted {
|
|
161
|
+
callbackLock.lock()
|
|
162
|
+
contentMetadataProviders[view.id] = callback
|
|
163
|
+
callbackLock.unlock()
|
|
164
|
+
}
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let result = outcome(bridge.clearContentMetadataProvider(viewId: u64(view.id)))
|
|
169
|
+
if result.isAccepted {
|
|
170
|
+
callbackLock.lock()
|
|
171
|
+
contentMetadataProviders.removeValue(forKey: view.id)
|
|
172
|
+
callbackLock.unlock()
|
|
173
|
+
}
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func registerVideoPreSendHook(view: RuntimeVideoView, hook: RuntimeVideoPreSendHook?) -> ForgeOutcome {
|
|
178
|
+
if let hook {
|
|
179
|
+
let callback = UniFFIVideoPreSendHook(view: view, hook: hook)
|
|
180
|
+
let result = outcome(bridge.setVideoPreSendHook(viewId: u64(view.id), hook: callback))
|
|
181
|
+
if result.isAccepted {
|
|
182
|
+
callbackLock.lock()
|
|
183
|
+
videoPreSendHooks[view.id] = callback
|
|
184
|
+
callbackLock.unlock()
|
|
185
|
+
}
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let result = outcome(bridge.clearVideoPreSendHook(viewId: u64(view.id)))
|
|
190
|
+
if result.isAccepted {
|
|
191
|
+
callbackLock.lock()
|
|
192
|
+
videoPreSendHooks.removeValue(forKey: view.id)
|
|
193
|
+
callbackLock.unlock()
|
|
194
|
+
}
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
func registerSessionPreSendHook(_ hook: RuntimeSessionPreSendHook?) -> ForgeOutcome {
|
|
199
|
+
if let hook {
|
|
200
|
+
let callback = UniFFISessionPreSendHook(hook: hook)
|
|
201
|
+
let result = outcome(bridge.setSessionPreSendHook(hook: callback))
|
|
202
|
+
if result.isAccepted {
|
|
203
|
+
callbackLock.lock()
|
|
204
|
+
sessionPreSendHook = callback
|
|
205
|
+
callbackLock.unlock()
|
|
206
|
+
}
|
|
207
|
+
return result
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let result = outcome(bridge.clearSessionPreSendHook())
|
|
211
|
+
if result.isAccepted {
|
|
212
|
+
callbackLock.lock()
|
|
213
|
+
sessionPreSendHook = nil
|
|
214
|
+
callbackLock.unlock()
|
|
215
|
+
}
|
|
216
|
+
return result
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func setCapturePlanListener(_ listener: ((ForgeCapturePlan) -> Void)?) -> ForgeOutcome {
|
|
220
|
+
if let listener {
|
|
221
|
+
let callback = UniFFICapturePlanListener(callback: listener)
|
|
222
|
+
// Registering retains the wrapper first so the synchronous
|
|
223
|
+
// latest-plan replay from the bridge is delivered through a
|
|
224
|
+
// wrapper the runtime keeps alive.
|
|
225
|
+
callbackLock.lock()
|
|
226
|
+
capturePlanListener = callback
|
|
227
|
+
callbackLock.unlock()
|
|
228
|
+
let result = outcome(bridge.setCapturePlanListener(listener: callback))
|
|
229
|
+
if !result.isAccepted {
|
|
230
|
+
callbackLock.lock()
|
|
231
|
+
capturePlanListener = nil
|
|
232
|
+
callbackLock.unlock()
|
|
233
|
+
}
|
|
234
|
+
return result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let result = outcome(bridge.clearCapturePlanListener())
|
|
238
|
+
if result.isAccepted {
|
|
239
|
+
callbackLock.lock()
|
|
240
|
+
capturePlanListener = nil
|
|
241
|
+
callbackLock.unlock()
|
|
242
|
+
}
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func capturePlan() -> ForgeCapturePlan? {
|
|
247
|
+
bridge.capturePlan().map(forgeCapturePlan)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func destroyVideoView(_ view: RuntimeVideoView) -> ForgeOutcome {
|
|
251
|
+
// Mirror the Android destroyVideoView contract: release both per-view
|
|
252
|
+
// callback registrations (bridge side and Swift-retained wrappers) so
|
|
253
|
+
// destroyed views never accumulate callback state across the app lifetime.
|
|
254
|
+
_ = bridge.clearContentMetadataProvider(viewId: u64(view.id))
|
|
255
|
+
_ = bridge.clearVideoPreSendHook(viewId: u64(view.id))
|
|
256
|
+
callbackLock.lock()
|
|
257
|
+
contentMetadataProviders.removeValue(forKey: view.id)
|
|
258
|
+
videoPreSendHooks.removeValue(forKey: view.id)
|
|
259
|
+
callbackLock.unlock()
|
|
260
|
+
return .accepted
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
func snapshotEvents() -> [ForgeRecordedEvent] {
|
|
264
|
+
bridge.snapshotEvents().map(recordedEvent)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func destroy() -> ForgeOutcome {
|
|
268
|
+
let result = outcome(bridge.destroy())
|
|
269
|
+
_ = bridge.clearCapturePlanListener()
|
|
270
|
+
callbackLock.lock()
|
|
271
|
+
contentMetadataProviders.removeAll()
|
|
272
|
+
videoPreSendHooks.removeAll()
|
|
273
|
+
sessionPreSendHook = nil
|
|
274
|
+
capturePlanListener = nil
|
|
275
|
+
callbackLock.unlock()
|
|
276
|
+
return result
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private func outcome(_ outcome: AppleBindingOutcome) -> ForgeOutcome {
|
|
281
|
+
switch outcome.status {
|
|
282
|
+
case .accepted:
|
|
283
|
+
return .accepted
|
|
284
|
+
case .deferred:
|
|
285
|
+
return .deferred
|
|
286
|
+
case .dropped:
|
|
287
|
+
return .dropped
|
|
288
|
+
case .ignoredDuplicate:
|
|
289
|
+
return .ignoredDuplicate
|
|
290
|
+
case .ignoredBeforeSessionStart:
|
|
291
|
+
return .ignoredBeforeSessionStart
|
|
292
|
+
case .ignoredInvalidState:
|
|
293
|
+
return .ignoredInvalidState
|
|
294
|
+
case .failedWithDiagnostic:
|
|
295
|
+
return .failedWithDiagnostic(outcome.reason ?? "native_runtime_failed")
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private func appleVideoEvent(_ event: PlayerEvent) -> AppleVideoEvent {
|
|
300
|
+
switch event {
|
|
301
|
+
case .initEvent: return .`init`
|
|
302
|
+
case .start: return .start
|
|
303
|
+
case .join: return .join
|
|
304
|
+
case .pause: return .pause
|
|
305
|
+
case .resume: return .resume
|
|
306
|
+
case .seekBegin: return .seekBegin
|
|
307
|
+
case .seekEnd: return .seekEnd
|
|
308
|
+
case .bufferBegin: return .bufferBegin
|
|
309
|
+
case .bufferEnd: return .bufferEnd
|
|
310
|
+
case .stop: return .stop
|
|
311
|
+
case .error: return .error
|
|
312
|
+
case .fatalError: return .fatalError
|
|
313
|
+
case .destroy: return .destroy
|
|
314
|
+
case .customVideoEvent: return .custom
|
|
315
|
+
case .customVideoEventEnd: return .customEnd
|
|
316
|
+
case .metadataUpdate: return .metadataUpdate
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private func appleAdEvent(_ event: AdEvent) -> AppleAdEvent {
|
|
321
|
+
switch event {
|
|
322
|
+
case .manifest: return .manifest
|
|
323
|
+
case .breakStart: return .breakStart
|
|
324
|
+
case .adInit: return .`init`
|
|
325
|
+
case .start: return .start
|
|
326
|
+
case .join: return .join
|
|
327
|
+
case .pause: return .pause
|
|
328
|
+
case .resume: return .resume
|
|
329
|
+
case .bufferBegin: return .bufferBegin
|
|
330
|
+
case .bufferEnd: return .bufferEnd
|
|
331
|
+
case .quartile: return .quartile
|
|
332
|
+
case .click: return .click
|
|
333
|
+
case .skip: return .skip
|
|
334
|
+
case .stop: return .stop
|
|
335
|
+
case .error: return .error
|
|
336
|
+
case .fatalError: return .fatalError
|
|
337
|
+
case .breakStop: return .breakStop
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private func appleContentMetadata(_ metadata: PlayerMetadataSnapshot) -> AppleContentMetadata {
|
|
342
|
+
AppleContentMetadata(
|
|
343
|
+
player: metadata.playerName,
|
|
344
|
+
playerVersion: metadata.playerVersion,
|
|
345
|
+
resource: metadata.resourceUrl,
|
|
346
|
+
parsedResource: metadata.parsedResource,
|
|
347
|
+
title: metadata.title,
|
|
348
|
+
program: metadata.program,
|
|
349
|
+
transactionCode: nil,
|
|
350
|
+
contentId: nil,
|
|
351
|
+
contentType: appleContentType(metadata.contentType),
|
|
352
|
+
playheadMs: metadata.playheadMs.map(u64),
|
|
353
|
+
durationMs: metadata.durationMs.map(u64),
|
|
354
|
+
bitrate: metadata.bitrate,
|
|
355
|
+
throughput: nil,
|
|
356
|
+
totalBytes: nil,
|
|
357
|
+
droppedFrames: nil,
|
|
358
|
+
rendition: metadata.rendition,
|
|
359
|
+
videoCodec: metadata.videoCodec,
|
|
360
|
+
audioCodec: metadata.audioCodec,
|
|
361
|
+
codecProfile: metadata.codecProfile,
|
|
362
|
+
containerFormat: metadata.containerFormat,
|
|
363
|
+
legacyCustomDimensions: metadata.legacyCustomDimensions.isEmpty
|
|
364
|
+
? nil : metadata.legacyCustomDimensions,
|
|
365
|
+
customDimensions: metadata.customDimensions.isEmpty
|
|
366
|
+
? nil : metadata.customDimensions,
|
|
367
|
+
customMetrics: metadata.customMetrics.isEmpty
|
|
368
|
+
? nil : metadata.customMetrics,
|
|
369
|
+
errorCode: metadata.errorCode,
|
|
370
|
+
errorMessage: metadata.errorMessage,
|
|
371
|
+
errorMetadata: metadata.errorMetadata,
|
|
372
|
+
errorLevel: metadata.errorLevel,
|
|
373
|
+
forceInit: metadata.forceInit,
|
|
374
|
+
convertFromBuffer: metadata.convertFromBuffer,
|
|
375
|
+
convertFromSeek: metadata.convertFromSeek,
|
|
376
|
+
customEventJson: metadata.customEvent.flatMap(customEventJson),
|
|
377
|
+
// Rust fills only unset typed fields from this overlay, so typed
|
|
378
|
+
// snapshot facts always win over extraParams keys.
|
|
379
|
+
extraParams: metadata.extraParams.isEmpty ? nil : metadata.extraParams
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private func appleMetadataSnapshot(_ snapshot: MetadataSnapshot) -> AppleMetadataSnapshot {
|
|
384
|
+
AppleMetadataSnapshot(
|
|
385
|
+
content: appleContentMetadata(snapshot.content),
|
|
386
|
+
qos: snapshot.qos.map(appleQosSnapshot)
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private func appleQosSnapshot(_ snapshot: QosSnapshot) -> AppleQosSnapshot {
|
|
391
|
+
AppleQosSnapshot(
|
|
392
|
+
throughput: snapshot.throughput,
|
|
393
|
+
fps: snapshot.fps,
|
|
394
|
+
latency: snapshot.latency,
|
|
395
|
+
playrate: snapshot.playrate,
|
|
396
|
+
droppedFrames: snapshot.droppedFrames.map { UInt64(clamping: $0) },
|
|
397
|
+
totalBytes: snapshot.totalBytes,
|
|
398
|
+
packetLoss: nil,
|
|
399
|
+
packetSent: nil,
|
|
400
|
+
droppedPackets: nil,
|
|
401
|
+
lostPackets: nil,
|
|
402
|
+
multicastAffectionTimeMs: nil
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private func appleAdMetadata(_ metadata: AdMetadataSnapshot) -> AppleAdMetadata {
|
|
407
|
+
AppleAdMetadata(
|
|
408
|
+
creativeId: metadata.creativeId ?? metadata.adId,
|
|
409
|
+
adUrl: metadata.adUrl,
|
|
410
|
+
adTitle: metadata.adTitle,
|
|
411
|
+
adDurationMs: metadata.adDurationMs.map(u64),
|
|
412
|
+
adPlayheadMs: metadata.adPlayheadMs.map(u64),
|
|
413
|
+
adBitrate: metadata.adBitrate,
|
|
414
|
+
position: metadata.position.map(appleAdPosition),
|
|
415
|
+
skippable: nil,
|
|
416
|
+
quartile: metadata.quartile.map { UInt32(clamping: $0) },
|
|
417
|
+
insertionType: appleAdInsertionType(metadata.insertionType),
|
|
418
|
+
viewability: metadata.viewability.map {
|
|
419
|
+
AppleAdViewabilitySample(
|
|
420
|
+
maxIntervalMs: $0.maxIntervalMs,
|
|
421
|
+
viewedDurationMs: $0.viewedDurationMs.map(u64)
|
|
422
|
+
)
|
|
423
|
+
},
|
|
424
|
+
errorCode: metadata.errorCode,
|
|
425
|
+
errorMessage: metadata.errorMessage,
|
|
426
|
+
errorMetadata: metadata.errorMetadata,
|
|
427
|
+
errorLevel: metadata.errorLevel,
|
|
428
|
+
// Rust fills only unset typed fields from this overlay, so typed
|
|
429
|
+
// snapshot facts always win over extraParams keys.
|
|
430
|
+
extraParams: metadata.extraParams.isEmpty ? nil : metadata.extraParams
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private func forgeCapturePlan(_ plan: AppleCapturePlan) -> ForgeCapturePlan {
|
|
435
|
+
// One-to-one flat passthrough into the shared factory so the typed plan
|
|
436
|
+
// (including the F3 sections knobs) cannot drift from the UniFFI shape.
|
|
437
|
+
ForgeCapturePlan.fromUniFFIFlat(
|
|
438
|
+
planSeq: plan.planSeq,
|
|
439
|
+
bits: plan.bits,
|
|
440
|
+
configVersion: plan.configVersion,
|
|
441
|
+
focusDwellThresholdMs: plan.focusDwellThresholdMs,
|
|
442
|
+
focusDedupeMs: plan.focusDedupeMs,
|
|
443
|
+
focusRepeatUnchangedDwell: plan.focusRepeatUnchangedDwell,
|
|
444
|
+
maxTargetTextLength: plan.maxTargetTextLength,
|
|
445
|
+
maxPayloadFields: plan.maxPayloadFields,
|
|
446
|
+
joinMethod: plan.joinMethod,
|
|
447
|
+
seekHeuristicEnabled: plan.seekHeuristicEnabled,
|
|
448
|
+
seekJumpThresholdMs: plan.seekJumpThresholdMs,
|
|
449
|
+
metadataSamplingMs: plan.metadataSamplingMs,
|
|
450
|
+
sectionsVisibilityThresholdPct: plan.sectionsVisibilityThresholdPct,
|
|
451
|
+
sectionsImpressionMinVisibleMs: plan.sectionsImpressionMinVisibleMs,
|
|
452
|
+
sectionsDwellMs: plan.sectionsDwellMs,
|
|
453
|
+
sectionsMaxTrackedItems: plan.sectionsMaxTrackedItems,
|
|
454
|
+
sectionsMaxSections: plan.sectionsMaxSections,
|
|
455
|
+
sectionsSectionSelector: plan.sectionsSectionSelector,
|
|
456
|
+
sectionsItemSelector: plan.sectionsItemSelector,
|
|
457
|
+
sectionsSectionNativeIds: plan.sectionsSectionNativeIds,
|
|
458
|
+
sectionsItemNativeIds: plan.sectionsItemNativeIds,
|
|
459
|
+
sectionsNameFrom: plan.sectionsNameFrom,
|
|
460
|
+
targetDataAttributes: plan.targetDataAttributes,
|
|
461
|
+
attributionExtraQueryKeys: plan.attributionExtraQueryKeys,
|
|
462
|
+
attributionIncludeLandingUrl: plan.attributionIncludeLandingUrl,
|
|
463
|
+
attributionDeriveFromDeepLinks: plan.attributionDeriveFromDeepLinks,
|
|
464
|
+
attributionInstallReferrerEnabled: plan.attributionInstallReferrerEnabled,
|
|
465
|
+
hash: plan.hash
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private func recordedEvent(_ event: AppleRuntimeEventSnapshot) -> ForgeRecordedEvent {
|
|
470
|
+
ForgeRecordedEvent(
|
|
471
|
+
eventName: event.eventName,
|
|
472
|
+
timestampMs: i64(event.timemarkMs),
|
|
473
|
+
payload: event.payload,
|
|
474
|
+
urgent: event.eventName == "sessionstart" || event.eventName == "sessionstop",
|
|
475
|
+
viewKey: event.viewId.map { String($0) }
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private func appleContentType(_ contentType: ContentType) -> AppleContentType {
|
|
480
|
+
switch contentType {
|
|
481
|
+
case .live: return .live
|
|
482
|
+
case .vod: return .vod
|
|
483
|
+
case .audio: return .audio
|
|
484
|
+
case .unknown: return .unknown
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private func appleAdPosition(_ position: AdPosition) -> AppleAdPosition {
|
|
489
|
+
switch position {
|
|
490
|
+
case .pre: return .pre
|
|
491
|
+
case .mid: return .mid
|
|
492
|
+
case .post: return .post
|
|
493
|
+
case .unknown: return .unknown
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private func appleAdInsertionType(_ insertionType: AdInsertionType) -> AppleAdInsertionType {
|
|
498
|
+
switch insertionType {
|
|
499
|
+
case .clientSide: return .clientSide
|
|
500
|
+
case .serverSide: return .serverSide
|
|
501
|
+
case .unknown: return .unknown
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private func customEventJson(_ payload: CustomVideoEventPayload) -> String? {
|
|
506
|
+
var object: [String: Any] = [
|
|
507
|
+
"name": payload.name,
|
|
508
|
+
"dimensions": payload.dimensions,
|
|
509
|
+
"values": payload.values,
|
|
510
|
+
"topLevel": payload.topLevel,
|
|
511
|
+
"hasEndDatetime": payload.hasEndDatetime,
|
|
512
|
+
]
|
|
513
|
+
if let streamKey = payload.streamKey {
|
|
514
|
+
object["streamKey"] = streamKey
|
|
515
|
+
}
|
|
516
|
+
guard JSONSerialization.isValidJSONObject(object),
|
|
517
|
+
let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]) else {
|
|
518
|
+
return nil
|
|
519
|
+
}
|
|
520
|
+
return String(data: data, encoding: .utf8)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private func u64(_ value: Int64) -> UInt64 {
|
|
524
|
+
UInt64(value < 0 ? 0 : value)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private func i64(_ value: UInt64) -> Int64 {
|
|
528
|
+
value > UInt64(Int64.max) ? Int64.max : Int64(value)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private final class UniFFIContentMetadataProvider: AppleContentMetadataProvider, @unchecked Sendable {
|
|
532
|
+
private weak var provider: RuntimeContentMetadataProvider?
|
|
533
|
+
|
|
534
|
+
init(provider: RuntimeContentMetadataProvider) {
|
|
535
|
+
self.provider = provider
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
func metadataSnapshot(viewId: UInt64, timestamp: AppleSemanticTimestamp) -> AppleMetadataSnapshot? {
|
|
539
|
+
provider?
|
|
540
|
+
.metadataSnapshot(
|
|
541
|
+
timemarkMs: i64(timestamp.timemarkMs),
|
|
542
|
+
elapsedRealtimeMs: i64(timestamp.elapsedRealtimeMs)
|
|
543
|
+
)
|
|
544
|
+
.map(appleMetadataSnapshot)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private final class UniFFIVideoPreSendHook: AppleVideoPreSendHook, @unchecked Sendable {
|
|
549
|
+
private let view: RuntimeVideoView
|
|
550
|
+
private let hook: RuntimeVideoPreSendHook
|
|
551
|
+
|
|
552
|
+
init(view: RuntimeVideoView, hook: RuntimeVideoPreSendHook) {
|
|
553
|
+
self.view = view
|
|
554
|
+
self.hook = hook
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
func willSend(eventName: String, viewId: UInt64, serviceName: String, params: [String: String]) -> [String: String] {
|
|
558
|
+
var mutableParams = params
|
|
559
|
+
hook.willSendRequest(
|
|
560
|
+
eventName: eventName,
|
|
561
|
+
view: view.id == i64(viewId) ? view : RuntimeVideoView(id: i64(viewId)),
|
|
562
|
+
serviceName: serviceName,
|
|
563
|
+
params: &mutableParams
|
|
564
|
+
)
|
|
565
|
+
return mutableParams
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private final class UniFFISessionPreSendHook: AppleSessionPreSendHook, @unchecked Sendable {
|
|
570
|
+
private let hook: RuntimeSessionPreSendHook
|
|
571
|
+
|
|
572
|
+
init(hook: RuntimeSessionPreSendHook) {
|
|
573
|
+
self.hook = hook
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
func willSend(serviceName: String, params: [String: String]) -> [String: String] {
|
|
577
|
+
var mutableParams = params
|
|
578
|
+
hook.willSendRequest(serviceName: serviceName, params: &mutableParams)
|
|
579
|
+
return mutableParams
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private final class UniFFICapturePlanListener: AppleCapturePlanListener, @unchecked Sendable {
|
|
584
|
+
private let callback: (ForgeCapturePlan) -> Void
|
|
585
|
+
|
|
586
|
+
init(callback: @escaping (ForgeCapturePlan) -> Void) {
|
|
587
|
+
self.callback = callback
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
func onPlan(plan: AppleCapturePlan) {
|
|
591
|
+
callback(forgeCapturePlan(plan))
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
#endif
|