ez-vid-ang 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,29 +57,61 @@ Add the required styles to your angular.json:
57
57
  > *eva-icons-and-fonts.scss* is optional if you provide custom icons and fonts for all components. It includes a prepared *.woff* file and utility classes for default icon usage.
58
58
  <br/>
59
59
 
60
- Import the needed modules into your standalone component or NgModule:
60
+ Import the needed components and types into your standalone component or NgModule:
61
61
  ```
62
62
  import { Component } from '@angular/core';
63
- import { EvaPlayer } from 'ez-vid-ang';
63
+ import {
64
+ EvaActiveChapter,
65
+ EvaBackward,
66
+ EvaBuffering,
67
+ EvaChapterMarker,
68
+ EvaOverlayPlay,
69
+ EvaControlsContainer, EvaControlsDivider,
70
+ EvaForward, EvaFullscreen, EvaHlsDirective,
71
+ EvaMute, EvaMuteAria, EvaPlaybackSpeed, EvaPlayer,
72
+ EvaPlayPause, EvaQualitySelector, EvaScrubBar,
73
+ EvaScrubBarBufferingTime, EvaScrubBarCurrentTime,
74
+ EvaSubtitleDisplay,EvaPictureInPicture,
75
+ EvaTimeDisplay, EvaTrack, EvaTrackSelector,
76
+ EvaVideoElementConfiguration, EvaVideoSource, EvaVolume
77
+ } from "ez-vid-ang";
64
78
 
65
79
  @Component({
66
80
  selector: 'lt-home-page',
67
81
  templateUrl: './home-page.html',
68
82
  styleUrl: './home-page.scss',
69
83
  imports: [
70
- EvaCoreModule,
71
- EvaControlsModule,
72
- EvaBufferingModule,
73
- EvaStreamingModule
84
+ EvaActiveChapter,
85
+ EvaBackward,
86
+ EvaBuffering,
87
+ EvaOverlayPlay,
88
+ EvaControlsContainer,
89
+ EvaControlsDivider,
90
+ EvaForward,
91
+ EvaFullscreen,
92
+ EvaHlsDirective,
93
+ EvaMute,
94
+ EvaPlaybackSpeed,
95
+ EvaPlayer,
96
+ EvaPlayPause,
97
+ EvaPictureInPicture,
98
+ EvaQualitySelector,
99
+ EvaScrubBar,
100
+ EvaScrubBarBufferingTime,
101
+ EvaScrubBarCurrentTime,
102
+ EvaSubtitleDisplay,
103
+ EvaTimeDisplay,
104
+ EvaTrackSelector,
105
+ EvaVolume
74
106
  ]
75
107
  })
76
108
  export class HomePage {}
77
109
 
78
110
  ```
79
111
 
80
- ## Modules
112
+ ## Components
81
113
 
82
- Library has four groups of componentse. You can click on the name to go to the documentation:
114
+ Library has four groups of components. You can click on the name to go to the documentation:
83
115
  - [**EvaCore**](documentation/core) – Main player component, directives, and providers
84
116
  - [**EvaControls**](documentation/controls) – Video control components and pipes
85
117
  - [**EvaBuffering**](documentation/buffering) – Loading and buffering indicators
@@ -1,6 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, signal, Injectable, inject, input, ChangeDetectionStrategy, Component, output, computed, Pipe, ElementRef, NgZone, HostListener, Renderer2, viewChild, effect, Directive, viewChildren } from '@angular/core';
2
+ import { EventEmitter, signal, Injectable, inject, input, ChangeDetectionStrategy, Component, output, computed, Pipe, ElementRef, NgZone, HostListener, Renderer2, viewChild, effect, Directive, SecurityContext, viewChildren } from '@angular/core';
3
3
  import { BehaviorSubject, Subject, fromEvent, throttleTime, merge, takeUntil } from 'rxjs';
4
+ import { DomSanitizer } from '@angular/platform-browser';
4
5
 
5
6
  /**
6
7
  * Enum of all possible playback states the Eva player can be in.
@@ -28,6 +29,8 @@ var EvaVideoEvent;
28
29
  EvaVideoEvent["CAN_PLAY_THROUGH"] = "canplaythrough";
29
30
  EvaVideoEvent["COMPLETE"] = "complete";
30
31
  EvaVideoEvent["DURATION_CHANGE"] = "durationchange";
32
+ EvaVideoEvent["ENTERED_PICTURE_IN_PICTURE"] = "enterpictureinpicture";
33
+ EvaVideoEvent["LEFT_PICTURE_IN_PICTURE"] = "leavepictureinpicture";
31
34
  EvaVideoEvent["EMPTIED"] = "emptied";
32
35
  EvaVideoEvent["ENCRYPTED"] = "encrypted";
33
36
  EvaVideoEvent["ENDED"] = "ended";
@@ -195,6 +198,14 @@ class EvaApi {
195
198
  */
196
199
  triggerUserInteraction = new Subject();
197
200
  currentSubtitleCue = signal(null, ...(ngDevMode ? [{ debugName: "currentSubtitleCue" }] : []));
201
+ /** The active `PictureInPictureWindow` instance. `null` when PiP is not active. */
202
+ pipWindow = null;
203
+ /**
204
+ * Broadcasts the current Picture-in-Picture state.
205
+ * Emits `true` when the player enters PiP, `false` when it leaves.
206
+ * Subscribed to by `EvaPictureInPicture` to keep its icon state in sync.
207
+ */
208
+ pictureInPictureSubject = new BehaviorSubject(false);
198
209
  // ─── Buffering Detection ──────────────────────────────────────────────────
