@jwplayer/jwplayer-react-native 1.2.0 → 1.3.1

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 (35) hide show
  1. package/README.md +114 -21
  2. package/RNJWPlayer.podspec +1 -1
  3. package/android/build.gradle +31 -5
  4. package/android/src/ima/java/com/jwplayer/rnjwplayer/ImaHelper.java +143 -0
  5. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerAds.java +41 -129
  6. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +19 -4
  7. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +385 -105
  8. package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +13 -1
  9. package/android/src/noima/java/com/jwplayer/rnjwplayer/ImaHelper.java +24 -0
  10. package/badges/version.svg +1 -1
  11. package/docs/CONFIG-REFERENCE.md +747 -0
  12. package/docs/MIGRATION-GUIDE.md +617 -0
  13. package/docs/PLATFORM-DIFFERENCES.md +693 -0
  14. package/docs/props.md +15 -3
  15. package/index.d.ts +207 -249
  16. package/ios/RNJWPlayer/RNJWPlayerView.swift +278 -21
  17. package/ios/RNJWPlayer/RNJWPlayerViewController.swift +33 -16
  18. package/package.json +2 -2
  19. package/types/advertising.d.ts +514 -0
  20. package/types/index.d.ts +21 -0
  21. package/types/legacy.d.ts +82 -0
  22. package/types/platform-specific.d.ts +641 -0
  23. package/types/playlist.d.ts +410 -0
  24. package/types/unified-config.d.ts +591 -0
  25. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  26. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  27. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  28. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  29. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  30. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  31. package/android/.gradle/8.9/gc.properties +0 -0
  32. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  33. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  34. package/android/.gradle/vcs-1/gc.properties +0 -0
  35. package/docs/types.md +0 -254
@@ -21,6 +21,7 @@ import android.os.Handler;
21
21
  import android.os.Looper;
22
22
  import android.util.Log;
23
23
  import android.view.View;
24
+ import android.view.View.MeasureSpec;
24
25
  import android.view.ViewGroup;
25
26
  import android.view.Window;
26
27
  import android.view.WindowManager;
@@ -318,6 +319,17 @@ public class RNJWPlayerView extends RelativeLayout implements
318
319
  getReactContext().addLifecycleEventListener(this);
319
320
  }
320
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
+
321
333
  private LifecycleObserver lifecycleObserver = new LifecycleEventObserver() {
322
334
  @Override
323
335
  public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
@@ -445,7 +457,13 @@ public class RNJWPlayerView extends RelativeLayout implements
445
457
  );
446
458
 
447
459
  mPlayer = null;
448
- 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
+ }
449
467
 
450
468
  getReactContext().removeLifecycleEventListener(this);
451
469
 
@@ -896,55 +914,278 @@ public class RNJWPlayerView extends RelativeLayout implements
896
914
  }
897
915
  }
