@djangocfg/ui-nextjs 2.1.81 → 2.1.83
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/package.json +4 -4
- package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +1146 -0
- package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +611 -0
- package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +560 -0
- package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +769 -0
- package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +373 -0
- package/src/tools/AudioPlayer/README.md +177 -205
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +9 -4
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +251 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +291 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +16 -26
- package/src/tools/AudioPlayer/components/index.ts +6 -1
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +16 -8
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
- package/src/tools/AudioPlayer/context/index.ts +14 -2
- package/src/tools/AudioPlayer/hooks/index.ts +11 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +6 -3
- package/src/tools/AudioPlayer/index.ts +31 -0
- package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +8 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +33 -9
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +13 -6
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +38 -22
- package/src/tools/index.ts +22 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
- package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
- package/src/tools/AudioPlayer/@refactoring2/PLAN.md +0 -372
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
# Phase 3: Hooks Extraction
|
|
2
|
-
|
|
3
|
-
## Source: `context.tsx` (lines 25-229)
|
|
4
|
-
|
|
5
|
-
Extract two internal hooks from context.tsx.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## `hooks/useSharedWebAudio.ts`
|
|
10
|
-
|
|
11
|
-
Web Audio API context management. Prevents "InvalidStateError" from multiple MediaElementSourceNodes.
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
'use client';
|
|
15
|
-
|
|
16
|
-
import { useRef, useEffect, useCallback } from 'react';
|
|
17
|
-
import type { SharedWebAudioContext } from '../types';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Manages a shared Web Audio context and source node.
|
|
21
|
-
* All analyzers share the same source to prevent InvalidStateError.
|
|
22
|
-
*/
|
|
23
|
-
export function useSharedWebAudio(audioElement: HTMLMediaElement | null): SharedWebAudioContext {
|
|
24
|
-
const audioContextRef = useRef<AudioContext | null>(null);
|
|
25
|
-
const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);
|
|
26
|
-
const connectedElementRef = useRef<HTMLMediaElement | null>(null);
|
|
27
|
-
const analyserNodesRef = useRef<Set<AnalyserNode>>(new Set());
|
|
28
|
-
|
|
29
|
-
// Initialize Web Audio on first play
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!audioElement) return;
|
|
32
|
-
|
|
33
|
-
if (connectedElementRef.current === audioElement && audioContextRef.current) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const initAudio = () => {
|
|
38
|
-
try {
|
|
39
|
-
if (!audioContextRef.current) {
|
|
40
|
-
const AudioContextClass = window.AudioContext ||
|
|
41
|
-
(window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
|
|
42
|
-
audioContextRef.current = new AudioContextClass();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const audioContext = audioContextRef.current;
|
|
46
|
-
|
|
47
|
-
if (connectedElementRef.current !== audioElement) {
|
|
48
|
-
if (sourceRef.current) {
|
|
49
|
-
try { sourceRef.current.disconnect(); } catch { /* ignore */ }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
sourceRef.current = audioContext.createMediaElementSource(audioElement);
|
|
53
|
-
sourceRef.current.connect(audioContext.destination);
|
|
54
|
-
connectedElementRef.current = audioElement;
|
|
55
|
-
}
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.warn('[SharedWebAudio] Could not initialize:', error);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const handlePlay = () => {
|
|
62
|
-
initAudio();
|
|
63
|
-
if (audioContextRef.current?.state === 'suspended') {
|
|
64
|
-
audioContextRef.current.resume();
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
audioElement.addEventListener('play', handlePlay);
|
|
69
|
-
if (!audioElement.paused) {
|
|
70
|
-
handlePlay();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return () => {
|
|
74
|
-
audioElement.removeEventListener('play', handlePlay);
|
|
75
|
-
};
|
|
76
|
-
}, [audioElement]);
|
|
77
|
-
|
|
78
|
-
const createAnalyser = useCallback((options?: { fftSize?: number; smoothing?: number }): AnalyserNode | null => {
|
|
79
|
-
if (!audioContextRef.current || !sourceRef.current) return null;
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const analyser = audioContextRef.current.createAnalyser();
|
|
83
|
-
analyser.fftSize = options?.fftSize ?? 256;
|
|
84
|
-
analyser.smoothingTimeConstant = options?.smoothing ?? 0.85;
|
|
85
|
-
|
|
86
|
-
sourceRef.current.connect(analyser);
|
|
87
|
-
analyser.connect(audioContextRef.current.destination);
|
|
88
|
-
|
|
89
|
-
analyserNodesRef.current.add(analyser);
|
|
90
|
-
return analyser;
|
|
91
|
-
} catch (error) {
|
|
92
|
-
console.warn('[SharedWebAudio] Could not create analyser:', error);
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
}, []);
|
|
96
|
-
|
|
97
|
-
const disconnectAnalyser = useCallback((analyser: AnalyserNode) => {
|
|
98
|
-
try {
|
|
99
|
-
analyser.disconnect();
|
|
100
|
-
analyserNodesRef.current.delete(analyser);
|
|
101
|
-
} catch { /* ignore */ }
|
|
102
|
-
}, []);
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
audioContext: audioContextRef.current,
|
|
106
|
-
sourceNode: sourceRef.current,
|
|
107
|
-
createAnalyser,
|
|
108
|
-
disconnectAnalyser,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## `hooks/useAudioAnalysis.ts`
|
|
116
|
-
|
|
117
|
-
Real-time audio frequency analysis for reactive effects.
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
'use client';
|
|
121
|
-
|
|
122
|
-
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
123
|
-
import type { SharedWebAudioContext, AudioLevels } from '../types';
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Analyzes audio frequencies for bass, mid, high, and overall levels.
|
|
127
|
-
* Uses shared Web Audio context to prevent duplicate source nodes.
|
|
128
|
-
*/
|
|
129
|
-
export function useAudioAnalysis(
|
|
130
|
-
sharedAudio: SharedWebAudioContext,
|
|
131
|
-
isPlaying: boolean
|
|
132
|
-
): AudioLevels {
|
|
133
|
-
const [levels, setLevels] = useState<AudioLevels>({ bass: 0, mid: 0, high: 0, overall: 0 });
|
|
134
|
-
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
135
|
-
const animationRef = useRef<number | null>(null);
|
|
136
|
-
const dataArrayRef = useRef<Uint8Array | null>(null);
|
|
137
|
-
|
|
138
|
-
const cleanup = useCallback(() => {
|
|
139
|
-
if (animationRef.current) {
|
|
140
|
-
cancelAnimationFrame(animationRef.current);
|
|
141
|
-
animationRef.current = null;
|
|
142
|
-
}
|
|
143
|
-
}, []);
|
|
144
|
-
|
|
145
|
-
// Create analyser when shared audio is ready
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
if (!sharedAudio.sourceNode || analyserRef.current) return;
|
|
148
|
-
|
|
149
|
-
const analyser = sharedAudio.createAnalyser({ fftSize: 256, smoothing: 0.85 });
|
|
150
|
-
if (analyser) {
|
|
151
|
-
analyserRef.current = analyser;
|
|
152
|
-
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return () => {
|
|
156
|
-
if (analyserRef.current) {
|
|
157
|
-
sharedAudio.disconnectAnalyser(analyserRef.current);
|
|
158
|
-
analyserRef.current = null;
|
|
159
|
-
dataArrayRef.current = null;
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}, [sharedAudio.sourceNode, sharedAudio.createAnalyser, sharedAudio.disconnectAnalyser]);
|
|
163
|
-
|
|
164
|
-
// Animation loop
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
if (!isPlaying || !analyserRef.current || !dataArrayRef.current) {
|
|
167
|
-
cleanup();
|
|
168
|
-
// Smooth fade out
|
|
169
|
-
setLevels(prev => ({
|
|
170
|
-
bass: prev.bass * 0.95 < 0.01 ? 0 : prev.bass * 0.95,
|
|
171
|
-
mid: prev.mid * 0.95,
|
|
172
|
-
high: prev.high * 0.95,
|
|
173
|
-
overall: prev.overall * 0.95,
|
|
174
|
-
}));
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const analyser = analyserRef.current;
|
|
179
|
-
const dataArray = dataArrayRef.current;
|
|
180
|
-
|
|
181
|
-
const animate = () => {
|
|
182
|
-
analyser.getByteFrequencyData(dataArray);
|
|
183
|
-
const binCount = dataArray.length;
|
|
184
|
-
|
|
185
|
-
// Bass (0-15%)
|
|
186
|
-
const bassEnd = Math.floor(binCount * 0.15);
|
|
187
|
-
let bassSum = 0;
|
|
188
|
-
for (let i = 0; i < bassEnd; i++) bassSum += dataArray[i];
|
|
189
|
-
const bass = bassSum / bassEnd / 255;
|
|
190
|
-
|
|
191
|
-
// Mids (15-50%)
|
|
192
|
-
const midStart = bassEnd;
|
|
193
|
-
const midEnd = Math.floor(binCount * 0.5);
|
|
194
|
-
let midSum = 0;
|
|
195
|
-
for (let i = midStart; i < midEnd; i++) midSum += dataArray[i];
|
|
196
|
-
const mid = midSum / (midEnd - midStart) / 255;
|
|
197
|
-
|
|
198
|
-
// Highs (50-100%)
|
|
199
|
-
const highStart = midEnd;
|
|
200
|
-
let highSum = 0;
|
|
201
|
-
for (let i = highStart; i < binCount; i++) highSum += dataArray[i];
|
|
202
|
-
const high = highSum / (binCount - highStart) / 255;
|
|
203
|
-
|
|
204
|
-
// Overall
|
|
205
|
-
let totalSum = 0;
|
|
206
|
-
for (let i = 0; i < binCount; i++) totalSum += dataArray[i];
|
|
207
|
-
const overall = totalSum / binCount / 255;
|
|
208
|
-
|
|
209
|
-
// Smooth with lerp
|
|
210
|
-
setLevels(prev => ({
|
|
211
|
-
bass: prev.bass * 0.7 + bass * 0.3,
|
|
212
|
-
mid: prev.mid * 0.7 + mid * 0.3,
|
|
213
|
-
high: prev.high * 0.7 + high * 0.3,
|
|
214
|
-
overall: prev.overall * 0.7 + overall * 0.3,
|
|
215
|
-
}));
|
|
216
|
-
|
|
217
|
-
animationRef.current = requestAnimationFrame(animate);
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
animationRef.current = requestAnimationFrame(animate);
|
|
221
|
-
return cleanup;
|
|
222
|
-
}, [isPlaying, cleanup]);
|
|
223
|
-
|
|
224
|
-
return levels;
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
## `hooks/useAudioHotkeys.ts`
|
|
231
|
-
|
|
232
|
-
Move existing file, update imports.
|
|
233
|
-
|
|
234
|
-
```typescript
|
|
235
|
-
// Same content, just update imports:
|
|
236
|
-
// - import type { ... } from '../types';
|
|
237
|
-
// - import { useAudioControls, useAudioState } from '../context';
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
---
|
|
241
|
-
|
|
242
|
-
## `hooks/useVisualization.tsx`
|
|
243
|
-
|
|
244
|
-
Rename from `useAudioVisualization.tsx`, update imports.
|
|
245
|
-
|
|
246
|
-
```typescript
|
|
247
|
-
// Same content, just update imports
|
|
248
|
-
// Export both old and new names for backward compatibility
|
|
249
|
-
export { useVisualization as useAudioVisualization };
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
## `hooks/index.ts`
|
|
255
|
-
|
|
256
|
-
```typescript
|
|
257
|
-
// Internal hooks (used by provider)
|
|
258
|
-
export { useSharedWebAudio } from './useSharedWebAudio';
|
|
259
|
-
export { useAudioAnalysis } from './useAudioAnalysis';
|
|
260
|
-
|
|
261
|
-
// Public hooks
|
|
262
|
-
export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
|
|
263
|
-
export type { AudioHotkeyOptions, ShortcutItem, ShortcutGroup } from './useAudioHotkeys';
|
|
264
|
-
|
|
265
|
-
export {
|
|
266
|
-
useVisualization,
|
|
267
|
-
useAudioVisualization, // backward compat alias
|
|
268
|
-
VisualizationProvider,
|
|
269
|
-
VARIANT_INFO,
|
|
270
|
-
INTENSITY_INFO,
|
|
271
|
-
COLOR_SCHEME_INFO,
|
|
272
|
-
} from './useVisualization';
|
|
273
|
-
export type {
|
|
274
|
-
VisualizationSettings,
|
|
275
|
-
VisualizationVariant,
|
|
276
|
-
VisualizationIntensity,
|
|
277
|
-
VisualizationColorScheme,
|
|
278
|
-
UseAudioVisualizationReturn,
|
|
279
|
-
VisualizationProviderProps,
|
|
280
|
-
} from './useVisualization';
|
|
281
|
-
```
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
# Phase 4: Context Refactoring
|
|
2
|
-
|
|
3
|
-
## Source: `context.tsx` (lines 231-574)
|
|
4
|
-
|
|
5
|
-
Split provider from selector hooks.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## `context/AudioProvider.tsx`
|
|
10
|
-
|
|
11
|
-
Provider component only (~150 lines).
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
'use client';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
createContext,
|
|
18
|
-
useRef,
|
|
19
|
-
useMemo,
|
|
20
|
-
useCallback,
|
|
21
|
-
useState,
|
|
22
|
-
useEffect,
|
|
23
|
-
type ReactNode,
|
|
24
|
-
} from 'react';
|
|
25
|
-
import { useWavesurfer } from '@wavesurfer/react';
|
|
26
|
-
import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
|
|
27
|
-
import { useSharedWebAudio, useAudioAnalysis } from '../hooks';
|
|
28
|
-
import { useAudioCache } from '../../../stores/mediaCache';
|
|
29
|
-
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// CONTEXT
|
|
32
|
-
// =============================================================================
|
|
33
|
-
|
|
34
|
-
export const AudioPlayerContext = createContext<AudioContextState | null>(null);
|
|
35
|
-
|
|
36
|
-
// =============================================================================
|
|
37
|
-
// PROVIDER PROPS
|
|
38
|
-
// =============================================================================
|
|
39
|
-
|
|
40
|
-
interface AudioProviderProps {
|
|
41
|
-
source: AudioSource;
|
|
42
|
-
autoPlay?: boolean;
|
|
43
|
-
waveformOptions?: WaveformOptions;
|
|
44
|
-
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
45
|
-
children: ReactNode;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// =============================================================================
|
|
49
|
-
// PROVIDER
|
|
50
|
-
// =============================================================================
|
|
51
|
-
|
|
52
|
-
export function AudioProvider({
|
|
53
|
-
source,
|
|
54
|
-
autoPlay = false,
|
|
55
|
-
waveformOptions = {},
|
|
56
|
-
containerRef,
|
|
57
|
-
children,
|
|
58
|
-
}: AudioProviderProps) {
|
|
59
|
-
// Cache for playback position persistence
|
|
60
|
-
const { saveAudioPosition, getAudioPosition } = useAudioCache();
|
|
61
|
-
const lastSavedTimeRef = useRef<number>(0);
|
|
62
|
-
|
|
63
|
-
// Memoize WaveSurfer options
|
|
64
|
-
const options = useMemo(
|
|
65
|
-
() => ({
|
|
66
|
-
container: containerRef,
|
|
67
|
-
url: source.uri,
|
|
68
|
-
waveColor: waveformOptions.waveColor || 'hsl(217 91% 60% / 0.3)',
|
|
69
|
-
progressColor: waveformOptions.progressColor || 'hsl(217 91% 60%)',
|
|
70
|
-
cursorColor: waveformOptions.cursorColor || 'hsl(217 91% 60%)',
|
|
71
|
-
cursorWidth: waveformOptions.cursorWidth ?? 2,
|
|
72
|
-
height: waveformOptions.height ?? 64,
|
|
73
|
-
barWidth: waveformOptions.barWidth ?? 3,
|
|
74
|
-
barRadius: waveformOptions.barRadius ?? 3,
|
|
75
|
-
barGap: waveformOptions.barGap ?? 2,
|
|
76
|
-
normalize: true,
|
|
77
|
-
interact: true,
|
|
78
|
-
hideScrollbar: true,
|
|
79
|
-
autoplay: autoPlay,
|
|
80
|
-
}),
|
|
81
|
-
[source.uri, autoPlay, waveformOptions, containerRef]
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const { wavesurfer, isReady, isPlaying, currentTime } = useWavesurfer(options);
|
|
85
|
-
|
|
86
|
-
// Restore cached position
|
|
87
|
-
useEffect(() => {
|
|
88
|
-
if (isReady && wavesurfer && source.uri) {
|
|
89
|
-
const cachedPosition = getAudioPosition(source.uri);
|
|
90
|
-
if (cachedPosition && cachedPosition > 0) {
|
|
91
|
-
const duration = wavesurfer.getDuration();
|
|
92
|
-
if (cachedPosition < duration - 1) {
|
|
93
|
-
wavesurfer.setTime(cachedPosition);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}, [isReady, wavesurfer, source.uri, getAudioPosition]);
|
|
98
|
-
|
|
99
|
-
// Save position periodically
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
if (!source.uri) return;
|
|
102
|
-
|
|
103
|
-
if (isPlaying && currentTime > 0) {
|
|
104
|
-
const timeSinceLastSave = currentTime - lastSavedTimeRef.current;
|
|
105
|
-
if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
|
|
106
|
-
saveAudioPosition(source.uri, currentTime);
|
|
107
|
-
lastSavedTimeRef.current = currentTime;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!isPlaying && currentTime > 0) {
|
|
112
|
-
saveAudioPosition(source.uri, currentTime);
|
|
113
|
-
lastSavedTimeRef.current = currentTime;
|
|
114
|
-
}
|
|
115
|
-
}, [isPlaying, currentTime, source.uri, saveAudioPosition]);
|
|
116
|
-
|
|
117
|
-
// Derived state
|
|
118
|
-
const duration = wavesurfer?.getDuration() ?? 0;
|
|
119
|
-
const volume = wavesurfer?.getVolume() ?? 1;
|
|
120
|
-
const isMuted = wavesurfer?.getMuted() ?? false;
|
|
121
|
-
|
|
122
|
-
// Loop state
|
|
123
|
-
const [isLooping, setIsLooping] = useState(false);
|
|
124
|
-
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
if (!wavesurfer) return;
|
|
127
|
-
|
|
128
|
-
const handleFinish = () => {
|
|
129
|
-
if (isLooping) {
|
|
130
|
-
wavesurfer.seekTo(0);
|
|
131
|
-
wavesurfer.play();
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
wavesurfer.on('finish', handleFinish);
|
|
136
|
-
return () => {
|
|
137
|
-
wavesurfer.un('finish', handleFinish);
|
|
138
|
-
};
|
|
139
|
-
}, [wavesurfer, isLooping]);
|
|
140
|
-
|
|
141
|
-
// Audio element
|
|
142
|
-
const audioElement = useMemo(() => wavesurfer?.getMediaElement() ?? null, [wavesurfer]);
|
|
143
|
-
|
|
144
|
-
// Shared Web Audio context
|
|
145
|
-
const sharedAudio = useSharedWebAudio(audioElement);
|
|
146
|
-
|
|
147
|
-
// Audio analysis
|
|
148
|
-
const audioLevels = useAudioAnalysis(sharedAudio, isPlaying);
|
|
149
|
-
|
|
150
|
-
// Actions
|
|
151
|
-
const play = useCallback(async () => { await wavesurfer?.play(); }, [wavesurfer]);
|
|
152
|
-
const pause = useCallback(() => { wavesurfer?.pause(); }, [wavesurfer]);
|
|
153
|
-
const togglePlay = useCallback(() => { wavesurfer?.playPause(); }, [wavesurfer]);
|
|
154
|
-
|
|
155
|
-
const seek = useCallback((time: number) => {
|
|
156
|
-
if (wavesurfer) {
|
|
157
|
-
wavesurfer.setTime(Math.max(0, Math.min(time, duration)));
|
|
158
|
-
}
|
|
159
|
-
}, [wavesurfer, duration]);
|
|
160
|
-
|
|
161
|
-
const seekTo = useCallback((progress: number) => {
|
|
162
|
-
if (wavesurfer) {
|
|
163
|
-
wavesurfer.seekTo(Math.max(0, Math.min(progress, 1)));
|
|
164
|
-
}
|
|
165
|
-
}, [wavesurfer]);
|
|
166
|
-
|
|
167
|
-
const skip = useCallback((seconds: number) => { wavesurfer?.skip(seconds); }, [wavesurfer]);
|
|
168
|
-
|
|
169
|
-
const setVolume = useCallback((vol: number) => {
|
|
170
|
-
if (wavesurfer) {
|
|
171
|
-
wavesurfer.setVolume(Math.max(0, Math.min(vol, 1)));
|
|
172
|
-
}
|
|
173
|
-
}, [wavesurfer]);
|
|
174
|
-
|
|
175
|
-
const toggleMute = useCallback(() => {
|
|
176
|
-
if (wavesurfer) {
|
|
177
|
-
wavesurfer.setMuted(!wavesurfer.getMuted());
|
|
178
|
-
}
|
|
179
|
-
}, [wavesurfer]);
|
|
180
|
-
|
|
181
|
-
const restart = useCallback(() => {
|
|
182
|
-
if (wavesurfer) {
|
|
183
|
-
wavesurfer.seekTo(0);
|
|
184
|
-
wavesurfer.play();
|
|
185
|
-
}
|
|
186
|
-
}, [wavesurfer]);
|
|
187
|
-
|
|
188
|
-
const toggleLoop = useCallback(() => { setIsLooping(prev => !prev); }, []);
|
|
189
|
-
const setLoop = useCallback((enabled: boolean) => { setIsLooping(enabled); }, []);
|
|
190
|
-
|
|
191
|
-
// Context value
|
|
192
|
-
const contextValue = useMemo<AudioContextState>(
|
|
193
|
-
() => ({
|
|
194
|
-
wavesurfer,
|
|
195
|
-
audioElement,
|
|
196
|
-
sharedAudio,
|
|
197
|
-
isReady,
|
|
198
|
-
isPlaying,
|
|
199
|
-
currentTime,
|
|
200
|
-
duration,
|
|
201
|
-
volume,
|
|
202
|
-
isMuted,
|
|
203
|
-
isLooping,
|
|
204
|
-
audioLevels,
|
|
205
|
-
play,
|
|
206
|
-
pause,
|
|
207
|
-
togglePlay,
|
|
208
|
-
seek,
|
|
209
|
-
seekTo,
|
|
210
|
-
skip,
|
|
211
|
-
setVolume,
|
|
212
|
-
toggleMute,
|
|
213
|
-
toggleLoop,
|
|
214
|
-
setLoop,
|
|
215
|
-
restart,
|
|
216
|
-
}),
|
|
217
|
-
[
|
|
218
|
-
wavesurfer, audioElement, sharedAudio,
|
|
219
|
-
isReady, isPlaying, currentTime, duration, volume, isMuted, isLooping,
|
|
220
|
-
audioLevels,
|
|
221
|
-
play, pause, togglePlay, seek, seekTo, skip, setVolume, toggleMute, toggleLoop, setLoop, restart,
|
|
222
|
-
]
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<AudioPlayerContext.Provider value={contextValue}>
|
|
227
|
-
{children}
|
|
228
|
-
</AudioPlayerContext.Provider>
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
## `context/selectors.ts`
|
|
236
|
-
|
|
237
|
-
Selector hooks for performance optimization.
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
'use client';
|
|
241
|
-
|
|
242
|
-
import { useContext } from 'react';
|
|
243
|
-
import { AudioPlayerContext } from './AudioProvider';
|
|
244
|
-
import type { AudioContextState } from '../types';
|
|
245
|
-
|
|
246
|
-
// =============================================================================
|
|
247
|
-
// MAIN HOOK
|
|
248
|
-
// =============================================================================
|
|
249
|
-
|
|
250
|
-
export function useAudio(): AudioContextState {
|
|
251
|
-
const context = useContext(AudioPlayerContext);
|
|
252
|
-
if (!context) {
|
|
253
|
-
throw new Error('useAudio must be used within an AudioProvider');
|
|
254
|
-
}
|
|
255
|
-
return context;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// =============================================================================
|
|
259
|
-
// SELECTIVE HOOKS - for performance optimization
|
|
260
|
-
// =============================================================================
|
|
261
|
-
|
|
262
|
-
/** Hook for playback controls only (no re-render on time updates) */
|
|
263
|
-
export function useAudioControls() {
|
|
264
|
-
const {
|
|
265
|
-
isReady,
|
|
266
|
-
play,
|
|
267
|
-
pause,
|
|
268
|
-
togglePlay,
|
|
269
|
-
skip,
|
|
270
|
-
restart,
|
|
271
|
-
setVolume,
|
|
272
|
-
toggleMute,
|
|
273
|
-
toggleLoop,
|
|
274
|
-
setLoop,
|
|
275
|
-
} = useAudio();
|
|
276
|
-
|
|
277
|
-
return {
|
|
278
|
-
isReady,
|
|
279
|
-
play,
|
|
280
|
-
pause,
|
|
281
|
-
togglePlay,
|
|
282
|
-
skip,
|
|
283
|
-
restart,
|
|
284
|
-
setVolume,
|
|
285
|
-
toggleMute,
|
|
286
|
-
toggleLoop,
|
|
287
|
-
setLoop,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Hook for playback state (read-only) */
|
|
292
|
-
export function useAudioState() {
|
|
293
|
-
const {
|
|
294
|
-
isReady,
|
|
295
|
-
isPlaying,
|
|
296
|
-
currentTime,
|
|
297
|
-
duration,
|
|
298
|
-
volume,
|
|
299
|
-
isMuted,
|
|
300
|
-
isLooping,
|
|
301
|
-
} = useAudio();
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
isReady,
|
|
305
|
-
isPlaying,
|
|
306
|
-
currentTime,
|
|
307
|
-
duration,
|
|
308
|
-
volume,
|
|
309
|
-
isMuted,
|
|
310
|
-
isLooping,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Hook for audio element access (for equalizer and reactive effects) */
|
|
315
|
-
export function useAudioElement() {
|
|
316
|
-
const { audioElement, sharedAudio, isPlaying, audioLevels } = useAudio();
|
|
317
|
-
return { audioElement, sharedAudio, isPlaying, audioLevels };
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
---
|
|
322
|
-
|
|
323
|
-
## `context/index.ts`
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
export { AudioPlayerContext, AudioProvider } from './AudioProvider';
|
|
327
|
-
export { useAudio, useAudioControls, useAudioState, useAudioElement } from './selectors';
|
|
328
|
-
```
|