@jwplayer/jwplayer-react-native 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -1
- package/RNJWPlayer.podspec +2 -2
- 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/build.gradle +1 -1
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +46 -6
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +134 -108
- package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +16 -0
- package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +10 -0
- package/badges/version.svg +1 -1
- package/index.d.ts +128 -7
- package/index.js +35 -0
- package/ios/RNJWPlayer/RNJWPlayerView.swift +125 -4
- package/ios/RNJWPlayer/RNJWPlayerViewController.swift +12 -0
- package/ios/RNJWPlayer/RNJWPlayerViewManager.m +3 -1
- package/ios/RNJWPlayer/RNJWPlayerViewManager.swift +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -275,11 +275,44 @@ Follow these steps to enable background audio sessions:
|
|
|
275
275
|
1. Set `backgroundAudioEnabled` to `true` in the `config`.
|
|
276
276
|
2. Ensure that background audio is set for [Android](https://docs.jwplayer.com/players/docs/android-enable-background-audio) or [iOS](https://docs.jwplayer.com/players/docs/ios-player-backgrounding-reference#configure-audio-playback).
|
|
277
277
|
|
|
278
|
+
#### Android Opt-Out Information
|
|
279
|
+
Suppose you are **not** using the background audio service on Android. In that case, you will need to modify the [manifest](https://github.com/jwplayer/jwplayer-react-native/blob/master/android/src/main/AndroidManifest.xml) from our library (either in the node_modules, or in your fork before compilation) so as not to register a service that you will never use. Google can and will reject your Play Store submission if this is skipped. See the modified manifest below, which shows what to remove (commented-out permissions and services). Additionally, once this is done, you can never set `backgroundAudioEnabled` to true on Android, or you will run into a fatal crash.
|
|
280
|
+
|
|
281
|
+
```xml
|
|
282
|
+
|
|
283
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
284
|
+
package="com.jwplayer.rnjwplayer">
|
|
285
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
286
|
+
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
287
|
+
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> -->
|
|
288
|
+
<uses-permission android:name="android.permission.BLUETOOTH"/>
|
|
289
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
|
290
|
+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
|
291
|
+
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
|
292
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
|
293
|
+
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> -->
|
|
294
|
+
<!-- READ_MEDIA_IMAGES, READ_MEDIA_VIDEO or READ_MEDIA_AUDIO.-->
|
|
295
|
+
|
|
296
|
+
<application>
|
|
297
|
+
<!-- <service
|
|
298
|
+
android:name="com.jwplayer.pub.api.background.MediaService"
|
|
299
|
+
android:foregroundServiceType="mediaPlayback"
|
|
300
|
+
android:exported="false">
|
|
301
|
+
<intent-filter>
|
|
302
|
+
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
|
303
|
+
</intent-filter>
|
|
304
|
+
</service> -->
|
|
305
|
+
</application>
|
|
306
|
+
|
|
307
|
+
</manifest>
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
|
|
278
311
|
<br /><br />
|
|
279
312
|
|
|
280
313
|
### Casting
|
|
281
314
|
|
|
282
|
-
[Android](#android-casting) | [iOS](#ios-casting)
|
|
315
|
+
[Android](#android-casting) | [iOS](#ios-casting) | [DRM Casting](#drm-casting)
|
|
283
316
|
|
|
284
317
|
JWP enables casting by default with a casting button.
|
|
285
318
|
|
|
@@ -342,6 +375,13 @@ typedef NS_ENUM(NSUInteger, GCKCastState) {
|
|
|
342
375
|
};
|
|
343
376
|
```
|
|
344
377
|
|
|
378
|
+
#### DRM Casting
|
|
379
|
+
Casting your DRM protected content requires some additional configuration and most likely a custom Chromecast Receiver.
|
|
380
|
+
|
|
381
|
+
See our Android and iOS documentation about creating ([iOS](https://docs.jwplayer.com/players/docs/ios-create-a-custom-receiver) / [Android](https://docs.jwplayer.com/players/docs/android-create-a-custom-receiver)) and sending data ([iOS](https://docs.jwplayer.com/players/docs/ios-enable-casting-to-chromecast-devices#send-custom-data-to-a-custom-receiver) / [Android](https://docs.jwplayer.com/players/docs/android-enable-casting-to-chromecast-devices#send-custom-data-to-a-custom-receiver)) for more information.
|
|
382
|
+
|
|
383
|
+
To send custom data to your receiver, use the `userInfo` prop and avoid using the reserved `sources` key, as the JWP SDK will append the DRM source information here for you to parse in your DRM enabled receiver.
|
|
384
|
+
|
|
345
385
|
<br /><br />
|
|
346
386
|
|
|
347
387
|
### DRM
|
package/RNJWPlayer.podspec
CHANGED
|
@@ -9,10 +9,10 @@ Pod::Spec.new do |s|
|
|
|
9
9
|
s.license = package['license']
|
|
10
10
|
s.authors = package['author']
|
|
11
11
|
s.homepage = package['homepage']
|
|
12
|
-
s.platform = :ios, "
|
|
12
|
+
s.platform = :ios, "15.0"
|
|
13
13
|
s.source = { :git => "https://github.com/jwplayer/jwplayer-react-native.git", :tag => "v#{s.version}" }
|
|
14
14
|
s.source_files = "ios/RNJWPlayer/*.{h,m,swift}"
|
|
15
|
-
s.dependency 'JWPlayerKit', '4.
|
|
15
|
+
s.dependency 'JWPlayerKit', '4.23.2'
|
|
16
16
|
s.dependency 'React-Core'
|
|
17
17
|
s.static_framework = true
|
|
18
18
|
s.info_plist = {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/android/build.gradle
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.jwplayer.rnjwplayer;
|
|
2
2
|
|
|
3
|
+
import android.util.Log;
|
|
4
|
+
|
|
3
5
|
import android.os.Handler;
|
|
4
6
|
import android.os.Looper;
|
|
5
7
|
|
|
@@ -13,17 +15,16 @@ import com.facebook.react.bridge.UIManager;
|
|
|
13
15
|
import com.facebook.react.bridge.ReadableMap;
|
|
14
16
|
import com.facebook.react.bridge.WritableArray;
|
|
15
17
|
import com.facebook.react.bridge.WritableMap;
|
|
16
|
-
import com.facebook.react.uimanager.IllegalViewOperationException;
|
|
17
|
-
import com.facebook.react.uimanager.NativeViewHierarchyManager;
|
|
18
|
-
import com.facebook.react.uimanager.UIBlock;
|
|
19
18
|
import com.facebook.react.uimanager.UIManagerHelper;
|
|
20
|
-
import com.facebook.react.uimanager.UIManagerModule;
|
|
21
19
|
import com.facebook.react.uimanager.common.UIManagerType;
|
|
22
20
|
import com.jwplayer.pub.api.JWPlayer;
|
|
23
21
|
import com.jwplayer.pub.api.PlayerState;
|
|
22
|
+
import com.jwplayer.pub.api.UiGroup;
|
|
24
23
|
import com.jwplayer.pub.api.configuration.PlayerConfig;
|
|
24
|
+
import com.jwplayer.pub.api.configuration.UiConfig;
|
|
25
25
|
import com.jwplayer.pub.api.media.adaptive.QualityLevel;
|
|
26
26
|
import com.jwplayer.pub.api.media.audio.AudioTrack;
|
|
27
|
+
|
|
27
28
|
import java.util.List;
|
|
28
29
|
|
|
29
30
|
public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
@@ -58,6 +59,21 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Creates a UiConfig that ensures PLAYER_CONTROLS_CONTAINER is always shown.
|
|
64
|
+
* If controls are not shown, the PLAYER_CONTROLS_CONTAINER UI Group is not displayed.
|
|
65
|
+
* This logic ensures that the PLAYER_CONTROLS_CONTAINER UI Group is displayed regardless if controls are shown or not.
|
|
66
|
+
* There is no way to recover controls if you do not show this UiGroup.
|
|
67
|
+
* But you are able to hide the controls still if it is shown.
|
|
68
|
+
*/
|
|
69
|
+
private UiConfig createUiConfigWithControlsContainer(JWPlayer player, UiConfig originalUiConfig) {
|
|
70
|
+
if (!player.getControls()) {
|
|
71
|
+
return new UiConfig.Builder(originalUiConfig).show(UiGroup.PLAYER_CONTROLS_CONTAINER).build();
|
|
72
|
+
} else {
|
|
73
|
+
return originalUiConfig;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
@ReactMethod
|
|
62
78
|
public void loadPlaylist(final int reactTag, final ReadableArray playlistItems) {
|
|
63
79
|
new Handler(Looper.getMainLooper()).post(() -> {
|
|
@@ -66,6 +82,8 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
66
82
|
JWPlayer player = playerView.mPlayerView.getPlayer();
|
|
67
83
|
|
|
68
84
|
PlayerConfig oldConfig = player.getConfig();
|
|
85
|
+
boolean wasFullscreen = player.getFullscreen();
|
|
86
|
+
UiConfig uiConfig = createUiConfigWithControlsContainer(player, oldConfig.getUiConfig());
|
|
69
87
|
PlayerConfig config = new PlayerConfig.Builder()
|
|
70
88
|
.autostart(oldConfig.getAutostart())
|
|
71
89
|
.nextUpOffset(oldConfig.getNextUpOffset())
|
|
@@ -75,7 +93,7 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
75
93
|
.displayTitle(oldConfig.getDisplayTitle())
|
|
76
94
|
.advertisingConfig(oldConfig.getAdvertisingConfig())
|
|
77
95
|
.stretching(oldConfig.getStretching())
|
|
78
|
-
.uiConfig(
|
|
96
|
+
.uiConfig(uiConfig)
|
|
79
97
|
.playlist(Util.createPlaylist(playlistItems))
|
|
80
98
|
.allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
|
|
81
99
|
.preload(oldConfig.getPreload())
|
|
@@ -85,6 +103,11 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
85
103
|
.build();
|
|
86
104
|
|
|
87
105
|
player.setup(config);
|
|
106
|
+
// if the player was fullscreen, set it to fullscreen again as the player is recreated
|
|
107
|
+
// The fullscreen view is still active but the internals don't know it is
|
|
108
|
+
if (wasFullscreen) {
|
|
109
|
+
player.setFullscreen(true, true);
|
|
110
|
+
}
|
|
88
111
|
}
|
|
89
112
|
});
|
|
90
113
|
}
|
|
@@ -97,6 +120,8 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
97
120
|
JWPlayer player = playerView.mPlayerView.getPlayer();
|
|
98
121
|
|
|
99
122
|
PlayerConfig oldConfig = player.getConfig();
|
|
123
|
+
boolean wasFullscreen = player.getFullscreen();
|
|
124
|
+
UiConfig uiConfig = createUiConfigWithControlsContainer(player, oldConfig.getUiConfig());
|
|
100
125
|
PlayerConfig config = new PlayerConfig.Builder()
|
|
101
126
|
.autostart(oldConfig.getAutostart())
|
|
102
127
|
.nextUpOffset(oldConfig.getNextUpOffset())
|
|
@@ -106,7 +131,7 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
106
131
|
.displayTitle(oldConfig.getDisplayTitle())
|
|
107
132
|
.advertisingConfig(oldConfig.getAdvertisingConfig())
|
|
108
133
|
.stretching(oldConfig.getStretching())
|
|
109
|
-
.uiConfig(
|
|
134
|
+
.uiConfig(uiConfig)
|
|
110
135
|
.playlistUrl(playlistUrl)
|
|
111
136
|
.allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
|
|
112
137
|
.preload(oldConfig.getPreload())
|
|
@@ -116,6 +141,11 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
116
141
|
.build();
|
|
117
142
|
|
|
118
143
|
player.setup(config);
|
|
144
|
+
// if the player was fullscreen, set it to fullscreen again as the player is recreated
|
|
145
|
+
// The fullscreen view is still active but the internals don't know it is
|
|
146
|
+
if (wasFullscreen) {
|
|
147
|
+
player.setFullscreen(true, true);
|
|
148
|
+
}
|
|
119
149
|
}
|
|
120
150
|
});
|
|
121
151
|
}
|
|
@@ -402,6 +432,16 @@ public class RNJWPlayerModule extends ReactContextBaseJavaModule {
|
|
|
402
432
|
});
|
|
403
433
|
}
|
|
404
434
|
|
|
435
|
+
@ReactMethod
|
|
436
|
+
/**
|
|
437
|
+
* Stub method for recreatePlayerWithConfig - this method is iOS only
|
|
438
|
+
* On Android, create a new player instance with the new configuration instead
|
|
439
|
+
*/
|
|
440
|
+
public void recreatePlayerWithConfig(final int reactTag, final ReadableMap config) {
|
|
441
|
+
// No-op on Android - this method is iOS only
|
|
442
|
+
Log.w("RNJWPlayer", "recreatePlayerWithConfig is not supported on Android. Create a new player instance with the new configuration instead.");
|
|
443
|
+
}
|
|
444
|
+
|
|
405
445
|
private int stateToInt(PlayerState playerState) {
|
|
406
446
|
switch (playerState) {
|
|
407
447
|
case IDLE:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.jwplayer.rnjwplayer;
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
import android.annotation.SuppressLint;
|
|
4
5
|
import android.app.Activity;
|
|
5
6
|
import android.app.ActivityManager;
|
|
6
7
|
import android.content.BroadcastReceiver;
|
|
@@ -372,9 +373,9 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
372
373
|
|
|
373
374
|
// If we are casting we need to break the cast session as there is no simple
|
|
374
375
|
// way to reconnect to an existing session if the player that created it is dead
|
|
375
|
-
|
|
376
|
+
|
|
376
377
|
// If this doesn't match your use case, using a single player object and load content
|
|
377
|
-
// into it rather than creating a new player for every piece of content.
|
|
378
|
+
// into it rather than creating a new player for every piece of content.
|
|
378
379
|
mPlayer.stop();
|
|
379
380
|
|
|
380
381
|
// send signal to JW SDK player is destroyed
|
|
@@ -537,7 +538,7 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
537
538
|
}
|
|
538
539
|
}
|
|
539
540
|
|
|
540
|
-
|
|
541
|
+
public void resolveNextPlaylistItem(ReadableMap playlistItem) {
|
|
541
542
|
if (itemUpdatePromise == null) {
|
|
542
543
|
return;
|
|
543
544
|
}
|
|
@@ -741,7 +742,7 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
741
742
|
public void run() {
|
|
742
743
|
// View may not have been removed properly (especially if returning from PiP)
|
|
743
744
|
mPlayerViewContainer.removeView(mPlayerView);
|
|
744
|
-
|
|
745
|
+
|
|
745
746
|
mPlayerViewContainer.addView(mPlayerView, new ViewGroup.LayoutParams(
|
|
746
747
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
747
748
|
ViewGroup.LayoutParams.MATCH_PARENT));
|
|
@@ -856,6 +857,7 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
856
857
|
}
|
|
857
858
|
}
|
|
858
859
|
|
|
860
|
+
@SuppressLint("UnspecifiedRegisterReceiverFlag")
|
|
859
861
|
private void registerReceiver() {
|
|
860
862
|
mReceiver = new PipHandlerReceiver();
|
|
861
863
|
IntentFilter intentFilter = new IntentFilter("onPictureInPictureModeChanged");
|
|
@@ -879,11 +881,28 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
879
881
|
|
|
880
882
|
}
|
|
881
883
|
|
|
884
|
+
/**
|
|
885
|
+
* Creates a UiConfig that ensures PLAYER_CONTROLS_CONTAINER is always shown.
|
|
886
|
+
* If controls are not shown, the PLAYER_CONTROLS_CONTAINER UI Group is not displayed.
|
|
887
|
+
* This logic ensures that the PLAYER_CONTROLS_CONTAINER UI Group is displayed regardless if controls are shown or not.
|
|
888
|
+
* There is no way to recover controls if you do not show this UiGroup.
|
|
889
|
+
* But you are able to hide the controls still if it is shown.
|
|
890
|
+
*/
|
|
891
|
+
private UiConfig createUiConfigWithControlsContainer(JWPlayer player, UiConfig originalUiConfig) {
|
|
892
|
+
if (!player.getControls()) {
|
|
893
|
+
return new UiConfig.Builder(originalUiConfig).show(UiGroup.PLAYER_CONTROLS_CONTAINER).build();
|
|
894
|
+
} else {
|
|
895
|
+
return originalUiConfig;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
882
899
|
public void setConfig(ReadableMap prop) {
|
|
883
900
|
if (mConfig == null || !mConfig.equals(prop)) {
|
|
884
901
|
if (mConfig != null && isOnlyDiff(prop, "playlist") && mPlayer != null) { // still safe check, even with JW
|
|
885
902
|
// JSON change
|
|
886
903
|
PlayerConfig oldConfig = mPlayer.getConfig();
|
|
904
|
+
boolean wasFullscreen = mPlayer.getFullscreen();
|
|
905
|
+
UiConfig uiConfig = createUiConfigWithControlsContainer(mPlayer, oldConfig.getUiConfig());
|
|
887
906
|
PlayerConfig config = new PlayerConfig.Builder()
|
|
888
907
|
.autostart(oldConfig.getAutostart())
|
|
889
908
|
.nextUpOffset(oldConfig.getNextUpOffset())
|
|
@@ -893,7 +912,7 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
893
912
|
.displayTitle(oldConfig.getDisplayTitle())
|
|
894
913
|
.advertisingConfig(oldConfig.getAdvertisingConfig())
|
|
895
914
|
.stretching(oldConfig.getStretching())
|
|
896
|
-
.uiConfig(
|
|
915
|
+
.uiConfig(uiConfig)
|
|
897
916
|
.playlist(Util.createPlaylist(mPlaylistProp))
|
|
898
917
|
.allowCrossProtocolRedirects(oldConfig.getAllowCrossProtocolRedirects())
|
|
899
918
|
.preload(oldConfig.getPreload())
|
|
@@ -903,6 +922,11 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
903
922
|
.build();
|
|
904
923
|
|
|
905
924
|
mPlayer.setup(config);
|
|
925
|
+
// if the player was fullscreen, set it to fullscreen again as the player is recreated
|
|
926
|
+
// The fullscreen view is still active but the internals don't know it is
|
|
927
|
+
if (wasFullscreen) {
|
|
928
|
+
mPlayer.setFullscreen(true, true);
|
|
929
|
+
}
|
|
906
930
|
} else {
|
|
907
931
|
if (prop.hasKey("license")) {
|
|
908
932
|
new LicenseUtil().setLicenseKey(getReactContext(), prop.getString("license"));
|
|
@@ -952,130 +976,132 @@ public class RNJWPlayerView extends RelativeLayout implements
|
|
|
952
976
|
.deepEquals(new ReadableArray[]{mPlaylistProp}, new ReadableArray[]{prop.getArray("playlist")});
|
|
953
977
|
}
|
|
954
978
|
|
|
955
|
-
private void
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
obj = MapUtil.toJSONObject(prop);
|
|
967
|
-
jwConfig = JsonHelper.parseConfigJson(obj);
|
|
968
|
-
isJwConfig = true;
|
|
969
|
-
} catch (Exception ex) {
|
|
970
|
-
Log.e("RNJWPlayerView", ex.toString());
|
|
971
|
-
isJwConfig = false; // not a valid jw config. Try to setup in legacy
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
if (!isJwConfig) {
|
|
976
|
-
// Legacy
|
|
977
|
-
if (playlistNotTheSame(prop)) {
|
|
978
|
-
List<PlaylistItem> playlist = new ArrayList<>();
|
|
979
|
-
mPlaylistProp = prop.getArray("playlist");
|
|
980
|
-
if (mPlaylistProp != null && mPlaylistProp.size() > 0) {
|
|
981
|
-
|
|
982
|
-
int j = 0;
|
|
983
|
-
while (mPlaylistProp.size() > j) {
|
|
984
|
-
ReadableMap playlistItem = mPlaylistProp.getMap(j);
|
|
985
|
-
|
|
986
|
-
PlaylistItem newPlayListItem = Util.getPlaylistItem((playlistItem));
|
|
987
|
-
playlist.add(newPlayListItem);
|
|
988
|
-
j++;
|
|
989
|
-
}
|
|
979
|
+
private void configurePlaylist(PlayerConfig.Builder configBuilder, ReadableMap prop) {
|
|
980
|
+
if (playlistNotTheSame(prop)) {
|
|
981
|
+
List<PlaylistItem> playlist = new ArrayList<>();
|
|
982
|
+
mPlaylistProp = prop.getArray("playlist");
|
|
983
|
+
if (mPlaylistProp != null && mPlaylistProp.size() > 0) {
|
|
984
|
+
int j = 0;
|
|
985
|
+
while (mPlaylistProp.size() > j) {
|
|
986
|
+
ReadableMap playlistItem = mPlaylistProp.getMap(j);
|
|
987
|
+
PlaylistItem newPlayListItem = Util.getPlaylistItem((playlistItem));
|
|
988
|
+
playlist.add(newPlayListItem);
|
|
989
|
+
j++;
|
|
990
990
|
}
|
|
991
|
-
|
|
992
|
-
configBuilder.playlist(playlist);
|
|
993
991
|
}
|
|
992
|
+
configBuilder.playlist(playlist);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
994
995
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
996
|
+
private void configureBasicSettings(PlayerConfig.Builder configBuilder, ReadableMap prop) {
|
|
997
|
+
if (prop.hasKey("autostart")) {
|
|
998
|
+
boolean autostart = prop.getBoolean("autostart");
|
|
999
|
+
configBuilder.autostart(autostart);
|
|
1000
|
+
}
|
|
1000
1001
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
configBuilder.nextUpOffset(offsetSeconds).nextUpOffsetPercentage(offsetPercentage);
|
|
1009
|
-
}
|
|
1002
|
+
if (prop.hasKey("nextUpStyle")) {
|
|
1003
|
+
ReadableMap nextUpStyle = prop.getMap("nextUpStyle");
|
|
1004
|
+
if (nextUpStyle != null && nextUpStyle.hasKey("offsetSeconds")
|
|
1005
|
+
&& nextUpStyle.hasKey("offsetPercentage")) {
|
|
1006
|
+
int offsetSeconds = prop.getInt("offsetSeconds");
|
|
1007
|
+
int offsetPercentage = prop.getInt("offsetPercentage");
|
|
1008
|
+
configBuilder.nextUpOffset(offsetSeconds).nextUpOffsetPercentage(offsetPercentage);
|
|
1010
1009
|
}
|
|
1010
|
+
}
|
|
1011
1011
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1012
|
+
if (prop.hasKey("repeat")) {
|
|
1013
|
+
boolean repeat = prop.getBoolean("repeat");
|
|
1014
|
+
configBuilder.repeat(repeat);
|
|
1015
|
+
}
|
|
1017
1016
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
boolean displayDescription = styling.getBoolean("displayDescription");
|
|
1024
|
-
configBuilder.displayDescription(displayDescription);
|
|
1025
|
-
}
|
|
1017
|
+
if (prop.hasKey("stretching")) {
|
|
1018
|
+
String stretching = prop.getString("stretching");
|
|
1019
|
+
configBuilder.stretching(stretching);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1026
1022
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1023
|
+
private void configureStyling(PlayerConfig.Builder configBuilder, ReadableMap prop) {
|
|
1024
|
+
if (prop.hasKey("styling")) {
|
|
1025
|
+
ReadableMap styling = prop.getMap("styling");
|
|
1026
|
+
if (styling != null) {
|
|
1027
|
+
if (styling.hasKey("displayDescription")) {
|
|
1028
|
+
boolean displayDescription = styling.getBoolean("displayDescription");
|
|
1029
|
+
configBuilder.displayDescription(displayDescription);
|
|
1030
|
+
}
|
|
1031
1031
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1032
|
+
if (styling.hasKey("displayTitle")) {
|
|
1033
|
+
boolean displayTitle = styling.getBoolean("displayTitle");
|
|
1034
|
+
configBuilder.displayTitle(displayTitle);
|
|
1035
1035
|
}
|
|
1036
|
-
}
|
|
1037
1036
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
ReadableMap ads = prop.getMap("advertising");
|
|
1041
|
-
AdvertisingConfig advertisingConfig = RNJWPlayerAds.getAdvertisingConfig(ads);
|
|
1042
|
-
if (advertisingConfig != null) {
|
|
1043
|
-
configBuilder.advertisingConfig(advertisingConfig);
|
|
1037
|
+
if (styling.hasKey("colors")) {
|
|
1038
|
+
mColors = styling.getMap("colors");
|
|
1044
1039
|
}
|
|
1045
1040
|
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1046
1043
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1044
|
+
private void configureAdvertising(PlayerConfig.Builder configBuilder, ReadableMap prop) {
|
|
1045
|
+
if (prop.hasKey("advertising")) {
|
|
1046
|
+
ReadableMap ads = prop.getMap("advertising");
|
|
1047
|
+
AdvertisingConfig advertisingConfig = RNJWPlayerAds.getAdvertisingConfig(ads);
|
|
1048
|
+
if (advertisingConfig != null) {
|
|
1049
|
+
configBuilder.advertisingConfig(advertisingConfig);
|
|
1051
1050
|
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1052
1053
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build();
|
|
1060
|
-
configBuilder.uiConfig(uiConfig);
|
|
1061
|
-
}
|
|
1054
|
+
private void configureUI(PlayerConfig.Builder configBuilder, ReadableMap prop) {
|
|
1055
|
+
if (prop.hasKey("controls")) {
|
|
1056
|
+
boolean controls = prop.getBoolean("controls");
|
|
1057
|
+
if (!controls) {
|
|
1058
|
+
UiConfig uiConfig = new UiConfig.Builder().hideAllControls().build();
|
|
1059
|
+
configBuilder.uiConfig(uiConfig);
|
|
1062
1060
|
}
|
|
1061
|
+
}
|
|
1063
1062
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
hideConfigBuilder.hide(uiGroup);
|
|
1073
|
-
}
|
|
1063
|
+
if (prop.hasKey("hideUIGroups")) {
|
|
1064
|
+
ReadableArray uiGroupsArray = prop.getArray("hideUIGroups");
|
|
1065
|
+
UiConfig.Builder hideConfigBuilder = new UiConfig.Builder().displayAllControls();
|
|
1066
|
+
for (int i = 0; i < uiGroupsArray.size(); i++) {
|
|
1067
|
+
if (uiGroupsArray.getType(i) == ReadableType.String) {
|
|
1068
|
+
UiGroup uiGroup = GROUP_TYPES.get(uiGroupsArray.getString(i));
|
|
1069
|
+
if (uiGroup != null) {
|
|
1070
|
+
hideConfigBuilder.hide(uiGroup);
|
|
1074
1071
|
}
|
|
1075
1072
|
}
|
|
1076
|
-
UiConfig hideJwControlbarUiConfig = hideConfigBuilder.build();
|
|
1077
|
-
configBuilder.uiConfig(hideJwControlbarUiConfig);
|
|
1078
1073
|
}
|
|
1074
|
+
UiConfig hideJwControlbarUiConfig = hideConfigBuilder.build();
|
|
1075
|
+
configBuilder.uiConfig(hideJwControlbarUiConfig);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
private void setupPlayer(ReadableMap prop) {
|
|
1080
|
+
PlayerConfig.Builder configBuilder = new PlayerConfig.Builder();
|
|
1081
|
+
|
|
1082
|
+
JSONObject obj;
|
|
1083
|
+
PlayerConfig jwConfig = null;
|
|
1084
|
+
Boolean forceLegacy = prop.hasKey("forceLegacyConfig") ? prop.getBoolean("forceLegacyConfig") : false;
|
|
1085
|
+
Boolean playlistItemCallbackEnabled = prop.hasKey("playlistItemCallbackEnabled") ? prop.getBoolean("playlistItemCallbackEnabled") : false;
|
|
1086
|
+
Boolean isJwConfig = false;
|
|
1087
|
+
|
|
1088
|
+
if (!forceLegacy) {
|
|
1089
|
+
try {
|
|
1090
|
+
obj = MapUtil.toJSONObject(prop);
|
|
1091
|
+
jwConfig = JsonHelper.parseConfigJson(obj);
|
|
1092
|
+
isJwConfig = true;
|
|
1093
|
+
} catch (Exception ex) {
|
|
1094
|
+
Log.e("RNJWPlayerView", ex.toString());
|
|
1095
|
+
isJwConfig = false; // not a valid jw config. Try to setup in legacy
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (!isJwConfig) {
|
|
1100
|
+
configurePlaylist(configBuilder, prop);
|
|
1101
|
+
configureBasicSettings(configBuilder, prop);
|
|
1102
|
+
configureStyling(configBuilder, prop);
|
|
1103
|
+
configureAdvertising(configBuilder, prop);
|
|
1104
|
+
configureUI(configBuilder, prop);
|
|
1079
1105
|
}
|
|
1080
1106
|
|
|
1081
1107
|
Context simpleContext = getNonBuggyContext(getReactContext(), getAppContext());
|
|
@@ -46,6 +46,22 @@ public class RNJWPlayerViewManager extends SimpleViewManager<RNJWPlayerView> {
|
|
|
46
46
|
view.mPlayerView.getPlayer().setControls(controls);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Recreates the player with a new configuration, handling cleanup and PiP state.
|
|
51
|
+
* This method ensures proper cleanup and state restoration during configuration changes.
|
|
52
|
+
*
|
|
53
|
+
* @param view The RNJWPlayerView instance
|
|
54
|
+
* @param config The new configuration to apply
|
|
55
|
+
*/
|
|
56
|
+
@ReactProp(name = "recreatePlayerWithConfig")
|
|
57
|
+
public void recreatePlayerWithConfig(RNJWPlayerView view, ReadableMap config) {
|
|
58
|
+
if (view == null || view.mPlayerView == null) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
view.mPlayerView.getPlayer().stop();
|
|
62
|
+
view.setConfig(config);
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
public Map getExportedCustomBubblingEventTypeConstants() {
|
|
50
66
|
return MapBuilder.builder()
|
|
51
67
|
.put(
|
|
@@ -17,6 +17,7 @@ import com.jwplayer.pub.api.media.captions.CaptionType;
|
|
|
17
17
|
import com.jwplayer.pub.api.media.playlists.MediaSource;
|
|
18
18
|
import com.jwplayer.pub.api.media.playlists.PlaylistItem;
|
|
19
19
|
|
|
20
|
+
import org.json.JSONException;
|
|
20
21
|
import org.json.JSONObject;
|
|
21
22
|
|
|
22
23
|
import java.io.IOException;
|
|
@@ -139,6 +140,15 @@ public class Util {
|
|
|
139
140
|
itemBuilder.description(desc);
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
if (playlistItem.hasKey("userInfo")) {
|
|
144
|
+
try {
|
|
145
|
+
JSONObject info = MapUtil.toJSONObject(playlistItem.getMap("userInfo"));
|
|
146
|
+
itemBuilder.userInfo(info);
|
|
147
|
+
} catch (JSONException e) {
|
|
148
|
+
Log.e("userInfo", "Error parsing `userInfo` from your playlist. Message: " + e.getLocalizedMessage());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
142
152
|
if (playlistItem.hasKey("image")) {
|
|
143
153
|
String image = playlistItem.getString("image");
|
|
144
154
|
itemBuilder.image(image);
|
package/badges/version.svg
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="version: 1.
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="90" height="20" role="img" aria-label="version: 1.2.0"><title>version: 1.2.0</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="90" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="51" height="20" fill="#555"/><rect x="51" width="39" height="20" fill="#007ec6"/><rect width="90" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="265" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="410">version</text><text x="265" y="140" transform="scale(.1)" fill="#fff" textLength="410">version</text><text aria-hidden="true" x="695" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="290">1.2.0</text><text x="695" y="140" transform="scale(.1)" fill="#fff" textLength="290">1.2.0</text></g></svg>
|
package/index.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
37
37
|
landscapeOnFullScreen?: boolean;
|
|
38
38
|
portraitOnExitFullScreen?: boolean;
|
|
39
39
|
exitFullScreenOnPortrait?: boolean;
|
|
40
|
+
enableLockScreenControls?: boolean;
|
|
41
|
+
pipEnabled?: boolean;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
type JwThumbnailPreview = 101 | 102 | 103;
|
|
@@ -172,6 +174,10 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
172
174
|
schedule?: { [key: string]: JwAdBreak };
|
|
173
175
|
imaDaiSettings?: JwImaDaiSettings;
|
|
174
176
|
httpheaders?: { [key: string]: string };
|
|
177
|
+
/**
|
|
178
|
+
* Data to be passed to Chromecast receiver (optional and typically used for DRM implementations)
|
|
179
|
+
*/
|
|
180
|
+
userInfo?: { [key: string]: any };
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
interface JwImaDaiSettings {
|
|
@@ -347,6 +353,12 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
347
353
|
recommendations?: string;
|
|
348
354
|
startTime?: number;
|
|
349
355
|
autostart?: boolean;
|
|
356
|
+
/**
|
|
357
|
+
* Data to be passed to Chromecast receiver (optional and typically used for DRM implementations)
|
|
358
|
+
*
|
|
359
|
+
* Only made available in legacy objects as there is no way to pass this otherwise
|
|
360
|
+
*/
|
|
361
|
+
userInfo?: { [key: string]: any };
|
|
350
362
|
}
|
|
351
363
|
type RelatedOnClicks = "play" | "link";
|
|
352
364
|
type RelatedOnCompletes = "show" | "hide" | "autoplay";
|
|
@@ -371,11 +383,15 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
371
383
|
buttons?: string;
|
|
372
384
|
backgroundColor?: string;
|
|
373
385
|
fontColor?: string;
|
|
374
|
-
timeslider?: {
|
|
386
|
+
timeslider?: {
|
|
387
|
+
thumb?: string;
|
|
388
|
+
rail?: string;
|
|
389
|
+
slider?: string;
|
|
390
|
+
};
|
|
375
391
|
};
|
|
376
392
|
font?: Font;
|
|
377
|
-
|
|
378
|
-
|
|
393
|
+
showTitle?: boolean;
|
|
394
|
+
showDesc?: boolean;
|
|
379
395
|
captionsStyle?: {
|
|
380
396
|
font?: Font;
|
|
381
397
|
fontColor?: string;
|
|
@@ -383,7 +399,7 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
383
399
|
highlightColor?: string;
|
|
384
400
|
edgeStyle?: EdgeStyles;
|
|
385
401
|
};
|
|
386
|
-
menuStyle
|
|
402
|
+
menuStyle?: {
|
|
387
403
|
font?: Font;
|
|
388
404
|
fontColor?: string;
|
|
389
405
|
backgroundColor?: string;
|
|
@@ -470,8 +486,12 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
470
486
|
fairplayCertUrl?: string;
|
|
471
487
|
contentUUID?: string;
|
|
472
488
|
viewOnly?: boolean;
|
|
473
|
-
enableLockScreenControls
|
|
474
|
-
pipEnabled
|
|
489
|
+
enableLockScreenControls?: boolean;
|
|
490
|
+
pipEnabled?: boolean;
|
|
491
|
+
offlineMessage?: string;
|
|
492
|
+
offlineImage?: string;
|
|
493
|
+
forceFullScreenOnLandscape?: boolean;
|
|
494
|
+
forceLandscapeOnFullScreen?: boolean;
|
|
475
495
|
}
|
|
476
496
|
interface BaseEvent<T> {
|
|
477
497
|
nativeEvent: T;
|
|
@@ -577,7 +597,54 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
577
597
|
onBeforeNextPlaylistItem?: (event: BaseEvent<PlaylistItemEventProps>) => void;
|
|
578
598
|
}
|
|
579
599
|
|
|
600
|
+
export const JWPlayerAdEvents: {
|
|
601
|
+
/// This event is reported when the ad break has come to an end.
|
|
602
|
+
JWAdEventTypeAdBreakEnd: 0;
|
|
603
|
+
/// This event is reported when the ad break has begun.
|
|
604
|
+
JWAdEventTypeAdBreakStart: 1;
|
|
605
|
+
/// This event is reported when the user taps the ad.
|
|
606
|
+
JWAdEventTypeClicked: 2;
|
|
607
|
+
/// This event is reported when the ad is done playing.
|
|
608
|
+
JWAdEventTypeComplete: 3;
|
|
609
|
+
/// This event is used to report the ad impression, supplying additional detailed information about the ad.
|
|
610
|
+
JWAdEventTypeImpression: 4;
|
|
611
|
+
/// This event reports meta data information associated with the ad.
|
|
612
|
+
JWAdEventTypeMeta: 5;
|
|
613
|
+
/// The event is reported when the ad pauses.
|
|
614
|
+
JWAdEventTypePause: 6;
|
|
615
|
+
/// This event is reported when the ad begins playing, even in the middle of the stream after it was paused.
|
|
616
|
+
JWAdEventTypePlay: 7;
|
|
617
|
+
/// The event reports data about the ad request, when the ad is about to be loaded.
|
|
618
|
+
JWAdEventTypeRequest: 8;
|
|
619
|
+
/// This event reports the schedule of ads across the currently playing content.
|
|
620
|
+
JWAdEventTypeSchedule: 9;
|
|
621
|
+
/// This event is reported when the user skips the ad.
|
|
622
|
+
JWAdEventTypeSkipped: 10;
|
|
623
|
+
/// This event is reported when the ad begins.
|
|
624
|
+
JWAdEventTypeStarted: 11;
|
|
625
|
+
/// This event relays information about ad companions.
|
|
626
|
+
JWAdEventTypeCompanion: 12;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
export const JWPlayerState: {
|
|
630
|
+
JWPlayerStateUnknown?: number;
|
|
631
|
+
JWPlayerStateIdle: number;
|
|
632
|
+
JWPlayerStateBuffering: number;
|
|
633
|
+
JWPlayerStatePlaying: number;
|
|
634
|
+
JWPlayerStatePaused: number;
|
|
635
|
+
JWPlayerStateComplete: number;
|
|
636
|
+
JWPlayerStateError: number | null;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
export const JWPlayerAdClients: {
|
|
640
|
+
JWAdClientJWPlayer: 0;
|
|
641
|
+
JWAdClientGoogleIMA: 1;
|
|
642
|
+
JWAdClientGoogleIMADAI: 2;
|
|
643
|
+
JWAdClientUnknown: 3;
|
|
644
|
+
};
|
|
645
|
+
|
|
580
646
|
export default class JWPlayer extends React.Component<PropsType> {
|
|
647
|
+
quite(): void;
|
|
581
648
|
pause(): void;
|
|
582
649
|
play(): void;
|
|
583
650
|
stop(): void;
|
|
@@ -591,6 +658,7 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
591
658
|
setControls(show: boolean): void;
|
|
592
659
|
setLockScreenControls(show: boolean): void;
|
|
593
660
|
seekTo(time: number): void;
|
|
661
|
+
changePlaylist(fileUrl: string): void;
|
|
594
662
|
/**
|
|
595
663
|
* Side load playlist items into an already setup player
|
|
596
664
|
* @param playlistItems `PlaylistItem` or `JwPlaylistItem`
|
|
@@ -602,7 +670,9 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
602
670
|
*/
|
|
603
671
|
loadPlaylistWithUrl(playlistUrl: string): void;
|
|
604
672
|
setFullscreen(fullScreen: boolean): void;
|
|
605
|
-
|
|
673
|
+
time(): Promise<number | null>;
|
|
674
|
+
position(): Promise<number | null>;
|
|
675
|
+
togglePIP(): void;
|
|
606
676
|
setUpCastController(): void;
|
|
607
677
|
presentCastDialog(): void;
|
|
608
678
|
connectedDevice(): Promise<CastingDevice | null>;
|
|
@@ -615,6 +685,57 @@ declare module "@jwplayer/jwplayer-react-native" {
|
|
|
615
685
|
setCurrentCaptions(index: number): void;
|
|
616
686
|
getCurrentCaptions(): Promise<number | null>;
|
|
617
687
|
setVisibility(visibility: boolean, controls: JWControlType[]): void;
|
|
688
|
+
/**
|
|
689
|
+
* Recreates the player with a new configuration, handling cleanup and PiP state.
|
|
690
|
+
*
|
|
691
|
+
* NOTE: This method is only available on iOS. On Android, create a new player instance
|
|
692
|
+
* with the new configuration instead.
|
|
693
|
+
*
|
|
694
|
+
* IMPORTANT: This method should only be called after the player has been properly
|
|
695
|
+
* initialized and is ready (i.e., after onPlayerReady has fired). Calling this
|
|
696
|
+
* method before the player is ready may lead to undefined behavior.
|
|
697
|
+
*
|
|
698
|
+
* This method performs a complete player recreation by:
|
|
699
|
+
* 1. Safely handling PiP state if active (waits for PiP to close)
|
|
700
|
+
* 2. Performing complete cleanup of the current player instance
|
|
701
|
+
* 3. Creating a new player instance with the provided config
|
|
702
|
+
*
|
|
703
|
+
* Use this method when you need to:
|
|
704
|
+
* - Switch between different DRM configurations
|
|
705
|
+
* - Handle content changes during PiP mode
|
|
706
|
+
* - Force a complete player recreation
|
|
707
|
+
*
|
|
708
|
+
* Do NOT use this method:
|
|
709
|
+
* - Before the player is ready (wait for onPlayerReady)
|
|
710
|
+
* - For simple playlist updates (use loadPlaylist instead)
|
|
711
|
+
* - When the player is not properly initialized
|
|
712
|
+
* - On Android (create a new player instance instead)
|
|
713
|
+
*
|
|
714
|
+
* @example
|
|
715
|
+
* ```typescript
|
|
716
|
+
* // Wait for player to be ready
|
|
717
|
+
* onPlayerReady={() => {
|
|
718
|
+
* // Now safe to use recreatePlayerWithConfig (iOS only)
|
|
719
|
+
* if (Platform.OS === 'ios') {
|
|
720
|
+
* playerRef.current?.recreatePlayerWithConfig({
|
|
721
|
+
* ...config,
|
|
722
|
+
* playlist: newPlaylist
|
|
723
|
+
* });
|
|
724
|
+
* } else {
|
|
725
|
+
* // On Android, create a new player instance
|
|
726
|
+
* setPlayerConfig({
|
|
727
|
+
* ...config,
|
|
728
|
+
* playlist: newPlaylist
|
|
729
|
+
* });
|
|
730
|
+
* }
|
|
731
|
+
* }}
|
|
732
|
+
* ```
|
|
733
|
+
*
|
|
734
|
+
* @platform ios
|
|
735
|
+
* @param config The new configuration to apply to the recreated player
|
|
736
|
+
* @throws May throw if called before player is ready or with invalid config
|
|
737
|
+
*/
|
|
738
|
+
recreatePlayerWithConfig(config: Config | JwConfig): void;
|
|
618
739
|
/**
|
|
619
740
|
* Only called inside `onBeforeNextPlaylistItem` callback, and once per callback
|
|
620
741
|
* @param playlistItem `PlaylistItem` or `JwPlaylistItem`
|
package/index.js
CHANGED
|
@@ -233,6 +233,7 @@ export default class JWPlayer extends Component {
|
|
|
233
233
|
desc: PropTypes.string,
|
|
234
234
|
mediaId: PropTypes.string,
|
|
235
235
|
autostart: PropTypes.bool,
|
|
236
|
+
userInfo: PropTypes.object,
|
|
236
237
|
recommendations: PropTypes.string,
|
|
237
238
|
tracks: PropTypes.arrayOf(
|
|
238
239
|
PropTypes.shape({
|
|
@@ -743,6 +744,40 @@ export default class JWPlayer extends Component {
|
|
|
743
744
|
}
|
|
744
745
|
}
|
|
745
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Recreates the player with a new configuration, handling cleanup and PiP state.
|
|
749
|
+
*
|
|
750
|
+
* IMPORTANT: This method should only be called after the player has been properly
|
|
751
|
+
* initialized and is ready (i.e., after onPlayerReady has fired). Calling this
|
|
752
|
+
* method before the player is ready may lead to undefined behavior.
|
|
753
|
+
*
|
|
754
|
+
* This method:
|
|
755
|
+
* 1. Safely handles PiP state if active
|
|
756
|
+
* 2. Performs complete cleanup of the current player instance
|
|
757
|
+
* 3. Creates a new player instance with the provided config
|
|
758
|
+
*
|
|
759
|
+
* Use this method when you need to:
|
|
760
|
+
* - Switch between different DRM configurations
|
|
761
|
+
* - Handle content changes during PiP mode
|
|
762
|
+
* - Force a complete player recreation
|
|
763
|
+
*
|
|
764
|
+
* @param {Config | JwConfig} config The new configuration to apply to the recreated player
|
|
765
|
+
* @throws {Error} May throw if called before player is ready or with invalid config
|
|
766
|
+
*/
|
|
767
|
+
recreatePlayerWithConfig(config) {
|
|
768
|
+
if (!config) {
|
|
769
|
+
console.warn('JWPlayer: Attempting to recreate player with null/undefined config');
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (RNJWPlayerManager) {
|
|
774
|
+
RNJWPlayerManager.recreatePlayerWithConfig(
|
|
775
|
+
this.getRNJWPlayerBridgeHandle(),
|
|
776
|
+
config
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
746
781
|
getRNJWPlayerBridgeHandle() {
|
|
747
782
|
return findNodeHandle(this[this.ref_key]);
|
|
748
783
|
}
|
|
@@ -275,9 +275,28 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
// Check if player is in PiP mode before loading new playlist
|
|
279
|
+
var isPipActive = false
|
|
280
|
+
var pipController: AVPictureInPictureController?
|
|
281
|
+
|
|
282
|
+
if let playerView = playerView {
|
|
283
|
+
pipController = playerView.pictureInPictureController
|
|
284
|
+
isPipActive = pipController?.isPictureInPictureActive ?? false
|
|
285
|
+
} else if let playerViewController = playerViewController {
|
|
286
|
+
pipController = playerViewController.playerView.pictureInPictureController
|
|
287
|
+
isPipActive = pipController?.isPictureInPictureActive ?? false
|
|
288
|
+
}
|
|
289
|
+
|
|
278
290
|
if let playerViewController = playerViewController {
|
|
279
|
-
|
|
291
|
+
// We must treat PiP mode differently and setup as a new config
|
|
292
|
+
// or else the player will become unresponsive
|
|
293
|
+
if isPipActive {
|
|
294
|
+
setNewConfig(config: config)
|
|
295
|
+
} else {
|
|
296
|
+
playerViewController.player.loadPlaylist(items: playlistArray)
|
|
297
|
+
}
|
|
280
298
|
} else if let playerView = playerView {
|
|
299
|
+
// If you use player only, consider doing a simpliar check for PiP as above
|
|
281
300
|
playerView.player.loadPlaylist(items: playlistArray)
|
|
282
301
|
} else {
|
|
283
302
|
setNewConfig(config: config)
|
|
@@ -288,6 +307,98 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
|
|
|
288
307
|
}
|
|
289
308
|
}
|
|
290
309
|
|
|
310
|
+
private var pendingPlayerConfig: [String: Any]?
|
|
311
|
+
private var playerConfigTimeout: Timer?
|
|
312
|
+
private let maxPendingTime: TimeInterval = 5.0 // Maximum time to wait for PiP to close
|
|
313
|
+
|
|
314
|
+
@objc func recreatePlayerWithConfig(_ config: [String: Any]) {
|
|
315
|
+
// Cancel any existing pending configuration
|
|
316
|
+
if pendingPlayerConfig != nil {
|
|
317
|
+
print("Warning: Overriding pending content switch")
|
|
318
|
+
playerConfigTimeout?.invalidate()
|
|
319
|
+
pendingPlayerConfig = nil
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Validate config
|
|
323
|
+
guard !config.isEmpty else {
|
|
324
|
+
print("Error: Empty config provided to recreatePlayerWithConfig")
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 1. Handle PiP state
|
|
329
|
+
var isPipActive = false
|
|
330
|
+
var pipController: AVPictureInPictureController?
|
|
331
|
+
|
|
332
|
+
if let playerView = playerView {
|
|
333
|
+
pipController = playerView.pictureInPictureController
|
|
334
|
+
isPipActive = pipController?.isPictureInPictureActive ?? false
|
|
335
|
+
} else if let playerViewController = playerViewController {
|
|
336
|
+
pipController = playerViewController.playerView.pictureInPictureController
|
|
337
|
+
isPipActive = pipController?.isPictureInPictureActive ?? false
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 2. If in PiP, store the config and exit PiP
|
|
341
|
+
if isPipActive {
|
|
342
|
+
guard let pipController = pipController else {
|
|
343
|
+
print("Warning: PiP appears active but controller is nil, proceeding with direct switch")
|
|
344
|
+
completePlayerReconfiguration(config: config)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
pendingPlayerConfig = config
|
|
349
|
+
|
|
350
|
+
// Set a timeout to prevent infinite waiting
|
|
351
|
+
playerConfigTimeout = Timer.scheduledTimer(withTimeInterval: maxPendingTime, repeats: false) { [weak self] _ in
|
|
352
|
+
guard let self = self else { return }
|
|
353
|
+
print("Warning: PiP close timeout reached, forcing content switch")
|
|
354
|
+
if let pendingConfig = self.pendingPlayerConfig {
|
|
355
|
+
self.pendingPlayerConfig = nil
|
|
356
|
+
self.completePlayerReconfiguration(config: pendingConfig)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Attempt to stop PiP
|
|
361
|
+
pipController.stopPictureInPicture()
|
|
362
|
+
|
|
363
|
+
} else {
|
|
364
|
+
completePlayerReconfiguration(config: config)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private func completePlayerReconfiguration(config: [String: Any]) {
|
|
369
|
+
// Clear any pending timeout
|
|
370
|
+
playerConfigTimeout?.invalidate()
|
|
371
|
+
playerConfigTimeout = nil
|
|
372
|
+
|
|
373
|
+
// Ensure we're on the main thread
|
|
374
|
+
if !Thread.isMainThread {
|
|
375
|
+
DispatchQueue.main.async { [weak self] in
|
|
376
|
+
self?.completePlayerReconfiguration(config: config)
|
|
377
|
+
}
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 1. Stop current playback safely
|
|
382
|
+
if let playerView = playerView {
|
|
383
|
+
let state = playerView.player.getState()
|
|
384
|
+
if state == .playing || state == .buffering {
|
|
385
|
+
playerView.player.stop()
|
|
386
|
+
}
|
|
387
|
+
} else if let playerViewController = playerViewController {
|
|
388
|
+
let state = playerViewController.player.getState()
|
|
389
|
+
if state == .playing || state == .buffering {
|
|
390
|
+
playerViewController.player.stop()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// 2. Reset player state
|
|
395
|
+
dismissPlayerViewController()
|
|
396
|
+
removePlayerView()
|
|
397
|
+
|
|
398
|
+
// 3. Set new config
|
|
399
|
+
setNewConfig(config: config)
|
|
400
|
+
}
|
|
401
|
+
|
|
291
402
|
func setNewConfig(config: [String : Any]) {
|
|
292
403
|
let forceLegacyConfig = config["forceLegacyConfig"] as? Bool?
|
|
293
404
|
let playlistItemCallback = config["playlistItemCallbackEnabled"] as? Bool?
|
|
@@ -653,8 +764,12 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
|
|
|
653
764
|
}
|
|
654
765
|
|
|
655
766
|
// Process other properties
|
|
656
|
-
if let mediaId = item["mediaId"] as? String {
|
|
657
|
-
|
|
767
|
+
if let mediaId = (item["mediaId"] as? String) ?? (item["mediaid"] as? String) {
|
|
768
|
+
itemBuilder.mediaId(mediaId)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if let userInfo = item["userInfo"] as? Dictionary<String, Any> {
|
|
772
|
+
itemBuilder.userInfo(userInfo)
|
|
658
773
|
}
|
|
659
774
|
|
|
660
775
|
if let title = item["title"] as? String {
|
|
@@ -1207,7 +1322,13 @@ class RNJWPlayerView: UIView, JWPlayerDelegate, JWPlayerStateDelegate,
|
|
|
1207
1322
|
}
|
|
1208
1323
|
|
|
1209
1324
|
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
|
|
1210
|
-
|
|
1325
|
+
// Handle any pending content switch
|
|
1326
|
+
if let config = pendingPlayerConfig {
|
|
1327
|
+
pendingPlayerConfig = nil
|
|
1328
|
+
DispatchQueue.main.async { [weak self] in
|
|
1329
|
+
self?.completePlayerReconfiguration(config: config)
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1211
1332
|
}
|
|
1212
1333
|
|
|
1213
1334
|
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
|
|
@@ -249,26 +249,38 @@ class RNJWPlayerViewController : JWPlayerViewController, JWPlayerViewControllerF
|
|
|
249
249
|
|
|
250
250
|
override func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
|
|
251
251
|
super.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
|
|
252
|
+
// Forward to parent view to handle pending config changes
|
|
253
|
+
parentView?.pictureInPictureControllerDidStopPictureInPicture(pictureInPictureController)
|
|
252
254
|
}
|
|
253
255
|
|
|
254
256
|
override func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
|
|
255
257
|
super.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
|
|
258
|
+
// Forward to parent view for logging/tracking
|
|
259
|
+
parentView?.pictureInPictureControllerDidStartPictureInPicture(pictureInPictureController)
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
override func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
|
|
259
263
|
super.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
|
|
264
|
+
// Forward to parent view for logging/tracking
|
|
265
|
+
parentView?.pictureInPictureControllerWillStopPictureInPicture(pictureInPictureController)
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
override func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
|
|
263
269
|
super.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
|
|
270
|
+
// Forward to parent view for error handling
|
|
271
|
+
parentView?.pictureInPictureController(pictureInPictureController, failedToStartPictureInPictureWithError: error)
|
|
264
272
|
}
|
|
265
273
|
|
|
266
274
|
override func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
|
|
267
275
|
super.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
|
|
276
|
+
// Forward to parent view for logging/tracking
|
|
277
|
+
parentView?.pictureInPictureControllerWillStartPictureInPicture(pictureInPictureController)
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
override func pictureInPictureController(_ pictureInPictureController:AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler:@escaping (Bool) -> Void) {
|
|
271
281
|
super.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
|
|
282
|
+
// Forward to parent view
|
|
283
|
+
parentView?.pictureInPictureController(pictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
// MARK: - JWPlayer State Delegate
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#import "RCTViewManager.h"
|
|
5
5
|
#endif
|
|
6
6
|
|
|
7
|
-
#import <JWPlayerKit/JWPlayerKit-
|
|
7
|
+
#import <JWPlayerKit/JWPlayerKit-Swift.h>
|
|
8
8
|
|
|
9
9
|
#import "RCTUIManager.h"
|
|
10
10
|
|
|
@@ -136,6 +136,8 @@ RCT_EXTERN_METHOD(reset)
|
|
|
136
136
|
|
|
137
137
|
RCT_EXTERN_METHOD(loadPlaylist: (nonnull NSNumber *)reactTag: (nonnull NSArray *)playlist)
|
|
138
138
|
|
|
139
|
+
RCT_EXTERN_METHOD(recreatePlayerWithConfig: (nonnull NSNumber *)reactTag: (nonnull NSDictionary *)config)
|
|
140
|
+
|
|
139
141
|
RCT_EXTERN_METHOD(loadPlaylistWithUrl: (nonnull NSNumber *)reactTag: (nonnull NSString *)playlist)
|
|
140
142
|
|
|
141
143
|
RCT_EXTERN_METHOD(setFullscreen: (nonnull NSNumber *)reactTag: (BOOL)fullscreen)
|
|
@@ -552,6 +552,19 @@ class RNJWPlayerViewManager: RCTViewManager {
|
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
+
@objc func recreatePlayerWithConfig(_ reactTag: NSNumber, _ config: NSDictionary) {
|
|
556
|
+
DispatchQueue.main.async {
|
|
557
|
+
guard let view = self.bridge?.uiManager.view(
|
|
558
|
+
forReactTag: reactTag
|
|
559
|
+
) as? RNJWPlayerView else {
|
|
560
|
+
print("Invalid view returned from registry, expecting RNJWPlayerView")
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
view.recreatePlayerWithConfig(config as? [String: Any] ?? [:])
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
555
568
|
@objc func loadPlaylistWithUrl(_ reactTag: NSNumber, _ playlistString: String) {
|
|
556
569
|
DispatchQueue.main.async {
|
|
557
570
|
guard let view = self.getPlayerView(reactTag: reactTag) else {
|