@jwplayer/jwplayer-react-native 1.1.3 → 1.3.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.
Files changed (36) hide show
  1. package/README.md +114 -21
  2. package/RNJWPlayer.podspec +1 -1
  3. package/android/build.gradle +14 -1
  4. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +27 -0
  5. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +373 -204
  6. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +16 -0
  7. package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +13 -1
  8. package/badges/version.svg +1 -1
  9. package/docs/CONFIG-REFERENCE.md +747 -0
  10. package/docs/MIGRATION-GUIDE.md +617 -0
  11. package/docs/PLATFORM-DIFFERENCES.md +693 -0
  12. package/docs/props.md +15 -3
  13. package/index.d.ts +225 -216
  14. package/index.js +34 -0
  15. package/ios/RNJWPlayer/RNJWPlayerView.swift +365 -10
  16. package/ios/RNJWPlayer/RNJWPlayerViewController.swift +45 -16
  17. package/ios/RNJWPlayer/RNJWPlayerViewManager.m +2 -0
  18. package/ios/RNJWPlayer/RNJWPlayerViewManager.swift +13 -0
  19. package/package.json +2 -2
  20. package/types/advertising.d.ts +514 -0
  21. package/types/index.d.ts +21 -0
  22. package/types/legacy.d.ts +82 -0
  23. package/types/platform-specific.d.ts +641 -0
  24. package/types/playlist.d.ts +410 -0
  25. package/types/unified-config.d.ts +591 -0
  26. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  27. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  28. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  29. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  30. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  31. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  32. package/android/.gradle/8.9/gc.properties +0 -0
  33. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  34. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  35. package/android/.gradle/vcs-1/gc.properties +0 -0
  36. package/docs/types.md +0 -254
@@ -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;
@@ -20,6 +21,7 @@ import android.os.Handler;
20
21
  import android.os.Looper;
21
22
  import android.util.Log;
22
23
  import android.view.View;
24
+ import android.view.View.MeasureSpec;
23
25
  import android.view.ViewGroup;
24
26
  import android.view.Window;
25
27
  import android.view.WindowManager;
@@ -317,6 +319,17 @@ public class RNJWPlayerView extends RelativeLayout implements
317
319
  getReactContext().addLifecycleEventListener(this);
318
320
  }
319
321
 
322
+ @Override
323
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
324
+ super.onLayout(changed, l, t, r, b);
325
+
326
+ // Standard React Native layout handling
327
+ // Since we're no longer constantly swapping views, this is simpler
328
+ if (mPlayerView != null) {
329
+ mPlayerView.layout(0, 0, r - l, b - t);
330
+ }
331
+ }
332
+
320
333
  private LifecycleObserver lifecycleObserver = new LifecycleEventObserver() {
321
334
  @Override
322
335
  public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
@@ -372,9 +385,9 @@ public class RNJWPlayerView extends RelativeLayout implements
372
385
 
373
386
  // If we are casting we need to break the cast session as there is no simple
374
387
  // way to reconnect to an existing session if the player that created it is dead
375
-
388
+
376
389
  // 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.
390
+ // into it rather than creating a new player for every piece of content.
378
391
  mPlayer.stop();
379
392
 
380
393
  // send signal to JW SDK player is destroyed
@@ -444,7 +457,13 @@ public class RNJWPlayerView extends RelativeLayout implements
444
457
  );
445
458
 
446
459
  mPlayer = null;
447
- mPlayerView = null;
460
+
461
+ // Remove the old player view from the view hierarchy to prevent
462
+ // the old UI controls from receiving touch events (fixes GitHub issue #188 crash)
463
+ if (mPlayerView != null) {
464
+ removeView(mPlayerView);
465
+ mPlayerView = null;
466
+ }
448
467
 
449
468
  getReactContext().removeLifecycleEventListener(this);
450
469
 
@@ -537,7 +556,7 @@ public class RNJWPlayerView extends RelativeLayout implements
537
556
  }
538
557
  }
539
558
 
