@condero/vue-audio-player
@condero/vue-audio-player
Vue 3 audio player with optional waveform visualization, repeat, speed control and A/B looping.
Features
- Progressive playback through the native audio element
- Optional client-side waveform visualization via Web Audio API (no external dependencies)
- Repeat — loop the entire track
- Speed — configurable rate presets via the
speedsprop (default0.5x–1.2xwith fine steps around1.0x), pitch-preserving by default (preservesPitch) - A/B Loop — mark two positions within a track and loop that section
- Dark/Light Mode — supports Bootstrap 5.3 theme variables and
prefers-color-scheme - Responsive — desktop and mobile (touch-friendly)
- Queue-friendly — events and exposed
play()/pause()for playlist-driven hosts, withplayWhenReadyplay intent, position/ratereporting, relative seeking (seekBy), acompactvariant and per-control switches for embedding in small bars
Installation
npm install @condero/vue-audio-player
Usage
<script setup>
import { AudioPlayer } from '@condero/vue-audio-player'
import '@condero/vue-audio-player/style.css'
</script>
<template>
<AudioPlayer src="/path/to/file.mp3" />
</template>
Props
| Prop | Type | Required | Description |
|---|---|---|---|
src |
String | yes | URL to the audio file |
autoplay |
Boolean | no | Starts playback after the initial audio load. Browser autoplay policy may require a manual tap. |
playWhenReady |
Boolean | no | Defaults to false. Play intent that survives src swaps: while true, every load starts playing on ready — the initial one and every swap (unlike autoplay, which covers the initial load only). Reactive: flipping it to false before the next ready cancels the auto-play. Gesture safety: set it to true from a user gesture (e.g. the click that starts the queue) or the browser may reject the play; a rejection is logged as a warning and the player stays paused — if you need the rejection itself (NotAllowedError), sequence play() from @ready instead. |
waveform |
Boolean | no | Defaults to false. When enabled, fetches and decodes the complete file in the background to generate a waveform. |
peaks |
Array | no | Precomputed waveform values from 0 to 1. Providing peaks avoids the waveform fetch. |
variant |
String | no | 'full' (default) or 'compact'. Compact shrinks the waveform bar, buttons and padding for embedding in small player bars (~76px tall with times hidden). Colors and Bootstrap 5.3 theming are unchanged. |
controls |
Object | no | Per-control visibility switches, all true by default: { play, volume, abLoop, repeat, times } (times is the current-time/duration row). A partial object only hides what it names false, e.g. :controls="{ abLoop: false }". |
speeds |
Array | no | Playback-rate presets for the speed select, treated as a set: rendered deduped, filtered to finite values > 0 and sorted ascending — the prop is a set, the component owns the order. Defaults to 0.5–1.2 in 10% steps plus the fine steps 1.05/1.1. If the active rate is no longer offered after a prop change, it snaps to the nearest preset. For choir practice a fine-grained ladder up to 2.0 works well, e.g. :speeds="[0.5, 0.75, 0.95, 1, 1.05, 1.2, 1.3, 1.5, 1.75, 2]". |
preservesPitch |
Boolean | no | Defaults to true: the element stretches time when the rate differs from 1, so playback stays in tune (choir training at 0.6x keeps the pitch). false gives the cassette-style speed pitch shift. Applied to the element's standard preservesPitch and the legacy webkitPreservesPitch, and re-asserted after every load. |
pauseFade |
Boolean | no | Defaults to true. De-clicks pause: the element volume ramps down over ~15 ms, the player pauses in silence, and the volume is restored — the stream never cuts at full amplitude, which is audible as a snap on some output chains. Set false for a hard, immediate pause. A play() during the fade cancels it; load() and volume changes reset it. On iOS Safari the element's volume is read-only, so the fade degrades to a ~15 ms delayed pause there. Note that paused fires after the fade completes. |
Events
| Event | Payload | Description |
|---|---|---|
ready |
{ src } |
The current source can play: the first canplay after each (re)load. Sequence play() here when swapping src programmatically (mobile Safari requires it). A stall recovery does not re-fire it. The src echo lets queue hosts drop stale events after rapid swaps. |
ended |
{ src } |
The current source finished naturally. Not emitted in repeat mode — the track loops silently. |
error |
MediaError, { src } |
The audio element failed. First payload is the native MediaError (code 4 = unsupported/forbidden source, 2 = network failure) or the raw error event when the element reports none — useful for detecting expired signed URLs. The second payload echoes the src that failed. |
playing |
— | Playback started — mirror of the element's play event, whether from the play button or a programmatic play(). |
paused |
— | Playback paused — mirror of the element's pause event. |
time |
{ currentTime, duration, progress, rate } |
Emitted on the element's timeupdate (and when metadata arrive or a seek settles). duration is null while unknown (metadata pending or a live/rangeless stream); progress is the 0..1 ratio, 0 while duration is unknown; rate is the active playback rate. Feed it to a rate-aware navigator.mediaSession.setPositionState() so the lock-screen scrub position does not drift when the rate ≠ 1. |
Payload note: events that were payload-less may have gained payloads in 2.2.0 (ready/ended now carry { src }, error gained a second argument); existing handlers that ignore arguments keep working.
Exposed methods
<script setup>
import { ref } from 'vue'
const player = ref(null)
</script>
<template>
<AudioPlayer
ref="player"
:src="currentSrc"
@ended="advanceToNextTrack"
@error="refetchSignedUrl"
@ready="player.play()"
/>
</template>
play()— starts playback and returns the audio element'splay()promise unchanged, so an autoplay-policy rejection (NotAllowedError) reaches the caller.pause()— pauses playback; honors thepauseFadeprop (a faded pause completes after ~15 ms).seekTo(seconds)— seeks to an absolute position. Clamped to[0, duration]; queued and applied after metadata arrive if they are not loaded yet (like timeline seeks, it never reloads the element).seekBy(delta)— relative seek for skip controls (e.g. MediaSessionseekforward/seekbackward±15s), clamped the same way. A no-op while duration is unknown, so hosts never trackcurrentTimethemselves.position— current playback position in seconds (read-only).duration— duration in seconds, ornullwhile unknown (metadata pending or a live/rangeless stream) — same semantics as thetimepayload.playbackRate— the active playback rate (read-only; change it via thespeedsselect).
The raw audio element is intentionally not exposed; position, volume and rate stay owned by the component.
Queue hosts
A playlist-driven host swaps src per track and keeps one long-lived player instance. With playWhenReady, the ready→play sequencing moves into the component:
<script setup>
import { ref } from 'vue'
import { AudioPlayer } from '@condero/vue-audio-player'
import '@condero/vue-audio-player/style.css'
const player = ref(null)
const currentSrc = ref(null)
const playWhenReady = ref(false)
const queue = [/* track urls */]
function startQueue() {
// The first `true` must originate from a user gesture (autoplay policy).
playWhenReady.value = true
currentSrc.value = queue[0]
}
function advance({ src }) {
if (src !== currentSrc.value) return // stale event from a rapid swap
currentSrc.value = queue[queue.indexOf(src) + 1]
}
</script>
<template>
<AudioPlayer
ref="player"
:src="currentSrc"
:play-when-ready="playWhenReady"
variant="compact"
:controls="{ times: false, abLoop: false, repeat: false }"
@ready="onReady"
@ended="advance"
@error="refetchSignedUrl"
@time="onTime"
/>
</template>
playWhenReadyplays every load onready, swaps included; flipping it tofalsebefore the nextreadycancels the auto-play. A policy rejection is logged and the player stays paused — sequenceplay()from@readyinstead if you need to handleNotAllowedErroryourself (both patterns can be combined; the extraplay()on a playing element is a harmless no-op).- The
srcecho inready/ended/errorpayloads lets you drop stale events after rapid swaps (seeadvanceabove). - The
timeevent feeds Media Session so the lock-screen scrub position tracks the queue:
function onTime({ currentTime, duration, rate }) {
if ('mediaSession' in navigator && duration !== null) {
navigator.mediaSession.setPositionState({
duration,
position: Math.min(currentTime, duration),
playbackRate: rate,
})
}
}
- MediaSession skip actions map to the exposed
seekBy():navigator.mediaSession.setActionHandler('seekforward', () => player.value.seekBy(15))andsetActionHandler('seekbackward', () => player.value.seekBy(-15)).
variant="compact" plus hiding times, abLoop and repeat yields a bar that fits a ~76px container (waveform 28px, 32px play button, 6px padding); controls hides anything else the host drives itself (e.g. play: false when the host renders its own button and drives the exposed play()/pause()).
Swapping sources
Changing the src prop loads the new source paused at 0:00 on the same audio element: position, buffered range, duration and A/B loop points reset, while volume, playback rate and repeat survive. autoplay applies to the initial load only — after a swap, either wait for ready and call the exposed play(), or set playWhenReady and let the component do the sequencing for every load.
Precomputing waveform peaks
Peaks are plain numbers from 0 to 1 — generate them once with ffmpeg and pass them via the peaks prop; the browser then renders the waveform without fetching or decoding anything.
This repo ships the demo track's peaks (public/peaks.json), generated by the same pipeline the production media backend uses:
npm run peaks # demo track -> public/peaks.json
node scripts/generate-peaks.mjs INPUT [OUT] # any file ffmpeg can decode
The pipeline treats ffmpeg purely as a decoder — first audio stream to raw mono 8 kHz 16-bit PCM on stdout:
ffmpeg -hide_banner -loglevel error -i INPUT \
-map a:0 -ac 1 -ar 8000 \
-f s16le -acodec pcm_s16le pipe:1
The script buckets that stream: every sample becomes |s| / 32768 (rounded to 4 decimals), maxima accumulate into 1024-sample coarse buckets, and those are max-spread onto the 800 final buckets — so any tool consuming the PCM pipe reproduces the same numbers. The artifact next to the audio bytes:
{
"version": 1,
"bucket_count": 800,
"sample_rate": 8000,
"duration_ms": 95243,
"peaks": [0.0, 0.0123, "..."]
}
Pass its peaks array to the prop: <AudioPlayer :peaks="artifact.peaks" />. Values saturate at 1.0 where the master exceeds full scale — PCM is 16-bit, so the pipeline cannot represent overshoot.
Player states
The composable distinguishes initial loading from later playback interruptions:
isLoadingis true only fromload()until the firstcanplayevent.isBufferingis true when an already-loaded track is waiting for more data.isSeekingis true while the media element is moving to a new position.isPlayingfollows the media element's play and pause events.
Controls are disabled only during isLoading; buffering and seeking show a small non-blocking indicator.
Streaming and seeking
By default, the player does not fetch or decode the audio file itself. The native audio element handles playback progressively, so playback can begin as soon as the browser has enough data. This works with any plain HTTP audio endpoint; endpoints that support HTTP Range requests (206 Partial Content) provide better seeking and buffering behavior.
The timeline seeks the existing audio element without replacing its src or calling load(). If duration metadata is not available yet, ratio-based timeline seeks are ignored; explicit time seeks are queued and applied after metadata arrives. Live or rangeless streams with an infinite duration do not support ratio-based seeking.
Breaking changes in 2.0.0
Waveform decoding is no longer enabled by default. Set waveform to opt into the client-side full-file decode, or pass backend-precomputed peaks. The new autoplay prop replaces DOM-based play-button workarounds.
Development
npm install
npm run dev # Demo with theme toggle at localhost:5173
npm run build # Build library (dist/)
npm test # Run the unit tests
Local usage without publishing
npm install ../path/to/vue-audio-player
After making changes: run npm run build in the player project, then reinstall in the target project.
Related Packages
Vue 3 components for @arraypress/waveform-bar — singleton mount with config-as-p...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 2.3.0 | ^3.3 | MIT | |
| 2.2.0 | ^3.3 | MIT | |
| 2.1.0 | ^3.3 | MIT | |
| 2.0.0 | ^3.3 | MIT | |
| 1.0.3 | ^3.3 | MIT | |
| 1.0.2 | ^3.3 | MIT | |
| 1.0.1 | ^3.3 | MIT | |
| 1.0.0 | ^3.3 | MIT |