Skip to content

Payload & API

Every fired event sends the same shape. Field order is deterministic: { event, url, time, duration, page, ...metadata }, then session, then title.

Field Type Description
event string 'play', 'listen', or 'complete'.
url string The player’s options.url (typically from data-url). Required — events without it are dropped with a warning.
time number For play / listen: floor(accumulated media-time). For complete: floor(currentTime).
duration number floor(duration) of the track, in seconds.
page string window.location.pathname — path only, no query string or host.
…metadata any Each key from your metadata config.
session string Present only when session is enabled.
title string Present only when the player has options.title (typically from data-title).
{
"event": "listen",
"url": "audio/ep-42.mp3",
"time": 30,
"duration": 180,
"page": "/podcast/ep-42",
"post_id": 456,
"session": "k3f9q2abc1",
"title": "Episode 42"
}
Event Terminal? Transport
play No Plain fetch POST, keepalive: false.
listen Yes navigator.sendBeacon (Blob, application/json), falls back to fetch + keepalive.
complete Yes Same as listen.

Delivery priority is absolute: if handler is set, it wins outright and endpoint is never called. Otherwise the payload is POSTed to endpoint. Terminal events (listen, complete) prefer sendBeacon so they survive page unload — but only when no custom headers are configured, since beacon can’t send headers. With custom headers, terminal events fall back to fetch + keepalive, and fetch failures are logged via console.error.

A full setup — players on the page, a tracker POSTing to your API, and a minimal server route to receive it.

<!-- 1. Players (auto-mounted by the player's data-* contract) -->
<div data-waveform-player
data-url="audio/ep-42.mp3"
data-title="Episode 42"></div>
<script src="/js/waveform-player.js"></script>
<script src="/js/waveform-tracker.js"></script>
// 2. Initialize the tracker ONCE, after the player script has loaded.
WaveformTracker.init({
endpoint: 'https://api.example.com/track',
events: { play: 3, listen: 30, complete: 90 },
headers: { 'Authorization': 'Bearer ' + window.API_TOKEN }, // forces fetch path
metadata: { post_id: 456, user_id: window.currentUserId },
session: true,
debug: false
});
// 3. Example endpoint (Express). The body is the payload object above.
app.post('/track', express.json(), (req, res) => {
const { event, url, time, duration, page, session, title, post_id } = req.body;
// event: 'play' | 'listen' | 'complete'
// time: media-seconds heard (play/listen) or currentTime (complete)
await db.listens.insert({
event, url, title, seconds: time, duration,
page, session, post_id,
at: new Date()
});
res.sendStatus(204); // beacons ignore the response; 2xx keeps logs clean
});

When you’d rather route events through your own analytics layer (Segment, GA, a queue) instead of HTTP, pass a handler. It receives every payload and fully replaces network delivery — endpoint is ignored.

WaveformTracker.init({
events: { listen: 30, complete: 90 },
handler(payload) {
// payload === { event, url, time, duration, page, ...metadata, session?, title? }
window.analytics?.track('Audio ' + payload.event, payload);
}
});

init() is the only entry point you normally need; the rest are for inspection, SPA control, and teardown.

Method Returns Description
init(config = {}) undefined Configure the singleton, attach document ready/destroy listeners, and track existing players. Call once.
trackPlayer(player) undefined Begin tracking one player. Validates player.container + player.options (warns and bails if invalid); idempotent.
untrackPlayer(player) undefined Stop tracking a player and remove its container listeners. No-op if not tracked.
trackAllPlayers() undefined Track every instance from WaveformPlayer.getAllInstances(). Called automatically by init().
getStats() Array One entry per tracked player: { url, title, elapsedTime, isTracking, sentEvents }.
getTrackedCount() number How many players are currently tracked.
reset() undefined Untrack all players, clear the trackers Map, and null config + sessionId. Does not remove the document ready/destroy listeners.
WaveformTracker.getTrackedCount();
// → 2
WaveformTracker.getStats();
// → [
// { url: 'audio/ep-42.mp3', title: 'Episode 42',
// elapsedTime: 47.3, isTracking: true, sentEvents: ['play', 'listen'] }
// ]

elapsedTime is accumulated media-seconds (a float); sentEvents lists the event names already fired this playback.

WaveformTracker is purely reactive — it listens to player events and never renders anything. For the player’s full event surface, see Player events.

Source Event Effect
document waveformplayer:ready Captured (capture phase) → trackPlayer(detail.player).
document waveformplayer:destroy Captured → untrackPlayer(detail.player). Prevents SPA leaks (player v1.8.0+).
container waveformplayer:play Start tracking; reset the media-time baseline (lastTime = null).
container waveformplayer:pause Stop tracking; clear the baseline. Accumulated elapsedTime is kept.
container waveformplayer:timeupdate { currentTime, duration } → accumulate deltas and run threshold checks.
container waveformplayer:ended { currentTime, duration } → maybe fire complete, then reset sentEvents / elapsedTime for replay. Reads time from the detail, so it works in the player’s external audio mode.

Every tracker message is prefixed [WaveformTracker].

  • warn and error are always emitted, even with debug: false, so integration mistakes (no endpoint/handler, missing url, a thrown handler, dropped events) surface immediately.
  • Verbose log tracing (player ready/destroy, play/pause, sent payloads, beacon fallbacks) is gated behind debug: true.

The init() validation warning — No endpoint or handler configured; events will not be delivered — fires regardless of debug.