@jwplayer/jwplayer-react-native 1.2.0 → 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 (32) 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 +19 -4
  5. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +270 -105
  6. package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +13 -1
  7. package/badges/version.svg +1 -1
  8. package/docs/CONFIG-REFERENCE.md +747 -0
  9. package/docs/MIGRATION-GUIDE.md +617 -0
  10. package/docs/PLATFORM-DIFFERENCES.md +693 -0
  11. package/docs/props.md +15 -3
  12. package/index.d.ts +207 -249
  13. package/ios/RNJWPlayer/RNJWPlayerView.swift +278 -21
  14. package/ios/RNJWPlayer/RNJWPlayerViewController.swift +33 -16
  15. package/package.json +2 -2
  16. package/types/advertising.d.ts +514 -0
  17. package/types/index.d.ts +21 -0
  18. package/types/legacy.d.ts +82 -0
  19. package/types/platform-specific.d.ts +641 -0
  20. package/types/playlist.d.ts +410 -0
  21. package/types/unified-config.d.ts +591 -0
  22. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  23. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  24. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  25. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  26. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  27. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  28. package/android/.gradle/8.9/gc.properties +0 -0
  29. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  30. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  31. package/android/.gradle/vcs-1/gc.properties +0 -0
  32. 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,174 @@ 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();
923
-
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
- }
926
+ // Set license key if provided
927
+ if (prop.hasKey("license")) {
928
+ new LicenseUtil().setLicenseKey(getReactContext(), prop.getString("license"));
930
929
  } 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");
935
- }
930
+ Log.e(TAG, "JW SDK license not set");
931
+ }
936
932
 
937
- // The entire config is different (other than the "playlist" key)
938
- 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);
939
949
  }
940
- } else {
941
- // No change
942
950
  }
943
951
 
944
952
  mConfig = prop;
945
953
  }
946
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
947
1080
  public boolean isOnlyDiff(ReadableMap prop, String keyName) {
1081
+ if (mConfig == null || prop == null) {
1082
+ return false;
1083
+ }
1084
+
948
1085
  // Convert ReadableMap to HashMap
949
1086
  Map<String, Object> mConfigMap = mConfig.toHashMap();
950
1087
  Map<String, Object> propMap = prop.toHashMap();
@@ -1052,12 +1189,23 @@ public class RNJWPlayerView extends RelativeLayout implements
1052
1189
  }
1053
1190
 
1054
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
1055
1194
  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
- }
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);
1061
1209
  }
1062
1210
 
1063
1211
  if (prop.hasKey("hideUIGroups")) {
@@ -1076,7 +1224,13 @@ public class RNJWPlayerView extends RelativeLayout implements
1076
1224
  }
1077
1225
  }
1078
1226
 
1079
- private void setupPlayer(ReadableMap prop) {
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) {
1080
1234
  PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
1081
1235
 
1082
1236
  JSONObject obj;
@@ -1091,8 +1245,8 @@ public class RNJWPlayerView extends RelativeLayout implements
1091
1245
  jwConfig = JsonHelper.parseConfigJson(obj);
1092
1246
  isJwConfig = true;
1093
1247
  } catch (Exception ex) {
1094
- Log.e("RNJWPlayerView", ex.toString());
1095
- isJwConfig = false; // not a valid jw config. Try to setup in legacy
1248
+ Log.e(TAG, "Not a valid JW config format, falling back to legacy: " + ex.toString());
1249
+ isJwConfig = false;
1096
1250
  }
1097
1251
  }
1098
1252
 
@@ -1106,26 +1260,30 @@ public class RNJWPlayerView extends RelativeLayout implements
1106
1260
 
1107
1261
  Context simpleContext = getNonBuggyContext(getReactContext(), getAppContext());
1108
1262
 
1263
+ // Ensure clean state before creating new player view
1109
1264
  this.destroyPlayer();
1110
1265
 
1266
+ // Create new player view
1111
1267
  mPlayerView = new RNJWPlayer(simpleContext);
1112
-
1113
1268
  mPlayerView.setFocusable(true);
1114
1269
  mPlayerView.setFocusableInTouchMode(true);
1115
1270
 
1116
- setLayoutParams(
1117
- 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));
1118
1275
  mPlayerView.setLayoutParams(new LinearLayout.LayoutParams(
1119
1276
  LinearLayout.LayoutParams.MATCH_PARENT,
1120
1277
  LinearLayout.LayoutParams.MATCH_PARENT));
1278
+
1279
+ // Add to view hierarchy - React Native will handle layout
1121
1280
  addView(mPlayerView);
1122
1281
 
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
-
1282
+ // Get player instance
1126
1283
  mPlayer = mPlayerView.getPlayer(this);
1127
1284
 
1128
- if (prop.hasKey("controls")) { // Hack for controls hiding not working right away
1285
+ // Apply view-specific props
1286
+ if (prop.hasKey("controls")) {
1129
1287
  mPlayerView.getPlayer().setControls(prop.getBoolean("controls"));
1130
1288
  }
1131
1289
 
@@ -1139,7 +1297,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1139
1297
  }
1140
1298
 
1141
1299
  if (prop.hasKey("portraitOnExitFullScreen")) {
1142
- portraitOnExitFullScreen = prop.getBoolean("fullScreenOnLandscape");
1300
+ portraitOnExitFullScreen = prop.getBoolean("portraitOnExitFullScreen");
1143
1301
  }
1144
1302
 
1145
1303
  if (prop.hasKey("playerInModal")) {
@@ -1151,6 +1309,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1151
1309
  mPlayerView.exitFullScreenOnPortrait = exitFullScreenOnPortrait;
1152
1310
  }
1153
1311
 
1312
+ // Setup player with config
1154
1313
  if (isJwConfig) {
1155
1314
  mPlayer.setup(jwConfig);
1156
1315
  } else {
@@ -1158,6 +1317,7 @@ public class RNJWPlayerView extends RelativeLayout implements
1158
1317
  mPlayer.setup(playerConfig);
1159
1318
  }
1160
1319
 
1320
+ // Configure PiP if enabled
1161
1321
  if (mActivity != null && prop.hasKey("pipEnabled")) {
1162
1322
  boolean pipEnabled = prop.getBoolean("pipEnabled");
1163
1323
  if (pipEnabled) {
@@ -1169,56 +1329,12 @@ public class RNJWPlayerView extends RelativeLayout implements
1169
1329
  }
1170
1330
  }
1171
1331
 
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")) {
1183
-
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
- }
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();
1199
1336
 
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
1337
+ // Setup audio
1222
1338
  audioManager = (AudioManager) simpleContext.getSystemService(Context.AUDIO_SERVICE);
1223
1339
 
1224
1340
  if (prop.hasKey("backgroundAudioEnabled")) {
@@ -1228,12 +1344,61 @@ public class RNJWPlayerView extends RelativeLayout implements
1228
1344
  setupPlayerView(backgroundAudioEnabled, playlistItemCallbackEnabled);
1229
1345
 
1230
1346
  if (backgroundAudioEnabled) {
1231
- audioManager = (AudioManager) simpleContext.getSystemService(Context.AUDIO_SERVICE);
1232
1347
  mMediaServiceController = new MediaServiceController.Builder((AppCompatActivity) mActivity, mPlayer)
1233
1348
  .build();
1234
1349
  }
1235
1350
  }
1236
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
+
1237
1402
  // Audio Focus
1238
1403
 
1239
1404
  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")) {
@@ -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.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>
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="version: 1.3.0"><title>version: 1.3.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.3.0</text><text x="695" y="140" transform="scale(.1)" fill="#fff" textLength="290">1.3.0</text></g></svg>