@shival99/z-ui 2.4.3 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/css/base.css +2 -1
- package/assets/css/global.css +23 -2
- package/assets/css/tailwind.css +7 -4
- package/fesm2022/shival99-z-ui-components-z-accordion.mjs +6 -6
- package/fesm2022/shival99-z-ui-components-z-accordion.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-audio-player.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-audio-player.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-autocomplete.mjs +27 -12
- package/fesm2022/shival99-z-ui-components-z-autocomplete.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-avatar.mjs +6 -6
- package/fesm2022/shival99-z-ui-components-z-avatar.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-breadcrumb.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-breadcrumb.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-button-group.mjs +11 -11
- package/fesm2022/shival99-z-ui-components-z-button-group.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-button.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-button.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-calendar.mjs +40 -40
- package/fesm2022/shival99-z-ui-components-z-calendar.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-card.mjs +7 -7
- package/fesm2022/shival99-z-ui-components-z-card.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-chat.mjs +4 -4
- package/fesm2022/shival99-z-ui-components-z-chat.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-checkbox.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-checkbox.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-code.mjs +58 -19
- package/fesm2022/shival99-z-ui-components-z-code.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-command.mjs +6 -5
- package/fesm2022/shival99-z-ui-components-z-command.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-drawer.mjs +17 -17
- package/fesm2022/shival99-z-ui-components-z-drawer.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-dropdown-menu.mjs +12 -4
- package/fesm2022/shival99-z-ui-components-z-dropdown-menu.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-editor.mjs +6 -6
- package/fesm2022/shival99-z-ui-components-z-editor.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-empty.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-empty.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-filter.mjs +27 -27
- package/fesm2022/shival99-z-ui-components-z-filter.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-gallery.mjs +42 -34
- package/fesm2022/shival99-z-ui-components-z-gallery.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-hover-glide.mjs +221 -0
- package/fesm2022/shival99-z-ui-components-z-hover-glide.mjs.map +1 -0
- package/fesm2022/shival99-z-ui-components-z-icon.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-icon.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-input.mjs +5 -5
- package/fesm2022/shival99-z-ui-components-z-input.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-kanban.mjs +4 -4
- package/fesm2022/shival99-z-ui-components-z-kanban.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-loading.mjs +18 -4
- package/fesm2022/shival99-z-ui-components-z-loading.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-marquee.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-marquee.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-media-player.mjs +346 -956
- package/fesm2022/shival99-z-ui-components-z-media-player.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-menu.mjs +8 -8
- package/fesm2022/shival99-z-ui-components-z-menu.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-metric-card.mjs +5 -5
- package/fesm2022/shival99-z-ui-components-z-metric-card.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-modal.mjs +17 -17
- package/fesm2022/shival99-z-ui-components-z-modal.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-number-ticker.mjs +352 -0
- package/fesm2022/shival99-z-ui-components-z-number-ticker.mjs.map +1 -0
- package/fesm2022/shival99-z-ui-components-z-overlay.mjs +9 -9
- package/fesm2022/shival99-z-ui-components-z-overlay.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-pagination.mjs +4 -4
- package/fesm2022/shival99-z-ui-components-z-pagination.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-popover.mjs +14 -14
- package/fesm2022/shival99-z-ui-components-z-popover.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-qrcode.mjs +32 -8
- package/fesm2022/shival99-z-ui-components-z-qrcode.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-radio.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-radio.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-scrollarea.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-scrollarea.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-select.mjs +181 -26
- package/fesm2022/shival99-z-ui-components-z-select.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-show-more.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-show-more.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-skeleton-auto.mjs +4 -4
- package/fesm2022/shival99-z-ui-components-z-skeleton-auto.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-skeleton.mjs +5 -5
- package/fesm2022/shival99-z-ui-components-z-skeleton.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-steps.mjs +4 -4
- package/fesm2022/shival99-z-ui-components-z-steps.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-switch.mjs +5 -5
- package/fesm2022/shival99-z-ui-components-z-switch.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-table.mjs +107 -105
- package/fesm2022/shival99-z-ui-components-z-table.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-tabs.mjs +327 -31
- package/fesm2022/shival99-z-ui-components-z-tabs.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-tags.mjs +15 -15
- package/fesm2022/shival99-z-ui-components-z-tags.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-timeline.mjs +4 -4
- package/fesm2022/shival99-z-ui-components-z-timeline.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-toast.mjs +10 -9
- package/fesm2022/shival99-z-ui-components-z-toast.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-tooltip.mjs +11 -11
- package/fesm2022/shival99-z-ui-components-z-tooltip.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-components-z-upload.mjs +3 -3
- package/fesm2022/shival99-z-ui-components-z-upload.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-i18n.mjs +6 -26
- package/fesm2022/shival99-z-ui-i18n.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-pipes.mjs +18 -18
- package/fesm2022/shival99-z-ui-pipes.mjs.map +1 -1
- package/fesm2022/shival99-z-ui-services.mjs +31 -31
- package/fesm2022/shival99-z-ui-services.mjs.map +1 -1
- package/package.json +21 -1
- package/types/shival99-z-ui-components-z-autocomplete.d.ts +6 -2
- package/types/shival99-z-ui-components-z-calendar.d.ts +4 -4
- package/types/shival99-z-ui-components-z-card.d.ts +21 -21
- package/types/shival99-z-ui-components-z-code.d.ts +16 -3
- package/types/shival99-z-ui-components-z-drawer.d.ts +1 -1
- package/types/shival99-z-ui-components-z-editor.d.ts +8 -8
- package/types/shival99-z-ui-components-z-gallery.d.ts +4 -1
- package/types/shival99-z-ui-components-z-hover-glide.d.ts +48 -0
- package/types/shival99-z-ui-components-z-icon.d.ts +1 -1
- package/types/shival99-z-ui-components-z-loading.d.ts +9 -2
- package/types/shival99-z-ui-components-z-media-player.d.ts +102 -172
- package/types/shival99-z-ui-components-z-metric-card.d.ts +19 -19
- package/types/shival99-z-ui-components-z-modal.d.ts +1 -1
- package/types/shival99-z-ui-components-z-number-ticker.d.ts +110 -0
- package/types/shival99-z-ui-components-z-overlay.d.ts +3 -3
- package/types/shival99-z-ui-components-z-popover.d.ts +11 -8
- package/types/shival99-z-ui-components-z-qrcode.d.ts +5 -2
- package/types/shival99-z-ui-components-z-select.d.ts +45 -7
- package/types/shival99-z-ui-components-z-switch.d.ts +22 -21
- package/types/shival99-z-ui-components-z-table.d.ts +1 -1
- package/types/shival99-z-ui-components-z-tabs.d.ts +59 -7
- package/types/shival99-z-ui-components-z-toast.d.ts +1 -1
- package/types/shival99-z-ui-components-z-tooltip.d.ts +2 -2
- package/types/shival99-z-ui-components-z-upload.d.ts +3 -3
- package/types/shival99-z-ui-services.d.ts +10 -10
|
@@ -1,39 +1,197 @@
|
|
|
1
|
-
import * as i1 from '@angular/common';
|
|
2
|
-
import { CommonModule } from '@angular/common';
|
|
3
1
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { inject,
|
|
5
|
-
import
|
|
6
|
-
import { FormsModule } from '@angular/forms';
|
|
7
|
-
import { DomSanitizer } from '@angular/platform-browser';
|
|
8
|
-
import { TranslatePipe } from '@ngx-translate/core';
|
|
9
|
-
import { ZButtonComponent } from '@shival99/z-ui/components/z-button';
|
|
10
|
-
import { ZIconComponent } from '@shival99/z-ui/components/z-icon';
|
|
11
|
-
import { ZPopoverDirective } from '@shival99/z-ui/components/z-popover';
|
|
2
|
+
import { inject, input, output, viewChild, computed, signal, effect, CUSTOM_ELEMENTS_SCHEMA, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { ZThemeService, ZTranslateService } from '@shival99/z-ui/services';
|
|
12
4
|
import { zMergeClasses } from '@shival99/z-ui/utils';
|
|
13
5
|
import { cva } from 'class-variance-authority';
|
|
6
|
+
import 'vidstack/player';
|
|
7
|
+
import 'vidstack/player/ui';
|
|
8
|
+
import 'vidstack/player/layouts/default';
|
|
9
|
+
import 'vidstack/player/layouts/plyr';
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
settings: true,
|
|
26
|
-
pip: true,
|
|
27
|
-
theater: true,
|
|
28
|
-
fullscreen: true,
|
|
11
|
+
const DEFAULT_MEDIA_PLAYER_CONFIG = {
|
|
12
|
+
autoPlay: false,
|
|
13
|
+
loop: false,
|
|
14
|
+
muted: false,
|
|
15
|
+
volume: 1,
|
|
16
|
+
playbackRate: 1,
|
|
17
|
+
subtitles: [],
|
|
18
|
+
thumbnailsVtt: '',
|
|
19
|
+
chaptersVtt: '',
|
|
20
|
+
playsInline: true,
|
|
29
21
|
};
|
|
22
|
+
/** Vietnamese translation of Vidstack's Default Layout UI words, applied when the app language is `vi`. */
|
|
23
|
+
const MEDIA_PLAYER_VI_DEFAULT_TRANSLATIONS = {
|
|
24
|
+
Announcements: 'Thông báo',
|
|
25
|
+
Accessibility: 'Trợ năng',
|
|
26
|
+
AirPlay: 'AirPlay',
|
|
27
|
+
Audio: 'Âm thanh',
|
|
28
|
+
Auto: 'Tự động',
|
|
29
|
+
Boost: 'Tăng cường',
|
|
30
|
+
Captions: 'Phụ đề',
|
|
31
|
+
'Caption Styles': 'Kiểu phụ đề',
|
|
32
|
+
'Captions look like this': 'Phụ đề trông như thế này',
|
|
33
|
+
Chapters: 'Chương',
|
|
34
|
+
'Closed-Captions Off': 'Tắt phụ đề',
|
|
35
|
+
'Closed-Captions On': 'Bật phụ đề',
|
|
36
|
+
Connected: 'Đã kết nối',
|
|
37
|
+
Continue: 'Tiếp tục',
|
|
38
|
+
Connecting: 'Đang kết nối',
|
|
39
|
+
Default: 'Mặc định',
|
|
40
|
+
Disabled: 'Đã tắt',
|
|
41
|
+
Disconnected: 'Đã ngắt kết nối',
|
|
42
|
+
'Display Background': 'Nền hiển thị',
|
|
43
|
+
Download: 'Tải xuống',
|
|
44
|
+
'Enter Fullscreen': 'Vào toàn màn hình',
|
|
45
|
+
'Enter PiP': 'Vào chế độ nổi',
|
|
46
|
+
'Exit Fullscreen': 'Thoát toàn màn hình',
|
|
47
|
+
'Exit PiP': 'Thoát chế độ nổi',
|
|
48
|
+
Font: 'Phông chữ',
|
|
49
|
+
Family: 'Kiểu chữ',
|
|
50
|
+
Fullscreen: 'Toàn màn hình',
|
|
51
|
+
'Google Cast': 'Google Cast',
|
|
52
|
+
'Keyboard Animations': 'Hiệu ứng phím tắt',
|
|
53
|
+
LIVE: 'TRỰC TIẾP',
|
|
54
|
+
Loop: 'Lặp lại',
|
|
55
|
+
Mute: 'Tắt tiếng',
|
|
56
|
+
Normal: 'Bình thường',
|
|
57
|
+
Off: 'Tắt',
|
|
58
|
+
Pause: 'Tạm dừng',
|
|
59
|
+
Play: 'Phát',
|
|
60
|
+
Playback: 'Phát lại',
|
|
61
|
+
PiP: 'Nổi',
|
|
62
|
+
Quality: 'Chất lượng',
|
|
63
|
+
Replay: 'Phát lại',
|
|
64
|
+
Reset: 'Đặt lại',
|
|
65
|
+
'Seek Backward': 'Tua lùi',
|
|
66
|
+
'Seek Forward': 'Tua tới',
|
|
67
|
+
Seek: 'Tua',
|
|
68
|
+
Settings: 'Cài đặt',
|
|
69
|
+
'Skip To Live': 'Về trực tiếp',
|
|
70
|
+
Speed: 'Tốc độ',
|
|
71
|
+
Size: 'Kích thước',
|
|
72
|
+
Color: 'Màu sắc',
|
|
73
|
+
Opacity: 'Độ mờ',
|
|
74
|
+
Shadow: 'Đổ bóng',
|
|
75
|
+
Text: 'Văn bản',
|
|
76
|
+
'Text Background': 'Nền văn bản',
|
|
77
|
+
Track: 'Track',
|
|
78
|
+
Unmute: 'Bật tiếng',
|
|
79
|
+
Volume: 'Âm lượng',
|
|
80
|
+
};
|
|
81
|
+
/** Vietnamese translation of Vidstack's Plyr Layout UI words, applied when the app language is `vi`. */
|
|
82
|
+
const MEDIA_PLAYER_VI_PLYR_TRANSLATIONS = {
|
|
83
|
+
Ad: 'Quảng cáo',
|
|
84
|
+
All: 'Tất cả',
|
|
85
|
+
AirPlay: 'AirPlay',
|
|
86
|
+
Audio: 'Âm thanh',
|
|
87
|
+
Auto: 'Tự động',
|
|
88
|
+
Buffered: 'Đã tải đệm',
|
|
89
|
+
Captions: 'Phụ đề',
|
|
90
|
+
'Current time': 'Thời gian hiện tại',
|
|
91
|
+
Default: 'Mặc định',
|
|
92
|
+
'Disable captions': 'Tắt phụ đề',
|
|
93
|
+
Disabled: 'Đã tắt',
|
|
94
|
+
Download: 'Tải xuống',
|
|
95
|
+
Duration: 'Thời lượng',
|
|
96
|
+
'Enable captions': 'Bật phụ đề',
|
|
97
|
+
Enabled: 'Đã bật',
|
|
98
|
+
End: 'Kết thúc',
|
|
99
|
+
'Enter Fullscreen': 'Vào toàn màn hình',
|
|
100
|
+
'Exit Fullscreen': 'Thoát toàn màn hình',
|
|
101
|
+
Forward: 'Tua tới',
|
|
102
|
+
'Go back to previous menu': 'Quay lại menu trước',
|
|
103
|
+
LIVE: 'TRỰC TIẾP',
|
|
104
|
+
Loop: 'Lặp lại',
|
|
105
|
+
Mute: 'Tắt tiếng',
|
|
106
|
+
Normal: 'Bình thường',
|
|
107
|
+
Pause: 'Tạm dừng',
|
|
108
|
+
'Enter PiP': 'Vào chế độ nổi',
|
|
109
|
+
'Exit PiP': 'Thoát chế độ nổi',
|
|
110
|
+
Play: 'Phát',
|
|
111
|
+
Played: 'Đã phát',
|
|
112
|
+
Quality: 'Chất lượng',
|
|
113
|
+
Reset: 'Đặt lại',
|
|
114
|
+
Restart: 'Phát lại từ đầu',
|
|
115
|
+
Rewind: 'Tua lùi',
|
|
116
|
+
Seek: 'Tua',
|
|
117
|
+
Settings: 'Cài đặt',
|
|
118
|
+
Speed: 'Tốc độ',
|
|
119
|
+
Start: 'Bắt đầu',
|
|
120
|
+
Unmute: 'Bật tiếng',
|
|
121
|
+
Volume: 'Âm lượng',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function mapSourceToProviderProps(source) {
|
|
125
|
+
switch (source.kind) {
|
|
126
|
+
case 'video':
|
|
127
|
+
return { src: source.src, type: 'video/mp4' };
|
|
128
|
+
case 'audio':
|
|
129
|
+
return { src: source.src, type: 'audio/mpeg' };
|
|
130
|
+
case 'hls':
|
|
131
|
+
return { src: source.src, type: 'application/x-mpegurl' };
|
|
132
|
+
case 'dash':
|
|
133
|
+
return { src: source.src, type: 'application/dash+xml' };
|
|
134
|
+
case 'youtube':
|
|
135
|
+
return { src: source.src, type: 'video/youtube' };
|
|
136
|
+
case 'vimeo':
|
|
137
|
+
return { src: source.src, type: 'video/vimeo' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Vidstack's own thumbnail-storyboard loader only resolves relative sprite paths inside a VTT
|
|
142
|
+
* file against the VTT's own URL when that URL is itself absolute (`^https?:`); a root-relative
|
|
143
|
+
* path like `/assets/foo.vtt` makes it silently fall back to resolving against the current page
|
|
144
|
+
* URL instead, breaking the sprite image path. Resolving to an absolute URL up front avoids that.
|
|
145
|
+
*/
|
|
146
|
+
function resolveAbsoluteUrl(url) {
|
|
147
|
+
if (!url) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return new URL(url, document.baseURI).href;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return url;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Assigns non-`undefined` entries of `source` onto `target`, leaving its own defaults for omitted keys. */
|
|
158
|
+
function assignDefinedProperties(target, source) {
|
|
159
|
+
for (const [key, value] of Object.entries(source)) {
|
|
160
|
+
if (value !== undefined) {
|
|
161
|
+
target[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function buildStateChange(event, state, activeSourceLabel) {
|
|
166
|
+
const bufferedEnd = typeof state.bufferedEnd === 'number'
|
|
167
|
+
? state.bufferedEnd
|
|
168
|
+
: state.buffered && state.buffered.length > 0
|
|
169
|
+
? state.buffered.end(state.buffered.length - 1)
|
|
170
|
+
: 0;
|
|
171
|
+
const snapshot = {
|
|
172
|
+
paused: state.paused,
|
|
173
|
+
playing: state.playing,
|
|
174
|
+
ended: state.ended,
|
|
175
|
+
waiting: state.waiting,
|
|
176
|
+
currentTime: state.currentTime,
|
|
177
|
+
duration: state.duration,
|
|
178
|
+
volume: state.volume,
|
|
179
|
+
muted: state.muted,
|
|
180
|
+
playbackRate: state.playbackRate,
|
|
181
|
+
bufferedRatio: state.duration > 0 ? Math.min(1, bufferedEnd / state.duration) : 0,
|
|
182
|
+
fullscreen: state.fullscreen,
|
|
183
|
+
theaterMode: false,
|
|
184
|
+
pictureInPicture: state.pictureInPicture,
|
|
185
|
+
activeSourceLabel,
|
|
186
|
+
activeCaptionsLabel: state.textTrack?.label ?? null,
|
|
187
|
+
};
|
|
188
|
+
return { event, state: snapshot };
|
|
189
|
+
}
|
|
30
190
|
|
|
31
191
|
const zMediaPlayerVariants = cva('relative overflow-hidden bg-black select-none transition-all duration-300 w-full flex flex-col justify-center items-center group/player outline-none', {
|
|
32
192
|
variants: {
|
|
33
193
|
zLayout: {
|
|
34
194
|
default: 'aspect-video rounded-sm shadow-sm border border-border max-w-5xl mx-auto',
|
|
35
|
-
theater: 'w-full aspect-[21/9] max-w-full',
|
|
36
|
-
fullscreen: 'fixed inset-0 w-screen h-screen z-[9999] rounded-none max-w-full aspect-none',
|
|
37
195
|
},
|
|
38
196
|
},
|
|
39
197
|
defaultVariants: {
|
|
@@ -41,978 +199,210 @@ const zMediaPlayerVariants = cva('relative overflow-hidden bg-black select-none
|
|
|
41
199
|
},
|
|
42
200
|
});
|
|
43
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Video, audio, HLS, DASH, YouTube, and Vimeo use Vidstack's Default or Plyr Layout skin
|
|
204
|
+
* (`zSkin`). `zConfig` mirrors Vidstack's own `<media-player>` props; `zDefaultLayoutConfig` /
|
|
205
|
+
* `zPlyrLayoutConfig` mirror the props of the currently active skin.
|
|
206
|
+
*/
|
|
44
207
|
class ZMediaPlayerComponent {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
_sanitizer = inject(DomSanitizer);
|
|
48
|
-
// Inputs
|
|
208
|
+
_themeService = inject(ZThemeService, { optional: true });
|
|
209
|
+
_translateService = inject(ZTranslateService, { optional: true });
|
|
49
210
|
class = input('', /* @ts-ignore */
|
|
50
211
|
...(ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []));
|
|
51
|
-
|
|
52
|
-
...(ngDevMode ? [{ debugName: "zSrc" }] : /* istanbul ignore next */ []));
|
|
53
|
-
zSources = input([], /* @ts-ignore */
|
|
212
|
+
zSources = input.required(/* @ts-ignore */
|
|
54
213
|
...(ngDevMode ? [{ debugName: "zSources" }] : /* istanbul ignore next */ []));
|
|
55
|
-
|
|
56
|
-
...(ngDevMode ? [{ debugName: "
|
|
57
|
-
|
|
58
|
-
...(ngDevMode ? [{ debugName: "
|
|
59
|
-
|
|
60
|
-
...(ngDevMode ? [{ debugName: "
|
|
61
|
-
|
|
62
|
-
...(ngDevMode ? [{ debugName: "
|
|
63
|
-
|
|
64
|
-
...(ngDevMode ? [{ debugName: "zVolume" }] : /* istanbul ignore next */ []));
|
|
65
|
-
zSubtitles = input([], /* @ts-ignore */
|
|
66
|
-
...(ngDevMode ? [{ debugName: "zSubtitles" }] : /* istanbul ignore next */ []));
|
|
67
|
-
/** URL to a WebVTT storyboard file powering the YouTube-style timeline hover preview. */
|
|
68
|
-
zThumbnailsVtt = input('', /* @ts-ignore */
|
|
69
|
-
...(ngDevMode ? [{ debugName: "zThumbnailsVtt" }] : /* istanbul ignore next */ []));
|
|
70
|
-
/** Toggles which control bar groups render. Unset groups default to visible. */
|
|
71
|
-
zControls = input({}, /* @ts-ignore */
|
|
72
|
-
...(ngDevMode ? [{ debugName: "zControls" }] : /* istanbul ignore next */ []));
|
|
73
|
-
// Outputs
|
|
74
|
-
zLoaded = output();
|
|
214
|
+
zConfig = input({}, /* @ts-ignore */
|
|
215
|
+
...(ngDevMode ? [{ debugName: "zConfig" }] : /* istanbul ignore next */ []));
|
|
216
|
+
zSkin = input('default', /* @ts-ignore */
|
|
217
|
+
...(ngDevMode ? [{ debugName: "zSkin" }] : /* istanbul ignore next */ []));
|
|
218
|
+
zDefaultLayoutConfig = input({}, /* @ts-ignore */
|
|
219
|
+
...(ngDevMode ? [{ debugName: "zDefaultLayoutConfig" }] : /* istanbul ignore next */ []));
|
|
220
|
+
zPlyrLayoutConfig = input({}, /* @ts-ignore */
|
|
221
|
+
...(ngDevMode ? [{ debugName: "zPlyrLayoutConfig" }] : /* istanbul ignore next */ []));
|
|
222
|
+
zStateChange = output();
|
|
75
223
|
zError = output();
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Element Refs sử dụng signal viewChild thay vì decorator
|
|
84
|
-
videoElement = viewChild('videoElement', /* @ts-ignore */
|
|
85
|
-
...(ngDevMode ? [{ debugName: "videoElement" }] : /* istanbul ignore next */ []));
|
|
86
|
-
timelineContainer = viewChild('timelineContainer', /* @ts-ignore */
|
|
87
|
-
...(ngDevMode ? [{ debugName: "timelineContainer" }] : /* istanbul ignore next */ []));
|
|
88
|
-
_settingsPopover = viewChild(ZPopoverDirective, /* @ts-ignore */
|
|
89
|
-
...(ngDevMode ? [{ debugName: "_settingsPopover" }] : /* istanbul ignore next */ []));
|
|
90
|
-
// Component Signals trạng thái trình phát
|
|
91
|
-
isPlaying = signal(false, /* @ts-ignore */
|
|
92
|
-
...(ngDevMode ? [{ debugName: "isPlaying" }] : /* istanbul ignore next */ []));
|
|
93
|
-
currentTime = signal(0, /* @ts-ignore */
|
|
94
|
-
...(ngDevMode ? [{ debugName: "currentTime" }] : /* istanbul ignore next */ []));
|
|
95
|
-
duration = signal(0, /* @ts-ignore */
|
|
96
|
-
...(ngDevMode ? [{ debugName: "duration" }] : /* istanbul ignore next */ []));
|
|
97
|
-
volume = signal(1.0, /* @ts-ignore */
|
|
98
|
-
...(ngDevMode ? [{ debugName: "volume" }] : /* istanbul ignore next */ []));
|
|
99
|
-
isMuted = signal(false, /* @ts-ignore */
|
|
100
|
-
...(ngDevMode ? [{ debugName: "isMuted" }] : /* istanbul ignore next */ []));
|
|
101
|
-
playbackSpeed = signal(1.0, /* @ts-ignore */
|
|
102
|
-
...(ngDevMode ? [{ debugName: "playbackSpeed" }] : /* istanbul ignore next */ []));
|
|
103
|
-
activeSourceSrc = signal(null, /* @ts-ignore */
|
|
104
|
-
...(ngDevMode ? [{ debugName: "activeSourceSrc" }] : /* istanbul ignore next */ []));
|
|
105
|
-
_activeQualitySourceLabel = signal(null, /* @ts-ignore */
|
|
106
|
-
...(ngDevMode ? [{ debugName: "_activeQualitySourceLabel" }] : /* istanbul ignore next */ []));
|
|
107
|
-
showControls = signal(true, /* @ts-ignore */
|
|
108
|
-
...(ngDevMode ? [{ debugName: "showControls" }] : /* istanbul ignore next */ []));
|
|
109
|
-
isPointerOver = signal(false, /* @ts-ignore */
|
|
110
|
-
...(ngDevMode ? [{ debugName: "isPointerOver" }] : /* istanbul ignore next */ []));
|
|
111
|
-
isBuffering = signal(false, /* @ts-ignore */
|
|
112
|
-
...(ngDevMode ? [{ debugName: "isBuffering" }] : /* istanbul ignore next */ []));
|
|
113
|
-
captionsEnabled = signal(false, /* @ts-ignore */
|
|
114
|
-
...(ngDevMode ? [{ debugName: "captionsEnabled" }] : /* istanbul ignore next */ []));
|
|
115
|
-
subtitleText = signal('', /* @ts-ignore */
|
|
116
|
-
...(ngDevMode ? [{ debugName: "subtitleText" }] : /* istanbul ignore next */ []));
|
|
117
|
-
hasSubtitles = computed(() => this.zSubtitles().length > 0, /* @ts-ignore */
|
|
118
|
-
...(ngDevMode ? [{ debugName: "hasSubtitles" }] : /* istanbul ignore next */ []));
|
|
119
|
-
isTheaterMode = signal(false, /* @ts-ignore */
|
|
120
|
-
...(ngDevMode ? [{ debugName: "isTheaterMode" }] : /* istanbul ignore next */ []));
|
|
121
|
-
isFullscreen = signal(false, /* @ts-ignore */
|
|
122
|
-
...(ngDevMode ? [{ debugName: "isFullscreen" }] : /* istanbul ignore next */ []));
|
|
123
|
-
isMiniplayer = signal(false, /* @ts-ignore */
|
|
124
|
-
...(ngDevMode ? [{ debugName: "isMiniplayer" }] : /* istanbul ignore next */ []));
|
|
125
|
-
showSettings = signal(false, /* @ts-ignore */
|
|
126
|
-
...(ngDevMode ? [{ debugName: "showSettings" }] : /* istanbul ignore next */ []));
|
|
127
|
-
settingsPanel = signal('main', /* @ts-ignore */
|
|
128
|
-
...(ngDevMode ? [{ debugName: "settingsPanel" }] : /* istanbul ignore next */ []));
|
|
129
|
-
isDragging = signal(false, /* @ts-ignore */
|
|
130
|
-
...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
|
|
131
|
-
// Tiến trình đệm và phát
|
|
132
|
-
bufferedProgress = signal(0, /* @ts-ignore */
|
|
133
|
-
...(ngDevMode ? [{ debugName: "bufferedProgress" }] : /* istanbul ignore next */ []));
|
|
134
|
-
progress = signal(0, /* @ts-ignore */
|
|
135
|
-
...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
|
|
136
|
-
// Trạng thái visual hiển thị
|
|
137
|
-
playPauseFlash = signal(null, /* @ts-ignore */
|
|
138
|
-
...(ngDevMode ? [{ debugName: "playPauseFlash" }] : /* istanbul ignore next */ []));
|
|
139
|
-
hoverTimeLabel = signal(null, /* @ts-ignore */
|
|
140
|
-
...(ngDevMode ? [{ debugName: "hoverTimeLabel" }] : /* istanbul ignore next */ []));
|
|
141
|
-
hoverTime = signal(null, /* @ts-ignore */
|
|
142
|
-
...(ngDevMode ? [{ debugName: "hoverTime" }] : /* istanbul ignore next */ []));
|
|
143
|
-
hoverX = signal(0, /* @ts-ignore */
|
|
144
|
-
...(ngDevMode ? [{ debugName: "hoverX" }] : /* istanbul ignore next */ []));
|
|
145
|
-
// Storyboard thumbnails cho preview khi hover timeline (kiểu YouTube)
|
|
146
|
-
_thumbnailCues = signal([], /* @ts-ignore */
|
|
147
|
-
...(ngDevMode ? [{ debugName: "_thumbnailCues" }] : /* istanbul ignore next */ []));
|
|
148
|
-
_positionedSubtitleTracks = new WeakSet();
|
|
149
|
-
// Lớp phủ tua nhanh nhảy số cộng dồn kiểu YouTube
|
|
150
|
-
skipOverlayDirection = signal(null, /* @ts-ignore */
|
|
151
|
-
...(ngDevMode ? [{ debugName: "skipOverlayDirection" }] : /* istanbul ignore next */ []));
|
|
152
|
-
skipOverlayValue = signal(0, /* @ts-ignore */
|
|
153
|
-
...(ngDevMode ? [{ debugName: "skipOverlayValue" }] : /* istanbul ignore next */ []));
|
|
154
|
-
skipOverlayPulse = signal(0, /* @ts-ignore */
|
|
155
|
-
...(ngDevMode ? [{ debugName: "skipOverlayPulse" }] : /* istanbul ignore next */ []));
|
|
156
|
-
// Kiểm tra trình duyệt hỗ trợ Picture-in-Picture
|
|
157
|
-
isPiPSupported = computed(() => {
|
|
158
|
-
if (typeof document === 'undefined') {
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
const doc = document;
|
|
162
|
-
const hasPiP = doc.pictureInPictureEnabled;
|
|
163
|
-
const hasRequestPiP = typeof HTMLVideoElement !== 'undefined' && 'requestPictureInPicture' in HTMLVideoElement.prototype;
|
|
164
|
-
if (hasPiP && hasRequestPiP) {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
const video = this.videoElement()?.nativeElement;
|
|
168
|
-
if (video) {
|
|
169
|
-
const supportsPresentationMode = video.webkitSupportsPresentationMode;
|
|
170
|
-
const hasWebkit = 'webkitSupportsPresentationMode' in video &&
|
|
171
|
-
typeof supportsPresentationMode === 'function' &&
|
|
172
|
-
supportsPresentationMode.call(video, 'picture-in-picture');
|
|
173
|
-
return !!hasWebkit;
|
|
174
|
-
}
|
|
175
|
-
const hasWebkitProto = typeof HTMLVideoElement !== 'undefined' &&
|
|
176
|
-
'webkitSupportsPresentationMode' in HTMLVideoElement.prototype &&
|
|
177
|
-
'webkitSetPresentationMode' in HTMLVideoElement.prototype;
|
|
178
|
-
return hasWebkitProto;
|
|
224
|
+
playerElement = viewChild('playerElement', /* @ts-ignore */
|
|
225
|
+
...(ngDevMode ? [{ debugName: "playerElement" }] : /* istanbul ignore next */ []));
|
|
226
|
+
layoutElement = viewChild('layoutElement', /* @ts-ignore */
|
|
227
|
+
...(ngDevMode ? [{ debugName: "layoutElement" }] : /* istanbul ignore next */ []));
|
|
228
|
+
activeSource = computed(() => {
|
|
229
|
+
const sources = this.zSources();
|
|
230
|
+
return sources.find(source => source.default) ?? sources[0] ?? null;
|
|
179
231
|
}, /* @ts-ignore */
|
|
180
|
-
...(ngDevMode ? [{ debugName: "
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
];
|
|
191
|
-
// Lưu trữ Timer ẩn thanh điều khiển và điều hướng
|
|
192
|
-
_controlsTimeout = null;
|
|
193
|
-
_flashTimeout = null;
|
|
194
|
-
_skipOverlayTimeout = null;
|
|
195
|
-
_bufferAnimationFrame = null;
|
|
196
|
-
_playbackAnimationFrame = null;
|
|
197
|
-
_mouseMoveCleanup = null;
|
|
198
|
-
_lastMouseMoveAt = 0;
|
|
199
|
-
_bufferedTarget = 0;
|
|
200
|
-
_accumulatedSkip = 0;
|
|
201
|
-
// Cập nhật CVA class dựa trên các trạng thái giao diện
|
|
202
|
-
zClasses = computed(() => {
|
|
203
|
-
let layout = 'default';
|
|
204
|
-
if (this.isTheaterMode()) {
|
|
205
|
-
layout = 'theater';
|
|
206
|
-
}
|
|
207
|
-
if (this.isFullscreen()) {
|
|
208
|
-
layout = 'fullscreen';
|
|
209
|
-
}
|
|
210
|
-
return zMergeClasses(zMediaPlayerVariants({ zLayout: layout }), this.class());
|
|
232
|
+
...(ngDevMode ? [{ debugName: "activeSource" }] : /* istanbul ignore next */ []));
|
|
233
|
+
isAudioSource = computed(() => this.activeSource()?.kind === 'audio', /* @ts-ignore */
|
|
234
|
+
...(ngDevMode ? [{ debugName: "isAudioSource" }] : /* istanbul ignore next */ []));
|
|
235
|
+
isPlyrSkin = computed(() => this.zSkin() === 'plyr', /* @ts-ignore */
|
|
236
|
+
...(ngDevMode ? [{ debugName: "isPlyrSkin" }] : /* istanbul ignore next */ []));
|
|
237
|
+
colorScheme = computed(() => {
|
|
238
|
+
if (!this._themeService) {
|
|
239
|
+
return 'system';
|
|
240
|
+
}
|
|
241
|
+
return this._themeService.isDark() ? 'dark' : 'light';
|
|
211
242
|
}, /* @ts-ignore */
|
|
212
|
-
...(ngDevMode ? [{ debugName: "
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
...DEFAULT_MEDIA_PLAYER_CONTROLS,
|
|
220
|
-
...this.zControls(),
|
|
221
|
-
}), /* @ts-ignore */
|
|
222
|
-
...(ngDevMode ? [{ debugName: "controls" }] : /* istanbul ignore next */ []));
|
|
223
|
-
effectiveSrc = computed(() => this.activeSourceSrc() ?? this.zSrc(), /* @ts-ignore */
|
|
224
|
-
...(ngDevMode ? [{ debugName: "effectiveSrc" }] : /* istanbul ignore next */ []));
|
|
225
|
-
qualitySources = computed(() => this.zSources().filter(source => !!source.src), /* @ts-ignore */
|
|
226
|
-
...(ngDevMode ? [{ debugName: "qualitySources" }] : /* istanbul ignore next */ []));
|
|
227
|
-
activeQualityLabel = computed(() => this._activeQualitySourceLabel() ?? this.qualitySources()[0]?.label ?? 'Auto', /* @ts-ignore */
|
|
228
|
-
...(ngDevMode ? [{ debugName: "activeQualityLabel" }] : /* istanbul ignore next */ []));
|
|
229
|
-
// Nhận diện link YouTube để nhúng iframe gốc thay vì thẻ <video>
|
|
230
|
-
youtubeVideoId = computed(() => YOUTUBE_URL_PATTERN.exec(this.effectiveSrc())?.[1] ?? null, /* @ts-ignore */
|
|
231
|
-
...(ngDevMode ? [{ debugName: "youtubeVideoId" }] : /* istanbul ignore next */ []));
|
|
232
|
-
isYouTube = computed(() => this.youtubeVideoId() !== null, /* @ts-ignore */
|
|
233
|
-
...(ngDevMode ? [{ debugName: "isYouTube" }] : /* istanbul ignore next */ []));
|
|
234
|
-
youtubeEmbedUrl = computed(() => {
|
|
235
|
-
const id = this.youtubeVideoId();
|
|
236
|
-
if (!id) {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
const params = new URLSearchParams({
|
|
240
|
-
autoplay: this.zAutoplay() ? '1' : '0',
|
|
241
|
-
mute: this.zMuted() ? '1' : '0',
|
|
242
|
-
loop: this.zLoop() ? '1' : '0',
|
|
243
|
-
playsinline: '1',
|
|
244
|
-
rel: '0',
|
|
245
|
-
modestbranding: '1',
|
|
246
|
-
});
|
|
247
|
-
if (this.zLoop()) {
|
|
248
|
-
params.set('playlist', id);
|
|
249
|
-
}
|
|
250
|
-
return this._sanitizer.bypassSecurityTrustResourceUrl(`https://www.youtube-nocookie.com/embed/${id}?${params.toString()}`);
|
|
243
|
+
...(ngDevMode ? [{ debugName: "colorScheme" }] : /* istanbul ignore next */ []));
|
|
244
|
+
_isVietnamese = computed(() => (this._translateService?.currentLang() ?? 'en') === 'vi', /* @ts-ignore */
|
|
245
|
+
...(ngDevMode ? [{ debugName: "_isVietnamese" }] : /* istanbul ignore next */ []));
|
|
246
|
+
defaultLayoutTranslations = computed(() => {
|
|
247
|
+
const base = this._isVietnamese() ? MEDIA_PLAYER_VI_DEFAULT_TRANSLATIONS : null;
|
|
248
|
+
const override = this.zDefaultLayoutConfig().translations;
|
|
249
|
+
return base || override ? { ...base, ...override } : null;
|
|
251
250
|
}, /* @ts-ignore */
|
|
252
|
-
...(ngDevMode ? [{ debugName: "
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
251
|
+
...(ngDevMode ? [{ debugName: "defaultLayoutTranslations" }] : /* istanbul ignore next */ []));
|
|
252
|
+
plyrLayoutTranslations = computed(() => {
|
|
253
|
+
const base = this._isVietnamese() ? MEDIA_PLAYER_VI_PLYR_TRANSLATIONS : null;
|
|
254
|
+
const override = this.zPlyrLayoutConfig().translations;
|
|
255
|
+
return base || override ? { ...base, ...override } : null;
|
|
256
|
+
}, /* @ts-ignore */
|
|
257
|
+
...(ngDevMode ? [{ debugName: "plyrLayoutTranslations" }] : /* istanbul ignore next */ []));
|
|
258
|
+
state = signal({
|
|
259
|
+
paused: true,
|
|
260
|
+
playing: false,
|
|
261
|
+
ended: false,
|
|
262
|
+
waiting: false,
|
|
263
|
+
currentTime: 0,
|
|
264
|
+
duration: 0,
|
|
265
|
+
volume: DEFAULT_MEDIA_PLAYER_CONFIG.volume ?? 1,
|
|
266
|
+
muted: DEFAULT_MEDIA_PLAYER_CONFIG.muted ?? false,
|
|
267
|
+
playbackRate: DEFAULT_MEDIA_PLAYER_CONFIG.playbackRate ?? 1,
|
|
268
|
+
bufferedRatio: 0,
|
|
269
|
+
fullscreen: false,
|
|
270
|
+
theaterMode: false,
|
|
271
|
+
pictureInPicture: false,
|
|
272
|
+
activeSourceLabel: null,
|
|
273
|
+
activeCaptionsLabel: null,
|
|
261
274
|
}, /* @ts-ignore */
|
|
262
|
-
...(ngDevMode ? [{ debugName: "
|
|
275
|
+
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
276
|
+
zClasses = computed(() => zMergeClasses(zMediaPlayerVariants(), this.class()), /* @ts-ignore */
|
|
277
|
+
...(ngDevMode ? [{ debugName: "zClasses" }] : /* istanbul ignore next */ []));
|
|
263
278
|
constructor() {
|
|
264
279
|
effect(() => {
|
|
265
|
-
const
|
|
266
|
-
const sources = this.
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
280
|
+
const player = this.playerElement()?.nativeElement;
|
|
281
|
+
const sources = this.zSources();
|
|
282
|
+
const source = this.activeSource();
|
|
283
|
+
if (!player || !source || sources.length === 0) {
|
|
269
284
|
return;
|
|
270
285
|
}
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (video.src !== src) {
|
|
280
|
-
video.src = src;
|
|
281
|
-
}
|
|
282
|
-
video.load();
|
|
283
|
-
}
|
|
286
|
+
const { subtitles: _subtitles, thumbnailsVtt: _thumbnailsVtt, chaptersVtt: _chaptersVtt, ...playerProps } = {
|
|
287
|
+
...DEFAULT_MEDIA_PLAYER_CONFIG,
|
|
288
|
+
...this.zConfig(),
|
|
289
|
+
};
|
|
290
|
+
const orderedSources = [...sources].sort((left, right) => Number(Boolean(right.default)) - Number(Boolean(left.default)));
|
|
291
|
+
const providerSources = orderedSources.map(mapSourceToProviderProps);
|
|
292
|
+
player.src = providerSources.length === 1 ? providerSources[0] : providerSources;
|
|
293
|
+
assignDefinedProperties(player, playerProps);
|
|
284
294
|
});
|
|
285
295
|
effect(() => {
|
|
286
|
-
const
|
|
287
|
-
if (!
|
|
288
|
-
this._thumbnailCues.set([]);
|
|
296
|
+
const layout = this.layoutElement()?.nativeElement;
|
|
297
|
+
if (!layout) {
|
|
289
298
|
return;
|
|
290
299
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
300
|
+
const thumbnails = resolveAbsoluteUrl(this.zConfig().thumbnailsVtt);
|
|
301
|
+
if (this.isPlyrSkin()) {
|
|
302
|
+
assignDefinedProperties(layout, {
|
|
303
|
+
thumbnails,
|
|
304
|
+
translations: this.plyrLayoutTranslations(),
|
|
305
|
+
...this.zPlyrLayoutConfig(),
|
|
306
|
+
});
|
|
298
307
|
return;
|
|
299
308
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
});
|
|
306
|
-
afterNextRender(() => {
|
|
307
|
-
this._initializeVideoState();
|
|
308
|
-
this._ngZone.runOutsideAngular(() => {
|
|
309
|
-
const host = this._elementRef.nativeElement;
|
|
310
|
-
const onMouseMove = () => {
|
|
311
|
-
this.isPointerOver.set(true);
|
|
312
|
-
const now = performance.now();
|
|
313
|
-
if (this.showControls() && now - this._lastMouseMoveAt < 200) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
this._lastMouseMoveAt = now;
|
|
317
|
-
this._resetControlsTimer();
|
|
318
|
-
};
|
|
319
|
-
host.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
320
|
-
this._mouseMoveCleanup = () => host.removeEventListener('mousemove', onMouseMove);
|
|
309
|
+
assignDefinedProperties(layout, {
|
|
310
|
+
thumbnails,
|
|
311
|
+
colorScheme: this.colorScheme(),
|
|
312
|
+
translations: this.defaultLayoutTranslations(),
|
|
313
|
+
...this.zDefaultLayoutConfig(),
|
|
321
314
|
});
|
|
322
315
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (this._controlsTimeout) {
|
|
327
|
-
clearTimeout(this._controlsTimeout);
|
|
328
|
-
}
|
|
329
|
-
if (this._flashTimeout) {
|
|
330
|
-
clearTimeout(this._flashTimeout);
|
|
331
|
-
}
|
|
332
|
-
if (this._skipOverlayTimeout) {
|
|
333
|
-
clearTimeout(this._skipOverlayTimeout);
|
|
334
|
-
}
|
|
335
|
-
if (this._bufferAnimationFrame !== null) {
|
|
336
|
-
cancelAnimationFrame(this._bufferAnimationFrame);
|
|
337
|
-
}
|
|
338
|
-
if (this._playbackAnimationFrame !== null) {
|
|
339
|
-
cancelAnimationFrame(this._playbackAnimationFrame);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
// Khởi tạo trạng thái âm lượng và tắt tiếng khi bắt đầu render
|
|
343
|
-
_initializeVideoState() {
|
|
344
|
-
const video = this.videoElement()?.nativeElement;
|
|
345
|
-
if (!video) {
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
video.volume = this.zVolume();
|
|
349
|
-
this.volume.set(this.zVolume());
|
|
350
|
-
video.muted = this.zMuted();
|
|
351
|
-
this.isMuted.set(this.zMuted());
|
|
352
|
-
this._resetControlsTimer();
|
|
353
|
-
}
|
|
354
|
-
// Thay đổi trạng thái Phát / Tạm dừng
|
|
355
|
-
togglePlay() {
|
|
356
|
-
const video = this.videoElement()?.nativeElement;
|
|
357
|
-
if (!video) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (video.paused) {
|
|
361
|
-
video.play().catch(console.error);
|
|
362
|
-
this._triggerFlashIcon('play');
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
video.pause();
|
|
366
|
-
this._triggerFlashIcon('pause');
|
|
367
|
-
}
|
|
368
|
-
// Kích hoạt flash icon giữa màn hình
|
|
369
|
-
_triggerFlashIcon(type) {
|
|
370
|
-
if (this._flashTimeout) {
|
|
371
|
-
clearTimeout(this._flashTimeout);
|
|
372
|
-
}
|
|
373
|
-
this.playPauseFlash.set(type);
|
|
374
|
-
this._flashTimeout = setTimeout(() => {
|
|
375
|
-
this.playPauseFlash.set(null);
|
|
376
|
-
}, 500);
|
|
377
|
-
}
|
|
378
|
-
// Bật / tắt tiếng (Mute)
|
|
379
|
-
toggleMute() {
|
|
380
|
-
const video = this.videoElement()?.nativeElement;
|
|
381
|
-
if (!video) {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const nextMuted = !video.muted;
|
|
385
|
-
video.muted = nextMuted;
|
|
386
|
-
this.isMuted.set(nextMuted);
|
|
387
|
-
this.zVolumeChange.emit(nextMuted ? 0 : this.volume());
|
|
388
|
-
}
|
|
389
|
-
// Điều chỉnh mức âm lượng từ slider
|
|
390
|
-
onVolumeChange(val) {
|
|
391
|
-
const video = this.videoElement()?.nativeElement;
|
|
392
|
-
if (!video) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
video.volume = val;
|
|
396
|
-
this.volume.set(val);
|
|
397
|
-
const isSilent = val === 0;
|
|
398
|
-
video.muted = isSilent;
|
|
399
|
-
this.isMuted.set(isSilent);
|
|
400
|
-
this.zVolumeChange.emit(val);
|
|
401
|
-
}
|
|
402
|
-
// Đặt tốc độ phát video
|
|
403
|
-
setPlaybackSpeed(speed) {
|
|
404
|
-
const video = this.videoElement()?.nativeElement;
|
|
405
|
-
if (!video) {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
video.playbackRate = speed;
|
|
409
|
-
this.playbackSpeed.set(speed);
|
|
410
|
-
this.closeSettingsPanel();
|
|
411
|
-
}
|
|
412
|
-
openSettingsPanel(panel) {
|
|
413
|
-
this.settingsPanel.set(panel);
|
|
414
|
-
}
|
|
415
|
-
closeSettingsPanel() {
|
|
416
|
-
this.settingsPanel.set('main');
|
|
417
|
-
}
|
|
418
|
-
onSettingsHidden() {
|
|
419
|
-
this.showSettings.set(false);
|
|
420
|
-
this.closeSettingsPanel();
|
|
421
|
-
if (this._controlsTimeout) {
|
|
422
|
-
clearTimeout(this._controlsTimeout);
|
|
423
|
-
this._controlsTimeout = null;
|
|
424
|
-
}
|
|
425
|
-
if (!this.isPointerOver()) {
|
|
426
|
-
this.showControls.set(false);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
this._controlsTimeout = setTimeout(() => {
|
|
430
|
-
this.showControls.set(false);
|
|
431
|
-
this._controlsTimeout = null;
|
|
432
|
-
}, 1000);
|
|
433
|
-
}
|
|
434
|
-
toggleInlineSettings() {
|
|
435
|
-
const nextVisible = !this.showSettings();
|
|
436
|
-
this.showSettings.set(nextVisible);
|
|
437
|
-
if (!nextVisible) {
|
|
438
|
-
this.closeSettingsPanel();
|
|
439
|
-
}
|
|
440
|
-
this.onMouseMove();
|
|
441
|
-
}
|
|
442
|
-
setQualitySource(source) {
|
|
443
|
-
if (source.label === this.activeQualityLabel()) {
|
|
444
|
-
this.closeSettingsPanel();
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
if (source.src === this.effectiveSrc()) {
|
|
448
|
-
this._activeQualitySourceLabel.set(source.label);
|
|
449
|
-
this.zQualityChange.emit(source);
|
|
450
|
-
this.closeSettingsPanel();
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
const video = this.videoElement()?.nativeElement;
|
|
454
|
-
const current = video?.currentTime ?? this.currentTime();
|
|
455
|
-
const shouldResume = !!video && !video.paused;
|
|
456
|
-
const playbackRate = video?.playbackRate ?? this.playbackSpeed();
|
|
457
|
-
if (video) {
|
|
458
|
-
let didResume = false;
|
|
459
|
-
const restorePosition = () => {
|
|
460
|
-
const safeTime = Number.isFinite(video.duration) ? Math.min(current, video.duration) : current;
|
|
461
|
-
video.playbackRate = playbackRate;
|
|
462
|
-
video.currentTime = safeTime;
|
|
463
|
-
this.currentTime.set(safeTime);
|
|
464
|
-
if (shouldResume && !didResume) {
|
|
465
|
-
didResume = true;
|
|
466
|
-
video.play().catch(console.error);
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
video.addEventListener('loadedmetadata', restorePosition, { once: true });
|
|
470
|
-
video.addEventListener('loadeddata', restorePosition, { once: true });
|
|
471
|
-
}
|
|
472
|
-
this.activeSourceSrc.set(source.src);
|
|
473
|
-
this._activeQualitySourceLabel.set(source.label);
|
|
474
|
-
this._setBufferedTarget(0);
|
|
475
|
-
this.zQualityChange.emit(source);
|
|
476
|
-
this.closeSettingsPanel();
|
|
477
|
-
}
|
|
478
|
-
// Tua đến mốc thời gian chỉ định
|
|
479
|
-
seekTo(time) {
|
|
480
|
-
const video = this.videoElement()?.nativeElement;
|
|
481
|
-
if (!video) {
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
video.currentTime = time;
|
|
485
|
-
this.currentTime.set(time);
|
|
486
|
-
this._syncPlaybackProgress(time);
|
|
487
|
-
}
|
|
488
|
-
// Bật/tắt chế độ rạp phim (Theater Mode)
|
|
489
|
-
toggleTheaterMode() {
|
|
490
|
-
if (this.isFullscreen()) {
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
const nextState = !this.isTheaterMode();
|
|
494
|
-
this.isTheaterMode.set(nextState);
|
|
495
|
-
this.zTheaterModeChange.emit(nextState);
|
|
496
|
-
}
|
|
497
|
-
// Bật/tắt chế độ toàn màn hình
|
|
498
|
-
toggleFullscreen() {
|
|
499
|
-
const wrapper = this._elementRef.nativeElement;
|
|
500
|
-
if (!wrapper) {
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
if (document.fullscreenElement) {
|
|
504
|
-
document.exitFullscreen().catch(console.error);
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
wrapper.requestFullscreen().catch(console.error);
|
|
508
|
-
}
|
|
509
|
-
// Lắng nghe sự kiện thay đổi fullscreen (liên kết qua host property của component)
|
|
510
|
-
onFullscreenStateChange() {
|
|
511
|
-
const isFull = !!document.fullscreenElement;
|
|
512
|
-
this.isFullscreen.set(isFull);
|
|
513
|
-
this.onSettingsHidden();
|
|
514
|
-
this.zFullscreenChange.emit(isFull);
|
|
515
|
-
}
|
|
516
|
-
// Trình phát thu nhỏ (Picture-in-Picture)
|
|
517
|
-
async toggleMiniplayer() {
|
|
518
|
-
const video = this.videoElement()?.nativeElement;
|
|
519
|
-
if (!video) {
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
try {
|
|
523
|
-
const doc = document;
|
|
524
|
-
if (doc.pictureInPictureEnabled && typeof video.requestPictureInPicture === 'function') {
|
|
525
|
-
if (doc.pictureInPictureElement) {
|
|
526
|
-
await doc.exitPictureInPicture();
|
|
527
|
-
this.isMiniplayer.set(false);
|
|
528
|
-
this.zMiniplayerChange.emit(false);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
await video.requestPictureInPicture();
|
|
532
|
-
this.isMiniplayer.set(true);
|
|
533
|
-
this.zMiniplayerChange.emit(true);
|
|
316
|
+
effect(onCleanup => {
|
|
317
|
+
const player = this.playerElement()?.nativeElement;
|
|
318
|
+
if (!player) {
|
|
534
319
|
return;
|
|
535
320
|
}
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.isMiniplayer.set(true);
|
|
557
|
-
this.zMiniplayerChange.emit(true);
|
|
558
|
-
}
|
|
559
|
-
onLeavePiP() {
|
|
560
|
-
this.isMiniplayer.set(false);
|
|
561
|
-
this.zMiniplayerChange.emit(false);
|
|
562
|
-
}
|
|
563
|
-
onWebkitPresentationModeChanged() {
|
|
564
|
-
const video = this.videoElement()?.nativeElement;
|
|
565
|
-
if (video) {
|
|
566
|
-
const isPiP = video.webkitPresentationMode === 'picture-in-picture';
|
|
567
|
-
this.isMiniplayer.set(isPiP);
|
|
568
|
-
this.zMiniplayerChange.emit(isPiP);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// Bật/tắt hiển thị phụ đề (CC)
|
|
572
|
-
toggleSubtitles() {
|
|
573
|
-
const video = this.videoElement()?.nativeElement;
|
|
574
|
-
if (!video) {
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
const tracks = video.textTracks;
|
|
578
|
-
const currentEnabled = this.captionsEnabled();
|
|
579
|
-
const targetState = !currentEnabled;
|
|
580
|
-
this.captionsEnabled.set(targetState);
|
|
581
|
-
let i = 0;
|
|
582
|
-
while (i < tracks.length) {
|
|
583
|
-
const track = tracks[i];
|
|
584
|
-
track.mode = targetState ? 'showing' : 'disabled';
|
|
585
|
-
if (targetState) {
|
|
586
|
-
this._positionSubtitleCues(track);
|
|
587
|
-
}
|
|
588
|
-
i++;
|
|
589
|
-
}
|
|
590
|
-
if (!targetState) {
|
|
591
|
-
this.subtitleText.set('');
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
this._updateSubtitleText(tracks);
|
|
595
|
-
}
|
|
596
|
-
_positionSubtitleCues(track) {
|
|
597
|
-
if (!this._positionedSubtitleTracks.has(track) && typeof track.addEventListener === 'function') {
|
|
598
|
-
track.addEventListener('cuechange', () => {
|
|
599
|
-
this._positionSubtitleCues(track);
|
|
600
|
-
this._updateSubtitleText(this.videoElement()?.nativeElement.textTracks);
|
|
321
|
+
const stateSubscription = player.subscribe(nextState => this._syncState(nextState));
|
|
322
|
+
const eventMap = {
|
|
323
|
+
'loaded-data': 'loaded',
|
|
324
|
+
play: 'play',
|
|
325
|
+
pause: 'pause',
|
|
326
|
+
ended: 'ended',
|
|
327
|
+
'time-update': 'time-update',
|
|
328
|
+
'volume-change': 'volume-change',
|
|
329
|
+
'rate-change': 'rate-change',
|
|
330
|
+
'fullscreen-change': 'fullscreen-change',
|
|
331
|
+
'picture-in-picture-change': 'pip-change',
|
|
332
|
+
'source-change': 'source-change',
|
|
333
|
+
'text-track-change': 'subtitle-change',
|
|
334
|
+
waiting: 'waiting',
|
|
335
|
+
'can-play': 'can-play',
|
|
336
|
+
};
|
|
337
|
+
const listeners = Object.entries(eventMap).map(([eventName, event]) => {
|
|
338
|
+
const listener = () => this._emitStateChange(event);
|
|
339
|
+
player.addEventListener(eventName, listener);
|
|
340
|
+
return () => player.removeEventListener(eventName, listener);
|
|
601
341
|
});
|
|
602
|
-
this.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (typeof VTTCue !== 'undefined' && cue instanceof VTTCue) {
|
|
612
|
-
cue.line = 0;
|
|
613
|
-
}
|
|
614
|
-
i++;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
_updateSubtitleText(tracks) {
|
|
618
|
-
if (!tracks) {
|
|
619
|
-
this.subtitleText.set('');
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
let i = 0;
|
|
623
|
-
while (i < tracks.length) {
|
|
624
|
-
const cues = tracks[i].activeCues;
|
|
625
|
-
if (cues && cues.length > 0) {
|
|
626
|
-
const cue = cues[0];
|
|
627
|
-
if ('text' in cue && typeof cue.text === 'string') {
|
|
628
|
-
this.subtitleText.set(cue.text);
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
i++;
|
|
633
|
-
}
|
|
634
|
-
this.subtitleText.set('');
|
|
635
|
-
}
|
|
636
|
-
// Cập nhật trạng thái khi video phát/dừng
|
|
637
|
-
onPlayStateChange(playing) {
|
|
638
|
-
this.isPlaying.set(playing);
|
|
639
|
-
this.zPlay.emit(playing);
|
|
640
|
-
if (!playing) {
|
|
641
|
-
const video = this.videoElement()?.nativeElement;
|
|
642
|
-
this._stopPlaybackProgressLoop();
|
|
643
|
-
this._syncPlaybackProgress(video?.currentTime ?? this.currentTime());
|
|
644
|
-
this._resetControlsTimer();
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
this._startPlaybackProgressLoop();
|
|
648
|
-
this._resetControlsTimer();
|
|
649
|
-
}
|
|
650
|
-
// Cập nhật mốc thời gian phát
|
|
651
|
-
onTimeUpdate() {
|
|
652
|
-
const video = this.videoElement()?.nativeElement;
|
|
653
|
-
if (!video || this.isDragging()) {
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
this.currentTime.set(video.currentTime);
|
|
657
|
-
this.zTimeUpdate.emit(video.currentTime);
|
|
658
|
-
this._syncPlaybackProgress(video.currentTime);
|
|
659
|
-
this.updateBufferedProgress();
|
|
660
|
-
}
|
|
661
|
-
onDurationChange() {
|
|
662
|
-
const video = this.videoElement()?.nativeElement;
|
|
663
|
-
if (!video) {
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
this.duration.set(video.duration);
|
|
667
|
-
this._syncPlaybackProgress(video.currentTime);
|
|
668
|
-
this.updateBufferedProgress();
|
|
669
|
-
}
|
|
670
|
-
onLoadedData() {
|
|
671
|
-
if (this.zSubtitles().some(subtitle => subtitle.default)) {
|
|
672
|
-
this.captionsEnabled.set(true);
|
|
673
|
-
}
|
|
674
|
-
this.zLoaded.emit();
|
|
675
|
-
}
|
|
676
|
-
onVideoError() {
|
|
677
|
-
const video = this.videoElement()?.nativeElement;
|
|
678
|
-
this.zError.emit(video?.error ? new Error(video.error.message || 'Media playback error') : null);
|
|
679
|
-
}
|
|
680
|
-
onSeeking(seeking) {
|
|
681
|
-
this.isBuffering.set(seeking);
|
|
682
|
-
if (!seeking) {
|
|
683
|
-
this.updateBufferedProgress();
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
updateBufferedProgress() {
|
|
687
|
-
const video = this.videoElement()?.nativeElement;
|
|
688
|
-
if (!video || video.buffered.length === 0 || this.duration() === 0) {
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
const end = video.buffered.end(video.buffered.length - 1);
|
|
692
|
-
this._setBufferedTarget((end / this.duration()) * 100);
|
|
693
|
-
}
|
|
694
|
-
_setBufferedTarget(target) {
|
|
695
|
-
this._bufferedTarget = Math.min(100, Math.max(0, target));
|
|
696
|
-
if (typeof requestAnimationFrame === 'undefined' || this._bufferAnimationFrame !== null) {
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
const animate = () => {
|
|
700
|
-
const current = this.bufferedProgress();
|
|
701
|
-
const distance = this._bufferedTarget - current;
|
|
702
|
-
if (Math.abs(distance) < 0.1) {
|
|
703
|
-
this.bufferedProgress.set(this._bufferedTarget);
|
|
704
|
-
this._bufferAnimationFrame = null;
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
this.bufferedProgress.set(current + distance * 0.08);
|
|
708
|
-
this._bufferAnimationFrame = requestAnimationFrame(animate);
|
|
709
|
-
};
|
|
710
|
-
this._bufferAnimationFrame = requestAnimationFrame(animate);
|
|
711
|
-
}
|
|
712
|
-
_startPlaybackProgressLoop() {
|
|
713
|
-
if (typeof requestAnimationFrame === 'undefined' || this._playbackAnimationFrame !== null) {
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
const animate = () => {
|
|
717
|
-
const video = this.videoElement()?.nativeElement;
|
|
718
|
-
if (!video || video.paused || video.ended) {
|
|
719
|
-
this._playbackAnimationFrame = null;
|
|
720
|
-
this._syncPlaybackProgress(video?.currentTime ?? this.currentTime());
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
this._syncPlaybackProgress(video.currentTime);
|
|
724
|
-
this._playbackAnimationFrame = requestAnimationFrame(animate);
|
|
725
|
-
};
|
|
726
|
-
this._playbackAnimationFrame = requestAnimationFrame(animate);
|
|
727
|
-
}
|
|
728
|
-
_stopPlaybackProgressLoop() {
|
|
729
|
-
if (this._playbackAnimationFrame === null) {
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
cancelAnimationFrame(this._playbackAnimationFrame);
|
|
733
|
-
this._playbackAnimationFrame = null;
|
|
734
|
-
}
|
|
735
|
-
_syncPlaybackProgress(time) {
|
|
736
|
-
const dur = this.duration();
|
|
737
|
-
if (!Number.isFinite(dur) || dur <= 0 || !Number.isFinite(time)) {
|
|
738
|
-
this.progress.set(0);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
this.progress.set(Math.min(100, Math.max(0, (time / dur) * 100)));
|
|
742
|
-
}
|
|
743
|
-
// Bắt đầu kéo thanh timeline phát
|
|
744
|
-
onTimelineDragStart(event) {
|
|
745
|
-
event.preventDefault();
|
|
746
|
-
this.isDragging.set(true);
|
|
747
|
-
this._updateTimeFromCoords(event);
|
|
748
|
-
const moveHandler = (moveEvent) => {
|
|
749
|
-
this._updateTimeFromCoords(moveEvent);
|
|
750
|
-
};
|
|
751
|
-
const upHandler = () => {
|
|
752
|
-
this.isDragging.set(false);
|
|
753
|
-
document.removeEventListener('mousemove', moveHandler);
|
|
754
|
-
document.removeEventListener('mouseup', upHandler);
|
|
755
|
-
this._resetControlsTimer();
|
|
756
|
-
};
|
|
757
|
-
document.addEventListener('mousemove', moveHandler);
|
|
758
|
-
document.addEventListener('mouseup', upHandler);
|
|
759
|
-
}
|
|
760
|
-
onTimelineHover(event) {
|
|
761
|
-
const container = this.timelineContainer()?.nativeElement;
|
|
762
|
-
if (!container) {
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
const rect = container.getBoundingClientRect();
|
|
766
|
-
const offsetX = event.clientX - rect.left;
|
|
767
|
-
this.hoverX.set(offsetX);
|
|
768
|
-
const ratio = Math.max(0, Math.min(1, offsetX / rect.width));
|
|
769
|
-
const hoverTime = ratio * this.duration();
|
|
770
|
-
this.hoverTime.set(hoverTime);
|
|
771
|
-
this.hoverTimeLabel.set(this._formatTime(hoverTime));
|
|
772
|
-
}
|
|
773
|
-
onTimelineHoverEnd() {
|
|
774
|
-
this.hoverTime.set(null);
|
|
775
|
-
this.hoverTimeLabel.set(null);
|
|
776
|
-
}
|
|
777
|
-
// Tải và phân tích file WebVTT storyboard cho preview ảnh nhỏ khi hover timeline
|
|
778
|
-
async _loadThumbnailVtt(vttUrl) {
|
|
779
|
-
try {
|
|
780
|
-
const response = await fetch(vttUrl);
|
|
781
|
-
if (!response.ok) {
|
|
782
|
-
throw new Error(`Failed to fetch thumbnails VTT: ${response.status}`);
|
|
783
|
-
}
|
|
784
|
-
const text = await response.text();
|
|
785
|
-
// `response.url` is always absolute (resolves relative input against the document, follows redirects),
|
|
786
|
-
// unlike `vttUrl` which callers may pass as a root-relative path.
|
|
787
|
-
this._thumbnailCues.set(this._parseThumbnailVtt(text, response.url));
|
|
788
|
-
}
|
|
789
|
-
catch (err) {
|
|
790
|
-
console.warn('Unable to load media player thumbnails VTT: ', err);
|
|
791
|
-
this._thumbnailCues.set([]);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
// Phân tích nội dung VTT thành danh sách cue { start, end, src, x, y, w, h }
|
|
795
|
-
_parseThumbnailVtt(text, baseUrl) {
|
|
796
|
-
const timeRangePattern = /(\d{2}:\d{2}:\d{2}[.,]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[.,]\d{3})/;
|
|
797
|
-
const lines = text.split(/\r?\n/);
|
|
798
|
-
const cues = [];
|
|
799
|
-
for (let i = 0; i < lines.length; i++) {
|
|
800
|
-
const match = timeRangePattern.exec(lines[i]);
|
|
801
|
-
if (!match) {
|
|
802
|
-
continue;
|
|
803
|
-
}
|
|
804
|
-
const imageLine = lines[i + 1]?.trim();
|
|
805
|
-
if (!imageLine) {
|
|
806
|
-
continue;
|
|
807
|
-
}
|
|
808
|
-
const [imageRef, xywhRef] = imageLine.split('#xywh=');
|
|
809
|
-
const xywh = xywhRef?.split(',').map(Number);
|
|
810
|
-
if (!imageRef || !xywh || xywh.length !== 4 || xywh.some(Number.isNaN)) {
|
|
811
|
-
continue;
|
|
812
|
-
}
|
|
813
|
-
cues.push({
|
|
814
|
-
start: this._parseVttTimestamp(match[1]),
|
|
815
|
-
end: this._parseVttTimestamp(match[2]),
|
|
816
|
-
src: new URL(imageRef, baseUrl).href,
|
|
817
|
-
x: xywh[0],
|
|
818
|
-
y: xywh[1],
|
|
819
|
-
w: xywh[2],
|
|
820
|
-
h: xywh[3],
|
|
342
|
+
const onError = (event) => this._emitError(event);
|
|
343
|
+
const onProviderChange = (event) => this._configureStreamingProvider(event);
|
|
344
|
+
player.addEventListener('error', onError);
|
|
345
|
+
player.addEventListener('provider-change', onProviderChange);
|
|
346
|
+
onCleanup(() => {
|
|
347
|
+
stateSubscription();
|
|
348
|
+
listeners.forEach(cleanup => cleanup());
|
|
349
|
+
player.removeEventListener('error', onError);
|
|
350
|
+
player.removeEventListener('provider-change', onProviderChange);
|
|
821
351
|
});
|
|
822
|
-
}
|
|
823
|
-
return cues;
|
|
824
|
-
}
|
|
825
|
-
_parseVttTimestamp(value) {
|
|
826
|
-
const [hours, minutes, secondsAndMs] = value.split(':');
|
|
827
|
-
const [seconds, milliseconds] = secondsAndMs.split(/[.,]/);
|
|
828
|
-
return Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds) + Number(milliseconds) / 1000;
|
|
829
|
-
}
|
|
830
|
-
_updateTimeFromCoords(event) {
|
|
831
|
-
const container = this.timelineContainer()?.nativeElement;
|
|
832
|
-
if (!container) {
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
const rect = container.getBoundingClientRect();
|
|
836
|
-
const offsetX = event.clientX - rect.left;
|
|
837
|
-
const { width } = rect;
|
|
838
|
-
let ratio = offsetX / width;
|
|
839
|
-
if (ratio < 0) {
|
|
840
|
-
ratio = 0;
|
|
841
|
-
}
|
|
842
|
-
if (ratio > 1) {
|
|
843
|
-
ratio = 1;
|
|
844
|
-
}
|
|
845
|
-
const targetTime = ratio * this.duration();
|
|
846
|
-
const video = this.videoElement()?.nativeElement;
|
|
847
|
-
if (video) {
|
|
848
|
-
video.currentTime = targetTime;
|
|
849
|
-
this.currentTime.set(targetTime);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
// Tải lại bộ đếm thời gian ẩn controls
|
|
853
|
-
onMouseMove() {
|
|
854
|
-
this._resetControlsTimer();
|
|
855
|
-
}
|
|
856
|
-
onMouseLeave() {
|
|
857
|
-
this.isPointerOver.set(false);
|
|
858
|
-
const video = this.videoElement()?.nativeElement;
|
|
859
|
-
if (video && !video.paused && !this.isDragging() && !this.showSettings()) {
|
|
860
|
-
if (this._controlsTimeout) {
|
|
861
|
-
clearTimeout(this._controlsTimeout);
|
|
862
|
-
this._controlsTimeout = null;
|
|
863
|
-
}
|
|
864
|
-
this.showControls.set(false);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
_resetControlsTimer() {
|
|
868
|
-
this.showControls.set(true);
|
|
869
|
-
if (this._controlsTimeout) {
|
|
870
|
-
clearTimeout(this._controlsTimeout);
|
|
871
|
-
}
|
|
872
|
-
const video = this.videoElement()?.nativeElement;
|
|
873
|
-
if (video && !video.paused && !this.showSettings() && !this.isDragging()) {
|
|
874
|
-
this._controlsTimeout = setTimeout(() => {
|
|
875
|
-
this.showControls.set(false);
|
|
876
|
-
}, 1000);
|
|
877
|
-
}
|
|
352
|
+
});
|
|
878
353
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (this.showSettings()) {
|
|
883
|
-
this._settingsPopover()?.hide();
|
|
884
|
-
this.onSettingsHidden();
|
|
354
|
+
_syncState(nextState) {
|
|
355
|
+
const player = this.playerElement()?.nativeElement;
|
|
356
|
+
if (!player) {
|
|
885
357
|
return;
|
|
886
358
|
}
|
|
887
|
-
|
|
359
|
+
const patch = nextState && typeof nextState === 'object' ? nextState : {};
|
|
360
|
+
const state = { ...player.state, ...patch };
|
|
361
|
+
this.state.set(buildStateChange('time-update', state, this.activeSource()?.label ?? null).state);
|
|
888
362
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (!video) {
|
|
363
|
+
_emitStateChange(event) {
|
|
364
|
+
const player = this.playerElement()?.nativeElement;
|
|
365
|
+
if (!player) {
|
|
893
366
|
return;
|
|
894
367
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
if (direction === 'right') {
|
|
899
|
-
this.seekTo(Math.min(this.duration(), video.currentTime + seconds));
|
|
900
|
-
}
|
|
901
|
-
this._triggerSkipOverlay(seconds, direction);
|
|
368
|
+
const change = buildStateChange(event, player.state, this.activeSource()?.label ?? null);
|
|
369
|
+
this.state.set(change.state);
|
|
370
|
+
this.zStateChange.emit(change);
|
|
902
371
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
this.skipOverlayDirection.set(null);
|
|
913
|
-
this.skipOverlayValue.set(0);
|
|
914
|
-
this._accumulatedSkip = 0;
|
|
915
|
-
}, 800);
|
|
372
|
+
_emitError(event) {
|
|
373
|
+
const { detail } = event;
|
|
374
|
+
const error = detail && typeof detail === 'object' ? detail : {};
|
|
375
|
+
this.zError.emit({
|
|
376
|
+
message: typeof error['message'] === 'string' ? error['message'] : 'Media playback error',
|
|
377
|
+
code: typeof error['code'] === 'string' || typeof error['code'] === 'number' ? error['code'] : undefined,
|
|
378
|
+
source: 'provider',
|
|
379
|
+
raw: detail,
|
|
380
|
+
});
|
|
916
381
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const
|
|
920
|
-
if (
|
|
921
|
-
(activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.hasAttribute('contenteditable'))) {
|
|
382
|
+
_configureStreamingProvider(event) {
|
|
383
|
+
const source = this.activeSource();
|
|
384
|
+
const { detail } = event;
|
|
385
|
+
if (!source || !detail || typeof detail !== 'object') {
|
|
922
386
|
return;
|
|
923
387
|
}
|
|
924
|
-
const
|
|
925
|
-
if (
|
|
388
|
+
const provider = detail;
|
|
389
|
+
if (source.kind === 'hls') {
|
|
390
|
+
provider.library = () => import('hls.js');
|
|
926
391
|
return;
|
|
927
392
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
case ' ':
|
|
931
|
-
event.preventDefault();
|
|
932
|
-
this.togglePlay();
|
|
933
|
-
return;
|
|
934
|
-
case 'k':
|
|
935
|
-
this.togglePlay();
|
|
936
|
-
return;
|
|
937
|
-
case 'm':
|
|
938
|
-
this.toggleMute();
|
|
939
|
-
return;
|
|
940
|
-
case 'f':
|
|
941
|
-
this.toggleFullscreen();
|
|
942
|
-
return;
|
|
943
|
-
case 't':
|
|
944
|
-
this.toggleTheaterMode();
|
|
945
|
-
return;
|
|
946
|
-
case 'i':
|
|
947
|
-
void this.toggleMiniplayer();
|
|
948
|
-
return;
|
|
949
|
-
case 'arrowleft':
|
|
950
|
-
event.preventDefault();
|
|
951
|
-
this.triggerSkip(5, 'left');
|
|
952
|
-
return;
|
|
953
|
-
case 'j':
|
|
954
|
-
this.triggerSkip(10, 'left');
|
|
955
|
-
return;
|
|
956
|
-
case 'arrowright':
|
|
957
|
-
event.preventDefault();
|
|
958
|
-
this.triggerSkip(5, 'right');
|
|
959
|
-
return;
|
|
960
|
-
case 'l':
|
|
961
|
-
this.triggerSkip(10, 'right');
|
|
962
|
-
return;
|
|
963
|
-
case 'arrowup':
|
|
964
|
-
event.preventDefault();
|
|
965
|
-
this.onVolumeChange(Math.min(1.0, this.volume() + 0.05));
|
|
966
|
-
return;
|
|
967
|
-
case 'arrowdown':
|
|
968
|
-
event.preventDefault();
|
|
969
|
-
this.onVolumeChange(Math.max(0, this.volume() - 0.05));
|
|
970
|
-
return;
|
|
971
|
-
case '0':
|
|
972
|
-
case '1':
|
|
973
|
-
case '2':
|
|
974
|
-
case '3':
|
|
975
|
-
case '4':
|
|
976
|
-
case '5':
|
|
977
|
-
case '6':
|
|
978
|
-
case '7':
|
|
979
|
-
case '8':
|
|
980
|
-
case '9':
|
|
981
|
-
this.seekTo((Number(key) / 10) * this.duration());
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
toggleSettings() {
|
|
986
|
-
this.showSettings.set(!this.showSettings());
|
|
987
|
-
}
|
|
988
|
-
// Định dạng hiển thị m:ss / h:mm:ss
|
|
989
|
-
_formatTime(time) {
|
|
990
|
-
if (Number.isNaN(time) || time === Infinity || time < 0) {
|
|
991
|
-
return '0:00';
|
|
992
|
-
}
|
|
993
|
-
const hours = Math.floor(time / 3600);
|
|
994
|
-
const minutes = Math.floor((time % 3600) / 60);
|
|
995
|
-
const seconds = Math.floor(time % 60);
|
|
996
|
-
const formattedSeconds = seconds < 10 ? `0${seconds}` : seconds;
|
|
997
|
-
if (hours > 0) {
|
|
998
|
-
const formattedMinutes = minutes < 10 ? `0${minutes}` : minutes;
|
|
999
|
-
return `${hours}:${formattedMinutes}:${formattedSeconds}`;
|
|
393
|
+
if (source.kind === 'dash') {
|
|
394
|
+
provider.library = () => import('dashjs');
|
|
1000
395
|
}
|
|
1001
|
-
return `${minutes}:${formattedSeconds}`;
|
|
1002
396
|
}
|
|
1003
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.
|
|
1004
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: ZMediaPlayerComponent, isStandalone: true, selector: "z-media-player", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, zSrc: { classPropertyName: "zSrc", publicName: "zSrc", isSignal: true, isRequired: true, transformFunction: null }, zSources: { classPropertyName: "zSources", publicName: "zSources", isSignal: true, isRequired: false, transformFunction: null }, zPoster: { classPropertyName: "zPoster", publicName: "zPoster", isSignal: true, isRequired: false, transformFunction: null }, zAutoplay: { classPropertyName: "zAutoplay", publicName: "zAutoplay", isSignal: true, isRequired: false, transformFunction: null }, zLoop: { classPropertyName: "zLoop", publicName: "zLoop", isSignal: true, isRequired: false, transformFunction: null }, zMuted: { classPropertyName: "zMuted", publicName: "zMuted", isSignal: true, isRequired: false, transformFunction: null }, zVolume: { classPropertyName: "zVolume", publicName: "zVolume", isSignal: true, isRequired: false, transformFunction: null }, zSubtitles: { classPropertyName: "zSubtitles", publicName: "zSubtitles", isSignal: true, isRequired: false, transformFunction: null }, zThumbnailsVtt: { classPropertyName: "zThumbnailsVtt", publicName: "zThumbnailsVtt", isSignal: true, isRequired: false, transformFunction: null }, zControls: { classPropertyName: "zControls", publicName: "zControls", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { zLoaded: "zLoaded", zError: "zError", zPlay: "zPlay", zTimeUpdate: "zTimeUpdate", zVolumeChange: "zVolumeChange", zFullscreenChange: "zFullscreenChange", zTheaterModeChange: "zTheaterModeChange", zMiniplayerChange: "zMiniplayerChange", zQualityChange: "zQualityChange" }, host: { listeners: { "keydown": "onKeyDown($event)", "mouseleave": "onMouseLeave()", "document:fullscreenchange": "onFullscreenStateChange()" }, properties: { "class": "zClasses()", "attr.tabindex": "0" } }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoElement"], descendants: true, isSignal: true }, { propertyName: "timelineContainer", first: true, predicate: ["timelineContainer"], descendants: true, isSignal: true }, { propertyName: "_settingsPopover", first: true, predicate: ZPopoverDirective, descendants: true, isSignal: true }], ngImport: i0, template: "@if (isYouTube()) {\n <!-- Nh\u00FAng iframe YouTube g\u1ED1c, d\u00F9ng control m\u1EB7c \u0111\u1ECBnh c\u1EE7a YouTube -->\n <iframe\n class=\"h-full w-full\"\n [src]=\"youtubeEmbedUrl()\"\n title=\"YouTube video player\"\n frameborder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowfullscreen></iframe>\n} @else {\n <!-- Th\u1EBB video ch\u00EDnh ph\u00E1t ngu\u1ED3n zSrc -->\n <video\n #videoElement\n class=\"h-full w-full cursor-pointer object-cover\"\n [src]=\"effectiveSrc()\"\n [poster]=\"zPoster()\"\n [loop]=\"zLoop()\"\n [autoplay]=\"zAutoplay()\"\n playsinline\n webkit-playsinline\n (click)=\"onVideoClick()\"\n (play)=\"onPlayStateChange(true)\"\n (pause)=\"onPlayStateChange(false)\"\n (loadeddata)=\"onLoadedData()\"\n (error)=\"onVideoError()\"\n (timeupdate)=\"onTimeUpdate()\"\n (durationchange)=\"onDurationChange()\"\n (seeking)=\"onSeeking(true)\"\n (seeked)=\"onSeeking(false)\"\n (waiting)=\"onSeeking(true)\"\n (playing)=\"onSeeking(false)\"\n (enterpictureinpicture)=\"onEnterPiP()\"\n (leavepictureinpicture)=\"onLeavePiP()\"\n (webkitpresentationmodechanged)=\"onWebkitPresentationModeChanged()\">\n @for (sub of zSubtitles(); track sub.src) {\n <track\n [src]=\"sub.src\"\n kind=\"subtitles\"\n [srclang]=\"sub.srclang\"\n [label]=\"sub.label\"\n [default]=\"sub.default || false\" />\n }\n </video>\n\n @if (subtitleText()) {\n <div\n class=\"pointer-events-none absolute inset-x-3 z-[11] mx-auto w-fit max-w-[calc(100%-1.5rem)] rounded-sm bg-black/80 px-4 py-2 text-center text-base leading-6 whitespace-pre-line text-white shadow-sm transition-[bottom] duration-200 ease-out sm:text-lg sm:leading-7\"\n [class.bottom-16]=\"showControls()\"\n [class.bottom-2]=\"!showControls()\">\n {{ subtitleText() }}\n </div>\n }\n\n <!-- V\u00F2ng tr\u00F2n loading gi\u1EEFa m\u00E0n h\u00ECnh khi video \u0111ang buffer -->\n @if (isBuffering()) {\n <div class=\"pointer-events-none absolute inset-0 z-[13] flex items-center justify-center\">\n <span class=\"z-media-player-loading-pulse\"></span>\n </div>\n }\n\n <!-- Hi\u1EC7u \u1EE9ng nh\u1EA5p nh\u00E1y Play/Pause l\u1EDBn gi\u1EEFa m\u00E0n h\u00ECnh t\u01B0\u01A1ng t\u1EF1 YouTube -->\n <div class=\"pointer-events-none absolute inset-0 z-[11] flex items-center justify-center\">\n <div\n class=\"z-flash-animation flex size-10 scale-75 items-center justify-center rounded-full bg-black/60 text-white opacity-0\"\n [class.z-flash-active]=\"!!playPauseFlash()\">\n @if (playPauseFlash(); as flashType) {\n <z-icon\n [zType]=\"flashType === 'play' ? 'tablerPlayerPlayFill' : 'tablerPlayerPauseFill'\"\n zSize=\"28\"\n [zStrokeWidth]=\"2\"\n [class.translate-x-[2px]]=\"flashType === 'play'\" />\n }\n </div>\n </div>\n\n <!-- L\u1EDBp ph\u1EE7 tua nhanh nh\u1EA3y s\u1ED1 c\u1ED9ng d\u1ED3n Tr\u00E1i (YouTube style) -->\n @if (skipOverlayValue() > 0 && skipOverlayDirection() === 'left') {\n <div class=\"z-double-tap-ripple-left\" [class.z-skip-pulse-alt]=\"skipOverlayPulse() % 2 === 1\">\n <div class=\"z-skip-overlay-content\">\n <span class=\"z-skip-overlay-number\">- {{ skipOverlayValue() }}</span>\n <span class=\"z-skip-overlay-arrows z-chevron-slide-left\">\n <z-icon zType=\"tablerChevronsLeft\" zSize=\"26\" [zStrokeWidth]=\"2.4\" />\n </span>\n </div>\n </div>\n }\n\n <!-- L\u1EDBp ph\u1EE7 tua nhanh nh\u1EA3y s\u1ED1 c\u1ED9ng d\u1ED3n Ph\u1EA3i (YouTube style) -->\n @if (skipOverlayValue() > 0 && skipOverlayDirection() === 'right') {\n <div class=\"z-double-tap-ripple-right\" [class.z-skip-pulse-alt]=\"skipOverlayPulse() % 2 === 1\">\n <div class=\"z-skip-overlay-content\">\n <span class=\"z-skip-overlay-arrows z-chevron-slide-right\">\n <z-icon zType=\"tablerChevronsRight\" zSize=\"26\" [zStrokeWidth]=\"2.4\" />\n </span>\n <span class=\"z-skip-overlay-number\">+ {{ skipOverlayValue() }}</span>\n </div>\n </div>\n }\n\n <!-- L\u1EDBp ph\u1EE7 gradient ph\u1EE7 s\u00E1t to\u00E0n b\u1ED9 m\u00E9p d\u01B0\u1EDBi video -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 z-[10] h-28 bg-gradient-to-t from-black/55 via-black/25 to-transparent transition-opacity duration-200 ease-out\"\n [class.opacity-0]=\"!showControls()\"></div>\n\n <!-- Thanh \u0111i\u1EC1u khi\u1EC3n d\u1EA1ng rounded-sm n\u1ED5i ph\u00EDa tr\u00EAn \u0111\u00E1y video -->\n <div\n class=\"z-media-player-controls absolute inset-x-2 bottom-2 z-[12] transition-opacity duration-200 ease-out select-none sm:inset-x-3 sm:bottom-3\"\n [class.opacity-0]=\"!showControls()\"\n [class.pointer-events-auto]=\"showControls()\"\n [class.pointer-events-none]=\"!showControls()\">\n <div class=\"z-media-player-controls-surface relative z-[1] rounded-sm px-2 py-1.5 text-sm text-white\">\n <div class=\"flex items-center gap-1.5 sm:gap-2\">\n <!-- N\u00FAt Ph\u00E1t / T\u1EA1m d\u1EEBng -->\n @if (controls().play) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_play_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [title]=\"(isPlaying() ? 'i18n_z_ui_media_player_pause' : 'i18n_z_ui_media_player_play') | translate\"\n (click)=\"togglePlay()\">\n <z-icon\n [zType]=\"isPlaying() ? 'tablerPlayerPauseFill' : 'tablerPlayerPlayFill'\"\n zSize=\"18\"\n [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- N\u00FAt Tua l\u00F9i / Tua ti\u1EBFn 5 gi\u00E2y -->\n @if (controls().skip) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_rewind_5_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 sm:flex\"\n title=\"Tua l\u00F9i 5 gi\u00E2y (Arrow Left)\"\n (click)=\"triggerSkip(5, 'left')\">\n <z-icon zType=\"tablerRewindBackward5\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_forward_5_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 sm:flex\"\n title=\"Tua ti\u1EBFn 5 gi\u00E2y (Arrow Right)\"\n (click)=\"triggerSkip(5, 'right')\">\n <z-icon zType=\"tablerRewindForward5\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- T\u1ED5 h\u1EE3p n\u00FAt Mute v\u00E0 Slider \u00E2m l\u01B0\u1EE3ng tr\u01B0\u1EE3t ngang -->\n @if (controls().volume) {\n <div class=\"group/volume hidden shrink-0 items-center sm:flex\">\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_mute_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [title]=\"(isMuted() ? 'i18n_z_ui_media_player_unmute' : 'i18n_z_ui_media_player_mute') | translate\"\n (click)=\"toggleMute()\">\n <z-icon\n [zType]=\"isMuted() ? 'tablerVolumeOff' : volume() < 0.5 ? 'tablerVolume2' : 'tablerVolume'\"\n zSize=\"18\"\n [zStrokeWidth]=\"2\" />\n </button>\n <!-- Thanh tr\u01B0\u1EE3t \u00E2m l\u01B0\u1EE3ng tr\u01B0\u1EE3t ngang m\u1EDF r\u1ED9ng khi hover -->\n <input\n type=\"range\"\n id=\"z_media_player_volume_slider\"\n min=\"0\"\n max=\"1\"\n step=\"0.05\"\n [ngModel]=\"volume()\"\n (ngModelChange)=\"onVolumeChange($event)\"\n class=\"accent-primary h-1 w-0 cursor-pointer rounded-lg bg-white/20 opacity-0 transition-all duration-200 outline-none group-hover/volume:w-16 group-hover/volume:opacity-100 focus-visible:w-16 focus-visible:opacity-100\" />\n </div>\n }\n\n <!-- Th\u1EDDi gian ph\u00E1t: g\u1ED9p chung \"\u0111\u00E3 ph\u00E1t / t\u1ED5ng th\u1EDDi l\u01B0\u1EE3ng\" th\u00E0nh m\u1ED9t nh\u00F3m duy nh\u1EA5t -->\n @if (controls().time) {\n <span class=\"shrink-0 rounded-full bg-black/30 px-2 py-0.5 font-mono text-xs whitespace-nowrap text-white/90\">\n {{ currentTimeLabel() }}\n <span class=\"text-white/50\">/</span>\n {{ durationLabel() }}\n </span>\n }\n\n <!-- Thanh Timeline (Progress Bar) t\u00F9y ch\u1EC9nh ki\u1EC3u YouTube, n\u1EB1m chung h\u00E0ng v\u1EDBi c\u00E1c n\u00FAt -->\n @if (controls().timeline) {\n <div\n #timelineContainer\n id=\"z_media_player_timeline\"\n class=\"group/timeline relative flex h-1 min-w-0 flex-1 cursor-pointer items-center\"\n (mousedown)=\"onTimelineDragStart($event)\"\n (mousemove)=\"onTimelineHover($event)\"\n (mouseleave)=\"onTimelineHoverEnd()\">\n <!-- Thanh n\u1EC1n x\u00E1m c\u1EE7a thanh timeline -->\n <div\n class=\"absolute inset-x-0 h-1 overflow-hidden rounded-full bg-white/25 transition-all group-hover/timeline:h-1.5\">\n <!-- Ti\u1EBFn tr\u00ECnh t\u1EA3i buffer c\u1EE7a video -->\n <div\n class=\"absolute top-0 left-0 h-full rounded-full bg-white/30\"\n [style.width.%]=\"bufferedProgress()\"></div>\n <!-- Ti\u1EBFn tr\u00ECnh ph\u00E1t hi\u1EC7n t\u1EA1i c\u1EE7a video -->\n <div class=\"bg-primary absolute top-0 left-0 h-full rounded-full\" [style.width.%]=\"progress()\"></div>\n </div>\n\n <!-- \u0110\u1EA7u k\u00E9o timeline (Thumb) \u0111\u1ECF ch\u1EC9 hi\u1EC3n th\u1ECB khi hover ho\u1EB7c k\u00E9o -->\n <div\n class=\"bg-primary pointer-events-none absolute size-3.5 -translate-x-1/2 scale-0 transform rounded-full opacity-0 transition-opacity duration-150 group-hover/timeline:scale-100 group-hover/timeline:opacity-100\"\n [style.left.%]=\"progress()\"></div>\n\n <!-- Preview \u1EA3nh nh\u1ECF storyboard khi hover timeline (ki\u1EC3u YouTube) -->\n @if (hoverThumbnailCue(); as cue) {\n <div\n class=\"pointer-events-none absolute z-[20] -translate-x-1/2 overflow-hidden rounded-md border-0 bg-black shadow-[0_12px_24px_-10px_rgba(0,0,0,0.85)]\"\n [style.left.px]=\"hoverX()\"\n [style.bottom.px]=\"24\"\n [style.width.px]=\"cue.w\"\n [style.height.px]=\"cue.h\">\n <div\n class=\"bg-no-repeat\"\n [style.width.px]=\"cue.w\"\n [style.height.px]=\"cue.h\"\n [style.background-image]=\"'url(' + cue.src + ')'\"\n [style.background-position]=\"'-' + cue.x + 'px -' + cue.y + 'px'\"></div>\n </div>\n }\n\n <!-- Tooltip hi\u1EC3n th\u1ECB m\u1ED1c th\u1EDDi gian khi hover -->\n @if (hoverTimeLabel()) {\n <div\n class=\"pointer-events-none absolute bottom-4 z-[20] -translate-x-1/2 rounded-xs bg-zinc-950/70 px-2 py-1 font-mono text-[11px] leading-none whitespace-nowrap text-zinc-100 shadow-[0_12px_24px_-10px_rgba(0,0,0,0.85)] backdrop-blur-2xl\"\n [style.left.px]=\"hoverX()\">\n {{ hoverTimeLabel() }}\n </div>\n }\n </div>\n }\n\n <!-- N\u00FAt m\u1EDF tr\u00ECnh \u0111\u01A1n C\u00E0i \u0111\u1EB7t b\u1EB1ng ZPopoverDirective -->\n @if (controls().settings) {\n @if (isFullscreen()) {\n <div class=\"relative shrink-0\">\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_settings_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:outline-none active:scale-90\"\n [class.bg-white/15]=\"showSettings()\"\n [title]=\"'i18n_z_ui_media_player_settings' | translate\"\n (click)=\"toggleInlineSettings()\">\n <z-icon zType=\"tablerSettings\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n <!-- Popover c\u00E0i \u0111\u1EB7t neo tr\u1EF1c ti\u1EBFp v\u00E0o n\u00FAt, v\u00EC overlay CDK kh\u00F4ng hi\u1EC3n th\u1ECB \u0111\u01B0\u1EE3c trong ch\u1EBF \u0111\u1ED9 Fullscreen g\u1ED1c -->\n @if (showSettings()) {\n <div class=\"z-media-player-popover absolute bottom-full left-1/2 mb-2 -translate-x-1/2\">\n <ng-container [ngTemplateOutlet]=\"speedMenuTemplate\" />\n </div>\n }\n </div>\n } @else {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_settings_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:outline-none active:scale-90 aria-expanded:!bg-white/15 aria-expanded:!text-white\"\n [title]=\"'i18n_z_ui_media_player_settings' | translate\"\n z-popover\n [zPopoverContent]=\"speedMenuTemplate\"\n zPosition=\"top\"\n zTrigger=\"click\"\n [zOffset]=\"9\"\n zClass=\"z-media-player-popover\"\n (zShow)=\"showSettings.set(true); onMouseMove()\"\n (zHide)=\"onSettingsHidden()\">\n <z-icon zType=\"tablerSettings\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n }\n\n <!-- N\u00FAt b\u1EADt/t\u1EAFt ph\u1EE5 \u0111\u1EC1 (CC) -->\n @if (controls().subtitles && hasSubtitles()) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"xs\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_subtitles_btn\"\n class=\"flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [class.bg-white!]=\"captionsEnabled()\"\n [class.text-black!]=\"captionsEnabled()\"\n [attr.aria-pressed]=\"captionsEnabled()\"\n [title]=\"'i18n_z_ui_media_player_subtitles' | translate\"\n (click)=\"toggleSubtitles()\">\n <span class=\"text-[11px] leading-none font-bold tracking-[-0.04em]\">CC</span>\n </button>\n }\n\n <!-- N\u00FAt Miniplayer (Picture-in-Picture) - \u1EA8n ho\u1EB7c disable n\u1EBFu browser ko h\u1ED7 tr\u1EE3 -->\n @if (controls().pip && isPiPSupported()) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_mini_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 sm:flex\"\n [title]=\"'i18n_z_ui_media_player_miniplayer' | translate\"\n (click)=\"toggleMiniplayer()\">\n <z-icon zType=\"tablerPictureInPicture\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- N\u00FAt b\u1EADt/t\u1EAFt ch\u1EBF \u0111\u1ED9 r\u1EA1p chi\u1EBFu phim (Theater Mode) -->\n @if (controls().theater) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n id=\"z_media_player_theater_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 md:flex\"\n [title]=\"\n (isTheaterMode() ? 'i18n_z_ui_media_player_default_mode' : 'i18n_z_ui_media_player_theater_mode')\n | translate\n \"\n (click)=\"toggleTheaterMode()\">\n <z-icon [zType]=\"isTheaterMode() ? 'tablerSquare' : 'tablerRectangle'\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- N\u00FAt b\u1EADt/t\u1EAFt ch\u1EBF \u0111\u1ED9 to\u00E0n m\u00E0n h\u00ECnh (Fullscreen) -->\n @if (controls().fullscreen) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_full_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [title]=\"\n (isFullscreen() ? 'i18n_z_ui_media_player_exit_fullscreen' : 'i18n_z_ui_media_player_fullscreen')\n | translate\n \"\n (click)=\"toggleFullscreen()\">\n <z-icon [zType]=\"isFullscreen() ? 'tablerMinimize' : 'tablerMaximize'\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n </div>\n </div>\n </div>\n\n <!-- Template c\u1EA5u h\u00ECnh Popover c\u00E0i \u0111\u1EB7t s\u1EED d\u1EE5ng ng-template -->\n <ng-template #speedMenuTemplate>\n <div\n class=\"z-media-player-settings flex w-max max-w-48 min-w-28 flex-col px-1 py-[4px] text-zinc-200\"\n [class.z-main-panel]=\"settingsPanel() === 'main'\"\n [class.z-sub-panel]=\"settingsPanel() !== 'main'\">\n @if (settingsPanel() === 'main') {\n <div class=\"z-media-player-settings-panel flex flex-col\">\n @if (qualitySources().length > 1) {\n <button type=\"button\" class=\"z-media-player-settings-item\" (click)=\"openSettingsPanel('quality')\">\n <span class=\"min-w-0 flex-1 truncate whitespace-nowrap\">\n {{ 'i18n_z_ui_media_player_quality' | translate }}\n </span>\n <span class=\"flex max-w-[5.5rem] min-w-0 items-center gap-1.5 text-zinc-400\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ activeQualityLabel() }}</span>\n <z-icon zType=\"tablerChevronRight\" zSize=\"13\" class=\"shrink-0\" />\n </span>\n </button>\n }\n <button type=\"button\" class=\"z-media-player-settings-item\" (click)=\"openSettingsPanel('speed')\">\n <span class=\"min-w-0 flex-1 truncate whitespace-nowrap\">\n {{ 'i18n_z_ui_media_player_speed' | translate }}\n </span>\n <span class=\"flex max-w-[5.5rem] min-w-0 items-center gap-1.5 text-zinc-400\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">\n {{ playbackSpeed() === 1 ? ('i18n_z_ui_media_player_normal' | translate) : playbackSpeed() + 'x' }}\n </span>\n <z-icon zType=\"tablerChevronRight\" zSize=\"13\" class=\"shrink-0\" />\n </span>\n </button>\n </div>\n }\n\n @if (settingsPanel() === 'quality') {\n <div class=\"z-media-player-settings-panel flex flex-col\">\n <button\n type=\"button\"\n [title]=\"'i18n_z_ui_media_player_back' | translate\"\n class=\"z-media-player-settings-back\"\n (click)=\"closeSettingsPanel()\">\n <z-icon zType=\"tablerChevronLeft\" zSize=\"13\" class=\"shrink-0\" />\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ 'i18n_z_ui_media_player_quality' | translate }}</span>\n </button>\n @for (source of qualitySources(); track source.label) {\n <button\n type=\"button\"\n [id]=\"'z_media_player_quality_btn_' + source.label\"\n class=\"z-media-player-settings-item\"\n [class.z-active]=\"activeQualityLabel() === source.label\"\n (click)=\"setQualitySource(source)\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ source.label }}</span>\n </button>\n }\n </div>\n }\n\n @if (settingsPanel() === 'speed') {\n <div class=\"z-media-player-settings-panel flex flex-col\">\n <button\n type=\"button\"\n [title]=\"'i18n_z_ui_media_player_back' | translate\"\n class=\"z-media-player-settings-back\"\n (click)=\"closeSettingsPanel()\">\n <z-icon zType=\"tablerChevronLeft\" zSize=\"13\" class=\"shrink-0\" />\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ 'i18n_z_ui_media_player_speed' | translate }}</span>\n </button>\n @for (speed of playbackSpeeds; track speed.value) {\n <button\n type=\"button\"\n [id]=\"'z_media_player_speed_btn_' + speed.value\"\n class=\"z-media-player-settings-item\"\n [class.z-active]=\"playbackSpeed() === speed.value\"\n (click)=\"setPlaybackSpeed(speed.value)\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ speed.label | translate }}</span>\n </button>\n }\n </div>\n }\n </div>\n </ng-template>\n}\n", styles: ["z-media-player{display:block;width:100%;container-type:inline-size;outline:none}z-media-player:focus,z-media-player:focus-visible{outline:none}z-media-player .z-media-player-controls-surface{position:relative;background-color:#0000008c;box-shadow:0 0 0 1px #0000001a,0 1px 3px #00000026,0 1px 2px -1px #00000026}z-media-player .z-media-player-controls-surface:after{position:absolute;inset:0;z-index:10;border-radius:inherit;box-shadow:inset 0 1px #ffffff1a,inset 0 0 0 1px #ffffff0d;content:\"\";pointer-events:none}z-media-player video::cue{visibility:hidden;background-color:#080808d9;color:#fff;font-family:inherit;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,.9);padding:4px 8px;border-radius:4px}z-media-player .z-media-player-loading-pulse{position:relative;width:64px;height:64px}z-media-player .z-media-player-loading-pulse:before,z-media-player .z-media-player-loading-pulse:after{position:absolute;inset:0;border-radius:9999px;background-color:#0000008c;content:\"\";animation:zUiMediaPlayerLoadingBreathe 2.2s ease-in-out infinite;will-change:transform}z-media-player .z-media-player-loading-pulse:after{inset:14px;background-color:#000000b3;animation-delay:-1.1s}@keyframes zUiMediaPlayerLoadingBreathe{0%,to{transform:scale(.82)}50%{transform:scale(1.08)}}@media(prefers-reduced-motion:reduce){z-media-player .z-media-player-loading-pulse:before,z-media-player .z-media-player-loading-pulse:after{animation:none;transform:scale(1)}}z-media-player .z-flash-animation{will-change:opacity,transform}z-media-player .z-flash-animation z-icon{width:20px!important;height:20px!important;display:inline-flex!important;align-items:center;justify-content:center}z-media-player .z-flash-animation z-icon ng-icon,z-media-player .z-flash-animation z-icon svg{width:100%!important;height:100%!important}@container (min-width: 480px){z-media-player .z-flash-animation{width:64px;height:64px}z-media-player .z-flash-animation z-icon{width:32px!important;height:32px!important}}@container (min-width: 800px){z-media-player .z-flash-animation{width:96px;height:96px}z-media-player .z-flash-animation z-icon{width:48px!important;height:48px!important}}z-media-player:fullscreen .z-flash-animation{width:112px;height:112px}z-media-player:fullscreen .z-flash-animation z-icon{width:56px!important;height:56px!important}z-media-player .z-flash-animation.z-flash-active{animation:zUiFlashIcon .5s ease-out forwards}@keyframes zUiFlashIcon{0%{opacity:0;transform:scale(.76)}18%{opacity:.95}to{opacity:0;transform:scale(1.18)}}@media(prefers-reduced-motion:reduce){z-media-player .z-flash-animation{animation-duration:.2s}}z-media-player .animate-fade-in{animation:zUiFadeIn .2s cubic-bezier(.16,1,.3,1)}@keyframes zUiFadeIn{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}z-media-player .z-double-tap-ripple-left,z-media-player .z-double-tap-ripple-right{position:absolute;top:0;bottom:0;width:35%;display:flex;align-items:center;z-index:11;pointer-events:none}z-media-player .z-double-tap-ripple-left:before,z-media-player .z-double-tap-ripple-right:before{position:absolute;inset:0;content:\"\";opacity:0;animation-duration:.72s;animation-fill-mode:forwards;animation-timing-function:cubic-bezier(.25,1,.5,1)}z-media-player .z-double-tap-ripple-left{left:0;justify-content:flex-start;padding-left:5%}z-media-player .z-double-tap-ripple-left:before{background:radial-gradient(circle at 0% 50%,#ffffff21,#fff0 70%);border-top-right-radius:100% 50%;border-bottom-right-radius:100% 50%;animation-name:zUiDoubleTapRippleLeft;transform-origin:left center}z-media-player .z-double-tap-ripple-left.z-skip-pulse-alt:before{animation-name:zUiDoubleTapRippleLeftAlt}z-media-player .z-double-tap-ripple-right{right:0;justify-content:flex-end;padding-right:5%}z-media-player .z-double-tap-ripple-right:before{background:radial-gradient(circle at 100% 50%,#ffffff21,#fff0 70%);border-top-left-radius:100% 50%;border-bottom-left-radius:100% 50%;animation-name:zUiDoubleTapRippleRight;transform-origin:right center}z-media-player .z-double-tap-ripple-right.z-skip-pulse-alt:before{animation-name:zUiDoubleTapRippleRightAlt}z-media-player .z-skip-overlay-content{position:relative;display:flex;align-items:center;justify-content:center;gap:6px;min-width:92px;color:#fff;font-family:inherit;font-size:18px;font-weight:700;line-height:1;text-shadow:0 2px 8px rgba(0,0,0,.75);-webkit-user-select:none;user-select:none}z-media-player .z-skip-overlay-number{min-width:42px;animation:zUiSkipNumberBump .22s cubic-bezier(.2,.85,.25,1) forwards;text-align:center;transform-origin:center}z-media-player .z-skip-pulse-alt .z-skip-overlay-number{animation-name:zUiSkipNumberBumpAlt}z-media-player .z-skip-overlay-arrows{display:inline-flex;align-items:center;color:#fffffff2;filter:drop-shadow(0 2px 6px rgba(0,0,0,.7))}@keyframes zUiDoubleTapRippleLeft{0%{opacity:0;transform:scaleX(.5);transform-origin:left center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:left center}}@keyframes zUiDoubleTapRippleLeftAlt{0%{opacity:0;transform:scaleX(.5);transform-origin:left center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:left center}}@keyframes zUiDoubleTapRippleRight{0%{opacity:0;transform:scaleX(.5);transform-origin:right center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:right center}}@keyframes zUiDoubleTapRippleRightAlt{0%{opacity:0;transform:scaleX(.5);transform-origin:right center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:right center}}z-media-player .z-chevron-slide-left{animation:zUiChevronSlideLeft .72s cubic-bezier(.25,1,.5,1) forwards}z-media-player .z-chevron-slide-right{animation:zUiChevronSlideRight .72s cubic-bezier(.25,1,.5,1) forwards}z-media-player .z-skip-pulse-alt .z-chevron-slide-left{animation-name:zUiChevronSlideLeftAlt}z-media-player .z-skip-pulse-alt .z-chevron-slide-right{animation-name:zUiChevronSlideRightAlt}@keyframes zUiSkipNumberBump{0%{opacity:.78;transform:scale(.92)}55%{opacity:1;transform:scale(1.12)}to{opacity:1;transform:scale(1)}}@keyframes zUiSkipNumberBumpAlt{0%{opacity:.78;transform:scale(.92)}55%{opacity:1;transform:scale(1.12)}to{opacity:1;transform:scale(1)}}@keyframes zUiChevronSlideLeft{0%{opacity:0;transform:translate(8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(-12px)}}@keyframes zUiChevronSlideRight{0%{opacity:0;transform:translate(-8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(12px)}}@keyframes zUiChevronSlideLeftAlt{0%{opacity:0;transform:translate(8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(-12px)}}@keyframes zUiChevronSlideRightAlt{0%{opacity:0;transform:translate(-8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(12px)}}z-media-player #z_media_player_volume_slider{-webkit-appearance:none;appearance:none;background:transparent}z-media-player #z_media_player_volume_slider::-webkit-slider-runnable-track{background:#ffffff40;height:3px;border-radius:9999px}z-media-player #z_media_player_volume_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#fff;height:10px;width:10px;border-radius:9999px;margin-top:-3.5px;transition:transform .15s ease}z-media-player #z_media_player_volume_slider:hover::-webkit-slider-thumb,z-media-player #z_media_player_volume_slider:focus-visible::-webkit-slider-thumb{transform:scale(1.3)}z-media-player #z_media_player_volume_slider::-moz-range-track{background:#ffffff40;height:3px;border-radius:9999px}z-media-player #z_media_player_volume_slider::-moz-range-thumb{background:#fff;height:10px;width:10px;border-radius:9999px;border:none;transition:transform .15s ease}z-media-player #z_media_player_volume_slider:hover::-moz-range-thumb,z-media-player #z_media_player_volume_slider:focus-visible::-moz-range-thumb{transform:scale(1.3)}.z-media-player-popover{background-color:#0000008c!important;border:none!important;box-shadow:0 0 0 1px #0000001a,0 1px 3px #00000026,0 1px 2px -1px #00000026!important;border-radius:8px!important;padding:0!important;transform-origin:bottom center}.z-media-player-popover:after{position:absolute;inset:0;z-index:10;border-radius:inherit;box-shadow:inset 0 1px #ffffff1a,inset 0 0 0 1px #ffffff0d;content:\"\";pointer-events:none}.z-media-player-popover[data-state=open]{animation:zUiMediaSettingsPopoverIn .16s cubic-bezier(.2,.85,.25,1) forwards}.z-media-player-popover[data-state=closed]{animation:zUiMediaSettingsPopoverOut .16s cubic-bezier(.4,0,.2,1) forwards!important}.z-media-player-popover .bg-popover{background-color:transparent!important;color:#e4e4e7!important}.z-media-player-popover .z-media-player-settings{overflow:hidden;transform-origin:bottom right;animation:zUiMediaSettingsPanelIn .16s cubic-bezier(.2,.85,.25,1)}.z-media-player-popover .z-media-player-settings-panel{gap:4px;transform-origin:top right;animation:zUiMediaSettingsPanelIn .14s cubic-bezier(.2,.85,.25,1);will-change:opacity,transform}.z-media-player-popover .z-media-player-settings.z-main-panel .z-media-player-settings-panel{animation-name:zUiMediaSettingsPanelFromLeft}.z-media-player-popover .z-media-player-settings.z-sub-panel .z-media-player-settings-panel{animation-name:zUiMediaSettingsPanelFromRight}.z-media-player-popover .z-media-player-settings-item,.z-media-player-popover .z-media-player-settings-back{position:relative;display:flex;width:100%;min-width:0;height:32px;min-height:32px;cursor:pointer;align-items:center;border:0;border-radius:4px;background-color:transparent;padding:0 8px;color:#e4e4e7;font-size:12px;font-weight:500;line-height:1;outline:none;transition:background-color .12s ease,color .12s ease}.z-media-player-popover .z-media-player-settings-item{justify-content:space-between;gap:12px;text-align:left}.z-media-player-popover .z-media-player-settings-item:before{position:absolute;top:7px;bottom:7px;left:0;width:2px;border-radius:9999px;background-color:#fff;content:\"\";opacity:0;transform:scaleY(.5);transition:opacity .12s ease,transform .12s ease}.z-media-player-popover .z-media-player-settings-item.z-active{background-color:#ffffff14;color:#fff;font-weight:600}.z-media-player-popover .z-media-player-settings-item.z-active:before{opacity:1;transform:scaleY(1)}.z-media-player-popover .z-media-player-settings-back{justify-content:flex-start;gap:8px;color:#f4f4f5;font-weight:600}.z-media-player-popover .z-media-player-settings-item:hover,.z-media-player-popover .z-media-player-settings-back:hover,.z-media-player-popover .z-media-player-settings-item:focus-visible,.z-media-player-popover .z-media-player-settings-back:focus-visible{background-color:#ffffff1a}@keyframes zUiMediaSettingsPopoverIn{0%{opacity:0;transform:translateY(6px) scale(.94)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes zUiMediaSettingsPopoverOut{0%{opacity:1;transform:translateY(0) scale(1)}to{opacity:0;transform:translateY(4px) scale(.96)}}@keyframes zUiMediaSettingsPanelIn{0%{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}@keyframes zUiMediaSettingsPanelFromRight{0%{opacity:0;transform:translate(8px)}to{opacity:1;transform:translate(0)}}@keyframes zUiMediaSettingsPanelFromLeft{0%{opacity:0;transform:translate(-5px)}to{opacity:1;transform:translate(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.RangeValueAccessor, selector: "input[type=range]:not([ngNoCva])[formControlName],input[type=range]:not([ngNoCva])[formControl],input[type=range]:not([ngNoCva])[ngModel]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: ZIconComponent, selector: "z-icon, [z-icon]", inputs: ["class", "zType", "zAnimatedType", "zAnimate", "zAnimationTrigger", "zSize", "zStrokeWidth", "zSvg"] }, { kind: "directive", type: ZPopoverDirective, selector: "[z-popover]", inputs: ["zPopoverContent", "zPosition", "zTrigger", "zPopoverTrigger", "zClass", "zShowDelay", "zHideDelay", "zDisabled", "zOffset", "zPopoverWidth", "zTriggerRef", "zManualClose", "zOutsideClickClose", "zScrollClose", "zSticky", "zShowArrow", "zKeyboardSafe"], outputs: ["zShow", "zHide", "zHideStart", "zControl", "zPositionChange", "zOutsideClick"], exportAs: ["zPopover"] }, { kind: "component", type: ZButtonComponent, selector: "z-button, button[z-button], a[z-button]", inputs: ["class", "zType", "zSize", "zShape", "zLabel", "zLoading", "zDisabled", "zTypeIcon", "zAnimatedTypeIcon", "zAnimateIcon", "zAnimationTriggerIcon", "zSizeIcon", "zStrokeWidthIcon", "zWave"], exportAs: ["zButton"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
397
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ZMediaPlayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
398
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: ZMediaPlayerComponent, isStandalone: true, selector: "z-media-player", inputs: { class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, zSources: { classPropertyName: "zSources", publicName: "zSources", isSignal: true, isRequired: true, transformFunction: null }, zConfig: { classPropertyName: "zConfig", publicName: "zConfig", isSignal: true, isRequired: false, transformFunction: null }, zSkin: { classPropertyName: "zSkin", publicName: "zSkin", isSignal: true, isRequired: false, transformFunction: null }, zDefaultLayoutConfig: { classPropertyName: "zDefaultLayoutConfig", publicName: "zDefaultLayoutConfig", isSignal: true, isRequired: false, transformFunction: null }, zPlyrLayoutConfig: { classPropertyName: "zPlyrLayoutConfig", publicName: "zPlyrLayoutConfig", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { zStateChange: "zStateChange", zError: "zError" }, host: { properties: { "class": "zClasses()" } }, viewQueries: [{ propertyName: "playerElement", first: true, predicate: ["playerElement"], descendants: true, isSignal: true }, { propertyName: "layoutElement", first: true, predicate: ["layoutElement"], descendants: true, isSignal: true }], ngImport: i0, template: "<media-player #playerElement>\n <media-provider>\n @for (subtitle of zConfig().subtitles ?? []; track subtitle.src) {\n <track\n kind=\"subtitles\"\n [label]=\"subtitle.label\"\n [src]=\"subtitle.src\"\n [srclang]=\"subtitle.srclang\"\n [default]=\"subtitle.default ?? false\" />\n }\n @if (zConfig().chaptersVtt) {\n <track kind=\"chapters\" [src]=\"zConfig().chaptersVtt!\" />\n }\n </media-provider>\n\n @if (isPlyrSkin()) {\n <media-plyr-layout #layoutElement></media-plyr-layout>\n } @else if (isAudioSource()) {\n <media-audio-layout #layoutElement></media-audio-layout>\n } @else {\n <media-video-layout #layoutElement></media-video-layout>\n }\n</media-player>\n", styles: ["@charset \"UTF-8\";z-media-player{display:flex;align-items:center;justify-content:center;width:100%;container-type:inline-size;--media-user-font-size: .75;--media-cue-padding-x: calc(var(--cue-font-size, 1em) * .4)}z-media-player [data-media-player][data-view-type=video]{width:auto;height:100%;max-width:100%;max-height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
1005
399
|
}
|
|
1006
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.
|
|
400
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ZMediaPlayerComponent, decorators: [{
|
|
1007
401
|
type: Component,
|
|
1008
|
-
args: [{ selector: 'z-media-player',
|
|
402
|
+
args: [{ selector: 'z-media-player', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
1009
403
|
'[class]': 'zClasses()',
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
'(mouseleave)': 'onMouseLeave()',
|
|
1013
|
-
'(document:fullscreenchange)': 'onFullscreenStateChange()',
|
|
1014
|
-
}, template: "@if (isYouTube()) {\n <!-- Nh\u00FAng iframe YouTube g\u1ED1c, d\u00F9ng control m\u1EB7c \u0111\u1ECBnh c\u1EE7a YouTube -->\n <iframe\n class=\"h-full w-full\"\n [src]=\"youtubeEmbedUrl()\"\n title=\"YouTube video player\"\n frameborder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowfullscreen></iframe>\n} @else {\n <!-- Th\u1EBB video ch\u00EDnh ph\u00E1t ngu\u1ED3n zSrc -->\n <video\n #videoElement\n class=\"h-full w-full cursor-pointer object-cover\"\n [src]=\"effectiveSrc()\"\n [poster]=\"zPoster()\"\n [loop]=\"zLoop()\"\n [autoplay]=\"zAutoplay()\"\n playsinline\n webkit-playsinline\n (click)=\"onVideoClick()\"\n (play)=\"onPlayStateChange(true)\"\n (pause)=\"onPlayStateChange(false)\"\n (loadeddata)=\"onLoadedData()\"\n (error)=\"onVideoError()\"\n (timeupdate)=\"onTimeUpdate()\"\n (durationchange)=\"onDurationChange()\"\n (seeking)=\"onSeeking(true)\"\n (seeked)=\"onSeeking(false)\"\n (waiting)=\"onSeeking(true)\"\n (playing)=\"onSeeking(false)\"\n (enterpictureinpicture)=\"onEnterPiP()\"\n (leavepictureinpicture)=\"onLeavePiP()\"\n (webkitpresentationmodechanged)=\"onWebkitPresentationModeChanged()\">\n @for (sub of zSubtitles(); track sub.src) {\n <track\n [src]=\"sub.src\"\n kind=\"subtitles\"\n [srclang]=\"sub.srclang\"\n [label]=\"sub.label\"\n [default]=\"sub.default || false\" />\n }\n </video>\n\n @if (subtitleText()) {\n <div\n class=\"pointer-events-none absolute inset-x-3 z-[11] mx-auto w-fit max-w-[calc(100%-1.5rem)] rounded-sm bg-black/80 px-4 py-2 text-center text-base leading-6 whitespace-pre-line text-white shadow-sm transition-[bottom] duration-200 ease-out sm:text-lg sm:leading-7\"\n [class.bottom-16]=\"showControls()\"\n [class.bottom-2]=\"!showControls()\">\n {{ subtitleText() }}\n </div>\n }\n\n <!-- V\u00F2ng tr\u00F2n loading gi\u1EEFa m\u00E0n h\u00ECnh khi video \u0111ang buffer -->\n @if (isBuffering()) {\n <div class=\"pointer-events-none absolute inset-0 z-[13] flex items-center justify-center\">\n <span class=\"z-media-player-loading-pulse\"></span>\n </div>\n }\n\n <!-- Hi\u1EC7u \u1EE9ng nh\u1EA5p nh\u00E1y Play/Pause l\u1EDBn gi\u1EEFa m\u00E0n h\u00ECnh t\u01B0\u01A1ng t\u1EF1 YouTube -->\n <div class=\"pointer-events-none absolute inset-0 z-[11] flex items-center justify-center\">\n <div\n class=\"z-flash-animation flex size-10 scale-75 items-center justify-center rounded-full bg-black/60 text-white opacity-0\"\n [class.z-flash-active]=\"!!playPauseFlash()\">\n @if (playPauseFlash(); as flashType) {\n <z-icon\n [zType]=\"flashType === 'play' ? 'tablerPlayerPlayFill' : 'tablerPlayerPauseFill'\"\n zSize=\"28\"\n [zStrokeWidth]=\"2\"\n [class.translate-x-[2px]]=\"flashType === 'play'\" />\n }\n </div>\n </div>\n\n <!-- L\u1EDBp ph\u1EE7 tua nhanh nh\u1EA3y s\u1ED1 c\u1ED9ng d\u1ED3n Tr\u00E1i (YouTube style) -->\n @if (skipOverlayValue() > 0 && skipOverlayDirection() === 'left') {\n <div class=\"z-double-tap-ripple-left\" [class.z-skip-pulse-alt]=\"skipOverlayPulse() % 2 === 1\">\n <div class=\"z-skip-overlay-content\">\n <span class=\"z-skip-overlay-number\">- {{ skipOverlayValue() }}</span>\n <span class=\"z-skip-overlay-arrows z-chevron-slide-left\">\n <z-icon zType=\"tablerChevronsLeft\" zSize=\"26\" [zStrokeWidth]=\"2.4\" />\n </span>\n </div>\n </div>\n }\n\n <!-- L\u1EDBp ph\u1EE7 tua nhanh nh\u1EA3y s\u1ED1 c\u1ED9ng d\u1ED3n Ph\u1EA3i (YouTube style) -->\n @if (skipOverlayValue() > 0 && skipOverlayDirection() === 'right') {\n <div class=\"z-double-tap-ripple-right\" [class.z-skip-pulse-alt]=\"skipOverlayPulse() % 2 === 1\">\n <div class=\"z-skip-overlay-content\">\n <span class=\"z-skip-overlay-arrows z-chevron-slide-right\">\n <z-icon zType=\"tablerChevronsRight\" zSize=\"26\" [zStrokeWidth]=\"2.4\" />\n </span>\n <span class=\"z-skip-overlay-number\">+ {{ skipOverlayValue() }}</span>\n </div>\n </div>\n }\n\n <!-- L\u1EDBp ph\u1EE7 gradient ph\u1EE7 s\u00E1t to\u00E0n b\u1ED9 m\u00E9p d\u01B0\u1EDBi video -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 z-[10] h-28 bg-gradient-to-t from-black/55 via-black/25 to-transparent transition-opacity duration-200 ease-out\"\n [class.opacity-0]=\"!showControls()\"></div>\n\n <!-- Thanh \u0111i\u1EC1u khi\u1EC3n d\u1EA1ng rounded-sm n\u1ED5i ph\u00EDa tr\u00EAn \u0111\u00E1y video -->\n <div\n class=\"z-media-player-controls absolute inset-x-2 bottom-2 z-[12] transition-opacity duration-200 ease-out select-none sm:inset-x-3 sm:bottom-3\"\n [class.opacity-0]=\"!showControls()\"\n [class.pointer-events-auto]=\"showControls()\"\n [class.pointer-events-none]=\"!showControls()\">\n <div class=\"z-media-player-controls-surface relative z-[1] rounded-sm px-2 py-1.5 text-sm text-white\">\n <div class=\"flex items-center gap-1.5 sm:gap-2\">\n <!-- N\u00FAt Ph\u00E1t / T\u1EA1m d\u1EEBng -->\n @if (controls().play) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_play_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [title]=\"(isPlaying() ? 'i18n_z_ui_media_player_pause' : 'i18n_z_ui_media_player_play') | translate\"\n (click)=\"togglePlay()\">\n <z-icon\n [zType]=\"isPlaying() ? 'tablerPlayerPauseFill' : 'tablerPlayerPlayFill'\"\n zSize=\"18\"\n [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- N\u00FAt Tua l\u00F9i / Tua ti\u1EBFn 5 gi\u00E2y -->\n @if (controls().skip) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_rewind_5_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 sm:flex\"\n title=\"Tua l\u00F9i 5 gi\u00E2y (Arrow Left)\"\n (click)=\"triggerSkip(5, 'left')\">\n <z-icon zType=\"tablerRewindBackward5\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_forward_5_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 sm:flex\"\n title=\"Tua ti\u1EBFn 5 gi\u00E2y (Arrow Right)\"\n (click)=\"triggerSkip(5, 'right')\">\n <z-icon zType=\"tablerRewindForward5\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- T\u1ED5 h\u1EE3p n\u00FAt Mute v\u00E0 Slider \u00E2m l\u01B0\u1EE3ng tr\u01B0\u1EE3t ngang -->\n @if (controls().volume) {\n <div class=\"group/volume hidden shrink-0 items-center sm:flex\">\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_mute_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [title]=\"(isMuted() ? 'i18n_z_ui_media_player_unmute' : 'i18n_z_ui_media_player_mute') | translate\"\n (click)=\"toggleMute()\">\n <z-icon\n [zType]=\"isMuted() ? 'tablerVolumeOff' : volume() < 0.5 ? 'tablerVolume2' : 'tablerVolume'\"\n zSize=\"18\"\n [zStrokeWidth]=\"2\" />\n </button>\n <!-- Thanh tr\u01B0\u1EE3t \u00E2m l\u01B0\u1EE3ng tr\u01B0\u1EE3t ngang m\u1EDF r\u1ED9ng khi hover -->\n <input\n type=\"range\"\n id=\"z_media_player_volume_slider\"\n min=\"0\"\n max=\"1\"\n step=\"0.05\"\n [ngModel]=\"volume()\"\n (ngModelChange)=\"onVolumeChange($event)\"\n class=\"accent-primary h-1 w-0 cursor-pointer rounded-lg bg-white/20 opacity-0 transition-all duration-200 outline-none group-hover/volume:w-16 group-hover/volume:opacity-100 focus-visible:w-16 focus-visible:opacity-100\" />\n </div>\n }\n\n <!-- Th\u1EDDi gian ph\u00E1t: g\u1ED9p chung \"\u0111\u00E3 ph\u00E1t / t\u1ED5ng th\u1EDDi l\u01B0\u1EE3ng\" th\u00E0nh m\u1ED9t nh\u00F3m duy nh\u1EA5t -->\n @if (controls().time) {\n <span class=\"shrink-0 rounded-full bg-black/30 px-2 py-0.5 font-mono text-xs whitespace-nowrap text-white/90\">\n {{ currentTimeLabel() }}\n <span class=\"text-white/50\">/</span>\n {{ durationLabel() }}\n </span>\n }\n\n <!-- Thanh Timeline (Progress Bar) t\u00F9y ch\u1EC9nh ki\u1EC3u YouTube, n\u1EB1m chung h\u00E0ng v\u1EDBi c\u00E1c n\u00FAt -->\n @if (controls().timeline) {\n <div\n #timelineContainer\n id=\"z_media_player_timeline\"\n class=\"group/timeline relative flex h-1 min-w-0 flex-1 cursor-pointer items-center\"\n (mousedown)=\"onTimelineDragStart($event)\"\n (mousemove)=\"onTimelineHover($event)\"\n (mouseleave)=\"onTimelineHoverEnd()\">\n <!-- Thanh n\u1EC1n x\u00E1m c\u1EE7a thanh timeline -->\n <div\n class=\"absolute inset-x-0 h-1 overflow-hidden rounded-full bg-white/25 transition-all group-hover/timeline:h-1.5\">\n <!-- Ti\u1EBFn tr\u00ECnh t\u1EA3i buffer c\u1EE7a video -->\n <div\n class=\"absolute top-0 left-0 h-full rounded-full bg-white/30\"\n [style.width.%]=\"bufferedProgress()\"></div>\n <!-- Ti\u1EBFn tr\u00ECnh ph\u00E1t hi\u1EC7n t\u1EA1i c\u1EE7a video -->\n <div class=\"bg-primary absolute top-0 left-0 h-full rounded-full\" [style.width.%]=\"progress()\"></div>\n </div>\n\n <!-- \u0110\u1EA7u k\u00E9o timeline (Thumb) \u0111\u1ECF ch\u1EC9 hi\u1EC3n th\u1ECB khi hover ho\u1EB7c k\u00E9o -->\n <div\n class=\"bg-primary pointer-events-none absolute size-3.5 -translate-x-1/2 scale-0 transform rounded-full opacity-0 transition-opacity duration-150 group-hover/timeline:scale-100 group-hover/timeline:opacity-100\"\n [style.left.%]=\"progress()\"></div>\n\n <!-- Preview \u1EA3nh nh\u1ECF storyboard khi hover timeline (ki\u1EC3u YouTube) -->\n @if (hoverThumbnailCue(); as cue) {\n <div\n class=\"pointer-events-none absolute z-[20] -translate-x-1/2 overflow-hidden rounded-md border-0 bg-black shadow-[0_12px_24px_-10px_rgba(0,0,0,0.85)]\"\n [style.left.px]=\"hoverX()\"\n [style.bottom.px]=\"24\"\n [style.width.px]=\"cue.w\"\n [style.height.px]=\"cue.h\">\n <div\n class=\"bg-no-repeat\"\n [style.width.px]=\"cue.w\"\n [style.height.px]=\"cue.h\"\n [style.background-image]=\"'url(' + cue.src + ')'\"\n [style.background-position]=\"'-' + cue.x + 'px -' + cue.y + 'px'\"></div>\n </div>\n }\n\n <!-- Tooltip hi\u1EC3n th\u1ECB m\u1ED1c th\u1EDDi gian khi hover -->\n @if (hoverTimeLabel()) {\n <div\n class=\"pointer-events-none absolute bottom-4 z-[20] -translate-x-1/2 rounded-xs bg-zinc-950/70 px-2 py-1 font-mono text-[11px] leading-none whitespace-nowrap text-zinc-100 shadow-[0_12px_24px_-10px_rgba(0,0,0,0.85)] backdrop-blur-2xl\"\n [style.left.px]=\"hoverX()\">\n {{ hoverTimeLabel() }}\n </div>\n }\n </div>\n }\n\n <!-- N\u00FAt m\u1EDF tr\u00ECnh \u0111\u01A1n C\u00E0i \u0111\u1EB7t b\u1EB1ng ZPopoverDirective -->\n @if (controls().settings) {\n @if (isFullscreen()) {\n <div class=\"relative shrink-0\">\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_settings_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:outline-none active:scale-90\"\n [class.bg-white/15]=\"showSettings()\"\n [title]=\"'i18n_z_ui_media_player_settings' | translate\"\n (click)=\"toggleInlineSettings()\">\n <z-icon zType=\"tablerSettings\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n <!-- Popover c\u00E0i \u0111\u1EB7t neo tr\u1EF1c ti\u1EBFp v\u00E0o n\u00FAt, v\u00EC overlay CDK kh\u00F4ng hi\u1EC3n th\u1ECB \u0111\u01B0\u1EE3c trong ch\u1EBF \u0111\u1ED9 Fullscreen g\u1ED1c -->\n @if (showSettings()) {\n <div class=\"z-media-player-popover absolute bottom-full left-1/2 mb-2 -translate-x-1/2\">\n <ng-container [ngTemplateOutlet]=\"speedMenuTemplate\" />\n </div>\n }\n </div>\n } @else {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_settings_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:border-transparent focus-visible:ring-0 focus-visible:outline-none active:scale-90 aria-expanded:!bg-white/15 aria-expanded:!text-white\"\n [title]=\"'i18n_z_ui_media_player_settings' | translate\"\n z-popover\n [zPopoverContent]=\"speedMenuTemplate\"\n zPosition=\"top\"\n zTrigger=\"click\"\n [zOffset]=\"9\"\n zClass=\"z-media-player-popover\"\n (zShow)=\"showSettings.set(true); onMouseMove()\"\n (zHide)=\"onSettingsHidden()\">\n <z-icon zType=\"tablerSettings\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n }\n\n <!-- N\u00FAt b\u1EADt/t\u1EAFt ph\u1EE5 \u0111\u1EC1 (CC) -->\n @if (controls().subtitles && hasSubtitles()) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"xs\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_subtitles_btn\"\n class=\"flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [class.bg-white!]=\"captionsEnabled()\"\n [class.text-black!]=\"captionsEnabled()\"\n [attr.aria-pressed]=\"captionsEnabled()\"\n [title]=\"'i18n_z_ui_media_player_subtitles' | translate\"\n (click)=\"toggleSubtitles()\">\n <span class=\"text-[11px] leading-none font-bold tracking-[-0.04em]\">CC</span>\n </button>\n }\n\n <!-- N\u00FAt Miniplayer (Picture-in-Picture) - \u1EA8n ho\u1EB7c disable n\u1EBFu browser ko h\u1ED7 tr\u1EE3 -->\n @if (controls().pip && isPiPSupported()) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_mini_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 sm:flex\"\n [title]=\"'i18n_z_ui_media_player_miniplayer' | translate\"\n (click)=\"toggleMiniplayer()\">\n <z-icon zType=\"tablerPictureInPicture\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- N\u00FAt b\u1EADt/t\u1EAFt ch\u1EBF \u0111\u1ED9 r\u1EA1p chi\u1EBFu phim (Theater Mode) -->\n @if (controls().theater) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n id=\"z_media_player_theater_btn\"\n class=\"hidden size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90 md:flex\"\n [title]=\"\n (isTheaterMode() ? 'i18n_z_ui_media_player_default_mode' : 'i18n_z_ui_media_player_theater_mode')\n | translate\n \"\n (click)=\"toggleTheaterMode()\">\n <z-icon [zType]=\"isTheaterMode() ? 'tablerSquare' : 'tablerRectangle'\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n\n <!-- N\u00FAt b\u1EADt/t\u1EAFt ch\u1EBF \u0111\u1ED9 to\u00E0n m\u00E0n h\u00ECnh (Fullscreen) -->\n @if (controls().fullscreen) {\n <button\n z-button\n zType=\"ghost\"\n zSize=\"sm\"\n zShape=\"default\"\n [zWave]=\"false\"\n type=\"button\"\n id=\"z_media_player_full_btn\"\n class=\"flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 border-none bg-transparent p-0 text-white shadow-none transition-[background-color,color,transform] outline-none hover:bg-white/15 hover:text-white focus:outline-none focus-visible:outline-none active:scale-90\"\n [title]=\"\n (isFullscreen() ? 'i18n_z_ui_media_player_exit_fullscreen' : 'i18n_z_ui_media_player_fullscreen')\n | translate\n \"\n (click)=\"toggleFullscreen()\">\n <z-icon [zType]=\"isFullscreen() ? 'tablerMinimize' : 'tablerMaximize'\" zSize=\"18\" [zStrokeWidth]=\"2\" />\n </button>\n }\n </div>\n </div>\n </div>\n\n <!-- Template c\u1EA5u h\u00ECnh Popover c\u00E0i \u0111\u1EB7t s\u1EED d\u1EE5ng ng-template -->\n <ng-template #speedMenuTemplate>\n <div\n class=\"z-media-player-settings flex w-max max-w-48 min-w-28 flex-col px-1 py-[4px] text-zinc-200\"\n [class.z-main-panel]=\"settingsPanel() === 'main'\"\n [class.z-sub-panel]=\"settingsPanel() !== 'main'\">\n @if (settingsPanel() === 'main') {\n <div class=\"z-media-player-settings-panel flex flex-col\">\n @if (qualitySources().length > 1) {\n <button type=\"button\" class=\"z-media-player-settings-item\" (click)=\"openSettingsPanel('quality')\">\n <span class=\"min-w-0 flex-1 truncate whitespace-nowrap\">\n {{ 'i18n_z_ui_media_player_quality' | translate }}\n </span>\n <span class=\"flex max-w-[5.5rem] min-w-0 items-center gap-1.5 text-zinc-400\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ activeQualityLabel() }}</span>\n <z-icon zType=\"tablerChevronRight\" zSize=\"13\" class=\"shrink-0\" />\n </span>\n </button>\n }\n <button type=\"button\" class=\"z-media-player-settings-item\" (click)=\"openSettingsPanel('speed')\">\n <span class=\"min-w-0 flex-1 truncate whitespace-nowrap\">\n {{ 'i18n_z_ui_media_player_speed' | translate }}\n </span>\n <span class=\"flex max-w-[5.5rem] min-w-0 items-center gap-1.5 text-zinc-400\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">\n {{ playbackSpeed() === 1 ? ('i18n_z_ui_media_player_normal' | translate) : playbackSpeed() + 'x' }}\n </span>\n <z-icon zType=\"tablerChevronRight\" zSize=\"13\" class=\"shrink-0\" />\n </span>\n </button>\n </div>\n }\n\n @if (settingsPanel() === 'quality') {\n <div class=\"z-media-player-settings-panel flex flex-col\">\n <button\n type=\"button\"\n [title]=\"'i18n_z_ui_media_player_back' | translate\"\n class=\"z-media-player-settings-back\"\n (click)=\"closeSettingsPanel()\">\n <z-icon zType=\"tablerChevronLeft\" zSize=\"13\" class=\"shrink-0\" />\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ 'i18n_z_ui_media_player_quality' | translate }}</span>\n </button>\n @for (source of qualitySources(); track source.label) {\n <button\n type=\"button\"\n [id]=\"'z_media_player_quality_btn_' + source.label\"\n class=\"z-media-player-settings-item\"\n [class.z-active]=\"activeQualityLabel() === source.label\"\n (click)=\"setQualitySource(source)\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ source.label }}</span>\n </button>\n }\n </div>\n }\n\n @if (settingsPanel() === 'speed') {\n <div class=\"z-media-player-settings-panel flex flex-col\">\n <button\n type=\"button\"\n [title]=\"'i18n_z_ui_media_player_back' | translate\"\n class=\"z-media-player-settings-back\"\n (click)=\"closeSettingsPanel()\">\n <z-icon zType=\"tablerChevronLeft\" zSize=\"13\" class=\"shrink-0\" />\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ 'i18n_z_ui_media_player_speed' | translate }}</span>\n </button>\n @for (speed of playbackSpeeds; track speed.value) {\n <button\n type=\"button\"\n [id]=\"'z_media_player_speed_btn_' + speed.value\"\n class=\"z-media-player-settings-item\"\n [class.z-active]=\"playbackSpeed() === speed.value\"\n (click)=\"setPlaybackSpeed(speed.value)\">\n <span class=\"min-w-0 truncate whitespace-nowrap\">{{ speed.label | translate }}</span>\n </button>\n }\n </div>\n }\n </div>\n </ng-template>\n}\n", styles: ["z-media-player{display:block;width:100%;container-type:inline-size;outline:none}z-media-player:focus,z-media-player:focus-visible{outline:none}z-media-player .z-media-player-controls-surface{position:relative;background-color:#0000008c;box-shadow:0 0 0 1px #0000001a,0 1px 3px #00000026,0 1px 2px -1px #00000026}z-media-player .z-media-player-controls-surface:after{position:absolute;inset:0;z-index:10;border-radius:inherit;box-shadow:inset 0 1px #ffffff1a,inset 0 0 0 1px #ffffff0d;content:\"\";pointer-events:none}z-media-player video::cue{visibility:hidden;background-color:#080808d9;color:#fff;font-family:inherit;font-size:16px;text-shadow:0 1px 2px rgba(0,0,0,.9);padding:4px 8px;border-radius:4px}z-media-player .z-media-player-loading-pulse{position:relative;width:64px;height:64px}z-media-player .z-media-player-loading-pulse:before,z-media-player .z-media-player-loading-pulse:after{position:absolute;inset:0;border-radius:9999px;background-color:#0000008c;content:\"\";animation:zUiMediaPlayerLoadingBreathe 2.2s ease-in-out infinite;will-change:transform}z-media-player .z-media-player-loading-pulse:after{inset:14px;background-color:#000000b3;animation-delay:-1.1s}@keyframes zUiMediaPlayerLoadingBreathe{0%,to{transform:scale(.82)}50%{transform:scale(1.08)}}@media(prefers-reduced-motion:reduce){z-media-player .z-media-player-loading-pulse:before,z-media-player .z-media-player-loading-pulse:after{animation:none;transform:scale(1)}}z-media-player .z-flash-animation{will-change:opacity,transform}z-media-player .z-flash-animation z-icon{width:20px!important;height:20px!important;display:inline-flex!important;align-items:center;justify-content:center}z-media-player .z-flash-animation z-icon ng-icon,z-media-player .z-flash-animation z-icon svg{width:100%!important;height:100%!important}@container (min-width: 480px){z-media-player .z-flash-animation{width:64px;height:64px}z-media-player .z-flash-animation z-icon{width:32px!important;height:32px!important}}@container (min-width: 800px){z-media-player .z-flash-animation{width:96px;height:96px}z-media-player .z-flash-animation z-icon{width:48px!important;height:48px!important}}z-media-player:fullscreen .z-flash-animation{width:112px;height:112px}z-media-player:fullscreen .z-flash-animation z-icon{width:56px!important;height:56px!important}z-media-player .z-flash-animation.z-flash-active{animation:zUiFlashIcon .5s ease-out forwards}@keyframes zUiFlashIcon{0%{opacity:0;transform:scale(.76)}18%{opacity:.95}to{opacity:0;transform:scale(1.18)}}@media(prefers-reduced-motion:reduce){z-media-player .z-flash-animation{animation-duration:.2s}}z-media-player .animate-fade-in{animation:zUiFadeIn .2s cubic-bezier(.16,1,.3,1)}@keyframes zUiFadeIn{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}z-media-player .z-double-tap-ripple-left,z-media-player .z-double-tap-ripple-right{position:absolute;top:0;bottom:0;width:35%;display:flex;align-items:center;z-index:11;pointer-events:none}z-media-player .z-double-tap-ripple-left:before,z-media-player .z-double-tap-ripple-right:before{position:absolute;inset:0;content:\"\";opacity:0;animation-duration:.72s;animation-fill-mode:forwards;animation-timing-function:cubic-bezier(.25,1,.5,1)}z-media-player .z-double-tap-ripple-left{left:0;justify-content:flex-start;padding-left:5%}z-media-player .z-double-tap-ripple-left:before{background:radial-gradient(circle at 0% 50%,#ffffff21,#fff0 70%);border-top-right-radius:100% 50%;border-bottom-right-radius:100% 50%;animation-name:zUiDoubleTapRippleLeft;transform-origin:left center}z-media-player .z-double-tap-ripple-left.z-skip-pulse-alt:before{animation-name:zUiDoubleTapRippleLeftAlt}z-media-player .z-double-tap-ripple-right{right:0;justify-content:flex-end;padding-right:5%}z-media-player .z-double-tap-ripple-right:before{background:radial-gradient(circle at 100% 50%,#ffffff21,#fff0 70%);border-top-left-radius:100% 50%;border-bottom-left-radius:100% 50%;animation-name:zUiDoubleTapRippleRight;transform-origin:right center}z-media-player .z-double-tap-ripple-right.z-skip-pulse-alt:before{animation-name:zUiDoubleTapRippleRightAlt}z-media-player .z-skip-overlay-content{position:relative;display:flex;align-items:center;justify-content:center;gap:6px;min-width:92px;color:#fff;font-family:inherit;font-size:18px;font-weight:700;line-height:1;text-shadow:0 2px 8px rgba(0,0,0,.75);-webkit-user-select:none;user-select:none}z-media-player .z-skip-overlay-number{min-width:42px;animation:zUiSkipNumberBump .22s cubic-bezier(.2,.85,.25,1) forwards;text-align:center;transform-origin:center}z-media-player .z-skip-pulse-alt .z-skip-overlay-number{animation-name:zUiSkipNumberBumpAlt}z-media-player .z-skip-overlay-arrows{display:inline-flex;align-items:center;color:#fffffff2;filter:drop-shadow(0 2px 6px rgba(0,0,0,.7))}@keyframes zUiDoubleTapRippleLeft{0%{opacity:0;transform:scaleX(.5);transform-origin:left center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:left center}}@keyframes zUiDoubleTapRippleLeftAlt{0%{opacity:0;transform:scaleX(.5);transform-origin:left center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:left center}}@keyframes zUiDoubleTapRippleRight{0%{opacity:0;transform:scaleX(.5);transform-origin:right center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:right center}}@keyframes zUiDoubleTapRippleRightAlt{0%{opacity:0;transform:scaleX(.5);transform-origin:right center}15%{opacity:1}80%{opacity:1}to{opacity:0;transform:scaleX(1);transform-origin:right center}}z-media-player .z-chevron-slide-left{animation:zUiChevronSlideLeft .72s cubic-bezier(.25,1,.5,1) forwards}z-media-player .z-chevron-slide-right{animation:zUiChevronSlideRight .72s cubic-bezier(.25,1,.5,1) forwards}z-media-player .z-skip-pulse-alt .z-chevron-slide-left{animation-name:zUiChevronSlideLeftAlt}z-media-player .z-skip-pulse-alt .z-chevron-slide-right{animation-name:zUiChevronSlideRightAlt}@keyframes zUiSkipNumberBump{0%{opacity:.78;transform:scale(.92)}55%{opacity:1;transform:scale(1.12)}to{opacity:1;transform:scale(1)}}@keyframes zUiSkipNumberBumpAlt{0%{opacity:.78;transform:scale(.92)}55%{opacity:1;transform:scale(1.12)}to{opacity:1;transform:scale(1)}}@keyframes zUiChevronSlideLeft{0%{opacity:0;transform:translate(8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(-12px)}}@keyframes zUiChevronSlideRight{0%{opacity:0;transform:translate(-8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(12px)}}@keyframes zUiChevronSlideLeftAlt{0%{opacity:0;transform:translate(8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(-12px)}}@keyframes zUiChevronSlideRightAlt{0%{opacity:0;transform:translate(-8px)}28%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(12px)}}z-media-player #z_media_player_volume_slider{-webkit-appearance:none;appearance:none;background:transparent}z-media-player #z_media_player_volume_slider::-webkit-slider-runnable-track{background:#ffffff40;height:3px;border-radius:9999px}z-media-player #z_media_player_volume_slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;background:#fff;height:10px;width:10px;border-radius:9999px;margin-top:-3.5px;transition:transform .15s ease}z-media-player #z_media_player_volume_slider:hover::-webkit-slider-thumb,z-media-player #z_media_player_volume_slider:focus-visible::-webkit-slider-thumb{transform:scale(1.3)}z-media-player #z_media_player_volume_slider::-moz-range-track{background:#ffffff40;height:3px;border-radius:9999px}z-media-player #z_media_player_volume_slider::-moz-range-thumb{background:#fff;height:10px;width:10px;border-radius:9999px;border:none;transition:transform .15s ease}z-media-player #z_media_player_volume_slider:hover::-moz-range-thumb,z-media-player #z_media_player_volume_slider:focus-visible::-moz-range-thumb{transform:scale(1.3)}.z-media-player-popover{background-color:#0000008c!important;border:none!important;box-shadow:0 0 0 1px #0000001a,0 1px 3px #00000026,0 1px 2px -1px #00000026!important;border-radius:8px!important;padding:0!important;transform-origin:bottom center}.z-media-player-popover:after{position:absolute;inset:0;z-index:10;border-radius:inherit;box-shadow:inset 0 1px #ffffff1a,inset 0 0 0 1px #ffffff0d;content:\"\";pointer-events:none}.z-media-player-popover[data-state=open]{animation:zUiMediaSettingsPopoverIn .16s cubic-bezier(.2,.85,.25,1) forwards}.z-media-player-popover[data-state=closed]{animation:zUiMediaSettingsPopoverOut .16s cubic-bezier(.4,0,.2,1) forwards!important}.z-media-player-popover .bg-popover{background-color:transparent!important;color:#e4e4e7!important}.z-media-player-popover .z-media-player-settings{overflow:hidden;transform-origin:bottom right;animation:zUiMediaSettingsPanelIn .16s cubic-bezier(.2,.85,.25,1)}.z-media-player-popover .z-media-player-settings-panel{gap:4px;transform-origin:top right;animation:zUiMediaSettingsPanelIn .14s cubic-bezier(.2,.85,.25,1);will-change:opacity,transform}.z-media-player-popover .z-media-player-settings.z-main-panel .z-media-player-settings-panel{animation-name:zUiMediaSettingsPanelFromLeft}.z-media-player-popover .z-media-player-settings.z-sub-panel .z-media-player-settings-panel{animation-name:zUiMediaSettingsPanelFromRight}.z-media-player-popover .z-media-player-settings-item,.z-media-player-popover .z-media-player-settings-back{position:relative;display:flex;width:100%;min-width:0;height:32px;min-height:32px;cursor:pointer;align-items:center;border:0;border-radius:4px;background-color:transparent;padding:0 8px;color:#e4e4e7;font-size:12px;font-weight:500;line-height:1;outline:none;transition:background-color .12s ease,color .12s ease}.z-media-player-popover .z-media-player-settings-item{justify-content:space-between;gap:12px;text-align:left}.z-media-player-popover .z-media-player-settings-item:before{position:absolute;top:7px;bottom:7px;left:0;width:2px;border-radius:9999px;background-color:#fff;content:\"\";opacity:0;transform:scaleY(.5);transition:opacity .12s ease,transform .12s ease}.z-media-player-popover .z-media-player-settings-item.z-active{background-color:#ffffff14;color:#fff;font-weight:600}.z-media-player-popover .z-media-player-settings-item.z-active:before{opacity:1;transform:scaleY(1)}.z-media-player-popover .z-media-player-settings-back{justify-content:flex-start;gap:8px;color:#f4f4f5;font-weight:600}.z-media-player-popover .z-media-player-settings-item:hover,.z-media-player-popover .z-media-player-settings-back:hover,.z-media-player-popover .z-media-player-settings-item:focus-visible,.z-media-player-popover .z-media-player-settings-back:focus-visible{background-color:#ffffff1a}@keyframes zUiMediaSettingsPopoverIn{0%{opacity:0;transform:translateY(6px) scale(.94)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes zUiMediaSettingsPopoverOut{0%{opacity:1;transform:translateY(0) scale(1)}to{opacity:0;transform:translateY(4px) scale(.96)}}@keyframes zUiMediaSettingsPanelIn{0%{opacity:0;transform:translateY(3px)}to{opacity:1;transform:translateY(0)}}@keyframes zUiMediaSettingsPanelFromRight{0%{opacity:0;transform:translate(8px)}to{opacity:1;transform:translate(0)}}@keyframes zUiMediaSettingsPanelFromLeft{0%{opacity:0;transform:translate(-5px)}to{opacity:1;transform:translate(0)}}\n"] }]
|
|
1015
|
-
}], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], zSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSrc", required: true }] }], zSources: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSources", required: false }] }], zPoster: [{ type: i0.Input, args: [{ isSignal: true, alias: "zPoster", required: false }] }], zAutoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "zAutoplay", required: false }] }], zLoop: [{ type: i0.Input, args: [{ isSignal: true, alias: "zLoop", required: false }] }], zMuted: [{ type: i0.Input, args: [{ isSignal: true, alias: "zMuted", required: false }] }], zVolume: [{ type: i0.Input, args: [{ isSignal: true, alias: "zVolume", required: false }] }], zSubtitles: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSubtitles", required: false }] }], zThumbnailsVtt: [{ type: i0.Input, args: [{ isSignal: true, alias: "zThumbnailsVtt", required: false }] }], zControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "zControls", required: false }] }], zLoaded: [{ type: i0.Output, args: ["zLoaded"] }], zError: [{ type: i0.Output, args: ["zError"] }], zPlay: [{ type: i0.Output, args: ["zPlay"] }], zTimeUpdate: [{ type: i0.Output, args: ["zTimeUpdate"] }], zVolumeChange: [{ type: i0.Output, args: ["zVolumeChange"] }], zFullscreenChange: [{ type: i0.Output, args: ["zFullscreenChange"] }], zTheaterModeChange: [{ type: i0.Output, args: ["zTheaterModeChange"] }], zMiniplayerChange: [{ type: i0.Output, args: ["zMiniplayerChange"] }], zQualityChange: [{ type: i0.Output, args: ["zQualityChange"] }], videoElement: [{ type: i0.ViewChild, args: ['videoElement', { isSignal: true }] }], timelineContainer: [{ type: i0.ViewChild, args: ['timelineContainer', { isSignal: true }] }], _settingsPopover: [{ type: i0.ViewChild, args: [i0.forwardRef(() => ZPopoverDirective), { isSignal: true }] }] } });
|
|
404
|
+
}, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "<media-player #playerElement>\n <media-provider>\n @for (subtitle of zConfig().subtitles ?? []; track subtitle.src) {\n <track\n kind=\"subtitles\"\n [label]=\"subtitle.label\"\n [src]=\"subtitle.src\"\n [srclang]=\"subtitle.srclang\"\n [default]=\"subtitle.default ?? false\" />\n }\n @if (zConfig().chaptersVtt) {\n <track kind=\"chapters\" [src]=\"zConfig().chaptersVtt!\" />\n }\n </media-provider>\n\n @if (isPlyrSkin()) {\n <media-plyr-layout #layoutElement></media-plyr-layout>\n } @else if (isAudioSource()) {\n <media-audio-layout #layoutElement></media-audio-layout>\n } @else {\n <media-video-layout #layoutElement></media-video-layout>\n }\n</media-player>\n", styles: ["@charset \"UTF-8\";z-media-player{display:flex;align-items:center;justify-content:center;width:100%;container-type:inline-size;--media-user-font-size: .75;--media-cue-padding-x: calc(var(--cue-font-size, 1em) * .4)}z-media-player [data-media-player][data-view-type=video]{width:auto;height:100%;max-width:100%;max-height:100%}\n"] }]
|
|
405
|
+
}], ctorParameters: () => [], propDecorators: { class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], zSources: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSources", required: true }] }], zConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "zConfig", required: false }] }], zSkin: [{ type: i0.Input, args: [{ isSignal: true, alias: "zSkin", required: false }] }], zDefaultLayoutConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "zDefaultLayoutConfig", required: false }] }], zPlyrLayoutConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "zPlyrLayoutConfig", required: false }] }], zStateChange: [{ type: i0.Output, args: ["zStateChange"] }], zError: [{ type: i0.Output, args: ["zError"] }], playerElement: [{ type: i0.ViewChild, args: ['playerElement', { isSignal: true }] }], layoutElement: [{ type: i0.ViewChild, args: ['layoutElement', { isSignal: true }] }] } });
|
|
1016
406
|
|
|
1017
407
|
/**
|
|
1018
408
|
* Generated bundle index. Do not edit.
|