@djangocfg/ui-nextjs 2.1.82 → 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.
Files changed (33) hide show
  1. package/package.json +4 -4
  2. package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +1146 -0
  3. package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +611 -0
  4. package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +560 -0
  5. package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +769 -0
  6. package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +373 -0
  7. package/src/tools/AudioPlayer/README.md +177 -205
  8. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +9 -4
  9. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +251 -0
  10. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +291 -0
  11. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  12. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +16 -26
  13. package/src/tools/AudioPlayer/components/index.ts +6 -1
  14. package/src/tools/AudioPlayer/context/AudioProvider.tsx +8 -3
  15. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
  16. package/src/tools/AudioPlayer/context/index.ts +14 -2
  17. package/src/tools/AudioPlayer/hooks/index.ts +11 -0
  18. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  19. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
  20. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +6 -3
  21. package/src/tools/AudioPlayer/index.ts +31 -0
  22. package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +8 -0
  23. package/src/tools/index.ts +22 -0
  24. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
  25. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
  26. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
  27. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
  28. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
  29. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
  30. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
  31. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
  32. package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
  33. package/src/tools/AudioPlayer/@refactoring2/PLAN.md +0 -372
@@ -0,0 +1,279 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * HybridWaveform - Real-time frequency visualization for hybrid player.
5
+ *
6
+ * Two modes:
7
+ * - 'frequency': Real-time frequency bars (default, requires playing audio)
8
+ * - 'static': Static progress bar (for when no analyser is available)
9
+ *
10
+ * Features:
11
+ * - Shows buffered regions
12
+ * - Click to seek
13
+ * - Responsive width
14
+ */
15
+
16
+ import { useRef, useEffect, useCallback, memo } from 'react';
17
+ import { useHybridAudioContext } from '../context/HybridAudioProvider';
18
+ import { cn } from '@djangocfg/ui-core/lib';
19
+
20
+ // =============================================================================
21
+ // TYPES
22
+ // =============================================================================
23
+
24
+ export interface HybridWaveformProps {
25
+ /** Visualization mode */
26
+ mode?: 'frequency' | 'static';
27
+ /** Canvas height in pixels */
28
+ height?: number;
29
+ /** Bar width in pixels */
30
+ barWidth?: number;
31
+ /** Gap between bars in pixels */
32
+ barGap?: number;
33
+ /** Bar border radius */
34
+ barRadius?: number;
35
+ /** Color for played portion */
36
+ progressColor?: string;
37
+ /** Color for unplayed portion */
38
+ waveColor?: string;
39
+ /** Color for buffered regions indicator */
40
+ bufferedColor?: string;
41
+ /** Additional CSS class */
42
+ className?: string;
43
+ /** Callback when user seeks */
44
+ onSeek?: (time: number) => void;
45
+ }
46
+
47
+ // =============================================================================
48
+ // COMPONENT
49
+ // =============================================================================
50
+
51
+ export const HybridWaveform = memo(function HybridWaveform({
52
+ mode = 'frequency',
53
+ height = 64,
54
+ barWidth = 3,
55
+ barGap = 2,
56
+ barRadius = 2,
57
+ progressColor = 'hsl(217 91% 60%)',
58
+ waveColor = 'hsl(217 91% 60% / 0.3)',
59
+ bufferedColor = 'hsl(217 91% 60% / 0.15)',
60
+ className,
61
+ onSeek,
62
+ }: HybridWaveformProps) {
63
+ const canvasRef = useRef<HTMLCanvasElement>(null);
64
+ const containerRef = useRef<HTMLDivElement>(null);
65
+ const animationRef = useRef<number | null>(null);
66
+ const { state, controls, webAudio } = useHybridAudioContext();
67
+
68
+ // Handle click to seek
69
+ const handleClick = useCallback(
70
+ (e: React.MouseEvent<HTMLCanvasElement>) => {
71
+ const canvas = canvasRef.current;
72
+ if (!canvas || !state.duration) return;
73
+
74
+ const rect = canvas.getBoundingClientRect();
75
+ const x = e.clientX - rect.left;
76
+ const progress = x / rect.width;
77
+ const time = state.duration * progress;
78
+
79
+ controls.seek(time);
80
+ onSeek?.(time);
81
+ },
82
+ [state.duration, controls, onSeek]
83
+ );
84
+
85
+ // Render frequency bars (real-time visualization)
86
+ const renderFrequency = useCallback(() => {
87
+ const canvas = canvasRef.current;
88
+ const analyser = webAudio.analyser;
89
+ if (!canvas) return;
90
+
91
+ const ctx = canvas.getContext('2d');
92
+ if (!ctx) return;
93
+
94
+ const { width, height: canvasHeight } = canvas;
95
+ const dpr = window.devicePixelRatio || 1;
96
+ const displayWidth = width / dpr;
97
+
98
+ // Get frequency data if analyser is available
99
+ let dataArray: Uint8Array<ArrayBuffer> | null = null;
100
+ if (analyser) {
101
+ dataArray = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
102
+ analyser.getByteFrequencyData(dataArray);
103
+ }
104
+
105
+ ctx.clearRect(0, 0, width, canvasHeight);
106
+
107
+ // Draw buffered regions at bottom
108
+ if (state.buffered && state.duration > 0) {
109
+ ctx.fillStyle = bufferedColor;
110
+ for (let i = 0; i < state.buffered.length; i++) {
111
+ const start = (state.buffered.start(i) / state.duration) * width;
112
+ const end = (state.buffered.end(i) / state.duration) * width;
113
+ ctx.fillRect(start, canvasHeight - 3 * dpr, end - start, 3 * dpr);
114
+ }
115
+ }
116
+
117
+ // Calculate bar count based on available width
118
+ const barCount = Math.floor(displayWidth / (barWidth + barGap));
119
+ const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
120
+ const progressX = width * progress;
121
+
122
+ // Draw bars
123
+ for (let i = 0; i < barCount; i++) {
124
+ let barHeight: number;
125
+
126
+ if (dataArray && state.isPlaying) {
127
+ // Real-time frequency data
128
+ const step = Math.floor(dataArray.length / barCount);
129
+ const value = dataArray[i * step] / 255;
130
+ barHeight = Math.max(4 * dpr, value * (canvasHeight - 6 * dpr) * 0.9);
131
+ } else {
132
+ // Static fallback - small bars
133
+ barHeight = 8 * dpr;
134
+ }
135
+
136
+ const x = i * (barWidth + barGap) * dpr;
137
+ const y = (canvasHeight - barHeight) / 2;
138
+
139
+ ctx.fillStyle = x < progressX ? progressColor : waveColor;
140
+
141
+ // Draw rounded rect
142
+ const radius = barRadius * dpr;
143
+ const rectWidth = barWidth * dpr;
144
+ ctx.beginPath();
145
+ ctx.roundRect(x, y, rectWidth, barHeight, radius);
146
+ ctx.fill();
147
+ }
148
+
149
+ // Continue animation if playing
150
+ if (state.isPlaying) {
151
+ animationRef.current = requestAnimationFrame(renderFrequency);
152
+ }
153
+ }, [
154
+ webAudio.analyser,
155
+ state.buffered,
156
+ state.duration,
157
+ state.currentTime,
158
+ state.isPlaying,
159
+ barWidth,
160
+ barGap,
161
+ barRadius,
162
+ progressColor,
163
+ waveColor,
164
+ bufferedColor,
165
+ ]);
166
+
167
+ // Render static progress bar
168
+ const renderStatic = useCallback(() => {
169
+ const canvas = canvasRef.current;
170
+ if (!canvas) return;
171
+
172
+ const ctx = canvas.getContext('2d');
173
+ if (!ctx) return;
174
+
175
+ const { width, height: canvasHeight } = canvas;
176
+ const dpr = window.devicePixelRatio || 1;
177
+
178
+ ctx.clearRect(0, 0, width, canvasHeight);
179
+
180
+ // Draw buffered regions
181
+ if (state.buffered && state.duration > 0) {
182
+ ctx.fillStyle = bufferedColor;
183
+ for (let i = 0; i < state.buffered.length; i++) {
184
+ const start = (state.buffered.start(i) / state.duration) * width;
185
+ const end = (state.buffered.end(i) / state.duration) * width;
186
+ ctx.fillRect(start, 0, end - start, canvasHeight);
187
+ }
188
+ }
189
+
190
+ // Draw progress bar
191
+ const progress = state.duration > 0 ? state.currentTime / state.duration : 0;
192
+ const progressWidth = width * progress;
193
+
194
+ // Background
195
+ ctx.fillStyle = waveColor;
196
+ ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, width, 4 * dpr);
197
+
198
+ // Progress
199
+ ctx.fillStyle = progressColor;
200
+ ctx.fillRect(0, canvasHeight / 2 - 2 * dpr, progressWidth, 4 * dpr);
201
+
202
+ // Handle
203
+ if (progress > 0) {
204
+ ctx.beginPath();
205
+ ctx.arc(progressWidth, canvasHeight / 2, 6 * dpr, 0, Math.PI * 2);
206
+ ctx.fill();
207
+ }
208
+ }, [state.buffered, state.duration, state.currentTime, progressColor, waveColor, bufferedColor]);
209
+
210
+ // Resize canvas to match container
211
+ useEffect(() => {
212
+ const container = containerRef.current;
213
+ const canvas = canvasRef.current;
214
+ if (!container || !canvas) return;
215
+
216
+ const resizeObserver = new ResizeObserver((entries) => {
217
+ const entry = entries[0];
218
+ if (!entry) return;
219
+
220
+ const dpr = window.devicePixelRatio || 1;
221
+ const displayWidth = entry.contentRect.width;
222
+ const displayHeight = height;
223
+
224
+ canvas.width = displayWidth * dpr;
225
+ canvas.height = displayHeight * dpr;
226
+ canvas.style.width = `${displayWidth}px`;
227
+ canvas.style.height = `${displayHeight}px`;
228
+
229
+ // Re-render after resize
230
+ if (mode === 'frequency') {
231
+ renderFrequency();
232
+ } else {
233
+ renderStatic();
234
+ }
235
+ });
236
+
237
+ resizeObserver.observe(container);
238
+ return () => resizeObserver.disconnect();
239
+ }, [height, mode, renderFrequency, renderStatic]);
240
+
241
+ // Animation loop for frequency mode
242
+ useEffect(() => {
243
+ if (mode === 'frequency') {
244
+ renderFrequency();
245
+ }
246
+
247
+ return () => {
248
+ if (animationRef.current) {
249
+ cancelAnimationFrame(animationRef.current);
250
+ }
251
+ };
252
+ }, [mode, renderFrequency]);
253
+
254
+ // Re-render on time change for static mode
255
+ useEffect(() => {
256
+ if (mode === 'static') {
257
+ renderStatic();
258
+ }
259
+ }, [mode, state.currentTime, renderStatic]);
260
+
261
+ // Re-render frequency when playback state changes
262
+ useEffect(() => {
263
+ if (mode === 'frequency' && !state.isPlaying) {
264
+ // One final render when stopped
265
+ renderFrequency();
266
+ }
267
+ }, [mode, state.isPlaying, renderFrequency]);
268
+
269
+ return (
270
+ <div ref={containerRef} className={cn('w-full', className)}>
271
+ <canvas
272
+ ref={canvasRef}
273
+ onClick={handleClick}
274
+ className="cursor-pointer"
275
+ style={{ width: '100%', height }}
276
+ />
277
+ </div>
278
+ );
279
+ });
@@ -1,36 +1,26 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * SimpleAudioPlayer - Easy-to-use audio player wrapper
4
+ * SimpleAudioPlayer - WaveSurfer-based audio player (Legacy)
5
5
  *
