@jwplayer/jwplayer-react-native 1.1.2 → 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.
package/README.md CHANGED
@@ -275,11 +275,44 @@ Follow these steps to enable background audio sessions:
275
275
  1. Set `backgroundAudioEnabled` to `true` in the `config`.
276
276
  2. Ensure that background audio is set for [Android](https://docs.jwplayer.com/players/docs/android-enable-background-audio) or [iOS](https://docs.jwplayer.com/players/docs/ios-player-backgrounding-reference#configure-audio-playback).
277
277
 
278
+ #### Android Opt-Out Information
279
+ Suppose you are **not** using the background audio service on Android. In that case, you will need to modify the [manifest](https://github.com/jwplayer/jwplayer-react-native/blob/master/android/src/main/AndroidManifest.xml) from our library (either in the node_modules, or in your fork before compilation) so as not to register a service that you will never use. Google can and will reject your Play Store submission if this is skipped. See the modified manifest below, which shows what to remove (commented-out permissions and services). Additionally, once this is done, you can never set `backgroundAudioEnabled` to true on Android, or you will run into a fatal crash.
280
+
281
+ ```xml
282
+
283
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
284
+ package="com.jwplayer.rnjwplayer">
285
+ <uses-permission android:name="android.permission.INTERNET" />
286
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
287
+ <!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> -->
288
+ <uses-permission android:name="android.permission.BLUETOOTH"/>
289
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
290
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
291
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
292
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
293
+ <!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> -->
294
+ <!-- READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READ_MEDIA_AUDIO.-->
295
+
296
+ <application>
297
+ <!-- <service
298
+ android:name="com.jwplayer.pub.api.background.MediaService"
299
+ android:foregroundServiceType="mediaPlayback"
300
+ android:exported="false">
301
+ <intent-filter>
302
+ <action android:name="android.intent.action.MEDIA_BUTTON" />
303
+ </intent-filter>
304
+ </service> -->
305
+ </application>
306
+
307
+ </manifest>
308
+
309
+ ```
310
+
278
311
  <br /><br />
279
312
 
280
313
  ### Casting
281
314
 
282
- [Android](#android-casting) | [iOS](#ios-casting)
315
+ [Android](#android-casting) | [iOS](#ios-casting) | [DRM Casting](#drm-casting)
283
316
 
284
317
  JWP enables casting by default with a casting button.
285
318
 
@@ -342,6 +375,13 @@ typedef NS_ENUM(NSUInteger, GCKCastState) {
342
375
  };
343
376
  ```
344
377
 
378
+ #### DRM Casting
379
+ Casting your DRM protected content requires some additional configuration and most likely a custom Chromecast Receiver.
380
+
381
+ See our Android and iOS documentation about creating ([iOS](https://docs.jwplayer.com/players/docs/ios-create-a-custom-receiver) / [Android](https://docs.jwplayer.com/players/docs/android-create-a-custom-receiver)) and sending data ([iOS](https://docs.jwplayer.com/players/docs/ios-enable-casting-to-chromecast-devices#send-custom-data-to-a-custom-receiver) / [Android](https://docs.jwplayer.com/players/docs/android-enable-casting-to-chromecast-devices#send-custom-data-to-a-custom-receiver)) for more information.
382
+
383
+ To send custom data to your receiver, use the `userInfo` prop and avoid using the reserved `sources` key, as the JWP SDK will append the DRM source information here for you to parse in your DRM enabled receiver.
384
+
345
385
  <br /><br />
346
386
 
347
387
  ### DRM
@@ -9,10 +9,10 @@ Pod::Spec.new do |s|
9
9
  s.license = package['license']
10
10
  s.authors = package['author']
11
11
  s.homepage = package['homepage']
12
- s.platform = :ios, "14.0"
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.22.0'
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
 
@@ -13,17 +15,16 @@ import com.facebook.react.bridge.UIManager;
13
15
  import com.facebook.react.bridge.ReadableMap;
14
16
  import com.facebook.react.bridge.WritableArray;
15
17
  import com.facebook.react.bridge.WritableMap;
16
- import com.facebook.react.uimanager.IllegalViewOperationException;
17
- import com.facebook.react.uimanager.NativeViewHierarchyManager;
18
- import com.facebook.react.uimanager.UIBlock;
19
18
  import com.facebook.react.uimanager.UIManagerHelper;
20
- import com.facebook.react.uimanager.UIManagerModule;
21
19
  import com.facebook.react.uimanager.common.UIManagerType;
22
20
  import com.jwplayer.pub.api.JWPlayer;
23
21
  import com.jwplayer.pub.api.PlayerState;
22
+ import com.jwplayer.pub.api.UiGroup;
24
23
  import com.jwplayer.pub.api.configuration.PlayerConfig;
24
+ import com.jwplayer.pub.api.configuration.UiConfig;
25
25
  import com.jwplayer.pub.api.media.adaptive.QualityLevel;
26
26
  import com.jwplayer.pub.api.media.audio.AudioTrack;
27
+
27
28
  import java.util.List;
28
29
 
29
30
  public class RNJWPlayerModule extends ReactContextBaseJavaModule {
@@ -58,6 +59,21 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
58
59
  }
59
60
  }
60
61
 
62
+ /**
63
+ * Creates a UiConfig that ensures PLAYER_CONTROLS_CONTAINER is always shown.
64
+ * If controls are not shown, the PLAYER_CONTROLS_CONTAINER UI Group is not displayed.
65
+ * This logic ensures that the PLAYER_CONTROLS_CONTAINER UI Group is displayed regardless if controls are shown or not.
66
+ * There is no way to recover controls if you do not show this UiGroup.
67
+ * But you are able to hide the controls still if it is shown.
68
+ */
69
+ private UiConfig createUiConfigWithControlsContainer(JWPlayer player, UiConfig originalUiConfig) {
70
+ if (!player.getControls()) {
71
+ return new UiConfig.Builder(originalUiConfig).show(UiGroup.PLAYER_CONTROLS_CONTAINER).build();
72
+ } else {
73
+ return originalUiConfig;
74
+ }
75
+ }
76
+
61
77
  @ReactMethod
62
78
  public void loadPlaylist(final int reactTag, final ReadableArray playlistItems) {
63
79
  new Handler(Looper.getMainLooper()).post(() -> {
@@ -66,6 +82,8 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
66
82
  JWPlayer player = playerView.mPlayerView.getPlayer();
67
83
 
68
84
  PlayerConfig oldConfig = player.getConfig();
85
+ boolean wasFullscreen = player.getFullscreen();
86
+ UiConfig uiConfig = createUiConfigWithControlsContainer(player, oldConfig.getUiConfig());
69
87
  PlayerConfig config = new PlayerConfig.Builder()
70
88
  .autostart(oldConfig.getAutostart())
71
89
  .nextUpOffset(oldConfig.getNextUpOffset())
@@ -75,7 +93,7 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
75
93
  .displayTitle(oldConfig.getDisplayTitle())
76
94
  .advertisingConfig(oldConfig.getAdvertisingConfig())
77
95
  .stretching(oldConfig.getStretching())
78
- .uiConfig(oldConfig.getUiConfig())
96
+ .uiConfig(uiConfig)
79
97
  .playlist(Util.createPlaylist(playlistItems))
80
98
  .allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
81
99
  .preload(oldConfig.getPreload())
@@ -85,6 +103,11 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
85
103
  .build();
86
104
 
87
105
  player.setup(config);
106
+ // if the player was fullscreen, set it to fullscreen again as the player is recreated
107
+ // The fullscreen view is still active but the internals don't know it is
108
+ if (wasFullscreen) {
109
+ player.setFullscreen(true, true);
110
+ }
88
111
  }
89
112
  });
90
113
  }
@@ -97,6 +120,8 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
97
120
  JWPlayer player = playerView.mPlayerView.getPlayer();
98
121
 
99
122
  PlayerConfig oldConfig = player.getConfig();
123
+ boolean wasFullscreen = player.getFullscreen();
124
+ UiConfig uiConfig = createUiConfigWithControlsContainer(player, oldConfig.getUiConfig());
100
125
  PlayerConfig config = new PlayerConfig.Builder()
101
126
  .autostart(oldConfig.getAutostart())
102
127
  .nextUpOffset(oldConfig.getNextUpOffset())
@@ -106,7 +131,7 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
106
131
  .displayTitle(oldConfig.getDisplayTitle())
107
132
  .advertisingConfig(oldConfig.getAdvertisingConfig())
108
133
  .stretching(oldConfig.getStretching())
109
- .uiConfig(oldConfig.getUiConfig())
134
+ .uiConfig(uiConfig)
110
135
  .playlistUrl(playlistUrl)
111
136
  .allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
112
137
  .preload(oldConfig.getPreload())
@@ -116,6 +141,11 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
116
141
  .build();
117
142
 
118
143
  player.setup(config);
144
+ // if the player was fullscreen, set it to fullscreen again as the player is recreated
145
+ // The fullscreen view is still active but the internals don't know it is
146
+ if (wasFullscreen) {
147
+ player.setFullscreen(true, true);
148
+ }
119
149
  }
120
150
  });
121
151
  }
@@ -402,6 +432,16 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
402
432
  });
403
433
  }
404
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
+
405
445
  private int stateToInt(PlayerState playerState) {
406
446
  switch (playerState) {
407
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");
@@ -879,11 +881,28 @@ public class RNJWPlayerView extends RelativeLayout implements
879
881
 
880
882
  }
881
883
 
884
+ /**
885
+ * Creates a UiConfig that ensures PLAYER_CONTROLS_CONTAINER is always shown.
886
+ * If controls are not shown, the PLAYER_CONTROLS_CONTAINER UI Group is not displayed.
887
+ * This logic ensures that the PLAYER_CONTROLS_CONTAINER UI Group is displayed regardless if controls are shown or not.
888
+ * There is no way to recover controls if you do not show this UiGroup.
889
+ * But you are able to hide the controls still if it is shown.
890
+ */
891
+ private UiConfig createUiConfigWithControlsContainer(JWPlayer player, UiConfig originalUiConfig) {
892
+ if (!player.getControls()) {
893
+ return new UiConfig.Builder(originalUiConfig).show(UiGroup.PLAYER_CONTROLS_CONTAINER).build();
894
+ } else {
895
+ return originalUiConfig;
896
+ }
897
+ }
898
+
882
899
  public void setConfig(ReadableMap prop) {
883
900
  if (mConfig == null || !mConfig.equals(prop)) {
884
901
  if (mConfig != null && isOnlyDiff(prop, "playlist") && mPlayer != null) { // still safe check, even with JW
885
902
  // JSON change
886
903
  PlayerConfig oldConfig = mPlayer.getConfig();
904
+ boolean wasFullscreen = mPlayer.getFullscreen();
905
+ UiConfig uiConfig = createUiConfigWithControlsContainer(mPlayer, oldConfig.getUiConfig());
887
906
  PlayerConfig config = new PlayerConfig.Builder()
888
907
  .autostart(oldConfig.getAutostart())
889
908
  .nextUpOffset(oldConfig.getNextUpOffset())
@@ -893,7 +912,7 @@ public class RNJWPlayerView extends RelativeLayout implements
893
912
  .displayTitle(oldConfig.getDisplayTitle())
894
913
  .advertisingConfig(oldConfig.getAdvertisingConfig())
895
914
  .stretching(oldConfig.getStretching())
896
- .uiConfig(oldConfig.getUiConfig())
915
+ .uiConfig(uiConfig)
897
916
  .playlist(Util.createPlaylist(mPlaylistProp))
898
917
  .allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
899
918
  .preload(oldConfig.getPreload())
@@ -903,6 +922,11 @@ public class RNJWPlayerView extends RelativeLayout implements
903
922
  .build();
904
923
 
905
924
  mPlayer.setup(config);
925
+ // if the player was fullscreen, set it to fullscreen again as the player is recreated
926
+ // The fullscreen view is still active but the internals don't know it is
927
+ if (wasFullscreen) {
928
+ mPlayer.setFullscreen(true, true);
929
+ }
906
930
  } else {
907
931
  if (prop.hasKey("license")) {
908
932
  new LicenseUtil().setLicenseKey(getReactContext(), prop.getString("license"));
@@ -952,130 +976,132 @@ public class RNJWPlayerView extends RelativeLayout implements
952
976
  .deepEquals(new ReadableArray[]{mPlaylistProp}, new ReadableArray[]{prop.getArray("playlist")});
953
977
  }
954
978
 
955
- private void setupPlayer(ReadableMap prop) {
956
- // Legacy
957
- PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
958
-
959
- JSONObject obj;
960
- PlayerConfig jwConfig = null;
961
- Boolean forceLegacy = prop.hasKey("forceLegacyConfig") ? prop.getBoolean("forceLegacyConfig") : false;
962
- Boolean playlistItemCallbackEnabled = prop.hasKey("playlistItemCallbackEnabled") ? prop.getBoolean("playlistItemCallbackEnabled") : false;
963
- Boolean isJwConfig = false;
964
- if (!forceLegacy) {
965
- try {
966
- obj = MapUtil.toJSONObject(prop);
967
- jwConfig = JsonHelper.parseConfigJson(obj);
968
- isJwConfig = true;
969
- } catch (Exception ex) {
970
- Log.e("RNJWPlayerView", ex.toString());
971
- isJwConfig = false; // not a valid jw config. Try to setup in legacy
972
- }
973
- }
974
-
975
- if (!isJwConfig) {
976
- // Legacy
977
- if (playlistNotTheSame(prop)) {
978
- List<PlaylistItem> playlist = new ArrayList<>();
979
- mPlaylistProp = prop.getArray("playlist");
980
- if (mPlaylistProp != null && mPlaylistProp.size() > 0) {
981
-
982
- int j = 0;
983
- while (mPlaylistProp.size() > j) {
984
- ReadableMap playlistItem = mPlaylistProp.getMap(j);
985
-
986
- PlaylistItem newPlayListItem = Util.getPlaylistItem((playlistItem));
987
- playlist.add(newPlayListItem);
988
- j++;
989
- }
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++;
990
990
  }
991
-
992
- configBuilder.playlist(playlist);
993
991
  }
992
+ configBuilder.playlist(playlist);
993
+ }
994
+ }
994
995
 
995
- // Legacy
996
- if (prop.hasKey("autostart")) {
997
- boolean autostart = prop.getBoolean("autostart");
998
- configBuilder.autostart(autostart);
999
- }
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
+ }
1000
1001
 
1001
- // Legacy
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);
1009
- }
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);
1010
1009
  }
1010
+ }
1011
1011
 
1012
- // Legacy
1013
- if (prop.hasKey("repeat")) {
1014
- boolean repeat = prop.getBoolean("repeat");
1015
- configBuilder.repeat(repeat);
1016
- }
1012
+ if (prop.hasKey("repeat")) {
1013
+ boolean repeat = prop.getBoolean("repeat");
1014
+ configBuilder.repeat(repeat);
1015
+ }
1017
1016
 
1018
- // Legacy
1019
- if (prop.hasKey("styling")) {
1020
- ReadableMap styling = prop.getMap("styling");
1021
- if (styling != null) {
1022
- if (styling.hasKey("displayDescription")) {
1023
- boolean displayDescription = styling.getBoolean("displayDescription");
1024
- configBuilder.displayDescription(displayDescription);
1025
- }
1017
+ if (prop.hasKey("stretching")) {
1018
+ String stretching = prop.getString("stretching");
1019
+ configBuilder.stretching(stretching);
1020
+ }
1021
+ }
1026
1022
 
1027
- if (styling.hasKey("displayTitle")) {
1028
- boolean displayTitle = styling.getBoolean("displayTitle");
1029
- configBuilder.displayTitle(displayTitle);
1030
- }
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
+ }
1031
1031
 
1032
- if (styling.hasKey("colors")) {
1033
- mColors = styling.getMap("colors");
1034
- }
1032
+ if (styling.hasKey("displayTitle")) {
1033
+ boolean displayTitle = styling.getBoolean("displayTitle");
1034
+ configBuilder.displayTitle(displayTitle);
1035
1035
  }
1036
- }
1037
1036
 
1038
- // Legacy
1039
- if (prop.hasKey("advertising")) {
1040
- ReadableMap ads = prop.getMap("advertising");
1041
- AdvertisingConfig advertisingConfig = RNJWPlayerAds.getAdvertisingConfig(ads);
1042
- if (advertisingConfig != null) {
1043
- configBuilder.advertisingConfig(advertisingConfig);
1037
+ if (styling.hasKey("colors")) {
1038
+ mColors = styling.getMap("colors");
1044
1039
  }
1045
1040
  }
1041
+ }
1042
+ }
1046
1043
 
1047
- // Legacy
1048
- if (prop.hasKey("stretching")) {
1049
- String stretching = prop.getString("stretching");
1050
- 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);
1051
1050
  }
1051
+ }
1052
+ }
1052
1053
 
1053
- // Legacy
1054
- // this isn't the ideal way to do controls...
1055
- // Better to just expose the `.setControls` method
1056
- if (prop.hasKey("controls")) {
1057
- boolean controls = prop.getBoolean("controls");
1058
- if (!controls) {
1059
- UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build();
1060
- configBuilder.uiConfig(uiConfig);
1061
- }
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);
1062
1060
  }
1061
+ }
1063
1062
 
1064
- // Legacy
1065
- if (prop.hasKey("hideUIGroups")) {
1066
- ReadableArray uiGroupsArray = prop.getArray("hideUIGroups");
1067
- UiConfig.Builder hideConfigBuilder = new UiConfig.Builder().displayAllControls();
1068
- for (int i = 0; i < uiGroupsArray.size(); i++) {
1069
- if (uiGroupsArray.getType(i) == ReadableType.String) {
1070
- UiGroup uiGroup = GROUP_TYPES.get(uiGroupsArray.getString(i));
1071
- if (uiGroup != null) {
1072
- hideConfigBuilder.hide(uiGroup);
1073
- }
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);
1074
1071
  }
1075
1072
  }
1076
- UiConfig hideJwControlbarUiConfig = hideConfigBuilder.build();
1077
- configBuilder.uiConfig(hideJwControlbarUiConfig);
1078
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);
1079
1105
  }
1080
1106
 
1081
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(
@@ -17,6 +17,7 @@ import com.jwplayer.pub.api.media.captions.CaptionType;
17
17
  import com.jwplayer.pub.api.media.playlists.MediaSource;
18
18
  import com.jwplayer.pub.api.media.playlists.PlaylistItem;
19
19
 
20
+ import org.json.JSONException;
20
21
  import org.json.JSONObject;
21
22
 
22
23
  import java.io.IOException;
@@ -139,6 +140,15 @@ public class Util {
139
140
  itemBuilder.description(desc);
140
141
  }
141
142
 
143
+ if (playlistItem.hasKey("userInfo")) {
144
+ try {
145
+ JSONObject info = MapUtil.toJSONObject(playlistItem.getMap("userInfo"));
146
+ itemBuilder.userInfo(info);
147
+ } catch (JSONException e) {
148
+ Log.e("userInfo", "Error parsing `userInfo` from your playlist. Message: " + e.getLocalizedMessage());
149
+ }
150
+ }
151
+
142
152
  if (playlistItem.hasKey("image")) {
143
153
  String image = playlistItem.getString("image");
144
154
  itemBuilder.image(image);
@@ -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.2"><title>version: 1.1.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.1.2</text><text x="695" y="140" transform="scale(.1)" fill="#fff" textLength="290">1.1.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.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
@@ -37,6 +37,8 @@ declare module "@jwplayer/jwplayer-react-native" {
37
37
  landscapeOnFullScreen?: boolean;
38
38
  portraitOnExitFullScreen?: boolean;
39
39
  exitFullScreenOnPortrait?: boolean;
40
+ enableLockScreenControls?: boolean;
41
+ pipEnabled?: boolean;
40
42
  }
41
43
 
42
44
  type JwThumbnailPreview = 101 | 102 | 103;
@@ -172,6 +174,10 @@ declare module "@jwplayer/jwplayer-react-native" {
172
174
  schedule?: { [key: string]: JwAdBreak };
173
175
  imaDaiSettings?: JwImaDaiSettings;
174
176
  httpheaders?: { [key: string]: string };
177
+ /**
178
+ * Data to be passed to Chromecast receiver (optional and typically used for DRM implementations)
179
+ */
180
+ userInfo?: { [key: string]: any };
175
181
  }
176
182
 
177
183
  interface JwImaDaiSettings {
@@ -347,6 +353,12 @@ declare module "@jwplayer/jwplayer-react-native" {
347
353
  recommendations?: string;
348
354
  startTime?: number;
349
355
  autostart?: boolean;
356
+ /**
357
+ * Data to be passed to Chromecast receiver (optional and typically used for DRM implementations)
358
+ *
359
+ * Only made available in legacy objects as there is no way to pass this otherwise
360
+ */
361
+ userInfo?: { [key: string]: any };
350
362
  }
351
363
  type RelatedOnClicks = "play" | "link";
352
364
  type RelatedOnCompletes = "show" | "hide" | "autoplay";
@@ -371,11 +383,15 @@ declare module "@jwplayer/jwplayer-react-native" {
371
383
  buttons?: string;
372
384
  backgroundColor?: string;
373
385
  fontColor?: string;
374
- timeslider?: { progress?: string; rail?: string; thumb?: string };
386
+ timeslider?: {
387
+ thumb?: string;
388
+ rail?: string;
389
+ slider?: string;
390
+ };
375
391
  };
376
392
  font?: Font;
377
- displayTitle?: boolean;
378
- displayDescription?: boolean;
393
+ showTitle?: boolean;
394
+ showDesc?: boolean;
379
395
  captionsStyle?: {
380
396
  font?: Font;
381
397
  fontColor?: string;
@@ -383,7 +399,7 @@ declare module "@jwplayer/jwplayer-react-native" {
383
399
  highlightColor?: string;
384
400
  edgeStyle?: EdgeStyles;
385
401
  };
386
- menuStyle: {
402
+ menuStyle?: {
387
403
  font?: Font;
388
404
  fontColor?: string;
389
405
  backgroundColor?: string;
@@ -470,8 +486,12 @@ declare module "@jwplayer/jwplayer-react-native" {
470
486
  fairplayCertUrl?: string;
471
487
  contentUUID?: string;
472
488
  viewOnly?: boolean;
473
- enableLockScreenControls: boolean;
474
- pipEnabled: boolean;
489
+ enableLockScreenControls?: boolean;
490
+ pipEnabled?: boolean;
491
+ offlineMessage?: string;
492
+ offlineImage?: string;
493
+ forceFullScreenOnLandscape?: boolean;
494
+ forceLandscapeOnFullScreen?: boolean;
475
495
  }
476
496
  interface BaseEvent<T> {
477
497
  nativeEvent: T;
@@ -577,7 +597,54 @@ declare module "@jwplayer/jwplayer-react-native" {
577
597
  onBeforeNextPlaylistItem?: (event: BaseEvent<PlaylistItemEventProps>) => void;
578
598
  }
579
599
 
600
+ export const JWPlayerAdEvents: {
601
+ /// This event is reported when the ad break has come to an end.
602
+ JWAdEventTypeAdBreakEnd: 0;
603
+ /// This event is reported when the ad break has begun.
604
+ JWAdEventTypeAdBreakStart: 1;
605
+ /// This event is reported when the user taps the ad.
606
+ JWAdEventTypeClicked: 2;
607
+ /// This event is reported when the ad is done playing.
608
+ JWAdEventTypeComplete: 3;
609
+ /// This event is used to report the ad impression, supplying additional detailed information about the ad.
610
+ JWAdEventTypeImpression: 4;
611
+ /// This event reports meta data information associated with the ad.
612
+ JWAdEventTypeMeta: 5;
613
+ /// The event is reported when the ad pauses.
614
+ JWAdEventTypePause: 6;
615
+ /// This event is reported when the ad begins playing, even in the middle of the stream after it was paused.
616
+ JWAdEventTypePlay: 7;
617
+ /// The event reports data about the ad request, when the ad is about to be loaded.
618
+ JWAdEventTypeRequest: 8;
619
+ /// This event reports the schedule of ads across the currently playing content.
620
+ JWAdEventTypeSchedule: 9;
621
+ /// This event is reported when the user skips the ad.
622
+ JWAdEventTypeSkipped: 10;
623
+ /// This event is reported when the ad begins.
624
+ JWAdEventTypeStarted: 11;
625
+ /// This event relays information about ad companions.
626
+ JWAdEventTypeCompanion: 12;
627
+ };
628
+
629
+ export const JWPlayerState: {
630
+ JWPlayerStateUnknown?: number;
631
+ JWPlayerStateIdle: number;
632
+ JWPlayerStateBuffering: number;
633
+ JWPlayerStatePlaying: number;
634
+ JWPlayerStatePaused: number;
635
+ JWPlayerStateComplete: number;
636
+ JWPlayerStateError: number | null;
637
+ };
638
+
639
+ export const JWPlayerAdClients: {
640
+ JWAdClientJWPlayer: 0;
641
+ JWAdClientGoogleIMA: 1;
642
+ JWAdClientGoogleIMADAI: 2;
643
+ JWAdClientUnknown: 3;
644
+ };
645
+
580
646
  export default class JWPlayer extends React.Component<PropsType> {
647
+ quite(): void;
581
648
  pause(): void;
582
649
  play(): void;
583
650
  stop(): void;
@@ -591,6 +658,7 @@ declare module "@jwplayer/jwplayer-react-native" {
591
658
  setControls(show: boolean): void;
592
659
  setLockScreenControls(show: boolean): void;
593
660
  seekTo(time: number): void;
661
+ changePlaylist(fileUrl: string): void;
594
662
  /**
595
663
  * Side load playlist items into an already setup player
596
664
  * @param playlistItems `PlaylistItem` or `JwPlaylistItem`
@@ -602,7 +670,9 @@ declare module "@jwplayer/jwplayer-react-native" {
602
670
  */
603
671
  loadPlaylistWithUrl(playlistUrl: string): void;
604
672
  setFullscreen(fullScreen: boolean): void;
605
- position(): Promise<number>;
673
+ time(): Promise<number | null>;
674
+ position(): Promise<number | null>;
675
+ togglePIP(): void;
606
676
  setUpCastController(): void;
607
677
  presentCastDialog(): void;
608
678
  connectedDevice(): Promise<CastingDevice | null>;
@@ -615,6 +685,57 @@ declare module "@jwplayer/jwplayer-react-native" {
615
685
  setCurrentCaptions(index: number): void;
616
686
  getCurrentCaptions(): Promise<number | null>;
617
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;
618
739
  /**
619
740
  * Only called inside `onBeforeNextPlaylistItem` callback, and once per callback
620
741
  * @param playlistItem `PlaylistItem` or `JwPlaylistItem`
package/index.js CHANGED
@@ -233,6 +233,7 @@ export default class JWPlayer extends Component {
233
233
  desc: PropTypes.string,
234
234
  mediaId: PropTypes.string,
235
235
  autostart: PropTypes.bool,
236
+ userInfo: PropTypes.object,
236
237
  recommendations: PropTypes.string,
237
238
  tracks: PropTypes.arrayOf(
238
239
  PropTypes.shape({
@@ -743,6 +744,40 @@ export default class JWPlayer extends Component {
743
744
  }
744
745
  }
745
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
+
746
781
  getRNJWPlayerBridgeHandle() {
747
782
  return findNodeHandle(this[this.ref_key]);
748
783
  }
@@ -275,9 +275,28 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
275
275
  }
276
276
  }
277
277
 
278
+ // Check if player is in PiP mode before loading new playlist
279
+ var isPipActive = false
280
+ var pipController: AVPictureInPictureController?
281
+
282
+ if let playerView = playerView {
283
+ pipController = playerView.pictureInPictureController
284
+ isPipActive = pipController?.isPictureInPictureActive ?? false
285
+ } else if let playerViewController = playerViewController {
286
+ pipController = playerViewController.playerView.pictureInPictureController
287
+ isPipActive = pipController?.isPictureInPictureActive ?? false
288
+ }
289
+
278
290
  if let playerViewController = playerViewController {
279
- playerViewController.player.loadPlaylist(items: playlistArray)
291
+ // We must treat PiP mode differently and setup as a new config
292
+ // or else the player will become unresponsive
293
+ if isPipActive {
294
+ setNewConfig(config: config)
295
+ } else {
296
+ playerViewController.player.loadPlaylist(items: playlistArray)
297
+ }
280
298
  } else if let playerView = playerView {
299
+ // If you use player only, consider doing a simpliar check for PiP as above
281
300
  playerView.player.loadPlaylist(items: playlistArray)
282
301
  } else {
283
302
  setNewConfig(config: config)
@@ -288,6 +307,98 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
288
307
  }
289
308
  }
290
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
+
291
402
  func setNewConfig(config: [String : Any]) {
292
403
  let forceLegacyConfig = config["forceLegacyConfig"] as? Bool?
293
404
  let playlistItemCallback = config["playlistItemCallbackEnabled"] as? Bool?
@@ -653,8 +764,12 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
653
764
  }
654
765
 
655
766
  // Process other properties
656
- if let mediaId = item["mediaId"] as? String {
657
- itemBuilder.mediaId(mediaId)
767
+ if let mediaId = (item["mediaId"] as? String) ?? (item["mediaid"] as? String) {
768
+ itemBuilder.mediaId(mediaId)
769
+ }
770
+
771
+ if let userInfo = item["userInfo"] as? Dictionary<String, Any> {
772
+ itemBuilder.userInfo(userInfo)
658
773
  }
659
774
 
660
775
  if let title = item["title"] as? String {
@@ -1207,7 +1322,13 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
1207
1322
  }
1208
1323
 
1209
1324
  func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
1210
-
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
+ }
1211
1332
  }
1212
1333
 
1213
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
@@ -4,7 +4,7 @@
4
4
  #import "RCTViewManager.h"
5
5
  #endif
6
6
 
7
- #import <JWPlayerKit/JWPlayerKit-swift.h>
7
+ #import <JWPlayerKit/JWPlayerKit-Swift.h>
8
8
 
9
9
  #import "RCTUIManager.h"
10
10
 
@@ -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.2",
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",