ts-video-player 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +438 -0
  3. package/dist/cdn.d.ts +10 -0
  4. package/dist/cdn.d.ts.map +1 -0
  5. package/dist/cdn.min.js +1046 -0
  6. package/dist/cdn.min.js.map +54 -0
  7. package/dist/cdn.test.d.ts +2 -0
  8. package/dist/cdn.test.d.ts.map +1 -0
  9. package/dist/chunk-36x4yhjn.js +765 -0
  10. package/dist/chunk-36x4yhjn.js.map +12 -0
  11. package/dist/chunk-eajts2jv.js +4243 -0
  12. package/dist/chunk-eajts2jv.js.map +42 -0
  13. package/dist/chunk-z1ehbz9f.js +380 -0
  14. package/dist/chunk-z1ehbz9f.js.map +19 -0
  15. package/dist/config.d.ts +9 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/core/audio-gain.d.ts +67 -0
  18. package/dist/core/audio-gain.d.ts.map +1 -0
  19. package/dist/core/caption-styles.d.ts +121 -0
  20. package/dist/core/caption-styles.d.ts.map +1 -0
  21. package/dist/core/chapters.d.ts +150 -0
  22. package/dist/core/chapters.d.ts.map +1 -0
  23. package/dist/core/error-overlay.d.ts +71 -0
  24. package/dist/core/error-overlay.d.ts.map +1 -0
  25. package/dist/core/events.d.ts +119 -0
  26. package/dist/core/events.d.ts.map +1 -0
  27. package/dist/core/features.d.ts +64 -0
  28. package/dist/core/features.d.ts.map +1 -0
  29. package/dist/core/i18n.d.ts +132 -0
  30. package/dist/core/i18n.d.ts.map +1 -0
  31. package/dist/core/index.d.ts +37 -0
  32. package/dist/core/index.d.ts.map +1 -0
  33. package/dist/core/live.d.ts +130 -0
  34. package/dist/core/live.d.ts.map +1 -0
  35. package/dist/core/media-session.d.ts +38 -0
  36. package/dist/core/media-session.d.ts.map +1 -0
  37. package/dist/core/orientation.d.ts +22 -0
  38. package/dist/core/orientation.d.ts.map +1 -0
  39. package/dist/core/popover.d.ts +42 -0
  40. package/dist/core/popover.d.ts.map +1 -0
  41. package/dist/core/remote-playback.d.ts +195 -0
  42. package/dist/core/remote-playback.d.ts.map +1 -0
  43. package/dist/core/settings-menu.d.ts +132 -0
  44. package/dist/core/settings-menu.d.ts.map +1 -0
  45. package/dist/core/spinner.d.ts +67 -0
  46. package/dist/core/spinner.d.ts.map +1 -0
  47. package/dist/core/state.d.ts +142 -0
  48. package/dist/core/state.d.ts.map +1 -0
  49. package/dist/core/text-tracks.d.ts +162 -0
  50. package/dist/core/text-tracks.d.ts.map +1 -0
  51. package/dist/core/themes.d.ts +213 -0
  52. package/dist/core/themes.d.ts.map +1 -0
  53. package/dist/core/thumbnails.d.ts +124 -0
  54. package/dist/core/thumbnails.d.ts.map +1 -0
  55. package/dist/core/tooltips.d.ts +100 -0
  56. package/dist/core/tooltips.d.ts.map +1 -0
  57. package/dist/elements/base.d.ts +3 -0
  58. package/dist/elements/base.d.ts.map +1 -0
  59. package/dist/elements/index.d.ts +33 -0
  60. package/dist/elements/index.d.ts.map +1 -0
  61. package/dist/elements/index.js +55 -0
  62. package/dist/elements/index.js.map +9 -0
  63. package/dist/elements/media-airplay-button.d.ts +13 -0
  64. package/dist/elements/media-airplay-button.d.ts.map +1 -0
  65. package/dist/elements/media-error-display.d.ts +13 -0
  66. package/dist/elements/media-error-display.d.ts.map +1 -0
  67. package/dist/elements/media-fullscreen-button.d.ts +13 -0
  68. package/dist/elements/media-fullscreen-button.d.ts.map +1 -0
  69. package/dist/elements/media-live-button.d.ts +13 -0
  70. package/dist/elements/media-live-button.d.ts.map +1 -0
  71. package/dist/elements/media-mute-button.d.ts +13 -0
  72. package/dist/elements/media-mute-button.d.ts.map +1 -0
  73. package/dist/elements/media-pip-button.d.ts +13 -0
  74. package/dist/elements/media-pip-button.d.ts.map +1 -0
  75. package/dist/elements/media-play-button.d.ts +14 -0
  76. package/dist/elements/media-play-button.d.ts.map +1 -0
  77. package/dist/elements/media-playback-rate-button.d.ts +10 -0
  78. package/dist/elements/media-playback-rate-button.d.ts.map +1 -0
  79. package/dist/elements/media-preview-time.d.ts +18 -0
  80. package/dist/elements/media-preview-time.d.ts.map +1 -0
  81. package/dist/elements/media-progress-bar.d.ts +16 -0
  82. package/dist/elements/media-progress-bar.d.ts.map +1 -0
  83. package/dist/elements/media-remote-playback-button.d.ts +16 -0
  84. package/dist/elements/media-remote-playback-button.d.ts.map +1 -0
  85. package/dist/elements/media-seek-button.d.ts +9 -0
  86. package/dist/elements/media-seek-button.d.ts.map +1 -0
  87. package/dist/elements/media-settings-menu.d.ts +32 -0
  88. package/dist/elements/media-settings-menu.d.ts.map +1 -0
  89. package/dist/elements/media-time-display.d.ts +21 -0
  90. package/dist/elements/media-time-display.d.ts.map +1 -0
  91. package/dist/elements/media-time-group.d.ts +22 -0
  92. package/dist/elements/media-time-group.d.ts.map +1 -0
  93. package/dist/elements/media-time-separator.d.ts +13 -0
  94. package/dist/elements/media-time-separator.d.ts.map +1 -0
  95. package/dist/elements/media-volume-slider.d.ts +16 -0
  96. package/dist/elements/media-volume-slider.d.ts.map +1 -0
  97. package/dist/elements/utils.d.ts +27 -0
  98. package/dist/elements/utils.d.ts.map +1 -0
  99. package/dist/elements/video-player.d.ts +23 -0
  100. package/dist/elements/video-player.d.ts.map +1 -0
  101. package/dist/elements/video-skin.d.ts +23 -0
  102. package/dist/elements/video-skin.d.ts.map +1 -0
  103. package/dist/features/captions.d.ts +8 -0
  104. package/dist/features/captions.d.ts.map +1 -0
  105. package/dist/features/create-player.d.ts +45 -0
  106. package/dist/features/create-player.d.ts.map +1 -0
  107. package/dist/features/fullscreen.d.ts +8 -0
  108. package/dist/features/fullscreen.d.ts.map +1 -0
  109. package/dist/features/gestures.d.ts +8 -0
  110. package/dist/features/gestures.d.ts.map +1 -0
  111. package/dist/features/index.d.ts +26 -0
  112. package/dist/features/index.d.ts.map +1 -0
  113. package/dist/features/index.js +31 -0
  114. package/dist/features/index.js.map +9 -0
  115. package/dist/features/keyboard.d.ts +8 -0
  116. package/dist/features/keyboard.d.ts.map +1 -0
  117. package/dist/features/pip.d.ts +8 -0
  118. package/dist/features/pip.d.ts.map +1 -0
  119. package/dist/features/playback.d.ts +8 -0
  120. package/dist/features/playback.d.ts.map +1 -0
  121. package/dist/features/quality.d.ts +8 -0
  122. package/dist/features/quality.d.ts.map +1 -0
  123. package/dist/features/types.d.ts +26 -0
  124. package/dist/features/types.d.ts.map +1 -0
  125. package/dist/features/volume.d.ts +8 -0
  126. package/dist/features/volume.d.ts.map +1 -0
  127. package/dist/index.d.ts +84 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +8149 -0
  130. package/dist/index.js.map +31 -0
  131. package/dist/layouts/index.d.ts +58 -0
  132. package/dist/layouts/index.d.ts.map +1 -0
  133. package/dist/player.d.ts +90 -0
  134. package/dist/player.d.ts.map +1 -0
  135. package/dist/plugins/ads.d.ts +311 -0
  136. package/dist/plugins/ads.d.ts.map +1 -0
  137. package/dist/plugins/analytics.d.ts +203 -0
  138. package/dist/plugins/analytics.d.ts.map +1 -0
  139. package/dist/plugins/end-screen.d.ts +184 -0
  140. package/dist/plugins/end-screen.d.ts.map +1 -0
  141. package/dist/plugins/index.d.ts +13 -0
  142. package/dist/plugins/index.d.ts.map +1 -0
  143. package/dist/plugins/index.js +20 -0
  144. package/dist/plugins/index.js.map +9 -0
  145. package/dist/plugins/skip-segments.d.ts +159 -0
  146. package/dist/plugins/skip-segments.d.ts.map +1 -0
  147. package/dist/plugins/watermark.d.ts +157 -0
  148. package/dist/plugins/watermark.d.ts.map +1 -0
  149. package/dist/providers/base.d.ts +56 -0
  150. package/dist/providers/base.d.ts.map +1 -0
  151. package/dist/providers/dash.d.ts +94 -0
  152. package/dist/providers/dash.d.ts.map +1 -0
  153. package/dist/providers/hls.d.ts +182 -0
  154. package/dist/providers/hls.d.ts.map +1 -0
  155. package/dist/providers/html5.d.ts +64 -0
  156. package/dist/providers/html5.d.ts.map +1 -0
  157. package/dist/providers/index.d.ts +41 -0
  158. package/dist/providers/index.d.ts.map +1 -0
  159. package/dist/providers/vimeo.d.ts +157 -0
  160. package/dist/providers/vimeo.d.ts.map +1 -0
  161. package/dist/providers/youtube.d.ts +134 -0
  162. package/dist/providers/youtube.d.ts.map +1 -0
  163. package/dist/stx/index.d.ts +59 -0
  164. package/dist/stx/index.d.ts.map +1 -0
  165. package/dist/stx/index.js +480 -0
  166. package/dist/stx/index.js.map +11 -0
  167. package/dist/types.d.ts +724 -0
  168. package/dist/types.d.ts.map +1 -0
  169. package/dist/ui/components.d.ts +121 -0
  170. package/dist/ui/components.d.ts.map +1 -0
  171. package/dist/ui/index.d.ts +9 -0
  172. package/dist/ui/index.d.ts.map +1 -0
  173. package/dist/ui/index.js +16 -0
  174. package/dist/ui/index.js.map +9 -0
  175. package/package.json +109 -0
  176. package/src/cdn.test.ts +10 -0
  177. package/src/cdn.ts +60 -0
  178. package/src/config.ts +25 -0
  179. package/src/core/audio-gain.ts +171 -0
  180. package/src/core/caption-styles.ts +568 -0
  181. package/src/core/chapters.ts +551 -0
  182. package/src/core/error-overlay.ts +354 -0
  183. package/src/core/events.ts +552 -0
  184. package/src/core/features.ts +287 -0
  185. package/src/core/i18n.ts +590 -0
  186. package/src/core/index.ts +249 -0
  187. package/src/core/live.ts +472 -0
  188. package/src/core/media-session.ts +137 -0
  189. package/src/core/orientation.ts +57 -0
  190. package/src/core/popover.ts +143 -0
  191. package/src/core/remote-playback.ts +661 -0
  192. package/src/core/settings-menu.ts +630 -0
  193. package/src/core/spinner.ts +310 -0
  194. package/src/core/state.ts +476 -0
  195. package/src/core/text-tracks.ts +599 -0
  196. package/src/core/themes.ts +607 -0
  197. package/src/core/thumbnails.ts +457 -0
  198. package/src/core/tooltips.ts +370 -0
  199. package/src/elements/base.ts +2 -0
  200. package/src/elements/index.ts +82 -0
  201. package/src/elements/media-airplay-button.ts +78 -0
  202. package/src/elements/media-error-display.ts +47 -0
  203. package/src/elements/media-fullscreen-button.ts +72 -0
  204. package/src/elements/media-live-button.ts +65 -0
  205. package/src/elements/media-mute-button.ts +69 -0
  206. package/src/elements/media-pip-button.ts +66 -0
  207. package/src/elements/media-play-button.ts +69 -0
  208. package/src/elements/media-playback-rate-button.ts +59 -0
  209. package/src/elements/media-preview-time.ts +75 -0
  210. package/src/elements/media-progress-bar.ts +200 -0
  211. package/src/elements/media-remote-playback-button.ts +108 -0
  212. package/src/elements/media-seek-button.ts +50 -0
  213. package/src/elements/media-settings-menu.ts +302 -0
  214. package/src/elements/media-time-display.ts +99 -0
  215. package/src/elements/media-time-group.ts +41 -0
  216. package/src/elements/media-time-separator.ts +35 -0
  217. package/src/elements/media-volume-slider.ts +140 -0
  218. package/src/elements/utils.ts +82 -0
  219. package/src/elements/video-player.ts +175 -0
  220. package/src/elements/video-skin.ts +127 -0
  221. package/src/features/captions.ts +56 -0
  222. package/src/features/create-player.ts +91 -0
  223. package/src/features/fullscreen.ts +44 -0
  224. package/src/features/gestures.ts +44 -0
  225. package/src/features/index.ts +44 -0
  226. package/src/features/keyboard.ts +89 -0
  227. package/src/features/pip.ts +47 -0
  228. package/src/features/playback.ts +68 -0
  229. package/src/features/quality.ts +19 -0
  230. package/src/features/types.ts +28 -0
  231. package/src/features/volume.ts +46 -0
  232. package/src/index.ts +635 -0
  233. package/src/layouts/index.ts +672 -0
  234. package/src/player.ts +821 -0
  235. package/src/plugins/ads.ts +1045 -0
  236. package/src/plugins/analytics.ts +673 -0
  237. package/src/plugins/end-screen.ts +793 -0
  238. package/src/plugins/index.ts +61 -0
  239. package/src/plugins/skip-segments.ts +537 -0
  240. package/src/plugins/watermark.ts +537 -0
  241. package/src/providers/base.ts +197 -0
  242. package/src/providers/dash.ts +588 -0
  243. package/src/providers/hls.ts +624 -0
  244. package/src/providers/html5.ts +473 -0
  245. package/src/providers/index.ts +97 -0
  246. package/src/providers/vimeo.ts +533 -0
  247. package/src/providers/youtube.ts +542 -0
  248. package/src/stx/index.ts +606 -0
  249. package/src/types.ts +874 -0
  250. package/src/ui/components.ts +910 -0
  251. package/src/ui/index.ts +29 -0
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Tooltips System
3
+ *
4
+ * Tooltips for player controls with smart positioning.
5
+ *
6
+ * @module core/tooltips
7
+ */
8
+
9
+ // =============================================================================
10
+ // Types
11
+ // =============================================================================
12
+
13
+ export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right'
14
+
15
+ export interface TooltipConfig {
16
+ /** Tooltip placement */
17
+ placement?: TooltipPlacement
18
+ /** Offset from target in pixels */
19
+ offset?: number
20
+ /** Show delay in ms */
21
+ showDelay?: number
22
+ /** Hide delay in ms */
23
+ hideDelay?: number
24
+ /** Container for boundary detection */
25
+ container?: HTMLElement
26
+ /** Custom class name */
27
+ className?: string
28
+ }
29
+
30
+ // =============================================================================
31
+ // Tooltip Component
32
+ // =============================================================================
33
+
34
+ export class Tooltip {
35
+ private element: HTMLElement
36
+ private target: HTMLElement | null = null
37
+ private config: Required<TooltipConfig>
38
+ private showTimeout: ReturnType<typeof setTimeout> | null = null
39
+ private hideTimeout: ReturnType<typeof setTimeout> | null = null
40
+ private visible = false
41
+
42
+ constructor(config: TooltipConfig = {}) {
43
+ this.config = {
44
+ placement: config.placement || 'top',
45
+ offset: config.offset ?? 8,
46
+ showDelay: config.showDelay ?? 300,
47
+ hideDelay: config.hideDelay ?? 0,
48
+ container: config.container || document.body,
49
+ className: config.className || '',
50
+ }
51
+
52
+ this.element = document.createElement('div')
53
+ this.element.className = `tsvp-tooltip ${this.config.className}`.trim()
54
+ this.element.setAttribute('role', 'tooltip')
55
+ this.element.style.cssText = `
56
+ position: absolute;
57
+ z-index: 1000;
58
+ padding: 6px 10px;
59
+ background: var(--tsvp-controls-tooltip-bg, rgba(0, 0, 0, 0.9));
60
+ color: var(--tsvp-controls-tooltip-text, #ffffff);
61
+ font-size: var(--tsvp-font-size-sm, 12px);
62
+ font-family: var(--tsvp-font-family, sans-serif);
63
+ border-radius: var(--tsvp-radius-sm, 4px);
64
+ white-space: nowrap;
65
+ pointer-events: none;
66
+ opacity: 0;
67
+ transform: scale(0.9);
68
+ transition: opacity 150ms ease, transform 150ms ease;
69
+ box-shadow: var(--tsvp-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.3));
70
+ `
71
+
72
+ this.hide()
73
+ }
74
+
75
+ /**
76
+ * Attach tooltip to a target element
77
+ */
78
+ attach(target: HTMLElement, text: string): void {
79
+ this.target = target
80
+ this.element.textContent = text
81
+
82
+ // Add to DOM if not already
83
+ if (!this.element.parentElement) {
84
+ this.config.container.appendChild(this.element)
85
+ }
86
+
87
+ // Event listeners
88
+ target.addEventListener('mouseenter', this.handleMouseEnter)
89
+ target.addEventListener('mouseleave', this.handleMouseLeave)
90
+ target.addEventListener('focus', this.handleFocus)
91
+ target.addEventListener('blur', this.handleBlur)
92
+ }
93
+
94
+ /**
95
+ * Detach from target
96
+ */
97
+ detach(): void {
98
+ if (this.target) {
99
+ this.target.removeEventListener('mouseenter', this.handleMouseEnter)
100
+ this.target.removeEventListener('mouseleave', this.handleMouseLeave)
101
+ this.target.removeEventListener('focus', this.handleFocus)
102
+ this.target.removeEventListener('blur', this.handleBlur)
103
+ this.target = null
104
+ }
105
+ this.hide()
106
+ }
107
+
108
+ /**
109
+ * Update tooltip text
110
+ */
111
+ setText(text: string): void {
112
+ this.element.textContent = text
113
+ if (this.visible) {
114
+ this.position()
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Show tooltip
120
+ */
121
+ show(): void {
122
+ if (this.hideTimeout) {
123
+ clearTimeout(this.hideTimeout)
124
+ this.hideTimeout = null
125
+ }
126
+
127
+ if (this.visible) return
128
+
129
+ this.showTimeout = setTimeout(() => {
130
+ this.visible = true
131
+ this.element.style.display = 'block'
132
+ this.position()
133
+
134
+ // Trigger animation
135
+ requestAnimationFrame(() => {
136
+ this.element.style.opacity = '1'
137
+ this.element.style.transform = 'scale(1)'
138
+ })
139
+ }, this.config.showDelay)
140
+ }
141
+
142
+ /**
143
+ * Hide tooltip
144
+ */
145
+ hide(): void {
146
+ if (this.showTimeout) {
147
+ clearTimeout(this.showTimeout)
148
+ this.showTimeout = null
149
+ }
150
+
151
+ if (!this.visible) {
152
+ this.element.style.display = 'none'
153
+ return
154
+ }
155
+
156
+ this.hideTimeout = setTimeout(() => {
157
+ this.visible = false
158
+ this.element.style.opacity = '0'
159
+ this.element.style.transform = 'scale(0.9)'
160
+
161
+ setTimeout(() => {
162
+ if (!this.visible) {
163
+ this.element.style.display = 'none'
164
+ }
165
+ }, 150)
166
+ }, this.config.hideDelay)
167
+ }
168
+
169
+ /**
170
+ * Position tooltip relative to target
171
+ */
172
+ private position(): void {
173
+ if (!this.target) return
174
+
175
+ const targetRect = this.target.getBoundingClientRect()
176
+ const tooltipRect = this.element.getBoundingClientRect()
177
+ const containerRect = this.config.container.getBoundingClientRect()
178
+
179
+ let top = 0
180
+ let left = 0
181
+
182
+ switch (this.config.placement) {
183
+ case 'top':
184
+ top = targetRect.top - tooltipRect.height - this.config.offset
185
+ left = targetRect.left + (targetRect.width - tooltipRect.width) / 2
186
+ break
187
+ case 'bottom':
188
+ top = targetRect.bottom + this.config.offset
189
+ left = targetRect.left + (targetRect.width - tooltipRect.width) / 2
190
+ break
191
+ case 'left':
192
+ top = targetRect.top + (targetRect.height - tooltipRect.height) / 2
193
+ left = targetRect.left - tooltipRect.width - this.config.offset
194
+ break
195
+ case 'right':
196
+ top = targetRect.top + (targetRect.height - tooltipRect.height) / 2
197
+ left = targetRect.right + this.config.offset
198
+ break
199
+ }
200
+
201
+ // Keep within container bounds
202
+ const minLeft = containerRect.left + 4
203
+ const maxLeft = containerRect.right - tooltipRect.width - 4
204
+ const minTop = containerRect.top + 4
205
+ const maxTop = containerRect.bottom - tooltipRect.height - 4
206
+
207
+ left = Math.max(minLeft, Math.min(maxLeft, left))
208
+ top = Math.max(minTop, Math.min(maxTop, top))
209
+
210
+ // Adjust for scroll
211
+ left += window.scrollX
212
+ top += window.scrollY
213
+
214
+ this.element.style.left = `${left}px`
215
+ this.element.style.top = `${top}px`
216
+ }
217
+
218
+ private handleMouseEnter = (): void => {
219
+ this.show()
220
+ }
221
+
222
+ private handleMouseLeave = (): void => {
223
+ this.hide()
224
+ }
225
+
226
+ private handleFocus = (): void => {
227
+ this.show()
228
+ }
229
+
230
+ private handleBlur = (): void => {
231
+ this.hide()
232
+ }
233
+
234
+ /**
235
+ * Get element
236
+ */
237
+ getElement(): HTMLElement {
238
+ return this.element
239
+ }
240
+
241
+ /**
242
+ * Check if visible
243
+ */
244
+ isVisible(): boolean {
245
+ return this.visible
246
+ }
247
+
248
+ /**
249
+ * Destroy tooltip
250
+ */
251
+ destroy(): void {
252
+ this.detach()
253
+ this.element.remove()
254
+ }
255
+ }
256
+
257
+ // =============================================================================
258
+ // Tooltip Manager
259
+ // =============================================================================
260
+
261
+ export class TooltipManager {
262
+ private tooltips = new Map<HTMLElement, Tooltip>()
263
+ private config: TooltipConfig
264
+
265
+ constructor(config: TooltipConfig = {}) {
266
+ this.config = config
267
+ }
268
+
269
+ /**
270
+ * Add tooltip to an element
271
+ */
272
+ add(target: HTMLElement, text: string, config?: Partial<TooltipConfig>): Tooltip {
273
+ // Remove existing tooltip if any
274
+ this.remove(target)
275
+
276
+ const tooltip = new Tooltip({ ...this.config, ...config })
277
+ tooltip.attach(target, text)
278
+ this.tooltips.set(target, tooltip)
279
+
280
+ return tooltip
281
+ }
282
+
283
+ /**
284
+ * Update tooltip text
285
+ */
286
+ update(target: HTMLElement, text: string): void {
287
+ const tooltip = this.tooltips.get(target)
288
+ if (tooltip) {
289
+ tooltip.setText(text)
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Remove tooltip from element
295
+ */
296
+ remove(target: HTMLElement): void {
297
+ const tooltip = this.tooltips.get(target)
298
+ if (tooltip) {
299
+ tooltip.destroy()
300
+ this.tooltips.delete(target)
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Hide all tooltips
306
+ */
307
+ hideAll(): void {
308
+ this.tooltips.forEach(tooltip => tooltip.hide())
309
+ }
310
+
311
+ /**
312
+ * Destroy all tooltips
313
+ */
314
+ destroy(): void {
315
+ this.tooltips.forEach(tooltip => tooltip.destroy())
316
+ this.tooltips.clear()
317
+ }
318
+ }
319
+
320
+ // =============================================================================
321
+ // Factory
322
+ // =============================================================================
323
+
324
+ export function createTooltip(config?: TooltipConfig): Tooltip {
325
+ return new Tooltip(config)
326
+ }
327
+
328
+ export function createTooltipManager(config?: TooltipConfig): TooltipManager {
329
+ return new TooltipManager(config)
330
+ }
331
+
332
+ // =============================================================================
333
+ // CSS Styles
334
+ // =============================================================================
335
+
336
+ export const TOOLTIP_STYLES = `
337
+ .tsvp-tooltip {
338
+ position: absolute;
339
+ z-index: 1000;
340
+ padding: 6px 10px;
341
+ background: var(--tsvp-controls-tooltip-bg, rgba(0, 0, 0, 0.9));
342
+ color: var(--tsvp-controls-tooltip-text, #ffffff);
343
+ font-size: var(--tsvp-font-size-sm, 12px);
344
+ font-family: var(--tsvp-font-family, sans-serif);
345
+ border-radius: var(--tsvp-radius-sm, 4px);
346
+ white-space: nowrap;
347
+ pointer-events: none;
348
+ box-shadow: var(--tsvp-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.3));
349
+ }
350
+
351
+ .tsvp-tooltip::after {
352
+ content: '';
353
+ position: absolute;
354
+ border: 6px solid transparent;
355
+ }
356
+
357
+ .tsvp-tooltip--top::after {
358
+ top: 100%;
359
+ left: 50%;
360
+ transform: translateX(-50%);
361
+ border-top-color: var(--tsvp-controls-tooltip-bg, rgba(0, 0, 0, 0.9));
362
+ }
363
+
364
+ .tsvp-tooltip--bottom::after {
365
+ bottom: 100%;
366
+ left: 50%;
367
+ transform: translateX(-50%);
368
+ border-bottom-color: var(--tsvp-controls-tooltip-bg, rgba(0, 0, 0, 0.9));
369
+ }
370
+ `
@@ -0,0 +1,2 @@
1
+ /** Browser HTMLElement base with an inert server-side constructor for import-safe SSR. */
2
+ export const HTMLElementBase: typeof HTMLElement = globalThis.HTMLElement ?? class {} as typeof HTMLElement
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ts-video-player Custom Elements
3
+ *
4
+ * Web Components for building video player interfaces.
5
+ *
6
+ * @module elements
7
+ */
8
+
9
+ export { VideoPlayerElement } from './video-player'
10
+ export { VideoSkin } from './video-skin'
11
+ export { MediaPlayButton } from './media-play-button'
12
+ export { MediaMuteButton } from './media-mute-button'
13
+ export { MediaFullscreenButton } from './media-fullscreen-button'
14
+ export { MediaPipButton } from './media-pip-button'
15
+ export { MediaTimeDisplay } from './media-time-display'
16
+ export { MediaTimeGroup } from './media-time-group'
17
+ export { MediaTimeSeparator } from './media-time-separator'
18
+ export { MediaPreviewTime } from './media-preview-time'
19
+ export { MediaProgressBar } from './media-progress-bar'
20
+ export { MediaVolumeSlider } from './media-volume-slider'
21
+ export { MediaSettingsMenu } from './media-settings-menu'
22
+ export { MediaSeekButton } from './media-seek-button'
23
+ export { MediaPlaybackRateButton } from './media-playback-rate-button'
24
+ export { MediaAirplayButton } from './media-airplay-button'
25
+ export { MediaLiveButton } from './media-live-button'
26
+ export { MediaErrorDisplay } from './media-error-display'
27
+ export { MediaRemotePlaybackButton } from './media-remote-playback-button'
28
+ export { formatTime, formatTimePhrase, toISODuration, resolvePlayer } from './utils'
29
+
30
+ import { VideoPlayerElement } from './video-player'
31
+ import { VideoSkin } from './video-skin'
32
+ import { MediaPlayButton } from './media-play-button'
33
+ import { MediaMuteButton } from './media-mute-button'
34
+ import { MediaFullscreenButton } from './media-fullscreen-button'
35
+ import { MediaPipButton } from './media-pip-button'
36
+ import { MediaTimeDisplay } from './media-time-display'
37
+ import { MediaTimeGroup } from './media-time-group'
38
+ import { MediaTimeSeparator } from './media-time-separator'
39
+ import { MediaPreviewTime } from './media-preview-time'
40
+ import { MediaProgressBar } from './media-progress-bar'
41
+ import { MediaVolumeSlider } from './media-volume-slider'
42
+ import { MediaSettingsMenu } from './media-settings-menu'
43
+ import { MediaSeekButton } from './media-seek-button'
44
+ import { MediaPlaybackRateButton } from './media-playback-rate-button'
45
+ import { MediaAirplayButton } from './media-airplay-button'
46
+ import { MediaLiveButton } from './media-live-button'
47
+ import { MediaErrorDisplay } from './media-error-display'
48
+ import { MediaRemotePlaybackButton } from './media-remote-playback-button'
49
+
50
+ const elements = [
51
+ ['video-player', VideoPlayerElement],
52
+ ['video-skin', VideoSkin],
53
+ ['media-play-button', MediaPlayButton],
54
+ ['media-mute-button', MediaMuteButton],
55
+ ['media-fullscreen-button', MediaFullscreenButton],
56
+ ['media-pip-button', MediaPipButton],
57
+ ['media-time-display', MediaTimeDisplay],
58
+ ['media-time-group', MediaTimeGroup],
59
+ ['media-time-separator', MediaTimeSeparator],
60
+ ['media-preview-time', MediaPreviewTime],
61
+ ['media-progress-bar', MediaProgressBar],
62
+ ['media-volume-slider', MediaVolumeSlider],
63
+ ['media-settings-menu', MediaSettingsMenu],
64
+ ['media-seek-button', MediaSeekButton],
65
+ ['media-playback-rate-button', MediaPlaybackRateButton],
66
+ ['media-airplay-button', MediaAirplayButton],
67
+ ['media-live-button', MediaLiveButton],
68
+ ['media-error-display', MediaErrorDisplay],
69
+ ['media-remote-playback-button', MediaRemotePlaybackButton],
70
+ ] as const
71
+
72
+ /**
73
+ * Register all custom elements.
74
+ * Safe to call multiple times — skips already-defined elements.
75
+ */
76
+ export function registerElements(): void {
77
+ for (const [name, ctor] of elements) {
78
+ if (!customElements.get(name)) {
79
+ customElements.define(name, ctor)
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * ts-video-player <media-airplay-button> Custom Element
3
+ *
4
+ * @module elements/media-airplay-button
5
+ */
6
+
7
+ import { HTMLElementBase } from './base'
8
+ import { resolvePlayer } from './utils'
9
+
10
+ const AIRPLAY_ICON = 'M6.2 19h11.6L12 13.2 6.2 19zM19 3H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h2v-2H5V5h14v10h-2v2h2a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z'
11
+
12
+ interface AirPlayVideo extends HTMLVideoElement {
13
+ webkitShowPlaybackTargetPicker?: () => void
14
+ webkitCurrentPlaybackTargetIsWireless?: boolean
15
+ }
16
+
17
+ export class MediaAirplayButton extends HTMLElementBase {
18
+ private cleanup: (() => void) | null = null
19
+
20
+ connectedCallback(): void {
21
+ if (!this.shadowRoot) {
22
+ const shadow = this.attachShadow({ mode: 'open' })
23
+ shadow.innerHTML = `
24
+ <style>
25
+ :host { display: none; }
26
+ :host([available]) { display: inline-flex; }
27
+ button { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 8px; background: transparent; border: 0; color: inherit; cursor: pointer; }
28
+ button:hover { opacity: 0.8; }
29
+ :host([connected]) button { color: var(--media-airplay-active-color, #38bdf8); }
30
+ </style>
31
+ <button part="button" type="button" aria-label="Play on AirPlay">
32
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon"><path d="${AIRPLAY_ICON}"/></svg>
33
+ </button>
34
+ `
35
+ }
36
+ queueMicrotask(() => this.attach())
37
+ }
38
+
39
+ disconnectedCallback(): void {
40
+ this.cleanup?.()
41
+ this.cleanup = null
42
+ }
43
+
44
+ private attach(): void {
45
+ const player = resolvePlayer(this)
46
+ const button = this.shadowRoot?.querySelector('button')
47
+ if (!player || !button) return
48
+
49
+ let video: AirPlayVideo | null = null
50
+ const availability = (): void => update()
51
+ const update = (): void => {
52
+ const media = player.provider?.mediaElement
53
+ const nextVideo = media instanceof HTMLVideoElement ? media as AirPlayVideo : null
54
+ if (nextVideo !== video) {
55
+ video?.removeEventListener('webkitplaybacktargetavailabilitychanged', availability)
56
+ video?.removeEventListener('webkitcurrentplaybacktargetiswirelesschanged', availability)
57
+ nextVideo?.addEventListener('webkitplaybacktargetavailabilitychanged', availability)
58
+ nextVideo?.addEventListener('webkitcurrentplaybacktargetiswirelesschanged', availability)
59
+ video = nextVideo
60
+ }
61
+ const available = typeof video?.webkitShowPlaybackTargetPicker === 'function'
62
+ this.toggleAttribute('available', available)
63
+ this.toggleAttribute('connected', video?.webkitCurrentPlaybackTargetIsWireless === true)
64
+ button.setAttribute('aria-pressed', String(video?.webkitCurrentPlaybackTargetIsWireless === true))
65
+ }
66
+ const click = (): void => video?.webkitShowPlaybackTargetPicker?.()
67
+ button.addEventListener('click', click)
68
+ const unsubscribe = player.subscribe(update)
69
+ update()
70
+
71
+ this.cleanup = () => {
72
+ unsubscribe()
73
+ button.removeEventListener('click', click)
74
+ video?.removeEventListener('webkitplaybacktargetavailabilitychanged', availability)
75
+ video?.removeEventListener('webkitcurrentplaybacktargetiswirelesschanged', availability)
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ts-video-player <media-error-display> Custom Element
3
+ *
4
+ * @module elements/media-error-display
5
+ */
6
+
7
+ import { HTMLElementBase } from './base'
8
+ import { resolvePlayer } from './utils'
9
+
10
+ export class MediaErrorDisplay extends HTMLElementBase {
11
+ private cleanup: (() => void) | null = null
12
+
13
+ connectedCallback(): void {
14
+ if (!this.shadowRoot) {
15
+ const shadow = this.attachShadow({ mode: 'open' })
16
+ shadow.innerHTML = `
17
+ <style>
18
+ :host { display: none; position: absolute; inset: 0; z-index: 5; place-items: center; padding: 24px; background: rgba(0, 0, 0, 0.76); color: white; text-align: center; }
19
+ :host([open]) { display: grid; }
20
+ p { max-width: 42rem; margin: 0; font: 500 14px/1.5 system-ui, sans-serif; }
21
+ </style>
22
+ <p part="message" role="alert" aria-live="assertive"></p>
23
+ `
24
+ }
25
+ queueMicrotask(() => this.attach())
26
+ }
27
+
28
+ disconnectedCallback(): void {
29
+ this.cleanup?.()
30
+ this.cleanup = null
31
+ }
32
+
33
+ private attach(): void {
34
+ const player = resolvePlayer(this)
35
+ const message = this.shadowRoot?.querySelector('p')
36
+ if (!player || !message) return
37
+ const update = (): void => {
38
+ const error = player.state.error
39
+ this.toggleAttribute('open', !!error)
40
+ message.textContent = error?.message || ''
41
+ if (error?.code !== undefined) this.setAttribute('data-code', String(error.code))
42
+ else this.removeAttribute('data-code')
43
+ }
44
+ this.cleanup = player.subscribe(update)
45
+ update()
46
+ }
47
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * ts-video-player <media-fullscreen-button> Custom Element
3
+ *
4
+ * @module elements/media-fullscreen-button
5
+ */
6
+
7
+ import { resolvePlayer } from './utils'
8
+
9
+ const ENTER_FS = 'M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z'
10
+ const EXIT_FS = 'M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z'
11
+
12
+ import { HTMLElementBase } from './base'
13
+
14
+ export class MediaFullscreenButton extends HTMLElementBase {
15
+ private _cleanup: (() => void) | null = null
16
+
17
+ connectedCallback(): void {
18
+ if (!this.shadowRoot) {
19
+ const shadow = this.attachShadow({ mode: 'open' })
20
+ shadow.innerHTML = `
21
+ <style>
22
+ :host { display: inline-flex; }
23
+ :host([hidden]) { display: none; }
24
+ button {
25
+ display: inline-flex; align-items: center; justify-content: center;
26
+ width: 40px; height: 40px; padding: 8px;
27
+ background: transparent; border: none; color: inherit; cursor: pointer;
28
+ }
29
+ button:hover { opacity: 0.8; }
30
+ button:disabled { opacity: 0.4; cursor: not-allowed; }
31
+ </style>
32
+ <button part="button" type="button" aria-label="Enter fullscreen">
33
+ <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" part="icon">
34
+ <path d="${ENTER_FS}"/>
35
+ </svg>
36
+ </button>
37
+ `
38
+ }
39
+
40
+ queueMicrotask(() => this.attach())
41
+ }
42
+
43
+ disconnectedCallback(): void {
44
+ this._cleanup?.()
45
+ this._cleanup = null
46
+ }
47
+
48
+ private attach(): void {
49
+ const player = resolvePlayer(this)
50
+ if (!player) return
51
+
52
+ const btn = this.shadowRoot!.querySelector('button')!
53
+ btn.addEventListener('click', () => player.toggleFullscreen())
54
+
55
+ const unsub = player.subscribe((state: any) => {
56
+ const avail = state.fullscreenAvailability
57
+ if (avail === 'unsupported') {
58
+ this.setAttribute('hidden', '')
59
+ } else {
60
+ this.removeAttribute('hidden')
61
+ }
62
+ btn.disabled = avail !== 'available'
63
+
64
+ const fs = state.fullscreen
65
+ btn.setAttribute('aria-label', fs ? 'Exit fullscreen' : 'Enter fullscreen')
66
+ const path = this.shadowRoot!.querySelector('path')!
67
+ path.setAttribute('d', fs ? EXIT_FS : ENTER_FS)
68
+ })
69
+
70
+ this._cleanup = unsub
71
+ }
72
+ }