<audio> element, it hands the native player a track list and reacts to the events that come back.
Loading a queue or feed never starts playback on its own, always follow it with a play command. Native playback also outlives web view reloads and in-app navigation, so re-sync your UI on every page load or it will render stale state.
Installation
- Bundle
- CDN
How it works
Assign one global handler,window.onAudioEvent, and every state change comes back through it: play, pause, track changes, buffering, feed pages, errors. To start, hand the native player a track list, then issue a play command. Loading the queue rebuilds the player but does not start playback, so play or playat always follows it.
window.onAudioEvent is assigned outside the isDespia gate because setting a property on window is harmless in a normal browser. Only the scheme calls are gated.
The track object
Both the inline queue and the feed responses use the same track shape.id, url, and title are required, everything else is optional.
Tracks missing
id, url, or title are dropped from the queue silently. author and poster are what populate the lock screen, not artist and artwork.
Load a fixed queue
Useaudio://setqueue when you have the full track list up front. It replaces the whole queue, stops any current playback, and rebuilds the player without autoplaying.
setqueue resets loop and skip_interval to their defaults when you omit them, while speed_rate and controls are preserved. To change a setting without rebuilding the queue, use audio://config instead.
Load a paginated feed
Useaudio://setfeed when tracks load incrementally from a server. The native player fetches the first page immediately, plays through it, and fetches the next page before the queue runs dry.
Your endpoint answers
GET with Accept: application/json and returns { "tracks": [ ... ] }. For pages after the first, the native player appends after=<id of the last track in the queue>, so return the tracks that come after that id. Return an empty tracks array to signal the end of the feed, playback then ends with an ended event after the last track. A failed page fetch emits feed_unavailable and is retried automatically as playback continues, it does not end the feed.
Re-issuing setfeed with the same url is an idempotent no-op, the player keeps running. Re-issuing with a different url continues playback during the fetch: if the currently playing track’s id appears in the new first page, playback carries on seamlessly at its new index, otherwise the player is torn down and you get a terminated event.
Playback controls
The lock-screen buttons fire these schemes automatically. Call them yourself to drive your own on-page controls, the resulting events flow back throughwindow.onAudioEvent either way.
next at the end of a feed queue fetches the next page first, so you see buffering then next, and at the true end of the queue it emits ended instead. With loop on, next restarts the current track. prev at index 0 is a no-op. Out-of-range playat indexes are ignored. skipforward and skipback accept an optional seconds param that overrides the configured skip_interval for that one call.
Reading events
Every state change arrives through the singlewindow.onAudioEvent handler. Dispatch on evt.type. Most events carry a full state object, and the queue-changing ones also carry the current queue.
queue is present only on queue-changing events, and duration_seconds is null until the asset reports it.
evt.state.
error string.
object
Carried by every event except
position. status is one of stopped, playing, paused, buffering. mode is inline or feed. current_index is the source of truth for the current track. position_seconds and duration_seconds are in seconds, duration_seconds is null until known. loop, skip_interval, and speed_rate reflect current config. feed_exhausted is true once the feed has returned its last page. queue is the full track array, present only on queue-flagged event types.object
The 1 Hz progress tick, flat and camelCase:
positionSeconds, durationSeconds (null until known), and status. Use it to drive your scrubber. Do not persist listening progress from it, use a webhook for that.object
Command-level failure.
error is one of unknown_command: ..., invalid_tracks_json, invalid_feed_url, no_track, invalid_seek_seconds. track_error reports invalid_url or the underlying playback error. feed_unavailable reports http_<status>, malformed_response: <preview>, or network_error.
Auto-advance and lock-screen actions emit the same events as your own calls, so your UI stays correct no matter who pressed the button. A track transition can emit
play immediately followed by next, treat next and prev as the track-changed signal and state.current_index as the source of truth. Events emitted while the web view is loading or backgrounded are buffered and flushed in order when it returns, except position ticks, which are dropped while the web view is away.
Update config without restarting playback
Useaudio://config to change settings on an active player without tearing it down. Toggle loop, change speed, swap which lock-screen controls show, or adjust the skip interval, playback continues through the change. Only the params you pass change, the rest are left untouched.
Each successful call fires
config_updated with the new values in state. The queue is not included, since config never changes the queue, and controls is not echoed back, since your code already knows what it sent.
Track progress on your backend
Register a webhook withaudio://webhook and the native player POSTs JSON to your endpoint on every playback milestone, including ones triggered from the lock screen while your web app is suspended. This is how you record listening progress that your page is not awake to see.
{ type, state } payload as the events above, with the full queue, plus an ISO-8601 timestamp. Milestone types are track.play, track.pause, track.next, track.prev, track.seek, track.skipforward, track.skipback, track.speed, track.ended, and player.terminated. Identify the listener yourself, for example with a signed token in the webhook URL. Omit or empty the url to clear the webhook. Delivery is fire-and-forget with no retries, so treat it as a progress signal, not a ledger.
Re-hydrate after a reload
Native playback survives web view reloads and in-app navigation, but a fresh page knows nothing about it. Callaudio://sync on every page load and SPA route change. It replies with a state event carrying the full state and queue, so you can rebuild your UI to match what is already playing.
Stop playback
Callaudio://terminate to fully stop. It tears down the player, releases the audio session so other apps resume their audio, clears the queue and feed state, and removes the Now Playing card. It emits a terminated event. Because the player keeps running after the user closes your page, always give them a visible stop control that calls this.
Lock screen and background behaviour
Every track shows a Now Playing card on the lock screen, Control Center, Dynamic Island, CarPlay, and watch, with title, artist, artwork, live position, and playback rate. Play and pause are always enabled. Thecontrols param on setqueue, setfeed, and config gates the optional buttons, comma-separated and case-insensitive.
All controls are enabled by default.
controls=skipforward,skipback,seek gives a podcast layout, and controls= with no value leaves only play and pause. This gates the system UI only, your audio:// commands always work regardless.
Playback continues when the app is backgrounded or the screen locks. Starting playback claims exclusive audio, so other apps like Apple Music pause, exactly like a native music app. The session activates on the first play and releases on terminate. A phone call, Siri, or alarm pauses playback and you get a pause event, then playback resumes with a play event once the system says the interruption is over. Unplugging headphones or disconnecting Bluetooth pauses playback and emits pause, following standard iOS etiquette.
Choosing the right command
audio://config exists because setqueue and setfeed are heavy operations that rebuild the player. Reach for config whenever you only need to change a setting.
Resources
NPM Package
despia-native