199
210
  /** Timeout reference used by the position-polling buffering detection. Cleared on each `timeupdate`. */
200
211
  bufferingTimeout;
@@ -685,6 +696,64 @@ class EvaApi {
685
696
  this.isBuffering.set(true);
686
697
  }
687
698
  }
699
+ // ─── Picture in picture ────────────────────────────────────────────────────────────
700
+ /**
701
+ * Toggles Picture-in-Picture mode for the assigned video element.
702
+ *
703
+ * - If this player's video element is currently in PiP, exits PiP via
704
+ * `document.exitPictureInPicture()`.
705
+ * - If another element is currently in PiP, exits that first, then enters PiP
706
+ * on this player's video element.
707
+ * - If PiP is not active, enters PiP via `requestPictureInPicture()`.
708
+ *
709
+ * No-ops if:
710
+ * - The player is not yet ready.
711
+ * - `document.pictureInPictureEnabled` is `false` (API not supported or blocked).
712
+ * - `assignedVideoElement.disablePictureInPicture` is `true`.
713
+ *
714
+ * State is tracked via native `enterpictureinpicture` / `leavepictureinpicture`
715
+ * events, not by the Promise resolution, to correctly handle external PiP changes.
716
+ *
717
+ * @returns A `Promise<void>` that resolves when the PiP state change completes.
718
+ */
719
+ async changePictureInPictureStatus() {
720
+ if (!this.validateVideoAndPlayerBeforeAction()) {
721
+ return;
722
+ }
723
+ if (!document.pictureInPictureEnabled) {
724
+ console.warn('[EvaApi] Picture-in-Picture is not supported or is disabled in this browser.');
725
+ return;
726
+ }
727
+ if (this.assignedVideoElement.disablePictureInPicture) {
728
+ console.warn('[EvaApi] Picture-in-Picture is disabled on this video element.');
729
+ return;
730
+ }
731
+ try {
732
+ if (document.pictureInPictureElement === this.assignedVideoElement) {
733
+ // This player is already in PiP — exit
734
+ await document.exitPictureInPicture();
735
+ }
736
+ else {
737
+ // Another element may be in PiP — the browser handles exiting it automatically
738
+ // before entering PiP on a new element, but we exit explicitly for safety
739
+ if (document.pictureInPictureElement) {
740
+ await document.exitPictureInPicture();
741
+ }
742
+ await this.assignedVideoElement.requestPictureInPicture();
743
+ }
744
+ }
745
+ catch (error) {
746
+ console.error('[EvaApi] Picture-in-Picture toggle failed:', error);
747
+ }
748
+ }
749
+ assignPictureInPictureWindow(p) {
750
+ this.pipWindow = p.pictureInPictureWindow;
751
+ this.pictureInPictureSubject.next(true);
752
+ }
753
+ removePictureInPictureWindow(_p) {
754
+ this.pipWindow = null;
755
+ this.pictureInPictureSubject.next(false);
756
+ }
688
757
  // ─── Utilities ────────────────────────────────────────────────────────────
