feat(asr): [PKG-ASR-09] add explicit capture ownership and cancel stale restarts

This commit is contained in:
Harvey Zhao committed 2026-09-13 16:19:49 +08:00
1 parent 5ab7da6e1f
commit 146b78d576
27 files changed
+12951 -64

No files matched your search

@@ -13,7 +13,7 @@ export class AudioGraph {
private direct = false
private workletLoaded = false
constructor(private readonly video: CaptureVideo, private readonly sampleRate: number) {}
constructor(private readonly video: CaptureVideo, private readonly sampleRate: number, private readonly captureOnly = false) {}
private assertOpen() {
if (this.closed)
@@ -49,12 +49,16 @@ export class AudioGraph {
}
this.assertOpen()
if (!this.source) {
try {
this.source = context.createMediaElementSource(this.video)
this.direct = true
if (!this.captureOnly) {
try {
this.source = context.createMediaElementSource(this.video)
this.direct = true
}
catch (error) {
console.warn('[artplayerPluginAsr] Direct connection failed:', error)
}
}
catch (error) {
console.warn('[artplayerPluginAsr] Direct connection failed:', error)
if (!this.source) {
const capture = this.video.captureStream || this.video.mozCaptureStream
if (!capture)
throw new Error('Could not establish audio source')
+6 -3
View File
@@ -34,8 +34,11 @@ export class Capture {
restart() {
const active = this.running || Boolean(this.starting)
this.pause()
if (this.graph && !this.graph.ownsMediaConnection)
return this.stop().then(() => active ? this.start() : undefined)
if (this.graph && !this.graph.ownsMediaConnection) {
const stopping = this.stop()
const epoch = this.epoch
return stopping.then(() => active && !this.terminal && this.epoch === epoch ? this.start() : undefined)
}
return active ? this.start() : Promise.resolve()
}
@@ -54,7 +57,7 @@ export class Capture {
const count = Math.floor(sampleRate * interval / 1000)
if (!Number.isSafeInteger(count) || count < 1 || !Number.isFinite(interval) || interval <= 0)
throw new Error('Audio chunk length must be positive and finite')
const graph = this.graph ??= new AudioGraph(this.video, sampleRate)
const graph = this.graph ??= new AudioGraph(this.video, sampleRate, this.options.captureOnly)
await graph.prepare()
if (!current()) {
if (!this.running && this.graph === graph)
+5 -2
View File
@@ -5,13 +5,16 @@ import style from './style.less?inline'
import { createSubtitles } from './subtitles'
export default function artplayerPluginAsr(option: AsrOptions = {}) {
const { length = 3, interval = 100, sampleRate = 16000, autoHideTimeout = 10000, onAudioChunk = () => null } = option
const { length = 3, interval = 100, sampleRate = 16000, autoHideTimeout = 10000, onAudioChunk = () => null, audioInput } = option
if (audioInput !== undefined && audioInput.type !== 'capture')
throw new TypeError('Unsupported ASR audio input')
const captureOnly = audioInput?.type === 'capture'
return (art: Artplayer): AsrResult => {
const layer = art.layers.add({ name: 'asr', html: '' })
if (!layer)
throw new Error('Could not create ASR subtitle layer')
const subtitles = createSubtitles(layer, length, autoHideTimeout)
const capture = new Capture(art.video, { interval, sampleRate, onAudioChunk }, subtitles.append)
const capture = new Capture(art.video, { interval, sampleRate, onAudioChunk, captureOnly }, subtitles.append)
const play = () => capture.start()
const pause = () => capture.pause()
const restart = () => capture.restart()
@@ -4,6 +4,7 @@ export interface AudioChunk {
}
export interface AsrOptions {
audioInput?: { type: 'capture' }
length?: number
interval?: number
sampleRate?: number
@@ -24,6 +25,7 @@ export interface CaptureVideo extends HTMLVideoElement {
}
export interface CaptureOptions {
captureOnly?: boolean
interval: number
sampleRate: number
onAudioChunk: (chunk: AudioChunk) => unknown