@capgo/capacitor-video-player 8.1.24 → 8.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 +11 -2
- package/android/src/main/java/com/capgo/videoplayer/FullscreenExoPlayerFragment.java +80 -32
- package/android/src/main/java/com/capgo/videoplayer/VideoPlayer.java +3 -4
- package/android/src/main/java/com/capgo/videoplayer/VideoPlayerPlugin.java +63 -15
- package/android/src/main/java/com/capgo/videoplayer/VideoSubtitleTrack.java +12 -0
- package/dist/docs.json +34 -2
- package/dist/esm/definitions.d.ts +22 -0
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +16 -17
- package/ios/Sources/VideoPlayerPlugin/HLSSubtitleResourceLoader.swift +105 -36
- package/ios/Sources/VideoPlayerPlugin/ProgressiveVideoPlayerItemFactory.swift +29 -7
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +45 -18
- package/ios/Sources/VideoPlayerPlugin/VideoSubtitleTrack.swift +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -482,8 +482,9 @@ Show again a previously hidden player UI (native fullscreen).
|
|
|
482
482
|
| **`mode`** | <code>string</code> | Player mode - "fullscreen" - "embedded" (Web only) |
|
|
483
483
|
| **`url`** | <code>string</code> | The url of the video to play |
|
|
484
484
|
| **`chromecastUrl`** | <code>string</code> | Optional override URL used when casting to Chromecast (iOS, Android). This lets you cast an alternative rendition/manifest (e.g. MPEG-DASH + Widevine) while keeping the on-device playback URL unchanged (e.g. HLS + FairPlay on iOS). |
|
|
485
|
-
| **`subtitle`** | <code>string</code> | The url of subtitle associated with the video
|
|
486
|
-
| **`language`** | <code>string</code> | The language of subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers
|
|
485
|
+
| **`subtitle`** | <code>string</code> | The url of subtitle associated with the video Prefer `subtitles` when providing more than one track. Kept for backward compatibility. |
|
|
486
|
+
| **`language`** | <code>string</code> | The language of subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers Prefer `subtitles` when providing more than one track. Kept for backward compatibility. |
|
|
487
|
+
| **`subtitles`** | <code>VideoSubtitle[]</code> | Multiple subtitle tracks (iOS, Android). When provided, takes precedence over `subtitle` / `language`. |
|
|
487
488
|
| **`subtitleOptions`** | <code><a href="#subtitleoptions">SubTitleOptions</a></code> | SubTitle Options |
|
|
488
489
|
| **`playerId`** | <code>string</code> | Id of DIV Element parent of the player |
|
|
489
490
|
| **`rate`** | <code>number</code> | Initial playing rate |
|
|
@@ -506,6 +507,14 @@ Show again a previously hidden player UI (native fullscreen).
|
|
|
506
507
|
| **`drm`** | <code><a href="#drmoptions">DrmOptions</a></code> | DRM configuration for protected content (iOS: FairPlay, Android: Widevine) When casting, Widevine's `certificateUrl` (license URL) is also forwarded to Chromecast receivers as `media.customData.laurl`. |
|
|
507
508
|
|
|
508
509
|
|
|
510
|
+
#### VideoSubtitle
|
|
511
|
+
|
|
512
|
+
| Prop | Type | Description |
|
|
513
|
+
| -------------- | ------------------- | ------------------------------------------------------------------------------------------------- |
|
|
514
|
+
| **`subtitle`** | <code>string</code> | The url of the subtitle file (vtt, srt, ...) |
|
|
515
|
+
| **`language`** | <code>string</code> | The language of the subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers |
|
|
516
|
+
|
|
517
|
+
|
|
509
518
|
#### SubTitleOptions
|
|
510
519
|
|
|
511
520
|
| Prop | Type | Description |
|
|
@@ -114,6 +114,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
114
114
|
public String chromecastUrl;
|
|
115
115
|
public Float videoRate;
|
|
116
116
|
public String playerId;
|
|
117
|
+
public List<VideoSubtitleTrack> subtitleTracks;
|
|
117
118
|
public String subTitle;
|
|
118
119
|
public String language;
|
|
119
120
|
public JSObject subTitleOptions;
|
|
@@ -154,6 +155,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
154
155
|
private long playbackPosition = 0;
|
|
155
156
|
private Uri uri = null;
|
|
156
157
|
private Uri sturi = null;
|
|
158
|
+
private List<Uri> subtitleUris = new ArrayList<>();
|
|
157
159
|
private ProgressBar Pbar;
|
|
158
160
|
private View view;
|
|
159
161
|
private ImageButton closeBtn;
|
|
@@ -397,15 +399,31 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
397
399
|
|
|
398
400
|
if (!isInternal) {
|
|
399
401
|
uri = Uri.parse(videoPath);
|
|
400
|
-
|
|
402
|
+
subtitleUris = new ArrayList<>();
|
|
403
|
+
if (subtitleTracks != null) {
|
|
404
|
+
for (VideoSubtitleTrack track : subtitleTracks) {
|
|
405
|
+
if (track != null && track.url != null && !track.url.isEmpty()) {
|
|
406
|
+
subtitleUris.add(Uri.parse(track.url));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
sturi = subtitleUris.isEmpty() ? null : subtitleUris.get(0);
|
|
411
|
+
if (sturi == null && subTitle != null && !subTitle.isEmpty()) {
|
|
412
|
+
sturi = Uri.parse(subTitle);
|
|
413
|
+
subtitleUris.add(sturi);
|
|
414
|
+
}
|
|
401
415
|
|
|
402
|
-
stForeColor = subTitleOptions
|
|
403
|
-
|
|
404
|
-
|
|
416
|
+
stForeColor = subTitleOptions != null && subTitleOptions.has("foregroundColor")
|
|
417
|
+
? subTitleOptions.getString("foregroundColor")
|
|
418
|
+
: "rgba(255,255,255,1)";
|
|
419
|
+
stBackColor = subTitleOptions != null && subTitleOptions.has("backgroundColor")
|
|
420
|
+
? subTitleOptions.getString("backgroundColor")
|
|
421
|
+
: "rgba(0,0,0,1)";
|
|
422
|
+
stFontSize = subTitleOptions != null && subTitleOptions.has("fontSize") ? subTitleOptions.getInteger("fontSize") : 16;
|
|
405
423
|
// get video type
|
|
406
424
|
vType = getVideoType(uri);
|
|
407
425
|
Log.v(TAG, "display url: " + uri);
|
|
408
|
-
Log.v(TAG, "display subtitle
|
|
426
|
+
Log.v(TAG, "display subtitle urls: " + subtitleUris.size());
|
|
409
427
|
Log.v(TAG, "display isTV: " + isTV);
|
|
410
428
|
Log.v(TAG, "display vType: " + vType);
|
|
411
429
|
}
|
|
@@ -648,7 +666,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
648
666
|
}
|
|
649
667
|
|
|
650
668
|
isInPictureInPictureMode = activity.isInPictureInPictureMode();
|
|
651
|
-
if (
|
|
669
|
+
if (!subtitleUris.isEmpty()) {
|
|
652
670
|
setSubtitle(true);
|
|
653
671
|
}
|
|
654
672
|
play();
|
|
@@ -688,7 +706,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
688
706
|
if (inPictureInPictureMode) {
|
|
689
707
|
linearLayout.setVisibility(View.INVISIBLE);
|
|
690
708
|
styledPlayerView.setUseController(false);
|
|
691
|
-
if (
|
|
709
|
+
if (!subtitleUris.isEmpty()) {
|
|
692
710
|
setSubtitle(true);
|
|
693
711
|
}
|
|
694
712
|
return;
|
|
@@ -698,7 +716,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
698
716
|
if (showControls) {
|
|
699
717
|
styledPlayerView.setUseController(true);
|
|
700
718
|
}
|
|
701
|
-
if (
|
|
719
|
+
if (!subtitleUris.isEmpty()) {
|
|
702
720
|
setSubtitle(false);
|
|
703
721
|
}
|
|
704
722
|
}
|
|
@@ -979,8 +997,8 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
979
997
|
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, "jeep-exoplayer-plugin");
|
|
980
998
|
mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri));
|
|
981
999
|
// Get the subtitles if any
|
|
982
|
-
if (
|
|
983
|
-
mediaSource =
|
|
1000
|
+
if (!subtitleUris.isEmpty()) {
|
|
1001
|
+
mediaSource = getSubTitles(mediaSource, subtitleUris, dataSourceFactory);
|
|
984
1002
|
}
|
|
985
1003
|
return mediaSource;
|
|
986
1004
|
}
|
|
@@ -1045,8 +1063,8 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1045
1063
|
mediaSource = new SsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem);
|
|
1046
1064
|
}
|
|
1047
1065
|
// Get the subtitles if any
|
|
1048
|
-
if (
|
|
1049
|
-
mediaSource =
|
|
1066
|
+
if (!subtitleUris.isEmpty()) {
|
|
1067
|
+
mediaSource = getSubTitles(mediaSource, subtitleUris, dataSourceFactory);
|
|
1050
1068
|
}
|
|
1051
1069
|
return mediaSource;
|
|
1052
1070
|
}
|
|
@@ -1101,22 +1119,22 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1101
1119
|
return ret;
|
|
1102
1120
|
}
|
|
1103
1121
|
|
|
1104
|
-
private MediaSource
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
String mimeType = getMimeType(sturi);
|
|
1122
|
+
private MediaSource getSubTitles(MediaSource mediaSource, List<Uri> subtitleUris, DataSource.Factory dataSourceFactory) {
|
|
1123
|
+
if (subtitleUris == null || subtitleUris.isEmpty()) {
|
|
1124
|
+
return mediaSource;
|
|
1125
|
+
}
|
|
1109
1126
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
subConfig,
|
|
1113
|
-
C.TIME_UNSET
|
|
1114
|
-
);
|
|
1127
|
+
MediaSource[] mediaSources = new MediaSource[subtitleUris.size() + 1];
|
|
1128
|
+
mediaSources[0] = mediaSource;
|
|
1115
1129
|
|
|
1116
|
-
|
|
1130
|
+
for (int i = 0; i < subtitleUris.size(); i++) {
|
|
1131
|
+
Uri trackUri = subtitleUris.get(i);
|
|
1132
|
+
String mimeType = getMimeType(trackUri);
|
|
1133
|
+
MediaItem.SubtitleConfiguration subConfig = buildSubtitleConfiguration(trackUri, mimeType, i);
|
|
1134
|
+
mediaSources[i + 1] = new SingleSampleMediaSource.Factory(dataSourceFactory).createMediaSource(subConfig, C.TIME_UNSET);
|
|
1135
|
+
}
|
|
1117
1136
|
|
|
1118
|
-
|
|
1119
|
-
return mediaSource;
|
|
1137
|
+
return new MergingMediaSource(mediaSources);
|
|
1120
1138
|
}
|
|
1121
1139
|
|
|
1122
1140
|
private String getMimeType(Uri sturi) {
|
|
@@ -1573,8 +1591,9 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1573
1591
|
if (mimeType != null) {
|
|
1574
1592
|
builder.setMimeType(mimeType);
|
|
1575
1593
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1594
|
+
List<MediaItem.SubtitleConfiguration> castSubtitles = buildSubtitleConfigurationsForCast();
|
|
1595
|
+
if (!castSubtitles.isEmpty()) {
|
|
1596
|
+
builder.setSubtitleConfigurations(castSubtitles);
|
|
1578
1597
|
}
|
|
1579
1598
|
return builder.build();
|
|
1580
1599
|
}
|
|
@@ -1591,16 +1610,45 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1591
1610
|
return artwork != null && !artwork.isEmpty();
|
|
1592
1611
|
}
|
|
1593
1612
|
|
|
1594
|
-
private MediaItem.SubtitleConfiguration
|
|
1595
|
-
|
|
1613
|
+
private List<MediaItem.SubtitleConfiguration> buildSubtitleConfigurationsForCast() {
|
|
1614
|
+
List<MediaItem.SubtitleConfiguration> configurations = new ArrayList<>();
|
|
1615
|
+
for (int i = 0; i < subtitleUris.size(); i++) {
|
|
1616
|
+
Uri trackUri = subtitleUris.get(i);
|
|
1617
|
+
if (trackUri != null && isNetworkUri(trackUri)) {
|
|
1618
|
+
configurations.add(buildSubtitleConfiguration(trackUri, getMimeType(trackUri), i));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
return configurations;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
private String languageForTrack(int index) {
|
|
1625
|
+
if (subtitleTracks != null && index >= 0 && index < subtitleTracks.size()) {
|
|
1626
|
+
String trackLanguage = subtitleTracks.get(index).language;
|
|
1627
|
+
if (trackLanguage != null && !trackLanguage.isEmpty()) {
|
|
1628
|
+
return trackLanguage;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (language != null && !language.isEmpty()) {
|
|
1632
|
+
return language;
|
|
1633
|
+
}
|
|
1634
|
+
return "en";
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
private MediaItem.SubtitleConfiguration buildSubtitleConfiguration(Uri subtitleUri, String mimeType, int index) {
|
|
1638
|
+
String trackLanguage = languageForTrack(index);
|
|
1639
|
+
String languageLabel = Locale.forLanguageTag(trackLanguage).getDisplayLanguage();
|
|
1640
|
+
if (languageLabel == null || languageLabel.isEmpty()) {
|
|
1641
|
+
languageLabel = trackLanguage;
|
|
1642
|
+
}
|
|
1643
|
+
int selectionFlags = index == 0 ? C.SELECTION_FLAG_DEFAULT : 0;
|
|
1596
1644
|
return new MediaItem.SubtitleConfiguration.Builder(subtitleUri)
|
|
1597
1645
|
.setMimeType(mimeType)
|
|
1598
1646
|
.setUri(subtitleUri)
|
|
1599
|
-
.setId(
|
|
1647
|
+
.setId(subtitleUri.toString())
|
|
1600
1648
|
.setLabel(languageLabel)
|
|
1601
1649
|
.setRoleFlags(C.ROLE_FLAG_CAPTION)
|
|
1602
|
-
.setSelectionFlags(
|
|
1603
|
-
.setLanguage(
|
|
1650
|
+
.setSelectionFlags(selectionFlags)
|
|
1651
|
+
.setLanguage(trackLanguage)
|
|
1604
1652
|
.build();
|
|
1605
1653
|
}
|
|
1606
1654
|
|
|
@@ -3,6 +3,7 @@ package com.capgo.videoplayer;
|
|
|
3
3
|
import android.content.Context;
|
|
4
4
|
import com.capgo.videoplayer.PickerVideo.PickerVideoFragment;
|
|
5
5
|
import com.getcapacitor.JSObject;
|
|
6
|
+
import java.util.List;
|
|
6
7
|
|
|
7
8
|
public class VideoPlayer {
|
|
8
9
|
|
|
@@ -22,8 +23,7 @@ public class VideoPlayer {
|
|
|
22
23
|
Boolean bkModeEnabled,
|
|
23
24
|
Boolean showControls,
|
|
24
25
|
String displayMode,
|
|
25
|
-
|
|
26
|
-
String language,
|
|
26
|
+
List<VideoSubtitleTrack> subtitleTracks,
|
|
27
27
|
JSObject subTitleOptions,
|
|
28
28
|
JSObject headers,
|
|
29
29
|
String title,
|
|
@@ -48,8 +48,7 @@ public class VideoPlayer {
|
|
|
48
48
|
fsFragment.bkModeEnabled = bkModeEnabled;
|
|
49
49
|
fsFragment.showControls = showControls;
|
|
50
50
|
fsFragment.displayMode = displayMode;
|
|
51
|
-
fsFragment.
|
|
52
|
-
fsFragment.language = language;
|
|
51
|
+
fsFragment.subtitleTracks = subtitleTracks;
|
|
53
52
|
fsFragment.subTitleOptions = subTitleOptions;
|
|
54
53
|
fsFragment.headers = headers;
|
|
55
54
|
fsFragment.title = title;
|
|
@@ -21,6 +21,7 @@ import com.capgo.videoplayer.PickerVideo.PickerVideoFragment;
|
|
|
21
21
|
import com.capgo.videoplayer.Utilities.FilesUtils;
|
|
22
22
|
import com.capgo.videoplayer.Utilities.FragmentUtils;
|
|
23
23
|
import com.getcapacitor.Bridge;
|
|
24
|
+
import com.getcapacitor.JSArray;
|
|
24
25
|
import com.getcapacitor.JSObject;
|
|
25
26
|
import com.getcapacitor.PermissionState;
|
|
26
27
|
import com.getcapacitor.Plugin;
|
|
@@ -29,8 +30,12 @@ import com.getcapacitor.PluginMethod;
|
|
|
29
30
|
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
30
31
|
import com.getcapacitor.annotation.Permission;
|
|
31
32
|
import com.getcapacitor.annotation.PermissionCallback;
|
|
33
|
+
import java.util.ArrayList;
|
|
32
34
|
import java.util.HashMap;
|
|
35
|
+
import java.util.List;
|
|
33
36
|
import java.util.Map;
|
|
37
|
+
import org.json.JSONException;
|
|
38
|
+
import org.json.JSONObject;
|
|
34
39
|
|
|
35
40
|
@CapacitorPlugin(
|
|
36
41
|
name = "VideoPlayer",
|
|
@@ -41,7 +46,7 @@ import java.util.Map;
|
|
|
41
46
|
)
|
|
42
47
|
public class VideoPlayerPlugin extends Plugin {
|
|
43
48
|
|
|
44
|
-
private final String pluginVersion = "8.
|
|
49
|
+
private final String pluginVersion = "8.2.0";
|
|
45
50
|
|
|
46
51
|
// Permission alias constants
|
|
47
52
|
private static final String PERMISSION_DENIED_ERROR = "Unable to access media videos, user denied permission request";
|
|
@@ -84,6 +89,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
84
89
|
private String playerId;
|
|
85
90
|
private String subtitle = "";
|
|
86
91
|
private String language = "";
|
|
92
|
+
private List<VideoSubtitleTrack> subtitleTracks = new ArrayList<>();
|
|
87
93
|
private JSObject subTitleOptions;
|
|
88
94
|
private JSObject drmOptions;
|
|
89
95
|
private final JSObject ret = new JSObject();
|
|
@@ -201,6 +207,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
201
207
|
if (call.getData().has("language")) {
|
|
202
208
|
language = call.getString("language");
|
|
203
209
|
}
|
|
210
|
+
subtitleTracks = parseSubtitleTracks(call);
|
|
204
211
|
subTitleOptions = new JSObject();
|
|
205
212
|
if (call.getData().has("subtitleOptions")) {
|
|
206
213
|
subTitleOptions = call.getObject("subtitleOptions");
|
|
@@ -988,14 +995,16 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
988
995
|
} else {
|
|
989
996
|
// get the videoPath
|
|
990
997
|
videoPath = filesUtils.getFilePath(url);
|
|
991
|
-
|
|
992
|
-
if (subtitle != null && subtitle.length() > 0) {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
998
|
+
List<VideoSubtitleTrack> resolvedSubtitleTracks = resolveSubtitleTracks(subtitleTracks);
|
|
999
|
+
if (resolvedSubtitleTracks.isEmpty() && subtitle != null && subtitle.length() > 0) {
|
|
1000
|
+
String path = filesUtils.getFilePath(subtitle);
|
|
1001
|
+
if (path != null) {
|
|
1002
|
+
resolvedSubtitleTracks.add(new VideoSubtitleTrack(path, language));
|
|
1003
|
+
}
|
|
996
1004
|
}
|
|
1005
|
+
subTitlePath = resolvedSubtitleTracks.isEmpty() ? null : resolvedSubtitleTracks.get(0).url;
|
|
997
1006
|
Log.v(TAG, "*** calculated videoPath: " + videoPath);
|
|
998
|
-
Log.v(TAG, "*** calculated
|
|
1007
|
+
Log.v(TAG, "*** calculated subtitleTracks: " + resolvedSubtitleTracks.size());
|
|
999
1008
|
if (videoPath != null) {
|
|
1000
1009
|
createFullScreenFragment(
|
|
1001
1010
|
call,
|
|
@@ -1008,8 +1017,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1008
1017
|
bkModeEnabled,
|
|
1009
1018
|
showControls,
|
|
1010
1019
|
displayMode,
|
|
1011
|
-
|
|
1012
|
-
language,
|
|
1020
|
+
resolvedSubtitleTracks,
|
|
1013
1021
|
subTitleOptions,
|
|
1014
1022
|
headers,
|
|
1015
1023
|
title,
|
|
@@ -1175,8 +1183,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1175
1183
|
bkModeEnabled,
|
|
1176
1184
|
showControls,
|
|
1177
1185
|
displayMode,
|
|
1178
|
-
|
|
1179
|
-
null,
|
|
1186
|
+
new ArrayList<>(),
|
|
1180
1187
|
null,
|
|
1181
1188
|
headers,
|
|
1182
1189
|
title,
|
|
@@ -1205,6 +1212,49 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1205
1212
|
);
|
|
1206
1213
|
}
|
|
1207
1214
|
|
|
1215
|
+
private List<VideoSubtitleTrack> parseSubtitleTracks(PluginCall call) {
|
|
1216
|
+
List<VideoSubtitleTrack> tracks = new ArrayList<>();
|
|
1217
|
+
if (call.getData().has("subtitles")) {
|
|
1218
|
+
try {
|
|
1219
|
+
JSArray subtitles = call.getArray("subtitles");
|
|
1220
|
+
if (subtitles != null) {
|
|
1221
|
+
for (int i = 0; i < subtitles.length(); i++) {
|
|
1222
|
+
JSONObject entry = subtitles.getJSONObject(i);
|
|
1223
|
+
String trackUrl = entry.optString("subtitle", "");
|
|
1224
|
+
if (trackUrl == null || trackUrl.isEmpty()) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
String trackLanguage = entry.optString("language", "");
|
|
1228
|
+
tracks.add(new VideoSubtitleTrack(trackUrl, trackLanguage));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
} catch (JSONException e) {
|
|
1232
|
+
Log.w(TAG, "Failed to parse subtitles array", e);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (tracks.isEmpty() && subtitle != null && subtitle.length() > 0) {
|
|
1236
|
+
tracks.add(new VideoSubtitleTrack(subtitle, language));
|
|
1237
|
+
}
|
|
1238
|
+
return tracks;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
private List<VideoSubtitleTrack> resolveSubtitleTracks(List<VideoSubtitleTrack> tracks) {
|
|
1242
|
+
List<VideoSubtitleTrack> resolved = new ArrayList<>();
|
|
1243
|
+
if (tracks == null) {
|
|
1244
|
+
return resolved;
|
|
1245
|
+
}
|
|
1246
|
+
for (VideoSubtitleTrack track : tracks) {
|
|
1247
|
+
if (track == null || track.url == null || track.url.isEmpty()) {
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
String resolvedPath = filesUtils.getFilePath(track.url);
|
|
1251
|
+
if (resolvedPath != null) {
|
|
1252
|
+
resolved.add(new VideoSubtitleTrack(resolvedPath, track.language));
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return resolved;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1208
1258
|
private void createFullScreenFragment(
|
|
1209
1259
|
final PluginCall call,
|
|
1210
1260
|
String videoPath,
|
|
@@ -1216,8 +1266,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1216
1266
|
Boolean bkModeEnabled,
|
|
1217
1267
|
Boolean showControls,
|
|
1218
1268
|
String displayMode,
|
|
1219
|
-
|
|
1220
|
-
String language,
|
|
1269
|
+
List<VideoSubtitleTrack> subtitleTracks,
|
|
1221
1270
|
JSObject subTitleOptions,
|
|
1222
1271
|
JSObject headers,
|
|
1223
1272
|
String title,
|
|
@@ -1243,8 +1292,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1243
1292
|
bkModeEnabled,
|
|
1244
1293
|
showControls,
|
|
1245
1294
|
displayMode,
|
|
1246
|
-
|
|
1247
|
-
language,
|
|
1295
|
+
subtitleTracks,
|
|
1248
1296
|
subTitleOptions,
|
|
1249
1297
|
headers,
|
|
1250
1298
|
title,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package com.capgo.videoplayer;
|
|
2
|
+
|
|
3
|
+
public class VideoSubtitleTrack {
|
|
4
|
+
|
|
5
|
+
public final String url;
|
|
6
|
+
public final String language;
|
|
7
|
+
|
|
8
|
+
public VideoSubtitleTrack(String url, String language) {
|
|
9
|
+
this.url = url;
|
|
10
|
+
this.language = language != null ? language : "";
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/docs.json
CHANGED
|
@@ -396,17 +396,26 @@
|
|
|
396
396
|
{
|
|
397
397
|
"name": "subtitle",
|
|
398
398
|
"tags": [],
|
|
399
|
-
"docs": "The url of subtitle associated with the video",
|
|
399
|
+
"docs": "The url of subtitle associated with the video\n\nPrefer `subtitles` when providing more than one track.\nKept for backward compatibility.",
|
|
400
400
|
"complexTypes": [],
|
|
401
401
|
"type": "string | undefined"
|
|
402
402
|
},
|
|
403
403
|
{
|
|
404
404
|
"name": "language",
|
|
405
405
|
"tags": [],
|
|
406
|
-
"docs": "The language of subtitle\nsee https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers",
|
|
406
|
+
"docs": "The language of subtitle\nsee https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n\nPrefer `subtitles` when providing more than one track.\nKept for backward compatibility.",
|
|
407
407
|
"complexTypes": [],
|
|
408
408
|
"type": "string | undefined"
|
|
409
409
|
},
|
|
410
|
+
{
|
|
411
|
+
"name": "subtitles",
|
|
412
|
+
"tags": [],
|
|
413
|
+
"docs": "Multiple subtitle tracks (iOS, Android).\nWhen provided, takes precedence over `subtitle` / `language`.",
|
|
414
|
+
"complexTypes": [
|
|
415
|
+
"VideoSubtitle"
|
|
416
|
+
],
|
|
417
|
+
"type": "VideoSubtitle[] | undefined"
|
|
418
|
+
},
|
|
410
419
|
{
|
|
411
420
|
"name": "subtitleOptions",
|
|
412
421
|
"tags": [],
|
|
@@ -553,6 +562,29 @@
|
|
|
553
562
|
}
|
|
554
563
|
]
|
|
555
564
|
},
|
|
565
|
+
{
|
|
566
|
+
"name": "VideoSubtitle",
|
|
567
|
+
"slug": "videosubtitle",
|
|
568
|
+
"docs": "",
|
|
569
|
+
"tags": [],
|
|
570
|
+
"methods": [],
|
|
571
|
+
"properties": [
|
|
572
|
+
{
|
|
573
|
+
"name": "subtitle",
|
|
574
|
+
"tags": [],
|
|
575
|
+
"docs": "The url of the subtitle file (vtt, srt, ...)",
|
|
576
|
+
"complexTypes": [],
|
|
577
|
+
"type": "string"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
"name": "language",
|
|
581
|
+
"tags": [],
|
|
582
|
+
"docs": "The language of the subtitle\nsee https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers",
|
|
583
|
+
"complexTypes": [],
|
|
584
|
+
"type": "string | undefined"
|
|
585
|
+
}
|
|
586
|
+
]
|
|
587
|
+
},
|
|
556
588
|
{
|
|
557
589
|
"name": "SubTitleOptions",
|
|
558
590
|
"slug": "subtitleoptions",
|
|
@@ -121,13 +121,24 @@ export interface capVideoPlayerOptions {
|
|
|
121
121
|
chromecastUrl?: string;
|
|
122
122
|
/**
|
|
123
123
|
* The url of subtitle associated with the video
|
|
124
|
+
*
|
|
125
|
+
* Prefer `subtitles` when providing more than one track.
|
|
126
|
+
* Kept for backward compatibility.
|
|
124
127
|
*/
|
|
125
128
|
subtitle?: string;
|
|
126
129
|
/**
|
|
127
130
|
* The language of subtitle
|
|
128
131
|
* see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers
|
|
132
|
+
*
|
|
133
|
+
* Prefer `subtitles` when providing more than one track.
|
|
134
|
+
* Kept for backward compatibility.
|
|
129
135
|
*/
|
|
130
136
|
language?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Multiple subtitle tracks (iOS, Android).
|
|
139
|
+
* When provided, takes precedence over `subtitle` / `language`.
|
|
140
|
+
*/
|
|
141
|
+
subtitles?: VideoSubtitle[];
|
|
131
142
|
/**
|
|
132
143
|
* SubTitle Options
|
|
133
144
|
*/
|
|
@@ -319,6 +330,17 @@ export interface capVideoPlayerResult {
|
|
|
319
330
|
*/
|
|
320
331
|
message?: string;
|
|
321
332
|
}
|
|
333
|
+
export interface VideoSubtitle {
|
|
334
|
+
/**
|
|
335
|
+
* The url of the subtitle file (vtt, srt, ...)
|
|
336
|
+
*/
|
|
337
|
+
subtitle: string;
|
|
338
|
+
/**
|
|
339
|
+
* The language of the subtitle
|
|
340
|
+
* see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers
|
|
341
|
+
*/
|
|
342
|
+
language?: string;
|
|
343
|
+
}
|
|
322
344
|
export interface SubTitleOptions {
|
|
323
345
|
/**
|
|
324
346
|
* Foreground Color in RGBA (default rgba(255,255,255,1)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface VideoPlayerPlugin {\n /**\n * Initialize a video player\n *\n */\n initPlayer(options: capVideoPlayerOptions): Promise<capVideoPlayerResult>;\n /**\n * Return if a given playerId is playing\n *\n */\n isPlaying(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Play the current video from a given playerId\n *\n */\n play(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Pause the current video from a given playerId\n *\n */\n pause(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the duration of the current video from a given playerId\n *\n */\n getDuration(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the current time of the current video from a given playerId\n *\n */\n getCurrentTime(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the current time to seek the current video to from a given playerId\n *\n */\n setCurrentTime(options: capVideoTimeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the volume of the current video from a given playerId\n *\n */\n getVolume(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the volume of the current video to from a given playerId\n *\n */\n setVolume(options: capVideoVolumeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the muted of the current video from a given playerId\n *\n */\n getMuted(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the muted of the current video to from a given playerId\n *\n */\n setMuted(options: capVideoMutedOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the rate of the current video from a given playerId\n *\n */\n setRate(options: capVideoRateOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the rate of the current video from a given playerId\n *\n */\n getRate(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Stop all players playing\n *\n */\n stopAllPlayers(): Promise<capVideoPlayerResult>;\n /**\n * Show controller\n *\n */\n showController(): Promise<capVideoPlayerResult>;\n /**\n * isControllerIsFullyVisible\n *\n */\n isControllerIsFullyVisible(): Promise<capVideoPlayerResult>;\n /**\n * Exit player\n *\n */\n exitPlayer(): Promise<capVideoPlayerResult>;\n /**\n * Hide the currently presented player UI without stopping playback (native fullscreen).\n *\n * Call `showPlayer()` to show it again.\n */\n hidePlayer(): Promise<capVideoPlayerResult>;\n /**\n * Show again a previously hidden player UI (native fullscreen).\n */\n showPlayer(): Promise<capVideoPlayerResult>;\n}\nexport interface capEchoOptions {\n /**\n * String to be echoed\n */\n\n value?: string;\n}\nexport interface capVideoPlayerOptions {\n /**\n * Player mode\n * - \"fullscreen\"\n * - \"embedded\" (Web only)\n */\n mode?: string;\n /**\n * The url of the video to play\n */\n url?: string;\n /**\n * Optional override URL used when casting to Chromecast (iOS, Android).\n *\n * This lets you cast an alternative rendition/manifest (e.g. MPEG-DASH + Widevine)\n * while keeping the on-device playback URL unchanged (e.g. HLS + FairPlay on iOS).\n */\n chromecastUrl?: string;\n /**\n * The url of subtitle associated with the video\n */\n subtitle?: string;\n /**\n * The language of subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n */\n language?: string;\n /**\n * SubTitle Options\n */\n subtitleOptions?: SubTitleOptions;\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Initial playing rate\n */\n rate?: number;\n /**\n * Exit on VideoEnd (iOS, Android)\n * default: true\n */\n exitOnEnd?: boolean;\n /**\n * Loop on VideoEnd when exitOnEnd false (iOS, Android)\n * default: false\n */\n loopOnEnd?: boolean;\n /**\n * Picture in Picture Enable (iOS, Android)\n * default: true\n */\n pipEnabled?: boolean;\n /**\n * Background Mode Enable (iOS, Android)\n * default: true\n */\n bkmodeEnabled?: boolean;\n /**\n * iOS audio session category behavior (iOS).\n * - \"ambient\": respects the iPhone silent switch (default)\n * - \"playback\": ignores the iPhone silent switch\n * - \"moviePlayback\": playback optimized for video and long-form AirPlay\n * default: \"ambient\"\n */\n audioCategory?: 'ambient' | 'playback' | 'moviePlayback';\n /**\n * Show Controls Enable (iOS, Android)\n * default: true\n */\n showControls?: boolean;\n /**\n * Display Mode [\"all\", \"portrait\", \"landscape\"] (iOS, Android)\n * default: \"all\"\n */\n displayMode?: string;\n /**\n * Component Tag or DOM Element Tag (React app)\n */\n componentTag?: string;\n /**\n * Player Width (mode \"embedded\" only)\n */\n width?: number;\n /**\n * Player height (mode \"embedded\" only)\n */\n height?: number;\n /**\n * Headers for the request (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n headers?: {\n [key: string]: string;\n };\n /**\n * Title shown in the player and Chromecast metadata (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n title?: string;\n /**\n * Subtitle shown below the title in the player and Chromecast metadata (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n smallTitle?: string;\n /**\n * ExoPlayer Progress Bar and Spinner color (Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * Must be a valid hex color code\n * default: #FFFFFF\n */\n accentColor?: string;\n /**\n * Chromecast enable/disable (iOS, Android)\n * iOS requires Google Cast SDK setup and local network Info.plist keys.\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: true\n */\n chromecast?: boolean;\n /**\n * Artwork url to be shown in Chromecast player (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: \"\"\n */\n artwork?: string;\n /**\n * DRM configuration for protected content (iOS: FairPlay, Android: Widevine)\n *\n * When casting, Widevine's `certificateUrl` (license URL) is also forwarded to Chromecast\n * receivers as `media.customData.laurl`.\n */\n drm?: DrmOptions;\n}\nexport interface capVideoPlayerIdOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n}\nexport interface capVideoRateOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Rate value\n */\n rate?: number;\n}\nexport interface capVideoVolumeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Volume value between [0 - 1]\n */\n volume?: number;\n}\nexport interface capVideoTimeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video time value you want to seek to\n */\n seektime?: number;\n}\nexport interface capVideoMutedOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Muted value true or false\n */\n muted?: boolean;\n}\nexport interface capVideoListener {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capExitListener {\n /**\n * Dismiss value true or false\n */\n dismiss?: boolean;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capVideoPlayerResult {\n /**\n * result set to true when successful else false\n */\n result?: boolean;\n /**\n * method name\n */\n method?: string;\n /**\n * value returned\n */\n value?: any;\n /**\n * message string\n */\n message?: string;\n}\nexport interface SubTitleOptions {\n /**\n * Foreground Color in RGBA (default rgba(255,255,255,1)\n */\n foregroundColor?: string;\n /**\n * Background Color in RGBA (default rgba(0,0,0,1)\n */\n backgroundColor?: string;\n /**\n * Font Size in pixels (default 16)\n */\n fontSize?: number;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\nexport interface FairPlayDrmOptions {\n /**\n * The URL to fetch the FairPlay certificate\n */\n certificateUrl?: string;\n /**\n * The URL to send the SPC and receive the CKC license (FairPlay license server URL)\n */\n contentKeySpcUrl?: string;\n /**\n * Optional FairPlay asset identifier used when generating the SPC.\n * Defaults to the `skd://` content identifier from the key request.\n */\n assetId?: string;\n}\nexport interface PlayreadyDrmOptions {\n /**\n * The URL to fetch the PlayReady license\n */\n certificateUrl?: string;\n}\nexport interface WidevineDrmOptions {\n /**\n * The URL to fetch the Widevine license\n */\n certificateUrl?: string;\n}\nexport interface DrmOptions {\n /**\n * FairPlay DRM configuration (iOS)\n */\n fairplay?: FairPlayDrmOptions;\n /**\n * PlayReady DRM configuration\n */\n playready?: PlayreadyDrmOptions;\n /**\n * Widevine DRM configuration (Android)\n */\n widevine?: WidevineDrmOptions;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface VideoPlayerPlugin {\n /**\n * Initialize a video player\n *\n */\n initPlayer(options: capVideoPlayerOptions): Promise<capVideoPlayerResult>;\n /**\n * Return if a given playerId is playing\n *\n */\n isPlaying(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Play the current video from a given playerId\n *\n */\n play(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Pause the current video from a given playerId\n *\n */\n pause(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the duration of the current video from a given playerId\n *\n */\n getDuration(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the current time of the current video from a given playerId\n *\n */\n getCurrentTime(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the current time to seek the current video to from a given playerId\n *\n */\n setCurrentTime(options: capVideoTimeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the volume of the current video from a given playerId\n *\n */\n getVolume(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the volume of the current video to from a given playerId\n *\n */\n setVolume(options: capVideoVolumeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the muted of the current video from a given playerId\n *\n */\n getMuted(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the muted of the current video to from a given playerId\n *\n */\n setMuted(options: capVideoMutedOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the rate of the current video from a given playerId\n *\n */\n setRate(options: capVideoRateOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the rate of the current video from a given playerId\n *\n */\n getRate(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Stop all players playing\n *\n */\n stopAllPlayers(): Promise<capVideoPlayerResult>;\n /**\n * Show controller\n *\n */\n showController(): Promise<capVideoPlayerResult>;\n /**\n * isControllerIsFullyVisible\n *\n */\n isControllerIsFullyVisible(): Promise<capVideoPlayerResult>;\n /**\n * Exit player\n *\n */\n exitPlayer(): Promise<capVideoPlayerResult>;\n /**\n * Hide the currently presented player UI without stopping playback (native fullscreen).\n *\n * Call `showPlayer()` to show it again.\n */\n hidePlayer(): Promise<capVideoPlayerResult>;\n /**\n * Show again a previously hidden player UI (native fullscreen).\n */\n showPlayer(): Promise<capVideoPlayerResult>;\n}\nexport interface capEchoOptions {\n /**\n * String to be echoed\n */\n\n value?: string;\n}\nexport interface capVideoPlayerOptions {\n /**\n * Player mode\n * - \"fullscreen\"\n * - \"embedded\" (Web only)\n */\n mode?: string;\n /**\n * The url of the video to play\n */\n url?: string;\n /**\n * Optional override URL used when casting to Chromecast (iOS, Android).\n *\n * This lets you cast an alternative rendition/manifest (e.g. MPEG-DASH + Widevine)\n * while keeping the on-device playback URL unchanged (e.g. HLS + FairPlay on iOS).\n */\n chromecastUrl?: string;\n /**\n * The url of subtitle associated with the video\n *\n * Prefer `subtitles` when providing more than one track.\n * Kept for backward compatibility.\n */\n subtitle?: string;\n /**\n * The language of subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n *\n * Prefer `subtitles` when providing more than one track.\n * Kept for backward compatibility.\n */\n language?: string;\n /**\n * Multiple subtitle tracks (iOS, Android).\n * When provided, takes precedence over `subtitle` / `language`.\n */\n subtitles?: VideoSubtitle[];\n /**\n * SubTitle Options\n */\n subtitleOptions?: SubTitleOptions;\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Initial playing rate\n */\n rate?: number;\n /**\n * Exit on VideoEnd (iOS, Android)\n * default: true\n */\n exitOnEnd?: boolean;\n /**\n * Loop on VideoEnd when exitOnEnd false (iOS, Android)\n * default: false\n */\n loopOnEnd?: boolean;\n /**\n * Picture in Picture Enable (iOS, Android)\n * default: true\n */\n pipEnabled?: boolean;\n /**\n * Background Mode Enable (iOS, Android)\n * default: true\n */\n bkmodeEnabled?: boolean;\n /**\n * iOS audio session category behavior (iOS).\n * - \"ambient\": respects the iPhone silent switch (default)\n * - \"playback\": ignores the iPhone silent switch\n * - \"moviePlayback\": playback optimized for video and long-form AirPlay\n * default: \"ambient\"\n */\n audioCategory?: 'ambient' | 'playback' | 'moviePlayback';\n /**\n * Show Controls Enable (iOS, Android)\n * default: true\n */\n showControls?: boolean;\n /**\n * Display Mode [\"all\", \"portrait\", \"landscape\"] (iOS, Android)\n * default: \"all\"\n */\n displayMode?: string;\n /**\n * Component Tag or DOM Element Tag (React app)\n */\n componentTag?: string;\n /**\n * Player Width (mode \"embedded\" only)\n */\n width?: number;\n /**\n * Player height (mode \"embedded\" only)\n */\n height?: number;\n /**\n * Headers for the request (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n headers?: {\n [key: string]: string;\n };\n /**\n * Title shown in the player and Chromecast metadata (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n title?: string;\n /**\n * Subtitle shown below the title in the player and Chromecast metadata (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n smallTitle?: string;\n /**\n * ExoPlayer Progress Bar and Spinner color (Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * Must be a valid hex color code\n * default: #FFFFFF\n */\n accentColor?: string;\n /**\n * Chromecast enable/disable (iOS, Android)\n * iOS requires Google Cast SDK setup and local network Info.plist keys.\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: true\n */\n chromecast?: boolean;\n /**\n * Artwork url to be shown in Chromecast player (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: \"\"\n */\n artwork?: string;\n /**\n * DRM configuration for protected content (iOS: FairPlay, Android: Widevine)\n *\n * When casting, Widevine's `certificateUrl` (license URL) is also forwarded to Chromecast\n * receivers as `media.customData.laurl`.\n */\n drm?: DrmOptions;\n}\nexport interface capVideoPlayerIdOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n}\nexport interface capVideoRateOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Rate value\n */\n rate?: number;\n}\nexport interface capVideoVolumeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Volume value between [0 - 1]\n */\n volume?: number;\n}\nexport interface capVideoTimeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video time value you want to seek to\n */\n seektime?: number;\n}\nexport interface capVideoMutedOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Muted value true or false\n */\n muted?: boolean;\n}\nexport interface capVideoListener {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capExitListener {\n /**\n * Dismiss value true or false\n */\n dismiss?: boolean;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capVideoPlayerResult {\n /**\n * result set to true when successful else false\n */\n result?: boolean;\n /**\n * method name\n */\n method?: string;\n /**\n * value returned\n */\n value?: any;\n /**\n * message string\n */\n message?: string;\n}\nexport interface VideoSubtitle {\n /**\n * The url of the subtitle file (vtt, srt, ...)\n */\n subtitle: string;\n /**\n * The language of the subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n */\n language?: string;\n}\nexport interface SubTitleOptions {\n /**\n * Foreground Color in RGBA (default rgba(255,255,255,1)\n */\n foregroundColor?: string;\n /**\n * Background Color in RGBA (default rgba(0,0,0,1)\n */\n backgroundColor?: string;\n /**\n * Font Size in pixels (default 16)\n */\n fontSize?: number;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\nexport interface FairPlayDrmOptions {\n /**\n * The URL to fetch the FairPlay certificate\n */\n certificateUrl?: string;\n /**\n * The URL to send the SPC and receive the CKC license (FairPlay license server URL)\n */\n contentKeySpcUrl?: string;\n /**\n * Optional FairPlay asset identifier used when generating the SPC.\n * Defaults to the `skd://` content identifier from the key request.\n */\n assetId?: string;\n}\nexport interface PlayreadyDrmOptions {\n /**\n * The URL to fetch the PlayReady license\n */\n certificateUrl?: string;\n}\nexport interface WidevineDrmOptions {\n /**\n * The URL to fetch the Widevine license\n */\n certificateUrl?: string;\n}\nexport interface DrmOptions {\n /**\n * FairPlay DRM configuration (iOS)\n */\n fairplay?: FairPlayDrmOptions;\n /**\n * PlayReady DRM configuration\n */\n playready?: PlayreadyDrmOptions;\n /**\n * Widevine DRM configuration (Android)\n */\n widevine?: WidevineDrmOptions;\n}\n"]}
|
|
@@ -3,6 +3,7 @@ import AVKit
|
|
|
3
3
|
import AVFoundation
|
|
4
4
|
import UIKit
|
|
5
5
|
|
|
6
|
+
// swiftlint:disable:next type_body_length
|
|
6
7
|
class FullscreenVideoPlayer: NSObject {
|
|
7
8
|
private var player: AVPlayer?
|
|
8
9
|
private var playerViewController: AVPlayerViewController?
|
|
@@ -35,8 +36,7 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
35
36
|
private var contentKeySession: AVContentKeySession?
|
|
36
37
|
private var castController: VideoPlayerCastController?
|
|
37
38
|
private weak var presentingViewController: UIViewController?
|
|
38
|
-
private var
|
|
39
|
-
private var subtitleLanguage: String?
|
|
39
|
+
private var subtitleTracks: [VideoSubtitleTrack] = []
|
|
40
40
|
private var hlsResourceLoader: HLSSubtitleResourceLoader?
|
|
41
41
|
|
|
42
42
|
init(
|
|
@@ -52,8 +52,7 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
52
52
|
title: String? = nil,
|
|
53
53
|
smallTitle: String? = nil,
|
|
54
54
|
artwork: String? = nil,
|
|
55
|
-
|
|
56
|
-
subtitleLanguage: String? = nil,
|
|
55
|
+
subtitleTracks: [VideoSubtitleTrack] = [],
|
|
57
56
|
fairplayCertificateUrl: String? = nil,
|
|
58
57
|
fairplayContentKeySpcUrl: String? = nil,
|
|
59
58
|
fairplayAssetId: String? = nil,
|
|
@@ -72,8 +71,7 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
72
71
|
self.title = title
|
|
73
72
|
self.smallTitle = smallTitle
|
|
74
73
|
self.artwork = artwork
|
|
75
|
-
self.
|
|
76
|
-
self.subtitleLanguage = subtitleLanguage
|
|
74
|
+
self.subtitleTracks = subtitleTracks
|
|
77
75
|
self.fairplayCertificateUrl = fairplayCertificateUrl
|
|
78
76
|
self.fairplayContentKeySpcUrl = fairplayContentKeySpcUrl
|
|
79
77
|
self.fairplayAssetId = fairplayAssetId
|
|
@@ -90,28 +88,30 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
90
88
|
|
|
91
89
|
configureAudioSession()
|
|
92
90
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
let resolvedTracks = subtitleTracks.compactMap { track -> (URL, String?)? in
|
|
92
|
+
guard let subtitleURL = track.resolvedURL else { return nil }
|
|
93
|
+
return (subtitleURL, track.language)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
guard !resolvedTracks.isEmpty else {
|
|
97
|
+
let asset = makeVideoAsset(url: url, subtitleTracks: [])
|
|
97
98
|
configurePlayer(with: AVPlayerItem(asset: asset))
|
|
98
99
|
completion()
|
|
99
100
|
return
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
if HLSVideoAssetFactory.isHLSStream(url) {
|
|
103
|
-
let asset = makeVideoAsset(url: url,
|
|
104
|
+
let asset = makeVideoAsset(url: url, subtitleTracks: subtitleTracks)
|
|
104
105
|
configurePlayer(with: AVPlayerItem(asset: asset))
|
|
105
106
|
completion()
|
|
106
107
|
return
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
Task {
|
|
110
|
-
let asset = makeVideoAsset(url: url,
|
|
111
|
+
let asset = makeVideoAsset(url: url, subtitleTracks: [])
|
|
111
112
|
let item = await ProgressiveVideoPlayerItemFactory.createPlayerItem(
|
|
112
113
|
videoAsset: asset,
|
|
113
|
-
|
|
114
|
-
subtitleLanguage: subtitleLanguage
|
|
114
|
+
subtitleTracks: subtitleTracks
|
|
115
115
|
)
|
|
116
116
|
await MainActor.run {
|
|
117
117
|
self.configurePlayer(with: item)
|
|
@@ -120,11 +120,10 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
private func makeVideoAsset(url: URL,
|
|
123
|
+
private func makeVideoAsset(url: URL, subtitleTracks: [VideoSubtitleTrack]) -> AVURLAsset {
|
|
124
124
|
let result = HLSVideoAssetFactory.makeAsset(
|
|
125
125
|
videoURL: url,
|
|
126
|
-
|
|
127
|
-
language: subtitleLanguage ?? "en"
|
|
126
|
+
subtitleTracks: subtitleTracks
|
|
128
127
|
)
|
|
129
128
|
hlsResourceLoader = result.resourceLoader
|
|
130
129
|
let asset = result.asset
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
2
|
import Foundation
|
|
3
|
+
import UniformTypeIdentifiers
|
|
3
4
|
|
|
4
5
|
enum HLSVideoAssetFactory {
|
|
5
6
|
static func isHLSStream(_ url: URL) -> Bool {
|
|
@@ -15,17 +16,21 @@ enum HLSVideoAssetFactory {
|
|
|
15
16
|
|
|
16
17
|
static func makeAsset(
|
|
17
18
|
videoURL: URL,
|
|
18
|
-
|
|
19
|
-
language: String
|
|
19
|
+
subtitleTracks: [VideoSubtitleTrack]
|
|
20
20
|
) -> (asset: AVURLAsset, resourceLoader: HLSSubtitleResourceLoader?) {
|
|
21
|
-
|
|
21
|
+
var resolvedTracks: [(url: URL, language: String)] = []
|
|
22
|
+
for track in subtitleTracks {
|
|
23
|
+
guard let subtitleURL = track.resolvedURL else { continue }
|
|
24
|
+
resolvedTracks.append((url: subtitleURL, language: track.language ?? "en"))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
guard !resolvedTracks.isEmpty, isHLSStream(videoURL) else {
|
|
22
28
|
return (AVURLAsset(url: videoURL), nil)
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
let resourceLoader = HLSSubtitleResourceLoader(
|
|
26
32
|
videoURL: videoURL,
|
|
27
|
-
|
|
28
|
-
language: language
|
|
33
|
+
subtitleTracks: resolvedTracks.map { HLSSubtitleTrack(url: $0.url, language: $0.language) }
|
|
29
34
|
)
|
|
30
35
|
guard let assetURL = HLSSubtitleResourceLoader.assetURL(for: videoURL) else {
|
|
31
36
|
return (AVURLAsset(url: videoURL), nil)
|
|
@@ -37,21 +42,30 @@ enum HLSVideoAssetFactory {
|
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
struct HLSSubtitleTrack {
|
|
46
|
+
let url: URL
|
|
47
|
+
let language: String
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
/// Injects external WebVTT subtitles into HLS master playlists via AVAssetResourceLoaderDelegate.
|
|
41
51
|
/// AVMutableComposition cannot demux HLS tracks, so sidecar subtitles must be wired through the manifest.
|
|
42
52
|
final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
43
53
|
static let videoScheme = "capgohls"
|
|
44
54
|
static let subtitlePlaylistScheme = "capgohls-sub"
|
|
45
55
|
private static let subtitleGroupID = "capgosubs"
|
|
56
|
+
private static let mpegURLContentType = UTType.m3uPlaylist.identifier
|
|
46
57
|
|
|
47
58
|
private let originalVideoURL: URL
|
|
48
|
-
private let
|
|
49
|
-
private let language: String
|
|
59
|
+
private let subtitleTracks: [HLSSubtitleTrack]
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
private struct LoadedResource {
|
|
62
|
+
let data: Data
|
|
63
|
+
let contentType: String?
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
init(videoURL: URL, subtitleTracks: [HLSSubtitleTrack]) {
|
|
52
67
|
self.originalVideoURL = videoURL
|
|
53
|
-
self.
|
|
54
|
-
self.language = language
|
|
68
|
+
self.subtitleTracks = subtitleTracks
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
static func assetURL(for videoURL: URL) -> URL? {
|
|
@@ -62,11 +76,14 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
62
76
|
return components.url
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
static func subtitlePlaylistURL(for videoURL: URL) -> URL? {
|
|
79
|
+
static func subtitlePlaylistURL(for videoURL: URL, index: Int) -> URL? {
|
|
66
80
|
guard var components = URLComponents(url: videoURL, resolvingAgainstBaseURL: false) else {
|
|
67
81
|
return nil
|
|
68
82
|
}
|
|
69
83
|
components.scheme = subtitlePlaylistScheme
|
|
84
|
+
var queryItems = components.queryItems ?? []
|
|
85
|
+
queryItems.append(URLQueryItem(name: "capgoSubIndex", value: String(index)))
|
|
86
|
+
components.queryItems = queryItems
|
|
70
87
|
return components.url
|
|
71
88
|
}
|
|
72
89
|
|
|
@@ -80,8 +97,8 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
80
97
|
|
|
81
98
|
Task {
|
|
82
99
|
do {
|
|
83
|
-
let
|
|
84
|
-
fulfill(loadingRequest, with:
|
|
100
|
+
let resource = try await loadData(for: requestURL)
|
|
101
|
+
fulfill(loadingRequest, with: resource)
|
|
85
102
|
} catch {
|
|
86
103
|
loadingRequest.finishLoading(with: error)
|
|
87
104
|
}
|
|
@@ -90,25 +107,40 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
90
107
|
return true
|
|
91
108
|
}
|
|
92
109
|
|
|
93
|
-
private func loadData(for requestURL: URL) async throws ->
|
|
110
|
+
private func loadData(for requestURL: URL) async throws -> LoadedResource {
|
|
94
111
|
if requestURL.scheme == Self.subtitlePlaylistScheme {
|
|
95
|
-
|
|
112
|
+
let index = subtitleIndex(from: requestURL) ?? 0
|
|
113
|
+
let track = subtitleTracks[min(max(index, 0), subtitleTracks.count - 1)]
|
|
114
|
+
let data = Data(subtitleMediaPlaylist(for: track.url).utf8)
|
|
115
|
+
return LoadedResource(data: data, contentType: Self.mpegURLContentType)
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
let fetchURL = originalURL(for: requestURL)
|
|
99
|
-
let (data,
|
|
119
|
+
let (data, response) = try await URLSession.shared.data(from: fetchURL)
|
|
120
|
+
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
|
|
121
|
+
throw URLError(.badServerResponse)
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
let manifest = String(data: data, encoding: .utf8) ?? ""
|
|
101
125
|
|
|
102
126
|
guard hasStreamInfTags(manifest) else {
|
|
103
|
-
|
|
127
|
+
let contentType = Self.contentTypeIdentifier(from: response)
|
|
128
|
+
return LoadedResource(data: data, contentType: contentType)
|
|
104
129
|
}
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
let modified = injectSubtitles(
|
|
132
|
+
into: manifest,
|
|
133
|
+
baseURL: response.url ?? fetchURL
|
|
134
|
+
)
|
|
135
|
+
return LoadedResource(data: Data(modified.utf8), contentType: Self.mpegURLContentType)
|
|
136
|
+
}
|
|
109
137
|
|
|
110
|
-
|
|
111
|
-
|
|
138
|
+
private func subtitleIndex(from requestURL: URL) -> Int? {
|
|
139
|
+
guard let components = URLComponents(url: requestURL, resolvingAgainstBaseURL: false),
|
|
140
|
+
let value = components.queryItems?.first(where: { $0.name == "capgoSubIndex" })?.value else {
|
|
141
|
+
return nil
|
|
142
|
+
}
|
|
143
|
+
return Int(value)
|
|
112
144
|
}
|
|
113
145
|
|
|
114
146
|
private func originalURL(for requestURL: URL) -> URL {
|
|
@@ -125,19 +157,46 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
125
157
|
manifest.contains("#EXT-X-STREAM-INF")
|
|
126
158
|
}
|
|
127
159
|
|
|
128
|
-
private func
|
|
129
|
-
let
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
private static func contentTypeIdentifier(from response: URLResponse) -> String? {
|
|
161
|
+
guard let mimeType = (response as? HTTPURLResponse)?.mimeType else {
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
return UTType(mimeType: mimeType)?.identifier
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func absoluteURI(for uri: String, relativeTo baseURL: URL) -> String {
|
|
168
|
+
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
169
|
+
guard let url = URL(string: trimmed, relativeTo: baseURL)?.absoluteURL else {
|
|
170
|
+
return trimmed
|
|
171
|
+
}
|
|
172
|
+
return url.absoluteString
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private func injectSubtitles(into manifest: String, baseURL: URL) -> String {
|
|
176
|
+
let mediaTags = subtitleTracks.enumerated().compactMap { index, track -> String? in
|
|
177
|
+
guard let subtitlePlaylistURI = Self.subtitlePlaylistURL(for: originalVideoURL, index: index)?.absoluteString else {
|
|
178
|
+
return nil
|
|
179
|
+
}
|
|
180
|
+
let isDefault = index == 0 ? "YES" : "NO"
|
|
181
|
+
let displayName = Locale(identifier: track.language).localizedString(forLanguageCode: track.language)
|
|
182
|
+
?? track.language
|
|
183
|
+
return """
|
|
184
|
+
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="\(Self.subtitleGroupID)",NAME="\(displayName)",DEFAULT=\(isDefault),AUTOSELECT=YES,FORCED=NO,LANGUAGE="\(track.language)",URI="\(subtitlePlaylistURI)"
|
|
185
|
+
"""
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
guard !mediaTags.isEmpty else {
|
|
189
|
+
return manifest
|
|
190
|
+
}
|
|
132
191
|
|
|
133
|
-
var lines = manifest.components(separatedBy: .newlines)
|
|
134
192
|
var result: [String] = []
|
|
135
193
|
var insertedMediaTag = false
|
|
194
|
+
var expectingVariantURI = false
|
|
136
195
|
|
|
137
|
-
for line in
|
|
138
|
-
if line.hasPrefix("#EXT-X-STREAM-INF") {
|
|
196
|
+
for line in manifest.components(separatedBy: .newlines) {
|
|
197
|
+
if line.hasPrefix("#EXT-X-STREAM-INF") || line.hasPrefix("#EXT-X-I-FRAME-STREAM-INF") {
|
|
139
198
|
if !insertedMediaTag {
|
|
140
|
-
result.append(
|
|
199
|
+
result.append(contentsOf: mediaTags)
|
|
141
200
|
insertedMediaTag = true
|
|
142
201
|
}
|
|
143
202
|
|
|
@@ -146,20 +205,28 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
146
205
|
} else {
|
|
147
206
|
result.append("\(line),SUBTITLES=\"\(Self.subtitleGroupID)\"")
|
|
148
207
|
}
|
|
208
|
+
expectingVariantURI = true
|
|
149
209
|
continue
|
|
150
210
|
}
|
|
151
211
|
|
|
212
|
+
if expectingVariantURI, !line.hasPrefix("#"), !line.isEmpty {
|
|
213
|
+
result.append(absoluteURI(for: line, relativeTo: baseURL))
|
|
214
|
+
expectingVariantURI = false
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
expectingVariantURI = false
|
|
152
219
|
result.append(line)
|
|
153
220
|
}
|
|
154
221
|
|
|
155
222
|
if !insertedMediaTag, let extm3uIndex = result.firstIndex(where: { $0.hasPrefix("#EXTM3U") }) {
|
|
156
|
-
result.insert(
|
|
223
|
+
result.insert(contentsOf: mediaTags, at: extm3uIndex + 1)
|
|
157
224
|
}
|
|
158
225
|
|
|
159
226
|
return result.joined(separator: "\n")
|
|
160
227
|
}
|
|
161
228
|
|
|
162
|
-
private func subtitleMediaPlaylist() -> String {
|
|
229
|
+
private func subtitleMediaPlaylist(for subtitleURL: URL) -> String {
|
|
163
230
|
"""
|
|
164
231
|
#EXTM3U
|
|
165
232
|
#EXT-X-VERSION:3
|
|
@@ -172,14 +239,16 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
172
239
|
"""
|
|
173
240
|
}
|
|
174
241
|
|
|
175
|
-
private func fulfill(_ loadingRequest: AVAssetResourceLoadingRequest, with
|
|
242
|
+
private func fulfill(_ loadingRequest: AVAssetResourceLoadingRequest, with resource: LoadedResource) {
|
|
176
243
|
if let contentRequest = loadingRequest.contentInformationRequest {
|
|
177
|
-
|
|
178
|
-
|
|
244
|
+
if let contentType = resource.contentType {
|
|
245
|
+
contentRequest.contentType = contentType
|
|
246
|
+
}
|
|
247
|
+
contentRequest.contentLength = Int64(resource.data.count)
|
|
179
248
|
contentRequest.isByteRangeAccessSupported = false
|
|
180
249
|
}
|
|
181
250
|
|
|
182
|
-
loadingRequest.dataRequest?.respond(with: data)
|
|
251
|
+
loadingRequest.dataRequest?.respond(with: resource.data)
|
|
183
252
|
loadingRequest.finishLoading()
|
|
184
253
|
}
|
|
185
254
|
}
|
|
@@ -4,10 +4,16 @@ import Foundation
|
|
|
4
4
|
enum ProgressiveVideoPlayerItemFactory {
|
|
5
5
|
static func createPlayerItem(
|
|
6
6
|
videoAsset: AVURLAsset,
|
|
7
|
-
|
|
8
|
-
subtitleLanguage: String?
|
|
7
|
+
subtitleTracks: [VideoSubtitleTrack]
|
|
9
8
|
) async -> AVPlayerItem {
|
|
10
|
-
let
|
|
9
|
+
let resolvedTracks = subtitleTracks.compactMap { track -> (URL, String?)? in
|
|
10
|
+
guard let url = track.resolvedURL else { return nil }
|
|
11
|
+
return (url, track.language)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
guard !resolvedTracks.isEmpty else {
|
|
15
|
+
return AVPlayerItem(asset: videoAsset)
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
do {
|
|
13
19
|
let videoDuration = try await videoAsset.load(.duration)
|
|
@@ -27,9 +33,14 @@ enum ProgressiveVideoPlayerItemFactory {
|
|
|
27
33
|
return AVPlayerItem(asset: videoAsset)
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
var preferredLanguage: String?
|
|
30
37
|
var subtitleAdded = false
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
|
|
39
|
+
for (index, track) in resolvedTracks.enumerated() {
|
|
40
|
+
let subtitleAsset = AVURLAsset(url: track.0)
|
|
41
|
+
let subtitleMediaTracks = try await subtitleAsset.loadTracks(withMediaType: .text)
|
|
42
|
+
guard let subtitleTrack = subtitleMediaTracks.first else { continue }
|
|
43
|
+
|
|
33
44
|
let compositionTrack = composition.addMutableTrack(
|
|
34
45
|
withMediaType: .text,
|
|
35
46
|
preferredTrackID: kCMPersistentTrackID_Invalid
|
|
@@ -41,13 +52,25 @@ enum ProgressiveVideoPlayerItemFactory {
|
|
|
41
52
|
of: subtitleTrack,
|
|
42
53
|
at: CMTime.zero
|
|
43
54
|
)
|
|
55
|
+
|
|
56
|
+
if let language = track.1, !language.isEmpty {
|
|
57
|
+
compositionTrack?.extendedLanguageTag = language
|
|
58
|
+
if preferredLanguage == nil {
|
|
59
|
+
preferredLanguage = language
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Keep first track as default selection preference
|
|
64
|
+
if index == 0, preferredLanguage == nil {
|
|
65
|
+
preferredLanguage = track.1
|
|
66
|
+
}
|
|
44
67
|
subtitleAdded = true
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
let playerItem = AVPlayerItem(asset: composition)
|
|
48
71
|
|
|
49
72
|
if subtitleAdded {
|
|
50
|
-
selectSubtitle(in: playerItem, language:
|
|
73
|
+
selectSubtitle(in: playerItem, language: preferredLanguage)
|
|
51
74
|
}
|
|
52
75
|
|
|
53
76
|
return playerItem
|
|
@@ -101,5 +124,4 @@ enum ProgressiveVideoPlayerItemFactory {
|
|
|
101
124
|
|| option.locale?.languageCode == language
|
|
102
125
|
}
|
|
103
126
|
}
|
|
104
|
-
|
|
105
127
|
}
|
|
@@ -8,7 +8,7 @@ import AVKit
|
|
|
8
8
|
*/
|
|
9
9
|
@objc(VideoPlayerPlugin)
|
|
10
10
|
public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
-
private let pluginVersion: String = "8.
|
|
11
|
+
private let pluginVersion: String = "8.2.0"
|
|
12
12
|
public let identifier = "VideoPlayerPlugin"
|
|
13
13
|
public let jsName = "VideoPlayer"
|
|
14
14
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -83,17 +83,8 @@ extension VideoPlayerPlugin {
|
|
|
83
83
|
let artwork = call.getString("artwork")
|
|
84
84
|
let chromecastUrl = call.getString("chromecastUrl")
|
|
85
85
|
let audioCategory = call.getString("audioCategory")
|
|
86
|
-
let
|
|
87
|
-
let
|
|
88
|
-
|
|
89
|
-
// Extract FairPlay DRM options if provided
|
|
90
|
-
let drm = call.getObject("drm")
|
|
91
|
-
let fairplay = drm?["fairplay"] as? [String: Any]
|
|
92
|
-
let fairplayCertificateUrl = fairplay?["certificateUrl"] as? String
|
|
93
|
-
let fairplayContentKeySpcUrl = fairplay?["contentKeySpcUrl"] as? String
|
|
94
|
-
let fairplayAssetId = fairplay?["assetId"] as? String
|
|
95
|
-
let widevine = drm?["widevine"] as? [String: Any]
|
|
96
|
-
let widevineLicenseUrl = widevine?["certificateUrl"] as? String
|
|
86
|
+
let subtitleTracks = Self.parseSubtitleTracks(from: call)
|
|
87
|
+
let drmOptions = Self.parseDrmOptions(from: call)
|
|
97
88
|
|
|
98
89
|
// Create video player
|
|
99
90
|
let player = FullscreenVideoPlayer(
|
|
@@ -109,12 +100,11 @@ extension VideoPlayerPlugin {
|
|
|
109
100
|
title: title,
|
|
110
101
|
smallTitle: smallTitle,
|
|
111
102
|
artwork: artwork,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
widevineLicenseUrl: widevineLicenseUrl,
|
|
103
|
+
subtitleTracks: subtitleTracks,
|
|
104
|
+
fairplayCertificateUrl: drmOptions.fairplayCertificateUrl,
|
|
105
|
+
fairplayContentKeySpcUrl: drmOptions.fairplayContentKeySpcUrl,
|
|
106
|
+
fairplayAssetId: drmOptions.fairplayAssetId,
|
|
107
|
+
widevineLicenseUrl: drmOptions.widevineLicenseUrl,
|
|
118
108
|
audioCategory: audioCategory
|
|
119
109
|
)
|
|
120
110
|
|
|
@@ -147,6 +137,43 @@ extension VideoPlayerPlugin {
|
|
|
147
137
|
}
|
|
148
138
|
}
|
|
149
139
|
|
|
140
|
+
private static func parseSubtitleTracks(from call: CAPPluginCall) -> [VideoSubtitleTrack] {
|
|
141
|
+
let subtitlesArray = call.getArray("subtitles", JSObject.self)?.map { entry -> [String: Any] in
|
|
142
|
+
var dict: [String: Any] = [:]
|
|
143
|
+
if let subtitleUrl = entry["subtitle"] as? String {
|
|
144
|
+
dict["subtitle"] = subtitleUrl
|
|
145
|
+
}
|
|
146
|
+
if let lang = entry["language"] as? String {
|
|
147
|
+
dict["language"] = lang
|
|
148
|
+
}
|
|
149
|
+
return dict
|
|
150
|
+
}
|
|
151
|
+
return VideoSubtitleTrackParser.parse(
|
|
152
|
+
from: subtitlesArray,
|
|
153
|
+
legacySubtitle: call.getString("subtitle"),
|
|
154
|
+
legacyLanguage: call.getString("language")
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private struct ParsedDrmOptions {
|
|
159
|
+
let fairplayCertificateUrl: String?
|
|
160
|
+
let fairplayContentKeySpcUrl: String?
|
|
161
|
+
let fairplayAssetId: String?
|
|
162
|
+
let widevineLicenseUrl: String?
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private static func parseDrmOptions(from call: CAPPluginCall) -> ParsedDrmOptions {
|
|
166
|
+
let drm = call.getObject("drm")
|
|
167
|
+
let fairplay = drm?["fairplay"] as? [String: Any]
|
|
168
|
+
let widevine = drm?["widevine"] as? [String: Any]
|
|
169
|
+
return ParsedDrmOptions(
|
|
170
|
+
fairplayCertificateUrl: fairplay?["certificateUrl"] as? String,
|
|
171
|
+
fairplayContentKeySpcUrl: fairplay?["contentKeySpcUrl"] as? String,
|
|
172
|
+
fairplayAssetId: fairplay?["assetId"] as? String,
|
|
173
|
+
widevineLicenseUrl: widevine?["certificateUrl"] as? String
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
150
177
|
private func configureCallbacks(for player: FullscreenVideoPlayer, playerId: String) {
|
|
151
178
|
player.setOnPlay { [weak self] in
|
|
152
179
|
self?.notifyListeners("jeepCapVideoPlayerPlay", data: [
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
struct VideoSubtitleTrack {
|
|
4
|
+
let url: String
|
|
5
|
+
let language: String?
|
|
6
|
+
|
|
7
|
+
var resolvedURL: URL? {
|
|
8
|
+
guard !url.isEmpty else { return nil }
|
|
9
|
+
return URL(string: url)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
enum VideoSubtitleTrackParser {
|
|
14
|
+
static func parse(from callSubtitles: [[String: Any]]?, legacySubtitle: String?, legacyLanguage: String?) -> [VideoSubtitleTrack] {
|
|
15
|
+
if let callSubtitles, !callSubtitles.isEmpty {
|
|
16
|
+
return callSubtitles.compactMap { entry in
|
|
17
|
+
guard let subtitle = entry["subtitle"] as? String, !subtitle.isEmpty else {
|
|
18
|
+
return nil
|
|
19
|
+
}
|
|
20
|
+
let language = entry["language"] as? String
|
|
21
|
+
return VideoSubtitleTrack(url: subtitle, language: language)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
guard let legacySubtitle, !legacySubtitle.isEmpty else {
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return [VideoSubtitleTrack(url: legacySubtitle, language: legacyLanguage)]
|
|
30
|
+
}
|
|
31
|
+
}
|