@npaw/forge-react-native 9.7.1 → 9.7.3
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/android/build.gradle +1 -1
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +10 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +59 -1
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +33 -0
- package/android/src/test/java/com/npaw/forge/reactnative/ForgeInstanceRegistryTest.java +65 -0
- package/app.plugin.js +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +8 -8
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +198 -1
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +12 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ConnectionType.swift +125 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +67 -1
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +4 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +80 -3
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +44 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/LocationObserver.swift +183 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +123 -2
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +59 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +17 -1
- package/ios/ForgeInstanceRegistry.swift +69 -0
- package/ios/ForgeModuleImpl.swift +53 -6
- package/ios/NPAWForgeTurboModule.mm +6 -1
- package/package.json +3 -1
- package/plugin/index.js +82 -0
- package/plugin/mutations.js +140 -0
- package/src/NPAWForge.ts +12 -1
- package/src/events.ts +21 -1
- package/src/index.ts +11 -1
- package/src/sections.ts +292 -0
- package/src/types.ts +23 -2
|
@@ -40,7 +40,7 @@ public final class NPAWForge: NSObject {
|
|
|
40
40
|
/// `scripts/apple/publish-local-shipped.sh` rewrites this literal to the
|
|
41
41
|
/// release version when staging the shipped package, and fails loudly if
|
|
42
42
|
/// it cannot find it. Keep it a plain stored literal for that reason.
|
|
43
|
-
public static let defaultPluginVersion = "9.7.
|
|
43
|
+
public static let defaultPluginVersion = "9.7.3"
|
|
44
44
|
public static let defaultAppName = "apple"
|
|
45
45
|
public static let defaultBuildVariant = "release"
|
|
46
46
|
public static let defaultLmaEndpoint = "https://lma.npaw.com"
|
|
@@ -89,6 +89,12 @@ public final class NPAWForge: NSObject {
|
|
|
89
89
|
private var destroyed = false
|
|
90
90
|
private let lock = NSRecursiveLock()
|
|
91
91
|
private var videoAdapters: [VideoAdapter] = []
|
|
92
|
+
/// Set when automatic connection type collection is on. Held so `destroy()`
|
|
93
|
+
/// can cancel the path monitor that belongs to this instance.
|
|
94
|
+
private var connectionTypeObserver: ForgeConnectionTypeObserver?
|
|
95
|
+
/// Set when the integrator allowed location observation. It reads nothing
|
|
96
|
+
/// until a capture plan carries the location bit.
|
|
97
|
+
private var locationObserver: ForgeLocationObserver?
|
|
92
98
|
|
|
93
99
|
public init(kernel: ForgeApple) {
|
|
94
100
|
self.kernel = kernel
|
|
@@ -102,6 +108,14 @@ public final class NPAWForge: NSObject {
|
|
|
102
108
|
kernel.subscribeEvents(observer)
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
/// Observes the kernel's rendered decision log - the same lines
|
|
112
|
+
/// `logLevel: trace` writes to the platform console. Registering is not
|
|
113
|
+
/// required for console output; it is for hosts that forward the stream
|
|
114
|
+
/// somewhere else, as the React Native binding does.
|
|
115
|
+
public func addLogListener(_ listener: @escaping (ForgeRenderedLog) -> Void) -> ForgeEventSubscription {
|
|
116
|
+
kernel.addLogListener(listener)
|
|
117
|
+
}
|
|
118
|
+
|
|
105
119
|
/// Observes kernel-derived capture plans (bitmask + focus/limit knobs +
|
|
106
120
|
/// behavior hints). The latest plan is replayed to listeners added after
|
|
107
121
|
/// the first emission. The kernel intake gates remain the semantic
|
|
@@ -232,12 +246,55 @@ public final class NPAWForge: NSObject {
|
|
|
232
246
|
defer { lock.unlock() }
|
|
233
247
|
guard !destroyed else { return .ignoredDuplicate }
|
|
234
248
|
destroyed = true
|
|
249
|
+
connectionTypeObserver?.stop()
|
|
250
|
+
connectionTypeObserver = nil
|
|
251
|
+
locationObserver?.stop()
|
|
252
|
+
locationObserver = nil
|
|
235
253
|
let adapters = videoAdapters
|
|
236
254
|
videoAdapters.removeAll()
|
|
237
255
|
adapters.forEach { _ = $0.destroy() }
|
|
238
256
|
return kernel.destroy()
|
|
239
257
|
}
|
|
240
258
|
|
|
259
|
+
/// Reports the device's current connection class to the kernel. Called by
|
|
260
|
+
/// the automatic observer; a `connectionType` the integrator set through
|
|
261
|
+
/// content metadata keeps precedence over it.
|
|
262
|
+
@discardableResult
|
|
263
|
+
public func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome {
|
|
264
|
+
kernel.setAutoConnectionType(connectionType)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Forwards a location fix to the kernel, which owns every threshold.
|
|
268
|
+
@discardableResult
|
|
269
|
+
public func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome {
|
|
270
|
+
kernel.recordLocationFix(fix)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Starts or stops location observation to match the capture plan. Nothing
|
|
274
|
+
/// reads location - not even the authorization status - until an operator
|
|
275
|
+
/// enables one of the two motion switches.
|
|
276
|
+
func applyLocationPlan(_ plan: ForgeCapturePlan) {
|
|
277
|
+
guard let observer = locationObserver else { return }
|
|
278
|
+
if plan.bits & ForgeCapturePlan.Bits.location != 0 {
|
|
279
|
+
observer.start()
|
|
280
|
+
} else {
|
|
281
|
+
observer.stop()
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func installLocationObserver(_ observer: ForgeLocationObserver) {
|
|
286
|
+
lock.lock()
|
|
287
|
+
locationObserver = observer
|
|
288
|
+
lock.unlock()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
func installConnectionTypeObserver(_ observer: ForgeConnectionTypeObserver) {
|
|
292
|
+
lock.lock()
|
|
293
|
+
connectionTypeObserver = observer
|
|
294
|
+
lock.unlock()
|
|
295
|
+
observer.start()
|
|
296
|
+
}
|
|
297
|
+
|
|
241
298
|
func buildVideoAdapter(
|
|
242
299
|
identifier: String,
|
|
243
300
|
options: VideoOptions,
|
|
@@ -299,8 +356,11 @@ public final class NPAWForge: NSObject {
|
|
|
299
356
|
private var preSessionEventQueueMaxAgeMs: Int64 = 30_000
|
|
300
357
|
private var logLevel: ForgeLogLevel = .warning
|
|
301
358
|
private var autoDeviceIdentityEnabled = true
|
|
359
|
+
private var autoConnectionTypeEnabled = true
|
|
360
|
+
private var autoLocationEnabled = true
|
|
302
361
|
private var deviceUuid: String?
|
|
303
362
|
private var deviceInfo: ForgeDeviceInfo?
|
|
363
|
+
private var framework: String?
|
|
304
364
|
private var services: ForgeAppleServices?
|
|
305
365
|
|
|
306
366
|
public init(accountCode: String) {
|
|
@@ -391,6 +451,42 @@ public final class NPAWForge: NSObject {
|
|
|
391
451
|
return self
|
|
392
452
|
}
|
|
393
453
|
|
|
454
|
+
/// Disables automatic collection of the device connection type
|
|
455
|
+
/// (`connectionType`: `wifi`, `cellular`, `ethernet`, `other` or
|
|
456
|
+
/// `unknown`).
|
|
457
|
+
///
|
|
458
|
+
/// Collection is on by default and uses `NWPathMonitor`, which needs no
|
|
459
|
+
/// entitlement and no user prompt. A `connectionType` supplied through
|
|
460
|
+
/// content metadata always wins, whether or not this is enabled.
|
|
461
|
+
@discardableResult
|
|
462
|
+
public func setAutoConnectionTypeEnabled(_ enabled: Bool) -> Builder {
|
|
463
|
+
autoConnectionTypeEnabled = enabled
|
|
464
|
+
return self
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/// Disables location observation entirely, whatever an operator
|
|
468
|
+
/// configures remotely.
|
|
469
|
+
///
|
|
470
|
+
/// Forge never requests location authorization and reads location only
|
|
471
|
+
/// when the app already holds one AND an operator enabled a motion
|
|
472
|
+
/// switch. This is the local override for integrators who would rather
|
|
473
|
+
/// Forge never touch location at all.
|
|
474
|
+
@discardableResult
|
|
475
|
+
public func setAutoLocationEnabled(_ enabled: Bool) -> Builder {
|
|
476
|
+
autoLocationEnabled = enabled
|
|
477
|
+
return self
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/// Declares the framework layered on top of this facade. First-party
|
|
481
|
+
/// bindings only: React Native sets `"react-native"` so its traffic is
|
|
482
|
+
/// distinguishable in `pluginInfo`. Unknown values are dropped by the
|
|
483
|
+
/// kernel, and the reported platform stays `apple` either way.
|
|
484
|
+
@discardableResult
|
|
485
|
+
public func setFramework(_ value: String?) -> Builder {
|
|
486
|
+
framework = value
|
|
487
|
+
return self
|
|
488
|
+
}
|
|
489
|
+
|
|
394
490
|
/// Overrides the automatically collected device identifier.
|
|
395
491
|
@discardableResult
|
|
396
492
|
public func setDeviceUuid(_ value: String?) -> Builder {
|
|
@@ -430,9 +526,34 @@ public final class NPAWForge: NSObject {
|
|
|
430
526
|
preSessionEventQueueMaxAgeMs: preSessionEventQueueMaxAgeMs,
|
|
431
527
|
logLevel: logLevel,
|
|
432
528
|
deviceUuid: resolvedDeviceUuid,
|
|
433
|
-
deviceInfo: resolvedDeviceInfo?.isEmpty == true ? nil : resolvedDeviceInfo
|
|
529
|
+
deviceInfo: resolvedDeviceInfo?.isEmpty == true ? nil : resolvedDeviceInfo,
|
|
530
|
+
framework: framework
|
|
434
531
|
)
|
|
435
532
|
let forge = NPAWForge(kernel: ForgeApple(config: config, services: resolvedServices))
|
|
533
|
+
if autoLocationEnabled {
|
|
534
|
+
// Created here but inert: it reads nothing until a capture plan
|
|
535
|
+
// carries the location bit. The plan registry replays the
|
|
536
|
+
// latest plan to a late subscriber, so this covers a plan that
|
|
537
|
+
// already arrived as well as later ones.
|
|
538
|
+
forge.installLocationObserver(
|
|
539
|
+
ForgeLocationObserver { [weak forge] fix in
|
|
540
|
+
forge?.recordLocationFix(fix)
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
_ = forge.addCapturePlanListener { [weak forge] plan in
|
|
544
|
+
forge?.applyLocationPlan(plan)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if autoConnectionTypeEnabled {
|
|
548
|
+
// Started once here, for the life of the instance, and
|
|
549
|
+
// cancelled in `destroy()`. A monitor per view is what would
|
|
550
|
+
// leak one per playback.
|
|
551
|
+
forge.installConnectionTypeObserver(
|
|
552
|
+
ForgeConnectionTypeObserver { [weak forge] connectionType in
|
|
553
|
+
forge?.setAutoConnectionType(connectionType)
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
}
|
|
436
557
|
ForgeAutomaticCaptureRegistry.installIfAvailable(forge)
|
|
437
558
|
return forge
|
|
438
559
|
}
|
|
@@ -26,6 +26,7 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
26
26
|
private var videoPreSendHooks: [Int64: UniFFIVideoPreSendHook] = [:]
|
|
27
27
|
private var sessionPreSendHook: UniFFISessionPreSendHook?
|
|
28
28
|
private var capturePlanListener: UniFFICapturePlanListener?
|
|
29
|
+
private var logListener: UniFFILogListener?
|
|
29
30
|
|
|
30
31
|
init(config: ForgeAppleConfig) {
|
|
31
32
|
bridge = ForgeRuntimeBridge(config: AppleRuntimeConfig(
|
|
@@ -40,6 +41,7 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
40
41
|
logLevel: config.logLevel.wireName,
|
|
41
42
|
deviceUuid: config.deviceUuid,
|
|
42
43
|
deviceInfoJson: config.deviceInfo?.toRuntimeJson(),
|
|
44
|
+
framework: config.framework,
|
|
43
45
|
preSessionEventPolicy: ApplePreSessionEventPolicy(
|
|
44
46
|
processEventsBeforeSessionStart: config.processEventsBeforeSessionStart,
|
|
45
47
|
queueLimitEvents: UInt64(max(0, config.preSessionEventQueueLimitEvents)),
|
|
@@ -56,6 +58,19 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
56
58
|
outcome(bridge.setLogLevel(logLevel: level.wireName))
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome {
|
|
62
|
+
outcome(bridge.setAutoConnectionType(connectionType: connectionType))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome {
|
|
66
|
+
outcome(bridge.recordLocationFix(
|
|
67
|
+
latitude: fix.latitude,
|
|
68
|
+
longitude: fix.longitude,
|
|
69
|
+
accuracyM: fix.accuracyMetres,
|
|
70
|
+
timestampMs: u64(fix.timestampMs)
|
|
71
|
+
))
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView {
|
|
60
75
|
RuntimeVideoView(id: Int64(clamping: bridge.createVideoView(
|
|
61
76
|
nowMs: u64(timestampMs),
|
|
@@ -245,6 +260,33 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
245
260
|
return result
|
|
246
261
|
}
|
|
247
262
|
|
|
263
|
+
func setLogListener(_ listener: ((ForgeRenderedLog) -> Void)?) -> ForgeOutcome {
|
|
264
|
+
if let listener {
|
|
265
|
+
let callback = UniFFILogListener(callback: listener)
|
|
266
|
+
// Retain the wrapper before registering: the bridge replays the
|
|
267
|
+
// logs rendered before this point synchronously, and they must
|
|
268
|
+
// land in a wrapper the runtime keeps alive.
|
|
269
|
+
callbackLock.lock()
|
|
270
|
+
logListener = callback
|
|
271
|
+
callbackLock.unlock()
|
|
272
|
+
let result = outcome(bridge.setLogListener(listener: callback))
|
|
273
|
+
if !result.isAccepted {
|
|
274
|
+
callbackLock.lock()
|
|
275
|
+
logListener = nil
|
|
276
|
+
callbackLock.unlock()
|
|
277
|
+
}
|
|
278
|
+
return result
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let result = outcome(bridge.clearLogListener())
|
|
282
|
+
if result.isAccepted {
|
|
283
|
+
callbackLock.lock()
|
|
284
|
+
logListener = nil
|
|
285
|
+
callbackLock.unlock()
|
|
286
|
+
}
|
|
287
|
+
return result
|
|
288
|
+
}
|
|
289
|
+
|
|
248
290
|
func capturePlan() -> ForgeCapturePlan? {
|
|
249
291
|
bridge.capturePlan().map(forgeCapturePlan)
|
|
250
292
|
}
|
|
@@ -269,11 +311,13 @@ final class UniFFIForgeRuntime: ForgeRuntime {
|
|
|
269
311
|
func destroy() -> ForgeOutcome {
|
|
270
312
|
let result = outcome(bridge.destroy())
|
|
271
313
|
_ = bridge.clearCapturePlanListener()
|
|
314
|
+
_ = bridge.clearLogListener()
|
|
272
315
|
callbackLock.lock()
|
|
273
316
|
contentMetadataProviders.removeAll()
|
|
274
317
|
videoPreSendHooks.removeAll()
|
|
275
318
|
sessionPreSendHook = nil
|
|
276
319
|
capturePlanListener = nil
|
|
320
|
+
logListener = nil
|
|
277
321
|
callbackLock.unlock()
|
|
278
322
|
return result
|
|
279
323
|
}
|
|
@@ -593,4 +637,19 @@ private final class UniFFICapturePlanListener: AppleCapturePlanListener, @unchec
|
|
|
593
637
|
callback(forgeCapturePlan(plan))
|
|
594
638
|
}
|
|
595
639
|
}
|
|
640
|
+
|
|
641
|
+
private final class UniFFILogListener: AppleLogListener, @unchecked Sendable {
|
|
642
|
+
private let callback: (ForgeRenderedLog) -> Void
|
|
643
|
+
|
|
644
|
+
init(callback: @escaping (ForgeRenderedLog) -> Void) {
|
|
645
|
+
self.callback = callback
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
func onLog(log: AppleRenderedLog) {
|
|
649
|
+
// An unrecognized level from the bridge is dropped rather than
|
|
650
|
+
// relabelled, matching ForgeLogger.emitRendered.
|
|
651
|
+
guard let level = ForgeLogLevel.parse(log.level) else { return }
|
|
652
|
+
callback(ForgeRenderedLog(level: level, tag: log.tag, message: log.message))
|
|
653
|
+
}
|
|
654
|
+
}
|
|
596
655
|
#endif
|
|
@@ -65,10 +65,26 @@ open class PlayerAdapter: NSObject {
|
|
|
65
65
|
if playerObject != nil { registerListeners() }
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/// Detaches the adapter from its view.
|
|
69
|
+
///
|
|
70
|
+
/// The unbind happens **before** the listeners are unregistered, and that
|
|
71
|
+
/// order is the whole point. Unregistering is not instantaneous from the
|
|
72
|
+
/// player's perspective: a KVO delivery already in flight on a player
|
|
73
|
+
/// thread, or a periodic-time-observer block already enqueued on the main
|
|
74
|
+
/// queue (which `removeTimeObserver` does not cancel), can still run while
|
|
75
|
+
/// it happens. With `videoAdapter` cleared first, `record` fails closed and
|
|
76
|
+
/// those late callbacks go nowhere.
|
|
77
|
+
///
|
|
78
|
+
/// Clearing it afterwards is what produced the tvOS phantom view: a player
|
|
79
|
+
/// being deallocated reported a transport change into a view the kernel had
|
|
80
|
+
/// just stopped, and the kernel is contracted to treat a post-stop start as
|
|
81
|
+
/// a replay, so it opened an empty replacement view for every playback
|
|
82
|
+
/// (forge-feedback `native/2026-08-13_rn-video-teardown-reinits-view`).
|
|
68
83
|
func unbindFromVideoAdapter() {
|
|
69
|
-
|
|
84
|
+
let wasBound = isBound
|
|
70
85
|
isBound = false
|
|
71
86
|
videoAdapter = nil
|
|
87
|
+
if wasBound { unregisterListeners() }
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
open func registerListeners() {}
|
|
@@ -0,0 +1,69 @@
|
|
|
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: Public lookup from a React Native instance id to the live Forge facade, so application code can attach a native adapter to a player the shipped packages do not cover.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
#if canImport(NPAWForgeCore)
|
|
17
|
+
import NPAWForgeCore
|
|
18
|
+
#elseif canImport(NPAWForgeApple)
|
|
19
|
+
import NPAWForgeApple
|
|
20
|
+
#endif
|
|
21
|
+
|
|
22
|
+
/// Resolves the native Forge facade behind a React Native instance id.
|
|
23
|
+
///
|
|
24
|
+
/// **This is public API.** `resolve` and `resolveVideoAdapter` carry the usual
|
|
25
|
+
/// stability commitment: they will not be renamed or removed without a major
|
|
26
|
+
/// version and a release note.
|
|
27
|
+
///
|
|
28
|
+
/// It exists because the React Native packages bind two players — Media3 on
|
|
29
|
+
/// Android and AVPlayer on Apple, both through `react-native-video` — while
|
|
30
|
+
/// Forge ships native adapters for many more. Rather than wait for a
|
|
31
|
+
/// first-party package per player, application code can host its own native
|
|
32
|
+
/// view, resolve the facade from the instance id it already has in JS, and
|
|
33
|
+
/// attach any Forge adapter to it.
|
|
34
|
+
///
|
|
35
|
+
/// The React Native design deliberately keeps native player instances off the
|
|
36
|
+
/// bridge, and this does not weaken that: only the opaque instance id crosses
|
|
37
|
+
/// it. Resolution and attachment happen entirely in native code.
|
|
38
|
+
///
|
|
39
|
+
/// ```swift
|
|
40
|
+
/// guard let forge = ForgeInstanceRegistry.resolve(instanceId),
|
|
41
|
+
/// let video = forge.getVideoAdapter() else { return }
|
|
42
|
+
/// video.setPlayerAdapter(MyPlayerAdapter(player: myPlayer))
|
|
43
|
+
/// ```
|
|
44
|
+
///
|
|
45
|
+
/// Detach symmetrically when the view goes away — the registry holds the
|
|
46
|
+
/// facade, not your player, and it will not release the adapter for you.
|
|
47
|
+
///
|
|
48
|
+
/// The Android counterpart is `com.npaw.forge.reactnative.ForgeInstanceRegistry`
|
|
49
|
+
/// and carries the same two members with the same meaning.
|
|
50
|
+
public enum ForgeInstanceRegistry {
|
|
51
|
+
|
|
52
|
+
/// The live facade for `instanceId`, or `nil` if no such instance exists
|
|
53
|
+
/// or it has already been destroyed.
|
|
54
|
+
public static func resolve(_ instanceId: String) -> NPAWForge? {
|
|
55
|
+
ForgeModuleImpl.shared.resolveInstance(instanceId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The video adapter registered under `identifier` on the instance's
|
|
59
|
+
/// facade, or `nil` if either the instance or the adapter is absent.
|
|
60
|
+
///
|
|
61
|
+
/// Pass `nil` for `identifier` to get the default adapter, matching
|
|
62
|
+
/// `NPAWForge.getVideoAdapter(identifier:)`.
|
|
63
|
+
public static func resolveVideoAdapter(
|
|
64
|
+
_ instanceId: String,
|
|
65
|
+
identifier: String? = nil
|
|
66
|
+
) -> VideoAdapter? {
|
|
67
|
+
resolve(instanceId)?.getVideoAdapter(identifier: identifier)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -33,15 +33,23 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
33
33
|
|
|
34
34
|
/// Set by the TurboModule shell; receives the channel body for every kernel event.
|
|
35
35
|
@objc public var eventSink: (([String: Any]) -> Void)?
|
|
36
|
+
/// Set by the TurboModule shell; receives the channel body for every line
|
|
37
|
+
/// of the kernel's rendered decision log.
|
|
38
|
+
@objc public var logSink: (([String: Any]) -> Void)?
|
|
36
39
|
|
|
37
40
|
private let lock = NSLock()
|
|
38
41
|
private var instances: [String: NPAWForge] = [:]
|
|
39
42
|
private var subscriptions: [String: ForgeEventSubscription] = [:]
|
|
43
|
+
private var logSubscriptions: [String: ForgeEventSubscription] = [:]
|
|
40
44
|
private var adReporters: [Int: ManualAdReporter] = [:]
|
|
41
45
|
private var nextInstanceId = 1
|
|
42
46
|
private var nextReporterId = 1
|
|
43
47
|
private var listenerCount = 0
|
|
44
48
|
|
|
49
|
+
/// Declared to the native facade so React Native traffic is
|
|
50
|
+
/// distinguishable in `pluginInfo`; the reported platform stays `apple`.
|
|
51
|
+
static let frameworkMarker = "react-native"
|
|
52
|
+
|
|
45
53
|
private static let errUnknownInstance = "E_FORGE_UNKNOWN_INSTANCE"
|
|
46
54
|
private static let errUnknownReporter = "E_FORGE_UNKNOWN_REPORTER"
|
|
47
55
|
private static let errBadArgument = "E_FORGE_BAD_ARGUMENT"
|
|
@@ -71,6 +79,8 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
71
79
|
return
|
|
72
80
|
}
|
|
73
81
|
let builder = NPAWForge.Builder(accountCode: accountCode)
|
|
82
|
+
// The binding knows what it is; this is not a host-settable config key.
|
|
83
|
+
builder.setFramework(ForgeModuleImpl.frameworkMarker)
|
|
74
84
|
applyConfig(builder, config: config)
|
|
75
85
|
let forge = builder.build()
|
|
76
86
|
|
|
@@ -95,6 +105,7 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
95
105
|
lock.lock()
|
|
96
106
|
let forge = instances.removeValue(forKey: instanceId)
|
|
97
107
|
let subscription = subscriptions.removeValue(forKey: instanceId)
|
|
108
|
+
let logSubscription = logSubscriptions.removeValue(forKey: instanceId)
|
|
98
109
|
lock.unlock()
|
|
99
110
|
|
|
100
111
|
guard let forge else {
|
|
@@ -102,6 +113,7 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
102
113
|
return
|
|
103
114
|
}
|
|
104
115
|
subscription?.cancel()
|
|
116
|
+
logSubscription?.cancel()
|
|
105
117
|
resolve(Self.outcomeToDictionary(forge.destroy()))
|
|
106
118
|
}
|
|
107
119
|
|
|
@@ -901,8 +913,13 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
901
913
|
lock.lock()
|
|
902
914
|
listenerCount = max(0, listenerCount - count)
|
|
903
915
|
let shouldCancel = listenerCount == 0
|
|
904
|
-
let cancelled = shouldCancel
|
|
905
|
-
|
|
916
|
+
let cancelled = shouldCancel
|
|
917
|
+
? Array(subscriptions.values) + Array(logSubscriptions.values)
|
|
918
|
+
: []
|
|
919
|
+
if shouldCancel {
|
|
920
|
+
subscriptions.removeAll()
|
|
921
|
+
logSubscriptions.removeAll()
|
|
922
|
+
}
|
|
906
923
|
lock.unlock()
|
|
907
924
|
|
|
908
925
|
cancelled.forEach { $0.cancel() }
|
|
@@ -910,9 +927,10 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
910
927
|
|
|
911
928
|
@objc public func invalidate() {
|
|
912
929
|
lock.lock()
|
|
913
|
-
let cancelled = Array(subscriptions.values)
|
|
930
|
+
let cancelled = Array(subscriptions.values) + Array(logSubscriptions.values)
|
|
914
931
|
let live = Array(instances.values)
|
|
915
932
|
subscriptions.removeAll()
|
|
933
|
+
logSubscriptions.removeAll()
|
|
916
934
|
instances.removeAll()
|
|
917
935
|
adReporters.removeAll()
|
|
918
936
|
listenerCount = 0
|
|
@@ -991,8 +1009,15 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
991
1009
|
guard let self, let sink = self.eventSink else { return }
|
|
992
1010
|
sink(Self.recordedEventToDictionary(instanceId: instanceId, event: event))
|
|
993
1011
|
}
|
|
1012
|
+
// The kernel's decision log rides its own channel: it is not an event,
|
|
1013
|
+
// and hosts subscribe to it separately.
|
|
1014
|
+
let logSubscription = forge.addLogListener { [weak self] log in
|
|
1015
|
+
guard let self, let sink = self.logSink else { return }
|
|
1016
|
+
sink(Self.renderedLogToDictionary(instanceId: instanceId, log: log))
|
|
1017
|
+
}
|
|
994
1018
|
lock.lock()
|
|
995
1019
|
subscriptions[instanceId] = subscription
|
|
1020
|
+
logSubscriptions[instanceId] = logSubscription
|
|
996
1021
|
lock.unlock()
|
|
997
1022
|
}
|
|
998
1023
|
|
|
@@ -1022,9 +1047,19 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
1022
1047
|
builder.setPreSessionEventQueueMaxAgeMs((value as? NSNumber)?.int64Value ?? 0)
|
|
1023
1048
|
case "allowLocalHttpForTesting":
|
|
1024
1049
|
builder.setLocalPlaintextTransportForTesting(value as? Bool ?? false)
|
|
1025
|
-
case "deviceUuid"
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1050
|
+
case "deviceUuid":
|
|
1051
|
+
// A non-string value is ignored rather than used to clear the
|
|
1052
|
+
// override: passing nil here would disable an identifier the
|
|
1053
|
+
// app did configure.
|
|
1054
|
+
if let deviceUuid = value as? String {
|
|
1055
|
+
builder.setDeviceUuid(deviceUuid)
|
|
1056
|
+
}
|
|
1057
|
+
case "autoDeviceIdentityEnabled":
|
|
1058
|
+
// Matches the Android mapper: a real Bool wins, anything else
|
|
1059
|
+
// is parsed leniently and falls back to false.
|
|
1060
|
+
builder.setAutoDeviceIdentityEnabled(
|
|
1061
|
+
value as? Bool ?? (String(describing: value).lowercased() == "true")
|
|
1062
|
+
)
|
|
1028
1063
|
default:
|
|
1029
1064
|
NSLog("[NPAWForgeRN] Unknown ForgeConfig key '%@' ignored (forward compatibility)", key)
|
|
1030
1065
|
}
|
|
@@ -1128,4 +1163,16 @@ public final class ForgeModuleImpl: NSObject {
|
|
|
1128
1163
|
}
|
|
1129
1164
|
return result
|
|
1130
1165
|
}
|
|
1166
|
+
|
|
1167
|
+
static func renderedLogToDictionary(
|
|
1168
|
+
instanceId: String,
|
|
1169
|
+
log: ForgeRenderedLog
|
|
1170
|
+
) -> [String: Any] {
|
|
1171
|
+
[
|
|
1172
|
+
"instanceId": instanceId,
|
|
1173
|
+
"level": log.level.wireName,
|
|
1174
|
+
"tag": log.tag,
|
|
1175
|
+
"message": log.message,
|
|
1176
|
+
]
|
|
1177
|
+
}
|
|
1131
1178
|
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
#endif
|
|
26
26
|
|
|
27
27
|
static NSString *const kForgeEventChannel = @"NPAWForgeEvent";
|
|
28
|
+
static NSString *const kForgeLogChannel = @"NPAWForgeLog";
|
|
28
29
|
|
|
29
30
|
@interface NPAWForgeReactNative : RCTEventEmitter
|
|
30
31
|
#ifdef RCT_NEW_ARCH_ENABLED
|
|
@@ -48,13 +49,16 @@ RCT_EXPORT_MODULE(NPAWForgeRN)
|
|
|
48
49
|
ForgeModuleImpl.shared.eventSink = ^(NSDictionary<NSString *, id> *body) {
|
|
49
50
|
[weakSelf sendEventWithName:kForgeEventChannel body:body];
|
|
50
51
|
};
|
|
52
|
+
ForgeModuleImpl.shared.logSink = ^(NSDictionary<NSString *, id> *body) {
|
|
53
|
+
[weakSelf sendEventWithName:kForgeLogChannel body:body];
|
|
54
|
+
};
|
|
51
55
|
}
|
|
52
56
|
return self;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
- (NSArray<NSString *> *)supportedEvents
|
|
56
60
|
{
|
|
57
|
-
return @[ kForgeEventChannel ];
|
|
61
|
+
return @[ kForgeEventChannel, kForgeLogChannel ];
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
- (void)startObserving
|
|
@@ -70,6 +74,7 @@ RCT_EXPORT_MODULE(NPAWForgeRN)
|
|
|
70
74
|
- (void)invalidate
|
|
71
75
|
{
|
|
72
76
|
ForgeModuleImpl.shared.eventSink = nil;
|
|
77
|
+
ForgeModuleImpl.shared.logSink = nil;
|
|
73
78
|
[ForgeModuleImpl.shared invalidate];
|
|
74
79
|
[super invalidate];
|
|
75
80
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npaw/forge-react-native",
|
|
3
|
-
"version": "9.7.
|
|
3
|
+
"version": "9.7.3",
|
|
4
4
|
"description": "NPAW Forge analytics for React Native: New Architecture TurboModule facade over the native Forge Android/Apple SDKs.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Victor Soto <victor.soto@nicepeopleatwork.com>",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"NPAWForgeReactNative.podspec",
|
|
12
12
|
"README.md",
|
|
13
13
|
"android",
|
|
14
|
+
"app.plugin.js",
|
|
14
15
|
"ios",
|
|
16
|
+
"plugin",
|
|
15
17
|
"src"
|
|
16
18
|
],
|
|
17
19
|
"scripts": {
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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: Expo config plugin wiring the Forge host-project mutations into expo prebuild; all mutation logic lives in ./mutations.js so it is testable without Expo installed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { addForgeMavenRepository, addKotlinMetadataFlag } = require('./mutations');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} ForgePluginOptions
|
|
21
|
+
* @property {boolean} [artifactoryRepository=true] Add the NPAW Artifactory Maven repository.
|
|
22
|
+
* @property {boolean} [kotlinMetadataFlag=true] Add `-Xskip-metadata-version-check` to the app module.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Applies the native host-project changes the Forge React Native packages need.
|
|
27
|
+
*
|
|
28
|
+
* Expo Go cannot load native modules, so this is for development builds and
|
|
29
|
+
* `expo prebuild`; it does not make Forge work in Expo Go. Every mutation is
|
|
30
|
+
* idempotent because prebuild runs repeatedly.
|
|
31
|
+
*
|
|
32
|
+
* ```json
|
|
33
|
+
* { "expo": { "plugins": ["@npaw/forge-react-native"] } }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* This does NOT disable bridgeless. That is a source-level change in
|
|
37
|
+
* MainApplication and AppDelegate — see ./mutations.js for why automating it
|
|
38
|
+
* would do more harm than good — and it stays a documented manual step for
|
|
39
|
+
* applications using the video adapter package.
|
|
40
|
+
*/
|
|
41
|
+
function withForge(config, options = {}) {
|
|
42
|
+
const { withProjectBuildGradle, withAppBuildGradle } = require('@expo/config-plugins');
|
|
43
|
+
|
|
44
|
+
const settings = {
|
|
45
|
+
artifactoryRepository: true,
|
|
46
|
+
kotlinMetadataFlag: true,
|
|
47
|
+
...options,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let next = config;
|
|
51
|
+
|
|
52
|
+
if (settings.artifactoryRepository) {
|
|
53
|
+
next = withProjectBuildGradle(next, (mod) => {
|
|
54
|
+
if (mod.modResults.language !== 'groovy') {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Forge config plugin: the project build.gradle is not Groovy; apply the Maven repository manually',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
mod.modResults.contents = addForgeMavenRepository(mod.modResults.contents);
|
|
60
|
+
return mod;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (settings.kotlinMetadataFlag) {
|
|
65
|
+
next = withAppBuildGradle(next, (mod) => {
|
|
66
|
+
if (mod.modResults.language !== 'groovy') {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Forge config plugin: the app build.gradle is not Groovy; apply the Kotlin flag manually',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
mod.modResults.contents = addKotlinMetadataFlag(mod.modResults.contents);
|
|
72
|
+
return mod;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = withForge;
|
|
80
|
+
module.exports.default = withForge;
|
|
81
|
+
// Re-exported so host projects and tests can reuse the raw mutations.
|
|
82
|
+
module.exports.mutations = { addForgeMavenRepository, addKotlinMetadataFlag };
|