@therealtinhtute/baroiplayer 1.0.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 (244) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/context/media.js +2 -0
  4. package/dist/context/media.js.map +1 -0
  5. package/dist/context/media.mjs +15 -0
  6. package/dist/context/media.mjs.map +1 -0
  7. package/dist/context/store.js +2 -0
  8. package/dist/context/store.js.map +1 -0
  9. package/dist/context/store.mjs +5 -0
  10. package/dist/context/store.mjs.map +1 -0
  11. package/dist/context-BMteAJDN.js +1140 -0
  12. package/dist/context-BMteAJDN.js.map +1 -0
  13. package/dist/context-DCJ_scmO.cjs +23 -0
  14. package/dist/context-DCJ_scmO.cjs.map +1 -0
  15. package/dist/core-events-B5W_j8yU.js +480 -0
  16. package/dist/core-events-B5W_j8yU.js.map +1 -0
  17. package/dist/core-events-CN0-Vhhq.cjs +2 -0
  18. package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
  19. package/dist/core-plugins-Bq9KLany.js +696 -0
  20. package/dist/core-plugins-Bq9KLany.js.map +1 -0
  21. package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
  22. package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
  23. package/dist/core.js +37 -0
  24. package/dist/core.js.map +1 -0
  25. package/dist/core.mjs +1217 -0
  26. package/dist/core.mjs.map +1 -0
  27. package/dist/enhanced-components-DHv0MFvG.js +1073 -0
  28. package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
  29. package/dist/enhanced-components-DTIObgB7.cjs +2 -0
  30. package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
  31. package/dist/hooks-C_r8wexn.cjs +4 -0
  32. package/dist/hooks-C_r8wexn.cjs.map +1 -0
  33. package/dist/hooks-Da2wVXlF.js +1630 -0
  34. package/dist/hooks-Da2wVXlF.js.map +1 -0
  35. package/dist/index.css +1 -0
  36. package/dist/index.js +112 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +6126 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/minimal-components-DSq5sQpy.js +2002 -0
  41. package/dist/minimal-components-DSq5sQpy.js.map +1 -0
  42. package/dist/minimal-components-DflWiihN.cjs +2 -0
  43. package/dist/minimal-components-DflWiihN.cjs.map +1 -0
  44. package/dist/minimal-components.css +1 -0
  45. package/dist/mona-loading.gif +0 -0
  46. package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
  47. package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
  48. package/dist/performance-monitor-rMW1RgV3.js +302 -0
  49. package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
  50. package/dist/utils-BnscpYYd.js +2798 -0
  51. package/dist/utils-BnscpYYd.js.map +1 -0
  52. package/dist/utils-D_czS_0K.cjs +9 -0
  53. package/dist/utils-D_czS_0K.cjs.map +1 -0
  54. package/dist/utils.js +2 -0
  55. package/dist/utils.js.map +1 -0
  56. package/dist/utils.mjs +7 -0
  57. package/dist/utils.mjs.map +1 -0
  58. package/package.json +114 -0
  59. package/src/adapters/videojs-adapter.tsx +470 -0
  60. package/src/assets/mona-loading.gif +0 -0
  61. package/src/components/AnimateLoading.tsx +115 -0
  62. package/src/components/LoadingState.tsx +248 -0
  63. package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
  64. package/src/components/audio-player.tsx +85 -0
  65. package/src/components/basic/index.ts +37 -0
  66. package/src/components/enhanced/advanced-features.tsx +316 -0
  67. package/src/components/enhanced/debug-panel.tsx +422 -0
  68. package/src/components/enhanced/enhanced-controls.tsx +600 -0
  69. package/src/components/enhanced/enhanced-media-player.tsx +509 -0
  70. package/src/components/enhanced/error-handling.tsx +345 -0
  71. package/src/components/enhanced/index.ts +39 -0
  72. package/src/components/enhanced/media-player-enhanced.tsx +349 -0
  73. package/src/components/examples/basic-video-player.tsx +55 -0
  74. package/src/components/examples/custom-controls-example.tsx +63 -0
  75. package/src/components/examples/enhanced-controls-example.tsx +114 -0
  76. package/src/components/examples/enhanced-media-player-example.tsx +293 -0
  77. package/src/components/examples/loading-states-demo.tsx +142 -0
  78. package/src/components/examples/react-19-demo.tsx +381 -0
  79. package/src/components/examples/volume-orientation-demo.tsx +114 -0
  80. package/src/components/icon-demo.tsx +209 -0
  81. package/src/components/icons/index.tsx +166 -0
  82. package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
  83. package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
  84. package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
  85. package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
  86. package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
  87. package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
  88. package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
  89. package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
  90. package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
  91. package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
  92. package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
  93. package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
  94. package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
  95. package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
  96. package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
  97. package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
  98. package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
  99. package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
  100. package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
  101. package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
  102. package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
  103. package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
  104. package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
  105. package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
  106. package/src/components/legacy.tsx +93 -0
  107. package/src/components/media-metadata-display.tsx +100 -0
  108. package/src/components/media-player-audio.tsx +39 -0
  109. package/src/components/media-player-controls.tsx +226 -0
  110. package/src/components/media-player-error.tsx +163 -0
  111. package/src/components/media-player-fullscreen.tsx +109 -0
  112. package/src/components/media-player-hls.tsx +111 -0
  113. package/src/components/media-player-loading.tsx +76 -0
  114. package/src/components/media-player-play.tsx +75 -0
  115. package/src/components/media-player-root.tsx +340 -0
  116. package/src/components/media-player-seek.tsx +168 -0
  117. package/src/components/media-player-time.tsx +99 -0
  118. package/src/components/media-player-tooltip.tsx +58 -0
  119. package/src/components/media-player-video.tsx +39 -0
  120. package/src/components/media-player-volume.tsx +155 -0
  121. package/src/components/media-player.tsx +257 -0
  122. package/src/components/media-suspense.tsx +362 -0
  123. package/src/components/player-settings-form.tsx +163 -0
  124. package/src/components/react-player-wrapper.tsx +651 -0
  125. package/src/components/simple-video-player.tsx +60 -0
  126. package/src/components/ui/enhanced-seek-bar.tsx +235 -0
  127. package/src/components/ui/fullscreen-button.tsx +60 -0
  128. package/src/components/ui/media-resource-preloader.tsx +395 -0
  129. package/src/components/ui/play-button.tsx +107 -0
  130. package/src/components/ui/seek-bar.tsx +161 -0
  131. package/src/components/ui/time-display.tsx +40 -0
  132. package/src/components/ui/volume-control.tsx +143 -0
  133. package/src/components/unified/base-media-player.tsx +246 -0
  134. package/src/components/unified/media-player.tsx +75 -0
  135. package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
  136. package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
  137. package/src/components/variants/minimal/MinimalControls.tsx +142 -0
  138. package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
  139. package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
  140. package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
  141. package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
  142. package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
  143. package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
  144. package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
  145. package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
  146. package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
  147. package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
  148. package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
  149. package/src/components/variants/minimal/components/RightControls.tsx +173 -0
  150. package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
  151. package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
  152. package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
  153. package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
  154. package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
  155. package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
  156. package/src/components/variants/minimal/index.ts +23 -0
  157. package/src/components/video-player.tsx +100 -0
  158. package/src/context/error-handling.ts +394 -0
  159. package/src/context/media-context.tsx +208 -0
  160. package/src/context/media-provider.tsx +265 -0
  161. package/src/context/media-store.ts +379 -0
  162. package/src/context/persistence.ts +453 -0
  163. package/src/core/events/event-system.ts +442 -0
  164. package/src/core/events/hooks.ts +302 -0
  165. package/src/core/events/index.ts +22 -0
  166. package/src/core/events/middleware.ts +269 -0
  167. package/src/core/index.ts +22 -0
  168. package/src/core/optimization/bundle-optimizer.tsx +308 -0
  169. package/src/core/performance/cicd.ts +96 -0
  170. package/src/core/performance/index.ts +38 -0
  171. package/src/core/performance/media-loading-optimizer.ts +538 -0
  172. package/src/core/performance/performance-budgets.ts +432 -0
  173. package/src/core/performance/performance-monitor.tsx +476 -0
  174. package/src/core/performance/performance-testing.ts +459 -0
  175. package/src/core/plugins/base-plugin.ts +224 -0
  176. package/src/core/plugins/enhanced-plugin-system.ts +411 -0
  177. package/src/core/plugins/examples/analytics-plugin.ts +242 -0
  178. package/src/core/plugins/index.ts +39 -0
  179. package/src/core/plugins/registry.ts +327 -0
  180. package/src/core/plugins/types.ts +159 -0
  181. package/src/core/plugins/utils.ts +316 -0
  182. package/src/core/progressive-loading.tsx +322 -0
  183. package/src/core/providers/enhanced-hls-provider.ts +339 -0
  184. package/src/core/providers/service-provider.tsx +168 -0
  185. package/src/core/utils.tsx +38 -0
  186. package/src/features/media-session/media-session-manager.ts +429 -0
  187. package/src/features/picture-in-picture/pip-manager.ts +429 -0
  188. package/src/features/playlist/playlist-manager.ts +606 -0
  189. package/src/features/subtitles/subtitle-manager.ts +441 -0
  190. package/src/features/subtitles/subtitle-parser.ts +280 -0
  191. package/src/features/thumbnails/thumbnail-generator.ts +519 -0
  192. package/src/hooks/accessibility/index.ts +27 -0
  193. package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
  194. package/src/hooks/accessibility/use-focus-management.ts +176 -0
  195. package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
  196. package/src/hooks/accessibility/use-screen-reader.ts +182 -0
  197. package/src/hooks/core/index.ts +47 -0
  198. package/src/hooks/legacy.ts +27 -0
  199. package/src/hooks/use-enhanced-media-player.ts +468 -0
  200. package/src/hooks/use-intelligent-preloading.ts +358 -0
  201. package/src/hooks/use-keyboard-controls.ts +115 -0
  202. package/src/hooks/use-media-actions.ts +175 -0
  203. package/src/hooks/use-media-metadata.ts +89 -0
  204. package/src/hooks/use-media-optimistic.ts +270 -0
  205. package/src/hooks/use-media-player.ts +312 -0
  206. package/src/hooks/use-media-state.ts +190 -0
  207. package/src/hooks/use-memory-optimization.ts +358 -0
  208. package/src/hooks/use-optimistic-controls.ts +92 -0
  209. package/src/hooks/use-optimized-media.ts +263 -0
  210. package/src/hooks/use-touch-gestures.ts +417 -0
  211. package/src/hooks/useEnhancedSubtitles.ts +258 -0
  212. package/src/hooks/useKeyboardShortcuts.ts +74 -0
  213. package/src/hooks/useMediaControls.ts +52 -0
  214. package/src/hooks/useScreenshot.ts +57 -0
  215. package/src/hooks/useSubtitles.ts +91 -0
  216. package/src/hooks/useVideoPlayer.ts +232 -0
  217. package/src/icons/components/FullscreenIcon.tsx +37 -0
  218. package/src/icons/components/SettingsIcon.tsx +38 -0
  219. package/src/icons/components/animated/LoadingIcon.tsx +43 -0
  220. package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
  221. package/src/icons/components/media/PauseIcon.tsx +39 -0
  222. package/src/icons/components/media/PlayIcon.tsx +38 -0
  223. package/src/icons/components/volume/VolumeIcon.tsx +39 -0
  224. package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
  225. package/src/icons/index.ts +56 -0
  226. package/src/icons/provider.tsx +64 -0
  227. package/src/icons/types.ts +40 -0
  228. package/src/index-enhanced.ts +35 -0
  229. package/src/index-legacy.ts +326 -0
  230. package/src/index.css +102 -0
  231. package/src/index.ts +216 -0
  232. package/src/lib/deprecation.ts +51 -0
  233. package/src/lib/format-time.ts +37 -0
  234. package/src/lib/index.ts +3 -0
  235. package/src/lib/utils.ts +6 -0
  236. package/src/styles/globals.css +462 -0
  237. package/src/styles/minimal-player.css +589 -0
  238. package/src/styles/pixel-art.css +46 -0
  239. package/src/styles/videojs.css +100 -0
  240. package/src/test/setup.ts +79 -0
  241. package/src/types/events.ts +47 -0
  242. package/src/types/formats.ts +95 -0
  243. package/src/types/index.ts +190 -0
  244. package/src/types/videojs-plugins.d.ts +14 -0
