@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.
@@ -13,5 +13,6 @@ Pod::Spec.new do |s|
13
13
  s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
14
  s.ios.deployment_target = '15.0'
15
15
  s.dependency 'Capacitor'
16
+ s.dependency 'google-cast-sdk', '~> 4.8.4'
16
17
  s.swift_version = '5.1'
17
18
  end
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
- npm install @capgo/capacitor-video-player
29
- npx cap sync
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.8";
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
- init(playerId: String, url: String, rate: Float, exitOnEnd: Bool, loopOnEnd: Bool, pipEnabled: Bool, showControls: Bool, fairplayCertificateUrl: String? = nil, fairplayContentKeySpcUrl: String? = nil) {
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.player?.play()
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
- userInfo: [NSLocalizedDescriptionKey: "Failed to fetch FairPlay certificate"])
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.8"
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.setupPlayer()
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
- // Setup callbacks
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-video-player",
3
- "version": "8.1.8",
3
+ "version": "8.1.9",
4
4
  "description": "Capacitor plugin to play video in native player",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",