6
- * Combines AudioProvider + AudioPlayer + optional reactive cover
7
- * in a single component with sensible defaults.
6
+ * @deprecated Consider using `HybridSimplePlayer` instead for better streaming
7
+ * support, lower memory usage, and guaranteed no audio crackling.
8
8
  *
9
- * @example
10
- * // Minimal usage
11
- * <SimpleAudioPlayer src="https://example.com/audio.mp3" />
9
+ * Use SimpleAudioPlayer only when you specifically need the static waveform
10
+ * visualization that shows the full audio amplitude shape.
12
11
  *
13
- * @example
14
- * // With cover art
15
- * <SimpleAudioPlayer
16
- * src={audioUrl}
17
- * title="Track Title"
18
- * artist="Artist Name"
19
- * coverArt="/path/to/cover.jpg"
20
- * />
12
+ * Migration:
13
+ * ```tsx
14
+ * // Before
15
+ * <SimpleAudioPlayer src={url} prefetch reactiveCover />
21
16
  *
22
- * @example
23
- * // Full customization
24
- * <SimpleAudioPlayer
25
- * src={audioUrl}
26
- * title="Track Title"
27
- * coverArt={coverUrl}
28
- * showWaveform
29
- * showEqualizer={false}
30
- * reactiveCover
31
- * variant="spotlight"
32
- * autoPlay={false}
33
- * />
17
+ * // After
18
+ * <HybridSimplePlayer src={url} reactiveCover />
19
+ * ```
20
+ *
21
+ * Key differences:
22
+ * - HybridSimplePlayer: Real-time frequency bars, native streaming, no prefetch needed
23
+ * - SimpleAudioPlayer: Static waveform shape, requires prefetch for streaming URLs
34
24
  */
