wavesurf 1.0.0 → 1.2.0
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/README.md +424 -186
- package/dist/index.d.mts +31 -2
- package/dist/index.d.ts +31 -2
- package/dist/index.js +255 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +255 -24
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
# wavesurf
|
|
2
2
|
|
|
3
|
-
A React audio player
|
|
3
|
+
A production-ready React audio player built on [WaveSurfer.js](https://wavesurfer.xyz/). Features waveform visualization, global state management, a persistent mini-player, and social sharing—everything you need for a music streaming experience.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why wavesurf?
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **Volume
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
Building a good audio player is harder than it looks. You need:
|
|
8
|
+
|
|
9
|
+
- **Global state** so only one song plays at a time (like Spotify)
|
|
10
|
+
- **Waveform visualization** that's performant and interactive
|
|
11
|
+
- **A persistent mini-player** that stays visible while users browse
|
|
12
|
+
- **Volume fade-in** so playback doesn't blast at full volume
|
|
13
|
+
- **Mobile responsiveness** across all screen sizes
|
|
14
|
+
- **Lazy loading** so pages with many tracks don't lag
|
|
15
|
+
|
|
16
|
+
wavesurf handles all of this out of the box, so you can focus on your actual product.
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
npm install wavesurf wavesurfer.js
|
|
22
|
-
# or
|
|
23
|
-
yarn add wavesurf wavesurfer.js
|
|
24
|
-
# or
|
|
25
|
-
pnpm add wavesurf wavesurfer.js
|
|
26
22
|
```
|
|
27
23
|
|
|
24
|
+
> **Why wavesurfer.js is a peer dependency:** You might already have it in your project, or want to control the version. Making it a peer dependency prevents duplicate bundles and version conflicts.
|
|
25
|
+
|
|
28
26
|
## Quick Start
|
|
29
27
|
|
|
30
|
-
### 1.
|
|
28
|
+
### 1. Add the Provider
|
|
29
|
+
|
|
30
|
+
Wrap your app (or the part that needs audio) with `AudioPlayerProvider`:
|
|
31
31
|
|
|
32
32
|
```tsx
|
|
33
33
|
import { AudioPlayerProvider } from 'wavesurf';
|
|
34
34
|
|
|
35
|
-
function App() {
|
|
35
|
+
export default function App() {
|
|
36
36
|
return (
|
|
37
37
|
<AudioPlayerProvider>
|
|
38
38
|
<YourApp />
|
|
@@ -41,12 +41,15 @@ function App() {
|
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
### 2. Add the
|
|
44
|
+
### 2. Add the Mini Player
|
|
45
|
+
|
|
46
|
+
Place `MiniPlayer` in your layout—it appears automatically when a song plays:
|
|
45
47
|
|
|
46
48
|
```tsx
|
|
47
49
|
import { MiniPlayer } from 'wavesurf';
|
|
50
|
+
import 'wavesurf/styles.css';
|
|
48
51
|
|
|
49
|
-
function Layout({ children }) {
|
|
52
|
+
export default function Layout({ children }) {
|
|
50
53
|
return (
|
|
51
54
|
<div>
|
|
52
55
|
{children}
|
|
@@ -56,25 +59,24 @@ function Layout({ children }) {
|
|
|
56
59
|
}
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
### 3.
|
|
62
|
+
### 3. Display Songs with WaveformPlayer
|
|
60
63
|
|
|
61
64
|
```tsx
|
|
62
65
|
import { WaveformPlayer } from 'wavesurf';
|
|
63
|
-
import 'wavesurf/styles.css';
|
|
64
66
|
|
|
65
|
-
function
|
|
67
|
+
function TrackList({ tracks }) {
|
|
66
68
|
return (
|
|
67
69
|
<div>
|
|
68
|
-
{
|
|
70
|
+
{tracks.map((track) => (
|
|
69
71
|
<WaveformPlayer
|
|
70
|
-
key={
|
|
72
|
+
key={track.id}
|
|
71
73
|
song={{
|
|
72
|
-
id:
|
|
73
|
-
title:
|
|
74
|
-
artist:
|
|
75
|
-
audioUrl:
|
|
76
|
-
duration:
|
|
77
|
-
peaks:
|
|
74
|
+
id: track.id,
|
|
75
|
+
title: track.title,
|
|
76
|
+
artist: track.artist,
|
|
77
|
+
audioUrl: track.url,
|
|
78
|
+
duration: track.duration,
|
|
79
|
+
peaks: track.peaks, // Optional but recommended
|
|
78
80
|
}}
|
|
79
81
|
/>
|
|
80
82
|
))}
|
|
@@ -83,164 +85,358 @@ function SongList({ songs }) {
|
|
|
83
85
|
}
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
|
|
88
|
+
That's it. Click play on any track, and the mini-player appears. Click another track, and it seamlessly switches.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Architecture & Design Decisions
|
|
93
|
+
|
|
94
|
+
### Global Audio Context
|
|
95
|
+
|
|
96
|
+
**Problem:** In a typical music app, you have multiple track listings, album pages, and a persistent player bar. Without global state, you'd have multiple `<audio>` elements fighting each other.
|
|
97
|
+
|
|
98
|
+
**Solution:** wavesurf uses React Context to maintain a single audio source. When you call `play()` from anywhere in your app, it:
|
|
99
|
+
|
|
100
|
+
1. Pauses any currently playing audio
|
|
101
|
+
2. Loads the new track
|
|
102
|
+
3. Starts playback with a volume fade-in
|
|
103
|
+
4. Notifies all `WaveformPlayer` components to update their UI
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
// Any component can control playback
|
|
107
|
+
const { play, pause, currentSong, isPlaying } = useAudioPlayer();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Waveform Visualization (Why WaveSurfer.js?)
|
|
111
|
+
|
|
112
|
+
**Problem:** Audio waveforms require decoding audio data and rendering thousands of bars. Doing this poorly kills performance.
|
|
113
|
+
|
|
114
|
+
**Solution:** WaveSurfer.js is the industry standard for web audio visualization. It handles:
|
|
115
|
+
|
|
116
|
+
- Efficient canvas rendering
|
|
117
|
+
- Audio decoding
|
|
118
|
+
- Responsive resize handling
|
|
119
|
+
- Click-to-seek interactions
|
|
120
|
+
|
|
121
|
+
wavesurf wraps WaveSurfer.js with React lifecycle management, so you don't deal with manual cleanup or memory leaks.
|
|
122
|
+
|
|
123
|
+
### Pre-computed Peaks (Performance)
|
|
124
|
+
|
|
125
|
+
**Problem:** Decoding audio to generate waveforms is slow—especially for longer tracks or pages with many songs. Users see loading spinners everywhere.
|
|
126
|
+
|
|
127
|
+
**Solution:** Generate peaks once (server-side), store them, and pass them to wavesurf:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<WaveformPlayer
|
|
131
|
+
song={{
|
|
132
|
+
id: '1',
|
|
133
|
+
title: 'My Song',
|
|
134
|
+
audioUrl: '/audio/song.mp3',
|
|
135
|
+
duration: 245,
|
|
136
|
+
peaks: [0.1, 0.3, 0.5, 0.8, ...], // Pre-computed!
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
When peaks are provided:
|
|
142
|
+
- **No audio decoding needed** — waveform renders instantly
|
|
143
|
+
- **No network request for audio** — until the user clicks play
|
|
144
|
+
- **Pages load faster** — even with 50+ tracks
|
|
145
|
+
|
|
146
|
+
#### How to Generate Peaks
|
|
147
|
+
|
|
148
|
+
Using [audiowaveform](https://github.com/bbc/audiowaveform) (recommended):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Install
|
|
152
|
+
brew install audiowaveform # macOS
|
|
153
|
+
apt install audiowaveform # Ubuntu
|
|
154
|
+
|
|
155
|
+
# Generate peaks
|
|
156
|
+
audiowaveform -i song.mp3 -o peaks.json --pixels-per-second 10 -b 8
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Or server-side with FFmpeg/Node.js—compute once when uploading audio, store in your database.
|
|
160
|
+
|
|
161
|
+
### Volume Fade-in (UX)
|
|
162
|
+
|
|
163
|
+
**Problem:** Clicking play and getting blasted with sudden audio is jarring. Users instinctively reach for the volume.
|
|
164
|
+
|
|
165
|
+
**Solution:** wavesurf fades volume from 0 to the user's set level over 3 seconds (configurable). This:
|
|
166
|
+
|
|
167
|
+
- Creates a professional, polished feel
|
|
168
|
+
- Prevents startling users
|
|
169
|
+
- Matches how streaming services behave
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
<AudioPlayerProvider config={{
|
|
173
|
+
fadeInEnabled: true, // default: true
|
|
174
|
+
fadeInDuration: 3000, // default: 3000ms
|
|
175
|
+
}}>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Volume Persistence (UX)
|
|
179
|
+
|
|
180
|
+
**Problem:** Users set their volume, navigate to another page, and it resets.
|
|
181
|
+
|
|
182
|
+
**Solution:** Volume is automatically saved to localStorage and restored on page load.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
<AudioPlayerProvider config={{
|
|
186
|
+
persistVolume: true, // default: true
|
|
187
|
+
storageKey: 'myAppVolume', // default: 'audioPlayerVolume'
|
|
188
|
+
defaultVolume: 0.8, // default: 1
|
|
189
|
+
}}>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Lazy Loading (Performance)
|
|
193
|
+
|
|
194
|
+
**Problem:** A page with 20 tracks means 20 WaveSurfer instances initializing at once, causing jank.
|
|
195
|
+
|
|
196
|
+
**Solution:** wavesurf uses IntersectionObserver to only initialize waveforms when they scroll into view:
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
<WaveformPlayer
|
|
200
|
+
song={song}
|
|
201
|
+
lazyLoad={true} // default: true
|
|
202
|
+
/>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Tracks off-screen are just empty containers until needed.
|
|
206
|
+
|
|
207
|
+
### Mini Player (UX Pattern)
|
|
208
|
+
|
|
209
|
+
**Problem:** Users want to browse your site while listening. A player embedded in the track list disappears when they navigate.
|
|
210
|
+
|
|
211
|
+
**Solution:** The `MiniPlayer` component is a fixed bar (bottom or top) that:
|
|
212
|
+
|
|
213
|
+
- Appears when playback starts
|
|
214
|
+
- Shows current track, progress, volume controls
|
|
215
|
+
- Has its own mini waveform for seeking
|
|
216
|
+
- Stays visible during navigation
|
|
217
|
+
- Can be closed by the user
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
<MiniPlayer
|
|
221
|
+
position="bottom" // or "top"
|
|
222
|
+
showVolume={true} // auto-hidden on mobile
|
|
223
|
+
showClose={true}
|
|
224
|
+
onClose={() => console.log('Player closed')}
|
|
225
|
+
/>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Components
|
|
87
231
|
|
|
88
232
|
### AudioPlayerProvider
|
|
89
233
|
|
|
90
|
-
|
|
234
|
+
Wraps your app to provide global audio state.
|
|
91
235
|
|
|
92
236
|
```tsx
|
|
93
|
-
<AudioPlayerProvider config={
|
|
237
|
+
<AudioPlayerProvider config={{
|
|
238
|
+
fadeInEnabled: true,
|
|
239
|
+
fadeInDuration: 3000,
|
|
240
|
+
persistVolume: true,
|
|
241
|
+
storageKey: 'audioPlayerVolume',
|
|
242
|
+
defaultVolume: 1,
|
|
243
|
+
onPlay: (song) => analytics.track('play', song),
|
|
244
|
+
onPause: () => analytics.track('pause'),
|
|
245
|
+
onEnd: () => analytics.track('songEnded'),
|
|
246
|
+
onTimeUpdate: (time) => {},
|
|
247
|
+
}}>
|
|
94
248
|
{children}
|
|
95
249
|
</AudioPlayerProvider>
|
|
96
250
|
```
|
|
97
251
|
|
|
98
|
-
#### Config Options
|
|
99
|
-
|
|
100
|
-
| Property | Type | Default | Description |
|
|
101
|
-
|----------|------|---------|-------------|
|
|
102
|
-
| `fadeInEnabled` | `boolean` | `true` | Enable volume fade-in effect on play |
|
|
103
|
-
| `fadeInDuration` | `number` | `3000` | Duration of fade-in in milliseconds |
|
|
104
|
-
| `persistVolume` | `boolean` | `true` | Persist volume to localStorage |
|
|
105
|
-
| `storageKey` | `string` | `'audioPlayerVolume'` | localStorage key for volume |
|
|
106
|
-
| `defaultVolume` | `number` | `1` | Default volume level (0-1) |
|
|
107
|
-
| `onPlay` | `(song: Song) => void` | - | Callback when playback starts |
|
|
108
|
-
| `onPause` | `() => void` | - | Callback when playback pauses |
|
|
109
|
-
| `onEnd` | `() => void` | - | Callback when song ends |
|
|
110
|
-
| `onTimeUpdate` | `(time: number) => void` | - | Callback on time update |
|
|
111
|
-
|
|
112
252
|
### useAudioPlayer Hook
|
|
113
253
|
|
|
114
|
-
Access
|
|
254
|
+
Access state and controls from any component:
|
|
115
255
|
|
|
116
256
|
```tsx
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
stop,
|
|
136
|
-
} = useAudioPlayer();
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<button onClick={togglePlay}>
|
|
140
|
-
{isPlaying ? 'Pause' : 'Play'}
|
|
141
|
-
</button>
|
|
142
|
-
);
|
|
143
|
-
}
|
|
257
|
+
const {
|
|
258
|
+
// State
|
|
259
|
+
currentSong, // Song | null
|
|
260
|
+
isPlaying, // boolean
|
|
261
|
+
currentTime, // number (seconds)
|
|
262
|
+
duration, // number (seconds)
|
|
263
|
+
volume, // number (0-1, user's saved volume)
|
|
264
|
+
displayVolume, // number (0-1, actual volume during fade)
|
|
265
|
+
isFadingIn, // boolean
|
|
266
|
+
|
|
267
|
+
// Actions
|
|
268
|
+
play, // (song: Song) => void
|
|
269
|
+
pause, // () => void
|
|
270
|
+
togglePlay, // () => void
|
|
271
|
+
seek, // (time: number) => void
|
|
272
|
+
setVolume, // (volume: number) => void
|
|
273
|
+
stop, // () => void
|
|
274
|
+
} = useAudioPlayer();
|
|
144
275
|
```
|
|
145
276
|
|
|
146
277
|
### WaveformPlayer
|
|
147
278
|
|
|
148
|
-
Displays a
|
|
279
|
+
Displays a track with waveform visualization:
|
|
149
280
|
|
|
150
281
|
```tsx
|
|
151
282
|
<WaveformPlayer
|
|
152
|
-
song={
|
|
283
|
+
song={{
|
|
284
|
+
id: string,
|
|
285
|
+
title: string,
|
|
286
|
+
artist?: string,
|
|
287
|
+
album?: string,
|
|
288
|
+
audioUrl: string,
|
|
289
|
+
duration?: number,
|
|
290
|
+
peaks?: number[],
|
|
291
|
+
}}
|
|
153
292
|
waveformConfig={{
|
|
154
293
|
waveColor: '#666666',
|
|
155
294
|
progressColor: '#D4AF37',
|
|
295
|
+
cursorColor: '#D4AF37',
|
|
296
|
+
barWidth: 2,
|
|
297
|
+
barGap: 1,
|
|
298
|
+
barRadius: 2,
|
|
156
299
|
height: 60,
|
|
157
300
|
}}
|
|
158
301
|
lazyLoad={true}
|
|
159
302
|
showTime={true}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)}
|
|
164
|
-
renderControls={(song, isPlaying) => (
|
|
165
|
-
<button>Share</button>
|
|
166
|
-
)}
|
|
303
|
+
standalone={false} // Use local audio instead of global context
|
|
304
|
+
className=""
|
|
305
|
+
renderHeader={(song, isPlaying) => <CustomHeader />}
|
|
306
|
+
renderControls={(song, isPlaying) => <CustomControls />}
|
|
167
307
|
/>
|
|
168
308
|
```
|
|
169
309
|
|
|
170
|
-
####
|
|
310
|
+
#### Standalone Mode
|
|
171
311
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
312
|
+
By default, `WaveformPlayer` uses the global `AudioPlayerProvider` context and works with the `MiniPlayer`. If you want a simpler setup—individual players that don't share state and don't show the mini player bar—use standalone mode:
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
// No AudioPlayerProvider needed
|
|
316
|
+
<WaveformPlayer
|
|
317
|
+
song={song}
|
|
318
|
+
standalone={true}
|
|
319
|
+
/>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**When to use standalone mode:**
|
|
323
|
+
- Simple pages with just one or two tracks
|
|
324
|
+
- Embedded players that shouldn't affect the rest of your site
|
|
325
|
+
- When you don't want the persistent mini player bar
|
|
326
|
+
|
|
327
|
+
**Standalone mode behavior:**
|
|
328
|
+
- Each player manages its own audio element
|
|
329
|
+
- Clicking play on one song automatically pauses others (even in standalone mode)
|
|
330
|
+
- No MiniPlayer appears
|
|
331
|
+
- Volume fade-in and persistence are not applied
|
|
181
332
|
|
|
182
333
|
### MiniPlayer
|
|
183
334
|
|
|
184
|
-
|
|
335
|
+
Persistent playback bar:
|
|
185
336
|
|
|
186
337
|
```tsx
|
|
187
338
|
<MiniPlayer
|
|
188
|
-
position="bottom"
|
|
339
|
+
position="bottom" // 'top' | 'bottom'
|
|
189
340
|
showVolume={true}
|
|
190
341
|
showClose={true}
|
|
191
|
-
onClose={() =>
|
|
192
|
-
className="
|
|
193
|
-
waveformConfig={{
|
|
194
|
-
progressColor: '#FF0000',
|
|
195
|
-
}}
|
|
342
|
+
onClose={() => {}}
|
|
343
|
+
className=""
|
|
344
|
+
waveformConfig={{...}}
|
|
196
345
|
/>
|
|
197
346
|
```
|
|
198
347
|
|
|
199
|
-
####
|
|
348
|
+
#### Persisting Across Route Changes
|
|
200
349
|
|
|
201
|
-
|
|
202
|
-
|------|------|---------|-------------|
|
|
203
|
-
| `position` | `'top' \| 'bottom'` | `'bottom'` | Position on screen |
|
|
204
|
-
| `showVolume` | `boolean` | `true` | Show volume control (auto-hidden on mobile) |
|
|
205
|
-
| `showClose` | `boolean` | `true` | Show close button |
|
|
206
|
-
| `onClose` | `() => void` | - | Callback when close is clicked |
|
|
207
|
-
| `className` | `string` | `''` | Additional CSS class |
|
|
208
|
-
| `waveformConfig` | `WaveformConfig` | - | Waveform styling options |
|
|
350
|
+
To keep the MiniPlayer visible and audio playing while users navigate between pages, place both `AudioPlayerProvider` and `MiniPlayer` in your **root layout**—not in individual pages.
|
|
209
351
|
|
|
210
|
-
|
|
352
|
+
**Next.js App Router:**
|
|
211
353
|
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
354
|
+
```tsx
|
|
355
|
+
// app/layout.tsx
|
|
356
|
+
import { AudioPlayerProvider, MiniPlayer } from 'wavesurf';
|
|
357
|
+
import 'wavesurf/styles.css';
|
|
358
|
+
|
|
359
|
+
export default function RootLayout({ children }) {
|
|
360
|
+
return (
|
|
361
|
+
<html>
|
|
362
|
+
<body>
|
|
363
|
+
<AudioPlayerProvider>
|
|
364
|
+
<Header />
|
|
365
|
+
<main>{children}</main>
|
|
366
|
+
<Footer />
|
|
367
|
+
<MiniPlayer />
|
|
368
|
+
</AudioPlayerProvider>
|
|
369
|
+
</body>
|
|
370
|
+
</html>
|
|
371
|
+
);
|
|
221
372
|
}
|
|
222
373
|
```
|
|
223
374
|
|
|
224
|
-
|
|
375
|
+
**Next.js Pages Router:**
|
|
225
376
|
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
377
|
+
```tsx
|
|
378
|
+
// pages/_app.tsx
|
|
379
|
+
import { AudioPlayerProvider, MiniPlayer } from 'wavesurf';
|
|
380
|
+
import 'wavesurf/styles.css';
|
|
381
|
+
|
|
382
|
+
export default function MyApp({ Component, pageProps }) {
|
|
383
|
+
return (
|
|
384
|
+
<AudioPlayerProvider>
|
|
385
|
+
<Component {...pageProps} />
|
|
386
|
+
<MiniPlayer />
|
|
387
|
+
</AudioPlayerProvider>
|
|
388
|
+
);
|
|
236
389
|
}
|
|
237
390
|
```
|
|
238
391
|
|
|
239
|
-
|
|
392
|
+
**React Router:**
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
// App.tsx
|
|
396
|
+
import { AudioPlayerProvider, MiniPlayer } from 'wavesurf';
|
|
397
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
398
|
+
import 'wavesurf/styles.css';
|
|
399
|
+
|
|
400
|
+
function App() {
|
|
401
|
+
return (
|
|
402
|
+
<AudioPlayerProvider>
|
|
403
|
+
<BrowserRouter>
|
|
404
|
+
<Routes>
|
|
405
|
+
<Route path="/" element={<Home />} />
|
|
406
|
+
<Route path="/album/:id" element={<Album />} />
|
|
407
|
+
</Routes>
|
|
408
|
+
</BrowserRouter>
|
|
409
|
+
<MiniPlayer />
|
|
410
|
+
</AudioPlayerProvider>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Why this works:** React Context state persists as long as the provider component stays mounted. By placing it in the root layout, the audio state survives page transitions. If you put the provider inside a page component, it unmounts on navigation and loses the current song.
|
|
416
|
+
|
|
417
|
+
### ShareButtons
|
|
418
|
+
|
|
419
|
+
Social sharing for tracks:
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
import { ShareButtons } from 'wavesurf';
|
|
423
|
+
|
|
424
|
+
<ShareButtons
|
|
425
|
+
url="https://mysite.com/track/123"
|
|
426
|
+
text="Check out this song!"
|
|
427
|
+
platforms={['facebook', 'twitter', 'whatsapp', 'copy']}
|
|
428
|
+
onShare={(platform, url) => analytics.track('share', { platform })}
|
|
429
|
+
showLabels={false}
|
|
430
|
+
/>
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Available platforms:** `facebook`, `twitter`, `whatsapp`, `linkedin`, `reddit`, `telegram`, `email`, `copy`
|
|
240
434
|
|
|
241
|
-
|
|
435
|
+
---
|
|
242
436
|
|
|
243
|
-
|
|
437
|
+
## Styling
|
|
438
|
+
|
|
439
|
+
### Using Default Styles
|
|
244
440
|
|
|
245
441
|
```tsx
|
|
246
442
|
import 'wavesurf/styles.css';
|
|
@@ -248,24 +444,27 @@ import 'wavesurf/styles.css';
|
|
|
248
444
|
|
|
249
445
|
### Customizing with CSS Variables
|
|
250
446
|
|
|
251
|
-
Override
|
|
447
|
+
Override any of these in your CSS:
|
|
252
448
|
|
|
253
449
|
```css
|
|
254
450
|
:root {
|
|
255
|
-
/*
|
|
256
|
-
--wsp-wave-color: #
|
|
257
|
-
--wsp-progress-color: #
|
|
258
|
-
--wsp-cursor-color: #
|
|
451
|
+
/* Waveform */
|
|
452
|
+
--wsp-wave-color: #666666;
|
|
453
|
+
--wsp-progress-color: #D4AF37;
|
|
454
|
+
--wsp-cursor-color: #D4AF37;
|
|
455
|
+
|
|
456
|
+
/* Backgrounds */
|
|
259
457
|
--wsp-background: transparent;
|
|
458
|
+
--wsp-background-secondary: rgba(255, 255, 255, 0.05);
|
|
260
459
|
|
|
261
|
-
/*
|
|
262
|
-
--wsp-button-bg: #
|
|
263
|
-
--wsp-button-bg-hover: #
|
|
264
|
-
--wsp-button-text: #
|
|
460
|
+
/* Buttons */
|
|
461
|
+
--wsp-button-bg: #D4AF37;
|
|
462
|
+
--wsp-button-bg-hover: #e5c04a;
|
|
463
|
+
--wsp-button-text: #000000;
|
|
265
464
|
|
|
266
|
-
/* Text
|
|
465
|
+
/* Text */
|
|
267
466
|
--wsp-text: #ffffff;
|
|
268
|
-
--wsp-text-muted: #
|
|
467
|
+
--wsp-text-muted: #a3a3a3;
|
|
269
468
|
|
|
270
469
|
/* Sizing */
|
|
271
470
|
--wsp-height: 60px;
|
|
@@ -273,54 +472,32 @@ Override the CSS variables to customize the appearance:
|
|
|
273
472
|
--wsp-button-size: 56px;
|
|
274
473
|
|
|
275
474
|
/* Mini Player */
|
|
276
|
-
--wsp-mini-bg: #
|
|
277
|
-
--wsp-mini-border-color: #
|
|
475
|
+
--wsp-mini-bg: #0a0a0a;
|
|
476
|
+
--wsp-mini-border-color: #D4AF37;
|
|
477
|
+
--wsp-mini-shadow: 0 -4px 20px rgba(0, 0, 0, 0.5);
|
|
478
|
+
|
|
479
|
+
/* Transitions */
|
|
480
|
+
--wsp-transition: 150ms ease;
|
|
278
481
|
}
|
|
279
482
|
```
|
|
280
483
|
|
|
281
|
-
###
|
|
484
|
+
### Custom Styling
|
|
282
485
|
|
|
283
|
-
|
|
486
|
+
All components use BEM-style class names you can target:
|
|
284
487
|
|
|
285
|
-
- `.wsp-player` -
|
|
488
|
+
- `.wsp-player` - WaveformPlayer container
|
|
286
489
|
- `.wsp-play-button` - Play/pause button
|
|
287
490
|
- `.wsp-waveform` - Waveform container
|
|
288
|
-
- `.wsp-time-display` - Time
|
|
289
|
-
- `.wsp-mini-player` -
|
|
290
|
-
- `.wsp-
|
|
291
|
-
- `.wsp-
|
|
491
|
+
- `.wsp-time-display` - Time labels
|
|
492
|
+
- `.wsp-mini-player` - MiniPlayer container
|
|
493
|
+
- `.wsp-share-buttons` - ShareButtons container
|
|
494
|
+
- `.wsp-share-button` - Individual share button
|
|
292
495
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
For optimal performance, especially with large audio files, you can pre-compute waveform peaks on your server. This eliminates the need to decode audio on the client.
|
|
296
|
-
|
|
297
|
-
### Generating peaks with audiowaveform
|
|
298
|
-
|
|
299
|
-
```bash
|
|
300
|
-
# Install audiowaveform (on Ubuntu/Debian)
|
|
301
|
-
sudo apt install audiowaveform
|
|
302
|
-
|
|
303
|
-
# Generate peaks JSON
|
|
304
|
-
audiowaveform -i audio.mp3 -o peaks.json --pixels-per-second 10
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
### Using peaks in your app
|
|
308
|
-
|
|
309
|
-
```tsx
|
|
310
|
-
const song = {
|
|
311
|
-
id: '1',
|
|
312
|
-
title: 'My Song',
|
|
313
|
-
audioUrl: '/audio/song.mp3',
|
|
314
|
-
duration: 180, // 3 minutes
|
|
315
|
-
peaks: peaksData, // Array of numbers from audiowaveform
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
<WaveformPlayer song={song} />
|
|
319
|
-
```
|
|
496
|
+
---
|
|
320
497
|
|
|
321
498
|
## TypeScript
|
|
322
499
|
|
|
323
|
-
All types are exported
|
|
500
|
+
All types are exported:
|
|
324
501
|
|
|
325
502
|
```typescript
|
|
326
503
|
import type {
|
|
@@ -331,18 +508,79 @@ import type {
|
|
|
331
508
|
WaveformConfig,
|
|
332
509
|
WaveformPlayerProps,
|
|
333
510
|
MiniPlayerProps,
|
|
511
|
+
SharePlatform,
|
|
512
|
+
ShareButtonsProps,
|
|
334
513
|
} from 'wavesurf';
|
|
335
514
|
```
|
|
336
515
|
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Examples
|
|
519
|
+
|
|
520
|
+
### Custom Play Button (Headless Usage)
|
|
521
|
+
|
|
522
|
+
```tsx
|
|
523
|
+
function CustomPlayButton({ song }) {
|
|
524
|
+
const { play, pause, currentSong, isPlaying } = useAudioPlayer();
|
|
525
|
+
const isThisSong = currentSong?.id === song.id;
|
|
526
|
+
const playing = isThisSong && isPlaying;
|
|
527
|
+
|
|
528
|
+
return (
|
|
529
|
+
<button onClick={() => playing ? pause() : play(song)}>
|
|
530
|
+
{playing ? 'Pause' : 'Play'}
|
|
531
|
+
</button>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Track Card with Share
|
|
537
|
+
|
|
538
|
+
```tsx
|
|
539
|
+
function TrackCard({ track }) {
|
|
540
|
+
const shareUrl = `https://mysite.com/track/${track.id}`;
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
<div className="track-card">
|
|
544
|
+
<WaveformPlayer song={track} />
|
|
545
|
+
<ShareButtons
|
|
546
|
+
url={shareUrl}
|
|
547
|
+
text={`Listen to ${track.title}`}
|
|
548
|
+
platforms={['twitter', 'whatsapp', 'copy']}
|
|
549
|
+
/>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Analytics Integration
|
|
556
|
+
|
|
557
|
+
```tsx
|
|
558
|
+
<AudioPlayerProvider config={{
|
|
559
|
+
onPlay: (song) => {
|
|
560
|
+
analytics.track('song_play', {
|
|
561
|
+
songId: song.id,
|
|
562
|
+
title: song.title,
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
onEnd: () => {
|
|
566
|
+
analytics.track('song_completed');
|
|
567
|
+
},
|
|
568
|
+
}}>
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
337
573
|
## Browser Support
|
|
338
574
|
|
|
339
|
-
|
|
575
|
+
Requires browsers with:
|
|
340
576
|
- Web Audio API
|
|
341
577
|
- CSS Custom Properties
|
|
342
|
-
- IntersectionObserver
|
|
578
|
+
- IntersectionObserver
|
|
343
579
|
|
|
344
580
|
All modern browsers (Chrome, Firefox, Safari, Edge) are supported.
|
|
345
581
|
|
|
582
|
+
---
|
|
583
|
+
|
|
346
584
|
## License
|
|
347
585
|
|
|
348
|
-
MIT
|
|
586
|
+
MIT © [TheDecipherist](https://github.com/TheDecipherist)
|