@capgo/capacitor-video-player 8.2.15 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -5
- package/android/src/main/java/com/capgo/videoplayer/FullscreenExoPlayerFragment.java +145 -53
- package/android/src/main/java/com/capgo/videoplayer/SubtitleOptionOverrides.java +84 -0
- package/android/src/main/java/com/capgo/videoplayer/VideoChapter.java +14 -0
- package/android/src/main/java/com/capgo/videoplayer/VideoPlayer.java +3 -0
- package/android/src/main/java/com/capgo/videoplayer/VideoPlayerPlugin.java +48 -2
- package/android/src/main/res/drawable/ic_chapters_white.xml +10 -0
- package/android/src/main/res/layout/exo_playback_control_view.xml +9 -0
- package/android/src/main/res/values/strings.xml +2 -0
- package/dist/docs.json +43 -4
- package/dist/esm/definitions.d.ts +53 -3
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/VideoPlayerPlugin/AVPlayerControlsVisibilityObserver.swift +13 -0
- package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +215 -0
- package/ios/Sources/VideoPlayerPlugin/SubtitleStyleOptions.swift +7 -0
- package/ios/Sources/VideoPlayerPlugin/VideoChapter.swift +45 -0
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerCastController.swift +23 -5
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +47 -1
- package/ios/Tests/VideoPlayerPluginTests/VideoChapterTests.swift +30 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -534,6 +534,7 @@ On web, the implementation returns `"web"`.
|
|
|
534
534
|
| **`subtitle`** | <code>string</code> | The url of subtitle associated with the video Prefer `subtitles` when providing more than one track. Kept for backward compatibility. |
|
|
535
535
|
| **`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. |
|
|
536
536
|
| **`subtitles`** | <code>VideoSubtitle[]</code> | Multiple subtitle tracks (iOS, Android). When provided, takes precedence over `subtitle` / `language`. |
|
|
537
|
+
| **`chapters`** | <code>VideoChapter[]</code> | Chapter markers for long-form content (iOS, Android). Shown as a chapter list in the native fullscreen player; selecting a chapter seeks to `startTime`. |
|
|
537
538
|
| **`subtitleOptions`** | <code><a href="#subtitleoptions">SubTitleOptions</a></code> | SubTitle Options |
|
|
538
539
|
| **`playerId`** | <code>string</code> | Id of DIV Element parent of the player |
|
|
539
540
|
| **`rate`** | <code>number</code> | Initial playing rate |
|
|
@@ -564,13 +565,31 @@ On web, the implementation returns `"web"`.
|
|
|
564
565
|
| **`language`** | <code>string</code> | The language of the subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers |
|
|
565
566
|
|
|
566
567
|
|
|
568
|
+
#### VideoChapter
|
|
569
|
+
|
|
570
|
+
| Prop | Type | Description |
|
|
571
|
+
| --------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
572
|
+
| **`title`** | <code>string</code> | Chapter title shown in the chapter list |
|
|
573
|
+
| **`startTime`** | <code>number</code> | Start time in seconds |
|
|
574
|
+
| **`endTime`** | <code>number</code> | End time in seconds (optional). Stored for future timeline markers; chapter jumps use `startTime` only in this release. |
|
|
575
|
+
|
|
576
|
+
|
|
567
577
|
#### SubTitleOptions
|
|
568
578
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
579
|
+
Optional styling for on-screen subtitles.
|
|
580
|
+
|
|
581
|
+
On iOS, colors and font size are applied through `AVPlayerItem`
|
|
582
|
+
`textStyleRules`, which Apple documents for WebVTT only. IMSC1 and other
|
|
583
|
+
in-manifest timed text formats are not styled through this API.
|
|
584
|
+
Inline WebVTT cue styles can take precedence over equivalent
|
|
585
|
+
`subtitleOptions` values; the plugin sets text style rules as a platform
|
|
586
|
+
fallback, not a guaranteed override of authored cue styling.
|
|
587
|
+
|
|
588
|
+
| Prop | Type | Description |
|
|
589
|
+
| --------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
590
|
+
| **`foregroundColor`** | <code>string</code> | Foreground color in RGBA. On Android, when the plugin applies subtitle styling, omitted values fall back to `rgba(255,255,255,1)`. When `subtitleOptions` is absent, embedded cue colors from the media can override that fallback. On iOS, omitted colors are not set by the plugin; the platform uses the viewer's caption appearance settings instead of fixed plugin defaults. |
|
|
591
|
+
| **`backgroundColor`** | <code>string</code> | Background color in RGBA. On Android, when the plugin applies subtitle styling in fullscreen, omitted values fall back to `rgba(0,0,0,1)`. In picture-in-picture, omitted values fall back to transparent when the plugin applies its caption style (for example sidecar subtitles or non-empty `subtitleOptions` that include a color override). An empty `subtitleOptions` object alone does not select that fallback. With font-size-only options, embedded cue styles stay enabled, so authored cue backgrounds can remain visible in PiP. When `subtitleOptions` is absent, embedded cue colors from the media can override the fallback. On iOS, omitted colors are not set by the plugin; the platform uses the viewer's caption appearance settings instead of fixed plugin defaults. |
|
|
592
|
+
| **`fontSize`** | <code>number</code> | Font size in CSS pixels. On Android, supplied values are rounded to the nearest integer before applying. When omitted on embedded tracks without a sidecar subtitle URI, Android uses `SubtitleView.setUserDefaultTextSize()` (the user's system caption size), not a fixed 16 px size. Sidecar subtitles still use a fixed default size when this property is omitted. On iOS, values map to AVFoundation relative size where 16 equals the system default (100%). |
|
|
574
593
|
|
|
575
594
|
|
|
576
595
|
#### DrmOptions
|
|
@@ -3,6 +3,7 @@ package com.capgo.videoplayer;
|
|
|
3
3
|
import android.annotation.SuppressLint;
|
|
4
4
|
import android.app.Activity;
|
|
5
5
|
import android.app.ActivityManager;
|
|
6
|
+
import android.app.AlertDialog;
|
|
6
7
|
import android.app.PictureInPictureParams;
|
|
7
8
|
import android.content.ContentUris;
|
|
8
9
|
import android.content.Context;
|
|
@@ -78,11 +79,13 @@ import com.google.android.exoplayer2.ui.CaptionStyleCompat;
|
|
|
78
79
|
import com.google.android.exoplayer2.ui.DefaultTimeBar;
|
|
79
80
|
import com.google.android.exoplayer2.ui.PlayerControlView;
|
|
80
81
|
import com.google.android.exoplayer2.ui.StyledPlayerView;
|
|
82
|
+
import com.google.android.exoplayer2.ui.SubtitleView;
|
|
81
83
|
import com.google.android.exoplayer2.upstream.DataSource;
|
|
82
84
|
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
|
83
85
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
|
84
86
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
|
85
87
|
import com.google.android.exoplayer2.util.MimeTypes;
|
|
88
|
+
import com.google.android.exoplayer2.util.Util;
|
|
86
89
|
import com.google.android.gms.cast.MediaInfo;
|
|
87
90
|
import com.google.android.gms.cast.MediaQueueItem;
|
|
88
91
|
import com.google.android.gms.cast.MediaTrack;
|
|
@@ -99,6 +102,7 @@ import java.net.HttpURLConnection;
|
|
|
99
102
|
import java.net.URL;
|
|
100
103
|
import java.util.ArrayList;
|
|
101
104
|
import java.util.Arrays;
|
|
105
|
+
import java.util.Formatter;
|
|
102
106
|
import java.util.HashMap;
|
|
103
107
|
import java.util.List;
|
|
104
108
|
import java.util.Locale;
|
|
@@ -115,6 +119,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
115
119
|
public Float videoRate;
|
|
116
120
|
public String playerId;
|
|
117
121
|
public List<VideoSubtitleTrack> subtitleTracks;
|
|
122
|
+
public List<VideoChapter> chapters = new ArrayList<>();
|
|
118
123
|
public String subTitle;
|
|
119
124
|
public String language;
|
|
120
125
|
public JSObject subTitleOptions;
|
|
@@ -170,6 +175,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
170
175
|
private ProgressBar Pbar;
|
|
171
176
|
private View view;
|
|
172
177
|
private ImageButton closeBtn;
|
|
178
|
+
private ImageButton chaptersBtn;
|
|
173
179
|
private ImageButton pipBtn;
|
|
174
180
|
private ImageButton resizeBtn;
|
|
175
181
|
private ConstraintLayout constLayout;
|
|
@@ -292,6 +298,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
closeBtn = view.findViewById(R.id.exo_close);
|
|
301
|
+
chaptersBtn = view.findViewById(R.id.exo_chapters);
|
|
295
302
|
pipBtn = view.findViewById(R.id.exo_pip);
|
|
296
303
|
styledPlayerView.requestFocus();
|
|
297
304
|
linearLayout.setVisibility(View.INVISIBLE);
|
|
@@ -338,6 +345,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
338
345
|
linearLayout.setVisibility(View.INVISIBLE);
|
|
339
346
|
Log.v(TAG, "**** in ExoPlayer.STATE_READY firstReadyToPlay " + firstReadyToPlay);
|
|
340
347
|
updateSubtitleButtonVisibility(player.getCurrentTracks());
|
|
348
|
+
updateChaptersButtonVisibility();
|
|
341
349
|
|
|
342
350
|
if (firstReadyToPlay) {
|
|
343
351
|
firstReadyToPlay = false;
|
|
@@ -367,6 +375,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
367
375
|
if (pipEnabled) {
|
|
368
376
|
pipBtn.setVisibility(View.VISIBLE);
|
|
369
377
|
}
|
|
378
|
+
updateChaptersButtonVisibility();
|
|
370
379
|
} else {
|
|
371
380
|
Log.v(TAG, "**** in ExoPlayer.STATE_READY going to notify playerItemPause ");
|
|
372
381
|
NotificationCenter.defaultCenter().postNotification("playerItemPause", info);
|
|
@@ -408,6 +417,16 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
408
417
|
Toast.makeText(context, "Device is a TV ", Toast.LENGTH_SHORT).show();
|
|
409
418
|
}
|
|
410
419
|
|
|
420
|
+
stForeColor = subTitleOptions != null ? subTitleOptions.optString("foregroundColor", "rgba(255,255,255,1)") : "rgba(255,255,255,1)";
|
|
421
|
+
stBackColor = subTitleOptions != null ? subTitleOptions.optString("backgroundColor", "rgba(0,0,0,1)") : "rgba(0,0,0,1)";
|
|
422
|
+
stFontSize = 16;
|
|
423
|
+
if (subTitleOptions != null && subTitleOptions.has("fontSize")) {
|
|
424
|
+
int fontSizeCandidate = (int) Math.round(subTitleOptions.optDouble("fontSize", 16));
|
|
425
|
+
if (fontSizeCandidate > 0) {
|
|
426
|
+
stFontSize = fontSizeCandidate;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
411
430
|
if (!isInternal) {
|
|
412
431
|
uri = Uri.parse(videoPath);
|
|
413
432
|
subtitleUris = new ArrayList<>();
|
|
@@ -427,15 +446,6 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
427
446
|
subtitleLanguages.add(language != null ? language : "en");
|
|
428
447
|
}
|
|
429
448
|
|
|
430
|
-
stForeColor =
|
|
431
|
-
subTitleOptions != null && subTitleOptions.has("foregroundColor")
|
|
432
|
-
? subTitleOptions.getString("foregroundColor")
|
|
433
|
-
: "rgba(255,255,255,1)";
|
|
434
|
-
stBackColor =
|
|
435
|
-
subTitleOptions != null && subTitleOptions.has("backgroundColor")
|
|
436
|
-
? subTitleOptions.getString("backgroundColor")
|
|
437
|
-
: "rgba(0,0,0,1)";
|
|
438
|
-
stFontSize = subTitleOptions != null && subTitleOptions.has("fontSize") ? subTitleOptions.getInteger("fontSize") : 16;
|
|
439
449
|
// get video type
|
|
440
450
|
vType = getVideoType(uri);
|
|
441
451
|
Log.v(TAG, "display url: " + uri);
|
|
@@ -506,6 +516,14 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
506
516
|
}
|
|
507
517
|
}
|
|
508
518
|
);
|
|
519
|
+
chaptersBtn.setOnClickListener(
|
|
520
|
+
new View.OnClickListener() {
|
|
521
|
+
@Override
|
|
522
|
+
public void onClick(View view) {
|
|
523
|
+
showChapterPicker();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
);
|
|
509
527
|
pipBtn.setOnClickListener(
|
|
510
528
|
new View.OnClickListener() {
|
|
511
529
|
@Override
|
|
@@ -682,9 +700,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
682
700
|
}
|
|
683
701
|
|
|
684
702
|
isInPictureInPictureMode = activity.isInPictureInPictureMode();
|
|
685
|
-
|
|
686
|
-
setSubtitle(true);
|
|
687
|
-
}
|
|
703
|
+
applySubtitleAppearance(true);
|
|
688
704
|
play();
|
|
689
705
|
handler.postDelayed(mRunnable, 100);
|
|
690
706
|
}
|
|
@@ -722,9 +738,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
722
738
|
if (inPictureInPictureMode) {
|
|
723
739
|
linearLayout.setVisibility(View.INVISIBLE);
|
|
724
740
|
styledPlayerView.setUseController(false);
|
|
725
|
-
|
|
726
|
-
setSubtitle(true);
|
|
727
|
-
}
|
|
741
|
+
applySubtitleAppearance(true);
|
|
728
742
|
return;
|
|
729
743
|
}
|
|
730
744
|
|
|
@@ -732,9 +746,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
732
746
|
if (showControls) {
|
|
733
747
|
styledPlayerView.setUseController(true);
|
|
734
748
|
}
|
|
735
|
-
|
|
736
|
-
setSubtitle(false);
|
|
737
|
-
}
|
|
749
|
+
applySubtitleAppearance(false);
|
|
738
750
|
}
|
|
739
751
|
|
|
740
752
|
private void checkPIPPermission() {
|
|
@@ -871,7 +883,10 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
871
883
|
}
|
|
872
884
|
}
|
|
873
885
|
if (sturi != null) {
|
|
874
|
-
|
|
886
|
+
applySubtitleAppearance(false);
|
|
887
|
+
styledPlayerView.setShowSubtitleButton(true);
|
|
888
|
+
} else {
|
|
889
|
+
applySubtitleAppearance(false);
|
|
875
890
|
}
|
|
876
891
|
}
|
|
877
892
|
}
|
|
@@ -955,7 +970,10 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
955
970
|
}
|
|
956
971
|
};
|
|
957
972
|
if (sturi != null) {
|
|
958
|
-
|
|
973
|
+
applySubtitleAppearance(false);
|
|
974
|
+
styledPlayerView.setShowSubtitleButton(true);
|
|
975
|
+
} else {
|
|
976
|
+
applySubtitleAppearance(false);
|
|
959
977
|
}
|
|
960
978
|
//Use Media Session Connector from the EXT library to enable MediaSession Controls in PIP.
|
|
961
979
|
mediaSession = new MediaSessionCompat(context, "capacitorvideoplayer");
|
|
@@ -966,6 +984,69 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
966
984
|
NotificationCenter.defaultCenter().postNotification("initializePlayer", info);
|
|
967
985
|
}
|
|
968
986
|
|
|
987
|
+
private boolean isActivePlayerReady() {
|
|
988
|
+
if (isCastSession) {
|
|
989
|
+
return castPlayer != null && castPlayer.getPlaybackState() == Player.STATE_READY;
|
|
990
|
+
}
|
|
991
|
+
return playerReady;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private void updateChaptersButtonVisibility() {
|
|
995
|
+
if (chaptersBtn == null) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
boolean visible = chapters != null && !chapters.isEmpty() && isActivePlayerReady() && showControls;
|
|
999
|
+
chaptersBtn.setVisibility(visible ? View.VISIBLE : View.GONE);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
private void showChapterPicker() {
|
|
1003
|
+
if (chapters == null || chapters.isEmpty()) {
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (player == null && !(isCastSession && castPlayer != null)) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
CharSequence[] labels = new CharSequence[chapters.size()];
|
|
1011
|
+
for (int i = 0; i < chapters.size(); i++) {
|
|
1012
|
+
labels[i] = formatChapterLabel(chapters.get(i));
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
new AlertDialog.Builder(requireActivity())
|
|
1016
|
+
.setTitle(R.string.chapters)
|
|
1017
|
+
.setItems(labels, (dialog, which) -> seekToChapter(chapters.get(which)))
|
|
1018
|
+
.show();
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private String formatChapterLabel(VideoChapter chapter) {
|
|
1022
|
+
long startMs = (long) (chapter.startTimeSeconds * 1000.0);
|
|
1023
|
+
StringBuilder builder = new StringBuilder();
|
|
1024
|
+
Formatter formatter = new Formatter(builder, Locale.getDefault());
|
|
1025
|
+
String timeLabel = Util.getStringForTime(builder, formatter, startMs);
|
|
1026
|
+
formatter.close();
|
|
1027
|
+
if (chapter.title.isEmpty()) {
|
|
1028
|
+
return timeLabel;
|
|
1029
|
+
}
|
|
1030
|
+
return chapter.title + " (" + timeLabel + ")";
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private void seekToChapter(VideoChapter chapter) {
|
|
1034
|
+
if (chapter == null) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
long targetMs = (long) (chapter.startTimeSeconds * 1000.0);
|
|
1038
|
+
if (isCastSession && castPlayer != null) {
|
|
1039
|
+
castPlayer.seekTo(targetMs);
|
|
1040
|
+
castPlayer.setPlayWhenReady(true);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (player == null) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
player.seekTo(targetMs);
|
|
1047
|
+
play();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
969
1050
|
private void updateSubtitleButtonVisibility(Tracks tracks) {
|
|
970
1051
|
if (styledPlayerView == null) return;
|
|
971
1052
|
boolean hasSidecar = !subtitleUris.isEmpty() || sturi != null;
|
|
@@ -985,29 +1066,53 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
985
1066
|
return false;
|
|
986
1067
|
}
|
|
987
1068
|
|
|
988
|
-
private void
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1069
|
+
private void applySubtitleAppearance(boolean transparentBackground) {
|
|
1070
|
+
SubtitleView subtitleView = styledPlayerView.getSubtitleView();
|
|
1071
|
+
boolean hasSidecar = sturi != null;
|
|
1072
|
+
boolean hasColorOverride = SubtitleOptionOverrides.hasColor(subTitleOptions);
|
|
1073
|
+
boolean hasBackgroundColorOverride = SubtitleOptionOverrides.hasBackgroundColor(subTitleOptions);
|
|
1074
|
+
boolean hasAnyOptions = SubtitleOptionOverrides.hasAny(subTitleOptions);
|
|
1075
|
+
boolean shouldApplyCaptionStyle = hasColorOverride || hasSidecar || (transparentBackground && hasAnyOptions);
|
|
1076
|
+
|
|
1077
|
+
if (hasColorOverride) {
|
|
1078
|
+
subtitleView.setApplyEmbeddedStyles(false);
|
|
1079
|
+
subtitleView.setApplyEmbeddedFontSizes(false);
|
|
1080
|
+
} else if (SubtitleOptionOverrides.hasFontSize(subTitleOptions)) {
|
|
1081
|
+
subtitleView.setApplyEmbeddedStyles(true);
|
|
1082
|
+
subtitleView.setApplyEmbeddedFontSizes(false);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (shouldApplyCaptionStyle) {
|
|
1086
|
+
int foreground = Color.WHITE;
|
|
1087
|
+
if (hasColorOverride) {
|
|
1088
|
+
Integer parsedForeground = SubtitleOptionOverrides.parseRgbaColor(stForeColor);
|
|
1089
|
+
if (parsedForeground != null) {
|
|
1090
|
+
foreground = parsedForeground;
|
|
1091
|
+
}
|
|
996
1092
|
}
|
|
997
|
-
|
|
998
|
-
|
|
1093
|
+
int background;
|
|
1094
|
+
if (transparentBackground && !hasBackgroundColorOverride) {
|
|
1095
|
+
background = Color.TRANSPARENT;
|
|
1096
|
+
} else {
|
|
1097
|
+
background = Color.BLACK;
|
|
1098
|
+
if (hasBackgroundColorOverride) {
|
|
1099
|
+
Integer parsedBackground = SubtitleOptionOverrides.parseRgbaColor(stBackColor);
|
|
1100
|
+
if (parsedBackground != null) {
|
|
1101
|
+
background = parsedBackground;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
999
1104
|
}
|
|
1000
|
-
|
|
1001
|
-
foreground = Color.TRANSPARENT;
|
|
1002
|
-
background = Color.TRANSPARENT;
|
|
1003
|
-
}
|
|
1004
|
-
styledPlayerView
|
|
1005
|
-
.getSubtitleView()
|
|
1006
|
-
.setStyle(
|
|
1105
|
+
subtitleView.setStyle(
|
|
1007
1106
|
new CaptionStyleCompat(foreground, background, Color.TRANSPARENT, CaptionStyleCompat.EDGE_TYPE_NONE, Color.WHITE, null)
|
|
1008
1107
|
);
|
|
1009
|
-
|
|
1010
|
-
|
|
1108
|
+
} else {
|
|
1109
|
+
subtitleView.setUserDefaultStyle();
|
|
1110
|
+
}
|
|
1111
|
+
if (SubtitleOptionOverrides.shouldUseFixedTextSize(subTitleOptions, hasSidecar)) {
|
|
1112
|
+
subtitleView.setFixedTextSize(TypedValue.COMPLEX_UNIT_DIP, stFontSize);
|
|
1113
|
+
} else {
|
|
1114
|
+
subtitleView.setUserDefaultTextSize();
|
|
1115
|
+
}
|
|
1011
1116
|
}
|
|
1012
1117
|
|
|
1013
1118
|
/**
|
|
@@ -1126,20 +1231,6 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1126
1231
|
}
|
|
1127
1232
|
}
|
|
1128
1233
|
|
|
1129
|
-
private int getColorFromRGBA(String rgbaColor) {
|
|
1130
|
-
int ret = 0;
|
|
1131
|
-
String color = rgbaColor.substring(rgbaColor.indexOf("(") + 1, rgbaColor.indexOf(")"));
|
|
1132
|
-
List<String> colors = Arrays.asList(color.split(","));
|
|
1133
|
-
if (colors.size() == 4) {
|
|
1134
|
-
ret =
|
|
1135
|
-
((Math.round(Float.parseFloat(colors.get(3).trim()) * 255) & 0xff) << 24) |
|
|
1136
|
-
((Integer.parseInt(colors.get(0).trim()) & 0xff) << 16) |
|
|
1137
|
-
((Integer.parseInt(colors.get(1).trim()) & 0xff) << 8) |
|
|
1138
|
-
(Integer.parseInt(colors.get(2).trim()) & 0xff);
|
|
1139
|
-
}
|
|
1140
|
-
return ret;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
1234
|
private MediaSource getSubTitles(MediaSource mediaSource, List<Uri> subtitleUris, DataSource.Factory dataSourceFactory) {
|
|
1144
1235
|
if (subtitleUris == null || subtitleUris.isEmpty()) {
|
|
1145
1236
|
return mediaSource;
|
|
@@ -1564,6 +1655,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1564
1655
|
};
|
|
1565
1656
|
switch (state) {
|
|
1566
1657
|
case CastPlayer.STATE_READY:
|
|
1658
|
+
updateChaptersButtonVisibility();
|
|
1567
1659
|
if (castPlayer.isPlaying()) {
|
|
1568
1660
|
NotificationCenter.defaultCenter().postNotification("playerItemPlay", info);
|
|
1569
1661
|
} else {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package com.capgo.videoplayer;
|
|
2
|
+
|
|
3
|
+
import java.util.Arrays;
|
|
4
|
+
import java.util.List;
|
|
5
|
+
import org.json.JSONObject;
|
|
6
|
+
|
|
7
|
+
final class SubtitleOptionOverrides {
|
|
8
|
+
|
|
9
|
+
private SubtitleOptionOverrides() {}
|
|
10
|
+
|
|
11
|
+
static boolean hasAny(JSONObject options) {
|
|
12
|
+
return options != null && options.length() > 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static boolean hasFontSize(JSONObject options) {
|
|
16
|
+
return hasFontSize(options != null && options.has("fontSize"), options != null ? options.optDouble("fontSize", 16) : 16);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static boolean hasFontSize(boolean hasFontSizeKey, double fontSizeValue) {
|
|
20
|
+
if (!hasFontSizeKey) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return Math.round(fontSizeValue) > 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static boolean hasColor(JSONObject options) {
|
|
27
|
+
if (options == null) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return hasColor(options.has("foregroundColor"), options.has("backgroundColor"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static boolean hasColor(boolean hasForegroundColor, boolean hasBackgroundColor) {
|
|
34
|
+
return hasForegroundColor || hasBackgroundColor;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static boolean hasBackgroundColor(JSONObject options) {
|
|
38
|
+
return options != null && hasBackgroundColor(options.has("backgroundColor"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static boolean hasBackgroundColor(boolean hasBackgroundColorKey) {
|
|
42
|
+
return hasBackgroundColorKey;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static boolean shouldUseFixedTextSize(JSONObject options, boolean hasSidecarUri) {
|
|
46
|
+
return hasFontSize(options) || hasSidecarUri;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static boolean shouldUseFixedTextSize(boolean hasFontSizeOverride, boolean hasSidecarUri) {
|
|
50
|
+
return hasFontSizeOverride || hasSidecarUri;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static Integer parseRgbaColor(String rgbaColor) {
|
|
54
|
+
if (rgbaColor == null || rgbaColor.length() < 5 || !rgbaColor.regionMatches(true, 0, "rgba", 0, 4)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
int open = rgbaColor.indexOf('(');
|
|
58
|
+
int close = rgbaColor.indexOf(')');
|
|
59
|
+
if (open < 0 || close <= open) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
String inner = rgbaColor.substring(open + 1, close);
|
|
63
|
+
List<String> colors = Arrays.asList(inner.split(","));
|
|
64
|
+
if (colors.size() != 4) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
int red = Integer.parseInt(colors.get(0).trim());
|
|
69
|
+
int green = Integer.parseInt(colors.get(1).trim());
|
|
70
|
+
int blue = Integer.parseInt(colors.get(2).trim());
|
|
71
|
+
float alpha = Float.parseFloat(colors.get(3).trim());
|
|
72
|
+
if (!Float.isFinite(alpha) || alpha < 0f || alpha > 1f) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
int alphaInt = Math.round(alpha * 255f) & 0xff;
|
|
79
|
+
return (alphaInt << 24) | ((red & 0xff) << 16) | ((green & 0xff) << 8) | (blue & 0xff);
|
|
80
|
+
} catch (NumberFormatException exception) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.capgo.videoplayer;
|
|
2
|
+
|
|
3
|
+
public class VideoChapter {
|
|
4
|
+
|
|
5
|
+
public final String title;
|
|
6
|
+
public final double startTimeSeconds;
|
|
7
|
+
public final Double endTimeSeconds;
|
|
8
|
+
|
|
9
|
+
public VideoChapter(String title, double startTimeSeconds, Double endTimeSeconds) {
|
|
10
|
+
this.title = title != null ? title : "";
|
|
11
|
+
this.startTimeSeconds = startTimeSeconds;
|
|
12
|
+
this.endTimeSeconds = endTimeSeconds;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -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.ArrayList;
|
|
6
7
|
import java.util.List;
|
|
7
8
|
|
|
8
9
|
public class VideoPlayer {
|
|
@@ -24,6 +25,7 @@ public class VideoPlayer {
|
|
|
24
25
|
Boolean showControls,
|
|
25
26
|
String displayMode,
|
|
26
27
|
List<VideoSubtitleTrack> subtitleTracks,
|
|
28
|
+
List<VideoChapter> chapters,
|
|
27
29
|
JSObject subTitleOptions,
|
|
28
30
|
JSObject headers,
|
|
29
31
|
String title,
|
|
@@ -49,6 +51,7 @@ public class VideoPlayer {
|
|
|
49
51
|
fsFragment.showControls = showControls;
|
|
50
52
|
fsFragment.displayMode = displayMode;
|
|
51
53
|
fsFragment.subtitleTracks = subtitleTracks;
|
|
54
|
+
fsFragment.chapters = chapters != null ? chapters : new ArrayList<>();
|
|
52
55
|
fsFragment.subTitleOptions = subTitleOptions;
|
|
53
56
|
fsFragment.headers = headers;
|
|
54
57
|
fsFragment.title = title;
|
|
@@ -46,7 +46,7 @@ import org.json.JSONObject;
|
|
|
46
46
|
)
|
|
47
47
|
public class VideoPlayerPlugin extends Plugin {
|
|
48
48
|
|
|
49
|
-
private final String pluginVersion = "8.
|
|
49
|
+
private final String pluginVersion = "8.3.0";
|
|
50
50
|
|
|
51
51
|
// Permission alias constants
|
|
52
52
|
private static final String PERMISSION_DENIED_ERROR = "Unable to access media videos, user denied permission request";
|
|
@@ -90,6 +90,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
90
90
|
private String subtitle = "";
|
|
91
91
|
private String language = "";
|
|
92
92
|
private List<VideoSubtitleTrack> subtitleTracks = new ArrayList<>();
|
|
93
|
+
private List<VideoChapter> chapters = new ArrayList<>();
|
|
93
94
|
private JSObject subTitleOptions;
|
|
94
95
|
private JSObject drmOptions;
|
|
95
96
|
private final JSObject ret = new JSObject();
|
|
@@ -208,6 +209,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
208
209
|
language = call.getString("language");
|
|
209
210
|
}
|
|
210
211
|
subtitleTracks = parseSubtitleTracks(call);
|
|
212
|
+
chapters = parseChapters(call);
|
|
211
213
|
subTitleOptions = new JSObject();
|
|
212
214
|
if (call.getData().has("subtitleOptions")) {
|
|
213
215
|
subTitleOptions = call.getObject("subtitleOptions");
|
|
@@ -982,6 +984,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
982
984
|
showControls,
|
|
983
985
|
displayMode,
|
|
984
986
|
resolvedSubtitleTracks,
|
|
987
|
+
chapters,
|
|
985
988
|
subTitleOptions,
|
|
986
989
|
headers,
|
|
987
990
|
title,
|
|
@@ -1144,7 +1147,8 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1144
1147
|
showControls,
|
|
1145
1148
|
displayMode,
|
|
1146
1149
|
new ArrayList<>(),
|
|
1147
|
-
|
|
1150
|
+
chapters,
|
|
1151
|
+
subTitleOptions,
|
|
1148
1152
|
headers,
|
|
1149
1153
|
title,
|
|
1150
1154
|
smallTitle,
|
|
@@ -1198,6 +1202,46 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1198
1202
|
return tracks;
|
|
1199
1203
|
}
|
|
1200
1204
|
|
|
1205
|
+
private List<VideoChapter> parseChapters(PluginCall call) {
|
|
1206
|
+
List<VideoChapter> parsed = new ArrayList<>();
|
|
1207
|
+
if (!call.getData().has("chapters")) {
|
|
1208
|
+
return parsed;
|
|
1209
|
+
}
|
|
1210
|
+
JSArray chapterArray = call.getArray("chapters");
|
|
1211
|
+
if (chapterArray == null) {
|
|
1212
|
+
return parsed;
|
|
1213
|
+
}
|
|
1214
|
+
for (int i = 0; i < chapterArray.length(); i++) {
|
|
1215
|
+
JSONObject entry = chapterArray.optJSONObject(i);
|
|
1216
|
+
if (entry == null) {
|
|
1217
|
+
Log.w(TAG, "Skipping malformed chapter entry at index " + i);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
Object rawTitle = entry.opt("title");
|
|
1221
|
+
if (!(rawTitle instanceof String)) {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
String chapterTitle = (String) rawTitle;
|
|
1225
|
+
if (chapterTitle.trim().isEmpty()) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (!entry.has("startTime")) {
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
double startTime = entry.optDouble("startTime", -1);
|
|
1232
|
+
if (Double.isNaN(startTime) || Double.isInfinite(startTime) || startTime < 0) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
Double endTime = entry.has("endTime") ? entry.optDouble("endTime", -1) : null;
|
|
1236
|
+
if (endTime != null && (Double.isNaN(endTime) || Double.isInfinite(endTime) || endTime <= startTime)) {
|
|
1237
|
+
endTime = null;
|
|
1238
|
+
}
|
|
1239
|
+
parsed.add(new VideoChapter(chapterTitle.trim(), startTime, endTime));
|
|
1240
|
+
}
|
|
1241
|
+
parsed.sort((left, right) -> Double.compare(left.startTimeSeconds, right.startTimeSeconds));
|
|
1242
|
+
return parsed;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1201
1245
|
private List<VideoSubtitleTrack> resolveSubtitleTracks(List<VideoSubtitleTrack> tracks) {
|
|
1202
1246
|
List<VideoSubtitleTrack> resolved = new ArrayList<>();
|
|
1203
1247
|
if (tracks == null) {
|
|
@@ -1227,6 +1271,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1227
1271
|
Boolean showControls,
|
|
1228
1272
|
String displayMode,
|
|
1229
1273
|
List<VideoSubtitleTrack> subtitleTracks,
|
|
1274
|
+
List<VideoChapter> chapters,
|
|
1230
1275
|
JSObject subTitleOptions,
|
|
1231
1276
|
JSObject headers,
|
|
1232
1277
|
String title,
|
|
@@ -1253,6 +1298,7 @@ public class VideoPlayerPlugin extends Plugin {
|
|
|
1253
1298
|
showControls,
|
|
1254
1299
|
displayMode,
|
|
1255
1300
|
subtitleTracks,
|
|
1301
|
+
chapters,
|
|
1256
1302
|
subTitleOptions,
|
|
1257
1303
|
headers,
|
|
1258
1304
|
title,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:width="24dp"
|
|
4
|
+
android:height="24dp"
|
|
5
|
+
android:viewportWidth="24"
|
|
6
|
+
android:viewportHeight="24">
|
|
7
|
+
<path
|
|
8
|
+
android:fillColor="#FFFFFFFF"
|
|
9
|
+
android:pathData="M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5S3.17,7.5 4,7.5 5.5,6.83 5.5,6 4.83,4.5 4,4.5zM4,16.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM7,19h14v-2H7v2zM7,13h14v-2H7v2zM7,5v2h14V5H7z" />
|
|
10
|
+
</vector>
|
|
@@ -115,6 +115,15 @@
|
|
|
115
115
|
android:background="?actionBarItemBackground"
|
|
116
116
|
android:visibility="gone"/>
|
|
117
117
|
|
|
118
|
+
<ImageButton
|
|
119
|
+
android:id="@+id/exo_chapters"
|
|
120
|
+
android:layout_width="48dp"
|
|
121
|
+
android:layout_height="48dp"
|
|
122
|
+
android:background="?actionBarItemBackground"
|
|
123
|
+
android:src="@drawable/ic_chapters_white"
|
|
124
|
+
android:visibility="gone"
|
|
125
|
+
android:contentDescription="@string/chapters_button"/>
|
|
126
|
+
|
|
118
127
|
<ImageButton
|
|
119
128
|
android:id="@+id/exo_pip"
|
|
120
129
|
android:layout_width="48dp"
|