Validates the built IIFE bundle — load component via <script> tag
| Property | Value |
|---|
Single test run using the mediarecorder backend. The simplest way to validate the component works.
<latency-test id="minimal-tester"></latency-test>
<script>
// inputStream and audioContext must be set by the host before start()
const tester = document.querySelector('#minimal-tester')
tester.inputStream = hostStream // host-managed MediaStream
tester.audioContext = hostAudioContext // host-managed AudioContext
tester.addEventListener('latency-result', e => {
console.log(`Latency: ${e.detail.latency} ms`)
})
tester.start()
</script>
Configure the number of test runs. Results are accumulated and displayed with mean, standard deviation, min, and max.
<!-- inputStream and audioContext must be set by the host before start() -->
<!-- Option 1: HTML attribute -->
<latency-test id="multi-tester" number-of-tests="5"></latency-test>
<script>
const tester = document.querySelector('#multi-tester')
tester.inputStream = hostStream
tester.audioContext = hostAudioContext
tester.addEventListener('latency-complete', e => {
console.log(`Mean: ${e.detail.mean} ms (${e.detail.results.length} runs)`)
})
tester.start()
</script>
<!-- Option 2: JS property -->
<latency-test id="multi-tester"></latency-test>
<script>
const tester = document.querySelector('#multi-tester')
tester.inputStream = hostStream
tester.audioContext = hostAudioContext
tester.numberOfTests = 5
tester.addEventListener('latency-complete', e => {
console.log(`Mean: ${e.detail.mean} ms (${e.detail.results.length} runs)`)
})
tester.start()
</script>
The host creates its own AudioContext and MediaStream, then passes them via properties. The component never closes or stops what it didn't create.
<latency-test id="context-tester"></latency-test>
<script>
const tester = document.querySelector('#context-tester')
const ac = new AudioContext({ latencyHint: 0 })
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: false, channelCount: 1 }
})
tester.audioContext = ac
tester.inputStream = stream
tester.start()
// Component never closes/stops host-provided resources
</script>
Run MediaRecorder and AudioWorklet back-to-back on the same source stream for direct A/B comparison.
<latency-test id="mode-mr" recording-mode="mediarecorder"></latency-test>
<latency-test id="mode-aw" recording-mode="audioworklet"></latency-test>
<script>
const mr = document.querySelector('#mode-mr')
const aw = document.querySelector('#mode-aw')
const ac = new AudioContext({ latencyHint: 0 })
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: false, channelCount: 1 }
})
mr.audioContext = aw.audioContext = ac
mr.inputStream = aw.inputStream = stream
mr.addEventListener('latency-complete', e => {
console.log(`MR: ${e.detail.mean.toFixed(2)} ms`)
})
aw.addEventListener('latency-complete', e => {
console.log(`AW: ${e.detail.mean.toFixed(2)} ms`)
})
mr.start()
</script>
Captures raw Float32 PCM directly in an AudioWorkletProcessor — no codec round-trip, sample-accurate timing. This is the planned v2 default backend. Use when you need the most accurate round-trip measurement.
<latency-test id="aw-tester" recording-mode="audioworklet"></latency-test>
<script>
const tester = document.querySelector('#aw-tester')
tester.inputStream = hostStream // host-managed MediaStream
tester.audioContext = hostAudioContext // host-managed AudioContext
tester.numberOfTests = 5
tester.addEventListener('latency-result', e => {
console.log(`${e.detail.latency.toFixed(2)} ms · ${e.detail.reliable ? 'reliable' : 'unreliable'}`)
})
tester.addEventListener('latency-complete', e => {
console.log(`AW mean: ${e.detail.mean.toFixed(2)} ms`)
})
tester.start()
</script>
Records the mic stream directly via MediaRecorder, without the dual-channel graph used by the default mode. Use as a fallback when the default mediarecorder mode fails because the browser downmixes the stereo capture to mono (it emits latency-error in that case), or to deliberately measure the direct-mic pipeline.
MediaRecorder.start() and the reference playback start. Results are not directly comparable with the 2ch or AudioWorklet panels.
<latency-test id="mr1ch-tester" recording-mode="mediarecorder-1ch"></latency-test>
<script>
const tester = document.querySelector('#mr1ch-tester')
tester.inputStream = hostStream // host-managed MediaStream
tester.audioContext = hostAudioContext // host-managed AudioContext
tester.numberOfTests = 5
tester.addEventListener('latency-result', e => {
console.log(`${e.detail.latency.toFixed(2)} ms · ${e.detail.reliable ? 'reliable' : 'unreliable'}`)
})
tester.addEventListener('latency-error', e => {
console.error(e.detail.message)
})
tester.start()
</script>
All six lifecycle events are logged in real-time with timestamps.
<latency-test id="lifecycle-tester"></latency-test>
<script>
// inputStream and audioContext must be set by the host before start()
const tester = document.querySelector('#lifecycle-tester')
tester.inputStream = hostStream
tester.audioContext = hostAudioContext
tester.addEventListener('latency-start', e => console.log('start'))
tester.addEventListener('latency-recording', e => console.log('recording'))
tester.addEventListener('latency-processing', e => console.log('processing'))
tester.addEventListener('latency-result', e => console.log(e.detail))
tester.addEventListener('latency-complete', e => console.log('complete'))
tester.addEventListener('latency-error', e => console.error(e.detail))
tester.start()
</script>
console.debug('[latency-test]', …) logging at internal checkpoints. Having debug on — or DevTools open — can perturb browser scheduling and affect latency accuracy. Do not use for measurements you intend to record.
Main-thread checkpoints only — cross-correlation worker logs appear in DevTools but not here.
<!-- inputStream and audioContext must be set by the host before start() -->
<!-- Option 1: HTML attribute (debug on from page load) -->
<latency-test id="tester" debug></latency-test>
<script>
const tester = document.querySelector('#tester')
tester.inputStream = hostStream
tester.audioContext = hostAudioContext
tester.addEventListener('latency-result', e => {
// [latency-test] lines appear in DevTools console
console.log(`Latency: ${e.detail.latency.toFixed(2)} ms`)
})
tester.start()
</script>
<!-- Option 2: Runtime toggle (enable before start() for full logs) -->
<latency-test id="tester"></latency-test>
<script>
const tester = document.querySelector('#tester')
tester.inputStream = hostStream
tester.audioContext = hostAudioContext
tester.debug = true // enable before start() to capture all checkpoints
tester.start()
</script>
The host builds a GainNode chain and passes the processed stream to the component. Use this when your recording setup has low mic levels — common on Safari ≥ 16 with echoCancellation: false (empirical value: 50×). A ChannelSplitterNode isolates the left channel, which also handles wired earpods on Safari that force a stereo stream with signal only on the left.
<latency-test id="tester" recording-mode="mediarecorder"></latency-test>
<script>
const ac = new AudioContext({ latencyHint: 0 })
const stream = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: false, noiseSuppression: false,
autoGainControl: false, channelCount: 1 }
})
// Set gainValue to 50 for Safari >= 16 with echoCancellation: false
const gainValue = 1
const source = ac.createMediaStreamSource(stream)
const splitter = ac.createChannelSplitter(2)
const gainNode = ac.createGain()
gainNode.gain.value = gainValue
const dest = ac.createMediaStreamDestination()
dest.channelCount = 1
source.connect(splitter)
splitter.connect(gainNode, 0) // left channel only
gainNode.connect(dest)
const tester = document.querySelector('#tester')
tester.audioContext = ac
tester.inputStream = dest.stream
tester.start()
</script>