540
- public void resolveNextPlaylistItem(ReadableMap playlistItem) {
559
+ public void resolveNextPlaylistItem(ReadableMap playlistItem) {
541
560
  if (itemUpdatePromise == null) {
542
561
  return;
543
562
  }
@@ -741,7 +760,7 @@ public class RNJWPlayerView extends RelativeLayout implements
741
760
  public void run() {
742
761
  // View may not have been removed properly (especially if returning from PiP)
743
762
  mPlayerViewContainer.removeView(mPlayerView);
744
-
763
+
745
764
  mPlayerViewContainer.addView(mPlayerView, new ViewGroup.LayoutParams(
746
765
  ViewGroup.LayoutParams.MATCH_PARENT,
747
766
  ViewGroup.LayoutParams.MATCH_PARENT));
@@ -856,6 +875,7 @@ public class RNJWPlayerView extends RelativeLayout implements
856
875
  }
857
876
  }
858
877
 
878
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
859
879
  private void registerReceiver() {
860
880
  mReceiver = new PipHandlerReceiver();
861
881
  IntentFilter intentFilter = new IntentFilter("onPictureInPictureModeChanged");
@@ -894,55 +914,174 @@ public class RNJWPlayerView extends RelativeLayout implements
894
914
  }
895
915
  }
896
916
 
