Files
ArtPlayer/test/asr-explicit-capture.test.js
T

241 lines
9.2 KiB
JavaScript

import assert from 'node:assert/strict'
import fs from 'node:fs'
import process from 'node:process'
// eslint-disable-next-line test/no-import-node-test -- Explicit ASR capture uses the repository runner.
import test from 'node:test'
import { asrAudioEnvironment, deferredAudio } from './helpers/asr-audio.js'
import { compilePackage } from './helpers/load.js'
const artifact = process.env.ARTPLAYER_ASR_ARTIFACT
const implementation = {
name: artifact || 'candidate-asr',
code: artifact ? fs.readFileSync(artifact, 'utf8') : await compilePackage('artplayer-plugin-asr', 'umd'),
}
function create(option = {}, capabilities = {}) {
const chunks = []
const environment = asrAudioEnvironment(implementation, {
sampleRate: 8000,
interval: 1,
audioInput: { type: 'capture' },
onAudioChunk(chunk) { chunks.push(chunk) },
...option,
}, capabilities)
const prototype = environment.context.AudioContext.prototype
const direct = prototype.createMediaElementSource
let directCalls = 0
prototype.createMediaElementSource = function (...args) {
directCalls++
return direct.apply(this, args)
}
return { ...environment, chunks, directCalls: () => directCalls }
}
function assertCapture(environment) {
assert.equal(environment.directCalls(), 0, 'Explicit capture must never attempt to bind the media element')
const source = environment.nodes.filter(node => node.kind === 'stream').at(-1)
const recorder = environment.recorders.at(-1)
assert.ok(source)
assert.deepEqual(source.connections, [recorder])
assert.equal(recorder.connections.length, 1)
const sink = recorder.connections[0]
assert.equal(sink.kind, 'gain')
assert.equal(sink.gain.value, 0)
assert.deepEqual(sink.connections, [environment.contexts.at(-1).destination])
return { source, recorder, sink }
}
async function assertChunk(environment) {
const before = environment.chunks.length
environment.send(new Float32Array(8).fill(0.5))
await environment.tick()
assert.equal(environment.chunks.length, before + 1)
const chunk = environment.chunks.at(-1)
assert.equal(chunk.pcm.byteLength, 16)
assert.equal(new DataView(chunk.pcm).getInt16(0, true), 16383)
assert.equal(new DataView(chunk.wav).getUint32(24, true), 8000)
}
function assertReleased(environment) {
assert.equal(environment.directCalls(), 0)
assert.equal(environment.intervals.size, 0)
assert.equal(environment.blobs.size, 0)
assert.ok(environment.contexts.every(context => context.closes === 1))
assert.ok(environment.nodes.every(node => node.connections.length === 0))
assert.ok(environment.recorders.every(recorder => recorder.port.onmessage === null))
}
for (const moz of [false, true]) {
test(`ASR explicit ${moz ? 'mozCaptureStream' : 'captureStream'} skips a working direct binding`, async () => {
const environment = create({}, { moz })
await environment.emit('play')
assertCapture(environment)
assert.equal(environment.captures(), 1)
await assertChunk(environment)
await environment.emit('destroy')
assertReleased(environment)
assert.equal(environment.tracks[0].stops, 1)
})
}
test('ASR omitted audioInput preserves direct-first acquisition', async () => {
const environment = create({ audioInput: undefined })
await environment.emit('play')
assert.equal(environment.directCalls(), 1)
assert.equal(environment.captures(), 0)
assert.ok(environment.nodes.some(node => node.kind === 'element'))
await assertChunk(environment)
await environment.emit('destroy')
assert.equal(environment.contexts[0].closes, 1)
})
for (const failure of ['unsupported', 'throws']) {
test(`ASR explicit capture ${failure} cleans up without attempting direct fallback`, async () => {
const environment = create({}, { capture: failure !== 'unsupported' })
let attempts = 0
if (failure === 'throws') {
environment.art.video.captureStream = () => {
attempts++
throw new Error('Capture is unavailable for this media')
}
}
await environment.emit('play')
assertReleased(environment)
assert.equal(environment.errors.length, 1)
assert.equal(environment.recorders.length, 0)
assert.equal(environment.tracks[0].stops, 0)
assert.equal(attempts, failure === 'throws' ? 1 : 0)
await environment.emit('destroy')
assertReleased(environment)
})
}
test('ASR explicit capture retains a silent sink through media mute and volume changes', async () => {
const environment = create()
await environment.emit('play')
const route = assertCapture(environment)
for (const [muted, volume] of [[true, 0], [false, 0.25], [false, 1]]) {
environment.art.muted = environment.art.video.muted = muted
environment.art.volume = environment.art.video.volume = volume
await environment.emit('video:volumechange')
assert.equal(assertCapture(environment).sink, route.sink)
assert.equal(environment.art.video.muted, muted)
assert.equal(environment.art.video.volume, volume)
await assertChunk(environment)
}
await environment.emit('destroy')
assertReleased(environment)
})
test('ASR explicit capture pause retains its stream and resumes with a fresh recorder', async () => {
const environment = create()
await environment.emit('play')
const before = assertCapture(environment)
await environment.emit('pause')
assert.equal(environment.intervals.size, 0)
assert.equal(environment.contexts[0].closes, 0)
assert.equal(environment.tracks[0].stops, 0)
assert.equal(before.recorder.port.onmessage, null)
for (const node of Object.values(before))
assert.deepEqual(node.connections, [])
await environment.emit('play')
const after = assertCapture(environment)
assert.equal(after.source, before.source)
assert.notEqual(after.recorder, before.recorder)
assert.equal(environment.captures(), 1)
await assertChunk(environment)
await environment.emit('destroy')
assertReleased(environment)
})
for (const operation of ['stop/play', 'restart']) {
test(`ASR explicit capture ${operation} releases the old graph and acquires another stream`, async () => {
const environment = create()
await environment.emit('play')
const before = assertCapture(environment)
if (operation === 'stop/play') {
await environment.plugin.stop()
assertReleased(environment)
await environment.emit('play')
}
else {
await environment.emit('restart')
}
const after = assertCapture(environment)
assert.notEqual(after.source, before.source)
assert.equal(environment.captures(), 2)
assert.equal(environment.contexts[0].closes, 1)
assert.equal(environment.contexts[1].closes, 0)
assert.equal(environment.tracks[0].stops, 1)
for (const node of Object.values(before))
assert.deepEqual(node.connections, [])
await assertChunk(environment)
await environment.emit('destroy')
assertReleased(environment)
assert.equal(environment.tracks[0].stops, 2)
await environment.emit('play')
assert.equal(environment.captures(), 2)
assert.equal(environment.intervals.size, 0)
})
}
test('ASR explicit capture snapshots the nested input choice before later caller mutation', async () => {
const input = { type: 'capture' }
const environment = create({ audioInput: input })
input.type = 'changed by caller'
await environment.emit('play')
assertCapture(environment)
await environment.plugin.stop()
await environment.emit('play')
assertCapture(environment)
assert.equal(environment.captures(), 2)
await environment.emit('destroy')
assertReleased(environment)
})
test('ASR explicit capture destroy during worklet loading never binds or captures late', async () => {
const moduleGate = deferredAudio()
const moduleEntered = deferredAudio()
const environment = create({}, { moduleGate, moduleEntered })
const starting = environment.emit('play')
await moduleEntered.promise
await environment.emit('destroy')
moduleGate.resolve()
await starting
assertReleased(environment)
assert.equal(environment.captures(), 0)
assert.equal(environment.recorders.length, 0)
assert.equal(environment.tracks[0].stops, 0)
})
for (const action of ['pause', 'stop']) {
test(`ASR explicit capture ${action} cancels a restart waiting for the old context to close`, async () => {
const environment = create()
await environment.emit('play')
assertCapture(environment)
const closeGate = deferredAudio()
const context = environment.contexts[0]
const close = context.close.bind(context)
context.close = async () => {
await close()
await closeGate.promise
}
const restarting = environment.emit('restart')
const cancelling = action === 'pause' ? environment.emit('pause') : environment.plugin.stop()
assert.equal(environment.intervals.size, 0)
closeGate.resolve()
await Promise.all([restarting, cancelling])
assert.equal(environment.captures(), 1, 'A superseded restart must not acquire another stream')
assert.equal(environment.contexts.length, 1)
assert.equal(environment.tracks[0].stops, 1)
assertReleased(environment)
await environment.emit('play')
assertCapture(environment)
assert.equal(environment.captures(), 2, 'A subsequent explicit play may start a new capture')
await assertChunk(environment)
await environment.emit('destroy')
assertReleased(environment)
assert.equal(environment.tracks[0].stops, 2)
})
}