@djangocfg/ui-nextjs 2.1.70 → 2.1.72
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/@refactoring2/ISSUE_ANALYSIS.md +187 -0
- package/src/tools/AudioPlayer/@refactoring2/PLAN.md +372 -0
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +3 -2
- package/src/tools/AudioPlayer/index.ts +27 -0
- package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +295 -0
- package/src/tools/AudioPlayer/progressive/WaveformCanvas.tsx +381 -0
- package/src/tools/AudioPlayer/progressive/index.ts +40 -0
- package/src/tools/AudioPlayer/progressive/peaks.ts +234 -0
- package/src/tools/AudioPlayer/progressive/types.ts +179 -0
- package/src/tools/AudioPlayer/progressive/useAudioElement.ts +289 -0
- package/src/tools/AudioPlayer/progressive/useProgressiveWaveform.ts +267 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +4 -9
- package/src/tools/index.ts +16 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useProgressiveWaveform - Hook for progressive audio waveform loading
|
|
5
|
+
*
|
|
6
|
+
* Fetches audio in chunks, decodes progressively, and extracts peaks.
|
|
7
|
+
* Provides loading state and partial waveform data as it loads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
11
|
+
import { extractPeaks, mergePeaks } from './peaks';
|
|
12
|
+
import type { LoadedRange, DecoderOptions } from './types';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// CONSTANTS
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CHUNK_SIZE = 512 * 1024; // 512KB
|
|
19
|
+
const DEFAULT_PEAKS_PER_SECOND = 50;
|
|
20
|
+
const MAX_PARALLEL_FETCHES = 2;
|
|
21
|
+
|
|
22
|
+
// =============================================================================
|
|
23
|
+
// TYPES
|
|
24
|
+
// =============================================================================
|
|
25
|
+
|
|
26
|
+
interface UseProgressiveWaveformOptions extends DecoderOptions {
|
|
27
|
+
/** Audio source URL */
|
|
28
|
+
url: string;
|
|
29
|
+
/** Known duration (from metadata) - improves initial render */
|
|
30
|
+
duration?: number;
|
|
31
|
+
/** Enabled */
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface UseProgressiveWaveformReturn {
|
|
36
|
+
/** Accumulated peaks (0-1 normalized) */
|
|
37
|
+
peaks: number[];
|
|
38
|
+
/** Loading progress (0-100) */
|
|
39
|
+
loadedPercent: number;
|
|
40
|
+
/** Is currently loading */
|
|
41
|
+
isLoading: boolean;
|
|
42
|
+
/** Is complete */
|
|
43
|
+
isComplete: boolean;
|
|
44
|
+
/** Error if any */
|
|
45
|
+
error: Error | null;
|
|
46
|
+
/** Loaded ranges (for visual indication) */
|
|
47
|
+
loadedRanges: LoadedRange[];
|
|
48
|
+
/** Detected duration */
|
|
49
|
+
detectedDuration: number;
|
|
50
|
+
/** Retry loading */
|
|
51
|
+
retry: () => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// AUDIO CONTEXT SINGLETON
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
let audioContextInstance: AudioContext | null = null;
|
|
59
|
+
|
|
60
|
+
function getAudioContext(): AudioContext {
|
|
61
|
+
if (!audioContextInstance) {
|
|
62
|
+
audioContextInstance = new (window.AudioContext || (window as any).webkitAudioContext)();
|
|
63
|
+
}
|
|
64
|
+
return audioContextInstance;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// HOOK
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
export function useProgressiveWaveform(
|
|
72
|
+
options: UseProgressiveWaveformOptions
|
|
73
|
+
): UseProgressiveWaveformReturn {
|
|
74
|
+
const {
|
|
75
|
+
url,
|
|
76
|
+
duration: knownDuration,
|
|
77
|
+
enabled = true,
|
|
78
|
+
chunkSize = DEFAULT_CHUNK_SIZE,
|
|
79
|
+
peaksPerSecond = DEFAULT_PEAKS_PER_SECOND,
|
|
80
|
+
parallelFetches = MAX_PARALLEL_FETCHES,
|
|
81
|
+
} = options;
|
|
82
|
+
|
|
83
|
+
// State
|
|
84
|
+
const [peaks, setPeaks] = useState<number[]>([]);
|
|
85
|
+
const [loadedPercent, setLoadedPercent] = useState(0);
|
|
86
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
87
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
88
|
+
const [error, setError] = useState<Error | null>(null);
|
|
89
|
+
const [loadedRanges, setLoadedRanges] = useState<LoadedRange[]>([]);
|
|
90
|
+
const [detectedDuration, setDetectedDuration] = useState(knownDuration || 0);
|
|
91
|
+
|
|
92
|
+
// Refs
|
|
93
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
94
|
+
const accumulatedDataRef = useRef<ArrayBuffer[]>([]);
|
|
95
|
+
const totalSizeRef = useRef(0);
|
|
96
|
+
const loadedSizeRef = useRef(0);
|
|
97
|
+
const retryCountRef = useRef(0);
|
|
98
|
+
|
|
99
|
+
// ==========================================================================
|
|
100
|
+
// DECODE ACCUMULATED DATA
|
|
101
|
+
// ==========================================================================
|
|
102
|
+
|
|
103
|
+
const decodeAccumulated = useCallback(async () => {
|
|
104
|
+
const chunks = accumulatedDataRef.current;
|
|
105
|
+
if (chunks.length === 0) return;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Combine all chunks
|
|
109
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
110
|
+
const combined = new Uint8Array(totalLength);
|
|
111
|
+
let offset = 0;
|
|
112
|
+
for (const chunk of chunks) {
|
|
113
|
+
combined.set(new Uint8Array(chunk), offset);
|
|
114
|
+
offset += chunk.byteLength;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Decode
|
|
118
|
+
const audioContext = getAudioContext();
|
|
119
|
+
const audioBuffer = await audioContext.decodeAudioData(combined.buffer.slice(0));
|
|
120
|
+
|
|
121
|
+
// Extract peaks
|
|
122
|
+
const targetPeaks = Math.ceil(audioBuffer.duration * peaksPerSecond);
|
|
123
|
+
const newPeaks = extractPeaks(audioBuffer, { length: targetPeaks });
|
|
124
|
+
|
|
125
|
+
setPeaks(newPeaks);
|
|
126
|
+
setDetectedDuration(audioBuffer.duration);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
// Decoding failed - might need more data
|
|
129
|
+
// This is expected for partial MP3 data
|
|
130
|
+
console.debug('Decode attempt failed, waiting for more data');
|
|
131
|
+
}
|
|
132
|
+
}, [peaksPerSecond]);
|
|
133
|
+
|
|
134
|
+
// ==========================================================================
|
|
135
|
+
// FETCH LOGIC
|
|
136
|
+
// ==========================================================================
|
|
137
|
+
|
|
138
|
+
const loadWaveform = useCallback(async () => {
|
|
139
|
+
if (!url || !enabled) return;
|
|
140
|
+
|
|
141
|
+
// Abort previous
|
|
142
|
+
abortControllerRef.current?.abort();
|
|
143
|
+
abortControllerRef.current = new AbortController();
|
|
144
|
+
const { signal } = abortControllerRef.current;
|
|
145
|
+
|
|
146
|
+
// Reset state
|
|
147
|
+
setIsLoading(true);
|
|
148
|
+
setIsComplete(false);
|
|
149
|
+
setError(null);
|
|
150
|
+
setPeaks([]);
|
|
151
|
+
setLoadedRanges([]);
|
|
152
|
+
setLoadedPercent(0);
|
|
153
|
+
accumulatedDataRef.current = [];
|
|
154
|
+
loadedSizeRef.current = 0;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Get file size with HEAD request
|
|
158
|
+
const headResponse = await fetch(url, { method: 'HEAD', signal });
|
|
159
|
+
const contentLength = headResponse.headers.get('content-length');
|
|
160
|
+
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
|
|
161
|
+
totalSizeRef.current = totalSize;
|
|
162
|
+
|
|
163
|
+
if (totalSize === 0) {
|
|
164
|
+
// Fallback: fetch entire file
|
|
165
|
+
const response = await fetch(url, { signal });
|
|
166
|
+
const data = await response.arrayBuffer();
|
|
167
|
+
accumulatedDataRef.current = [data];
|
|
168
|
+
await decodeAccumulated();
|
|
169
|
+
setIsComplete(true);
|
|
170
|
+
setLoadedPercent(100);
|
|
171
|
+
setIsLoading(false);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Fetch in chunks
|
|
176
|
+
let offset = 0;
|
|
177
|
+
const ranges: LoadedRange[] = [];
|
|
178
|
+
|
|
179
|
+
while (offset < totalSize) {
|
|
180
|
+
if (signal.aborted) return;
|
|
181
|
+
|
|
182
|
+
const end = Math.min(offset + chunkSize - 1, totalSize - 1);
|
|
183
|
+
|
|
184
|
+
const response = await fetch(url, {
|
|
185
|
+
headers: { Range: `bytes=${offset}-${end}` },
|
|
186
|
+
signal,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!response.ok && response.status !== 206) {
|
|
190
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const chunk = await response.arrayBuffer();
|
|
194
|
+
accumulatedDataRef.current.push(chunk);
|
|
195
|
+
loadedSizeRef.current += chunk.byteLength;
|
|
196
|
+
|
|
197
|
+
// Update progress
|
|
198
|
+
const progress = (loadedSizeRef.current / totalSize) * 100;
|
|
199
|
+
setLoadedPercent(progress);
|
|
200
|
+
|
|
201
|
+
// Update ranges
|
|
202
|
+
const rangeStart = offset / totalSize;
|
|
203
|
+
const rangeEnd = (offset + chunk.byteLength) / totalSize;
|
|
204
|
+
ranges.push({ start: rangeStart, end: rangeEnd });
|
|
205
|
+
setLoadedRanges([...ranges]);
|
|
206
|
+
|
|
207
|
+
// Try to decode periodically
|
|
208
|
+
if (accumulatedDataRef.current.length % 2 === 0 || offset + chunkSize >= totalSize) {
|
|
209
|
+
await decodeAccumulated();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
offset += chunkSize;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Final decode
|
|
216
|
+
await decodeAccumulated();
|
|
217
|
+
setIsComplete(true);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if ((err as Error).name === 'AbortError') return;
|
|
220
|
+
|
|
221
|
+
const error = err instanceof Error ? err : new Error('Loading failed');
|
|
222
|
+
setError(error);
|
|
223
|
+
console.error('Waveform loading error:', error);
|
|
224
|
+
} finally {
|
|
225
|
+
setIsLoading(false);
|
|
226
|
+
}
|
|
227
|
+
}, [url, enabled, chunkSize, decodeAccumulated]);
|
|
228
|
+
|
|
229
|
+
// ==========================================================================
|
|
230
|
+
// RETRY
|
|
231
|
+
// ==========================================================================
|
|
232
|
+
|
|
233
|
+
const retry = useCallback(() => {
|
|
234
|
+
retryCountRef.current += 1;
|
|
235
|
+
loadWaveform();
|
|
236
|
+
}, [loadWaveform]);
|
|
237
|
+
|
|
238
|
+
// ==========================================================================
|
|
239
|
+
// EFFECTS
|
|
240
|
+
// ==========================================================================
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
loadWaveform();
|
|
244
|
+
|
|
245
|
+
return () => {
|
|
246
|
+
abortControllerRef.current?.abort();
|
|
247
|
+
};
|
|
248
|
+
}, [loadWaveform]);
|
|
249
|
+
|
|
250
|
+
// Update duration from prop
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (knownDuration && knownDuration > 0 && detectedDuration === 0) {
|
|
253
|
+
setDetectedDuration(knownDuration);
|
|
254
|
+
}
|
|
255
|
+
}, [knownDuration, detectedDuration]);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
peaks,
|
|
259
|
+
loadedPercent,
|
|
260
|
+
isLoading,
|
|
261
|
+
isComplete,
|
|
262
|
+
error,
|
|
263
|
+
loadedRanges,
|
|
264
|
+
detectedDuration,
|
|
265
|
+
retry,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -17,19 +17,14 @@ import { MediaPlayer, MediaProvider, Poster } from '@vidstack/react';
|
|
|
17
17
|
import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
|
|
18
18
|
import { useVideoCache } from '../../../stores/mediaCache';
|
|
19
19
|
|
|
20
|
-
import type { MediaPlayerInstance } from '@vidstack/react';
|
|
20
|
+
import type { MediaPlayerInstance, PlayerSrc } from '@vidstack/react';
|
|
21
21
|
import type { VidstackProviderProps, VideoPlayerRef, ErrorFallbackProps } from '../types';
|
|
22
22
|
|
|
23
|
-
/**
|
|
24
|
-
* Vidstack source type (string or object with type hint)
|
|
25
|
-
*/
|
|
26
|
-
type VidstackSrc = string | { src: string; type: string };
|
|
27
|
-
|
|
28
23
|
/**
|
|
29
24
|
* Convert source to Vidstack-compatible format
|
|
30
25
|
* Returns object with explicit type for HLS/DASH to ensure proper loader selection
|
|
31
26
|
*/
|
|
32
|
-
function getVidstackSrc(source: VidstackProviderProps['source']):
|
|
27
|
+
function getVidstackSrc(source: VidstackProviderProps['source']): PlayerSrc {
|
|
33
28
|
switch (source.type) {
|
|
34
29
|
case 'youtube':
|
|
35
30
|
return `youtube/${source.id}`;
|
|
@@ -37,9 +32,9 @@ function getVidstackSrc(source: VidstackProviderProps['source']): VidstackSrc {
|
|
|
37
32
|
return `vimeo/${source.id}`;
|
|
38
33
|
case 'hls':
|
|
39
34
|
// Explicit type needed because URL may have query params that hide .m3u8 extension
|
|
40
|
-
return { src: source.url, type: 'application/x-mpegurl' };
|
|
35
|
+
return { src: source.url, type: 'application/x-mpegurl' } as PlayerSrc;
|
|
41
36
|
case 'dash':
|
|
42
|
-
return { src: source.url, type: 'application/dash+xml' };
|
|
37
|
+
return { src: source.url, type: 'application/dash+xml' } as PlayerSrc;
|
|
43
38
|
case 'url':
|
|
44
39
|
return source.url;
|
|
45
40
|
default:
|
package/src/tools/index.ts
CHANGED
|
@@ -101,6 +101,15 @@ export {
|
|
|
101
101
|
VARIANT_INFO,
|
|
102
102
|
INTENSITY_INFO,
|
|
103
103
|
COLOR_SCHEME_INFO,
|
|
104
|
+
// Progressive Audio Player (streaming-friendly)
|
|
105
|
+
ProgressiveAudioPlayer,
|
|
106
|
+
WaveformCanvas,
|
|
107
|
+
useProgressiveAudioElement,
|
|
108
|
+
useProgressiveWaveform,
|
|
109
|
+
extractPeaks,
|
|
110
|
+
mergePeaks,
|
|
111
|
+
resamplePeaks,
|
|
112
|
+
smoothPeaks,
|
|
104
113
|
} from './AudioPlayer';
|
|
105
114
|
export type {
|
|
106
115
|
SimpleAudioPlayerProps,
|
|
@@ -120,6 +129,13 @@ export type {
|
|
|
120
129
|
AudioHotkeyOptions,
|
|
121
130
|
ShortcutItem,
|
|
122
131
|
ShortcutGroup,
|
|
132
|
+
// Progressive types
|
|
133
|
+
ProgressiveAudioPlayerProps,
|
|
134
|
+
WaveformStyle,
|
|
135
|
+
WaveformData,
|
|
136
|
+
LoadedRange,
|
|
137
|
+
ProgressiveAudioState,
|
|
138
|
+
ProgressiveAudioControls,
|
|
123
139
|
} from './AudioPlayer';
|
|
124
140
|
|
|
125
141
|
// Export ImageViewer
|