@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,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