@jwplayer/jwplayer-react-native 1.3.2 → 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 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
@@ -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.0'
15
+ s.dependency 'JWPlayerKit', '4.25.1'
16
16
  s.dependency 'React-Core'
17
17
  s.static_framework = true
18
18
  s.info_plist = {
@@ -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
- view.mPlayerView.getPlayer().setControls(controls);
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
- view.mPlayerView.getPlayer().stop();
62
- view.setConfig(config);
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() {
@@ -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.2"><title>version: 1.3.2</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.2</text><text x="695" y="140" transform="scale(.1)" fill="#fff" textLength="290">1.3.2</text></g></svg>
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
- if (playerViewController != nil) {
1259
- playerViewController.player.pause() // hack for stop not always stopping on unmount
1260
- playerViewController.player.stop()
1261
- playerViewController.enableLockScreenControls = false
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
- playerViewController.player.configurePlayer(with: configuration)
1275
+ pvc.player.configurePlayer(with: configuration)
1270
1276
  } catch {
1271
1277
  print(error)
1272
1278
  }
1273
1279
 
1274
-
1275
- playerViewController.parentView = nil
1276
- playerViewController.setVisibility(.hidden, for:[.pictureInPictureButton])
1277
- playerViewController.view.removeFromSuperview()
1278
- playerViewController.removeFromParent()
1279
- playerViewController.willMove(toParent: nil)
1280
- playerViewController.removeDelegates()
1281
- playerViewController = nil
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
- if (playerView != nil) {
1334
- playerView.player.stop()
1335
- playerView.removeFromSuperview()
1336
- playerView = nil
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwplayer/jwplayer-react-native",
3
- "version": "1.3.2",
3
+ "version": "1.3.3",
4
4
  "description": "React-native Android/iOS plugin for JWPlayer SDK (https://www.jwplayer.com/)",
5
5
  "main": "index.js",
6
6
  "types": "./index.d.ts",