@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
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.
|
|
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
|
-
|
|
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';
|