@npaw/forge-react-native-video 9.7.3 → 10.7.4
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/NPAWForgeReactNativeVideo.podspec +54 -10
- package/README.md +24 -12
- package/android/build.gradle +62 -3
- package/android/src/main/java/com/npaw/forge/reactnative/video/RNVideoHeaderToken.java +50 -0
- package/android/src/rnv7/java/com/npaw/forge/reactnative/video/ForgeRNVideoPlugin.java +65 -0
- package/android/src/rnv7/java/com/npaw/forge/reactnative/video/NPAWForgeVideoModule.java +388 -0
- package/android/src/{main → rnv7}/java/com/npaw/forge/reactnative/video/NPAWForgeVideoPackage.java +2 -2
- package/android/src/test/java/com/npaw/forge/reactnative/video/RNVideoHeaderTokenTest.java +68 -0
- package/generations.json +31 -0
- package/ios/rnv7/ForgeRNVideoPlugin.swift +64 -0
- package/ios/rnv7/ForgeVideoModuleImpl.swift +474 -0
- package/package.json +6 -4
- package/android/src/main/java/com/npaw/forge/reactnative/video/NPAWForgeVideoModule.java +0 -341
- package/ios/ForgeRNVPlayerPlugin.swift +0 -122
- package/ios/ForgeVideoModuleImpl.swift +0 -363
|
@@ -1,363 +0,0 @@
|
|
|
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 attach coordinator + module implementation for react-native-video on iOS. Mirrors the Android RNVideoAttachCoordinator semantics: auto single-player policy, X-NPAW-Forge token matching via asset identity, source-URI fallback, per-binding stopOnPlayerRemoved, double-create guard.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import AVFoundation
|
|
16
|
-
import Foundation
|
|
17
|
-
#if canImport(NPAWForgeCore)
|
|
18
|
-
import NPAWForgeCore
|
|
19
|
-
import NPAWForgeAVPlayerAdapter
|
|
20
|
-
#elseif canImport(NPAWForgeApple)
|
|
21
|
-
import NPAWForgeApple
|
|
22
|
-
#endif
|
|
23
|
-
#if canImport(NPAWForgeReactNative)
|
|
24
|
-
import NPAWForgeReactNative
|
|
25
|
-
#endif
|
|
26
|
-
|
|
27
|
-
public typealias ForgeVideoResolveBlock = (Any?) -> Void
|
|
28
|
-
public typealias ForgeVideoRejectBlock = (String, String) -> Void
|
|
29
|
-
|
|
30
|
-
@objc(ForgeVideoModuleImpl)
|
|
31
|
-
public final class ForgeVideoModuleImpl: NSObject {
|
|
32
|
-
|
|
33
|
-
@objc public static let shared = ForgeVideoModuleImpl()
|
|
34
|
-
static let tokenHeader = "x-npaw-forge"
|
|
35
|
-
|
|
36
|
-
private struct AttachRequest {
|
|
37
|
-
let instanceId: String
|
|
38
|
-
let identifier: String
|
|
39
|
-
let policy: String
|
|
40
|
-
let headerToken: String?
|
|
41
|
-
let sourceUri: String?
|
|
42
|
-
let stopOnPlayerRemoved: Bool
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private struct Binding {
|
|
46
|
-
let request: AttachRequest
|
|
47
|
-
let playerId: String
|
|
48
|
-
let playerIdentity: ObjectIdentifier
|
|
49
|
-
let adapter: AVPlayerAdapter
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private let lock = NSLock()
|
|
53
|
-
private var livePlayers: [(id: String, player: AVPlayer)] = []
|
|
54
|
-
private var seenPlayerIdentities = Set<ObjectIdentifier>()
|
|
55
|
-
private var assetFacts: [ObjectIdentifier: (token: String?, uri: String?)] = [:]
|
|
56
|
-
private var pending: [AttachRequest] = []
|
|
57
|
-
private var bindings: [String: Binding] = [:] // key: instanceId + " " + identifier
|
|
58
|
-
|
|
59
|
-
private let resolveForge: (String) -> NPAWForge?
|
|
60
|
-
|
|
61
|
-
private override init() {
|
|
62
|
-
self.resolveForge = { ForgeModuleImpl.shared.resolveInstance($0) }
|
|
63
|
-
super.init()
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// MARK: - Spec surface
|
|
67
|
-
|
|
68
|
-
@objc public func attach(
|
|
69
|
-
_ instanceId: String,
|
|
70
|
-
identifier: String,
|
|
71
|
-
options: [String: Any]?,
|
|
72
|
-
resolve: @escaping ForgeVideoResolveBlock,
|
|
73
|
-
reject: @escaping ForgeVideoRejectBlock
|
|
74
|
-
) {
|
|
75
|
-
ForgeRNVPlayerPlugin.registerOnce()
|
|
76
|
-
|
|
77
|
-
guard let forge = resolveForge(instanceId) else {
|
|
78
|
-
reject("E_FORGE_UNKNOWN_INSTANCE", "No NPAWForge instance '\(instanceId)'")
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
ensureVideo(forge, identifier: identifier)
|
|
82
|
-
|
|
83
|
-
let headerToken = nonEmpty(options?["headerToken"] as? String)
|
|
84
|
-
let sourceUri = nonEmpty(options?["sourceUri"] as? String)
|
|
85
|
-
var policy = nonEmpty(options?["policy"] as? String)
|
|
86
|
-
if policy == nil {
|
|
87
|
-
policy = (headerToken != nil || sourceUri != nil) ? "match" : "auto"
|
|
88
|
-
}
|
|
89
|
-
let request = AttachRequest(
|
|
90
|
-
instanceId: instanceId,
|
|
91
|
-
identifier: identifier,
|
|
92
|
-
policy: policy ?? "auto",
|
|
93
|
-
headerToken: headerToken,
|
|
94
|
-
sourceUri: sourceUri,
|
|
95
|
-
stopOnPlayerRemoved: options?["stopOnPlayerRemoved"] as? Bool ?? true
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
lock.lock()
|
|
99
|
-
let key = bindingKey(instanceId, identifier)
|
|
100
|
-
if bindings[key] != nil {
|
|
101
|
-
lock.unlock()
|
|
102
|
-
resolve(attachResult(code: "ignored_duplicate", accepted: false, attachment: "bound"))
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
pending.removeAll { $0.instanceId == instanceId && $0.identifier == identifier }
|
|
106
|
-
|
|
107
|
-
if request.policy == "auto" {
|
|
108
|
-
let unbound = unboundPlayersLocked()
|
|
109
|
-
if unbound.count > 1 {
|
|
110
|
-
lock.unlock()
|
|
111
|
-
resolve(
|
|
112
|
-
attachResult(
|
|
113
|
-
code: "unsupported_capability",
|
|
114
|
-
accepted: false,
|
|
115
|
-
attachment: "pending",
|
|
116
|
-
reason: "ambiguous_auto_attach"
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
if let candidate = unbound.first {
|
|
122
|
-
bindLocked(request, playerId: candidate.id, player: candidate.player)
|
|
123
|
-
lock.unlock()
|
|
124
|
-
resolve(attachResult(code: "accepted", accepted: true, attachment: "bound"))
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
} else if let candidate = findMatchLocked(request) {
|
|
128
|
-
bindLocked(request, playerId: candidate.id, player: candidate.player)
|
|
129
|
-
lock.unlock()
|
|
130
|
-
resolve(attachResult(code: "accepted", accepted: true, attachment: "bound"))
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
pending.append(request)
|
|
135
|
-
lock.unlock()
|
|
136
|
-
scheduleReevaluations()
|
|
137
|
-
resolve(attachResult(code: "accepted", accepted: true, attachment: "pending"))
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
@objc public func detach(
|
|
141
|
-
_ instanceId: String,
|
|
142
|
-
identifier: String,
|
|
143
|
-
resolve: @escaping ForgeVideoResolveBlock,
|
|
144
|
-
reject: @escaping ForgeVideoRejectBlock
|
|
145
|
-
) {
|
|
146
|
-
lock.lock()
|
|
147
|
-
pending.removeAll { $0.instanceId == instanceId && $0.identifier == identifier }
|
|
148
|
-
let removed = bindings.removeValue(forKey: bindingKey(instanceId, identifier))
|
|
149
|
-
lock.unlock()
|
|
150
|
-
|
|
151
|
-
guard let removed else {
|
|
152
|
-
resolve(attachResult(code: "ignored_invalid_state", accepted: false, attachment: "pending"))
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
unbindOnMain(removed, fireStop: true)
|
|
156
|
-
resolve(attachResult(code: "accepted", accepted: true, attachment: "pending"))
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
@objc public func invalidate() {
|
|
160
|
-
lock.lock()
|
|
161
|
-
livePlayers.removeAll()
|
|
162
|
-
seenPlayerIdentities.removeAll()
|
|
163
|
-
assetFacts.removeAll()
|
|
164
|
-
pending.removeAll()
|
|
165
|
-
let current = Array(bindings.values)
|
|
166
|
-
bindings.removeAll()
|
|
167
|
-
lock.unlock()
|
|
168
|
-
|
|
169
|
-
for binding in current {
|
|
170
|
-
unbindOnMain(binding, fireStop: false)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// MARK: - Plugin callbacks
|
|
175
|
-
|
|
176
|
-
func playerCreated(id: String, player: AVPlayer) {
|
|
177
|
-
let identity = ObjectIdentifier(player)
|
|
178
|
-
lock.lock()
|
|
179
|
-
if seenPlayerIdentities.contains(identity) {
|
|
180
|
-
lock.unlock()
|
|
181
|
-
NSLog("[NPAWForgeRNV] duplicate onInstanceCreated suppressed id=%@", id)
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
seenPlayerIdentities.insert(identity)
|
|
185
|
-
livePlayers.append((id: id, player: player))
|
|
186
|
-
lock.unlock()
|
|
187
|
-
|
|
188
|
-
reevaluate()
|
|
189
|
-
scheduleReevaluations()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
func playerRemoved(id: String, player: AVPlayer) {
|
|
193
|
-
let identity = ObjectIdentifier(player)
|
|
194
|
-
var affected: [Binding] = []
|
|
195
|
-
|
|
196
|
-
lock.lock()
|
|
197
|
-
livePlayers.removeAll { $0.id == id && ObjectIdentifier($0.player) == identity }
|
|
198
|
-
seenPlayerIdentities.remove(identity)
|
|
199
|
-
for (key, binding) in bindings where binding.playerId == id {
|
|
200
|
-
bindings.removeValue(forKey: key)
|
|
201
|
-
affected.append(binding)
|
|
202
|
-
}
|
|
203
|
-
lock.unlock()
|
|
204
|
-
|
|
205
|
-
for binding in affected {
|
|
206
|
-
unbindOnMain(binding, fireStop: binding.request.stopOnPlayerRemoved)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
func sourceSeen(asset: AVAsset, token: String?, uri: String?) {
|
|
211
|
-
lock.lock()
|
|
212
|
-
assetFacts[ObjectIdentifier(asset)] = (token: token, uri: uri)
|
|
213
|
-
lock.unlock()
|
|
214
|
-
reevaluate()
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// MARK: - Coordinator internals
|
|
218
|
-
|
|
219
|
-
private func bindingKey(_ instanceId: String, _ identifier: String) -> String {
|
|
220
|
-
"\(instanceId) \(identifier)"
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private func ensureVideo(_ forge: NPAWForge, identifier: String) {
|
|
224
|
-
if forge.getVideoAdapter(identifier: identifier) == nil {
|
|
225
|
-
_ = forge.videoBuilder().setIdentifier(identifier).build()
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private func unboundPlayersLocked() -> [(id: String, player: AVPlayer)] {
|
|
230
|
-
let boundIds = Set(bindings.values.map { $0.playerId })
|
|
231
|
-
return livePlayers.filter { !boundIds.contains($0.id) }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
private func factsFor(_ player: AVPlayer) -> (token: String?, uri: String?) {
|
|
235
|
-
if let asset = player.currentItem?.asset {
|
|
236
|
-
if let stored = assetFacts[ObjectIdentifier(asset)] {
|
|
237
|
-
return stored
|
|
238
|
-
}
|
|
239
|
-
if let urlAsset = asset as? AVURLAsset {
|
|
240
|
-
return (token: nil, uri: urlAsset.url.absoluteString)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return (token: nil, uri: nil)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
private func findMatchLocked(_ request: AttachRequest) -> (id: String, player: AVPlayer)? {
|
|
247
|
-
let candidates = unboundPlayersLocked()
|
|
248
|
-
if let token = request.headerToken {
|
|
249
|
-
for candidate in candidates where factsFor(candidate.player).token == token {
|
|
250
|
-
return candidate
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
if let uri = request.sourceUri {
|
|
254
|
-
for candidate in candidates where factsFor(candidate.player).uri == uri {
|
|
255
|
-
return candidate
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return nil
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private func bindLocked(_ request: AttachRequest, playerId: String, player: AVPlayer) {
|
|
262
|
-
let adapter = AVPlayerAdapter(player: player)
|
|
263
|
-
bindings[bindingKey(request.instanceId, request.identifier)] = Binding(
|
|
264
|
-
request: request,
|
|
265
|
-
playerId: playerId,
|
|
266
|
-
playerIdentity: ObjectIdentifier(player),
|
|
267
|
-
adapter: adapter
|
|
268
|
-
)
|
|
269
|
-
let instanceId = request.instanceId
|
|
270
|
-
let identifier = request.identifier
|
|
271
|
-
DispatchQueue.main.async { [weak self] in
|
|
272
|
-
guard let self, let forge = self.resolveForge(instanceId) else { return }
|
|
273
|
-
self.ensureVideo(forge, identifier: identifier)
|
|
274
|
-
guard let video = forge.getVideoAdapter(identifier: identifier) else { return }
|
|
275
|
-
NSLog("[NPAWForgeRNV] binding AVPlayerAdapter playerId=%@ identifier=%@", playerId, identifier)
|
|
276
|
-
_ = video.setPlayerAdapter(adapter)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
private func reevaluate() {
|
|
281
|
-
lock.lock()
|
|
282
|
-
var resolved: [AttachRequest] = []
|
|
283
|
-
for request in pending {
|
|
284
|
-
if request.policy == "auto" {
|
|
285
|
-
let unbound = unboundPlayersLocked()
|
|
286
|
-
if unbound.count == 1, let candidate = unbound.first {
|
|
287
|
-
bindLocked(request, playerId: candidate.id, player: candidate.player)
|
|
288
|
-
resolved.append(request)
|
|
289
|
-
}
|
|
290
|
-
} else if let candidate = findMatchLocked(request) {
|
|
291
|
-
bindLocked(request, playerId: candidate.id, player: candidate.player)
|
|
292
|
-
resolved.append(request)
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
pending.removeAll { request in
|
|
296
|
-
resolved.contains {
|
|
297
|
-
$0.instanceId == request.instanceId && $0.identifier == request.identifier
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
lock.unlock()
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/// currentItem/asset facts can settle after creation; retry matching a few
|
|
304
|
-
/// times so 'match' attaches converge without a dedicated KVO web.
|
|
305
|
-
private func scheduleReevaluations() {
|
|
306
|
-
for delay in [0.5, 1.5, 3.0] {
|
|
307
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
308
|
-
self?.reevaluate()
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/// Tears the binding down: detach the player adapter, then fire the
|
|
314
|
-
/// terminal stop.
|
|
315
|
-
///
|
|
316
|
-
/// The detach comes first deliberately. By the time this block runs,
|
|
317
|
-
/// react-native-video has already released its player - `playerRemoved`
|
|
318
|
-
/// arrives on the main thread and this hop defers a full runloop turn - so
|
|
319
|
-
/// the adapter is observing a player that is being deallocated. Detaching
|
|
320
|
-
/// first means the view is closed to it before the stop makes the view
|
|
321
|
-
/// terminal, and nothing the dying player reports afterwards can be
|
|
322
|
-
/// mistaken for a replay.
|
|
323
|
-
///
|
|
324
|
-
/// The stop still carries the final position: it never read the player
|
|
325
|
-
/// (`fireStop` builds its metadata from params), and the kernel backfills
|
|
326
|
-
/// `playhead` from the last observed position when the firing params carry
|
|
327
|
-
/// none - `backfill_stop_playhead`, added for
|
|
328
|
-
/// `apple/2026-07-13_ios-stop-playhead-zero-instead-of-final-position`.
|
|
329
|
-
private func unbindOnMain(_ binding: Binding, fireStop: Bool) {
|
|
330
|
-
let instanceId = binding.request.instanceId
|
|
331
|
-
let identifier = binding.request.identifier
|
|
332
|
-
DispatchQueue.main.async { [weak self] in
|
|
333
|
-
guard let self, let forge = self.resolveForge(instanceId) else { return }
|
|
334
|
-
guard let video = forge.getVideoAdapter(identifier: identifier) else { return }
|
|
335
|
-
_ = video.setPlayerAdapter(nil)
|
|
336
|
-
if fireStop {
|
|
337
|
-
_ = video.fireStop()
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
private func nonEmpty(_ value: String?) -> String? {
|
|
343
|
-
guard let value, !value.isEmpty else { return nil }
|
|
344
|
-
return value
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private func attachResult(
|
|
348
|
-
code: String,
|
|
349
|
-
accepted: Bool,
|
|
350
|
-
attachment: String,
|
|
351
|
-
reason: String? = nil
|
|
352
|
-
) -> [String: Any] {
|
|
353
|
-
var result: [String: Any] = [
|
|
354
|
-
"code": code,
|
|
355
|
-
"isAccepted": accepted,
|
|
356
|
-
"attachment": attachment,
|
|
357
|
-
]
|
|
358
|
-
if let reason {
|
|
359
|
-
result["reason"] = reason
|
|
360
|
-
}
|
|
361
|
-
return result
|
|
362
|
-
}
|
|
363
|
-
}
|