@djangocfg/ui-nextjs 2.1.82 → 2.1.84
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/README.md +108 -242
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
- package/src/tools/AudioPlayer/components/{SimpleAudioPlayer.tsx → HybridSimplePlayer.tsx} +61 -69
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +5 -5
- package/src/tools/AudioPlayer/components/index.ts +7 -6
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -6
- package/src/tools/AudioPlayer/hooks/index.ts +14 -10
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/{useAudioAnalysis.ts → useHybridAudioAnalysis.ts} +23 -38
- package/src/tools/AudioPlayer/index.ts +37 -70
- package/src/tools/AudioPlayer/types/index.ts +10 -18
- package/src/tools/index.ts +60 -43
- 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
- package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +0 -200
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +0 -231
- package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +0 -99
- package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +0 -64
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +0 -371
- package/src/tools/AudioPlayer/context/selectors.ts +0 -96
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +0 -150
- package/src/tools/AudioPlayer/hooks/useAudioSource.ts +0 -155
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +0 -106
- package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +0 -295
- package/src/tools/AudioPlayer/progressive/WaveformCanvas.tsx +0 -381
- package/src/tools/AudioPlayer/progressive/index.ts +0 -40
- package/src/tools/AudioPlayer/progressive/peaks.ts +0 -234
- package/src/tools/AudioPlayer/progressive/types.ts +0 -179
- package/src/tools/AudioPlayer/progressive/useAudioElement.ts +0 -340
- package/src/tools/AudioPlayer/progressive/useProgressiveWaveform.ts +0 -267
- package/src/tools/AudioPlayer/types/audio.ts +0 -121
- package/src/tools/AudioPlayer/types/components.ts +0 -98
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.84",
|
|
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.84",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.84",
|
|
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.84",
|
|
114
114
|
"@types/node": "^24.7.2",
|
|
115
115
|
"eslint": "^9.37.0",
|
|
116
116
|
"tailwindcss-animate": "1.0.7",
|
|
@@ -1,289 +1,172 @@
|
|
|
1
1
|
# AudioPlayer
|
|
2
2
|
|
|
3
|
-
Audio player with
|
|
3
|
+
Audio player with native HTML5 streaming and audio-reactive visualizations.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- Native HTML5 audio playback (no crackling, native streaming support)
|
|
8
|
+
- Web Audio API for real-time frequency analysis
|
|
8
9
|
- Audio-reactive cover effects (glow, orbs, spotlight, mesh)
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- Volume control with mute
|
|
12
|
-
- Skip forward/backward
|
|
13
|
-
- Playback speed control
|
|
10
|
+
- Frequency visualization waveform
|
|
11
|
+
- Full playback controls
|
|
14
12
|
|
|
15
|
-
## Quick Start
|
|
13
|
+
## Quick Start
|
|
16
14
|
|
|
17
15
|
```tsx
|
|
18
|
-
import {
|
|
16
|
+
import { HybridSimplePlayer } from '@djangocfg/ui-nextjs';
|
|
19
17
|
|
|
20
|
-
//
|
|
21
|
-
<
|
|
18
|
+
// Simple usage
|
|
19
|
+
<HybridSimplePlayer src="https://example.com/audio.mp3" />
|
|
22
20
|
|
|
23
|
-
// With metadata
|
|
24
|
-
<
|
|
21
|
+
// With metadata and reactive cover
|
|
22
|
+
<HybridSimplePlayer
|
|
25
23
|
src={audioUrl}
|
|
26
24
|
title="Track Title"
|
|
27
25
|
artist="Artist Name"
|
|
28
26
|
coverArt="/path/to/cover.jpg"
|
|
27
|
+
reactiveCover
|
|
28
|
+
variant="spotlight"
|
|
29
29
|
/>
|
|
30
30
|
|
|
31
31
|
// Full customization
|
|
32
|
-
<
|
|
32
|
+
<HybridSimplePlayer
|
|
33
33
|
src={audioUrl}
|
|
34
34
|
title="Track Title"
|
|
35
35
|
coverArt={coverUrl}
|
|
36
36
|
showWaveform
|
|
37
|
-
|
|
37
|
+
waveformMode="frequency" // 'frequency' | 'static'
|
|
38
|
+
showLoop
|
|
38
39
|
reactiveCover
|
|
39
|
-
variant="spotlight"
|
|
40
|
-
|
|
40
|
+
variant="spotlight" // 'glow' | 'orbs' | 'spotlight' | 'mesh' | 'none'
|
|
41
|
+
intensity="medium" // 'subtle' | 'medium' | 'strong'
|
|
42
|
+
colorScheme="primary" // 'primary' | 'vibrant' | 'cool' | 'warm'
|
|
43
|
+
layout="horizontal" // 'vertical' | 'horizontal'
|
|
41
44
|
/>
|
|
42
45
|
```
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
## Props
|
|
45
48
|
|
|
46
49
|
| Prop | Type | Default | Description |
|
|
47
50
|
|------|------|---------|-------------|
|
|
48
51
|
| `src` | `string` | required | Audio URL |
|
|
49
|
-
| `prefetch` | `boolean` | `true` | Pre-fetch audio as blob (required for streaming URLs to enable seek) |
|
|
50
52
|
| `title` | `string` | - | Track title |
|
|
51
53
|
| `artist` | `string` | - | Artist name |
|
|
52
54
|
| `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
|
|
53
55
|
| `coverSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Cover art size |
|
|
54
|
-
| `showWaveform` | `boolean` | `true` | Show
|
|
55
|
-
| `
|
|
56
|
+
| `showWaveform` | `boolean` | `true` | Show frequency visualization |
|
|
57
|
+
| `waveformMode` | `'frequency' \| 'static'` | `'frequency'` | Visualization mode |
|
|
58
|
+
| `waveformHeight` | `number` | `64` | Waveform height in pixels |
|
|
56
59
|
| `showTimer` | `boolean` | `true` | Show time display |
|
|
57
60
|
| `showVolume` | `boolean` | `true` | Show volume control |
|
|
58
|
-
| `showLoop` | `boolean` | `true` | Show loop
|
|
61
|
+
| `showLoop` | `boolean` | `true` | Show loop button |
|
|
59
62
|
| `reactiveCover` | `boolean` | `true` | Enable reactive effects |
|
|
60
|
-
| `variant` | `VisualizationVariant` |
|
|
61
|
-
| `intensity` | `EffectIntensity` |
|
|
62
|
-
| `colorScheme` | `EffectColorScheme` |
|
|
63
|
+
| `variant` | `VisualizationVariant` | `'spotlight'` | Effect variant |
|
|
64
|
+
| `intensity` | `EffectIntensity` | `'medium'` | Effect intensity |
|
|
65
|
+
| `colorScheme` | `EffectColorScheme` | `'primary'` | Effect colors |
|
|
63
66
|
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
67
|
+
| `loop` | `boolean` | `false` | Loop playback |
|
|
64
68
|
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
|
|
65
69
|
|
|
66
|
-
> **Note:** The `prefetch` option is enabled by default. This fetches the entire audio file as a blob before loading into WaveSurfer, which is required for seeking to work correctly with streaming URLs. For very large files (> 50MB), consider using `prefetch={false}` and the Progressive player mode instead.
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
70
|
## Advanced Usage
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
### HybridAudioProvider + HybridAudioPlayer
|
|
73
|
+
|
|
74
|
+
For custom layouts:
|
|
73
75
|
|
|
74
76
|
```tsx
|
|
75
|
-
import {
|
|
76
|
-
|
|
77
|
+
import {
|
|
78
|
+
HybridAudioProvider,
|
|
79
|
+
HybridAudioPlayer,
|
|
80
|
+
AudioReactiveCover,
|
|
81
|
+
useHybridAudioContext
|
|
82
|
+
} from '@djangocfg/ui-nextjs';
|
|
83
|
+
|
|
84
|
+
function MyPlayer({ audioUrl }: { audioUrl: string }) {
|
|
85
|
+
return (
|
|
86
|
+
<HybridAudioProvider src={audioUrl}>
|
|
87
|
+
<AudioReactiveCover variant="spotlight" onClick={handleClick}>
|
|
88
|
+
<img src={coverUrl} alt="Cover" />
|
|
89
|
+
</AudioReactiveCover>
|
|
90
|
+
<HybridAudioPlayer showWaveform showControls />
|
|
91
|
+
<CustomControls />
|
|
92
|
+
</HybridAudioProvider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
77
95
|
|
|
78
|
-
function
|
|
79
|
-
const
|
|
96
|
+
function CustomControls() {
|
|
97
|
+
const { state, controls, audioLevels } = useHybridAudioContext();
|
|
80
98
|
|
|
81
99
|
return (
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
showControls
|
|
89
|
-
showWaveform
|
|
90
|
-
showTimer
|
|
91
|
-
showVolume
|
|
92
|
-
/>
|
|
93
|
-
</AudioProvider>
|
|
100
|
+
<div>
|
|
101
|
+
<p>Bass level: {(audioLevels.bass * 100).toFixed(0)}%</p>
|
|
102
|
+
<button onClick={controls.togglePlay}>
|
|
103
|
+
{state.isPlaying ? 'Pause' : 'Play'}
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
94
106
|
);
|
|
95
107
|
}
|
|
96
108
|
```
|
|
97
109
|
|
|
98
|
-
##
|
|
110
|
+
## Hooks
|
|
99
111
|
|
|
100
|
-
###
|
|
112
|
+
### useHybridAudioContext
|
|
101
113
|
|
|
102
|
-
|
|
114
|
+
Full context access:
|
|
103
115
|
|
|
104
116
|
```tsx
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
waveformOptions={{
|
|
113
|
-
waveColor: 'hsl(217 91% 60% / 0.3)',
|
|
114
|
-
progressColor: 'hsl(217 91% 60%)',
|
|
115
|
-
height: 64,
|
|
116
|
-
barWidth: 3,
|
|
117
|
-
barRadius: 3,
|
|
118
|
-
barGap: 2,
|
|
119
|
-
}}
|
|
120
|
-
>
|
|
121
|
-
{children}
|
|
122
|
-
</AudioProvider>
|
|
117
|
+
const {
|
|
118
|
+
state, // { isReady, isPlaying, currentTime, duration, volume, isMuted, isLooping }
|
|
119
|
+
controls, // { play, pause, togglePlay, seek, skip, setVolume, toggleMute, toggleLoop }
|
|
120
|
+
audioLevels, // { bass, mid, high, overall }
|
|
121
|
+
webAudio, // { context, analyser, sourceNode }
|
|
122
|
+
audioRef, // React ref to HTMLAudioElement
|
|
123
|
+
} = useHybridAudioContext();
|
|
123
124
|
```
|
|
124
125
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
| Prop | Type | Default | Description |
|
|
128
|
-
|------|------|---------|-------------|
|
|
129
|
-
| `uri` | `string` | required | Audio URL |
|
|
130
|
-
| `prefetch` | `boolean` | `false` | Pre-fetch as blob (enables seek for streaming URLs) |
|
|
131
|
-
|
|
132
|
-
### AudioPlayer
|
|
126
|
+
### Specialized Hooks
|
|
133
127
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|------|------|---------|-------------|
|
|
138
|
-
| `showControls` | `boolean` | `true` | Show playback controls |
|
|
139
|
-
| `showWaveform` | `boolean` | `true` | Show waveform visualization |
|
|
140
|
-
| `showEqualizer` | `boolean` | `false` | Show equalizer animation |
|
|
141
|
-
| `showTimer` | `boolean` | `true` | Show time display |
|
|
142
|
-
| `showVolume` | `boolean` | `true` | Show volume slider |
|
|
143
|
-
| `className` | `string` | - | Additional CSS class |
|
|
128
|
+
```tsx
|
|
129
|
+
// State only
|
|
130
|
+
const { isPlaying, currentTime, duration } = useHybridAudioState();
|
|
144
131
|
|
|
145
|
-
|
|
132
|
+
// Controls only (no re-render on time updates)
|
|
133
|
+
const { play, pause, togglePlay, seek } = useHybridAudioControls();
|
|
146
134
|
|
|
147
|
-
|
|
135
|
+
// Audio levels for reactive effects
|
|
136
|
+
const { bass, mid, high, overall } = useHybridAudioLevels();
|
|
148
137
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
barCount={24}
|
|
152
|
-
height={48}
|
|
153
|
-
gap={2}
|
|
154
|
-
showPeaks
|
|
155
|
-
barColor="hsl(217 91% 60%)"
|
|
156
|
-
peakColor="hsl(217 91% 70%)"
|
|
157
|
-
/>
|
|
138
|
+
// Web Audio API access
|
|
139
|
+
const { context, analyser, sourceNode } = useHybridWebAudio();
|
|
158
140
|
```
|
|
159
141
|
|
|
142
|
+
## Components
|
|
143
|
+
|
|
160
144
|
### AudioReactiveCover
|
|
161
145
|
|
|
162
|
-
Album art wrapper with audio-reactive effects
|
|
146
|
+
Album art wrapper with audio-reactive effects:
|
|
163
147
|
|
|
164
148
|
```tsx
|
|
165
149
|
<AudioReactiveCover
|
|
166
|
-
variant="spotlight"
|
|
167
|
-
intensity="medium"
|
|
168
|
-
colorScheme="primary"
|
|
150
|
+
variant="spotlight" // 'glow' | 'orbs' | 'spotlight' | 'mesh'
|
|
151
|
+
intensity="medium" // 'subtle' | 'medium' | 'strong'
|
|
152
|
+
colorScheme="primary" // 'primary' | 'vibrant' | 'cool' | 'warm'
|
|
169
153
|
onClick={() => nextVariant()}
|
|
170
154
|
>
|
|
171
155
|
<img src={coverArt} alt="Album cover" />
|
|
172
156
|
</AudioReactiveCover>
|
|
173
157
|
```
|
|
174
158
|
|
|
175
|
-
###
|
|
176
|
-
|
|
177
|
-
Button to cycle through visualization variants.
|
|
178
|
-
|
|
179
|
-
```tsx
|
|
180
|
-
<VisualizationToggle compact />
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Hooks
|
|
184
|
-
|
|
185
|
-
### useAudio
|
|
159
|
+
### HybridWaveform
|
|
186
160
|
|
|
187
|
-
|
|
161
|
+
Real-time frequency visualization:
|
|
188
162
|
|
|
189
163
|
```tsx
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
isMuted,
|
|
197
|
-
audioLevels, // { bass, mid, high, overall }
|
|
198
|
-
play,
|
|
199
|
-
pause,
|
|
200
|
-
togglePlay,
|
|
201
|
-
seek,
|
|
202
|
-
seekTo,
|
|
203
|
-
skip,
|
|
204
|
-
setVolume,
|
|
205
|
-
toggleMute,
|
|
206
|
-
restart,
|
|
207
|
-
} = useAudio();
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### useAudioControls
|
|
211
|
-
|
|
212
|
-
Controls only (no re-render on time updates).
|
|
213
|
-
|
|
214
|
-
```tsx
|
|
215
|
-
const { play, pause, togglePlay, skip, restart } = useAudioControls();
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### useAudioState
|
|
219
|
-
|
|
220
|
-
Read-only playback state.
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
const { isPlaying, currentTime, duration, volume } = useAudioState();
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### useAudioElement
|
|
227
|
-
|
|
228
|
-
Access audio element for custom visualizations.
|
|
229
|
-
|
|
230
|
-
```tsx
|
|
231
|
-
const { audioElement, isPlaying, audioLevels } = useAudioElement();
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### useAudioVisualization
|
|
235
|
-
|
|
236
|
-
Manage visualization settings (persisted in localStorage).
|
|
237
|
-
|
|
238
|
-
```tsx
|
|
239
|
-
const {
|
|
240
|
-
settings, // { enabled, variant, intensity, colorScheme }
|
|
241
|
-
toggle,
|
|
242
|
-
setSetting,
|
|
243
|
-
nextVariant,
|
|
244
|
-
nextIntensity,
|
|
245
|
-
nextColorScheme,
|
|
246
|
-
reset,
|
|
247
|
-
} = useAudioVisualization();
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### useAudioHotkeys
|
|
251
|
-
|
|
252
|
-
Enable keyboard shortcuts.
|
|
253
|
-
|
|
254
|
-
```tsx
|
|
255
|
-
useAudioHotkeys({
|
|
256
|
-
enabled: true,
|
|
257
|
-
skipDuration: 10,
|
|
258
|
-
volumeStep: 0.1,
|
|
259
|
-
});
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
## Keyboard Shortcuts
|
|
263
|
-
|
|
264
|
-
| Key | Action |
|
|
265
|
-
|-----|--------|
|
|
266
|
-
| `Space` | Play/Pause |
|
|
267
|
-
| `←` / `J` | Skip 10s backward |
|
|
268
|
-
| `→` / `L` | Skip 10s forward |
|
|
269
|
-
| `↑` | Volume up |
|
|
270
|
-
| `↓` | Volume down |
|
|
271
|
-
| `M` | Mute/Unmute |
|
|
272
|
-
| `0-9` | Jump to 0-90% |
|
|
273
|
-
|
|
274
|
-
## Waveform Options
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
interface WaveformOptions {
|
|
278
|
-
waveColor?: string; // Unplayed wave color
|
|
279
|
-
progressColor?: string; // Played wave color
|
|
280
|
-
cursorColor?: string; // Playhead cursor color
|
|
281
|
-
cursorWidth?: number; // Cursor width in px
|
|
282
|
-
height?: number; // Waveform height in px
|
|
283
|
-
barWidth?: number; // Bar width in px
|
|
284
|
-
barRadius?: number; // Bar corner radius
|
|
285
|
-
barGap?: number; // Gap between bars
|
|
286
|
-
}
|
|
164
|
+
<HybridWaveform
|
|
165
|
+
mode="frequency" // 'frequency' | 'static'
|
|
166
|
+
height={64}
|
|
167
|
+
barWidth={3}
|
|
168
|
+
barGap={2}
|
|
169
|
+
/>
|
|
287
170
|
```
|
|
288
171
|
|
|
289
172
|
## Effect Variants
|
|
@@ -301,40 +184,23 @@ interface WaveformOptions {
|
|
|
301
184
|
```
|
|
302
185
|
AudioPlayer/
|
|
303
186
|
├── index.ts # Public API exports
|
|
304
|
-
├── types/
|
|
305
|
-
│ ├── index.ts # Type re-exports
|
|
306
|
-
│ ├── audio.ts # Audio state & source types
|
|
307
|
-
│ ├── components.ts # Component prop types
|
|
308
|
-
│ └── effects.ts # Visualization effect types
|
|
187
|
+
├── types/ # TypeScript types
|
|
309
188
|
├── hooks/
|
|
310
|
-
│ ├──
|
|
311
|
-
│ ├──
|
|
312
|
-
│
|
|
313
|
-
│ ├── useVisualization.tsx # Visualization settings
|
|
314
|
-
│ ├── useAudioAnalysis.ts # Web Audio frequency analysis
|
|
315
|
-
│ └── useSharedWebAudio.ts # Shared AudioContext
|
|
316
|
-
├── utils/
|
|
317
|
-
│ ├── index.ts
|
|
318
|
-
│ └── formatTime.ts # Time formatting
|
|
189
|
+
│ ├── useHybridAudio.ts # HTML5 audio + Web Audio hook
|
|
190
|
+
│ ├── useHybridAudioAnalysis.ts # Frequency analysis
|
|
191
|
+
│ └── useVisualization.tsx # Visualization settings
|
|
319
192
|
├── context/
|
|
320
|
-
│
|
|
321
|
-
│ ├── AudioProvider.tsx # Audio state provider
|
|
322
|
-
│ └── selectors.ts # useAudio, useAudioControls hooks
|
|
193
|
+
│ └── HybridAudioProvider.tsx # Audio context provider
|
|
323
194
|
├── components/
|
|
324
|
-
│ ├──
|
|
325
|
-
│ ├──
|
|
326
|
-
│ ├──
|
|
327
|
-
│
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
│ └── ReactiveCover/
|
|
331
|
-
│ ├── index.ts
|
|
332
|
-
│ ├── AudioReactiveCover.tsx # Reactive album art
|
|
333
|
-
│ └── effects/ # Effect components
|
|
334
|
-
│ ├── GlowEffect.tsx
|
|
335
|
-
│ ├── OrbsEffect.tsx
|
|
336
|
-
│ ├── SpotlightEffect.tsx
|
|
337
|
-
│ └── MeshEffect.tsx
|
|
338
|
-
└── effects/
|
|
339
|
-
└── index.ts # Effect calculations
|
|
195
|
+
│ ├── HybridAudioPlayer.tsx # Main player component
|
|
196
|
+
│ ├── HybridSimplePlayer.tsx # All-in-one wrapper
|
|
197
|
+
│ ├── HybridWaveform.tsx # Frequency visualization
|
|
198
|
+
│ └── ReactiveCover/ # Reactive effects
|
|
199
|
+
├── effects/ # Effect calculations
|
|
200
|
+
└── utils/ # Utilities
|
|
340
201
|
```
|
|
202
|
+
|
|
203
|
+
Key design:
|
|
204
|
+
- HTML5 `<audio>` for playback (native streaming, no crackling)
|
|
205
|
+
- Web Audio API AnalyserNode for visualization only (not connected to output)
|
|
206
|
+
- Audio graph: `source → destination` + `source → analyser` (passive)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HybridAudioPlayer - Audio playback controls with frequency visualization
|
|
5
|
+
*
|
|
6
|
+
* Uses HybridAudioContext for state management.
|
|
7
|
+
* Native HTML5 audio for playback, Web Audio API for visualization only.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Real-time frequency visualization
|
|
11
|
+
* - Playback controls (play, pause, skip, restart)
|
|
12
|
+
* - Volume control with mute
|
|
13
|
+
* - Loop toggle
|
|
14
|
+
* - Keyboard shortcuts
|
|
15
|
+
* - Timer display
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { memo } from 'react';
|
|
19
|
+
import {
|
|
20
|
+
Play,
|
|
21
|
+
Pause,
|
|
22
|
+
RotateCcw,
|
|
23
|
+
SkipBack,
|
|
24
|
+
SkipForward,
|
|
25
|
+
Volume2,
|
|
26
|
+
VolumeX,
|
|
27
|
+
Loader2,
|
|
28
|
+
Repeat,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
import { Button, Slider, cn } from '@djangocfg/ui-nextjs';
|
|
31
|
+
import { useHybridAudioContext } from '../context/HybridAudioProvider';
|
|
32
|
+
import { HybridWaveform } from './HybridWaveform';
|
|
33
|
+
import { formatTime } from '../utils';
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// TYPES
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
export interface HybridAudioPlayerProps {
|
|
40
|
+
/** Show playback controls */
|
|
41
|
+
showControls?: boolean;
|
|
42
|
+
/** Show frequency waveform */
|
|
43
|
+
showWaveform?: boolean;
|
|
44
|
+
/** Waveform visualization mode */
|
|
45
|
+
waveformMode?: 'frequency' | 'static';
|
|
46
|
+
/** Waveform height in pixels */
|
|
47
|
+
waveformHeight?: number;
|
|
48
|
+
/** Show time display */
|
|
49
|
+
showTimer?: boolean;
|
|
50
|
+
/** Show volume control */
|
|
51
|
+
showVolume?: boolean;
|
|
52
|
+
/** Show loop button */
|
|
53
|
+
showLoop?: boolean;
|
|
54
|
+
/** Additional CSS class */
|
|
55
|
+
className?: string;
|
|
56
|
+
/** Inline styles */
|
|
57
|
+
style?: React.CSSProperties;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// COMPONENT
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
export const HybridAudioPlayer = memo(function HybridAudioPlayer({
|
|
65
|
+
showControls = true,
|
|
66
|
+
showWaveform = true,
|
|
67
|
+
waveformMode = 'frequency',
|
|
68
|
+
waveformHeight = 64,
|
|
69
|
+
showTimer = true,
|
|
70
|
+
showVolume = true,
|
|
71
|
+
showLoop = true,
|
|
72
|
+
className,
|
|
73
|
+
style,
|
|
74
|
+
}: HybridAudioPlayerProps) {
|
|
75
|
+
const { state, controls } = useHybridAudioContext();
|
|
76
|
+
|
|
77
|
+
const isLoading = !state.isReady;
|
|
78
|
+
|
|
79
|
+
const handleVolumeChange = (value: number[]) => {
|
|
80
|
+
controls.setVolume(value[0] / 100);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={cn('flex flex-col gap-3 p-4 rounded-lg bg-card border', className)}
|
|
86
|
+
style={style}
|
|
87
|
+
>
|
|
88
|
+
{/* Frequency Waveform */}
|
|
89
|
+
{showWaveform && (
|
|
90
|
+
<div className="relative">
|
|
91
|
+
<HybridWaveform
|
|
92
|
+
mode={waveformMode}
|
|
93
|
+
height={waveformHeight}
|
|
94
|
+
className={cn(isLoading && 'opacity-50')}
|
|
95
|
+
/>
|
|
96
|
+
{isLoading && (
|
|
97
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
98
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{/* Timer */}
|
|
105
|
+
{showTimer && (
|
|
106
|
+
<div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
|
|
107
|
+
<span>{formatTime(state.currentTime)}</span>
|
|
108
|
+
<span>{formatTime(state.duration)}</span>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Controls */}
|
|
113
|
+
{showControls && (
|
|
114
|
+
<div className="flex items-center justify-center gap-1">
|
|
115
|
+
{/* Restart */}
|
|
116
|
+
<Button
|
|
117
|
+
variant="ghost"
|
|
118
|
+
size="icon"
|
|
119
|
+
className="h-9 w-9"
|
|
120
|
+
onClick={controls.restart}
|
|
121
|
+
disabled={!state.isReady}
|
|
122
|
+
title="Restart"
|
|
123
|
+
>
|
|
124
|
+
<RotateCcw className="h-4 w-4" />
|
|
125
|
+
</Button>
|
|
126
|
+
|
|
127
|
+
{/* Skip back 5s */}
|
|
128
|
+
<Button
|
|
129
|
+
variant="ghost"
|
|
130
|
+
size="icon"
|
|
131
|
+
className="h-9 w-9"
|
|
132
|
+
onClick={() => controls.skip(-5)}
|
|
133
|
+
disabled={!state.isReady}
|
|
134
|
+
title="Back 5 seconds"
|
|
135
|
+
>
|
|
136
|
+
<SkipBack className="h-4 w-4" />
|
|
137
|
+
</Button>
|
|
138
|
+
|
|
139
|
+
{/* Play/Pause */}
|
|
140
|
+
<Button
|
|
141
|
+
variant="default"
|
|
142
|
+
size="icon"
|
|
143
|
+
className="h-12 w-12 rounded-full"
|
|
144
|
+
onClick={controls.togglePlay}
|
|
145
|
+
disabled={!state.isReady && !isLoading}
|
|
146
|
+
title={state.isPlaying ? 'Pause' : 'Play'}
|
|
147
|
+
>
|
|
148
|
+
{isLoading ? (
|
|
149
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
150
|
+
) : state.isPlaying ? (
|
|
151
|
+
<Pause className="h-5 w-5" />
|
|
152
|
+
) : (
|
|
153
|
+
<Play className="h-5 w-5 ml-0.5" />
|
|
154
|
+
)}
|
|
155
|
+
</Button>
|
|
156
|
+
|
|
157
|
+
{/* Skip forward 5s */}
|
|
158
|
+
<Button
|
|
159
|
+
variant="ghost"
|
|
160
|
+
size="icon"
|
|
161
|
+
className="h-9 w-9"
|
|
162
|
+
onClick={() => controls.skip(5)}
|
|
163
|
+
disabled={!state.isReady}
|
|
164
|
+
title="Forward 5 seconds"
|
|
165
|
+
>
|
|
166
|
+
<SkipForward className="h-4 w-4" />
|
|
167
|
+
</Button>
|
|
168
|
+
|
|
169
|
+
{/* Volume */}
|
|
170
|
+
{showVolume && (
|
|
171
|
+
<>
|
|
172
|
+
<Button
|
|
173
|
+
variant="ghost"
|
|
174
|
+
size="icon"
|
|
175
|
+
className="h-9 w-9"
|
|
176
|
+
onClick={controls.toggleMute}
|
|
177
|
+
title={state.isMuted ? 'Unmute' : 'Mute'}
|
|
178
|
+
>
|
|
179
|
+
{state.isMuted || state.volume === 0 ? (
|
|
180
|
+
<VolumeX className="h-4 w-4" />
|
|
181
|
+
) : (
|
|
182
|
+
<Volume2 className="h-4 w-4" />
|
|
183
|
+
)}
|
|
184
|
+
</Button>
|
|
185
|
+
|
|
186
|
+
<Slider
|
|
187
|
+
value={[state.isMuted ? 0 : state.volume * 100]}
|
|
188
|
+
max={100}
|
|
189
|
+
step={1}
|
|
190
|
+
onValueChange={handleVolumeChange}
|
|
191
|
+
className="w-20"
|
|
192
|
+
aria-label="Volume"
|
|
193
|
+
/>
|
|
194
|
+
</>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Loop/Repeat */}
|
|
198
|
+
{showLoop && (
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="icon"
|
|
202
|
+
className={cn('h-9 w-9', state.isLooping && 'text-primary')}
|
|
203
|
+
onClick={controls.toggleLoop}
|
|
204
|
+
disabled={!state.isReady}
|
|
205
|
+
title={state.isLooping ? 'Disable loop' : 'Enable loop'}
|
|
206
|
+
>
|
|
207
|
+
<Repeat className="h-4 w-4" />
|
|
208
|
+
</Button>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export default HybridAudioPlayer;
|