diff --git a/package.json b/package.json
index 4d5d93099..570e5936b 100644
--- a/package.json
+++ b/package.json
@@ -51,7 +51,7 @@
"ci:build": "yarn build:types && yarn build all && yarn build:i18n && yarn build:ts && yarn build:docs && yarn test:imports",
"test:imports": "node --test test/esm.test.js test/i18n.test.js test/ssr.test.js",
"typecheck": "node scripts/typecheck.mjs",
- "test:unit": "node --test test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
+ "test:unit": "node --test test/asr-audio.test.js test/asr.test.js test/jassub.test.js test/multiple-subtitles-merge.test.js test/multiple-subtitles-lifecycle.test.js test/multiple-subtitles-failures.test.js test/multiple-subtitles.test.js test/multiple-subtitles-vendor.test.js test/vtt-thumbnail-parser.test.js test/vtt-thumbnail-lifecycle.test.js test/vtt-thumbnail.test.js test/auto-thumbnail-frames.test.js test/auto-thumbnail-lifecycle.test.js test/auto-thumbnail.test.js test/thumbnail-emitter.test.js test/thumbnail-runtime.test.js test/thumbnail-vendor.test.js test/thumbnail-lifecycle.test.js test/thumbnail-input.test.js test/thumbnail.test.js test/iframe-navigation.test.js test/iframe-boundaries.test.js test/iframe-lifecycle.test.js test/iframe.test.js test/mediabunny.test.js test/mediabunny-shim.test.js test/mediabunny-coordination.test.js test/mediabunny-video.test.js test/mediabunny-audio.test.js test/mediabunny-hls.test.js test/mediabunny-entry.test.js test/mediabunny-capability.test.js test/mediabunny-load.test.js test/mediabunny-input.test.js test/dpip.test.js test/dpip-lifecycle.test.js test/canvas.test.js test/canvas-lifecycle.test.js test/ambilight.test.js test/ambilight-lifecycle.test.js test/ambilight-proxy.test.js test/vast.test.js test/vast-lifecycle.test.js test/ads.test.js test/ads-lifecycle.test.js test/playback.test.js test/dash-control.test.js test/dash-contract.test.js test/dash-lifecycle.test.js test/dash-events.test.js test/hls-control.test.js test/audio-track.test.js test/public-behavior.test.js test/helpers.test.js test/chapter.test.js test/utils.test.js test/resource-scope.test.js test/instance-lifecycle.test.js test/options.test.js test/media-hosts.test.js test/plugins.test.js test/source.test.js test/playback-properties.test.js test/media-events.test.js test/template-resources.test.js test/core-vendor.test.js test/component-resources.test.js test/setting-model.test.js test/setting-layout.test.js test/setting-resources.test.js test/subtitle.test.js test/display-native.test.js test/display-video-fullscreen.test.js test/display-pip.test.js test/display-mini.test.js test/display-sizing.test.js test/display-orientation.test.js test/hotkey.test.js test/listener-registry.test.js test/global-events.test.js test/pointer-events.test.js test/gesture.test.js test/event-scheduling.test.js test/notice.test.js test/fast-forward.test.js test/auto-playback.test.js test/builtin-layers.test.js test/prompt-components.test.js test/screenshot.test.js test/thumbnails.test.js test/progress.test.js test/environment.test.js test/storage.test.js test/facade-properties.test.js test/dom-boundaries.test.js test/initialization.test.js test/entry.test.js test/accessibility-button.test.js test/accessibility-focus.test.js test/accessibility-slider.test.js",
"test:coverage": "node --test test/coverage.test.js && node scripts/coverage.mjs",
"test": "yarn test:node && yarn test:baseline",
"test:browser": "playwright test",
@@ -94,7 +94,7 @@
"check:ci": "node refactor/scripts/ci-workflow.mjs",
"test:ci": "node --test test/ci-summary.test.js refactor/scripts/ci-workflow.test.mjs refactor/scripts/impact.test.mjs test/package-runtime.test.js",
"test:package:runtime": "node scripts/package-runtime.mjs --canonical",
- "test:asr": "node --test test/asr.test.js"
+ "test:asr": "node --test test/asr.test.js test/asr-audio.test.js"
},
"browserslist": "last 1 Chrome version",
"devDependencies": {
diff --git a/refactor/baselines/asr-audio-validation.json b/refactor/baselines/asr-audio-validation.json
new file mode 100644
index 000000000..19ac8c537
--- /dev/null
+++ b/refactor/baselines/asr-audio-validation.json
@@ -0,0 +1,551 @@
+{
+ "schemaVersion": 1,
+ "task": "PKG-ASR-02",
+ "baseCommit": "a22586a172a4f061205c514267473189c6ce759d",
+ "capturedAt": "2026-09-13T06:08:57.980Z",
+ "unit": {
+ "historicalImplementations": 7,
+ "audioScenariosPerImplementation": 19,
+ "audioPassed": 133,
+ "publicPassed": 28,
+ "provenancePassed": 4,
+ "command": "node --test test/asr.test.js test/asr-audio.test.js refactor/scripts/asr-contract.test.mjs"
+ },
+ "browser": {
+ "passed": 8,
+ "skipped": 4,
+ "failed": 0,
+ "rows": [
+ {
+ "title": "ASR 2.0.0 + published: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "chromium",
+ "status": "passed",
+ "browser": "153.0.8010.12",
+ "inputs": {
+ "version": "2.0.0",
+ "core": "published",
+ "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6",
+ "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 852,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 849,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.0.0 + candidate: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "chromium",
+ "status": "passed",
+ "browser": "153.0.8010.12",
+ "inputs": {
+ "version": "2.0.0",
+ "core": "candidate",
+ "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6",
+ "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 837,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 849,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 850,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.1.0 + published: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "chromium",
+ "status": "passed",
+ "browser": "153.0.8010.12",
+ "inputs": {
+ "version": "2.1.0",
+ "core": "published",
+ "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
+ "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 850,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.1.0 + candidate: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "chromium",
+ "status": "passed",
+ "browser": "153.0.8010.12",
+ "inputs": {
+ "version": "2.1.0",
+ "core": "candidate",
+ "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
+ "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 860,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 850,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 852,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.0.0 + published: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "firefox",
+ "status": "passed",
+ "browser": "155.0",
+ "inputs": {
+ "version": "2.0.0",
+ "core": "published",
+ "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6",
+ "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 899,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 852,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 838,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 849,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.0.0 + candidate: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "firefox",
+ "status": "passed",
+ "browser": "155.0",
+ "inputs": {
+ "version": "2.0.0",
+ "core": "candidate",
+ "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6",
+ "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 0,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 852,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 849,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.1.0 + published: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "firefox",
+ "status": "passed",
+ "browser": "155.0",
+ "inputs": {
+ "version": "2.1.0",
+ "core": "published",
+ "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
+ "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 0,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 840,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 852,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.1.0 + candidate: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "firefox",
+ "status": "passed",
+ "browser": "155.0",
+ "inputs": {
+ "version": "2.1.0",
+ "core": "candidate",
+ "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
+ "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "function",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "function"
+ },
+ "chunks": [
+ {
+ "samples": 1600,
+ "max": 0,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 852,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ },
+ {
+ "samples": 1600,
+ "max": 851,
+ "wavBytes": 3244,
+ "sampleRate": 16000
+ }
+ ],
+ "annotations": []
+ },
+ {
+ "title": "ASR 2.0.0 + published: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "webkit",
+ "status": "skipped",
+ "browser": "26.6",
+ "inputs": {
+ "version": "2.0.0",
+ "core": "published",
+ "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6",
+ "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "undefined",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "undefined"
+ },
+ "chunks": [],
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Windows WebKit 26.6 has no AudioContext/AudioWorkletNode; native ASR acceptance remains open",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-audio.spec.js",
+ "line": 27,
+ "column": 14
+ }
+ }
+ ]
+ },
+ {
+ "title": "ASR 2.0.0 + candidate: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "webkit",
+ "status": "skipped",
+ "browser": "26.6",
+ "inputs": {
+ "version": "2.0.0",
+ "core": "candidate",
+ "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6",
+ "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "undefined",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "undefined"
+ },
+ "chunks": [],
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Windows WebKit 26.6 has no AudioContext/AudioWorkletNode; native ASR acceptance remains open",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-audio.spec.js",
+ "line": 27,
+ "column": 14
+ }
+ }
+ ]
+ },
+ {
+ "title": "ASR 2.1.0 + published: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "webkit",
+ "status": "skipped",
+ "browser": "26.6",
+ "inputs": {
+ "version": "2.1.0",
+ "core": "published",
+ "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
+ "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "undefined",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "undefined"
+ },
+ "chunks": [],
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Windows WebKit 26.6 has no AudioContext/AudioWorkletNode; native ASR acceptance remains open",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-audio.spec.js",
+ "line": 27,
+ "column": 14
+ }
+ }
+ ]
+ },
+ {
+ "title": "ASR 2.1.0 + candidate: native Worklet supplies local PCM and stop closes AudioContext",
+ "project": "webkit",
+ "status": "skipped",
+ "browser": "26.6",
+ "inputs": {
+ "version": "2.1.0",
+ "core": "candidate",
+ "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d",
+ "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==",
+ "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56",
+ "media": "local mono AAC 440 Hz tone",
+ "networkService": false
+ },
+ "capabilities": {
+ "AudioContext": "undefined",
+ "webkitAudioContext": "undefined",
+ "AudioWorkletNode": "undefined"
+ },
+ "chunks": [],
+ "annotations": [
+ {
+ "type": "skip",
+ "description": "Windows WebKit 26.6 has no AudioContext/AudioWorkletNode; native ASR acceptance remains open",
+ "location": {
+ "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-audio.spec.js",
+ "line": 27,
+ "column": 14
+ }
+ }
+ ]
+ }
+ ],
+ "reportPath": "refactor/.cache/asr02-browser-final/report.json",
+ "reportSha256": "e6c6de70406c39a5d24c55505aa77837054c681c613baebcc30d38fcb9fce993",
+ "firstRun": {
+ "passed": 8,
+ "failed": 4,
+ "reason": "Fixture extended an unavailable AudioContext constructor; independent native probe found all three WebAudio globals undefined in Windows WebKit 26.6.",
+ "reportPath": "refactor/.cache/asr02-browser-before/report.json"
+ }
+ },
+ "limits": [
+ "Controlled audio is not native audio evidence.",
+ "Windows WebKit has no WebAudio; Safari/device acceptance remains pending.",
+ "Native tests cover initial capture and stop only; candidate resource fixes need new regressions.",
+ "No external ASR service or microphone request."
+ ]
+}
diff --git a/refactor/changes/2026-09-13-PKG-ASR-02-audio-baseline.md b/refactor/changes/2026-09-13-PKG-ASR-02-audio-baseline.md
new file mode 100644
index 000000000..594f866b7
--- /dev/null
+++ b/refactor/changes/2026-09-13-PKG-ASR-02-audio-baseline.md
@@ -0,0 +1,45 @@
+# PKG-ASR-02:ASR 音频与错误基线
+
+在 ASR-01 固定的 7 个实现上运行 19 类场景:npm 2.0.0/2.1.0 main/legacy、
+冻结工作区源码/main/legacy。133 项受控音频测试通过;这些测试明确观察历史
+缺陷,不能接到候选上要求保留缺陷。32 项既有公开契约仍通过。
+
+## 可重跑证据
+
+- `yarn test:asr` 运行公开契约及音频历史场景,161 项;归档校验另有 4 项。
+- `yarn test:browser test/browser/asr-audio.spec.js` 使用实际发布包及本地 AAC 样本,
+ 两版插件乘新旧核心在 Chromium/Firefox 共 8 项通过;Windows WebKit 26.6 的
+ AudioContext、webkitAudioContext、AudioWorkletNode 均不存在,4 项明确跳过。
+- 初次浏览器执行因假设 AudioContext 可用而在 WebKit 失败;独立原生探针确认
+ 能力缺失后先记录能力再做精确平台断言,不放宽 Chromium/Firefox 要求。
+- 每个真正采集通过的场景至少 3 块非零音频,每块 1600 个样本、3244 字节 WAV、
+ 16000Hz,并渲染本地回调字幕,显式 stop 后实际 AudioContext 为 closed。
+- 报告、引擎版本、输入哈希和限制见 [持久证据](../baselines/asr-audio-validation.json)。
+ 没有调用外部 ASR、没有真实麦克风授权、没有真机 Safari 验收。
+
+## 复现结果与修复职责
+
+| 行为 | 实际观察 | 后续 |
+| --- | --- | --- |
+| PCM/WAV | 钳位、PCM16LE、mono WAV 头、首通道、定制采样字节精确匹配 | 保持编码契约 |
+| 队列 | 两次半块输入全部丢弃;超出阈值的尾部也丢弃 | ASR-CHUNK-01 / ASR-03 |
+| 异步回调 | 并发无背压、乱序旧字幕覆盖新字幕,拒绝透出 async interval | ASR-CHUNK-01 / ASR-03 |
+| 停止 | pause/stop/destroy 后在途结果仍 append 并创建 hide timer | ASR-LIFE-01 / ASR-03 |
+| 恢复 | 已有 direct source 未复用,改走捕获流;无 captureStream 时恢复失败 | ASR-LIFE-01 / ASR-03 |
+| 初始化 | 并发 play 产生两图两 interval,stop 只清最后一组;pause 不取消在途 setup | ASR-LIFE-01 / ASR-03 |
+| 异常 | addModule 拒绝留下 context/source/URL;显式 stop 仍未回收 URL | ASR-LIFE-01 / ASR-03 |
+| 终止竞态 | destroy 后 addModule 完成仍继续访问已清空 context | ASR-LIFE-01 / ASR-03 |
+| 切源 | restart 不清旧 PCM、不使旧识别结果失效 | ASR-LIFE-01 / ASR-03 |
+| 增益 | 初始 gain=1,首次 volumechange 才跟随 art.volume | 原生音量对照后修复,避免重复衰减 |
+
+受控环境执行实际 inline Worklet 源码;显式 Promise gate 控制初始化和识别顺序。
+AudioContext/节点/捕获流由测试替代,无法证明音量、设备采样率、CORS 或音源恢复
+在所有真实引擎中正常。实际浏览器目前只证明初次采集、字幕与 stop,恢复/切源/
+并发修复后还需原生回归。ASR-05 保持未完成。
+
+## 协作、兼容与回退
+
+子代理独立编写受控 helper/tests,主代理审查并接入任务、浏览器与报告。没有生产
+源码、类型、版本或依赖改变。候选修复将增加独立断言,历史缺陷不会被删掉或改绿。
+只运行本包测试、定向 ESLint 和文档台账检查,未重复全量 CI。
+独立提交回退可移除新增测试、证据和命令接入,不改变播放器行为。
diff --git a/refactor/plan.md b/refactor/plan.md
index 1b5d43e89..9efe68b5b 100644
--- a/refactor/plan.md
+++ b/refactor/plan.md
@@ -4,7 +4,7 @@
基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 225 项,范围 22 个包及工作区/示例。
-状态:todo 85 / doing 16 / blocked 0 / done 124 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
+状态:todo 84 / doing 16 / blocked 0 / done 125 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。
前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。
@@ -252,7 +252,7 @@
| ID | 范围 / 步骤 | 前置依赖 | 交付物 | 验收条件 | 风险 | 状态 |
| --- | --- | --- | --- | --- | --- | --- |
| PKG-ASR-01 | artplayer-plugin-asr
核对包契约与历史用法 | BASE-05 | length/interval/sampleRate/onAudioChunk、append/hide/stop 的真实能力 | 源码/声明/README/demo/发布包差异已登记;公开形状和版本范围冻结 | H | done |
-| PKG-ASR-02 | artplayer-plugin-asr
建立特有行为与错误测试 | PKG-ASR-01, ENG-03, ENG-05 | PCM/WAV、chunk 时序、回调慢/拒绝、重复初始化与停止 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | todo |
+| PKG-ASR-02 | artplayer-plugin-asr
建立特有行为与错误测试 | PKG-ASR-01, ENG-03, ENG-05 | PCM/WAV、chunk 时序、回调慢/拒绝、重复初始化与停止 | 旧版本行为可重跑,成功/失败/切源/销毁有必要断言 | H | done |
| PKG-ASR-03 | artplayer-plugin-asr
整理内部职责与资源 | PKG-ASR-02, CORE-10, CORE-18 | AudioContext/Worklet/Stream 生命周期和背压分离 | 结构变化和缺陷修复分开记录;原 API/事件/资源生命周期通过 | H | todo |
| PKG-ASR-04 | artplayer-plugin-asr
迁移自有源码和公开类型 | PKG-ASR-03, ENG-04, ENG-06, CORE-07 | 主线程/Worklet/音频 buffer 与异步回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | todo |
| PKG-ASR-05 | artplayer-plugin-asr
验证新旧核心和组合 | PKG-ASR-04, CORE-22 | 真实 WebAudio 分块、播放暂停/切源/销毁,不引入网络 ASR | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo |
@@ -527,6 +527,7 @@
- PKG-MULTI-SUB-07: [记录](baselines/multiple-subtitles-timestamps.json) [记录](changes/2026-09-13-PKG-MULTI-SUB-07-timestamps.md)
- PKG-JASSUB-01: [记录](baselines/jassub-release.json) [记录](baselines/jassub-vendor.json) [记录](baselines/jassub-font-metadata.json) [记录](baselines/jassub-contract.md) [记录](baselines/jassub-contract-validation.json) [记录](changes/2026-09-13-PKG-JASSUB-01-baseline.md)
- PKG-ASR-01: [记录](changes/2026-09-13-PKG-ASR-01-contracts.md) [记录](baselines/asr-contract.md) [记录](baselines/asr-release.json)
+- PKG-ASR-02: [记录](changes/2026-09-13-PKG-ASR-02-audio-baseline.md) [记录](baselines/asr-audio-validation.json)
- PKG-ADS-01: [记录](baselines/ads-release.json) [记录](baselines/ads-contract.md) [记录](baselines/ads-contract-validation.json) [记录](changes/2026-09-12-PKG-ADS-01-contract.md) [记录](scripts/ads-contract.test.mjs)
- PKG-ADS-02: [记录](changes/2026-09-12-PKG-ADS-02-tests.md) [记录](ads-validation.md) [记录](baselines/ads-validation.json)
- PKG-ADS-03: [记录](changes/2026-09-12-PKG-ADS-03-lifecycle.md) [记录](ads-validation.md) [记录](baselines/ads-lifecycle-validation.json)
diff --git a/refactor/progress.md b/refactor/progress.md
index 6902a8e59..6d2f2d278 100644
--- a/refactor/progress.md
+++ b/refactor/progress.md
@@ -1,5 +1,16 @@
# 进度与证据
+## PKG-ASR-02 音频与错误基线完成
+
+子代理交付并由主代理审查的 133 项音频历史测试通过,连同公开契约/来源共 165 项。
+复现丢样本、异步乱序、停止后字幕回写、初始化竞态、URL/节点残留和切源旧数据。
+真实 Chromium/Firefox 共 8 组合通过;Windows WebKit 26.6 原生 WebAudio 全局缺失,
+4 项明确跳过,未计入采集通过;首次失败及能力诊断保留。未请求外部识别服务。
+见[变更](changes/2026-09-13-PKG-ASR-02-audio-baseline.md)和[证据](baselines/asr-audio-validation.json)。
+225 项:125 done、16 doing、84 todo;205 风险,音频队列/生命周期风险升级为已复现。
+下一步 ASR-03 拆分生产源码并逐项修复,历史测试保留,候选正确行为另建回归。
+
+
## PKG-ASR-01 发布契约完成
冻结实际 npm 2.0.0/2.1.0 共 12 成员及工作区 9 个 Git 输入;32 项契约测试通过,
diff --git a/refactor/risk-table.md b/refactor/risk-table.md
index fa1ed810d..51d9d0679 100644
--- a/refactor/risk-table.md
+++ b/refactor/risk-table.md
@@ -207,5 +207,5 @@
| CORE-DEFAULTS-SSR-01 | resolved / 已复现 | Reading static defaults throws when navigator is absent | CORE-25 |
| ENG-DIST-PUBLIC-01 | resolved / 已复现 | Vite copied authored declarations into library distribution | ENG-12 |
| ASR-TYPE-01 | open / 源码/产物事实 | ASR declarations omit asynchronous stop and callback text results | PKG-ASR-04 |
-| ASR-CHUNK-01 | open / 源码/产物事实 | ASR tick discards underfilled chunks and overrun samples in source | PKG-ASR-02, PKG-ASR-03 |
-| ASR-LIFE-01 | open / 源码/产物事实 | ASR partial initialization, pause resume and stale callback cleanup need controlled evidence | PKG-ASR-02, PKG-ASR-03, PKG-ASR-05 |
+| ASR-CHUNK-01 | open / 已复现 | ASR tick discards underfilled chunks and overrun samples in source | PKG-ASR-02, PKG-ASR-03 |
+| ASR-LIFE-01 | open / 已复现 | ASR partial initialization, pause resume and stale callback cleanup need controlled evidence | PKG-ASR-02, PKG-ASR-03, PKG-ASR-05 |
diff --git a/refactor/risks.json b/refactor/risks.json
index 2dcb0290d..b450d3ec4 100644
--- a/refactor/risks.json
+++ b/refactor/risks.json
@@ -4560,7 +4560,7 @@
{
"id": "ASR-CHUNK-01",
"title": "ASR tick discards underfilled chunks and overrun samples in source",
- "confirmation": "source-observed",
+ "confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-ASR-02",
@@ -4568,7 +4568,10 @@
],
"evidence": [
"refactor/baselines/asr-contract.md",
- "refactor/baselines/asr-release.json"
+ "refactor/baselines/asr-release.json",
+ "test/asr-audio.test.js",
+ "refactor/changes/2026-09-13-PKG-ASR-02-audio-baseline.md",
+ "refactor/baselines/asr-audio-validation.json"
],
"compatibleResolution": "Reproduce exact sample loss and retain queue remainders with bounded, ordered callback delivery.",
"closureCriteria": "Exact PCM/WAV byte sequence and slow/rejected callback regression tests pass without losing or reordering accepted input."
@@ -4576,7 +4579,7 @@
{
"id": "ASR-LIFE-01",
"title": "ASR partial initialization, pause resume and stale callback cleanup need controlled evidence",
- "confirmation": "source-observed",
+ "confirmation": "reproduced",
"status": "open",
"owners": [
"PKG-ASR-02",
@@ -4585,7 +4588,10 @@
],
"evidence": [
"refactor/baselines/asr-contract.md",
- "refactor/baselines/asr-release.json"
+ "refactor/baselines/asr-release.json",
+ "test/asr-audio.test.js",
+ "refactor/changes/2026-09-13-PKG-ASR-02-audio-baseline.md",
+ "refactor/baselines/asr-audio-validation.json"
],
"compatibleResolution": "Own pending setup, URLs, source nodes and callbacks across pause, stop, restart and terminal destruction.",
"closureCriteria": "Failure/race regression tests plus native WebAudio playback and cleanup evidence prove resource ownership."
diff --git a/refactor/tasks.json b/refactor/tasks.json
index 006f6d3bc..b468ecf9e 100644
--- a/refactor/tasks.json
+++ b/refactor/tasks.json
@@ -2639,11 +2639,14 @@
"ENG-03",
"ENG-05"
],
- "status": "todo",
+ "status": "done",
"risk": "H",
"deliverable": "PCM/WAV、chunk 时序、回调慢/拒绝、重复初始化与停止",
"acceptance": "旧版本行为可重跑,成功/失败/切源/销毁有必要断言",
- "evidence": []
+ "evidence": [
+ "changes/2026-09-13-PKG-ASR-02-audio-baseline.md",
+ "baselines/asr-audio-validation.json"
+ ]
},
{
"id": "PKG-ASR-03",
diff --git a/test/asr-audio.test.js b/test/asr-audio.test.js
new file mode 100644
index 000000000..93592cff0
--- /dev/null
+++ b/test/asr-audio.test.js
@@ -0,0 +1,319 @@
+import assert from 'node:assert/strict'
+import { Buffer } from 'node:buffer'
+// eslint-disable-next-line test/no-import-node-test -- Historical ASR audio uses the repository runner.
+import test from 'node:test'
+import { asrAudioEnvironment, deferredAudio, runAsrWorklet } from './helpers/asr-audio.js'
+import { asrHistorical } from './helpers/asr.js'
+
+// These fixtures are immutable historical implementations, never the candidate.
+// Defect observations must become separate red/green candidate regressions when fixed.
+const implementations = await asrHistorical()
+
+function samples(pcm) {
+ const view = new DataView(pcm)
+ return Array.from({ length: pcm.byteLength / 2 }, (_, index) => view.getInt16(index * 2, true))
+}
+
+for (const implementation of implementations) {
+ const label = `ASR audio history ${implementation.name}`
+
+ test(`${label}: default PCM clamping and mono WAV bytes`, async () => {
+ const chunks = []
+ const environment = asrAudioEnvironment(implementation, { onAudioChunk: chunk => chunks.push(chunk) })
+ await environment.emit('play')
+ assert.equal(environment.contexts[0].options.sampleRate, 16000)
+ assert.equal([...environment.intervals.values()][0].delay, 100)
+ const input = new Float32Array(1600)
+ input.set([-2, -1, -0.5, 0, 0.5, 1, 2, Number.NaN])
+ environment.send(input)
+ await environment.tick()
+ assert.equal(chunks.length, 1)
+ assert.deepEqual(Object.keys(chunks[0]), ['pcm', 'wav'])
+ assert.equal(chunks[0].pcm.byteLength, 3200)
+ assert.deepEqual(samples(chunks[0].pcm).slice(0, 8), [-32768, -32768, -16384, 0, 16383, 32767, 32767, 0])
+ const wav = Buffer.from(chunks[0].wav)
+ assert.equal(wav.length, 3244)
+ assert.equal(wav.toString('ascii', 0, 4), 'RIFF')
+ assert.equal(wav.readUInt32LE(4), 3236)
+ assert.equal(wav.toString('ascii', 8, 16), 'WAVEfmt ')
+ assert.equal(wav.readUInt32LE(16), 16)
+ assert.equal(wav.readUInt16LE(20), 1)
+ assert.equal(wav.readUInt16LE(22), 1)
+ assert.equal(wav.readUInt32LE(24), 16000)
+ assert.equal(wav.readUInt32LE(28), 32000)
+ assert.equal(wav.readUInt16LE(32), 2)
+ assert.equal(wav.readUInt16LE(34), 16)
+ assert.equal(wav.toString('ascii', 36, 40), 'data')
+ assert.equal(wav.readUInt32LE(40), 3200)
+ assert.deepEqual(wav.subarray(44), Buffer.from(chunks[0].pcm))
+ await environment.plugin.stop()
+ })
+
+ test(`${label}: custom sampling concatenates worklet messages in order`, async () => {
+ const chunks = []
+ const environment = asrAudioEnvironment(implementation, { sampleRate: 8000, interval: 1, onAudioChunk: chunk => chunks.push(chunk) })
+ await environment.emit('play')
+ environment.send([-1, -0.5, 0])
+ environment.send([0.5, 1, 0, -1, 1])
+ await environment.tick()
+ assert.equal(chunks.length, 1)
+ assert.deepEqual(samples(chunks[0].pcm), [-32768, -16384, 0, 16383, 32767, 0, -32768, 32767])
+ assert.equal(new DataView(chunks[0].wav).getUint32(24, true), 8000)
+ assert.equal([...environment.intervals.values()][0].delay, 1)
+ await environment.plugin.stop()
+ })
+
+ test(`${label}: actual worklet emits first channel and survives empty input`, async () => {
+ const environment = asrAudioEnvironment(implementation)
+ await environment.emit('play')
+ const { processor, messages } = await runAsrWorklet(environment.modules[0].blob)
+ const left = Float32Array.of(0.1, 0.2)
+ const right = Float32Array.of(0.9, 0.8)
+ assert.equal(processor.process([]), true)
+ assert.equal(processor.process([[]]), true)
+ assert.equal(processor.process([[left, right]]), true)
+ assert.equal(messages.length, 1)
+ assert.equal(messages[0], left)
+ assert.equal(environment.blobs.size, 0)
+ assert.equal(environment.revoked.length, 1)
+ await environment.plugin.stop()
+ })
+
+ test(`${label}: historical defect drops sub-threshold audio each tick and excess samples`, async () => {
+ const chunks = []
+ const environment = asrAudioEnvironment(implementation, { sampleRate: 8000, interval: 1, onAudioChunk: chunk => chunks.push(chunk) })
+ await environment.emit('play')
+ for (let index = 0; index < 2; index++) {
+ environment.send([1, 1, 1, 1])
+ await environment.tick()
+ }
+ assert.equal(chunks.length, 0, 'Eight samples across two ticks were lost')
+ environment.send(new Float32Array(12).fill(0.5))
+ await environment.tick()
+ environment.send(new Float32Array(4).fill(-0.5))
+ await environment.tick()
+ assert.equal(chunks.length, 1, 'Four excess samples were not retained for the next four')
+ assert.deepEqual(samples(chunks[0].pcm), Array.from({ length: 8 }, () => 16383))
+ await environment.plugin.stop()
+ })
+
+ test(`${label}: historical defect overlaps slow callbacks and paints stale completion`, async () => {
+ const pending = []
+ const environment = asrAudioEnvironment(implementation, { sampleRate: 8000, interval: 1, onAudioChunk: () => {
+ const call = deferredAudio()
+ pending.push(call)
+ return call.promise
+ } })
+ await environment.emit('play')
+ environment.send(new Float32Array(8))
+ const first = environment.tick()
+ environment.send(new Float32Array(8))
+ const second = environment.tick()
+ assert.equal(pending.length, 2, 'No backpressure while the first callback is pending')
+ pending[1].resolve('New result.')
+ await second
+ assert.equal(environment.layer.innerHTML, '