@jwplayer/jwplayer-react-native 1.1.3 → 1.2.0

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.
@@ -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.23.1'
15
+ s.dependency 'JWPlayerKit', '4.23.2'
16
16
  s.dependency 'React-Core'
17
17
  s.static_framework = true
18
18
  s.info_plist = {
@@ -73,7 +73,7 @@ allprojects {
73
73
  }
74
74
  }
75
75
 
76
- def jwPlayerVersion = "4.21.0"
76
+ def jwPlayerVersion = "4.21.1"
77
77
  def exoplayerVersion = "2.18.7" // Deprecated. Use Media3 when targeting JW SDK > 4.16.0
78
78
  def media3ExoVersion = "1.4.1"
79
79
 
@@ -1,5 +1,7 @@
1
1
  package com.jwplayer.rnjwplayer;
2
2
 
3
+ import android.util.Log;
4
+
3
5
  import android.os.Handler;
4
6
  import android.os.Looper;
5
7
 
@@ -430,6 +432,16 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
430
432
  });
431
433
  }
432
434
 
435
+ @ReactMethod
436
+ /**
437
+ * Stub method for recreatePlayerWithConfig - this method is iOS only
438
+ * On Android, create a new player instance with the new configuration instead
439
+ */
440
+ public void recreatePlayerWithConfig(final int reactTag, final ReadableMap config) {
441
+ // No-op on Android - this method is iOS only
442
+ Log.w("RNJWPlayer", "recreatePlayerWithConfig is not supported on Android. Create a new player instance with the new configuration instead.");
443
+ }
444
+
433
445
  private int stateToInt(PlayerState playerState) {
434
446
  switch (playerState) {
435
447
  case IDLE:
@@ -1,6 +1,7 @@
1
1
  package com.jwplayer.rnjwplayer;
2
2
 
3
3
 
4
+ import android.annotation.SuppressLint;
4
5
  import android.app.Activity;
5
6
  import android.app.ActivityManager;
6
7
  import android.content.BroadcastReceiver;
@@ -372,9 +373,9 @@ public class RNJWPlayerView extends RelativeLayout implements
372
373
 
373
374
  // If we are casting we need to break the cast session as there is no simple
374
375
  // way to reconnect to an existing session if the player that created it is dead
375
-
376
+
376
377
  // If this doesn't match your use case, using a single player object and load content
377
- // into it rather than creating a new player for every piece of content.
378
+ // into it rather than creating a new player for every piece of content.
378
379
  mPlayer.stop();
379
380
 
380
381
  // send signal to JW SDK player is destroyed
@@ -537,7 +538,7 @@ public class RNJWPlayerView extends RelativeLayout implements
537
538
  }
538
539
  }
539
540
 
