@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.
- package/README.md +114 -21
- package/RNJWPlayer.podspec +1 -1
- package/android/build.gradle +31 -5
- package/android/src/ima/java/com/jwplayer/rnjwplayer/ImaHelper.java +143 -0
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerAds.java +41 -129
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +19 -4
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +385 -105
- package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +13 -1
- package/android/src/noima/java/com/jwplayer/rnjwplayer/ImaHelper.java +24 -0
- package/badges/version.svg +1 -1
- package/docs/CONFIG-REFERENCE.md +747 -0
- package/docs/MIGRATION-GUIDE.md +617 -0
- package/docs/PLATFORM-DIFFERENCES.md +693 -0
- package/docs/props.md +15 -3
- package/index.d.ts +207 -249
- package/ios/RNJWPlayer/RNJWPlayerView.swift +278 -21
- package/ios/RNJWPlayer/RNJWPlayerViewController.swift +33 -16
- package/package.json +2 -2
- package/types/advertising.d.ts +514 -0
- package/types/index.d.ts +21 -0
- package/types/legacy.d.ts +82 -0
- package/types/platform-specific.d.ts +641 -0
- package/types/playlist.d.ts +410 -0
- package/types/unified-config.d.ts +591 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
- 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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
938
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
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("
|
|
1095
|
-
isJwConfig = false;
|
|
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
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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("
|
|
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
|
|
1174
|
-
// be
|
|
1175
|
-
|
|
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
|
-
|
|
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
|
+
|