@jwplayer/jwplayer-react-native 1.3.1 → 1.3.3
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/CLAUDE.md +105 -0
- package/RNJWPlayer.podspec +1 -1
- package/android/src/ima/java/com/jwplayer/rnjwplayer/ImaHelper.java +0 -1
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +21 -1
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +19 -3
- package/badges/version.svg +1 -1
- package/ios/RNJWPlayer/RNJWPlayerView.swift +39 -17
- package/package.json +1 -1
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
This is `@jwplayer/jwplayer-react-native`, a React Native bridge library for the JW Player native SDKs (JWPlayerKit for iOS and JW SDK 4 for Android). It wraps native video player functionality for use in React Native applications.
|
|
8
|
+
|
|
9
|
+
## Common Commands
|
|
10
|
+
|
|
11
|
+
### Library Development
|
|
12
|
+
```bash
|
|
13
|
+
npm run lint # Lint the library (index.js and example)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Example App (from Example/ directory)
|
|
17
|
+
```bash
|
|
18
|
+
cd Example
|
|
19
|
+
yarn # Install dependencies
|
|
20
|
+
yarn ios # Run iOS app
|
|
21
|
+
yarn android # Run Android app
|
|
22
|
+
yarn start # Start Metro bundler
|
|
23
|
+
yarn test # Run Jest tests
|
|
24
|
+
pod install --project-directory=ios # Install iOS CocoaPods
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Building for Release
|
|
28
|
+
The library is published to npm as `@jwplayer/jwplayer-react-native`. Version badges are generated with:
|
|
29
|
+
```bash
|
|
30
|
+
npm run badges:version
|
|
31
|
+
npm run badges:license
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Architecture
|
|
35
|
+
|
|
36
|
+
### Bridge Structure
|
|
37
|
+
- **index.js**: Main React component (`JWPlayer`) that wraps the native view and exposes methods via `NativeModules`
|
|
38
|
+
- **index.d.ts**: TypeScript type definitions for the entire public API
|
|
39
|
+
|
|
40
|
+
### Native Implementations
|
|
41
|
+
- **iOS** (`ios/RNJWPlayer/`):
|
|
42
|
+
- `RNJWPlayerView.swift`: Main player view implementation (~89KB, handles player lifecycle, events, config parsing)
|
|
43
|
+
- `RNJWPlayerViewController.swift`: View controller for fullscreen/PiP support
|
|
44
|
+
- `RNJWPlayerViewManager.swift` + `.m`: React Native view manager bridge
|
|
45
|
+
- `RNJWPlayerAds.swift`: Advertising configuration helpers
|
|
46
|
+
|
|
47
|
+
- **Android** (`android/src/main/java/com/jwplayer/rnjwplayer/`):
|
|
48
|
+
- `RNJWPlayerView.java`: Main player view implementation
|
|
49
|
+
- `RNJWPlayerViewManager.java`: React Native view manager
|
|
50
|
+
- `RNJWPlayerModule.java`: Native module for imperative methods
|
|
51
|
+
- `RNJWPlayerAds.java`: Ad configuration handling
|
|
52
|
+
- `ImaHelper.java`: Conditional IMA support (separate source sets in `src/ima/` and `src/noima/`)
|
|
53
|
+
|
|
54
|
+
### Configuration System
|
|
55
|
+
The library supports two configuration modes:
|
|
56
|
+
1. **Modern Configuration** (recommended): Uses `JWPlayerConfig` type matching JW Player Delivery API format
|
|
57
|
+
2. **Legacy Configuration**: Set `forceLegacyConfig={true}` for older builder patterns
|
|
58
|
+
|
|
59
|
+
Type definitions are split across:
|
|
60
|
+
- `types/unified-config.d.ts`: Main configuration interface
|
|
61
|
+
- `types/advertising.d.ts`: Ad configuration types (VAST, IMA, IMA DAI)
|
|
62
|
+
- `types/playlist.d.ts`: Playlist and media item types
|
|
63
|
+
- `types/platform-specific.d.ts`: iOS/Android specific options
|
|
64
|
+
|
|
65
|
+
### Feature Flags (Build-time)
|
|
66
|
+
Features are conditionally compiled via build flags:
|
|
67
|
+
|
|
68
|
+
**iOS** (in Podfile):
|
|
69
|
+
```ruby
|
|
70
|
+
$RNJWPlayerUseGoogleIMA = true # Enables IMA ads
|
|
71
|
+
$RNJWPlayerUseGoogleCast = true # Enables Chromecast
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Android** (in app's build.gradle ext{}):
|
|
75
|
+
```groovy
|
|
76
|
+
RNJWPlayerUseGoogleIMA = true # Enables IMA ads
|
|
77
|
+
RNJWPlayerUseGoogleCast = true # Enables Chromecast
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### SDK Versions
|
|
81
|
+
- iOS: JWPlayerKit 4.25.0 (defined in `RNJWPlayer.podspec`)
|
|
82
|
+
- Android: JW Player 4.24.0 (defined in `android/build.gradle`)
|
|
83
|
+
|
|
84
|
+
## Key Implementation Details
|
|
85
|
+
|
|
86
|
+
### Player State Constants
|
|
87
|
+
Player states differ between platforms:
|
|
88
|
+
- iOS: `JWPlayerStateIOS` (0=Unknown, 1=Idle, 2=Buffering, 3=Playing, 4=Paused, 5=Complete, 6=Error)
|
|
89
|
+
- Android: `JWPlayerStateAndroid` (0=Idle, 1=Buffering, 2=Playing, 3=Paused, 4=Complete, Error=null)
|
|
90
|
+
|
|
91
|
+
### Event Flow
|
|
92
|
+
Native events flow through React Native's event system. The component forwards events like `onPlayerReady`, `onPlay`, `onPause`, `onTime`, `onComplete`, `onPlayerError`, etc.
|
|
93
|
+
|
|
94
|
+
### Imperative Methods
|
|
95
|
+
Methods like `play()`, `pause()`, `seekTo()`, `setFullscreen()` are called via `NativeModules` using the player's bridge handle obtained through `findNodeHandle()`.
|
|
96
|
+
|
|
97
|
+
## Example App Structure
|
|
98
|
+
|
|
99
|
+
The Example app (`Example/`) demonstrates various use cases:
|
|
100
|
+
- `SingleExample.js`: Basic player setup
|
|
101
|
+
- `DRMExample.js`: Fairplay/Widevine DRM playback
|
|
102
|
+
- `TypeScriptExample.tsx`: Modern TypeScript configuration
|
|
103
|
+
- `GlobalPlayerExample.js`: Player with full-screen and controls
|
|
104
|
+
- `ListExample.js`: Multiple players in a list
|
|
105
|
+
- `YoutubeExample.js`: Playing YouTube content
|
package/RNJWPlayer.podspec
CHANGED
|
@@ -12,7 +12,7 @@ Pod::Spec.new do |s|
|
|
|
12
12
|
s.platform = :ios, "15.0"
|
|
13
13
|
s.source = { :git => "https://github.com/jwplayer/jwplayer-react-native.git", :tag => "v#{s.version}" }
|
|
14
14
|
s.source_files = "ios/RNJWPlayer/*.{h,m,swift}"
|
|
15
|
-
s.dependency 'JWPlayerKit', '4.25.
|
|
15
|
+
s.dependency 'JWPlayerKit', '4.25.1'
|
|
16
16
|
s.dependency 'React-Core'
|
|
17
17
|
s.static_framework = true
|
|
18
18
|
s.info_plist = {
|
|
@@ -21,7 +21,6 @@ import java.util.Objects;
|
|
|
21
21
|
public class ImaHelper {
|
|
22
22
|
|
|
23
23
|
public static AdvertisingConfig configureImaOrDai(ReadableMap ads, List<AdBreak> adSchedule) {
|
|
24
|
-
cd /Users/jmilham/source/sdk/react/jwplayer-react-native/Example && yarn remove @jwplayer/jwplayer-react-native && yarn add file:../ 2>&1 | tail -20 // Check both "client" (JWPlayer JSON format) and "adClient" (RN wrapper format)
|
|
25
24
|
String adClientType = null;
|
|
26
25
|
if (ads.hasKey("adClient")) {
|
|
27
26
|
adClientType = ads.getString("adClient");
|
|
@@ -243,6 +243,9 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
243
243
|
// Add completion handler field
|
|
244
244
|
PlaylistItemDecision itemUpdatePromise = null;
|
|
245
245
|
|
|
246
|
+
// Flag to prevent race conditions during player destruction
|
|
247
|
+
private volatile boolean isDestroying = false;
|
|
248
|
+
|
|
246
249
|
private void doBindService() {
|
|
247
250
|
if (mMediaServiceController != null) {
|
|
248
251
|
if (!isBackgroundAudioServiceRunning()) {
|
|
@@ -380,7 +383,22 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
380
383
|
// }
|
|
381
384
|
|
|
382
385
|
public void destroyPlayer() {
|
|
383
|
-
if (mPlayer != null) {
|
|
386
|
+
if (mPlayer != null && !isDestroying) {
|
|
387
|
+
isDestroying = true;
|
|
388
|
+
|
|
389
|
+
// Disable touch events immediately to prevent race conditions
|
|
390
|
+
if (mPlayerView != null) {
|
|
391
|
+
Handler mainHandler = new Handler(Looper.getMainLooper());
|
|
392
|
+
mainHandler.post(() -> {
|
|
393
|
+
if (mPlayerView != null) {
|
|
394
|
+
mPlayerView.setClickable(false);
|
|
395
|
+
mPlayerView.setFocusable(false);
|
|
396
|
+
mPlayerView.setEnabled(false);
|
|
397
|
+
mPlayerView.setOnTouchListener(null);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
384
402
|
unRegisterReceiver();
|
|
385
403
|
|
|
386
404
|
// If we are casting we need to break the cast session as there is no simple
|
|
@@ -480,6 +498,8 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
480
498
|
audioManager = null;
|
|
481
499
|
|
|
482
500
|
doUnbindService();
|
|
501
|
+
|
|
502
|
+
isDestroying = false; // Reset flag for potential reuse
|
|
483
503
|
}
|
|
484
504
|
}
|
|
485
505
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.jwplayer.rnjwplayer;
|
|
2
2
|
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
|
|
3
5
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
4
6
|
import com.facebook.react.bridge.ReadableMap;
|
|
5
7
|
import com.facebook.react.common.MapBuilder;
|
|
@@ -43,7 +45,14 @@ public class RNJWPlayerViewManager extends SimpleViewManager<RNJWPlayerView> {
|
|
|
43
45
|
if (view == null || view.mPlayerView == null) {
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
|
-
|
|
48
|
+
// Add null check for getPlayer() to prevent crashes
|
|
49
|
+
try {
|
|
50
|
+
if (view.mPlayerView.getPlayer() != null) {
|
|
51
|
+
view.mPlayerView.getPlayer().setControls(controls);
|
|
52
|
+
}
|
|
53
|
+
} catch (Exception e) {
|
|
54
|
+
Log.w(TAG, "Error setting controls: " + e.getMessage());
|
|
55
|
+
}
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/**
|
|
@@ -58,8 +67,15 @@ public class RNJWPlayerViewManager extends SimpleViewManager<RNJWPlayerView> {
|
|
|
58
67
|
if (view == null || view.mPlayerView == null) {
|
|
59
68
|
return;
|
|
60
69
|
}
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
// Add null check for getPlayer() to prevent crashes
|
|
71
|
+
try {
|
|
72
|
+
if (view.mPlayerView.getPlayer() != null) {
|
|
73
|
+
view.mPlayerView.getPlayer().stop();
|
|
74
|
+
}
|
|
75
|
+
view.setConfig(config);
|
|
76
|
+
} catch (Exception e) {
|
|
77
|
+
Log.w(TAG, "Error recreating player: " + e.getMessage());
|
|
78
|
+
}
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
public Map getExportedCustomBubblingEventTypeConstants() {
|
package/badges/version.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="version: 1.3.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="version: 1.3.3"><title>version: 1.3.3</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="51" height="20" fill="#555"/><rect x="51" width="39" height="20" fill="#007ec6"/><rect width="90" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="265" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="410">version</text><text x="265" y="140" transform="scale(.1)" fill="#fff" textLength="410">version</text><text aria-hidden="true" x="695" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="290">1.3.3</text><text x="695" y="140" transform="scale(.1)" fill="#fff" textLength="290">1.3.3</text></g></svg>
|
|
@@ -1255,10 +1255,16 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
|
|
|
1255
1255
|
}
|
|
1256
1256
|
|
|
1257
1257
|
func dismissPlayerViewController() {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1258
|
+
guard playerViewController != nil else { return }
|
|
1259
|
+
|
|
1260
|
+
// Ensure UI operations happen on main thread to prevent crashes
|
|
1261
|
+
// when deinit is called from a background thread during unmount
|
|
1262
|
+
let cleanup = { [self] in
|
|
1263
|
+
guard let pvc = self.playerViewController else { return }
|
|
1264
|
+
|
|
1265
|
+
pvc.player.pause() // hack for stop not always stopping on unmount
|
|
1266
|
+
pvc.player.stop()
|
|
1267
|
+
pvc.enableLockScreenControls = false
|
|
1262
1268
|
|
|
1263
1269
|
// hack for stop not always stopping on unmount
|
|
1264
1270
|
let configBuilder:JWPlayerConfigurationBuilder! = JWPlayerConfigurationBuilder()
|
|
@@ -1266,19 +1272,24 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
|
|
|
1266
1272
|
|
|
1267
1273
|
do {
|
|
1268
1274
|
let configuration: JWPlayerConfiguration = try configBuilder.build()
|
|
1269
|
-
|
|
1275
|
+
pvc.player.configurePlayer(with: configuration)
|
|
1270
1276
|
} catch {
|
|
1271
1277
|
print(error)
|
|
1272
1278
|
}
|
|
1273
1279
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
playerViewController
|
|
1281
|
-
|
|
1280
|
+
pvc.parentView = nil
|
|
1281
|
+
pvc.setVisibility(.hidden, for:[.pictureInPictureButton])
|
|
1282
|
+
pvc.view.removeFromSuperview()
|
|
1283
|
+
pvc.removeFromParent()
|
|
1284
|
+
pvc.willMove(toParent: nil)
|
|
1285
|
+
pvc.removeDelegates()
|
|
1286
|
+
self.playerViewController = nil
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if Thread.isMainThread {
|
|
1290
|
+
cleanup()
|
|
1291
|
+
} else {
|
|
1292
|
+
DispatchQueue.main.sync(execute: cleanup)
|
|
1282
1293
|
}
|
|
1283
1294
|
}
|
|
1284
1295
|
|
|
@@ -1330,10 +1341,21 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
|
|
|
1330
1341
|
}
|
|
1331
1342
|
|
|
1332
1343
|
func removePlayerView() {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1344
|
+
guard playerView != nil else { return }
|
|
1345
|
+
|
|
1346
|
+
// Ensure UI operations happen on main thread to prevent crashes
|
|
1347
|
+
// when deinit is called from a background thread during unmount
|
|
1348
|
+
let cleanup = { [self] in
|
|
1349
|
+
guard let pv = self.playerView else { return }
|
|
1350
|
+
pv.player.stop()
|
|
1351
|
+
pv.removeFromSuperview()
|
|
1352
|
+
self.playerView = nil
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if Thread.isMainThread {
|
|
1356
|
+
cleanup()
|
|
1357
|
+
} else {
|
|
1358
|
+
DispatchQueue.main.sync(execute: cleanup)
|
|
1337
1359
|
}
|
|
1338
1360
|
}
|
|
1339
1361
|
|