Reach for callbacks when…
You construct the player in JS and want one handler bound for its whole life. They’re terser and the player is the (usually only) argument.
The player exposes its lifecycle two ways. Pick whichever fits how you wired the player up:
on* constructor callbacks — passed as options when you build the player.
Best when you own the new WaveformPlayer(...) call and want a single handler
per instance.waveformplayer:* DOM events — bubbling CustomEvents dispatched on the
player’s container element. Best for declarative (data-waveform-player)
players you never constructed by hand, for attaching/detaching listeners after
init, or for delegating from an ancestor.Both paths fire from the same internal points, so a given moment (say, playback
starting) triggers both the onPlay callback and the
waveformplayer:play event. Use one, the other, or both.
| Moment | Callback | DOM event |
|---|---|---|
| Ready (post-construct) | — | waveformplayer:ready |
| Waveform drawn | onLoad |
— |
| Play | onPlay |
waveformplayer:play |
| Pause | onPause |
waveformplayer:pause |
| Progress tick | onTimeUpdate |
waveformplayer:timeupdate |
| Track ended | onEnd |
waveformplayer:ended |
| Load/decode/audio error | onError |
— |
| Destroyed | — | waveformplayer:destroy |
| Play requested (external) | — | waveformplayer:request-play |
| Pause requested (external) | — | waveformplayer:request-pause |
| Seek requested (external) | — | waveformplayer:request-seek |
Note the asymmetry: onLoad / onError have no DOM-event twin, and
ready / destroy / request-* have no callback twin.
Pass these as options. Each defaults to null. They are plain functions invoked
with positional arguments — there is no event object and nothing to
preventDefault().
| Callback | Signature | Fires when |
|---|---|---|
onLoad |
(player) => void |
A track’s waveform has finished drawing (after peaks are fetched/generated and rendered). |
onPlay |
(player) => void |
Playback starts — both self and external modes. |
onPause |
(player) => void |
Playback pauses — both modes. Also runs once as part of onEnd. |
onEnd |
(player) => void |
The track reaches its end. In external mode this is synthesized when progress reaches >= 1. |
onError |
(error, player) => void |
A load, decode, or audio error occurs. This is the single funnel for surfaced errors. |
onTimeUpdate |
(currentTime, duration, player) => void |
On every progress tick. currentTime and duration are in seconds. Same argument order in both modes. |
import WaveformPlayer from '@arraypress/waveform-player';import '@arraypress/waveform-player/dist/waveform-player.css';
const player = new WaveformPlayer('#player', { url: '/audio/track.mp3',
onLoad: (p) => { console.log('ready to play:', p.options.title); }, onPlay: (p) => updateUI('playing'), onPause: (p) => updateUI('paused'), onTimeUpdate: (currentTime, duration, p) => { progressEl.textContent = `${currentTime.toFixed(1)} / ${duration.toFixed(1)}s`; }, onEnd: (p) => { console.log('finished:', p.options.url); playNext(); }, onError: (error, p) => { console.error('playback failed:', error); },});Every event is a bubbling CustomEvent dispatched on the player’s container
(player.container). Read the payload from event.detail. detail.player is
always the originating WaveformPlayer instance, and detail.url is the current
track URL.
| Event | detail shape |
Cancelable | Modes |
|---|---|---|---|
waveformplayer:ready |
{ player, url } |
no | both |
waveformplayer:play |
{ player, url } |
no | both |
waveformplayer:pause |
{ player, url } |
no | both |
waveformplayer:timeupdate |
{ player, currentTime, duration, progress, url } |
no | both |
waveformplayer:ended |
{ player, url, currentTime, duration } |
no | both |
waveformplayer:destroy |
{ player, url } |
no | both |
waveformplayer:request-play |
track detail (see below) | yes | external only |
waveformplayer:request-pause |
track detail | yes | external only |
waveformplayer:request-seek |
track detail + { percent } |
yes | external only |
In the timeupdate detail, progress is a 0..1 fraction of the track
(currentTime / duration), handy for driving a progress bar without dividing
yourself. The ended detail reports currentTime equal to duration.
const el = player.container;
el.addEventListener('waveformplayer:ready', (e) => { console.log('ready:', e.detail.url);});
el.addEventListener('waveformplayer:timeupdate', (e) => { const { currentTime, duration, progress } = e.detail; bar.style.width = `${progress * 100}%`;});
el.addEventListener('waveformplayer:ended', (e) => { console.log('ended at', e.detail.currentTime, 'of', e.detail.duration);});Because the events bubble, a single delegated listener can cover many players — including declarative ones you never constructed:
// One listener for every player on the page, now or later.document.addEventListener('waveformplayer:play', (e) => { analytics.track('audio_play', { url: e.detail.url, id: e.detail.player.id, });});waveformplayer:destroydestroy() emits waveformplayer:destroy first, before tearing anything
down, so listeners get a chance to release references (the player, the element,
cached detail objects) before the container is emptied and listeners are
aborted.
player.container.addEventListener('waveformplayer:destroy', (e) => { cleanupFor(e.detail.player.id);});In audioMode: 'external' the player owns no <audio> element — it is a
visualization surface driven by an external clock via setPlayingState() and
setProgress(). When the user interacts with it (clicks the play button, clicks
the canvas, or seeks with the keyboard), the player can’t act on its own, so it
asks the controller by dispatching a cancelable request event.
These three events fire only in external mode:
| Event | Extra detail | Triggered by |
|---|---|---|
waveformplayer:request-play |
— | play() / togglePlay() while paused |
waveformplayer:request-pause |
— | pause() / togglePlay() while playing |
waveformplayer:request-seek |
{ percent } (0..1) |
canvas click or keyboard slider seek |
All three carry the full track detail object (the seek event adds
percent):
{ url: string, title: string | null, artist: string | null, artwork: string | null, markers: Array<{ time: number, label: string, color?: string }>, waveform: number[] | string | null, id: string, player: WaveformPlayer}This shape mirrors the input that WaveformBar.play() expects, so a controller
can forward the detail straight through.
Call event.preventDefault() to veto the player’s default reaction:
request-play / request-pause: preventDefault() cancels the visual state
flip — the player stays as it was until your controller drives it with
setPlayingState().request-seek: without preventDefault() the local progress overlay
advances optimistically to percent so the canvas repaints immediately.
Call preventDefault() if you’d rather wait and drive the position yourself
via setProgress().const viz = new WaveformPlayer('#viz', { url: '/audio/track.mp3', audioMode: 'external', waveform: peaks, // pre-computed; skips Web Audio decode});
const audio = document.querySelector('#my-audio');
viz.container.addEventListener('waveformplayer:request-play', (e) => { e.preventDefault(); // we own playback audio.play();});
viz.container.addEventListener('waveformplayer:request-pause', (e) => { e.preventDefault(); audio.pause();});
viz.container.addEventListener('waveformplayer:request-seek', (e) => { // Let the overlay move optimistically (no preventDefault), // then sync the real clock. audio.currentTime = e.detail.percent * audio.duration;});
// Pump the visuals from the real <audio> clock:audio.addEventListener('play', () => viz.setPlayingState(true));audio.addEventListener('pause', () => viz.setPlayingState(false));audio.addEventListener('timeupdate', () => viz.setProgress(audio.currentTime, audio.duration));Reach for callbacks when…
You construct the player in JS and want one handler bound for its whole life. They’re terser and the player is the (usually only) argument.
Reach for DOM events when…
The player is declarative (data-waveform-player), you need to add/remove
listeners dynamically, you want to delegate from an ancestor, or you need to
veto an external-mode request with preventDefault().
All document- and element-level listeners the player registers internally are
tied to a single AbortController, so destroy() removes every one of them in a
single step (and disconnects the ResizeObserver). You are responsible only for
listeners you added with addEventListener — remove those yourself, ideally
in response to waveformplayer:destroy.
const onEnded = (e) => playNext(e.detail.player);player.container.addEventListener('waveformplayer:ended', onEnded);
// later…player.container.addEventListener('waveformplayer:destroy', () => { player.container.removeEventListener('waveformplayer:ended', onEnded);});on* callbacks and audioMode.