898
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
+ */
899
924
  public void setConfig(ReadableMap prop) {
900
925
  if (mConfig == null || !mConfig.equals(prop)) {
901
- if (mConfig != null && isOnlyDiff(prop, "playlist") && mPlayer != null) { // still safe check, even with JW
902
- // JSON change
903
- PlayerConfig oldConfig = mPlayer.getConfig();
904
- boolean wasFullscreen = mPlayer.getFullscreen();
905
- UiConfig uiConfig = createUiConfigWithControlsContainer(mPlayer, oldConfig.getUiConfig());
906
- PlayerConfig config = new PlayerConfig.Builder()
907
- .autostart(oldConfig.getAutostart())
908
- .nextUpOffset(oldConfig.getNextUpOffset())
909
- .repeat(oldConfig.getRepeat())
910
- .relatedConfig(oldConfig.getRelatedConfig())
911
- .displayDescription(oldConfig.getDisplayDescription())
912
- .displayTitle(oldConfig.getDisplayTitle())
913
- .advertisingConfig(oldConfig.getAdvertisingConfig())
914
- .stretching(oldConfig.getStretching())
915
- .uiConfig(uiConfig)
916
- .playlist(Util.createPlaylist(mPlaylistProp))
917
- .allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
918
- .preload(oldConfig.getPreload())
919
- .useTextureView(oldConfig.useTextureView())
920
- .thumbnailPreview(oldConfig.getThumbnailPreview())
921
- .mute(oldConfig.getMute())
922
- .build();
926
+ // Set license key if provided
927
+ if (prop.hasKey("license")) {
928
+ new LicenseUtil().setLicenseKey(getReactContext(), prop.getString("license"));
929
+ } else {
930
+ Log.e(TAG, "JW SDK license not set");
931
+ }
923
932
 
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
- }
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);
930
945
  } else {
931
- if (prop.hasKey("license")) {
932
- new LicenseUtil().setLicenseKey(getReactContext(), prop.getString("license"));
933
- } else {
934
- Log.e(TAG, "JW SDK license not set");
946
+ // Normal case: reconfigure existing player without recreation
947
+ Log.d(TAG, "Reconfiguring existing player without recreation");
948
+ this.reconfigurePlayer(prop);
949
+ }
950
+ }
951
+
952
+ mConfig = prop;
953
+ }
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
+ * Checks for IMA configuration when IMA is disabled and logs a warning.
1039
+ *
1040
+ * @param obj The JSONObject to check (for JSON parser path)
1041
+ * @param prop The ReadableMap to check (for legacy builder path)
1042
+ */
1043
+ private void checkAndWarnImaConfig(JSONObject obj, ReadableMap prop) {
1044
+ if (BuildConfig.USE_IMA) {
1045
+ return; // IMA is enabled, no warning needed
1046
+ }
1047
+
1048
+ String clientValue = getClientValue(obj, prop);
1049
+
1050
+ if (clientValue != null && isImaClient(clientValue)) {
1051
+ String warningMessage = "⚠️ Google IMA advertising is not enabled. " +
1052
+ "To use IMA ads, add 'RNJWPlayerUseGoogleIMA = true' to your app/build.gradle ext {} block. " +
1053
+ "Current client: " + clientValue + ". Player will load without ads.";
1054
+ Log.w(TAG, warningMessage);
1055
+ }
1056
+ }
1057
+
1058
+ /**
1059
+ * Extracts the client value from either JSONObject or ReadableMap
1060
+ */
1061
+ private String getClientValue(JSONObject obj, ReadableMap prop) {
1062
+ // Check JSON object (for JSON parser path)
1063
+ if (obj != null && obj.has("advertising")) {
1064
+ try {
1065
+ JSONObject advertising = obj.getJSONObject("advertising");
1066
+ if (advertising.has("client")) {
1067
+ return advertising.getString("client");
1068
+ } else if (advertising.has("adClient")) {
1069
+ return advertising.getString("adClient");
935
1070
  }
1071
+ } catch (Exception e) {
1072
+ // Silently continue if we can't parse
1073
+ }
1074
+ }
1075
+
1076
+ // Check ReadableMap (for legacy builder path)
1077
+ if (prop != null && prop.hasKey("advertising")) {
1078
+ ReadableMap advertising = prop.getMap("advertising");
1079
+ if (advertising != null) {
1080
+ if (advertising.hasKey("client")) {
1081
+ return advertising.getString("client");
1082
+ } else if (advertising.hasKey("adClient")) {
1083
+ return advertising.getString("adClient");
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ return null;
1089
+ }
1090
+
1091
+ /**
1092
+ * Checks if a client value indicates IMA usage
1093
+ */
1094
+ private boolean isImaClient(String clientValue) {
1095
+ if (clientValue == null) {
1096
+ return false;
1097
+ }
1098
+ return "ima".equalsIgnoreCase(clientValue) ||
1099
+ "ima_dai".equalsIgnoreCase(clientValue) ||
1100
+ "GoogleIMA".equalsIgnoreCase(clientValue) ||
1101
+ "GoogleIMADAI".equalsIgnoreCase(clientValue) ||
1102
+ "IMA_DAI".equalsIgnoreCase(clientValue) ||
1103
+ "googima".equalsIgnoreCase(clientValue);
1104
+ }
1105
+
1106
+ /**
1107
+ * Checks if advertising config contains IMA when IMA is disabled.
1108
+ * Used to determine if we should skip configureAdvertising() in legacy builder.
1109
+ */
1110
+ private boolean shouldSkipAdvertising(ReadableMap prop) {
1111
+ if (BuildConfig.USE_IMA || !prop.hasKey("advertising")) {
1112
+ return false;
1113
+ }
1114
+
1115
+ ReadableMap advertising = prop.getMap("advertising");
1116
+ if (advertising == null) {
1117
+ return false;
1118
+ }
1119
+
1120
+ String clientValue = null;
1121
+ if (advertising.hasKey("client")) {
1122
+ clientValue = advertising.getString("client");
1123
+ } else if (advertising.hasKey("adClient")) {
1124
+ clientValue = advertising.getString("adClient");
1125
+ }
1126
+
1127
+ return isImaClient(clientValue);
1128
+ }
1129
+
1130
+ /**
1131
+ * Builds a PlayerConfig from React Native props, preserving relevant old config values.
1132
+ * This ensures smooth transitions when reconfiguring the player.
1133
+ */
1134
+ private PlayerConfig buildPlayerConfig(ReadableMap prop, PlayerConfig oldConfig) {
1135
+ PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
1136
+
1137
+ // Try to parse as JW config first
1138
+ JSONObject obj;
1139
+ PlayerConfig jwConfig = null;
1140
+ Boolean forceLegacy = prop.hasKey("forceLegacyConfig") ? prop.getBoolean("forceLegacyConfig") : false;
1141
+ Boolean isJwConfig = false;
936
1142
 
937
- // The entire config is different (other than the "playlist" key)
938
- this.setupPlayer(prop);
1143
+ if (!forceLegacy) {
1144
+ try {
1145
+ obj = MapUtil.toJSONObject(prop);
1146
+
1147
+ // Check for IMA config and log warning if IMA is disabled
1148
+ // Don't modify JSON - let parser handle it internally
1149
+ checkAndWarnImaConfig(obj, null);
1150
+
1151
+ jwConfig = JsonHelper.parseConfigJson(obj);
1152
+ isJwConfig = true;
1153
+ return jwConfig; // Return directly if valid JW config
1154
+ } catch (Exception ex) {
1155
+ Log.d(TAG, "Not a JW config format, using legacy builder: " + ex.getMessage());
1156
+ isJwConfig = false;
939
1157
  }
940
- } else {
941
- // No change
942
1158
  }
943
1159
 
944
- mConfig = prop;
1160
+ // Legacy config building
1161
+ configurePlaylist(configBuilder, prop);
1162
+ configureBasicSettings(configBuilder, prop);
1163
+ configureStyling(configBuilder, prop);
1164
+
1165
+ // Check and warn about IMA config, then conditionally configure advertising
1166
+ checkAndWarnImaConfig(null, prop);
1167
+ if (!shouldSkipAdvertising(prop)) {
1168
+ configureAdvertising(configBuilder, prop);
1169
+ }
1170
+
1171
+ configureUI(configBuilder, prop);
1172
+
1173
+ return configBuilder.build();
945
1174
  }
946
1175
 
1176
+ /**
1177
+ * Utility method to check if only a specific key differs between configs.
1178
+ * This is kept for potential future optimizations or debugging, but is no longer
1179
+ * used in the main setConfig flow since we now reconfigure the player for all changes.
1180
+ *
1181
+ * @deprecated Consider using reconfigurePlayer() for all config changes instead
1182
+ */
1183
+ @Deprecated
947
1184
  public boolean isOnlyDiff(ReadableMap prop, String keyName) {
1185
+ if (mConfig == null || prop == null) {
1186
+ return false;
1187
+ }
1188
+
948
1189
  // Convert ReadableMap to HashMap
949
1190
  Map<String, Object> mConfigMap = mConfig.toHashMap();
950
1191
  Map<String, Object> propMap = prop.toHashMap();
@@ -1052,12 +1293,23 @@ public class RNJWPlayerView extends RelativeLayout implements
1052
1293
  }
1053
1294
 
1054
1295
  private void configureUI(PlayerConfig.Builder configBuilder, ReadableMap prop) {
1296
+ // Handle controls property - default to true if not specified
1297
+ boolean controls = true; // Default to showing controls
1055
1298
  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);
1060
- }
1299
+ controls = prop.getBoolean("controls");
1300
+ }
1301
+
1302
+ if (!controls) {
1303
+ UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build();
1304
+ configBuilder.uiConfig(uiConfig);
1305
+ } else {
1306
+ // Explicitly show controls and ensure controls container is visible
1307
+ // This ensures controls work even if setControls() is called later
1308
+ UiConfig uiConfig = new UiConfig.Builder()
1309
+ .displayAllControls()
1310
+ .show(UiGroup.PLAYER_CONTROLS_CONTAINER)
1311
+ .build();
1312
+ configBuilder.uiConfig(uiConfig);
1061
1313
  }
1062
1314
 
1063
1315
  if (prop.hasKey("hideUIGroups")) {
@@ -1076,7 +1328,13 @@ public class RNJWPlayerView extends RelativeLayout implements
1076
1328
  }
1077
1329
  }