917
+ /**
918
+ * Main entry point for setting/updating player configuration.
919
+ * Uses a smart approach: only recreate the player view when absolutely necessary,
920
+ * otherwise reconfigure the existing player instance.
921
+ *
922
+ * This follows JWPlayer SDK's intended usage pattern and significantly reduces overhead.
923
+ */
897
924
  public void setConfig(ReadableMap prop) {
898
925
  if (mConfig == null || !mConfig.equals(prop)) {
899
- if (mConfig != null && isOnlyDiff(prop, "playlist") && mPlayer != null) { // still safe check, even with JW
900
- // JSON change
901
- PlayerConfig oldConfig = mPlayer.getConfig();
902
- boolean wasFullscreen = mPlayer.getFullscreen();
903
- UiConfig uiConfig = createUiConfigWithControlsContainer(mPlayer, oldConfig.getUiConfig());
904
- PlayerConfig config = new PlayerConfig.Builder()
905
- .autostart(oldConfig.getAutostart())
906
- .nextUpOffset(oldConfig.getNextUpOffset())
907
- .repeat(oldConfig.getRepeat())
908
- .relatedConfig(oldConfig.getRelatedConfig())
909
- .displayDescription(oldConfig.getDisplayDescription())
910
- .displayTitle(oldConfig.getDisplayTitle())
911
- .advertisingConfig(oldConfig.getAdvertisingConfig())
912
- .stretching(oldConfig.getStretching())
913
- .uiConfig(uiConfig)
914
- .playlist(Util.createPlaylist(mPlaylistProp))
915
- .allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
916
- .preload(oldConfig.getPreload())
917
- .useTextureView(oldConfig.useTextureView())
918
- .thumbnailPreview(oldConfig.getThumbnailPreview())
919
- .mute(oldConfig.getMute())
920
- .build();
921
-
922
- mPlayer.setup(config);
923
- // if the player was fullscreen, set it to fullscreen again as the player is recreated
924
- // The fullscreen view is still active but the internals don't know it is
925
- if (wasFullscreen) {
926
- mPlayer.setFullscreen(true, true);
927
- }
926
+ // Set license key if provided
927
+ if (prop.hasKey("license")) {
928
+ new LicenseUtil().setLicenseKey(getReactContext(), prop.getString("license"));
928
929
  } else {
929
- if (prop.hasKey("license")) {
930
- new LicenseUtil().setLicenseKey(getReactContext(), prop.getString("license"));
931
- } else {
932
- Log.e(TAG, "JW SDK license not set");
933
- }
930
+ Log.e(TAG, "JW SDK license not set");
931
+ }
934
932
 
935
- // The entire config is different (other than the "playlist" key)
936
- this.setupPlayer(prop);
933
+ // First time setup - need to create player view
934
+ if (mPlayer == null) {
935
+ this.createPlayerView(prop);
936
+ mConfig = prop;
937
+ return;
938
+ }
939
+
940
+ // Check if we need full player recreation (rare cases only)
941
+ if (requiresPlayerRecreation(prop)) {
942
+ Log.d(TAG, "Player recreation required - destroying and recreating player view");
943
+ this.destroyPlayer();
944
+ this.createPlayerView(prop);
945
+ } else {
946
+ // Normal case: reconfigure existing player without recreation
947
+ Log.d(TAG, "Reconfiguring existing player without recreation");
948
+ this.reconfigurePlayer(prop);
937
949
  }
938
- } else {
939
- // No change
940
950
  }
941
951
 
942
952
  mConfig = prop;
943
953
  }
944
954
 
955
+ /**
956
+ * Determines if a config change requires full player view recreation.
957
+ * Only return true for changes that genuinely cannot be handled by reconfiguration.
958
+ *
959
+ * Currently, the JWPlayer SDK can handle almost all config changes via setup(),
960
+ * so we only recreate for critical changes like license updates.
961
+ */
962
+ private boolean requiresPlayerRecreation(ReadableMap prop) {
963
+ if (mConfig == null || mPlayer == null) {
964
+ return true;
965
+ }
966
+
967
+ // License change requires recreation
968
+ if (prop.hasKey("license") && mConfig.hasKey("license")) {
969
+ String newLicense = prop.getString("license");
970
+ String oldLicense = mConfig.getString("license");
971
+ if (newLicense != null && !newLicense.equals(oldLicense)) {
972
+ return true;
973
+ }
974
+ }
975
+
976
+ // Add other cases here if needed in the future
977
+ // For example: switching between playerView and playerViewController modes
978
+
979
+ return false;
980
+ }
981
+
982
+ /**
983
+ * Reconfigures the existing player instance with new settings.
984
+ * This is the preferred path for config updates as it preserves the player instance
985
+ * and video surface, following JWPlayer SDK's design intent.
986
+ *
987
+ * Based on the pattern used in loadPlaylist() and loadPlaylistWithUrl().
988
+ */
989
+ private void reconfigurePlayer(ReadableMap prop) {
990
+ if (mPlayer == null) {
991
+ Log.e(TAG, "Cannot reconfigure - player is null");
992
+ return;
993
+ }
994
+
995
+ PlayerConfig oldConfig = mPlayer.getConfig();
996
+ boolean wasFullscreen = mPlayer.getFullscreen();
997
+ boolean currentControlsState = mPlayer.getControls();
998
+
999
+ // Stop playback before reconfiguration to avoid issues (Issue #188 fix)
1000
+ mPlayer.stop();
1001
+
1002
+ // Build new configuration
1003
+ PlayerConfig newConfig = buildPlayerConfig(prop, oldConfig);
1004
+
1005
+ // ALWAYS ensure PLAYER_CONTROLS_CONTAINER is shown in UiConfig after setup.
1006
+ // This prevents issues where controls are off and JWPlayer SDK hides UI groups,
1007
+ // leaving them in a state where setControls(true) won't work.
1008
+ // We'll manage controls state via setControls() API after setup for clean state management.
1009
+ UiConfig fixedUiConfig = new UiConfig.Builder(newConfig.getUiConfig())
1010
+ .show(UiGroup.PLAYER_CONTROLS_CONTAINER)
1011
+ .build();
1012
+ newConfig = new PlayerConfig.Builder(newConfig)
1013
+ .uiConfig(fixedUiConfig)
1014
+ .build();
1015
+
1016
+ // Apply new configuration to existing player
1017
+ mPlayer.setup(newConfig);
1018
+
1019
+ // Now manage controls state via API (after setup, when UI groups are in clean state)
1020
+ if (prop.hasKey("controls")) {
1021
+ // Developer explicitly set controls in props - use that value
1022
+ mPlayer.setControls(prop.getBoolean("controls"));
1023
+ } else if (!currentControlsState) {
1024
+ // Controls were off before reconfigure and no explicit prop provided
1025
+ // Restore the off state (after ensuring UI groups are visible)
1026
+ mPlayer.setControls(false);
1027
+ }
1028
+ // Note: If controls were on and no prop provided, they'll stay on (default from configureUI)
1029
+
1030
+ // Restore fullscreen state if needed
1031
+ // The fullscreen view is still active but internals need to be notified
1032
+ if (wasFullscreen) {
1033
+ mPlayer.setFullscreen(true, true);
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Builds a PlayerConfig from React Native props, preserving relevant old config values.
1039
+ * This ensures smooth transitions when reconfiguring the player.
1040
+ */
1041
+ private PlayerConfig buildPlayerConfig(ReadableMap prop, PlayerConfig oldConfig) {
1042
+ PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
1043
+
1044
+ // Try to parse as JW config first
1045
+ JSONObject obj;
1046
+ PlayerConfig jwConfig = null;
1047
+ Boolean forceLegacy = prop.hasKey("forceLegacyConfig") ? prop.getBoolean("forceLegacyConfig") : false;
1048
+ Boolean isJwConfig = false;
1049
+
1050
+ if (!forceLegacy) {
1051
+ try {
1052
+ obj = MapUtil.toJSONObject(prop);
1053
+ jwConfig = JsonHelper.parseConfigJson(obj);
1054
+ isJwConfig = true;
1055
+ return jwConfig; // Return directly if valid JW config
1056
+ } catch (Exception ex) {
1057
+ Log.d(TAG, "Not a JW config format, using legacy builder");
1058
+ isJwConfig = false;
1059
+ }
1060
+ }
1061
+
1062
+ // Legacy config building
1063
+ configurePlaylist(configBuilder, prop);
1064
+ configureBasicSettings(configBuilder, prop);
1065
+ configureStyling(configBuilder, prop);
1066
+ configureAdvertising(configBuilder, prop);
1067
+ configureUI(configBuilder, prop);
1068
+
1069
+ return configBuilder.build();
1070
+ }
1071
+
1072
+ /**
1073
+ * Utility method to check if only a specific key differs between configs.
1074
+ * This is kept for potential future optimizations or debugging, but is no longer
1075
+ * used in the main setConfig flow since we now reconfigure the player for all changes.
1076
+ *
1077
+ * @deprecated Consider using reconfigurePlayer() for all config changes instead
1078
+ */
1079
+ @Deprecated
945
1080
  public boolean isOnlyDiff(ReadableMap prop, String keyName) {
1081
+ if (mConfig == null || prop == null) {
1082
+ return false;
1083
+ }
1084
+
946
1085
  // Convert ReadableMap to HashMap
947
1086
  Map<String, Object> mConfigMap = mConfig.toHashMap();
948
1087
  Map<String, Object> propMap = prop.toHashMap();
@@ -974,154 +1113,177 @@ public class RNJWPlayerView extends RelativeLayout implements
974
1113
  .deepEquals(new ReadableArray[]{mPlaylistProp}, new ReadableArray[]{prop.getArray("playlist")});
975
1114
  }
976
1115
 
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
1116
+ private void configurePlaylist(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1117
+ if (playlistNotTheSame(prop)) {
1118
+ List<PlaylistItem> playlist = new ArrayList<>();
1119
+ mPlaylistProp = prop.getArray("playlist");
1120
+ if (mPlaylistProp != null && mPlaylistProp.size() > 0) {
1121
+ int j = 0;
1122
+ while (mPlaylistProp.size() > j) {
1123
+ ReadableMap playlistItem = mPlaylistProp.getMap(j);
1124
+ PlaylistItem newPlayListItem = Util.getPlaylistItem((playlistItem));
1125
+ playlist.add(newPlayListItem);
1126
+ j++;
1127
+ }
994
1128
  }
1129
+ configBuilder.playlist(playlist);
995
1130
  }
1131
+ }
996
1132
 
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
- }
1012
- }
1133
+ private void configureBasicSettings(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1134
+ if (prop.hasKey("autostart")) {
1135
+ boolean autostart = prop.getBoolean("autostart");
1136
+ configBuilder.autostart(autostart);
1137
+ }
1013
1138
 
1014
- configBuilder.playlist(playlist);
1139
+ if (prop.hasKey("nextUpStyle")) {
1140
+ ReadableMap nextUpStyle = prop.getMap("nextUpStyle");
1141
+ if (nextUpStyle != null && nextUpStyle.hasKey("offsetSeconds")
1142
+ && nextUpStyle.hasKey("offsetPercentage")) {
1143
+ int offsetSeconds = prop.getInt("offsetSeconds");
1144
+ int offsetPercentage = prop.getInt("offsetPercentage");
1145
+ configBuilder.nextUpOffset(offsetSeconds).nextUpOffsetPercentage(offsetPercentage);
1015
1146
  }
1147
+ }
1016
1148
 
1017
- // Legacy
1018
- if (prop.hasKey("autostart")) {
1019
- boolean autostart = prop.getBoolean("autostart");
1020
- configBuilder.autostart(autostart);
1021
- }
1149
+ if (prop.hasKey("repeat")) {
1150
+ boolean repeat = prop.getBoolean("repeat");
1151
+ configBuilder.repeat(repeat);
1152
+ }
1153
+
1154
+ if (prop.hasKey("stretching")) {
1155
+ String stretching = prop.getString("stretching");
1156
+ configBuilder.stretching(stretching);
1157
+ }
1158
+ }
1022
1159
 
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);
1160
+ private void configureStyling(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1161
+ if (prop.hasKey("styling")) {
1162
+ ReadableMap styling = prop.getMap("styling");
1163
+ if (styling != null) {
1164
+ if (styling.hasKey("displayDescription")) {
1165
+ boolean displayDescription = styling.getBoolean("displayDescription");
1166
+ configBuilder.displayDescription(displayDescription);
1167
+ }
1168
+
1169
+ if (styling.hasKey("displayTitle")) {
1170
+ boolean displayTitle = styling.getBoolean("displayTitle");
1171
+ configBuilder.displayTitle(displayTitle);
1031
1172
  }
1032
- }
1033
1173
 
1034
- // Legacy
1035
- if (prop.hasKey("repeat")) {
1036
- boolean repeat = prop.getBoolean("repeat");
1037
- configBuilder.repeat(repeat);
1174
+ if (styling.hasKey("colors")) {
1175
+ mColors = styling.getMap("colors");
1176
+ }
1038
1177
  }
1178
+ }
1179
+ }
1039
1180
 
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
- }
1181
+ private void configureAdvertising(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1182
+ if (prop.hasKey("advertising")) {
1183
+ ReadableMap ads = prop.getMap("advertising");
1184
+ AdvertisingConfig advertisingConfig = RNJWPlayerAds.getAdvertisingConfig(ads);
1185
+ if (advertisingConfig != null) {
1186
+ configBuilder.advertisingConfig(advertisingConfig);
1187
+ }
1188
+ }
1189
+ }
1048
1190
 