689
758
  /**
690
759
  * Returns whether the current source is a live stream.
@@ -784,9 +853,13 @@ class EvaApi {
784
853
  if (this.trackTimeout) {
785
854
  clearTimeout(this.trackTimeout);
786
855
  }
856
+ if (this.pipWindow) {
857
+ this.pipWindow = null;
858
+ }
787
859
  // Clear the registered streaming quality function
788
860
  this.qualityFn = null;
789
861
  // Complete all subjects — notifies subscribers and prevents further emissions
862
+ this.pictureInPictureSubject.complete();
790
863
  this.videoStateSubject.complete();
791
864
  this.videoVolumeSubject.complete();
792
865
  this.playbackRateSubject.complete();
@@ -961,6 +1034,24 @@ function transformEvaPlayPauseAria(v) {
961
1034
  }
962
1035
  };
963
1036
  }
1037
+ function transformEvaPictureInPictureAria(v) {
1038
+ if (!v) {
1039
+ return {
1040
+ ariaLabel: "Picture in picture",
1041
+ ariaValueText: {
1042
+ ariaLabelActivated: "Picture in picture is active",
1043
+ ariaLabelDeactivated: "Picture in picture is invactive",
1044
+ }
1045
+ };
1046
+ }
1047
+ return {
1048
+ ariaLabel: v.ariaLabel ? v.ariaLabel : "Picture in picture",
1049
+ ariaValueText: {
1050
+ ariaLabelActivated: v.ariaValueText && v.ariaValueText.ariaLabelActivated ? v.ariaValueText.ariaLabelActivated : "Picture in picture is active",
1051
+ ariaLabelDeactivated: v.ariaValueText && v.ariaValueText.ariaLabelDeactivated ? v.ariaValueText.ariaLabelDeactivated : "Picture in picture is invactive",
1052
+ }
1053
+ };
1054
+ }
964
1055
  function validateAndTransformEvaForwardAndBackwardSeconds(v) {
965
1056
  if (!v) {
966
1057
  return 10;
@@ -2398,6 +2489,135 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
2398
2489
  }, template: "<ng-content></ng-content>", styles: [":host{display:flex;align-items:center;justify-content:center;width:100%;height:calc(100% - var(--eva-control-element-height));cursor:pointer;color:var(--eva-icon-color);user-select:none;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;position:absolute;top:0;left:0;transition:visibility var(--eva-transition-duration) linear,background-color var(--eva-transition-duration) linear,opacity var(--eva-transition-duration) linear;background-color:#0000;z-index:301;visibility:hidden;opacity:0}:host(.eva-display-overlay-play){background-color:var(--eva-overlay-play-background-color)!important;opacity:1!important;visibility:visible!important}\n"] }]
2399
2490
  }], propDecorators: { evaOvelayPlayAria: [{ type: i0.Input, args: [{ isSignal: true, alias: "evaOvelayPlayAria", required: false }] }], evaCustomIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "evaCustomIcon", required: false }] }] } });
2400
2491
 
2492
+ /**
2493
+ * Picture-in-Picture toggle button for the Eva video player.
2494
+ *
2495
+ * Subscribes to `EvaApi.pictureInPictureSubject` to track whether the player is
2496
+ * currently in PiP mode and updates its icon and ARIA attributes accordingly.
2497
+ * Delegates the actual PiP toggle to `EvaApi.changePictureInPictureStatus()`, which
2498
+ * handles browser support checks, the `disablePictureInPicture` guard, and correctly
2499
+ * exiting another element's PiP session before entering a new one.
2500
+ *
2501
+ * State is kept in sync with native browser events (`enterpictureinpicture` /
2502
+ * `leavepictureinpicture`) registered by `EvaApi`, so the button correctly reflects
2503
+ * PiP changes triggered externally (e.g. by the browser's native controls or another
2504
+ * player instance).
2505
+ *
2506
+ * Keyboard support:
2507
+ * - `Enter` (13) and `Space` (32) trigger the same action as a click.
2508
+ *
2509
+ * @example
2510
+ * // Minimal usage
2511
+ * <eva-picture-in-picture />
2512
+ *
2513
+ * @example
2514
+ * // With custom ARIA labels
2515
+ * <eva-picture-in-picture
2516
+ * [evaAria]="{
2517
+ * ariaLabel: 'Picture in picture',
2518
+ * ariaValueText: {
2519
+ * ariaLabelActivated: 'Exit picture-in-picture',
2520
+ * ariaLabelDeactivated: 'Enter picture-in-picture'
2521
+ * }
2522
+ * }"
2523
+ * />
2524
+ *
2525
+ * @example
2526
+ * // With a custom icon
2527
+ * <eva-picture-in-picture [evaCustomIcon]="true">
2528
+ * <my-pip-icon />
2529
+ * </eva-picture-in-picture>
2530
+ */
2531
+ class EvaPictureInPicture {
2532
+ evaApi = inject(EvaApi);
2533
+ /**
2534
+ * When `true`, suppresses all built-in icon classes so you can project a
2535
+ * custom icon via content projection.
2536
+ *
2537
+ * @default false
2538
+ */
2539
+ evaCustomIcon = input(false, ...(ngDevMode ? [{ debugName: "evaCustomIcon" }] : []));
2540
+ /**
2541
+ * ARIA configuration for the PiP button.
2542
+ *
2543
+ * All properties are optional — defaults are applied via `transformEvaPictureInPictureAria`.
2544
+ * - `ariaLabel` — static label for the button element.
2545
+ * - `ariaValueText.ariaLabelActivated` — `aria-valuetext` when PiP is active.
2546
+ * - `ariaValueText.ariaLabelDeactivated` — `aria-valuetext` when PiP is inactive.
2547
+ */
2548
+ evaAria = input(transformEvaPictureInPictureAria(undefined), { ...(ngDevMode ? { debugName: "evaAria" } : {}), transform: transformEvaPictureInPictureAria });
2549
+ /**
2550
+ * Whether this player's video element is currently in Picture-in-Picture mode.
2551
+ * Updated by subscribing to `EvaApi.pictureInPictureSubject`.
2552
+ */
2553
+ isPictureInPictureActive = signal(false, ...(ngDevMode ? [{ debugName: "isPictureInPictureActive" }] : []));
2554
+ /**
2555
+ * Static `aria-label` for the host button element.
2556
+ * Sourced from `evaAria().ariaLabel` — does not change with PiP state.
2557
+ */
2558
+ ariaLabel = computed(() => {
2559
+ return this.evaAria().ariaLabel;
2560
+ }, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
2561
+ /**
2562
+ * Dynamic `aria-valuetext` for the host element.
2563
+ * Switches between `ariaValueText.ariaLabelActivated` and
2564
+ * `ariaValueText.ariaLabelDeactivated` based on the current PiP state,
2565
+ * giving screen readers a meaningful description of the current button action.
2566
+ */
2567
+ ariaValueText = computed(() => {
2568
+ return this.isPictureInPictureActive()
2569
+ ? this.evaAria().ariaValueText.ariaLabelActivated
2570
+ : this.evaAria().ariaValueText.ariaLabelDeactivated;
2571
+ }, ...(ngDevMode ? [{ debugName: "ariaValueText" }] : []));
2572
+ /** Subscription to `EvaApi.pictureInPictureSubject`. Cleaned up in `ngOnDestroy`. */
2573
+ pip$ = null;
2574
+ /**
2575
+ * Subscribes to `EvaApi.pictureInPictureSubject` to keep `isPictureInPictureActive`
2576
+ * in sync with the native PiP state, including changes triggered externally by the browser.
2577
+ */
2578
+ ngOnInit() {
2579
+ this.pip$ = this.evaApi.pictureInPictureSubject.subscribe((isActive) => {
2580
+ this.isPictureInPictureActive.set(isActive);
2581
+ });
2582
+ }
2583
+ /** Unsubscribes from `EvaApi.pictureInPictureSubject` to prevent memory leaks. */
2584
+ ngOnDestroy() {
2585
+ this.pip$?.unsubscribe();
2586
+ }
2587
+ /**
2588
+ * Delegates the PiP toggle to `EvaApi.changePictureInPictureStatus()`.
2589
+ * Called on host click and from `pipClickedKeyboard`.
2590
+ */
2591
+ pipClicked() {
2592
+ this.evaApi.changePictureInPictureStatus();
2593
+ }
2594
+ /**
2595
+ * Handles keyboard events on the host element.
2596
+ * Triggers `pipClicked()` on `Enter` (13) or `Space` (32) keypress.
2597
+ *
2598
+ * @param k - The native `KeyboardEvent` from the host element.
2599
+ */
2600
+ pipClickedKeyboard(k) {
2601
+ if (k.keyCode === 13 || k.keyCode === 32) {
2602
+ k.preventDefault();
2603
+ this.pipClicked();
2604
+ }
2605
+ }
2606
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EvaPictureInPicture, deps: [], target: i0.ɵɵFactoryTarget.Component });
2607
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: EvaPictureInPicture, isStandalone: true, selector: "eva-picture-in-picture", inputs: { evaCustomIcon: { classPropertyName: "evaCustomIcon", publicName: "evaCustomIcon", isSignal: true, isRequired: false, transformFunction: null }, evaAria: { classPropertyName: "evaAria", publicName: "evaAria", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "tabindex": "0", "role": "button" }, listeners: { "click": "pipClicked()", "keydown": "pipClickedKeyboard($event)" }, properties: { "attr.aria-label": "ariaLabel()", "attr.aria-valuetext": "ariaValueText()" } }, ngImport: i0, template: "@if(evaCustomIcon()){\n<ng-content></ng-content>\n}\n@else {\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"var(--eva-icon-color)\"\n\tstyle=\"width: 50%; height: 50%\">\n\t<path\n\t\td=\"M19,11H11V17H19V11M23,19V5C23,3.88 22.1,3 21,3H3A2,2 0 0,0 1,5V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19M21,19H3V4.97H21V19Z\" />\n</svg>\n}", styles: [":host{display:flex;justify-content:center;align-items:center;height:var(--eva-control-element-height);width:50px;cursor:pointer;color:var(--eva-icon-color);user-select:none;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}:host>svg{pointer-events:none!important;user-select:none!important;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2608
+ }
2609
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EvaPictureInPicture, decorators: [{
2610
+ type: Component,
2611
+ args: [{ selector: 'eva-picture-in-picture', changeDetection: ChangeDetectionStrategy.OnPush, host: {
2612
+ "tabindex": "0",
2613
+ "role": "button",
2614
+ "[attr.aria-label]": "ariaLabel()",
2615
+ "[attr.aria-valuetext]": "ariaValueText()",
2616
+ "(click)": "pipClicked()",
2617
+ "(keydown)": "pipClickedKeyboard($event)"
2618
+ }, template: "@if(evaCustomIcon()){\n<ng-content></ng-content>\n}\n@else {\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"var(--eva-icon-color)\"\n\tstyle=\"width: 50%; height: 50%\">\n\t<path\n\t\td=\"M19,11H11V17H19V11M23,19V5C23,3.88 22.1,3 21,3H3A2,2 0 0,0 1,5V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19M21,19H3V4.97H21V19Z\" />\n</svg>\n}", styles: [":host{display:flex;justify-content:center;align-items:center;height:var(--eva-control-element-height);width:50px;cursor:pointer;color:var(--eva-icon-color);user-select:none;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}:host>svg{pointer-events:none!important;user-select:none!important;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}\n"] }]
2619
+ }], propDecorators: { evaCustomIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "evaCustomIcon", required: false }] }], evaAria: [{ type: i0.Input, args: [{ isSignal: true, alias: "evaAria", required: false }] }] } });
2620
+
2401
2621
  /**
2402
2622
  * Pure pipe that formats a time value in seconds into a display string
2403
2623
  * for use in Eva video player time displays.
@@ -3877,18 +4097,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
3877
4097
  *
3878
4098
  * Renders the currently active subtitle cue sourced directly from
3879
4099
  * `EvaApi.currentSubtitleCue`. The component is visible only when a cue is active
3880
- * and automatically adjusts its bottom position based on whether the controls
3881
- * container is visible, ensuring subtitles never overlap the controls bar.
4100
+ * AND the player is not in Picture-in-Picture mode. It automatically adjusts its
4101
+ * bottom position based on whether the controls container is visible, ensuring
4102
+ * subtitles never overlap the controls bar.
3882
4103
  *
3883
4104
  * Visibility and positioning are driven entirely by host class and style bindings:
3884
- * - `eva-subtitle-display--visible` — applied when `currentSubtitleCue` is non-null.
4105
+ * - `eva-subtitle-display--visible` — applied when `currentSubtitleCue` is non-null
4106
+ * AND `pipWindowActive` is `false`. When PiP is active, this class is suppressed
4107
+ * because `EvaApi.setupPipListeners()` switches the active `TextTrack` to
4108
+ * `mode="showing"`, handing subtitle rendering off to the browser's native PiP UI.
3885
4109
  * - `padding-bottom` — switches between a minimal offset (`8px`) when controls are
3886
- * hidden and a calculated offset accounting for `--eva-control-element-height`,
3887
- * `--eva-scrub-bar-heights`, and a `12px` gap when controls are visible.
4110
+ * hidden and a calculated offset (`--eva-control-element-height + --eva-scrub-bar-heights + 12px`)
4111
+ * when controls are visible.
3888
4112
  *
3889
4113
  * The `cue` signal is read directly from `EvaApi.currentSubtitleCue`, which is
3890
4114
  * updated by `EvaCueChangeDirective` on each native `cuechange` event. No additional
3891
- * subscription or polling is required.
4115
+ * subscription is required for the cue text itself.
3892
4116
  *
3893
4117
  * @example
3894
4118
  * // Place inside eva-player to render active subtitle cues
@@ -3911,32 +4135,48 @@ class EvaSubtitleDisplay {
3911
4135
  * Updated by subscribing to `EvaApi.componentsContainerVisibilityStateSubject`.
3912
4136
  */
