@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 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
- | Prop | Type | Description |
570
- | --------------------- | ------------------- | ----------------------------------------------------- |
571
- | **`foregroundColor`** | <code>string</code> | Foreground Color in RGBA (default rgba(255,255,255,1) |
572
- | **`backgroundColor`** | <code>string</code> | Background Color in RGBA (default rgba(0,0,0,1) |
573
- | **`fontSize`** | <code>number</code> | Font Size in pixels (default 16) |
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
- if (!subtitleUris.isEmpty()) {
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
- if (!subtitleUris.isEmpty()) {
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
- if (!subtitleUris.isEmpty()) {
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
- setSubtitle(false);
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
- setSubtitle(false);
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 setSubtitle(boolean transparent) {
989
- int foreground;
990
- int background;
991
- if (!transparent) {
992
- foreground = Color.WHITE;
993
- background = Color.BLACK;
994
- if (stForeColor.length() > 4 && stForeColor.substring(0, 4).equals("rgba")) {
995
- foreground = getColorFromRGBA(stForeColor);
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
- if (stBackColor.length() > 4 && stBackColor.substring(0, 4).equals("rgba")) {
998
- background = getColorFromRGBA(stBackColor);
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
- } else {
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
- styledPlayerView.getSubtitleView().setFixedTextSize(TypedValue.COMPLEX_UNIT_DIP, stFontSize);
1010
- styledPlayerView.setShowSubtitleButton(true);
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.2.15";
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
- null,
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"
@@ -1,3 +1,5 @@
1
1
  <resources>
2
2
  <string name="my_string">Just a simple string</string>
3
+ <string name="chapters">Chapters</string>
4
+ <string name="chapters_button">Chapters button</string>
3
5
  </resources>