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 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
- const targetVolume = Math.max(state.volume, MIN_FADE_IN_VOLUME);
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.volume, pause, fadeInVolume]);
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
- const {
325
- play: contextPlay,
326
- togglePlay: contextTogglePlay,
327
- seek: contextSeek,
328
- currentSong,
329
- isPlaying: contextIsPlaying,
330
- currentTime: contextCurrentTime
331
- } = useAudioPlayer();
332
- const isThisSongPlaying = currentSong?.id === song.id;
333
- const isPlaying = isThisSongPlaying && contextIsPlaying;
334
- const currentTime = isThisSongPlaying ? contextCurrentTime : 0;
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 (!wavesurferRef.current || !isThisSongPlaying) return;
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 && contextCurrentTime >= 0) {
339
- const progress = contextCurrentTime / waveDuration;
385
+ if (waveDuration > 0 && relevantCurrentTime >= 0) {
386
+ const progress = relevantCurrentTime / waveDuration;
340
387
  wavesurferRef.current.seekTo(Math.min(progress, 1));
341
388
  }
342
- }, [contextCurrentTime, isThisSongPlaying]);
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 (isThisSongPlaying) {
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
- isThisSongPlaying,
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 (isThisSongPlaying) {
404
- contextTogglePlay();
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
- contextPlay(song);
476
+ if (isThisSongPlayingInContext) {
477
+ contextTogglePlay?.();
478
+ } else {
479
+ contextPlay?.(song);
480
+ }
407
481
  }
408
- }, [song, isThisSongPlaying, contextPlay, contextTogglePlay]);
482
+ }, [song, useStandaloneMode, localIsPlaying, isThisSongPlayingInContext, contextPlay, contextTogglePlay]);
409
483
  return /* @__PURE__ */ jsxRuntime.jsxs(
410
484
  "div",
411
485
  {