@capgo/capacitor-video-player 8.1.7 → 8.1.9
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/CapgoCapacitorVideoPlayer.podspec +1 -0
- package/Package.swift +4 -2
- package/README.md +99 -27
- package/android/src/main/java/com/capgo/videoplayer/FullscreenExoPlayerFragment.java +286 -32
- package/android/src/main/java/com/capgo/videoplayer/VideoPlayerPlugin.java +1 -1
- package/dist/docs.json +4 -4
- package/dist/esm/definitions.d.ts +5 -4
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +100 -4
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerCastController.swift +693 -0
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +39 -27
- package/package.json +1 -1
package/Package.swift
CHANGED
|
@@ -10,14 +10,16 @@ let package = Package(
|
|
|
10
10
|
targets: ["VideoPlayerPlugin"])
|
|
11
11
|
],
|
|
12
12
|
dependencies: [
|
|
13
|
-
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
|
|
14
|
+
.package(url: "https://github.com/SRGSSR/google-cast-sdk.git", exact: "4.8.4")
|
|
14
15
|
],
|
|
15
16
|
targets: [
|
|
16
17
|
.target(
|
|
17
18
|
name: "VideoPlayerPlugin",
|
|
18
19
|
dependencies: [
|
|
19
20
|
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
20
|
-
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
21
|
+
.product(name: "Cordova", package: "capacitor-swift-pm"),
|
|
22
|
+
.product(name: "GoogleCast", package: "google-cast-sdk")
|
|
21
23
|
],
|
|
22
24
|
path: "ios/Sources/VideoPlayerPlugin"),
|
|
23
25
|
.testTarget(
|
package/README.md
CHANGED
|
@@ -25,10 +25,82 @@ The most complete doc is available here: https://capgo.app/docs/plugins/video-pl
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
bun add @capgo/capacitor-video-player
|
|
29
|
+
bunx cap sync
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## iOS Chromecast setup
|
|
33
|
+
|
|
34
|
+
iOS Chromecast uses the Google Cast SDK default media receiver. When `chromecast` is enabled, the plugin adds a Cast button over the fullscreen iOS player, loads the current media on the selected receiver, and keeps plugin play/pause/seek/volume calls pointed at the active Cast session.
|
|
35
|
+
|
|
36
|
+
For Cast discovery on iOS, add local network keys to your app `Info.plist`.
|
|
37
|
+
|
|
38
|
+
```xml
|
|
39
|
+
<key>NSBonjourServices</key>
|
|
40
|
+
<array>
|
|
41
|
+
<string>_googlecast._tcp</string>
|
|
42
|
+
<string>_CC1AD845._googlecast._tcp</string>
|
|
43
|
+
</array>
|
|
44
|
+
<key>NSLocalNetworkUsageDescription</key>
|
|
45
|
+
<string>$(PRODUCT_NAME) uses the local network to discover Cast-enabled devices on your WiFi network.</string>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
await VideoPlayer.initPlayer({
|
|
50
|
+
playerId: 'fullscreen',
|
|
51
|
+
mode: 'fullscreen',
|
|
52
|
+
url: 'https://example.com/video.mp4',
|
|
53
|
+
title: 'Video title',
|
|
54
|
+
smallTitle: 'Video subtitle',
|
|
55
|
+
artwork: 'https://example.com/poster.jpg',
|
|
56
|
+
chromecast: true,
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Supported Features
|
|
61
|
+
|
|
62
|
+
| Feature | iOS | Android | Web |
|
|
63
|
+
| ------------------------------ | --- | ------- | --- |
|
|
64
|
+
| Fullscreen native video player | ✅ | ✅ | ❌ |
|
|
65
|
+
| Embedded web video player | ❌ | ❌ | ✅ |
|
|
66
|
+
| Picture in Picture | ✅ | ✅ | ❌ |
|
|
67
|
+
| Subtitles | ✅ | ✅ | ✅ |
|
|
68
|
+
| DRM-protected playback | ✅ | ✅ | ❌ |
|
|
69
|
+
| Chromecast sender playback | ❌ | ✅ | ❌ |
|
|
70
|
+
|
|
71
|
+
## Chromecast (Android)
|
|
72
|
+
|
|
73
|
+
Chromecast is supported on Android in fullscreen mode. Enable it with the `chromecast` option. When a Cast session starts, the plugin loads the same media URL on the receiver, keeps the current playback position, and routes play, pause, seek, and timeline controls through the Cast session.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
await VideoPlayer.initPlayer({
|
|
77
|
+
mode: 'fullscreen',
|
|
78
|
+
playerId: 'main-player',
|
|
79
|
+
url: 'https://example.com/video.m3u8',
|
|
80
|
+
title: 'Video title',
|
|
81
|
+
smallTitle: 'Optional subtitle',
|
|
82
|
+
artwork: 'https://example.com/artwork.jpg',
|
|
83
|
+
chromecast: true,
|
|
84
|
+
showControls: true,
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Chromecast Options
|
|
89
|
+
|
|
90
|
+
| Option | Type | Default | Description |
|
|
91
|
+
| ------------ | --------- | ------- | ---------------------------------------------------------- |
|
|
92
|
+
| `chromecast` | `boolean` | `true` | Shows the Android Cast button and enables sender playback. |
|
|
93
|
+
| `title` | `string` | `""` | Title shown in the sender controls and receiver metadata. |
|
|
94
|
+
| `smallTitle` | `string` | `""` | Subtitle shown in the sender controls and metadata. |
|
|
95
|
+
| `artwork` | `string` | `""` | Image URL used for Cast metadata and sender Cast artwork. |
|
|
96
|
+
|
|
97
|
+
### Requirements and Notes
|
|
98
|
+
|
|
99
|
+
- The media URL must be reachable by the Chromecast device, not only by the Android app.
|
|
100
|
+
- Cast support depends on the receiver app and device media support. MP4, HLS, DASH, and SmoothStreaming streams are mapped to Cast-compatible MIME types by the plugin.
|
|
101
|
+
- Widevine DRM metadata is forwarded to the Cast media item. DRM-protected streams may still require a receiver that supports your license server and DRM flow.
|
|
102
|
+
- Request headers used by the Android local player are not automatically available to the Chromecast receiver. Use public URLs, signed URLs, cookies supported by your receiver, or a custom receiver for secured media.
|
|
103
|
+
|
|
32
104
|
## API
|
|
33
105
|
|
|
34
106
|
<docgen-index>
|
|
@@ -345,31 +417,31 @@ Exit player
|
|
|
345
417
|
|
|
346
418
|
#### capVideoPlayerOptions
|
|
347
419
|
|
|
348
|
-
| Prop | Type | Description
|
|
349
|
-
| --------------------- | ----------------------------------------------------------- |
|
|
350
|
-
| **`mode`** | <code>string</code> | Player mode - "fullscreen" - "embedded" (Web only)
|
|
351
|
-
| **`url`** | <code>string</code> | The url of the video to play
|
|
352
|
-
| **`subtitle`** | <code>string</code> | The url of subtitle associated with the video
|
|
353
|
-
| **`language`** | <code>string</code> | The language of subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers
|
|
354
|
-
| **`subtitleOptions`** | <code><a href="#subtitleoptions">SubTitleOptions</a></code> | SubTitle Options
|
|
355
|
-
| **`playerId`** | <code>string</code> | Id of DIV Element parent of the player
|
|
356
|
-
| **`rate`** | <code>number</code> | Initial playing rate
|
|
357
|
-
| **`exitOnEnd`** | <code>boolean</code> | Exit on VideoEnd (iOS, Android) default: true
|
|
358
|
-
| **`loopOnEnd`** | <code>boolean</code> | Loop on VideoEnd when exitOnEnd false (iOS, Android) default: false
|
|
359
|
-
| **`pipEnabled`** | <code>boolean</code> | Picture in Picture Enable (iOS, Android) default: true
|
|
360
|
-
| **`bkmodeEnabled`** | <code>boolean</code> | Background Mode Enable (iOS, Android) default: true
|
|
361
|
-
| **`showControls`** | <code>boolean</code> | Show Controls Enable (iOS, Android) default: true
|
|
362
|
-
| **`displayMode`** | <code>string</code> | Display Mode ["all", "portrait", "landscape"] (iOS, Android) default: "all"
|
|
363
|
-
| **`componentTag`** | <code>string</code> | Component Tag or DOM Element Tag (React app)
|
|
364
|
-
| **`width`** | <code>number</code> | Player Width (mode "embedded" only)
|
|
365
|
-
| **`height`** | <code>number</code> | Player height (mode "embedded" only)
|
|
366
|
-
| **`headers`** | <code>{ [key: string]: string; }</code> | Headers for the request (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX)
|
|
367
|
-
| **`title`** | <code>string</code> | Title shown in the player (Android) by Manuel García Marín (https://github.com/PhantomPainX)
|
|
368
|
-
| **`smallTitle`** | <code>string</code> | Subtitle shown below the title in the player (Android) by Manuel García Marín (https://github.com/PhantomPainX)
|
|
369
|
-
| **`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
|
|
370
|
-
| **`chromecast`** | <code>boolean</code> | Chromecast enable/disable (Android) by Manuel García Marín (https://github.com/PhantomPainX) default: true
|
|
371
|
-
| **`artwork`** | <code>string</code> | Artwork url to be shown in Chromecast player by Manuel García Marín (https://github.com/PhantomPainX) default: ""
|
|
372
|
-
| **`drm`** | <code><a href="#drmoptions">DrmOptions</a></code> | DRM configuration for protected content (iOS: FairPlay, Android: Widevine)
|
|
420
|
+
| Prop | Type | Description |
|
|
421
|
+
| --------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
422
|
+
| **`mode`** | <code>string</code> | Player mode - "fullscreen" - "embedded" (Web only) |
|
|
423
|
+
| **`url`** | <code>string</code> | The url of the video to play |
|
|
424
|
+
| **`subtitle`** | <code>string</code> | The url of subtitle associated with the video |
|
|
425
|
+
| **`language`** | <code>string</code> | The language of subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers |
|
|
426
|
+
| **`subtitleOptions`** | <code><a href="#subtitleoptions">SubTitleOptions</a></code> | SubTitle Options |
|
|
427
|
+
| **`playerId`** | <code>string</code> | Id of DIV Element parent of the player |
|
|
428
|
+
| **`rate`** | <code>number</code> | Initial playing rate |
|
|
429
|
+
| **`exitOnEnd`** | <code>boolean</code> | Exit on VideoEnd (iOS, Android) default: true |
|
|
430
|
+
| **`loopOnEnd`** | <code>boolean</code> | Loop on VideoEnd when exitOnEnd false (iOS, Android) default: false |
|
|
431
|
+
| **`pipEnabled`** | <code>boolean</code> | Picture in Picture Enable (iOS, Android) default: true |
|
|
432
|
+
| **`bkmodeEnabled`** | <code>boolean</code> | Background Mode Enable (iOS, Android) default: true |
|
|
433
|
+
| **`showControls`** | <code>boolean</code> | Show Controls Enable (iOS, Android) default: true |
|
|
434
|
+
| **`displayMode`** | <code>string</code> | Display Mode ["all", "portrait", "landscape"] (iOS, Android) default: "all" |
|
|
435
|
+
| **`componentTag`** | <code>string</code> | Component Tag or DOM Element Tag (React app) |
|
|
436
|
+
| **`width`** | <code>number</code> | Player Width (mode "embedded" only) |
|
|
437
|
+
| **`height`** | <code>number</code> | Player height (mode "embedded" only) |
|
|
438
|
+
| **`headers`** | <code>{ [key: string]: string; }</code> | Headers for the request (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
|
|
439
|
+
| **`title`** | <code>string</code> | Title shown in the player and Chromecast metadata (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX) |
|
|
440
|
+
| **`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) |
|
|
441
|
+
| **`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 |
|
|
442
|
+
| **`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 |
|
|
443
|
+
| **`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: "" |
|
|
444
|
+
| **`drm`** | <code><a href="#drmoptions">DrmOptions</a></code> | DRM configuration for protected content (iOS: FairPlay, Android: Widevine) |
|
|
373
445
|
|
|
374
446
|
|
|
375
447
|
#### SubTitleOptions
|
|
@@ -56,6 +56,8 @@ import com.google.android.exoplayer2.PlaybackParameters;
|
|
|
56
56
|
import com.google.android.exoplayer2.Player;
|
|
57
57
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
|
58
58
|
import com.google.android.exoplayer2.ext.cast.CastPlayer;
|
|
59
|
+
import com.google.android.exoplayer2.ext.cast.DefaultMediaItemConverter;
|
|
60
|
+
import com.google.android.exoplayer2.ext.cast.MediaItemConverter;
|
|
59
61
|
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener;
|
|
60
62
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
|
61
63
|
import com.google.android.exoplayer2.source.MediaSource;
|
|
@@ -79,16 +81,21 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
|
|
79
81
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
|
80
82
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
|
81
83
|
import com.google.android.exoplayer2.util.MimeTypes;
|
|
84
|
+
import com.google.android.gms.cast.MediaInfo;
|
|
85
|
+
import com.google.android.gms.cast.MediaQueueItem;
|
|
86
|
+
import com.google.android.gms.cast.MediaTrack;
|
|
82
87
|
import com.google.android.gms.cast.framework.CastButtonFactory;
|
|
83
88
|
import com.google.android.gms.cast.framework.CastContext;
|
|
84
89
|
import com.google.android.gms.cast.framework.CastState;
|
|
85
90
|
import com.google.android.gms.cast.framework.CastStateListener;
|
|
91
|
+
import com.google.android.gms.common.images.WebImage;
|
|
86
92
|
import com.google.android.gms.tasks.OnCompleteListener;
|
|
87
93
|
import com.google.android.gms.tasks.Task;
|
|
88
94
|
import java.io.IOException;
|
|
89
95
|
import java.io.InputStream;
|
|
90
96
|
import java.net.HttpURLConnection;
|
|
91
97
|
import java.net.URL;
|
|
98
|
+
import java.util.ArrayList;
|
|
92
99
|
import java.util.Arrays;
|
|
93
100
|
import java.util.HashMap;
|
|
94
101
|
import java.util.List;
|
|
@@ -97,6 +104,7 @@ import java.util.Map;
|
|
|
97
104
|
import java.util.concurrent.Executor;
|
|
98
105
|
import java.util.concurrent.Executors;
|
|
99
106
|
import org.json.JSONException;
|
|
107
|
+
import org.json.JSONObject;
|
|
100
108
|
|
|
101
109
|
public class FullscreenExoPlayerFragment extends Fragment {
|
|
102
110
|
|
|
@@ -188,7 +196,6 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
188
196
|
private MediaRouteButton mediaRouteButton;
|
|
189
197
|
private CastContext castContext;
|
|
190
198
|
private CastPlayer castPlayer;
|
|
191
|
-
private MediaItem mediaItem;
|
|
192
199
|
private MediaRouter mRouter;
|
|
193
200
|
private MediaRouter.Callback mCallback = new EmptyCallback();
|
|
194
201
|
private MediaRouteSelector mSelector;
|
|
@@ -996,18 +1003,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
996
1003
|
mediaSources[0] = mediaSource;
|
|
997
1004
|
String mimeType = getMimeType(sturi);
|
|
998
1005
|
|
|
999
|
-
|
|
1000
|
-
String languageLabel = Locale.forLanguageTag(language).getDisplayLanguage();
|
|
1001
|
-
MediaItem.SubtitleConfiguration subConfig = new MediaItem.SubtitleConfiguration.Builder(sturi)
|
|
1002
|
-
.setMimeType(mimeType)
|
|
1003
|
-
.setUri(sturi)
|
|
1004
|
-
.setId(subTitle)
|
|
1005
|
-
.setLabel(languageLabel)
|
|
1006
|
-
.setRoleFlags(C.ROLE_FLAG_CAPTION)
|
|
1007
|
-
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
|
1008
|
-
.setLanguage(language)
|
|
1009
|
-
.build();
|
|
1010
|
-
|
|
1006
|
+
MediaItem.SubtitleConfiguration subConfig = buildSubtitleConfiguration(sturi, mimeType);
|
|
1011
1007
|
SingleSampleMediaSource subtitleSource = new SingleSampleMediaSource.Factory(dataSourceFactory).createMediaSource(
|
|
1012
1008
|
subConfig,
|
|
1013
1009
|
C.TIME_UNSET
|
|
@@ -1331,7 +1327,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1331
1327
|
public void onComplete(Task<CastContext> task) {
|
|
1332
1328
|
if (task.isSuccessful()) {
|
|
1333
1329
|
castContext = task.getResult();
|
|
1334
|
-
castPlayer = new CastPlayer(castContext);
|
|
1330
|
+
castPlayer = new CastPlayer(castContext, new SubtitleMediaItemConverter());
|
|
1335
1331
|
mRouter = MediaRouter.getInstance(context);
|
|
1336
1332
|
mSelector = new MediaRouteSelector.Builder()
|
|
1337
1333
|
.addControlCategories(
|
|
@@ -1355,38 +1351,37 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1355
1351
|
};
|
|
1356
1352
|
CastButtonFactory.setUpMediaRouteButton(context, mediaRouteButton);
|
|
1357
1353
|
|
|
1358
|
-
|
|
1359
|
-
if (artwork != "") {
|
|
1360
|
-
movieMetadata = new MediaMetadata.Builder()
|
|
1361
|
-
.setTitle(title)
|
|
1362
|
-
.setSubtitle(smallTitle)
|
|
1363
|
-
.setMediaType(MediaMetadata.MEDIA_TYPE_MOVIE)
|
|
1364
|
-
.setArtworkUri(Uri.parse(artwork))
|
|
1365
|
-
.build();
|
|
1354
|
+
if (hasArtwork()) {
|
|
1366
1355
|
new setCastImage().execute();
|
|
1367
|
-
} else {
|
|
1368
|
-
movieMetadata = new MediaMetadata.Builder().setTitle(title).setSubtitle(smallTitle).build();
|
|
1369
1356
|
}
|
|
1370
|
-
mediaItem = new MediaItem.Builder()
|
|
1371
|
-
.setUri(videoPath)
|
|
1372
|
-
.setMimeType(MimeTypes.VIDEO_UNKNOWN)
|
|
1373
|
-
.setMediaMetadata(movieMetadata)
|
|
1374
|
-
.build();
|
|
1375
1357
|
|
|
1376
1358
|
castPlayer.setSessionAvailabilityListener(
|
|
1377
1359
|
new SessionAvailabilityListener() {
|
|
1378
1360
|
@Override
|
|
1379
1361
|
public void onCastSessionAvailable() {
|
|
1362
|
+
Uri castUri = getCastUri();
|
|
1363
|
+
if (!isNetworkUri(castUri)) {
|
|
1364
|
+
Toast.makeText(
|
|
1365
|
+
context,
|
|
1366
|
+
"Chromecast requires a network-reachable media URL",
|
|
1367
|
+
Toast.LENGTH_SHORT
|
|
1368
|
+
).show();
|
|
1369
|
+
castContext.getSessionManager().endCurrentSession(false);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1380
1372
|
isCastSession = true;
|
|
1381
1373
|
final Long videoPosition = player.getCurrentPosition();
|
|
1374
|
+
final boolean shouldPlay = player.getPlayWhenReady();
|
|
1382
1375
|
if (pipEnabled) {
|
|
1383
1376
|
pipBtn.setVisibility(View.GONE);
|
|
1384
1377
|
}
|
|
1385
1378
|
resizeBtn.setVisibility(View.GONE);
|
|
1386
1379
|
player.setPlayWhenReady(false);
|
|
1387
1380
|
cast_image.setVisibility(View.VISIBLE);
|
|
1388
|
-
castPlayer.setMediaItem(mediaItem, videoPosition);
|
|
1389
1381
|
styledPlayerView.setPlayer(castPlayer);
|
|
1382
|
+
castPlayer.setMediaItem(buildCastMediaItem(castUri), videoPosition);
|
|
1383
|
+
castPlayer.setPlayWhenReady(shouldPlay);
|
|
1384
|
+
castPlayer.prepare();
|
|
1390
1385
|
styledPlayerView.setControllerShowTimeoutMs(0);
|
|
1391
1386
|
styledPlayerView.setControllerHideOnTouch(false);
|
|
1392
1387
|
//We perform a click because for some weird reason, the layout is black until the user clicks on it
|
|
@@ -1395,16 +1390,20 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1395
1390
|
|
|
1396
1391
|
@Override
|
|
1397
1392
|
public void onCastSessionUnavailable() {
|
|
1393
|
+
if (!isCastSession) return;
|
|
1398
1394
|
isCastSession = false;
|
|
1399
1395
|
final Long videoPosition = castPlayer.getCurrentPosition();
|
|
1396
|
+
final boolean shouldPlay = castPlayer.getPlayWhenReady();
|
|
1400
1397
|
if (pipEnabled) {
|
|
1401
1398
|
pipBtn.setVisibility(View.VISIBLE);
|
|
1402
1399
|
}
|
|
1403
1400
|
resizeBtn.setVisibility(View.VISIBLE);
|
|
1404
1401
|
cast_image.setVisibility(View.GONE);
|
|
1402
|
+
castPlayer.stop();
|
|
1403
|
+
castPlayer.clearMediaItems();
|
|
1405
1404
|
styledPlayerView.setPlayer(player);
|
|
1406
|
-
player.setPlayWhenReady(true);
|
|
1407
1405
|
player.seekTo(videoPosition);
|
|
1406
|
+
player.setPlayWhenReady(shouldPlay);
|
|
1408
1407
|
styledPlayerView.setControllerShowTimeoutMs(3000);
|
|
1409
1408
|
styledPlayerView.setControllerHideOnTouch(true);
|
|
1410
1409
|
}
|
|
@@ -1418,7 +1417,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1418
1417
|
Map<String, Object> info = new HashMap<String, Object>() {
|
|
1419
1418
|
{
|
|
1420
1419
|
put("fromPlayerId", playerId);
|
|
1421
|
-
put("currentTime", String.valueOf(
|
|
1420
|
+
put("currentTime", String.valueOf(castPlayer.getCurrentPosition() / 1000));
|
|
1422
1421
|
}
|
|
1423
1422
|
};
|
|
1424
1423
|
switch (state) {
|
|
@@ -1449,6 +1448,261 @@ public class FullscreenExoPlayerFragment extends Fragment {
|
|
|
1449
1448
|
|
|
1450
1449
|
private final class EmptyCallback extends MediaRouter.Callback {}
|
|
1451
1450
|
|
|
1451
|
+
private Uri getCastUri() {
|
|
1452
|
+
return uri != null ? uri : Uri.parse(videoPath);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
private boolean isNetworkUri(Uri mediaUri) {
|
|
1456
|
+
String scheme = mediaUri.getScheme();
|
|
1457
|
+
return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
private MediaItem buildCastMediaItem(Uri castUri) {
|
|
1461
|
+
MediaItem.Builder builder = buildMediaItem(castUri).buildUpon().setMediaMetadata(buildCastMediaMetadata());
|
|
1462
|
+
String mimeType = getMediaItemMimeType(castUri);
|
|
1463
|
+
if (mimeType != null) {
|
|
1464
|
+
builder.setMimeType(mimeType);
|
|
1465
|
+
}
|
|
1466
|
+
if (sturi != null && isNetworkUri(sturi)) {
|
|
1467
|
+
builder.setSubtitleConfigurations(Arrays.asList(buildSubtitleConfiguration(sturi, getMimeType(sturi))));
|
|
1468
|
+
}
|
|
1469
|
+
return builder.build();
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private MediaMetadata buildCastMediaMetadata() {
|
|
1473
|
+
MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder().setTitle(title).setSubtitle(smallTitle);
|
|
1474
|
+
if (hasArtwork()) {
|
|
1475
|
+
metadataBuilder.setMediaType(MediaMetadata.MEDIA_TYPE_MOVIE).setArtworkUri(Uri.parse(artwork));
|
|
1476
|
+
}
|
|
1477
|
+
return metadataBuilder.build();
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private boolean hasArtwork() {
|
|
1481
|
+
return artwork != null && !artwork.isEmpty();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private MediaItem.SubtitleConfiguration buildSubtitleConfiguration(Uri subtitleUri, String mimeType) {
|
|
1485
|
+
String languageLabel = Locale.forLanguageTag(language).getDisplayLanguage();
|
|
1486
|
+
return new MediaItem.SubtitleConfiguration.Builder(subtitleUri)
|
|
1487
|
+
.setMimeType(mimeType)
|
|
1488
|
+
.setUri(subtitleUri)
|
|
1489
|
+
.setId(subTitle)
|
|
1490
|
+
.setLabel(languageLabel)
|
|
1491
|
+
.setRoleFlags(C.ROLE_FLAG_CAPTION)
|
|
1492
|
+
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
|
1493
|
+
.setLanguage(language)
|
|
1494
|
+
.build();
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
private String getMediaItemMimeType(Uri mediaUri) {
|
|
1498
|
+
String mediaType = vType != null ? vType : getVideoType(mediaUri);
|
|
1499
|
+
switch (mediaType) {
|
|
1500
|
+
case "dash":
|
|
1501
|
+
case "mpd":
|
|
1502
|
+
return MimeTypes.APPLICATION_MPD;
|
|
1503
|
+
case "m3u8":
|
|
1504
|
+
return MimeTypes.APPLICATION_M3U8;
|
|
1505
|
+
case "ism":
|
|
1506
|
+
return MimeTypes.APPLICATION_SS;
|
|
1507
|
+
case "webm":
|
|
1508
|
+
return MimeTypes.VIDEO_WEBM;
|
|
1509
|
+
case "ogv":
|
|
1510
|
+
return "video/ogg";
|
|
1511
|
+
case "3gp":
|
|
1512
|
+
return "video/3gpp";
|
|
1513
|
+
case "flv":
|
|
1514
|
+
return "video/x-flv";
|
|
1515
|
+
case "mp4":
|
|
1516
|
+
return MimeTypes.VIDEO_MP4;
|
|
1517
|
+
case "ytube":
|
|
1518
|
+
case "":
|
|
1519
|
+
default:
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private final class SubtitleMediaItemConverter implements MediaItemConverter {
|
|
1525
|
+
|
|
1526
|
+
private static final String KEY_MEDIA_ITEM = "mediaItem";
|
|
1527
|
+
private static final String KEY_PLAYER_CONFIG = "exoPlayerConfig";
|
|
1528
|
+
private static final String KEY_MEDIA_ID = "mediaId";
|
|
1529
|
+
private static final String KEY_URI = "uri";
|
|
1530
|
+
private static final String KEY_TITLE = "title";
|
|
1531
|
+
private static final String KEY_MIME_TYPE = "mimeType";
|
|
1532
|
+
private static final String KEY_DRM_CONFIGURATION = "drmConfiguration";
|
|
1533
|
+
private static final String KEY_UUID = "uuid";
|
|
1534
|
+
private static final String KEY_LICENSE_URI = "licenseUri";
|
|
1535
|
+
private static final String KEY_REQUEST_HEADERS = "requestHeaders";
|
|
1536
|
+
|
|
1537
|
+
private final DefaultMediaItemConverter defaultConverter = new DefaultMediaItemConverter();
|
|
1538
|
+
|
|
1539
|
+
@Override
|
|
1540
|
+
public MediaQueueItem toMediaQueueItem(MediaItem mediaItem) {
|
|
1541
|
+
MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
|
|
1542
|
+
if (localConfiguration == null) {
|
|
1543
|
+
throw new IllegalArgumentException("The item must specify its local configuration");
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
MediaInfo.Builder mediaInfoBuilder = new MediaInfo.Builder(getContentId(mediaItem, localConfiguration))
|
|
1547
|
+
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
|
1548
|
+
.setContentUrl(localConfiguration.uri.toString())
|
|
1549
|
+
.setMetadata(buildCastMediaInfoMetadata(mediaItem))
|
|
1550
|
+
.setCustomData(buildCastCustomData(mediaItem));
|
|
1551
|
+
|
|
1552
|
+
if (localConfiguration.mimeType != null) {
|
|
1553
|
+
mediaInfoBuilder.setContentType(localConfiguration.mimeType);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
ArrayList<Long> activeTrackIds = new ArrayList<>();
|
|
1557
|
+
List<MediaTrack> mediaTracks = buildCastSubtitleTracks(localConfiguration.subtitleConfigurations, activeTrackIds);
|
|
1558
|
+
if (!mediaTracks.isEmpty()) {
|
|
1559
|
+
mediaInfoBuilder.setMediaTracks(mediaTracks);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
MediaQueueItem.Builder mediaQueueItemBuilder = new MediaQueueItem.Builder(mediaInfoBuilder.build());
|
|
1563
|
+
if (!activeTrackIds.isEmpty()) {
|
|
1564
|
+
mediaQueueItemBuilder.setActiveTrackIds(toLongArray(activeTrackIds));
|
|
1565
|
+
}
|
|
1566
|
+
return mediaQueueItemBuilder.build();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
@Override
|
|
1570
|
+
public MediaItem toMediaItem(MediaQueueItem mediaQueueItem) {
|
|
1571
|
+
return defaultConverter.toMediaItem(mediaQueueItem);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
private String getContentId(MediaItem mediaItem, MediaItem.LocalConfiguration localConfiguration) {
|
|
1575
|
+
return mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? localConfiguration.uri.toString() : mediaItem.mediaId;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
private com.google.android.gms.cast.MediaMetadata buildCastMediaInfoMetadata(MediaItem mediaItem) {
|
|
1579
|
+
MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
|
|
1580
|
+
String mimeType = localConfiguration != null ? localConfiguration.mimeType : null;
|
|
1581
|
+
com.google.android.gms.cast.MediaMetadata metadata = new com.google.android.gms.cast.MediaMetadata(
|
|
1582
|
+
mimeType != null && MimeTypes.isAudio(mimeType)
|
|
1583
|
+
? com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_MUSIC_TRACK
|
|
1584
|
+
: com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_MOVIE
|
|
1585
|
+
);
|
|
1586
|
+
|
|
1587
|
+
if (mediaItem.mediaMetadata.title != null) {
|
|
1588
|
+
metadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, mediaItem.mediaMetadata.title.toString());
|
|
1589
|
+
}
|
|
1590
|
+
if (mediaItem.mediaMetadata.subtitle != null) {
|
|
1591
|
+
metadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_SUBTITLE, mediaItem.mediaMetadata.subtitle.toString());
|
|
1592
|
+
}
|
|
1593
|
+
if (mediaItem.mediaMetadata.artworkUri != null) {
|
|
1594
|
+
metadata.addImage(new WebImage(mediaItem.mediaMetadata.artworkUri));
|
|
1595
|
+
}
|
|
1596
|
+
return metadata;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
private List<MediaTrack> buildCastSubtitleTracks(
|
|
1600
|
+
List<MediaItem.SubtitleConfiguration> subtitleConfigurations,
|
|
1601
|
+
ArrayList<Long> activeTrackIds
|
|
1602
|
+
) {
|
|
1603
|
+
ArrayList<MediaTrack> mediaTracks = new ArrayList<>();
|
|
1604
|
+
for (int i = 0; i < subtitleConfigurations.size(); i++) {
|
|
1605
|
+
MediaItem.SubtitleConfiguration subtitleConfiguration = subtitleConfigurations.get(i);
|
|
1606
|
+
if (subtitleConfiguration.uri == null) {
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
long trackId = i + 1L;
|
|
1610
|
+
MediaTrack.Builder trackBuilder = new MediaTrack.Builder(trackId, MediaTrack.TYPE_TEXT)
|
|
1611
|
+
.setContentId(subtitleConfiguration.uri.toString())
|
|
1612
|
+
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES);
|
|
1613
|
+
if (subtitleConfiguration.mimeType != null) {
|
|
1614
|
+
trackBuilder.setContentType(subtitleConfiguration.mimeType);
|
|
1615
|
+
}
|
|
1616
|
+
if (subtitleConfiguration.label != null) {
|
|
1617
|
+
trackBuilder.setName(subtitleConfiguration.label.toString());
|
|
1618
|
+
}
|
|
1619
|
+
if (subtitleConfiguration.language != null) {
|
|
1620
|
+
trackBuilder.setLanguage(subtitleConfiguration.language);
|
|
1621
|
+
}
|
|
1622
|
+
mediaTracks.add(trackBuilder.build());
|
|
1623
|
+
if ((subtitleConfiguration.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0) {
|
|
1624
|
+
activeTrackIds.add(trackId);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return mediaTracks;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
private long[] toLongArray(List<Long> values) {
|
|
1631
|
+
long[] result = new long[values.size()];
|
|
1632
|
+
for (int i = 0; i < values.size(); i++) {
|
|
1633
|
+
result[i] = values.get(i);
|
|
1634
|
+
}
|
|
1635
|
+
return result;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
private JSONObject buildCastCustomData(MediaItem mediaItem) {
|
|
1639
|
+
try {
|
|
1640
|
+
JSONObject customData = new JSONObject();
|
|
1641
|
+
customData.put(KEY_MEDIA_ITEM, buildMediaItemJson(mediaItem));
|
|
1642
|
+
JSONObject playerConfig = buildPlayerConfigJson(mediaItem);
|
|
1643
|
+
if (playerConfig != null) {
|
|
1644
|
+
customData.put(KEY_PLAYER_CONFIG, playerConfig);
|
|
1645
|
+
}
|
|
1646
|
+
return customData;
|
|
1647
|
+
} catch (JSONException e) {
|
|
1648
|
+
throw new RuntimeException(e);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
private JSONObject buildMediaItemJson(MediaItem mediaItem) throws JSONException {
|
|
1653
|
+
MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
|
|
1654
|
+
JSONObject json = new JSONObject();
|
|
1655
|
+
json.put(KEY_MEDIA_ID, mediaItem.mediaId);
|
|
1656
|
+
if (mediaItem.mediaMetadata.title != null) {
|
|
1657
|
+
json.put(KEY_TITLE, mediaItem.mediaMetadata.title.toString());
|
|
1658
|
+
}
|
|
1659
|
+
json.put(KEY_URI, localConfiguration.uri.toString());
|
|
1660
|
+
if (localConfiguration.mimeType != null) {
|
|
1661
|
+
json.put(KEY_MIME_TYPE, localConfiguration.mimeType);
|
|
1662
|
+
}
|
|
1663
|
+
if (localConfiguration.drmConfiguration != null) {
|
|
1664
|
+
json.put(KEY_DRM_CONFIGURATION, buildDrmConfigurationJson(localConfiguration.drmConfiguration));
|
|
1665
|
+
}
|
|
1666
|
+
return json;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
private JSONObject buildDrmConfigurationJson(MediaItem.DrmConfiguration drmConfiguration) throws JSONException {
|
|
1670
|
+
JSONObject json = new JSONObject();
|
|
1671
|
+
json.put(KEY_UUID, drmConfiguration.scheme.toString());
|
|
1672
|
+
json.put(KEY_LICENSE_URI, drmConfiguration.licenseUri.toString());
|
|
1673
|
+
json.put(KEY_REQUEST_HEADERS, new JSONObject(drmConfiguration.licenseRequestHeaders));
|
|
1674
|
+
return json;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
private JSONObject buildPlayerConfigJson(MediaItem mediaItem) throws JSONException {
|
|
1678
|
+
MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration;
|
|
1679
|
+
if (localConfiguration == null || localConfiguration.drmConfiguration == null) {
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
MediaItem.DrmConfiguration drmConfiguration = localConfiguration.drmConfiguration;
|
|
1684
|
+
String protectionSystem;
|
|
1685
|
+
if (C.WIDEVINE_UUID.equals(drmConfiguration.scheme)) {
|
|
1686
|
+
protectionSystem = "widevine";
|
|
1687
|
+
} else if (C.PLAYREADY_UUID.equals(drmConfiguration.scheme)) {
|
|
1688
|
+
protectionSystem = "playready";
|
|
1689
|
+
} else {
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
JSONObject json = new JSONObject();
|
|
1694
|
+
json.put("withCredentials", false);
|
|
1695
|
+
json.put("protectionSystem", protectionSystem);
|
|
1696
|
+
if (drmConfiguration.licenseUri != null) {
|
|
1697
|
+
json.put("licenseUrl", drmConfiguration.licenseUri.toString());
|
|
1698
|
+
}
|
|
1699
|
+
if (!drmConfiguration.licenseRequestHeaders.isEmpty()) {
|
|
1700
|
+
json.put("headers", new JSONObject(drmConfiguration.licenseRequestHeaders));
|
|
1701
|
+
}
|
|
1702
|
+
return json;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1452
1706
|
@Override
|
|
1453
1707
|
public void onConfigurationChanged(Configuration newConfig) {
|
|
1454
1708
|
super.onConfigurationChanged(newConfig);
|
|
@@ -38,7 +38,7 @@ import java.util.Map;
|
|
|
38
38
|
)
|
|
39
39
|
public class VideoPlayerPlugin extends Plugin {
|
|
40
40
|
|
|
41
|
-
private final String pluginVersion = "8.1.
|
|
41
|
+
private final String pluginVersion = "8.1.9";
|
|
42
42
|
|
|
43
43
|
// Permission alias constants
|
|
44
44
|
private static final String PERMISSION_DENIED_ERROR = "Unable to access media videos, user denied permission request";
|
package/dist/docs.json
CHANGED
|
@@ -472,14 +472,14 @@
|
|
|
472
472
|
{
|
|
473
473
|
"name": "title",
|
|
474
474
|
"tags": [],
|
|
475
|
-
"docs": "Title shown in the player (Android)\nby Manuel García Marín (https://github.com/PhantomPainX)",
|
|
475
|
+
"docs": "Title shown in the player and Chromecast metadata (iOS, Android)\nby Manuel García Marín (https://github.com/PhantomPainX)",
|
|
476
476
|
"complexTypes": [],
|
|
477
477
|
"type": "string | undefined"
|
|
478
478
|
},
|
|
479
479
|
{
|
|
480
480
|
"name": "smallTitle",
|
|
481
481
|
"tags": [],
|
|
482
|
-
"docs": "Subtitle shown below the title in the player (Android)\nby Manuel García Marín (https://github.com/PhantomPainX)",
|
|
482
|
+
"docs": "Subtitle shown below the title in the player and Chromecast metadata (iOS, Android)\nby Manuel García Marín (https://github.com/PhantomPainX)",
|
|
483
483
|
"complexTypes": [],
|
|
484
484
|
"type": "string | undefined"
|
|
485
485
|
},
|
|
@@ -493,14 +493,14 @@
|
|
|
493
493
|
{
|
|
494
494
|
"name": "chromecast",
|
|
495
495
|
"tags": [],
|
|
496
|
-
"docs": "Chromecast enable/disable (Android)\nby Manuel García Marín (https://github.com/PhantomPainX)\ndefault: true",
|
|
496
|
+
"docs": "Chromecast enable/disable (iOS, Android)\niOS requires Google Cast SDK setup and local network Info.plist keys.\nby Manuel García Marín (https://github.com/PhantomPainX)\ndefault: true",
|
|
497
497
|
"complexTypes": [],
|
|
498
498
|
"type": "boolean | undefined"
|
|
499
499
|
},
|
|
500
500
|
{
|
|
501
501
|
"name": "artwork",
|
|
502
502
|
"tags": [],
|
|
503
|
-
"docs": "Artwork url to be shown in Chromecast player\nby Manuel García Marín (https://github.com/PhantomPainX)\ndefault: \"\"",
|
|
503
|
+
"docs": "Artwork url to be shown in Chromecast player (iOS, Android)\nby Manuel García Marín (https://github.com/PhantomPainX)\ndefault: \"\"",
|
|
504
504
|
"complexTypes": [],
|
|
505
505
|
"type": "string | undefined"
|
|
506
506
|
},
|