3913
4137
  controlsCointainerNotVisible = signal(false, ...(ngDevMode ? [{ debugName: "controlsCointainerNotVisible" }] : []));
4138
+ /**
4139
+ * Whether the player is currently in Picture-in-Picture mode.
4140
+ * When `true`, the `eva-subtitle-display--visible` class is suppressed so the
4141
+ * Angular subtitle overlay is hidden. The browser takes over subtitle rendering
4142
+ * inside the PiP window via the active `TextTrack` (mode switched to `"showing"`
4143
+ * by `EvaApi.setupPipListeners()`).
4144
+ * Updated by subscribing to `EvaApi.pictureInPictureSubject`.
4145
+ */
4146
+ pipWindowActive = signal(false, ...(ngDevMode ? [{ debugName: "pipWindowActive" }] : []));
3914
4147
  /** Subscription to controls container visibility changes. Cleaned up in `ngOnDestroy`. */
3915
- controlsVisibiliti$ = null;
4148
+ controlsVisibility$ = null;
4149
+ /** Subscription to Picture-in-Picture state changes. Cleaned up in `ngOnDestroy`. */
4150
+ pipWindowActive$ = null;
3916
4151
  /**
3917
- * Subscribes to `EvaApi.componentsContainerVisibilityStateSubject` to track
3918
- * whether the controls container is currently visible, so the subtitle
3919
- * position can adjust accordingly.
4152
+ * Subscribes to:
4153
+ * - `EvaApi.componentsContainerVisibilityStateSubject` to adjust `padding-bottom`
4154
+ * based on controls bar visibility.
4155
+ * - `EvaApi.pictureInPictureSubject` — to suppress the subtitle overlay when PiP is active.
3920
4156
  */
