@capgo/capacitor-video-player 8.2.14 → 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.
Files changed (22) hide show
  1. package/README.md +68 -33
  2. package/android/src/main/java/com/capgo/videoplayer/FullscreenExoPlayerFragment.java +145 -53
  3. package/android/src/main/java/com/capgo/videoplayer/SubtitleOptionOverrides.java +84 -0
  4. package/android/src/main/java/com/capgo/videoplayer/VideoChapter.java +14 -0
  5. package/android/src/main/java/com/capgo/videoplayer/VideoPlayer.java +3 -0
  6. package/android/src/main/java/com/capgo/videoplayer/VideoPlayerPlugin.java +48 -2
  7. package/android/src/main/res/drawable/ic_chapters_white.xml +10 -0
  8. package/android/src/main/res/layout/exo_playback_control_view.xml +9 -0
  9. package/android/src/main/res/values/strings.xml +2 -0
  10. package/dist/docs.json +44 -5
  11. package/dist/esm/definitions.d.ts +58 -6
  12. package/dist/esm/definitions.js.map +1 -1
  13. package/ios/Sources/VideoPlayerPlugin/AVPlayerControlsVisibilityObserver.swift +13 -0
  14. package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +243 -33
  15. package/ios/Sources/VideoPlayerPlugin/SubtitleStyleOptions.swift +7 -0
  16. package/ios/Sources/VideoPlayerPlugin/VideoChapter.swift +45 -0
  17. package/ios/Sources/VideoPlayerPlugin/VideoPlayerAudioSessionCoordinator.swift +101 -0
  18. package/ios/Sources/VideoPlayerPlugin/VideoPlayerCastController.swift +23 -5
  19. package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +49 -1
  20. package/ios/Tests/VideoPlayerPluginTests/VideoChapterTests.swift +30 -0
  21. package/ios/Tests/VideoPlayerPluginTests/VideoPlayerAudioSessionCategoryPolicyTests.swift +52 -0
  22. package/package.json +6 -4
package/README.md CHANGED
@@ -149,6 +149,22 @@ Picture in Picture on Android requires your app activity to declare PiP support
149
149
 
150
150
  `launchMode="singleTask"` and the `configChanges` flags prevent the player activity from being recreated when entering or leaving PiP.
151
151
 
152
+ ## iOS Picture in Picture and background audio
153
+
154
+ On iOS the native player configures `AVAudioSession` for PiP and background playback when `pipEnabled` or `bkmodeEnabled` is true (both default to true). Your app still needs the right plist and capability entries:
155
+
156
+ 1. In Xcode, add the **Background Modes** capability and enable **Audio, AirPlay, and Picture in Picture**.
157
+ 2. In `ios/App/App/Info.plist`, include the audio background mode:
158
+
159
+ ```xml
160
+ <key>UIBackgroundModes</key>
161
+ <array>
162
+ <string>audio</string>
163
+ </array>
164
+ ```
165
+
166
+ Test PiP on a physical iPhone (the Simulator does not support Picture in Picture). Setting `audioCategory: 'ambient'` respects the silent switch but silences audio on screen lock; leave it unset or use `playback` for audible background playback.
167
+
152
168
  ## API
153
169
 
154
170
  <docgen-index>
@@ -510,34 +526,35 @@ On web, the implementation returns `"web"`.
510
526
 
511
527
  #### capVideoPlayerOptions
512
528
 
