@npaw/forge-react-native-video 9.7.5 → 10.7.5

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.
@@ -1,341 +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: react-native-video glue TurboModule for Android. Implements RNVExoplayerPlugin, smuggles the X-NPAW-Forge header token into the MediaItem tag via overrideMediaItemBuilder, caches player facts on the main thread, and delegates policy to RNVideoAttachCoordinator to bind Forge's Media3PlayerAdapter.
13
- */
14
-
15
- package com.npaw.forge.reactnative.video;
16
-
17
- import androidx.annotation.Nullable;
18
- import android.os.Handler;
19
- import android.os.Looper;
20
- import androidx.media3.common.MediaItem;
21
- import androidx.media3.datasource.DataSource;
22
- import androidx.media3.exoplayer.ExoPlayer;
23
- import androidx.media3.exoplayer.drm.DrmSessionManager;
24
- import androidx.media3.exoplayer.source.MediaSource;
25
- import com.brentvatne.common.api.Source;
26
- import com.brentvatne.exoplayer.DRMManagerSpec;
27
- import com.brentvatne.exoplayer.RNVExoplayerPlugin;
28
- import com.brentvatne.react.ReactNativeVideoManager;
29
- import com.facebook.react.bridge.Arguments;
30
- import com.facebook.react.bridge.Promise;
31
- import com.facebook.react.bridge.ReactApplicationContext;
32
- import com.facebook.react.bridge.ReadableMap;
33
- import com.facebook.react.bridge.UiThreadUtil;
34
- import com.facebook.react.bridge.WritableMap;
35
- import com.npaw.forge.android.NPAWForge;
36
- import com.npaw.forge.android.adapters.media3.Media3PlayerAdapter;
37
- import com.npaw.forge.android.video.VideoAdapter;
38
- import com.npaw.forge.reactnative.ForgeInstanceRegistry;
39
- import java.util.HashMap;
40
- import java.util.Locale;
41
- import java.util.Map;
42
- import java.util.concurrent.ConcurrentHashMap;
43
-
44
- public class NPAWForgeVideoModule extends NativeNPAWForgeVideoSpec
45
- implements RNVExoplayerPlugin {
46
-
47
- public static final String NAME = "NPAWForgeVideoRN";
48
- /** Header customers set on source.requestHeaders for multi-player matching. */
49
- public static final String TOKEN_HEADER = "x-npaw-forge";
50
- private static final String TAG_PREFIX = "npaw-forge:";
51
- private static final String E_UNKNOWN_INSTANCE = "E_FORGE_UNKNOWN_INSTANCE";
52
-
53
- private static final class PlayerFacts {
54
- @Nullable volatile String token;
55
- @Nullable volatile String uri;
56
- }
57
-
58
- private final ConcurrentHashMap<String, PlayerFacts> facts = new ConcurrentHashMap<>();
59
- private final ConcurrentHashMap<String, ExoPlayer> playersById = new ConcurrentHashMap<>();
60
- private final ConcurrentHashMap<String, Media3PlayerAdapter> adaptersByPlayerId =
61
- new ConcurrentHashMap<>();
62
- private final Handler mainHandler = new Handler(Looper.getMainLooper());
63
-
64
- private final RNVideoAttachCoordinator<ExoPlayer> coordinator =
65
- new RNVideoAttachCoordinator<>(
66
- new RNVideoAttachCoordinator.PlayerIntrospection() {
67
- @Nullable
68
- @Override
69
- public String currentToken(String playerId) {
70
- PlayerFacts f = facts.get(playerId);
71
- return f == null ? null : f.token;
72
- }
73
-
74
- @Nullable
75
- @Override
76
- public String currentUri(String playerId) {
77
- PlayerFacts f = facts.get(playerId);
78
- return f == null ? null : f.uri;
79
- }
80
- },
81
- new RNVideoAttachCoordinator.Binder<ExoPlayer>() {
82
- @Override
83
- public void bind(String instanceId, String identifier, String playerId, ExoPlayer player) {
84
- bindOnMain(instanceId, identifier, playerId, player);
85
- }
86
-
87
- @Override
88
- public void unbind(
89
- String instanceId, String identifier, String playerId, boolean fireStop) {
90
- unbindOnMain(instanceId, identifier, playerId, fireStop);
91
- }
92
- });
93
-
94
- public NPAWForgeVideoModule(ReactApplicationContext reactContext) {
95
- super(reactContext);
96
- ReactNativeVideoManager.Companion.getInstance().registerPlugin(this);
97
- }
98
-
99
- @Override
100
- public String getName() {
101
- return NAME;
102
- }
103
-
104
- // ---- spec surface ----
105
-
106
- @Override
107
- public void attach(String instanceId, String identifier, @Nullable ReadableMap options, Promise promise) {
108
- NPAWForge forge = ForgeInstanceRegistry.resolve(instanceId);
109
- if (forge == null) {
110
- promise.reject(E_UNKNOWN_INSTANCE, "No NPAWForge instance '" + instanceId + "'");
111
- return;
112
- }
113
- ensureVideo(forge, identifier);
114
-
115
- Map<String, Object> map = options == null ? new HashMap<>() : options.toHashMap();
116
- String headerToken = stringOrNull(map.get("headerToken"));
117
- String sourceUri = stringOrNull(map.get("sourceUri"));
118
- String policy = stringOrNull(map.get("policy"));
119
- if (policy == null) {
120
- policy = (headerToken != null || sourceUri != null) ? "match" : "auto";
121
- }
122
- boolean stopOnPlayerRemoved =
123
- !(map.get("stopOnPlayerRemoved") instanceof Boolean)
124
- || (Boolean) map.get("stopOnPlayerRemoved");
125
-
126
- RNVideoAttachCoordinator.AttachOutcome outcome =
127
- coordinator.attach(
128
- new RNVideoAttachCoordinator.AttachRequest(
129
- instanceId, identifier, policy, headerToken, sourceUri, stopOnPlayerRemoved));
130
- scheduleFactRefreshes();
131
- promise.resolve(attachResult(outcome));
132
- }
133
-
134
- @Override
135
- public void detach(String instanceId, String identifier, Promise promise) {
136
- boolean removed = coordinator.detach(instanceId, identifier, true);
137
- WritableMap result = Arguments.createMap();
138
- result.putString("code", removed ? "accepted" : "ignored_invalid_state");
139
- result.putBoolean("isAccepted", removed);
140
- result.putString("attachment", "pending");
141
- promise.resolve(result);
142
- }
143
-
144
- @Override
145
- public void invalidate() {
146
- ReactNativeVideoManager.Companion.getInstance().unregisterPlugin(this);
147
- coordinator.clear();
148
- facts.clear();
149
- playersById.clear();
150
- adaptersByPlayerId.clear();
151
- super.invalidate();
152
- }
153
-
154
- // ---- RNVExoplayerPlugin ----
155
-
156
- @Override
157
- public void onInstanceCreated(String id, ExoPlayer player) {
158
- facts.putIfAbsent(id, new PlayerFacts());
159
- playersById.put(id, player);
160
- UiThreadUtil.runOnUiThread(
161
- () -> {
162
- refreshFacts(id, player);
163
- coordinator.playerCreated(id, player);
164
- });
165
- scheduleFactRefreshes();
166
- }
167
-
168
- @Override
169
- public void onInstanceRemoved(String id, ExoPlayer player) {
170
- playersById.remove(id);
171
- facts.remove(id);
172
- coordinator.playerRemoved(id);
173
- }
174
-
175
- // ---- RNVExoplayerPlugin optional hooks ----
176
-
177
- @Nullable
178
- @Override
179
- public DRMManagerSpec getDRMManager() {
180
- return null;
181
- }
182
-
183
- @Nullable
184
- @Override
185
- public DrmSessionManager overrideDrmSessionManager(Source source, DrmSessionManager drmSessionManager) {
186
- return null;
187
- }
188
-
189
- @Nullable
190
- @Override
191
- public DataSource.Factory overrideMediaDataSourceFactory(
192
- Source source, DataSource.Factory mediaDataSourceFactory) {
193
- return null;
194
- }
195
-
196
- @Nullable
197
- @Override
198
- public MediaSource.Factory overrideMediaSourceFactory(
199
- Source source, MediaSource.Factory mediaSourceFactory, DataSource.Factory mediaDataSourceFactory) {
200
- return null;
201
- }
202
-
203
- @Nullable
204
- @Override
205
- public MediaItem.Builder overrideMediaItemBuilder(Source source, MediaItem.Builder mediaItemBuilder) {
206
- String token = tokenFromHeaders(source.getHeaders());
207
- if (token == null) {
208
- return null;
209
- }
210
- // The tag rides the MediaItem so introspection can read it back from
211
- // player.currentMediaItem on the main thread - deterministic multi-player
212
- // correlation without joining override->create call ordering.
213
- return mediaItemBuilder.setTag(TAG_PREFIX + token);
214
- }
215
-
216
- @Override
217
- public boolean shouldDisableCache(Source source) {
218
- return false;
219
- }
220
-
221
- @Override
222
- public void onInstanceCreated(String id, Object player) {
223
- if (player instanceof ExoPlayer) {
224
- onInstanceCreated(id, (ExoPlayer) player);
225
- }
226
- }
227
-
228
- @Override
229
- public void onInstanceRemoved(String id, Object player) {
230
- if (player instanceof ExoPlayer) {
231
- onInstanceRemoved(id, (ExoPlayer) player);
232
- }
233
- }
234
-
235
- // ---- internals ----
236
-
237
- private void ensureVideo(NPAWForge forge, String identifier) {
238
- if (forge.getVideoAdapter(identifier) == null) {
239
- forge.videoBuilder().setIdentifier(identifier).build();
240
- }
241
- }
242
-
243
- /**
244
- * Sources settle after player creation; instead of a media3 Player.Listener
245
- * (whose default-method desugaring is fragile across media3 versions), a
246
- * few delayed main-thread refreshes converge token/URI facts and re-run the
247
- * pending-match evaluation.
248
- */
249
- private void scheduleFactRefreshes() {
250
- for (long delayMs : new long[] {0L, 500L, 1500L, 3000L}) {
251
- mainHandler.postDelayed(
252
- () -> {
253
- for (Map.Entry<String, ExoPlayer> entry : playersById.entrySet()) {
254
- refreshFacts(entry.getKey(), entry.getValue());
255
- }
256
- coordinator.playerSourceSettled();
257
- },
258
- delayMs);
259
- }
260
- }
261
-
262
- /** Main thread only. */
263
- private void refreshFacts(String playerId, ExoPlayer player) {
264
- PlayerFacts f = facts.computeIfAbsent(playerId, key -> new PlayerFacts());
265
- MediaItem item = player.getCurrentMediaItem();
266
- if (item != null && item.localConfiguration != null) {
267
- Object tag = item.localConfiguration.tag;
268
- f.token =
269
- tag instanceof String && ((String) tag).startsWith(TAG_PREFIX)
270
- ? ((String) tag).substring(TAG_PREFIX.length())
271
- : null;
272
- f.uri = item.localConfiguration.uri.toString();
273
- }
274
- }
275
-
276
- private void bindOnMain(String instanceId, String identifier, String playerId, ExoPlayer player) {
277
- UiThreadUtil.runOnUiThread(
278
- () -> {
279
- NPAWForge forge = ForgeInstanceRegistry.resolve(instanceId);
280
- if (forge == null || player == null) {
281
- return;
282
- }
283
- ensureVideo(forge, identifier);
284
- VideoAdapter video = forge.getVideoAdapter(identifier);
285
- if (video == null) {
286
- return;
287
- }
288
- Media3PlayerAdapter adapter = new Media3PlayerAdapter(player);
289
- video.setPlayerAdapter(adapter);
290
- adaptersByPlayerId.put(playerId, adapter);
291
- });
292
- }
293
-
294
- private void unbindOnMain(String instanceId, String identifier, String playerId, boolean fireStop) {
295
- UiThreadUtil.runOnUiThread(
296
- () -> {
297
- adaptersByPlayerId.remove(playerId);
298
- VideoAdapter video = ForgeInstanceRegistry.resolveVideoAdapter(instanceId, identifier);
299
- if (video == null) {
300
- return;
301
- }
302
- if (fireStop) {
303
- video.fireStop();
304
- }
305
- video.setPlayerAdapter(null);
306
- });
307
- }
308
-
309
- @Nullable
310
- private static String tokenFromHeaders(@Nullable Map<String, String> headers) {
311
- if (headers == null) {
312
- return null;
313
- }
314
- for (Map.Entry<String, String> entry : headers.entrySet()) {
315
- if (entry.getKey() != null
316
- && TOKEN_HEADER.equals(entry.getKey().toLowerCase(Locale.ROOT))) {
317
- return entry.getValue();
318
- }
319
- }
320
- return null;
321
- }
322
-
323
- @Nullable
324
- private static String stringOrNull(@Nullable Object value) {
325
- if (value instanceof String && !((String) value).isEmpty()) {
326
- return (String) value;
327
- }
328
- return null;
329
- }
330
-
331
- private static WritableMap attachResult(RNVideoAttachCoordinator.AttachOutcome outcome) {
332
- WritableMap result = Arguments.createMap();
333
- result.putString("code", outcome.code);
334
- result.putBoolean("isAccepted", outcome.accepted);
335
- result.putString("attachment", outcome.attachment);
336
- if (outcome.reason != null) {
337
- result.putString("reason", outcome.reason);
338
- }
339
- return result;
340
- }
341
- }
@@ -1,122 +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: Mandatory RNVAVPlayerPlugin subclass (split object from the TurboModule); forwards react-native-video player lifecycle and source/asset sightings to the Forge attach coordinator.
13
- */
14
-
15
- import AVFoundation
16
- import Foundation
17
- #if canImport(react_native_video)
18
- import react_native_video
19
- #endif
20
-
21
- /// react-native-video stores plugins in an unordered Set and RCTVideo has two
22
- /// onInstanceCreated call sites, so this object registers exactly once and the
23
- /// coordinator dedupes repeated deliveries of the same player.
24
- final class ForgeRNVPlayerPlugin: RNVAVPlayerPlugin {
25
-
26
- static let shared = ForgeRNVPlayerPlugin()
27
- private static var registered = false
28
- private static let registrationLock = NSLock()
29
-
30
- static func registerOnce() {
31
- registrationLock.lock()
32
- defer { registrationLock.unlock() }
33
- guard !registered else { return }
34
- registered = true
35
- ReactNativeVideoManager.shared.registerPlugin(plugin: shared)
36
- }
37
-
38
- override func onInstanceCreated(id: String, player: AVPlayer) {
39
- NSLog("[NPAWForgeRNV] player created id=%@", id)
40
- ForgeVideoModuleImpl.shared.playerCreated(id: id, player: player)
41
- }
42
-
43
- override func onInstanceRemoved(id: String, player: AVPlayer) {
44
- ForgeVideoModuleImpl.shared.playerRemoved(id: id, player: player)
45
- }
46
-
47
- /// Called for every AVPlayerItem creation; we never override the asset,
48
- /// but we record (asset identity -> token/uri) so the coordinator can
49
- /// correlate players with sources deterministically.
50
- ///
51
- /// VideoSource's fields are `internal` to react-native-video, so the
52
- /// token/uri are read via Mirror reflection (read-only), with the
53
- /// AVURLAsset URL as the fallback. Follow-up filed to propose public
54
- /// accessors upstream.
55
- override func overridePlayerAsset(source: VideoSource, asset: AVAsset) async -> OverridePlayerAssetResult? {
56
- var token: String?
57
- var uri: String?
58
- var sawUriLabel = false
59
- var sawHeadersLabel = false
60
- let mirror = Mirror(reflecting: source)
61
- for child in mirror.children {
62
- switch child.label {
63
- case "uri":
64
- sawUriLabel = true
65
- uri = child.value as? String ?? (child.value as? String?) ?? nil
66
- case "requestHeaders":
67
- sawHeadersLabel = true
68
- let headers = (child.value as? [String: Any]) ?? (child.value as? [String: Any]?) ?? nil
69
- if let headers {
70
- for (key, value) in headers where key.lowercased() == ForgeVideoModuleImpl.tokenHeader {
71
- token = value as? String
72
- }
73
- }
74
- default:
75
- break
76
- }
77
- }
78
- // Reflection has no compile-time contract, so a renamed field would
79
- // otherwise degrade in silence: `uri` falls back to the asset URL below
80
- // and playback still reports, while token correlation quietly stops.
81
- // Say so once, loudly enough to be found in a device log. The CI-side
82
- // guard is __tests__/rn-video-source-contract.test.ts.
83
- if !sawUriLabel || !sawHeadersLabel {
84
- ForgeRNVPlayerPlugin.reportReflectionDrift(sawUri: sawUriLabel, sawHeaders: sawHeadersLabel)
85
- }
86
- if uri == nil, let urlAsset = asset as? AVURLAsset {
87
- uri = urlAsset.url.absoluteString
88
- }
89
- ForgeVideoModuleImpl.shared.sourceSeen(asset: asset, token: token, uri: uri)
90
- return nil
91
- }
92
-
93
- /// One-shot notice that `VideoSource`'s shape no longer matches what the
94
- /// reflection expects.
95
- ///
96
- /// Reported once per process: this runs for every player item, and a
97
- /// per-item log on a list screen would bury the very message it exists to
98
- /// surface. Reuses the same static-lock pattern as `registerOnce`.
99
- private static var reflectionDriftReported = false
100
-
101
- private static func reportReflectionDrift(sawUri: Bool, sawHeaders: Bool) {
102
- registrationLock.lock()
103
- if reflectionDriftReported {
104
- registrationLock.unlock()
105
- return
106
- }
107
- reflectionDriftReported = true
108
- registrationLock.unlock()
109
-
110
- let missing = [sawUri ? nil : "uri", sawHeaders ? nil : "requestHeaders"]
111
- .compactMap { $0 }
112
- .joined(separator: ", ")
113
- NSLog(
114
- "[NPAWForge] react-native-video VideoSource no longer exposes: %@. "
115
- + "Forge reads these by reflection because they are internal upstream. "
116
- + "Playback reporting continues, but token correlation is degraded. "
117
- + "Check the installed react-native-video version against the "
118
- + "supported range (>=6.11.0 <7).",
119
- missing
120
- )
121
- }
122
- }