540
- public void resolveNextPlaylistItem(ReadableMap playlistItem) {
541
+ public void resolveNextPlaylistItem(ReadableMap playlistItem) {
541
542
  if (itemUpdatePromise == null) {
542
543
  return;
543
544
  }
@@ -741,7 +742,7 @@ public class RNJWPlayerView extends RelativeLayout implements
741
742
  public void run() {
742
743
  // View may not have been removed properly (especially if returning from PiP)
743
744
  mPlayerViewContainer.removeView(mPlayerView);
744
-
745
+
745
746
  mPlayerViewContainer.addView(mPlayerView, new ViewGroup.LayoutParams(
746
747
  ViewGroup.LayoutParams.MATCH_PARENT,
747
748
  ViewGroup.LayoutParams.MATCH_PARENT));
@@ -856,6 +857,7 @@ public class RNJWPlayerView extends RelativeLayout implements
856
857
  }
857
858
  }
858
859
 
860
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
859
861
  private void registerReceiver() {
860
862
  mReceiver = new PipHandlerReceiver();
861
863
  IntentFilter intentFilter = new IntentFilter("onPictureInPictureModeChanged");
@@ -974,130 +976,132 @@ public class RNJWPlayerView extends RelativeLayout implements
974
976
  .deepEquals(new ReadableArray[]{mPlaylistProp}, new ReadableArray[]{prop.getArray("playlist")});
975
977
  }
976
978
 
977
- private void setupPlayer(ReadableMap prop) {
978
- // Legacy
979
- PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
980
-
981
- JSONObject obj;
982
- PlayerConfig jwConfig = null;
983
- Boolean forceLegacy = prop.hasKey("forceLegacyConfig") ? prop.getBoolean("forceLegacyConfig") : false;
984
- Boolean playlistItemCallbackEnabled = prop.hasKey("playlistItemCallbackEnabled") ? prop.getBoolean("playlistItemCallbackEnabled") : false;
985
- Boolean isJwConfig = false;
986
- if (!forceLegacy) {
987
- try {
988
- obj = MapUtil.toJSONObject(prop);
989
- jwConfig = JsonHelper.parseConfigJson(obj);
990
- isJwConfig = true;
991
- } catch (Exception ex) {
992
- Log.e("RNJWPlayerView", ex.toString());
993
- isJwConfig = false; // not a valid jw config. Try to setup in legacy
994
- }
995
- }
996
-
997
- if (!isJwConfig) {
998
- // Legacy
999
- if (playlistNotTheSame(prop)) {
1000
- List<PlaylistItem> playlist = new ArrayList<>();
1001
- mPlaylistProp = prop.getArray("playlist");
1002
- if (mPlaylistProp != null && mPlaylistProp.size() > 0) {
1003
-
1004
- int j = 0;
1005
- while (mPlaylistProp.size() > j) {
1006
- ReadableMap playlistItem = mPlaylistProp.getMap(j);
1007
-
1008
- PlaylistItem newPlayListItem = Util.getPlaylistItem((playlistItem));
1009
- playlist.add(newPlayListItem);
1010
- j++;
1011
- }
979
+ private void configurePlaylist(PlayerConfig.Builder configBuilder, ReadableMap prop) {
980
+ if (playlistNotTheSame(prop)) {
981
+ List<PlaylistItem> playlist = new ArrayList<>();
982
+ mPlaylistProp = prop.getArray("playlist");
983
+ if (mPlaylistProp != null && mPlaylistProp.size() > 0) {
984
+ int j = 0;
985
+ while (mPlaylistProp.size() > j) {
986
+ ReadableMap playlistItem = mPlaylistProp.getMap(j);
987
+ PlaylistItem newPlayListItem = Util.getPlaylistItem((playlistItem));
988
+ playlist.add(newPlayListItem);
989
+ j++;
1012
990
  }
1013
-
1014
- configBuilder.playlist(playlist);
1015
991
  }
992
+ configBuilder.playlist(playlist);
993
+ }
994
+ }
1016
995
 
1017
- // Legacy
1018
- if (prop.hasKey("autostart")) {
1019
- boolean autostart = prop.getBoolean("autostart");
1020
- configBuilder.autostart(autostart);
1021
- }
996
+ private void configureBasicSettings(PlayerConfig.Builder configBuilder, ReadableMap prop) {
997
+ if (prop.hasKey("autostart")) {
998
+ boolean autostart = prop.getBoolean("autostart");
999
+ configBuilder.autostart(autostart);
1000
+ }
1022
1001
 
1023
- // Legacy
1024
- if (prop.hasKey("nextUpStyle")) {
1025
- ReadableMap nextUpStyle = prop.getMap("nextUpStyle");
1026
- if (nextUpStyle != null && nextUpStyle.hasKey("offsetSeconds")
1027
- && nextUpStyle.hasKey("offsetPercentage")) {
1028
- int offsetSeconds = prop.getInt("offsetSeconds");
1029
- int offsetPercentage = prop.getInt("offsetPercentage");
1030
- configBuilder.nextUpOffset(offsetSeconds).nextUpOffsetPercentage(offsetPercentage);
1031
- }
1002
+ if (prop.hasKey("nextUpStyle")) {
1003
+ ReadableMap nextUpStyle = prop.getMap("nextUpStyle");
1004
+ if (nextUpStyle != null && nextUpStyle.hasKey("offsetSeconds")
1005
+ && nextUpStyle.hasKey("offsetPercentage")) {
1006
+ int offsetSeconds = prop.getInt("offsetSeconds");
1007
+ int offsetPercentage = prop.getInt("offsetPercentage");
1008
+ configBuilder.nextUpOffset(offsetSeconds).nextUpOffsetPercentage(offsetPercentage);
1032
1009
  }
1010
+ }
1033
1011
 
1034
- // Legacy
1035
- if (prop.hasKey("repeat")) {
1036
- boolean repeat = prop.getBoolean("repeat");
1037
- configBuilder.repeat(repeat);
1038
- }
1012
+ if (prop.hasKey("repeat")) {
1013
+ boolean repeat = prop.getBoolean("repeat");
1014
+ configBuilder.repeat(repeat);
1015
+ }
1039
1016
 
1040
- // Legacy
1041
- if (prop.hasKey("styling")) {
1042
- ReadableMap styling = prop.getMap("styling");
1043
- if (styling != null) {
1044
- if (styling.hasKey("displayDescription")) {
1045
- boolean displayDescription = styling.getBoolean("displayDescription");
1046
- configBuilder.displayDescription(displayDescription);
1047
- }
1017
+ if (prop.hasKey("stretching")) {
1018
+ String stretching = prop.getString("stretching");
1019
+ configBuilder.stretching(stretching);
1020
+ }
1021
+ }
1048
1022
 
1049
- if (styling.hasKey("displayTitle")) {
1050
- boolean displayTitle = styling.getBoolean("displayTitle");
1051
- configBuilder.displayTitle(displayTitle);
1052
- }
1023
+ private void configureStyling(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1024
+ if (prop.hasKey("styling")) {
1025
+ ReadableMap styling = prop.getMap("styling");
1026
+ if (styling != null) {
1027
+ if (styling.hasKey("displayDescription")) {
1028
+ boolean displayDescription = styling.getBoolean("displayDescription");
1029
+ configBuilder.displayDescription(displayDescription);
1030
+ }
1053
1031
 
1054
- if (styling.hasKey("colors")) {
1055
- mColors = styling.getMap("colors");
1056
- }
1032
+ if (styling.hasKey("displayTitle")) {
1033
+ boolean displayTitle = styling.getBoolean("displayTitle");
1034
+ configBuilder.displayTitle(displayTitle);
1057
1035
  }
1058
- }
1059
1036
 
1060
- // Legacy
1061
- if (prop.hasKey("advertising")) {
1062
- ReadableMap ads = prop.getMap("advertising");
1063
- AdvertisingConfig advertisingConfig = RNJWPlayerAds.getAdvertisingConfig(ads);
1064
- if (advertisingConfig != null) {
1065
- configBuilder.advertisingConfig(advertisingConfig);
1037
+ if (styling.hasKey("colors")) {
1038
+ mColors = styling.getMap("colors");
1066
1039
  }
1067
1040
  }
1041
+ }
1042
+ }
1068
1043
 
1069
- // Legacy
1070
- if (prop.hasKey("stretching")) {
1071
- String stretching = prop.getString("stretching");
1072
- configBuilder.stretching(stretching);
1044
+ private void configureAdvertising(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1045
+ if (prop.hasKey("advertising")) {
1046
+ ReadableMap ads = prop.getMap("advertising");
1047
+ AdvertisingConfig advertisingConfig = RNJWPlayerAds.getAdvertisingConfig(ads);
1048
+ if (advertisingConfig != null) {
1049
+ configBuilder.advertisingConfig(advertisingConfig);
1073
1050
  }
1051
+ }
1052
+ }
1074
1053
 
1075
- // Legacy
1076
- // this isn't the ideal way to do controls...
1077
- // Better to just expose the `.setControls` method
1078
- if (prop.hasKey("controls")) {
1079
- boolean controls = prop.getBoolean("controls");
1080
- if (!controls) {
1081
- UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build();
1082
- configBuilder.uiConfig(uiConfig);
1083
- }
1054
+ private void configureUI(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1055
+ if (prop.hasKey("controls")) {
1056
+ boolean controls = prop.getBoolean("controls");
1057
+ if (!controls) {
1058
+ UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build();
1059
+ configBuilder.uiConfig(uiConfig);
1084
1060
  }
1061
+ }
1085
1062
 
1086
- // Legacy
1087
- if (prop.hasKey("hideUIGroups")) {
1088
- ReadableArray uiGroupsArray = prop.getArray("hideUIGroups");
1089
- UiConfig.Builder hideConfigBuilder = new UiConfig.Builder().displayAllControls();
1090
- for (int i = 0; i < uiGroupsArray.size(); i++) {
1091
- if (uiGroupsArray.getType(i) == ReadableType.String) {
1092
- UiGroup uiGroup = GROUP_TYPES.get(uiGroupsArray.getString(i));
1093
- if (uiGroup != null) {
1094
- hideConfigBuilder.hide(uiGroup);
1095
- }
1063
+ if (prop.hasKey("hideUIGroups")) {
1064
+ ReadableArray uiGroupsArray = prop.getArray("hideUIGroups");
1065
+ UiConfig.Builder hideConfigBuilder = new UiConfig.Builder().displayAllControls();
1066
+ for (int i = 0; i < uiGroupsArray.size(); i++) {
1067
+ if (uiGroupsArray.getType(i) == ReadableType.String) {
1068
+ UiGroup uiGroup = GROUP_TYPES.get(uiGroupsArray.getString(i));
1069
+ if (uiGroup != null) {
1070
+ hideConfigBuilder.hide(uiGroup);
1096
1071
  }
1097
1072
  }
1098
- UiConfig hideJwControlbarUiConfig = hideConfigBuilder.build();
1099
- configBuilder.uiConfig(hideJwControlbarUiConfig);
1100
1073
  }
1074
+ UiConfig hideJwControlbarUiConfig = hideConfigBuilder.build();
1075
+ configBuilder.uiConfig(hideJwControlbarUiConfig);
1076
+ }
1077
+ }
1078
+
1079
+ private void setupPlayer(ReadableMap prop) {
1080
+ PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
1081
+
1082
+ JSONObject obj;
1083
+ PlayerConfig jwConfig = null;
1084
+ Boolean forceLegacy = prop.hasKey("forceLegacyConfig") ? prop.getBoolean("forceLegacyConfig") : false;
1085
+ Boolean playlistItemCallbackEnabled = prop.hasKey("playlistItemCallbackEnabled") ? prop.getBoolean("playlistItemCallbackEnabled") : false;
1086
+ Boolean isJwConfig = false;
1087
+
1088
+ if (!forceLegacy) {
1089
+ try {
1090
+ obj = MapUtil.toJSONObject(prop);
1091
+ jwConfig = JsonHelper.parseConfigJson(obj);
1092
+ isJwConfig = true;
1093
+ } catch (Exception ex) {
1094
+ Log.e("RNJWPlayerView", ex.toString());
1095
+ isJwConfig = false; // not a valid jw config. Try to setup in legacy
1096
+ }
1097
+ }
1098
+
1099
+ if (!isJwConfig) {
1100
+ configurePlaylist(configBuilder, prop);
1101
+ configureBasicSettings(configBuilder, prop);
1102
+ configureStyling(configBuilder, prop);
1103
+ configureAdvertising(configBuilder, prop);
1104
+ configureUI(configBuilder, prop);
1101
1105
  }
1102
1106
 
1103
1107
  Context simpleContext = getNonBuggyContext(getReactContext(), getAppContext());
@@ -46,6 +46,22 @@ public class RNJWPlayerViewManager extends SimpleViewManager<RNJWPlayerView> {
46
46
  view.mPlayerView.getPlayer().setControls(controls);
47
47
  }
48
48
 
49
+ /**
50
+ * Recreates the player with a new configuration, handling cleanup and PiP state.
51
+ * This method ensures proper cleanup and state restoration during configuration changes.
52
+ *
53
+ * @param view The RNJWPlayerView instance
54
+ * @param config The new configuration to apply
55
+ */
56
+ @ReactProp(name = "recreatePlayerWithConfig")
57
+ public void recreatePlayerWithConfig(RNJWPlayerView view, ReadableMap config) {
58
+ if (view == null || view.mPlayerView == null) {
59
+ return;
60
+ }
61
+ view.mPlayerView.getPlayer().stop();
62
+ view.setConfig(config);
63
+ }
64
+
49
65
  public Map getExportedCustomBubblingEventTypeConstants() {
50
66
  return MapBuilder.builder()
51
67
  .put(
@@ -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.1.3"><title>version: 1.1.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.1.3</text><text x="695" y="140" transform="scale(.1)" fill="#fff" textLength="290">1.1.3</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.2.0"><title>version: 1.2.0</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.2.0</text><text x="695" y="140" transform="scale(.1)" fill="#fff" textLength="290">1.2.0</text></g></svg>
package/index.d.ts CHANGED
@@ -685,6 +685,57 @@ declare module "@jwplayer/jwplayer-react-native" {
685
685
  setCurrentCaptions(index: number): void;
686
686
  getCurrentCaptions(): Promise<number | null>;
687
687
  setVisibility(visibility: boolean, controls: JWControlType[]): void;
688
+ /**
689
+ * Recreates the player with a new configuration, handling cleanup and PiP state.
690
+ *
691
+ * NOTE: This method is only available on iOS. On Android, create a new player instance
692
+ * with the new configuration instead.
693
+ *
694
+ * IMPORTANT: This method should only be called after the player has been properly
695
+ * initialized and is ready (i.e., after onPlayerReady has fired). Calling this
696
+ * method before the player is ready may lead to undefined behavior.
697
+ *
698
+ * This method performs a complete player recreation by:
699
+ * 1. Safely handling PiP state if active (waits for PiP to close)
700
+ * 2. Performing complete cleanup of the current player instance
701
+ * 3. Creating a new player instance with the provided config
702
+ *
703
+ * Use this method when you need to:
704
+ * - Switch between different DRM configurations
705
+ * - Handle content changes during PiP mode
706
+ * - Force a complete player recreation
707
+ *
708
+ * Do NOT use this method:
709
+ * - Before the player is ready (wait for onPlayerReady)
710
+ * - For simple playlist updates (use loadPlaylist instead)
711
+ * - When the player is not properly initialized
712
+ * - On Android (create a new player instance instead)
713
+ *
714
+ * @example
715
+ * ```typescript
716
+ * // Wait for player to be ready
717
+ * onPlayerReady={() => {
718
+ * // Now safe to use recreatePlayerWithConfig (iOS only)
719
+ * if (Platform.OS === 'ios') {
720
+ * playerRef.current?.recreatePlayerWithConfig({
721
+ * ...config,
722
+ * playlist: newPlaylist
723
+ * });
724
+ * } else {
725
+ * // On Android, create a new player instance
726
+ * setPlayerConfig({
727
+ * ...config,
728
+ * playlist: newPlaylist
729
+ * });
730
+ * }
731
+ * }}
732
+ * ```
733
+ *
734
+ * @platform ios
735
+ * @param config The new configuration to apply to the recreated player
736
+ * @throws May throw if called before player is ready or with invalid config
737
+ */
738
+ recreatePlayerWithConfig(config: Config | JwConfig): void;
688
739
  /**
689
740
  * Only called inside `onBeforeNextPlaylistItem` callback, and once per callback
690
741
  * @param playlistItem `PlaylistItem` or `JwPlaylistItem`
package/index.js CHANGED
@@ -744,6 +744,40 @@ export default class JWPlayer extends Component {
744
744
  }
745
745
  }
746
746
 
747
+ /**
748
+ * Recreates the player with a new configuration, handling cleanup and PiP state.
749
+ *
750
+ * IMPORTANT: This method should only be called after the player has been properly
751
+ * initialized and is ready (i.e., after onPlayerReady has fired). Calling this
752
+ * method before the player is ready may lead to undefined behavior.
753
+ *
754
+ * This method:
755
+ * 1. Safely handles PiP state if active
756
+ * 2. Performs complete cleanup of the current player instance
757
+ * 3. Creates a new player instance with the provided config
758
+ *
759
+ * Use this method when you need to:
760
+ * - Switch between different DRM configurations
761
+ * - Handle content changes during PiP mode
762
+ * - Force a complete player recreation
763
+ *
764
+ * @param {Config | JwConfig} config The new configuration to apply to the recreated player
765
+ * @throws {Error} May throw if called before player is ready or with invalid config
766
+ */
767
+ recreatePlayerWithConfig(config) {
768
+ if (!config) {
769
+ console.warn('JWPlayer: Attempting to recreate player with null/undefined config');
770
+ return;
771
+ }
772
+
773
+ if (RNJWPlayerManager) {
774
+ RNJWPlayerManager.recreatePlayerWithConfig(
775
+ this.getRNJWPlayerBridgeHandle(),
776
+ config
777
+ );
778
+ }
779
+ }
780
+
747
781
  getRNJWPlayerBridgeHandle() {
748
782
  return findNodeHandle(this[this.ref_key]);
749
783
  }
@@ -307,6 +307,98 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
307
307
  }
308
308
  }
309
309
 
310
+ private var pendingPlayerConfig: [String: Any]?
311
+ private var playerConfigTimeout: Timer?
312
+ private let maxPendingTime: TimeInterval = 5.0 // Maximum time to wait for PiP to close
313
+
314
+ @objc func recreatePlayerWithConfig(_ config: [String: Any]) {
315
+ // Cancel any existing pending configuration
316
+ if pendingPlayerConfig != nil {
317
+ print("Warning: Overriding pending content switch")
318
+ playerConfigTimeout?.invalidate()
319
+ pendingPlayerConfig = nil
320
+ }
321
+
322
+ // Validate config
323
+ guard !config.isEmpty else {
324
+ print("Error: Empty config provided to recreatePlayerWithConfig")
325
+ return
326
+ }
327
+
328
+ // 1. Handle PiP state
329
+ var isPipActive = false
330
+ var pipController: AVPictureInPictureController?
331
+
332
+ if let playerView = playerView {
333
+ pipController = playerView.pictureInPictureController
334
+ isPipActive = pipController?.isPictureInPictureActive ?? false
335
+ } else if let playerViewController = playerViewController {
336
+ pipController = playerViewController.playerView.pictureInPictureController
337
+ isPipActive = pipController?.isPictureInPictureActive ?? false
338
+ }
339
+
340
+ // 2. If in PiP, store the config and exit PiP
341
+ if isPipActive {
342
+ guard let pipController = pipController else {
343
+ print("Warning: PiP appears active but controller is nil, proceeding with direct switch")
344
+ completePlayerReconfiguration(config: config)
345
+ return
346
+ }
347
+
348
+ pendingPlayerConfig = config
349
+
350
+ // Set a timeout to prevent infinite waiting
351
+ playerConfigTimeout = Timer.scheduledTimer(withTimeInterval: maxPendingTime, repeats: false) { [weak self] _ in
352
+ guard let self = self else { return }
353
+ print("Warning: PiP close timeout reached, forcing content switch")
354
+ if let pendingConfig = self.pendingPlayerConfig {
355
+ self.pendingPlayerConfig = nil
356
+ self.completePlayerReconfiguration(config: pendingConfig)
357
+ }
358
+ }
359
+
360
+ // Attempt to stop PiP
361
+ pipController.stopPictureInPicture()
362
+
363
+ } else {
364
+ completePlayerReconfiguration(config: config)
365
+ }
366
+ }
367
+
368
+ private func completePlayerReconfiguration(config: [String: Any]) {
369
+ // Clear any pending timeout
370
+ playerConfigTimeout?.invalidate()
371
+ playerConfigTimeout = nil
372
+
373
+ // Ensure we're on the main thread
374
+ if !Thread.isMainThread {
375
+ DispatchQueue.main.async { [weak self] in
376
+ self?.completePlayerReconfiguration(config: config)
377
+ }
378
+ return
379
+ }
380
+
381
+ // 1. Stop current playback safely
382
+ if let playerView = playerView {
383
+ let state = playerView.player.getState()
384
+ if state == .playing || state == .buffering {
385
+ playerView.player.stop()
386
+ }
387
+ } else if let playerViewController = playerViewController {
388
+ let state = playerViewController.player.getState()
389
+ if state == .playing || state == .buffering {
390
+ playerViewController.player.stop()
391
+ }
392
+ }
393
+
394
+ // 2. Reset player state
395
+ dismissPlayerViewController()
396
+ removePlayerView()
397
+
398
+ // 3. Set new config
399
+ setNewConfig(config: config)
400
+ }
401
+
310
402
  func setNewConfig(config: [String : Any]) {
311
403
  let forceLegacyConfig = config["forceLegacyConfig"] as? Bool?
312
404
  let playlistItemCallback = config["playlistItemCallbackEnabled"] as? Bool?
@@ -1230,7 +1322,13 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
1230
1322
  }
1231
1323
 
1232
1324
  func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
1233
-
1325
+ // Handle any pending content switch
1326
+ if let config = pendingPlayerConfig {
1327
+ pendingPlayerConfig = nil
1328
+ DispatchQueue.main.async { [weak self] in
1329
+ self?.completePlayerReconfiguration(config: config)
1330
+ }
1331
+ }
1234
1332
  }
1235
1333
 
1236
1334
  func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
@@ -249,26 +249,38 @@ class RNJWPlayerViewController : JWPlayerViewController, JWPlayerViewControllerF
249
249
 
250
250
  override func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
251
251
  super.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
252
+ // Forward to parent view to handle pending config changes
253
+ parentView?.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
252
254
  }
253
255
 
254
256
  override func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
255
257
  super.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
258
+ // Forward to parent view for logging/tracking
259
+ parentView?.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
256
260
  }
257
261
 
258
262
  override func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
259
263
  super.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
264
+ // Forward to parent view for logging/tracking
265
+ parentView?.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
260
266
  }
261
267
 
262
268
  override func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
263
269
  super.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
270
+ // Forward to parent view for error handling
271
+ parentView?.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
264
272
  }
265
273
 
266
274
  override func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
267
275
  super.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
276
+ // Forward to parent view for logging/tracking
277
+ parentView?.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
268
278
  }
269
279
 
270
280
  override func pictureInPictureController(_ pictureInPictureController:AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler:@escaping (Bool) -> Void) {
271
281
  super.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
282
+ // Forward to parent view
283
+ parentView?.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
272
284
  }
273
285
 
274
286
  // MARK: - JWPlayer State Delegate
@@ -136,6 +136,8 @@ RCT_EXTERN_METHOD(reset)
136
136
 
137
137
  RCT_EXTERN_METHOD(loadPlaylist: (nonnull NSNumber *)reactTag: (nonnull NSArray *)playlist)
138
138
 
139
+ RCT_EXTERN_METHOD(recreatePlayerWithConfig: (nonnull NSNumber *)reactTag: (nonnull NSDictionary *)config)
140
+
139
141
  RCT_EXTERN_METHOD(loadPlaylistWithUrl: (nonnull NSNumber *)reactTag: (nonnull NSString *)playlist)
140
142
 
141
143
  RCT_EXTERN_METHOD(setFullscreen: (nonnull NSNumber *)reactTag: (BOOL)fullscreen)
@@ -552,6 +552,19 @@ class RNJWPlayerViewManager: RCTViewManager {
552
552
  }
553
553
  }
554
554
 
555
+ @objc func recreatePlayerWithConfig(_ reactTag: NSNumber, _ config: NSDictionary) {
556
+ DispatchQueue.main.async {
557
+ guard let view = self.bridge?.uiManager.view(
558
+ forReactTag: reactTag
559
+ ) as? RNJWPlayerView else {
560
+ print("Invalid view returned from registry, expecting RNJWPlayerView")
561
+ return
562
+ }
563
+
564
+ view.recreatePlayerWithConfig(config as? [String: Any] ?? [:])
565
+ }
566
+ }
567
+
555
568
  @objc func loadPlaylistWithUrl(_ reactTag: NSNumber, _ playlistString: String) {
556
569
  DispatchQueue.main.async {
557
570
  guard let view = self.getPlayerView(reactTag: reactTag) else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwplayer/jwplayer-react-native",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
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",