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,44 @@
1
+ /**
2
+ * Fullscreen feature — fullscreen state + availability detection.
3
+ *
4
+ * @module features/fullscreen
5
+ */
6
+
7
+ import type { Feature, FeatureContext } from './types'
8
+ import { detectFullscreenAvailability } from '../core/features'
9
+ import { onFullscreenChange } from '../core/events'
10
+
11
+ export const fullscreen: Feature = {
12
+ name: 'fullscreen',
13
+ stateKeys: ['fullscreen', 'fullscreenAvailability', 'canFullscreen'],
14
+
15
+ setup(ctx: FeatureContext) {
16
+ const { store, container, getMediaElement } = ctx
17
+
18
+ const update = () => {
19
+ const video = getMediaElement() as HTMLVideoElement | null
20
+ const availability = detectFullscreenAvailability(container, video)
21
+ store.batch({
22
+ fullscreenAvailability: availability,
23
+ canFullscreen: availability === 'available',
24
+ })
25
+ }
26
+
27
+ update()
28
+
29
+ const cleanup = onFullscreenChange((fs) => {
30
+ store.set('fullscreen', fs)
31
+ })
32
+
33
+ // Re-detect after metadata loaded
34
+ const onMeta = () => update()
35
+ const media = getMediaElement()
36
+ media?.addEventListener('loadedmetadata', onMeta)
37
+
38
+ return () => {
39
+ cleanup()
40
+ const m = getMediaElement()
41
+ m?.removeEventListener('loadedmetadata', onMeta)
42
+ }
43
+ },
44
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Gestures feature — touch gesture detection.
3
+ *
4
+ * @module features/gestures
5
+ */
6
+
7
+ import type { Feature, FeatureContext } from './types'
8
+ import { createGestureDetector } from '../core/events'
9
+
10
+ export const gestures: Feature = {
11
+ name: 'gestures',
12
+
13
+ setup(ctx: FeatureContext) {
14
+ const { container, options } = ctx
15
+
16
+ if (options.gestures === false) return
17
+
18
+ const cleanup = createGestureDetector(container, {
19
+ onDoubleTap: (x) => {
20
+ // Double-tap left third: seek back, right third: seek forward
21
+ const rect = container.getBoundingClientRect()
22
+ const third = rect.width / 3
23
+ const relX = x - rect.left
24
+ const media = ctx.getMediaElement()
25
+ if (!media) return
26
+ if (relX < third) {
27
+ media.currentTime = Math.max(0, media.currentTime - 10)
28
+ } else if (relX > third * 2) {
29
+ media.currentTime = Math.min(media.duration, media.currentTime + 10)
30
+ }
31
+ },
32
+ onSwipeUp: () => {
33
+ const media = ctx.getMediaElement()
34
+ if (media) media.volume = Math.min(1, media.volume + 0.1)
35
+ },
36
+ onSwipeDown: () => {
37
+ const media = ctx.getMediaElement()
38
+ if (media) media.volume = Math.max(0, media.volume - 0.1)
39
+ },
40
+ })
41
+
42
+ return cleanup
43
+ },
44
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ts-video-player Composable Features
3
+ *
4
+ * Tree-shakeable feature composition system.
5
+ *
6
+ * @module features
7
+ */
8
+
9
+ // Feature implementations
10
+ export { playback } from './playback'
11
+ export { volume } from './volume'
12
+ export { fullscreen } from './fullscreen'
13
+ export { pip } from './pip'
14
+ export { captions } from './captions'
15
+ export { quality } from './quality'
16
+ export { keyboard } from './keyboard'
17
+ export { gestures } from './gestures'
18
+
19
+ // Types
20
+ export type { Feature, FeatureContext } from './types'
21
+
22
+ // Factory
23
+ export { createComposablePlayer } from './create-player'
24
+ export type { ComposablePlayer, ComposablePlayerOptions } from './create-player'
25
+
26
+ // Feature bundles
27
+ import { playback } from './playback'
28
+ import { volume } from './volume'
29
+ import { fullscreen } from './fullscreen'
30
+ import { pip } from './pip'
31
+ import { captions } from './captions'
32
+ import { quality } from './quality'
33
+ import { keyboard } from './keyboard'
34
+ import { gestures } from './gestures'
35
+ import type { Feature } from './types'
36
+
37
+ /** All features for video playback */
38
+ export const videoFeatures: Feature[] = [playback, volume, fullscreen, pip, captions, quality, keyboard, gestures]
39
+
40
+ /** Features for audio-only playback */
41
+ export const audioFeatures: Feature[] = [playback, volume, keyboard]
42
+
43
+ /** Minimal features (playback only) */
44
+ export const minimalFeatures: Feature[] = [playback]
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Keyboard feature — keyboard shortcut handling.
3
+ *
4
+ * @module features/keyboard
5
+ */
6
+
7
+ import type { Feature, FeatureContext } from './types'
8
+ import { createKeyboardHandler, type KeyboardActions } from '../core/events'
9
+
10
+ export const keyboard: Feature = {
11
+ name: 'keyboard',
12
+
13
+ setup(ctx: FeatureContext) {
14
+ const { container, options } = ctx
15
+
16
+ if (options.keyboard === false) return
17
+
18
+ const actions: KeyboardActions = {
19
+ togglePlay: () => {
20
+ const media = ctx.getMediaElement()
21
+ if (!media) return
22
+ if (media.paused) media.play()
23
+ else media.pause()
24
+ },
25
+ toggleMute: () => {
26
+ const media = ctx.getMediaElement()
27
+ if (media) media.muted = !media.muted
28
+ },
29
+ toggleFullscreen: () => {
30
+ // Handled at player level
31
+ },
32
+ togglePiP: () => {
33
+ // Handled at player level
34
+ },
35
+ toggleCaptions: () => {
36
+ // Handled at player level
37
+ },
38
+ seekBackward: () => {
39
+ const media = ctx.getMediaElement()
40
+ if (media) media.currentTime = Math.max(0, media.currentTime - 5)
41
+ },
42
+ seekForward: () => {
43
+ const media = ctx.getMediaElement()
44
+ if (media) media.currentTime = Math.min(media.duration, media.currentTime + 5)
45
+ },
46
+ volumeUp: () => {
47
+ const media = ctx.getMediaElement()
48
+ if (media) media.volume = Math.min(1, media.volume + 0.1)
49
+ },
50
+ volumeDown: () => {
51
+ const media = ctx.getMediaElement()
52
+ if (media) media.volume = Math.max(0, media.volume - 0.1)
53
+ },
54
+ speedUp: () => {
55
+ const media = ctx.getMediaElement()
56
+ if (media) media.playbackRate = Math.min(4, media.playbackRate + 0.25)
57
+ },
58
+ speedDown: () => {
59
+ const media = ctx.getMediaElement()
60
+ if (media) media.playbackRate = Math.max(0.25, media.playbackRate - 0.25)
61
+ },
62
+ seekStart: () => {
63
+ const media = ctx.getMediaElement()
64
+ if (media) media.currentTime = 0
65
+ },
66
+ seekEnd: () => {
67
+ const media = ctx.getMediaElement()
68
+ if (media) media.currentTime = media.duration
69
+ },
70
+ seekPercent: (percent) => {
71
+ const media = ctx.getMediaElement()
72
+ if (media) media.currentTime = (percent / 100) * media.duration
73
+ },
74
+ }
75
+
76
+ const config = typeof options.keyboard === 'object' ? options.keyboard as Record<string, any> : {}
77
+ const handler = createKeyboardHandler(actions, {
78
+ seekStep: config.seekStep,
79
+ volumeStep: config.volumeStep,
80
+ })
81
+
82
+ const target = config.global ? document : container
83
+ target.addEventListener('keydown', handler as EventListener)
84
+
85
+ return () => {
86
+ target.removeEventListener('keydown', handler as EventListener)
87
+ }
88
+ },
89
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Picture-in-Picture feature — PiP state + availability detection.
3
+ *
4
+ * @module features/pip
5
+ */
6
+
7
+ import type { Feature, FeatureContext } from './types'
8
+ import { detectPipAvailability } from '../core/features'
9
+ import { onPiPChange } from '../core/events'
10
+
11
+ export const pip: Feature = {
12
+ name: 'pip',
13
+ stateKeys: ['pictureInPicture', 'pipAvailability', 'canPictureInPicture'],
14
+
15
+ setup(ctx: FeatureContext) {
16
+ const { store, getMediaElement } = ctx
17
+
18
+ const update = () => {
19
+ const video = getMediaElement() as HTMLVideoElement | null
20
+ const availability = detectPipAvailability(video)
21
+ store.batch({
22
+ pipAvailability: availability,
23
+ canPictureInPicture: availability === 'available',
24
+ })
25
+ }
26
+
27
+ update()
28
+
29
+ let cleanupPiP: (() => void) | null = null
30
+ const media = getMediaElement()
31
+ if (media instanceof HTMLVideoElement) {
32
+ cleanupPiP = onPiPChange(media, (isPiP) => {
33
+ store.set('pictureInPicture', isPiP)
34
+ })
35
+ }
36
+
37
+ // Re-detect after metadata loaded
38
+ const onMeta = () => update()
39
+ media?.addEventListener('loadedmetadata', onMeta)
40
+
41
+ return () => {
42
+ cleanupPiP?.()
43
+ const m = getMediaElement()
44
+ m?.removeEventListener('loadedmetadata', onMeta)
45
+ }
46
+ },
47
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Playback feature — core playback state management.
3
+ *
4
+ * @module features/playback
5
+ */
6
+
7
+ import type { Feature, FeatureContext } from './types'
8
+
9
+ export const playback: Feature = {
10
+ name: 'playback',
11
+ stateKeys: ['paused', 'playing', 'ended', 'seeking', 'waiting', 'currentTime', 'duration', 'playbackRate', 'loop', 'autoplay', 'playbackState'],
12
+
13
+ setup(ctx: FeatureContext) {
14
+ const { store, getMediaElement } = ctx
15
+
16
+ const onPlay = () => store.batch({ paused: false, ended: false })
17
+ const onPause = () => store.set('paused', true)
18
+ const onPlaying = () => store.batch({ playing: true, waiting: false, playbackState: 'playing' })
19
+ const onWaiting = () => store.batch({ waiting: true, playbackState: 'buffering' })
20
+ const onSeeking = () => store.set('seeking', true)
21
+ const onSeeked = () => store.set('seeking', false)
22
+ const onEnded = () => store.batch({ ended: true, playing: false, playbackState: 'ended' })
23
+ const onTimeUpdate = () => {
24
+ const media = getMediaElement()
25
+ if (media) store.set('currentTime', media.currentTime)
26
+ }
27
+ const onDurationChange = () => {
28
+ const media = getMediaElement()
29
+ if (media) store.set('duration', media.duration)
30
+ }
31
+ const onRateChange = () => {
32
+ const media = getMediaElement()
33
+ if (media) store.set('playbackRate', media.playbackRate)
34
+ }
35
+
36
+ const bind = () => {
37
+ const media = getMediaElement()
38
+ if (!media) return
39
+ media.addEventListener('play', onPlay)
40
+ media.addEventListener('pause', onPause)
41
+ media.addEventListener('playing', onPlaying)
42
+ media.addEventListener('waiting', onWaiting)
43
+ media.addEventListener('seeking', onSeeking)
44
+ media.addEventListener('seeked', onSeeked)
45
+ media.addEventListener('ended', onEnded)
46
+ media.addEventListener('timeupdate', onTimeUpdate)
47
+ media.addEventListener('durationchange', onDurationChange)
48
+ media.addEventListener('ratechange', onRateChange)
49
+ }
50
+
51
+ bind()
52
+
53
+ return () => {
54
+ const media = getMediaElement()
55
+ if (!media) return
56
+ media.removeEventListener('play', onPlay)
57
+ media.removeEventListener('pause', onPause)
58
+ media.removeEventListener('playing', onPlaying)
59
+ media.removeEventListener('waiting', onWaiting)
60
+ media.removeEventListener('seeking', onSeeking)
61
+ media.removeEventListener('seeked', onSeeked)
62
+ media.removeEventListener('ended', onEnded)
63
+ media.removeEventListener('timeupdate', onTimeUpdate)
64
+ media.removeEventListener('durationchange', onDurationChange)
65
+ media.removeEventListener('ratechange', onRateChange)
66
+ }
67
+ },
68
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Quality feature — quality selection state.
3
+ *
4
+ * @module features/quality
5
+ */
6
+
7
+ import type { Feature, FeatureContext } from './types'
8
+
9
+ export const quality: Feature = {
10
+ name: 'quality',
11
+ stateKeys: ['qualities', 'autoQuality'],
12
+
13
+ setup(_ctx: FeatureContext) {
14
+ // Quality management is provider-specific (HLS/DASH).
15
+ // This feature exists as a placeholder for the state keys
16
+ // and to allow tree-shaking when quality UI is not needed.
17
+ return undefined
18
+ },
19
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ts-video-player Feature Types
3
+ *
4
+ * @module features/types
5
+ */
6
+
7
+ import type { PlayerState } from '../types'
8
+ import type { StateStore } from '../core/state'
9
+
10
+ export interface FeatureContext {
11
+ /** The player container element */
12
+ container: HTMLElement
13
+ /** The state store */
14
+ store: StateStore
15
+ /** Options passed to the player */
16
+ options: Record<string, unknown>
17
+ /** Get the current media element (may be null before load) */
18
+ getMediaElement: () => HTMLMediaElement | null
19
+ }
20
+
21
+ export interface Feature {
22
+ /** Unique feature name */
23
+ name: string
24
+ /** State keys this feature manages */
25
+ stateKeys?: (keyof PlayerState)[]
26
+ /** Initialize the feature. Returns a cleanup function. */
27
+ setup(ctx: FeatureContext): (() => void) | void
28
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Volume feature — volume state + availability detection.
3
+ *
4
+ * @module features/volume
5
+ */
6
+
7
+ import type { Feature, FeatureContext } from './types'
8
+ import { detectVolumeAvailability, probeVolumeAvailability } from '../core/features'
9
+
10
+ export const volume: Feature = {
11
+ name: 'volume',
12
+ stateKeys: ['volume', 'muted', 'volumeAvailability'],
13
+
14
+ setup(ctx: FeatureContext) {
15
+ const { store, getMediaElement } = ctx
16
+
17
+ // Initial sync detection
18
+ const media = getMediaElement()
19
+ store.set('volumeAvailability', detectVolumeAvailability(media))
20
+
21
+ const onVolumeChange = () => {
22
+ const m = getMediaElement()
23
+ if (m) store.batch({ volume: m.volume, muted: m.muted })
24
+ }
25
+
26
+ const onLoadedData = () => {
27
+ const m = getMediaElement()
28
+ if (!m) return
29
+ probeVolumeAvailability(m).then((availability) => {
30
+ store.set('volumeAvailability', availability)
31
+ })
32
+ }
33
+
34
+ if (media) {
35
+ media.addEventListener('volumechange', onVolumeChange)
36
+ media.addEventListener('loadeddata', onLoadedData)
37
+ }
38
+
39
+ return () => {
40
+ const m = getMediaElement()
41
+ if (!m) return
42
+ m.removeEventListener('volumechange', onVolumeChange)
43
+ m.removeEventListener('loadeddata', onLoadedData)
44
+ }
45
+ },
46
+ }