@ngxsplayer/ngx-smart-player 0.0.1-next.0 → 0.1.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.
@@ -1,9 +1,1861 @@
1
- import { PlayerFacade, createSsaiManifestRewritePlugin } from '@ngxsp/core';
1
+ import { PlayerFacade, createSsaiManifestRewritePlugin, describeSubtitleFile, subtitleTextToVtt, decodeSubtitleBytes } from '@ngxsp/core';
2
2
  export * from '@ngxsp/core';
3
3
  import * as i0 from '@angular/core';
4
- import { EventEmitter, HostListener, ViewChild, Output, Input, ChangeDetectionStrategy, Component, makeEnvironmentProviders } from '@angular/core';
4
+ import { Input, ChangeDetectionStrategy, Component, EventEmitter, Output, HostListener, ViewChild, input, output, computed, inject, ChangeDetectorRef, ElementRef, InjectionToken, makeEnvironmentProviders } from '@angular/core';
5
5
  import { CommonModule } from '@angular/common';
6
6
  import { Subscription } from 'rxjs';
7
+ import { NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl, NgxspLabeledControl, NgxspLiveBadge } from '@ngxsplayer/ngx-smart-player-ui-core';
8
+
9
+ class SmartPlayerIconComponent {
10
+ name = 'play';
11
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: SmartPlayerIconComponent, isStandalone: true, selector: "ngxsp-smart-icon", inputs: { name: "name" }, ngImport: i0, template: `
13
+ @switch (name) {
14
+ @case ('play') {
15
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.25 6.82c0-1.3 1.43-2.1 2.54-1.43l8.12 4.93a1.96 1.96 0 0 1 0 3.36l-8.12 4.93a1.67 1.67 0 0 1-2.54-1.43V6.82Z"/></svg>
16
+ }
17
+ @case ('pause') {
18
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
19
+ }
20
+ @case ('prev') {
21
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M19 6.55v10.9a1.25 1.25 0 0 1-1.93 1.05l-8.2-5.45a1.25 1.25 0 0 1 0-2.1l8.2-5.45A1.25 1.25 0 0 1 19 6.55Z"/></svg>
22
+ }
23
+ @case ('next') {
24
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M5 6.55v10.9a1.25 1.25 0 0 0 1.93 1.05l8.2-5.45a1.25 1.25 0 0 0 0-2.1l-8.2-5.45A1.25 1.25 0 0 0 5 6.55Z"/></svg>
25
+ }
26
+ @case ('seek-back') {
27
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 13a6.5 6.5 0 1 0 6.5-6.5H8M10 4.5 8 6.5l2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
28
+ }
29
+ @case ('seek-forward') {
30
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18.5 13a6.5 6.5 0 1 1-6.5-6.5H16M14 4.5 16 6.5l-2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
31
+ }
32
+ @case ('volume-mute') {
33
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
34
+ }
35
+ @case ('volume-low') {
36
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95"/></svg>
37
+ }
38
+ @case ('volume-high') {
39
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95m2.75-7.7a7.5 7.5 0 0 1 0 10.45"/></svg>
40
+ }
41
+ @case ('shuffle') {
42
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
43
+ }
44
+ @case ('repeat') {
45
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/></svg>
46
+ }
47
+ @case ('repeat-one') {
48
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/><path d="M11 10.9 12.4 10v4.1"/></svg>
49
+ }
50
+ @case ('tracks') {
51
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg>
52
+ }
53
+ @case ('captions') {
54
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/></svg>
55
+ }
56
+ @case ('captions-off') {
57
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/><path stroke-linecap="round" stroke-width="1.8" d="M6 6l12 12"/></svg>
58
+ }
59
+ @case ('speed') {
60
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg>
61
+ }
62
+ @case ('pip') {
63
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
64
+ }
65
+ @case ('pip-active') {
66
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5" fill="currentColor" fill-opacity=".22"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
67
+ }
68
+ @case ('airplay') {
69
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m12 14 4.5 6h-9L12 14Z"/></svg>
70
+ }
71
+ @case ('airplay-active') {
72
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path fill="currentColor" stroke="none" d="m12 13.4 5.2 6.9H6.8L12 13.4Z"/></svg>
73
+ }
74
+ @case ('cast') {
75
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/></svg>
76
+ }
77
+ @case ('cast-active') {
78
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/><circle cx="7" cy="19" r="1.5" fill="currentColor" stroke="none"/></svg>
79
+ }
80
+ @case ('devices') {
81
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.4"/><path d="M18.75 11.25h.01"/></svg>
82
+ }
83
+ @case ('devices-active') {
84
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.6" fill="currentColor" stroke="none"/><path d="M18.75 11.25h.01"/></svg>
85
+ }
86
+ @case ('radio') {
87
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 8 16.5 3.75"/><rect x="3" y="8" width="18" height="12.5" rx="2.5"/><circle cx="15.25" cy="14.25" r="2.75"/><path d="M6.75 12.75h3.5M6.75 15.75h3.5"/></svg>
88
+ }
89
+ @case ('server') {
90
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="3.25" width="16" height="11" rx="2"/><path d="M7.5 6.75h.01M7.5 10.75h.01M10.5 6.75h6M10.5 10.75h6"/><path d="M12 14.25v4.5M3.5 20.5h6.75M13.75 20.5h6.75"/><circle cx="12" cy="20.5" r="1.6"/></svg>
91
+ }
92
+ @case ('window-mini') {
93
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 12.5 15 9.5M11 9.5h4v4"/><rect x="5.5" y="13" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
94
+ }
95
+ @case ('window-bar') {
96
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="5" y="15" width="14" height="2.8" rx="1" fill="currentColor" stroke="none"/></svg>
97
+ }
98
+ @case ('cinema') {
99
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
100
+ }
101
+ @case ('cinema-active') {
102
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2" fill="currentColor" fill-opacity=".22"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
103
+ }
104
+ @case ('info') {
105
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.5h.01"/></svg>
106
+ }
107
+ @case ('lyrics') {
108
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5.5h16v10H10l-4.5 4v-4H4z"/><path d="M8 9.5h8M8 12.5h5"/></svg>
109
+ }
110
+ @case ('window-restore') {
111
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 14.5 15 9.5M11 9.5h4v4"/></svg>
112
+ }
113
+ @case ('bell') {
114
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.75 10.4a5.25 5.25 0 0 1 10.5 0c0 4.65 1.75 5.6 1.75 5.6H5s1.75-.95 1.75-5.6Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 19a2.15 2.15 0 0 0 4 0"/></svg>
115
+ }
116
+ @case ('thumb-up') {
117
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 10.5v9.25H4.7a1.7 1.7 0 0 1-1.7-1.7V12.2a1.7 1.7 0 0 1 1.7-1.7h2.55Zm0 0 4.85-7.05a1.75 1.75 0 0 1 3.18 1.16l-.44 3.2h3.88a2.1 2.1 0 0 1 2.05 2.55l-1.35 6.45a3.7 3.7 0 0 1-3.62 2.94H7.25"/></svg>
118
+ }
119
+ @case ('thumb-down') {
120
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 13.5V4.25H4.7A1.7 1.7 0 0 0 3 5.95v5.85a1.7 1.7 0 0 0 1.7 1.7h2.55Zm0 0 4.85 7.05a1.75 1.75 0 0 0 3.18-1.16l-.44-3.2h3.88a2.1 2.1 0 0 0 2.05-2.55L19.42 7.2a3.7 3.7 0 0 0-3.62-2.94H7.25"/></svg>
121
+ }
122
+ @case ('share') {
123
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M18.25 8.25 21 5.5l-2.75-2.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.4 5.5h-5.2A8.2 8.2 0 0 0 7 13.7v.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 11.5v6.75A1.75 1.75 0 0 0 5.75 20h12.5A1.75 1.75 0 0 0 20 18.25V14"/></svg>
124
+ }
125
+ @case ('playlist-plus') {
126
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 6.5h11M4 11.5h11M4 16.5h8"/><path stroke-linecap="round" stroke-width="1.8" d="M18 13.5v6m-3-3h6"/></svg>
127
+ }
128
+ @case ('clock') {
129
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 7.75v4.65l3.45 2.05"/></svg>
130
+ }
131
+ @case ('home') {
132
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 11.5 8-6.75 8 6.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.5 10.5v8.25h11v-8.25"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 18.75v-5h4v5"/></svg>
133
+ }
134
+ @case ('layers') {
135
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M12 3.75 3.75 8 12 12.25 20.25 8 12 3.75Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12 7 3.6 7-3.6M5 16l7 3.6 7-3.6"/></svg>
136
+ }
137
+ @case ('video') {
138
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 5v14M16 5v14M4 9h4M4 15h4M16 9h4M16 15h4"/></svg>
139
+ }
140
+ @case ('music') {
141
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 18.25a2.75 2.75 0 1 1-1.6-2.5V6.5l10-2v11.25a2.75 2.75 0 1 1-1.6-2.5V8.2l-8.4 1.68"/></svg>
142
+ }
143
+ @case ('broadcast') {
144
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="2" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8.7 8.7a4.65 4.65 0 0 0 0 6.6m6.6 0a4.65 4.65 0 0 0 0-6.6M6.1 6.1a8.35 8.35 0 0 0 0 11.8m11.8 0a8.35 8.35 0 0 0 0-11.8"/></svg>
145
+ }
146
+ @case ('heart') {
147
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.2 6.2c-1.8-1.9-4.8-1.8-6.5.2L12 8.3l-1.7-1.9c-1.7-2-4.7-2.1-6.5-.2-1.9 2-1.7 5.1.4 6.9L12 20l7.8-6.9c2.1-1.8 2.3-4.9.4-6.9Z"/></svg>
148
+ }
149
+ @case ('download') {
150
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 4v10m0 0 4-4m-4 4-4-4M5 19h14"/></svg>
151
+ }
152
+ @case ('trash') {
153
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 6.75h15M9.75 6.75V5.25A1.5 1.5 0 0 1 11.25 3.75h1.5a1.5 1.5 0 0 1 1.5 1.5v1.5M6.75 6.75l.7 12a1.5 1.5 0 0 0 1.5 1.5h6.1a1.5 1.5 0 0 0 1.5-1.5l.7-12M10.5 10.5v6M13.5 10.5v6"/></svg>
154
+ }
155
+ @case ('folder') {
156
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3.75 7.5A2.25 2.25 0 0 1 6 5.25h4.15l2.1 2.25H18a2.25 2.25 0 0 1 2.25 2.25v6.75A2.25 2.25 0 0 1 18 18.75H6a2.25 2.25 0 0 1-2.25-2.25v-9Z"/></svg>
157
+ }
158
+ @case ('settings') {
159
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 15.25A3.25 3.25 0 1 0 12 8.75a3.25 3.25 0 0 0 0 6.5Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M19.4 13.5a7.8 7.8 0 0 0 .05-3l2-1.55-2-3.45-2.42.98a8 8 0 0 0-2.58-1.5L14.1 2.4h-4.2l-.35 2.58a8 8 0 0 0-2.58 1.5L4.55 5.5l-2 3.45 2 1.55a7.8 7.8 0 0 0 .05 3l-2.05 1.55 2 3.45 2.46-.98a8 8 0 0 0 2.54 1.47l.35 2.61h4.2l.35-2.6a8 8 0 0 0 2.54-1.48l2.46.98 2-3.45-2.05-1.55Z"/></svg>
160
+ }
161
+ @case ('search') {
162
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="10.75" cy="10.75" r="6.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="m15.4 15.4 4.85 4.85"/></svg>
163
+ }
164
+ @case ('grid') {
165
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="4" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/></svg>
166
+ }
167
+ @case ('list') {
168
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.8" cy="6" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="12" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="18" r="1.2" fill="currentColor" stroke="none"/></svg>
169
+ }
170
+ @case ('more') {
171
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>
172
+ }
173
+ @case ('waveform') {
174
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 13v-2m4 5V8m4 11V5m4 10V9m4 4v-2"/></svg>
175
+ }
176
+ @case ('sliders') {
177
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h5m6 0h5M4 17h9m6 0h1"/><circle cx="12" cy="7" r="2.25" stroke-width="1.8"/><circle cx="16" cy="17" r="2.25" stroke-width="1.8"/></svg>
178
+ }
179
+ @case ('network') {
180
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="2.3" stroke-width="1.8"/><circle cx="6" cy="18" r="2.3" stroke-width="1.8"/><circle cx="18" cy="18" r="2.3" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M11 7.1 7.1 15.9M13 7.1l3.9 8.8M8.3 18h7.4"/></svg>
181
+ }
182
+ @case ('cloud') {
183
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 18.5h10.2a4.05 4.05 0 0 0 .5-8.07 6.25 6.25 0 0 0-12-1.75A4.95 4.95 0 0 0 7.25 18.5Z"/></svg>
184
+ }
185
+ @case ('spark') {
186
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 3.5 14.3 9l5.7 2.2-5.7 2.3L12 20l-2.3-6.5L4 11.2 9.7 9 12 3.5Z"/></svg>
187
+ }
188
+ @case ('disc') {
189
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><circle cx="12" cy="12" r="2.25" stroke-width="1.8"/></svg>
190
+ }
191
+ @case ('user') {
192
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="8.5" r="3.75" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 19.75a7.25 7.25 0 0 1 14.5 0"/></svg>
193
+ }
194
+ @case ('tag') {
195
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M3.75 5.75v5.1c0 .6.24 1.17.66 1.6l7.4 7.4a2 2 0 0 0 2.83 0l5.1-5.1a2 2 0 0 0 0-2.83l-7.4-7.4a2.25 2.25 0 0 0-1.6-.66h-5a2 2 0 0 0-2 1.89Z"/><circle cx="8.25" cy="8.25" r="1.25" fill="currentColor" stroke="none"/></svg>
196
+ }
197
+ @case ('close') {
198
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="m6.5 6.5 11 11m0-11-11 11"/></svg>
199
+ }
200
+ @case ('plus') {
201
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M12 5v14M5 12h14"/></svg>
202
+ }
203
+ @case ('check') {
204
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12.5 4.5 4.5L19 7.5"/></svg>
205
+ }
206
+ @case ('menu') {
207
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h16M4 12h16M4 17h16"/></svg>
208
+ }
209
+ @case ('panel-right') {
210
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5" stroke-width="1.8"/><path stroke-width="1.8" d="M14.5 4.5v15"/></svg>
211
+ }
212
+ @case ('chevron-left') {
213
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m14.5 6-6 6 6 6"/></svg>
214
+ }
215
+ @case ('chevron-right') {
216
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m9.5 6 6 6-6 6"/></svg>
217
+ }
218
+ @case ('chevron-up') {
219
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 14.5 6-6 6 6"/></svg>
220
+ }
221
+ @case ('chevron-down') {
222
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 9.5 6 6 6-6"/></svg>
223
+ }
224
+ @case ('fullscreen') {
225
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 9V5.5a1 1 0 0 1 1-1H9m6 0h3.5a1 1 0 0 1 1 1V9m0 6v3.5a1 1 0 0 1-1 1H15m-6 0H5.5a1 1 0 0 1-1-1V15"/></svg>
226
+ }
227
+ @case ('fullscreen-exit') {
228
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 4.5V8a1 1 0 0 1-1 1H4.5m15 0H16a1 1 0 0 1-1-1V4.5m0 15V16a1 1 0 0 1 1-1h3.5m-15 0H8a1 1 0 0 1 1 1v3.5"/></svg>
229
+ }
230
+ }
231
+ `, isInline: true, styles: [":host{display:block;width:1em;height:1em;line-height:0;flex:0 0 auto}svg{display:block;width:100%;height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
232
+ }
233
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerIconComponent, decorators: [{
234
+ type: Component,
235
+ args: [{ selector: 'ngxsp-smart-icon', standalone: true, template: `
236
+ @switch (name) {
237
+ @case ('play') {
238
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.25 6.82c0-1.3 1.43-2.1 2.54-1.43l8.12 4.93a1.96 1.96 0 0 1 0 3.36l-8.12 4.93a1.67 1.67 0 0 1-2.54-1.43V6.82Z"/></svg>
239
+ }
240
+ @case ('pause') {
241
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
242
+ }
243
+ @case ('prev') {
244
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M19 6.55v10.9a1.25 1.25 0 0 1-1.93 1.05l-8.2-5.45a1.25 1.25 0 0 1 0-2.1l8.2-5.45A1.25 1.25 0 0 1 19 6.55Z"/></svg>
245
+ }
246
+ @case ('next') {
247
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18 5.25a1 1 0 0 1 1 1v11.5a1 1 0 1 1-2 0V6.25a1 1 0 0 1 1-1Z"/><path d="M5 6.55v10.9a1.25 1.25 0 0 0 1.93 1.05l8.2-5.45a1.25 1.25 0 0 0 0-2.1l-8.2-5.45A1.25 1.25 0 0 0 5 6.55Z"/></svg>
248
+ }
249
+ @case ('seek-back') {
250
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 13a6.5 6.5 0 1 0 6.5-6.5H8M10 4.5 8 6.5l2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
251
+ }
252
+ @case ('seek-forward') {
253
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18.5 13a6.5 6.5 0 1 1-6.5-6.5H16M14 4.5 16 6.5l-2 2"/><text x="12" y="15.8" font-size="7.5" font-weight="700" text-anchor="middle" fill="currentColor" stroke="none" font-family="system-ui, sans-serif">10</text></svg>
254
+ }
255
+ @case ('volume-mute') {
256
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
257
+ }
258
+ @case ('volume-low') {
259
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95"/></svg>
260
+ }
261
+ @case ('volume-high') {
262
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95m2.75-7.7a7.5 7.5 0 0 1 0 10.45"/></svg>
263
+ }
264
+ @case ('shuffle') {
265
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
266
+ }
267
+ @case ('repeat') {
268
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/></svg>
269
+ }
270
+ @case ('repeat-one') {
271
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 3.5 20 6.5 17 9.5"/><path d="M4 11.5v-1a4 4 0 0 1 4-4h12"/><path d="M7 20.5 4 17.5 7 14.5"/><path d="M20 12.5v1a4 4 0 0 1-4 4H4"/><path d="M11 10.9 12.4 10v4.1"/></svg>
272
+ }
273
+ @case ('tracks') {
274
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg>
275
+ }
276
+ @case ('captions') {
277
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/></svg>
278
+ }
279
+ @case ('captions-off') {
280
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/><path stroke-linecap="round" stroke-width="1.8" d="M6 6l12 12"/></svg>
281
+ }
282
+ @case ('speed') {
283
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg>
284
+ }
285
+ @case ('pip') {
286
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
287
+ }
288
+ @case ('pip-active') {
289
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5" fill="currentColor" fill-opacity=".22"/><rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none"/></svg>
290
+ }
291
+ @case ('airplay') {
292
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m12 14 4.5 6h-9L12 14Z"/></svg>
293
+ }
294
+ @case ('airplay-active') {
295
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 16.5H4.75A1.75 1.75 0 0 1 3 14.75v-8A1.75 1.75 0 0 1 4.75 5h14.5A1.75 1.75 0 0 1 21 6.75v8a1.75 1.75 0 0 1-1.75 1.75H18"/><path fill="currentColor" stroke="none" d="m12 13.4 5.2 6.9H6.8L12 13.4Z"/></svg>
296
+ }
297
+ @case ('cast') {
298
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/></svg>
299
+ }
300
+ @case ('cast-active') {
301
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 18.25a3.5 3.5 0 0 1 3.5 3.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 13.75A8 8 0 0 1 12.75 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 9.25A12.5 12.5 0 0 1 17.25 21.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M5.5 5.5h13a1.75 1.75 0 0 1 1.75 1.75v4.5"/><circle cx="7" cy="19" r="1.5" fill="currentColor" stroke="none"/></svg>
302
+ }
303
+ @case ('devices') {
304
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.4"/><path d="M18.75 11.25h.01"/></svg>
305
+ }
306
+ @case ('devices-active') {
307
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.75" y="4.75" width="12.5" height="9.5" rx="1.75"/><path d="M6.5 18.75h5M9 14.25v4.5"/><rect x="16.25" y="8.25" width="5" height="12" rx="1.5"/><circle cx="18.75" cy="16.25" r="1.6" fill="currentColor" stroke="none"/><path d="M18.75 11.25h.01"/></svg>
308
+ }
309
+ @case ('radio') {
310
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 8 16.5 3.75"/><rect x="3" y="8" width="18" height="12.5" rx="2.5"/><circle cx="15.25" cy="14.25" r="2.75"/><path d="M6.75 12.75h3.5M6.75 15.75h3.5"/></svg>
311
+ }
312
+ @case ('server') {
313
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="3.25" width="16" height="11" rx="2"/><path d="M7.5 6.75h.01M7.5 10.75h.01M10.5 6.75h6M10.5 10.75h6"/><path d="M12 14.25v4.5M3.5 20.5h6.75M13.75 20.5h6.75"/><circle cx="12" cy="20.5" r="1.6"/></svg>
314
+ }
315
+ @case ('window-mini') {
316
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 12.5 15 9.5M11 9.5h4v4"/><rect x="5.5" y="13" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
317
+ }
318
+ @case ('window-bar') {
319
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><rect x="5" y="15" width="14" height="2.8" rx="1" fill="currentColor" stroke="none"/></svg>
320
+ }
321
+ @case ('cinema') {
322
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
323
+ }
324
+ @case ('cinema-active') {
325
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="7" width="20" height="10" rx="2" fill="currentColor" fill-opacity=".22"/><path d="M6 12h12M6 12l2-2M6 12l2 2M18 12l-2-2M18 12l-2 2"/></svg>
326
+ }
327
+ @case ('info') {
328
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.5h.01"/></svg>
329
+ }
330
+ @case ('lyrics') {
331
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5.5h16v10H10l-4.5 4v-4H4z"/><path d="M8 9.5h8M8 12.5h5"/></svg>
332
+ }
333
+ @case ('window-restore') {
334
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4.5" width="18" height="15" rx="2.5"/><path d="M10 14.5 15 9.5M11 9.5h4v4"/></svg>
335
+ }
336
+ @case ('bell') {
337
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.75 10.4a5.25 5.25 0 0 1 10.5 0c0 4.65 1.75 5.6 1.75 5.6H5s1.75-.95 1.75-5.6Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 19a2.15 2.15 0 0 0 4 0"/></svg>
338
+ }
339
+ @case ('thumb-up') {
340
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 10.5v9.25H4.7a1.7 1.7 0 0 1-1.7-1.7V12.2a1.7 1.7 0 0 1 1.7-1.7h2.55Zm0 0 4.85-7.05a1.75 1.75 0 0 1 3.18 1.16l-.44 3.2h3.88a2.1 2.1 0 0 1 2.05 2.55l-1.35 6.45a3.7 3.7 0 0 1-3.62 2.94H7.25"/></svg>
341
+ }
342
+ @case ('thumb-down') {
343
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 13.5V4.25H4.7A1.7 1.7 0 0 0 3 5.95v5.85a1.7 1.7 0 0 0 1.7 1.7h2.55Zm0 0 4.85 7.05a1.75 1.75 0 0 0 3.18-1.16l-.44-3.2h3.88a2.1 2.1 0 0 0 2.05-2.55L19.42 7.2a3.7 3.7 0 0 0-3.62-2.94H7.25"/></svg>
344
+ }
345
+ @case ('share') {
346
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M18.25 8.25 21 5.5l-2.75-2.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.4 5.5h-5.2A8.2 8.2 0 0 0 7 13.7v.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 11.5v6.75A1.75 1.75 0 0 0 5.75 20h12.5A1.75 1.75 0 0 0 20 18.25V14"/></svg>
347
+ }
348
+ @case ('playlist-plus') {
349
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 6.5h11M4 11.5h11M4 16.5h8"/><path stroke-linecap="round" stroke-width="1.8" d="M18 13.5v6m-3-3h6"/></svg>
350
+ }
351
+ @case ('clock') {
352
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 7.75v4.65l3.45 2.05"/></svg>
353
+ }
354
+ @case ('home') {
355
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 11.5 8-6.75 8 6.75"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6.5 10.5v8.25h11v-8.25"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 18.75v-5h4v5"/></svg>
356
+ }
357
+ @case ('layers') {
358
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M12 3.75 3.75 8 12 12.25 20.25 8 12 3.75Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12 7 3.6 7-3.6M5 16l7 3.6 7-3.6"/></svg>
359
+ }
360
+ @case ('video') {
361
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="5" width="16" height="14" rx="2.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 5v14M16 5v14M4 9h4M4 15h4M16 9h4M16 15h4"/></svg>
362
+ }
363
+ @case ('music') {
364
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 18.25a2.75 2.75 0 1 1-1.6-2.5V6.5l10-2v11.25a2.75 2.75 0 1 1-1.6-2.5V8.2l-8.4 1.68"/></svg>
365
+ }
366
+ @case ('broadcast') {
367
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="2" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8.7 8.7a4.65 4.65 0 0 0 0 6.6m6.6 0a4.65 4.65 0 0 0 0-6.6M6.1 6.1a8.35 8.35 0 0 0 0 11.8m11.8 0a8.35 8.35 0 0 0 0-11.8"/></svg>
368
+ }
369
+ @case ('heart') {
370
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20.2 6.2c-1.8-1.9-4.8-1.8-6.5.2L12 8.3l-1.7-1.9c-1.7-2-4.7-2.1-6.5-.2-1.9 2-1.7 5.1.4 6.9L12 20l7.8-6.9c2.1-1.8 2.3-4.9.4-6.9Z"/></svg>
371
+ }
372
+ @case ('download') {
373
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 4v10m0 0 4-4m-4 4-4-4M5 19h14"/></svg>
374
+ }
375
+ @case ('trash') {
376
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 6.75h15M9.75 6.75V5.25A1.5 1.5 0 0 1 11.25 3.75h1.5a1.5 1.5 0 0 1 1.5 1.5v1.5M6.75 6.75l.7 12a1.5 1.5 0 0 0 1.5 1.5h6.1a1.5 1.5 0 0 0 1.5-1.5l.7-12M10.5 10.5v6M13.5 10.5v6"/></svg>
377
+ }
378
+ @case ('folder') {
379
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3.75 7.5A2.25 2.25 0 0 1 6 5.25h4.15l2.1 2.25H18a2.25 2.25 0 0 1 2.25 2.25v6.75A2.25 2.25 0 0 1 18 18.75H6a2.25 2.25 0 0 1-2.25-2.25v-9Z"/></svg>
380
+ }
381
+ @case ('settings') {
382
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 15.25A3.25 3.25 0 1 0 12 8.75a3.25 3.25 0 0 0 0 6.5Z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M19.4 13.5a7.8 7.8 0 0 0 .05-3l2-1.55-2-3.45-2.42.98a8 8 0 0 0-2.58-1.5L14.1 2.4h-4.2l-.35 2.58a8 8 0 0 0-2.58 1.5L4.55 5.5l-2 3.45 2 1.55a7.8 7.8 0 0 0 .05 3l-2.05 1.55 2 3.45 2.46-.98a8 8 0 0 0 2.54 1.47l.35 2.61h4.2l.35-2.6a8 8 0 0 0 2.54-1.48l2.46.98 2-3.45-2.05-1.55Z"/></svg>
383
+ }
384
+ @case ('search') {
385
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="10.75" cy="10.75" r="6.25" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="m15.4 15.4 4.85 4.85"/></svg>
386
+ }
387
+ @case ('grid') {
388
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="4" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="4" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="4" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/><rect x="14" y="14" width="6" height="6" rx="1.4" stroke-width="1.8"/></svg>
389
+ }
390
+ @case ('list') {
391
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.8" cy="6" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="12" r="1.2" fill="currentColor" stroke="none"/><circle cx="4.8" cy="18" r="1.2" fill="currentColor" stroke="none"/></svg>
392
+ }
393
+ @case ('more') {
394
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="19" cy="12" r="1.7"/></svg>
395
+ }
396
+ @case ('waveform') {
397
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 13v-2m4 5V8m4 11V5m4 10V9m4 4v-2"/></svg>
398
+ }
399
+ @case ('sliders') {
400
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h5m6 0h5M4 17h9m6 0h1"/><circle cx="12" cy="7" r="2.25" stroke-width="1.8"/><circle cx="16" cy="17" r="2.25" stroke-width="1.8"/></svg>
401
+ }
402
+ @case ('network') {
403
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="5" r="2.3" stroke-width="1.8"/><circle cx="6" cy="18" r="2.3" stroke-width="1.8"/><circle cx="18" cy="18" r="2.3" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M11 7.1 7.1 15.9M13 7.1l3.9 8.8M8.3 18h7.4"/></svg>
404
+ }
405
+ @case ('cloud') {
406
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7.25 18.5h10.2a4.05 4.05 0 0 0 .5-8.07 6.25 6.25 0 0 0-12-1.75A4.95 4.95 0 0 0 7.25 18.5Z"/></svg>
407
+ }
408
+ @case ('spark') {
409
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 3.5 14.3 9l5.7 2.2-5.7 2.3L12 20l-2.3-6.5L4 11.2 9.7 9 12 3.5Z"/></svg>
410
+ }
411
+ @case ('disc') {
412
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="8.25" stroke-width="1.8"/><circle cx="12" cy="12" r="2.25" stroke-width="1.8"/></svg>
413
+ }
414
+ @case ('user') {
415
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><circle cx="12" cy="8.5" r="3.75" stroke-width="1.8"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.75 19.75a7.25 7.25 0 0 1 14.5 0"/></svg>
416
+ }
417
+ @case ('tag') {
418
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linejoin="round" stroke-width="1.8" d="M3.75 5.75v5.1c0 .6.24 1.17.66 1.6l7.4 7.4a2 2 0 0 0 2.83 0l5.1-5.1a2 2 0 0 0 0-2.83l-7.4-7.4a2.25 2.25 0 0 0-1.6-.66h-5a2 2 0 0 0-2 1.89Z"/><circle cx="8.25" cy="8.25" r="1.25" fill="currentColor" stroke="none"/></svg>
419
+ }
420
+ @case ('close') {
421
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="m6.5 6.5 11 11m0-11-11 11"/></svg>
422
+ }
423
+ @case ('plus') {
424
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M12 5v14M5 12h14"/></svg>
425
+ }
426
+ @case ('check') {
427
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m5 12.5 4.5 4.5L19 7.5"/></svg>
428
+ }
429
+ @case ('menu') {
430
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-width="1.8" d="M4 7h16M4 12h16M4 17h16"/></svg>
431
+ }
432
+ @case ('panel-right') {
433
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5" stroke-width="1.8"/><path stroke-width="1.8" d="M14.5 4.5v15"/></svg>
434
+ }
435
+ @case ('chevron-left') {
436
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m14.5 6-6 6 6 6"/></svg>
437
+ }
438
+ @case ('chevron-right') {
439
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m9.5 6 6 6-6 6"/></svg>
440
+ }
441
+ @case ('chevron-up') {
442
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 14.5 6-6 6 6"/></svg>
443
+ }
444
+ @case ('chevron-down') {
445
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m6 9.5 6 6 6-6"/></svg>
446
+ }
447
+ @case ('fullscreen') {
448
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4.5 9V5.5a1 1 0 0 1 1-1H9m6 0h3.5a1 1 0 0 1 1 1V9m0 6v3.5a1 1 0 0 1-1 1H15m-6 0H5.5a1 1 0 0 1-1-1V15"/></svg>
449
+ }
450
+ @case ('fullscreen-exit') {
451
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 4.5V8a1 1 0 0 1-1 1H4.5m15 0H16a1 1 0 0 1-1-1V4.5m0 15V16a1 1 0 0 1 1-1h3.5m-15 0H8a1 1 0 0 1 1 1v3.5"/></svg>
452
+ }
453
+ }
454
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:1em;height:1em;line-height:0;flex:0 0 auto}svg{display:block;width:100%;height:100%}\n"] }]
455
+ }], propDecorators: { name: [{
456
+ type: Input,
457
+ args: [{ required: true }]
458
+ }] } });
459
+
460
+ /**
461
+ * The settings of a player, grouped in sections. Colours come from the host through CSS variables
462
+ * (`--ngxsp-settings-surface`, `-text`, `-muted`, `-line`, `-control`, `-accent`, `-accent-text`, `-radius`).
463
+ */
464
+ class SmartSettingsComponent {
465
+ sections = [];
466
+ settingChange = new EventEmitter();
467
+ settingAction = new EventEmitter();
468
+ /**
469
+ * Sends what was typed and empties the fields at once. Nothing secret stays in the component:
470
+ * the host is the only place it goes, and it does not come back through an input.
471
+ */
472
+ submitSecret(event, id) {
473
+ event.preventDefault();
474
+ const form = event.target;
475
+ const inputs = Array.from(form.querySelectorAll('input'));
476
+ const value = inputs.at(-1)?.value ?? '';
477
+ const previous = inputs.length > 1 ? inputs[0].value : undefined;
478
+ inputs.forEach((input) => { input.value = ''; });
479
+ if (!value)
480
+ return;
481
+ this.settingChange.emit({ id, value, ...(previous === undefined ? {} : { previous }) });
482
+ }
483
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartSettingsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
484
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: SmartSettingsComponent, isStandalone: true, selector: "ngxsp-settings", inputs: { sections: "sections" }, outputs: { settingChange: "settingChange", settingAction: "settingAction" }, ngImport: i0, template: `
485
+ @for (section of sections; track section.id) {
486
+ <section class="ngxsp-settings-section" [attr.aria-labelledby]="'ngxsp-settings-' + section.id">
487
+ <h2 [id]="'ngxsp-settings-' + section.id">{{ section.title }}</h2>
488
+ <ul class="ngxsp-settings-items">
489
+ @for (item of section.items; track item.id) {
490
+ <li class="ngxsp-settings-item" [class.stacked]="item.kind === 'list'" [class.wraps]="item.kind === 'choice' || item.kind === 'action'">
491
+ <span class="ngxsp-settings-copy">
492
+ <strong [id]="'ngxsp-setting-' + item.id">{{ item.label }}</strong>
493
+ @if (item.description) { <small [id]="'ngxsp-setting-help-' + item.id">{{ item.description }}</small> }
494
+ </span>
495
+ @switch (item.kind) {
496
+ @case ('switch') {
497
+ <button class="ngxsp-settings-switch" type="button" role="switch" [attr.aria-checked]="item.value" [disabled]="item.disabled" [attr.aria-labelledby]="'ngxsp-setting-' + item.id" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingChange.emit({ id: item.id, value: !item.value })">
498
+ <span class="ngxsp-settings-knob" aria-hidden="true"></span>
499
+ </button>
500
+ }
501
+ @case ('choice') {
502
+ <span class="ngxsp-settings-choice" role="group" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
503
+ @for (option of item.options; track option.id) {
504
+ <button type="button" [class.active]="option.id === item.value" [attr.aria-pressed]="option.id === item.value" (click)="option.id !== item.value && settingChange.emit({ id: item.id, value: option.id })">{{ option.label }}</button>
505
+ }
506
+ </span>
507
+ }
508
+ @case ('secret') {
509
+ <form class="ngxsp-settings-secret" (submit)="submitSecret($event, item.id)">
510
+ @if (item.secondPlaceholder) {
511
+ <input
512
+ #firstSecret
513
+ type="password"
514
+ autocomplete="off"
515
+ [attr.inputmode]="item.digits ? 'numeric' : null"
516
+ [attr.placeholder]="item.secondPlaceholder"
517
+ [attr.aria-label]="item.secondPlaceholder"
518
+ [disabled]="item.disabled || item.busy"
519
+ />
520
+ }
521
+ <input
522
+ #secondSecret
523
+ type="password"
524
+ autocomplete="off"
525
+ [attr.inputmode]="item.digits ? 'numeric' : null"
526
+ [attr.placeholder]="item.placeholder ?? null"
527
+ [attr.aria-label]="item.placeholder ?? item.label"
528
+ [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null"
529
+ [disabled]="item.disabled || item.busy"
530
+ />
531
+ <button class="ngxsp-settings-button" type="submit" [disabled]="item.disabled || item.busy">{{ item.actionLabel }}</button>
532
+ </form>
533
+ }
534
+ @case ('action') {
535
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.disabled || item.busy" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
536
+ }
537
+ @case ('list') {
538
+ <ul class="ngxsp-settings-entries" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
539
+ @for (entry of item.entries; track entry.id) {
540
+ <li>
541
+ <span class="ngxsp-settings-entry"><span>{{ entry.label }}</span>@if (entry.detail) { <small>{{ entry.detail }}</small> }</span>
542
+ @if (!entry.fixed) {
543
+ <button class="ngxsp-settings-remove" type="button" [title]="item.removeLabel" [attr.aria-label]="item.removeLabel + ' - ' + entry.label" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id, entryId: entry.id })">
544
+ <ngxsp-smart-icon name="close" />
545
+ </button>
546
+ }
547
+ </li>
548
+ } @empty {
549
+ @if (item.emptyLabel) { <li class="empty">{{ item.emptyLabel }}</li> }
550
+ }
551
+ </ul>
552
+ @if (item.actionLabel) {
553
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
554
+ }
555
+ }
556
+ }
557
+ </li>
558
+ }
559
+ </ul>
560
+ </section>
561
+ }
562
+ `, isInline: true, styles: [":host{--_surface:var(--ngxsp-settings-surface, rgb(255 255 255 / .04));--_text:var(--ngxsp-settings-text, currentColor);--_muted:var(--ngxsp-settings-muted, rgb(127 127 127));--_line:var(--ngxsp-settings-line, rgb(127 127 127 / .3));--_control:var(--ngxsp-settings-control, rgb(127 127 127 / .2));--_accent:var(--ngxsp-settings-accent, #5b5bd6);--_accent-text:var(--ngxsp-settings-accent-text, #fff);--_radius:var(--ngxsp-settings-radius, 10px);display:grid;gap:28px;max-width:760px;color:var(--_text)}.ngxsp-settings-section{display:grid;gap:10px}h2{margin:0;font-size:13px;font-weight:600;letter-spacing:.02em;color:var(--_muted);text-transform:uppercase}.ngxsp-settings-items{margin:0;padding:0;list-style:none;display:grid;gap:2px;border-radius:var(--_radius);overflow:hidden}.ngxsp-settings-item{display:flex;align-items:center;justify-content:space-between;gap:12px 24px;padding:14px 16px;background:var(--_surface)}.ngxsp-settings-item.stacked{display:grid;justify-content:stretch}.ngxsp-settings-item.wraps{flex-wrap:wrap}.ngxsp-settings-item.wraps .ngxsp-settings-copy{flex:1 1 220px}.ngxsp-settings-copy{min-width:0;display:grid;gap:4px}.ngxsp-settings-copy strong{font-size:14px;font-weight:600}.ngxsp-settings-copy small{font-size:12px;line-height:1.45;color:var(--_muted)}.ngxsp-settings-switch{position:relative;flex:0 0 auto;width:42px;height:24px;padding:0;border:1px solid var(--_line);border-radius:999px;background:var(--_control);cursor:pointer;transition:background-color .16s ease,border-color .16s ease}.ngxsp-settings-knob{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px #0000004d;transition:transform .16s ease}.ngxsp-settings-switch[aria-checked=true]{border-color:var(--_accent);background:var(--_accent)}.ngxsp-settings-switch[aria-checked=true] .ngxsp-settings-knob{transform:translate(18px)}.ngxsp-settings-choice{flex:0 0 auto;display:inline-flex;padding:2px;gap:2px;border:1px solid var(--_line);border-radius:999px;background:var(--_control)}.ngxsp-settings-choice button{height:28px;padding:0 12px;border:0;border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-choice button.active{background:var(--_accent);color:var(--_accent-text)}.ngxsp-settings-button{justify-self:start;flex:0 0 auto;height:32px;padding:0 14px;border:1px solid var(--_line);border-radius:999px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-button:disabled,.ngxsp-settings-switch:disabled,.ngxsp-settings-remove:disabled{opacity:.5;cursor:default}.ngxsp-settings-secret{display:flex;align-items:center;gap:8px;min-width:0}.ngxsp-settings-secret input{flex:1 1 96px;min-width:0;height:32px;padding:0 10px;border:1px solid var(--_line);border-radius:8px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;letter-spacing:.14em}.ngxsp-settings-secret input::placeholder{color:var(--_muted);letter-spacing:0}.ngxsp-settings-entries{margin:0;padding:0;list-style:none;display:grid;gap:4px}.ngxsp-settings-entries li{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 8px 8px 12px;border:1px solid var(--_line);border-radius:8px}.ngxsp-settings-entries li.empty{display:block;color:var(--_muted);font-size:12px}.ngxsp-settings-entry{min-width:0;display:grid;gap:2px;font-size:13px}.ngxsp-settings-entry small{overflow:hidden;color:var(--_muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.ngxsp-settings-remove{flex:0 0 auto;width:30px;height:30px;display:grid;place-items:center;padding:0;border:0;border-radius:8px;background:transparent;color:var(--_muted);cursor:pointer}.ngxsp-settings-remove:hover{background:var(--_control);color:var(--_text)}.ngxsp-settings-remove ngxsp-smart-icon{width:16px;height:16px}button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}@media(max-width:560px){.ngxsp-settings-secret,.ngxsp-settings-choice{flex-wrap:wrap}}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
563
+ }
564
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartSettingsComponent, decorators: [{
565
+ type: Component,
566
+ args: [{ selector: 'ngxsp-settings', standalone: true, imports: [SmartPlayerIconComponent], template: `
567
+ @for (section of sections; track section.id) {
568
+ <section class="ngxsp-settings-section" [attr.aria-labelledby]="'ngxsp-settings-' + section.id">
569
+ <h2 [id]="'ngxsp-settings-' + section.id">{{ section.title }}</h2>
570
+ <ul class="ngxsp-settings-items">
571
+ @for (item of section.items; track item.id) {
572
+ <li class="ngxsp-settings-item" [class.stacked]="item.kind === 'list'" [class.wraps]="item.kind === 'choice' || item.kind === 'action'">
573
+ <span class="ngxsp-settings-copy">
574
+ <strong [id]="'ngxsp-setting-' + item.id">{{ item.label }}</strong>
575
+ @if (item.description) { <small [id]="'ngxsp-setting-help-' + item.id">{{ item.description }}</small> }
576
+ </span>
577
+ @switch (item.kind) {
578
+ @case ('switch') {
579
+ <button class="ngxsp-settings-switch" type="button" role="switch" [attr.aria-checked]="item.value" [disabled]="item.disabled" [attr.aria-labelledby]="'ngxsp-setting-' + item.id" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingChange.emit({ id: item.id, value: !item.value })">
580
+ <span class="ngxsp-settings-knob" aria-hidden="true"></span>
581
+ </button>
582
+ }
583
+ @case ('choice') {
584
+ <span class="ngxsp-settings-choice" role="group" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
585
+ @for (option of item.options; track option.id) {
586
+ <button type="button" [class.active]="option.id === item.value" [attr.aria-pressed]="option.id === item.value" (click)="option.id !== item.value && settingChange.emit({ id: item.id, value: option.id })">{{ option.label }}</button>
587
+ }
588
+ </span>
589
+ }
590
+ @case ('secret') {
591
+ <form class="ngxsp-settings-secret" (submit)="submitSecret($event, item.id)">
592
+ @if (item.secondPlaceholder) {
593
+ <input
594
+ #firstSecret
595
+ type="password"
596
+ autocomplete="off"
597
+ [attr.inputmode]="item.digits ? 'numeric' : null"
598
+ [attr.placeholder]="item.secondPlaceholder"
599
+ [attr.aria-label]="item.secondPlaceholder"
600
+ [disabled]="item.disabled || item.busy"
601
+ />
602
+ }
603
+ <input
604
+ #secondSecret
605
+ type="password"
606
+ autocomplete="off"
607
+ [attr.inputmode]="item.digits ? 'numeric' : null"
608
+ [attr.placeholder]="item.placeholder ?? null"
609
+ [attr.aria-label]="item.placeholder ?? item.label"
610
+ [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null"
611
+ [disabled]="item.disabled || item.busy"
612
+ />
613
+ <button class="ngxsp-settings-button" type="submit" [disabled]="item.disabled || item.busy">{{ item.actionLabel }}</button>
614
+ </form>
615
+ }
616
+ @case ('action') {
617
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.disabled || item.busy" [attr.aria-describedby]="item.description ? 'ngxsp-setting-help-' + item.id : null" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
618
+ }
619
+ @case ('list') {
620
+ <ul class="ngxsp-settings-entries" [attr.aria-labelledby]="'ngxsp-setting-' + item.id">
621
+ @for (entry of item.entries; track entry.id) {
622
+ <li>
623
+ <span class="ngxsp-settings-entry"><span>{{ entry.label }}</span>@if (entry.detail) { <small>{{ entry.detail }}</small> }</span>
624
+ @if (!entry.fixed) {
625
+ <button class="ngxsp-settings-remove" type="button" [title]="item.removeLabel" [attr.aria-label]="item.removeLabel + ' - ' + entry.label" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id, entryId: entry.id })">
626
+ <ngxsp-smart-icon name="close" />
627
+ </button>
628
+ }
629
+ </li>
630
+ } @empty {
631
+ @if (item.emptyLabel) { <li class="empty">{{ item.emptyLabel }}</li> }
632
+ }
633
+ </ul>
634
+ @if (item.actionLabel) {
635
+ <button class="ngxsp-settings-button" type="button" [disabled]="item.busy" (click)="settingAction.emit({ id: item.id })">{{ item.actionLabel }}</button>
636
+ }
637
+ }
638
+ }
639
+ </li>
640
+ }
641
+ </ul>
642
+ </section>
643
+ }
644
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--_surface:var(--ngxsp-settings-surface, rgb(255 255 255 / .04));--_text:var(--ngxsp-settings-text, currentColor);--_muted:var(--ngxsp-settings-muted, rgb(127 127 127));--_line:var(--ngxsp-settings-line, rgb(127 127 127 / .3));--_control:var(--ngxsp-settings-control, rgb(127 127 127 / .2));--_accent:var(--ngxsp-settings-accent, #5b5bd6);--_accent-text:var(--ngxsp-settings-accent-text, #fff);--_radius:var(--ngxsp-settings-radius, 10px);display:grid;gap:28px;max-width:760px;color:var(--_text)}.ngxsp-settings-section{display:grid;gap:10px}h2{margin:0;font-size:13px;font-weight:600;letter-spacing:.02em;color:var(--_muted);text-transform:uppercase}.ngxsp-settings-items{margin:0;padding:0;list-style:none;display:grid;gap:2px;border-radius:var(--_radius);overflow:hidden}.ngxsp-settings-item{display:flex;align-items:center;justify-content:space-between;gap:12px 24px;padding:14px 16px;background:var(--_surface)}.ngxsp-settings-item.stacked{display:grid;justify-content:stretch}.ngxsp-settings-item.wraps{flex-wrap:wrap}.ngxsp-settings-item.wraps .ngxsp-settings-copy{flex:1 1 220px}.ngxsp-settings-copy{min-width:0;display:grid;gap:4px}.ngxsp-settings-copy strong{font-size:14px;font-weight:600}.ngxsp-settings-copy small{font-size:12px;line-height:1.45;color:var(--_muted)}.ngxsp-settings-switch{position:relative;flex:0 0 auto;width:42px;height:24px;padding:0;border:1px solid var(--_line);border-radius:999px;background:var(--_control);cursor:pointer;transition:background-color .16s ease,border-color .16s ease}.ngxsp-settings-knob{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 2px #0000004d;transition:transform .16s ease}.ngxsp-settings-switch[aria-checked=true]{border-color:var(--_accent);background:var(--_accent)}.ngxsp-settings-switch[aria-checked=true] .ngxsp-settings-knob{transform:translate(18px)}.ngxsp-settings-choice{flex:0 0 auto;display:inline-flex;padding:2px;gap:2px;border:1px solid var(--_line);border-radius:999px;background:var(--_control)}.ngxsp-settings-choice button{height:28px;padding:0 12px;border:0;border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-choice button.active{background:var(--_accent);color:var(--_accent-text)}.ngxsp-settings-button{justify-self:start;flex:0 0 auto;height:32px;padding:0 14px;border:1px solid var(--_line);border-radius:999px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}.ngxsp-settings-button:disabled,.ngxsp-settings-switch:disabled,.ngxsp-settings-remove:disabled{opacity:.5;cursor:default}.ngxsp-settings-secret{display:flex;align-items:center;gap:8px;min-width:0}.ngxsp-settings-secret input{flex:1 1 96px;min-width:0;height:32px;padding:0 10px;border:1px solid var(--_line);border-radius:8px;background:var(--_control);color:var(--_text);font:inherit;font-size:12px;letter-spacing:.14em}.ngxsp-settings-secret input::placeholder{color:var(--_muted);letter-spacing:0}.ngxsp-settings-entries{margin:0;padding:0;list-style:none;display:grid;gap:4px}.ngxsp-settings-entries li{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 8px 8px 12px;border:1px solid var(--_line);border-radius:8px}.ngxsp-settings-entries li.empty{display:block;color:var(--_muted);font-size:12px}.ngxsp-settings-entry{min-width:0;display:grid;gap:2px;font-size:13px}.ngxsp-settings-entry small{overflow:hidden;color:var(--_muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.ngxsp-settings-remove{flex:0 0 auto;width:30px;height:30px;display:grid;place-items:center;padding:0;border:0;border-radius:8px;background:transparent;color:var(--_muted);cursor:pointer}.ngxsp-settings-remove:hover{background:var(--_control);color:var(--_text)}.ngxsp-settings-remove ngxsp-smart-icon{width:16px;height:16px}button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}@media(max-width:560px){.ngxsp-settings-secret,.ngxsp-settings-choice{flex-wrap:wrap}}\n"] }]
645
+ }], propDecorators: { sections: [{
646
+ type: Input
647
+ }], settingChange: [{
648
+ type: Output
649
+ }], settingAction: [{
650
+ type: Output
651
+ }] } });
652
+
653
+ let nextDialogId = 0;
654
+ /**
655
+ * A question the user answers before anything happens (another device asking to send its playback here). The
656
+ * cautious answer has the focus and Escape; the dialog stays until one of the two buttons is pressed. Colours come
657
+ * from the host through CSS variables (`--ngxsp-dialog-surface`, `-text`, `-muted`, `-line`, `-accent`,
658
+ * `-accent-text`, `-radius`).
659
+ */
660
+ class SmartConfirmDialogComponent {
661
+ title = '';
662
+ message = '';
663
+ confirmLabel = 'OK';
664
+ cancelLabel = 'Cancel';
665
+ confirmed = new EventEmitter();
666
+ cancelled = new EventEmitter();
667
+ cancelButton;
668
+ id = `ngxsp-dialog-${++nextDialogId}`;
669
+ ngAfterViewInit() {
670
+ this.cancelButton?.nativeElement.focus({ preventScroll: true });
671
+ }
672
+ onEscape(event) {
673
+ event.preventDefault();
674
+ this.cancelled.emit();
675
+ }
676
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartConfirmDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
677
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.33", type: SmartConfirmDialogComponent, isStandalone: true, selector: "ngxsp-confirm-dialog", inputs: { title: "title", message: "message", confirmLabel: "confirmLabel", cancelLabel: "cancelLabel" }, outputs: { confirmed: "confirmed", cancelled: "cancelled" }, host: { listeners: { "document:keydown.escape": "onEscape($event)" } }, viewQueries: [{ propertyName: "cancelButton", first: true, predicate: ["cancelButton"], descendants: true }], ngImport: i0, template: `
678
+ <div class="ngxsp-dialog-backdrop" role="presentation">
679
+ <section class="ngxsp-dialog" role="alertdialog" aria-modal="true" [attr.aria-labelledby]="id + '-title'" [attr.aria-describedby]="id + '-message'">
680
+ <h2 [id]="id + '-title'">{{ title }}</h2>
681
+ <p [id]="id + '-message'">{{ message }}</p>
682
+ <div class="ngxsp-dialog-actions">
683
+ <button #cancelButton class="ngxsp-dialog-button" type="button" (click)="cancelled.emit()">{{ cancelLabel }}</button>
684
+ <button class="ngxsp-dialog-button primary" type="button" (click)="confirmed.emit()">{{ confirmLabel }}</button>
685
+ </div>
686
+ </section>
687
+ </div>
688
+ `, isInline: true, styles: [":host{--_surface:var(--ngxsp-dialog-surface, #17181d);--_text:var(--ngxsp-dialog-text, #ededf0);--_muted:var(--ngxsp-dialog-muted, #a3a5ae);--_line:var(--ngxsp-dialog-line, rgb(255 255 255 / .13));--_accent:var(--ngxsp-dialog-accent, #5b5bd6);--_accent-text:var(--ngxsp-dialog-accent-text, #fff);--_radius:var(--ngxsp-dialog-radius, 14px)}.ngxsp-dialog-backdrop{position:fixed;inset:0;z-index:100;display:grid;place-items:center;padding:16px;background:#05060899;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.ngxsp-dialog{width:min(420px,100%);display:grid;gap:12px;padding:20px;border:1px solid var(--_line);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);box-shadow:0 24px 64px #00000073}h2{margin:0;font-size:17px;font-weight:650;line-height:1.3}p{margin:0;color:var(--_muted);font-size:14px;line-height:1.5}.ngxsp-dialog-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px;margin-top:4px}.ngxsp-dialog-button{min-height:36px;padding:0 16px;border:1px solid var(--_line);border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:13px;font-weight:600;cursor:pointer}.ngxsp-dialog-button.primary{border-color:var(--_accent);background:var(--_accent);color:var(--_accent-text)}.ngxsp-dialog-button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
689
+ }
690
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartConfirmDialogComponent, decorators: [{
691
+ type: Component,
692
+ args: [{ selector: 'ngxsp-confirm-dialog', standalone: true, template: `
693
+ <div class="ngxsp-dialog-backdrop" role="presentation">
694
+ <section class="ngxsp-dialog" role="alertdialog" aria-modal="true" [attr.aria-labelledby]="id + '-title'" [attr.aria-describedby]="id + '-message'">
695
+ <h2 [id]="id + '-title'">{{ title }}</h2>
696
+ <p [id]="id + '-message'">{{ message }}</p>
697
+ <div class="ngxsp-dialog-actions">
698
+ <button #cancelButton class="ngxsp-dialog-button" type="button" (click)="cancelled.emit()">{{ cancelLabel }}</button>
699
+ <button class="ngxsp-dialog-button primary" type="button" (click)="confirmed.emit()">{{ confirmLabel }}</button>
700
+ </div>
701
+ </section>
702
+ </div>
703
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{--_surface:var(--ngxsp-dialog-surface, #17181d);--_text:var(--ngxsp-dialog-text, #ededf0);--_muted:var(--ngxsp-dialog-muted, #a3a5ae);--_line:var(--ngxsp-dialog-line, rgb(255 255 255 / .13));--_accent:var(--ngxsp-dialog-accent, #5b5bd6);--_accent-text:var(--ngxsp-dialog-accent-text, #fff);--_radius:var(--ngxsp-dialog-radius, 14px)}.ngxsp-dialog-backdrop{position:fixed;inset:0;z-index:100;display:grid;place-items:center;padding:16px;background:#05060899;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.ngxsp-dialog{width:min(420px,100%);display:grid;gap:12px;padding:20px;border:1px solid var(--_line);border-radius:var(--_radius);background:var(--_surface);color:var(--_text);box-shadow:0 24px 64px #00000073}h2{margin:0;font-size:17px;font-weight:650;line-height:1.3}p{margin:0;color:var(--_muted);font-size:14px;line-height:1.5}.ngxsp-dialog-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px;margin-top:4px}.ngxsp-dialog-button{min-height:36px;padding:0 16px;border:1px solid var(--_line);border-radius:999px;background:transparent;color:var(--_text);font:inherit;font-size:13px;font-weight:600;cursor:pointer}.ngxsp-dialog-button.primary{border-color:var(--_accent);background:var(--_accent);color:var(--_accent-text)}.ngxsp-dialog-button:focus-visible{outline:2px solid var(--_accent);outline-offset:2px}\n"] }]
704
+ }], propDecorators: { title: [{
705
+ type: Input
706
+ }], message: [{
707
+ type: Input
708
+ }], confirmLabel: [{
709
+ type: Input
710
+ }], cancelLabel: [{
711
+ type: Input
712
+ }], confirmed: [{
713
+ type: Output
714
+ }], cancelled: [{
715
+ type: Output
716
+ }], cancelButton: [{
717
+ type: ViewChild,
718
+ args: ['cancelButton']
719
+ }], onEscape: [{
720
+ type: HostListener,
721
+ args: ['document:keydown.escape', ['$event']]
722
+ }] } });
723
+
724
+ const DEFAULT_PLAYER_MENU_LABELS = {
725
+ captions: 'Captions',
726
+ off: 'Off',
727
+ audio: 'Audio',
728
+ singleAudio: 'Single audio track',
729
+ quality: 'Quality',
730
+ auto: 'Auto',
731
+ singleQuality: 'Single quality',
732
+ speed: 'Playback speed',
733
+ subtitles: 'Subtitles',
734
+ current: 'Showing',
735
+ noSubtitles: 'No subtitles in this video yet.',
736
+ loadSubtitles: 'Load a file (SRT, VTT)…',
737
+ tracks: 'Tracks',
738
+ media: 'Media',
739
+ };
740
+ /**
741
+ * Menu de pistes (sous-titres / audio / qualité) et de vitesse de lecture.
742
+ * Extrait du SmartPlayerComponent (décomposition, cf. docs/AUDIT.md).
743
+ * Sans logique de lecture : émet des événements, l'hôte applique l'action.
744
+ */
745
+ class PlayerMenuComponent {
746
+ openMenu = input(null, ...(ngDevMode ? [{ debugName: "openMenu" }] : []));
747
+ isAudioMode = input(false, ...(ngDevMode ? [{ debugName: "isAudioMode" }] : []));
748
+ textTracks = input([], ...(ngDevMode ? [{ debugName: "textTracks" }] : []));
749
+ audioTracks = input([], ...(ngDevMode ? [{ debugName: "audioTracks" }] : []));
750
+ videoTracks = input([], ...(ngDevMode ? [{ debugName: "videoTracks" }] : []));
751
+ selectedTextTrackId = input(null, ...(ngDevMode ? [{ debugName: "selectedTextTrackId" }] : []));
752
+ selectedAudioTrackId = input(null, ...(ngDevMode ? [{ debugName: "selectedAudioTrackId" }] : []));
753
+ selectedVideoTrackId = input(null, ...(ngDevMode ? [{ debugName: "selectedVideoTrackId" }] : []));
754
+ playbackRates = input([], ...(ngDevMode ? [{ debugName: "playbackRates" }] : []));
755
+ playbackRate = input(1, ...(ngDevMode ? [{ debugName: "playbackRate" }] : []));
756
+ labels = input(DEFAULT_PLAYER_MENU_LABELS, ...(ngDevMode ? [{ debugName: "labels" }] : []));
757
+ selectText = output();
758
+ selectAudio = output();
759
+ selectVideo = output();
760
+ selectRate = output();
761
+ /** "Load a file" in the subtitles menu: the host opens its file picker. */
762
+ loadSubtitles = output();
763
+ currentTextLabel() {
764
+ const track = this.textTracks().find((entry) => entry.id === this.selectedTextTrackId());
765
+ return track ? this.trackLabel(track) : this.labels().off;
766
+ }
767
+ /** "French" for "fr", in the labels' locale. */
768
+ languageName(code) {
769
+ try {
770
+ return new Intl.DisplayNames(this.labels().locale ? [this.labels().locale] : undefined, { type: 'language' }).of(code) ?? code;
771
+ }
772
+ catch {
773
+ return code;
774
+ }
775
+ }
776
+ trackLabel(track) {
777
+ return track.label || (track.language ? this.languageName(track.language) : track.id);
778
+ }
779
+ qualityLabel(track) {
780
+ if (track.label)
781
+ return track.label;
782
+ const parts = [];
783
+ if (track.height)
784
+ parts.push(`${track.height}p`);
785
+ if (track.bandwidth)
786
+ parts.push(`${(track.bandwidth / 1_000_000).toFixed(1)} Mbps`);
787
+ return parts.join(' • ') || track.id;
788
+ }
789
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
790
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerMenuComponent, isStandalone: true, selector: "ngxsp-player-menu", inputs: { openMenu: { classPropertyName: "openMenu", publicName: "openMenu", isSignal: true, isRequired: false, transformFunction: null }, isAudioMode: { classPropertyName: "isAudioMode", publicName: "isAudioMode", isSignal: true, isRequired: false, transformFunction: null }, textTracks: { classPropertyName: "textTracks", publicName: "textTracks", isSignal: true, isRequired: false, transformFunction: null }, audioTracks: { classPropertyName: "audioTracks", publicName: "audioTracks", isSignal: true, isRequired: false, transformFunction: null }, videoTracks: { classPropertyName: "videoTracks", publicName: "videoTracks", isSignal: true, isRequired: false, transformFunction: null }, selectedTextTrackId: { classPropertyName: "selectedTextTrackId", publicName: "selectedTextTrackId", isSignal: true, isRequired: false, transformFunction: null }, selectedAudioTrackId: { classPropertyName: "selectedAudioTrackId", publicName: "selectedAudioTrackId", isSignal: true, isRequired: false, transformFunction: null }, selectedVideoTrackId: { classPropertyName: "selectedVideoTrackId", publicName: "selectedVideoTrackId", isSignal: true, isRequired: false, transformFunction: null }, playbackRates: { classPropertyName: "playbackRates", publicName: "playbackRates", isSignal: true, isRequired: false, transformFunction: null }, playbackRate: { classPropertyName: "playbackRate", publicName: "playbackRate", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectText: "selectText", selectAudio: "selectAudio", selectVideo: "selectVideo", selectRate: "selectRate", loadSubtitles: "loadSubtitles" }, ngImport: i0, template: `
791
+ @if (openMenu() === 'tracks') {
792
+ <div class="ngxsp-menu">
793
+ <div class="ngxsp-menuSection">
794
+ <div class="ngxsp-menuTitle">{{ labels().captions }}</div>
795
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
796
+ @for (track of textTracks(); track track.id) {
797
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
798
+ {{ trackLabel(track) }}
799
+ </button>
800
+ }
801
+ </div>
802
+
803
+ <div class="ngxsp-menuSection">
804
+ <div class="ngxsp-menuTitle">{{ labels().audio }}</div>
805
+ @if (audioTracks().length) {
806
+ @for (track of audioTracks(); track track.id) {
807
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedAudioTrackId() === track.id" (click)="selectAudio.emit(track.id)">
808
+ {{ trackLabel(track) }}
809
+ </button>
810
+ }
811
+ } @else {
812
+ <div class="ngxsp-menuHint">{{ labels().singleAudio }}</div>
813
+ }
814
+ </div>
815
+
816
+ @if (!isAudioMode()) {
817
+ <div class="ngxsp-menuSection">
818
+ <div class="ngxsp-menuTitle">{{ labels().quality }}</div>
819
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === null" (click)="selectVideo.emit(null)">{{ labels().auto }}</button>
820
+ @if (videoTracks().length) {
821
+ @for (track of videoTracks(); track track.id) {
822
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === track.id" (click)="selectVideo.emit(track.id)">
823
+ {{ qualityLabel(track) }}
824
+ </button>
825
+ }
826
+ } @else {
827
+ <div class="ngxsp-menuHint">{{ labels().singleQuality }}</div>
828
+ }
829
+ </div>
830
+ }
831
+ </div>
832
+ }
833
+
834
+ @if (openMenu() === 'speed') {
835
+ <div class="ngxsp-menu ngxsp-menuCompact">
836
+ <div class="ngxsp-menuSection">
837
+ <div class="ngxsp-menuTitle">{{ labels().speed }}</div>
838
+ @for (rate of playbackRates(); track rate) {
839
+ <button type="button" class="ngxsp-menuItem" [class.active]="playbackRate() === rate" (click)="selectRate.emit(rate)">
840
+ {{ rate }}x
841
+ </button>
842
+ }
843
+ </div>
844
+ </div>
845
+ }
846
+
847
+ @if (openMenu() === 'captions') {
848
+ <div class="ngxsp-menu ngxsp-menuCompact ngxsp-menuCaptions">
849
+ <div class="ngxsp-menuSection">
850
+ <div class="ngxsp-menuTitle">{{ labels().subtitles }}</div>
851
+ <div class="ngxsp-menuCurrent">{{ labels().current }} <strong>{{ currentTextLabel() }}</strong></div>
852
+ <div class="ngxsp-menuScroll" role="group" [attr.aria-label]="labels().subtitles">
853
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" [attr.aria-pressed]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
854
+ @for (track of textTracks(); track track.id) {
855
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" [attr.aria-pressed]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
856
+ <span class="ngxsp-menuItemLabel">{{ trackLabel(track) }}</span>
857
+ @if (track.language && track.label) { <small>{{ languageName(track.language) }}</small> }
858
+ </button>
859
+ }
860
+ </div>
861
+ @if (!textTracks().length) { <div class="ngxsp-menuHint">{{ labels().noSubtitles }}</div> }
862
+ <button type="button" class="ngxsp-menuItem ngxsp-menuAction" (click)="loadSubtitles.emit()">{{ labels().loadSubtitles }}</button>
863
+ </div>
864
+ </div>
865
+ }
866
+ `, isInline: true, styles: [":host{display:block}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin:6px 0 4px;padding:12px;border-radius:10px;background:#121317eb;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.ngxsp-menuCompact{grid-template-columns:minmax(160px,220px);justify-self:end}.ngxsp-menuSection{display:flex;flex-direction:column;gap:2px}.ngxsp-menuTitle{padding:0 8px 6px;font-size:12px;font-weight:600;color:#fff9}.ngxsp-menuItem{height:32px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:#ffffffe6;text-align:left;cursor:pointer;font:inherit;font-size:13px}.ngxsp-menuItem:hover{background:#ffffff1a}.ngxsp-menuItem.active{background:#8b90ff2e;color:var(--nsp-media-accent, #8b90ff);font-weight:500}.ngxsp-menuItem:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:-2px}.ngxsp-menuHint{font-size:12px;color:#fff9;padding:6px 8px}.ngxsp-menuCaptions{grid-template-columns:minmax(200px,280px)}.ngxsp-menuCurrent{padding:0 8px 8px;font-size:12px;color:#fff9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuCurrent strong{color:#ffffffeb;font-weight:600}.ngxsp-menuScroll{display:flex;flex-direction:column;gap:2px;max-height:min(36vh,232px);overflow:auto;overscroll-behavior:contain}.ngxsp-menuItem{display:flex;align-items:center;gap:8px;flex:none}.ngxsp-menuItemLabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuItem small{flex:none;font-size:11px;color:#ffffff8c}.ngxsp-menuAction{margin-top:6px;border-top:1px solid rgb(255 255 255 / .1);border-radius:0 0 6px 6px;color:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-menu{grid-template-columns:1fr}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
867
+ }
868
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerMenuComponent, decorators: [{
869
+ type: Component,
870
+ args: [{ selector: 'ngxsp-player-menu', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
871
+ @if (openMenu() === 'tracks') {
872
+ <div class="ngxsp-menu">
873
+ <div class="ngxsp-menuSection">
874
+ <div class="ngxsp-menuTitle">{{ labels().captions }}</div>
875
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
876
+ @for (track of textTracks(); track track.id) {
877
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
878
+ {{ trackLabel(track) }}
879
+ </button>
880
+ }
881
+ </div>
882
+
883
+ <div class="ngxsp-menuSection">
884
+ <div class="ngxsp-menuTitle">{{ labels().audio }}</div>
885
+ @if (audioTracks().length) {
886
+ @for (track of audioTracks(); track track.id) {
887
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedAudioTrackId() === track.id" (click)="selectAudio.emit(track.id)">
888
+ {{ trackLabel(track) }}
889
+ </button>
890
+ }
891
+ } @else {
892
+ <div class="ngxsp-menuHint">{{ labels().singleAudio }}</div>
893
+ }
894
+ </div>
895
+
896
+ @if (!isAudioMode()) {
897
+ <div class="ngxsp-menuSection">
898
+ <div class="ngxsp-menuTitle">{{ labels().quality }}</div>
899
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === null" (click)="selectVideo.emit(null)">{{ labels().auto }}</button>
900
+ @if (videoTracks().length) {
901
+ @for (track of videoTracks(); track track.id) {
902
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId() === track.id" (click)="selectVideo.emit(track.id)">
903
+ {{ qualityLabel(track) }}
904
+ </button>
905
+ }
906
+ } @else {
907
+ <div class="ngxsp-menuHint">{{ labels().singleQuality }}</div>
908
+ }
909
+ </div>
910
+ }
911
+ </div>
912
+ }
913
+
914
+ @if (openMenu() === 'speed') {
915
+ <div class="ngxsp-menu ngxsp-menuCompact">
916
+ <div class="ngxsp-menuSection">
917
+ <div class="ngxsp-menuTitle">{{ labels().speed }}</div>
918
+ @for (rate of playbackRates(); track rate) {
919
+ <button type="button" class="ngxsp-menuItem" [class.active]="playbackRate() === rate" (click)="selectRate.emit(rate)">
920
+ {{ rate }}x
921
+ </button>
922
+ }
923
+ </div>
924
+ </div>
925
+ }
926
+
927
+ @if (openMenu() === 'captions') {
928
+ <div class="ngxsp-menu ngxsp-menuCompact ngxsp-menuCaptions">
929
+ <div class="ngxsp-menuSection">
930
+ <div class="ngxsp-menuTitle">{{ labels().subtitles }}</div>
931
+ <div class="ngxsp-menuCurrent">{{ labels().current }} <strong>{{ currentTextLabel() }}</strong></div>
932
+ <div class="ngxsp-menuScroll" role="group" [attr.aria-label]="labels().subtitles">
933
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === null" [attr.aria-pressed]="selectedTextTrackId() === null" (click)="selectText.emit(null)">{{ labels().off }}</button>
934
+ @for (track of textTracks(); track track.id) {
935
+ <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId() === track.id" [attr.aria-pressed]="selectedTextTrackId() === track.id" (click)="selectText.emit(track.id)">
936
+ <span class="ngxsp-menuItemLabel">{{ trackLabel(track) }}</span>
937
+ @if (track.language && track.label) { <small>{{ languageName(track.language) }}</small> }
938
+ </button>
939
+ }
940
+ </div>
941
+ @if (!textTracks().length) { <div class="ngxsp-menuHint">{{ labels().noSubtitles }}</div> }
942
+ <button type="button" class="ngxsp-menuItem ngxsp-menuAction" (click)="loadSubtitles.emit()">{{ labels().loadSubtitles }}</button>
943
+ </div>
944
+ </div>
945
+ }
946
+ `, styles: [":host{display:block}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin:6px 0 4px;padding:12px;border-radius:10px;background:#121317eb;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)}.ngxsp-menuCompact{grid-template-columns:minmax(160px,220px);justify-self:end}.ngxsp-menuSection{display:flex;flex-direction:column;gap:2px}.ngxsp-menuTitle{padding:0 8px 6px;font-size:12px;font-weight:600;color:#fff9}.ngxsp-menuItem{height:32px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:#ffffffe6;text-align:left;cursor:pointer;font:inherit;font-size:13px}.ngxsp-menuItem:hover{background:#ffffff1a}.ngxsp-menuItem.active{background:#8b90ff2e;color:var(--nsp-media-accent, #8b90ff);font-weight:500}.ngxsp-menuItem:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:-2px}.ngxsp-menuHint{font-size:12px;color:#fff9;padding:6px 8px}.ngxsp-menuCaptions{grid-template-columns:minmax(200px,280px)}.ngxsp-menuCurrent{padding:0 8px 8px;font-size:12px;color:#fff9;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuCurrent strong{color:#ffffffeb;font-weight:600}.ngxsp-menuScroll{display:flex;flex-direction:column;gap:2px;max-height:min(36vh,232px);overflow:auto;overscroll-behavior:contain}.ngxsp-menuItem{display:flex;align-items:center;gap:8px;flex:none}.ngxsp-menuItemLabel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ngxsp-menuItem small{flex:none;font-size:11px;color:#ffffff8c}.ngxsp-menuAction{margin-top:6px;border-top:1px solid rgb(255 255 255 / .1);border-radius:0 0 6px 6px;color:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-menu{grid-template-columns:1fr}}\n"] }]
947
+ }], propDecorators: { openMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "openMenu", required: false }] }], isAudioMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAudioMode", required: false }] }], textTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "textTracks", required: false }] }], audioTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "audioTracks", required: false }] }], videoTracks: [{ type: i0.Input, args: [{ isSignal: true, alias: "videoTracks", required: false }] }], selectedTextTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedTextTrackId", required: false }] }], selectedAudioTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedAudioTrackId", required: false }] }], selectedVideoTrackId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedVideoTrackId", required: false }] }], playbackRates: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRates", required: false }] }], playbackRate: [{ type: i0.Input, args: [{ isSignal: true, alias: "playbackRate", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], selectText: [{ type: i0.Output, args: ["selectText"] }], selectAudio: [{ type: i0.Output, args: ["selectAudio"] }], selectVideo: [{ type: i0.Output, args: ["selectVideo"] }], selectRate: [{ type: i0.Output, args: ["selectRate"] }], loadSubtitles: [{ type: i0.Output, args: ["loadSubtitles"] }] } });
948
+
949
+ /**
950
+ * Surcouches du lecteur : carte d'erreur de lecture et panneau de diagnostic.
951
+ * Extrait du SmartPlayerComponent (décomposition, cf. docs/AUDIT.md).
952
+ * Sans accès au moteur : reçoit l'erreur/diagnostic en entrée et émet les actions.
953
+ */
954
+ class PlayerOverlaysComponent {
955
+ error = input(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
956
+ debug = input(false, ...(ngDevMode ? [{ debugName: "debug" }] : []));
957
+ debugJson = input('{}', ...(ngDevMode ? [{ debugName: "debugJson" }] : []));
958
+ retry = output();
959
+ requestPlay = output();
960
+ copyJson = output();
961
+ downloadJson = output();
962
+ code = computed(() => this.error()?.code ?? 'UNKNOWN', ...(ngDevMode ? [{ debugName: "code" }] : []));
963
+ autoplayBlocked = computed(() => this.code() === 'AUTOPLAY_BLOCKED', ...(ngDevMode ? [{ debugName: "autoplayBlocked" }] : []));
964
+ title = computed(() => {
965
+ switch (this.code()) {
966
+ case 'ENGINE_MISSING':
967
+ return 'Playback engine missing';
968
+ case 'AUTOPLAY_BLOCKED':
969
+ return 'Click to start playback';
970
+ case 'NETWORK_HTTP':
971
+ return 'Network error';
972
+ case 'DECODE_ERROR':
973
+ return 'Decode error';
974
+ case 'SRC_NOT_SUPPORTED':
975
+ return 'Format not supported';
976
+ default:
977
+ return 'Playback error';
978
+ }
979
+ }, ...(ngDevMode ? [{ debugName: "title" }] : []));
980
+ hint = computed(() => {
981
+ switch (this.code()) {
982
+ case 'ENGINE_MISSING':
983
+ return 'This source requires an additional engine. Install @ngxsp/engine-shaka and pass it via [engines].';
984
+ case 'AUTOPLAY_BLOCKED':
985
+ return 'Your browser blocked autoplay. Press Play to start.';
986
+ case 'NETWORK_HTTP':
987
+ return 'Check your connection or try again.';
988
+ default:
989
+ return this.error()?.message ? String(this.error().message) : 'Unknown error';
990
+ }
991
+ }, ...(ngDevMode ? [{ debugName: "hint" }] : []));
992
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerOverlaysComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
993
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerOverlaysComponent, isStandalone: true, selector: "ngxsp-player-overlays", inputs: { error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, debugJson: { classPropertyName: "debugJson", publicName: "debugJson", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { retry: "retry", requestPlay: "requestPlay", copyJson: "copyJson", downloadJson: "downloadJson" }, ngImport: i0, template: `
994
+ @if (error()) {
995
+ <div class="ngxsp-overlay" role="alert">
996
+ <div class="ngxsp-overlay-card">
997
+ <span class="ngxsp-overlay-icon" aria-hidden="true">!</span>
998
+ <div class="ngxsp-overlay-title">{{ title() }}</div>
999
+ <div class="ngxsp-overlay-msg">{{ hint() }}</div>
1000
+ <div class="ngxsp-overlay-actions">
1001
+ <button type="button" class="ngxsp-btn" (click)="retry.emit()">Retry</button>
1002
+ @if (autoplayBlocked()) {
1003
+ <button type="button" class="ngxsp-btn primary" (click)="requestPlay.emit()">Play</button>
1004
+ }
1005
+ </div>
1006
+ </div>
1007
+ </div>
1008
+ }
1009
+
1010
+ @if (debug()) {
1011
+ <details class="ngxsp-debug">
1012
+ <summary>Debug</summary>
1013
+ <div class="ngxsp-debug-actions">
1014
+ <button type="button" class="ngxsp-btn" (click)="copyJson.emit()">Copy JSON</button>
1015
+ <button type="button" class="ngxsp-btn" (click)="downloadJson.emit()">Download</button>
1016
+ </div>
1017
+ <pre>{{ debugJson() }}</pre>
1018
+ </details>
1019
+ }
1020
+ `, isInline: true, styles: [":host{display:contents}.ngxsp-btn{height:32px;padding:0 14px;border:1px solid rgb(255 255 255 / .16);border-radius:6px;background:#ffffff0f;color:#fff;font:inherit;font-size:13px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn:hover{background:#ffffff1f}.ngxsp-btn.primary{border-color:transparent;background:var(--nsp-accent-strong, #5d63ee)}.ngxsp-btn:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:2px}.ngxsp-overlay{position:absolute;inset:0;z-index:5;display:grid;place-items:center;padding:24px;background:#000000c7;text-align:center}.ngxsp-overlay-card{display:grid;justify-items:center;gap:8px;max-width:440px}.ngxsp-overlay-icon{width:40px;height:40px;display:grid;place-items:center;margin-bottom:4px;border-radius:999px;background:#f25f5c29;color:var(--nsp-danger, #f25f5c);font-size:18px;font-weight:700}.ngxsp-overlay-title{font-size:16px;font-weight:600;color:#fff}.ngxsp-overlay-msg{color:#ffffffb3;font-size:13px;line-height:1.5}.ngxsp-overlay-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:8px}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease;color:#fff}:host-context(.ngxsp-ui-visible) .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;width:max-content;margin-left:auto;padding:4px 10px;border-radius:999px;background:#0009;color:#fffc;font-size:12px}.ngxsp-debug[open]{padding:12px;max-height:calc(100% - 24px);overflow:auto;border-radius:10px;background:#121317f5;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14}.ngxsp-debug-actions{display:flex;gap:8px;margin:8px 0;flex-wrap:wrap}.ngxsp-debug pre{margin:0;white-space:pre-wrap;word-break:break-word;font-family:var(--nsp-font-mono, ui-monospace, Menlo, monospace);font-size:11px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1021
+ }
1022
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerOverlaysComponent, decorators: [{
1023
+ type: Component,
1024
+ args: [{ selector: 'ngxsp-player-overlays', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
1025
+ @if (error()) {
1026
+ <div class="ngxsp-overlay" role="alert">
1027
+ <div class="ngxsp-overlay-card">
1028
+ <span class="ngxsp-overlay-icon" aria-hidden="true">!</span>
1029
+ <div class="ngxsp-overlay-title">{{ title() }}</div>
1030
+ <div class="ngxsp-overlay-msg">{{ hint() }}</div>
1031
+ <div class="ngxsp-overlay-actions">
1032
+ <button type="button" class="ngxsp-btn" (click)="retry.emit()">Retry</button>
1033
+ @if (autoplayBlocked()) {
1034
+ <button type="button" class="ngxsp-btn primary" (click)="requestPlay.emit()">Play</button>
1035
+ }
1036
+ </div>
1037
+ </div>
1038
+ </div>
1039
+ }
1040
+
1041
+ @if (debug()) {
1042
+ <details class="ngxsp-debug">
1043
+ <summary>Debug</summary>
1044
+ <div class="ngxsp-debug-actions">
1045
+ <button type="button" class="ngxsp-btn" (click)="copyJson.emit()">Copy JSON</button>
1046
+ <button type="button" class="ngxsp-btn" (click)="downloadJson.emit()">Download</button>
1047
+ </div>
1048
+ <pre>{{ debugJson() }}</pre>
1049
+ </details>
1050
+ }
1051
+ `, styles: [":host{display:contents}.ngxsp-btn{height:32px;padding:0 14px;border:1px solid rgb(255 255 255 / .16);border-radius:6px;background:#ffffff0f;color:#fff;font:inherit;font-size:13px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn:hover{background:#ffffff1f}.ngxsp-btn.primary{border-color:transparent;background:var(--nsp-accent-strong, #5d63ee)}.ngxsp-btn:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:2px}.ngxsp-overlay{position:absolute;inset:0;z-index:5;display:grid;place-items:center;padding:24px;background:#000000c7;text-align:center}.ngxsp-overlay-card{display:grid;justify-items:center;gap:8px;max-width:440px}.ngxsp-overlay-icon{width:40px;height:40px;display:grid;place-items:center;margin-bottom:4px;border-radius:999px;background:#f25f5c29;color:var(--nsp-danger, #f25f5c);font-size:18px;font-weight:700}.ngxsp-overlay-title{font-size:16px;font-weight:600;color:#fff}.ngxsp-overlay-msg{color:#ffffffb3;font-size:13px;line-height:1.5}.ngxsp-overlay-actions{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:8px}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease;color:#fff}:host-context(.ngxsp-ui-visible) .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;width:max-content;margin-left:auto;padding:4px 10px;border-radius:999px;background:#0009;color:#fffc;font-size:12px}.ngxsp-debug[open]{padding:12px;max-height:calc(100% - 24px);overflow:auto;border-radius:10px;background:#121317f5;box-shadow:0 12px 32px #00000073,0 0 0 1px #ffffff14}.ngxsp-debug-actions{display:flex;gap:8px;margin:8px 0;flex-wrap:wrap}.ngxsp-debug pre{margin:0;white-space:pre-wrap;word-break:break-word;font-family:var(--nsp-font-mono, ui-monospace, Menlo, monospace);font-size:11px}\n"] }]
1052
+ }], propDecorators: { error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], debugJson: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugJson", required: false }] }], retry: [{ type: i0.Output, args: ["retry"] }], requestPlay: [{ type: i0.Output, args: ["requestPlay"] }], copyJson: [{ type: i0.Output, args: ["copyJson"] }], downloadJson: [{ type: i0.Output, args: ["downloadJson"] }] } });
1053
+
1054
+ /**
1055
+ * Barre de contrôle du skin classique, extraite du SmartPlayerComponent
1056
+ * (décomposition, cf. docs/AUDIT.md). Sans accès au moteur ni au <video> :
1057
+ * reçoit l'état + les capacités (cast/PiP) en entrée et émet les commandes ;
1058
+ * l'hôte applique l'action (et possède l'élément vidéo pour PiP/cast).
1059
+ */
1060
+ class PlayerControlsComponent {
1061
+ playing = input(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
1062
+ isAudioMode = input(false, ...(ngDevMode ? [{ debugName: "isAudioMode" }] : []));
1063
+ hasPlaylistControls = input(false, ...(ngDevMode ? [{ debugName: "hasPlaylistControls" }] : []));
1064
+ canPrev = input(false, ...(ngDevMode ? [{ debugName: "canPrev" }] : []));
1065
+ canNext = input(false, ...(ngDevMode ? [{ debugName: "canNext" }] : []));
1066
+ shuffleActive = input(false, ...(ngDevMode ? [{ debugName: "shuffleActive" }] : []));
1067
+ shuffleLabel = input('Shuffle: off', ...(ngDevMode ? [{ debugName: "shuffleLabel" }] : []));
1068
+ repeatMode = input('off', ...(ngDevMode ? [{ debugName: "repeatMode" }] : []));
1069
+ repeatLabel = input('Repeat: off', ...(ngDevMode ? [{ debugName: "repeatLabel" }] : []));
1070
+ captionsActive = input(false, ...(ngDevMode ? [{ debugName: "captionsActive" }] : []));
1071
+ tracksLabel = input('Tracks', ...(ngDevMode ? [{ debugName: "tracksLabel" }] : []));
1072
+ captionsLabel = input('Load subtitles', ...(ngDevMode ? [{ debugName: "captionsLabel" }] : []));
1073
+ speedLabel = input('1x', ...(ngDevMode ? [{ debugName: "speedLabel" }] : []));
1074
+ volumeLabel = input('Volume', ...(ngDevMode ? [{ debugName: "volumeLabel" }] : []));
1075
+ muteLabel = input('Mute', ...(ngDevMode ? [{ debugName: "muteLabel" }] : []));
1076
+ unmuteLabel = input('Restore sound', ...(ngDevMode ? [{ debugName: "unmuteLabel" }] : []));
1077
+ showAirPlay = input(false, ...(ngDevMode ? [{ debugName: "showAirPlay" }] : []));
1078
+ airPlayActive = input(false, ...(ngDevMode ? [{ debugName: "airPlayActive" }] : []));
1079
+ airPlayLabel = input('AirPlay', ...(ngDevMode ? [{ debugName: "airPlayLabel" }] : []));
1080
+ showCast = input(false, ...(ngDevMode ? [{ debugName: "showCast" }] : []));
1081
+ castActive = input(false, ...(ngDevMode ? [{ debugName: "castActive" }] : []));
1082
+ castLabel = input('Play on device', ...(ngDevMode ? [{ debugName: "castLabel" }] : []));
1083
+ castHint = input('', ...(ngDevMode ? [{ debugName: "castHint" }] : []));
1084
+ pipSupported = input(false, ...(ngDevMode ? [{ debugName: "pipSupported" }] : []));
1085
+ pipActive = input(false, ...(ngDevMode ? [{ debugName: "pipActive" }] : []));
1086
+ pipLabel = input('PiP', ...(ngDevMode ? [{ debugName: "pipLabel" }] : []));
1087
+ volume = input(1, ...(ngDevMode ? [{ debugName: "volume" }] : []));
1088
+ currentTime = input(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1089
+ duration = input(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
1090
+ buffered = input(0, ...(ngDevMode ? [{ debugName: "buffered" }] : []));
1091
+ profile = input('vod', ...(ngDevMode ? [{ debugName: "profile" }] : []));
1092
+ playToggle = output();
1093
+ seekRelative = output();
1094
+ prev = output();
1095
+ next = output();
1096
+ toggleShuffle = output();
1097
+ cycleRepeat = output();
1098
+ openMenu = output();
1099
+ captions = output();
1100
+ airPlay = output();
1101
+ cast = output();
1102
+ pip = output();
1103
+ mute = output();
1104
+ scrub = output();
1105
+ volumeChange = output();
1106
+ fmtTime(sec) {
1107
+ const s = Math.max(0, Math.floor(sec || 0));
1108
+ const h = Math.floor(s / 3600);
1109
+ const m = Math.floor((s % 3600) / 60);
1110
+ const r = s % 60;
1111
+ if (h > 0)
1112
+ return `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
1113
+ return `${m}:${String(r).padStart(2, '0')}`;
1114
+ }
1115
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1116
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerControlsComponent, isStandalone: true, selector: "ngxsp-player-controls", inputs: { playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, isAudioMode: { classPropertyName: "isAudioMode", publicName: "isAudioMode", isSignal: true, isRequired: false, transformFunction: null }, hasPlaylistControls: { classPropertyName: "hasPlaylistControls", publicName: "hasPlaylistControls", isSignal: true, isRequired: false, transformFunction: null }, canPrev: { classPropertyName: "canPrev", publicName: "canPrev", isSignal: true, isRequired: false, transformFunction: null }, canNext: { classPropertyName: "canNext", publicName: "canNext", isSignal: true, isRequired: false, transformFunction: null }, shuffleActive: { classPropertyName: "shuffleActive", publicName: "shuffleActive", isSignal: true, isRequired: false, transformFunction: null }, shuffleLabel: { classPropertyName: "shuffleLabel", publicName: "shuffleLabel", isSignal: true, isRequired: false, transformFunction: null }, repeatMode: { classPropertyName: "repeatMode", publicName: "repeatMode", isSignal: true, isRequired: false, transformFunction: null }, repeatLabel: { classPropertyName: "repeatLabel", publicName: "repeatLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsActive: { classPropertyName: "captionsActive", publicName: "captionsActive", isSignal: true, isRequired: false, transformFunction: null }, tracksLabel: { classPropertyName: "tracksLabel", publicName: "tracksLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsLabel: { classPropertyName: "captionsLabel", publicName: "captionsLabel", isSignal: true, isRequired: false, transformFunction: null }, speedLabel: { classPropertyName: "speedLabel", publicName: "speedLabel", isSignal: true, isRequired: false, transformFunction: null }, volumeLabel: { classPropertyName: "volumeLabel", publicName: "volumeLabel", isSignal: true, isRequired: false, transformFunction: null }, muteLabel: { classPropertyName: "muteLabel", publicName: "muteLabel", isSignal: true, isRequired: false, transformFunction: null }, unmuteLabel: { classPropertyName: "unmuteLabel", publicName: "unmuteLabel", isSignal: true, isRequired: false, transformFunction: null }, showAirPlay: { classPropertyName: "showAirPlay", publicName: "showAirPlay", isSignal: true, isRequired: false, transformFunction: null }, airPlayActive: { classPropertyName: "airPlayActive", publicName: "airPlayActive", isSignal: true, isRequired: false, transformFunction: null }, airPlayLabel: { classPropertyName: "airPlayLabel", publicName: "airPlayLabel", isSignal: true, isRequired: false, transformFunction: null }, showCast: { classPropertyName: "showCast", publicName: "showCast", isSignal: true, isRequired: false, transformFunction: null }, castActive: { classPropertyName: "castActive", publicName: "castActive", isSignal: true, isRequired: false, transformFunction: null }, castLabel: { classPropertyName: "castLabel", publicName: "castLabel", isSignal: true, isRequired: false, transformFunction: null }, castHint: { classPropertyName: "castHint", publicName: "castHint", isSignal: true, isRequired: false, transformFunction: null }, pipSupported: { classPropertyName: "pipSupported", publicName: "pipSupported", isSignal: true, isRequired: false, transformFunction: null }, pipActive: { classPropertyName: "pipActive", publicName: "pipActive", isSignal: true, isRequired: false, transformFunction: null }, pipLabel: { classPropertyName: "pipLabel", publicName: "pipLabel", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, buffered: { classPropertyName: "buffered", publicName: "buffered", isSignal: true, isRequired: false, transformFunction: null }, profile: { classPropertyName: "profile", publicName: "profile", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playToggle: "playToggle", seekRelative: "seekRelative", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", openMenu: "openMenu", captions: "captions", airPlay: "airPlay", cast: "cast", pip: "pip", mute: "mute", scrub: "scrub", volumeChange: "volumeChange" }, ngImport: i0, template: `
1117
+ <div class="ngxsp-controls">
1118
+ <ngxsp-play-button class="ngxsp-btn" [playing]="playing()" (playToggle)="playToggle.emit()">
1119
+ @if (playing()) {
1120
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1121
+ } @else {
1122
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1123
+ }
1124
+ </ngxsp-play-button>
1125
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
1126
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1127
+ </button>
1128
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
1129
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1130
+ </button>
1131
+ @if (hasPlaylistControls()) {
1132
+ <button type="button" class="ngxsp-btn" (click)="prev.emit()" [disabled]="!canPrev()" aria-label="Previous item" title="Previous item"><ngxsp-smart-icon class="ngxsp-icon" name="prev" /></button>
1133
+ <button type="button" class="ngxsp-btn" (click)="next.emit()" [disabled]="!canNext()" aria-label="Next item" title="Next item"><ngxsp-smart-icon class="ngxsp-icon" name="next" /></button>
1134
+ <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1135
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1136
+ </button>
1137
+ <button type="button" class="ngxsp-btn" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1138
+ @if (repeatMode() === 'one') {
1139
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1140
+ } @else {
1141
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1142
+ }
1143
+ </button>
1144
+ }
1145
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="tracks" /></button>
1146
+ <button type="button" class="ngxsp-btn" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1147
+ @if (captionsActive()) {
1148
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1149
+ } @else {
1150
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1151
+ }
1152
+ </button>
1153
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="speed" /></button>
1154
+ @if (showAirPlay()) {
1155
+ <button type="button" class="ngxsp-btn" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1156
+ @if (airPlayActive()) {
1157
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay-active" />
1158
+ } @else {
1159
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay" />
1160
+ }
1161
+ </button>
1162
+ }
1163
+ @if (showCast()) {
1164
+ <button type="button" class="ngxsp-btn" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1165
+ @if (castActive()) {
1166
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1167
+ } @else {
1168
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1169
+ }
1170
+ </button>
1171
+ }
1172
+ @if (!isAudioMode() && pipSupported()) {
1173
+ <button type="button" class="ngxsp-btn" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1174
+ @if (pipActive()) {
1175
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1176
+ } @else {
1177
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1178
+ }
1179
+ </button>
1180
+ }
1181
+ <ngxsp-volume-control
1182
+ class="ngxsp-volumeControl"
1183
+ [volume]="volume()"
1184
+ [volumeLabel]="volumeLabel()"
1185
+ [muteLabel]="muteLabel()"
1186
+ [unmuteLabel]="unmuteLabel()"
1187
+ (muteToggle)="mute.emit()"
1188
+ (volumeChange)="volumeChange.emit($event)"
1189
+ />
1190
+
1191
+ <ngxsp-time-slider
1192
+ style="flex:1 1 160px; min-width:160px"
1193
+ [currentTime]="currentTime()"
1194
+ [duration]="duration()"
1195
+ [buffered]="buffered()"
1196
+ [profile]="profile()"
1197
+ (seek)="scrub.emit($event)"
1198
+ />
1199
+
1200
+ <div class="ngxsp-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1201
+
1202
+ </div>
1203
+ `, isInline: true, styles: [":host{display:block}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0d,0 10px 24px #0206172e;transition:border-color .18s ease,background .18s ease,transform .18s ease,color .18s ease}.ngxsp-btn.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong, rgba(191,219,254,.24));color:#d8f3ff}.ngxsp-btn:hover{border-color:#bfdbfe57;background:linear-gradient(180deg,#182e47f0,#0c1b2cf0)}.ngxsp-btn:disabled{opacity:.38;cursor:not-allowed}.ngxsp-btn:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-time{color:var(--ngxsp-text-dim, rgba(230,238,248,.72));font-variant-numeric:tabular-nums}.ngxsp-volumeControl{--ngxsp-control-size: 38px;--ngxsp-volume-range-max: 140px;--ngxsp-volume-button-bg: linear-gradient(180deg, rgba(19,34,54,.92), rgba(11,21,35,.92));--ngxsp-volume-button-border: 1px solid var(--ngxsp-border, rgba(148,163,184,.18));--ngxsp-volume-accent: var(--nsp-accent, #7dd3fc);--ngxsp-volume-thumb-bg: var(--nsp-text, #f8fbff);color:var(--ngxsp-text, #e6eef8)}@media(max-width:640px){.ngxsp-controls{padding:8px;gap:6px}.ngxsp-btn{padding:7px 9px;border-radius:10px}.ngxsp-icon{width:16px;height:16px}}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: NgxspPlayButton, selector: "ngxsp-play-button", inputs: ["playing", "disabled", "playLabel", "pauseLabel"], outputs: ["playToggle"] }, { kind: "component", type: NgxspTimeSlider, selector: "ngxsp-time-slider", inputs: ["currentTime", "duration", "buffered", "profile", "disabled", "durationHintSec", "markers", "step", "bigStep", "ariaLabel", "liveLabel"], outputs: ["seek"] }, { kind: "component", type: NgxspVolumeControl, selector: "ngxsp-volume-control", inputs: ["volume", "muted", "disabled", "compact", "showValue", "step", "volumeLabel", "muteLabel", "unmuteLabel"], outputs: ["volumeChange", "muteToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1204
+ }
1205
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsComponent, decorators: [{
1206
+ type: Component,
1207
+ args: [{ selector: 'ngxsp-player-controls', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SmartPlayerIconComponent, NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl], template: `
1208
+ <div class="ngxsp-controls">
1209
+ <ngxsp-play-button class="ngxsp-btn" [playing]="playing()" (playToggle)="playToggle.emit()">
1210
+ @if (playing()) {
1211
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1212
+ } @else {
1213
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1214
+ }
1215
+ </ngxsp-play-button>
1216
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
1217
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1218
+ </button>
1219
+ <button type="button" class="ngxsp-btn" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
1220
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1221
+ </button>
1222
+ @if (hasPlaylistControls()) {
1223
+ <button type="button" class="ngxsp-btn" (click)="prev.emit()" [disabled]="!canPrev()" aria-label="Previous item" title="Previous item"><ngxsp-smart-icon class="ngxsp-icon" name="prev" /></button>
1224
+ <button type="button" class="ngxsp-btn" (click)="next.emit()" [disabled]="!canNext()" aria-label="Next item" title="Next item"><ngxsp-smart-icon class="ngxsp-icon" name="next" /></button>
1225
+ <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1226
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1227
+ </button>
1228
+ <button type="button" class="ngxsp-btn" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1229
+ @if (repeatMode() === 'one') {
1230
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1231
+ } @else {
1232
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1233
+ }
1234
+ </button>
1235
+ }
1236
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="tracks" /></button>
1237
+ <button type="button" class="ngxsp-btn" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1238
+ @if (captionsActive()) {
1239
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1240
+ } @else {
1241
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1242
+ }
1243
+ </button>
1244
+ <button type="button" class="ngxsp-btn" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()"><ngxsp-smart-icon class="ngxsp-icon" name="speed" /></button>
1245
+ @if (showAirPlay()) {
1246
+ <button type="button" class="ngxsp-btn" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1247
+ @if (airPlayActive()) {
1248
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay-active" />
1249
+ } @else {
1250
+ <ngxsp-smart-icon class="ngxsp-icon" name="airplay" />
1251
+ }
1252
+ </button>
1253
+ }
1254
+ @if (showCast()) {
1255
+ <button type="button" class="ngxsp-btn" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1256
+ @if (castActive()) {
1257
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast-active" />
1258
+ } @else {
1259
+ <ngxsp-smart-icon class="ngxsp-icon" name="cast" />
1260
+ }
1261
+ </button>
1262
+ }
1263
+ @if (!isAudioMode() && pipSupported()) {
1264
+ <button type="button" class="ngxsp-btn" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1265
+ @if (pipActive()) {
1266
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1267
+ } @else {
1268
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1269
+ }
1270
+ </button>
1271
+ }
1272
+ <ngxsp-volume-control
1273
+ class="ngxsp-volumeControl"
1274
+ [volume]="volume()"
1275
+ [volumeLabel]="volumeLabel()"
1276
+ [muteLabel]="muteLabel()"
1277
+ [unmuteLabel]="unmuteLabel()"
1278
+ (muteToggle)="mute.emit()"
1279
+ (volumeChange)="volumeChange.emit($event)"
1280
+ />
1281
+
1282
+ <ngxsp-time-slider
1283
+ style="flex:1 1 160px; min-width:160px"
1284
+ [currentTime]="currentTime()"
1285
+ [duration]="duration()"
1286
+ [buffered]="buffered()"
1287
+ [profile]="profile()"
1288
+ (seek)="scrub.emit($event)"
1289
+ />
1290
+
1291
+ <div class="ngxsp-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1292
+
1293
+ </div>
1294
+ `, styles: [":host{display:block}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:linear-gradient(180deg,#132236eb,#0b1523eb);border:1px solid var(--ngxsp-border, rgba(148,163,184,.18));color:var(--ngxsp-text, #e6eef8);border-radius:12px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;box-shadow:inset 0 1px #ffffff0d,0 10px 24px #0206172e;transition:border-color .18s ease,background .18s ease,transform .18s ease,color .18s ease}.ngxsp-btn.accent{background:linear-gradient(180deg,#14304af5,#0d2135f5);border-color:var(--ngxsp-border-strong, rgba(191,219,254,.24));color:#d8f3ff}.ngxsp-btn:hover{border-color:#bfdbfe57;background:linear-gradient(180deg,#182e47f0,#0c1b2cf0)}.ngxsp-btn:disabled{opacity:.38;cursor:not-allowed}.ngxsp-btn:focus-visible{outline:none;box-shadow:var(--ngxsp-focus-ring, 0 0 0 3px rgb(108 182 255 / .5))}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-time{color:var(--ngxsp-text-dim, rgba(230,238,248,.72));font-variant-numeric:tabular-nums}.ngxsp-volumeControl{--ngxsp-control-size: 38px;--ngxsp-volume-range-max: 140px;--ngxsp-volume-button-bg: linear-gradient(180deg, rgba(19,34,54,.92), rgba(11,21,35,.92));--ngxsp-volume-button-border: 1px solid var(--ngxsp-border, rgba(148,163,184,.18));--ngxsp-volume-accent: var(--nsp-accent, #7dd3fc);--ngxsp-volume-thumb-bg: var(--nsp-text, #f8fbff);color:var(--ngxsp-text, #e6eef8)}@media(max-width:640px){.ngxsp-controls{padding:8px;gap:6px}.ngxsp-btn{padding:7px 9px;border-radius:10px}.ngxsp-icon{width:16px;height:16px}}\n"] }]
1295
+ }], propDecorators: { playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }], isAudioMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAudioMode", required: false }] }], hasPlaylistControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPlaylistControls", required: false }] }], canPrev: [{ type: i0.Input, args: [{ isSignal: true, alias: "canPrev", required: false }] }], canNext: [{ type: i0.Input, args: [{ isSignal: true, alias: "canNext", required: false }] }], shuffleActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleActive", required: false }] }], shuffleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleLabel", required: false }] }], repeatMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatMode", required: false }] }], repeatLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatLabel", required: false }] }], captionsActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsActive", required: false }] }], tracksLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tracksLabel", required: false }] }], captionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsLabel", required: false }] }], speedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "speedLabel", required: false }] }], volumeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "volumeLabel", required: false }] }], muteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "muteLabel", required: false }] }], unmuteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unmuteLabel", required: false }] }], showAirPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAirPlay", required: false }] }], airPlayActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayActive", required: false }] }], airPlayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayLabel", required: false }] }], showCast: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCast", required: false }] }], castActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "castActive", required: false }] }], castLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "castLabel", required: false }] }], castHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "castHint", required: false }] }], pipSupported: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipSupported", required: false }] }], pipActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipActive", required: false }] }], pipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipLabel", required: false }] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], buffered: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffered", required: false }] }], profile: [{ type: i0.Input, args: [{ isSignal: true, alias: "profile", required: false }] }], playToggle: [{ type: i0.Output, args: ["playToggle"] }], seekRelative: [{ type: i0.Output, args: ["seekRelative"] }], prev: [{ type: i0.Output, args: ["prev"] }], next: [{ type: i0.Output, args: ["next"] }], toggleShuffle: [{ type: i0.Output, args: ["toggleShuffle"] }], cycleRepeat: [{ type: i0.Output, args: ["cycleRepeat"] }], openMenu: [{ type: i0.Output, args: ["openMenu"] }], captions: [{ type: i0.Output, args: ["captions"] }], airPlay: [{ type: i0.Output, args: ["airPlay"] }], cast: [{ type: i0.Output, args: ["cast"] }], pip: [{ type: i0.Output, args: ["pip"] }], mute: [{ type: i0.Output, args: ["mute"] }], scrub: [{ type: i0.Output, args: ["scrub"] }], volumeChange: [{ type: i0.Output, args: ["volumeChange"] }] } });
1296
+
1297
+ /**
1298
+ * Barre de contrôle du skin vod (top bar + center dock + bottom bar),
1299
+ * extraite du SmartPlayerComponent (décomposition, cf. docs/AUDIT.md).
1300
+ * Le menu pistes/vitesse est projeté par l'hôte via <ng-content>.
1301
+ * PiP/cast restent au parent (propriétaire du <video>) : capacités en entrée,
1302
+ * commandes en sortie.
1303
+ */
1304
+ class PlayerControlsVodComponent {
1305
+ playing = input(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
1306
+ isAudioMode = input(false, ...(ngDevMode ? [{ debugName: "isAudioMode" }] : []));
1307
+ displayTitle = input('Player', ...(ngDevMode ? [{ debugName: "displayTitle" }] : []));
1308
+ nowPlayingLabel = input('Now playing', ...(ngDevMode ? [{ debugName: "nowPlayingLabel" }] : []));
1309
+ nowListeningLabel = input('Now listening', ...(ngDevMode ? [{ debugName: "nowListeningLabel" }] : []));
1310
+ fullscreenHintLabel = input('Double-click for fullscreen', ...(ngDevMode ? [{ debugName: "fullscreenHintLabel" }] : []));
1311
+ statusLabel = input('Ready', ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
1312
+ keyboardHint = input('', ...(ngDevMode ? [{ debugName: "keyboardHint" }] : []));
1313
+ hasPlaylistControls = input(false, ...(ngDevMode ? [{ debugName: "hasPlaylistControls" }] : []));
1314
+ canPrev = input(false, ...(ngDevMode ? [{ debugName: "canPrev" }] : []));
1315
+ canNext = input(false, ...(ngDevMode ? [{ debugName: "canNext" }] : []));
1316
+ previousLabel = input('Previous item', ...(ngDevMode ? [{ debugName: "previousLabel" }] : []));
1317
+ nextLabel = input('Next item', ...(ngDevMode ? [{ debugName: "nextLabel" }] : []));
1318
+ playLabel = input('Play', ...(ngDevMode ? [{ debugName: "playLabel" }] : []));
1319
+ pauseLabel = input('Pause', ...(ngDevMode ? [{ debugName: "pauseLabel" }] : []));
1320
+ shuffleActive = input(false, ...(ngDevMode ? [{ debugName: "shuffleActive" }] : []));
1321
+ shuffleLabel = input('Shuffle: off', ...(ngDevMode ? [{ debugName: "shuffleLabel" }] : []));
1322
+ repeatMode = input('off', ...(ngDevMode ? [{ debugName: "repeatMode" }] : []));
1323
+ repeatLabel = input('Repeat: off', ...(ngDevMode ? [{ debugName: "repeatLabel" }] : []));
1324
+ captionsActive = input(false, ...(ngDevMode ? [{ debugName: "captionsActive" }] : []));
1325
+ tracksLabel = input('Tracks', ...(ngDevMode ? [{ debugName: "tracksLabel" }] : []));
1326
+ captionsLabel = input('Load subtitles', ...(ngDevMode ? [{ debugName: "captionsLabel" }] : []));
1327
+ speedLabel = input('1x', ...(ngDevMode ? [{ debugName: "speedLabel" }] : []));
1328
+ /** "1×", "1.5×": the rate on the speed pill. */
1329
+ speedBadge = computed(() => this.speedLabel().replace(/x$/i, '×'), ...(ngDevMode ? [{ debugName: "speedBadge" }] : []));
1330
+ volumeLabel = input('Volume', ...(ngDevMode ? [{ debugName: "volumeLabel" }] : []));
1331
+ muteLabel = input('Mute', ...(ngDevMode ? [{ debugName: "muteLabel" }] : []));
1332
+ unmuteLabel = input('Restore sound', ...(ngDevMode ? [{ debugName: "unmuteLabel" }] : []));
1333
+ showAirPlay = input(false, ...(ngDevMode ? [{ debugName: "showAirPlay" }] : []));
1334
+ airPlayActive = input(false, ...(ngDevMode ? [{ debugName: "airPlayActive" }] : []));
1335
+ airPlayLabel = input('AirPlay', ...(ngDevMode ? [{ debugName: "airPlayLabel" }] : []));
1336
+ showCast = input(false, ...(ngDevMode ? [{ debugName: "showCast" }] : []));
1337
+ castActive = input(false, ...(ngDevMode ? [{ debugName: "castActive" }] : []));
1338
+ castLabel = input('Play on device', ...(ngDevMode ? [{ debugName: "castLabel" }] : []));
1339
+ castHint = input('', ...(ngDevMode ? [{ debugName: "castHint" }] : []));
1340
+ pipSupported = input(false, ...(ngDevMode ? [{ debugName: "pipSupported" }] : []));
1341
+ pipActive = input(false, ...(ngDevMode ? [{ debugName: "pipActive" }] : []));
1342
+ pipLabel = input('PiP', ...(ngDevMode ? [{ debugName: "pipLabel" }] : []));
1343
+ fullscreenLabel = input('Fullscreen', ...(ngDevMode ? [{ debugName: "fullscreenLabel" }] : []));
1344
+ volume = input(1, ...(ngDevMode ? [{ debugName: "volume" }] : []));
1345
+ currentTime = input(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1346
+ duration = input(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
1347
+ buffered = input(0, ...(ngDevMode ? [{ debugName: "buffered" }] : []));
1348
+ bufferPercent = input(0, ...(ngDevMode ? [{ debugName: "bufferPercent" }] : []));
1349
+ profile = input('vod', ...(ngDevMode ? [{ debugName: "profile" }] : []));
1350
+ timelineDurationSec = input(0, ...(ngDevMode ? [{ debugName: "timelineDurationSec" }] : []));
1351
+ timelineMarkers = input([], ...(ngDevMode ? [{ debugName: "timelineMarkers" }] : []));
1352
+ playToggle = output();
1353
+ seekRelative = output();
1354
+ prev = output();
1355
+ next = output();
1356
+ toggleShuffle = output();
1357
+ cycleRepeat = output();
1358
+ openMenu = output();
1359
+ captions = output();
1360
+ airPlay = output();
1361
+ cast = output();
1362
+ pip = output();
1363
+ fullscreen = output();
1364
+ mute = output();
1365
+ scrub = output();
1366
+ volumeChange = output();
1367
+ fmtTime(sec) {
1368
+ const s = Math.max(0, Math.floor(sec || 0));
1369
+ const h = Math.floor(s / 3600);
1370
+ const m = Math.floor((s % 3600) / 60);
1371
+ const r = s % 60;
1372
+ if (h > 0)
1373
+ return `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
1374
+ return `${m}:${String(r).padStart(2, '0')}`;
1375
+ }
1376
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsVodComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1377
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerControlsVodComponent, isStandalone: true, selector: "ngxsp-player-controls-vod", inputs: { playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, isAudioMode: { classPropertyName: "isAudioMode", publicName: "isAudioMode", isSignal: true, isRequired: false, transformFunction: null }, displayTitle: { classPropertyName: "displayTitle", publicName: "displayTitle", isSignal: true, isRequired: false, transformFunction: null }, nowPlayingLabel: { classPropertyName: "nowPlayingLabel", publicName: "nowPlayingLabel", isSignal: true, isRequired: false, transformFunction: null }, nowListeningLabel: { classPropertyName: "nowListeningLabel", publicName: "nowListeningLabel", isSignal: true, isRequired: false, transformFunction: null }, fullscreenHintLabel: { classPropertyName: "fullscreenHintLabel", publicName: "fullscreenHintLabel", isSignal: true, isRequired: false, transformFunction: null }, statusLabel: { classPropertyName: "statusLabel", publicName: "statusLabel", isSignal: true, isRequired: false, transformFunction: null }, keyboardHint: { classPropertyName: "keyboardHint", publicName: "keyboardHint", isSignal: true, isRequired: false, transformFunction: null }, hasPlaylistControls: { classPropertyName: "hasPlaylistControls", publicName: "hasPlaylistControls", isSignal: true, isRequired: false, transformFunction: null }, canPrev: { classPropertyName: "canPrev", publicName: "canPrev", isSignal: true, isRequired: false, transformFunction: null }, canNext: { classPropertyName: "canNext", publicName: "canNext", isSignal: true, isRequired: false, transformFunction: null }, previousLabel: { classPropertyName: "previousLabel", publicName: "previousLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null }, playLabel: { classPropertyName: "playLabel", publicName: "playLabel", isSignal: true, isRequired: false, transformFunction: null }, pauseLabel: { classPropertyName: "pauseLabel", publicName: "pauseLabel", isSignal: true, isRequired: false, transformFunction: null }, shuffleActive: { classPropertyName: "shuffleActive", publicName: "shuffleActive", isSignal: true, isRequired: false, transformFunction: null }, shuffleLabel: { classPropertyName: "shuffleLabel", publicName: "shuffleLabel", isSignal: true, isRequired: false, transformFunction: null }, repeatMode: { classPropertyName: "repeatMode", publicName: "repeatMode", isSignal: true, isRequired: false, transformFunction: null }, repeatLabel: { classPropertyName: "repeatLabel", publicName: "repeatLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsActive: { classPropertyName: "captionsActive", publicName: "captionsActive", isSignal: true, isRequired: false, transformFunction: null }, tracksLabel: { classPropertyName: "tracksLabel", publicName: "tracksLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsLabel: { classPropertyName: "captionsLabel", publicName: "captionsLabel", isSignal: true, isRequired: false, transformFunction: null }, speedLabel: { classPropertyName: "speedLabel", publicName: "speedLabel", isSignal: true, isRequired: false, transformFunction: null }, volumeLabel: { classPropertyName: "volumeLabel", publicName: "volumeLabel", isSignal: true, isRequired: false, transformFunction: null }, muteLabel: { classPropertyName: "muteLabel", publicName: "muteLabel", isSignal: true, isRequired: false, transformFunction: null }, unmuteLabel: { classPropertyName: "unmuteLabel", publicName: "unmuteLabel", isSignal: true, isRequired: false, transformFunction: null }, showAirPlay: { classPropertyName: "showAirPlay", publicName: "showAirPlay", isSignal: true, isRequired: false, transformFunction: null }, airPlayActive: { classPropertyName: "airPlayActive", publicName: "airPlayActive", isSignal: true, isRequired: false, transformFunction: null }, airPlayLabel: { classPropertyName: "airPlayLabel", publicName: "airPlayLabel", isSignal: true, isRequired: false, transformFunction: null }, showCast: { classPropertyName: "showCast", publicName: "showCast", isSignal: true, isRequired: false, transformFunction: null }, castActive: { classPropertyName: "castActive", publicName: "castActive", isSignal: true, isRequired: false, transformFunction: null }, castLabel: { classPropertyName: "castLabel", publicName: "castLabel", isSignal: true, isRequired: false, transformFunction: null }, castHint: { classPropertyName: "castHint", publicName: "castHint", isSignal: true, isRequired: false, transformFunction: null }, pipSupported: { classPropertyName: "pipSupported", publicName: "pipSupported", isSignal: true, isRequired: false, transformFunction: null }, pipActive: { classPropertyName: "pipActive", publicName: "pipActive", isSignal: true, isRequired: false, transformFunction: null }, pipLabel: { classPropertyName: "pipLabel", publicName: "pipLabel", isSignal: true, isRequired: false, transformFunction: null }, fullscreenLabel: { classPropertyName: "fullscreenLabel", publicName: "fullscreenLabel", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, buffered: { classPropertyName: "buffered", publicName: "buffered", isSignal: true, isRequired: false, transformFunction: null }, bufferPercent: { classPropertyName: "bufferPercent", publicName: "bufferPercent", isSignal: true, isRequired: false, transformFunction: null }, profile: { classPropertyName: "profile", publicName: "profile", isSignal: true, isRequired: false, transformFunction: null }, timelineDurationSec: { classPropertyName: "timelineDurationSec", publicName: "timelineDurationSec", isSignal: true, isRequired: false, transformFunction: null }, timelineMarkers: { classPropertyName: "timelineMarkers", publicName: "timelineMarkers", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playToggle: "playToggle", seekRelative: "seekRelative", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", openMenu: "openMenu", captions: "captions", airPlay: "airPlay", cast: "cast", pip: "pip", fullscreen: "fullscreen", mute: "mute", scrub: "scrub", volumeChange: "volumeChange" }, ngImport: i0, template: `
1378
+ <div class="ngxsp-vod-top">
1379
+ <div class="ngxsp-vod-titleWrap">
1380
+ <div class="ngxsp-vod-eyebrow">{{ isAudioMode() ? nowListeningLabel() : nowPlayingLabel() }}</div>
1381
+ <div class="ngxsp-vod-title">{{ displayTitle() }}</div>
1382
+ </div>
1383
+ <div class="ngxsp-vod-spacer"></div>
1384
+ <div class="ngxsp-vod-status" [attr.data-state]="playing() ? 'playing' : 'idle'">{{ statusLabel() }}</div>
1385
+ @if (!isAudioMode()) {
1386
+ <div class="ngxsp-vod-topHint">{{ fullscreenHintLabel() }}</div>
1387
+ }
1388
+ </div>
1389
+
1390
+ <div class="ngxsp-vod-centerDock">
1391
+ @if (hasPlaylistControls()) {
1392
+ <button type="button" class="ngxsp-vod-side" (click)="prev.emit()" [disabled]="!canPrev()" [attr.aria-label]="previousLabel()">
1393
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1394
+ </button>
1395
+ }
1396
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds">
1397
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1398
+ </button>
1399
+ <ngxsp-play-button class="ngxsp-vod-center" [playing]="playing()" [playLabel]="playLabel()" [pauseLabel]="pauseLabel()" (playToggle)="playToggle.emit()">
1400
+ @if (playing()) {
1401
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="pause" />
1402
+ } @else {
1403
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="play" />
1404
+ }
1405
+ </ngxsp-play-button>
1406
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds">
1407
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1408
+ </button>
1409
+ @if (hasPlaylistControls()) {
1410
+ <button type="button" class="ngxsp-vod-side" (click)="next.emit()" [disabled]="!canNext()" [attr.aria-label]="nextLabel()">
1411
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1412
+ </button>
1413
+ }
1414
+ </div>
1415
+
1416
+ <div class="ngxsp-controls ngxsp-vod-bottom">
1417
+ <div class="ngxsp-vod-metaRow">
1418
+ @if (hasPlaylistControls()) {
1419
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-prev" (click)="prev.emit()" [disabled]="!canPrev()" [attr.title]="previousLabel()" [attr.aria-label]="previousLabel()">
1420
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1421
+ </button>
1422
+ }
1423
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-play" (click)="playToggle.emit()" [attr.title]="playing() ? pauseLabel() : playLabel()" [attr.aria-label]="playing() ? pauseLabel() : playLabel()">
1424
+ @if (playing()) {
1425
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1426
+ } @else {
1427
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1428
+ }
1429
+ </button>
1430
+ @if (hasPlaylistControls()) {
1431
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-next" (click)="next.emit()" [disabled]="!canNext()" [attr.title]="nextLabel()" [attr.aria-label]="nextLabel()">
1432
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1433
+ </button>
1434
+ }
1435
+ <div class="ngxsp-vod-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1436
+ <div class="ngxsp-vod-volumeGroup">
1437
+ <ngxsp-volume-control
1438
+ class="ngxsp-vod-volumeControl"
1439
+ [compact]="true"
1440
+ [volume]="volume()"
1441
+ [volumeLabel]="volumeLabel()"
1442
+ [muteLabel]="muteLabel()"
1443
+ [unmuteLabel]="unmuteLabel()"
1444
+ (muteToggle)="mute.emit()"
1445
+ (volumeChange)="volumeChange.emit($event)"
1446
+ />
1447
+ </div>
1448
+ <ng-content select="[ngxsp-player-inline-controls]" />
1449
+ <div class="ngxsp-vod-spacer"></div>
1450
+ @if (hasPlaylistControls()) {
1451
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1452
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1453
+ </button>
1454
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1455
+ @if (repeatMode() === 'one') {
1456
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1457
+ } @else {
1458
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1459
+ }
1460
+ </button>
1461
+ }
1462
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-tracks" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()">
1463
+ <ngxsp-smart-icon class="ngxsp-icon" name="tracks" />
1464
+ </button>
1465
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-captions" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1466
+ @if (captionsActive()) {
1467
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1468
+ } @else {
1469
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1470
+ }
1471
+ </button>
1472
+ <!-- The rate itself, as a pill: what it is shows at a glance. -->
1473
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-speed" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()">
1474
+ <span class="ngxsp-vod-speedBadge">{{ speedBadge() }}</span>
1475
+ </button>
1476
+ @if (showAirPlay()) {
1477
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-airplay" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1478
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" />
1479
+ </button>
1480
+ }
1481
+ @if (showCast()) {
1482
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-cast" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1483
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="castActive() ? 'devices-active' : 'devices'" />
1484
+ </button>
1485
+ }
1486
+ @if (!isAudioMode() && pipSupported()) {
1487
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-pip" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1488
+ @if (pipActive()) {
1489
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1490
+ } @else {
1491
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1492
+ }
1493
+ </button>
1494
+ }
1495
+ @if (!isAudioMode()) {
1496
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-fullscreen" (click)="fullscreen.emit()" [attr.title]="fullscreenLabel()" [attr.aria-label]="fullscreenLabel()">
1497
+ <ngxsp-smart-icon class="ngxsp-icon" name="fullscreen" />
1498
+ </button>
1499
+ }
1500
+ </div>
1501
+
1502
+ <ngxsp-time-slider
1503
+ class="ngxsp-vod-range"
1504
+ [currentTime]="currentTime()"
1505
+ [duration]="duration()"
1506
+ [buffered]="buffered()"
1507
+ [profile]="profile()"
1508
+ [durationHintSec]="timelineDurationSec()"
1509
+ [markers]="timelineMarkers()"
1510
+ (seek)="scrub.emit($event)"
1511
+ />
1512
+
1513
+ <ng-content select="ngxsp-player-menu" />
1514
+ </div>
1515
+ `, isInline: true, styles: [":host{display:contents}.ngxsp-icon{width:20px;height:20px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:28px;height:28px}.ngxsp-vod-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:16px 18px 40px;background:linear-gradient(to bottom,#0000009e,#0000);pointer-events:none;opacity:0;transition:opacity .2s ease;z-index:3}:host-context(.ngxsp-ui-visible) .ngxsp-vod-top{opacity:1}.ngxsp-vod-titleWrap{min-width:0;display:flex;flex-direction:column;gap:2px}.ngxsp-vod-eyebrow{font-size:12px;font-weight:600;color:#ffffffb8}.ngxsp-vod-title{font-weight:600;font-size:16px;color:#fff;text-shadow:0 1px 2px rgb(0 0 0 / .5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-vod-topHint{display:none}.ngxsp-vod-status{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px;border-radius:999px;background:#00000073;color:#ffffffdb;font-size:11px;font-weight:500;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.ngxsp-vod-status:before{content:\"\";width:6px;height:6px;border-radius:50%;background:#ffffff80}.ngxsp-vod-status[data-state=playing]:before{background:var(--nsp-success, #3ecf8e)}.ngxsp-vod-spacer{flex:1}.ngxsp-vod-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:20px;z-index:4;opacity:0;visibility:hidden;transform:scale(.96);transition:opacity .2s ease,transform .2s ease,visibility 0s linear .2s;pointer-events:none}:host-context(.ngxsp-ui-visible) .ngxsp-vod-centerDock{opacity:1;visibility:visible;transform:none;pointer-events:auto;transition:opacity .2s ease,transform .2s ease,visibility 0s}:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-centerDock,:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-top{display:none}.ngxsp-vod-center{height:68px;width:68px;border-radius:999px;border:0;background:#fffffff0;color:#0d0e11;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 12px 40px #00000073;transition:transform .15s ease,background-color .15s ease}.ngxsp-vod-center:hover{transform:scale(1.05);background:#fff}.ngxsp-vod-side{position:relative;height:48px;min-width:48px;width:48px;border-radius:999px;border:0;background:#00000073;color:#fff;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);transition:background-color .15s ease}.ngxsp-vod-side-wide{min-width:48px}.ngxsp-vod-side-wide .ngxsp-icon{width:24px;height:24px}.ngxsp-vod-side:hover{background:#000000a6}.ngxsp-vod-bottom{position:absolute;left:0;right:0;bottom:0;padding:40px 14px 8px;background:linear-gradient(180deg,#0000,#0000008c 45%,#000000d1);display:flex;flex-direction:column;align-items:stretch;gap:0;z-index:4;opacity:0;transition:opacity .2s ease}:host-context(.ngxsp-ui-visible) .ngxsp-vod-bottom{opacity:1}.ngxsp-vod-metaRow{display:flex;align-items:center;gap:4px;width:100%;min-height:40px}.ngxsp-vod-speedBadge{box-sizing:border-box;min-width:30px;padding:2px 5px;border:1.5px solid currentColor;border-radius:999px;font-size:11px;font-weight:700;line-height:1.2;font-variant-numeric:tabular-nums;white-space:nowrap}.ngxsp-vod-time{padding:0 8px;color:#ffffffeb;font-variant-numeric:tabular-nums;font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-vod-range{order:-1;min-width:100%;min-height:32px;--ngxsp-track-size:4px;--ngxsp-track-bg:rgb(255 255 255 / .22);--ngxsp-buffered-bg:rgb(255 255 255 / .34);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-thumb-size:13px;--ngxsp-thumb-bg:#fff;--ngxsp-marker-width:2px;--ngxsp-marker-height:8px;--ngxsp-marker-bg:rgb(0 0 0 / .55);--ngxsp-marker-active-bg:#fff;--ngxsp-marker-active-shadow:none;--ngxsp-preview-bg:rgb(13 14 17 / .92);--ngxsp-preview-border:rgb(255 255 255 / .12);--ngxsp-preview-muted:rgb(255 255 255 / .64)}.ngxsp-vod-range:hover{--ngxsp-track-size:6px}.ngxsp-vod-volumeGroup{display:inline-flex;align-items:center}.ngxsp-vod-volumeControl{--ngxsp-control-size:36px;--ngxsp-volume-compact-min:72px;--ngxsp-volume-compact-max:104px;--ngxsp-volume-button-bg:transparent;--ngxsp-volume-button-border:0;--ngxsp-volume-button-radius:6px;--ngxsp-volume-accent:#fff;--ngxsp-volume-track-bg:rgb(255 255 255 / .28);--ngxsp-volume-thumb-bg:#fff;--ngxsp-volume-thumb-size:12px;--ngxsp-volume-thumb-shadow:none;--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);color:#fff}.ngxsp-vod-label{font-size:12px;color:#ffffffa3}.ngxsp-vod-chip{position:relative;width:36px;height:36px;min-width:36px;min-height:36px;padding:0;border:0;border-radius:6px;background:transparent;color:#ffffffeb;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background-color .15s ease,color .15s ease}.ngxsp-vod-chip:hover:not(:disabled){background:#ffffff1f;color:#fff}.ngxsp-vod-chip:disabled{opacity:.4;cursor:default}.ngxsp-vod-chip.accent{color:var(--nsp-media-accent, #8b90ff)}.ngxsp-vod-chip-quiet{background:transparent}.ngxsp-vod-center:disabled,.ngxsp-vod-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-vod-center:focus-visible,.ngxsp-vod-side:focus-visible,.ngxsp-vod-chip:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:1px}.ngxsp-buffer{height:4px;border-radius:999px;background:#ffffff1f;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-vod-top{padding:12px 14px 28px}.ngxsp-vod-centerDock{gap:14px}.ngxsp-vod-center{height:58px;width:58px}.ngxsp-vod-side{height:42px;width:42px;min-width:42px}.ngxsp-vod-bottom{padding:36px 8px 6px}.ngxsp-vod-chip-speed,.ngxsp-vod-chip-tracks{display:none}}@media(max-width:640px){.ngxsp-icon{width:18px;height:18px}.ngxsp-icon-lg{width:24px;height:24px}.ngxsp-vod-title{font-size:14px}.ngxsp-vod-status{display:none}.ngxsp-vod-time{font-size:11px;padding:0 4px}.ngxsp-vod-center{height:52px;width:52px}.ngxsp-vod-side{height:38px;width:38px;min-width:38px}.ngxsp-vod-metaRow{min-height:34px;gap:2px}.ngxsp-vod-chip{width:32px;height:32px;min-width:32px;min-height:32px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-shell{grid-template-columns:32px 64px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-range{opacity:1;visibility:visible;transform:none;pointer-events:auto}.ngxsp-vod-chip-playlist,.ngxsp-vod-chip-captions,.ngxsp-vod-chip-pip,.ngxsp-vod-top,.ngxsp-vod-side{display:none}}@media(prefers-reduced-motion:reduce){.ngxsp-vod-top,.ngxsp-vod-centerDock,.ngxsp-vod-bottom,.ngxsp-vod-center,.ngxsp-vod-side,.ngxsp-vod-chip{transition:none}}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: NgxspPlayButton, selector: "ngxsp-play-button", inputs: ["playing", "disabled", "playLabel", "pauseLabel"], outputs: ["playToggle"] }, { kind: "component", type: NgxspTimeSlider, selector: "ngxsp-time-slider", inputs: ["currentTime", "duration", "buffered", "profile", "disabled", "durationHintSec", "markers", "step", "bigStep", "ariaLabel", "liveLabel"], outputs: ["seek"] }, { kind: "component", type: NgxspVolumeControl, selector: "ngxsp-volume-control", inputs: ["volume", "muted", "disabled", "compact", "showValue", "step", "volumeLabel", "muteLabel", "unmuteLabel"], outputs: ["volumeChange", "muteToggle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1516
+ }
1517
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsVodComponent, decorators: [{
1518
+ type: Component,
1519
+ args: [{ selector: 'ngxsp-player-controls-vod', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SmartPlayerIconComponent, NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl], template: `
1520
+ <div class="ngxsp-vod-top">
1521
+ <div class="ngxsp-vod-titleWrap">
1522
+ <div class="ngxsp-vod-eyebrow">{{ isAudioMode() ? nowListeningLabel() : nowPlayingLabel() }}</div>
1523
+ <div class="ngxsp-vod-title">{{ displayTitle() }}</div>
1524
+ </div>
1525
+ <div class="ngxsp-vod-spacer"></div>
1526
+ <div class="ngxsp-vod-status" [attr.data-state]="playing() ? 'playing' : 'idle'">{{ statusLabel() }}</div>
1527
+ @if (!isAudioMode()) {
1528
+ <div class="ngxsp-vod-topHint">{{ fullscreenHintLabel() }}</div>
1529
+ }
1530
+ </div>
1531
+
1532
+ <div class="ngxsp-vod-centerDock">
1533
+ @if (hasPlaylistControls()) {
1534
+ <button type="button" class="ngxsp-vod-side" (click)="prev.emit()" [disabled]="!canPrev()" [attr.aria-label]="previousLabel()">
1535
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1536
+ </button>
1537
+ }
1538
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(-10)" aria-label="Seek backward 10 seconds">
1539
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-back" />
1540
+ </button>
1541
+ <ngxsp-play-button class="ngxsp-vod-center" [playing]="playing()" [playLabel]="playLabel()" [pauseLabel]="pauseLabel()" (playToggle)="playToggle.emit()">
1542
+ @if (playing()) {
1543
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="pause" />
1544
+ } @else {
1545
+ <ngxsp-smart-icon class="ngxsp-icon ngxsp-icon-lg" name="play" />
1546
+ }
1547
+ </ngxsp-play-button>
1548
+ <button type="button" class="ngxsp-vod-side ngxsp-vod-side-wide" (click)="seekRelative.emit(10)" aria-label="Seek forward 10 seconds">
1549
+ <ngxsp-smart-icon class="ngxsp-icon" name="seek-forward" />
1550
+ </button>
1551
+ @if (hasPlaylistControls()) {
1552
+ <button type="button" class="ngxsp-vod-side" (click)="next.emit()" [disabled]="!canNext()" [attr.aria-label]="nextLabel()">
1553
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1554
+ </button>
1555
+ }
1556
+ </div>
1557
+
1558
+ <div class="ngxsp-controls ngxsp-vod-bottom">
1559
+ <div class="ngxsp-vod-metaRow">
1560
+ @if (hasPlaylistControls()) {
1561
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-prev" (click)="prev.emit()" [disabled]="!canPrev()" [attr.title]="previousLabel()" [attr.aria-label]="previousLabel()">
1562
+ <ngxsp-smart-icon class="ngxsp-icon" name="prev" />
1563
+ </button>
1564
+ }
1565
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-play" (click)="playToggle.emit()" [attr.title]="playing() ? pauseLabel() : playLabel()" [attr.aria-label]="playing() ? pauseLabel() : playLabel()">
1566
+ @if (playing()) {
1567
+ <ngxsp-smart-icon class="ngxsp-icon" name="pause" />
1568
+ } @else {
1569
+ <ngxsp-smart-icon class="ngxsp-icon" name="play" />
1570
+ }
1571
+ </button>
1572
+ @if (hasPlaylistControls()) {
1573
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-next" (click)="next.emit()" [disabled]="!canNext()" [attr.title]="nextLabel()" [attr.aria-label]="nextLabel()">
1574
+ <ngxsp-smart-icon class="ngxsp-icon" name="next" />
1575
+ </button>
1576
+ }
1577
+ <div class="ngxsp-vod-time">{{ fmtTime(currentTime()) }} / {{ fmtTime(duration()) }}</div>
1578
+ <div class="ngxsp-vod-volumeGroup">
1579
+ <ngxsp-volume-control
1580
+ class="ngxsp-vod-volumeControl"
1581
+ [compact]="true"
1582
+ [volume]="volume()"
1583
+ [volumeLabel]="volumeLabel()"
1584
+ [muteLabel]="muteLabel()"
1585
+ [unmuteLabel]="unmuteLabel()"
1586
+ (muteToggle)="mute.emit()"
1587
+ (volumeChange)="volumeChange.emit($event)"
1588
+ />
1589
+ </div>
1590
+ <ng-content select="[ngxsp-player-inline-controls]" />
1591
+ <div class="ngxsp-vod-spacer"></div>
1592
+ @if (hasPlaylistControls()) {
1593
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="shuffleActive()" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel()" [attr.aria-label]="shuffleLabel()">
1594
+ <ngxsp-smart-icon class="ngxsp-icon" name="shuffle" />
1595
+ </button>
1596
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-playlist" [class.accent]="repeatMode() !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel()" [attr.aria-label]="repeatLabel()">
1597
+ @if (repeatMode() === 'one') {
1598
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat-one" />
1599
+ } @else {
1600
+ <ngxsp-smart-icon class="ngxsp-icon" name="repeat" />
1601
+ }
1602
+ </button>
1603
+ }
1604
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-tracks" (click)="openMenu.emit('tracks')" [attr.title]="tracksLabel()" [attr.aria-label]="tracksLabel()">
1605
+ <ngxsp-smart-icon class="ngxsp-icon" name="tracks" />
1606
+ </button>
1607
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-captions" (click)="captions.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1608
+ @if (captionsActive()) {
1609
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions" />
1610
+ } @else {
1611
+ <ngxsp-smart-icon class="ngxsp-icon" name="captions-off" />
1612
+ }
1613
+ </button>
1614
+ <!-- The rate itself, as a pill: what it is shows at a glance. -->
1615
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-speed" (click)="openMenu.emit('speed')" [attr.title]="speedLabel()" [attr.aria-label]="speedLabel()">
1616
+ <span class="ngxsp-vod-speedBadge">{{ speedBadge() }}</span>
1617
+ </button>
1618
+ @if (showAirPlay()) {
1619
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-airplay" [class.accent]="airPlayActive()" (click)="airPlay.emit()" [attr.title]="airPlayLabel()" [attr.aria-label]="airPlayLabel()">
1620
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" />
1621
+ </button>
1622
+ }
1623
+ @if (showCast()) {
1624
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-cast" [class.accent]="castActive()" (click)="cast.emit()" [attr.title]="castHint()" [attr.aria-label]="castLabel()">
1625
+ <ngxsp-smart-icon class="ngxsp-icon" [name]="castActive() ? 'devices-active' : 'devices'" />
1626
+ </button>
1627
+ }
1628
+ @if (!isAudioMode() && pipSupported()) {
1629
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-pip" (click)="pip.emit()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1630
+ @if (pipActive()) {
1631
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip-active" />
1632
+ } @else {
1633
+ <ngxsp-smart-icon class="ngxsp-icon" name="pip" />
1634
+ }
1635
+ </button>
1636
+ }
1637
+ @if (!isAudioMode()) {
1638
+ <button type="button" class="ngxsp-vod-chip ngxsp-vod-chip-fullscreen" (click)="fullscreen.emit()" [attr.title]="fullscreenLabel()" [attr.aria-label]="fullscreenLabel()">
1639
+ <ngxsp-smart-icon class="ngxsp-icon" name="fullscreen" />
1640
+ </button>
1641
+ }
1642
+ </div>
1643
+
1644
+ <ngxsp-time-slider
1645
+ class="ngxsp-vod-range"
1646
+ [currentTime]="currentTime()"
1647
+ [duration]="duration()"
1648
+ [buffered]="buffered()"
1649
+ [profile]="profile()"
1650
+ [durationHintSec]="timelineDurationSec()"
1651
+ [markers]="timelineMarkers()"
1652
+ (seek)="scrub.emit($event)"
1653
+ />
1654
+
1655
+ <ng-content select="ngxsp-player-menu" />
1656
+ </div>
1657
+ `, styles: [":host{display:contents}.ngxsp-icon{width:20px;height:20px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:28px;height:28px}.ngxsp-vod-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:16px 18px 40px;background:linear-gradient(to bottom,#0000009e,#0000);pointer-events:none;opacity:0;transition:opacity .2s ease;z-index:3}:host-context(.ngxsp-ui-visible) .ngxsp-vod-top{opacity:1}.ngxsp-vod-titleWrap{min-width:0;display:flex;flex-direction:column;gap:2px}.ngxsp-vod-eyebrow{font-size:12px;font-weight:600;color:#ffffffb8}.ngxsp-vod-title{font-weight:600;font-size:16px;color:#fff;text-shadow:0 1px 2px rgb(0 0 0 / .5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ngxsp-vod-topHint{display:none}.ngxsp-vod-status{display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 10px;border-radius:999px;background:#00000073;color:#ffffffdb;font-size:11px;font-weight:500;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}.ngxsp-vod-status:before{content:\"\";width:6px;height:6px;border-radius:50%;background:#ffffff80}.ngxsp-vod-status[data-state=playing]:before{background:var(--nsp-success, #3ecf8e)}.ngxsp-vod-spacer{flex:1}.ngxsp-vod-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:20px;z-index:4;opacity:0;visibility:hidden;transform:scale(.96);transition:opacity .2s ease,transform .2s ease,visibility 0s linear .2s;pointer-events:none}:host-context(.ngxsp-ui-visible) .ngxsp-vod-centerDock{opacity:1;visibility:visible;transform:none;pointer-events:auto;transition:opacity .2s ease,transform .2s ease,visibility 0s}:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-centerDock,:host-context(.ngxsp-root[data-kind=\"error\"]) .ngxsp-vod-top{display:none}.ngxsp-vod-center{height:68px;width:68px;border-radius:999px;border:0;background:#fffffff0;color:#0d0e11;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 12px 40px #00000073;transition:transform .15s ease,background-color .15s ease}.ngxsp-vod-center:hover{transform:scale(1.05);background:#fff}.ngxsp-vod-side{position:relative;height:48px;min-width:48px;width:48px;border-radius:999px;border:0;background:#00000073;color:#fff;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);transition:background-color .15s ease}.ngxsp-vod-side-wide{min-width:48px}.ngxsp-vod-side-wide .ngxsp-icon{width:24px;height:24px}.ngxsp-vod-side:hover{background:#000000a6}.ngxsp-vod-bottom{position:absolute;left:0;right:0;bottom:0;padding:40px 14px 8px;background:linear-gradient(180deg,#0000,#0000008c 45%,#000000d1);display:flex;flex-direction:column;align-items:stretch;gap:0;z-index:4;opacity:0;transition:opacity .2s ease}:host-context(.ngxsp-ui-visible) .ngxsp-vod-bottom{opacity:1}.ngxsp-vod-metaRow{display:flex;align-items:center;gap:4px;width:100%;min-height:40px}.ngxsp-vod-speedBadge{box-sizing:border-box;min-width:30px;padding:2px 5px;border:1.5px solid currentColor;border-radius:999px;font-size:11px;font-weight:700;line-height:1.2;font-variant-numeric:tabular-nums;white-space:nowrap}.ngxsp-vod-time{padding:0 8px;color:#ffffffeb;font-variant-numeric:tabular-nums;font-size:12px;font-weight:500;white-space:nowrap}.ngxsp-vod-range{order:-1;min-width:100%;min-height:32px;--ngxsp-track-size:4px;--ngxsp-track-bg:rgb(255 255 255 / .22);--ngxsp-buffered-bg:rgb(255 255 255 / .34);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-thumb-size:13px;--ngxsp-thumb-bg:#fff;--ngxsp-marker-width:2px;--ngxsp-marker-height:8px;--ngxsp-marker-bg:rgb(0 0 0 / .55);--ngxsp-marker-active-bg:#fff;--ngxsp-marker-active-shadow:none;--ngxsp-preview-bg:rgb(13 14 17 / .92);--ngxsp-preview-border:rgb(255 255 255 / .12);--ngxsp-preview-muted:rgb(255 255 255 / .64)}.ngxsp-vod-range:hover{--ngxsp-track-size:6px}.ngxsp-vod-volumeGroup{display:inline-flex;align-items:center}.ngxsp-vod-volumeControl{--ngxsp-control-size:36px;--ngxsp-volume-compact-min:72px;--ngxsp-volume-compact-max:104px;--ngxsp-volume-button-bg:transparent;--ngxsp-volume-button-border:0;--ngxsp-volume-button-radius:6px;--ngxsp-volume-accent:#fff;--ngxsp-volume-track-bg:rgb(255 255 255 / .28);--ngxsp-volume-thumb-bg:#fff;--ngxsp-volume-thumb-size:12px;--ngxsp-volume-thumb-shadow:none;--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);color:#fff}.ngxsp-vod-label{font-size:12px;color:#ffffffa3}.ngxsp-vod-chip{position:relative;width:36px;height:36px;min-width:36px;min-height:36px;padding:0;border:0;border-radius:6px;background:transparent;color:#ffffffeb;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background-color .15s ease,color .15s ease}.ngxsp-vod-chip:hover:not(:disabled){background:#ffffff1f;color:#fff}.ngxsp-vod-chip:disabled{opacity:.4;cursor:default}.ngxsp-vod-chip.accent{color:var(--nsp-media-accent, #8b90ff)}.ngxsp-vod-chip-quiet{background:transparent}.ngxsp-vod-center:disabled,.ngxsp-vod-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-vod-center:focus-visible,.ngxsp-vod-side:focus-visible,.ngxsp-vod-chip:focus-visible{outline:2px solid var(--nsp-media-accent, #8b90ff);outline-offset:1px}.ngxsp-buffer{height:4px;border-radius:999px;background:#ffffff1f;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:var(--nsp-media-accent, #8b90ff)}@media(max-width:860px){.ngxsp-vod-top{padding:12px 14px 28px}.ngxsp-vod-centerDock{gap:14px}.ngxsp-vod-center{height:58px;width:58px}.ngxsp-vod-side{height:42px;width:42px;min-width:42px}.ngxsp-vod-bottom{padding:36px 8px 6px}.ngxsp-vod-chip-speed,.ngxsp-vod-chip-tracks{display:none}}@media(max-width:640px){.ngxsp-icon{width:18px;height:18px}.ngxsp-icon-lg{width:24px;height:24px}.ngxsp-vod-title{font-size:14px}.ngxsp-vod-status{display:none}.ngxsp-vod-time{font-size:11px;padding:0 4px}.ngxsp-vod-center{height:52px;width:52px}.ngxsp-vod-side{height:38px;width:38px;min-width:38px}.ngxsp-vod-metaRow{min-height:34px;gap:2px}.ngxsp-vod-chip{width:32px;height:32px;min-width:32px;min-height:32px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-shell{grid-template-columns:32px 64px}.ngxsp-vod-bottom .ngxsp-vod-volumeControl.ngxsp-vc-compact ::ng-deep .ngxsp-vc-range{opacity:1;visibility:visible;transform:none;pointer-events:auto}.ngxsp-vod-chip-playlist,.ngxsp-vod-chip-captions,.ngxsp-vod-chip-pip,.ngxsp-vod-top,.ngxsp-vod-side{display:none}}@media(prefers-reduced-motion:reduce){.ngxsp-vod-top,.ngxsp-vod-centerDock,.ngxsp-vod-bottom,.ngxsp-vod-center,.ngxsp-vod-side,.ngxsp-vod-chip{transition:none}}\n"] }]
1658
+ }], propDecorators: { playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }], isAudioMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isAudioMode", required: false }] }], displayTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayTitle", required: false }] }], nowPlayingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nowPlayingLabel", required: false }] }], nowListeningLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nowListeningLabel", required: false }] }], fullscreenHintLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullscreenHintLabel", required: false }] }], statusLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusLabel", required: false }] }], keyboardHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardHint", required: false }] }], hasPlaylistControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasPlaylistControls", required: false }] }], canPrev: [{ type: i0.Input, args: [{ isSignal: true, alias: "canPrev", required: false }] }], canNext: [{ type: i0.Input, args: [{ isSignal: true, alias: "canNext", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], playLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "playLabel", required: false }] }], pauseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseLabel", required: false }] }], shuffleActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleActive", required: false }] }], shuffleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "shuffleLabel", required: false }] }], repeatMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatMode", required: false }] }], repeatLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "repeatLabel", required: false }] }], captionsActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsActive", required: false }] }], tracksLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tracksLabel", required: false }] }], captionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsLabel", required: false }] }], speedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "speedLabel", required: false }] }], volumeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "volumeLabel", required: false }] }], muteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "muteLabel", required: false }] }], unmuteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unmuteLabel", required: false }] }], showAirPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAirPlay", required: false }] }], airPlayActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayActive", required: false }] }], airPlayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayLabel", required: false }] }], showCast: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCast", required: false }] }], castActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "castActive", required: false }] }], castLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "castLabel", required: false }] }], castHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "castHint", required: false }] }], pipSupported: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipSupported", required: false }] }], pipActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipActive", required: false }] }], pipLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipLabel", required: false }] }], fullscreenLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullscreenLabel", required: false }] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], buffered: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffered", required: false }] }], bufferPercent: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferPercent", required: false }] }], profile: [{ type: i0.Input, args: [{ isSignal: true, alias: "profile", required: false }] }], timelineDurationSec: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineDurationSec", required: false }] }], timelineMarkers: [{ type: i0.Input, args: [{ isSignal: true, alias: "timelineMarkers", required: false }] }], playToggle: [{ type: i0.Output, args: ["playToggle"] }], seekRelative: [{ type: i0.Output, args: ["seekRelative"] }], prev: [{ type: i0.Output, args: ["prev"] }], next: [{ type: i0.Output, args: ["next"] }], toggleShuffle: [{ type: i0.Output, args: ["toggleShuffle"] }], cycleRepeat: [{ type: i0.Output, args: ["cycleRepeat"] }], openMenu: [{ type: i0.Output, args: ["openMenu"] }], captions: [{ type: i0.Output, args: ["captions"] }], airPlay: [{ type: i0.Output, args: ["airPlay"] }], cast: [{ type: i0.Output, args: ["cast"] }], pip: [{ type: i0.Output, args: ["pip"] }], fullscreen: [{ type: i0.Output, args: ["fullscreen"] }], mute: [{ type: i0.Output, args: ["mute"] }], scrub: [{ type: i0.Output, args: ["scrub"] }], volumeChange: [{ type: i0.Output, args: ["volumeChange"] }] } });
1659
+
1660
+ /**
1661
+ * Skin « édition » du lecteur : registre instrument (cf. docs/UX_VISION.md).
1662
+ * Titre display, coins francs, libellés en capitales, accent champagne, et le
1663
+ * traitement direct (NgxspLiveBadge + « Au direct ») en profil `live`.
1664
+ * Compose les primitives de @ngxsp/ui-core. Le menu est projeté via <ng-content>.
1665
+ */
1666
+ class PlayerControlsEditionComponent {
1667
+ profile = input('vod', ...(ngDevMode ? [{ debugName: "profile" }] : []));
1668
+ playing = input(false, ...(ngDevMode ? [{ debugName: "playing" }] : []));
1669
+ displayTitle = input('Player', ...(ngDevMode ? [{ debugName: "displayTitle" }] : []));
1670
+ metaLabel = input('', ...(ngDevMode ? [{ debugName: "metaLabel" }] : []));
1671
+ statusLabel = input('', ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
1672
+ behindSec = input(0, ...(ngDevMode ? [{ debugName: "behindSec" }] : []));
1673
+ liveLabel = input('Direct', ...(ngDevMode ? [{ debugName: "liveLabel" }] : []));
1674
+ captionsActive = input(false, ...(ngDevMode ? [{ debugName: "captionsActive" }] : []));
1675
+ speedLabel = input('1x', ...(ngDevMode ? [{ debugName: "speedLabel" }] : []));
1676
+ showAirPlay = input(false, ...(ngDevMode ? [{ debugName: "showAirPlay" }] : []));
1677
+ airPlayActive = input(false, ...(ngDevMode ? [{ debugName: "airPlayActive" }] : []));
1678
+ airPlayLabel = input('AirPlay', ...(ngDevMode ? [{ debugName: "airPlayLabel" }] : []));
1679
+ showCast = input(false, ...(ngDevMode ? [{ debugName: "showCast" }] : []));
1680
+ castActive = input(false, ...(ngDevMode ? [{ debugName: "castActive" }] : []));
1681
+ pipSupported = input(false, ...(ngDevMode ? [{ debugName: "pipSupported" }] : []));
1682
+ pipActive = input(false, ...(ngDevMode ? [{ debugName: "pipActive" }] : []));
1683
+ volume = input(1, ...(ngDevMode ? [{ debugName: "volume" }] : []));
1684
+ volumeLabel = input('Volume', ...(ngDevMode ? [{ debugName: "volumeLabel" }] : []));
1685
+ muteLabel = input('Mute', ...(ngDevMode ? [{ debugName: "muteLabel" }] : []));
1686
+ unmuteLabel = input('Restore sound', ...(ngDevMode ? [{ debugName: "unmuteLabel" }] : []));
1687
+ currentTime = input(0, ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1688
+ duration = input(0, ...(ngDevMode ? [{ debugName: "duration" }] : []));
1689
+ buffered = input(0, ...(ngDevMode ? [{ debugName: "buffered" }] : []));
1690
+ playToggle = output();
1691
+ seekRelative = output();
1692
+ openMenu = output();
1693
+ captions = output();
1694
+ airPlay = output();
1695
+ cast = output();
1696
+ pip = output();
1697
+ mute = output();
1698
+ scrub = output();
1699
+ volumeChange = output();
1700
+ goLive = output();
1701
+ fmtTime(sec) {
1702
+ const s = Math.max(0, Math.floor(sec || 0));
1703
+ const h = Math.floor(s / 3600);
1704
+ const m = Math.floor((s % 3600) / 60);
1705
+ const r = s % 60;
1706
+ if (h > 0)
1707
+ return `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
1708
+ return `${m}:${String(r).padStart(2, '0')}`;
1709
+ }
1710
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsEditionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1711
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: PlayerControlsEditionComponent, isStandalone: true, selector: "ngxsp-player-controls-edition", inputs: { profile: { classPropertyName: "profile", publicName: "profile", isSignal: true, isRequired: false, transformFunction: null }, playing: { classPropertyName: "playing", publicName: "playing", isSignal: true, isRequired: false, transformFunction: null }, displayTitle: { classPropertyName: "displayTitle", publicName: "displayTitle", isSignal: true, isRequired: false, transformFunction: null }, metaLabel: { classPropertyName: "metaLabel", publicName: "metaLabel", isSignal: true, isRequired: false, transformFunction: null }, statusLabel: { classPropertyName: "statusLabel", publicName: "statusLabel", isSignal: true, isRequired: false, transformFunction: null }, behindSec: { classPropertyName: "behindSec", publicName: "behindSec", isSignal: true, isRequired: false, transformFunction: null }, liveLabel: { classPropertyName: "liveLabel", publicName: "liveLabel", isSignal: true, isRequired: false, transformFunction: null }, captionsActive: { classPropertyName: "captionsActive", publicName: "captionsActive", isSignal: true, isRequired: false, transformFunction: null }, speedLabel: { classPropertyName: "speedLabel", publicName: "speedLabel", isSignal: true, isRequired: false, transformFunction: null }, showAirPlay: { classPropertyName: "showAirPlay", publicName: "showAirPlay", isSignal: true, isRequired: false, transformFunction: null }, airPlayActive: { classPropertyName: "airPlayActive", publicName: "airPlayActive", isSignal: true, isRequired: false, transformFunction: null }, airPlayLabel: { classPropertyName: "airPlayLabel", publicName: "airPlayLabel", isSignal: true, isRequired: false, transformFunction: null }, showCast: { classPropertyName: "showCast", publicName: "showCast", isSignal: true, isRequired: false, transformFunction: null }, castActive: { classPropertyName: "castActive", publicName: "castActive", isSignal: true, isRequired: false, transformFunction: null }, pipSupported: { classPropertyName: "pipSupported", publicName: "pipSupported", isSignal: true, isRequired: false, transformFunction: null }, pipActive: { classPropertyName: "pipActive", publicName: "pipActive", isSignal: true, isRequired: false, transformFunction: null }, volume: { classPropertyName: "volume", publicName: "volume", isSignal: true, isRequired: false, transformFunction: null }, volumeLabel: { classPropertyName: "volumeLabel", publicName: "volumeLabel", isSignal: true, isRequired: false, transformFunction: null }, muteLabel: { classPropertyName: "muteLabel", publicName: "muteLabel", isSignal: true, isRequired: false, transformFunction: null }, unmuteLabel: { classPropertyName: "unmuteLabel", publicName: "unmuteLabel", isSignal: true, isRequired: false, transformFunction: null }, currentTime: { classPropertyName: "currentTime", publicName: "currentTime", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "duration", isSignal: true, isRequired: false, transformFunction: null }, buffered: { classPropertyName: "buffered", publicName: "buffered", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { playToggle: "playToggle", seekRelative: "seekRelative", openMenu: "openMenu", captions: "captions", airPlay: "airPlay", cast: "cast", pip: "pip", mute: "mute", scrub: "scrub", volumeChange: "volumeChange", goLive: "goLive" }, ngImport: i0, template: `
1712
+ <div class="ed-top">
1713
+ @if (profile() === 'live') {
1714
+ <ngxsp-live-badge [behindSec]="behindSec()" />
1715
+ }
1716
+ <div class="ed-titleWrap">
1717
+ <div class="ed-title">{{ displayTitle() }}</div>
1718
+ <div class="ed-meta">{{ metaLabel() }}</div>
1719
+ </div>
1720
+ <span class="ed-spacer"></span>
1721
+ <div class="ed-status">{{ statusLabel() }}</div>
1722
+ </div>
1723
+
1724
+ <div class="ed-bottom">
1725
+ <div class="ed-scrubRow">
1726
+ <span class="ed-time">{{ fmtTime(currentTime()) }}</span>
1727
+ <ngxsp-time-slider
1728
+ style="flex:1"
1729
+ [currentTime]="currentTime()"
1730
+ [duration]="duration()"
1731
+ [buffered]="buffered()"
1732
+ [profile]="profile()"
1733
+ (seek)="scrub.emit($event)"
1734
+ />
1735
+ <span class="ed-time ed-time-dim">{{ profile() === 'live' ? liveLabel() : fmtTime(duration()) }}</span>
1736
+ </div>
1737
+
1738
+ <div class="ed-controlsRow">
1739
+ @if (profile() === 'live') {
1740
+ <button type="button" class="ed-live" (click)="goLive.emit()">
1741
+ <ngxsp-smart-icon class="ed-icon" name="next" /> Au direct
1742
+ </button>
1743
+ <ngxsp-play-button class="ed-transport" [playing]="playing()" (playToggle)="playToggle.emit()">
1744
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1745
+ </ngxsp-play-button>
1746
+ } @else {
1747
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(-10)" aria-label="Reculer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-back" /></button>
1748
+ <ngxsp-play-button class="ed-transport ed-transport-primary" [playing]="playing()" (playToggle)="playToggle.emit()">
1749
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1750
+ </ngxsp-play-button>
1751
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(10)" aria-label="Avancer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-forward" /></button>
1752
+ }
1753
+
1754
+ <span class="ed-spacer"></span>
1755
+
1756
+ <ngxsp-labeled-control label="Sous-titres" [pressed]="captionsActive()" (activate)="captions.emit()"><ngxsp-smart-icon class="ed-icon" [name]="captionsActive() ? 'captions' : 'captions-off'" /></ngxsp-labeled-control>
1757
+ <ngxsp-labeled-control label="Pistes" (activate)="openMenu.emit('tracks')"><ngxsp-smart-icon class="ed-icon" name="tracks" /></ngxsp-labeled-control>
1758
+ <ngxsp-labeled-control [label]="speedLabel()" (activate)="openMenu.emit('speed')"><ngxsp-smart-icon class="ed-icon" name="speed" /></ngxsp-labeled-control>
1759
+ @if (showAirPlay()) {
1760
+ <ngxsp-labeled-control [label]="airPlayLabel()" [active]="airPlayActive()" (activate)="airPlay.emit()"><ngxsp-smart-icon class="ed-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" /></ngxsp-labeled-control>
1761
+ }
1762
+ @if (showCast()) {
1763
+ <ngxsp-labeled-control label="Diffuser" [active]="castActive()" (activate)="cast.emit()"><ngxsp-smart-icon class="ed-icon" [name]="castActive() ? 'cast-active' : 'cast'" /></ngxsp-labeled-control>
1764
+ }
1765
+ @if (pipSupported()) {
1766
+ <ngxsp-labeled-control label="PiP" [active]="pipActive()" (activate)="pip.emit()"><ngxsp-smart-icon class="ed-icon" [name]="pipActive() ? 'pip-active' : 'pip'" /></ngxsp-labeled-control>
1767
+ }
1768
+ <ngxsp-volume-control
1769
+ class="ed-volumeControl"
1770
+ [compact]="true"
1771
+ [volume]="volume()"
1772
+ [volumeLabel]="volumeLabel()"
1773
+ [muteLabel]="muteLabel()"
1774
+ [unmuteLabel]="unmuteLabel()"
1775
+ (muteToggle)="mute.emit()"
1776
+ (volumeChange)="volumeChange.emit($event)"
1777
+ />
1778
+ </div>
1779
+
1780
+ <ng-content />
1781
+ </div>
1782
+ `, isInline: true, styles: [":host{display:contents}.ed-icon{width:20px;height:20px;display:block}.ed-top{position:absolute;left:0;right:0;top:0;z-index:3;display:flex;align-items:center;gap:14px;padding:18px 22px 14px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 55%,transparent);border-bottom:1px solid var(--nsp-line, rgb(255 255 255 / .1));opacity:0;transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-top{opacity:1}.ed-titleWrap{display:flex;flex-direction:column;gap:6px}.ed-title{font-size:24px;font-weight:500;letter-spacing:-.025em;line-height:1;color:var(--nsp-text, #f4f2ec)}.ed-meta{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--nsp-text-2, #b4afa6)}.ed-status{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--nsp-text-3, #7c776f)}.ed-spacer{flex:1}.ed-bottom{position:absolute;left:0;right:0;bottom:0;z-index:4;display:flex;flex-direction:column;gap:14px;padding:16px 22px 18px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 82%,transparent);border-top:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));opacity:0;transform:translateY(8px);transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease),transform var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-bottom{opacity:1;transform:translateY(0)}.ed-scrubRow{display:flex;align-items:center;gap:14px}.ed-time{font-size:13px;font-weight:500;font-variant-numeric:tabular-nums;color:var(--nsp-text, #f4f2ec)}.ed-time-dim{color:var(--nsp-text-2, #b4afa6)}.ed-controlsRow{display:flex;align-items:center;gap:6px}.ed-transport{all:unset;box-sizing:border-box;width:46px;height:46px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));border-radius:var(--nsp-radius-sharp, 3px);color:var(--nsp-text, #f4f2ec);cursor:pointer}.ed-transport-primary{background:var(--nsp-accent, #e3b26a);color:var(--nsp-on-accent, #14161b);border:none}.ed-transport:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-live{all:unset;box-sizing:border-box;height:46px;padding:0 16px;display:inline-flex;align-items:center;gap:8px;background:var(--nsp-live, #e5402a);color:#fff;cursor:pointer;border-radius:var(--nsp-radius-sharp, 3px);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase}.ed-live:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-volumeControl{--ngxsp-control-size: 46px;--ngxsp-volume-button-radius: var(--nsp-radius-sharp, 3px);--ngxsp-volume-button-bg: transparent;--ngxsp-volume-button-border: 1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));--ngxsp-volume-accent: var(--nsp-accent, #e3b26a);--ngxsp-volume-compact-min: 96px;--ngxsp-volume-compact-max: 150px;--ngxsp-volume-color: var(--nsp-text, #f4f2ec);--ngxsp-focus-ring: var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}\n"], dependencies: [{ kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }, { kind: "component", type: NgxspPlayButton, selector: "ngxsp-play-button", inputs: ["playing", "disabled", "playLabel", "pauseLabel"], outputs: ["playToggle"] }, { kind: "component", type: NgxspTimeSlider, selector: "ngxsp-time-slider", inputs: ["currentTime", "duration", "buffered", "profile", "disabled", "durationHintSec", "markers", "step", "bigStep", "ariaLabel", "liveLabel"], outputs: ["seek"] }, { kind: "component", type: NgxspVolumeControl, selector: "ngxsp-volume-control", inputs: ["volume", "muted", "disabled", "compact", "showValue", "step", "volumeLabel", "muteLabel", "unmuteLabel"], outputs: ["volumeChange", "muteToggle"] }, { kind: "component", type: NgxspLabeledControl, selector: "ngxsp-labeled-control", inputs: ["label", "active", "pressed", "disabled"], outputs: ["activate"] }, { kind: "component", type: NgxspLiveBadge, selector: "ngxsp-live-badge", inputs: ["liveLabel", "behindSec"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1783
+ }
1784
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: PlayerControlsEditionComponent, decorators: [{
1785
+ type: Component,
1786
+ args: [{ selector: 'ngxsp-player-controls-edition', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [SmartPlayerIconComponent, NgxspPlayButton, NgxspTimeSlider, NgxspVolumeControl, NgxspLabeledControl, NgxspLiveBadge], template: `
1787
+ <div class="ed-top">
1788
+ @if (profile() === 'live') {
1789
+ <ngxsp-live-badge [behindSec]="behindSec()" />
1790
+ }
1791
+ <div class="ed-titleWrap">
1792
+ <div class="ed-title">{{ displayTitle() }}</div>
1793
+ <div class="ed-meta">{{ metaLabel() }}</div>
1794
+ </div>
1795
+ <span class="ed-spacer"></span>
1796
+ <div class="ed-status">{{ statusLabel() }}</div>
1797
+ </div>
1798
+
1799
+ <div class="ed-bottom">
1800
+ <div class="ed-scrubRow">
1801
+ <span class="ed-time">{{ fmtTime(currentTime()) }}</span>
1802
+ <ngxsp-time-slider
1803
+ style="flex:1"
1804
+ [currentTime]="currentTime()"
1805
+ [duration]="duration()"
1806
+ [buffered]="buffered()"
1807
+ [profile]="profile()"
1808
+ (seek)="scrub.emit($event)"
1809
+ />
1810
+ <span class="ed-time ed-time-dim">{{ profile() === 'live' ? liveLabel() : fmtTime(duration()) }}</span>
1811
+ </div>
1812
+
1813
+ <div class="ed-controlsRow">
1814
+ @if (profile() === 'live') {
1815
+ <button type="button" class="ed-live" (click)="goLive.emit()">
1816
+ <ngxsp-smart-icon class="ed-icon" name="next" /> Au direct
1817
+ </button>
1818
+ <ngxsp-play-button class="ed-transport" [playing]="playing()" (playToggle)="playToggle.emit()">
1819
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1820
+ </ngxsp-play-button>
1821
+ } @else {
1822
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(-10)" aria-label="Reculer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-back" /></button>
1823
+ <ngxsp-play-button class="ed-transport ed-transport-primary" [playing]="playing()" (playToggle)="playToggle.emit()">
1824
+ @if (playing()) { <ngxsp-smart-icon class="ed-icon" name="pause" /> } @else { <ngxsp-smart-icon class="ed-icon" name="play" /> }
1825
+ </ngxsp-play-button>
1826
+ <button type="button" class="ed-transport" (click)="seekRelative.emit(10)" aria-label="Avancer 10 secondes"><ngxsp-smart-icon class="ed-icon" name="seek-forward" /></button>
1827
+ }
1828
+
1829
+ <span class="ed-spacer"></span>
1830
+
1831
+ <ngxsp-labeled-control label="Sous-titres" [pressed]="captionsActive()" (activate)="captions.emit()"><ngxsp-smart-icon class="ed-icon" [name]="captionsActive() ? 'captions' : 'captions-off'" /></ngxsp-labeled-control>
1832
+ <ngxsp-labeled-control label="Pistes" (activate)="openMenu.emit('tracks')"><ngxsp-smart-icon class="ed-icon" name="tracks" /></ngxsp-labeled-control>
1833
+ <ngxsp-labeled-control [label]="speedLabel()" (activate)="openMenu.emit('speed')"><ngxsp-smart-icon class="ed-icon" name="speed" /></ngxsp-labeled-control>
1834
+ @if (showAirPlay()) {
1835
+ <ngxsp-labeled-control [label]="airPlayLabel()" [active]="airPlayActive()" (activate)="airPlay.emit()"><ngxsp-smart-icon class="ed-icon" [name]="airPlayActive() ? 'airplay-active' : 'airplay'" /></ngxsp-labeled-control>
1836
+ }
1837
+ @if (showCast()) {
1838
+ <ngxsp-labeled-control label="Diffuser" [active]="castActive()" (activate)="cast.emit()"><ngxsp-smart-icon class="ed-icon" [name]="castActive() ? 'cast-active' : 'cast'" /></ngxsp-labeled-control>
1839
+ }
1840
+ @if (pipSupported()) {
1841
+ <ngxsp-labeled-control label="PiP" [active]="pipActive()" (activate)="pip.emit()"><ngxsp-smart-icon class="ed-icon" [name]="pipActive() ? 'pip-active' : 'pip'" /></ngxsp-labeled-control>
1842
+ }
1843
+ <ngxsp-volume-control
1844
+ class="ed-volumeControl"
1845
+ [compact]="true"
1846
+ [volume]="volume()"
1847
+ [volumeLabel]="volumeLabel()"
1848
+ [muteLabel]="muteLabel()"
1849
+ [unmuteLabel]="unmuteLabel()"
1850
+ (muteToggle)="mute.emit()"
1851
+ (volumeChange)="volumeChange.emit($event)"
1852
+ />
1853
+ </div>
1854
+
1855
+ <ng-content />
1856
+ </div>
1857
+ `, styles: [":host{display:contents}.ed-icon{width:20px;height:20px;display:block}.ed-top{position:absolute;left:0;right:0;top:0;z-index:3;display:flex;align-items:center;gap:14px;padding:18px 22px 14px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 55%,transparent);border-bottom:1px solid var(--nsp-line, rgb(255 255 255 / .1));opacity:0;transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-top{opacity:1}.ed-titleWrap{display:flex;flex-direction:column;gap:6px}.ed-title{font-size:24px;font-weight:500;letter-spacing:-.025em;line-height:1;color:var(--nsp-text, #f4f2ec)}.ed-meta{font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--nsp-text-2, #b4afa6)}.ed-status{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--nsp-text-3, #7c776f)}.ed-spacer{flex:1}.ed-bottom{position:absolute;left:0;right:0;bottom:0;z-index:4;display:flex;flex-direction:column;gap:14px;padding:16px 22px 18px;background:color-mix(in srgb,var(--nsp-bg, #14161b) 82%,transparent);border-top:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));opacity:0;transform:translateY(8px);transition:opacity var(--nsp-dur, .18s) var(--nsp-ease, ease),transform var(--nsp-dur, .18s) var(--nsp-ease, ease)}:host-context(.ngxsp-ui-visible) .ed-bottom{opacity:1;transform:translateY(0)}.ed-scrubRow{display:flex;align-items:center;gap:14px}.ed-time{font-size:13px;font-weight:500;font-variant-numeric:tabular-nums;color:var(--nsp-text, #f4f2ec)}.ed-time-dim{color:var(--nsp-text-2, #b4afa6)}.ed-controlsRow{display:flex;align-items:center;gap:6px}.ed-transport{all:unset;box-sizing:border-box;width:46px;height:46px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));border-radius:var(--nsp-radius-sharp, 3px);color:var(--nsp-text, #f4f2ec);cursor:pointer}.ed-transport-primary{background:var(--nsp-accent, #e3b26a);color:var(--nsp-on-accent, #14161b);border:none}.ed-transport:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-live{all:unset;box-sizing:border-box;height:46px;padding:0 16px;display:inline-flex;align-items:center;gap:8px;background:var(--nsp-live, #e5402a);color:#fff;cursor:pointer;border-radius:var(--nsp-radius-sharp, 3px);font-size:12px;font-weight:500;letter-spacing:.1em;text-transform:uppercase}.ed-live:focus-visible{outline:none;box-shadow:var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}.ed-volumeControl{--ngxsp-control-size: 46px;--ngxsp-volume-button-radius: var(--nsp-radius-sharp, 3px);--ngxsp-volume-button-bg: transparent;--ngxsp-volume-button-border: 1px solid var(--nsp-line-strong, rgb(255 255 255 / .18));--ngxsp-volume-accent: var(--nsp-accent, #e3b26a);--ngxsp-volume-compact-min: 96px;--ngxsp-volume-compact-max: 150px;--ngxsp-volume-color: var(--nsp-text, #f4f2ec);--ngxsp-focus-ring: var(--nsp-focus-ring, 0 0 0 3px rgb(227 178 106 / .55))}\n"] }]
1858
+ }], propDecorators: { profile: [{ type: i0.Input, args: [{ isSignal: true, alias: "profile", required: false }] }], playing: [{ type: i0.Input, args: [{ isSignal: true, alias: "playing", required: false }] }], displayTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayTitle", required: false }] }], metaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "metaLabel", required: false }] }], statusLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusLabel", required: false }] }], behindSec: [{ type: i0.Input, args: [{ isSignal: true, alias: "behindSec", required: false }] }], liveLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "liveLabel", required: false }] }], captionsActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionsActive", required: false }] }], speedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "speedLabel", required: false }] }], showAirPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAirPlay", required: false }] }], airPlayActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayActive", required: false }] }], airPlayLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "airPlayLabel", required: false }] }], showCast: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCast", required: false }] }], castActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "castActive", required: false }] }], pipSupported: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipSupported", required: false }] }], pipActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "pipActive", required: false }] }], volume: [{ type: i0.Input, args: [{ isSignal: true, alias: "volume", required: false }] }], volumeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "volumeLabel", required: false }] }], muteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "muteLabel", required: false }] }], unmuteLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "unmuteLabel", required: false }] }], currentTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentTime", required: false }] }], duration: [{ type: i0.Input, args: [{ isSignal: true, alias: "duration", required: false }] }], buffered: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffered", required: false }] }], playToggle: [{ type: i0.Output, args: ["playToggle"] }], seekRelative: [{ type: i0.Output, args: ["seekRelative"] }], openMenu: [{ type: i0.Output, args: ["openMenu"] }], captions: [{ type: i0.Output, args: ["captions"] }], airPlay: [{ type: i0.Output, args: ["airPlay"] }], cast: [{ type: i0.Output, args: ["cast"] }], pip: [{ type: i0.Output, args: ["pip"] }], mute: [{ type: i0.Output, args: ["mute"] }], scrub: [{ type: i0.Output, args: ["scrub"] }], volumeChange: [{ type: i0.Output, args: ["volumeChange"] }], goLive: [{ type: i0.Output, args: ["goLive"] }] } });
7
1859
 
8
1860
  class SmartPlayerComponent {
9
1861
  config;
@@ -13,23 +1865,73 @@ class SmartPlayerComponent {
13
1865
  debug = false;
14
1866
  /** Optional start time (seconds) used for "resume playback". */
15
1867
  startTimeSec = null;
1868
+ /** Starts the first source as soon as it is loaded. WebViews and desktop shells do not block it like browsers do. */
1869
+ autoplayOnInit = true;
1870
+ /** Increment when the user picks something to play: a new source starts, the current one resumes. */
1871
+ playRequest = 0;
16
1872
  canPrev = false;
17
1873
  canNext = false;
1874
+ previousLabel = 'Previous item';
1875
+ nextLabel = 'Next item';
1876
+ playLabel = 'Play';
1877
+ pauseLabel = 'Pause';
18
1878
  shuffleActive = false;
19
1879
  shuffleLabel = 'Shuffle: off';
20
1880
  repeatMode = 'off';
21
1881
  repeatLabel = 'Repeat: off';
22
1882
  hasPlaylistControls = false;
1883
+ mediaMode = 'video';
1884
+ castEnabled = false;
1885
+ /** The media plays on a device the app drives (castEnabled): the devices button shows it. */
1886
+ castConnected = false;
1887
+ airPlayAvailable = false;
1888
+ airPlayActive = false;
1889
+ documentShortcuts = true;
1890
+ nowPlayingLabel = 'Now playing';
1891
+ nowListeningLabel = 'Now listening';
1892
+ fullscreenHintLabel = 'Double-click for fullscreen';
1893
+ fullscreenLabel = 'Fullscreen';
1894
+ readyStatusLabel = 'Ready';
1895
+ playingStatusLabel = 'Playing';
1896
+ pausedStatusLabel = 'Paused';
1897
+ bufferingStatusLabel = 'Buffering';
1898
+ loadingStatusLabel = 'Loading';
1899
+ endedStatusLabel = 'Ended';
1900
+ errorStatusLabel = 'Error';
1901
+ outputVolume = null;
1902
+ outputVolumeLabel = '';
1903
+ outputMuteLabel = 'Mute';
1904
+ outputUnmuteLabel = 'Restore sound';
1905
+ timelineDurationSec = null;
1906
+ timelineMarkers = [];
1907
+ // Noms volontairement alignés sur les événements média natifs (API publique).
1908
+ // eslint-disable-next-line @angular-eslint/no-output-native
23
1909
  ended = new EventEmitter();
24
1910
  videoSize = new EventEmitter();
25
- /** Emits playback position updates. */
1911
+ /**
1912
+ * Emits playback position updates, tagged with the media they belong to: a host switching tracks can still receive
1913
+ * a position of the previous one, and must not file it under the new one.
1914
+ */
1915
+ // eslint-disable-next-line @angular-eslint/no-output-native
26
1916
  progress = new EventEmitter();
27
1917
  prev = new EventEmitter();
28
1918
  next = new EventEmitter();
29
1919
  toggleShuffle = new EventEmitter();
30
1920
  cycleRepeat = new EventEmitter();
1921
+ /** CC pressed; the vod and edition skins also open their subtitles menu. */
31
1922
  subtitles = new EventEmitter();
1923
+ /** A subtitle file was read (ok) or could not be (error). */
1924
+ subtitlesLoaded = new EventEmitter();
1925
+ /** The words of the player's menus and of its subtitles button. */
1926
+ menuLabels = {};
1927
+ cast = new EventEmitter();
1928
+ stateChange = new EventEmitter();
1929
+ playbackToggle = new EventEmitter();
1930
+ outputVolumeChange = new EventEmitter();
32
1931
  videoEl;
1932
+ captionsLayer;
1933
+ /** Absent in audio mode, which has no subtitles. */
1934
+ subtitleFileInput;
33
1935
  state = null;
34
1936
  duration = 0;
35
1937
  currentTime = 0;
@@ -40,12 +1942,33 @@ class SmartPlayerComponent {
40
1942
  tracks = [];
41
1943
  openMenu = null;
42
1944
  controlsVisible = true;
1945
+ surfaceGesture = null;
1946
+ castState = 'disconnected';
43
1947
  playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
44
1948
  facade = new PlayerFacade();
1949
+ cdr = inject(ChangeDetectorRef);
45
1950
  sub = new Subscription();
46
1951
  hideControlsTimer = null;
1952
+ surfaceClickTimer = null;
1953
+ gestureTimer = null;
1954
+ remotePlaybackCleanup = null;
1955
+ initialized = false;
1956
+ loadedSourceSignature = '';
1957
+ loadSequence = 0;
1958
+ loading = false;
1959
+ pendingPlay = false;
1960
+ /** Whether the load in progress was asked to start playback. */
1961
+ loadingAutoplay = false;
1962
+ /** Load whose end was already reported; reset when playback starts again (repeat one). */
1963
+ endedLoadSequence = -1;
1964
+ pendingSeekSec = null;
47
1965
  get uiSkin() {
48
- return (this.config?.ui?.skin ?? 'classic') === 'netflix' ? 'netflix' : 'classic';
1966
+ const skin = this.config?.ui?.skin;
1967
+ return skin === 'vod' || skin === 'edition' ? skin : 'classic';
1968
+ }
1969
+ /** Libellé méta du skin édition (genre · format). */
1970
+ get editionMeta() {
1971
+ return this.isAudioMode ? 'Audio' : 'Vidéo';
49
1972
  }
50
1973
  get uiVisible() {
51
1974
  const kind = this.state?.kind;
@@ -54,11 +1977,27 @@ class SmartPlayerComponent {
54
1977
  get displayTitle() {
55
1978
  const ui = this.config?.ui;
56
1979
  const src = this.config?.source;
57
- return (ui?.title ??
58
- ui?.displayTitle ??
59
- src?.title ??
60
- src?.contentId ??
61
- 'Player');
1980
+ return ui?.title ?? ui?.displayTitle ?? src?.title ?? src?.contentId ?? 'Player';
1981
+ }
1982
+ get displaySubtitle() {
1983
+ const ui = this.config?.ui;
1984
+ const src = this.config?.source;
1985
+ return (ui?.subtitle ??
1986
+ ui?.description ??
1987
+ src?.subtitle ??
1988
+ src?.artist ??
1989
+ src?.album ??
1990
+ this.audioTracks.find((track) => track.active)?.label ??
1991
+ 'Audio playback');
1992
+ }
1993
+ get isAudioMode() {
1994
+ return this.mediaMode === 'audio';
1995
+ }
1996
+ /** Profil de source transmis aux primitives UI (cf. @ngxsp/ui-core). */
1997
+ get sourceProfile() {
1998
+ if (this.config?.source?.live)
1999
+ return 'live';
2000
+ return this.isAudioMode ? 'audio' : 'vod';
62
2001
  }
63
2002
  get audioTracks() {
64
2003
  return this.tracks.filter((track) => track.kind === 'audio');
@@ -78,45 +2017,109 @@ class SmartPlayerComponent {
78
2017
  get selectedVideoTrackId() {
79
2018
  return this.videoTracks.find((track) => track.active)?.id ?? null;
80
2019
  }
2020
+ ngOnChanges(changes) {
2021
+ if (changes['outputVolume'])
2022
+ this.syncOutputVolumeInput();
2023
+ if (!this.initialized)
2024
+ return;
2025
+ const playRequested = !!changes['playRequest'] && !changes['playRequest'].firstChange;
2026
+ if (changes['engines'] && !changes['engines'].firstChange) {
2027
+ this.facade.setEngines(this.engines);
2028
+ // Playback moves to a device (or comes back): the media is loaded again from the same position. It keeps playing
2029
+ // when it moves to a device, and waits on this one when it comes back, unless the host asks for playback.
2030
+ // A different media arriving in the same change is not a move: it loads below from its own start, or it would
2031
+ // inherit the position of the one it replaces.
2032
+ if (this.sourceSignature(this.config) === this.loadedSourceSignature) {
2033
+ const kind = this.state?.kind;
2034
+ const wasPlaying = kind === 'playing' || kind === 'buffering' || (this.loading && (this.loadingAutoplay || this.pendingPlay));
2035
+ // Hosts keep their own engines (adaptive streaming) in the list: only the remote engine means a device.
2036
+ const toDevice = (this.engines ?? []).some((engine) => engine.id === 'remote');
2037
+ if (this.pendingSeekSec === null && this.currentTime > 0.5)
2038
+ this.pendingSeekSec = this.currentTime;
2039
+ void this.loadConfig(playRequested || (toDevice && wasPlaying));
2040
+ return;
2041
+ }
2042
+ }
2043
+ if (changes['config'] && this.sourceSignature(this.config) !== this.loadedSourceSignature) {
2044
+ // Keeps playing when the source is swapped while playing or stalled, right after the previous media ended (queue
2045
+ // chaining) or while a load meant to start is still in progress (a local copy replacing the network source),
2046
+ // and starts when the host asked for playback; nothing starts on its own otherwise.
2047
+ const kind = this.state?.kind;
2048
+ const autoplay = playRequested || kind === 'playing' || kind === 'buffering' || kind === 'ended'
2049
+ || (this.loading && (this.loadingAutoplay || this.pendingPlay));
2050
+ // A position still waiting for the previous media belongs to it: a session restored on a
2051
+ // paused track must not send the next file there (past its end, it would "finish" at once).
2052
+ // The new media starts where `startTimeSec` says.
2053
+ this.pendingSeekSec = null;
2054
+ void this.loadConfig(autoplay);
2055
+ return;
2056
+ }
2057
+ if (playRequested) {
2058
+ // Same source picked again (a paused track the listener clicks): play it, once any load has finished.
2059
+ if (this.loading)
2060
+ this.pendingPlay = true;
2061
+ else
2062
+ void this.facade.play().catch(() => { });
2063
+ }
2064
+ }
81
2065
  ngOnInit() {
82
2066
  const plugins = [...(this.plugins ?? [])];
83
2067
  // v1.11: SSAI (server-side stitched streams) – rewrite manifest URL if configured.
84
2068
  if (this.config?.ads?.enabled && this.config?.ads?.mode === 'ssai') {
85
2069
  plugins.push(createSsaiManifestRewritePlugin());
86
2070
  }
87
- this.facade.attach(this.videoEl.nativeElement, plugins, this.config, this.ports, this.engines);
2071
+ const media = this.mediaElementForMode();
2072
+ this.watchAirPlay(media);
2073
+ this.facade.attach(media, plugins, this.config, this.ports, this.engines);
2074
+ // markForCheck() requis : la stratégie OnPush ne détecte pas les mutations
2075
+ // poussées depuis des souscriptions RxJS (cf. docs/AUDIT.md, manquement #7).
88
2076
  this.sub.add(this.facade.state$.subscribe((s) => {
89
2077
  this.state = s;
2078
+ // Engines without a media element (remote devices) only report the end through their state.
2079
+ if (s.kind === 'ended')
2080
+ this.onEnded();
2081
+ else if (s.kind === 'playing')
2082
+ this.endedLoadSequence = -1;
2083
+ this.stateChange.emit(s);
90
2084
  this.syncControlVisibility();
2085
+ this.cdr.markForCheck();
91
2086
  }));
92
2087
  this.sub.add(this.facade.time$.subscribe((t) => {
2088
+ // While a new source loads, the time still reported is the previous media's. Passed on, a
2089
+ // host would file it under the new track and resume that track there — past its end for a
2090
+ // short file, which then "finishes" at once.
2091
+ if (this.loading)
2092
+ return;
93
2093
  this.currentTime = t.currentTime;
94
2094
  this.duration = t.duration;
95
- this.progress.emit({ currentTime: this.currentTime, duration: this.duration });
2095
+ this.progress.emit({
2096
+ currentTime: this.currentTime,
2097
+ duration: this.duration,
2098
+ contentId: this.config?.source?.contentId,
2099
+ sourceUrl: this.config?.source?.url,
2100
+ });
2101
+ this.cdr.markForCheck();
96
2102
  }));
97
2103
  this.sub.add(this.facade.bufferAheadSec$.subscribe((value) => {
98
2104
  this.bufferAheadSec = value;
2105
+ this.cdr.markForCheck();
99
2106
  }));
100
2107
  this.sub.add(this.facade.tracks$.subscribe((tracks) => {
101
2108
  this.tracks = tracks;
2109
+ this.cdr.markForCheck();
102
2110
  }));
103
- this.facade.setVolume(this.volume);
104
- this.facade.load(this.config)
105
- .then(async () => {
106
- const start = Number(this.startTimeSec ?? 0);
107
- if (Number.isFinite(start) && start > 0.2) {
108
- try {
109
- const safe = Math.max(0, Math.min(start, (this.duration || start + 1) - 0.3));
110
- await this.facade.seekTo(safe);
111
- }
112
- catch { /* ignore */ }
113
- }
114
- await this.facade.play().catch(() => { });
115
- })
116
- .catch(() => { });
2111
+ this.sub.add(this.facade.captionCues$.subscribe((cues) => this.drawCaptions(cues)));
2112
+ this.bindRemotePlaybackState();
2113
+ this.applyVolume();
2114
+ this.initialized = true;
2115
+ void this.loadConfig(this.autoplayOnInit);
117
2116
  }
118
2117
  ngOnDestroy() {
2118
+ this.controlsObserver?.disconnect();
119
2119
  this.clearHideControlsTimer();
2120
+ this.clearSurfaceClickTimer();
2121
+ this.clearGestureTimer();
2122
+ this.remotePlaybackCleanup?.();
120
2123
  this.sub.unsubscribe();
121
2124
  this.facade.destroy();
122
2125
  }
@@ -125,7 +2128,7 @@ class SmartPlayerComponent {
125
2128
  }
126
2129
  get debugJson() {
127
2130
  try {
128
- const d = this.facade.diagnostics?.() ?? {};
2131
+ const d = this.facade.diagnostics();
129
2132
  const tel = this.ports?.telemetry?.export?.();
130
2133
  return JSON.stringify({ ...d, telemetry: tel }, null, 2);
131
2134
  }
@@ -133,9 +2136,8 @@ class SmartPlayerComponent {
133
2136
  return '{}';
134
2137
  }
135
2138
  }
136
- get autoplayBlocked() {
137
- const s = this.state;
138
- return s?.kind === 'error' && s?.error?.code === 'AUTOPLAY_BLOCKED';
2139
+ get currentError() {
2140
+ return this.state?.kind === 'error' ? this.state.error : null;
139
2141
  }
140
2142
  async copyDebug() {
141
2143
  try {
@@ -165,20 +2167,36 @@ class SmartPlayerComponent {
165
2167
  else
166
2168
  this.facade.play();
167
2169
  }
2170
+ requestPlaybackToggle() {
2171
+ const intent = this.playbackToggleIntent();
2172
+ this.togglePlay();
2173
+ this.playbackToggle.emit(intent);
2174
+ this.showSurfaceGesture(intent);
2175
+ }
168
2176
  seekRel(deltaSec) {
169
- this.facade.seekTo(Math.max(0, this.currentTime + deltaSec));
2177
+ this.seekTo(this.currentTime + deltaSec);
2178
+ }
2179
+ seekTo(timeSec) {
2180
+ const value = Number(timeSec);
2181
+ if (!Number.isFinite(value))
2182
+ return;
2183
+ const target = Math.max(0, value);
2184
+ this.pendingSeekSec = target;
2185
+ // A load in progress applies the pending position once the media is ready: seeking now would hit no engine.
2186
+ if (this.loading)
2187
+ return;
2188
+ void this.applySeek(target).then((applied) => {
2189
+ if (applied && this.pendingSeekSec === target)
2190
+ this.pendingSeekSec = null;
2191
+ });
170
2192
  }
171
2193
  onScrub(value) {
172
2194
  const v = Number(value);
173
2195
  if (Number.isFinite(v))
174
- this.facade.seekTo(v);
2196
+ this.seekTo(v);
175
2197
  }
176
2198
  onVolume(value) {
177
- const v = Math.max(0, Math.min(1, Number(value)));
178
- this.volume = Number.isFinite(v) ? v : this.volume;
179
- if (this.volume > 0)
180
- this.lastVolumeBeforeMute = this.volume;
181
- this.facade.setVolume(this.volume);
2199
+ this.setVolume(value, true);
182
2200
  }
183
2201
  toggleMute() {
184
2202
  if (this.volume <= 0.001) {
@@ -188,7 +2206,8 @@ class SmartPlayerComponent {
188
2206
  this.lastVolumeBeforeMute = this.volume;
189
2207
  this.volume = 0;
190
2208
  }
191
- this.facade.setVolume(this.volume);
2209
+ this.applyVolume();
2210
+ this.outputVolumeChange.emit(this.volume);
192
2211
  this.onInteraction();
193
2212
  }
194
2213
  async setPlaybackRate(rate) {
@@ -204,6 +2223,14 @@ class SmartPlayerComponent {
204
2223
  const v = this.videoEl?.nativeElement;
205
2224
  if (!v)
206
2225
  return;
2226
+ // A position requested while the media was loading and not applied yet is applied now that it can be.
2227
+ if (this.pendingSeekSec !== null && !this.loading) {
2228
+ const target = this.pendingSeekSec;
2229
+ void this.applySeek(target).then((applied) => {
2230
+ if (applied && this.pendingSeekSec === target)
2231
+ this.pendingSeekSec = null;
2232
+ });
2233
+ }
207
2234
  const width = v.videoWidth || 0;
208
2235
  const height = v.videoHeight || 0;
209
2236
  if (width > 0 && height > 0) {
@@ -211,10 +2238,20 @@ class SmartPlayerComponent {
211
2238
  }
212
2239
  }
213
2240
  onEnded() {
214
- // facade already emits 'ended' based on engine events; this output lets host apps chain playlists.
2241
+ // The media element's event and the engine state both announce the same end: handle it once per playback.
2242
+ if (this.endedLoadSequence === this.loadSequence)
2243
+ return;
2244
+ this.endedLoadSequence = this.loadSequence;
2245
+ // Repeat one replays the same media; otherwise hosts are told so they can chain their queue.
2246
+ if (this.repeatMode === 'one') {
2247
+ void this.facade.seekTo(0).then(() => this.facade.play()).catch(() => { });
2248
+ return;
2249
+ }
215
2250
  this.ended.emit();
216
2251
  }
217
2252
  toggleFullscreen() {
2253
+ if (this.isAudioMode)
2254
+ return;
218
2255
  const host = this.videoEl.nativeElement.parentElement;
219
2256
  if (!host)
220
2257
  return;
@@ -231,6 +2268,72 @@ class SmartPlayerComponent {
231
2268
  this.openMenu = this.openMenu === menu ? null : menu;
232
2269
  this.onInteraction(this.openMenu !== null);
233
2270
  }
2271
+ get playerMenuLabels() {
2272
+ return { ...DEFAULT_PLAYER_MENU_LABELS, ...this.menuLabels };
2273
+ }
2274
+ onCaptionsClick() {
2275
+ this.toggleMenu('captions');
2276
+ this.subtitles.emit();
2277
+ }
2278
+ openSubtitlePicker() {
2279
+ this.openMenu = null;
2280
+ this.onInteraction();
2281
+ this.subtitleFileInput?.nativeElement.click();
2282
+ }
2283
+ async onSubtitleFileChosen(event) {
2284
+ const input = event.target;
2285
+ const file = input.files?.[0];
2286
+ input.value = '';
2287
+ if (file)
2288
+ await this.loadSubtitleFile(file);
2289
+ }
2290
+ /** Reads a SubRip or WebVTT file and shows it over the video. */
2291
+ async loadSubtitleFile(file) {
2292
+ const { label, language } = describeSubtitleFile(file.name);
2293
+ try {
2294
+ const vtt = subtitleTextToVtt(decodeSubtitleBytes(new Uint8Array(await file.arrayBuffer())));
2295
+ if (!/-->/.test(vtt))
2296
+ throw new Error('No subtitle in this file');
2297
+ await this.facade.addExternalTextTrack({ label, ...(language ? { language } : {}), vtt });
2298
+ this.subtitlesLoaded.emit({ label, ok: true });
2299
+ }
2300
+ catch (error) {
2301
+ this.subtitlesLoaded.emit({ label, ok: false, error: error instanceof Error ? error.message : String(error) });
2302
+ }
2303
+ }
2304
+ controlsObserver;
2305
+ /** While the controls show, the subtitles sit just above them, however tall they are (a menu makes them taller). */
2306
+ followControlsHeight() {
2307
+ if (this.controlsObserver || typeof ResizeObserver !== 'function')
2308
+ return;
2309
+ const bar = this.videoEl.nativeElement.parentElement?.querySelector('.ngxsp-vod-bottom');
2310
+ const layer = this.captionsLayer?.nativeElement;
2311
+ if (!bar || !layer)
2312
+ return;
2313
+ // The top of the bar is a fade over the picture: the subtitles may overlap it.
2314
+ const lift = () => layer.style.setProperty('--ngxsp-captions-lift', `${Math.max(0, bar.offsetHeight - 24)}px`);
2315
+ this.controlsObserver = new ResizeObserver(lift);
2316
+ this.controlsObserver.observe(bar);
2317
+ lift();
2318
+ }
2319
+ /** Draws the cues of the loaded subtitles (their markup parsed by the browser: italics, bold, no script). */
2320
+ drawCaptions(cues) {
2321
+ const layer = this.captionsLayer?.nativeElement;
2322
+ if (!layer)
2323
+ return;
2324
+ if (cues.length)
2325
+ this.followControlsHeight();
2326
+ layer.replaceChildren(...cues.map((cue) => {
2327
+ const line = document.createElement('span');
2328
+ line.className = 'ngxsp-cue';
2329
+ const html = cue.getCueAsHTML?.();
2330
+ if (html)
2331
+ line.append(html);
2332
+ else
2333
+ line.textContent = cue.text ?? '';
2334
+ return line;
2335
+ }));
2336
+ }
234
2337
  async selectTextTrack(trackId) {
235
2338
  this.openMenu = null;
236
2339
  this.onInteraction();
@@ -263,6 +2366,8 @@ class SmartPlayerComponent {
263
2366
  this.onInteraction();
264
2367
  }
265
2368
  isPiPSupported() {
2369
+ if (this.isAudioMode)
2370
+ return false;
266
2371
  const video = this.videoEl?.nativeElement;
267
2372
  const doc = document;
268
2373
  return !!doc.pictureInPictureEnabled && !video?.disablePictureInPicture;
@@ -275,8 +2380,96 @@ class SmartPlayerComponent {
275
2380
  const doc = document;
276
2381
  return !!doc.pictureInPictureElement;
277
2382
  }
2383
+ async requestCast() {
2384
+ const video = this.videoEl?.nativeElement;
2385
+ if (!video)
2386
+ return;
2387
+ try {
2388
+ if (video.remote?.prompt) {
2389
+ this.castState = video.remote.state ?? 'connecting';
2390
+ await video.remote.prompt();
2391
+ this.castState = video.remote.state ?? this.castState;
2392
+ }
2393
+ else if (typeof video.webkitShowPlaybackTargetPicker === 'function') {
2394
+ video.webkitShowPlaybackTargetPicker();
2395
+ }
2396
+ }
2397
+ catch {
2398
+ this.castState = video.remote?.state ?? 'disconnected';
2399
+ }
2400
+ this.onInteraction();
2401
+ }
2402
+ isCastSupported() {
2403
+ const video = this.videoEl?.nativeElement;
2404
+ return !!video && (!!video.remote?.prompt || typeof video.webkitShowPlaybackTargetPicker === 'function');
2405
+ }
2406
+ isCastActive() {
2407
+ return this.castConnected || this.castState === 'connected';
2408
+ }
2409
+ showCastButton() {
2410
+ return this.castEnabled || this.isCastSupported();
2411
+ }
2412
+ /**
2413
+ * AirPlay, as WebKit offers it: the system decides what is reachable and draws the picker
2414
+ * itself. Nothing is discovered here, and nothing is sent — the route belongs to the device.
2415
+ * Everywhere else these stay false and the button never appears.
2416
+ */
2417
+ showAirPlayButton() {
2418
+ return this.airPlayAvailable;
2419
+ }
2420
+ isAirPlayActive() {
2421
+ return this.airPlayActive;
2422
+ }
2423
+ airPlayLabel() {
2424
+ return this.airPlayActive ? 'Stop AirPlay' : 'AirPlay';
2425
+ }
2426
+ openAirPlayPicker() {
2427
+ const media = this.videoEl?.nativeElement;
2428
+ media?.webkitShowPlaybackTargetPicker?.();
2429
+ }
2430
+ /** Wired to the media element whenever one is created, for video and for audio alike. */
2431
+ watchAirPlay(media) {
2432
+ const target = media;
2433
+ if (!('webkitShowPlaybackTargetPicker' in target))
2434
+ return;
2435
+ this.airPlayActive = target.webkitCurrentPlaybackTargetIsWireless === true;
2436
+ media.addEventListener('webkitplaybacktargetavailabilitychanged', (event) => {
2437
+ this.airPlayAvailable = event.availability === 'available';
2438
+ });
2439
+ media.addEventListener('webkitcurrentplaybacktargetiswirelesschanged', () => {
2440
+ this.airPlayActive = target.webkitCurrentPlaybackTargetIsWireless === true;
2441
+ });
2442
+ }
2443
+ castLabel() {
2444
+ if (this.castEnabled)
2445
+ return 'Play on device';
2446
+ if (this.castState === 'connected')
2447
+ return 'Playing on remote device';
2448
+ if (this.castState === 'connecting')
2449
+ return 'Connecting to remote device';
2450
+ return 'Play on device';
2451
+ }
2452
+ castHintLabel() {
2453
+ if (this.castEnabled)
2454
+ return 'Choose a device on your local network.';
2455
+ if (this.castState === 'connected')
2456
+ return 'This media is currently playing on a nearby device.';
2457
+ if (this.castState === 'connecting')
2458
+ return 'Trying to connect to a nearby playback device.';
2459
+ return 'Available when your platform supports remote playback on the same network.';
2460
+ }
2461
+ onCastClick() {
2462
+ if (this.castEnabled) {
2463
+ this.cast.emit();
2464
+ this.onInteraction();
2465
+ return;
2466
+ }
2467
+ void this.requestCast();
2468
+ }
278
2469
  captionsLabel() {
279
- return this.selectedTextTrackId !== null ? 'Captions on' : 'Load subtitles';
2470
+ const labels = this.playerMenuLabels;
2471
+ const shown = this.textTracks.find((track) => track.id === this.selectedTextTrackId);
2472
+ return shown ? `${labels.subtitles} - ${shown.label || shown.language || shown.id}` : labels.subtitles;
280
2473
  }
281
2474
  volumeLabel() {
282
2475
  if (this.volume <= 0.001)
@@ -285,50 +2478,36 @@ class SmartPlayerComponent {
285
2478
  return 'Vol';
286
2479
  return 'Volume';
287
2480
  }
288
- volumeIcon() {
289
- if (this.volume <= 0.001)
290
- return '🔇';
291
- if (this.volume < 0.5)
292
- return '🔉';
293
- return '🔊';
294
- }
295
2481
  playbackRateLabel() {
296
2482
  return `${this.playbackRate}x`;
297
2483
  }
298
2484
  statusLabel() {
299
2485
  const kind = this.state?.kind;
300
2486
  if (kind === 'playing')
301
- return this.playbackRate === 1 ? 'Playing' : `Playing ${this.playbackRate}x`;
2487
+ return this.playbackRate === 1 ? this.playingStatusLabel : `${this.playingStatusLabel} ${this.playbackRate}x`;
302
2488
  if (kind === 'paused')
303
- return 'Paused';
2489
+ return this.pausedStatusLabel;
304
2490
  if (kind === 'buffering')
305
- return 'Buffering';
2491
+ return this.bufferingStatusLabel;
306
2492
  if (kind === 'loading')
307
- return 'Loading';
2493
+ return this.loadingStatusLabel;
308
2494
  if (kind === 'ended')
309
- return 'Ended';
2495
+ return this.endedStatusLabel;
310
2496
  if (kind === 'error')
311
- return 'Error';
312
- return 'Ready';
313
- }
314
- keyboardHintLabel() {
315
- return 'Space play • Arrows seek • F fullscreen';
316
- }
317
- tracksButtonLabel() {
318
- return this.textTracks.length || this.audioTracks.length ? 'Tracks' : 'Media';
319
- }
320
- trackLabel(track) {
321
- return track.label || track.language || track.id;
2497
+ return this.errorStatusLabel;
2498
+ return this.readyStatusLabel;
322
2499
  }
323
- qualityLabel(track) {
324
- if (track.label)
325
- return track.label;
326
- const parts = [];
327
- if (track.height)
328
- parts.push(`${track.height}p`);
329
- if (track.bandwidth)
330
- parts.push(`${(track.bandwidth / 1_000_000).toFixed(1)} Mbps`);
331
- return parts.join(' • ') || track.id;
2500
+ keyboardHintLabel() {
2501
+ return this.isAudioMode ? 'Space play • Arrows seek • C device' : 'Space play • Arrows seek • F fullscreen • C device';
2502
+ }
2503
+ tracksButtonLabel() {
2504
+ return this.textTracks.length || this.audioTracks.length ? this.playerMenuLabels.tracks : this.playerMenuLabels.media;
2505
+ }
2506
+ onPrimarySurfaceDoubleClick() {
2507
+ if (this.isAudioMode)
2508
+ return;
2509
+ this.clearSurfaceClickTimer();
2510
+ this.toggleFullscreen();
332
2511
  }
333
2512
  bufferPercent() {
334
2513
  if (!this.duration || !Number.isFinite(this.duration) || this.duration <= 0)
@@ -345,12 +2524,32 @@ class SmartPlayerComponent {
345
2524
  return `${m}:${String(r).padStart(2, '0')}`;
346
2525
  }
347
2526
  noop() { }
2527
+ /**
2528
+ * Music plays in a real <audio> element: mobile browsers and Android WebViews pause <video> elements as soon as
2529
+ * the page is hidden, which would stop the music when the app goes to the background or the screen turns off.
2530
+ */
2531
+ mediaElementForMode() {
2532
+ const video = this.videoEl.nativeElement;
2533
+ if (!this.isAudioMode || typeof document === 'undefined')
2534
+ return video;
2535
+ const audio = document.createElement('audio');
2536
+ audio.preload = 'metadata';
2537
+ audio.setAttribute('x-webkit-airplay', 'allow');
2538
+ audio.addEventListener('timeupdate', () => this.noop());
2539
+ audio.addEventListener('loadedmetadata', () => this.onLoadedMetadata());
2540
+ audio.addEventListener('ended', () => this.onEnded());
2541
+ video.replaceWith(audio);
2542
+ // The facade and helpers only use HTMLMediaElement members for audio (video-only APIs are feature-checked).
2543
+ this.videoEl = new ElementRef(audio);
2544
+ return this.videoEl.nativeElement;
2545
+ }
348
2546
  onInteraction(persist = false) {
349
2547
  this.controlsVisible = true;
350
2548
  this.clearHideControlsTimer();
351
2549
  if (!persist && this.state?.kind === 'playing' && this.openMenu === null) {
352
2550
  this.hideControlsTimer = setTimeout(() => {
353
2551
  this.controlsVisible = false;
2552
+ this.cdr.markForCheck();
354
2553
  }, 2200);
355
2554
  }
356
2555
  }
@@ -364,11 +2563,18 @@ class SmartPlayerComponent {
364
2563
  const target = event.target;
365
2564
  if (!target)
366
2565
  return;
367
- if (target.closest('.ngxsp-menu, .ngxsp-nfx-chip, .ngxsp-btn, .ngxsp-debug, .ngxsp-nfx-side, .ngxsp-nfx-center, .ngxsp-range')) {
2566
+ if (target.closest('button, input, textarea, select, summary, [contenteditable="true"], .ngxsp-menu, .ngxsp-controls, .ngxsp-debug, .ngxsp-overlay, .ngxsp-range')) {
2567
+ return;
2568
+ }
2569
+ if (event.detail > 1) {
2570
+ this.clearSurfaceClickTimer();
368
2571
  return;
369
2572
  }
370
2573
  this.openMenu = null;
371
2574
  this.onInteraction();
2575
+ if (this.uiSkin === 'vod' && !this.isAudioMode) {
2576
+ this.queueSurfacePlaybackToggle();
2577
+ }
372
2578
  }
373
2579
  onEscape() {
374
2580
  if (this.openMenu !== null) {
@@ -377,11 +2583,13 @@ class SmartPlayerComponent {
377
2583
  }
378
2584
  }
379
2585
  onKeydown(event) {
2586
+ if (!this.documentShortcuts)
2587
+ return;
380
2588
  if (this.isInteractiveTarget(event.target))
381
2589
  return;
382
2590
  if (event.code === 'Space') {
383
2591
  event.preventDefault();
384
- this.togglePlay();
2592
+ this.requestPlaybackToggle();
385
2593
  this.onInteraction();
386
2594
  return;
387
2595
  }
@@ -398,6 +2606,8 @@ class SmartPlayerComponent {
398
2606
  return;
399
2607
  }
400
2608
  if (event.code === 'KeyF') {
2609
+ if (this.isAudioMode)
2610
+ return;
401
2611
  event.preventDefault();
402
2612
  this.toggleFullscreen();
403
2613
  this.onInteraction();
@@ -406,8 +2616,110 @@ class SmartPlayerComponent {
406
2616
  if (event.code === 'KeyM') {
407
2617
  event.preventDefault();
408
2618
  this.toggleMute();
2619
+ return;
2620
+ }
2621
+ if (event.code === 'KeyC') {
2622
+ if (!this.showCastButton())
2623
+ return;
2624
+ event.preventDefault();
2625
+ this.onCastClick();
2626
+ }
2627
+ }
2628
+ async loadConfig(autoplay) {
2629
+ const sequence = ++this.loadSequence;
2630
+ this.loading = true;
2631
+ this.loadingAutoplay = autoplay;
2632
+ this.loadedSourceSignature = this.sourceSignature(this.config);
2633
+ this.duration = 0;
2634
+ this.currentTime = 0;
2635
+ this.bufferAheadSec = 0;
2636
+ this.tracks = [];
2637
+ this.cdr.markForCheck();
2638
+ try {
2639
+ await this.facade.load(this.config);
2640
+ if (this.pendingSeekSec !== null) {
2641
+ const target = this.pendingSeekSec;
2642
+ // Cleared only when the position really reached an engine; otherwise the metadata event retries it.
2643
+ if (await this.applySeek(target) && this.pendingSeekSec === target)
2644
+ this.pendingSeekSec = null;
2645
+ }
2646
+ else {
2647
+ await this.applyStartTime();
2648
+ }
2649
+ // A newer load owns playback: only the latest request may start it.
2650
+ if (sequence === this.loadSequence && (autoplay || this.pendingPlay)) {
2651
+ this.pendingPlay = false;
2652
+ await this.facade.play().catch(() => { });
2653
+ }
2654
+ }
2655
+ catch {
2656
+ // The facade has already emitted a typed player error.
2657
+ }
2658
+ finally {
2659
+ if (sequence === this.loadSequence) {
2660
+ this.loading = false;
2661
+ // A position asked for while this load was finishing, after it looked for one, is applied now.
2662
+ const target = this.pendingSeekSec;
2663
+ if (target !== null) {
2664
+ void this.applySeek(target).then((applied) => {
2665
+ if (applied && this.pendingSeekSec === target)
2666
+ this.pendingSeekSec = null;
2667
+ });
2668
+ }
2669
+ }
2670
+ }
2671
+ }
2672
+ async applyStartTime() {
2673
+ const start = Number(this.startTimeSec ?? 0);
2674
+ if (!Number.isFinite(start) || start <= 0.2)
2675
+ return;
2676
+ await this.applySeek(start);
2677
+ }
2678
+ async applySeek(timeSec) {
2679
+ try {
2680
+ return await this.facade.seekTo(this.safeSeekTime(timeSec));
2681
+ }
2682
+ catch {
2683
+ // Best effort: some engines only allow seeking after enough metadata is loaded.
2684
+ return false;
409
2685
  }
410
2686
  }
2687
+ safeSeekTime(timeSec) {
2688
+ return Math.max(0, Math.min(timeSec, (this.duration || timeSec + 1) - 0.3));
2689
+ }
2690
+ sourceSignature(config) {
2691
+ const source = config?.source;
2692
+ if (!source)
2693
+ return '';
2694
+ return [
2695
+ source.type,
2696
+ source.url,
2697
+ source.contentId,
2698
+ source.live ? 'live' : 'vod',
2699
+ source.offlineEligible ? 'offline' : 'online',
2700
+ ].join('|');
2701
+ }
2702
+ bindRemotePlaybackState() {
2703
+ const video = this.videoEl?.nativeElement;
2704
+ const remote = video?.remote;
2705
+ if (!video || !remote)
2706
+ return;
2707
+ const sync = () => {
2708
+ this.castState = remote.state ?? 'disconnected';
2709
+ this.cdr.markForCheck();
2710
+ };
2711
+ sync();
2712
+ remote.addEventListener?.('connecting', sync);
2713
+ remote.addEventListener?.('connect', sync);
2714
+ remote.addEventListener?.('disconnect', sync);
2715
+ video.addEventListener('webkitcurrentplaybacktargetiswirelesschanged', sync);
2716
+ this.remotePlaybackCleanup = () => {
2717
+ remote.removeEventListener?.('connecting', sync);
2718
+ remote.removeEventListener?.('connect', sync);
2719
+ remote.removeEventListener?.('disconnect', sync);
2720
+ video.removeEventListener('webkitcurrentplaybacktargetiswirelesschanged', sync);
2721
+ };
2722
+ }
411
2723
  syncControlVisibility() {
412
2724
  if (this.state?.kind === 'playing' && this.openMenu === null) {
413
2725
  this.onInteraction();
@@ -416,647 +2728,562 @@ class SmartPlayerComponent {
416
2728
  this.clearHideControlsTimer();
417
2729
  this.controlsVisible = true;
418
2730
  }
2731
+ applyVolume() {
2732
+ try {
2733
+ const media = this.videoEl.nativeElement;
2734
+ media.volume = this.volume;
2735
+ // iOS ignores volume writes: without this, muting and the volume slider have no effect on iPhone and iPad.
2736
+ media.muted = this.volume <= 0.001;
2737
+ }
2738
+ catch {
2739
+ // Some browser environments reject direct volume writes; the engine path remains available.
2740
+ }
2741
+ this.facade.setVolume(this.volume);
2742
+ }
2743
+ syncOutputVolumeInput() {
2744
+ this.setVolume(this.outputVolume, false);
2745
+ }
2746
+ setOutputVolume(value) {
2747
+ this.setVolume(value, false);
2748
+ }
2749
+ setVolume(value, emitChange) {
2750
+ const nextVolume = this.normalizeVolume(value, this.volume);
2751
+ if (Math.abs(nextVolume - this.volume) < 0.001)
2752
+ return;
2753
+ this.volume = nextVolume;
2754
+ if (this.volume > 0)
2755
+ this.lastVolumeBeforeMute = this.volume;
2756
+ if (this.initialized)
2757
+ this.applyVolume();
2758
+ if (emitChange)
2759
+ this.outputVolumeChange.emit(this.volume);
2760
+ }
2761
+ normalizeVolume(value, fallback) {
2762
+ const numeric = Number(value);
2763
+ if (!Number.isFinite(numeric))
2764
+ return fallback;
2765
+ return Math.max(0, Math.min(1, numeric));
2766
+ }
419
2767
  clearHideControlsTimer() {
420
2768
  if (this.hideControlsTimer) {
421
2769
  clearTimeout(this.hideControlsTimer);
422
2770
  this.hideControlsTimer = null;
423
2771
  }
424
2772
  }
2773
+ queueSurfacePlaybackToggle() {
2774
+ this.clearSurfaceClickTimer();
2775
+ this.surfaceClickTimer = setTimeout(() => {
2776
+ this.surfaceClickTimer = null;
2777
+ this.requestPlaybackToggle();
2778
+ this.cdr.markForCheck();
2779
+ }, 180);
2780
+ }
2781
+ clearSurfaceClickTimer() {
2782
+ if (this.surfaceClickTimer) {
2783
+ clearTimeout(this.surfaceClickTimer);
2784
+ this.surfaceClickTimer = null;
2785
+ }
2786
+ }
2787
+ playbackToggleIntent() {
2788
+ return this.state?.kind === 'playing' ? 'pause' : 'play';
2789
+ }
2790
+ showSurfaceGesture(intent) {
2791
+ this.surfaceGesture = intent;
2792
+ this.clearGestureTimer();
2793
+ this.gestureTimer = setTimeout(() => {
2794
+ this.surfaceGesture = null;
2795
+ this.cdr.markForCheck();
2796
+ }, 520);
2797
+ this.cdr.markForCheck();
2798
+ }
2799
+ clearGestureTimer() {
2800
+ if (this.gestureTimer) {
2801
+ clearTimeout(this.gestureTimer);
2802
+ this.gestureTimer = null;
2803
+ }
2804
+ }
425
2805
  isInteractiveTarget(target) {
426
2806
  const el = target;
427
2807
  if (!el)
428
2808
  return false;
429
2809
  return !!el.closest('input, button, textarea, select, summary, [contenteditable="true"]');
430
2810
  }
431
- errorTitle() {
432
- const s = this.state;
433
- const code = s?.error?.code ?? s?.error?.name ?? 'UNKNOWN';
434
- if (code === 'ENGINE_MISSING')
435
- return 'Playback engine missing';
436
- if (code === 'AUTOPLAY_BLOCKED')
437
- return 'Click to start playback';
438
- if (code === 'NETWORK_HTTP')
439
- return 'Network error';
440
- if (code === 'DECODE_ERROR')
441
- return 'Decode error';
442
- if (code === 'SRC_NOT_SUPPORTED')
443
- return 'Format not supported';
444
- return 'Playback error';
445
- }
446
- errorHint() {
447
- const s = this.state;
448
- const code = s?.error?.code ?? s?.error?.name ?? 'UNKNOWN';
449
- if (code === 'ENGINE_MISSING') {
450
- return 'This source requires an additional engine. Install @ngxsp/engine-shaka and pass it via [engines].';
451
- }
452
- if (code === 'AUTOPLAY_BLOCKED')
453
- return 'Your browser blocked autoplay. Press Play to start.';
454
- if (code === 'NETWORK_HTTP')
455
- return 'Check your connection or try again.';
456
- return s?.error?.message ? String(s.error.message) : 'Unknown error';
457
- }
458
- async onRetry() {
459
- try {
460
- await this.facade.reload();
461
- }
462
- catch { }
463
- }
464
- async onUserPlay() {
465
- try {
466
- await this.facade.play();
467
- }
468
- catch { }
469
- }
470
2811
  emitNext() {
471
2812
  this.next.emit();
472
2813
  }
473
2814
  emitPrev() {
474
2815
  this.prev.emit();
475
2816
  }
476
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SmartPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
477
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", type: SmartPlayerComponent, isStandalone: true, selector: "ngx-smart-player", inputs: { config: "config", plugins: "plugins", ports: "ports", engines: "engines", debug: "debug", startTimeSec: "startTimeSec", canPrev: "canPrev", canNext: "canNext", shuffleActive: "shuffleActive", shuffleLabel: "shuffleLabel", repeatMode: "repeatMode", repeatLabel: "repeatLabel", hasPlaylistControls: "hasPlaylistControls" }, outputs: { ended: "ended", videoSize: "videoSize", progress: "progress", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", subtitles: "subtitles" }, host: { listeners: { "document:keydown.escape": "onEscape()", "document:keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "videoEl", first: true, predicate: ["video"], descendants: true, static: true }], ngImport: i0, template: `
478
- <div class="ngxsp-root" [class.ngxsp-skin-netflix]="uiSkin==='netflix'" [class.ngxsp-ui-visible]="uiVisible" [attr.data-kind]="state?.kind">
479
- <div class="ngxsp-video" (dblclick)="toggleFullscreen()" (mousemove)="onInteraction()" (mouseenter)="onInteraction()" (mouseleave)="onPointerLeave()" (focusin)="onInteraction()" (touchstart)="onInteraction(true)" (click)="onRootClick($event)">
480
- <video #video playsinline (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
481
- @if (state?.kind==='error') {
482
- <div class="ngxsp-overlay" role="alert">
483
- <div class="ngxsp-overlay-card">
484
- <div class="ngxsp-overlay-title">Playback error</div>
485
- <div class="ngxsp-overlay-msg">
486
- <div style="font-weight:600; margin-bottom:6px;">{{errorTitle()}}</div>
487
- <div style="opacity:.85; font-size: 13px; line-height: 1.3;">{{errorHint()}}</div>
488
- <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">
489
- <button type="button" class="ngxsp-btn" (click)="reload()">Retry</button>
490
- @if (autoplayBlocked) {
491
- <button type="button" class="ngxsp-btn" (click)="tryPlay()">Play</button>
492
- }
493
- </div>
494
- </div>
2817
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2818
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.33", type: SmartPlayerComponent, isStandalone: true, selector: "ngx-smart-player", inputs: { config: "config", plugins: "plugins", ports: "ports", engines: "engines", debug: "debug", startTimeSec: "startTimeSec", autoplayOnInit: "autoplayOnInit", playRequest: "playRequest", canPrev: "canPrev", canNext: "canNext", previousLabel: "previousLabel", nextLabel: "nextLabel", playLabel: "playLabel", pauseLabel: "pauseLabel", shuffleActive: "shuffleActive", shuffleLabel: "shuffleLabel", repeatMode: "repeatMode", repeatLabel: "repeatLabel", hasPlaylistControls: "hasPlaylistControls", mediaMode: "mediaMode", castEnabled: "castEnabled", castConnected: "castConnected", documentShortcuts: "documentShortcuts", nowPlayingLabel: "nowPlayingLabel", nowListeningLabel: "nowListeningLabel", fullscreenHintLabel: "fullscreenHintLabel", fullscreenLabel: "fullscreenLabel", readyStatusLabel: "readyStatusLabel", playingStatusLabel: "playingStatusLabel", pausedStatusLabel: "pausedStatusLabel", bufferingStatusLabel: "bufferingStatusLabel", loadingStatusLabel: "loadingStatusLabel", endedStatusLabel: "endedStatusLabel", errorStatusLabel: "errorStatusLabel", outputVolume: "outputVolume", outputVolumeLabel: "outputVolumeLabel", outputMuteLabel: "outputMuteLabel", outputUnmuteLabel: "outputUnmuteLabel", timelineDurationSec: "timelineDurationSec", timelineMarkers: "timelineMarkers", menuLabels: "menuLabels" }, outputs: { ended: "ended", videoSize: "videoSize", progress: "progress", prev: "prev", next: "next", toggleShuffle: "toggleShuffle", cycleRepeat: "cycleRepeat", subtitles: "subtitles", subtitlesLoaded: "subtitlesLoaded", cast: "cast", stateChange: "stateChange", playbackToggle: "playbackToggle", outputVolumeChange: "outputVolumeChange" }, host: { listeners: { "document:keydown.escape": "onEscape()", "document:keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "videoEl", first: true, predicate: ["video"], descendants: true, static: true }, { propertyName: "captionsLayer", first: true, predicate: ["captionsLayer"], descendants: true, static: true }, { propertyName: "subtitleFileInput", first: true, predicate: ["subtitleFileInput"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
2819
+ <div class="ngxsp-root" [class.ngxsp-skin-vod]="uiSkin==='vod'" [class.ngxsp-ui-visible]="uiVisible" [class.ngxsp-mode-audio]="isAudioMode" [class.ngxsp-mode-video]="!isAudioMode" [attr.data-kind]="state?.kind">
2820
+ <!-- Surface de présentation : interactions pointeur non essentielles, toutes
2821
+ dupliquées au clavier par @HostListener('document:keydown') (Espace/flèches/F/M/C)
2822
+ et par les contrôles focusables. -->
2823
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
2824
+ <div class="ngxsp-video" (dblclick)="onPrimarySurfaceDoubleClick()" (mousemove)="onInteraction()" (mouseenter)="onInteraction()" (mouseleave)="onPointerLeave()" (focusin)="onInteraction()" (touchstart)="onInteraction(true)" (click)="onRootClick($event)">
2825
+ <video #video playsinline x-webkit-airplay="allow" (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
2826
+ <!-- Subtitles loaded from a file, drawn here at the bottom of the picture, above the controls when they show. -->
2827
+ <div #captionsLayer class="ngxsp-captions" aria-hidden="true"></div>
2828
+ @if (!isAudioMode) {
2829
+ <input #subtitleFileInput class="ngxsp-subtitleInput" type="file" accept=".srt,.vtt,text/vtt,application/x-subrip" hidden (change)="onSubtitleFileChosen($event)" />
2830
+ }
2831
+ @if (isAudioMode) {
2832
+ <div class="ngxsp-audio-stage" aria-hidden="true">
2833
+ <div class="ngxsp-audio-disc"></div>
2834
+ <div class="ngxsp-audio-copy">
2835
+ <div class="ngxsp-audio-eyebrow">Now Listening</div>
2836
+ <div class="ngxsp-audio-title">{{displayTitle}}</div>
2837
+ <div class="ngxsp-audio-subtitle">{{displaySubtitle}}</div>
2838
+ </div>
2839
+ <div class="ngxsp-audio-bars">
2840
+ <span></span><span></span><span></span><span></span><span></span>
495
2841
  </div>
496
2842
  </div>
497
2843
  }
498
-
499
- @if (debug) {
500
- <details class="ngxsp-debug" (click)="$event.stopPropagation()">
501
- <summary>Debug</summary>
502
- <div style="display:flex; gap:8px; margin:8px 0; flex-wrap:wrap;">
503
- <button type="button" class="ngxsp-btn" (click)="copyDebug()">Copy JSON</button>
504
- <button type="button" class="ngxsp-btn" (click)="downloadDebug()">Download</button>
505
- </div>
506
- <pre>{{debugJson}}</pre>
507
- </details>
2844
+ <ngxsp-player-overlays
2845
+ [error]="currentError"
2846
+ [debug]="debug"
2847
+ [debugJson]="debugJson"
2848
+ (retry)="reload()"
2849
+ (requestPlay)="tryPlay()"
2850
+ (copyJson)="copyDebug()"
2851
+ (downloadJson)="downloadDebug()"
2852
+ />
2853
+ @if (surfaceGesture) {
2854
+ <div class="ngxsp-gesturePulse" aria-hidden="true">
2855
+ @if (surfaceGesture === 'pause') {
2856
+ <ngxsp-smart-icon name="pause" />
2857
+ } @else {
2858
+ <ngxsp-smart-icon name="play" />
2859
+ }
2860
+ </div>
508
2861
  }
509
2862
 
510
- @if (uiSkin==='netflix') {
511
- <div class="ngxsp-nfx-top">
512
- <div class="ngxsp-nfx-titleWrap">
513
- <div class="ngxsp-nfx-eyebrow">Now Playing</div>
514
- <div class="ngxsp-nfx-title">{{displayTitle}}</div>
515
- </div>
516
- <div class="ngxsp-nfx-spacer"></div>
517
- <div class="ngxsp-nfx-status">{{ statusLabel() }}</div>
518
- <div class="ngxsp-nfx-topHint">Double-click for fullscreen</div>
519
- </div>
520
-
521
- <div class="ngxsp-nfx-centerDock">
522
- @if (hasPlaylistControls) {
523
- <button type="button" class="ngxsp-nfx-side" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item">
524
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.53 12 18 18.47v-12L9.53 12Zm-1.53 6.25a.75.75 0 0 0 1.25-.56V6.31a.75.75 0 0 0-1.25-.56L1.06 11.44a.75.75 0 0 0 0 1.12L8 18.25Z"/></svg>
525
- </button>
526
- }
527
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds">
528
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 5H5m0 0v4m0-4 4.5 4.5A7 7 0 1 1 5 12"/></svg>
529
- <span class="ngxsp-iconBadge">10</span>
530
- </button>
531
- <button type="button" class="ngxsp-nfx-center" (click)="togglePlay()" aria-label="Play/Pause">
532
- @if (state?.kind==='playing') {
533
- <svg class="ngxsp-icon ngxsp-icon-lg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
534
- } @else {
535
- <svg class="ngxsp-icon ngxsp-icon-lg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.25 6.82c0-1.3 1.43-2.1 2.54-1.43l8.12 4.93a1.96 1.96 0 0 1 0 3.36l-8.12 4.93a1.67 1.67 0 0 1-2.54-1.43V6.82Z"/></svg>
536
- }
537
- </button>
538
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(10)" aria-label="Seek forward 10 seconds">
539
- <span class="ngxsp-iconBadge">10</span>
540
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 5h4m0 0v4m0-4-4.5 4.5A7 7 0 1 0 19 12"/></svg>
541
- </button>
542
- @if (hasPlaylistControls) {
543
- <button type="button" class="ngxsp-nfx-side" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item">
544
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14.47 12 6 5.53v12L14.47 12ZM16 5.75a.75.75 0 0 0-1.25.56v11.38a.75.75 0 0 0 1.25.56l6.94-5.69a.75.75 0 0 0 0-1.12L16 5.75Z"/></svg>
545
- </button>
546
- }
547
- </div>
548
-
549
- <div class="ngxsp-controls ngxsp-nfx-bottom">
550
- <div class="ngxsp-nfx-metaRow">
551
- <div class="ngxsp-nfx-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
552
- <div class="ngxsp-nfx-volumeGroup">
553
- <button type="button" class="ngxsp-nfx-chip ngxsp-nfx-chip-quiet" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
554
- @if (volume <= 0.001) {
555
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
556
- } @else if (volume < 0.5) {
557
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95"/></svg>
558
- } @else {
559
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95m2.75-7.7a7.5 7.5 0 0 1 0 10.45"/></svg>
560
- }
561
- </button>
562
- <input class="ngxsp-range ngxsp-nfx-volume" type="range"
563
- min="0" max="1" step="0.01"
564
- [value]="volume"
565
- (input)="onVolume(($any($event.target).value))"
566
- aria-label="Volume"
567
- />
568
- </div>
569
- <div class="ngxsp-nfx-spacer"></div>
570
- @if (hasPlaylistControls) {
571
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
572
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
573
- </button>
574
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
575
- @if (repeatMode === 'one') {
576
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
577
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/>
578
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/>
579
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/>
580
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/>
581
- <text x="16.8" y="17.6" fill="currentColor" stroke="none" font-size="7.5" font-weight="700" text-anchor="middle">1</text>
582
- </svg>
583
- } @else {
584
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/></svg>
585
- }
586
- </button>
587
- }
588
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()">
589
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg>
590
- </button>
591
- <button type="button" class="ngxsp-nfx-chip" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
592
- @if (selectedTextTrackId !== null) {
593
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/></svg>
594
- } @else {
595
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/><path stroke-linecap="round" stroke-width="1.8" d="M6 6l12 12"/></svg>
596
- }
597
- </button>
598
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()">
599
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg>
600
- </button>
601
- @if (isPiPSupported()) {
602
- <button type="button" class="ngxsp-nfx-chip" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
603
- @if (isPiPActive()) {
604
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
605
- } @else {
606
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" stroke-width="1.8"/></svg>
607
- }
608
- </button>
609
- }
610
- </div>
611
-
612
- <input class="ngxsp-range ngxsp-nfx-range" type="range"
613
- min="0" [max]="duration || 0" step="0.1"
614
- [value]="currentTime"
615
- (input)="onScrub(($any($event.target).value))"
616
- aria-label="Seek"
617
- />
618
-
619
- <div class="ngxsp-nfx-metaRow">
620
- <div class="ngxsp-nfx-spacer"></div>
621
- <div class="ngxsp-nfx-label">{{ keyboardHintLabel() }}</div>
622
- </div>
623
-
624
- <div class="ngxsp-buffer" aria-hidden="true">
625
- <div class="ngxsp-bufferFill" [style.width.%]="bufferPercent()"></div>
626
- </div>
627
-
628
- @if (openMenu === 'tracks') {
629
- <div class="ngxsp-menu" (click)="$event.stopPropagation()">
630
- <div class="ngxsp-menuSection">
631
- <div class="ngxsp-menuTitle">Captions</div>
632
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId === null" (click)="selectTextTrack(null)">Off</button>
633
- @for (track of textTracks; track track.id) {
634
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId === track.id" (click)="selectTextTrack(track.id)">
635
- {{ trackLabel(track) }}
636
- </button>
637
- }
638
- </div>
639
-
640
- <div class="ngxsp-menuSection">
641
- <div class="ngxsp-menuTitle">Audio</div>
642
- @if (audioTracks.length) {
643
- @for (track of audioTracks; track track.id) {
644
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedAudioTrackId === track.id" (click)="selectAudioTrack(track.id)">
645
- {{ trackLabel(track) }}
646
- </button>
647
- }
648
- } @else {
649
- <div class="ngxsp-menuHint">Single audio track</div>
650
- }
651
- </div>
652
-
653
- <div class="ngxsp-menuSection">
654
- <div class="ngxsp-menuTitle">Quality</div>
655
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId === null" (click)="selectVideoTrack(null)">Auto</button>
656
- @if (videoTracks.length) {
657
- @for (track of videoTracks; track track.id) {
658
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId === track.id" (click)="selectVideoTrack(track.id)">
659
- {{ qualityLabel(track) }}
660
- </button>
661
- }
662
- } @else {
663
- <div class="ngxsp-menuHint">Single quality</div>
664
- }
665
- </div>
666
- </div>
667
- }
668
-
669
- @if (openMenu === 'speed') {
670
- <div class="ngxsp-menu ngxsp-menuCompact" (click)="$event.stopPropagation()">
671
- <div class="ngxsp-menuSection">
672
- <div class="ngxsp-menuTitle">Playback speed</div>
673
- @for (rate of playbackRates; track rate) {
674
- <button type="button" class="ngxsp-menuItem" [class.active]="playbackRate === rate" (click)="setPlaybackRate(rate)">
675
- {{ rate }}x
676
- </button>
677
- }
678
- </div>
2863
+ @if (uiSkin==='vod') {
2864
+ <ngxsp-player-controls-vod
2865
+ [playing]="state?.kind==='playing'"
2866
+ [isAudioMode]="isAudioMode"
2867
+ [displayTitle]="displayTitle"
2868
+ [nowPlayingLabel]="nowPlayingLabel"
2869
+ [nowListeningLabel]="nowListeningLabel"
2870
+ [fullscreenHintLabel]="fullscreenHintLabel"
2871
+ [statusLabel]="statusLabel()"
2872
+ [keyboardHint]="keyboardHintLabel()"
2873
+ [hasPlaylistControls]="hasPlaylistControls"
2874
+ [canPrev]="canPrev"
2875
+ [canNext]="canNext"
2876
+ [previousLabel]="previousLabel"
2877
+ [nextLabel]="nextLabel"
2878
+ [playLabel]="playLabel"
2879
+ [pauseLabel]="pauseLabel"
2880
+ [shuffleActive]="shuffleActive"
2881
+ [shuffleLabel]="shuffleLabel"
2882
+ [repeatMode]="repeatMode"
2883
+ [repeatLabel]="repeatLabel"
2884
+ [captionsActive]="selectedTextTrackId !== null"
2885
+ [tracksLabel]="tracksButtonLabel()"
2886
+ [captionsLabel]="captionsLabel()"
2887
+ [speedLabel]="playbackRateLabel()"
2888
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
2889
+ [muteLabel]="outputMuteLabel"
2890
+ [unmuteLabel]="outputUnmuteLabel"
2891
+ [showAirPlay]="showAirPlayButton()"
2892
+ [airPlayActive]="isAirPlayActive()"
2893
+ [airPlayLabel]="airPlayLabel()"
2894
+ (airPlay)="openAirPlayPicker()"
2895
+ [showCast]="showCastButton()"
2896
+ [castActive]="isCastActive()"
2897
+ [castLabel]="castLabel()"
2898
+ [castHint]="castHintLabel()"
2899
+ [pipSupported]="isPiPSupported()"
2900
+ [pipActive]="isPiPActive()"
2901
+ [pipLabel]="pipLabel()"
2902
+ [fullscreenLabel]="fullscreenLabel"
2903
+ [volume]="volume"
2904
+ [currentTime]="currentTime"
2905
+ [duration]="duration"
2906
+ [buffered]="bufferAheadSec"
2907
+ [bufferPercent]="bufferPercent()"
2908
+ [profile]="sourceProfile"
2909
+ [timelineDurationSec]="timelineDurationSec ?? 0"
2910
+ [timelineMarkers]="timelineMarkers"
2911
+ (playToggle)="requestPlaybackToggle()"
2912
+ (seekRelative)="seekRel($event)"
2913
+ (prev)="emitPrev()"
2914
+ (next)="emitNext()"
2915
+ (toggleShuffle)="toggleShuffle.emit()"
2916
+ (cycleRepeat)="cycleRepeat.emit()"
2917
+ (openMenu)="toggleMenu($event)"
2918
+ (captions)="onCaptionsClick()"
2919
+ (cast)="onCastClick()"
2920
+ (pip)="togglePiP()"
2921
+ (fullscreen)="toggleFullscreen()"
2922
+ (mute)="toggleMute()"
2923
+ (scrub)="onScrub($event)"
2924
+ (volumeChange)="onVolume($event)"
2925
+ >
2926
+ <div ngxsp-player-inline-controls class="ngxsp-inline-projection">
2927
+ <ng-content select="[ngxsp-player-inline-controls]" />
679
2928
  </div>
680
- }
681
- </div>
682
- } @else {
683
- <div class="ngxsp-controls">
684
- <button type="button" class="ngxsp-btn" (click)="togglePlay()" aria-label="Play/Pause" title="Play/Pause">
685
- @if (state?.kind==='playing') {
686
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
687
- } @else {
688
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.25 6.82c0-1.3 1.43-2.1 2.54-1.43l8.12 4.93a1.96 1.96 0 0 1 0 3.36l-8.12 4.93a1.67 1.67 0 0 1-2.54-1.43V6.82Z"/></svg>
689
- }
690
- </button>
691
- <button type="button" class="ngxsp-btn" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
692
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 5H5m0 0v4m0-4 4.5 4.5A7 7 0 1 1 5 12"/></svg>
693
- <span class="ngxsp-iconBadge">10</span>
694
- </button>
695
- <button type="button" class="ngxsp-btn" (click)="seekRel(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
696
- <span class="ngxsp-iconBadge">10</span>
697
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 5h4m0 0v4m0-4-4.5 4.5A7 7 0 1 0 19 12"/></svg>
698
- </button>
699
- @if (hasPlaylistControls) {
700
- <button type="button" class="ngxsp-btn" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item" title="Previous item"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.53 12 18 18.47v-12L9.53 12Zm-1.53 6.25a.75.75 0 0 0 1.25-.56V6.31a.75.75 0 0 0-1.25-.56L1.06 11.44a.75.75 0 0 0 0 1.12L8 18.25Z"/></svg></button>
701
- <button type="button" class="ngxsp-btn" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item" title="Next item"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14.47 12 6 5.53v12L14.47 12ZM16 5.75a.75.75 0 0 0-1.25.56v11.38a.75.75 0 0 0 1.25.56l6.94-5.69a.75.75 0 0 0 0-1.12L16 5.75Z"/></svg></button>
702
- <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
703
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
704
- </button>
705
- <button type="button" class="ngxsp-btn" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
706
- @if (repeatMode === 'one') {
707
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
708
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/>
709
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/>
710
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/>
711
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/>
712
- <text x="16.8" y="17.6" fill="currentColor" stroke="none" font-size="7.5" font-weight="700" text-anchor="middle">1</text>
713
- </svg>
714
- } @else {
715
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/></svg>
716
- }
717
- </button>
718
- }
719
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg></button>
720
- <button type="button" class="ngxsp-btn" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
721
- @if (selectedTextTrackId !== null) {
722
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/></svg>
723
- } @else {
724
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/><path stroke-linecap="round" stroke-width="1.8" d="M6 6l12 12"/></svg>
725
- }
726
- </button>
727
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg></button>
728
- @if (isPiPSupported()) {
729
- <button type="button" class="ngxsp-btn" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
730
- @if (isPiPActive()) {
731
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
732
- } @else {
733
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" stroke-width="1.8"/></svg>
734
- }
735
- </button>
736
- }
737
- <button type="button" class="ngxsp-btn" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
738
- @if (volume <= 0.001) {
739
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
740
- } @else if (volume < 0.5) {
741
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95"/></svg>
742
- } @else {
743
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95m2.75-7.7a7.5 7.5 0 0 1 0 10.45"/></svg>
744
- }
745
- </button>
746
-
747
- <input class="ngxsp-range" type="range"
748
- min="0" [max]="duration || 0" step="0.1"
749
- [value]="currentTime"
750
- (input)="onScrub(($any($event.target).value))"
2929
+ <ngxsp-player-menu
2930
+ [openMenu]="openMenu"
2931
+ [isAudioMode]="isAudioMode"
2932
+ [textTracks]="textTracks"
2933
+ [audioTracks]="audioTracks"
2934
+ [videoTracks]="videoTracks"
2935
+ [selectedTextTrackId]="selectedTextTrackId"
2936
+ [selectedAudioTrackId]="selectedAudioTrackId"
2937
+ [selectedVideoTrackId]="selectedVideoTrackId"
2938
+ [playbackRates]="playbackRates"
2939
+ [playbackRate]="playbackRate"
2940
+ [labels]="playerMenuLabels"
2941
+ (loadSubtitles)="openSubtitlePicker()"
2942
+ (selectText)="selectTextTrack($event)"
2943
+ (selectAudio)="selectAudioTrack($event)"
2944
+ (selectVideo)="selectVideoTrack($event)"
2945
+ (selectRate)="setPlaybackRate($event)"
751
2946
  />
752
-
753
- <div class="ngxsp-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
754
-
755
- <input class="ngxsp-range" style="max-width:140px" type="range"
756
- min="0" max="1" step="0.01"
757
- [value]="volume"
758
- (input)="onVolume(($any($event.target).value))"
759
- aria-label="Volume"
2947
+ </ngxsp-player-controls-vod>
2948
+ } @else if (uiSkin==='edition') {
2949
+ <ngxsp-player-controls-edition
2950
+ [profile]="sourceProfile"
2951
+ [playing]="state?.kind==='playing'"
2952
+ [displayTitle]="displayTitle"
2953
+ [metaLabel]="editionMeta"
2954
+ [statusLabel]="statusLabel()"
2955
+ [captionsActive]="selectedTextTrackId !== null"
2956
+ [speedLabel]="playbackRateLabel()"
2957
+ [showAirPlay]="showAirPlayButton()"
2958
+ [airPlayActive]="isAirPlayActive()"
2959
+ [airPlayLabel]="airPlayLabel()"
2960
+ (airPlay)="openAirPlayPicker()"
2961
+ [showCast]="showCastButton()"
2962
+ [castActive]="isCastActive()"
2963
+ [pipSupported]="isPiPSupported()"
2964
+ [pipActive]="isPiPActive()"
2965
+ [volume]="volume"
2966
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
2967
+ [muteLabel]="outputMuteLabel"
2968
+ [unmuteLabel]="outputUnmuteLabel"
2969
+ [currentTime]="currentTime"
2970
+ [duration]="duration"
2971
+ [buffered]="bufferAheadSec"
2972
+ (playToggle)="requestPlaybackToggle()"
2973
+ (seekRelative)="seekRel($event)"
2974
+ (openMenu)="toggleMenu($event)"
2975
+ (captions)="onCaptionsClick()"
2976
+ (cast)="onCastClick()"
2977
+ (pip)="togglePiP()"
2978
+ (mute)="toggleMute()"
2979
+ (scrub)="onScrub($event)"
2980
+ (volumeChange)="onVolume($event)"
2981
+ (goLive)="reload()"
2982
+ >
2983
+ <ngxsp-player-menu
2984
+ [openMenu]="openMenu"
2985
+ [isAudioMode]="isAudioMode"
2986
+ [textTracks]="textTracks"
2987
+ [audioTracks]="audioTracks"
2988
+ [videoTracks]="videoTracks"
2989
+ [selectedTextTrackId]="selectedTextTrackId"
2990
+ [selectedAudioTrackId]="selectedAudioTrackId"
2991
+ [selectedVideoTrackId]="selectedVideoTrackId"
2992
+ [playbackRates]="playbackRates"
2993
+ [playbackRate]="playbackRate"
2994
+ [labels]="playerMenuLabels"
2995
+ (loadSubtitles)="openSubtitlePicker()"
2996
+ (selectText)="selectTextTrack($event)"
2997
+ (selectAudio)="selectAudioTrack($event)"
2998
+ (selectVideo)="selectVideoTrack($event)"
2999
+ (selectRate)="setPlaybackRate($event)"
760
3000
  />
761
- </div>
3001
+ </ngxsp-player-controls-edition>
3002
+ } @else {
3003
+ <ngxsp-player-controls
3004
+ [playing]="state?.kind==='playing'"
3005
+ [isAudioMode]="isAudioMode"
3006
+ [hasPlaylistControls]="hasPlaylistControls"
3007
+ [canPrev]="canPrev"
3008
+ [canNext]="canNext"
3009
+ [shuffleActive]="shuffleActive"
3010
+ [shuffleLabel]="shuffleLabel"
3011
+ [repeatMode]="repeatMode"
3012
+ [repeatLabel]="repeatLabel"
3013
+ [captionsActive]="selectedTextTrackId !== null"
3014
+ [tracksLabel]="tracksButtonLabel()"
3015
+ [captionsLabel]="captionsLabel()"
3016
+ [speedLabel]="playbackRateLabel()"
3017
+ [volumeLabel]="volumeLabel()"
3018
+ [showCast]="showCastButton()"
3019
+ [castActive]="isCastActive()"
3020
+ [castLabel]="castLabel()"
3021
+ [castHint]="castHintLabel()"
3022
+ [pipSupported]="isPiPSupported()"
3023
+ [pipActive]="isPiPActive()"
3024
+ [pipLabel]="pipLabel()"
3025
+ [volume]="volume"
3026
+ [muteLabel]="outputMuteLabel"
3027
+ [unmuteLabel]="outputUnmuteLabel"
3028
+ [currentTime]="currentTime"
3029
+ [duration]="duration"
3030
+ [buffered]="bufferAheadSec"
3031
+ [profile]="sourceProfile"
3032
+ (playToggle)="requestPlaybackToggle()"
3033
+ (seekRelative)="seekRel($event)"
3034
+ (prev)="emitPrev()"
3035
+ (next)="emitNext()"
3036
+ (toggleShuffle)="toggleShuffle.emit()"
3037
+ (cycleRepeat)="cycleRepeat.emit()"
3038
+ (openMenu)="toggleMenu($event)"
3039
+ (captions)="subtitles.emit()"
3040
+ (cast)="onCastClick()"
3041
+ (pip)="togglePiP()"
3042
+ (mute)="toggleMute()"
3043
+ (scrub)="onScrub($event)"
3044
+ (volumeChange)="onVolume($event)"
3045
+ />
762
3046
  }
763
3047
 
764
3048
  </div>
765
3049
  </div>
766
- `, isInline: true, styles: [".ngxsp-root{position:relative;width:100%;height:100%;background:#000;color:#fff;font-family:system-ui,sans-serif}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate}.ngxsp-video video{width:100%;height:100%;display:block;background:#000;object-fit:contain}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:#ffffff1f;border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:10px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn.accent{background:#563f1b80;border-color:#e8be763d;color:#f3d59c}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:32px;height:32px}.ngxsp-iconBadge{font-size:11px;font-weight:800;line-height:1}.ngxsp-range{flex:1;min-width:160px}.ngxsp-time{opacity:.85;font-variant-numeric:tabular-nums}.ngxsp-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;background:#000000b3;padding:20px;text-align:center}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease}.ngxsp-root.ngxsp-ui-visible .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;background:#0a0a0ad1;border:1px solid rgba(255,255,255,.16);border-radius:999px;padding:8px 12px;width:max-content;margin-left:auto}.ngxsp-debug[open]{background:#0a0a0aeb;border:1px solid rgba(255,255,255,.16);border-radius:16px;padding:12px;max-height:calc(100% - 24px);overflow:auto}.ngxsp-overlay-title{font-size:18px;font-weight:600}.ngxsp-overlay-msg{opacity:.9;max-width:720px}.ngxsp-skin-netflix .ngxsp-video{position:relative}.ngxsp-skin-netflix .ngxsp-video:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,#0000 32%,#00000038);opacity:.85;pointer-events:none;z-index:0}.ngxsp-skin-netflix .ngxsp-video:after{content:\"\";position:absolute;inset:auto 0 0;height:180px;background:linear-gradient(to top,#000000e6,#0000007a 38%,#0000);opacity:0;transition:opacity .18s ease;pointer-events:none}.ngxsp-skin-netflix.ngxsp-ui-visible .ngxsp-video:after{opacity:1}.ngxsp-nfx-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:18px 18px 22px;background:linear-gradient(to bottom,#000000c7,#0000);pointer-events:none;opacity:0;transition:opacity .18s ease;z-index:3}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-top{opacity:1}.ngxsp-nfx-titleWrap{display:flex;flex-direction:column;gap:3px}.ngxsp-nfx-eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.16em;opacity:.62}.ngxsp-nfx-title{font-weight:700;letter-spacing:.01em;opacity:.98;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,.6);font-size:18px}.ngxsp-nfx-topHint{font-size:12px;opacity:.6;padding-top:4px}.ngxsp-nfx-status{font-size:11px;text-transform:uppercase;letter-spacing:.12em;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;opacity:.78}.ngxsp-nfx-spacer{flex:1}.ngxsp-nfx-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:14px;z-index:4;opacity:0;transform:translateY(10px) scale(.98);transition:opacity .18s ease,transform .18s ease}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-centerDock{opacity:1;transform:translateY(0) scale(1)}.ngxsp-nfx-center{height:96px;width:96px;border-radius:999px;border:1px solid rgba(255,255,255,.26);background:#f8f8f81f;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);box-shadow:0 20px 60px #00000059}.ngxsp-nfx-side{height:58px;min-width:58px;border-radius:999px;border:1px solid rgba(255,255,255,.18);background:#0808086b;color:#fff;padding:0 18px;cursor:pointer;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-weight:700;font-size:14px;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-nfx-side-wide{min-width:78px}.ngxsp-nfx-bottom{position:absolute;left:14px;right:14px;bottom:14px;padding:12px 14px 14px;background:linear-gradient(to top,#070707eb,#070707b3);border:1px solid rgba(255,255,255,.1);border-radius:20px;gap:10px;flex-direction:column;align-items:stretch;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);z-index:4;opacity:0;transform:translateY(12px);transition:opacity .18s ease,transform .18s ease;box-shadow:0 16px 50px #00000052}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-bottom{opacity:1;transform:translateY(0)}.ngxsp-nfx-metaRow{display:flex;align-items:center;gap:10px;width:100%}.ngxsp-nfx-time{opacity:.92;font-variant-numeric:tabular-nums;font-size:12px;font-weight:600}.ngxsp-nfx-range{min-width:100%}.ngxsp-nfx-volumeGroup{display:inline-flex;align-items:center;gap:8px}.ngxsp-nfx-volume{max-width:0;min-width:0;opacity:0;transform:scaleX(.72);transform-origin:left center;pointer-events:none;transition:opacity .18s ease,transform .18s ease,max-width .18s ease}.ngxsp-nfx-volumeGroup:hover .ngxsp-nfx-volume,.ngxsp-nfx-volumeGroup:focus-within .ngxsp-nfx-volume{max-width:160px;min-width:96px;opacity:1;transform:scaleX(1);pointer-events:auto}.ngxsp-nfx-label{font-size:12px;opacity:.72}.ngxsp-nfx-chip{background:#ffffff14;border:1px solid rgba(255,255,255,.16);color:#fff;border-radius:999px;padding:8px 12px;cursor:pointer;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-nfx-chip.accent{background:#563f1b80;border-color:#e8be763d;color:#f3d59c}.ngxsp-nfx-chip-quiet{background:#ffffff0d}.ngxsp-nfx-center:hover,.ngxsp-nfx-side:hover,.ngxsp-nfx-chip:hover{border-color:#ffffff4d;background:#1818189e}.ngxsp-nfx-center:disabled,.ngxsp-nfx-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-range{appearance:none;height:4px;border-radius:999px;background:linear-gradient(to right,#fffffff2,#ffffff47);outline:none}.ngxsp-range::-webkit-slider-thumb{appearance:none;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 0 0 3px #ffffff29}.ngxsp-range::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:#fff;box-shadow:0 0 0 3px #ffffff29}.ngxsp-range::-moz-range-track{height:4px;border:none;border-radius:999px;background:#ffffff38}.ngxsp-buffer{height:4px;border-radius:999px;background:#ffffff14;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:linear-gradient(to right,#f5f5f547,#f5f5f5d1)}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;padding-top:6px}.ngxsp-menuCompact{grid-template-columns:minmax(180px,240px)}.ngxsp-menuSection{display:flex;flex-direction:column;gap:6px}.ngxsp-menuTitle{font-size:11px;text-transform:uppercase;letter-spacing:.14em;opacity:.58}.ngxsp-menuItem{background:#ffffff0d;border:1px solid rgba(255,255,255,.1);color:#fff;border-radius:12px;padding:9px 11px;text-align:left;cursor:pointer;font-size:12px}.ngxsp-menuItem.active{border-color:#e8be7647;background:#563f1b61;color:#f3d59c}.ngxsp-menuHint{font-size:12px;opacity:.62;padding:8px 2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3050
+ `, isInline: true, styles: [".ngxsp-root{--ngxsp-bg:#000;--ngxsp-bg-elev:var(--nsp-surface-1, #17181d);--ngxsp-bg-soft:var(--nsp-surface-2, #1e1f25);--ngxsp-border:var(--nsp-line, rgb(255 255 255 / .07));--ngxsp-border-strong:var(--nsp-line-strong, rgb(255 255 255 / .13));--ngxsp-text:var(--nsp-text, #ededf0);--ngxsp-text-dim:var(--nsp-text-2, #a3a5ae);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-accent-soft:rgb(139 144 255 / .14);--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);position:relative;width:100%;height:100%;background:#000;color:var(--ngxsp-text);font-family:var(--nsp-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, sans-serif)}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate;container-type:inline-size}.ngxsp-captions{position:absolute;left:6%;right:6%;bottom:5%;z-index:3;display:flex;flex-direction:column;align-items:center;gap:.15em;pointer-events:none;text-align:center;font-size:clamp(13px,2.6cqw,34px);line-height:1.3;transition:bottom .2s ease}.ngxsp-captions:empty{display:none}.ngxsp-ui-visible .ngxsp-captions{bottom:calc(2% + var(--ngxsp-captions-lift, 96px))}.ngxsp-captions ::ng-deep .ngxsp-cue{max-width:100%;padding:.08em .45em;border-radius:.2em;background:#000000bd;color:#fff;white-space:pre-line;text-shadow:0 1px 2px rgb(0 0 0 / .5)}.ngxsp-mode-audio .ngxsp-captions{display:none}.ngxsp-video video{width:100%;height:100%;display:block;background:#000;object-fit:contain}.ngxsp-mode-audio .ngxsp-video video{opacity:.02;filter:blur(10px) saturate(120%);transform:scale(1.08)}.ngxsp-audio-stage{position:absolute;inset:0;z-index:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:40px 24px 140px;background:radial-gradient(circle at 50% 24%,rgba(96,165,250,.22),transparent 30%),radial-gradient(circle at 22% 78%,rgba(45,212,191,.12),transparent 28%),linear-gradient(180deg,#040c18eb,#050a12d1);pointer-events:none;text-align:center}.ngxsp-audio-disc{width:min(32vw,220px);aspect-ratio:1;border-radius:50%;background:radial-gradient(circle at center,#f8fafce6 0 10%,#0f172aeb 11% 20%,#64748bd9 21% 22%,#0f172afa 23% 100%);box-shadow:0 24px 80px #0f172a80,inset 0 0 0 10px #ffffff0d;animation:ngxspSpin 18s linear infinite}.ngxsp-audio-copy{display:flex;flex-direction:column;gap:6px;max-width:min(76ch,100%)}.ngxsp-audio-eyebrow{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#bfdbfecc;font-weight:700}.ngxsp-audio-title{font-size:36px;font-weight:800;letter-spacing:0;color:#f8fbff;text-shadow:0 18px 60px rgba(15,23,42,.6)}.ngxsp-audio-subtitle{font-size:16px;color:#e2e8f0c7}.ngxsp-audio-bars{display:flex;align-items:flex-end;gap:8px;height:40px}.ngxsp-audio-bars span{width:6px;height:16px;border-radius:999px;background:linear-gradient(180deg,#93c5fdf2,#22d3ee8c);animation:ngxspBars 1.15s ease-in-out infinite;box-shadow:0 0 18px #38bdf838}.ngxsp-audio-bars span:nth-child(2){animation-delay:.12s}.ngxsp-audio-bars span:nth-child(3){animation-delay:.24s}.ngxsp-audio-bars span:nth-child(4){animation-delay:.36s}.ngxsp-audio-bars span:nth-child(5){animation-delay:.48s}.ngxsp-skin-vod .ngxsp-video{position:relative}.ngxsp-skin-vod .ngxsp-video:before{content:none}.ngxsp-skin-vod .ngxsp-video:after{content:none}.ngxsp-mode-audio.ngxsp-skin-vod .ngxsp-video:before{background:linear-gradient(180deg,#03081259,#0308121f);opacity:1}.ngxsp-inline-projection{display:inline-flex;flex:0 0 auto;align-items:center;gap:8px;width:max-content}.ngxsp-inline-projection:empty{display:none}.ngxsp-gesturePulse{position:absolute;inset:0;z-index:5;display:grid;place-items:center;pointer-events:none;color:#f8fbff;animation:ngxspGesturePulse .52s ease both}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px;padding:18px;box-sizing:border-box;display:grid;place-items:center;border-radius:50%;background:#00000080;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media(max-width:640px){.ngxsp-audio-stage{padding:28px 16px 128px;gap:14px}.ngxsp-audio-disc{width:min(48vw,168px)}.ngxsp-audio-title{font-size:28px}.ngxsp-audio-subtitle{font-size:14px}.ngxsp-skin-vod .ngxsp-inline-projection{display:none}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px}}@keyframes ngxspSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes ngxspBars{0%,to{height:14px;opacity:.45}50%{height:40px;opacity:1}}@keyframes ngxspGesturePulse{0%{opacity:0;transform:scale(.82)}24%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.16)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: PlayerMenuComponent, selector: "ngxsp-player-menu", inputs: ["openMenu", "isAudioMode", "textTracks", "audioTracks", "videoTracks", "selectedTextTrackId", "selectedAudioTrackId", "selectedVideoTrackId", "playbackRates", "playbackRate", "labels"], outputs: ["selectText", "selectAudio", "selectVideo", "selectRate", "loadSubtitles"] }, { kind: "component", type: PlayerOverlaysComponent, selector: "ngxsp-player-overlays", inputs: ["error", "debug", "debugJson"], outputs: ["retry", "requestPlay", "copyJson", "downloadJson"] }, { kind: "component", type: PlayerControlsComponent, selector: "ngxsp-player-controls", inputs: ["playing", "isAudioMode", "hasPlaylistControls", "canPrev", "canNext", "shuffleActive", "shuffleLabel", "repeatMode", "repeatLabel", "captionsActive", "tracksLabel", "captionsLabel", "speedLabel", "volumeLabel", "muteLabel", "unmuteLabel", "showAirPlay", "airPlayActive", "airPlayLabel", "showCast", "castActive", "castLabel", "castHint", "pipSupported", "pipActive", "pipLabel", "volume", "currentTime", "duration", "buffered", "profile"], outputs: ["playToggle", "seekRelative", "prev", "next", "toggleShuffle", "cycleRepeat", "openMenu", "captions", "airPlay", "cast", "pip", "mute", "scrub", "volumeChange"] }, { kind: "component", type: PlayerControlsVodComponent, selector: "ngxsp-player-controls-vod", inputs: ["playing", "isAudioMode", "displayTitle", "nowPlayingLabel", "nowListeningLabel", "fullscreenHintLabel", "statusLabel", "keyboardHint", "hasPlaylistControls", "canPrev", "canNext", "previousLabel", "nextLabel", "playLabel", "pauseLabel", "shuffleActive", "shuffleLabel", "repeatMode", "repeatLabel", "captionsActive", "tracksLabel", "captionsLabel", "speedLabel", "volumeLabel", "muteLabel", "unmuteLabel", "showAirPlay", "airPlayActive", "airPlayLabel", "showCast", "castActive", "castLabel", "castHint", "pipSupported", "pipActive", "pipLabel", "fullscreenLabel", "volume", "currentTime", "duration", "buffered", "bufferPercent", "profile", "timelineDurationSec", "timelineMarkers"], outputs: ["playToggle", "seekRelative", "prev", "next", "toggleShuffle", "cycleRepeat", "openMenu", "captions", "airPlay", "cast", "pip", "fullscreen", "mute", "scrub", "volumeChange"] }, { kind: "component", type: PlayerControlsEditionComponent, selector: "ngxsp-player-controls-edition", inputs: ["profile", "playing", "displayTitle", "metaLabel", "statusLabel", "behindSec", "liveLabel", "captionsActive", "speedLabel", "showAirPlay", "airPlayActive", "airPlayLabel", "showCast", "castActive", "pipSupported", "pipActive", "volume", "volumeLabel", "muteLabel", "unmuteLabel", "currentTime", "duration", "buffered"], outputs: ["playToggle", "seekRelative", "openMenu", "captions", "airPlay", "cast", "pip", "mute", "scrub", "volumeChange", "goLive"] }, { kind: "component", type: SmartPlayerIconComponent, selector: "ngxsp-smart-icon", inputs: ["name"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
767
3051
  }
768
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SmartPlayerComponent, decorators: [{
3052
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.33", ngImport: i0, type: SmartPlayerComponent, decorators: [{
769
3053
  type: Component,
770
- args: [{ selector: 'ngx-smart-player', standalone: true, imports: [CommonModule], template: `
771
- <div class="ngxsp-root" [class.ngxsp-skin-netflix]="uiSkin==='netflix'" [class.ngxsp-ui-visible]="uiVisible" [attr.data-kind]="state?.kind">
772
- <div class="ngxsp-video" (dblclick)="toggleFullscreen()" (mousemove)="onInteraction()" (mouseenter)="onInteraction()" (mouseleave)="onPointerLeave()" (focusin)="onInteraction()" (touchstart)="onInteraction(true)" (click)="onRootClick($event)">
773
- <video #video playsinline (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
774
- @if (state?.kind==='error') {
775
- <div class="ngxsp-overlay" role="alert">
776
- <div class="ngxsp-overlay-card">
777
- <div class="ngxsp-overlay-title">Playback error</div>
778
- <div class="ngxsp-overlay-msg">
779
- <div style="font-weight:600; margin-bottom:6px;">{{errorTitle()}}</div>
780
- <div style="opacity:.85; font-size: 13px; line-height: 1.3;">{{errorHint()}}</div>
781
- <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">
782
- <button type="button" class="ngxsp-btn" (click)="reload()">Retry</button>
783
- @if (autoplayBlocked) {
784
- <button type="button" class="ngxsp-btn" (click)="tryPlay()">Play</button>
785
- }
786
- </div>
787
- </div>
3054
+ args: [{ selector: 'ngx-smart-player', standalone: true, imports: [CommonModule, PlayerMenuComponent, PlayerOverlaysComponent, PlayerControlsComponent, PlayerControlsVodComponent, PlayerControlsEditionComponent, SmartPlayerIconComponent], template: `
3055
+ <div class="ngxsp-root" [class.ngxsp-skin-vod]="uiSkin==='vod'" [class.ngxsp-ui-visible]="uiVisible" [class.ngxsp-mode-audio]="isAudioMode" [class.ngxsp-mode-video]="!isAudioMode" [attr.data-kind]="state?.kind">
3056
+ <!-- Surface de présentation : interactions pointeur non essentielles, toutes
3057
+ dupliquées au clavier par @HostListener('document:keydown') (Espace/flèches/F/M/C)
3058
+ et par les contrôles focusables. -->
3059
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
3060
+ <div class="ngxsp-video" (dblclick)="onPrimarySurfaceDoubleClick()" (mousemove)="onInteraction()" (mouseenter)="onInteraction()" (mouseleave)="onPointerLeave()" (focusin)="onInteraction()" (touchstart)="onInteraction(true)" (click)="onRootClick($event)">
3061
+ <video #video playsinline x-webkit-airplay="allow" (timeupdate)="noop()" (loadedmetadata)="onLoadedMetadata()" (ended)="onEnded()"></video>
3062
+ <!-- Subtitles loaded from a file, drawn here at the bottom of the picture, above the controls when they show. -->
3063
+ <div #captionsLayer class="ngxsp-captions" aria-hidden="true"></div>
3064
+ @if (!isAudioMode) {
3065
+ <input #subtitleFileInput class="ngxsp-subtitleInput" type="file" accept=".srt,.vtt,text/vtt,application/x-subrip" hidden (change)="onSubtitleFileChosen($event)" />
3066
+ }
3067
+ @if (isAudioMode) {
3068
+ <div class="ngxsp-audio-stage" aria-hidden="true">
3069
+ <div class="ngxsp-audio-disc"></div>
3070
+ <div class="ngxsp-audio-copy">
3071
+ <div class="ngxsp-audio-eyebrow">Now Listening</div>
3072
+ <div class="ngxsp-audio-title">{{displayTitle}}</div>
3073
+ <div class="ngxsp-audio-subtitle">{{displaySubtitle}}</div>
3074
+ </div>
3075
+ <div class="ngxsp-audio-bars">
3076
+ <span></span><span></span><span></span><span></span><span></span>
788
3077
  </div>
789
3078
  </div>
790
3079
  }
791
-
792
- @if (debug) {
793
- <details class="ngxsp-debug" (click)="$event.stopPropagation()">
794
- <summary>Debug</summary>
795
- <div style="display:flex; gap:8px; margin:8px 0; flex-wrap:wrap;">
796
- <button type="button" class="ngxsp-btn" (click)="copyDebug()">Copy JSON</button>
797
- <button type="button" class="ngxsp-btn" (click)="downloadDebug()">Download</button>
798
- </div>
799
- <pre>{{debugJson}}</pre>
800
- </details>
3080
+ <ngxsp-player-overlays
3081
+ [error]="currentError"
3082
+ [debug]="debug"
3083
+ [debugJson]="debugJson"
3084
+ (retry)="reload()"
3085
+ (requestPlay)="tryPlay()"
3086
+ (copyJson)="copyDebug()"
3087
+ (downloadJson)="downloadDebug()"
3088
+ />
3089
+ @if (surfaceGesture) {
3090
+ <div class="ngxsp-gesturePulse" aria-hidden="true">
3091
+ @if (surfaceGesture === 'pause') {
3092
+ <ngxsp-smart-icon name="pause" />
3093
+ } @else {
3094
+ <ngxsp-smart-icon name="play" />
3095
+ }
3096
+ </div>
801
3097
  }
802
3098
 
803
- @if (uiSkin==='netflix') {
804
- <div class="ngxsp-nfx-top">
805
- <div class="ngxsp-nfx-titleWrap">
806
- <div class="ngxsp-nfx-eyebrow">Now Playing</div>
807
- <div class="ngxsp-nfx-title">{{displayTitle}}</div>
808
- </div>
809
- <div class="ngxsp-nfx-spacer"></div>
810
- <div class="ngxsp-nfx-status">{{ statusLabel() }}</div>
811
- <div class="ngxsp-nfx-topHint">Double-click for fullscreen</div>
812
- </div>
813
-
814
- <div class="ngxsp-nfx-centerDock">
815
- @if (hasPlaylistControls) {
816
- <button type="button" class="ngxsp-nfx-side" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item">
817
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.53 12 18 18.47v-12L9.53 12Zm-1.53 6.25a.75.75 0 0 0 1.25-.56V6.31a.75.75 0 0 0-1.25-.56L1.06 11.44a.75.75 0 0 0 0 1.12L8 18.25Z"/></svg>
818
- </button>
819
- }
820
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds">
821
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 5H5m0 0v4m0-4 4.5 4.5A7 7 0 1 1 5 12"/></svg>
822
- <span class="ngxsp-iconBadge">10</span>
823
- </button>
824
- <button type="button" class="ngxsp-nfx-center" (click)="togglePlay()" aria-label="Play/Pause">
825
- @if (state?.kind==='playing') {
826
- <svg class="ngxsp-icon ngxsp-icon-lg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
827
- } @else {
828
- <svg class="ngxsp-icon ngxsp-icon-lg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.25 6.82c0-1.3 1.43-2.1 2.54-1.43l8.12 4.93a1.96 1.96 0 0 1 0 3.36l-8.12 4.93a1.67 1.67 0 0 1-2.54-1.43V6.82Z"/></svg>
829
- }
830
- </button>
831
- <button type="button" class="ngxsp-nfx-side ngxsp-nfx-side-wide" (click)="seekRel(10)" aria-label="Seek forward 10 seconds">
832
- <span class="ngxsp-iconBadge">10</span>
833
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 5h4m0 0v4m0-4-4.5 4.5A7 7 0 1 0 19 12"/></svg>
834
- </button>
835
- @if (hasPlaylistControls) {
836
- <button type="button" class="ngxsp-nfx-side" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item">
837
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14.47 12 6 5.53v12L14.47 12ZM16 5.75a.75.75 0 0 0-1.25.56v11.38a.75.75 0 0 0 1.25.56l6.94-5.69a.75.75 0 0 0 0-1.12L16 5.75Z"/></svg>
838
- </button>
839
- }
840
- </div>
841
-
842
- <div class="ngxsp-controls ngxsp-nfx-bottom">
843
- <div class="ngxsp-nfx-metaRow">
844
- <div class="ngxsp-nfx-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
845
- <div class="ngxsp-nfx-volumeGroup">
846
- <button type="button" class="ngxsp-nfx-chip ngxsp-nfx-chip-quiet" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
847
- @if (volume <= 0.001) {
848
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
849
- } @else if (volume < 0.5) {
850
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95"/></svg>
851
- } @else {
852
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95m2.75-7.7a7.5 7.5 0 0 1 0 10.45"/></svg>
853
- }
854
- </button>
855
- <input class="ngxsp-range ngxsp-nfx-volume" type="range"
856
- min="0" max="1" step="0.01"
857
- [value]="volume"
858
- (input)="onVolume(($any($event.target).value))"
859
- aria-label="Volume"
860
- />
861
- </div>
862
- <div class="ngxsp-nfx-spacer"></div>
863
- @if (hasPlaylistControls) {
864
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
865
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
866
- </button>
867
- <button type="button" class="ngxsp-nfx-chip" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
868
- @if (repeatMode === 'one') {
869
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
870
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/>
871
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/>
872
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/>
873
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/>
874
- <text x="16.8" y="17.6" fill="currentColor" stroke="none" font-size="7.5" font-weight="700" text-anchor="middle">1</text>
875
- </svg>
876
- } @else {
877
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/></svg>
878
- }
879
- </button>
880
- }
881
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()">
882
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg>
883
- </button>
884
- <button type="button" class="ngxsp-nfx-chip" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
885
- @if (selectedTextTrackId !== null) {
886
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/></svg>
887
- } @else {
888
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/><path stroke-linecap="round" stroke-width="1.8" d="M6 6l12 12"/></svg>
889
- }
890
- </button>
891
- <button type="button" class="ngxsp-nfx-chip" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()">
892
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg>
893
- </button>
894
- @if (isPiPSupported()) {
895
- <button type="button" class="ngxsp-nfx-chip" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
896
- @if (isPiPActive()) {
897
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
898
- } @else {
899
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" stroke-width="1.8"/></svg>
900
- }
901
- </button>
902
- }
903
- </div>
904
-
905
- <input class="ngxsp-range ngxsp-nfx-range" type="range"
906
- min="0" [max]="duration || 0" step="0.1"
907
- [value]="currentTime"
908
- (input)="onScrub(($any($event.target).value))"
909
- aria-label="Seek"
910
- />
911
-
912
- <div class="ngxsp-nfx-metaRow">
913
- <div class="ngxsp-nfx-spacer"></div>
914
- <div class="ngxsp-nfx-label">{{ keyboardHintLabel() }}</div>
915
- </div>
916
-
917
- <div class="ngxsp-buffer" aria-hidden="true">
918
- <div class="ngxsp-bufferFill" [style.width.%]="bufferPercent()"></div>
919
- </div>
920
-
921
- @if (openMenu === 'tracks') {
922
- <div class="ngxsp-menu" (click)="$event.stopPropagation()">
923
- <div class="ngxsp-menuSection">
924
- <div class="ngxsp-menuTitle">Captions</div>
925
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId === null" (click)="selectTextTrack(null)">Off</button>
926
- @for (track of textTracks; track track.id) {
927
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedTextTrackId === track.id" (click)="selectTextTrack(track.id)">
928
- {{ trackLabel(track) }}
929
- </button>
930
- }
931
- </div>
932
-
933
- <div class="ngxsp-menuSection">
934
- <div class="ngxsp-menuTitle">Audio</div>
935
- @if (audioTracks.length) {
936
- @for (track of audioTracks; track track.id) {
937
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedAudioTrackId === track.id" (click)="selectAudioTrack(track.id)">
938
- {{ trackLabel(track) }}
939
- </button>
940
- }
941
- } @else {
942
- <div class="ngxsp-menuHint">Single audio track</div>
943
- }
944
- </div>
945
-
946
- <div class="ngxsp-menuSection">
947
- <div class="ngxsp-menuTitle">Quality</div>
948
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId === null" (click)="selectVideoTrack(null)">Auto</button>
949
- @if (videoTracks.length) {
950
- @for (track of videoTracks; track track.id) {
951
- <button type="button" class="ngxsp-menuItem" [class.active]="selectedVideoTrackId === track.id" (click)="selectVideoTrack(track.id)">
952
- {{ qualityLabel(track) }}
953
- </button>
954
- }
955
- } @else {
956
- <div class="ngxsp-menuHint">Single quality</div>
957
- }
958
- </div>
959
- </div>
960
- }
961
-
962
- @if (openMenu === 'speed') {
963
- <div class="ngxsp-menu ngxsp-menuCompact" (click)="$event.stopPropagation()">
964
- <div class="ngxsp-menuSection">
965
- <div class="ngxsp-menuTitle">Playback speed</div>
966
- @for (rate of playbackRates; track rate) {
967
- <button type="button" class="ngxsp-menuItem" [class.active]="playbackRate === rate" (click)="setPlaybackRate(rate)">
968
- {{ rate }}x
969
- </button>
970
- }
971
- </div>
3099
+ @if (uiSkin==='vod') {
3100
+ <ngxsp-player-controls-vod
3101
+ [playing]="state?.kind==='playing'"
3102
+ [isAudioMode]="isAudioMode"
3103
+ [displayTitle]="displayTitle"
3104
+ [nowPlayingLabel]="nowPlayingLabel"
3105
+ [nowListeningLabel]="nowListeningLabel"
3106
+ [fullscreenHintLabel]="fullscreenHintLabel"
3107
+ [statusLabel]="statusLabel()"
3108
+ [keyboardHint]="keyboardHintLabel()"
3109
+ [hasPlaylistControls]="hasPlaylistControls"
3110
+ [canPrev]="canPrev"
3111
+ [canNext]="canNext"
3112
+ [previousLabel]="previousLabel"
3113
+ [nextLabel]="nextLabel"
3114
+ [playLabel]="playLabel"
3115
+ [pauseLabel]="pauseLabel"
3116
+ [shuffleActive]="shuffleActive"
3117
+ [shuffleLabel]="shuffleLabel"
3118
+ [repeatMode]="repeatMode"
3119
+ [repeatLabel]="repeatLabel"
3120
+ [captionsActive]="selectedTextTrackId !== null"
3121
+ [tracksLabel]="tracksButtonLabel()"
3122
+ [captionsLabel]="captionsLabel()"
3123
+ [speedLabel]="playbackRateLabel()"
3124
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
3125
+ [muteLabel]="outputMuteLabel"
3126
+ [unmuteLabel]="outputUnmuteLabel"
3127
+ [showAirPlay]="showAirPlayButton()"
3128
+ [airPlayActive]="isAirPlayActive()"
3129
+ [airPlayLabel]="airPlayLabel()"
3130
+ (airPlay)="openAirPlayPicker()"
3131
+ [showCast]="showCastButton()"
3132
+ [castActive]="isCastActive()"
3133
+ [castLabel]="castLabel()"
3134
+ [castHint]="castHintLabel()"
3135
+ [pipSupported]="isPiPSupported()"
3136
+ [pipActive]="isPiPActive()"
3137
+ [pipLabel]="pipLabel()"
3138
+ [fullscreenLabel]="fullscreenLabel"
3139
+ [volume]="volume"
3140
+ [currentTime]="currentTime"
3141
+ [duration]="duration"
3142
+ [buffered]="bufferAheadSec"
3143
+ [bufferPercent]="bufferPercent()"
3144
+ [profile]="sourceProfile"
3145
+ [timelineDurationSec]="timelineDurationSec ?? 0"
3146
+ [timelineMarkers]="timelineMarkers"
3147
+ (playToggle)="requestPlaybackToggle()"
3148
+ (seekRelative)="seekRel($event)"
3149
+ (prev)="emitPrev()"
3150
+ (next)="emitNext()"
3151
+ (toggleShuffle)="toggleShuffle.emit()"
3152
+ (cycleRepeat)="cycleRepeat.emit()"
3153
+ (openMenu)="toggleMenu($event)"
3154
+ (captions)="onCaptionsClick()"
3155
+ (cast)="onCastClick()"
3156
+ (pip)="togglePiP()"
3157
+ (fullscreen)="toggleFullscreen()"
3158
+ (mute)="toggleMute()"
3159
+ (scrub)="onScrub($event)"
3160
+ (volumeChange)="onVolume($event)"
3161
+ >
3162
+ <div ngxsp-player-inline-controls class="ngxsp-inline-projection">
3163
+ <ng-content select="[ngxsp-player-inline-controls]" />
972
3164
  </div>
973
- }
974
- </div>
975
- } @else {
976
- <div class="ngxsp-controls">
977
- <button type="button" class="ngxsp-btn" (click)="togglePlay()" aria-label="Play/Pause" title="Play/Pause">
978
- @if (state?.kind==='playing') {
979
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7.5 5.25A1.25 1.25 0 0 1 8.75 6.5v11A1.25 1.25 0 0 1 6.25 17.5v-11A1.25 1.25 0 0 1 7.5 5.25Zm8 0a1.25 1.25 0 0 1 1.25 1.25v11a1.25 1.25 0 1 1-2.5 0v-11A1.25 1.25 0 0 1 15.5 5.25Z"/></svg>
980
- } @else {
981
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8.25 6.82c0-1.3 1.43-2.1 2.54-1.43l8.12 4.93a1.96 1.96 0 0 1 0 3.36l-8.12 4.93a1.67 1.67 0 0 1-2.54-1.43V6.82Z"/></svg>
982
- }
983
- </button>
984
- <button type="button" class="ngxsp-btn" (click)="seekRel(-10)" aria-label="Seek backward 10 seconds" title="Seek backward 10 seconds">
985
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M9 5H5m0 0v4m0-4 4.5 4.5A7 7 0 1 1 5 12"/></svg>
986
- <span class="ngxsp-iconBadge">10</span>
987
- </button>
988
- <button type="button" class="ngxsp-btn" (click)="seekRel(10)" aria-label="Seek forward 10 seconds" title="Seek forward 10 seconds">
989
- <span class="ngxsp-iconBadge">10</span>
990
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 5h4m0 0v4m0-4-4.5 4.5A7 7 0 1 0 19 12"/></svg>
991
- </button>
992
- @if (hasPlaylistControls) {
993
- <button type="button" class="ngxsp-btn" (click)="emitPrev()" [disabled]="!canPrev" aria-label="Previous item" title="Previous item"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9.53 12 18 18.47v-12L9.53 12Zm-1.53 6.25a.75.75 0 0 0 1.25-.56V6.31a.75.75 0 0 0-1.25-.56L1.06 11.44a.75.75 0 0 0 0 1.12L8 18.25Z"/></svg></button>
994
- <button type="button" class="ngxsp-btn" (click)="emitNext()" [disabled]="!canNext" aria-label="Next item" title="Next item"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14.47 12 6 5.53v12L14.47 12ZM16 5.75a.75.75 0 0 0-1.25.56v11.38a.75.75 0 0 0 1.25.56l6.94-5.69a.75.75 0 0 0 0-1.12L16 5.75Z"/></svg></button>
995
- <button type="button" class="ngxsp-btn" [class.accent]="shuffleActive" (click)="toggleShuffle.emit()" [attr.title]="shuffleLabel" [attr.aria-label]="shuffleLabel">
996
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 6h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 6-4.5 4.5"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 7h3.2c1 0 1.95.4 2.66 1.1l6.28 6.3A3.75 3.75 0 0 0 18.82 15H20"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 15h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m20 18-4.56-4.56"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 17h3.2c1 0 1.95-.4 2.66-1.1l1.54-1.54"/></svg>
997
- </button>
998
- <button type="button" class="ngxsp-btn" [class.accent]="repeatMode !== 'off'" (click)="cycleRepeat.emit()" [attr.title]="repeatLabel" [attr.aria-label]="repeatLabel">
999
- @if (repeatMode === 'one') {
1000
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
1001
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/>
1002
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/>
1003
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/>
1004
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/>
1005
- <text x="16.8" y="17.6" fill="currentColor" stroke="none" font-size="7.5" font-weight="700" text-anchor="middle">1</text>
1006
- </svg>
1007
- } @else {
1008
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M17 4h3v3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M20 7l-2.8-2.8A4 4 0 0 0 14.37 3H8a4 4 0 0 0-4 4v1"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M7 20H4v-3"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m4 17 2.8 2.8A4 4 0 0 0 9.63 21H16a4 4 0 0 0 4-4v-1"/></svg>
1009
- }
1010
- </button>
1011
- }
1012
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('tracks')" [attr.title]="tracksButtonLabel()" [attr.aria-label]="tracksButtonLabel()"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6.75h16M4 12h16M4 17.25h16"/></svg></button>
1013
- <button type="button" class="ngxsp-btn" (click)="subtitles.emit()" [attr.title]="captionsLabel()" [attr.aria-label]="captionsLabel()">
1014
- @if (selectedTextTrackId !== null) {
1015
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/></svg>
1016
- } @else {
1017
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><path stroke-linecap="round" stroke-width="1.8" d="M8 11.25h2.5m2 0H14m2 0h.5M8 14.75h3.5m2 0H16"/><path stroke-linecap="round" stroke-width="1.8" d="M6 6l12 12"/></svg>
1018
- }
1019
- </button>
1020
- <button type="button" class="ngxsp-btn" (click)="toggleMenu('speed')" [attr.title]="playbackRateLabel()" [attr.aria-label]="playbackRateLabel()"><svg class="ngxsp-icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M5.25 5.82c0-1.3 1.43-2.1 2.54-1.43l5.71 3.47a1.96 1.96 0 0 1 0 3.36L7.79 14.7a1.67 1.67 0 0 1-2.54-1.43V5.82Zm8.5 0c0-1.3 1.43-2.1 2.54-1.43L22 7.86a1.96 1.96 0 0 1 0 3.36l-5.71 3.47a1.67 1.67 0 0 1-2.54-1.43V5.82Z"/></svg></button>
1021
- @if (isPiPSupported()) {
1022
- <button type="button" class="ngxsp-btn" (click)="togglePiP()" [attr.title]="pipLabel()" [attr.aria-label]="pipLabel()">
1023
- @if (isPiPActive()) {
1024
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" fill="currentColor" stroke="none"/></svg>
1025
- } @else {
1026
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><rect x="3.5" y="5.5" width="17" height="13" rx="2.5" stroke-width="1.8"/><rect x="12.25" y="11.25" width="5.25" height="4.5" rx="1" stroke-width="1.8"/></svg>
1027
- }
1028
- </button>
1029
- }
1030
- <button type="button" class="ngxsp-btn" (click)="toggleMute()" [attr.title]="volumeLabel()" [attr.aria-label]="volumeLabel()">
1031
- @if (volume <= 0.001) {
1032
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm3.5 5.5 5 5m0-5-5 5"/></svg>
1033
- } @else if (volume < 0.5) {
1034
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95"/></svg>
1035
- } @else {
1036
- <svg class="ngxsp-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M11 5 6.5 9H3.75v6h2.75L11 19V5Zm4.5 4.75a3.5 3.5 0 0 1 0 4.95m2.75-7.7a7.5 7.5 0 0 1 0 10.45"/></svg>
1037
- }
1038
- </button>
1039
-
1040
- <input class="ngxsp-range" type="range"
1041
- min="0" [max]="duration || 0" step="0.1"
1042
- [value]="currentTime"
1043
- (input)="onScrub(($any($event.target).value))"
3165
+ <ngxsp-player-menu
3166
+ [openMenu]="openMenu"
3167
+ [isAudioMode]="isAudioMode"
3168
+ [textTracks]="textTracks"
3169
+ [audioTracks]="audioTracks"
3170
+ [videoTracks]="videoTracks"
3171
+ [selectedTextTrackId]="selectedTextTrackId"
3172
+ [selectedAudioTrackId]="selectedAudioTrackId"
3173
+ [selectedVideoTrackId]="selectedVideoTrackId"
3174
+ [playbackRates]="playbackRates"
3175
+ [playbackRate]="playbackRate"
3176
+ [labels]="playerMenuLabels"
3177
+ (loadSubtitles)="openSubtitlePicker()"
3178
+ (selectText)="selectTextTrack($event)"
3179
+ (selectAudio)="selectAudioTrack($event)"
3180
+ (selectVideo)="selectVideoTrack($event)"
3181
+ (selectRate)="setPlaybackRate($event)"
1044
3182
  />
1045
-
1046
- <div class="ngxsp-time">{{fmtTime(currentTime)}} / {{fmtTime(duration)}}</div>
1047
-
1048
- <input class="ngxsp-range" style="max-width:140px" type="range"
1049
- min="0" max="1" step="0.01"
1050
- [value]="volume"
1051
- (input)="onVolume(($any($event.target).value))"
1052
- aria-label="Volume"
3183
+ </ngxsp-player-controls-vod>
3184
+ } @else if (uiSkin==='edition') {
3185
+ <ngxsp-player-controls-edition
3186
+ [profile]="sourceProfile"
3187
+ [playing]="state?.kind==='playing'"
3188
+ [displayTitle]="displayTitle"
3189
+ [metaLabel]="editionMeta"
3190
+ [statusLabel]="statusLabel()"
3191
+ [captionsActive]="selectedTextTrackId !== null"
3192
+ [speedLabel]="playbackRateLabel()"
3193
+ [showAirPlay]="showAirPlayButton()"
3194
+ [airPlayActive]="isAirPlayActive()"
3195
+ [airPlayLabel]="airPlayLabel()"
3196
+ (airPlay)="openAirPlayPicker()"
3197
+ [showCast]="showCastButton()"
3198
+ [castActive]="isCastActive()"
3199
+ [pipSupported]="isPiPSupported()"
3200
+ [pipActive]="isPiPActive()"
3201
+ [volume]="volume"
3202
+ [volumeLabel]="outputVolumeLabel || volumeLabel()"
3203
+ [muteLabel]="outputMuteLabel"
3204
+ [unmuteLabel]="outputUnmuteLabel"
3205
+ [currentTime]="currentTime"
3206
+ [duration]="duration"
3207
+ [buffered]="bufferAheadSec"
3208
+ (playToggle)="requestPlaybackToggle()"
3209
+ (seekRelative)="seekRel($event)"
3210
+ (openMenu)="toggleMenu($event)"
3211
+ (captions)="onCaptionsClick()"
3212
+ (cast)="onCastClick()"
3213
+ (pip)="togglePiP()"
3214
+ (mute)="toggleMute()"
3215
+ (scrub)="onScrub($event)"
3216
+ (volumeChange)="onVolume($event)"
3217
+ (goLive)="reload()"
3218
+ >
3219
+ <ngxsp-player-menu
3220
+ [openMenu]="openMenu"
3221
+ [isAudioMode]="isAudioMode"
3222
+ [textTracks]="textTracks"
3223
+ [audioTracks]="audioTracks"
3224
+ [videoTracks]="videoTracks"
3225
+ [selectedTextTrackId]="selectedTextTrackId"
3226
+ [selectedAudioTrackId]="selectedAudioTrackId"
3227
+ [selectedVideoTrackId]="selectedVideoTrackId"
3228
+ [playbackRates]="playbackRates"
3229
+ [playbackRate]="playbackRate"
3230
+ [labels]="playerMenuLabels"
3231
+ (loadSubtitles)="openSubtitlePicker()"
3232
+ (selectText)="selectTextTrack($event)"
3233
+ (selectAudio)="selectAudioTrack($event)"
3234
+ (selectVideo)="selectVideoTrack($event)"
3235
+ (selectRate)="setPlaybackRate($event)"
1053
3236
  />
1054
- </div>
3237
+ </ngxsp-player-controls-edition>
3238
+ } @else {
3239
+ <ngxsp-player-controls
3240
+ [playing]="state?.kind==='playing'"
3241
+ [isAudioMode]="isAudioMode"
3242
+ [hasPlaylistControls]="hasPlaylistControls"
3243
+ [canPrev]="canPrev"
3244
+ [canNext]="canNext"
3245
+ [shuffleActive]="shuffleActive"
3246
+ [shuffleLabel]="shuffleLabel"
3247
+ [repeatMode]="repeatMode"
3248
+ [repeatLabel]="repeatLabel"
3249
+ [captionsActive]="selectedTextTrackId !== null"
3250
+ [tracksLabel]="tracksButtonLabel()"
3251
+ [captionsLabel]="captionsLabel()"
3252
+ [speedLabel]="playbackRateLabel()"
3253
+ [volumeLabel]="volumeLabel()"
3254
+ [showCast]="showCastButton()"
3255
+ [castActive]="isCastActive()"
3256
+ [castLabel]="castLabel()"
3257
+ [castHint]="castHintLabel()"
3258
+ [pipSupported]="isPiPSupported()"
3259
+ [pipActive]="isPiPActive()"
3260
+ [pipLabel]="pipLabel()"
3261
+ [volume]="volume"
3262
+ [muteLabel]="outputMuteLabel"
3263
+ [unmuteLabel]="outputUnmuteLabel"
3264
+ [currentTime]="currentTime"
3265
+ [duration]="duration"
3266
+ [buffered]="bufferAheadSec"
3267
+ [profile]="sourceProfile"
3268
+ (playToggle)="requestPlaybackToggle()"
3269
+ (seekRelative)="seekRel($event)"
3270
+ (prev)="emitPrev()"
3271
+ (next)="emitNext()"
3272
+ (toggleShuffle)="toggleShuffle.emit()"
3273
+ (cycleRepeat)="cycleRepeat.emit()"
3274
+ (openMenu)="toggleMenu($event)"
3275
+ (captions)="subtitles.emit()"
3276
+ (cast)="onCastClick()"
3277
+ (pip)="togglePiP()"
3278
+ (mute)="toggleMute()"
3279
+ (scrub)="onScrub($event)"
3280
+ (volumeChange)="onVolume($event)"
3281
+ />
1055
3282
  }
1056
3283
 
1057
3284
  </div>
1058
3285
  </div>
1059
- `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".ngxsp-root{position:relative;width:100%;height:100%;background:#000;color:#fff;font-family:system-ui,sans-serif}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate}.ngxsp-video video{width:100%;height:100%;display:block;background:#000;object-fit:contain}.ngxsp-controls{display:flex;gap:8px;align-items:center;padding:10px;flex-wrap:wrap}.ngxsp-btn{background:#ffffff1f;border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:10px;padding:8px 10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-btn.accent{background:#563f1b80;border-color:#e8be763d;color:#f3d59c}.ngxsp-icon{width:18px;height:18px;display:block;flex:0 0 auto}.ngxsp-icon-lg{width:32px;height:32px}.ngxsp-iconBadge{font-size:11px;font-weight:800;line-height:1}.ngxsp-range{flex:1;min-width:160px}.ngxsp-time{opacity:.85;font-variant-numeric:tabular-nums}.ngxsp-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;background:#000000b3;padding:20px;text-align:center}.ngxsp-debug{position:absolute;top:12px;right:12px;z-index:6;margin:0;max-width:min(420px,calc(100% - 24px));opacity:0;transform:translateY(-6px);transition:opacity .18s ease,transform .18s ease}.ngxsp-root.ngxsp-ui-visible .ngxsp-debug,.ngxsp-debug[open]{opacity:1;transform:translateY(0)}.ngxsp-debug summary{cursor:pointer;list-style:none;background:#0a0a0ad1;border:1px solid rgba(255,255,255,.16);border-radius:999px;padding:8px 12px;width:max-content;margin-left:auto}.ngxsp-debug[open]{background:#0a0a0aeb;border:1px solid rgba(255,255,255,.16);border-radius:16px;padding:12px;max-height:calc(100% - 24px);overflow:auto}.ngxsp-overlay-title{font-size:18px;font-weight:600}.ngxsp-overlay-msg{opacity:.9;max-width:720px}.ngxsp-skin-netflix .ngxsp-video{position:relative}.ngxsp-skin-netflix .ngxsp-video:before{content:\"\";position:absolute;inset:0;background:radial-gradient(circle at center,#0000 32%,#00000038);opacity:.85;pointer-events:none;z-index:0}.ngxsp-skin-netflix .ngxsp-video:after{content:\"\";position:absolute;inset:auto 0 0;height:180px;background:linear-gradient(to top,#000000e6,#0000007a 38%,#0000);opacity:0;transition:opacity .18s ease;pointer-events:none}.ngxsp-skin-netflix.ngxsp-ui-visible .ngxsp-video:after{opacity:1}.ngxsp-nfx-top{position:absolute;left:0;right:0;top:0;display:flex;align-items:flex-start;gap:12px;padding:18px 18px 22px;background:linear-gradient(to bottom,#000000c7,#0000);pointer-events:none;opacity:0;transition:opacity .18s ease;z-index:3}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-top{opacity:1}.ngxsp-nfx-titleWrap{display:flex;flex-direction:column;gap:3px}.ngxsp-nfx-eyebrow{font-size:11px;text-transform:uppercase;letter-spacing:.16em;opacity:.62}.ngxsp-nfx-title{font-weight:700;letter-spacing:.01em;opacity:.98;pointer-events:none;text-shadow:0 1px 2px rgba(0,0,0,.6);font-size:18px}.ngxsp-nfx-topHint{font-size:12px;opacity:.6;padding-top:4px}.ngxsp-nfx-status{font-size:11px;text-transform:uppercase;letter-spacing:.12em;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:#ffffff0f;opacity:.78}.ngxsp-nfx-spacer{flex:1}.ngxsp-nfx-centerDock{position:absolute;inset:0;margin:auto;width:max-content;height:max-content;display:flex;align-items:center;gap:14px;z-index:4;opacity:0;transform:translateY(10px) scale(.98);transition:opacity .18s ease,transform .18s ease}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-centerDock{opacity:1;transform:translateY(0) scale(1)}.ngxsp-nfx-center{height:96px;width:96px;border-radius:999px;border:1px solid rgba(255,255,255,.26);background:#f8f8f81f;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);box-shadow:0 20px 60px #00000059}.ngxsp-nfx-side{height:58px;min-width:58px;border-radius:999px;border:1px solid rgba(255,255,255,.18);background:#0808086b;color:#fff;padding:0 18px;cursor:pointer;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-weight:700;font-size:14px;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-nfx-side-wide{min-width:78px}.ngxsp-nfx-bottom{position:absolute;left:14px;right:14px;bottom:14px;padding:12px 14px 14px;background:linear-gradient(to top,#070707eb,#070707b3);border:1px solid rgba(255,255,255,.1);border-radius:20px;gap:10px;flex-direction:column;align-items:stretch;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);z-index:4;opacity:0;transform:translateY(12px);transition:opacity .18s ease,transform .18s ease;box-shadow:0 16px 50px #00000052}.ngxsp-root.ngxsp-ui-visible .ngxsp-nfx-bottom{opacity:1;transform:translateY(0)}.ngxsp-nfx-metaRow{display:flex;align-items:center;gap:10px;width:100%}.ngxsp-nfx-time{opacity:.92;font-variant-numeric:tabular-nums;font-size:12px;font-weight:600}.ngxsp-nfx-range{min-width:100%}.ngxsp-nfx-volumeGroup{display:inline-flex;align-items:center;gap:8px}.ngxsp-nfx-volume{max-width:0;min-width:0;opacity:0;transform:scaleX(.72);transform-origin:left center;pointer-events:none;transition:opacity .18s ease,transform .18s ease,max-width .18s ease}.ngxsp-nfx-volumeGroup:hover .ngxsp-nfx-volume,.ngxsp-nfx-volumeGroup:focus-within .ngxsp-nfx-volume{max-width:160px;min-width:96px;opacity:1;transform:scaleX(1);pointer-events:auto}.ngxsp-nfx-label{font-size:12px;opacity:.72}.ngxsp-nfx-chip{background:#ffffff14;border:1px solid rgba(255,255,255,.16);color:#fff;border-radius:999px;padding:8px 12px;cursor:pointer;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;gap:8px}.ngxsp-nfx-chip.accent{background:#563f1b80;border-color:#e8be763d;color:#f3d59c}.ngxsp-nfx-chip-quiet{background:#ffffff0d}.ngxsp-nfx-center:hover,.ngxsp-nfx-side:hover,.ngxsp-nfx-chip:hover{border-color:#ffffff4d;background:#1818189e}.ngxsp-nfx-center:disabled,.ngxsp-nfx-side:disabled{opacity:.38;cursor:not-allowed}.ngxsp-range{appearance:none;height:4px;border-radius:999px;background:linear-gradient(to right,#fffffff2,#ffffff47);outline:none}.ngxsp-range::-webkit-slider-thumb{appearance:none;width:14px;height:14px;border-radius:50%;background:#fff;box-shadow:0 0 0 3px #ffffff29}.ngxsp-range::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:#fff;box-shadow:0 0 0 3px #ffffff29}.ngxsp-range::-moz-range-track{height:4px;border:none;border-radius:999px;background:#ffffff38}.ngxsp-buffer{height:4px;border-radius:999px;background:#ffffff14;overflow:hidden}.ngxsp-bufferFill{height:100%;border-radius:999px;background:linear-gradient(to right,#f5f5f547,#f5f5f5d1)}.ngxsp-menu{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;padding-top:6px}.ngxsp-menuCompact{grid-template-columns:minmax(180px,240px)}.ngxsp-menuSection{display:flex;flex-direction:column;gap:6px}.ngxsp-menuTitle{font-size:11px;text-transform:uppercase;letter-spacing:.14em;opacity:.58}.ngxsp-menuItem{background:#ffffff0d;border:1px solid rgba(255,255,255,.1);color:#fff;border-radius:12px;padding:9px 11px;text-align:left;cursor:pointer;font-size:12px}.ngxsp-menuItem.active{border-color:#e8be7647;background:#563f1b61;color:#f3d59c}.ngxsp-menuHint{font-size:12px;opacity:.62;padding:8px 2px}\n"] }]
3286
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".ngxsp-root{--ngxsp-bg:#000;--ngxsp-bg-elev:var(--nsp-surface-1, #17181d);--ngxsp-bg-soft:var(--nsp-surface-2, #1e1f25);--ngxsp-border:var(--nsp-line, rgb(255 255 255 / .07));--ngxsp-border-strong:var(--nsp-line-strong, rgb(255 255 255 / .13));--ngxsp-text:var(--nsp-text, #ededf0);--ngxsp-text-dim:var(--nsp-text-2, #a3a5ae);--ngxsp-accent:var(--nsp-media-accent, #8b90ff);--ngxsp-accent-soft:rgb(139 144 255 / .14);--ngxsp-focus-ring:0 0 0 2px var(--nsp-media-accent, #8b90ff);position:relative;width:100%;height:100%;background:#000;color:var(--ngxsp-text);font-family:var(--nsp-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Ubuntu, sans-serif)}.ngxsp-video{position:relative;width:100%;height:100%;isolation:isolate;container-type:inline-size}.ngxsp-captions{position:absolute;left:6%;right:6%;bottom:5%;z-index:3;display:flex;flex-direction:column;align-items:center;gap:.15em;pointer-events:none;text-align:center;font-size:clamp(13px,2.6cqw,34px);line-height:1.3;transition:bottom .2s ease}.ngxsp-captions:empty{display:none}.ngxsp-ui-visible .ngxsp-captions{bottom:calc(2% + var(--ngxsp-captions-lift, 96px))}.ngxsp-captions ::ng-deep .ngxsp-cue{max-width:100%;padding:.08em .45em;border-radius:.2em;background:#000000bd;color:#fff;white-space:pre-line;text-shadow:0 1px 2px rgb(0 0 0 / .5)}.ngxsp-mode-audio .ngxsp-captions{display:none}.ngxsp-video video{width:100%;height:100%;display:block;background:#000;object-fit:contain}.ngxsp-mode-audio .ngxsp-video video{opacity:.02;filter:blur(10px) saturate(120%);transform:scale(1.08)}.ngxsp-audio-stage{position:absolute;inset:0;z-index:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:40px 24px 140px;background:radial-gradient(circle at 50% 24%,rgba(96,165,250,.22),transparent 30%),radial-gradient(circle at 22% 78%,rgba(45,212,191,.12),transparent 28%),linear-gradient(180deg,#040c18eb,#050a12d1);pointer-events:none;text-align:center}.ngxsp-audio-disc{width:min(32vw,220px);aspect-ratio:1;border-radius:50%;background:radial-gradient(circle at center,#f8fafce6 0 10%,#0f172aeb 11% 20%,#64748bd9 21% 22%,#0f172afa 23% 100%);box-shadow:0 24px 80px #0f172a80,inset 0 0 0 10px #ffffff0d;animation:ngxspSpin 18s linear infinite}.ngxsp-audio-copy{display:flex;flex-direction:column;gap:6px;max-width:min(76ch,100%)}.ngxsp-audio-eyebrow{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:#bfdbfecc;font-weight:700}.ngxsp-audio-title{font-size:36px;font-weight:800;letter-spacing:0;color:#f8fbff;text-shadow:0 18px 60px rgba(15,23,42,.6)}.ngxsp-audio-subtitle{font-size:16px;color:#e2e8f0c7}.ngxsp-audio-bars{display:flex;align-items:flex-end;gap:8px;height:40px}.ngxsp-audio-bars span{width:6px;height:16px;border-radius:999px;background:linear-gradient(180deg,#93c5fdf2,#22d3ee8c);animation:ngxspBars 1.15s ease-in-out infinite;box-shadow:0 0 18px #38bdf838}.ngxsp-audio-bars span:nth-child(2){animation-delay:.12s}.ngxsp-audio-bars span:nth-child(3){animation-delay:.24s}.ngxsp-audio-bars span:nth-child(4){animation-delay:.36s}.ngxsp-audio-bars span:nth-child(5){animation-delay:.48s}.ngxsp-skin-vod .ngxsp-video{position:relative}.ngxsp-skin-vod .ngxsp-video:before{content:none}.ngxsp-skin-vod .ngxsp-video:after{content:none}.ngxsp-mode-audio.ngxsp-skin-vod .ngxsp-video:before{background:linear-gradient(180deg,#03081259,#0308121f);opacity:1}.ngxsp-inline-projection{display:inline-flex;flex:0 0 auto;align-items:center;gap:8px;width:max-content}.ngxsp-inline-projection:empty{display:none}.ngxsp-gesturePulse{position:absolute;inset:0;z-index:5;display:grid;place-items:center;pointer-events:none;color:#f8fbff;animation:ngxspGesturePulse .52s ease both}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px;padding:18px;box-sizing:border-box;display:grid;place-items:center;border-radius:50%;background:#00000080;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media(max-width:640px){.ngxsp-audio-stage{padding:28px 16px 128px;gap:14px}.ngxsp-audio-disc{width:min(48vw,168px)}.ngxsp-audio-title{font-size:28px}.ngxsp-audio-subtitle{font-size:14px}.ngxsp-skin-vod .ngxsp-inline-projection{display:none}.ngxsp-gesturePulse ngxsp-smart-icon{width:64px;height:64px}}@keyframes ngxspSpin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes ngxspBars{0%,to{height:14px;opacity:.45}50%{height:40px;opacity:1}}@keyframes ngxspGesturePulse{0%{opacity:0;transform:scale(.82)}24%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(1.16)}}\n"] }]
1060
3287
  }], propDecorators: { config: [{
1061
3288
  type: Input,
1062
3289
  args: [{ required: true }]
@@ -1070,10 +3297,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1070
3297
  type: Input
1071
3298
  }], startTimeSec: [{
1072
3299
  type: Input
3300
+ }], autoplayOnInit: [{
3301
+ type: Input
3302
+ }], playRequest: [{
3303
+ type: Input
1073
3304
  }], canPrev: [{
1074
3305
  type: Input
1075
3306
  }], canNext: [{
1076
3307
  type: Input
3308
+ }], previousLabel: [{
3309
+ type: Input
3310
+ }], nextLabel: [{
3311
+ type: Input
3312
+ }], playLabel: [{
3313
+ type: Input
3314
+ }], pauseLabel: [{
3315
+ type: Input
1077
3316
  }], shuffleActive: [{
1078
3317
  type: Input
1079
3318
  }], shuffleLabel: [{
@@ -1084,6 +3323,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1084
3323
  type: Input
1085
3324
  }], hasPlaylistControls: [{
1086
3325
  type: Input
3326
+ }], mediaMode: [{
3327
+ type: Input
3328
+ }], castEnabled: [{
3329
+ type: Input
3330
+ }], castConnected: [{
3331
+ type: Input
3332
+ }], documentShortcuts: [{
3333
+ type: Input
3334
+ }], nowPlayingLabel: [{
3335
+ type: Input
3336
+ }], nowListeningLabel: [{
3337
+ type: Input
3338
+ }], fullscreenHintLabel: [{
3339
+ type: Input
3340
+ }], fullscreenLabel: [{
3341
+ type: Input
3342
+ }], readyStatusLabel: [{
3343
+ type: Input
3344
+ }], playingStatusLabel: [{
3345
+ type: Input
3346
+ }], pausedStatusLabel: [{
3347
+ type: Input
3348
+ }], bufferingStatusLabel: [{
3349
+ type: Input
3350
+ }], loadingStatusLabel: [{
3351
+ type: Input
3352
+ }], endedStatusLabel: [{
3353
+ type: Input
3354
+ }], errorStatusLabel: [{
3355
+ type: Input
3356
+ }], outputVolume: [{
3357
+ type: Input
3358
+ }], outputVolumeLabel: [{
3359
+ type: Input
3360
+ }], outputMuteLabel: [{
3361
+ type: Input
3362
+ }], outputUnmuteLabel: [{
3363
+ type: Input
3364
+ }], timelineDurationSec: [{
3365
+ type: Input
3366
+ }], timelineMarkers: [{
3367
+ type: Input
1087
3368
  }], ended: [{
1088
3369
  type: Output
1089
3370
  }], videoSize: [{
@@ -1100,9 +3381,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1100
3381
  type: Output
1101
3382
  }], subtitles: [{
1102
3383
  type: Output
3384
+ }], subtitlesLoaded: [{
3385
+ type: Output
3386
+ }], menuLabels: [{
3387
+ type: Input
3388
+ }], cast: [{
3389
+ type: Output
3390
+ }], stateChange: [{
3391
+ type: Output
3392
+ }], playbackToggle: [{
3393
+ type: Output
3394
+ }], outputVolumeChange: [{
3395
+ type: Output
1103
3396
  }], videoEl: [{
1104
3397
  type: ViewChild,
1105
3398
  args: ['video', { static: true }]
3399
+ }], captionsLayer: [{
3400
+ type: ViewChild,
3401
+ args: ['captionsLayer', { static: true }]
3402
+ }], subtitleFileInput: [{
3403
+ type: ViewChild,
3404
+ args: ['subtitleFileInput']
1106
3405
  }], onEscape: [{
1107
3406
  type: HostListener,
1108
3407
  args: ['document:keydown.escape']
@@ -1112,13 +3411,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1112
3411
  }] } });
1113
3412
 
1114
3413
  /**
1115
- * Register provider implementations for subtitles/metadata.
1116
- * You can call this in your app `bootstrapApplication()` providers array.
3414
+ * Jetons d'injection typés pour les providers de sous-titres / métadonnées.
3415
+ * (Remplace les jetons `string` non typés — cf. docs/AUDIT.md, manquement #10.)
3416
+ * Un consommateur peut les injecter directement :
3417
+ * `inject(SMART_PLAYER_SUBTITLE_PROVIDERS)`.
3418
+ */
3419
+ const SMART_PLAYER_SUBTITLE_PROVIDERS = new InjectionToken('SMART_PLAYER_SUBTITLE_PROVIDERS', { factory: () => [] });
3420
+ const SMART_PLAYER_METADATA_PROVIDERS = new InjectionToken('SMART_PLAYER_METADATA_PROVIDERS', { factory: () => [] });
3421
+ /**
3422
+ * Enregistre les implémentations de providers (sous-titres / métadonnées).
3423
+ * À appeler dans le tableau `providers` de `bootstrapApplication()`.
1117
3424
  */
1118
3425
  function provideSmartPlayer(cfg) {
1119
3426
  return makeEnvironmentProviders([
1120
- { provide: 'SMART_PLAYER_SUBTITLE_PROVIDERS', useValue: cfg.subtitleProviders ?? [] },
1121
- { provide: 'SMART_PLAYER_METADATA_PROVIDERS', useValue: cfg.metadataProviders ?? [] }
3427
+ { provide: SMART_PLAYER_SUBTITLE_PROVIDERS, useValue: cfg.subtitleProviders ?? [] },
3428
+ { provide: SMART_PLAYER_METADATA_PROVIDERS, useValue: cfg.metadataProviders ?? [] },
1122
3429
  ]);
1123
3430
  }
1124
3431
 
@@ -1126,5 +3433,5 @@ function provideSmartPlayer(cfg) {
1126
3433
  * Generated bundle index. Do not edit.
1127
3434
  */
1128
3435
 
1129
- export { SmartPlayerComponent, provideSmartPlayer };
3436
+ export { DEFAULT_PLAYER_MENU_LABELS, PlayerControlsComponent, PlayerControlsEditionComponent, PlayerControlsVodComponent, SMART_PLAYER_METADATA_PROVIDERS, SMART_PLAYER_SUBTITLE_PROVIDERS, SmartConfirmDialogComponent, SmartPlayerComponent, SmartPlayerIconComponent, SmartSettingsComponent, provideSmartPlayer };
1130
3437
  //# sourceMappingURL=ngxsplayer-ngx-smart-player.mjs.map