35
25
 
36
26
  import { useRef, type ReactNode } from 'react';
@@ -2,13 +2,18 @@
2
2
  * AudioPlayer components - Public API
3
3
  */
4
4
 
5
- // Main components
5
+ // WaveSurfer-based components (original)
6
6
  export { AudioPlayer } from './AudioPlayer';
7
7
  export { SimpleAudioPlayer, type SimpleAudioPlayerProps } from './SimpleAudioPlayer';
8
8
  export { AudioEqualizer } from './AudioEqualizer';
9
9
  export { AudioShortcutsPopover } from './AudioShortcutsPopover';
10
10
  export { VisualizationToggle, type VisualizationToggleProps } from './VisualizationToggle';
11
11
 
12
+ // Hybrid player components (HTML5 audio + Web Audio for visualization)
13
+ export { HybridAudioPlayer, type HybridAudioPlayerProps } from './HybridAudioPlayer';
14
+ export { HybridSimplePlayer, type HybridSimplePlayerProps } from './HybridSimplePlayer';
15
+ export { HybridWaveform, type HybridWaveformProps } from './HybridWaveform';
16
+
12
17
  // ReactiveCover
13
18
  export {
14
19
  AudioReactiveCover,
@@ -1,10 +1,15 @@
1
1
  'use client';
2
2
 
3
3
  /**
4
- * AudioProvider - Shared audio state and controls
4
+ * AudioProvider - WaveSurfer-based audio context (Legacy)
5
5
  *
6
- * Provides centralized audio state management using WaveSurfer.js
7
- * All child components can access playback state and controls
6
+ * @deprecated Consider using `HybridAudioProvider` instead for better streaming
7
+ * support, lower memory usage, and guaranteed no audio crackling.
8
+ *
9
+ * Provides centralized audio state management using WaveSurfer.js.
10
+ * All child components can access playback state and controls.
11
+ *
12
+ * @see HybridAudioProvider for the recommended alternative
8
13
  */
9
14
 
10
15
  import {
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * HybridAudioProvider - Context provider for hybrid audio player.
5
+ *
6
+ * Provides audio state, controls, and analysis data to child components.
7
+ * Uses native HTML5 audio for playback with Web Audio API for visualization only.
8
+ */
9
+
10
+ import { createContext, useContext, useMemo, type ReactNode } from 'react';
11
+ import {
12
+ useHybridAudio,
13
+ type UseHybridAudioOptions,
14
+ type HybridAudioState,
15
+ type HybridAudioControls,
16
+ type HybridWebAudioAPI,
17
+ } from '../hooks/useHybridAudio';
18
+ import { useHybridAudioAnalysis } from '../hooks/useHybridAudioAnalysis';
19
+ import type { AudioLevels } from '../effects';
20
+
21
+ // =============================================================================
22
+ // TYPES
23
+ // =============================================================================
24
+
25
+ export interface HybridAudioContextValue {
26
+ // Audio state
27
+ state: HybridAudioState;
28
+
29
+ // Controls
30
+ controls: HybridAudioControls;
31
+
32
+ // Web Audio (for visualizations)
33
+ webAudio: HybridWebAudioAPI;
34
+
35
+ // Audio levels (for reactive effects)
36
+ audioLevels: AudioLevels;
37
+
38
+ // Audio element ref (for custom integrations)
39
+ audioRef: React.RefObject<HTMLAudioElement | null>;
40
+ }
41
+
42
+ // =============================================================================
43
+ // CONTEXT
44
+ // =============================================================================
45
+
46
+ const HybridAudioContext = createContext<HybridAudioContextValue | null>(null);
47
+
48
+ // =============================================================================
49
+ // PROVIDER
50
+ // =============================================================================
51
+
52
+ export interface HybridAudioProviderProps extends UseHybridAudioOptions {
53
+ children: ReactNode;
54
+ }
55
+
56
+ export function HybridAudioProvider({ children, ...options }: HybridAudioProviderProps) {
57
+ const { audioRef, state, controls, webAudio } = useHybridAudio(options);
58
+
59
+ // Audio analysis for reactive effects
60
+ const audioLevels = useHybridAudioAnalysis(webAudio.analyser, state.isPlaying);
61
+
62
+ const value = useMemo<HybridAudioContextValue>(
63
+ () => ({
64
+ state,
65
+ controls,
66
+ webAudio,
67
+ audioLevels,
68
+ audioRef,
69
+ }),
70
+ [state, controls, webAudio, audioLevels, audioRef]
71
+ );
72
+
73
+ return <HybridAudioContext.Provider value={value}>{children}</HybridAudioContext.Provider>;
74
+ }
75
+
76
+ // =============================================================================
77
+ // HOOKS
78
+ // =============================================================================
79
+
80
+ /**
81
+ * Access full hybrid audio context
82
+ */
83
+ export function useHybridAudioContext(): HybridAudioContextValue {
84
+ const context = useContext(HybridAudioContext);
85
+ if (!context) {
86
+ throw new Error('useHybridAudioContext must be used within HybridAudioProvider');
87
+ }
88
+ return context;
89
+ }
90
+
91
+ /**
92
+ * Access audio state only (read-only)
93
+ */
94
+ export function useHybridAudioState(): HybridAudioState {
95
+ const { state } = useHybridAudioContext();
96
+ return state;
97
+ }
98
+
99
+ /**
100
+ * Access audio controls only (no state re-renders)
101
+ */
102
+ export function useHybridAudioControls(): HybridAudioControls {
103
+ const { controls } = useHybridAudioContext();
104
+ return controls;
105
+ }
106
+
107
+ /**
108
+ * Access audio levels for reactive effects
109
+ */
110
+ export function useHybridAudioLevels(): AudioLevels {
111
+ const { audioLevels } = useHybridAudioContext();
112
+ return audioLevels;
113
+ }
114
+
115
+ /**
116
+ * Access Web Audio API for custom visualizations
117
+ */
118
+ export function useHybridWebAudio(): HybridWebAudioAPI {
119
+ const { webAudio } = useHybridAudioContext();
120
+ return webAudio;
121
+ }
@@ -4,8 +4,20 @@
4
4
  * Re-exports provider and selector hooks
5
5
  */
6
6
 
7
- // Provider
7
+ // WaveSurfer-based provider (original)
8
8
  export { AudioProvider, AudioPlayerContext } from './AudioProvider';
9
9
 
10
- // Selector hooks
10
+ // Selector hooks for WaveSurfer provider
11
11
  export { useAudio, useAudioControls, useAudioState, useAudioElement } from './selectors';
12
+
13
+ // Hybrid provider (HTML5 audio + Web Audio for visualization)
14
+ export {
15
+ HybridAudioProvider,
16
+ useHybridAudioContext,
17
+ useHybridAudioState,
18
+ useHybridAudioControls,
19
+ useHybridAudioLevels,
20
+ useHybridWebAudio,
21
+ type HybridAudioContextValue,
22
+ type HybridAudioProviderProps,
23
+ } from './HybridAudioProvider';
@@ -8,6 +8,17 @@ export { useAudioAnalysis } from './useAudioAnalysis';
8
8
  export { useAudioSource } from './useAudioSource';
9
9
  export type { UseAudioSourceResult } from './useAudioSource';
10
10
 
11
+ // Hybrid player hooks
12
+ export { useHybridAudio } from './useHybridAudio';
13
+ export type {
14
+ UseHybridAudioOptions,
15
+ HybridAudioState,
16
+ HybridAudioControls,
17
+ HybridWebAudioAPI,
18
+ UseHybridAudioReturn,
19
+ } from './useHybridAudio';
20
+ export { useHybridAudioAnalysis } from './useHybridAudioAnalysis';
21
+
11
22
  // Public hooks
12
23
  export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
13
24
  export type { AudioHotkeyOptions, ShortcutItem, ShortcutGroup } from './useAudioHotkeys';