@@ -0,0 +1,188 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+
3
+ export interface AccessibilityPreferences {
4
+ prefersReducedMotion: boolean
5
+ prefersHighContrast: boolean
6
+ prefersColorScheme: 'light' | 'dark' | 'no-preference'
7
+ prefersReducedTransparency: boolean
8
+ }
9
+
10
+ export function useAccessibilityPreferences(): AccessibilityPreferences {
11
+ const [preferences, setPreferences] = useState<AccessibilityPreferences>({
12
+ prefersReducedMotion: false,
13
+ prefersHighContrast: false,
14
+ prefersColorScheme: 'no-preference',
15
+ prefersReducedTransparency: false
16
+ })
17
+
18
+ const updatePreferences = useCallback(() => {
19
+ if (typeof window === 'undefined') return
20
+
21
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
22
+ const prefersHighContrast = window.matchMedia('(prefers-contrast: high)').matches
23
+ const prefersReducedTransparency = window.matchMedia('(prefers-reduced-transparency: reduce)').matches
24
+
25
+ let prefersColorScheme: 'light' | 'dark' | 'no-preference' = 'no-preference'
26
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
27
+ prefersColorScheme = 'dark'
28
+ } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
29
+ prefersColorScheme = 'light'
30
+ }
31
+
32
+ setPreferences({
33
+ prefersReducedMotion,
34
+ prefersHighContrast,
35
+ prefersColorScheme,
36
+ prefersReducedTransparency
37
+ })
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ if (typeof window === 'undefined') return
42
+
43
+ // Initial check
44
+ updatePreferences()
45
+
46
+ // Set up media query listeners
47
+ const mediaQueries = [
48
+ window.matchMedia('(prefers-reduced-motion: reduce)'),
49
+ window.matchMedia('(prefers-contrast: high)'),
50
+ window.matchMedia('(prefers-color-scheme: dark)'),
51
+ window.matchMedia('(prefers-color-scheme: light)'),
52
+ window.matchMedia('(prefers-reduced-transparency: reduce)')
53
+ ]
54
+
55
+ // Add listeners
56
+ mediaQueries.forEach(mq => {
57
+ mq.addEventListener('change', updatePreferences)
58
+ })
59
+
60
+ // Cleanup
61
+ return () => {
62
+ mediaQueries.forEach(mq => {
63
+ mq.removeEventListener('change', updatePreferences)
64
+ })
65
+ }
66
+ }, [updatePreferences])
67
+
68
+ return preferences
69
+ }
70
+
71
+ // Hook for applying accessibility styles
72
+ export function useAccessibilityStyles() {
73
+ const preferences = useAccessibilityPreferences()
74
+
75
+ // Generate CSS custom properties based on preferences
76
+ const cssVariables = useCallback(() => {
77
+ const vars: Record<string, string> = {}
78
+
79
+ if (preferences.prefersHighContrast) {
80
+ vars['--media-contrast'] = 'high'
81
+ vars['--media-bg-opacity'] = '1'
82
+ vars['--media-border-width'] = '2px'
83
+ vars['--media-focus-outline-width'] = '3px'
84
+ } else {
85
+ vars['--media-contrast'] = 'normal'
86
+ vars['--media-bg-opacity'] = '0.8'
87
+ vars['--media-border-width'] = '1px'
88
+ vars['--media-focus-outline-width'] = '2px'
89
+ }
90
+
91
+ if (preferences.prefersReducedMotion) {
92
+ vars['--media-transition-duration'] = '0ms'
93
+ vars['--media-animation-duration'] = '0ms'
94
+ vars['--media-motion'] = 'none'
95
+ } else {
96
+ vars['--media-transition-duration'] = '200ms'
97
+ vars['--media-animation-duration'] = '300ms'
98
+ vars['--media-motion'] = 'auto'
99
+ }
100
+
101
+ if (preferences.prefersReducedTransparency) {
102
+ vars['--media-backdrop-blur'] = '0px'
103
+ vars['--media-bg-opacity'] = '1'
104
+ } else {
105
+ vars['--media-backdrop-blur'] = '10px'
106
+ }
107
+
108
+ return vars
109
+ }, [preferences])
110
+
111
+ // Apply styles to document
112
+ const applyStyles = useCallback(() => {
113
+ if (typeof document === 'undefined') return
114
+
115
+ const vars = cssVariables()
116
+ Object.entries(vars).forEach(([property, value]) => {
117
+ document.documentElement.style.setProperty(property, value)
118
+ })
119
+ }, [cssVariables])
120
+
121
+ useEffect(() => {
122
+ applyStyles()
123
+ }, [applyStyles])
124
+
125
+ return {
126
+ preferences,
127
+ cssVariables: cssVariables(),
128
+ applyStyles
129
+ }
130
+ }
131
+
132
+ // Hook for managing focus visibility
133
+ export function useFocusVisible() {
134
+ const [isFocusVisible, setIsFocusVisible] = useState(false)
135
+
136
+ useEffect(() => {
137
+ if (typeof window === 'undefined') return
138
+
139
+ let hadKeyboardEvent = true
140
+ let keyboardThrottleTimeoutID: NodeJS.Timeout
141
+
142
+ const handleKeyDown = (e: KeyboardEvent) => {
143
+ if (e.metaKey || e.altKey || e.ctrlKey) {
144
+ return
145
+ }
146
+
147
+ hadKeyboardEvent = true
148
+ setIsFocusVisible(true)
149
+ }
150
+
151
+ const handlePointerDown = () => {
152
+ hadKeyboardEvent = false
153
+ setIsFocusVisible(false)
154
+ }
155
+
156
+ const handleFocus = (e: FocusEvent) => {
157
+ if (hadKeyboardEvent || (e.target as HTMLElement)?.matches(':focus-visible')) {
158
+ setIsFocusVisible(true)
159
+ }
160
+ }
161
+
162
+ const handleBlur = () => {
163
+ setIsFocusVisible(false)
164
+ }
165
+
166
+ document.addEventListener('keydown', handleKeyDown, true)
167
+ document.addEventListener('mousedown', handlePointerDown, true)
168
+ document.addEventListener('pointerdown', handlePointerDown, true)
169
+ document.addEventListener('touchstart', handlePointerDown, true)
170
+ document.addEventListener('focus', handleFocus, true)
171
+ document.addEventListener('blur', handleBlur, true)
172
+
173
+ return () => {
174
+ document.removeEventListener('keydown', handleKeyDown, true)
175
+ document.removeEventListener('mousedown', handlePointerDown, true)
176
+ document.removeEventListener('pointerdown', handlePointerDown, true)
177
+ document.removeEventListener('touchstart', handlePointerDown, true)
178
+ document.removeEventListener('focus', handleFocus, true)
179
+ document.removeEventListener('blur', handleBlur, true)
180
+
181
+ if (keyboardThrottleTimeoutID) {
182
+ clearTimeout(keyboardThrottleTimeoutID)
183
+ }
184
+ }
185
+ }, [])
186
+
187
+ return isFocusVisible
188
+ }
@@ -0,0 +1,176 @@
1
+ import { useRef, useCallback, useEffect } from 'react'
2
+
3
+ export interface FocusOptions {
4
+ preventScroll?: boolean
5
+ restoreFocus?: boolean
6
+ }
7
+
8
+ export interface UseFocusManagementOptions {
9
+ restoreFocus?: boolean
10
+ initialFocus?: string | HTMLElement | (() => HTMLElement | null)
11
+ }
12
+
13
+ export function useFocusManagement({
14
+ restoreFocus = true,
15
+ initialFocus
16
+ }: UseFocusManagementOptions = {}) {
17
+ const previousFocusRef = useRef<HTMLElement | null>(null)
18
+ const containerRef = useRef<HTMLElement | null>(null)
19
+
20
+ // Store the previously focused element
21
+ const storePreviousFocus = useCallback(() => {
22
+ previousFocusRef.current = document.activeElement as HTMLElement
23
+ }, [])
24
+
25
+ // Restore focus to the previously focused element
26
+ const restorePreviousFocus = useCallback((options: FocusOptions = {}) => {
27
+ if (previousFocusRef.current && restoreFocus) {
28
+ previousFocusRef.current.focus({
29
+ preventScroll: options.preventScroll
30
+ })
31
+ previousFocusRef.current = null
32
+ }
33
+ }, [restoreFocus])
34
+
35
+ // Focus an element by selector or element
36
+ const focusElement = useCallback((
37
+ target: string | HTMLElement | (() => HTMLElement | null),
38
+ options: FocusOptions = {}
39
+ ) => {
40
+ let element: HTMLElement | null = null
41
+
42
+ if (typeof target === 'string') {
43
+ element = containerRef.current?.querySelector(target) as HTMLElement ||
44
+ document.querySelector(target) as HTMLElement
45
+ } else if (typeof target === 'function') {
46
+ element = target()
47
+ } else {
48
+ element = target
49
+ }
50
+
51
+ if (element) {
52
+ element.focus({
53
+ preventScroll: options.preventScroll
54
+ })
55
+ }
56
+ }, [])
57
+
58
+ // Get all focusable elements within a container
59
+ const getFocusableElements = useCallback((container?: HTMLElement) => {
60
+ const root = container || containerRef.current || document
61
+ const focusableSelectors = [
62
+ 'button:not([disabled])',
63
+ 'input:not([disabled])',
64
+ 'textarea:not([disabled])',
65
+ 'select:not([disabled])',
66
+ 'a[href]',
67
+ '[tabindex]:not([tabindex="-1"])',
68
+ 'audio[controls]',
69
+ 'video[controls]',
70
+ '[contenteditable]:not([contenteditable="false"])'
71
+ ].join(', ')
72
+
73
+ return Array.from(root.querySelectorAll(focusableSelectors)) as HTMLElement[]
74
+ }, [])
75
+
76
+ // Create a focus trap within the container
77
+ const createFocusTrap = useCallback(() => {
78
+ const handleKeyDown = (event: KeyboardEvent) => {
79
+ if (event.key !== 'Tab') return
80
+
81
+ const focusableElements = getFocusableElements()
82
+ if (focusableElements.length === 0) return
83
+
84
+ const firstFocusable = focusableElements[0]
85
+ const lastFocusable = focusableElements[focusableElements.length - 1]
86
+
87
+ if (event.shiftKey) {
88
+ // Shift + Tab
89
+ if (document.activeElement === firstFocusable) {
90
+ event.preventDefault()
91
+ lastFocusable.focus()
92
+ }
93
+ } else {
94
+ // Tab
95
+ if (document.activeElement === lastFocusable) {
96
+ event.preventDefault()
97
+ firstFocusable.focus()
98
+ }
99
+ }
100
+ }
101
+
102
+ document.addEventListener('keydown', handleKeyDown)
103
+ return () => document.removeEventListener('keydown', handleKeyDown)
104
+ }, [getFocusableElements])
105
+
106
+ // Navigate to next/previous focusable element
107
+ const navigateFocus = useCallback((direction: 'next' | 'previous') => {
108
+ const focusableElements = getFocusableElements()
109
+ const currentIndex = focusableElements.indexOf(document.activeElement as HTMLElement)
110
+
111
+ let nextIndex: number
112
+ if (direction === 'next') {
113
+ nextIndex = currentIndex + 1 >= focusableElements.length ? 0 : currentIndex + 1
114
+ } else {
115
+ nextIndex = currentIndex - 1 < 0 ? focusableElements.length - 1 : currentIndex - 1
116
+ }
117
+
118
+ focusableElements[nextIndex]?.focus()
119
+ }, [getFocusableElements])
120
+
121
+ // Initialize focus on mount
122
+ useEffect(() => {
123
+ if (initialFocus) {
124
+ const timer = setTimeout(() => {
125
+ focusElement(initialFocus)
126
+ }, 0)
127
+ return () => clearTimeout(timer)
128
+ }
129
+ }, [initialFocus, focusElement])
130
+
131
+ return {
132
+ containerRef,
133
+ storePreviousFocus,
134
+ restorePreviousFocus,
135
+ focusElement,
136
+ getFocusableElements,
137
+ createFocusTrap,
138
+ navigateFocus
139
+ }
140
+ }
141
+
142
+ // Hook for managing focus within media player controls
143
+ export function useMediaPlayerFocus() {
144
+ const focusManager = useFocusManagement({
145
+ restoreFocus: true,
146
+ initialFocus: '[data-media-control="play-pause"]'
147
+ })
148
+
149
+ // Focus on play/pause button
150
+ const focusPlayPause = useCallback(() => {
151
+ focusManager.focusElement('[data-media-control="play-pause"]')
152
+ }, [focusManager])
153
+
154
+ // Focus on seek bar
155
+ const focusSeekBar = useCallback(() => {
156
+ focusManager.focusElement('[data-media-control="seek-bar"]')
157
+ }, [focusManager])
158
+
159
+ // Focus on volume control
160
+ const focusVolumeControl = useCallback(() => {
161
+ focusManager.focusElement('[data-media-control="volume"]')
162
+ }, [focusManager])
163
+
164
+ // Focus on settings button
165
+ const focusSettings = useCallback(() => {
166
+ focusManager.focusElement('[data-media-control="settings"]')
167
+ }, [focusManager])
168
+
169
+ return {
170
+ ...focusManager,
171
+ focusPlayPause,
172
+ focusSeekBar,
173
+ focusVolumeControl,
174
+ focusSettings
175
+ }
176
+ }
@@ -0,0 +1,144 @@
1
+ import { useEffect, useCallback, useRef } from 'react'
2
+
3
+ export interface KeyboardShortcut {
4
+ key: string
5
+ ctrlKey?: boolean
6
+ altKey?: boolean
7
+ shiftKey?: boolean
8
+ action: () => void
9
+ description: string
10
+ preventDefault?: boolean
11
+ }
12
+
13
+ export interface UseKeyboardShortcutsOptions {
14
+ shortcuts: KeyboardShortcut[]
15
+ enabled?: boolean
16
+ target?: HTMLElement | null
17
+ }
18
+
19
+ export function useKeyboardShortcuts({
20
+ shortcuts,
21
+ enabled = true,
22
+ target
23
+ }: UseKeyboardShortcutsOptions) {
24
+ const shortcutsRef = useRef(shortcuts)
25
+ shortcutsRef.current = shortcuts
26
+
27
+ const handleKeyDown = useCallback((event: KeyboardEvent) => {
28
+ if (!enabled) return
29
+
30
+ // Don't trigger shortcuts when user is typing in inputs
31
+ if (
32
+ event.target instanceof HTMLInputElement ||
33
+ event.target instanceof HTMLTextAreaElement ||
34
+ (event.target as HTMLElement)?.contentEditable === 'true'
35
+ ) {
36
+ return
37
+ }
38
+
39
+ const shortcut = shortcutsRef.current.find(s =>
40
+ s.key.toLowerCase() === event.key.toLowerCase() &&
41
+ !!s.ctrlKey === event.ctrlKey &&
42
+ !!s.altKey === event.altKey &&
43
+ !!s.shiftKey === event.shiftKey
44
+ )
45
+
46
+ if (shortcut) {
47
+ if (shortcut.preventDefault !== false) {
48
+ event.preventDefault()
49
+ }
50
+ shortcut.action()
51
+ }
52
+ }, [enabled])
53
+
54
+ useEffect(() => {
55
+ const element = target || document
56
+ element.addEventListener('keydown', handleKeyDown)
57
+
58
+ return () => {
59
+ element.removeEventListener('keydown', handleKeyDown)
60
+ }
61
+ }, [handleKeyDown, target])
62
+
63
+ return {
64
+ shortcuts: shortcuts.map(s => ({
65
+ key: s.key,
66
+ description: s.description,
67
+ modifiers: {
68
+ ctrl: s.ctrlKey,
69
+ alt: s.altKey,
70
+ shift: s.shiftKey
71
+ }
72
+ }))
73
+ }
74
+ }
75
+
76
+ // Pre-defined media player shortcuts
77
+ export const createMediaKeyboardShortcuts = (controls: {
78
+ play: () => void
79
+ pause: () => void
80
+ togglePlayPause: () => void
81
+ seekForward: (seconds?: number) => void
82
+ seekBackward: (seconds?: number) => void
83
+ volumeUp: () => void
84
+ volumeDown: () => void
85
+ toggleMute: () => void
86
+ toggleFullscreen: () => void
87
+ showHelp?: () => void
88
+ }): KeyboardShortcut[] => [
89
+ {
90
+ key: ' ',
91
+ action: controls.togglePlayPause,
92
+ description: 'Play/Pause'
93
+ },
94
+ {
95
+ key: 'k',
96
+ action: controls.togglePlayPause,
97
+ description: 'Play/Pause (K)'
98
+ },
99
+ {
100
+ key: 'ArrowLeft',
101
+ action: () => controls.seekBackward(10),
102
+ description: 'Seek backward 10 seconds'
103
+ },
104
+ {
105
+ key: 'ArrowRight',
106
+ action: () => controls.seekForward(10),
107
+ description: 'Seek forward 10 seconds'
108
+ },
109
+ {
110
+ key: 'j',
111
+ action: () => controls.seekBackward(10),
112
+ description: 'Seek backward 10 seconds (J)'
113
+ },
114
+ {
115
+ key: 'l',
116
+ action: () => controls.seekForward(10),
117
+ description: 'Seek forward 10 seconds (L)'
118
+ },
119
+ {
120
+ key: 'ArrowUp',
121
+ action: controls.volumeUp,
122
+ description: 'Volume up'
123
+ },
124
+ {
125
+ key: 'ArrowDown',
126
+ action: controls.volumeDown,
127
+ description: 'Volume down'
128
+ },
129
+ {
130
+ key: 'm',
131
+ action: controls.toggleMute,
132
+ description: 'Toggle mute'
133
+ },
134
+ {
135
+ key: 'f',
136
+ action: controls.toggleFullscreen,
137
+ description: 'Toggle fullscreen'
138
+ },
139
+ ...(controls.showHelp ? [{
140
+ key: '?',
141
+ action: controls.showHelp,
142
+ description: 'Show keyboard shortcuts help'
143
+ }] : [])
144
+ ]
@@ -0,0 +1,182 @@
1
+ import { useRef, useCallback, useEffect } from 'react'
2
+
3
+ export type AnnouncementPriority = 'assertive' | 'polite' | 'off'
4
+
5
+ export interface UseScreenReaderOptions {
6
+ defaultPriority?: AnnouncementPriority
7
+ debounceMs?: number
8
+ }
9
+
10
+ export function useScreenReader({
11
+ defaultPriority = 'polite',
12
+ debounceMs = 100
13
+ }: UseScreenReaderOptions = {}) {
14
+ const liveRegionRef = useRef<HTMLDivElement | null>(null)
15
+ const timeoutRef = useRef<NodeJS.Timeout>()
16
+
17
+ // Create or get the live region for announcements
18
+ const getLiveRegion = useCallback((priority: AnnouncementPriority) => {
19
+ if (!liveRegionRef.current) {
20
+ const liveRegion = document.createElement('div')
21
+ liveRegion.setAttribute('aria-live', priority)
22
+ liveRegion.setAttribute('aria-atomic', 'true')
23
+ liveRegion.setAttribute('class', 'sr-only')
24
+ liveRegion.style.cssText = `
25
+ position: absolute !important;
26
+ width: 1px !important;
27
+ height: 1px !important;
28
+ padding: 0 !important;
29
+ margin: -1px !important;
30
+ overflow: hidden !important;
31
+ clip: rect(0, 0, 0, 0) !important;
32
+ white-space: nowrap !important;
33
+ border: 0 !important;
34
+ `
35
+ document.body.appendChild(liveRegion)
36
+ liveRegionRef.current = liveRegion
37
+ }
38
+
39
+ // Update priority if needed
40
+ if (liveRegionRef.current.getAttribute('aria-live') !== priority) {
41
+ liveRegionRef.current.setAttribute('aria-live', priority)
42
+ }
43
+
44
+ return liveRegionRef.current
45
+ }, [])
46
+
47
+ // Announce a message to screen readers
48
+ const announce = useCallback((
49
+ message: string,
50
+ priority: AnnouncementPriority = defaultPriority
51
+ ) => {
52
+ if (!message.trim() || priority === 'off') return
53
+
54
+ // Clear existing timeout
55
+ if (timeoutRef.current) {
56
+ clearTimeout(timeoutRef.current)
57
+ }
58
+
59
+ // Debounce announcements
60
+ timeoutRef.current = setTimeout(() => {
61
+ const liveRegion = getLiveRegion(priority)
62
+
63
+ // Clear and then set the message to ensure it's announced
64
+ liveRegion.textContent = ''
65
+
66
+ // Use requestAnimationFrame to ensure the clear happens first
67
+ requestAnimationFrame(() => {
68
+ liveRegion.textContent = message
69
+ })
70
+ }, debounceMs)
71
+ }, [defaultPriority, debounceMs, getLiveRegion])
72
+
73
+ // Cleanup on unmount
74
+ useEffect(() => {
75
+ return () => {
76
+ if (timeoutRef.current) {
77
+ clearTimeout(timeoutRef.current)
78
+ }
79
+ if (liveRegionRef.current) {
80
+ document.body.removeChild(liveRegionRef.current)
81
+ liveRegionRef.current = null
82
+ }
83
+ }
84
+ }, [])
85
+
86
+ return { announce }
87
+ }
88
+
89
+ // Hook specifically for media player screen reader announcements
90
+ export function useMediaScreenReader() {
91
+ const { announce } = useScreenReader({ defaultPriority: 'polite' })
92
+
93
+ // Announce playback state changes
94
+ const announcePlaybackState = useCallback((isPlaying: boolean) => {
95
+ announce(isPlaying ? 'Playing' : 'Paused', 'assertive')
96
+ }, [announce])
97
+
98
+ // Announce volume changes
99
+ const announceVolume = useCallback((volume: number, isMuted: boolean) => {
100
+ if (isMuted) {
101
+ announce('Muted', 'assertive')
102
+ } else {
103
+ const volumePercent = Math.round(volume * 100)
104
+ announce(`Volume ${volumePercent}%`, 'polite')
105
+ }
106
+ }, [announce])
107
+
108
+ // Announce time position changes
109
+ const announceTime = useCallback((currentTime: number, duration: number) => {
110
+ const formatTime = (seconds: number) => {
111
+ const hours = Math.floor(seconds / 3600)
112
+ const minutes = Math.floor((seconds % 3600) / 60)
113
+ const secs = Math.floor(seconds % 60)
114
+
115
+ if (hours > 0) {
116
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
117
+ }
118
+ return `${minutes}:${secs.toString().padStart(2, '0')}`
119
+ }
120
+
121
+ const current = formatTime(currentTime)
122
+ const total = formatTime(duration)
123
+ announce(`${current} of ${total}`, 'polite')
124
+ }, [announce])
125
+
126
+ // Announce fullscreen state changes
127
+ const announceFullscreen = useCallback((isFullscreen: boolean) => {
128
+ announce(isFullscreen ? 'Entered fullscreen' : 'Exited fullscreen', 'assertive')
129
+ }, [announce])
130
+
131
+ // Announce subtitle state changes
132
+ const announceSubtitles = useCallback((enabled: boolean, language?: string) => {
133
+ if (enabled) {
134
+ announce(language ? `Subtitles enabled in ${language}` : 'Subtitles enabled', 'assertive')
135
+ } else {
136
+ announce('Subtitles disabled', 'assertive')
137
+ }
138
+ }, [announce])
139
+
140
+ // Announce playback speed changes
141
+ const announcePlaybackSpeed = useCallback((speed: number) => {
142
+ if (speed === 1) {
143
+ announce('Normal playback speed', 'assertive')
144
+ } else {
145
+ announce(`Playback speed ${speed}x`, 'assertive')
146
+ }
147
+ }, [announce])
148
+
149
+ // Announce quality changes
150
+ const announceQuality = useCallback((quality: string) => {
151
+ announce(`Quality changed to ${quality}`, 'polite')
152
+ }, [announce])
153
+
154
+ // Announce loading state
155
+ const announceLoading = useCallback((isLoading: boolean) => {
156
+ announce(isLoading ? 'Loading...' : 'Loading complete', 'polite')
157
+ }, [announce])
158
+
159
+ // Announce errors
160
+ const announceError = useCallback((error: string) => {
161
+ announce(`Error: ${error}`, 'assertive')
162
+ }, [announce])
163
+
164
+ // Announce buffering state
165
+ const announceBuffering = useCallback((isBuffering: boolean) => {
166
+ announce(isBuffering ? 'Buffering...' : 'Buffering complete', 'polite')
167
+ }, [announce])
168
+
169
+ return {
170
+ announce,
171
+ announcePlaybackState,
172
+ announceVolume,
173
+ announceTime,
174
+ announceFullscreen,
175
+ announceSubtitles,
176
+ announcePlaybackSpeed,
177
+ announceQuality,
178
+ announceLoading,
179
+ announceError,
180
+ announceBuffering
181
+ }
182
+ }