1049
- if (styling.hasKey("displayTitle")) {
1050
- boolean displayTitle = styling.getBoolean("displayTitle");
1051
- configBuilder.displayTitle(displayTitle);
1052
- }
1191
+ private void configureUI(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1192
+ // Handle controls property - default to true if not specified
1193
+ boolean controls = true; // Default to showing controls
1194
+ if (prop.hasKey("controls")) {
1195
+ controls = prop.getBoolean("controls");
1196
+ }
1197
+
1198
+ if (!controls) {
1199
+ UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build();
1200
+ configBuilder.uiConfig(uiConfig);
1201
+ } else {
1202
+ // Explicitly show controls and ensure controls container is visible
1203
+ // This ensures controls work even if setControls() is called later
1204
+ UiConfig uiConfig = new UiConfig.Builder()
1205
+ .displayAllControls()
1206
+ .show(UiGroup.PLAYER_CONTROLS_CONTAINER)
1207
+ .build();
1208
+ configBuilder.uiConfig(uiConfig);
1209
+ }
1053
1210
 
1054
- if (styling.hasKey("colors")) {
1055
- mColors = styling.getMap("colors");
1211
+ if (prop.hasKey("hideUIGroups")) {
1212
+ ReadableArray uiGroupsArray = prop.getArray("hideUIGroups");
1213
+ UiConfig.Builder hideConfigBuilder = new UiConfig.Builder().displayAllControls();
1214
+ for (int i = 0; i < uiGroupsArray.size(); i++) {
1215
+ if (uiGroupsArray.getType(i) == ReadableType.String) {
1216
+ UiGroup uiGroup = GROUP_TYPES.get(uiGroupsArray.getString(i));
1217
+ if (uiGroup != null) {
1218
+ hideConfigBuilder.hide(uiGroup);
1056
1219
  }
1057
1220
  }
1058
1221
  }
1222
+ UiConfig hideJwControlbarUiConfig = hideConfigBuilder.build();
1223
+ configBuilder.uiConfig(hideJwControlbarUiConfig);
1224
+ }
1225
+ }
1059
1226
 
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);
1066
- }
1067
- }
1227
+ /**
1228
+ * Creates a new player view and initializes it with the provided configuration.
1229
+ * This should only be called for initial setup or when full recreation is required.
1230
+ *
1231
+ * Note: This method calls destroyPlayer() first to ensure clean state.
1232
+ */
1233
+ private void createPlayerView(ReadableMap prop) {
1234
+ PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
1068
1235
 
1069
- // Legacy
1070
- if (prop.hasKey("stretching")) {
1071
- String stretching = prop.getString("stretching");
1072
- configBuilder.stretching(stretching);
1073
- }
1236
+ JSONObject obj;
1237
+ PlayerConfig jwConfig = null;
1238
+ Boolean forceLegacy = prop.hasKey("forceLegacyConfig") ? prop.getBoolean("forceLegacyConfig") : false;
1239
+ Boolean playlistItemCallbackEnabled = prop.hasKey("playlistItemCallbackEnabled") ? prop.getBoolean("playlistItemCallbackEnabled") : false;
1240
+ Boolean isJwConfig = false;
1074
1241
 
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
- }
1242
+ if (!forceLegacy) {
1243
+ try {
1244
+ obj = MapUtil.toJSONObject(prop);
1245
+ jwConfig = JsonHelper.parseConfigJson(obj);
1246
+ isJwConfig = true;
1247
+ } catch (Exception ex) {
1248
+ Log.e(TAG, "Not a valid JW config format, falling back to legacy: " + ex.toString());
1249
+ isJwConfig = false;
1084
1250
  }
1251
+ }
1085
1252
 
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
- }
1096
- }
1097
- }
1098
- UiConfig hideJwControlbarUiConfig = hideConfigBuilder.build();
1099
- configBuilder.uiConfig(hideJwControlbarUiConfig);
1100
- }
1253
+ if (!isJwConfig) {
1254
+ configurePlaylist(configBuilder, prop);
1255
+ configureBasicSettings(configBuilder, prop);
1256
+ configureStyling(configBuilder, prop);
1257
+ configureAdvertising(configBuilder, prop);
1258
+ configureUI(configBuilder, prop);
1101
1259
  }
