@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
|
@@ -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
|
+
}
|
package/android/src/{main → rnv7}/java/com/npaw/forge/reactnative/video/NPAWForgeVideoPackage.java
RENAMED
|
@@ -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
|
+
}
|
package/generations.json
ADDED
|
@@ -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
|
+
}
|