3921
4157
  ngOnInit() {
3922
- this.controlsVisibiliti$ = this.evaAPI.componentsContainerVisibilityStateSubject.subscribe((a) => {
4158
+ this.controlsVisibility$ = this.evaAPI.componentsContainerVisibilityStateSubject.subscribe((a) => {
3923
4159
  this.controlsCointainerNotVisible.set(a);
3924
4160
  });
4161
+ this.pipWindowActive$ = this.evaAPI.pictureInPictureSubject.subscribe((a) => {
4162
+ this.pipWindowActive.set(a);
4163
+ });
3925
4164
  }
3926
- /** Unsubscribes from the controls visibility subscription to prevent memory leaks. */
4165
+ /** Unsubscribes from all active subscriptions to prevent memory leaks. */
3927
4166
  ngOnDestroy() {
3928
- this.controlsVisibiliti$?.unsubscribe();
4167
+ this.controlsVisibility$?.unsubscribe();
4168
+ this.pipWindowActive$?.unsubscribe();
3929
4169
  }
3930
4170
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EvaSubtitleDisplay, deps: [], target: i0.ɵɵFactoryTarget.Component });
3931
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: EvaSubtitleDisplay, isStandalone: true, selector: "eva-subtitle-display", host: { properties: { "class.eva-subtitle-display": "true", "class.eva-subtitle-display--visible": "cue() !== null", "style.padding-bottom": "controlsCointainerNotVisible() ? '8px' : 'calc(var(--eva-control-element-height) + var(--eva-scrub-bar-heights) + 12px)'" } }, ngImport: i0, template: "@if (cue()) {\n<span class=\"eva-subtitle-cue\" [innerHTML]=\"cue()\"></span>\n}", styles: [":host{display:block;position:absolute;inset-inline:0;bottom:0;pointer-events:none;text-align:center;z-index:10;transition:padding-bottom var(--eva-transition-duration) ease-in-out}.eva-subtitle-cue{display:inline-block;font-size:var(--eva-subtitle-font-size);line-height:1.4;font-family:var(--eva-subtitle-font-family);color:var(--eva-subtitle-color, #ffffff);background-color:var(--eva-subtitle-background, rgba(0, 0, 0, .72));padding:var(--eva-subtitle-padding);border-radius:3px;white-space:pre-line}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4171
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: EvaSubtitleDisplay, isStandalone: true, selector: "eva-subtitle-display", host: { properties: { "class.eva-subtitle-display--visible": "cue() !== null && !pipWindowActive()", "style.padding-bottom": "controlsCointainerNotVisible() ? '8px' : 'calc(var(--eva-control-element-height) + var(--eva-scrub-bar-heights) + 12px)'" } }, ngImport: i0, template: "@if (cue() && !pipWindowActive()) {\n<span class=\"eva-subtitle-cue\" [innerHTML]=\"cue()\"></span>\n}", styles: [":host{display:none;visibility:hidden;position:absolute;inset-inline:0;bottom:0;pointer-events:none;text-align:center;z-index:10;transition:padding-bottom var(--eva-transition-duration) ease-in-out}:host(.eva-subtitle-display--visible){display:block!important;visibility:visible!important}.eva-subtitle-cue{display:inline-block;font-size:var(--eva-subtitle-font-size);line-height:1.4;font-family:var(--eva-subtitle-font-family);color:var(--eva-subtitle-color, #ffffff);background-color:var(--eva-subtitle-background, rgba(0, 0, 0, .72));padding:var(--eva-subtitle-padding);border-radius:3px;white-space:pre-line}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3932
4172
  }
3933
4173
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EvaSubtitleDisplay, decorators: [{
3934
4174
  type: Component,
3935
4175
  args: [{ selector: "eva-subtitle-display", changeDetection: ChangeDetectionStrategy.OnPush, host: {
3936
- "[class.eva-subtitle-display]": "true",
3937
- "[class.eva-subtitle-display--visible]": "cue() !== null",
4176
+ // Suppressed during PiP — the browser renders subtitles natively inside the PiP window
4177
+ "[class.eva-subtitle-display--visible]": "cue() !== null && !pipWindowActive()",
3938
4178
  "[style.padding-bottom]": "controlsCointainerNotVisible() ? '8px' : 'calc(var(--eva-control-element-height) + var(--eva-scrub-bar-heights) + 12px)'"
3939
- }, template: "@if (cue()) {\n<span class=\"eva-subtitle-cue\" [innerHTML]=\"cue()\"></span>\n}", styles: [":host{display:block;position:absolute;inset-inline:0;bottom:0;pointer-events:none;text-align:center;z-index:10;transition:padding-bottom var(--eva-transition-duration) ease-in-out}.eva-subtitle-cue{display:inline-block;font-size:var(--eva-subtitle-font-size);line-height:1.4;font-family:var(--eva-subtitle-font-family);color:var(--eva-subtitle-color, #ffffff);background-color:var(--eva-subtitle-background, rgba(0, 0, 0, .72));padding:var(--eva-subtitle-padding);border-radius:3px;white-space:pre-line}\n"] }]
4179
+ }, template: "@if (cue() && !pipWindowActive()) {\n<span class=\"eva-subtitle-cue\" [innerHTML]=\"cue()\"></span>\n}", styles: [":host{display:none;visibility:hidden;position:absolute;inset-inline:0;bottom:0;pointer-events:none;text-align:center;z-index:10;transition:padding-bottom var(--eva-transition-duration) ease-in-out}:host(.eva-subtitle-display--visible){display:block!important;visibility:visible!important}.eva-subtitle-cue{display:inline-block;font-size:var(--eva-subtitle-font-size);line-height:1.4;font-family:var(--eva-subtitle-font-family);color:var(--eva-subtitle-color, #ffffff);background-color:var(--eva-subtitle-background, rgba(0, 0, 0, .72));padding:var(--eva-subtitle-padding);border-radius:3px;white-space:pre-line}\n"] }]
3940
4180
  }] });