1102
1260
 
1103
1261
  Context simpleContext = getNonBuggyContext(getReactContext(), getAppContext());
1104
1262
 
1263
+ // Ensure clean state before creating new player view
1105
1264
  this.destroyPlayer();
1106
1265
 
1266
+ // Create new player view
1107
1267
  mPlayerView = new RNJWPlayer(simpleContext);
1108
-
1109
1268
  mPlayerView.setFocusable(true);
1110
1269
  mPlayerView.setFocusableInTouchMode(true);
1111
1270
 
1112
- setLayoutParams(
1113
- new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1271
+ // Set layout parameters
1272
+ setLayoutParams(new ViewGroup.LayoutParams(
1273
+ ViewGroup.LayoutParams.MATCH_PARENT,
1274
+ ViewGroup.LayoutParams.MATCH_PARENT));
1114
1275
  mPlayerView.setLayoutParams(new LinearLayout.LayoutParams(
1115
1276
  LinearLayout.LayoutParams.MATCH_PARENT,
1116
1277
  LinearLayout.LayoutParams.MATCH_PARENT));
1278
+
1279
+ // Add to view hierarchy - React Native will handle layout
1117
1280
  addView(mPlayerView);
1118
1281
 
1119
- // Ensure we have a valid state before applying to the player
1120
- registry.setCurrentState(registry.getCurrentState()); // This is a hack to ensure player and view know the lifecycle state
1121
-
1282
+ // Get player instance
1122
1283
  mPlayer = mPlayerView.getPlayer(this);
1123
1284
 
1124
- if (prop.hasKey("controls")) { // Hack for controls hiding not working right away
1285
+ // Apply view-specific props
1286
+ if (prop.hasKey("controls")) {
1125
1287
  mPlayerView.getPlayer().setControls(prop.getBoolean("controls"));
1126
1288
  }
1127
1289
 
@@ -1135,7 +1297,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1135
1297
  }
1136
1298
 
1137
1299
  if (prop.hasKey("portraitOnExitFullScreen")) {
1138
- portraitOnExitFullScreen = prop.getBoolean("fullScreenOnLandscape");
1300
+ portraitOnExitFullScreen = prop.getBoolean("portraitOnExitFullScreen");
1139
1301
  }
1140
1302
 
1141
1303
  if (prop.hasKey("playerInModal")) {
@@ -1147,6 +1309,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1147
1309
  mPlayerView.exitFullScreenOnPortrait = exitFullScreenOnPortrait;
1148
1310
  }
1149
1311
 
1312
+ // Setup player with config
1150
1313
  if (isJwConfig) {
1151
1314
  mPlayer.setup(jwConfig);
1152
1315
  } else {
@@ -1154,6 +1317,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1154
1317
  mPlayer.setup(playerConfig);
1155
1318
  }
1156
1319
 
1320
+ // Configure PiP if enabled
1157
1321
  if (mActivity != null && prop.hasKey("pipEnabled")) {
1158
1322
  boolean pipEnabled = prop.getBoolean("pipEnabled");
1159
1323
  if (pipEnabled) {
@@ -1165,56 +1329,12 @@ public class RNJWPlayerView extends RelativeLayout implements
1165
1329
  }
1166
1330
  }
1167
1331
 
1168
- // Legacy
1169
- // This isn't the ideal way to do this on Android. All drawables/colors/themes shoudld
1170
- // be targed using styling. See `https://docs.jwplayer.com/players/docs/android-styling-guide`
1171
- // for more information on how best to override the JWP styles using XML. If you are unsure of a
1172
- // color/drawable/theme, open an `Ask` issue.
1173
- if (mColors != null) {
1174
- if (mColors.hasKey("backgroundColor")) {
1175
- mPlayerView.setBackgroundColor(Color.parseColor("#" + mColors.getString("backgroundColor")));
1176
- }
1177
-
1178
- if (mColors.hasKey("buttons")) {
1179
-
1180
- }
1181
-
1182
- if (mColors.hasKey("timeslider")) {
1183
- CueMarkerSeekbar seekBar = findViewById(com.longtailvideo.jwplayer.R.id.controlbar_seekbar);
1184
- ReadableMap timeslider = mColors.getMap("timeslider");
1185
- if (timeslider != null) {
1186
- LayerDrawable progressDrawable = (LayerDrawable) seekBar.getProgressDrawable();
1187
-
1188
- if (timeslider.hasKey("progress")) {
1189
- // seekBar.getProgressDrawable().setColorFilter(Color.parseColor("#" +
1190
- // timeslider.getString("progress")), PorterDuff.Mode.SRC_IN);
1191
- Drawable processDrawable = progressDrawable.findDrawableByLayerId(android.R.id.progress);
1192
- processDrawable.setColorFilter(Color.parseColor("#" + timeslider.getString("progress")),
1193
- PorterDuff.Mode.SRC_IN);
1194
- }
1195
-
1196
- if (timeslider.hasKey("buffer")) {
1197
- Drawable secondaryProgressDrawable = progressDrawable
1198
- .findDrawableByLayerId(android.R.id.secondaryProgress);
1199
- secondaryProgressDrawable.setColorFilter(Color.parseColor("#" + timeslider.getString("buffer")),
1200
- PorterDuff.Mode.SRC_IN);
1201
- }
1202
-
1203
- if (timeslider.hasKey("rail")) {
1204
- Drawable backgroundDrawable = progressDrawable.findDrawableByLayerId(android.R.id.background);
1205
- backgroundDrawable.setColorFilter(Color.parseColor("#" + timeslider.getString("rail")),
1206
- PorterDuff.Mode.SRC_IN);
1207
- }
1208
-
1209
- if (timeslider.hasKey("thumb")) {
1210
- seekBar.getThumb().setColorFilter(Color.parseColor("#" + timeslider.getString("thumb")),
1211
- PorterDuff.Mode.SRC_IN);
1212
- }
1213
- }
1214
- }
1215
- }
1332
+ // Legacy styling support
1333
+ // NOTE: This isn't the ideal way to do this on Android. All drawables/colors/themes should
1334
+ // be targeted using styling. See https://docs.jwplayer.com/players/docs/android-styling-guide
1335
+ applyLegacyStyling();
1216
1336
 
1217
- // Needed to handle volume control
1337
+ // Setup audio
1218
1338
  audioManager = (AudioManager) simpleContext.getSystemService(Context.AUDIO_SERVICE);
1219
1339
 
1220
1340
  if (prop.hasKey("backgroundAudioEnabled")) {
@@ -1224,12 +1344,61 @@ public class RNJWPlayerView extends RelativeLayout implements
1224
1344
  setupPlayerView(backgroundAudioEnabled, playlistItemCallbackEnabled);
1225
1345
 
1226
1346
  if (backgroundAudioEnabled) {
1227
- audioManager = (AudioManager) simpleContext.getSystemService(Context.AUDIO_SERVICE);
1228
1347
  mMediaServiceController = new MediaServiceController.Builder((AppCompatActivity) mActivity, mPlayer)
1229
1348
  .build();
1230
1349
  }
1231
1350
  }
1232
1351
 
1352
+ /**
1353
+ * Applies legacy color/styling customizations.
1354
+ * Extracted to separate method for clarity.
1355
+ */
1356
+ private void applyLegacyStyling() {
1357
+ if (mColors == null) {
1358
+ return;
1359
+ }
1360
+
1361
+ if (mColors.hasKey("backgroundColor")) {
1362
+ mPlayerView.setBackgroundColor(Color.parseColor("#" + mColors.getString("backgroundColor")));
1363
+ }
1364
+
1365
+ if (mColors.hasKey("timeslider")) {
1366
+ CueMarkerSeekbar seekBar = findViewById(com.longtailvideo.jwplayer.R.id.controlbar_seekbar);
1367
+ ReadableMap timeslider = mColors.getMap("timeslider");
1368
+ if (timeslider != null && seekBar != null) {
1369
+ LayerDrawable progressDrawable = (LayerDrawable) seekBar.getProgressDrawable();
1370
+
1371
+ if (timeslider.hasKey("progress")) {
1372
+ Drawable processDrawable = progressDrawable.findDrawableByLayerId(android.R.id.progress);
1373
+ processDrawable.setColorFilter(
1374
+ Color.parseColor("#" + timeslider.getString("progress")),
1375
+ PorterDuff.Mode.SRC_IN);
1376
+ }
1377
+
1378
+ if (timeslider.hasKey("buffer")) {
1379
+ Drawable secondaryProgressDrawable = progressDrawable
1380
+ .findDrawableByLayerId(android.R.id.secondaryProgress);
1381
+ secondaryProgressDrawable.setColorFilter(
1382
+ Color.parseColor("#" + timeslider.getString("buffer")),
1383
+ PorterDuff.Mode.SRC_IN);
1384
+ }
1385
+
1386
+ if (timeslider.hasKey("rail")) {
1387
+ Drawable backgroundDrawable = progressDrawable.findDrawableByLayerId(android.R.id.background);
1388
+ backgroundDrawable.setColorFilter(
1389
+ Color.parseColor("#" + timeslider.getString("rail")),
1390
+ PorterDuff.Mode.SRC_IN);
1391
+ }
1392
+
1393
+ if (timeslider.hasKey("thumb")) {
1394
+ seekBar.getThumb().setColorFilter(
1395
+ Color.parseColor("#" + timeslider.getString("thumb")),
1396
+ PorterDuff.Mode.SRC_IN);
1397
+ }
1398
+ }
1399
+ }
1400
+ }
1401
+
1233
1402
  // Audio Focus
1234
1403
 
1235
1404
  public void requestAudioFocus() {