@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.70",
3
+ "version": "2.1.72",
4
4
  "description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -58,8 +58,8 @@
58
58
  "check": "tsc --noEmit"
59
59
  },
60
60
  "peerDependencies": {
61
- "@djangocfg/api": "^2.1.70",
62
- "@djangocfg/ui-core": "^2.1.70",
61
+ "@djangocfg/api": "^2.1.72",
62
+ "@djangocfg/ui-core": "^2.1.72",
63
63
  "@types/react": "^19.1.0",
64
64
  "@types/react-dom": "^19.1.0",
65
65
  "consola": "^3.4.2",
@@ -110,7 +110,7 @@
110
110
  "wavesurfer.js": "^7.12.1"
111
111
  },
112
112
  "devDependencies": {
113
- "@djangocfg/typescript-config": "^2.1.70",
113
+ "@djangocfg/typescript-config": "^2.1.72",
114
114
  "@types/node": "^24.7.2",
115
115
  "eslint": "^9.37.0",
116
116
  "tailwindcss-animate": "1.0.7",
@@ -0,0 +1,187 @@
1
+ # Audio Player Seek Issue Analysis
2
+
3
+ **Date:** 2025-12-27
4
+ **Issue:** При seek на позицию > 2 минут аудио перестаёт воспроизводиться
5
+
6
+ ## Симптомы
7
+
8
+ 1. Трек начинает проигрываться нормально
9
+ 2. Seek на позицию < 2 минут работает
10
+ 3. Seek на позицию > 2 минут - воспроизведение останавливается
11
+ 4. После seek > 2 минут, возврат на < 2 минут тоже не работает
12
+
13
+ ## Корневая причина
14
+
15
+ ### Проблема в архитектуре streaming + WaveSurfer
16
+
17
+ **WaveSurfer.js** по умолчанию загружает **весь аудиофайл** в память перед воспроизведением. Но в текущей реализации используется **HTTP Range streaming**, который возвращает данные **по частям**.
18
+
19
+ #### Ключевые константы (Django backend):
20
+
21
+ ```python
22
+ # viewsets.py
23
+ DEFAULT_CHUNK_SIZE = 2 * 1024 * 1024 # 2 MB
24
+ MAX_CHUNK_SIZE = 10 * 1024 * 1024 # 10 MB
25
+ ```
26
+
27
+ #### Что происходит:
28
+
29
+ 1. **Загрузка аудио:**
30
+ - WaveSurfer запрашивает URL: `/api/terminal/media-stream/{session_id}/stream/?path=...`
31
+ - Backend возвращает **только первые 2 MB** (DEFAULT_CHUNK_SIZE)
32
+ - WaveSurfer декодирует эти 2 MB и считает файл "готовым"
33
+
34
+ 2. **При seek на < 2 минут:**
35
+ - Данные уже в буфере (первые 2 MB)
36
+ - Seek работает
37
+
38
+ 3. **При seek на > 2 минут:**
39
+ - Данные НЕ загружены (за пределами первых 2 MB)
40
+ - WaveSurfer пытается seek в "пустоту"
41
+ - HTML5 Audio element не может воспроизвести незагруженные данные
42
+ - Плеер "зависает"
43
+
44
+ 4. **После неудачного seek:**
45
+ - Состояние плеера повреждено
46
+ - Даже возврат на загруженную часть не работает
47
+
48
+ ### Почему именно 2 минуты?
49
+
50
+ MP3 128kbps ≈ 16 KB/сек
51
+ 2 MB / 16 KB = **125 секунд ≈ 2 минуты**
52
+
53
+ Для других битрейтов:
54
+ - 192kbps: ~87 секунд
55
+ - 256kbps: ~65 секунд
56
+ - 320kbps: ~52 секунды
57
+
58
+ ## Решения
59
+
60
+ ### Вариант 1: Полная загрузка файла (Рекомендуется для небольших файлов)
61
+
62
+ **Изменить AudioViewer.tsx:**
63
+
64
+ ```typescript
65
+ // Вместо streaming для аудио < 50MB, загружать полностью
66
+ const shouldFullLoad = file.size < 50 * 1024 * 1024 && isAudio;
67
+
68
+ if (shouldFullLoad) {
69
+ // Загрузить через RPC или fetch весь файл
70
+ const response = await fetch(streamUrl);
71
+ const blob = await response.blob();
72
+ const blobUrl = URL.createObjectURL(blob);
73
+ setSrc(blobUrl);
74
+ } else {
75
+ // Streaming для больших файлов
76
+ setSrc(streamUrl);
77
+ }
78
+ ```
79
+
80
+ ### Вариант 2: Использовать Media Source Extensions (MSE)
81
+
82
+ WaveSurfer поддерживает MSE для progressive loading:
83
+
84
+ ```typescript
85
+ // AudioProvider.tsx
86
+ const options = useMemo(() => ({
87
+ container: containerRef,
88
+ url: source.uri,
89
+ // Включить backend режим для streaming
90
+ backend: 'MediaElement',
91
+ mediaControls: false,
92
+ // ...
93
+ }), [...]);
94
+ ```
95
+
96
+ **Требует изменений в backend** для правильной обработки Range requests.
97
+
98
+ ### Вариант 3: Исправить backend Range handling
99
+
100
+ **Django viewsets.py** - убрать ограничение chunk size для аудио:
101
+
102
+ ```python
103
+ def stream(self, request, session_id: str):
104
+ # Для аудио - возвращать весь файл или больший chunk
105
+ if mime_type.startswith('audio/') and file_size < 100 * 1024 * 1024:
106
+ # Возвращать весь файл для аудио < 100MB
107
+ chunk_size = file_size
108
+ else:
109
+ chunk_size = min(requested_length or DEFAULT_CHUNK_SIZE, MAX_CHUNK_SIZE)
110
+ ```
111
+
112
+ ### Вариант 4: Использовать HTML5 Audio напрямую (без WaveSurfer waveform)
113
+
114
+ Для streaming источников отключить waveform rendering:
115
+
116
+ ```typescript
117
+ <SimpleAudioPlayer
118
+ src={streamUrl}
119
+ showWaveform={false} // Отключить waveform для streaming
120
+ // ...
121
+ />
122
+ ```
123
+
124
+ HTML5 `<audio>` элемент сам обрабатывает Range requests корректно.
125
+
126
+ ## Быстрое временное решение
127
+
128
+ В `AudioViewer.tsx` изменить условие для использования blob вместо streaming для аудио:
129
+
130
+ ```typescript
131
+ // Текущий код:
132
+ const shouldStream = (file.loadMethod === 'http_stream' || file.loadMethod === 'http_transcode')
133
+ && !hasContent && sessionId;
134
+
135
+ // Изменить на:
136
+ const isAudio = file.mimeType?.startsWith('audio/');
137
+ const shouldStream = !isAudio && (file.loadMethod === 'http_stream' || file.loadMethod === 'http_transcode')
138
+ && !hasContent && sessionId;
139
+
140
+ // Для аудио - загружать через fetch
141
+ if (isAudio && sessionId && !hasContent) {
142
+ const fetchAudio = async () => {
143
+ const url = getStreamUrlWithTranscode(sessionId, file.path, needsTranscode);
144
+ const response = await fetch(url);
145
+ const blob = await response.blob();
146
+ setSrc(URL.createObjectURL(blob));
147
+ };
148
+ fetchAudio();
149
+ }
150
+ ```
151
+
152
+ ## Дополнительные находки
153
+
154
+ ### Баг в useAudioHotkeys.ts (исправлен)
155
+
156
+ В `/projects/solution/frontend/packages/ui-nextjs/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts` был баг:
157
+
158
+ ```typescript
159
+ // Было (НЕПРАВИЛЬНО):
160
+ skip(duration * percent - duration * (volume || 0));
161
+
162
+ // Стало (ПРАВИЛЬНО):
163
+ const targetTime = duration * percent;
164
+ skip(targetTime - currentTime);
165
+ ```
166
+
167
+ Использовался `volume` (громкость) вместо `currentTime` для расчёта seek позиции.
168
+
169
+ ## Файлы для изменения
170
+
171
+ 1. **Frontend:**
172
+ - `cmdop/projects/solution/frontend/apps/web/app/_layouts/FileWorkspace/viewers/media/AudioViewer.tsx`
173
+
174
+ 2. **Backend (опционально):**
175
+ - `cmdop/projects/solution/django/apps/terminal/views/api/media_stream/viewsets.py`
176
+
177
+ 3. **UI Library (уже исправлено):**
178
+ - `djangocfg/projects/solution/frontend/packages/ui-nextjs/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts`
179
+
180
+ ## Архитектурная рекомендация
181
+
182
+ Для аудиофайлов рекомендуется:
183
+ - **< 50 MB**: Полная загрузка в blob (лучший UX, работает seek)
184
+ - **50-200 MB**: HTML5 Audio без waveform (streaming, но без визуализации)
185
+ - **> 200 MB**: Показать предупреждение, предложить скачать
186
+
187
+ WaveSurfer waveform visualization требует полной загрузки файла для корректной работы.
@@ -0,0 +1,372 @@
1
+ # Progressive Audio Player - План рефакторинга
2
+
3
+ ## Проблема
4
+
5
+ WaveSurfer.js требует полной загрузки аудиофайла для рендеринга waveform. При HTTP Range streaming backend возвращает только 2MB chunks, что приводит к:
6
+ - Seek на позицию > 2 минут не работает
7
+ - После неудачного seek плеер "зависает"
8
+
9
+ ## Решение
10
+
11
+ Создать собственный `ProgressiveAudioPlayer` который:
12
+ 1. Использует HTML5 `<audio>` для воспроизведения (нативные Range requests)
13
+ 2. Рисует waveform прогрессивно по мере загрузки
14
+ 3. Визуально показывает загруженную/незагруженную область
15
+ 4. Полный контроль над seek поведением
16
+
17
+ ---
18
+
19
+ ## Архитектура
20
+
21
+ ```
22
+ ┌──────────────────────────────────────────────────────────────┐
23
+ │ ProgressiveAudioPlayer │
24
+ ├──────────────────────────────────────────────────────────────┤
25
+ │ │
26
+ │ ┌────────────────────────────────────────────────────────┐ │
27
+ │ │ WaveformCanvas │ │
28
+ │ │ ┌──────────────────┬─────────────────────────────────┐│ │
29
+ │ │ │ ████████████████ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ││ │
30
+ │ │ │ (загружено) │ (placeholder/loading) ││ │
31
+ │ │ └──────────────────┴─────────────────────────────────┘│ │
32
+ │ │ ▲ cursor │ │
33
+ │ └────────────────────────────────────────────────────────┘ │
34
+ │ │
35
+ │ ┌────────────────────────────────────────────────────────┐ │
36
+ │ │ <audio> (hidden) - нативный HTML5 player │ │
37
+ │ │ - src={streamingUrl} │ │
38
+ │ │ - браузер сам делает Range requests │ │
39
+ │ └────────────────────────────────────────────────────────┘ │
40
+ │ │
41
+ │ ┌────────────────────────────────────────────────────────┐ │
42
+ │ │ Controls │ │
43
+ │ │ [⏮] [▶/⏸] [⏭] [🔊━━━━] [🔁] 00:00 / 03:45 │ │
44
+ │ └────────────────────────────────────────────────────────┘ │
45
+ │ │
46
+ └──────────────────────────────────────────────────────────────┘
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Компоненты
52
+
53
+ ### 1. `useProgressiveWaveform` - хук для загрузки и декодирования
54
+
55
+ ```typescript
56
+ interface UseProgressiveWaveformOptions {
57
+ url: string;
58
+ chunkSize?: number; // размер chunk для загрузки (default: 512KB)
59
+ samplesPerChunk?: number; // сколько peaks на chunk (default: 100)
60
+ }
61
+
62
+ interface UseProgressiveWaveformReturn {
63
+ peaks: number[]; // массив peaks [0-1]
64
+ loadedPercent: number; // сколько загружено (0-100)
65
+ isLoading: boolean;
66
+ error: Error | null;
67
+ loadedRanges: [number, number][]; // загруженные диапазоны
68
+ }
69
+ ```
70
+
71
+ **Алгоритм:**
72
+ 1. Получить Content-Length через HEAD запрос
73
+ 2. Разбить на chunks
74
+ 3. Загружать chunks последовательно или параллельно
75
+ 4. Декодировать через AudioContext.decodeAudioData()
76
+ 5. Извлечь peaks из decoded buffer
77
+ 6. Обновлять состояние по мере загрузки
78
+
79
+ ### 2. `WaveformCanvas` - canvas компонент
80
+
81
+ ```typescript
82
+ interface WaveformCanvasProps {
83
+ peaks: number[];
84
+ loadedPercent: number;
85
+ currentTime: number;
86
+ duration: number;
87
+ buffered: TimeRanges; // из audio.buffered
88
+ onSeek: (time: number) => void;
89
+
90
+ // Стилизация
91
+ waveColor?: string;
92
+ progressColor?: string;
93
+ loadingColor?: string; // цвет незагруженной области
94
+ cursorColor?: string;
95
+ barWidth?: number;
96
+ barGap?: number;
97
+ height?: number;
98
+ }
99
+ ```
100
+
101
+ **Отрисовка:**
102
+ 1. Фон - незагруженная область (полупрозрачная или placeholder pattern)
103
+ 2. Загруженные peaks - основной цвет
104
+ 3. Проигранная часть - progressColor
105
+ 4. Курсор текущей позиции
106
+ 5. Buffered ranges индикатор (тонкая полоса снизу)
107
+
108
+ ### 3. `useAudioElement` - хук для управления audio
109
+
110
+ ```typescript
111
+ interface UseAudioElementOptions {
112
+ src: string;
113
+ autoPlay?: boolean;
114
+ onTimeUpdate?: (time: number) => void;
115
+ onDurationChange?: (duration: number) => void;
116
+ onBufferUpdate?: (buffered: TimeRanges) => void;
117
+ onEnded?: () => void;
118
+ onError?: (error: Error) => void;
119
+ }
120
+
121
+ interface UseAudioElementReturn {
122
+ audioRef: RefObject<HTMLAudioElement>;
123
+ isPlaying: boolean;
124
+ currentTime: number;
125
+ duration: number;
126
+ volume: number;
127
+ isMuted: boolean;
128
+ buffered: TimeRanges;
129
+
130
+ // Actions
131
+ play: () => Promise<void>;
132
+ pause: () => void;
133
+ seek: (time: number) => void;
134
+ setVolume: (vol: number) => void;
135
+ toggleMute: () => void;
136
+ }
137
+ ```
138
+
139
+ ### 4. `ProgressiveAudioPlayer` - главный компонент
140
+
141
+ ```typescript
142
+ interface ProgressiveAudioPlayerProps {
143
+ src: string;
144
+ title?: string;
145
+ artist?: string;
146
+ coverArt?: string;
147
+
148
+ // Features
149
+ showWaveform?: boolean;
150
+ showControls?: boolean;
151
+ showTimer?: boolean;
152
+ showVolume?: boolean;
153
+ showLoop?: boolean;
154
+
155
+ // Callbacks
156
+ onPlay?: () => void;
157
+ onPause?: () => void;
158
+ onEnded?: () => void;
159
+ onError?: (error: Error) => void;
160
+
161
+ // Styling
162
+ className?: string;
163
+ waveformOptions?: WaveformOptions;
164
+ }
165
+ ```
166
+
167
+ ### 5. `ProgressiveAudioContext` - контекст (опционально)
168
+
169
+ Для совместимости с существующими хуками:
170
+ - `useAudioControls()`
171
+ - `useAudioState()`
172
+ - `useAudioElement()`
173
+
174
+ ---
175
+
176
+ ## Файловая структура
177
+
178
+ ```
179
+ AudioPlayer/
180
+ ├── @refactoring2/
181
+ │ └── PLAN.md (этот файл)
182
+
183
+ ├── components/
184
+ │ ├── ProgressiveAudioPlayer.tsx # NEW - главный компонент
185
+ │ ├── WaveformCanvas.tsx # NEW - canvas waveform
186
+ │ ├── AudioPlayer.tsx # существующий (WaveSurfer)
187
+ │ └── SimpleAudioPlayer.tsx # существующий
188
+
189
+ ├── hooks/
190
+ │ ├── useProgressiveWaveform.ts # NEW - загрузка + декодирование
191
+ │ ├── useAudioElement.ts # NEW - управление audio
192
+ │ ├── useWaveformRenderer.ts # NEW - рендеринг на canvas
193
+ │ └── useAudioHotkeys.ts # существующий (переиспользуем)
194
+
195
+ ├── context/
196
+ │ ├── ProgressiveAudioProvider.tsx # NEW - контекст
197
+ │ └── AudioProvider.tsx # существующий (WaveSurfer)
198
+
199
+ ├── utils/
200
+ │ ├── peaks.ts # NEW - извлечение peaks
201
+ │ ├── audioDecoder.ts # NEW - декодирование chunks
202
+ │ └── formatTime.ts # существующий
203
+
204
+ └── types/
205
+ └── progressive.ts # NEW - типы
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Этапы реализации
211
+
212
+ ### Этап 1: Базовые хуки (core) ✅ DONE
213
+
214
+ **Файлы:**
215
+ - `progressive/useAudioElement.ts` ✅
216
+ - `progressive/peaks.ts` ✅
217
+ - `progressive/types.ts` ✅
218
+
219
+ **Задачи:**
220
+ 1. [x] Создать `useAudioElement` - обёртка над HTMLAudioElement
221
+ 2. [x] Создать `extractPeaks(audioBuffer)` - извлечение peaks из AudioBuffer
222
+ 3. [x] Создать утилиты для peaks (merge, resample, smooth)
223
+ 4. [x] Типы для всех новых компонентов
224
+
225
+ ### Этап 2: Progressive загрузка ✅ DONE
226
+
227
+ **Файлы:**
228
+ - `progressive/useProgressiveWaveform.ts` ✅
229
+
230
+ **Задачи:**
231
+ 1. [x] HEAD запрос для Content-Length
232
+ 2. [x] Chunked fetch с Range headers
233
+ 3. [x] Декодирование каждого chunk
234
+ 4. [x] Накопление peaks в состоянии
235
+ 5. [x] Обработка ошибок и retry
236
+
237
+ ### Этап 3: Canvas рендеринг ✅ DONE
238
+
239
+ **Файлы:**
240
+ - `progressive/WaveformCanvas.tsx` ✅
241
+
242
+ **Задачи:**
243
+ 1. [x] Canvas setup с правильным DPI
244
+ 2. [x] Рендеринг peaks (bars)
245
+ 3. [x] Progress overlay (проигранная часть)
246
+ 4. [x] Loading indicator для незагруженной части
247
+ 5. [x] Cursor (текущая позиция)
248
+ 6. [x] Buffered ranges indicator
249
+ 7. [x] Click/drag для seek
250
+ 8. [x] Hover preview
251
+
252
+ ### Этап 4: Главный компонент ✅ DONE
253
+
254
+ **Файлы:**
255
+ - `progressive/ProgressiveAudioPlayer.tsx` ✅
256
+ - `progressive/index.ts` ✅
257
+
258
+ **Задачи:**
259
+ 1. [x] Собрать всё вместе
260
+ 2. [x] Controls UI (play/pause, volume, etc.)
261
+ 3. [x] Timer display
262
+ 4. [x] Export из index.ts
263
+ 5. [ ] Keyboard shortcuts (переиспользовать useAudioHotkeys) - TODO
264
+
265
+ ### Этап 5: Интеграция и тесты 🔄 IN PROGRESS
266
+
267
+ **Задачи:**
268
+ 1. [x] Export из AudioPlayer/index.ts
269
+ 2. [ ] Обновить AudioViewer в cmdop - использовать ProgressiveAudioPlayer
270
+ 3. [ ] Playground example
271
+ 4. [ ] Тестирование с разными форматами/размерами
272
+ 5. [ ] Добавить Context для совместимости с существующими хуками
273
+
274
+ ---
275
+
276
+ ## Технические детали
277
+
278
+ ### Декодирование chunks
279
+
280
+ ```typescript
281
+ async function decodeChunk(
282
+ chunk: ArrayBuffer,
283
+ audioContext: AudioContext
284
+ ): Promise<Float32Array> {
285
+ // Для MP3/AAC нужен полный frame
286
+ // Возможно потребуется накопление данных
287
+ const audioBuffer = await audioContext.decodeAudioData(chunk);
288
+ return audioBuffer.getChannelData(0); // mono
289
+ }
290
+ ```
291
+
292
+ **Проблема:** `decodeAudioData` требует валидный аудио-контейнер. Для streaming нужно:
293
+ - Либо накапливать достаточно данных для декодирования
294
+ - Либо использовать Web Codecs API (AudioDecoder)
295
+ - Либо генерировать peaks на backend
296
+
297
+ ### Альтернатива: Backend peaks
298
+
299
+ Если декодирование на frontend сложное, можно:
300
+ 1. Go генерирует peaks через FFmpeg при первом запросе
301
+ 2. Кэширует в файл рядом с аудио
302
+ 3. Frontend загружает peaks JSON отдельно
303
+
304
+ ```bash
305
+ # FFmpeg генерация peaks
306
+ ffmpeg -i audio.mp3 -af "asetnsamples=n=1024,astats=metadata=1:reset=1" -f null - 2>&1 | grep lavfi.astats
307
+ ```
308
+
309
+ Или использовать `audiowaveform`:
310
+ ```bash
311
+ audiowaveform -i audio.mp3 -o peaks.json --pixels-per-second 10
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Миграция
317
+
318
+ ### Для cmdop AudioViewer
319
+
320
+ ```typescript
321
+ // Было:
322
+ <SimpleAudioPlayer src={streamUrl} showWaveform />
323
+
324
+ // Стало:
325
+ <ProgressiveAudioPlayer src={streamUrl} showWaveform />
326
+ ```
327
+
328
+ ### Backwards compatibility
329
+
330
+ - Существующие компоненты остаются для blob/local файлов
331
+ - `ProgressiveAudioPlayer` для streaming URLs
332
+ - `SimpleAudioPlayer` получает prop `progressive?: boolean`
333
+
334
+ ---
335
+
336
+ ## Оценка сложности
337
+
338
+ | Компонент | Сложность | Строки кода |
339
+ |-----------|-----------|-------------|
340
+ | useAudioElement | Низкая | ~80 |
341
+ | peaks utils | Низкая | ~50 |
342
+ | useProgressiveWaveform | Высокая | ~150 |
343
+ | WaveformCanvas | Средняя | ~200 |
344
+ | ProgressiveAudioPlayer | Средняя | ~150 |
345
+ | Context | Низкая | ~100 |
346
+ | **Итого** | | **~730** |
347
+
348
+ **Сравнение:** WaveSurfer.js = ~15,000 строк
349
+
350
+ ---
351
+
352
+ ## Риски и альтернативы
353
+
354
+ ### Риск 1: decodeAudioData требует полный файл
355
+
356
+ **Решение:** Использовать Web Codecs API или генерировать peaks на backend
357
+
358
+ ### Риск 2: Производительность canvas на длинных треках
359
+
360
+ **Решение:** Виртуализация - рендерить только видимую область
361
+
362
+ ### Риск 3: Совместимость браузеров
363
+
364
+ **Решение:** AudioContext поддерживается везде, Web Codecs - fallback на backend peaks
365
+
366
+ ---
367
+
368
+ ## Следующий шаг
369
+
370
+ Начать с **Этапа 1** - базовые хуки и утилиты.
371
+
372
+ Или сначала проверить **backend peaks** подход - если Go может генерировать peaks, это упростит frontend.
@@ -30,7 +30,7 @@ export function useAudioHotkeys(options: AudioHotkeyOptions = {}) {
30
30
  const { enabled = true, skipDuration = 10, volumeStep = 0.1 } = options;
31
31
 
32
32
  const { togglePlay, skip, setVolume, toggleMute, toggleLoop, isReady } = useAudioControls();
33
- const { volume, duration } = useAudioState();
33
+ const { volume, duration, currentTime } = useAudioState();
34
34
  const device = useDeviceDetect();
35
35
 
36
36
  // Play/Pause - Space
@@ -97,7 +97,8 @@ export function useAudioHotkeys(options: AudioHotkeyOptions = {}) {
97
97
  (e) => {
98
98
  if (!duration) return;
99
99
  const percent = parseInt(e.key, 10) / 10;
100
- skip(duration * percent - duration * (volume || 0));
100
+ const targetTime = duration * percent;
101
+ skip(targetTime - currentTime);
101
102
  },
102
103
  { enabled: enabled && isReady, description: 'Seek to percentage' }
103
104
  );
@@ -137,3 +137,30 @@ export type {
137
137
  // =============================================================================
138
138
 
139
139
  export { formatTime } from './utils';
140
+
141
+ // =============================================================================
142
+ // PROGRESSIVE AUDIO PLAYER (streaming-friendly)
143
+ // =============================================================================
144
+
145
+ export {
146
+ // Components
147
+ ProgressiveAudioPlayer,
148
+ WaveformCanvas,
149
+ // Hooks
150
+ useAudioElement as useProgressiveAudioElement,
151
+ useProgressiveWaveform,
152
+ // Utilities
153
+ extractPeaks,
154
+ mergePeaks,
155
+ resamplePeaks,
156
+ smoothPeaks,
157
+ } from './progressive';
158
+
159
+ export type {
160
+ ProgressiveAudioPlayerProps,
161
+ WaveformStyle,
162
+ WaveformData,
163
+ LoadedRange,
164
+ AudioState as ProgressiveAudioState,
165
+ AudioControls as ProgressiveAudioControls,
166
+ } from './progressive';