@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.
- package/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/context/media.js +2 -0
- package/dist/context/media.js.map +1 -0
- package/dist/context/media.mjs +15 -0
- package/dist/context/media.mjs.map +1 -0
- package/dist/context/store.js +2 -0
- package/dist/context/store.js.map +1 -0
- package/dist/context/store.mjs +5 -0
- package/dist/context/store.mjs.map +1 -0
- package/dist/context-BMteAJDN.js +1140 -0
- package/dist/context-BMteAJDN.js.map +1 -0
- package/dist/context-DCJ_scmO.cjs +23 -0
- package/dist/context-DCJ_scmO.cjs.map +1 -0
- package/dist/core-events-B5W_j8yU.js +480 -0
- package/dist/core-events-B5W_j8yU.js.map +1 -0
- package/dist/core-events-CN0-Vhhq.cjs +2 -0
- package/dist/core-events-CN0-Vhhq.cjs.map +1 -0
- package/dist/core-plugins-Bq9KLany.js +696 -0
- package/dist/core-plugins-Bq9KLany.js.map +1 -0
- package/dist/core-plugins-CPDSHMiQ.cjs +2 -0
- package/dist/core-plugins-CPDSHMiQ.cjs.map +1 -0
- package/dist/core.js +37 -0
- package/dist/core.js.map +1 -0
- package/dist/core.mjs +1217 -0
- package/dist/core.mjs.map +1 -0
- package/dist/enhanced-components-DHv0MFvG.js +1073 -0
- package/dist/enhanced-components-DHv0MFvG.js.map +1 -0
- package/dist/enhanced-components-DTIObgB7.cjs +2 -0
- package/dist/enhanced-components-DTIObgB7.cjs.map +1 -0
- package/dist/hooks-C_r8wexn.cjs +4 -0
- package/dist/hooks-C_r8wexn.cjs.map +1 -0
- package/dist/hooks-Da2wVXlF.js +1630 -0
- package/dist/hooks-Da2wVXlF.js.map +1 -0
- package/dist/index.css +1 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +6126 -0
- package/dist/index.mjs.map +1 -0
- package/dist/minimal-components-DSq5sQpy.js +2002 -0
- package/dist/minimal-components-DSq5sQpy.js.map +1 -0
- package/dist/minimal-components-DflWiihN.cjs +2 -0
- package/dist/minimal-components-DflWiihN.cjs.map +1 -0
- package/dist/minimal-components.css +1 -0
- package/dist/mona-loading.gif +0 -0
- package/dist/performance-monitor-CUW6f5ll.cjs +2 -0
- package/dist/performance-monitor-CUW6f5ll.cjs.map +1 -0
- package/dist/performance-monitor-rMW1RgV3.js +302 -0
- package/dist/performance-monitor-rMW1RgV3.js.map +1 -0
- package/dist/utils-BnscpYYd.js +2798 -0
- package/dist/utils-BnscpYYd.js.map +1 -0
- package/dist/utils-D_czS_0K.cjs +9 -0
- package/dist/utils-D_czS_0K.cjs.map +1 -0
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +7 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +114 -0
- package/src/adapters/videojs-adapter.tsx +470 -0
- package/src/assets/mona-loading.gif +0 -0
- package/src/components/AnimateLoading.tsx +115 -0
- package/src/components/LoadingState.tsx +248 -0
- package/src/components/accessibility/keyboard-help-overlay.tsx +213 -0
- package/src/components/audio-player.tsx +85 -0
- package/src/components/basic/index.ts +37 -0
- package/src/components/enhanced/advanced-features.tsx +316 -0
- package/src/components/enhanced/debug-panel.tsx +422 -0
- package/src/components/enhanced/enhanced-controls.tsx +600 -0
- package/src/components/enhanced/enhanced-media-player.tsx +509 -0
- package/src/components/enhanced/error-handling.tsx +345 -0
- package/src/components/enhanced/index.ts +39 -0
- package/src/components/enhanced/media-player-enhanced.tsx +349 -0
- package/src/components/examples/basic-video-player.tsx +55 -0
- package/src/components/examples/custom-controls-example.tsx +63 -0
- package/src/components/examples/enhanced-controls-example.tsx +114 -0
- package/src/components/examples/enhanced-media-player-example.tsx +293 -0
- package/src/components/examples/loading-states-demo.tsx +142 -0
- package/src/components/examples/react-19-demo.tsx +381 -0
- package/src/components/examples/volume-orientation-demo.tsx +114 -0
- package/src/components/icon-demo.tsx +209 -0
- package/src/components/icons/index.tsx +166 -0
- package/src/components/icons/svg-icons/ArrowLeftIcon.tsx +18 -0
- package/src/components/icons/svg-icons/ArrowRightIcon.tsx +18 -0
- package/src/components/icons/svg-icons/AudioIcon.tsx +22 -0
- package/src/components/icons/svg-icons/BackwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/CameraIcon.tsx +19 -0
- package/src/components/icons/svg-icons/CheckIcon.tsx +19 -0
- package/src/components/icons/svg-icons/EnablePiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/ForwardIcon.tsx +12 -0
- package/src/components/icons/svg-icons/FullscreenEnterIcon.tsx +23 -0
- package/src/components/icons/svg-icons/FullscreenExitIcon.tsx +23 -0
- package/src/components/icons/svg-icons/LoadingIcon.tsx +32 -0
- package/src/components/icons/svg-icons/PauseIcon.tsx +23 -0
- package/src/components/icons/svg-icons/PlayIcon.tsx +21 -0
- package/src/components/icons/svg-icons/PlaybackSpeedIcon.tsx +23 -0
- package/src/components/icons/svg-icons/QualityIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SettingsIcon.tsx +12 -0
- package/src/components/icons/svg-icons/SliderIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleIcon.tsx +21 -0
- package/src/components/icons/svg-icons/SubtitleOffIcon.tsx +25 -0
- package/src/components/icons/svg-icons/UnPiPIcon.tsx +17 -0
- package/src/components/icons/svg-icons/VolumeMutedIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeOneIcon.tsx +22 -0
- package/src/components/icons/svg-icons/VolumeThreeIcon.tsx +16 -0
- package/src/components/icons/svg-icons/VolumeTwoIcon.tsx +16 -0
- package/src/components/legacy.tsx +93 -0
- package/src/components/media-metadata-display.tsx +100 -0
- package/src/components/media-player-audio.tsx +39 -0
- package/src/components/media-player-controls.tsx +226 -0
- package/src/components/media-player-error.tsx +163 -0
- package/src/components/media-player-fullscreen.tsx +109 -0
- package/src/components/media-player-hls.tsx +111 -0
- package/src/components/media-player-loading.tsx +76 -0
- package/src/components/media-player-play.tsx +75 -0
- package/src/components/media-player-root.tsx +340 -0
- package/src/components/media-player-seek.tsx +168 -0
- package/src/components/media-player-time.tsx +99 -0
- package/src/components/media-player-tooltip.tsx +58 -0
- package/src/components/media-player-video.tsx +39 -0
- package/src/components/media-player-volume.tsx +155 -0
- package/src/components/media-player.tsx +257 -0
- package/src/components/media-suspense.tsx +362 -0
- package/src/components/player-settings-form.tsx +163 -0
- package/src/components/react-player-wrapper.tsx +651 -0
- package/src/components/simple-video-player.tsx +60 -0
- package/src/components/ui/enhanced-seek-bar.tsx +235 -0
- package/src/components/ui/fullscreen-button.tsx +60 -0
- package/src/components/ui/media-resource-preloader.tsx +395 -0
- package/src/components/ui/play-button.tsx +107 -0
- package/src/components/ui/seek-bar.tsx +161 -0
- package/src/components/ui/time-display.tsx +40 -0
- package/src/components/ui/volume-control.tsx +143 -0
- package/src/components/unified/base-media-player.tsx +246 -0
- package/src/components/unified/media-player.tsx +75 -0
- package/src/components/variants/minimal/AccessibleMinimalPlayer.tsx +561 -0
- package/src/components/variants/minimal/EnhancedMinimalMediaPlayer.tsx +552 -0
- package/src/components/variants/minimal/MinimalControls.tsx +142 -0
- package/src/components/variants/minimal/MinimalLoading.tsx +32 -0
- package/src/components/variants/minimal/MinimalMediaPlayer.tsx +315 -0
- package/src/components/variants/minimal/MinimalOverlay.tsx +64 -0
- package/src/components/variants/minimal/MinimalPlayButton.tsx +65 -0
- package/src/components/variants/minimal/MinimalSeekBar.tsx +150 -0
- package/src/components/variants/minimal/MinimalVolumeControl.tsx +80 -0
- package/src/components/variants/minimal/SimpleMinimalPlayer.tsx +210 -0
- package/src/components/variants/minimal/components/CenterPlayButton.tsx +22 -0
- package/src/components/variants/minimal/components/ControlBar.tsx +146 -0
- package/src/components/variants/minimal/components/EnhancedSettingsMenu.tsx +361 -0
- package/src/components/variants/minimal/components/PlaybackControls.tsx +51 -0
- package/src/components/variants/minimal/components/RightControls.tsx +173 -0
- package/src/components/variants/minimal/components/SeekBar.tsx +131 -0
- package/src/components/variants/minimal/components/SettingsDropdown.tsx +70 -0
- package/src/components/variants/minimal/components/SubtitleDisplay.tsx +21 -0
- package/src/components/variants/minimal/components/TimeDisplay.tsx +18 -0
- package/src/components/variants/minimal/components/VideoElement.tsx +40 -0
- package/src/components/variants/minimal/components/VolumeControl.tsx +217 -0
- package/src/components/variants/minimal/index.ts +23 -0
- package/src/components/video-player.tsx +100 -0
- package/src/context/error-handling.ts +394 -0
- package/src/context/media-context.tsx +208 -0
- package/src/context/media-provider.tsx +265 -0
- package/src/context/media-store.ts +379 -0
- package/src/context/persistence.ts +453 -0
- package/src/core/events/event-system.ts +442 -0
- package/src/core/events/hooks.ts +302 -0
- package/src/core/events/index.ts +22 -0
- package/src/core/events/middleware.ts +269 -0
- package/src/core/index.ts +22 -0
- package/src/core/optimization/bundle-optimizer.tsx +308 -0
- package/src/core/performance/cicd.ts +96 -0
- package/src/core/performance/index.ts +38 -0
- package/src/core/performance/media-loading-optimizer.ts +538 -0
- package/src/core/performance/performance-budgets.ts +432 -0
- package/src/core/performance/performance-monitor.tsx +476 -0
- package/src/core/performance/performance-testing.ts +459 -0
- package/src/core/plugins/base-plugin.ts +224 -0
- package/src/core/plugins/enhanced-plugin-system.ts +411 -0
- package/src/core/plugins/examples/analytics-plugin.ts +242 -0
- package/src/core/plugins/index.ts +39 -0
- package/src/core/plugins/registry.ts +327 -0
- package/src/core/plugins/types.ts +159 -0
- package/src/core/plugins/utils.ts +316 -0
- package/src/core/progressive-loading.tsx +322 -0
- package/src/core/providers/enhanced-hls-provider.ts +339 -0
- package/src/core/providers/service-provider.tsx +168 -0
- package/src/core/utils.tsx +38 -0
- package/src/features/media-session/media-session-manager.ts +429 -0
- package/src/features/picture-in-picture/pip-manager.ts +429 -0
- package/src/features/playlist/playlist-manager.ts +606 -0
- package/src/features/subtitles/subtitle-manager.ts +441 -0
- package/src/features/subtitles/subtitle-parser.ts +280 -0
- package/src/features/thumbnails/thumbnail-generator.ts +519 -0
- package/src/hooks/accessibility/index.ts +27 -0
- package/src/hooks/accessibility/use-accessibility-preferences.ts +188 -0
- package/src/hooks/accessibility/use-focus-management.ts +176 -0
- package/src/hooks/accessibility/use-keyboard-shortcuts.ts +144 -0
- package/src/hooks/accessibility/use-screen-reader.ts +182 -0
- package/src/hooks/core/index.ts +47 -0
- package/src/hooks/legacy.ts +27 -0
- package/src/hooks/use-enhanced-media-player.ts +468 -0
- package/src/hooks/use-intelligent-preloading.ts +358 -0
- package/src/hooks/use-keyboard-controls.ts +115 -0
- package/src/hooks/use-media-actions.ts +175 -0
- package/src/hooks/use-media-metadata.ts +89 -0
- package/src/hooks/use-media-optimistic.ts +270 -0
- package/src/hooks/use-media-player.ts +312 -0
- package/src/hooks/use-media-state.ts +190 -0
- package/src/hooks/use-memory-optimization.ts +358 -0
- package/src/hooks/use-optimistic-controls.ts +92 -0
- package/src/hooks/use-optimized-media.ts +263 -0
- package/src/hooks/use-touch-gestures.ts +417 -0
- package/src/hooks/useEnhancedSubtitles.ts +258 -0
- package/src/hooks/useKeyboardShortcuts.ts +74 -0
- package/src/hooks/useMediaControls.ts +52 -0
- package/src/hooks/useScreenshot.ts +57 -0
- package/src/hooks/useSubtitles.ts +91 -0
- package/src/hooks/useVideoPlayer.ts +232 -0
- package/src/icons/components/FullscreenIcon.tsx +37 -0
- package/src/icons/components/SettingsIcon.tsx +38 -0
- package/src/icons/components/animated/LoadingIcon.tsx +43 -0
- package/src/icons/components/animated/PlayPauseIcon.tsx +50 -0
- package/src/icons/components/media/PauseIcon.tsx +39 -0
- package/src/icons/components/media/PlayIcon.tsx +38 -0
- package/src/icons/components/volume/VolumeIcon.tsx +39 -0
- package/src/icons/components/volume/VolumeOffIcon.tsx +39 -0
- package/src/icons/index.ts +56 -0
- package/src/icons/provider.tsx +64 -0
- package/src/icons/types.ts +40 -0
- package/src/index-enhanced.ts +35 -0
- package/src/index-legacy.ts +326 -0
- package/src/index.css +102 -0
- package/src/index.ts +216 -0
- package/src/lib/deprecation.ts +51 -0
- package/src/lib/format-time.ts +37 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/globals.css +462 -0
- package/src/styles/minimal-player.css +589 -0
- package/src/styles/pixel-art.css +46 -0
- package/src/styles/videojs.css +100 -0
- package/src/test/setup.ts +79 -0
- package/src/types/events.ts +47 -0
- package/src/types/formats.ts +95 -0
- package/src/types/index.ts +190 -0
- 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
|
+
}
|