513
- | Prop | Type | Description |
514
- | --------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
515
- | **`mode`** | <code>string</code> | Player mode - "fullscreen" - "embedded" (Web only) |
516
- | **`url`** | <code>string</code> | The url of the video to play |
517
- | **`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). |
518
- | **`subtitle`** | <code>string</code> | The url of subtitle associated with the video Prefer `subtitles` when providing more than one track. Kept for backward compatibility. |
519
- | **`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. |
520
- | **`subtitles`** | <code>VideoSubtitle[]</code> | Multiple subtitle tracks (iOS, Android). When provided, takes precedence over `subtitle` / `language`. |
521
- | **`subtitleOptions`** | <code><a href="#subtitleoptions">SubTitleOptions</a></code> | SubTitle Options |
522
- | **`playerId`** | <code>string</code> | Id of DIV Element parent of the player |
523
- | **`rate`** | <code>number</code> | Initial playing rate |
524
- | **`exitOnEnd`** | <code>boolean</code> | Exit on VideoEnd (iOS, Android) default: true |
525
- | **`loopOnEnd`** | <code>boolean</code> | Loop on VideoEnd when exitOnEnd false (iOS, Android) default: false |
526
- | **`pipEnabled`** | <code>boolean</code> | Picture in Picture Enable (iOS, Android) default: true |
527
- | **`bkmodeEnabled`** | <code>boolean</code> | Background Mode Enable (iOS, Android) default: true |
528
- | **`audioCategory`** | <code>'ambient' \| 'playback' \| 'moviePlayback'</code> | iOS audio session category behavior (iOS). - "ambient": respects the iPhone silent switch (default) - "playback": ignores the iPhone silent switch - "moviePlayback": playback optimized for video and long-form AirPlay default: "ambient" |
529
- | **`showControls`** | <code>boolean</code> | Show Controls Enable (iOS, Android) default: true |
530
- | **`displayMode`** | <code>string</code> | Display Mode ["all", "portrait", "landscape"] (iOS, Android) default: "all" |
531
- | **`componentTag`** | <code>string</code> | Component Tag or DOM Element Tag (React app) |
532
- | **`width`** | <code>number</code> | Player Width (mode "embedded" only) |
533
- | **`height`** | <code>number</code> | Player height (mode "embedded" only) |
534
- | **`headers`** | <code>{ [key: string]: string; }</code> | Headers for the request (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
535
- | **`title`** | <code>string</code> | Title shown in the player and Chromecast metadata (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
536
- | **`smallTitle`** | <code>string</code> | Subtitle shown below the title in the player and Chromecast metadata (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
537
- | **`accentColor`** | <code>string</code> | ExoPlayer Progress Bar and Spinner color (Android) by Manuel García Marín (https://github.com/PhantomPainX) Must be a valid hex color code default: #FFFFFF |
538
- | **`chromecast`** | <code>boolean</code> | Chromecast enable/disable (iOS, Android) iOS requires Google Cast SDK setup and local network Info.plist keys. by Manuel García Marín (https://github.com/PhantomPainX) default: true |
539
- | **`artwork`** | <code>string</code> | Artwork url to be shown in Chromecast player (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) default: "" |
540
- | **`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`. |
529
+ | Prop | Type | Description |
530
+ | --------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
531
+ | **`mode`** | <code>string</code> | Player mode - "fullscreen" - "embedded" (Web only) |
532
+ | **`url`** | <code>string</code> | The url of the video to play |
533
+ | **`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). |
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
+ | **`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
+ | **`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`. |
538
+ | **`subtitleOptions`** | <code><a href="#subtitleoptions">SubTitleOptions</a></code> | SubTitle Options |
539
+ | **`playerId`** | <code>string</code> | Id of DIV Element parent of the player |
540
+ | **`rate`** | <code>number</code> | Initial playing rate |
541
+ | **`exitOnEnd`** | <code>boolean</code> | Exit on VideoEnd (iOS, Android) default: true |
542
+ | **`loopOnEnd`** | <code>boolean</code> | Loop on VideoEnd when exitOnEnd false (iOS, Android) default: false |
543
+ | **`pipEnabled`** | <code>boolean</code> | Picture in Picture Enable (iOS, Android) default: true |
544
+ | **`bkmodeEnabled`** | <code>boolean</code> | Background Mode Enable (iOS, Android) default: true |
545
+ | **`audioCategory`** | <code>'ambient' \| 'playback' \| 'moviePlayback'</code> | iOS audio session category behavior (iOS). - "ambient": respects the iPhone silent switch - "playback": ignores the iPhone silent switch - "moviePlayback": playback optimized for video and long-form AirPlay. When omitted, the plugin resolves the category from other options: `"playback"` if `pipEnabled` or `bkmodeEnabled` is true (both default to true), otherwise `"ambient"`. |
546
+ | **`showControls`** | <code>boolean</code> | Show Controls Enable (iOS, Android) default: true |
547
+ | **`displayMode`** | <code>string</code> | Display Mode ["all", "portrait", "landscape"] (iOS, Android) default: "all" |
548
+ | **`componentTag`** | <code>string</code> | Component Tag or DOM Element Tag (React app) |
549
+ | **`width`** | <code>number</code> | Player Width (mode "embedded" only) |
550
+ | **`height`** | <code>number</code> | Player height (mode "embedded" only) |
551
+ | **`headers`** | <code>{ [key: string]: string; }</code> | Headers for the request (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
552
+ | **`title`** | <code>string</code> | Title shown in the player and Chromecast metadata (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
553
+ | **`smallTitle`** | <code>string</code> | Subtitle shown below the title in the player and Chromecast metadata (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
554
+ | **`accentColor`** | <code>string</code> | ExoPlayer Progress Bar and Spinner color (Android) by Manuel García Marín (https://github.com/PhantomPainX) Must be a valid hex color code default: #FFFFFF |
555
+ | **`chromecast`** | <code>boolean</code> | Chromecast enable/disable (iOS, Android) iOS requires Google Cast SDK setup and local network Info.plist keys. by Manuel García Marín (https://github.com/PhantomPainX) default: true |
556
+ | **`artwork`** | <code>string</code> | Artwork url to be shown in Chromecast player (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) default: "" |
557
+ | **`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`. |
541
558
 
542
559
 
543
560
  #### VideoSubtitle
@@ -548,13 +565,31 @@ On web, the implementation returns `"web"`.
548
565
  | **`language`** | <code>string</code> | The language of the subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers |
549
566
 
550
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
+
551
577
  #### SubTitleOptions
552
578
 
553
- | Prop | Type | Description |
554
- | --------------------- | ------------------- | ----------------------------------------------------- |
555
- | **`foregroundColor`** | <code>string</code> | Foreground Color in RGBA (default rgba(255,255,255,1) |
556
- | **`backgroundColor`** | <code>string</code> | Background Color in RGBA (default rgba(0,0,0,1) |
557
- | **`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%). |
558
593
 
559
594
 
560
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;