@npaw/forge-react-native-video 9.7.4 → 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/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/{rnv6 → rnv7}/java/com/npaw/forge/reactnative/video/NPAWForgeVideoPackage.java +2 -2
- package/ios/rnv7/ForgeRNVideoPlugin.swift +64 -0
- package/ios/rnv7/ForgeVideoModuleImpl.swift +474 -0
- package/package.json +3 -2
- package/android/src/rnv6/java/com/npaw/forge/reactnative/video/NPAWForgeVideoModule.java +0 -341
- package/ios/rnv6/ForgeRNVPlayerPlugin.swift +0 -122
- package/ios/rnv6/ForgeVideoModuleImpl.swift +0 -363
|
@@ -0,0 +1,474 @@
|
|
|
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 7 on iOS. Same ObjC surface and outcome contract as the v6 generation (auto single-player policy, X-NPAW-Forge token matching, source-URI fallback, per-binding stopOnPlayerRemoved), but the facts come straight from the public HybridVideoPlayer source (uri + config.headers) and one AVPlayer per hybrid means one bind per player - no asset-identity map, no Mirror reflection.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import AVFoundation
|
|
16
|
+
import Foundation
|
|
17
|
+
#if canImport(NitroModules)
|
|
18
|
+
import NitroModules
|
|
19
|
+
#endif
|
|
20
|
+
#if canImport(ReactNativeVideo)
|
|
21
|
+
import ReactNativeVideo
|
|
22
|
+
#endif
|
|
23
|
+
#if canImport(NPAWForgeCore)
|
|
24
|
+
import NPAWForgeCore
|
|
25
|
+
import NPAWForgeAVPlayerAdapter
|
|
26
|
+
#elseif canImport(NPAWForgeApple)
|
|
27
|
+
import NPAWForgeApple
|
|
28
|
+
#endif
|
|
29
|
+
#if canImport(NPAWForgeReactNative)
|
|
30
|
+
import NPAWForgeReactNative
|
|
31
|
+
#endif
|
|
32
|
+
|
|
33
|
+
public typealias ForgeVideoResolveBlock = (Any?) -> Void
|
|
34
|
+
public typealias ForgeVideoRejectBlock = (String, String) -> Void
|
|
35
|
+
|
|
36
|
+
@objc(ForgeVideoModuleImpl)
|
|
37
|
+
public final class ForgeVideoModuleImpl: NSObject {
|
|
38
|
+
|
|
39
|
+
@objc public static let shared = ForgeVideoModuleImpl()
|
|
40
|
+
static let tokenHeader = "x-npaw-forge"
|
|
41
|
+
|
|
42
|
+
private struct AttachRequest {
|
|
43
|
+
let instanceId: String
|
|
44
|
+
let identifier: String
|
|
45
|
+
let policy: String
|
|
46
|
+
let headerToken: String?
|
|
47
|
+
let sourceUri: String?
|
|
48
|
+
let stopOnPlayerRemoved: Bool
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// One rn-video player as this module sees it. Upstream hands out no id, so
|
|
52
|
+
/// the module mints one; the hybrid is held weakly (upstream owns its
|
|
53
|
+
/// lifetime), the AVPlayer strongly (the adapter observes it).
|
|
54
|
+
private final class LivePlayer {
|
|
55
|
+
let id: String
|
|
56
|
+
let identity: ObjectIdentifier
|
|
57
|
+
weak var hybrid: (any NativeVideoPlayer)?
|
|
58
|
+
let player: AVPlayer
|
|
59
|
+
// Facts snapshot, refreshed from the hybrid while it is alive.
|
|
60
|
+
var token: String?
|
|
61
|
+
var uri: String?
|
|
62
|
+
|
|
63
|
+
/// Whether this AVPlayer showed play intent (`timeControlStatus` other
|
|
64
|
+
/// than `.paused`) for its current item. That is exactly the condition
|
|
65
|
+
/// under which `AVPlayerAdapter` opens a started view - and therefore
|
|
66
|
+
/// fires the terminal stop itself when the item is later replaced with
|
|
67
|
+
/// nil. See `unbindOnMain`.
|
|
68
|
+
private var playIntent = false
|
|
69
|
+
private let playIntentLock = NSLock()
|
|
70
|
+
private var observations: [NSKeyValueObservation] = []
|
|
71
|
+
|
|
72
|
+
init(id: String, hybrid: any NativeVideoPlayer) {
|
|
73
|
+
self.id = id
|
|
74
|
+
self.identity = ObjectIdentifier(hybrid as AnyObject)
|
|
75
|
+
self.hybrid = hybrid
|
|
76
|
+
self.player = hybrid.player
|
|
77
|
+
observations.append(
|
|
78
|
+
player.observe(\.timeControlStatus, options: [.initial, .new]) { [weak self] player, _ in
|
|
79
|
+
guard let self, player.timeControlStatus != .paused else { return }
|
|
80
|
+
self.playIntentLock.lock()
|
|
81
|
+
self.playIntent = true
|
|
82
|
+
self.playIntentLock.unlock()
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
observations.append(
|
|
86
|
+
player.observe(\.currentItem, options: [.new]) { [weak self] player, _ in
|
|
87
|
+
// A NEW item resets the adapter's started flag (it stops the old
|
|
88
|
+
// view first); a transition to nil must keep the flag, because
|
|
89
|
+
// that is the teardown we consult it for.
|
|
90
|
+
guard let self, player.currentItem != nil else { return }
|
|
91
|
+
self.playIntentLock.lock()
|
|
92
|
+
self.playIntent = false
|
|
93
|
+
self.playIntentLock.unlock()
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
deinit {
|
|
99
|
+
observations.forEach { $0.invalidate() }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var sawPlayIntent: Bool {
|
|
103
|
+
playIntentLock.lock()
|
|
104
|
+
defer { playIntentLock.unlock() }
|
|
105
|
+
return playIntent
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private struct Binding {
|
|
110
|
+
let request: AttachRequest
|
|
111
|
+
let playerId: String
|
|
112
|
+
let adapter: AVPlayerAdapter
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private let lock = NSLock()
|
|
116
|
+
private var livePlayers: [LivePlayer] = []
|
|
117
|
+
private var sequence = 0
|
|
118
|
+
private var pending: [AttachRequest] = []
|
|
119
|
+
private var bindings: [String: Binding] = [:] // key: instanceId + " " + identifier
|
|
120
|
+
|
|
121
|
+
private let resolveForge: (String) -> NPAWForge?
|
|
122
|
+
|
|
123
|
+
private override init() {
|
|
124
|
+
self.resolveForge = { ForgeModuleImpl.shared.resolveInstance($0) }
|
|
125
|
+
super.init()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MARK: - Spec surface
|
|
129
|
+
|
|
130
|
+
@objc public func attach(
|
|
131
|
+
_ instanceId: String,
|
|
132
|
+
identifier: String,
|
|
133
|
+
options: [String: Any]?,
|
|
134
|
+
resolve: @escaping ForgeVideoResolveBlock,
|
|
135
|
+
reject: @escaping ForgeVideoRejectBlock
|
|
136
|
+
) {
|
|
137
|
+
ForgeRNVideoPlugin.registerOnce()
|
|
138
|
+
|
|
139
|
+
guard let forge = resolveForge(instanceId) else {
|
|
140
|
+
reject("E_FORGE_UNKNOWN_INSTANCE", "No NPAWForge instance '\(instanceId)'")
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
ensureVideo(forge, identifier: identifier)
|
|
144
|
+
|
|
145
|
+
let headerToken = nonEmpty(options?["headerToken"] as? String)
|
|
146
|
+
let sourceUri = nonEmpty(options?["sourceUri"] as? String)
|
|
147
|
+
var policy = nonEmpty(options?["policy"] as? String)
|
|
148
|
+
if policy == nil {
|
|
149
|
+
policy = (headerToken != nil || sourceUri != nil) ? "match" : "auto"
|
|
150
|
+
}
|
|
151
|
+
let request = AttachRequest(
|
|
152
|
+
instanceId: instanceId,
|
|
153
|
+
identifier: identifier,
|
|
154
|
+
policy: policy ?? "auto",
|
|
155
|
+
headerToken: headerToken,
|
|
156
|
+
sourceUri: sourceUri,
|
|
157
|
+
stopOnPlayerRemoved: options?["stopOnPlayerRemoved"] as? Bool ?? true
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
lock.lock()
|
|
161
|
+
let key = bindingKey(instanceId, identifier)
|
|
162
|
+
if bindings[key] != nil {
|
|
163
|
+
lock.unlock()
|
|
164
|
+
resolve(attachResult(code: "ignored_duplicate", accepted: false, attachment: "bound"))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
pending.removeAll { $0.instanceId == instanceId && $0.identifier == identifier }
|
|
168
|
+
refreshFactsLocked()
|
|
169
|
+
|
|
170
|
+
if request.policy == "auto" {
|
|
171
|
+
let unbound = unboundPlayersLocked()
|
|
172
|
+
if unbound.count > 1 {
|
|
173
|
+
lock.unlock()
|
|
174
|
+
resolve(
|
|
175
|
+
attachResult(
|
|
176
|
+
code: "unsupported_capability",
|
|
177
|
+
accepted: false,
|
|
178
|
+
attachment: "pending",
|
|
179
|
+
reason: "ambiguous_auto_attach"
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
if let candidate = unbound.first {
|
|
185
|
+
bindLocked(request, to: candidate)
|
|
186
|
+
lock.unlock()
|
|
187
|
+
resolve(attachResult(code: "accepted", accepted: true, attachment: "bound"))
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
} else if let candidate = findMatchLocked(request) {
|
|
191
|
+
bindLocked(request, to: candidate)
|
|
192
|
+
lock.unlock()
|
|
193
|
+
resolve(attachResult(code: "accepted", accepted: true, attachment: "bound"))
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
pending.append(request)
|
|
198
|
+
lock.unlock()
|
|
199
|
+
scheduleReevaluations()
|
|
200
|
+
resolve(attachResult(code: "accepted", accepted: true, attachment: "pending"))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@objc public func detach(
|
|
204
|
+
_ instanceId: String,
|
|
205
|
+
identifier: String,
|
|
206
|
+
resolve: @escaping ForgeVideoResolveBlock,
|
|
207
|
+
reject: @escaping ForgeVideoRejectBlock
|
|
208
|
+
) {
|
|
209
|
+
lock.lock()
|
|
210
|
+
pending.removeAll { $0.instanceId == instanceId && $0.identifier == identifier }
|
|
211
|
+
let removed = bindings.removeValue(forKey: bindingKey(instanceId, identifier))
|
|
212
|
+
lock.unlock()
|
|
213
|
+
|
|
214
|
+
guard let removed else {
|
|
215
|
+
resolve(attachResult(code: "ignored_invalid_state", accepted: false, attachment: "pending"))
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
unbindOnMain(removed, fireStop: true, live: livePlayer(id: removed.playerId))
|
|
219
|
+
resolve(attachResult(code: "accepted", accepted: true, attachment: "pending"))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
@objc public func invalidate() {
|
|
223
|
+
lock.lock()
|
|
224
|
+
livePlayers.removeAll()
|
|
225
|
+
pending.removeAll()
|
|
226
|
+
let current = Array(bindings.values)
|
|
227
|
+
bindings.removeAll()
|
|
228
|
+
lock.unlock()
|
|
229
|
+
|
|
230
|
+
for binding in current {
|
|
231
|
+
unbindOnMain(binding, fireStop: false, live: nil)
|
|
232
|
+
}
|
|
233
|
+
ForgeRNVideoPlugin.unregister()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// MARK: - Plugin callbacks (main thread)
|
|
237
|
+
|
|
238
|
+
func playerCreated(_ hybrid: any NativeVideoPlayer) {
|
|
239
|
+
let identity = ObjectIdentifier(hybrid as AnyObject)
|
|
240
|
+
lock.lock()
|
|
241
|
+
if livePlayers.contains(where: { $0.identity == identity }) {
|
|
242
|
+
// v7 has a single create call site; the guard stays because it is free
|
|
243
|
+
// and a duplicate would otherwise double-bind.
|
|
244
|
+
lock.unlock()
|
|
245
|
+
NSLog("[NPAWForgeRNV] duplicate onPlayerCreated suppressed")
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
sequence += 1
|
|
249
|
+
let live = LivePlayer(id: "rnv7-\(sequence)", hybrid: hybrid)
|
|
250
|
+
refreshFacts(live)
|
|
251
|
+
livePlayers.append(live)
|
|
252
|
+
NSLog("[NPAWForgeRNV] player created id=%@ uri=%@", live.id, live.uri ?? "-")
|
|
253
|
+
lock.unlock()
|
|
254
|
+
|
|
255
|
+
reevaluate()
|
|
256
|
+
scheduleReevaluations()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
func playerDestroyed(_ hybrid: (any NativeVideoPlayer)?) {
|
|
260
|
+
var affected: [(Binding, LivePlayer)] = []
|
|
261
|
+
var removed: [LivePlayer] = []
|
|
262
|
+
|
|
263
|
+
lock.lock()
|
|
264
|
+
let identity = hybrid.map { ObjectIdentifier($0 as AnyObject) }
|
|
265
|
+
livePlayers.removeAll { live in
|
|
266
|
+
// The notified hybrid, plus anything whose weak reference already
|
|
267
|
+
// cleared (upstream can notify from the hybrid's deinit).
|
|
268
|
+
let gone = live.identity == identity || live.hybrid == nil
|
|
269
|
+
if gone { removed.append(live) }
|
|
270
|
+
return gone
|
|
271
|
+
}
|
|
272
|
+
for (key, binding) in bindings {
|
|
273
|
+
if let live = removed.first(where: { $0.id == binding.playerId }) {
|
|
274
|
+
bindings.removeValue(forKey: key)
|
|
275
|
+
affected.append((binding, live))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
lock.unlock()
|
|
279
|
+
|
|
280
|
+
// Upstream notifies on the main thread AFTER it replaced the item with
|
|
281
|
+
// nil, and the AVPlayer keeps delivering transitional KVO (rate /
|
|
282
|
+
// timeControlStatus) for a moment. Detach synchronously here, on this
|
|
283
|
+
// very runloop turn, so those late deliveries meet a detached adapter
|
|
284
|
+
// instead of re-opening a view the adapter has just stopped.
|
|
285
|
+
for (binding, live) in affected {
|
|
286
|
+
unbindOnMain(binding, fireStop: binding.request.stopOnPlayerRemoved, live: live)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// MARK: - Coordinator internals
|
|
291
|
+
|
|
292
|
+
private func bindingKey(_ instanceId: String, _ identifier: String) -> String {
|
|
293
|
+
"\(instanceId) \(identifier)"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private func livePlayer(id: String) -> LivePlayer? {
|
|
297
|
+
lock.lock()
|
|
298
|
+
defer { lock.unlock() }
|
|
299
|
+
return livePlayers.first { $0.id == id }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func ensureVideo(_ forge: NPAWForge, identifier: String) {
|
|
303
|
+
if forge.getVideoAdapter(identifier: identifier) == nil {
|
|
304
|
+
_ = forge.videoBuilder().setIdentifier(identifier).build()
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private func unboundPlayersLocked() -> [LivePlayer] {
|
|
309
|
+
let boundIds = Set(bindings.values.map { $0.playerId })
|
|
310
|
+
return livePlayers.filter { !boundIds.contains($0.id) }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// Facts live on the hybrid's public source (`uri`, `config.headers`); the
|
|
314
|
+
/// source can be replaced after creation, so they are re-read on every
|
|
315
|
+
/// evaluation while the hybrid is alive and the last snapshot is kept after.
|
|
316
|
+
private func refreshFacts(_ live: LivePlayer) {
|
|
317
|
+
guard let hybrid = live.hybrid else { return }
|
|
318
|
+
let source = hybrid.source
|
|
319
|
+
live.uri = source.uri
|
|
320
|
+
var token: String?
|
|
321
|
+
if let headers = source.config.headers {
|
|
322
|
+
for (key, value) in headers where key.lowercased() == ForgeVideoModuleImpl.tokenHeader {
|
|
323
|
+
token = value.isEmpty ? nil : value
|
|
324
|
+
if token != nil { break }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
live.token = token
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private func refreshFactsLocked() {
|
|
331
|
+
for live in livePlayers {
|
|
332
|
+
refreshFacts(live)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private func findMatchLocked(_ request: AttachRequest) -> LivePlayer? {
|
|
337
|
+
let candidates = unboundPlayersLocked()
|
|
338
|
+
if let token = request.headerToken {
|
|
339
|
+
for candidate in candidates where candidate.token == token {
|
|
340
|
+
return candidate
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if let uri = request.sourceUri {
|
|
344
|
+
for candidate in candidates where candidate.uri == uri {
|
|
345
|
+
return candidate
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return nil
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func bindLocked(_ request: AttachRequest, to live: LivePlayer) {
|
|
352
|
+
let adapter = AVPlayerAdapter(player: live.player)
|
|
353
|
+
bindings[bindingKey(request.instanceId, request.identifier)] = Binding(
|
|
354
|
+
request: request,
|
|
355
|
+
playerId: live.id,
|
|
356
|
+
adapter: adapter
|
|
357
|
+
)
|
|
358
|
+
let instanceId = request.instanceId
|
|
359
|
+
let identifier = request.identifier
|
|
360
|
+
let playerId = live.id
|
|
361
|
+
DispatchQueue.main.async { [weak self] in
|
|
362
|
+
guard let self, let forge = self.resolveForge(instanceId) else { return }
|
|
363
|
+
self.ensureVideo(forge, identifier: identifier)
|
|
364
|
+
guard let video = forge.getVideoAdapter(identifier: identifier) else { return }
|
|
365
|
+
NSLog("[NPAWForgeRNV] binding AVPlayerAdapter playerId=%@ identifier=%@", playerId, identifier)
|
|
366
|
+
_ = video.setPlayerAdapter(adapter)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private func reevaluate() {
|
|
371
|
+
lock.lock()
|
|
372
|
+
refreshFactsLocked()
|
|
373
|
+
var resolved: [AttachRequest] = []
|
|
374
|
+
for request in pending {
|
|
375
|
+
if request.policy == "auto" {
|
|
376
|
+
let unbound = unboundPlayersLocked()
|
|
377
|
+
if unbound.count == 1, let candidate = unbound.first {
|
|
378
|
+
bindLocked(request, to: candidate)
|
|
379
|
+
resolved.append(request)
|
|
380
|
+
}
|
|
381
|
+
} else if let candidate = findMatchLocked(request) {
|
|
382
|
+
bindLocked(request, to: candidate)
|
|
383
|
+
resolved.append(request)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
pending.removeAll { request in
|
|
387
|
+
resolved.contains {
|
|
388
|
+
$0.instanceId == request.instanceId && $0.identifier == request.identifier
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
lock.unlock()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// Sources can be replaced after creation (replaceSourceAsync); retry matching
|
|
395
|
+
/// a few times so 'match' attaches converge without observing upstream state.
|
|
396
|
+
private func scheduleReevaluations() {
|
|
397
|
+
for delay in [0.5, 1.5, 3.0] {
|
|
398
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
399
|
+
self?.reevaluate()
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/// Tears the binding down: detach the player adapter, then fire the terminal
|
|
405
|
+
/// stop unless the adapter already did. Detach first on purpose: by the time
|
|
406
|
+
/// this block runs upstream may already have released its player, so the
|
|
407
|
+
/// view is closed to the adapter before the stop makes it terminal (same
|
|
408
|
+
/// rationale as the v6 generation). The stop keeps the final position
|
|
409
|
+
/// through the kernel's playhead backfill.
|
|
410
|
+
///
|
|
411
|
+
/// rn-video 7 tears a player down by `replaceCurrentItem(with: nil)` BEFORE
|
|
412
|
+
/// it notifies plugins (`HybridVideoPlayer.releaseOnMainThread`), while
|
|
413
|
+
/// rn-video 6 notified first. So on the removal path the still-attached
|
|
414
|
+
/// `AVPlayerAdapter` has already seen the item transition and, if the view
|
|
415
|
+
/// had started, has already fired its terminal stop. A second stop would
|
|
416
|
+
/// make the kernel open a replacement view (`init -> stop`, the very phantom
|
|
417
|
+
/// `native/2026-08-13_rn-video-teardown-reinits-view` fixed for v6), so the
|
|
418
|
+
/// explicit stop is skipped exactly when the item is already gone AND the
|
|
419
|
+
/// player had shown play intent (= the adapter's own start condition). A
|
|
420
|
+
/// player torn down before it ever played keeps the explicit stop, which
|
|
421
|
+
/// closes its init-only view as a genuine exit-before-start.
|
|
422
|
+
private func unbindOnMain(_ binding: Binding, fireStop: Bool, live: LivePlayer?) {
|
|
423
|
+
let instanceId = binding.request.instanceId
|
|
424
|
+
let identifier = binding.request.identifier
|
|
425
|
+
let playerId = binding.playerId
|
|
426
|
+
let work: () -> Void = { [weak self] in
|
|
427
|
+
guard let self, let forge = self.resolveForge(instanceId) else { return }
|
|
428
|
+
guard let video = forge.getVideoAdapter(identifier: identifier) else { return }
|
|
429
|
+
_ = video.setPlayerAdapter(nil)
|
|
430
|
+
guard fireStop else { return }
|
|
431
|
+
if let live, live.player.currentItem == nil, live.sawPlayIntent {
|
|
432
|
+
NSLog(
|
|
433
|
+
"[NPAWForgeRNV] terminal stop already reported by the adapter for playerId=%@ identifier=%@ (item removed upstream before the plugin was notified)",
|
|
434
|
+
playerId, identifier)
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
_ = video.fireStop()
|
|
438
|
+
}
|
|
439
|
+
// Run inline when already on the main thread (the plugin's removal
|
|
440
|
+
// callback): deferring by a runloop turn is exactly the window in which
|
|
441
|
+
// a dying AVPlayer's late KVO could still reach the adapter.
|
|
442
|
+
if Thread.isMainThread {
|
|
443
|
+
work()
|
|
444
|
+
} else {
|
|
445
|
+
DispatchQueue.main.async(execute: work)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private func nonEmpty(_ value: String?) -> String? {
|
|
450
|
+
guard let value, !value.isEmpty else { return nil }
|
|
451
|
+
return value
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private func attachResult(
|
|
455
|
+
code: String,
|
|
456
|
+
accepted: Bool,
|
|
457
|
+
attachment: String,
|
|
458
|
+
reason: String? = nil
|
|
459
|
+
) -> [String: Any] {
|
|
460
|
+
var result: [String: Any] = [
|
|
461
|
+
"code": code,
|
|
462
|
+
"isAccepted": accepted,
|
|
463
|
+
"attachment": attachment,
|
|
464
|
+
]
|
|
465
|
+
if let reason {
|
|
466
|
+
result["reason"] = reason
|
|
467
|
+
}
|
|
468
|
+
// One line per attach/detach outcome in the device log, next to the
|
|
469
|
+
// player-created/binding lines, so a smoke run is legible without the
|
|
470
|
+
// JS console (RN >= 0.79 no longer echoes console.log to Metro).
|
|
471
|
+
NSLog("[NPAWForgeRNV] outcome code=%@ attachment=%@ reason=%@", code, attachment, reason ?? "-")
|
|
472
|
+
return result
|
|
473
|
+
}
|
|
474
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npaw/forge-react-native-video",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.7.4",
|
|
4
4
|
"description": "react-native-video adapter glue for NPAW Forge: attaches the native Media3/AVPlayer Forge adapters to react-native-video's internal players via its native plugin API (react-native-video 6 and 7 generations, one major each).",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Victor Soto <victor.soto@nicepeopleatwork.com>",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"@npaw/forge-react-native": "^9.7.4",
|
|
24
24
|
"react": "*",
|
|
25
25
|
"react-native": "*",
|
|
26
|
-
"react-native-video": ">=
|
|
26
|
+
"react-native-video": ">=7.0.0-beta.11 <8",
|
|
27
|
+
"react-native-nitro-modules": ">=0.35.0"
|
|
27
28
|
},
|
|
28
29
|
"codegenConfig": {
|
|
29
30
|
"name": "NPAWForgeVideoSpec",
|