@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,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: Apple video option builder and ordered override transport JSON writer.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
public struct VideoOptions: Equatable {
|
|
18
|
+
public enum JoinTimeCalculationMode: String {
|
|
19
|
+
case `default` = "DEFAULT"
|
|
20
|
+
case legacy = "LEGACY"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public var metadata: [String: String]
|
|
24
|
+
public var joinTimeCalculationMode: JoinTimeCalculationMode?
|
|
25
|
+
public var adJoinFromAdBreakStart: Bool?
|
|
26
|
+
public var adConnectedRequiresAdJoin: Bool?
|
|
27
|
+
public var contentIsLiveNoSeek: Bool?
|
|
28
|
+
public var ignorePauseSmallEvents: Bool?
|
|
29
|
+
public var forceInit: Bool?
|
|
30
|
+
public var waitForMetadata: Bool?
|
|
31
|
+
public var waitForMetadataTimeout: Int64?
|
|
32
|
+
public var pendingMetadata: [String]?
|
|
33
|
+
public var overridesJson: String
|
|
34
|
+
|
|
35
|
+
public var hasContentDurationOverride: Bool {
|
|
36
|
+
metadata["mediaDuration"] != nil
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public init(
|
|
40
|
+
metadata: [String: String] = [:],
|
|
41
|
+
joinTimeCalculationMode: JoinTimeCalculationMode? = nil,
|
|
42
|
+
adJoinFromAdBreakStart: Bool? = nil,
|
|
43
|
+
adConnectedRequiresAdJoin: Bool? = nil,
|
|
44
|
+
contentIsLiveNoSeek: Bool? = nil,
|
|
45
|
+
ignorePauseSmallEvents: Bool? = nil,
|
|
46
|
+
forceInit: Bool? = nil,
|
|
47
|
+
waitForMetadata: Bool? = nil,
|
|
48
|
+
waitForMetadataTimeout: Int64? = nil,
|
|
49
|
+
pendingMetadata: [String]? = nil,
|
|
50
|
+
overridesJson: String? = nil
|
|
51
|
+
) {
|
|
52
|
+
// Mirror the Android VideoOptions(metadata) constructor: local option keys
|
|
53
|
+
// are extracted into typed controls before normalization filters them out
|
|
54
|
+
// of the wire metadata.
|
|
55
|
+
self.metadata = Self.normalize(metadata)
|
|
56
|
+
self.joinTimeCalculationMode = joinTimeCalculationMode
|
|
57
|
+
?? metadata["joinTimeCalculationMode"].flatMap { JoinTimeCalculationMode(rawValue: $0.uppercased()) }
|
|
58
|
+
self.adJoinFromAdBreakStart = adJoinFromAdBreakStart ?? Self.boolOption(metadata["adJoinFromAdBreakStart"])
|
|
59
|
+
self.adConnectedRequiresAdJoin = adConnectedRequiresAdJoin ?? Self.boolOption(metadata["adConnectedRequiresAdJoin"])
|
|
60
|
+
self.contentIsLiveNoSeek = contentIsLiveNoSeek ?? Self.boolOption(metadata["contentIsLiveNoSeek"])
|
|
61
|
+
self.ignorePauseSmallEvents = ignorePauseSmallEvents ?? Self.boolOption(metadata["ignorePauseSmallEvents"])
|
|
62
|
+
let resolvedForceInit = forceInit ?? Self.boolOption(metadata["forceInit"])
|
|
63
|
+
let resolvedWaitForMetadata = waitForMetadata ?? Self.boolOption(metadata["waitForMetadata"])
|
|
64
|
+
let resolvedWaitForMetadataTimeout = waitForMetadataTimeout ?? metadata["waitForMetadataTimeout"].flatMap(Int64.init)
|
|
65
|
+
let resolvedPendingMetadata = pendingMetadata ?? metadata["pendingMetadata"].map {
|
|
66
|
+
$0.split(separator: ",")
|
|
67
|
+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
68
|
+
.filter { !$0.isEmpty }
|
|
69
|
+
}
|
|
70
|
+
self.forceInit = resolvedForceInit
|
|
71
|
+
self.waitForMetadata = resolvedWaitForMetadata
|
|
72
|
+
self.waitForMetadataTimeout = resolvedWaitForMetadataTimeout
|
|
73
|
+
self.pendingMetadata = resolvedPendingMetadata
|
|
74
|
+
self.overridesJson = overridesJson ?? OverrideBundleTransport(metadata: self.metadata)
|
|
75
|
+
.withControl("forceInit", resolvedForceInit)
|
|
76
|
+
.withControl("waitForMetadata", resolvedWaitForMetadata)
|
|
77
|
+
.withControl("waitForMetadataTimeoutMs", resolvedWaitForMetadataTimeout)
|
|
78
|
+
.withControl("pendingMetadata", resolvedPendingMetadata)
|
|
79
|
+
.toJson()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private static func boolOption(_ value: String?) -> Bool? {
|
|
83
|
+
switch value?.lowercased() {
|
|
84
|
+
case "true": return true
|
|
85
|
+
case "false": return false
|
|
86
|
+
default: return nil
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Mirrors the Android `withAdapterWaitForMetadataDefault`: applies the
|
|
91
|
+
/// adapter-level `waitForMetadata` default without overriding an explicit
|
|
92
|
+
/// integrator choice. Returns `self` unchanged when `waitForMetadata` was
|
|
93
|
+
/// set explicitly (true or false) or when the transport JSON already
|
|
94
|
+
/// carries the control key.
|
|
95
|
+
func withAdapterWaitForMetadataDefault() -> VideoOptions {
|
|
96
|
+
if waitForMetadata != nil { return self }
|
|
97
|
+
guard let patchedJson = Self.patchControlWaitForMetadata(overridesJson) else { return self }
|
|
98
|
+
var patched = self
|
|
99
|
+
patched.waitForMetadata = true
|
|
100
|
+
patched.overridesJson = patchedJson
|
|
101
|
+
return patched
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Adds `control.waitForMetadata = true` to an existing override transport
|
|
105
|
+
/// JSON document. Returns `nil` when the JSON already carries the control
|
|
106
|
+
/// key or cannot be parsed, so callers keep the original options.
|
|
107
|
+
private static func patchControlWaitForMetadata(_ json: String) -> String? {
|
|
108
|
+
guard let data = json.data(using: .utf8),
|
|
109
|
+
let root = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
|
|
110
|
+
return nil
|
|
111
|
+
}
|
|
112
|
+
let existingControl = root["control"] as? [String: Any]
|
|
113
|
+
if existingControl?["waitForMetadata"] != nil { return nil }
|
|
114
|
+
var control = existingControl ?? [:]
|
|
115
|
+
control["waitForMetadata"] = true
|
|
116
|
+
var patched = root
|
|
117
|
+
patched["control"] = control
|
|
118
|
+
guard JSONSerialization.isValidJSONObject(patched),
|
|
119
|
+
let patchedData = try? JSONSerialization.data(withJSONObject: patched, options: [.sortedKeys]),
|
|
120
|
+
let patchedJson = String(data: patchedData, encoding: .utf8),
|
|
121
|
+
!patchedJson.isEmpty else {
|
|
122
|
+
return nil
|
|
123
|
+
}
|
|
124
|
+
return patchedJson
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public static func builder() -> Builder {
|
|
128
|
+
Builder()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public final class Builder {
|
|
132
|
+
private var metadata: [String: String] = [:]
|
|
133
|
+
private var controls: [String: Any] = [:]
|
|
134
|
+
private var joinTimeCalculationMode: JoinTimeCalculationMode?
|
|
135
|
+
private var adJoinFromAdBreakStart: Bool?
|
|
136
|
+
private var adConnectedRequiresAdJoin: Bool?
|
|
137
|
+
private var contentIsLiveNoSeek: Bool?
|
|
138
|
+
private var ignorePauseSmallEvents: Bool?
|
|
139
|
+
private var forceInit: Bool?
|
|
140
|
+
private var waitForMetadata: Bool?
|
|
141
|
+
private var waitForMetadataTimeout: Int64?
|
|
142
|
+
private var pendingMetadata: [String]?
|
|
143
|
+
|
|
144
|
+
public init() {}
|
|
145
|
+
|
|
146
|
+
@discardableResult public func metadata(_ value: [String: String]?) -> Builder {
|
|
147
|
+
value.map(Self.normalize)?.forEach { metadata[$0.key] = $0.value }
|
|
148
|
+
return self
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@discardableResult public func contentTitle(_ value: String?) -> Builder { api("title", value) }
|
|
152
|
+
@discardableResult public func contentId(_ value: String?) -> Builder { api("contentId", value) }
|
|
153
|
+
@discardableResult public func contentResource(_ value: String?) -> Builder { api("resource", value) }
|
|
154
|
+
@discardableResult public func contentDuration(_ value: Double?) -> Builder { api("mediaDuration", value) }
|
|
155
|
+
@discardableResult public func contentType(_ value: String?) -> Builder { api("contentType", value) }
|
|
156
|
+
@discardableResult public func live(_ value: Bool?) -> Builder { api("live", value) }
|
|
157
|
+
@discardableResult public func contentBitrate(_ value: Int64?) -> Builder { api("bitrate", value) }
|
|
158
|
+
@discardableResult public func contentRendition(_ value: String?) -> Builder { api("rendition", value) }
|
|
159
|
+
@discardableResult public func contentCustomDimensions(_ value: [String: Any]?) -> Builder { api("dimensions", value) }
|
|
160
|
+
@discardableResult public func contentCustomDimension1(_ value: String?) -> Builder { api("param1", value) }
|
|
161
|
+
@discardableResult public func adTitle(_ value: String?) -> Builder { api("adTitle", value) }
|
|
162
|
+
@discardableResult public func adResource(_ value: String?) -> Builder { api("adResource", value) }
|
|
163
|
+
@discardableResult public func adCampaign(_ value: String?) -> Builder { api("adCampaign", value) }
|
|
164
|
+
@discardableResult public func adCreativeId(_ value: String?) -> Builder { api("adCreativeId", value) }
|
|
165
|
+
@discardableResult public func adProvider(_ value: String?) -> Builder { api("adProvider", value) }
|
|
166
|
+
@discardableResult public func transportFormat(_ value: String?) -> Builder { api("transportFormat", value) }
|
|
167
|
+
@discardableResult public func urlToParse(_ value: String?) -> Builder { api("parsedResource", value) }
|
|
168
|
+
|
|
169
|
+
@discardableResult public func joinTimeCalculationMode(_ mode: JoinTimeCalculationMode?) -> Builder {
|
|
170
|
+
joinTimeCalculationMode = mode
|
|
171
|
+
return self
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@discardableResult public func adJoinFromAdBreakStart(_ enabled: Bool?) -> Builder {
|
|
175
|
+
adJoinFromAdBreakStart = enabled
|
|
176
|
+
return self
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@discardableResult public func adConnectedRequiresAdJoin(_ enabled: Bool?) -> Builder {
|
|
180
|
+
adConnectedRequiresAdJoin = enabled
|
|
181
|
+
return self
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@discardableResult public func contentIsLiveNoSeek(_ enabled: Bool?) -> Builder {
|
|
185
|
+
contentIsLiveNoSeek = enabled
|
|
186
|
+
return self
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@discardableResult public func ignorePauseSmallEvents(_ enabled: Bool?) -> Builder {
|
|
190
|
+
ignorePauseSmallEvents = enabled
|
|
191
|
+
return self
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@discardableResult public func isForceInit(_ enabled: Bool?) -> Builder {
|
|
195
|
+
forceInit = enabled
|
|
196
|
+
controls["forceInit"] = enabled
|
|
197
|
+
return self
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@discardableResult public func waitForMetadata(_ enabled: Bool?) -> Builder {
|
|
201
|
+
waitForMetadata = enabled
|
|
202
|
+
controls["waitForMetadata"] = enabled
|
|
203
|
+
return self
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@discardableResult public func waitForMetadataTimeout(_ timeoutMs: Int64?) -> Builder {
|
|
207
|
+
waitForMetadataTimeout = timeoutMs
|
|
208
|
+
controls["waitForMetadataTimeoutMs"] = timeoutMs
|
|
209
|
+
return self
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@discardableResult public func pendingMetadata(_ keys: [String]?) -> Builder {
|
|
213
|
+
pendingMetadata = keys
|
|
214
|
+
controls["pendingMetadata"] = keys
|
|
215
|
+
return self
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public func build() -> VideoOptions {
|
|
219
|
+
var transport = OverrideBundleTransport(metadata: metadata)
|
|
220
|
+
controls.forEach { transport = transport.withControl($0.key, $0.value) }
|
|
221
|
+
return VideoOptions(
|
|
222
|
+
metadata: metadata,
|
|
223
|
+
joinTimeCalculationMode: joinTimeCalculationMode,
|
|
224
|
+
adJoinFromAdBreakStart: adJoinFromAdBreakStart,
|
|
225
|
+
adConnectedRequiresAdJoin: adConnectedRequiresAdJoin,
|
|
226
|
+
contentIsLiveNoSeek: contentIsLiveNoSeek,
|
|
227
|
+
ignorePauseSmallEvents: ignorePauseSmallEvents,
|
|
228
|
+
forceInit: forceInit,
|
|
229
|
+
waitForMetadata: waitForMetadata,
|
|
230
|
+
waitForMetadataTimeout: waitForMetadataTimeout,
|
|
231
|
+
pendingMetadata: pendingMetadata,
|
|
232
|
+
overridesJson: transport.toJson()
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@discardableResult private func api(_ key: String, _ value: Any?) -> Builder {
|
|
237
|
+
guard let value, !Self.nonGoKeys.contains(key) else { return self }
|
|
238
|
+
metadata[key] = Self.encode(value)
|
|
239
|
+
return self
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private static func normalize(_ value: [String: String]) -> [String: String] {
|
|
243
|
+
VideoOptions.normalize(value)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private static func encode(_ value: Any) -> String {
|
|
247
|
+
if let value = value as? String { return value }
|
|
248
|
+
if let value = value as? Bool { return String(value) }
|
|
249
|
+
if let value = value as? NSNumber { return value.stringValue }
|
|
250
|
+
if JSONSerialization.isValidJSONObject(value),
|
|
251
|
+
let data = try? JSONSerialization.data(withJSONObject: value),
|
|
252
|
+
let json = String(data: data, encoding: .utf8) {
|
|
253
|
+
return json
|
|
254
|
+
}
|
|
255
|
+
return String(describing: value)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private static let nonGoKeys: Set<String> = [
|
|
259
|
+
"sendTotalBytes",
|
|
260
|
+
"pluginLogs",
|
|
261
|
+
"triggeredEvents",
|
|
262
|
+
"experiments",
|
|
263
|
+
"preloadDuration",
|
|
264
|
+
"profile",
|
|
265
|
+
"bucket",
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private static func normalize(_ value: [String: String]) -> [String: String] {
|
|
270
|
+
var output: [String: String] = [:]
|
|
271
|
+
for (key, value) in value {
|
|
272
|
+
guard let normalized = normalizedKey(key) else { continue }
|
|
273
|
+
output[normalized] = normalizedTimeValue(key: key, value: value)
|
|
274
|
+
}
|
|
275
|
+
return output
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// Mirrors the Android `normalizedApiKey` rules (VideoOptions.kt): local-only
|
|
279
|
+
/// and internal adapter keys are filtered, legacy snake_case and dotted aliases
|
|
280
|
+
/// map to camelCase wire keys, custom-dimension prefixes map to param/extraparam
|
|
281
|
+
/// keys, and unknown keys containing '_' or '.' never leak onto the wire.
|
|
282
|
+
private static func normalizedKey(_ rawKey: String) -> String? {
|
|
283
|
+
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
284
|
+
if localOptionKeys.contains(key) || internalOptionKeys.contains(key) || nonGoApiKeys.contains(key) {
|
|
285
|
+
return nil
|
|
286
|
+
}
|
|
287
|
+
switch key {
|
|
288
|
+
case "player_name": return "player"
|
|
289
|
+
case "player_version": return "playerVersion"
|
|
290
|
+
case "resource_url", "content.resource", "contentResource": return "resource"
|
|
291
|
+
case "content_type", "content.type", "contentType": return "contentType"
|
|
292
|
+
case "playhead_ms": return "playhead"
|
|
293
|
+
case "duration_ms": return "mediaDuration"
|
|
294
|
+
case "bitrate_kbps": return "bitrate"
|
|
295
|
+
case "dropped_frames": return "droppedFrames"
|
|
296
|
+
case "error_code": return "errorCode"
|
|
297
|
+
case "error_message": return "errorMsg"
|
|
298
|
+
case "creative_id": return "adCreativeId"
|
|
299
|
+
case "ad_duration_ms": return "adDuration"
|
|
300
|
+
case "ad_playhead_ms": return "adPlayhead"
|
|
301
|
+
case "insertion_type": return "adInsertionType"
|
|
302
|
+
case "content.id", "content_id": return "contentId"
|
|
303
|
+
case "content.title", "contentTitle": return "title"
|
|
304
|
+
case "content.isLive", "contentIsLive": return "live"
|
|
305
|
+
case "content.duration", "contentDuration": return "mediaDuration"
|
|
306
|
+
case "content.bitrate", "contentBitrate": return "bitrate"
|
|
307
|
+
case "content.rendition", "contentRendition": return "rendition"
|
|
308
|
+
case "content.language", "contentLanguage": return "contentLanguage"
|
|
309
|
+
case "content.metadata", "contentMetadata": return "properties"
|
|
310
|
+
case "content.metrics", "contentMetrics": return "metrics"
|
|
311
|
+
case "ad.resource", "adResource": return "adResource"
|
|
312
|
+
case "ad.title", "adTitle": return "adTitle"
|
|
313
|
+
case "ad.creativeId", "adCreativeId": return "adCreativeId"
|
|
314
|
+
case "ad.adapterVersion", "adAdapterVersion": return "adAdapterVersion"
|
|
315
|
+
case "ad.provider", "adProvider": return "adProvider"
|
|
316
|
+
case "ad.metadata", "adMetadata": return "adProperties"
|
|
317
|
+
default:
|
|
318
|
+
if key.hasPrefix("content.customDimension.") {
|
|
319
|
+
return Int(key.dropFirst("content.customDimension.".count)).map { "param\($0)" }
|
|
320
|
+
}
|
|
321
|
+
if key.hasPrefix("ad.customDimension.") {
|
|
322
|
+
return Int(key.dropFirst("ad.customDimension.".count)).map { "extraparam\($0)" }
|
|
323
|
+
}
|
|
324
|
+
return key.contains("_") || key.contains(".") ? nil : key
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/// Option keys consumed by the binding itself (matching Android's
|
|
329
|
+
/// `localOptionKeys`); they configure behavior and never reach the wire.
|
|
330
|
+
private static let localOptionKeys: Set<String> = [
|
|
331
|
+
"joinTimeCalculationMode",
|
|
332
|
+
"adJoinFromAdBreakStart",
|
|
333
|
+
"adConnectedRequiresAdJoin",
|
|
334
|
+
"contentIsLiveNoSeek",
|
|
335
|
+
"ignorePauseSmallEvents",
|
|
336
|
+
"waitForMetadata",
|
|
337
|
+
"waitForMetadataTimeout",
|
|
338
|
+
"adPostRollTimeout",
|
|
339
|
+
"delegateToAdAdapter",
|
|
340
|
+
"autoRestartOnMaxTimeExpiration",
|
|
341
|
+
"parseManifest",
|
|
342
|
+
"parseCdnNode",
|
|
343
|
+
"parseCdnNodeDisableFallback",
|
|
344
|
+
"parseCdnNodeAllowedGetMethodOnRetry",
|
|
345
|
+
"parseManifestAuth",
|
|
346
|
+
"parseCdnNodeList",
|
|
347
|
+
"parseCdnNameHeaderList",
|
|
348
|
+
"parseCdnNodeHeaderList",
|
|
349
|
+
"parseCdnNodeRequestDelayed",
|
|
350
|
+
"errorsToIgnore",
|
|
351
|
+
"legacyErrorNames",
|
|
352
|
+
"fatalErrors",
|
|
353
|
+
"nonFatalErrors",
|
|
354
|
+
"forceInit",
|
|
355
|
+
"pendingMetadata",
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
/// Adapter-internal descriptor keys (matching Android's `internalOptionKeys`);
|
|
359
|
+
/// they describe the integration, not the content, and never reach the wire.
|
|
360
|
+
private static let internalOptionKeys: Set<String> = [
|
|
361
|
+
"adapter_id",
|
|
362
|
+
"adapterId",
|
|
363
|
+
"video_adapter_id",
|
|
364
|
+
"videoAdapterId",
|
|
365
|
+
"ad_adapter_id",
|
|
366
|
+
"adAdapterId",
|
|
367
|
+
"player_family",
|
|
368
|
+
"playerFamily",
|
|
369
|
+
"category",
|
|
370
|
+
"version",
|
|
371
|
+
"adapterVersion",
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
/// Non-Go API keys dropped on every platform (matching Android's `nonGoApiKeys`).
|
|
375
|
+
private static let nonGoApiKeys: Set<String> = [
|
|
376
|
+
"profile",
|
|
377
|
+
"bucket",
|
|
378
|
+
"experiments",
|
|
379
|
+
"preloadDuration",
|
|
380
|
+
"triggeredEvents",
|
|
381
|
+
"sendTotalBytes",
|
|
382
|
+
"pluginLogs",
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
private static func normalizedTimeValue(key: String, value: String) -> String {
|
|
386
|
+
switch key {
|
|
387
|
+
case "playhead_ms", "duration_ms", "ad_duration_ms", "ad_playhead_ms":
|
|
388
|
+
guard let millis = Double(value) else { return value }
|
|
389
|
+
return String(millis / 1000.0)
|
|
390
|
+
default:
|
|
391
|
+
return value
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
struct OverrideBundleTransport: Equatable {
|
|
397
|
+
private var scalars: [String: String]
|
|
398
|
+
private var controls: [String: Any]
|
|
399
|
+
|
|
400
|
+
init(metadata: [String: String]) {
|
|
401
|
+
self.scalars = metadata
|
|
402
|
+
self.controls = [:]
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func withControl(_ key: String, _ value: Any?) -> OverrideBundleTransport {
|
|
406
|
+
guard let value else { return self }
|
|
407
|
+
var copy = self
|
|
408
|
+
copy.controls[key] = value
|
|
409
|
+
return copy
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
func toJson() -> String {
|
|
413
|
+
var object: [String: Any] = [:]
|
|
414
|
+
if !scalars.isEmpty {
|
|
415
|
+
object["scalars"] = scalars
|
|
416
|
+
}
|
|
417
|
+
if !controls.isEmpty {
|
|
418
|
+
object["control"] = controls
|
|
419
|
+
}
|
|
420
|
+
guard JSONSerialization.isValidJSONObject(object),
|
|
421
|
+
let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]),
|
|
422
|
+
let json = String(data: data, encoding: .utf8) else {
|
|
423
|
+
return "{}"
|
|
424
|
+
}
|
|
425
|
+
return json
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
static func == (lhs: OverrideBundleTransport, rhs: OverrideBundleTransport) -> Bool {
|
|
429
|
+
lhs.toJson() == rhs.toJson()
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
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: Swift network capture installer and payload adapter for Apple URLSession swizzling.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
#if canImport(NPAWForgeCore)
|
|
17
|
+
import NPAWForgeCore
|
|
18
|
+
#endif
|
|
19
|
+
#if canImport(NPAWForgeNetworkCaptureObjC)
|
|
20
|
+
import NPAWForgeNetworkCaptureObjC
|
|
21
|
+
#endif
|
|
22
|
+
|
|
23
|
+
public struct NetworkCaptureConfiguration: Equatable {
|
|
24
|
+
public var enabled: Bool
|
|
25
|
+
public var privacy: NetworkPrivacyConfiguration
|
|
26
|
+
public var blockedHosts: Set<String>
|
|
27
|
+
|
|
28
|
+
public init(
|
|
29
|
+
enabled: Bool = false,
|
|
30
|
+
privacy: NetworkPrivacyConfiguration = NetworkPrivacyConfiguration(),
|
|
31
|
+
blockedHosts: Set<String> = ["lma.npaw.com"]
|
|
32
|
+
) {
|
|
33
|
+
self.enabled = enabled
|
|
34
|
+
self.privacy = privacy
|
|
35
|
+
self.blockedHosts = blockedHosts.map { $0.lowercased() }.asSet
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public enum ForgeNetworkCapture {
|
|
40
|
+
private static let lock = NSLock()
|
|
41
|
+
private static var activeForge: NPAWForge?
|
|
42
|
+
private static var activeConfiguration = NetworkCaptureConfiguration()
|
|
43
|
+
|
|
44
|
+
@discardableResult
|
|
45
|
+
public static func install(
|
|
46
|
+
forge: NPAWForge,
|
|
47
|
+
configuration: NetworkCaptureConfiguration = NetworkCaptureConfiguration(enabled: true)
|
|
48
|
+
) -> ForgeOutcome {
|
|
49
|
+
lock.lock()
|
|
50
|
+
activeForge = forge
|
|
51
|
+
activeConfiguration = configuration
|
|
52
|
+
lock.unlock()
|
|
53
|
+
guard configuration.enabled else { return .ignoredInvalidState }
|
|
54
|
+
let installed = NPAWForgeNetworkSwizzler.install(
|
|
55
|
+
callback: { payload in
|
|
56
|
+
handle(payload: payload)
|
|
57
|
+
},
|
|
58
|
+
blockedHosts: configuration.blockedHosts
|
|
59
|
+
)
|
|
60
|
+
return installed ? .accepted : .failedWithDiagnostic("network_swizzler_install_failed")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Stops automatic capture and clears the active forge/configuration so
|
|
64
|
+
/// swizzled traffic and manual tracking no-op until the next install. The
|
|
65
|
+
/// underlying method swizzles stay installed for the process lifetime;
|
|
66
|
+
/// `install(forge:configuration:)` resumes capture by re-arming the callback.
|
|
67
|
+
@discardableResult
|
|
68
|
+
public static func uninstall() -> ForgeOutcome {
|
|
69
|
+
lock.lock()
|
|
70
|
+
activeForge = nil
|
|
71
|
+
activeConfiguration = NetworkCaptureConfiguration()
|
|
72
|
+
lock.unlock()
|
|
73
|
+
NPAWForgeNetworkSwizzler.uninstall()
|
|
74
|
+
return .accepted
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public static func track(_ record: NetworkRequestRecord) -> ForgeOutcome {
|
|
78
|
+
lock.lock()
|
|
79
|
+
let forge = activeForge
|
|
80
|
+
let configuration = activeConfiguration
|
|
81
|
+
lock.unlock()
|
|
82
|
+
guard configuration.enabled else { return .ignoredInvalidState }
|
|
83
|
+
return forge?.trackNetworkRequest(record.sanitized(configuration: configuration.privacy)) ?? .ignoredInvalidState
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static func handle(payload: [String: String]) {
|
|
87
|
+
lock.lock()
|
|
88
|
+
let forge = activeForge
|
|
89
|
+
let configuration = activeConfiguration
|
|
90
|
+
lock.unlock()
|
|
91
|
+
guard configuration.enabled, let forge else { return }
|
|
92
|
+
var record = NetworkRequestRecord(
|
|
93
|
+
targetUrl: payload["targetUrl"] ?? "unknown",
|
|
94
|
+
method: payload["method"] ?? "GET",
|
|
95
|
+
responseStatusCode: Int(payload["responseStatusCode"] ?? "0") ?? 0,
|
|
96
|
+
durationMs: Int64(payload["durationMs"] ?? "0") ?? 0,
|
|
97
|
+
outcome: NetworkRequestOutcome(rawValue: payload["outcome"] ?? "unknown") ?? .unknown,
|
|
98
|
+
eventId: payload["eventId"] ?? UUID().uuidString,
|
|
99
|
+
source: NetworkRequestSource(rawValue: payload["source"] ?? "automatic_urlsession") ?? .automaticURLSession,
|
|
100
|
+
timestampMs: Int64(payload["timestampMs"] ?? "0") ?? 0
|
|
101
|
+
)
|
|
102
|
+
record.errorMessage = payload["errorMessage"]
|
|
103
|
+
record.requestBytes = payload["requestBytes"].flatMap(Int64.init)
|
|
104
|
+
record.responseBytes = payload["responseBytes"].flatMap(Int64.init)
|
|
105
|
+
record.responseHeaders = payload
|
|
106
|
+
.filter { $0.key.hasPrefix("responseHeader.") }
|
|
107
|
+
.reduce(into: [String: String]()) { output, element in
|
|
108
|
+
output[String(element.key.dropFirst("responseHeader.".count))] = element.value
|
|
109
|
+
}
|
|
110
|
+
_ = forge.trackNetworkRequest(record.sanitized(configuration: configuration.privacy))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private extension Array where Element == String {
|
|
115
|
+
var asSet: Set<String> { Set(self) }
|
|
116
|
+
}
|