@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.
@@ -0,0 +1,388 @@
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: react-native-video 7 glue TurboModule for Android. Owns a ForgeRNVideoPlugin (upstream ReactNativeVideoPlugin subclass), tracks rn-video's HybridVideoPlayer objects by identity, reads the source URI and X-NPAW-Forge header token straight from the public source config (no MediaItem tag smuggling), copes with the ExoPlayer instance swap that upstream performs on deferred initialisation, and delegates policy to the shared RNVideoAttachCoordinator to bind Forge's Media3PlayerAdapter.
13
+ */
14
+
15
+ package com.npaw.forge.reactnative.video;
16
+
17
+ import android.os.Handler;
18
+ import android.os.Looper;
19
+ import androidx.annotation.NonNull;
20
+ import androidx.annotation.Nullable;
21
+ import androidx.annotation.OptIn;
22
+ import androidx.media3.common.util.UnstableApi;
23
+ import androidx.media3.exoplayer.ExoPlayer;
24
+ import com.facebook.react.bridge.Arguments;
25
+ import com.facebook.react.bridge.Promise;
26
+ import com.facebook.react.bridge.ReactApplicationContext;
27
+ import com.facebook.react.bridge.ReadableMap;
28
+ import com.facebook.react.bridge.UiThreadUtil;
29
+ import com.facebook.react.bridge.WritableMap;
30
+ import com.margelo.nitro.video.HybridVideoPlayer;
31
+ import com.margelo.nitro.video.HybridVideoPlayerSourceSpec;
32
+ import com.margelo.nitro.video.NativeVideoConfig;
33
+ import com.npaw.forge.android.NPAWForge;
34
+ import com.npaw.forge.android.adapters.media3.Media3PlayerAdapter;
35
+ import com.npaw.forge.android.video.VideoAdapter;
36
+ import com.npaw.forge.reactnative.ForgeInstanceRegistry;
37
+ import java.lang.ref.WeakReference;
38
+ import java.util.ArrayList;
39
+ import java.util.HashMap;
40
+ import java.util.List;
41
+ import java.util.Map;
42
+ import java.util.concurrent.ConcurrentHashMap;
43
+ import java.util.concurrent.atomic.AtomicInteger;
44
+
45
+ @OptIn(markerClass = UnstableApi.class)
46
+ public class NPAWForgeVideoModule extends NativeNPAWForgeVideoSpec
47
+ implements ForgeRNVideoPlugin.Listener {
48
+
49
+ public static final String NAME = "NPAWForgeVideoRN";
50
+ /** Header customers set on the source config for multi-player matching. */
51
+ public static final String TOKEN_HEADER = RNVideoHeaderToken.HEADER;
52
+ private static final String E_UNKNOWN_INSTANCE = "E_FORGE_UNKNOWN_INSTANCE";
53
+ private static final String PLAYER_ID_PREFIX = "rnv7-";
54
+ /** Poll cadence while a player created with initializeOnCreation=false waits for its real ExoPlayer. */
55
+ private static final long DEFERRED_INIT_POLL_MS = 500L;
56
+
57
+ /**
58
+ * One rn-video player as this module sees it. rn-video 7 hands out no id, so the
59
+ * module mints one and keeps only a weak reference to the hybrid object (upstream
60
+ * owns its lifetime; the JS-side VideoPlayer releases it).
61
+ */
62
+ private static final class PlayerRecord {
63
+ final String id;
64
+ final WeakReference<HybridVideoPlayer> hybrid;
65
+ @Nullable volatile String token;
66
+ @Nullable volatile String uri;
67
+ // Main-thread state of the current binding, if any.
68
+ @Nullable String boundInstanceId;
69
+ @Nullable String boundIdentifier;
70
+ @Nullable ExoPlayer boundPlayer;
71
+ @Nullable Media3PlayerAdapter adapter;
72
+
73
+ PlayerRecord(String id, HybridVideoPlayer hybrid) {
74
+ this.id = id;
75
+ this.hybrid = new WeakReference<>(hybrid);
76
+ }
77
+
78
+ boolean isBoundTo(String instanceId, String identifier) {
79
+ return instanceId.equals(boundInstanceId) && identifier.equals(boundIdentifier);
80
+ }
81
+ }
82
+
83
+ private final ConcurrentHashMap<String, PlayerRecord> records = new ConcurrentHashMap<>();
84
+ private final AtomicInteger sequence = new AtomicInteger();
85
+ private final Handler mainHandler = new Handler(Looper.getMainLooper());
86
+ @Nullable private volatile ForgeRNVideoPlugin plugin;
87
+
88
+ private final RNVideoAttachCoordinator<HybridVideoPlayer> coordinator =
89
+ new RNVideoAttachCoordinator<>(
90
+ new RNVideoAttachCoordinator.PlayerIntrospection() {
91
+ @Nullable
92
+ @Override
93
+ public String currentToken(String playerId) {
94
+ PlayerRecord record = records.get(playerId);
95
+ return record == null ? null : record.token;
96
+ }
97
+
98
+ @Nullable
99
+ @Override
100
+ public String currentUri(String playerId) {
101
+ PlayerRecord record = records.get(playerId);
102
+ return record == null ? null : record.uri;
103
+ }
104
+ },
105
+ new RNVideoAttachCoordinator.Binder<HybridVideoPlayer>() {
106
+ @Override
107
+ public void bind(
108
+ String instanceId, String identifier, String playerId, HybridVideoPlayer player) {
109
+ bindOnMain(instanceId, identifier, playerId);
110
+ }
111
+
112
+ @Override
113
+ public void unbind(
114
+ String instanceId, String identifier, String playerId, boolean fireStop) {
115
+ unbindOnMain(instanceId, identifier, playerId, fireStop);
116
+ }
117
+ });
118
+
119
+ public NPAWForgeVideoModule(ReactApplicationContext reactContext) {
120
+ super(reactContext);
121
+ // Register where upstream iterates its registry (VideoManager notifies plugins
122
+ // under runOnMainThreadSync). PluginsRegistry replaces a same-id plugin, so a
123
+ // dev-menu reload that re-creates this module cannot leave a stale plugin behind.
124
+ UiThreadUtil.runOnUiThread(
125
+ () -> {
126
+ if (plugin == null) {
127
+ plugin = new ForgeRNVideoPlugin(this);
128
+ }
129
+ });
130
+ }
131
+
132
+ @Override
133
+ public String getName() {
134
+ return NAME;
135
+ }
136
+
137
+ // ---- spec surface ----
138
+
139
+ @Override
140
+ public void attach(
141
+ String instanceId, String identifier, @Nullable ReadableMap options, Promise promise) {
142
+ NPAWForge forge = ForgeInstanceRegistry.resolve(instanceId);
143
+ if (forge == null) {
144
+ promise.reject(E_UNKNOWN_INSTANCE, "No NPAWForge instance '" + instanceId + "'");
145
+ return;
146
+ }
147
+ ensureVideo(forge, identifier);
148
+
149
+ Map<String, Object> map = options == null ? new HashMap<>() : options.toHashMap();
150
+ String headerToken = stringOrNull(map.get("headerToken"));
151
+ String sourceUri = stringOrNull(map.get("sourceUri"));
152
+ String policy = stringOrNull(map.get("policy"));
153
+ if (policy == null) {
154
+ policy = (headerToken != null || sourceUri != null) ? "match" : "auto";
155
+ }
156
+ boolean stopOnPlayerRemoved =
157
+ !(map.get("stopOnPlayerRemoved") instanceof Boolean)
158
+ || (Boolean) map.get("stopOnPlayerRemoved");
159
+
160
+ RNVideoAttachCoordinator.AttachOutcome outcome =
161
+ coordinator.attach(
162
+ new RNVideoAttachCoordinator.AttachRequest(
163
+ instanceId, identifier, policy, headerToken, sourceUri, stopOnPlayerRemoved));
164
+ scheduleFactRefreshes();
165
+ promise.resolve(attachResult(outcome));
166
+ }
167
+
168
+ @Override
169
+ public void detach(String instanceId, String identifier, Promise promise) {
170
+ boolean removed = coordinator.detach(instanceId, identifier, true);
171
+ WritableMap result = Arguments.createMap();
172
+ result.putString("code", removed ? "accepted" : "ignored_invalid_state");
173
+ result.putBoolean("isAccepted", removed);
174
+ result.putString("attachment", "pending");
175
+ promise.resolve(result);
176
+ }
177
+
178
+ @Override
179
+ public void invalidate() {
180
+ ForgeRNVideoPlugin current = plugin;
181
+ plugin = null;
182
+ if (current != null) {
183
+ UiThreadUtil.runOnUiThread(current::unregister);
184
+ }
185
+ coordinator.clear();
186
+ records.clear();
187
+ super.invalidate();
188
+ }
189
+
190
+ // ---- ForgeRNVideoPlugin.Listener (main thread) ----
191
+
192
+ @Override
193
+ public void onRnvPlayerCreated(@NonNull HybridVideoPlayer hybrid) {
194
+ if (findRecord(hybrid) != null) {
195
+ return; // defensive: upstream registers each hybrid once
196
+ }
197
+ String id = PLAYER_ID_PREFIX + sequence.incrementAndGet();
198
+ PlayerRecord record = new PlayerRecord(id, hybrid);
199
+ records.put(id, record);
200
+ refreshFacts(record, hybrid);
201
+ coordinator.playerCreated(id, hybrid);
202
+ scheduleFactRefreshes();
203
+ }
204
+
205
+ @Override
206
+ public void onRnvPlayerDestroyed(@Nullable HybridVideoPlayer hybrid) {
207
+ if (hybrid != null) {
208
+ PlayerRecord record = findRecord(hybrid);
209
+ if (record != null) {
210
+ removeRecord(record);
211
+ }
212
+ }
213
+ // Upstream's WeakReference can already be cleared; anything we track whose
214
+ // hybrid is gone is gone too.
215
+ for (PlayerRecord record : new ArrayList<>(records.values())) {
216
+ if (record.hybrid.get() == null) {
217
+ removeRecord(record);
218
+ }
219
+ }
220
+ }
221
+
222
+ // ---- internals ----
223
+
224
+ @Nullable
225
+ private PlayerRecord findRecord(HybridVideoPlayer hybrid) {
226
+ for (PlayerRecord record : records.values()) {
227
+ if (record.hybrid.get() == hybrid) {
228
+ return record;
229
+ }
230
+ }
231
+ return null;
232
+ }
233
+
234
+ private void removeRecord(PlayerRecord record) {
235
+ records.remove(record.id);
236
+ coordinator.playerRemoved(record.id);
237
+ }
238
+
239
+ private void ensureVideo(NPAWForge forge, String identifier) {
240
+ if (forge.getVideoAdapter(identifier) == null) {
241
+ forge.videoBuilder().setIdentifier(identifier).build();
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Facts come straight from the public source config. Sources can be replaced after
247
+ * creation (replaceSourceAsync) and the real ExoPlayer can appear after a deferred
248
+ * initialize(); a few delayed main-thread refreshes converge both and re-run the
249
+ * pending-match evaluation, mirroring the v6 module's cadence.
250
+ */
251
+ private void scheduleFactRefreshes() {
252
+ for (long delayMs : new long[] {0L, 500L, 1500L, 3000L}) {
253
+ mainHandler.postDelayed(
254
+ () -> {
255
+ for (PlayerRecord record : records.values()) {
256
+ HybridVideoPlayer hybrid = record.hybrid.get();
257
+ if (hybrid == null) {
258
+ continue;
259
+ }
260
+ refreshFacts(record, hybrid);
261
+ rebindIfPlayerSwapped(record, hybrid);
262
+ }
263
+ coordinator.playerSourceSettled();
264
+ },
265
+ delayMs);
266
+ }
267
+ }
268
+
269
+ /** Reads uri + token; safe on any thread (the config is an immutable data class). */
270
+ private static void refreshFacts(PlayerRecord record, HybridVideoPlayer hybrid) {
271
+ try {
272
+ HybridVideoPlayerSourceSpec source = hybrid.getSource();
273
+ record.uri = source.getUri();
274
+ NativeVideoConfig config = source.getConfig();
275
+ record.token = RNVideoHeaderToken.fromHeaders(config.getHeaders());
276
+ } catch (RuntimeException ignored) {
277
+ // `source` is a Kotlin lateinit upstream; it is always assigned before the
278
+ // player is registered, but a racing release must not take the module down.
279
+ }
280
+ }
281
+
282
+ private void bindOnMain(String instanceId, String identifier, String playerId) {
283
+ UiThreadUtil.runOnUiThread(
284
+ () -> {
285
+ PlayerRecord record = records.get(playerId);
286
+ if (record == null) {
287
+ return;
288
+ }
289
+ record.boundInstanceId = instanceId;
290
+ record.boundIdentifier = identifier;
291
+ record.boundPlayer = null;
292
+ record.adapter = null;
293
+ attachAdapterWhenReady(record, instanceId, identifier);
294
+ });
295
+ }
296
+
297
+ /** Main thread. Binds Media3PlayerAdapter to the hybrid's REAL ExoPlayer. */
298
+ private void attachAdapterWhenReady(PlayerRecord record, String instanceId, String identifier) {
299
+ if (!record.isBoundTo(instanceId, identifier)) {
300
+ return; // detached or rebound meanwhile
301
+ }
302
+ HybridVideoPlayer hybrid = record.hybrid.get();
303
+ if (hybrid == null) {
304
+ return;
305
+ }
306
+ if (!hybrid.getLoadedWithSource()) {
307
+ // initializeOnCreation=false: upstream still holds its temporary ExoPlayer and
308
+ // will replace it inside initialize()/preload(). Binding the placeholder would
309
+ // observe nothing, so wait for the real one.
310
+ mainHandler.postDelayed(
311
+ () -> attachAdapterWhenReady(record, instanceId, identifier), DEFERRED_INIT_POLL_MS);
312
+ return;
313
+ }
314
+ attachAdapter(record, hybrid, instanceId, identifier);
315
+ }
316
+
317
+ /** Main thread. */
318
+ private void attachAdapter(
319
+ PlayerRecord record, HybridVideoPlayer hybrid, String instanceId, String identifier) {
320
+ NPAWForge forge = ForgeInstanceRegistry.resolve(instanceId);
321
+ if (forge == null) {
322
+ return;
323
+ }
324
+ ensureVideo(forge, identifier);
325
+ VideoAdapter video = forge.getVideoAdapter(identifier);
326
+ if (video == null) {
327
+ return;
328
+ }
329
+ ExoPlayer player = hybrid.getPlayer();
330
+ Media3PlayerAdapter adapter = new Media3PlayerAdapter(player);
331
+ video.setPlayerAdapter(adapter);
332
+ record.boundPlayer = player;
333
+ record.adapter = adapter;
334
+ }
335
+
336
+ /** Main thread. A late initialize() swaps the ExoPlayer under a live binding: follow it. */
337
+ private void rebindIfPlayerSwapped(PlayerRecord record, HybridVideoPlayer hybrid) {
338
+ if (record.boundPlayer == null || record.boundInstanceId == null || record.boundIdentifier == null) {
339
+ return;
340
+ }
341
+ if (!hybrid.getLoadedWithSource() || hybrid.getPlayer() == record.boundPlayer) {
342
+ return;
343
+ }
344
+ // The previous instance was upstream's placeholder (or a stale player): it never
345
+ // reported anything, so replace the adapter silently rather than firing Stop.
346
+ attachAdapter(record, hybrid, record.boundInstanceId, record.boundIdentifier);
347
+ }
348
+
349
+ private void unbindOnMain(String instanceId, String identifier, String playerId, boolean fireStop) {
350
+ UiThreadUtil.runOnUiThread(
351
+ () -> {
352
+ PlayerRecord record = records.get(playerId);
353
+ if (record != null && record.isBoundTo(instanceId, identifier)) {
354
+ record.boundInstanceId = null;
355
+ record.boundIdentifier = null;
356
+ record.boundPlayer = null;
357
+ record.adapter = null;
358
+ }
359
+ VideoAdapter video = ForgeInstanceRegistry.resolveVideoAdapter(instanceId, identifier);
360
+ if (video == null) {
361
+ return;
362
+ }
363
+ if (fireStop) {
364
+ video.fireStop();
365
+ }
366
+ video.setPlayerAdapter(null);
367
+ });
368
+ }
369
+
370
+ @Nullable
371
+ private static String stringOrNull(@Nullable Object value) {
372
+ if (value instanceof String && !((String) value).isEmpty()) {
373
+ return (String) value;
374
+ }
375
+ return null;
376
+ }
377
+
378
+ private static WritableMap attachResult(RNVideoAttachCoordinator.AttachOutcome outcome) {
379
+ WritableMap result = Arguments.createMap();
380
+ result.putString("code", outcome.code);
381
+ result.putBoolean("isAccepted", outcome.accepted);
382
+ result.putString("attachment", outcome.attachment);
383
+ if (outcome.reason != null) {
384
+ result.putString("reason", outcome.reason);
385
+ }
386
+ return result;
387
+ }
388
+ }
@@ -9,11 +9,12 @@
9
9
  *
10
10
  *
11
11
  * @author: Victor Soto <victor.soto@nicepeopleatwork.com>
12
- * @description: React package registering the NPAWForgeVideo TurboModule for autolinking.
12
+ * @description: React package registering the NPAWForgeVideo TurboModule for autolinking (react-native-video 7 generation; same class name and module id as the v6 generation so JS and autolinking are generation-agnostic).
13
13
  */
14
14
 
15
15
  package com.npaw.forge.reactnative.video;
16
16
 
17
+ import androidx.annotation.Nullable;
17
18
  import com.facebook.react.TurboReactPackage;
18
19
  import com.facebook.react.bridge.NativeModule;
19
20
  import com.facebook.react.bridge.ReactApplicationContext;
@@ -21,7 +22,6 @@ import com.facebook.react.module.model.ReactModuleInfo;
21
22
  import com.facebook.react.module.model.ReactModuleInfoProvider;
22
23
  import java.util.HashMap;
23
24
  import java.util.Map;
24
- import androidx.annotation.Nullable;
25
25
 
26
26
  public class NPAWForgeVideoPackage extends TurboReactPackage {
27
27
 
@@ -0,0 +1,68 @@
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: JVM unit tests for the header-token extraction shared by the react-native-video 7 module (case-insensitive header key, empty and missing values, first non-empty casing wins).
13
+ */
14
+
15
+ package com.npaw.forge.reactnative.video;
16
+
17
+ import static org.junit.Assert.assertEquals;
18
+ import static org.junit.Assert.assertNull;
19
+
20
+ import java.util.HashMap;
21
+ import java.util.LinkedHashMap;
22
+ import java.util.Map;
23
+ import org.junit.Test;
24
+
25
+ public class RNVideoHeaderTokenTest {
26
+
27
+ @Test
28
+ public void nullMapYieldsNull() {
29
+ assertNull(RNVideoHeaderToken.fromHeaders(null));
30
+ }
31
+
32
+ @Test
33
+ public void missingHeaderYieldsNull() {
34
+ Map<String, String> headers = new HashMap<>();
35
+ headers.put("Authorization", "Bearer x");
36
+ assertNull(RNVideoHeaderToken.fromHeaders(headers));
37
+ }
38
+
39
+ @Test
40
+ public void matchesTheHeaderCaseInsensitively() {
41
+ Map<String, String> headers = new HashMap<>();
42
+ headers.put("X-NPAW-Forge", "main");
43
+ assertEquals("main", RNVideoHeaderToken.fromHeaders(headers));
44
+
45
+ headers.clear();
46
+ headers.put("x-npaw-forge", "pip");
47
+ assertEquals("pip", RNVideoHeaderToken.fromHeaders(headers));
48
+
49
+ headers.clear();
50
+ headers.put("X-Npaw-FORGE", "mixed");
51
+ assertEquals("mixed", RNVideoHeaderToken.fromHeaders(headers));
52
+ }
53
+
54
+ @Test
55
+ public void emptyValueCountsAsAbsent() {
56
+ Map<String, String> headers = new HashMap<>();
57
+ headers.put("X-NPAW-Forge", "");
58
+ assertNull(RNVideoHeaderToken.fromHeaders(headers));
59
+ }
60
+
61
+ @Test
62
+ public void firstNonEmptyCasingWins() {
63
+ Map<String, String> headers = new LinkedHashMap<>();
64
+ headers.put("x-npaw-forge", "");
65
+ headers.put("X-NPAW-Forge", "second");
66
+ assertEquals("second", RNVideoHeaderToken.fromHeaders(headers));
67
+ }
68
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$comment": "Single source of truth for the react-native-video generations shipped by @npaw/forge-react-native-video. Read by android/build.gradle, the podspec, the jest guard, and scripts/react-native/stage-react-native-npm.sh. Same npm package name, one major per generation.",
3
+ "package": "@npaw/forge-react-native-video",
4
+ "generations": {
5
+ "rnv6": {
6
+ "reactNativeVideo": ">=6.11.0 <7",
7
+ "reactNativeVideoMajor": 6,
8
+ "verifiedUpstream": "6.19.2",
9
+ "packageMajor": "forge",
10
+ "distTag": "latest",
11
+ "androidSourceSet": "src/rnv6/java",
12
+ "iosDir": "ios/rnv6",
13
+ "podDependency": "react-native-video",
14
+ "platforms": ["ios", "tvos"]
15
+ },
16
+ "rnv7": {
17
+ "reactNativeVideo": ">=7.0.0-beta.11 <8",
18
+ "reactNativeVideoMajor": 7,
19
+ "verifiedUpstream": "7.0.0-beta.11",
20
+ "upstreamStatus": "beta",
21
+ "packageMajor": 10,
22
+ "distTag": "rn-video-7",
23
+ "androidSourceSet": "src/rnv7/java",
24
+ "iosDir": "ios/rnv7",
25
+ "podDependency": "ReactNativeVideo",
26
+ "extraPodDependencies": ["NitroModules"],
27
+ "nitroModules": ">=0.35.0",
28
+ "platforms": ["ios"]
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,64 @@
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: react-native-video 7 plugin object for iOS. Subclasses the upstream ReactNativeVideoPlugin (whose initializer registers it in PluginsRegistry.shared, keyed by id) and forwards player create/destroy notifications, delivered on the main thread, to the Forge video module implementation.
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
+
24
+ /// One plugin object per process. rn-video 7 keys plugins by `id`
25
+ /// ("RNV_Plugin_NPAWForge") and replaces a same-id registration, so even a
26
+ /// double registration could not duplicate callbacks; this class still creates
27
+ /// the object exactly once and unregisters it on module invalidation.
28
+ final class ForgeRNVideoPlugin: ReactNativeVideoPlugin {
29
+
30
+ static let pluginName = "NPAWForge"
31
+ private static var current: ForgeRNVideoPlugin?
32
+ private static let registrationLock = NSLock()
33
+
34
+ static func registerOnce() {
35
+ registrationLock.lock()
36
+ defer { registrationLock.unlock() }
37
+ guard current == nil else { return }
38
+ current = ForgeRNVideoPlugin()
39
+ }
40
+
41
+ static func unregister() {
42
+ registrationLock.lock()
43
+ defer { registrationLock.unlock() }
44
+ guard let plugin = current else { return }
45
+ PluginsRegistry.shared.unregister(plugin: plugin)
46
+ current = nil
47
+ }
48
+
49
+ private init() {
50
+ // The upstream initializer performs the registration.
51
+ super.init(name: ForgeRNVideoPlugin.pluginName)
52
+ }
53
+
54
+ override func onPlayerCreated(player: Weak<NativeVideoPlayer>) {
55
+ guard let hybrid = player.value else { return }
56
+ ForgeVideoModuleImpl.shared.playerCreated(hybrid)
57
+ }
58
+
59
+ override func onPlayerDestroyed(player: Weak<NativeVideoPlayer>) {
60
+ // `value` is nil when upstream notifies from the hybrid's deinit; the module
61
+ // then sweeps whichever tracked player has gone away.
62
+ ForgeVideoModuleImpl.shared.playerDestroyed(player.value)
63
+ }
64
+ }