1078
1330
 
1079
- private void setupPlayer(ReadableMap prop) {
1331
+ /**
1332
+ * Creates a new player view and initializes it with the provided configuration.
1333
+ * This should only be called for initial setup or when full recreation is required.
1334
+ *
1335
+ * Note: This method calls destroyPlayer() first to ensure clean state.
1336
+ */
1337
+ private void createPlayerView(ReadableMap prop) {
1080
1338
  PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
1081
1339
 
1082
1340
  JSONObject obj;
@@ -1088,11 +1346,16 @@ public class RNJWPlayerView extends RelativeLayout implements
1088
1346
  if (!forceLegacy) {
1089
1347
  try {
1090
1348
  obj = MapUtil.toJSONObject(prop);
1349
+
1350
+ // Check for IMA config and log warning if IMA is disabled
1351
+ // Don't modify JSON - let parser handle it internally
1352
+ checkAndWarnImaConfig(obj, null);
1353
+
1091
1354
  jwConfig = JsonHelper.parseConfigJson(obj);
1092
1355
  isJwConfig = true;
1093
1356
  } catch (Exception ex) {
1094
- Log.e("RNJWPlayerView", ex.toString());
1095
- isJwConfig = false; // not a valid jw config. Try to setup in legacy
1357
+ Log.e(TAG, "Not a valid JW config format, falling back to legacy: " + ex.toString());
1358
+ isJwConfig = false;
1096
1359
  }
1097
1360
  }
1098
1361
 
@@ -1100,32 +1363,42 @@ public class RNJWPlayerView extends RelativeLayout implements
1100
1363
  configurePlaylist(configBuilder, prop);
1101
1364
  configureBasicSettings(configBuilder, prop);
1102
1365
  configureStyling(configBuilder, prop);
1103
- configureAdvertising(configBuilder, prop);
1366
+
1367
+ // Check and warn about IMA config, then conditionally configure advertising
1368
+ checkAndWarnImaConfig(null, prop);
1369
+ if (!shouldSkipAdvertising(prop)) {
1370
+ configureAdvertising(configBuilder, prop);
1371
+ }
1372
+
1104
1373
  configureUI(configBuilder, prop);
1105
1374
  }
1106
1375
 
1107
1376
  Context simpleContext = getNonBuggyContext(getReactContext(), getAppContext());
1108
1377
 
1378
+ // Ensure clean state before creating new player view
1109
1379
  this.destroyPlayer();
1110
1380
 
1381
+ // Create new player view
1111
1382
  mPlayerView = new RNJWPlayer(simpleContext);
1112
-
1113
1383
  mPlayerView.setFocusable(true);
1114
1384
  mPlayerView.setFocusableInTouchMode(true);
1115
1385
 
1116
- setLayoutParams(
1117
- new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1386
+ // Set layout parameters
1387
+ setLayoutParams(new ViewGroup.LayoutParams(
1388
+ ViewGroup.LayoutParams.MATCH_PARENT,
1389
+ ViewGroup.LayoutParams.MATCH_PARENT));
1118
1390
  mPlayerView.setLayoutParams(new LinearLayout.LayoutParams(
1119
1391
  LinearLayout.LayoutParams.MATCH_PARENT,
1120
1392
  LinearLayout.LayoutParams.MATCH_PARENT));
1393
+
1394
+ // Add to view hierarchy - React Native will handle layout
1121
1395
  addView(mPlayerView);
1122
1396
 
1123
- // Ensure we have a valid state before applying to the player
1124
- registry.setCurrentState(registry.getCurrentState()); // This is a hack to ensure player and view know the lifecycle state
1125
-
1397
+ // Get player instance
1126
1398
  mPlayer = mPlayerView.getPlayer(this);
1127
1399
 
1128
- if (prop.hasKey("controls")) { // Hack for controls hiding not working right away
1400
+ // Apply view-specific props
1401
+ if (prop.hasKey("controls")) {
1129
1402
  mPlayerView.getPlayer().setControls(prop.getBoolean("controls"));
1130
1403
  }
1131
1404
 
@@ -1139,7 +1412,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1139
1412
  }
1140
1413
 
1141
1414
  if (prop.hasKey("portraitOnExitFullScreen")) {
1142
- portraitOnExitFullScreen = prop.getBoolean("fullScreenOnLandscape");
1415
+ portraitOnExitFullScreen = prop.getBoolean("portraitOnExitFullScreen");
1143
1416
  }
1144
1417
 
1145
1418
  if (prop.hasKey("playerInModal")) {
@@ -1151,6 +1424,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1151
1424
  mPlayerView.exitFullScreenOnPortrait = exitFullScreenOnPortrait;
1152
1425
  }
1153
1426
 
1427
+ // Setup player with config
1154
1428
  if (isJwConfig) {
1155
1429
  mPlayer.setup(jwConfig);
1156
1430
  } else {
@@ -1158,6 +1432,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1158
1432
  mPlayer.setup(playerConfig);
1159
1433
  }
1160
1434
 
1435
+ // Configure PiP if enabled
1161
1436
  if (mActivity != null && prop.hasKey("pipEnabled")) {
1162
1437
  boolean pipEnabled = prop.getBoolean("pipEnabled");
1163
1438
  if (pipEnabled) {
@@ -1169,56 +1444,12 @@ public class RNJWPlayerView extends RelativeLayout implements
1169
1444
  }
1170
1445
  }
1171
1446
 
1172
- // Legacy
1173
- // This isn't the ideal way to do this on Android. All drawables/colors/themes shoudld
1174
- // be targed using styling. See `https://docs.jwplayer.com/players/docs/android-styling-guide`
1175
- // for more information on how best to override the JWP styles using XML. If you are unsure of a
1176
- // color/drawable/theme, open an `Ask` issue.
1177
- if (mColors != null) {
1178
- if (mColors.hasKey("backgroundColor")) {
1179
- mPlayerView.setBackgroundColor(Color.parseColor("#" + mColors.getString("backgroundColor")));
1180
- }
1181
-
1182
- if (mColors.hasKey("buttons")) {
1447
+ // Legacy styling support
1448
+ // NOTE: This isn't the ideal way to do this on Android. All drawables/colors/themes should
1449
+ // be targeted using styling. See https://docs.jwplayer.com/players/docs/android-styling-guide
1450
+ applyLegacyStyling();
1183
1451
 
1184
- }
1185
-
1186
- if (mColors.hasKey("timeslider")) {
1187
- CueMarkerSeekbar seekBar = findViewById(com.longtailvideo.jwplayer.R.id.controlbar_seekbar);
1188
- ReadableMap timeslider = mColors.getMap("timeslider");
1189
- if (timeslider != null) {
1190
- LayerDrawable progressDrawable = (LayerDrawable) seekBar.getProgressDrawable();
1191
-
1192
- if (timeslider.hasKey("progress")) {
1193
- // seekBar.getProgressDrawable().setColorFilter(Color.parseColor("#" +
1194
- // timeslider.getString("progress")), PorterDuff.Mode.SRC_IN);
1195
- Drawable processDrawable = progressDrawable.findDrawableByLayerId(android.R.id.progress);
1196
- processDrawable.setColorFilter(Color.parseColor("#" + timeslider.getString("progress")),
1197
- PorterDuff.Mode.SRC_IN);
1198
- }
1199
-
1200
- if (timeslider.hasKey("buffer")) {
1201
- Drawable secondaryProgressDrawable = progressDrawable
1202
- .findDrawableByLayerId(android.R.id.secondaryProgress);
1203
- secondaryProgressDrawable.setColorFilter(Color.parseColor("#" + timeslider.getString("buffer")),
1204
- PorterDuff.Mode.SRC_IN);
1205
- }
1206
-
1207
- if (timeslider.hasKey("rail")) {
1208
- Drawable backgroundDrawable = progressDrawable.findDrawableByLayerId(android.R.id.background);
1209
- backgroundDrawable.setColorFilter(Color.parseColor("#" + timeslider.getString("rail")),
1210
- PorterDuff.Mode.SRC_IN);
1211
- }
1212
-
1213
- if (timeslider.hasKey("thumb")) {
1214
- seekBar.getThumb().setColorFilter(Color.parseColor("#" + timeslider.getString("thumb")),
1215
- PorterDuff.Mode.SRC_IN);
1216
- }
1217
- }
1218
- }
1219
- }
1220
-
1221
- // Needed to handle volume control
1452
+ // Setup audio
1222
1453
  audioManager = (AudioManager) simpleContext.getSystemService(Context.AUDIO_SERVICE);
1223
1454
 
1224
1455
  if (prop.hasKey("backgroundAudioEnabled")) {
@@ -1228,12 +1459,61 @@ public class RNJWPlayerView extends RelativeLayout implements
1228
1459
  setupPlayerView(backgroundAudioEnabled, playlistItemCallbackEnabled);
1229
1460
 
1230
1461
  if (backgroundAudioEnabled) {
1231
- audioManager = (AudioManager) simpleContext.getSystemService(Context.AUDIO_SERVICE);
1232
1462
  mMediaServiceController = new MediaServiceController.Builder((AppCompatActivity) mActivity, mPlayer)
1233
1463
  .build();
1234
1464
  }
1235
1465
  }
1236
1466
 
1467
+ /**
1468
+ * Applies legacy color/styling customizations.
1469
+ * Extracted to separate method for clarity.
1470
+ */
1471
+ private void applyLegacyStyling() {
1472
+ if (mColors == null) {
1473
+ return;
1474
+ }
1475
+
1476
+ if (mColors.hasKey("backgroundColor")) {
1477
+ mPlayerView.setBackgroundColor(Color.parseColor("#" + mColors.getString("backgroundColor")));
1478
+ }
1479
+
1480
+ if (mColors.hasKey("timeslider")) {
1481
+ CueMarkerSeekbar seekBar = findViewById(com.longtailvideo.jwplayer.R.id.controlbar_seekbar);
1482
+ ReadableMap timeslider = mColors.getMap("timeslider");
1483
+ if (timeslider != null && seekBar != null) {
1484
+ LayerDrawable progressDrawable = (LayerDrawable) seekBar.getProgressDrawable();
1485
+
1486
+ if (timeslider.hasKey("progress")) {
1487
+ Drawable processDrawable = progressDrawable.findDrawableByLayerId(android.R.id.progress);
1488
+ processDrawable.setColorFilter(
1489
+ Color.parseColor("#" + timeslider.getString("progress")),
1490
+ PorterDuff.Mode.SRC_IN);
1491
+ }
1492
+
1493
+ if (timeslider.hasKey("buffer")) {
1494
+ Drawable secondaryProgressDrawable = progressDrawable
1495
+ .findDrawableByLayerId(android.R.id.secondaryProgress);
1496
+ secondaryProgressDrawable.setColorFilter(
1497
+ Color.parseColor("#" + timeslider.getString("buffer")),
1498
+ PorterDuff.Mode.SRC_IN);
1499
+ }
1500
+
1501
+ if (timeslider.hasKey("rail")) {
1502
+ Drawable backgroundDrawable = progressDrawable.findDrawableByLayerId(android.R.id.background);
1503
+ backgroundDrawable.setColorFilter(
1504
+ Color.parseColor("#" + timeslider.getString("rail")),
1505
+ PorterDuff.Mode.SRC_IN);
1506
+ }
1507
+
1508
+ if (timeslider.hasKey("thumb")) {
1509
+ seekBar.getThumb().setColorFilter(
1510
+ Color.parseColor("#" + timeslider.getString("thumb")),
1511
+ PorterDuff.Mode.SRC_IN);
1512
+ }
1513
+ }
1514
+ }
1515
+ }
1516
+
1237
1517
  // Audio Focus
1238
1518
 
1239
1519
  public void requestAudioFocus() {
@@ -55,6 +55,14 @@ public class Util {
55
55
  out.close();
56
56
  }
57
57
  }
58
+
59
+ // Check response code before reading
60
+ int responseCode = urlConnection.getResponseCode();
61
+
62
+ if (responseCode != HttpURLConnection.HTTP_OK) {
63
+ throw new IOException("HTTP POST failed with code " + responseCode);
64
+ }
65
+
58
66
  // Read and return the response body.
59
67
  InputStream inputStream = urlConnection.getInputStream();
60
68
  try {
@@ -62,6 +70,9 @@ public class Util {
62
70
  } finally {
63
71
  inputStream.close();
64
72
  }
73
+ } catch (IOException e) {
74
+ Log.e("Util", "❌ [HTTP POST] Exception: " + e.getMessage(), e);
75
+ throw e;
65
76
  } finally {
66
77
  if (urlConnection != null) {
67
78
  urlConnection.disconnect();
@@ -194,7 +205,8 @@ public class Util {
194
205
  }
195
206
 
196
207
  if (playlistItem.hasKey("authUrl")) {
197
- itemBuilder.mediaDrmCallback(new WidevineCallback(playlistItem.getString("authUrl")));
208
+ String authUrl = playlistItem.getString("authUrl");
209
+ itemBuilder.mediaDrmCallback(new WidevineCallback(authUrl));
198
210
  }
199
211
 
200
212
  if (playlistItem.hasKey("adSchedule")) {
@@ -0,0 +1,24 @@
1
+ package com.jwplayer.rnjwplayer;
2
+
3
+ import com.facebook.react.bridge.ReadableMap;
4
+ import com.jwplayer.pub.api.configuration.ads.AdvertisingConfig;
5
+ import com.jwplayer.pub.api.media.ads.AdBreak;
6
+
7
+ import java.util.List;
8
+
9
+ /**
10
+ * Stub implementation when IMA is disabled.
11
+ * Provides clear error messages for users attempting to use IMA without enabling it.
12
+ */
13
+ public class ImaHelper {
14
+
15
+ public static AdvertisingConfig configureImaOrDai(ReadableMap ads, List<AdBreak> adSchedule) {
16
+ // Note: adSchedule parameter is unused in stub - we always throw before using it
17
+ // Passing a valid adSchedule would cause a runtime exception if Google IMA is not enabled
18
+ throw new RuntimeException(
19
+ "Google IMA is not enabled. " +
20
+ "To use IMA ads, add 'RNJWPlayerUseGoogleIMA = true' to your app/build.gradle ext {} block."
21
+ );
22
+ }
23
+ }
24
+