@ngxsplayer/ngx-smart-player 0.0.1-next.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.
@@ -0,0 +1,1130 @@
1
+ import { PlayerFacade, createSsaiManifestRewritePlugin } from '@ngxsp/core';
2
+ export * from '@ngxsp/core';
3
+ import * as i0 from '@angular/core';
4
+ import { EventEmitter, HostListener, ViewChild, Output, Input, ChangeDetectionStrategy, Component, makeEnvironmentProviders } from '@angular/core';
5
+ import { CommonModule } from '@angular/common';
6
+ import { Subscription } from 'rxjs';
7
+
8
+ class SmartPlayerComponent {
9
+ config;
10
+ plugins = [];
11
+ ports;
12
+ engines;
13
+ debug = false;
14
+ /** Optional start time (seconds) used for "resume playback". */
15
+ startTimeSec = null;
16
+ canPrev = false;
17
+ canNext = false;
18
+ shuffleActive = false;
19
+ shuffleLabel = 'Shuffle: off';
20
+ repeatMode = 'off';
21
+ repeatLabel = 'Repeat: off';
22
+ hasPlaylistControls = false;
23
+ ended = new EventEmitter();
24
+ videoSize = new EventEmitter();
25
+ /** Emits playback position updates. */
26
+ progress = new EventEmitter();
27
+ prev = new EventEmitter();
28
+ next = new EventEmitter();
29
+ toggleShuffle = new EventEmitter();
30
+ cycleRepeat = new EventEmitter();
31
+ subtitles = new EventEmitter();
32
+ videoEl;
33
+ state = null;
34
+ duration = 0;
35
+ currentTime = 0;
36
+ volume = 1;
37
+ lastVolumeBeforeMute = 1;
38
+ playbackRate = 1;
39
+ bufferAheadSec = 0;
40
+ tracks = [];
41
+ openMenu = null;
42
+ controlsVisible = true;
43
+ playbackRates = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
44
+ facade = new PlayerFacade();
45
+ sub = new Subscription();
46
+ hideControlsTimer = null;
47
+ get uiSkin() {
48
+ return (this.config?.ui?.skin ?? 'classic') === 'netflix' ? 'netflix' : 'classic';
49
+ }
50
+ get uiVisible() {
51
+ const kind = this.state?.kind;
52
+ return this.controlsVisible || this.openMenu !== null || kind === 'paused' || kind === 'loading' || kind === 'buffering' || kind === 'error';
53
+ }
54
+ get displayTitle() {
55
+ const ui = this.config?.ui;
56
+ const src = this.config?.source;
57
+ return (ui?.title ??
58
+ ui?.displayTitle ??
59
+ src?.title ??
60
+ src?.contentId ??
61
+ 'Player');
62
+ }
63
+ get audioTracks() {
64
+ return this.tracks.filter((track) => track.kind === 'audio');
65
+ }
66
+ get textTracks() {
67
+ return this.tracks.filter((track) => track.kind === 'text');
68
+ }
69
+ get videoTracks() {
70
+ return this.tracks.filter((track) => track.kind === 'video');
71
+ }
72
+ get selectedAudioTrackId() {
73
+ return this.audioTracks.find((track) => track.active)?.id ?? null;
74
+ }
75
+ get selectedTextTrackId() {
76
+ return this.textTracks.find((track) => track.active)?.id ?? null;
77
+ }
78
+ get selectedVideoTrackId() {
79
+ return this.videoTracks.find((track) => track.active)?.id ?? null;
80
+ }
81
+ ngOnInit() {
82
+ const plugins = [...(this.plugins ?? [])];
83
+ // v1.11: SSAI (server-side stitched streams) – rewrite manifest URL if configured.
84
+ if (this.config?.ads?.enabled && this.config?.ads?.mode === 'ssai') {
85
+ plugins.push(createSsaiManifestRewritePlugin());
86
+ }
87
+ this.facade.attach(this.videoEl.nativeElement, plugins, this.config, this.ports, this.engines);
88
+ this.sub.add(this.facade.state$.subscribe((s) => {
89
+ this.state = s;
90
+ this.syncControlVisibility();
91
+ }));
92
+ this.sub.add(this.facade.time$.subscribe((t) => {
93
+ this.currentTime = t.currentTime;
94
+ this.duration = t.duration;
95
+ this.progress.emit({ currentTime: this.currentTime, duration: this.duration });
96
+ }));
97
+ this.sub.add(this.facade.bufferAheadSec$.subscribe((value) => {
98
+ this.bufferAheadSec = value;
99
+ }));
100
+ this.sub.add(this.facade.tracks$.subscribe((tracks) => {
101
+ this.tracks = tracks;
102
+ }));
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(() => { });
117
+ }
118
+ ngOnDestroy() {
119
+ this.clearHideControlsTimer();
120
+ this.sub.unsubscribe();
121
+ this.facade.destroy();
122
+ }
123
+ tryPlay() {
124
+ void this.facade.play().catch(() => { });
125
+ }
126
+ get debugJson() {
127
+ try {
128
+ const d = this.facade.diagnostics?.() ?? {};
129
+ const tel = this.ports?.telemetry?.export?.();
130
+ return JSON.stringify({ ...d, telemetry: tel }, null, 2);
131
+ }
132
+ catch {
133
+ return '{}';
134
+ }
135
+ }
136
+ get autoplayBlocked() {
137
+ const s = this.state;
138
+ return s?.kind === 'error' && s?.error?.code === 'AUTOPLAY_BLOCKED';
139
+ }
140
+ async copyDebug() {
141
+ try {
142
+ await navigator.clipboard.writeText(this.debugJson);
143
+ }
144
+ catch {
145
+ // clipboard may be unavailable
146
+ }
147
+ }
148
+ downloadDebug() {
149
+ try {
150
+ const blob = new Blob([this.debugJson], { type: 'application/json' });
151
+ const url = URL.createObjectURL(blob);
152
+ const a = document.createElement('a');
153
+ a.href = url;
154
+ a.download = 'ngxsp-diagnostics.json';
155
+ a.click();
156
+ setTimeout(() => URL.revokeObjectURL(url), 0);
157
+ }
158
+ catch {
159
+ // ignore
160
+ }
161
+ }
162
+ togglePlay() {
163
+ if (this.state?.kind === 'playing')
164
+ this.facade.pause();
165
+ else
166
+ this.facade.play();
167
+ }
168
+ seekRel(deltaSec) {
169
+ this.facade.seekTo(Math.max(0, this.currentTime + deltaSec));
170
+ }
171
+ onScrub(value) {
172
+ const v = Number(value);
173
+ if (Number.isFinite(v))
174
+ this.facade.seekTo(v);
175
+ }
176
+ 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);
182
+ }
183
+ toggleMute() {
184
+ if (this.volume <= 0.001) {
185
+ this.volume = Math.max(0.5, this.lastVolumeBeforeMute || 0.5);
186
+ }
187
+ else {
188
+ this.lastVolumeBeforeMute = this.volume;
189
+ this.volume = 0;
190
+ }
191
+ this.facade.setVolume(this.volume);
192
+ this.onInteraction();
193
+ }
194
+ async setPlaybackRate(rate) {
195
+ this.playbackRate = rate;
196
+ this.openMenu = null;
197
+ this.onInteraction();
198
+ await this.facade.setRate(rate);
199
+ }
200
+ reload() {
201
+ this.facade.reload().catch(() => void 0);
202
+ }
203
+ onLoadedMetadata() {
204
+ const v = this.videoEl?.nativeElement;
205
+ if (!v)
206
+ return;
207
+ const width = v.videoWidth || 0;
208
+ const height = v.videoHeight || 0;
209
+ if (width > 0 && height > 0) {
210
+ this.videoSize.emit({ width, height });
211
+ }
212
+ }
213
+ onEnded() {
214
+ // facade already emits 'ended' based on engine events; this output lets host apps chain playlists.
215
+ this.ended.emit();
216
+ }
217
+ toggleFullscreen() {
218
+ const host = this.videoEl.nativeElement.parentElement;
219
+ if (!host)
220
+ return;
221
+ const doc = document;
222
+ const el = host;
223
+ if (!doc.fullscreenElement && el.requestFullscreen) {
224
+ el.requestFullscreen().catch(() => void 0);
225
+ }
226
+ else if (doc.exitFullscreen) {
227
+ doc.exitFullscreen().catch(() => void 0);
228
+ }
229
+ }
230
+ toggleMenu(menu) {
231
+ this.openMenu = this.openMenu === menu ? null : menu;
232
+ this.onInteraction(this.openMenu !== null);
233
+ }
234
+ async selectTextTrack(trackId) {
235
+ this.openMenu = null;
236
+ this.onInteraction();
237
+ await this.facade.setTextTrack(trackId);
238
+ }
239
+ async selectAudioTrack(trackId) {
240
+ this.openMenu = null;
241
+ this.onInteraction();
242
+ await this.facade.setAudioTrack(trackId);
243
+ }
244
+ async selectVideoTrack(trackId) {
245
+ this.openMenu = null;
246
+ this.onInteraction();
247
+ await this.facade.setVideoTrack(trackId);
248
+ }
249
+ async togglePiP() {
250
+ const video = this.videoEl.nativeElement;
251
+ const doc = document;
252
+ try {
253
+ if (doc.pictureInPictureElement) {
254
+ await doc.exitPictureInPicture?.();
255
+ }
256
+ else {
257
+ await video.requestPictureInPicture?.();
258
+ }
259
+ }
260
+ catch {
261
+ // Ignore unsupported or blocked PiP transitions.
262
+ }
263
+ this.onInteraction();
264
+ }
265
+ isPiPSupported() {
266
+ const video = this.videoEl?.nativeElement;
267
+ const doc = document;
268
+ return !!doc.pictureInPictureEnabled && !video?.disablePictureInPicture;
269
+ }
270
+ pipLabel() {
271
+ const doc = document;
272
+ return doc.pictureInPictureElement ? 'Exit PiP' : 'PiP';
273
+ }
274
+ isPiPActive() {
275
+ const doc = document;
276
+ return !!doc.pictureInPictureElement;
277
+ }
278
+ captionsLabel() {
279
+ return this.selectedTextTrackId !== null ? 'Captions on' : 'Load subtitles';
280
+ }
281
+ volumeLabel() {
282
+ if (this.volume <= 0.001)
283
+ return 'Muted';
284
+ if (this.volume < 0.5)
285
+ return 'Vol';
286
+ return 'Volume';
287
+ }
288
+ volumeIcon() {
289
+ if (this.volume <= 0.001)
290
+ return '🔇';
291
+ if (this.volume < 0.5)
292
+ return '🔉';
293
+ return '🔊';
294
+ }
295
+ playbackRateLabel() {
296
+ return `${this.playbackRate}x`;
297
+ }
298
+ statusLabel() {
299
+ const kind = this.state?.kind;
300
+ if (kind === 'playing')
301
+ return this.playbackRate === 1 ? 'Playing' : `Playing ${this.playbackRate}x`;
302
+ if (kind === 'paused')
303
+ return 'Paused';
304
+ if (kind === 'buffering')
305
+ return 'Buffering';
306
+ if (kind === 'loading')
307
+ return 'Loading';
308
+ if (kind === 'ended')
309
+ return 'Ended';
310
+ 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;
322
+ }
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;
332
+ }
333
+ bufferPercent() {
334
+ if (!this.duration || !Number.isFinite(this.duration) || this.duration <= 0)
335
+ return 0;
336
+ return Math.max(0, Math.min(100, (this.bufferAheadSec / this.duration) * 100));
337
+ }
338
+ fmtTime(sec) {
339
+ const s = Math.max(0, Math.floor(sec || 0));
340
+ const h = Math.floor(s / 3600);
341
+ const m = Math.floor((s % 3600) / 60);
342
+ const r = s % 60;
343
+ if (h > 0)
344
+ return `${h}:${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
345
+ return `${m}:${String(r).padStart(2, '0')}`;
346
+ }
347
+ noop() { }
348
+ onInteraction(persist = false) {
349
+ this.controlsVisible = true;
350
+ this.clearHideControlsTimer();
351
+ if (!persist && this.state?.kind === 'playing' && this.openMenu === null) {
352
+ this.hideControlsTimer = setTimeout(() => {
353
+ this.controlsVisible = false;
354
+ }, 2200);
355
+ }
356
+ }
357
+ onPointerLeave() {
358
+ if (this.state?.kind === 'playing' && this.openMenu === null) {
359
+ this.clearHideControlsTimer();
360
+ this.controlsVisible = false;
361
+ }
362
+ }
363
+ onRootClick(event) {
364
+ const target = event.target;
365
+ if (!target)
366
+ return;
367
+ if (target.closest('.ngxsp-menu, .ngxsp-nfx-chip, .ngxsp-btn, .ngxsp-debug, .ngxsp-nfx-side, .ngxsp-nfx-center, .ngxsp-range')) {
368
+ return;
369
+ }
370
+ this.openMenu = null;
371
+ this.onInteraction();
372
+ }
373
+ onEscape() {
374
+ if (this.openMenu !== null) {
375
+ this.openMenu = null;
376
+ this.onInteraction();
377
+ }
378
+ }
379
+ onKeydown(event) {
380
+ if (this.isInteractiveTarget(event.target))
381
+ return;
382
+ if (event.code === 'Space') {
383
+ event.preventDefault();
384
+ this.togglePlay();
385
+ this.onInteraction();
386
+ return;
387
+ }
388
+ if (event.code === 'ArrowLeft') {
389
+ event.preventDefault();
390
+ this.seekRel(-10);
391
+ this.onInteraction();
392
+ return;
393
+ }
394
+ if (event.code === 'ArrowRight') {
395
+ event.preventDefault();
396
+ this.seekRel(10);
397
+ this.onInteraction();
398
+ return;
399
+ }
400
+ if (event.code === 'KeyF') {
401
+ event.preventDefault();
402
+ this.toggleFullscreen();
403
+ this.onInteraction();
404
+ return;
405
+ }
406
+ if (event.code === 'KeyM') {
407
+ event.preventDefault();
408
+ this.toggleMute();
409
+ }
410
+ }
411
+ syncControlVisibility() {
412
+ if (this.state?.kind === 'playing' && this.openMenu === null) {
413
+ this.onInteraction();
414
+ return;
415
+ }
416
+ this.clearHideControlsTimer();
417
+ this.controlsVisible = true;
418
+ }
419
+ clearHideControlsTimer() {
420
+ if (this.hideControlsTimer) {
421
+ clearTimeout(this.hideControlsTimer);
422
+ this.hideControlsTimer = null;
423
+ }
424
+ }
425
+ isInteractiveTarget(target) {
426
+ const el = target;
427
+ if (!el)
428
+ return false;
429
+ return !!el.closest('input, button, textarea, select, summary, [contenteditable="true"]');
430
+ }
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
+ emitNext() {
471
+ this.next.emit();
472
+ }
473
+ emitPrev() {
474
+ this.prev.emit();
475
+ }
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>
495
+ </div>
496
+ </div>
497
+ }
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>
508
+ }
509
+
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>
679
+ </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))"
751
+ />
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"
760
+ />
761
+ </div>
762
+ }
763
+
764
+ </div>
765
+ </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 });
767
+ }
768
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: SmartPlayerComponent, decorators: [{
769
+ 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>
788
+ </div>
789
+ </div>
790
+ }
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>
801
+ }
802
+
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>
972
+ </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))"
1044
+ />
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"
1053
+ />
1054
+ </div>
1055
+ }
1056
+
1057
+ </div>
1058
+ </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"] }]
1060
+ }], propDecorators: { config: [{
1061
+ type: Input,
1062
+ args: [{ required: true }]
1063
+ }], plugins: [{
1064
+ type: Input
1065
+ }], ports: [{
1066
+ type: Input
1067
+ }], engines: [{
1068
+ type: Input
1069
+ }], debug: [{
1070
+ type: Input
1071
+ }], startTimeSec: [{
1072
+ type: Input
1073
+ }], canPrev: [{
1074
+ type: Input
1075
+ }], canNext: [{
1076
+ type: Input
1077
+ }], shuffleActive: [{
1078
+ type: Input
1079
+ }], shuffleLabel: [{
1080
+ type: Input
1081
+ }], repeatMode: [{
1082
+ type: Input
1083
+ }], repeatLabel: [{
1084
+ type: Input
1085
+ }], hasPlaylistControls: [{
1086
+ type: Input
1087
+ }], ended: [{
1088
+ type: Output
1089
+ }], videoSize: [{
1090
+ type: Output
1091
+ }], progress: [{
1092
+ type: Output
1093
+ }], prev: [{
1094
+ type: Output
1095
+ }], next: [{
1096
+ type: Output
1097
+ }], toggleShuffle: [{
1098
+ type: Output
1099
+ }], cycleRepeat: [{
1100
+ type: Output
1101
+ }], subtitles: [{
1102
+ type: Output
1103
+ }], videoEl: [{
1104
+ type: ViewChild,
1105
+ args: ['video', { static: true }]
1106
+ }], onEscape: [{
1107
+ type: HostListener,
1108
+ args: ['document:keydown.escape']
1109
+ }], onKeydown: [{
1110
+ type: HostListener,
1111
+ args: ['document:keydown', ['$event']]
1112
+ }] } });
1113
+
1114
+ /**
1115
+ * Register provider implementations for subtitles/metadata.
1116
+ * You can call this in your app `bootstrapApplication()` providers array.
1117
+ */
1118
+ function provideSmartPlayer(cfg) {
1119
+ return makeEnvironmentProviders([
1120
+ { provide: 'SMART_PLAYER_SUBTITLE_PROVIDERS', useValue: cfg.subtitleProviders ?? [] },
1121
+ { provide: 'SMART_PLAYER_METADATA_PROVIDERS', useValue: cfg.metadataProviders ?? [] }
1122
+ ]);
1123
+ }
1124
+
1125
+ /**
1126
+ * Generated bundle index. Do not edit.
1127
+ */
1128
+
1129
+ export { SmartPlayerComponent, provideSmartPlayer };
1130
+ //# sourceMappingURL=ngxsplayer-ngx-smart-player.mjs.map