@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.
- package/README.md +68 -33
- 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 +44 -5
- package/dist/esm/definitions.d.ts +58 -6
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/VideoPlayerPlugin/AVPlayerControlsVisibilityObserver.swift +13 -0
- package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +243 -33
- package/ios/Sources/VideoPlayerPlugin/SubtitleStyleOptions.swift +7 -0
- package/ios/Sources/VideoPlayerPlugin/VideoChapter.swift +45 -0
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerAudioSessionCoordinator.swift +101 -0
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerCastController.swift +23 -5
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +49 -1
- package/ios/Tests/VideoPlayerPluginTests/VideoChapterTests.swift +30 -0
- package/ios/Tests/VideoPlayerPluginTests/VideoPlayerAudioSessionCategoryPolicyTests.swift +52 -0
- 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
|
-
| **`
|
|
522
|
-
| **`
|
|
523
|
-
| **`
|
|
524
|
-
| **`
|
|
525
|
-
| **`
|
|
526
|
-
| **`
|
|
527
|
-
| **`
|
|
528
|
-
| **`
|
|
529
|
-
| **`
|
|
530
|
-
| **`
|
|
531
|
-
| **`
|
|
532
|
-
| **`
|
|
533
|
-
| **`
|
|
534
|
-
| **`
|
|
535
|
-
| **`
|
|
536
|
-
| **`
|
|
537
|
-
| **`
|
|
538
|
-
| **`
|
|
539
|
-
| **`
|
|
540
|
-
| **`
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
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;
|