wavesurf 1.1.0 → 1.2.1
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 +93 -0
- package/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +105 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +105 -31
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +18 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -300,12 +300,36 @@ Displays a track with waveform visualization:
|
|
|
300
300
|
}}
|
|
301
301
|
lazyLoad={true}
|
|
302
302
|
showTime={true}
|
|
303
|
+
standalone={false} // Use local audio instead of global context
|
|
303
304
|
className=""
|
|
304
305
|
renderHeader={(song, isPlaying) => <CustomHeader />}
|
|
305
306
|
renderControls={(song, isPlaying) => <CustomControls />}
|
|
306
307
|
/>
|
|
307
308
|
```
|
|
308
309
|
|
|
310
|
+
#### Standalone Mode
|
|
311
|
+
|
|
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
|
|
332
|
+
|
|
309
333
|
### MiniPlayer
|
|
310
334
|
|
|
311
335
|
Persistent playback bar:
|
|
@@ -321,6 +345,75 @@ Persistent playback bar:
|
|
|
321
345
|
/>
|
|
322
346
|
```
|
|
323
347
|
|
|
348
|
+
#### Persisting Across Route Changes
|
|
349
|
+
|
|
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.
|
|
351
|
+
|
|
352
|
+
**Next.js App Router:**
|
|
353
|
+
|
|
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
|
+
);
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Next.js Pages Router:**
|
|
376
|
+
|
|
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
|
+
);
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
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
|
+
|
|
324
417
|
### ShareButtons
|
|
325
418
|
|
|
326
419
|
Social sharing for tracks:
|
package/dist/index.d.mts
CHANGED
|
@@ -123,6 +123,12 @@ interface WaveformPlayerProps {
|
|
|
123
123
|
renderHeader?: (song: Song, isPlaying: boolean) => React.ReactNode;
|
|
124
124
|
/** Custom render function for additional controls */
|
|
125
125
|
renderControls?: (song: Song, isPlaying: boolean) => React.ReactNode;
|
|
126
|
+
/**
|
|
127
|
+
* Standalone mode - play audio locally without global context/MiniPlayer.
|
|
128
|
+
* Use this when you want a simple player without the persistent mini player bar.
|
|
129
|
+
* (default: false)
|
|
130
|
+
*/
|
|
131
|
+
standalone?: boolean;
|
|
126
132
|
}
|
|
127
133
|
/**
|
|
128
134
|
* Position of the mini player.
|
|
@@ -195,7 +201,7 @@ interface AudioPlayerProviderProps {
|
|
|
195
201
|
declare function AudioPlayerProvider({ children, config: userConfig, }: AudioPlayerProviderProps): react_jsx_runtime.JSX.Element;
|
|
196
202
|
declare function useAudioPlayer(): AudioPlayerContextValue;
|
|
197
203
|
|
|
198
|
-
declare function WaveformPlayer({ song, waveformConfig: userWaveformConfig, lazyLoad, showTime, className, renderHeader, renderControls, }: WaveformPlayerProps): react_jsx_runtime.JSX.Element;
|
|
204
|
+
declare function WaveformPlayer({ song, waveformConfig: userWaveformConfig, lazyLoad, showTime, className, renderHeader, renderControls, standalone, }: WaveformPlayerProps): react_jsx_runtime.JSX.Element;
|
|
199
205
|
|
|
200
206
|
declare function MiniPlayer({ position, showVolume, showClose, onClose, className, waveformConfig: userWaveformConfig, }: MiniPlayerProps): react_jsx_runtime.JSX.Element | null;
|
|
201
207
|
|
package/dist/index.d.ts
CHANGED
|
@@ -123,6 +123,12 @@ interface WaveformPlayerProps {
|
|
|
123
123
|
renderHeader?: (song: Song, isPlaying: boolean) => React.ReactNode;
|
|
124
124
|
/** Custom render function for additional controls */
|
|
125
125
|
renderControls?: (song: Song, isPlaying: boolean) => React.ReactNode;
|
|
126
|
+
/**
|
|
127
|
+
* Standalone mode - play audio locally without global context/MiniPlayer.
|
|
128
|
+
* Use this when you want a simple player without the persistent mini player bar.
|
|
129
|
+
* (default: false)
|
|
130
|
+
*/
|
|
131
|
+
standalone?: boolean;
|
|
126
132
|
}
|
|
127
133
|
/**
|
|
128
134
|
* Position of the mini player.
|
|
@@ -195,7 +201,7 @@ interface AudioPlayerProviderProps {
|
|
|
195
201
|
declare function AudioPlayerProvider({ children, config: userConfig, }: AudioPlayerProviderProps): react_jsx_runtime.JSX.Element;
|
|
196
202
|
declare function useAudioPlayer(): AudioPlayerContextValue;
|
|
197
203
|
|
|
198
|
-
declare function WaveformPlayer({ song, waveformConfig: userWaveformConfig, lazyLoad, showTime, className, renderHeader, renderControls, }: WaveformPlayerProps): react_jsx_runtime.JSX.Element;
|
|
204
|
+
declare function WaveformPlayer({ song, waveformConfig: userWaveformConfig, lazyLoad, showTime, className, renderHeader, renderControls, standalone, }: WaveformPlayerProps): react_jsx_runtime.JSX.Element;
|
|
199
205
|
|
|
200
206
|
declare function MiniPlayer({ position, showVolume, showClose, onClose, className, waveformConfig: userWaveformConfig, }: MiniPlayerProps): react_jsx_runtime.JSX.Element | null;
|
|
201
207
|
|
package/dist/index.js
CHANGED
|
@@ -176,10 +176,7 @@ function AudioPlayerProvider({
|
|
|
176
176
|
if (state.isPlaying) {
|
|
177
177
|
pause();
|
|
178
178
|
} else {
|
|
179
|
-
|
|
180
|
-
if (configRef.current.fadeInEnabled) {
|
|
181
|
-
audioRef.current.volume = 0;
|
|
182
|
-
}
|
|
179
|
+
audioRef.current.volume = state.displayVolume;
|
|
183
180
|
audioRef.current.play().then(() => {
|
|
184
181
|
if (typeof window !== "undefined") {
|
|
185
182
|
window.dispatchEvent(
|
|
@@ -188,13 +185,10 @@ function AudioPlayerProvider({
|
|
|
188
185
|
})
|
|
189
186
|
);
|
|
190
187
|
}
|
|
191
|
-
if (configRef.current.fadeInEnabled) {
|
|
192
|
-
fadeInVolume(targetVolume);
|
|
193
|
-
}
|
|
194
188
|
}).catch(() => {
|
|
195
189
|
});
|
|
196
190
|
}
|
|
197
|
-
}, [state.isPlaying, state.currentSong, state.
|
|
191
|
+
}, [state.isPlaying, state.currentSong, state.displayVolume, pause]);
|
|
198
192
|
const seek = react.useCallback((time) => {
|
|
199
193
|
if (!audioRef.current) return;
|
|
200
194
|
audioRef.current.currentTime = time;
|
|
@@ -304,6 +298,7 @@ var DEFAULT_WAVEFORM_CONFIG = {
|
|
|
304
298
|
height: 60,
|
|
305
299
|
normalize: true
|
|
306
300
|
};
|
|
301
|
+
react.createContext(null);
|
|
307
302
|
function WaveformPlayer({
|
|
308
303
|
song,
|
|
309
304
|
waveformConfig: userWaveformConfig,
|
|
@@ -311,35 +306,87 @@ function WaveformPlayer({
|
|
|
311
306
|
showTime = true,
|
|
312
307
|
className = "",
|
|
313
308
|
renderHeader,
|
|
314
|
-
renderControls
|
|
309
|
+
renderControls,
|
|
310
|
+
standalone = false
|
|
315
311
|
}) {
|
|
316
312
|
const waveformConfig = { ...DEFAULT_WAVEFORM_CONFIG, ...userWaveformConfig };
|
|
317
313
|
const containerRef = react.useRef(null);
|
|
318
314
|
const wavesurferRef = react.useRef(null);
|
|
315
|
+
const localAudioRef = react.useRef(null);
|
|
319
316
|
const [isReady, setIsReady] = react.useState(false);
|
|
320
317
|
const [totalDuration, setTotalDuration] = react.useState(song.duration || 0);
|
|
318
|
+
const [localIsPlaying, setLocalIsPlaying] = react.useState(false);
|
|
319
|
+
const [localCurrentTime, setLocalCurrentTime] = react.useState(0);
|
|
321
320
|
const { ref: wrapperRef, isVisible } = useLazyLoad({
|
|
322
321
|
forceVisible: !lazyLoad
|
|
323
322
|
});
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
const
|
|
334
|
-
const
|
|
323
|
+
let contextValue = null;
|
|
324
|
+
try {
|
|
325
|
+
if (!standalone) {
|
|
326
|
+
contextValue = useAudioPlayer();
|
|
327
|
+
}
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
const useStandaloneMode = standalone || !contextValue;
|
|
331
|
+
const contextPlay = contextValue?.play;
|
|
332
|
+
const contextTogglePlay = contextValue?.togglePlay;
|
|
333
|
+
const contextSeek = contextValue?.seek;
|
|
334
|
+
const contextCurrentSong = contextValue?.currentSong;
|
|
335
|
+
const contextIsPlaying = contextValue?.isPlaying ?? false;
|
|
336
|
+
const contextCurrentTime = contextValue?.currentTime ?? 0;
|
|
337
|
+
const isThisSongPlayingInContext = !useStandaloneMode && contextCurrentSong?.id === song.id;
|
|
338
|
+
const isPlaying = useStandaloneMode ? localIsPlaying : isThisSongPlayingInContext && contextIsPlaying;
|
|
339
|
+
const currentTime = useStandaloneMode ? localCurrentTime : isThisSongPlayingInContext ? contextCurrentTime : 0;
|
|
335
340
|
react.useEffect(() => {
|
|
336
|
-
if (!
|
|
341
|
+
if (!useStandaloneMode) return;
|
|
342
|
+
const audio = new Audio();
|
|
343
|
+
audio.preload = "metadata";
|
|
344
|
+
localAudioRef.current = audio;
|
|
345
|
+
const handleTimeUpdate = () => {
|
|
346
|
+
setLocalCurrentTime(audio.currentTime);
|
|
347
|
+
};
|
|
348
|
+
const handleEnded = () => {
|
|
349
|
+
setLocalIsPlaying(false);
|
|
350
|
+
setLocalCurrentTime(0);
|
|
351
|
+
};
|
|
352
|
+
const handleLoadedMetadata = () => {
|
|
353
|
+
setTotalDuration(audio.duration);
|
|
354
|
+
};
|
|
355
|
+
audio.addEventListener("timeupdate", handleTimeUpdate);
|
|
356
|
+
audio.addEventListener("ended", handleEnded);
|
|
357
|
+
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
|
|
358
|
+
return () => {
|
|
359
|
+
audio.removeEventListener("timeupdate", handleTimeUpdate);
|
|
360
|
+
audio.removeEventListener("ended", handleEnded);
|
|
361
|
+
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
|
362
|
+
audio.pause();
|
|
363
|
+
audio.src = "";
|
|
364
|
+
};
|
|
365
|
+
}, [useStandaloneMode]);
|
|
366
|
+
react.useEffect(() => {
|
|
367
|
+
if (!useStandaloneMode) return;
|
|
368
|
+
const handleOtherPlayerPlay = (event) => {
|
|
369
|
+
if (event.detail !== song.id && localAudioRef.current) {
|
|
370
|
+
localAudioRef.current.pause();
|
|
371
|
+
setLocalIsPlaying(false);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
window.addEventListener(MINI_PLAYER_PLAY_EVENT, handleOtherPlayerPlay);
|
|
375
|
+
return () => {
|
|
376
|
+
window.removeEventListener(MINI_PLAYER_PLAY_EVENT, handleOtherPlayerPlay);
|
|
377
|
+
};
|
|
378
|
+
}, [useStandaloneMode, song.id]);
|
|
379
|
+
react.useEffect(() => {
|
|
380
|
+
if (!wavesurferRef.current) return;
|
|
381
|
+
const relevantCurrentTime = useStandaloneMode ? localCurrentTime : contextCurrentTime;
|
|
382
|
+
const shouldSync = useStandaloneMode ? localIsPlaying : isThisSongPlayingInContext;
|
|
383
|
+
if (!shouldSync) return;
|
|
337
384
|
const waveDuration = wavesurferRef.current.getDuration();
|
|
338
|
-
if (waveDuration > 0 &&
|
|
339
|
-
const progress =
|
|
385
|
+
if (waveDuration > 0 && relevantCurrentTime >= 0) {
|
|
386
|
+
const progress = relevantCurrentTime / waveDuration;
|
|
340
387
|
wavesurferRef.current.seekTo(Math.min(progress, 1));
|
|
341
388
|
}
|
|
342
|
-
}, [contextCurrentTime,
|
|
389
|
+
}, [localCurrentTime, contextCurrentTime, useStandaloneMode, localIsPlaying, isThisSongPlayingInContext]);
|
|
343
390
|
react.useEffect(() => {
|
|
344
391
|
if (!containerRef.current || !isVisible) return;
|
|
345
392
|
const hasPeaks = song.peaks && song.peaks.length > 0;
|
|
@@ -354,18 +401,24 @@ function WaveformPlayer({
|
|
|
354
401
|
height: waveformConfig.height,
|
|
355
402
|
normalize: waveformConfig.normalize,
|
|
356
403
|
interact: true,
|
|
357
|
-
// Allow clicking on waveform to seek
|
|
358
404
|
// Only load audio URL if we don't have peaks (needed to generate waveform)
|
|
359
405
|
url: hasPeaks ? void 0 : song.audioUrl,
|
|
360
406
|
peaks: hasPeaks ? [song.peaks] : void 0,
|
|
361
407
|
duration: hasPeaks ? song.duration || 0 : void 0
|
|
362
408
|
});
|
|
409
|
+
wavesurfer.setMuted(true);
|
|
363
410
|
wavesurfer.on("ready", () => {
|
|
364
411
|
setIsReady(true);
|
|
365
412
|
setTotalDuration(wavesurfer.getDuration() || song.duration || 0);
|
|
413
|
+
wavesurfer.setMuted(true);
|
|
366
414
|
});
|
|
367
415
|
wavesurfer.on("interaction", (newTime) => {
|
|
368
|
-
if (
|
|
416
|
+
if (useStandaloneMode) {
|
|
417
|
+
if (localAudioRef.current) {
|
|
418
|
+
localAudioRef.current.currentTime = newTime;
|
|
419
|
+
setLocalCurrentTime(newTime);
|
|
420
|
+
}
|
|
421
|
+
} else if (isThisSongPlayingInContext && contextSeek) {
|
|
369
422
|
contextSeek(newTime);
|
|
370
423
|
}
|
|
371
424
|
});
|
|
@@ -387,7 +440,8 @@ function WaveformPlayer({
|
|
|
387
440
|
song.peaks,
|
|
388
441
|
song.duration,
|
|
389
442
|
isVisible,
|
|
390
|
-
|
|
443
|
+
useStandaloneMode,
|
|
444
|
+
isThisSongPlayingInContext,
|
|
391
445
|
contextSeek,
|
|
392
446
|
waveformConfig.waveColor,
|
|
393
447
|
waveformConfig.progressColor,
|
|
@@ -400,12 +454,32 @@ function WaveformPlayer({
|
|
|
400
454
|
]);
|
|
401
455
|
const handlePlayClick = react.useCallback(() => {
|
|
402
456
|
if (!song.id || !song.audioUrl) return;
|
|
403
|
-
if (
|
|
404
|
-
|
|
457
|
+
if (useStandaloneMode) {
|
|
458
|
+
if (!localAudioRef.current) return;
|
|
459
|
+
if (localIsPlaying) {
|
|
460
|
+
localAudioRef.current.pause();
|
|
461
|
+
setLocalIsPlaying(false);
|
|
462
|
+
} else {
|
|
463
|
+
if (typeof window !== "undefined") {
|
|
464
|
+
window.dispatchEvent(
|
|
465
|
+
new CustomEvent(MINI_PLAYER_PLAY_EVENT, { detail: song.id })
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
if (localAudioRef.current.src !== song.audioUrl) {
|
|
469
|
+
localAudioRef.current.src = song.audioUrl;
|
|
470
|
+
}
|
|
471
|
+
localAudioRef.current.play().catch(() => {
|
|
472
|
+
});
|
|
473
|
+
setLocalIsPlaying(true);
|
|
474
|
+
}
|
|
405
475
|
} else {
|
|
406
|
-
|
|
476
|
+
if (isThisSongPlayingInContext) {
|
|
477
|
+
contextTogglePlay?.();
|
|
478
|
+
} else {
|
|
479
|
+
contextPlay?.(song);
|
|
480
|
+
}
|
|
407
481
|
}
|
|
408
|
-
}, [song,
|
|
482
|
+
}, [song, useStandaloneMode, localIsPlaying, isThisSongPlayingInContext, contextPlay, contextTogglePlay]);
|
|
409
483
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
410
484
|
"div",
|
|
411
485
|
{
|