Skip to content

Getting Started

WaveformTracker is a headless, ~2KB analytics add-on for WaveformPlayer. It measures how much of your audio people actually listen to — real media-time, not wall-clock — and POSTs a compact JSON event to your endpoint (or hands it to your own callback) when a listener crosses a threshold you define.

It renders no DOM, ships no CSS, sets no cookies, and collects no PII. Drop it on a page, call init(), and it auto-discovers every player.

Media-time, not wall-clock

Engagement accumulates from currentTime deltas. 1.5x/2x playback is fully credited; paused and idle time is not.

Seeks ignored

A forward jump of 5s or more only counts when real time kept pace with it (a throttled background tab); anything beyond that is a seek and dropped — scrubbing never inflates your numbers.

Privacy-first

No cookies, no localStorage, no PII. The optional session id is a random in-memory string, regenerated every reload.

Survives unload

Terminal events go out with fetch + keepalive — or navigator.sendBeacon for a same-origin endpoint — so they land even as the page navigates away.

Because it’s headless, the tracker is easiest to see with a live readout. The player below is wired to a tracker whose handler renders each captured event straight into the panel (no server) — the listen threshold is lowered to 5s so it fires quickly. Press play, let it run, pause, scrub — a jump ahead isn’t credited, so scrubbing won’t hurry the listen event along:

Captured events
  1. Press play — events stream in here.

In production you’d swap that handler for an endpoint and these exact payloads would POST to your server.

WaveformTracker credits media-time consumed, derived from the player’s waveformplayer:timeupdate events. On every update it computes the delta between the current and previous currentTime and accumulates it as elapsedTime — but only when the delta is a genuine forward step:

Delta between updates Credited? Why
0 < delta < 5s Yes Normal playback, including 1.5x / 2x (faster playback still advances currentTime)
delta <= 0 No A loop, rewind, or non-advance — nothing was consumed
delta >= 5s, within the real time that passed Yes A background tab whose updates were throttled, reporting minutes of playback in one jump (1.1.0+)
delta >= 5s, beyond it No Treated as a seek (see SEEK_THRESHOLD below) and dropped

The stretch between the last timeupdate and the track’s ended event is credited by the same rule (1.1.0+), so a track that finishes in a hidden tab isn’t short-changed.

The result is an honest “seconds of content heard” number. Pausing stops crediting and clears the delta baseline, but it does not reset the accumulated total — resuming continues where you left off. The accumulated total and the fired-event set reset when the track ended fires, which re-arms every event for a replay, and when the player moves to a different URL, so a track swapped in mid-play starts from zero instead of inheriting the previous track’s time (1.0.2+).

WaveformTracker.SEEK_THRESHOLD // static class constant = 5

SEEK_THRESHOLD is the forward currentTime jump (in seconds) below which a delta is always credited as playback. It is a class-level constant, not a config option — it lives on the class, so window.WaveformTracker.SEEK_THRESHOLD (the exported singleton) is undefined, and there is no runtime knob to change it. The player fires timeupdate far more often than once every 5 seconds, so foreground playback never trips it.

A larger jump isn’t automatically a seek (1.1.0+). Browsers throttle hidden tabs, so the player’s updates can stall while the audio plays on, then report minutes of playback at once. The tracker compares the jump with the wall-clock time since the previous update and credits it when it is no more than elapsed × playbackRate × 1.5 + 1s (50% headroom for timer jitter, plus a 1-second SEEK_SLACK); only a jump beyond that is a seek. In external audio mode there is no <audio> element to read the rate from, so 1x is assumed.

WaveformTracker has a peer dependency on @arraypress/waveform-player@^1.8.0. Load and initialize the player first — it dispatches the events the tracker consumes — then load the tracker.

Terminal window
npm install @arraypress/waveform-player @arraypress/waveform-tracker

The default export is a singleton instance, not the class — never new it. It is also assigned to window.WaveformTracker in browser builds.

import WaveformPlayer from '@arraypress/waveform-player';
import WaveformTracker from '@arraypress/waveform-tracker';
// Players exist on the page (auto-mounted via data-waveform-player, or constructed)…
WaveformTracker.init({ endpoint: '/api/track' });

From CommonJS, the singleton is the default export: require('@arraypress/waveform-tracker').default (1.1.0+ — earlier versions returned {}). The package’s exports map sends import to the ESM build and require to the CJS build; ./dist/*, ./src/* and ./package.json stay reachable for direct paths.

You don’t attach the tracker to a player by hand. Calling init() does three things:

  1. Reads existing players. It immediately calls WaveformPlayer.getAllInstances() and tracks every player already on the page.

  2. Listens for new players. It adds capturing-phase listeners for waveformplayer:ready on document, so any player created later (lazy-mounted, SPA route change) is tracked automatically.

  3. Cleans up destroyed players. It listens for waveformplayer:destroy and untracks the player, removing its listeners — this prevents the tracker’s internal Map from leaking players (and their DOM) in single-page apps. Requires player v1.8.0+.

// Call this as early as players may appear.
WaveformTracker.init({
endpoint: 'https://api.example.com/track',
events: { play: 3, listen: 30, complete: 90 },
metadata: { post_id: 456 }
});