@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,89 @@
|
|
|
1
|
+
import { use, cache } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface MediaMetadata {
|
|
4
|
+
title?: string
|
|
5
|
+
artist?: string
|
|
6
|
+
duration: number
|
|
7
|
+
format: string
|
|
8
|
+
width?: number
|
|
9
|
+
height?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Cache metadata fetches to prevent duplicate requests
|
|
13
|
+
const fetchMetadata = cache(async (src: string): Promise<MediaMetadata> => {
|
|
14
|
+
try {
|
|
15
|
+
// Create a video element to get metadata
|
|
16
|
+
const video = document.createElement('video')
|
|
17
|
+
video.crossOrigin = 'anonymous'
|
|
18
|
+
video.preload = 'metadata'
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const timeout = setTimeout(() => {
|
|
22
|
+
video.removeEventListener('loadedmetadata', onMetadata)
|
|
23
|
+
video.removeEventListener('error', onError)
|
|
24
|
+
reject(new Error('Metadata loading timeout'))
|
|
25
|
+
}, 10000) // 10 second timeout
|
|
26
|
+
|
|
27
|
+
const onMetadata = () => {
|
|
28
|
+
clearTimeout(timeout)
|
|
29
|
+
video.removeEventListener('loadedmetadata', onMetadata)
|
|
30
|
+
video.removeEventListener('error', onError)
|
|
31
|
+
|
|
32
|
+
const metadata: MediaMetadata = {
|
|
33
|
+
duration: video.duration || 0,
|
|
34
|
+
format: getFormatFromSrc(src),
|
|
35
|
+
width: video.videoWidth || undefined,
|
|
36
|
+
height: video.videoHeight || undefined,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resolve(metadata)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const onError = (error: Event) => {
|
|
43
|
+
clearTimeout(timeout)
|
|
44
|
+
video.removeEventListener('loadedmetadata', onMetadata)
|
|
45
|
+
video.removeEventListener('error', onError)
|
|
46
|
+
reject(new Error('Failed to load media metadata'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
video.addEventListener('loadedmetadata', onMetadata)
|
|
50
|
+
video.addEventListener('error', onError)
|
|
51
|
+
video.src = src
|
|
52
|
+
})
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`Failed to fetch metadata: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
function getFormatFromSrc(src: string): string {
|
|
59
|
+
const url = new URL(src, window.location.href)
|
|
60
|
+
const pathname = url.pathname.toLowerCase()
|
|
61
|
+
|
|
62
|
+
if (pathname.endsWith('.mp4')) return 'MP4'
|
|
63
|
+
if (pathname.endsWith('.webm')) return 'WebM'
|
|
64
|
+
if (pathname.endsWith('.ogg')) return 'OGG'
|
|
65
|
+
if (pathname.endsWith('.m3u8')) return 'HLS'
|
|
66
|
+
if (pathname.endsWith('.mpd')) return 'DASH'
|
|
67
|
+
|
|
68
|
+
return 'Unknown'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useMediaMetadata(src: string): MediaMetadata {
|
|
72
|
+
// Use the React 19 use() hook for async data
|
|
73
|
+
return use(fetchMetadata(src))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Utility function to format time
|
|
77
|
+
export function formatTime(seconds: number): string {
|
|
78
|
+
if (!isFinite(seconds) || isNaN(seconds)) return '0:00'
|
|
79
|
+
|
|
80
|
+
const hours = Math.floor(seconds / 3600)
|
|
81
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
82
|
+
const secs = Math.floor(seconds % 60)
|
|
83
|
+
|
|
84
|
+
if (hours > 0) {
|
|
85
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
|
86
|
+
} else {
|
|
87
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useOptimistic } from "react"
|
|
4
|
+
import type { MediaState } from "../context/media-context"
|
|
5
|
+
import type { OptimisticMediaAction, EnhancedMediaStore } from "../context/media-store"
|
|
6
|
+
|
|
7
|
+
// Optimistic update reducer function
|
|
8
|
+
function optimisticMediaReducer(
|
|
9
|
+
state: MediaState,
|
|
10
|
+
action: OptimisticMediaAction
|
|
11
|
+
): MediaState {
|
|
12
|
+
if (!action.optimistic) return state
|
|
13
|
+
|
|
14
|
+
// Apply immediate optimistic updates for better UX
|
|
15
|
+
switch (action.type) {
|
|
16
|
+
case "PLAY":
|
|
17
|
+
return { ...state, isPlaying: true, error: null }
|
|
18
|
+
|
|
19
|
+
case "PAUSE":
|
|
20
|
+
return { ...state, isPlaying: false }
|
|
21
|
+
|
|
22
|
+
case "SEEK":
|
|
23
|
+
return { ...state, currentTime: action.detail }
|
|
24
|
+
|
|
25
|
+
case "SET_VOLUME":
|
|
26
|
+
return { ...state, volume: Math.max(0, Math.min(1, action.detail)) }
|
|
27
|
+
|
|
28
|
+
case "MUTE":
|
|
29
|
+
return { ...state, isMuted: true }
|
|
30
|
+
|
|
31
|
+
case "UNMUTE":
|
|
32
|
+
return { ...state, isMuted: false }
|
|
33
|
+
|
|
34
|
+
case "TOGGLE_MUTE":
|
|
35
|
+
return { ...state, isMuted: !state.isMuted }
|
|
36
|
+
|
|
37
|
+
case "ENTER_FULLSCREEN":
|
|
38
|
+
return { ...state, isFullscreen: true }
|
|
39
|
+
|
|
40
|
+
case "EXIT_FULLSCREEN":
|
|
41
|
+
return { ...state, isFullscreen: false }
|
|
42
|
+
|
|
43
|
+
case "SET_PLAYBACK_RATE":
|
|
44
|
+
return { ...state, playbackRate: action.detail }
|
|
45
|
+
|
|
46
|
+
case "TOGGLE_SUBTITLES":
|
|
47
|
+
return { ...state, subtitlesShowing: !state.subtitlesShowing }
|
|
48
|
+
|
|
49
|
+
case "PREVIEW_TIME":
|
|
50
|
+
return { ...state, previewTime: action.detail }
|
|
51
|
+
|
|
52
|
+
case "CLEAR_PREVIEW":
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
previewTime: null,
|
|
56
|
+
previewImage: null,
|
|
57
|
+
previewCoords: null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
default:
|
|
61
|
+
return state
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Media optimistic state hook using React 19's useOptimistic
|
|
66
|
+
export function useMediaOptimistic(store: EnhancedMediaStore) {
|
|
67
|
+
const actualState = React.useSyncExternalStore(
|
|
68
|
+
store.subscribe,
|
|
69
|
+
store.getState,
|
|
70
|
+
store.getState
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const [optimisticState, addOptimistic] = useOptimistic(
|
|
74
|
+
actualState,
|
|
75
|
+
optimisticMediaReducer
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Enhanced dispatch that uses optimistic updates
|
|
79
|
+
const dispatchOptimistic = React.useCallback(async (action: OptimisticMediaAction) => {
|
|
80
|
+
// Add optimistic update immediately
|
|
81
|
+
if (action.optimistic) {
|
|
82
|
+
addOptimistic(action)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Dispatch the actual action
|
|
86
|
+
await store.dispatchOptimistic(action)
|
|
87
|
+
}, [store, addOptimistic])
|
|
88
|
+
|
|
89
|
+
// Quick actions with optimistic updates
|
|
90
|
+
const optimisticActions = React.useMemo(() => ({
|
|
91
|
+
// Play/Pause with immediate feedback
|
|
92
|
+
togglePlayPause: async () => {
|
|
93
|
+
const action: OptimisticMediaAction = {
|
|
94
|
+
type: optimisticState.isPlaying ? "PAUSE" : "PLAY",
|
|
95
|
+
optimistic: true,
|
|
96
|
+
timestamp: Date.now()
|
|
97
|
+
}
|
|
98
|
+
await dispatchOptimistic(action)
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Volume control with immediate slider feedback
|
|
102
|
+
setVolumeOptimistic: async (volume: number) => {
|
|
103
|
+
const action: OptimisticMediaAction = {
|
|
104
|
+
type: "SET_VOLUME",
|
|
105
|
+
detail: Math.max(0, Math.min(1, volume)),
|
|
106
|
+
optimistic: true,
|
|
107
|
+
timestamp: Date.now()
|
|
108
|
+
}
|
|
109
|
+
await dispatchOptimistic(action)
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// Seek with immediate scrub feedback
|
|
113
|
+
seekOptimistic: async (time: number) => {
|
|
114
|
+
const action: OptimisticMediaAction = {
|
|
115
|
+
type: "SEEK",
|
|
116
|
+
detail: time,
|
|
117
|
+
optimistic: true,
|
|
118
|
+
timestamp: Date.now()
|
|
119
|
+
}
|
|
120
|
+
await dispatchOptimistic(action)
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// Mute toggle with immediate icon change
|
|
124
|
+
toggleMuteOptimistic: async () => {
|
|
125
|
+
const action: OptimisticMediaAction = {
|
|
126
|
+
type: "TOGGLE_MUTE",
|
|
127
|
+
optimistic: true,
|
|
128
|
+
timestamp: Date.now()
|
|
129
|
+
}
|
|
130
|
+
await dispatchOptimistic(action)
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// Fullscreen toggle with immediate layout change
|
|
134
|
+
toggleFullscreenOptimistic: async () => {
|
|
135
|
+
const action: OptimisticMediaAction = {
|
|
136
|
+
type: optimisticState.isFullscreen ? "EXIT_FULLSCREEN" : "ENTER_FULLSCREEN",
|
|
137
|
+
optimistic: true,
|
|
138
|
+
timestamp: Date.now()
|
|
139
|
+
}
|
|
140
|
+
await dispatchOptimistic(action)
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Playback rate with immediate feedback
|
|
144
|
+
setPlaybackRateOptimistic: async (rate: number) => {
|
|
145
|
+
const action: OptimisticMediaAction = {
|
|
146
|
+
type: "SET_PLAYBACK_RATE",
|
|
147
|
+
detail: rate,
|
|
148
|
+
optimistic: true,
|
|
149
|
+
timestamp: Date.now()
|
|
150
|
+
}
|
|
151
|
+
await dispatchOptimistic(action)
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Subtitle toggle with immediate visibility change
|
|
155
|
+
toggleSubtitlesOptimistic: async () => {
|
|
156
|
+
const action: OptimisticMediaAction = {
|
|
157
|
+
type: "TOGGLE_SUBTITLES",
|
|
158
|
+
optimistic: true,
|
|
159
|
+
timestamp: Date.now()
|
|
160
|
+
}
|
|
161
|
+
await dispatchOptimistic(action)
|
|
162
|
+
}
|
|
163
|
+
}), [optimisticState, dispatchOptimistic])
|
|
164
|
+
|
|
165
|
+
// State selectors that return optimistic values
|
|
166
|
+
const selectors = React.useMemo(() => ({
|
|
167
|
+
isPlaying: optimisticState.isPlaying,
|
|
168
|
+
currentTime: optimisticState.currentTime,
|
|
169
|
+
volume: optimisticState.volume,
|
|
170
|
+
isMuted: optimisticState.isMuted,
|
|
171
|
+
isFullscreen: optimisticState.isFullscreen,
|
|
172
|
+
playbackRate: optimisticState.playbackRate,
|
|
173
|
+
subtitlesShowing: optimisticState.subtitlesShowing,
|
|
174
|
+
previewTime: optimisticState.previewTime,
|
|
175
|
+
|
|
176
|
+
// Computed values
|
|
177
|
+
progress: optimisticState.duration > 0
|
|
178
|
+
? optimisticState.currentTime / optimisticState.duration
|
|
179
|
+
: 0,
|
|
180
|
+
|
|
181
|
+
formattedTime: formatTime(optimisticState.currentTime),
|
|
182
|
+
formattedDuration: formatTime(optimisticState.duration),
|
|
183
|
+
|
|
184
|
+
// State checks
|
|
185
|
+
canPlay: !optimisticState.isLoading && !optimisticState.error,
|
|
186
|
+
hasError: !!optimisticState.error,
|
|
187
|
+
isBuffering: optimisticState.isLoading
|
|
188
|
+
}), [optimisticState])
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
state: optimisticState,
|
|
192
|
+
actualState,
|
|
193
|
+
...selectors,
|
|
194
|
+
...optimisticActions,
|
|
195
|
+
dispatchOptimistic
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Time formatting utility
|
|
200
|
+
function formatTime(seconds: number): string {
|
|
201
|
+
if (!seconds || !isFinite(seconds)) return "0:00"
|
|
202
|
+
|
|
203
|
+
const mins = Math.floor(seconds / 60)
|
|
204
|
+
const secs = Math.floor(seconds % 60)
|
|
205
|
+
const hours = Math.floor(mins / 60)
|
|
206
|
+
|
|
207
|
+
if (hours > 0) {
|
|
208
|
+
return `${hours}:${String(mins % 60).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return `${mins}:${String(secs).padStart(2, '0')}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Hook for components that need to detect optimistic updates
|
|
215
|
+
export function useOptimisticUpdates(store: EnhancedMediaStore) {
|
|
216
|
+
const [hasOptimisticUpdates, setHasOptimisticUpdates] = React.useState(false)
|
|
217
|
+
|
|
218
|
+
React.useEffect(() => {
|
|
219
|
+
const unsubscribe = store.subscribe(() => {
|
|
220
|
+
const actualState = store.getState()
|
|
221
|
+
const optimisticState = store.getOptimisticState()
|
|
222
|
+
|
|
223
|
+
const hasUpdates = JSON.stringify(actualState) !== JSON.stringify(optimisticState)
|
|
224
|
+
setHasOptimisticUpdates(hasUpdates)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return unsubscribe
|
|
228
|
+
}, [store])
|
|
229
|
+
|
|
230
|
+
return hasOptimisticUpdates
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Hook to get both actual and optimistic state for debugging
|
|
234
|
+
export function useMediaStateComparison(store: EnhancedMediaStore) {
|
|
235
|
+
const actualState = React.useSyncExternalStore(
|
|
236
|
+
store.subscribe,
|
|
237
|
+
store.getState,
|
|
238
|
+
store.getState
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const optimisticState = React.useSyncExternalStore(
|
|
242
|
+
store.subscribe,
|
|
243
|
+
store.getOptimisticState,
|
|
244
|
+
store.getOptimisticState
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const differences = React.useMemo(() => {
|
|
248
|
+
const diffs: any[] = []
|
|
249
|
+
|
|
250
|
+
const stateKeys = Object.keys(actualState) as Array<keyof MediaState>
|
|
251
|
+
stateKeys.forEach(key => {
|
|
252
|
+
if (JSON.stringify(actualState[key]) !== JSON.stringify(optimisticState[key])) {
|
|
253
|
+
diffs.push({
|
|
254
|
+
key: key as string,
|
|
255
|
+
actual: actualState[key],
|
|
256
|
+
optimistic: optimisticState[key]
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
return diffs
|
|
262
|
+
}, [actualState, optimisticState])
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
actualState,
|
|
266
|
+
optimisticState,
|
|
267
|
+
differences,
|
|
268
|
+
hasOptimisticUpdates: differences.length > 0
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from "react"
|
|
4
|
+
import type { MediaPlayerControls } from "../types/index"
|
|
5
|
+
import { useMediaState } from "./use-media-state"
|
|
6
|
+
|
|
7
|
+
interface UseMediaPlayerOptions {
|
|
8
|
+
src: string | string[]
|
|
9
|
+
autoPlay?: boolean
|
|
10
|
+
loop?: boolean
|
|
11
|
+
muted?: boolean
|
|
12
|
+
volume?: number
|
|
13
|
+
preload?: "none" | "metadata" | "auto"
|
|
14
|
+
crossOrigin?: "anonymous" | "use-credentials"
|
|
15
|
+
playbackRates?: number[]
|
|
16
|
+
keyboardControls?: boolean
|
|
17
|
+
showControls?: boolean
|
|
18
|
+
customControls?: boolean
|
|
19
|
+
onPlay?: () => void
|
|
20
|
+
onPause?: () => void
|
|
21
|
+
onTimeUpdate?: (currentTime: number) => void
|
|
22
|
+
onDurationChange?: (duration: number) => void
|
|
23
|
+
onVolumeChange?: (volume: number) => void
|
|
24
|
+
onError?: (error: string) => void
|
|
25
|
+
onLoadStart?: () => void
|
|
26
|
+
onLoadedData?: () => void
|
|
27
|
+
onEnded?: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useMediaPlayer(options: UseMediaPlayerOptions) {
|
|
31
|
+
const mediaRef = useRef<HTMLVideoElement | HTMLAudioElement>(null)
|
|
32
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
state,
|
|
36
|
+
setPlaying,
|
|
37
|
+
setCurrentTime,
|
|
38
|
+
setDuration,
|
|
39
|
+
setVolume,
|
|
40
|
+
setMuted,
|
|
41
|
+
setLoading,
|
|
42
|
+
setFullscreen,
|
|
43
|
+
setError,
|
|
44
|
+
setBuffered
|
|
45
|
+
} = useMediaState()
|
|
46
|
+
|
|
47
|
+
// Initialize media element with options
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const media = mediaRef.current
|
|
50
|
+
if (!media) return
|
|
51
|
+
|
|
52
|
+
// Set initial properties
|
|
53
|
+
if (options.volume !== undefined) {
|
|
54
|
+
media.volume = options.volume
|
|
55
|
+
setVolume(options.volume)
|
|
56
|
+
}
|
|
57
|
+
if (options.muted !== undefined) {
|
|
58
|
+
media.muted = options.muted
|
|
59
|
+
setMuted(options.muted)
|
|
60
|
+
}
|
|
61
|
+
if (options.autoPlay !== undefined) {
|
|
62
|
+
media.autoplay = options.autoPlay
|
|
63
|
+
}
|
|
64
|
+
if (options.loop !== undefined) {
|
|
65
|
+
media.loop = options.loop
|
|
66
|
+
}
|
|
67
|
+
if (options.preload !== undefined) {
|
|
68
|
+
media.preload = options.preload
|
|
69
|
+
}
|
|
70
|
+
if (options.crossOrigin !== undefined) {
|
|
71
|
+
media.crossOrigin = options.crossOrigin
|
|
72
|
+
}
|
|
73
|
+
}, [options, setVolume, setMuted])
|
|
74
|
+
|
|
75
|
+
// Event handlers
|
|
76
|
+
const handlePlay = () => {
|
|
77
|
+
setPlaying(true)
|
|
78
|
+
options.onPlay?.()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const handlePause = () => {
|
|
82
|
+
setPlaying(false)
|
|
83
|
+
options.onPause?.()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleTimeUpdate = () => {
|
|
87
|
+
const media = mediaRef.current
|
|
88
|
+
if (!media) return
|
|
89
|
+
|
|
90
|
+
setCurrentTime(media.currentTime)
|
|
91
|
+
options.onTimeUpdate?.(media.currentTime)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleDurationChange = () => {
|
|
95
|
+
const media = mediaRef.current
|
|
96
|
+
if (!media) return
|
|
97
|
+
|
|
98
|
+
setDuration(media.duration)
|
|
99
|
+
options.onDurationChange?.(media.duration)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const handleVolumeChange = () => {
|
|
103
|
+
const media = mediaRef.current
|
|
104
|
+
if (!media) return
|
|
105
|
+
|
|
106
|
+
setVolume(media.volume)
|
|
107
|
+
setMuted(media.muted)
|
|
108
|
+
options.onVolumeChange?.(media.volume)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const handleLoadStart = () => {
|
|
112
|
+
setLoading(true)
|
|
113
|
+
setError(null)
|
|
114
|
+
options.onLoadStart?.()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleLoadedData = () => {
|
|
118
|
+
setLoading(false)
|
|
119
|
+
options.onLoadedData?.()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleError = () => {
|
|
123
|
+
const media = mediaRef.current
|
|
124
|
+
if (!media) return
|
|
125
|
+
|
|
126
|
+
const errorMessage = media.error?.message || "An error occurred while loading the media"
|
|
127
|
+
setError(errorMessage)
|
|
128
|
+
setLoading(false)
|
|
129
|
+
options.onError?.(errorMessage)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const handleEnded = () => {
|
|
133
|
+
setPlaying(false)
|
|
134
|
+
options.onEnded?.()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const handleProgress = () => {
|
|
138
|
+
const media = mediaRef.current
|
|
139
|
+
if (!media) return
|
|
140
|
+
|
|
141
|
+
setBuffered(media.buffered)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Attach event listeners
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const media = mediaRef.current
|
|
147
|
+
if (!media) return
|
|
148
|
+
|
|
149
|
+
media.addEventListener("play", handlePlay)
|
|
150
|
+
media.addEventListener("pause", handlePause)
|
|
151
|
+
media.addEventListener("timeupdate", handleTimeUpdate)
|
|
152
|
+
media.addEventListener("durationchange", handleDurationChange)
|
|
153
|
+
media.addEventListener("volumechange", handleVolumeChange)
|
|
154
|
+
media.addEventListener("loadstart", handleLoadStart)
|
|
155
|
+
media.addEventListener("loadeddata", handleLoadedData)
|
|
156
|
+
media.addEventListener("error", handleError)
|
|
157
|
+
media.addEventListener("ended", handleEnded)
|
|
158
|
+
media.addEventListener("progress", handleProgress)
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
media.removeEventListener("play", handlePlay)
|
|
162
|
+
media.removeEventListener("pause", handlePause)
|
|
163
|
+
media.removeEventListener("timeupdate", handleTimeUpdate)
|
|
164
|
+
media.removeEventListener("durationchange", handleDurationChange)
|
|
165
|
+
media.removeEventListener("volumechange", handleVolumeChange)
|
|
166
|
+
media.removeEventListener("loadstart", handleLoadStart)
|
|
167
|
+
media.removeEventListener("loadeddata", handleLoadedData)
|
|
168
|
+
media.removeEventListener("error", handleError)
|
|
169
|
+
media.removeEventListener("ended", handleEnded)
|
|
170
|
+
media.removeEventListener("progress", handleProgress)
|
|
171
|
+
}
|
|
172
|
+
}, [
|
|
173
|
+
handlePlay,
|
|
174
|
+
handlePause,
|
|
175
|
+
handleTimeUpdate,
|
|
176
|
+
handleDurationChange,
|
|
177
|
+
handleVolumeChange,
|
|
178
|
+
handleLoadStart,
|
|
179
|
+
handleLoadedData,
|
|
180
|
+
handleError,
|
|
181
|
+
handleEnded,
|
|
182
|
+
handleProgress
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
// Control functions
|
|
186
|
+
const play = async () => {
|
|
187
|
+
const media = mediaRef.current
|
|
188
|
+
if (!media) return
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await media.play()
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error("Failed to play media:", error)
|
|
194
|
+
setError("Failed to play media")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const pause = () => {
|
|
199
|
+
const media = mediaRef.current
|
|
200
|
+
if (!media) return
|
|
201
|
+
|
|
202
|
+
media.pause()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const toggle = () => {
|
|
206
|
+
if (state.isPlaying) {
|
|
207
|
+
pause()
|
|
208
|
+
} else {
|
|
209
|
+
play()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const seek = (time: number) => {
|
|
214
|
+
const media = mediaRef.current
|
|
215
|
+
if (!media) return
|
|
216
|
+
|
|
217
|
+
media.currentTime = Math.max(0, Math.min(time, media.duration || 0))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const setVolumeControl = (volume: number) => {
|
|
221
|
+
const media = mediaRef.current
|
|
222
|
+
if (!media) return
|
|
223
|
+
|
|
224
|
+
const clampedVolume = Math.max(0, Math.min(1, volume))
|
|
225
|
+
media.volume = clampedVolume
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const mute = () => {
|
|
229
|
+
const media = mediaRef.current
|
|
230
|
+
if (!media) return
|
|
231
|
+
|
|
232
|
+
media.muted = true
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const unmute = () => {
|
|
236
|
+
const media = mediaRef.current
|
|
237
|
+
if (!media) return
|
|
238
|
+
|
|
239
|
+
media.muted = false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const toggleMute = () => {
|
|
243
|
+
const media = mediaRef.current
|
|
244
|
+
if (!media) return
|
|
245
|
+
|
|
246
|
+
media.muted = !media.muted
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const enterFullscreen = async () => {
|
|
250
|
+
const container = containerRef.current
|
|
251
|
+
if (!container || !document.fullscreenEnabled) return
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await container.requestFullscreen()
|
|
255
|
+
setFullscreen(true)
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error("Failed to enter fullscreen:", error)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const exitFullscreen = async () => {
|
|
262
|
+
if (!document.fullscreenElement) return
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await document.exitFullscreen()
|
|
266
|
+
setFullscreen(false)
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error("Failed to exit fullscreen:", error)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const toggleFullscreen = () => {
|
|
273
|
+
if (document.fullscreenElement) {
|
|
274
|
+
exitFullscreen()
|
|
275
|
+
} else {
|
|
276
|
+
enterFullscreen()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Listen for fullscreen changes
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
const handleFullscreenChange = () => {
|
|
283
|
+
setFullscreen(!!document.fullscreenElement)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange)
|
|
287
|
+
return () => {
|
|
288
|
+
document.removeEventListener("fullscreenchange", handleFullscreenChange)
|
|
289
|
+
}
|
|
290
|
+
}, [setFullscreen])
|
|
291
|
+
|
|
292
|
+
const controls: MediaPlayerControls = {
|
|
293
|
+
play,
|
|
294
|
+
pause,
|
|
295
|
+
toggle,
|
|
296
|
+
seek,
|
|
297
|
+
setVolume: setVolumeControl,
|
|
298
|
+
mute,
|
|
299
|
+
unmute,
|
|
300
|
+
toggleMute,
|
|
301
|
+
enterFullscreen,
|
|
302
|
+
exitFullscreen,
|
|
303
|
+
toggleFullscreen
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
state,
|
|
308
|
+
controls,
|
|
309
|
+
mediaRef,
|
|
310
|
+
containerRef
|
|
311
|
+
}
|
|
312
|
+
}
|