@capgo/capacitor-video-player 8.2.15 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/docs.json CHANGED
@@ -431,6 +431,15 @@
431
431
  ],
432
432
  "type": "VideoSubtitle[] | undefined"
433
433
  },
434
+ {
435
+ "name": "chapters",
436
+ "tags": [],
437
+ "docs": "Chapter markers for long-form content (iOS, Android).\nShown as a chapter list in the native fullscreen player; selecting a chapter seeks to `startTime`.",
438
+ "complexTypes": [
439
+ "VideoChapter"
440
+ ],
441
+ "type": "VideoChapter[] | undefined"
442
+ },
434
443
  {
435
444
  "name": "subtitleOptions",
436
445
  "tags": [],
@@ -600,31 +609,61 @@
600
609
  }
601
610
  ]
602
611
  },
612
+ {
613
+ "name": "VideoChapter",
614
+ "slug": "videochapter",
615
+ "docs": "",
616
+ "tags": [],
617
+ "methods": [],
618
+ "properties": [
619
+ {
620
+ "name": "title",
621
+ "tags": [],
622
+ "docs": "Chapter title shown in the chapter list",
623
+ "complexTypes": [],
624
+ "type": "string"
625
+ },
626
+ {
627
+ "name": "startTime",
628
+ "tags": [],
629
+ "docs": "Start time in seconds",
630
+ "complexTypes": [],
631
+ "type": "number"
632
+ },
633
+ {
634
+ "name": "endTime",
635
+ "tags": [],
636
+ "docs": "End time in seconds (optional). Stored for future timeline markers; chapter jumps use `startTime` only in this release.",
637
+ "complexTypes": [],
638
+ "type": "number | undefined"
639
+ }
640
+ ]
641
+ },
603
642
  {
604
643
  "name": "SubTitleOptions",
605
644
  "slug": "subtitleoptions",
606
- "docs": "",
645
+ "docs": "Optional styling for on-screen subtitles.\n\nOn iOS, colors and font size are applied through `AVPlayerItem`\n`textStyleRules`, which Apple documents for WebVTT only. IMSC1 and other\nin-manifest timed text formats are not styled through this API.\nInline WebVTT cue styles can take precedence over equivalent\n`subtitleOptions` values; the plugin sets text style rules as a platform\nfallback, not a guaranteed override of authored cue styling.",
607
646
  "tags": [],
608
647
  "methods": [],
609
648
  "properties": [
610
649
  {
611
650
  "name": "foregroundColor",
612
651
  "tags": [],
613
- "docs": "Foreground Color in RGBA (default rgba(255,255,255,1)",
652
+ "docs": "Foreground color in RGBA. On Android, when the plugin applies subtitle\nstyling, omitted values fall back to `rgba(255,255,255,1)`. When\n`subtitleOptions` is absent, embedded cue colors from the media can\noverride that fallback. On iOS, omitted colors are not set by the plugin;\nthe platform uses the viewer's caption appearance settings instead of fixed\nplugin defaults.",
614
653
  "complexTypes": [],
615
654
  "type": "string | undefined"
616
655
  },
617
656
  {
618
657
  "name": "backgroundColor",
619
658
  "tags": [],
620
- "docs": "Background Color in RGBA (default rgba(0,0,0,1)",
659
+ "docs": "Background color in RGBA. On Android, when the plugin applies subtitle\nstyling in fullscreen, omitted values fall back to `rgba(0,0,0,1)`. In\npicture-in-picture, omitted values fall back to transparent when the plugin\napplies its caption style (for example sidecar subtitles or non-empty\n`subtitleOptions` that include a color override). An empty\n`subtitleOptions` object alone does not select that fallback. With\nfont-size-only options, embedded cue styles stay enabled, so authored cue\nbackgrounds can remain visible in PiP. When `subtitleOptions` is absent,\nembedded cue colors from the media can override the fallback. On iOS,\nomitted colors are not set by the plugin; the platform uses the viewer's\ncaption appearance settings instead of fixed plugin defaults.",
621
660
  "complexTypes": [],
622
661
  "type": "string | undefined"
623
662
  },
624
663
  {
625
664
  "name": "fontSize",
626
665
  "tags": [],
627
- "docs": "Font Size in pixels (default 16)",
666
+ "docs": "Font size in CSS pixels. On Android, supplied values are rounded to the\nnearest integer before applying. When omitted on embedded tracks without a\nsidecar subtitle URI, Android uses `SubtitleView.setUserDefaultTextSize()`\n(the user's system caption size), not a fixed 16 px size. Sidecar subtitles\nstill use a fixed default size when this property is omitted. On iOS,\nvalues map to AVFoundation relative size where 16 equals the system default\n(100%).",
628
667
  "complexTypes": [],
629
668
  "type": "number | undefined"
630
669
  }
@@ -148,6 +148,11 @@ export interface capVideoPlayerOptions {
148
148
  * When provided, takes precedence over `subtitle` / `language`.
149
149
  */
150
150
  subtitles?: VideoSubtitle[];
151
+ /**
152
+ * Chapter markers for long-form content (iOS, Android).
153
+ * Shown as a chapter list in the native fullscreen player; selecting a chapter seeks to `startTime`.
154
+ */
155
+ chapters?: VideoChapter[];
151
156
  /**
152
157
  * SubTitle Options
153
158
  */
@@ -352,17 +357,62 @@ export interface VideoSubtitle {
352
357
  */
353
358
  language?: string;
354
359
  }
360
+ export interface VideoChapter {
361
+ /**
362
+ * Chapter title shown in the chapter list
363
+ */
364
+ title: string;
365
+ /**
366
+ * Start time in seconds
367
+ */
368
+ startTime: number;
369
+ /**
370
+ * End time in seconds (optional). Stored for future timeline markers; chapter jumps use `startTime` only in this release.
371
+ */
372
+ endTime?: number;
373
+ }
374
+ /**
375
+ * Optional styling for on-screen subtitles.
376
+ *
377
+ * On iOS, colors and font size are applied through `AVPlayerItem`
378
+ * `textStyleRules`, which Apple documents for WebVTT only. IMSC1 and other
379
+ * in-manifest timed text formats are not styled through this API.
380
+ * Inline WebVTT cue styles can take precedence over equivalent
381
+ * `subtitleOptions` values; the plugin sets text style rules as a platform
382
+ * fallback, not a guaranteed override of authored cue styling.
383
+ */
355
384
  export interface SubTitleOptions {
356
385
  /**
357
- * Foreground Color in RGBA (default rgba(255,255,255,1)
386
+ * Foreground color in RGBA. On Android, when the plugin applies subtitle
387
+ * styling, omitted values fall back to `rgba(255,255,255,1)`. When
388
+ * `subtitleOptions` is absent, embedded cue colors from the media can
389
+ * override that fallback. On iOS, omitted colors are not set by the plugin;
390
+ * the platform uses the viewer's caption appearance settings instead of fixed
391
+ * plugin defaults.
358
392
  */
359
393
  foregroundColor?: string;
360
394
  /**
361
- * Background Color in RGBA (default rgba(0,0,0,1)
395
+ * Background color in RGBA. On Android, when the plugin applies subtitle
396
+ * styling in fullscreen, omitted values fall back to `rgba(0,0,0,1)`. In
397
+ * picture-in-picture, omitted values fall back to transparent when the plugin
398
+ * applies its caption style (for example sidecar subtitles or non-empty
399
+ * `subtitleOptions` that include a color override). An empty
400
+ * `subtitleOptions` object alone does not select that fallback. With
401
+ * font-size-only options, embedded cue styles stay enabled, so authored cue
402
+ * backgrounds can remain visible in PiP. When `subtitleOptions` is absent,
403
+ * embedded cue colors from the media can override the fallback. On iOS,
404
+ * omitted colors are not set by the plugin; the platform uses the viewer's
405
+ * caption appearance settings instead of fixed plugin defaults.
362
406
  */
363
407
  backgroundColor?: string;
364
408
  /**
365
- * Font Size in pixels (default 16)
409
+ * Font size in CSS pixels. On Android, supplied values are rounded to the
410
+ * nearest integer before applying. When omitted on embedded tracks without a
411
+ * sidecar subtitle URI, Android uses `SubtitleView.setUserDefaultTextSize()`
412
+ * (the user's system caption size), not a fixed 16 px size. Sidecar subtitles
413
+ * still use a fixed default size when this property is omitted. On iOS,
414
+ * values map to AVFoundation relative size where 16 equals the system default
415
+ * (100%).
366
416
  */
367
417
  fontSize?: number;
368
418
  }
@@ -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 /**\n * Hide the currently presented player UI without stopping playback (native fullscreen).\n *\n * Call `showPlayer()` to show it again.\n */\n hidePlayer(): Promise<capVideoPlayerResult>;\n /**\n * Show again a previously hidden player UI (native fullscreen).\n */\n showPlayer(): Promise<capVideoPlayerResult>;\n /**\n * Get the native Capacitor plugin version.\n * On web, the implementation returns `\"web\"`.\n *\n * @returns Promise resolving to the plugin version string for this build (`\"web\"` on web)\n */\n getPluginVersion(): Promise<{ version: string }>;\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 * Optional override URL used when casting to Chromecast (iOS, Android).\n *\n * This lets you cast an alternative rendition/manifest (e.g. MPEG-DASH + Widevine)\n * while keeping the on-device playback URL unchanged (e.g. HLS + FairPlay on iOS).\n */\n chromecastUrl?: string;\n /**\n * The url of subtitle associated with the video\n *\n * Prefer `subtitles` when providing more than one track.\n * Kept for backward compatibility.\n */\n subtitle?: string;\n /**\n * The language of subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n *\n * Prefer `subtitles` when providing more than one track.\n * Kept for backward compatibility.\n */\n language?: string;\n /**\n * Multiple subtitle tracks (iOS, Android).\n * When provided, takes precedence over `subtitle` / `language`.\n */\n subtitles?: VideoSubtitle[];\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 * iOS audio session category behavior (iOS).\n * - \"ambient\": respects the iPhone silent switch\n * - \"playback\": ignores the iPhone silent switch\n * - \"moviePlayback\": playback optimized for video and long-form AirPlay.\n *\n * When omitted, the plugin resolves the category from other options: `\"playback\"` if\n * `pipEnabled` or `bkmodeEnabled` is true (both default to true), otherwise `\"ambient\"`.\n */\n audioCategory?: 'ambient' | 'playback' | 'moviePlayback';\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 * When casting, Widevine's `certificateUrl` (license URL) is also forwarded to Chromecast\n * receivers as `media.customData.laurl`.\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 VideoSubtitle {\n /**\n * The url of the subtitle file (vtt, srt, ...)\n */\n subtitle: string;\n /**\n * The language of the subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n */\n language?: 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}\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 /**\n * Optional FairPlay asset identifier used when generating the SPC.\n * Defaults to the `skd://` content identifier from the key request.\n */\n assetId?: 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 /**\n * Hide the currently presented player UI without stopping playback (native fullscreen).\n *\n * Call `showPlayer()` to show it again.\n */\n hidePlayer(): Promise<capVideoPlayerResult>;\n /**\n * Show again a previously hidden player UI (native fullscreen).\n */\n showPlayer(): Promise<capVideoPlayerResult>;\n /**\n * Get the native Capacitor plugin version.\n * On web, the implementation returns `\"web\"`.\n *\n * @returns Promise resolving to the plugin version string for this build (`\"web\"` on web)\n */\n getPluginVersion(): Promise<{ version: string }>;\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 * Optional override URL used when casting to Chromecast (iOS, Android).\n *\n * This lets you cast an alternative rendition/manifest (e.g. MPEG-DASH + Widevine)\n * while keeping the on-device playback URL unchanged (e.g. HLS + FairPlay on iOS).\n */\n chromecastUrl?: string;\n /**\n * The url of subtitle associated with the video\n *\n * Prefer `subtitles` when providing more than one track.\n * Kept for backward compatibility.\n */\n subtitle?: string;\n /**\n * The language of subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n *\n * Prefer `subtitles` when providing more than one track.\n * Kept for backward compatibility.\n */\n language?: string;\n /**\n * Multiple subtitle tracks (iOS, Android).\n * When provided, takes precedence over `subtitle` / `language`.\n */\n subtitles?: VideoSubtitle[];\n /**\n * Chapter markers for long-form content (iOS, Android).\n * Shown as a chapter list in the native fullscreen player; selecting a chapter seeks to `startTime`.\n */\n chapters?: VideoChapter[];\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 * iOS audio session category behavior (iOS).\n * - \"ambient\": respects the iPhone silent switch\n * - \"playback\": ignores the iPhone silent switch\n * - \"moviePlayback\": playback optimized for video and long-form AirPlay.\n *\n * When omitted, the plugin resolves the category from other options: `\"playback\"` if\n * `pipEnabled` or `bkmodeEnabled` is true (both default to true), otherwise `\"ambient\"`.\n */\n audioCategory?: 'ambient' | 'playback' | 'moviePlayback';\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 * When casting, Widevine's `certificateUrl` (license URL) is also forwarded to Chromecast\n * receivers as `media.customData.laurl`.\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 VideoSubtitle {\n /**\n * The url of the subtitle file (vtt, srt, ...)\n */\n subtitle: string;\n /**\n * The language of the subtitle\n * see https://github.com/libyal/libfwnt/wiki/Language-Code-identifiers\n */\n language?: string;\n}\nexport interface VideoChapter {\n /**\n * Chapter title shown in the chapter list\n */\n title: string;\n /**\n * Start time in seconds\n */\n startTime: number;\n /**\n * End time in seconds (optional). Stored for future timeline markers; chapter jumps use `startTime` only in this release.\n */\n endTime?: number;\n}\n/**\n * Optional styling for on-screen subtitles.\n *\n * On iOS, colors and font size are applied through `AVPlayerItem`\n * `textStyleRules`, which Apple documents for WebVTT only. IMSC1 and other\n * in-manifest timed text formats are not styled through this API.\n * Inline WebVTT cue styles can take precedence over equivalent\n * `subtitleOptions` values; the plugin sets text style rules as a platform\n * fallback, not a guaranteed override of authored cue styling.\n */\nexport interface SubTitleOptions {\n /**\n * Foreground color in RGBA. On Android, when the plugin applies subtitle\n * styling, omitted values fall back to `rgba(255,255,255,1)`. When\n * `subtitleOptions` is absent, embedded cue colors from the media can\n * override that fallback. On iOS, omitted colors are not set by the plugin;\n * the platform uses the viewer's caption appearance settings instead of fixed\n * plugin defaults.\n */\n foregroundColor?: string;\n /**\n * Background color in RGBA. On Android, when the plugin applies subtitle\n * styling in fullscreen, omitted values fall back to `rgba(0,0,0,1)`. In\n * picture-in-picture, omitted values fall back to transparent when the plugin\n * applies its caption style (for example sidecar subtitles or non-empty\n * `subtitleOptions` that include a color override). An empty\n * `subtitleOptions` object alone does not select that fallback. With\n * font-size-only options, embedded cue styles stay enabled, so authored cue\n * backgrounds can remain visible in PiP. When `subtitleOptions` is absent,\n * embedded cue colors from the media can override the fallback. On iOS,\n * omitted colors are not set by the plugin; the platform uses the viewer's\n * caption appearance settings instead of fixed plugin defaults.\n */\n backgroundColor?: string;\n /**\n * Font size in CSS pixels. On Android, supplied values are rounded to the\n * nearest integer before applying. When omitted on embedded tracks without a\n * sidecar subtitle URI, Android uses `SubtitleView.setUserDefaultTextSize()`\n * (the user's system caption size), not a fixed 16 px size. Sidecar subtitles\n * still use a fixed default size when this property is omitted. On iOS,\n * values map to AVFoundation relative size where 16 equals the system default\n * (100%).\n */\n fontSize?: number;\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 /**\n * Optional FairPlay asset identifier used when generating the SPC.\n * Defaults to the `skd://` content identifier from the key request.\n */\n assetId?: 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"]}
@@ -221,7 +221,20 @@ enum VideoPlayerCastSeekSync {
221
221
  }
222
222
 
223
223
  enum VideoPlayerCastOverlayLayout {
224
+ static let topRowOffset: CGFloat = 60
225
+ static let trailingInset: CGFloat = 16
226
+ static let controlSize: CGFloat = 44
227
+ static let verticalStackSpacing: CGFloat = 8
228
+
224
229
  static func overlayView(for playerViewController: AVPlayerViewController) -> UIView? {
225
230
  playerViewController.contentOverlayView
226
231
  }
232
+
233
+ /// Cast uses the top trailing slot; chapter stacks below when Cast overlay is enabled.
234
+ static func chapterButtonTopOffset(chromecastEnabled: Bool) -> CGFloat {
235
+ guard chromecastEnabled else {
236
+ return topRowOffset
237
+ }
238
+ return topRowOffset + controlSize + verticalStackSpacing
239
+ }
227
240
  }
@@ -42,6 +42,11 @@ class FullscreenVideoPlayer: NSObject {
42
42
  private var castController: VideoPlayerCastController?
43
43
  private weak var presentingViewController: UIViewController?
44
44
  private var subtitleTracks: [VideoSubtitleTrack] = []
45
+ private var subtitleStyleOptions: SubtitleStyleOptions?
46
+ private var chapters: [VideoChapter] = []
47
+ private var chapterButton: UIButton?
48
+ private let controlsVisibilityObserver = AVPlayerControlsVisibilityObserver()
49
+ private var arePlaybackControlsVisible = true
45
50
  private var hlsResourceLoader: HLSSubtitleResourceLoader?
46
51
  private var subtitleButton: UIButton?
47
52
  private var subtitleSelectionObserver: NSObjectProtocol?
@@ -64,6 +69,8 @@ class FullscreenVideoPlayer: NSObject {
64
69
  smallTitle: String? = nil,
65
70
  artwork: String? = nil,
66
71
  subtitleTracks: [VideoSubtitleTrack] = [],
72
+ subtitleStyleOptions: SubtitleStyleOptions? = nil,
73
+ chapters: [VideoChapter] = [],
67
74
  fairplayCertificateUrl: String? = nil,
68
75
  fairplayContentKeySpcUrl: String? = nil,
69
76
  fairplayAssetId: String? = nil,
@@ -84,6 +91,8 @@ class FullscreenVideoPlayer: NSObject {
84
91
  self.smallTitle = smallTitle
85
92
  self.artwork = artwork
86
93
  self.subtitleTracks = subtitleTracks
94
+ self.subtitleStyleOptions = subtitleStyleOptions
95
+ self.chapters = chapters
87
96
  self.fairplayCertificateUrl = fairplayCertificateUrl
88
97
  self.fairplayContentKeySpcUrl = fairplayContentKeySpcUrl
89
98
  self.fairplayAssetId = fairplayAssetId
@@ -217,6 +226,7 @@ class FullscreenVideoPlayer: NSObject {
217
226
  }
218
227
 
219
228
  private func configurePlayer(with item: AVPlayerItem) {
229
+ applySubtitleStyle(to: item)
220
230
  playerItem = item
221
231
  player = AVPlayer(playerItem: playerItem)
222
232
  player?.rate = rate
@@ -235,12 +245,70 @@ class FullscreenVideoPlayer: NSObject {
235
245
  }
236
246
  playerVC.adaptivePresentationDelegate = self
237
247
  playerViewController = playerVC
248
+ if showControls {
249
+ controlsVisibilityObserver.start(observing: playerVC) { [weak self] visible in
250
+ self?.arePlaybackControlsVisible = visible
251
+ self?.applyChapterButtonControlsVisibility()
252
+ }
253
+ }
238
254
 
239
255
  setupChromecast()
240
256
  setupObservers()
241
257
  setupSubtitleSelectionObserver()
242
258
  }
243
259
 
260
+ private func applySubtitleStyle(to item: AVPlayerItem) {
261
+ guard let subtitleStyleOptions else { return }
262
+
263
+ var attributes: [String: Any] = [:]
264
+ if let fontSize = subtitleStyleOptions.fontSize, fontSize.isFinite, fontSize > 0 {
265
+ let percent = fontSize / 16.0 * 100.0
266
+ attributes[kCMTextMarkupAttribute_RelativeFontSize as String] = NSNumber(value: percent)
267
+ }
268
+ if let foregroundColor = subtitleStyleOptions.foregroundColor,
269
+ let colorComponents = Self.parseRGBAComponents(foregroundColor) {
270
+ attributes[kCMTextMarkupAttribute_ForegroundColorARGB as String] = colorComponents
271
+ }
272
+ if let backgroundColor = subtitleStyleOptions.backgroundColor,
273
+ let colorComponents = Self.parseRGBAComponents(backgroundColor) {
274
+ attributes[kCMTextMarkupAttribute_BackgroundColorARGB as String] = colorComponents
275
+ }
276
+
277
+ guard !attributes.isEmpty,
278
+ let rule = AVTextStyleRule(textMarkupAttributes: attributes as [String: Any]) else {
279
+ return
280
+ }
281
+ item.textStyleRules = [rule]
282
+ }
283
+
284
+ private static func parseRGBAComponents(_ value: String) -> [NSNumber]? {
285
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
286
+ guard trimmed.hasPrefix("rgba("), trimmed.hasSuffix(")") else {
287
+ return nil
288
+ }
289
+
290
+ let inner = trimmed.dropFirst(5).dropLast()
291
+ let parts = inner.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
292
+ guard parts.count == 4,
293
+ let red = Double(parts[0]), red.isFinite,
294
+ let green = Double(parts[1]), green.isFinite,
295
+ let blue = Double(parts[2]), blue.isFinite,
296
+ let alpha = Double(parts[3]), alpha.isFinite,
297
+ (0...255).contains(red),
298
+ (0...255).contains(green),
299
+ (0...255).contains(blue),
300
+ (0...1).contains(alpha) else {
301
+ return nil
302
+ }
303
+
304
+ return [
305
+ NSNumber(value: alpha),
306
+ NSNumber(value: red / 255.0),
307
+ NSNumber(value: green / 255.0),
308
+ NSNumber(value: blue / 255.0)
309
+ ]
310
+ }
311
+
244
312
  private func setupSubtitleSelectionObserver() {
245
313
  guard subtitleSelectionObserver == nil, let playerItem else { return }
246
314
  subtitleSelectionObserver = NotificationCenter.default.addObserver(
@@ -274,6 +342,144 @@ class FullscreenVideoPlayer: NSObject {
274
342
  return nonHlsSubtitleModeEnabled && validNonHlsSubtitleTracks.count > 1 && shouldShowCustomSubtitleButton()
275
343
  }
276
344
 
345
+ private func shouldShowChapterButton() -> Bool {
346
+ guard showControls && !chapters.isEmpty else { return false }
347
+ if castController?.isCasting == true {
348
+ return true
349
+ }
350
+ return playerItem?.status == .readyToPlay
351
+ }
352
+
353
+ private func refreshChapterButton() {
354
+ DispatchQueue.main.async { [weak self] in
355
+ self?.installOrUpdateChapterButtonIfNeeded()
356
+ }
357
+ }
358
+
359
+ private func installOrUpdateChapterButtonIfNeeded() {
360
+ guard shouldShowChapterButton(),
361
+ let playerVC = playerViewController else {
362
+ chapterButton?.removeFromSuperview()
363
+ chapterButton = nil
364
+ return
365
+ }
366
+
367
+ if chapterButton == nil {
368
+ guard let overlayView = playerVC.contentOverlayView else {
369
+ return
370
+ }
371
+ let button = UIButton(type: .system)
372
+ button.translatesAutoresizingMaskIntoConstraints = false
373
+ if #available(iOS 13.0, *) {
374
+ button.setImage(UIImage(systemName: "list.bullet"), for: .normal)
375
+ } else {
376
+ button.setTitle("Ch", for: .normal)
377
+ }
378
+ button.tintColor = .white
379
+ button.backgroundColor = UIColor.black.withAlphaComponent(0.45)
380
+ button.layer.cornerRadius = 22
381
+ button.clipsToBounds = true
382
+ if #available(iOS 14.0, *) {
383
+ button.showsMenuAsPrimaryAction = true
384
+ } else {
385
+ button.addTarget(self, action: #selector(showChapterActionSheet), for: .touchUpInside)
386
+ }
387
+ overlayView.addSubview(button)
388
+ let layout = VideoPlayerCastOverlayLayout.self
389
+ NSLayoutConstraint.activate([
390
+ button.topAnchor.constraint(
391
+ equalTo: overlayView.safeAreaLayoutGuide.topAnchor,
392
+ constant: layout.chapterButtonTopOffset(chromecastEnabled: chromecast)
393
+ ),
394
+ button.trailingAnchor.constraint(
395
+ equalTo: overlayView.safeAreaLayoutGuide.trailingAnchor,
396
+ constant: -layout.trailingInset
397
+ ),
398
+ button.widthAnchor.constraint(equalToConstant: layout.controlSize),
399
+ button.heightAnchor.constraint(equalToConstant: layout.controlSize)
400
+ ])
401
+ chapterButton = button
402
+ }
403
+
404
+ if #available(iOS 14.0, *) {
405
+ chapterButton?.menu = buildChapterMenu()
406
+ }
407
+ chapterButton?.accessibilityLabel = "Chapters"
408
+ applyChapterButtonControlsVisibility()
409
+ }
410
+
411
+ private func applyChapterButtonControlsVisibility() {
412
+ chapterButton?.isHidden = !arePlaybackControlsVisible
413
+ chapterButton?.alpha = arePlaybackControlsVisible ? 1 : 0
414
+ }
415
+
416
+ @available(iOS 14.0, *)
417
+ private func buildChapterMenu() -> UIMenu {
418
+ let actions = chapters.map { chapter in
419
+ UIAction(title: Self.chapterDisplayName(chapter)) { [weak self] _ in
420
+ self?.seekToChapter(chapter)
421
+ }
422
+ }
423
+ return UIMenu(title: "Chapters", children: actions)
424
+ }
425
+
426
+ @objc
427
+ private func showChapterActionSheet() {
428
+ guard let playerVC = playerViewController, !chapters.isEmpty else {
429
+ return
430
+ }
431
+
432
+ let sheet = UIAlertController(title: "Chapters", message: nil, preferredStyle: .actionSheet)
433
+ for chapter in chapters {
434
+ sheet.addAction(UIAlertAction(title: Self.chapterDisplayName(chapter), style: .default) { [weak self] _ in
435
+ self?.seekToChapter(chapter)
436
+ })
437
+ }
438
+ sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
439
+ if let popover = sheet.popoverPresentationController,
440
+ let chapterButton {
441
+ popover.sourceView = chapterButton
442
+ popover.sourceRect = chapterButton.bounds
443
+ }
444
+ playerVC.present(sheet, animated: true)
445
+ }
446
+
447
+ private func seekToChapter(_ chapter: VideoChapter) {
448
+ if castController?.setCurrentTime(chapter.startTime) == true {
449
+ play()
450
+ return
451
+ }
452
+ let target = CMTime(seconds: chapter.startTime, preferredTimescale: 600)
453
+ player?.seek(to: target, toleranceBefore: .zero, toleranceAfter: .zero)
454
+ play()
455
+ }
456
+
457
+ private static func chapterDisplayName(_ chapter: VideoChapter) -> String {
458
+ let timeLabel = formatChapterTime(chapter.startTime)
459
+ let title = chapter.title.trimmingCharacters(in: .whitespacesAndNewlines)
460
+ if title.isEmpty {
461
+ return timeLabel
462
+ }
463
+ return "\(title) (\(timeLabel))"
464
+ }
465
+
466
+ private static func formatChapterTime(_ seconds: TimeInterval) -> String {
467
+ guard seconds.isFinite else {
468
+ return "0:00"
469
+ }
470
+ let clamped = min(max(0, seconds), Double(Int.max))
471
+ guard let totalSeconds = Int(exactly: clamped.rounded()) else {
472
+ return "0:00"
473
+ }
474
+ let hours = totalSeconds / 3600
475
+ let minutes = (totalSeconds % 3600) / 60
476
+ let secs = totalSeconds % 60
477
+ if hours > 0 {
478
+ return String(format: "%d:%02d:%02d", hours, minutes, secs)
479
+ }
480
+ return String(format: "%d:%02d", minutes, secs)
481
+ }
482
+
277
483
  private func refreshSubtitleButton() {
278
484
  DispatchQueue.main.async { [weak self] in
279
485
  self?.installOrUpdateSubtitleButtonIfNeeded()
@@ -575,6 +781,7 @@ class FullscreenVideoPlayer: NSObject {
575
781
 
576
782
  let oldItem = playerItem
577
783
  removePlayerItemObservers(oldItem)
784
+ applySubtitleStyle(to: newItem)
578
785
  playerItem = newItem
579
786
  player.replaceCurrentItem(with: newItem)
580
787
  addPlayerItemObservers(newItem)
@@ -642,6 +849,9 @@ class FullscreenVideoPlayer: NSObject {
642
849
  castController?.setOnEnd { [weak self] in
643
850
  self?.handlePlaybackEnded()
644
851
  }
852
+ castController?.setOnCastingStateChange { [weak self] in
853
+ self?.refreshChapterButton()
854
+ }
645
855
  castController?.attach(to: playerViewController, player: player)
646
856
  }
647
857
 
@@ -667,6 +877,7 @@ class FullscreenVideoPlayer: NSObject {
667
877
  if let item = object as? AVPlayerItem {
668
878
  if item.status == .readyToPlay {
669
879
  refreshSubtitleButton()
880
+ refreshChapterButton()
670
881
  emitReadyIfNeeded()
671
882
  }
672
883
  }
@@ -710,6 +921,7 @@ class FullscreenVideoPlayer: NSObject {
710
921
  presentingViewController = viewController
711
922
  viewController.present(playerVC, animated: true) {
712
923
  self.refreshSubtitleButton()
924
+ self.refreshChapterButton()
713
925
  self.castController?.installOverlayIfNeeded()
714
926
  self.play()
715
927
  completion()
@@ -757,6 +969,7 @@ class FullscreenVideoPlayer: NSObject {
757
969
  }
758
970
 
759
971
  private func cleanup(stopRemoteMedia: Bool = false) {
972
+ controlsVisibilityObserver.stop()
760
973
  nonHlsBootstrapLoadId = nil
761
974
  castController?.detach(stopRemoteMedia: stopRemoteMedia)
762
975
  castController = nil
@@ -766,6 +979,8 @@ class FullscreenVideoPlayer: NSObject {
766
979
  }
767
980
  subtitleButton?.removeFromSuperview()
768
981
  subtitleButton = nil
982
+ chapterButton?.removeFromSuperview()
983
+ chapterButton = nil
769
984
  contentKeySession?.setDelegate(nil, queue: nil)
770
985
  contentKeySession = nil
771
986
  hlsResourceLoader = nil
@@ -0,0 +1,7 @@
1
+ import Foundation
2
+
3
+ struct SubtitleStyleOptions {
4
+ let foregroundColor: String?
5
+ let backgroundColor: String?
6
+ let fontSize: Double?
7
+ }
@@ -0,0 +1,45 @@
1
+ import Foundation
2
+
3
+ struct VideoChapter {
4
+ let title: String
5
+ let startTime: TimeInterval
6
+ let endTime: TimeInterval?
7
+
8
+ init?(dictionary: [String: Any]) {
9
+ guard let title = dictionary["title"] as? String,
10
+ !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
11
+ return nil
12
+ }
13
+
14
+ let startTime: TimeInterval
15
+ if let number = dictionary["startTime"] as? NSNumber {
16
+ if CFGetTypeID(number) == CFBooleanGetTypeID() {
17
+ return nil
18
+ }
19
+ startTime = number.doubleValue
20
+ } else if let value = dictionary["startTime"] as? Double {
21
+ startTime = value
22
+ } else {
23
+ return nil
24
+ }
25
+
26
+ if !startTime.isFinite || startTime < 0 || startTime >= Double(Int.max) {
27
+ return nil
28
+ }
29
+
30
+ var endTime: TimeInterval?
31
+ if let number = dictionary["endTime"] as? NSNumber {
32
+ endTime = number.doubleValue
33
+ } else if let value = dictionary["endTime"] as? Double {
34
+ endTime = value
35
+ }
36
+
37
+ if let endTimeValue = endTime, endTimeValue <= startTime {
38
+ endTime = nil
39
+ }
40
+
41
+ self.title = title
42
+ self.startTime = startTime
43
+ self.endTime = endTime
44
+ }
45
+ }
@@ -29,7 +29,12 @@ final class VideoPlayerCastController: NSObject {
29
29
  private weak var observedRemoteMediaClient: GCKRemoteMediaClient?
30
30
  private var mediaLoadRequest: GCKRequest?
31
31
  private var pendingCastCommands: [PendingCastCommand] = []
32
- private var isLoadedOnCast = false
32
+ private var isLoadedOnCast = false {
33
+ didSet {
34
+ guard oldValue != isLoadedOnCast else { return }
35
+ onCastingStateChange?()
36
+ }
37
+ }
33
38
  private var isLoadingOnCast = false
34
39
  private var localWasPlaying = false
35
40
  private var isDetached = false
@@ -38,6 +43,7 @@ final class VideoPlayerCastController: NSObject {
38
43
  private var onPlay: (() -> Void)?
39
44
  private var onPause: (() -> Void)?
40
45
  private var onEnd: (() -> Void)?
46
+ private var onCastingStateChange: (() -> Void)?
41
47
  private let controlsVisibilityObserver = AVPlayerControlsVisibilityObserver()
42
48
  private var localSeekTimeObserver: Any?
43
49
  private var timeJumpObserver: NSObjectProtocol?
@@ -275,6 +281,10 @@ final class VideoPlayerCastController: NSObject {
275
281
  func setOnEnd(_ callback: @escaping () -> Void) {
276
282
  onEnd = callback
277
283
  }
284
+
285
+ func setOnCastingStateChange(_ callback: @escaping () -> Void) {
286
+ onCastingStateChange = callback
287
+ }
278
288
  }
279
289
 
280
290
  private extension VideoPlayerCastController {
@@ -364,13 +374,17 @@ private extension VideoPlayerCastController {
364
374
  button.accessibilityLabel = "Cast"
365
375
 
366
376
  overlayView.addSubview(button)
377
+ let layout = VideoPlayerCastOverlayLayout.self
367
378
  NSLayoutConstraint.activate([
368
- button.widthAnchor.constraint(equalToConstant: 44),
369
- button.heightAnchor.constraint(equalToConstant: 44),
379
+ button.widthAnchor.constraint(equalToConstant: layout.controlSize),
380
+ button.heightAnchor.constraint(equalToConstant: layout.controlSize),
370
381
  // Position below AVPlayerViewController's top row of controls (Done/X and route picker)
371
382
  // using trailing to stay away from the leading-side dismiss button.
372
- button.topAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.topAnchor, constant: 60),
373
- button.trailingAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.trailingAnchor, constant: -16)
383
+ button.topAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.topAnchor, constant: layout.topRowOffset),
384
+ button.trailingAnchor.constraint(
385
+ equalTo: overlayView.safeAreaLayoutGuide.trailingAnchor,
386
+ constant: -layout.trailingInset
387
+ )
374
388
  ])
375
389
 
376
390
  castButton = button
@@ -930,6 +944,10 @@ final class VideoPlayerCastController {
930
944
  func setOnEnd(_ callback: @escaping () -> Void) {
931
945
  _ = callback
932
946
  }
947
+
948
+ func setOnCastingStateChange(_ callback: @escaping () -> Void) {
949
+ _ = callback
950
+ }
933
951
  }
934
952
 
935
953
  #endif