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,606 @@
1
+ /**
2
+ * ts-video-player stx Integration
3
+ *
4
+ * Deep integration with the stx templating framework.
5
+ * Provides components, directives, and runtime for seamless video embedding.
6
+ *
7
+ * @module stx
8
+ */
9
+
10
+ import type {
11
+ VideoComponentProps,
12
+ VideoRenderResult,
13
+ PlayerOptions,
14
+ Src,
15
+ } from '../types'
16
+ import { extractYouTubeId } from '../providers/youtube'
17
+
18
+ // =============================================================================
19
+ // Video Component
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Render the <Video> component for stx
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const result = await renderVideoComponent({
28
+ * src: '/videos/intro.mp4',
29
+ * poster: '/images/poster.jpg',
30
+ * controls: true,
31
+ * lazy: true,
32
+ * })
33
+ * ```
34
+ */
35
+ export async function renderVideoComponent(
36
+ props: VideoComponentProps,
37
+ _context: { isDev?: boolean } = {},
38
+ ): Promise<VideoRenderResult> {
39
+ const {
40
+ src,
41
+ poster,
42
+ title,
43
+ class: className,
44
+ style,
45
+ id = `ts-video-${randomId()}`,
46
+ width,
47
+ height,
48
+ options = {},
49
+ lazy = true,
50
+ autoplay = false,
51
+ loop = false,
52
+ muted = false,
53
+ controls = true,
54
+ playsinline = true,
55
+ preload = 'metadata',
56
+ crossorigin,
57
+ controlsList,
58
+ disablePictureInPicture = false,
59
+ theme = 'default',
60
+ } = props
61
+
62
+ // Detect provider type
63
+ const providerType = detectProviderType(src)
64
+ const isEmbed = providerType === 'youtube' || providerType === 'vimeo'
65
+
66
+ // Build player options
67
+ const playerOptions: PlayerOptions = {
68
+ ...options,
69
+ autoplay,
70
+ loop,
71
+ muted,
72
+ playsinline,
73
+ preload,
74
+ poster,
75
+ title,
76
+ controls,
77
+ crossorigin,
78
+ controlsList,
79
+ disablePictureInPicture,
80
+ }
81
+
82
+ // Build container attributes
83
+ const attrs: string[] = [
84
+ `id="${id}"`,
85
+ `class="ts-video-player ts-video-player--${theme}${className ? ' ' + escapeAttr(className) : ''}"`,
86
+ `data-ts-video-player`,
87
+ `data-provider="${providerType}"`,
88
+ ]
89
+
90
+ if (style) attrs.push(`style="${escapeAttr(style)}"`)
91
+ if (width) attrs.push(`data-width="${width}"`)
92
+ if (height) attrs.push(`data-height="${height}"`)
93
+ if (lazy) attrs.push(`data-lazy`)
94
+
95
+ // Build data config
96
+ const config = {
97
+ src: normalizeSrc(src),
98
+ options: playerOptions,
99
+ }
100
+ attrs.push(`data-config='${JSON.stringify(config)}'`)
101
+
102
+ // Build HTML
103
+ let html = ''
104
+
105
+ if (isEmbed) {
106
+ // Embed placeholder with poster
107
+ html = buildEmbedPlaceholder(id, attrs, poster, title, providerType, src)
108
+ } else {
109
+ // Native video with poster
110
+ html = buildNativeVideoPlaceholder(id, attrs, poster, title, lazy, preload, src, {
111
+ crossorigin,
112
+ controlsList,
113
+ disablePictureInPicture,
114
+ })
115
+ }
116
+
117
+ // Build initialization script
118
+ const script = generateInitScript(id, lazy)
119
+
120
+ // Build styles
121
+ const css = generatePlayerCSS(theme)
122
+
123
+ return { html, script, css }
124
+ }
125
+
126
+ /**
127
+ * Parse <Video> component from template
128
+ */
129
+ export function parseVideoComponent(content: string): VideoComponentProps | null {
130
+ // Simple attribute parsing
131
+ const srcMatch = content.match(/src=["']([^"']+)["']/)
132
+ const posterMatch = content.match(/poster=["']([^"']+)["']/)
133
+ const titleMatch = content.match(/title=["']([^"']+)["']/)
134
+ const classMatch = content.match(/class=["']([^"']+)["']/)
135
+ const idMatch = content.match(/id=["']([^"']+)["']/)
136
+ const widthMatch = content.match(/width=["']?(\d+)["']?/)
137
+ const heightMatch = content.match(/height=["']?(\d+)["']?/)
138
+
139
+ if (!srcMatch) return null
140
+
141
+ return {
142
+ src: srcMatch[1],
143
+ poster: posterMatch?.[1],
144
+ title: titleMatch?.[1],
145
+ class: classMatch?.[1],
146
+ id: idMatch?.[1],
147
+ width: widthMatch ? parseInt(widthMatch[1], 10) : undefined,
148
+ height: heightMatch ? parseInt(heightMatch[1], 10) : undefined,
149
+ autoplay: content.includes('autoplay'),
150
+ loop: content.includes('loop'),
151
+ muted: content.includes('muted'),
152
+ controls: !content.includes('controls="false"') && !content.includes(':controls="false"'),
153
+ playsinline: !content.includes('playsinline="false"'),
154
+ lazy: !content.includes('lazy="false"') && !content.includes(':lazy="false"'),
155
+ }
156
+ }
157
+
158
+ // =============================================================================
159
+ // Video Directive
160
+ // =============================================================================
161
+
162
+ /**
163
+ * Create the @video directive for stx
164
+ *
165
+ * Usage:
166
+ * ```html
167
+ * @video('/videos/intro.mp4', { poster: '/poster.jpg', controls: true })
168
+ * @video(youtubeUrl, { lazy: true })
169
+ * ```
170
+ */
171
+ export function createVideoDirective(): { name: string, hasEndTag: boolean, description: string, transform: (content: string, params: Record<string, unknown>, context: Record<string, unknown>) => Promise<string> } {
172
+ return {
173
+ name: 'video',
174
+ hasEndTag: false,
175
+ description: 'Embed a video player with full playback controls',
176
+
177
+ transform: async (
178
+ content: string,
179
+ params: Record<string, unknown>,
180
+ context: Record<string, unknown>,
181
+ ): Promise<string> => {
182
+ const props = parseVideoDirectiveArgs(content, params, context)
183
+
184
+ if (!props.src) {
185
+ console.warn('@video directive: missing src argument')
186
+ return '<!-- @video: missing src -->'
187
+ }
188
+
189
+ const isDev = context.__isDev as boolean || false
190
+ const result = await renderVideoComponent(props, { isDev })
191
+
192
+ let output = result.html
193
+ if (result.css) {
194
+ output += `\n<style>${result.css}</style>`
195
+ }
196
+ if (result.script) {
197
+ output += `\n<script>${result.script}</script>`
198
+ }
199
+
200
+ return output
201
+ },
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Parse @video directive arguments
207
+ */
208
+ function parseVideoDirectiveArgs(
209
+ content: string,
210
+ params: Record<string, unknown>,
211
+ context: Record<string, unknown>,
212
+ ): VideoComponentProps {
213
+ // If params already structured
214
+ if (params.src) {
215
+ return {
216
+ src: String(params.src),
217
+ ...(params.options || params) as Partial<VideoComponentProps>,
218
+ }
219
+ }
220
+
221
+ // Parse from content: @video('/path/to/video.mp4', { options })
222
+ let args = content.trim()
223
+ if (args.startsWith('@video(') || args.startsWith('(')) {
224
+ args = args.replace(/^@video\(/, '').replace(/^\(/, '').replace(/\)$/, '')
225
+ }
226
+
227
+ // Split into src and options
228
+ const parts = splitArgs(args)
229
+ const src = resolveValue(parts[0], context) as string || ''
230
+ let options: Partial<VideoComponentProps> = {}
231
+
232
+ if (parts[1]) {
233
+ const resolved = resolveValue(parts[1], context)
234
+ if (typeof resolved === 'object' && resolved !== null) {
235
+ options = resolved as Partial<VideoComponentProps>
236
+ }
237
+ }
238
+
239
+ return { src, ...options }
240
+ }
241
+
242
+ // =============================================================================
243
+ // Helpers
244
+ // =============================================================================
245
+
246
+ function detectProviderType(src: Src | Src[]): string {
247
+ const firstSrc = Array.isArray(src) ? src[0] : src
248
+ const url = typeof firstSrc === 'string' ? firstSrc : (firstSrc as any).src || ''
249
+
250
+ if (extractYouTubeId(url)) return 'youtube'
251
+ if (url.includes('vimeo.com')) return 'vimeo'
252
+ if (url.endsWith('.m3u8') || url.includes('application/x-mpegURL')) return 'hls'
253
+ if (url.endsWith('.mpd') || url.includes('application/dash+xml')) return 'dash'
254
+
255
+ return 'html5'
256
+ }
257
+
258
+ function normalizeSrc(src: Src | Src[]): Src | Src[] {
259
+ return src
260
+ }
261
+
262
+ function buildEmbedPlaceholder(
263
+ _id: string,
264
+ attrs: string[],
265
+ poster: string | undefined,
266
+ title: string | undefined,
267
+ _provider: string,
268
+ _src: Src | Src[],
269
+ ): string {
270
+ const posterStyle = poster ? `background-image: url('${escapeAttr(poster)}');` : ''
271
+
272
+ return `
273
+ <div ${attrs.join(' ')}>
274
+ <div class="ts-video-player__container">
275
+ <div class="ts-video-player__placeholder" style="${posterStyle}" data-poster>
276
+ <button class="ts-video-player__play-button" aria-label="Play ${escapeAttr(title || 'video')}">
277
+ <svg viewBox="0 0 24 24" fill="currentColor" width="64" height="64">
278
+ <path d="M8 5v14l11-7z"/>
279
+ </svg>
280
+ </button>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ `.trim()
285
+ }
286
+
287
+ function buildNativeVideoPlaceholder(
288
+ _id: string,
289
+ attrs: string[],
290
+ poster: string | undefined,
291
+ title: string | undefined,
292
+ lazy: boolean,
293
+ preload: string,
294
+ src: Src | Src[],
295
+ options: Pick<VideoComponentProps, 'crossorigin' | 'controlsList' | 'disablePictureInPicture'>,
296
+ ): string {
297
+ const firstSrc = Array.isArray(src) ? src[0] : src
298
+ const url = typeof firstSrc === 'string' ? firstSrc : (firstSrc as any).src || ''
299
+
300
+ const videoAttrs = [
301
+ `class="ts-video-player__media"`,
302
+ `playsinline`,
303
+ lazy ? 'preload="none"' : `preload="${preload}"`,
304
+ ]
305
+
306
+ if (lazy) {
307
+ videoAttrs.push(`data-poster="${escapeAttr(poster || '')}"`)
308
+ videoAttrs.push(`data-src="${escapeAttr(url)}"`)
309
+ } else {
310
+ if (poster) videoAttrs.push(`poster="${escapeAttr(poster)}"`)
311
+ videoAttrs.push(`src="${escapeAttr(url)}"`)
312
+ }
313
+
314
+ if (title) videoAttrs.push(`title="${escapeAttr(title)}"`)
315
+ if (options.crossorigin !== undefined) videoAttrs.push(`crossorigin="${escapeAttr(options.crossorigin)}"`)
316
+ if (options.controlsList?.length) videoAttrs.push(`controlslist="${escapeAttr(options.controlsList.join(' '))}"`)
317
+ if (options.disablePictureInPicture) videoAttrs.push('disablepictureinpicture')
318
+
319
+ return `
320
+ <div ${attrs.join(' ')}>
321
+ <div class="ts-video-player__container">
322
+ <video ${videoAttrs.join(' ')}>
323
+ ${!lazy ? buildSourceTags(src) : ''}
324
+ </video>
325
+ </div>
326
+ </div>
327
+ `.trim()
328
+ }
329
+
330
+ function buildSourceTags(src: Src | Src[]): string {
331
+ const sources = Array.isArray(src) ? src : [src]
332
+ return sources
333
+ .map((s) => {
334
+ if (typeof s === 'string') {
335
+ return `<source src="${escapeAttr(s)}" type="${getMimeType(s)}" />`
336
+ }
337
+ const mediaSource = s as { src: string; type?: string }
338
+ return `<source src="${escapeAttr(mediaSource.src)}" type="${mediaSource.type || getMimeType(mediaSource.src)}" />`
339
+ })
340
+ .join('\n')
341
+ }
342
+
343
+ function getMimeType(src: string): string {
344
+ const path = src.split(/[?#]/, 1)[0]
345
+ const ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase()
346
+ const types: Record<string, string> = {
347
+ mp4: 'video/mp4',
348
+ webm: 'video/webm',
349
+ ogg: 'video/ogg',
350
+ ogv: 'video/ogg',
351
+ mov: 'video/quicktime',
352
+ m3u8: 'application/x-mpegURL',
353
+ mpd: 'application/dash+xml',
354
+ mp3: 'audio/mpeg',
355
+ wav: 'audio/wav',
356
+ flac: 'audio/flac',
357
+ m4a: 'audio/mp4',
358
+ aac: 'audio/aac',
359
+ oga: 'audio/ogg',
360
+ opus: 'audio/ogg; codecs=opus',
361
+ }
362
+ return types[ext || ''] || 'video/mp4'
363
+ }
364
+
365
+ function generateInitScript(id: string, _lazy: boolean): string {
366
+ return `
367
+ (function() {
368
+ const container = document.getElementById('${id}');
369
+ if (!container) return;
370
+
371
+ const config = JSON.parse(container.dataset.config || '{}');
372
+ const isLazy = container.hasAttribute('data-lazy');
373
+
374
+ function initPlayer() {
375
+ if (typeof VideoPlayer === 'undefined') {
376
+ // Load the player library
377
+ const script = document.createElement('script');
378
+ script.src = 'https://cdn.jsdelivr.net/npm/ts-video-player@latest/dist/index.min.js';
379
+ script.onload = function() { createPlayer(); };
380
+ document.head.appendChild(script);
381
+ } else {
382
+ createPlayer();
383
+ }
384
+ }
385
+
386
+ function createPlayer() {
387
+ const player = VideoPlayer.createPlayer(container, config.options || {});
388
+ if (config.src) {
389
+ player.setSrc(config.src);
390
+ }
391
+ container.__videoPlayer = player;
392
+ }
393
+
394
+ if (isLazy) {
395
+ // Use IntersectionObserver for lazy loading
396
+ const observer = new IntersectionObserver(function(entries) {
397
+ entries.forEach(function(entry) {
398
+ if (entry.isIntersecting) {
399
+ initPlayer();
400
+ observer.disconnect();
401
+ }
402
+ });
403
+ }, { rootMargin: '50px' });
404
+ observer.observe(container);
405
+ } else {
406
+ // Initialize immediately
407
+ if (document.readyState === 'loading') {
408
+ document.addEventListener('DOMContentLoaded', initPlayer);
409
+ } else {
410
+ initPlayer();
411
+ }
412
+ }
413
+
414
+ // Handle play button click (for embed placeholders)
415
+ const playButton = container.querySelector('.ts-video-player__play-button');
416
+ if (playButton) {
417
+ playButton.addEventListener('click', function() {
418
+ initPlayer();
419
+ // Wait for player to initialize then play
420
+ const checkPlayer = setInterval(function() {
421
+ if (container.__videoPlayer && container.__videoPlayer.ready) {
422
+ container.__videoPlayer.play();
423
+ clearInterval(checkPlayer);
424
+ }
425
+ }, 100);
426
+ });
427
+ }
428
+ })();
429
+ `.trim()
430
+ }
431
+
432
+ function generatePlayerCSS(_theme: string): string {
433
+ return `
434
+ .ts-video-player {
435
+ position: relative;
436
+ width: 100%;
437
+ background: #000;
438
+ }
439
+
440
+ .ts-video-player__container {
441
+ position: relative;
442
+ width: 100%;
443
+ padding-bottom: 56.25%; /* 16:9 */
444
+ }
445
+
446
+ .ts-video-player__media,
447
+ .ts-video-player__placeholder {
448
+ position: absolute;
449
+ top: 0;
450
+ left: 0;
451
+ width: 100%;
452
+ height: 100%;
453
+ object-fit: contain;
454
+ }
455
+
456
+ .ts-video-player__placeholder {
457
+ display: flex;
458
+ align-items: center;
459
+ justify-content: center;
460
+ background-color: #1a1a1a;
461
+ background-size: cover;
462
+ background-position: center;
463
+ cursor: pointer;
464
+ }
465
+
466
+ .ts-video-player__play-button {
467
+ display: flex;
468
+ align-items: center;
469
+ justify-content: center;
470
+ width: 80px;
471
+ height: 80px;
472
+ background: rgba(0, 0, 0, 0.7);
473
+ border: none;
474
+ border-radius: 50%;
475
+ color: white;
476
+ cursor: pointer;
477
+ transition: transform 0.2s, background 0.2s;
478
+ }
479
+
480
+ .ts-video-player__play-button:hover {
481
+ transform: scale(1.1);
482
+ background: rgba(0, 0, 0, 0.9);
483
+ }
484
+
485
+ .ts-video-player__play-button svg {
486
+ margin-left: 4px;
487
+ }
488
+
489
+ /* Loading state */
490
+ .ts-video-player[data-loading="true"]::after {
491
+ content: '';
492
+ position: absolute;
493
+ top: 50%;
494
+ left: 50%;
495
+ width: 40px;
496
+ height: 40px;
497
+ margin: -20px 0 0 -20px;
498
+ border: 3px solid rgba(255, 255, 255, 0.3);
499
+ border-top-color: white;
500
+ border-radius: 50%;
501
+ animation: ts-video-spin 0.8s linear infinite;
502
+ }
503
+
504
+ @keyframes ts-video-spin {
505
+ to { transform: rotate(360deg); }
506
+ }
507
+
508
+ /* Controls hidden */
509
+ .ts-video-player[data-controls-visible="false"] .ts-video-player__controls {
510
+ opacity: 0;
511
+ pointer-events: none;
512
+ }
513
+ `.trim()
514
+ }
515
+
516
+ function escapeAttr(str: string): string {
517
+ return str
518
+ .replace(/&/g, '&amp;')
519
+ .replace(/"/g, '&quot;')
520
+ .replace(/'/g, '&#039;')
521
+ .replace(/</g, '&lt;')
522
+ .replace(/>/g, '&gt;')
523
+ }
524
+
525
+ function randomId(): string {
526
+ return Math.random().toString(36).slice(2, 8)
527
+ }
528
+
529
+ function splitArgs(str: string): string[] {
530
+ const args: string[] = []
531
+ let current = ''
532
+ let depth = 0
533
+ let inString = false
534
+ let stringChar = ''
535
+
536
+ for (const char of str) {
537
+ if ((char === '"' || char === '\'') && !inString) {
538
+ inString = true
539
+ stringChar = char
540
+ } else if (char === stringChar && inString) {
541
+ inString = false
542
+ stringChar = ''
543
+ }
544
+
545
+ if (!inString) {
546
+ if (char === '{' || char === '[') depth++
547
+ if (char === '}' || char === ']') depth--
548
+ if (char === ',' && depth === 0) {
549
+ args.push(current.trim())
550
+ current = ''
551
+ continue
552
+ }
553
+ }
554
+
555
+ current += char
556
+ }
557
+
558
+ if (current.trim()) {
559
+ args.push(current.trim())
560
+ }
561
+
562
+ return args
563
+ }
564
+
565
+ function resolveValue(value: string | undefined, context: Record<string, unknown>): unknown {
566
+ if (!value) return undefined
567
+
568
+ const trimmed = value.trim()
569
+
570
+ // Remove quotes
571
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith('\'') && trimmed.endsWith('\''))) {
572
+ return trimmed.slice(1, -1)
573
+ }
574
+
575
+ // Try to parse as JSON
576
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
577
+ try {
578
+ return JSON.parse(trimmed)
579
+ } catch {
580
+ // Not valid JSON
581
+ }
582
+ }
583
+
584
+ // Check context
585
+ if (trimmed in context) {
586
+ return context[trimmed]
587
+ }
588
+
589
+ return trimmed
590
+ }
591
+
592
+ // =============================================================================
593
+ // Exports
594
+ // =============================================================================
595
+
596
+ export const videoDirective: { name: string, hasEndTag: boolean, description: string, transform: (content: string, params: Record<string, unknown>, context: Record<string, unknown>) => Promise<string> } = createVideoDirective()
597
+
598
+ /**
599
+ * Register ts-video-player directives with stx
600
+ */
601
+ export function registerVideoDirectives(stx: { config: { customDirectives?: any[] } }): void {
602
+ if (!stx.config.customDirectives) {
603
+ stx.config.customDirectives = []
604
+ }
605
+ stx.config.customDirectives.push(videoDirective)
606
+ }