3941
4181
 
3942
4182
  /**
@@ -4958,6 +5198,8 @@ class EvaMediaEventListenersDirective {
4958
5198
  canPlayThrough$ = null;
4959
5199
  complete$ = null;
4960
5200
  durationChange$ = null;
5201
+ enteredPiP$ = null;
5202
+ leftPiP$ = null;
4961
5203
  emptied$ = null;
4962
5204
  encrypted$ = null;
4963
5205
  ended$ = null;
@@ -4987,6 +5229,8 @@ class EvaMediaEventListenersDirective {
4987
5229
  completeSub = null;
4988
5230
  durationChangeSub = null;
4989
5231
  emptiedSub = null;
5232
+ enteredPipSub = null;
5233
+ leftPipSub = null;
4990
5234
  encryptedSub = null;
4991
5235
  endedSub = null;
4992
5236
  errorSub = null;
@@ -5017,6 +5261,9 @@ class EvaMediaEventListenersDirective {
5017
5261
  this.complete$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.COMPLETE);
5018
5262
  this.durationChange$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.DURATION_CHANGE);
5019
5263
  this.emptied$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.EMPTIED);
5264
+ this.enteredPiP$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.ENTERED_PICTURE_IN_PICTURE);
5265
+ this.leftPiP$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.LEFT_PICTURE_IN_PICTURE);
5266
+ this.emptied$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.EMPTIED);
5020
5267
  this.encrypted$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.ENCRYPTED);
5021
5268
  this.ended$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.ENDED);
5022
5269
  this.error$ = fromEvent(this.elementRef.nativeElement, EvaVideoEvent.ERROR);
@@ -5059,6 +5306,14 @@ class EvaMediaEventListenersDirective {
5059
5306
  /** Stub — no `EvaApi` side effect yet. */
5060
5307
  this.emptiedSub = this.emptied$.subscribe(() => {
5061
5308
  });
5309
+ this.enteredPipSub = this.enteredPiP$.subscribe((pip) => {
5310
+ this.evaAPI.assignPictureInPictureWindow(pip);
5311
+ });
5312
+ this.leftPipSub = this.leftPiP$.subscribe((pip) => {
5313
+ this.evaAPI.removePictureInPictureWindow(pip);
5314
+ });
5315
+ this.emptiedSub = this.emptied$.subscribe(() => {
5316
+ });
5062
5317
  /** Stub — no `EvaApi` side effect yet. */
5063
5318
  this.encryptedSub = this.encrypted$.subscribe(() => {
5064
5319
  });
