@capgo/capacitor-video-player 8.1.8 → 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 +55 -27
- 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,8 +25,36 @@ 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
|
+
```
|
|
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
|
+
});
|
|
30
58
|
```
|
|
31
59
|
|
|
32
60
|
## Supported Features
|
|
@@ -389,31 +417,31 @@ Exit player
|
|
|
389
417
|
|
|
390
418
|
#### capVideoPlayerOptions
|
|
391
419
|
|
|
392
|
-
| Prop | Type | Description
|
|
393
|
-
| --------------------- | ----------------------------------------------------------- |
|
|
394
|
-
| **`mode`** | <code>string</code> | Player mode - "fullscreen" - "embedded" (Web only)
|
|
395
|
-
| **`url`** | <code>string</code> | The url of the video to play
|
|
396
|
-
| **`subtitle`** | <code>string</code> | The url of subtitle associated with the video
|
|
397
|
-
| **`language`** | <code>string</code> | The language of subtitle see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers
|
|
398
|
-
| **`subtitleOptions`** | <code><a href="#subtitleoptions">SubTitleOptions</a></code> | SubTitle Options
|
|
399
|
-
| **`playerId`** | <code>string</code> | Id of DIV Element parent of the player
|
|
400
|
-
| **`rate`** | <code>number</code> | Initial playing rate
|
|
401
|
-
| **`exitOnEnd`** | <code>boolean</code> | Exit on VideoEnd (iOS, Android) default: true
|
|
402
|
-
| **`loopOnEnd`** | <code>boolean</code> | Loop on VideoEnd when exitOnEnd false (iOS, Android) default: false
|
|
403
|
-
| **`pipEnabled`** | <code>boolean</code> | Picture in Picture Enable (iOS, Android) default: true
|
|
404
|
-
| **`bkmodeEnabled`** | <code>boolean</code> | Background Mode Enable (iOS, Android) default: true
|
|
405
|
-
| **`showControls`** | <code>boolean</code> | Show Controls Enable (iOS, Android) default: true
|
|
406
|
-
| **`displayMode`** | <code>string</code> | Display Mode ["all", "portrait", "landscape"] (iOS, Android) default: "all"
|
|
407
|
-
| **`componentTag`** | <code>string</code> | Component Tag or DOM Element Tag (React app)
|
|
408
|
-
| **`width`** | <code>number</code> | Player Width (mode "embedded" only)
|
|
409
|
-
| **`height`** | <code>number</code> | Player height (mode "embedded" only)
|
|
410
|
-
| **`headers`** | <code>{ [key: string]: string; }</code> | Headers for the request (iOS, Android) by Manuel García Marín (https://github.com/PhantomPainX)
|
|
411
|
-
| **`title`** | <code>string</code> | Title shown in the player (Android) by Manuel García Marín (https://github.com/PhantomPainX)
|
|
412
|
-
| **`smallTitle`** | <code>string</code> | Subtitle shown below the title in the player (Android) by Manuel García Marín (https://github.com/PhantomPainX)
|
|
413
|
-
| **`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
|
|
414
|
-
| **`chromecast`** | <code>boolean</code> | Chromecast enable/disable (Android) by Manuel García Marín (https://github.com/PhantomPainX) default: true
|
|
415
|
-
| **`artwork`** | <code>string</code> | Artwork url to be shown in Chromecast player by Manuel García Marín (https://github.com/PhantomPainX) default: ""
|
|
416
|
-
| **`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) |
|
|
417
445
|
|
|
418
446
|
|
|
419
447
|
#### SubTitleOptions
|
|
@@ -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
|
},
|
|
@@ -173,12 +173,12 @@ export interface capVideoPlayerOptions {
|
|
|
173
173
|
[key: string]: string;
|
|
174
174
|
};
|
|
175
175
|
/**
|
|
176
|
-
* Title shown in the player (Android)
|
|
176
|
+
* Title shown in the player and Chromecast metadata (iOS, Android)
|
|
177
177
|
* by Manuel García Marín (https://github.com/PhantomPainX)
|
|
178
178
|
*/
|
|
179
179
|
title?: string;
|
|
180
180
|
/**
|
|
181
|
-
* Subtitle shown below the title in the player (Android)
|
|
181
|
+
* Subtitle shown below the title in the player and Chromecast metadata (iOS, Android)
|
|
182
182
|
* by Manuel García Marín (https://github.com/PhantomPainX)
|
|
183
183
|
*/
|
|
184
184
|
smallTitle?: string;
|
|
@@ -190,13 +190,14 @@ export interface capVideoPlayerOptions {
|
|
|
190
190
|
*/
|
|
191
191
|
accentColor?: string;
|
|
192
192
|
/**
|
|
193
|
-
* Chromecast enable/disable (Android)
|
|
193
|
+
* Chromecast enable/disable (iOS, Android)
|
|
194
|
+
* iOS requires Google Cast SDK setup and local network Info.plist keys.
|
|
194
195
|
* by Manuel García Marín (https://github.com/PhantomPainX)
|
|
195
196
|
* default: true
|
|
196
197
|
*/
|
|
197
198
|
chromecast?: boolean;
|
|
198
199
|
/**
|
|
199
|
-
* Artwork url to be shown in Chromecast player
|
|
200
|
+
* Artwork url to be shown in Chromecast player (iOS, Android)
|
|
200
201
|
* by Manuel García Marín (https://github.com/PhantomPainX)
|
|
201
202
|
* default: ""
|
|
202
203
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface VideoPlayerPlugin {\n /**\n * Initialize a video player\n *\n */\n initPlayer(options: capVideoPlayerOptions): Promise<capVideoPlayerResult>;\n /**\n * Return if a given playerId is playing\n *\n */\n isPlaying(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Play the current video from a given playerId\n *\n */\n play(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Pause the current video from a given playerId\n *\n */\n pause(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the duration of the current video from a given playerId\n *\n */\n getDuration(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the current time of the current video from a given playerId\n *\n */\n getCurrentTime(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the current time to seek the current video to from a given playerId\n *\n */\n setCurrentTime(options: capVideoTimeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the volume of the current video from a given playerId\n *\n */\n getVolume(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the volume of the current video to from a given playerId\n *\n */\n setVolume(options: capVideoVolumeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the muted of the current video from a given playerId\n *\n */\n getMuted(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the muted of the current video to from a given playerId\n *\n */\n setMuted(options: capVideoMutedOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the rate of the current video from a given playerId\n *\n */\n setRate(options: capVideoRateOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the rate of the current video from a given playerId\n *\n */\n getRate(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Stop all players playing\n *\n */\n stopAllPlayers(): Promise<capVideoPlayerResult>;\n /**\n * Show controller\n *\n */\n showController(): Promise<capVideoPlayerResult>;\n /**\n * isControllerIsFullyVisible\n *\n */\n isControllerIsFullyVisible(): Promise<capVideoPlayerResult>;\n /**\n * Exit player\n *\n */\n exitPlayer(): Promise<capVideoPlayerResult>;\n}\nexport interface capEchoOptions {\n /**\n * String to be echoed\n */\n\n value?: string;\n}\nexport interface capVideoPlayerOptions {\n /**\n * Player mode\n * - \"fullscreen\"\n * - \"embedded\" (Web only)\n */\n mode?: string;\n /**\n * The url of the video to play\n */\n url?: string;\n /**\n * The url of subtitle associated with the video\n */\n subtitle?: string;\n /**\n * The language of subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n */\n language?: string;\n /**\n * SubTitle Options\n */\n subtitleOptions?: SubTitleOptions;\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Initial playing rate\n */\n rate?: number;\n /**\n * Exit on VideoEnd (iOS, Android)\n * default: true\n */\n exitOnEnd?: boolean;\n /**\n * Loop on VideoEnd when exitOnEnd false (iOS, Android)\n * default: false\n */\n loopOnEnd?: boolean;\n /**\n * Picture in Picture Enable (iOS, Android)\n * default: true\n */\n pipEnabled?: boolean;\n /**\n * Background Mode Enable (iOS, Android)\n * default: true\n */\n bkmodeEnabled?: boolean;\n /**\n * Show Controls Enable (iOS, Android)\n * default: true\n */\n showControls?: boolean;\n /**\n * Display Mode [\"all\", \"portrait\", \"landscape\"] (iOS, Android)\n * default: \"all\"\n */\n displayMode?: string;\n /**\n * Component Tag or DOM Element Tag (React app)\n */\n componentTag?: string;\n /**\n * Player Width (mode \"embedded\" only)\n */\n width?: number;\n /**\n * Player height (mode \"embedded\" only)\n */\n height?: number;\n /**\n * Headers for the request (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n headers?: {\n [key: string]: string;\n };\n /**\n * Title shown in the player (Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n title?: string;\n /**\n * Subtitle shown below the title in the player (Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n smallTitle?: string;\n /**\n * ExoPlayer Progress Bar and Spinner color (Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * Must be a valid hex color code\n * default: #FFFFFF\n */\n accentColor?: string;\n /**\n * Chromecast enable/disable (Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: true\n */\n chromecast?: boolean;\n /**\n * Artwork url to be shown in Chromecast player\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: \"\"\n */\n artwork?: string;\n /**\n * DRM configuration for protected content (iOS: FairPlay, Android: Widevine)\n */\n drm?: DrmOptions;\n}\nexport interface capVideoPlayerIdOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n}\nexport interface capVideoRateOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Rate value\n */\n rate?: number;\n}\nexport interface capVideoVolumeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Volume value between [0 - 1]\n */\n volume?: number;\n}\nexport interface capVideoTimeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video time value you want to seek to\n */\n seektime?: number;\n}\nexport interface capVideoMutedOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Muted value true or false\n */\n muted?: boolean;\n}\nexport interface capVideoListener {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capExitListener {\n /**\n * Dismiss value true or false\n */\n dismiss?: boolean;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capVideoPlayerResult {\n /**\n * result set to true when successful else false\n */\n result?: boolean;\n /**\n * method name\n */\n method?: string;\n /**\n * value returned\n */\n value?: any;\n /**\n * message string\n */\n message?: string;\n}\nexport interface SubTitleOptions {\n /**\n * Foreground Color in RGBA (default rgba(255,255,255,1)\n */\n foregroundColor?: string;\n /**\n * Background Color in RGBA (default rgba(0,0,0,1)\n */\n backgroundColor?: string;\n /**\n * Font Size in pixels (default 16)\n */\n fontSize?: number;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\nexport interface FairPlayDrmOptions {\n /**\n * The URL to fetch the FairPlay certificate\n */\n certificateUrl?: string;\n /**\n * The URL to send the SPC and receive the CKC license (FairPlay license server URL)\n */\n contentKeySpcUrl?: string;\n}\nexport interface PlayreadyDrmOptions {\n /**\n * The URL to fetch the PlayReady license\n */\n certificateUrl?: string;\n}\nexport interface WidevineDrmOptions {\n /**\n * The URL to fetch the Widevine license\n */\n certificateUrl?: string;\n}\nexport interface DrmOptions {\n /**\n * FairPlay DRM configuration (iOS)\n */\n fairplay?: FairPlayDrmOptions;\n /**\n * PlayReady DRM configuration\n */\n playready?: PlayreadyDrmOptions;\n /**\n * Widevine DRM configuration (Android)\n */\n widevine?: WidevineDrmOptions;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["export interface VideoPlayerPlugin {\n /**\n * Initialize a video player\n *\n */\n initPlayer(options: capVideoPlayerOptions): Promise<capVideoPlayerResult>;\n /**\n * Return if a given playerId is playing\n *\n */\n isPlaying(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Play the current video from a given playerId\n *\n */\n play(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Pause the current video from a given playerId\n *\n */\n pause(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the duration of the current video from a given playerId\n *\n */\n getDuration(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the current time of the current video from a given playerId\n *\n */\n getCurrentTime(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the current time to seek the current video to from a given playerId\n *\n */\n setCurrentTime(options: capVideoTimeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the volume of the current video from a given playerId\n *\n */\n getVolume(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the volume of the current video to from a given playerId\n *\n */\n setVolume(options: capVideoVolumeOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the muted of the current video from a given playerId\n *\n */\n getMuted(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the muted of the current video to from a given playerId\n *\n */\n setMuted(options: capVideoMutedOptions): Promise<capVideoPlayerResult>;\n /**\n * Set the rate of the current video from a given playerId\n *\n */\n setRate(options: capVideoRateOptions): Promise<capVideoPlayerResult>;\n /**\n * Get the rate of the current video from a given playerId\n *\n */\n getRate(options: capVideoPlayerIdOptions): Promise<capVideoPlayerResult>;\n /**\n * Stop all players playing\n *\n */\n stopAllPlayers(): Promise<capVideoPlayerResult>;\n /**\n * Show controller\n *\n */\n showController(): Promise<capVideoPlayerResult>;\n /**\n * isControllerIsFullyVisible\n *\n */\n isControllerIsFullyVisible(): Promise<capVideoPlayerResult>;\n /**\n * Exit player\n *\n */\n exitPlayer(): Promise<capVideoPlayerResult>;\n}\nexport interface capEchoOptions {\n /**\n * String to be echoed\n */\n\n value?: string;\n}\nexport interface capVideoPlayerOptions {\n /**\n * Player mode\n * - \"fullscreen\"\n * - \"embedded\" (Web only)\n */\n mode?: string;\n /**\n * The url of the video to play\n */\n url?: string;\n /**\n * The url of subtitle associated with the video\n */\n subtitle?: string;\n /**\n * The language of subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n */\n language?: string;\n /**\n * SubTitle Options\n */\n subtitleOptions?: SubTitleOptions;\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Initial playing rate\n */\n rate?: number;\n /**\n * Exit on VideoEnd (iOS, Android)\n * default: true\n */\n exitOnEnd?: boolean;\n /**\n * Loop on VideoEnd when exitOnEnd false (iOS, Android)\n * default: false\n */\n loopOnEnd?: boolean;\n /**\n * Picture in Picture Enable (iOS, Android)\n * default: true\n */\n pipEnabled?: boolean;\n /**\n * Background Mode Enable (iOS, Android)\n * default: true\n */\n bkmodeEnabled?: boolean;\n /**\n * Show Controls Enable (iOS, Android)\n * default: true\n */\n showControls?: boolean;\n /**\n * Display Mode [\"all\", \"portrait\", \"landscape\"] (iOS, Android)\n * default: \"all\"\n */\n displayMode?: string;\n /**\n * Component Tag or DOM Element Tag (React app)\n */\n componentTag?: string;\n /**\n * Player Width (mode \"embedded\" only)\n */\n width?: number;\n /**\n * Player height (mode \"embedded\" only)\n */\n height?: number;\n /**\n * Headers for the request (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n headers?: {\n [key: string]: string;\n };\n /**\n * Title shown in the player and Chromecast metadata (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n title?: string;\n /**\n * Subtitle shown below the title in the player and Chromecast metadata (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n */\n smallTitle?: string;\n /**\n * ExoPlayer Progress Bar and Spinner color (Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * Must be a valid hex color code\n * default: #FFFFFF\n */\n accentColor?: string;\n /**\n * Chromecast enable/disable (iOS, Android)\n * iOS requires Google Cast SDK setup and local network Info.plist keys.\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: true\n */\n chromecast?: boolean;\n /**\n * Artwork url to be shown in Chromecast player (iOS, Android)\n * by Manuel García Marín (https://github.com/PhantomPainX)\n * default: \"\"\n */\n artwork?: string;\n /**\n * DRM configuration for protected content (iOS: FairPlay, Android: Widevine)\n */\n drm?: DrmOptions;\n}\nexport interface capVideoPlayerIdOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n}\nexport interface capVideoRateOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Rate value\n */\n rate?: number;\n}\nexport interface capVideoVolumeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Volume value between [0 - 1]\n */\n volume?: number;\n}\nexport interface capVideoTimeOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video time value you want to seek to\n */\n seektime?: number;\n}\nexport interface capVideoMutedOptions {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Muted value true or false\n */\n muted?: boolean;\n}\nexport interface capVideoListener {\n /**\n * Id of DIV Element parent of the player\n */\n playerId?: string;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capExitListener {\n /**\n * Dismiss value true or false\n */\n dismiss?: boolean;\n /**\n * Video current time when listener triggered\n */\n currentTime?: number;\n}\nexport interface capVideoPlayerResult {\n /**\n * result set to true when successful else false\n */\n result?: boolean;\n /**\n * method name\n */\n method?: string;\n /**\n * value returned\n */\n value?: any;\n /**\n * message string\n */\n message?: string;\n}\nexport interface SubTitleOptions {\n /**\n * Foreground Color in RGBA (default rgba(255,255,255,1)\n */\n foregroundColor?: string;\n /**\n * Background Color in RGBA (default rgba(0,0,0,1)\n */\n backgroundColor?: string;\n /**\n * Font Size in pixels (default 16)\n */\n fontSize?: number;\n\n /**\n * Get the native Capacitor plugin version\n *\n * @returns {Promise<{ id: string }>} an Promise with version for this device\n * @throws An error if the something went wrong\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\nexport interface FairPlayDrmOptions {\n /**\n * The URL to fetch the FairPlay certificate\n */\n certificateUrl?: string;\n /**\n * The URL to send the SPC and receive the CKC license (FairPlay license server URL)\n */\n contentKeySpcUrl?: string;\n}\nexport interface PlayreadyDrmOptions {\n /**\n * The URL to fetch the PlayReady license\n */\n certificateUrl?: string;\n}\nexport interface WidevineDrmOptions {\n /**\n * The URL to fetch the Widevine license\n */\n certificateUrl?: string;\n}\nexport interface DrmOptions {\n /**\n * FairPlay DRM configuration (iOS)\n */\n fairplay?: FairPlayDrmOptions;\n /**\n * PlayReady DRM configuration\n */\n playready?: PlayreadyDrmOptions;\n /**\n * Widevine DRM configuration (Android)\n */\n widevine?: WidevineDrmOptions;\n}\n"]}
|
|
@@ -13,6 +13,10 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
13
13
|
private var loopOnEnd: Bool
|
|
14
14
|
private var pipEnabled: Bool
|
|
15
15
|
private var showControls: Bool
|
|
16
|
+
private var chromecast: Bool
|
|
17
|
+
private var title: String?
|
|
18
|
+
private var smallTitle: String?
|
|
19
|
+
private var artwork: String?
|
|
16
20
|
private var rate: Float
|
|
17
21
|
private var timeObserver: Any?
|
|
18
22
|
private var onPlay: (() -> Void)?
|
|
@@ -23,8 +27,23 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
23
27
|
private var fairplayCertificateUrl: String?
|
|
24
28
|
private var fairplayContentKeySpcUrl: String?
|
|
25
29
|
private var contentKeySession: AVContentKeySession?
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
private var castController: VideoPlayerCastController?
|
|
31
|
+
|
|
32
|
+
init(
|
|
33
|
+
playerId: String,
|
|
34
|
+
url: String,
|
|
35
|
+
rate: Float,
|
|
36
|
+
exitOnEnd: Bool,
|
|
37
|
+
loopOnEnd: Bool,
|
|
38
|
+
pipEnabled: Bool,
|
|
39
|
+
showControls: Bool,
|
|
40
|
+
chromecast: Bool,
|
|
41
|
+
title: String? = nil,
|
|
42
|
+
smallTitle: String? = nil,
|
|
43
|
+
artwork: String? = nil,
|
|
44
|
+
fairplayCertificateUrl: String? = nil,
|
|
45
|
+
fairplayContentKeySpcUrl: String? = nil
|
|
46
|
+
) {
|
|
28
47
|
self.playerId = playerId
|
|
29
48
|
self.videoUrl = url
|
|
30
49
|
self.rate = rate
|
|
@@ -32,6 +51,10 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
32
51
|
self.loopOnEnd = loopOnEnd
|
|
33
52
|
self.pipEnabled = pipEnabled
|
|
34
53
|
self.showControls = showControls
|
|
54
|
+
self.chromecast = chromecast
|
|
55
|
+
self.title = title
|
|
56
|
+
self.smallTitle = smallTitle
|
|
57
|
+
self.artwork = artwork
|
|
35
58
|
self.fairplayCertificateUrl = fairplayCertificateUrl
|
|
36
59
|
self.fairplayContentKeySpcUrl = fairplayContentKeySpcUrl
|
|
37
60
|
super.init()
|
|
@@ -66,11 +89,37 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
66
89
|
|
|
67
90
|
// Picture in Picture support
|
|
68
91
|
playerViewController?.allowsPictureInPicturePlayback = pipEnabled
|
|
92
|
+
setupChromecast()
|
|
69
93
|
|
|
70
94
|
// Setup observers
|
|
71
95
|
setupObservers()
|
|
72
96
|
}
|
|
73
97
|
|
|
98
|
+
private func setupChromecast() {
|
|
99
|
+
guard chromecast,
|
|
100
|
+
let playerViewController = playerViewController,
|
|
101
|
+
let player = player else {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
castController = VideoPlayerCastController(
|
|
106
|
+
videoUrl: videoUrl,
|
|
107
|
+
title: title,
|
|
108
|
+
smallTitle: smallTitle,
|
|
109
|
+
artwork: artwork
|
|
110
|
+
)
|
|
111
|
+
castController?.setOnPlay { [weak self] in
|
|
112
|
+
self?.onPlay?()
|
|
113
|
+
}
|
|
114
|
+
castController?.setOnPause { [weak self] in
|
|
115
|
+
self?.onPause?()
|
|
116
|
+
}
|
|
117
|
+
castController?.setOnEnd { [weak self] in
|
|
118
|
+
self?.handlePlaybackEnded()
|
|
119
|
+
}
|
|
120
|
+
castController?.attach(to: playerViewController, player: player)
|
|
121
|
+
}
|
|
122
|
+
|
|
74
123
|
private func setupObservers() {
|
|
75
124
|
guard let player = player else { return }
|
|
76
125
|
|
|
@@ -115,7 +164,14 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
115
164
|
}
|
|
116
165
|
|
|
117
166
|
@objc private func playerDidFinishPlaying() {
|
|
167
|
+
handlePlaybackEnded()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private func handlePlaybackEnded() {
|
|
118
171
|
if loopOnEnd {
|
|
172
|
+
if castController?.restartPlayback() == true {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
119
175
|
player?.seek(to: .zero)
|
|
120
176
|
player?.play()
|
|
121
177
|
} else if exitOnEnd {
|
|
@@ -133,13 +189,14 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
133
189
|
}
|
|
134
190
|
|
|
135
191
|
viewController.present(playerVC, animated: true) {
|
|
136
|
-
self.
|
|
192
|
+
self.play()
|
|
137
193
|
completion()
|
|
138
194
|
}
|
|
139
195
|
}
|
|
140
196
|
|
|
141
197
|
func dismiss() {
|
|
142
198
|
let currentTime = getCurrentTime()
|
|
199
|
+
castController?.detach(stopRemoteMedia: true)
|
|
143
200
|
playerViewController?.dismiss(animated: true) { [weak self] in
|
|
144
201
|
self?.cleanup()
|
|
145
202
|
self?.onExit?(currentTime)
|
|
@@ -147,6 +204,8 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
147
204
|
}
|
|
148
205
|
|
|
149
206
|
private func cleanup() {
|
|
207
|
+
castController?.detach(stopRemoteMedia: false)
|
|
208
|
+
castController = nil
|
|
150
209
|
if let observer = timeObserver {
|
|
151
210
|
player?.removeObserver(self, forKeyPath: "rate")
|
|
152
211
|
player?.removeTimeObserver(observer)
|
|
@@ -163,54 +222,91 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
163
222
|
// MARK: - Playback Control
|
|
164
223
|
|
|
165
224
|
func play() {
|
|
225
|
+
if castController?.play() == true {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
166
228
|
player?.play()
|
|
167
229
|
}
|
|
168
230
|
|
|
169
231
|
func pause() {
|
|
232
|
+
if castController?.pause() == true {
|
|
233
|
+
return
|
|
234
|
+
}
|
|
170
235
|
player?.pause()
|
|
171
236
|
}
|
|
172
237
|
|
|
173
238
|
func isPlaying() -> Bool {
|
|
239
|
+
if castController?.isCasting == true {
|
|
240
|
+
return castController?.isPlaying() ?? false
|
|
241
|
+
}
|
|
174
242
|
guard let player = player else { return false }
|
|
175
243
|
return player.rate > 0
|
|
176
244
|
}
|
|
177
245
|
|
|
178
246
|
func getDuration() -> Double {
|
|
247
|
+
if castController?.isCasting == true {
|
|
248
|
+
return castController?.getDuration() ?? 0
|
|
249
|
+
}
|
|
179
250
|
guard let duration = playerItem?.duration else { return 0 }
|
|
180
251
|
return CMTimeGetSeconds(duration)
|
|
181
252
|
}
|
|
182
253
|
|
|
183
254
|
func getCurrentTime() -> Double {
|
|
255
|
+
if castController?.isCasting == true {
|
|
256
|
+
return castController?.getCurrentTime() ?? 0
|
|
257
|
+
}
|
|
184
258
|
guard let currentTime = player?.currentTime() else { return 0 }
|
|
185
259
|
return CMTimeGetSeconds(currentTime)
|
|
186
260
|
}
|
|
187
261
|
|
|
188
262
|
func setCurrentTime(_ time: Double) {
|
|
263
|
+
if castController?.setCurrentTime(time) == true {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
189
266
|
let cmTime = CMTime(seconds: time, preferredTimescale: 600)
|
|
190
267
|
player?.seek(to: cmTime)
|
|
191
268
|
}
|
|
192
269
|
|
|
193
270
|
func getVolume() -> Float {
|
|
271
|
+
if castController?.isCasting == true {
|
|
272
|
+
return castController?.getVolume() ?? 0
|
|
273
|
+
}
|
|
194
274
|
return player?.volume ?? 0
|
|
195
275
|
}
|
|
196
276
|
|
|
197
277
|
func setVolume(_ volume: Float) {
|
|
278
|
+
if castController?.setVolume(volume) == true {
|
|
279
|
+
return
|
|
280
|
+
}
|
|
198
281
|
player?.volume = volume
|
|
199
282
|
}
|
|
200
283
|
|
|
201
284
|
func getMuted() -> Bool {
|
|
285
|
+
if castController?.isCasting == true {
|
|
286
|
+
return castController?.getMuted() ?? false
|
|
287
|
+
}
|
|
202
288
|
return player?.isMuted ?? false
|
|
203
289
|
}
|
|
204
290
|
|
|
205
291
|
func setMuted(_ muted: Bool) {
|
|
292
|
+
if castController?.setMuted(muted) == true {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
206
295
|
player?.isMuted = muted
|
|
207
296
|
}
|
|
208
297
|
|
|
209
298
|
func getRate() -> Float {
|
|
299
|
+
if castController?.isCasting == true {
|
|
300
|
+
return castController?.getRate() ?? 0
|
|
301
|
+
}
|
|
210
302
|
return player?.rate ?? 0
|
|
211
303
|
}
|
|
212
304
|
|
|
213
305
|
func setRate(_ rate: Float) {
|
|
306
|
+
if castController?.setRate(rate) == true {
|
|
307
|
+
self.rate = rate
|
|
308
|
+
return
|
|
309
|
+
}
|
|
214
310
|
player?.rate = rate
|
|
215
311
|
self.rate = rate
|
|
216
312
|
}
|
|
@@ -278,7 +374,7 @@ extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
|
|
|
278
374
|
guard let certData = certData else {
|
|
279
375
|
keyRequest.processContentKeyResponseError(
|
|
280
376
|
certError ?? NSError(domain: "VideoPlayer", code: -2,
|
|
281
|
-
|
|
377
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to fetch FairPlay certificate"])
|
|
282
378
|
)
|
|
283
379
|
return
|
|
284
380
|
}
|
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import AVKit
|
|
3
|
+
import Foundation
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
#if canImport(GoogleCast)
|
|
7
|
+
import GoogleCast
|
|
8
|
+
|
|
9
|
+
final class VideoPlayerCastController: NSObject {
|
|
10
|
+
private enum PendingCastCommand {
|
|
11
|
+
case play
|
|
12
|
+
case pause
|
|
13
|
+
case seek(Double)
|
|
14
|
+
case volume(Float)
|
|
15
|
+
case muted(Bool)
|
|
16
|
+
case rate(Float)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private let videoUrl: String
|
|
20
|
+
private let title: String?
|
|
21
|
+
private let smallTitle: String?
|
|
22
|
+
private let artwork: String?
|
|
23
|
+
|
|
24
|
+
private weak var player: AVPlayer?
|
|
25
|
+
private weak var playerViewController: AVPlayerViewController?
|
|
26
|
+
private weak var castButton: GCKUICastButton?
|
|
27
|
+
private weak var observedRemoteMediaClient: GCKRemoteMediaClient?
|
|
28
|
+
private var mediaLoadRequest: GCKRequest?
|
|
29
|
+
private var pendingCastCommands: [PendingCastCommand] = []
|
|
30
|
+
private var isLoadedOnCast = false
|
|
31
|
+
private var isLoadingOnCast = false
|
|
32
|
+
private var localWasPlaying = false
|
|
33
|
+
private var isDetached = false
|
|
34
|
+
private var lastRemoteIsPlaying: Bool?
|
|
35
|
+
private var didNotifyRemoteEnd = false
|
|
36
|
+
private var onPlay: (() -> Void)?
|
|
37
|
+
private var onPause: (() -> Void)?
|
|
38
|
+
private var onEnd: (() -> Void)?
|
|
39
|
+
|
|
40
|
+
var isCasting: Bool {
|
|
41
|
+
return remoteMediaClient != nil && isLoadedOnCast
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private var remoteMediaClient: GCKRemoteMediaClient? {
|
|
45
|
+
guard GCKCastContext.isSharedInstanceInitialized() else {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
return GCKCastContext.sharedInstance().sessionManager.currentCastSession?.remoteMediaClient
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?) {
|
|
52
|
+
self.videoUrl = videoUrl
|
|
53
|
+
self.title = title
|
|
54
|
+
self.smallTitle = smallTitle
|
|
55
|
+
self.artwork = artwork
|
|
56
|
+
super.init()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func attach(to playerViewController: AVPlayerViewController, player: AVPlayer) {
|
|
60
|
+
self.playerViewController = playerViewController
|
|
61
|
+
self.player = player
|
|
62
|
+
|
|
63
|
+
let attachOnMain = { [weak self] in
|
|
64
|
+
guard let self = self,
|
|
65
|
+
Self.configureCastContext() else {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
GCKCastContext.sharedInstance().sessionManager.add(self)
|
|
70
|
+
self.addCastButton(to: playerViewController)
|
|
71
|
+
self.loadMediaIfCastSessionAvailable()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if Thread.isMainThread {
|
|
75
|
+
attachOnMain()
|
|
76
|
+
} else {
|
|
77
|
+
DispatchQueue.main.async(execute: attachOnMain)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func detach(stopRemoteMedia: Bool) {
|
|
82
|
+
guard !isDetached else {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
isDetached = true
|
|
86
|
+
clearMediaLoadRequest()
|
|
87
|
+
pendingCastCommands.removeAll()
|
|
88
|
+
|
|
89
|
+
DispatchQueue.main.async { [weak self] in
|
|
90
|
+
guard let self = self,
|
|
91
|
+
GCKCastContext.isSharedInstanceInitialized() else {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if stopRemoteMedia {
|
|
96
|
+
self.remoteMediaClient?.stop()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
self.stopRemoteMediaObservation()
|
|
100
|
+
GCKCastContext.sharedInstance().sessionManager.remove(self)
|
|
101
|
+
self.castButton?.removeFromSuperview()
|
|
102
|
+
self.castButton = nil
|
|
103
|
+
self.player = nil
|
|
104
|
+
self.playerViewController = nil
|
|
105
|
+
self.isLoadedOnCast = false
|
|
106
|
+
self.isLoadingOnCast = false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func play() -> Bool {
|
|
111
|
+
guard let remoteMediaClient = remoteMediaClient else {
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
guard isLoadedOnCast else {
|
|
115
|
+
return enqueuePendingCastCommand(.play)
|
|
116
|
+
}
|
|
117
|
+
remoteMediaClient.play()
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func pause() -> Bool {
|
|
122
|
+
guard let remoteMediaClient = remoteMediaClient else {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
guard isLoadedOnCast else {
|
|
126
|
+
return enqueuePendingCastCommand(.pause)
|
|
127
|
+
}
|
|
128
|
+
remoteMediaClient.pause()
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func setCurrentTime(_ time: Double) -> Bool {
|
|
133
|
+
guard let remoteMediaClient = remoteMediaClient else {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
guard isLoadedOnCast else {
|
|
137
|
+
return enqueuePendingCastCommand(.seek(time))
|
|
138
|
+
}
|
|
139
|
+
let options = GCKMediaSeekOptions()
|
|
140
|
+
options.interval = time
|
|
141
|
+
options.relative = false
|
|
142
|
+
options.resumeState = .unchanged
|
|
143
|
+
remoteMediaClient.seek(with: options)
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func isPlaying() -> Bool {
|
|
148
|
+
guard let playerState = remoteMediaClient?.mediaStatus?.playerState else {
|
|
149
|
+
return false
|
|
150
|
+
}
|
|
151
|
+
return playerState == .playing || playerState == .buffering
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func getDuration() -> Double {
|
|
155
|
+
return remoteMediaClient?.mediaStatus?.mediaInformation?.streamDuration ?? 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func getCurrentTime() -> Double {
|
|
159
|
+
return remoteMediaClient?.approximateStreamPosition() ?? 0
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func getVolume() -> Float {
|
|
163
|
+
return remoteMediaClient?.mediaStatus?.volume ?? 0
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
func setVolume(_ volume: Float) -> Bool {
|
|
167
|
+
guard let remoteMediaClient = remoteMediaClient else {
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
guard isLoadedOnCast else {
|
|
171
|
+
return enqueuePendingCastCommand(.volume(volume))
|
|
172
|
+
}
|
|
173
|
+
remoteMediaClient.setStreamVolume(volume)
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
func getMuted() -> Bool {
|
|
178
|
+
return remoteMediaClient?.mediaStatus?.isMuted ?? false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func setMuted(_ muted: Bool) -> Bool {
|
|
182
|
+
guard let remoteMediaClient = remoteMediaClient else {
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
guard isLoadedOnCast else {
|
|
186
|
+
return enqueuePendingCastCommand(.muted(muted))
|
|
187
|
+
}
|
|
188
|
+
remoteMediaClient.setStreamMuted(muted)
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func getRate() -> Float {
|
|
193
|
+
return remoteMediaClient?.mediaStatus?.playbackRate ?? 0
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func setRate(_ rate: Float) -> Bool {
|
|
197
|
+
guard let remoteMediaClient = remoteMediaClient else {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
guard isLoadedOnCast else {
|
|
201
|
+
return enqueuePendingCastCommand(.rate(rate))
|
|
202
|
+
}
|
|
203
|
+
remoteMediaClient.setPlaybackRate(rate)
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func restartPlayback() -> Bool {
|
|
208
|
+
guard let remoteMediaClient = remoteMediaClient,
|
|
209
|
+
let mediaInfo = makeMediaInformation() else {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
didNotifyRemoteEnd = false
|
|
214
|
+
isLoadedOnCast = false
|
|
215
|
+
isLoadingOnCast = true
|
|
216
|
+
clearMediaLoadRequest()
|
|
217
|
+
|
|
218
|
+
let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
|
|
219
|
+
mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
|
|
220
|
+
mediaLoadRequestDataBuilder.autoplay = NSNumber(value: true)
|
|
221
|
+
mediaLoadRequestDataBuilder.startTime = 0
|
|
222
|
+
let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
|
|
223
|
+
request.delegate = self
|
|
224
|
+
mediaLoadRequest = request
|
|
225
|
+
return true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func setOnPlay(_ callback: @escaping () -> Void) {
|
|
229
|
+
onPlay = callback
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func setOnPause(_ callback: @escaping () -> Void) {
|
|
233
|
+
onPause = callback
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
func setOnEnd(_ callback: @escaping () -> Void) {
|
|
237
|
+
onEnd = callback
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private extension VideoPlayerCastController {
|
|
242
|
+
static func configureCastContext() -> Bool {
|
|
243
|
+
guard Thread.isMainThread else {
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if GCKCastContext.isSharedInstanceInitialized() {
|
|
248
|
+
return true
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// This plugin intentionally uses the default media receiver.
|
|
252
|
+
let criteria = GCKDiscoveryCriteria(applicationID: kGCKDefaultMediaReceiverApplicationID)
|
|
253
|
+
let options = GCKCastOptions(discoveryCriteria: criteria)
|
|
254
|
+
var error: GCKError?
|
|
255
|
+
return GCKCastContext.setSharedInstanceWith(options, error: &error)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func addCastButton(to playerViewController: AVPlayerViewController) {
|
|
259
|
+
guard castButton == nil else {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
guard let overlayView = playerViewController.view else {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
overlayView.isUserInteractionEnabled = true
|
|
268
|
+
let button = GCKUICastButton(frame: .zero)
|
|
269
|
+
button.translatesAutoresizingMaskIntoConstraints = false
|
|
270
|
+
button.tintColor = .white
|
|
271
|
+
button.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
|
272
|
+
button.layer.cornerRadius = 22
|
|
273
|
+
button.clipsToBounds = true
|
|
274
|
+
button.accessibilityLabel = "Cast"
|
|
275
|
+
|
|
276
|
+
overlayView.addSubview(button)
|
|
277
|
+
NSLayoutConstraint.activate([
|
|
278
|
+
button.widthAnchor.constraint(equalToConstant: 44),
|
|
279
|
+
button.heightAnchor.constraint(equalToConstant: 44),
|
|
280
|
+
button.topAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.topAnchor, constant: 16),
|
|
281
|
+
button.trailingAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.trailingAnchor, constant: -16)
|
|
282
|
+
])
|
|
283
|
+
|
|
284
|
+
castButton = button
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
func loadMediaIfCastSessionAvailable() {
|
|
288
|
+
guard let remoteMediaClient = remoteMediaClient,
|
|
289
|
+
let mediaInfo = makeMediaInformation() else {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
observeRemoteMediaClient(remoteMediaClient)
|
|
294
|
+
let playPosition = player?.currentTime().seconds ?? 0
|
|
295
|
+
localWasPlaying = (player?.rate ?? 0) > 0
|
|
296
|
+
player?.pause()
|
|
297
|
+
isLoadedOnCast = false
|
|
298
|
+
isLoadingOnCast = true
|
|
299
|
+
lastRemoteIsPlaying = nil
|
|
300
|
+
didNotifyRemoteEnd = false
|
|
301
|
+
clearMediaLoadRequest()
|
|
302
|
+
|
|
303
|
+
let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
|
|
304
|
+
mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
|
|
305
|
+
mediaLoadRequestDataBuilder.autoplay = NSNumber(value: localWasPlaying)
|
|
306
|
+
mediaLoadRequestDataBuilder.startTime = playPosition.isFinite ? playPosition : 0
|
|
307
|
+
let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
|
|
308
|
+
request.delegate = self
|
|
309
|
+
mediaLoadRequest = request
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
func resumeCastSession(_ session: GCKSession) {
|
|
313
|
+
clearMediaLoadRequest()
|
|
314
|
+
pendingCastCommands.removeAll()
|
|
315
|
+
isLoadingOnCast = false
|
|
316
|
+
|
|
317
|
+
guard let remoteMediaClient = session.remoteMediaClient else {
|
|
318
|
+
isLoadedOnCast = false
|
|
319
|
+
stopRemoteMediaObservation()
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
observeRemoteMediaClient(remoteMediaClient)
|
|
324
|
+
isLoadedOnCast = isCurrentVideo(remoteMediaClient.mediaStatus?.mediaInformation)
|
|
325
|
+
handleRemoteMediaStatus(remoteMediaClient.mediaStatus)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func observeRemoteMediaClient(_ remoteMediaClient: GCKRemoteMediaClient) {
|
|
329
|
+
if let observedRemoteMediaClient = observedRemoteMediaClient,
|
|
330
|
+
observedRemoteMediaClient === remoteMediaClient {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
observedRemoteMediaClient?.remove(self)
|
|
335
|
+
remoteMediaClient.add(self)
|
|
336
|
+
observedRemoteMediaClient = remoteMediaClient
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
func stopRemoteMediaObservation() {
|
|
340
|
+
observedRemoteMediaClient?.remove(self)
|
|
341
|
+
observedRemoteMediaClient = nil
|
|
342
|
+
lastRemoteIsPlaying = nil
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private func enqueuePendingCastCommand(_ command: PendingCastCommand) -> Bool {
|
|
346
|
+
guard isLoadingOnCast else {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
pendingCastCommands.append(command)
|
|
351
|
+
if pendingCastCommands.count > 20 {
|
|
352
|
+
pendingCastCommands.removeFirst(pendingCastCommands.count - 20)
|
|
353
|
+
}
|
|
354
|
+
return true
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
func flushPendingCastCommands() {
|
|
358
|
+
guard let remoteMediaClient = remoteMediaClient,
|
|
359
|
+
isLoadedOnCast else {
|
|
360
|
+
pendingCastCommands.removeAll()
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let commands = pendingCastCommands
|
|
365
|
+
pendingCastCommands.removeAll()
|
|
366
|
+
for command in commands {
|
|
367
|
+
apply(command, to: remoteMediaClient)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private func apply(_ command: PendingCastCommand, to remoteMediaClient: GCKRemoteMediaClient) {
|
|
372
|
+
switch command {
|
|
373
|
+
case .play:
|
|
374
|
+
remoteMediaClient.play()
|
|
375
|
+
case .pause:
|
|
376
|
+
remoteMediaClient.pause()
|
|
377
|
+
case .seek(let time):
|
|
378
|
+
let options = GCKMediaSeekOptions()
|
|
379
|
+
options.interval = time
|
|
380
|
+
options.relative = false
|
|
381
|
+
options.resumeState = .unchanged
|
|
382
|
+
remoteMediaClient.seek(with: options)
|
|
383
|
+
case .volume(let volume):
|
|
384
|
+
remoteMediaClient.setStreamVolume(volume)
|
|
385
|
+
case .muted(let muted):
|
|
386
|
+
remoteMediaClient.setStreamMuted(muted)
|
|
387
|
+
case .rate(let rate):
|
|
388
|
+
remoteMediaClient.setPlaybackRate(rate)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func applyPendingCastCommandsToLocalPlayer() -> Bool {
|
|
393
|
+
var didApplyPlaybackCommand = false
|
|
394
|
+
let commands = pendingCastCommands
|
|
395
|
+
pendingCastCommands.removeAll()
|
|
396
|
+
for command in commands {
|
|
397
|
+
switch command {
|
|
398
|
+
case .play:
|
|
399
|
+
player?.play()
|
|
400
|
+
didApplyPlaybackCommand = true
|
|
401
|
+
case .pause:
|
|
402
|
+
player?.pause()
|
|
403
|
+
didApplyPlaybackCommand = true
|
|
404
|
+
case .seek(let time):
|
|
405
|
+
player?.seek(to: CMTime(seconds: time, preferredTimescale: 600))
|
|
406
|
+
case .volume(let volume):
|
|
407
|
+
player?.volume = volume
|
|
408
|
+
case .muted(let muted):
|
|
409
|
+
player?.isMuted = muted
|
|
410
|
+
case .rate(let rate):
|
|
411
|
+
player?.rate = rate
|
|
412
|
+
didApplyPlaybackCommand = true
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return didApplyPlaybackCommand
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
func makeMediaInformation() -> GCKMediaInformation? {
|
|
419
|
+
guard let url = URL(string: videoUrl) else {
|
|
420
|
+
return nil
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let metadata = GCKMediaMetadata()
|
|
424
|
+
metadata.setString(title ?? url.lastPathComponent, forKey: kGCKMetadataKeyTitle)
|
|
425
|
+
if let smallTitle = smallTitle, !smallTitle.isEmpty {
|
|
426
|
+
metadata.setString(smallTitle, forKey: kGCKMetadataKeySubtitle)
|
|
427
|
+
}
|
|
428
|
+
if let artwork = artwork,
|
|
429
|
+
let artworkUrl = URL(string: artwork) {
|
|
430
|
+
metadata.addImage(GCKImage(url: artworkUrl, width: 480, height: 360))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let builder = GCKMediaInformationBuilder(contentURL: url)
|
|
434
|
+
builder.streamType = .buffered
|
|
435
|
+
builder.contentType = contentType(for: url)
|
|
436
|
+
builder.metadata = metadata
|
|
437
|
+
if let duration = player?.currentItem?.duration.seconds,
|
|
438
|
+
duration.isFinite,
|
|
439
|
+
duration > 0 {
|
|
440
|
+
builder.streamDuration = duration
|
|
441
|
+
}
|
|
442
|
+
return builder.build()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func isCurrentVideo(_ mediaInformation: GCKMediaInformation?) -> Bool {
|
|
446
|
+
guard let mediaInformation = mediaInformation else {
|
|
447
|
+
return false
|
|
448
|
+
}
|
|
449
|
+
if mediaInformation.contentID == videoUrl {
|
|
450
|
+
return true
|
|
451
|
+
}
|
|
452
|
+
guard let expectedUrl = URL(string: videoUrl) else {
|
|
453
|
+
return false
|
|
454
|
+
}
|
|
455
|
+
return mediaInformation.contentURL == expectedUrl
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
func contentType(for url: URL) -> String {
|
|
459
|
+
switch url.pathExtension.lowercased() {
|
|
460
|
+
case "m3u8":
|
|
461
|
+
return "application/x-mpegURL"
|
|
462
|
+
case "mpd":
|
|
463
|
+
return "application/dash+xml"
|
|
464
|
+
case "mov":
|
|
465
|
+
return "video/quicktime"
|
|
466
|
+
case "m4v":
|
|
467
|
+
return "video/x-m4v"
|
|
468
|
+
case "webm":
|
|
469
|
+
return "video/webm"
|
|
470
|
+
case "mp4":
|
|
471
|
+
return "video/mp4"
|
|
472
|
+
default:
|
|
473
|
+
return "video/mp4"
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
func resumeLocalPlayback(from session: GCKSession) {
|
|
478
|
+
guard !isDetached,
|
|
479
|
+
isLoadedOnCast else {
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
let remoteMediaClient = session.remoteMediaClient
|
|
484
|
+
let position = remoteMediaClient?.approximateStreamPosition() ?? 0
|
|
485
|
+
let shouldResume = remoteMediaClient?.mediaStatus?.playerState == .playing ||
|
|
486
|
+
remoteMediaClient?.mediaStatus?.playerState == .buffering
|
|
487
|
+
|
|
488
|
+
isLoadedOnCast = false
|
|
489
|
+
isLoadingOnCast = false
|
|
490
|
+
stopRemoteMediaObservation()
|
|
491
|
+
clearMediaLoadRequest()
|
|
492
|
+
pendingCastCommands.removeAll()
|
|
493
|
+
let seekTime = CMTime(seconds: position, preferredTimescale: 600)
|
|
494
|
+
player?.seek(to: seekTime) { [weak self] _ in
|
|
495
|
+
if shouldResume {
|
|
496
|
+
self?.player?.play()
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
func clearMediaLoadRequest() {
|
|
502
|
+
mediaLoadRequest?.delegate = nil
|
|
503
|
+
mediaLoadRequest = nil
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
func completeMediaLoadRequest(_ request: GCKRequest) {
|
|
507
|
+
guard request === mediaLoadRequest else {
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
clearMediaLoadRequest()
|
|
511
|
+
isLoadingOnCast = false
|
|
512
|
+
isLoadedOnCast = true
|
|
513
|
+
didNotifyRemoteEnd = false
|
|
514
|
+
flushPendingCastCommands()
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
func failMediaLoadRequest(_ request: GCKRequest) {
|
|
518
|
+
guard request === mediaLoadRequest else {
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
cancelPendingCastHandoff(resumeLocalIfNeeded: true)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
func cancelPendingCastHandoff(resumeLocalIfNeeded: Bool) {
|
|
525
|
+
clearMediaLoadRequest()
|
|
526
|
+
isLoadingOnCast = false
|
|
527
|
+
isLoadedOnCast = false
|
|
528
|
+
if !pendingCastCommands.isEmpty {
|
|
529
|
+
let didApplyPlaybackCommand = applyPendingCastCommandsToLocalPlayer()
|
|
530
|
+
if localWasPlaying && !didApplyPlaybackCommand {
|
|
531
|
+
player?.play()
|
|
532
|
+
}
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if localWasPlaying {
|
|
536
|
+
player?.play()
|
|
537
|
+
} else if resumeLocalIfNeeded {
|
|
538
|
+
player?.pause()
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
func shouldHandleRemoteMediaStatus(_ mediaStatus: GCKMediaStatus) -> Bool {
|
|
543
|
+
if isLoadedOnCast || isLoadingOnCast {
|
|
544
|
+
return true
|
|
545
|
+
}
|
|
546
|
+
guard isCurrentVideo(mediaStatus.mediaInformation) else {
|
|
547
|
+
return false
|
|
548
|
+
}
|
|
549
|
+
isLoadedOnCast = true
|
|
550
|
+
return true
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
func handleRemoteMediaStatus(_ mediaStatus: GCKMediaStatus?) {
|
|
554
|
+
guard !isDetached,
|
|
555
|
+
let mediaStatus = mediaStatus,
|
|
556
|
+
shouldHandleRemoteMediaStatus(mediaStatus) else {
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
switch mediaStatus.playerState {
|
|
561
|
+
case .playing, .buffering:
|
|
562
|
+
didNotifyRemoteEnd = false
|
|
563
|
+
if lastRemoteIsPlaying != true {
|
|
564
|
+
DispatchQueue.main.async { [weak self] in
|
|
565
|
+
self?.onPlay?()
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
lastRemoteIsPlaying = true
|
|
569
|
+
case .paused:
|
|
570
|
+
if lastRemoteIsPlaying == true {
|
|
571
|
+
DispatchQueue.main.async { [weak self] in
|
|
572
|
+
self?.onPause?()
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
lastRemoteIsPlaying = false
|
|
576
|
+
case .idle:
|
|
577
|
+
if mediaStatus.idleReason == .finished,
|
|
578
|
+
!didNotifyRemoteEnd {
|
|
579
|
+
didNotifyRemoteEnd = true
|
|
580
|
+
DispatchQueue.main.async { [weak self] in
|
|
581
|
+
self?.onEnd?()
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
lastRemoteIsPlaying = false
|
|
585
|
+
default:
|
|
586
|
+
break
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
extension VideoPlayerCastController: GCKSessionManagerListener {
|
|
592
|
+
@objc func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKSession) {
|
|
593
|
+
loadMediaIfCastSessionAvailable()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
@objc func sessionManager(_ sessionManager: GCKSessionManager, didResumeSession session: GCKSession) {
|
|
597
|
+
resumeCastSession(session)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
@objc func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKSession, withError error: Error?) {
|
|
601
|
+
if isLoadedOnCast {
|
|
602
|
+
resumeLocalPlayback(from: session)
|
|
603
|
+
} else if isLoadingOnCast {
|
|
604
|
+
cancelPendingCastHandoff(resumeLocalIfNeeded: true)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
@objc func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKSession, withError error: Error) {
|
|
609
|
+
cancelPendingCastHandoff(resumeLocalIfNeeded: true)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
extension VideoPlayerCastController: GCKRequestDelegate {
|
|
614
|
+
@objc func requestDidComplete(_ request: GCKRequest) {
|
|
615
|
+
completeMediaLoadRequest(request)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
@objc func request(_ request: GCKRequest, didFailWithError error: GCKError) {
|
|
619
|
+
failMediaLoadRequest(request)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
@objc func request(_ request: GCKRequest, didAbortWith abortReason: GCKRequestAbortReason) {
|
|
623
|
+
failMediaLoadRequest(request)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
extension VideoPlayerCastController: GCKRemoteMediaClientListener {
|
|
628
|
+
@objc(remoteMediaClient:didUpdateMediaStatus:)
|
|
629
|
+
func remoteMediaClient(_ client: GCKRemoteMediaClient, didUpdate mediaStatus: GCKMediaStatus?) {
|
|
630
|
+
handleRemoteMediaStatus(mediaStatus)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#else
|
|
635
|
+
|
|
636
|
+
final class VideoPlayerCastController {
|
|
637
|
+
var isCasting: Bool {
|
|
638
|
+
return false
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?) {
|
|
642
|
+
_ = videoUrl
|
|
643
|
+
_ = title
|
|
644
|
+
_ = smallTitle
|
|
645
|
+
_ = artwork
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
func attach(to playerViewController: AVPlayerViewController, player: AVPlayer) {
|
|
649
|
+
_ = playerViewController
|
|
650
|
+
_ = player
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
func detach(stopRemoteMedia: Bool) {
|
|
654
|
+
_ = stopRemoteMedia
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
func play() -> Bool { return false }
|
|
658
|
+
func pause() -> Bool { return false }
|
|
659
|
+
func isPlaying() -> Bool { return false }
|
|
660
|
+
func getDuration() -> Double { return 0 }
|
|
661
|
+
func getCurrentTime() -> Double { return 0 }
|
|
662
|
+
func setCurrentTime(_ time: Double) -> Bool {
|
|
663
|
+
_ = time
|
|
664
|
+
return false
|
|
665
|
+
}
|
|
666
|
+
func getVolume() -> Float { return 0 }
|
|
667
|
+
func setVolume(_ volume: Float) -> Bool {
|
|
668
|
+
_ = volume
|
|
669
|
+
return false
|
|
670
|
+
}
|
|
671
|
+
func getMuted() -> Bool { return false }
|
|
672
|
+
func setMuted(_ muted: Bool) -> Bool {
|
|
673
|
+
_ = muted
|
|
674
|
+
return false
|
|
675
|
+
}
|
|
676
|
+
func getRate() -> Float { return 0 }
|
|
677
|
+
func setRate(_ rate: Float) -> Bool {
|
|
678
|
+
_ = rate
|
|
679
|
+
return false
|
|
680
|
+
}
|
|
681
|
+
func restartPlayback() -> Bool { return false }
|
|
682
|
+
func setOnPlay(_ callback: @escaping () -> Void) {
|
|
683
|
+
_ = callback
|
|
684
|
+
}
|
|
685
|
+
func setOnPause(_ callback: @escaping () -> Void) {
|
|
686
|
+
_ = callback
|
|
687
|
+
}
|
|
688
|
+
func setOnEnd(_ callback: @escaping () -> Void) {
|
|
689
|
+
_ = callback
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
#endif
|
|
@@ -8,7 +8,7 @@ import AVKit
|
|
|
8
8
|
*/
|
|
9
9
|
@objc(VideoPlayerPlugin)
|
|
10
10
|
public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
-
private let pluginVersion: String = "8.1.
|
|
11
|
+
private let pluginVersion: String = "8.1.9"
|
|
12
12
|
public let identifier = "VideoPlayerPlugin"
|
|
13
13
|
public let jsName = "VideoPlayer"
|
|
14
14
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -38,7 +38,9 @@ public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
38
38
|
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
39
39
|
call.resolve(["version": self.pluginVersion])
|
|
40
40
|
}
|
|
41
|
+
}
|
|
41
42
|
|
|
43
|
+
extension VideoPlayerPlugin {
|
|
42
44
|
@objc func initPlayer(_ call: CAPPluginCall) {
|
|
43
45
|
guard let playerId = call.getString("playerId") else {
|
|
44
46
|
call.resolve([
|
|
@@ -73,6 +75,10 @@ public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
73
75
|
let loopOnEnd = call.getBool("loopOnEnd") ?? false
|
|
74
76
|
let pipEnabled = call.getBool("pipEnabled") ?? true
|
|
75
77
|
let showControls = call.getBool("showControls") ?? true
|
|
78
|
+
let chromecast = call.getBool("chromecast") ?? true
|
|
79
|
+
let title = call.getString("title")
|
|
80
|
+
let smallTitle = call.getString("smallTitle")
|
|
81
|
+
let artwork = call.getString("artwork")
|
|
76
82
|
|
|
77
83
|
// Extract FairPlay DRM options if provided
|
|
78
84
|
let drm = call.getObject("drm")
|
|
@@ -89,13 +95,43 @@ public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
89
95
|
loopOnEnd: loopOnEnd,
|
|
90
96
|
pipEnabled: pipEnabled,
|
|
91
97
|
showControls: showControls,
|
|
98
|
+
chromecast: chromecast,
|
|
99
|
+
title: title,
|
|
100
|
+
smallTitle: smallTitle,
|
|
101
|
+
artwork: artwork,
|
|
92
102
|
fairplayCertificateUrl: fairplayCertificateUrl,
|
|
93
103
|
fairplayContentKeySpcUrl: fairplayContentKeySpcUrl
|
|
94
104
|
)
|
|
95
105
|
|
|
96
|
-
player
|
|
106
|
+
// Present player
|
|
107
|
+
DispatchQueue.main.async {
|
|
108
|
+
guard let viewController = self.bridge?.viewController else {
|
|
109
|
+
call.resolve([
|
|
110
|
+
"result": false,
|
|
111
|
+
"method": "initPlayer",
|
|
112
|
+
"message": "Unable to get view controller"
|
|
113
|
+
])
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
player.setupPlayer()
|
|
118
|
+
self.configureCallbacks(for: player, playerId: playerId)
|
|
119
|
+
|
|
120
|
+
// Store player
|
|
121
|
+
self.videoPlayers[playerId] = player
|
|
122
|
+
self.currentPlayerId = playerId
|
|
123
|
+
|
|
124
|
+
player.present(on: viewController) {
|
|
125
|
+
call.resolve([
|
|
126
|
+
"result": true,
|
|
127
|
+
"method": "initPlayer",
|
|
128
|
+
"value": playerId
|
|
129
|
+
])
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
97
133
|
|
|
98
|
-
|
|
134
|
+
private func configureCallbacks(for player: FullscreenVideoPlayer, playerId: String) {
|
|
99
135
|
player.setOnPlay { [weak self] in
|
|
100
136
|
self?.notifyListeners("jeepCapVideoPlayerPlay", data: [
|
|
101
137
|
"fromPlayerId": playerId,
|
|
@@ -134,30 +170,6 @@ public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
134
170
|
"currentTime": currentTime
|
|
135
171
|
])
|
|
136
172
|
}
|
|
137
|
-
|
|
138
|
-
// Store player
|
|
139
|
-
videoPlayers[playerId] = player
|
|
140
|
-
currentPlayerId = playerId
|
|
141
|
-
|
|
142
|
-
// Present player
|
|
143
|
-
DispatchQueue.main.async {
|
|
144
|
-
guard let viewController = self.bridge?.viewController else {
|
|
145
|
-
call.resolve([
|
|
146
|
-
"result": false,
|
|
147
|
-
"method": "initPlayer",
|
|
148
|
-
"message": "Unable to get view controller"
|
|
149
|
-
])
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
player.present(on: viewController) {
|
|
154
|
-
call.resolve([
|
|
155
|
-
"result": true,
|
|
156
|
-
"method": "initPlayer",
|
|
157
|
-
"value": playerId
|
|
158
|
-
])
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
173
|
}
|
|
162
174
|
|
|
163
175
|
@objc func isPlaying(_ call: CAPPluginCall) {
|