Skip to content

Tracker

  • Listening in a background tab is counted. Browsers throttle hidden tabs, so the player’s timeupdates can stall and then report minutes of playback as one jump — which the tracker discarded as a seek. A forward jump over 5s is now credited when it fits the real time that passed (at the current playback rate, with 50% headroom plus SEEK_SLACK, 1s), and only treated as a seek when it clearly exceeds it. The stretch between the last timeupdate and ended is credited the same way. Small jumps behave as before.
  • Live streams send duration: 0 instead of null. A stream’s duration is Infinity, which JSON serialises as null; payloads now always carry a number, with 0 meaning unknown. complete is explicitly skipped when the duration isn’t finite (it could never fire, but only by accident).
  • reset() fully detaches the tracker. It cleared the config but left the document ready/destroy listeners attached, so a player created afterwards was still tracked and its first timeupdate threw (this.config was null). reset() now removes those listeners, and trackPlayer() without a config (before init() or after reset()) warns and does nothing.
  • Thresholds crossed in the last second before ended, pause or destroy are no longer lost. The checks run at most once a second, and ended only looked at complete before resetting, so a play or listen threshold crossed inside that window was dropped. ended, pause and untrackPlayer() (which waveformplayer:destroy calls) now run every check unthrottled first.
  • complete needs real listening, not just the playhead position. It fired as soon as the position reached the threshold, so scrubbing to 95% (or to the end, via ended) counted as a completion with a second heard. It now also requires the listener to have heard half the audio up to the threshold (COMPLETE_ENGAGEMENT, 0.5), in media time: with complete: 90 on a 4-minute track, 108s. Both the timeupdate and ended paths apply it. Expect fewer complete events: skip-to-the-end sessions no longer count.
  • A threshold of 0 fires instead of disabling the event. events.play: 0 (or listen: 0, complete: 0) was falsy and silently turned the event off; it now fires on the first check after playback starts. Omitting the key, null or false still disables an event.
  • init() can be called more than once. Each call added another pair of document listeners, so every player was handled twice per extra call. A second init() now reconfigures the tracker in place: the new config applies to players already tracked, and the session id is kept, since it’s the same page session. After reset(), init() starts afresh with a new id.
  • Cross-origin endpoints receive listen and complete via fetch keepalive instead of sendBeacon. A beacon’s application/json body isn’t CORS-safelisted and beacons always send credentials, so an endpoint on another origin answering Access-Control-Allow-Origin: * never got those events — and sendBeacon still reported success, so nothing fell back. Terminal events to another origin now use fetch with keepalive: true (still Content-Type: application/json); the endpoint must answer the CORS preflight for Content-Type. Same-origin endpoints keep using sendBeacon (unless custom headers are set), and it remains the fallback when fetch is unavailable.
  • The package declares "type": "module" and an exports map. import resolves to the ESM build and require to the new CJS build; ./dist/*, ./src/* and ./package.json stay reachable. Paths outside those are no longer importable.
  • A CommonJS build, dist/waveform-tracker.cjs, now the main entry. main pointed at the IIFE bundle, so require('@arraypress/waveform-tracker') returned {}. It now returns { default: tracker }, matching @arraypress/waveform-player’s CJS build.
  • prepublishOnly runs the tests and a fresh build, so npm publish can’t ship a stale dist/.
  • Per-track state resets when the player’s URL changes. A player outlives many tracks, and load()/loadTrack() swap the URL mid-play without an ended in between, so the new track inherited the previous one’s elapsed time (firing listen early) and its sent events (never firing play or listen at all). Direct load(url) swaps also need @arraypress/waveform-player 1.27.1, where load() records the new URL.
  • Destroyed players are released. The tracker listens for waveformplayer:destroy and untracks the player, so SPAs that create and destroy players no longer leak them.
  • complete on ended works in external audio mode, reading the final time from the event detail instead of player.audio.
  • Engagement is counted in media time, from currentTime deltas, so 1.5x/2x playback is credited for the content actually heard. Forward jumps over 5s are treated as seeks and not credited.
  • listen and complete are delivered with sendBeacon (falling back to fetch with keepalive) so they survive page navigation.
  • Configuration problems and delivery failures always log, via console.warn/console.error with a [WaveformTracker] prefix; tracing stays behind debug.
  • Peer dependency raised to @arraypress/waveform-player ^1.8.0.
  • Peer dependency raised to @arraypress/waveform-player ^1.7.2. No source changes.
  • Initial release: play, listen and complete events for every WaveformPlayer on the page, delivered to an endpoint or a custom handler.