@@ -5156,6 +5411,8 @@ class EvaMediaEventListenersDirective {
5156
5411
  this.completeSub?.unsubscribe();
5157
5412
  this.durationChangeSub?.unsubscribe();
5158
5413
  this.emptiedSub?.unsubscribe();
5414
+ this.enteredPipSub?.unsubscribe();
5415
+ this.leftPipSub?.unsubscribe();
5159
5416
  this.encryptedSub?.unsubscribe();
5160
5417
  this.endedSub?.unsubscribe();
5161
5418
  this.errorSub?.unsubscribe();
@@ -5298,6 +5555,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
5298
5555
  * `startingVolume`, which is validated and clamped via `validateAndPrepareStartingVideoVolume`
5299
5556
  * before being assigned.
5300
5557
  *
5558
+ * Sanitization strategy per property type:
5559
+ * - `poster` — sanitized with `SecurityContext.URL` as it is rendered as a resource URL.
5560
+ * - `width` / `height` / `startingVolume` — assigned as typed numbers; no sanitization needed.
5561
+ * - `autoplay` / `controls` / `loop` / `muted` / `playinline` /
5562
+ * `disablePictureInPicture` / `disableRemotePlayback` — assigned as typed booleans; no sanitization needed.
5563
+ * - `crossorigin` / `preload` — constrained enum strings assigned directly to typed DOM properties; no sanitization needed.
5564
+ *
5301
5565
  * Supported configuration properties (all optional):
5302
5566
  * - `width` / `height` — dimensions of the video element
5303
5567
  * - `autoplay` — start playback automatically
@@ -5321,6 +5585,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
5321
5585
  class EvaVideoConfigurationDirective {
5322
5586
  evaAPI = inject(EvaApi);
5323
5587
  elementRef = inject((ElementRef));
5588
+ sanitizer = inject(DomSanitizer);
5324
5589
  /**
5325
5590
  * The configuration object to apply to the native `<video>` element.
5326
5591
  *
@@ -5341,7 +5606,6 @@ class EvaVideoConfigurationDirective {
5341
5606
  * @param changes - The `SimpleChanges` map provided by Angular.
5342
5607
  */
5343
5608
  ngOnChanges(changes) {
5344
- // Only apply changes if view is initialized and videoConfig changed
5345
5609
  if (this.isViewInitialized && changes['videoConfig']) {
5346
5610
  this.applyConfiguration();
5347
5611
  }
@@ -5358,6 +5622,11 @@ class EvaVideoConfigurationDirective {
5358
5622
  * Iterates over all supported `EvaVideoElementConfiguration` properties and
5359
5623
  * applies each one directly to the native `<video>` element if the value is truthy.
5360
5624
  *
5625
+ * Sanitization is applied only where values are rendered as resource URLs:
5626
+ * - `poster` is sanitized with `SecurityContext.URL`.
5627
+ * - All other properties are typed DOM assignments (boolean, number, or constrained
5628
+ * enum string) and do not require sanitization.
5629
+ *
5361
5630
  * `startingVolume` is passed through `validateAndPrepareStartingVideoVolume`
5362
5631
  * before assignment to ensure it is clamped to a valid `[0, 1]` range.
5363
5632
  *
@@ -5367,44 +5636,53 @@ class EvaVideoConfigurationDirective {
5367
5636
  if (!this.evaVideoConfig()) {
5368
5637
  return;
5369
5638
  }
5370
- if (this.evaVideoConfig().width) {
5371
- this.elementRef.nativeElement.width = this.evaVideoConfig().width;
5639
+ const config = this.evaVideoConfig();
5640
+ // Numeric properties — assigned as typed numbers, no sanitization needed
5641
+ if (config.width) {
5642
+ this.elementRef.nativeElement.width = config.width;
5372
5643
  }
5373
- if (this.evaVideoConfig().height) {
5374
- this.elementRef.nativeElement.height = this.evaVideoConfig().height;
5644
+ if (config.height) {
5645
+ this.elementRef.nativeElement.height = config.height;
5375
5646
  }
5376
- if (this.evaVideoConfig().autoplay) {
5377
- this.elementRef.nativeElement.autoplay = this.evaVideoConfig().autoplay;
5647
+ // Boolean properties — assigned as typed booleans, no sanitization needed
5648
+ if (config.autoplay) {
5649
+ this.elementRef.nativeElement.autoplay = config.autoplay;
5378
5650
  }
5379
- if (this.evaVideoConfig().controls) {
5380
- this.elementRef.nativeElement.controls = this.evaVideoConfig().controls;
5651
+ if (config.controls) {
5652
+ this.elementRef.nativeElement.controls = config.controls;
5381
5653
  }
5382
- if (this.evaVideoConfig().crossorigin) {
5383
- this.elementRef.nativeElement.crossOrigin = this.evaVideoConfig().crossorigin;
5654
+ if (config.disablePictureInPicture) {
5655
+ this.elementRef.nativeElement.disablePictureInPicture = config.disablePictureInPicture;
5384
5656
  }
5385
- if (this.evaVideoConfig().disablePictureInPicture) {
5386
- this.elementRef.nativeElement.disablePictureInPicture = this.evaVideoConfig().disablePictureInPicture;
5657
+ if (config.disableRemotePlayback) {
5658
+ this.elementRef.nativeElement.disableRemotePlayback = config.disableRemotePlayback;
5387
5659
  }
5388
- if (this.evaVideoConfig().disableRemotePlayback) {
5389
- this.elementRef.nativeElement.disableRemotePlayback = this.evaVideoConfig().disableRemotePlayback;
5660
+ if (config.loop) {
5661
+ this.elementRef.nativeElement.loop = config.loop;
5390
5662
  }
5391
- if (this.evaVideoConfig().loop) {
5392
- this.elementRef.nativeElement.loop = this.evaVideoConfig().loop;
5663
+ if (config.muted) {
5664
+ this.elementRef.nativeElement.muted = config.muted;
5393
5665
  }
5394
- if (this.evaVideoConfig().muted) {
5395
- this.elementRef.nativeElement.muted = this.evaVideoConfig().muted;
5666
+ if (config.playinline) {
5667
+ this.elementRef.nativeElement.playsInline = config.playinline;
5396
5668
  }
5397
- if (this.evaVideoConfig().playinline) {
5398
- this.elementRef.nativeElement.playsInline = this.evaVideoConfig().playinline;
5669
+ // Constrained enum strings — typed DOM properties with a fixed set of valid values,
5670
+ // no sanitization needed
5671
+ if (config.crossorigin) {
5672
+ this.elementRef.nativeElement.crossOrigin = config.crossorigin;
5399
5673
  }
5400
- if (this.evaVideoConfig().poster) {
5401
- this.elementRef.nativeElement.poster = this.evaVideoConfig().poster;
5674
+ if (config.preload) {
5675
+ this.elementRef.nativeElement.preload = config.preload;
5402
5676
  }
5403
- if (this.evaVideoConfig().preload) {
5404
- this.elementRef.nativeElement.preload = this.evaVideoConfig().preload;
5677
+ // URL property — sanitized with SecurityContext.URL as the browser fetches this
5678
+ // as a resource and it is reflected in the DOM as an attribute
5679
+ if (config.poster) {
5680
+ this.elementRef.nativeElement.poster = this.sanitizer.sanitize(SecurityContext.URL, config.poster) ?? '';
5405
5681
  }
5406
- if (this.evaVideoConfig().startingVolume) {
5407
- this.elementRef.nativeElement.volume = validateAndPrepareStartingVideoVolume(this.evaVideoConfig().startingVolume);
5682
+ // Volume — validated and clamped to [0, 1] by validateAndPrepareStartingVideoVolume,
5683
+ // assigned as a typed number, no sanitization needed
5684
+ if (config.startingVolume) {
5685
+ this.elementRef.nativeElement.volume = validateAndPrepareStartingVideoVolume(config.startingVolume);
5408
5686
  }
5409
5687
  }
5410
5688
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: EvaVideoConfigurationDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -5970,5 +6248,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
5970
6248
  * Generated bundle index. Do not edit.
5971
6249
  */
5972
6250
 
5973
- export { EvaActiveChapter, EvaBackward, EvaBuffering, EvaControlsContainer, EvaControlsDivider, EvaCueChangeDirective, EvaDashDirective, EvaForward, EvaFullscreen, EvaHlsDirective, EvaMediaEventListenersDirective, EvaMute, EvaOverlayPlay, EvaPlayPause, EvaPlaybackSpeed, EvaPlayer, EvaQualitySelector, EvaScrubBar, EvaScrubBarBufferingTime, EvaScrubBarCurrentTime, EvaState, EvaSubtitleDisplay, EvaTimeDisplay, EvaTimeDisplayPipe, EvaTrackSelector, EvaUserInteractionEventsDirective, EvaVideoConfigurationDirective, EvaVideoEvent, EvaVolume, isValidTrackKind, isValidVideoEvent, transformEvaActiveChaptedAria, transformEvaBackwardAria, transformEvaControlsDividerAria, transformEvaForwardAria, transformEvaFullscreenAria, transformEvaMuteAria, transformEvaOverlayPlayAria, transformEvaPlayPauseAria, transformEvaPlaybackSpeedAria, transformEvaScrubBarAria, transformEvaTimeDisplayAria, transformEvaVolumeAria, validateAndTransformEvaForwardAndBackwardSeconds, validateAndTransformVolumeRange };
6251
+ export { EvaActiveChapter, EvaBackward, EvaBuffering, EvaControlsContainer, EvaControlsDivider, EvaCueChangeDirective, EvaDashDirective, EvaForward, EvaFullscreen, EvaHlsDirective, EvaMediaEventListenersDirective, EvaMute, EvaOverlayPlay, EvaPictureInPicture, EvaPlayPause, EvaPlaybackSpeed, EvaPlayer, EvaQualitySelector, EvaScrubBar, EvaScrubBarBufferingTime, EvaScrubBarCurrentTime, EvaState, EvaSubtitleDisplay, EvaTimeDisplay, EvaTimeDisplayPipe, EvaTrackSelector, EvaUserInteractionEventsDirective, EvaVideoConfigurationDirective, EvaVideoEvent, EvaVolume, isValidTrackKind, isValidVideoEvent, transformEvaActiveChaptedAria, transformEvaBackwardAria, transformEvaControlsDividerAria, transformEvaForwardAria, transformEvaFullscreenAria, transformEvaMuteAria, transformEvaOverlayPlayAria, transformEvaPictureInPictureAria, transformEvaPlayPauseAria, transformEvaPlaybackSpeedAria, transformEvaScrubBarAria, transformEvaTimeDisplayAria, transformEvaVolumeAria, validateAndTransformEvaForwardAndBackwardSeconds, validateAndTransformVolumeRange };
5974
6252
  //# sourceMappingURL=ez-vid-ang.mjs.map