From 6d7b9292f867f3c464702eda985c800be7139b4f Mon Sep 17 00:00:00 2001 From: Harvey Zhao Date: Sun, 13 Sep 2026 15:40:27 +0800 Subject: [PATCH] fix(asr): [PKG-ASR-08] keep captured fallback off the audible route --- docs/compiled/artplayer-plugin-asr.js | 2 +- docs/compiled/artplayer-plugin-asr.legacy.js | 2 +- docs/compiled/artplayer-plugin-asr.mjs | 6 +- package.json | 4 +- packages/artplayer-plugin-asr/ARCHITECTURE.md | 21 +- .../dist/artplayer-plugin-asr.js | 2 +- .../dist/artplayer-plugin-asr.legacy.js | 2 +- .../dist/artplayer-plugin-asr.mjs | 6 +- .../artplayer-plugin-asr/src/audio-graph.ts | 6 +- .../baselines/asr-fallback-validation.json | 4631 +++++++++++++++++ .../2026-09-13-PKG-ASR-08-capture-output.md | 43 + refactor/plan.md | 11 +- refactor/progress.md | 15 + refactor/risk-table.md | 2 + refactor/risks.json | 35 + refactor/tasks.json | 29 +- test/asr-fallback-routing.test.js | 130 + test/browser/asr-fallback.spec.js | 173 + test/helpers/asr-audio.js | 9 +- 19 files changed, 5104 insertions(+), 25 deletions(-) create mode 100644 refactor/baselines/asr-fallback-validation.json create mode 100644 refactor/changes/2026-09-13-PKG-ASR-08-capture-output.md create mode 100644 test/asr-fallback-routing.test.js create mode 100644 test/browser/asr-fallback.spec.js diff --git a/docs/compiled/artplayer-plugin-asr.js b/docs/compiled/artplayer-plugin-asr.js index 1aa5c71ce..fa6df2967 100644 --- a/docs/compiled/artplayer-plugin-asr.js +++ b/docs/compiled/artplayer-plugin-asr.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";class t{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}async initialize(){this.assertOpen();const t=window,e=window.AudioContext||t.webkitAudioContext;if(!e)throw new Error("AudioContext is not supported");const i=this.context??(this.context=new e({sampleRate:this.sampleRate}));if("suspended"===i.state&&await i.resume(),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{await i.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=i.createMediaElementSource(this.video),this.direct=!0}catch(s){console.warn("[artplayerPluginAsr] Direct connection failed:",s);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=i.createMediaStreamSource(this.stream)}}prepare(){return this.preparation??(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const r=this.gain=s||e.createGain();s||(r.gain.value=1);const n=this.recorder=new AudioWorkletNode(e,"recorder-processor");n.port.onmessage=e=>{this.closed||this.recorder!==n||t(e.data)},i.connect(n),i.connect(r),r.connect(e.destination)}disconnect(){this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),this.gain?.disconnect(),this.gain=void 0,this.source?.disconnect()}async close(){if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),this.stream?.getTracks().forEach(t=>t.stop()),this.stream=void 0;const t=this.context;this.context=void 0,this.source=void 0,t&&"closed"!==t.state&&await t.close()}}class e{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const e=this.epoch,i=()=>!this.terminal&&this.epoch===e,s=(async()=>{if(await this.closing,!i())return;const{sampleRate:e,interval:s}=this.options,r=Math.floor(e*s/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(s)||s<=0)throw new Error("Audio chunk length must be positive and finite");const n=this.graph??(this.graph=new t(this.video,e));await n.prepare(),i()?(n.connect(t=>{if(this.running&&this.graph===n)return this.queue.length+t.length>Math.max(60*e,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),s)):this.running||this.graph!==n||n.idle()})().catch(async t=>{i()&&(console.error("[artplayerPluginAsr] Initialization failed:",t),await this.stop())}).finally(()=>{this.starting===s&&(this.starting=void 0)});return this.starting=s,s}async tick(t){if(!this.running||this.pending)return;const e=this.queue.take(t);if(!e)return;const i=this.epoch,s=this.pending={};try{const t=await this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function s(t={}){const{length:e=3,interval:s=100,sampleRate:r=16e3,autoHideTimeout:n=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,r=!1;function n(){clearTimeout(s),s=void 0}function o(){r||(t.style.display="none")}return{append:function(a){r||"string"!==typeof a||(n(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(/(?<=[\u3001\u3002\uff01\uff1f!?.])\s*/u).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){r=!0,n()}}}(a,e,n),c=new i(t.video,{interval:s,sampleRate:r,onAudioChunk:o},h.append),l=()=>c.start(),d=()=>c.pause(),p=()=>c.restart(),u=()=>(h.destroy(),t.off("play",l),t.off("pause",d),t.off("restart",p),t.off("destroy",u),c.stop(!0));return t.on("play",l),t.on("pause",d),t.on("restart",p),t.on("destroy",u),{name:"artplayerPluginAsr",stop:()=>c.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(s,"default",{value:s}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return s}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";class t{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}async initialize(){this.assertOpen();const t=window,e=window.AudioContext||t.webkitAudioContext;if(!e)throw new Error("AudioContext is not supported");const i=this.context??(this.context=new e({sampleRate:this.sampleRate}));if("suspended"===i.state&&await i.resume(),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{await i.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=i.createMediaElementSource(this.video),this.direct=!0}catch(s){console.warn("[artplayerPluginAsr] Direct connection failed:",s);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=i.createMediaStreamSource(this.stream)}}prepare(){return this.preparation??(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const r=this.gain=s||e.createGain();s||(r.gain.value=this.direct?1:0);const n=this.recorder=new AudioWorkletNode(e,"recorder-processor");n.port.onmessage=e=>{this.closed||this.recorder!==n||t(e.data)},i.connect(n),this.direct?i.connect(r):n.connect(r),r.connect(e.destination)}disconnect(){this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),this.gain?.disconnect(),this.gain=void 0,this.source?.disconnect()}async close(){if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),this.stream?.getTracks().forEach(t=>t.stop()),this.stream=void 0;const t=this.context;this.context=void 0,this.source=void 0,t&&"closed"!==t.state&&await t.close()}}class e{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const e=this.epoch,i=()=>!this.terminal&&this.epoch===e,s=(async()=>{if(await this.closing,!i())return;const{sampleRate:e,interval:s}=this.options,r=Math.floor(e*s/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(s)||s<=0)throw new Error("Audio chunk length must be positive and finite");const n=this.graph??(this.graph=new t(this.video,e));await n.prepare(),i()?(n.connect(t=>{if(this.running&&this.graph===n)return this.queue.length+t.length>Math.max(60*e,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),s)):this.running||this.graph!==n||n.idle()})().catch(async t=>{i()&&(console.error("[artplayerPluginAsr] Initialization failed:",t),await this.stop())}).finally(()=>{this.starting===s&&(this.starting=void 0)});return this.starting=s,s}async tick(t){if(!this.running||this.pending)return;const e=this.queue.take(t);if(!e)return;const i=this.epoch,s=this.pending={};try{const t=await this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function s(t={}){const{length:e=3,interval:s=100,sampleRate:r=16e3,autoHideTimeout:n=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,r=!1;function n(){clearTimeout(s),s=void 0}function o(){r||(t.style.display="none")}return{append:function(a){r||"string"!==typeof a||(n(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(/(?<=[\u3001\u3002\uff01\uff1f!?.])\s*/u).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){r=!0,n()}}}(a,e,n),c=new i(t.video,{interval:s,sampleRate:r,onAudioChunk:o},h.append),l=()=>c.start(),d=()=>c.pause(),p=()=>c.restart(),u=()=>(h.destroy(),t.off("play",l),t.off("pause",d),t.off("restart",p),t.off("destroy",u),c.stop(!0));return t.on("play",l),t.on("pause",d),t.on("restart",p),t.on("destroy",u),{name:"artplayerPluginAsr",stop:()=>c.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(s,"default",{value:s}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return s}); diff --git a/docs/compiled/artplayer-plugin-asr.legacy.js b/docs/compiled/artplayer-plugin-asr.legacy.js index b44f4b3f5..09aaa34ad 100644 --- a/docs/compiled/artplayer-plugin-asr.legacy.js +++ b/docs/compiled/artplayer-plugin-asr.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";var t=(t,e,i)=>new Promise((s,n)=>{var r=t=>{try{a(i.next(t))}catch(e){n(e)}},o=t=>{try{a(i.throw(t))}catch(e){n(e)}},a=t=>t.done?s(t.value):Promise.resolve(t.value).then(r,o);a((i=i.apply(t,e)).next())});class e{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}initialize(){return t(this,null,function*(){var t;this.assertOpen();const e=window,i=window.AudioContext||e.webkitAudioContext;if(!i)throw new Error("AudioContext is not supported");const s=null!=(t=this.context)?t:this.context=new i({sampleRate:this.sampleRate});if("suspended"===s.state&&(yield s.resume()),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{yield s.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=s.createMediaElementSource(this.video),this.direct=!0}catch(n){console.warn("[artplayerPluginAsr] Direct connection failed:",n);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=s.createMediaStreamSource(this.stream)}})}prepare(){return null!=this.preparation||(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const n=this.gain=s||e.createGain();s||(n.gain.value=1);const r=this.recorder=new AudioWorkletNode(e,"recorder-processor");r.port.onmessage=e=>{this.closed||this.recorder!==r||t(e.data)},i.connect(r),i.connect(n),n.connect(e.destination)}disconnect(){var t,e;this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),null==(t=this.gain)||t.disconnect(),this.gain=void 0,null==(e=this.source)||e.disconnect()}close(){return t(this,null,function*(){var t;if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),null==(t=this.stream)||t.getTracks().forEach(t=>t.stop()),this.stream=void 0;const e=this.context;this.context=void 0,this.source=void 0,e&&"closed"!==e.state&&(yield e.close())})}}class i{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const i=this.epoch,s=()=>!this.terminal&&this.epoch===i,n=(()=>t(this,null,function*(){var t;if(yield this.closing,!s())return;const{sampleRate:i,interval:n}=this.options,r=Math.floor(i*n/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(n)||n<=0)throw new Error("Audio chunk length must be positive and finite");const o=null!=(t=this.graph)?t:this.graph=new e(this.video,i);yield o.prepare(),s()?(o.connect(t=>{if(this.running&&this.graph===o)return this.queue.length+t.length>Math.max(60*i,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),n)):this.running||this.graph!==o||o.idle()}))().catch(e=>t(this,null,function*(){s()&&(console.error("[artplayerPluginAsr] Initialization failed:",e),yield this.stop())})).finally(()=>{this.starting===n&&(this.starting=void 0)});return this.starting=n,n}tick(e){return t(this,null,function*(){if(!this.running||this.pending)return;const t=this.queue.take(e);if(!t)return;const i=this.epoch,s=this.pending={};try{const e=yield this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function n(t={}){const{length:e=3,interval:i=100,sampleRate:n=16e3,autoHideTimeout:r=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,n=!1;function r(){clearTimeout(s),s=void 0}function o(){n||(t.style.display="none")}return{append:function(a){n||"string"!==typeof a||(r(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(new RegExp("(?<=[\u3001\u3002\uff01\uff1f!?.])\\s*","u")).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){n=!0,r()}}}(a,e,r),l=new s(t.video,{interval:i,sampleRate:n,onAudioChunk:o},h.append),c=()=>l.start(),d=()=>l.pause(),u=()=>l.restart(),p=()=>(h.destroy(),t.off("play",c),t.off("pause",d),t.off("restart",u),t.off("destroy",p),l.stop(!0));return t.on("play",c),t.on("pause",d),t.on("restart",u),t.on("destroy",p),{name:"artplayerPluginAsr",stop:()=>l.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(n,"default",{value:n}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return n}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";var t=(t,e,i)=>new Promise((s,n)=>{var r=t=>{try{a(i.next(t))}catch(e){n(e)}},o=t=>{try{a(i.throw(t))}catch(e){n(e)}},a=t=>t.done?s(t.value):Promise.resolve(t.value).then(r,o);a((i=i.apply(t,e)).next())});class e{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}initialize(){return t(this,null,function*(){var t;this.assertOpen();const e=window,i=window.AudioContext||e.webkitAudioContext;if(!i)throw new Error("AudioContext is not supported");const s=null!=(t=this.context)?t:this.context=new i({sampleRate:this.sampleRate});if("suspended"===s.state&&(yield s.resume()),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{yield s.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=s.createMediaElementSource(this.video),this.direct=!0}catch(n){console.warn("[artplayerPluginAsr] Direct connection failed:",n);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=s.createMediaStreamSource(this.stream)}})}prepare(){return null!=this.preparation||(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const n=this.gain=s||e.createGain();s||(n.gain.value=this.direct?1:0);const r=this.recorder=new AudioWorkletNode(e,"recorder-processor");r.port.onmessage=e=>{this.closed||this.recorder!==r||t(e.data)},i.connect(r),this.direct?i.connect(n):r.connect(n),n.connect(e.destination)}disconnect(){var t,e;this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),null==(t=this.gain)||t.disconnect(),this.gain=void 0,null==(e=this.source)||e.disconnect()}close(){return t(this,null,function*(){var t;if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),null==(t=this.stream)||t.getTracks().forEach(t=>t.stop()),this.stream=void 0;const e=this.context;this.context=void 0,this.source=void 0,e&&"closed"!==e.state&&(yield e.close())})}}class i{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const i=this.epoch,s=()=>!this.terminal&&this.epoch===i,n=(()=>t(this,null,function*(){var t;if(yield this.closing,!s())return;const{sampleRate:i,interval:n}=this.options,r=Math.floor(i*n/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(n)||n<=0)throw new Error("Audio chunk length must be positive and finite");const o=null!=(t=this.graph)?t:this.graph=new e(this.video,i);yield o.prepare(),s()?(o.connect(t=>{if(this.running&&this.graph===o)return this.queue.length+t.length>Math.max(60*i,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),n)):this.running||this.graph!==o||o.idle()}))().catch(e=>t(this,null,function*(){s()&&(console.error("[artplayerPluginAsr] Initialization failed:",e),yield this.stop())})).finally(()=>{this.starting===n&&(this.starting=void 0)});return this.starting=n,n}tick(e){return t(this,null,function*(){if(!this.running||this.pending)return;const t=this.queue.take(e);if(!t)return;const i=this.epoch,s=this.pending={};try{const e=yield this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function n(t={}){const{length:e=3,interval:i=100,sampleRate:n=16e3,autoHideTimeout:r=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,n=!1;function r(){clearTimeout(s),s=void 0}function o(){n||(t.style.display="none")}return{append:function(a){n||"string"!==typeof a||(r(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(new RegExp("(?<=[\u3001\u3002\uff01\uff1f!?.])\\s*","u")).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){n=!0,r()}}}(a,e,r),l=new s(t.video,{interval:i,sampleRate:n,onAudioChunk:o},h.append),c=()=>l.start(),d=()=>l.pause(),u=()=>l.restart(),p=()=>(h.destroy(),t.off("play",c),t.off("pause",d),t.off("restart",u),t.off("destroy",p),l.stop(!0));return t.on("play",c),t.on("pause",d),t.on("restart",u),t.on("destroy",p),{name:"artplayerPluginAsr",stop:()=>l.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(n,"default",{value:n}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return n}); diff --git a/docs/compiled/artplayer-plugin-asr.mjs b/docs/compiled/artplayer-plugin-asr.mjs index fea4c7923..3fdbb9574 100644 --- a/docs/compiled/artplayer-plugin-asr.mjs +++ b/docs/compiled/artplayer-plugin-asr.mjs @@ -99,14 +99,16 @@ class AudioGraph { this.disconnect(); const gain = this.gain = previousGain || context.createGain(); if (!previousGain) - gain.gain.value = 1; + gain.gain.value = this.direct ? 1 : 0; const recorder = this.recorder = new AudioWorkletNode(context, "recorder-processor"); recorder.port.onmessage = (event) => { if (!this.closed && this.recorder === recorder) receive(event.data); }; source.connect(recorder); - source.connect(gain); + if (this.direct) + source.connect(gain); + else recorder.connect(gain); gain.connect(context.destination); } disconnect() { diff --git a/package.json b/package.json index 1679ae1d7..6e1477bf4 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 test/asr-distribution.test.js", "typecheck": "node scripts/typecheck.mjs", - "test:unit": "node --test test/asr-encoding.test.js test/asr-lifecycle.test.js 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:unit": "node --test test/asr-encoding.test.js test/asr-lifecycle.test.js 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/asr-fallback-routing.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-audio.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-distribution.test.js", + "test:asr": "node --test test/asr.test.js test/asr-audio.test.js test/asr-encoding.test.js test/asr-lifecycle.test.js test/asr-distribution.test.js test/asr-fallback-routing.test.js", "test:asr-types-package": "node refactor/scripts/asr-package-types.mjs" }, "browserslist": "last 1 Chrome version", diff --git a/packages/artplayer-plugin-asr/ARCHITECTURE.md b/packages/artplayer-plugin-asr/ARCHITECTURE.md index 410104869..6651cc2e4 100644 --- a/packages/artplayer-plugin-asr/ARCHITECTURE.md +++ b/packages/artplayer-plugin-asr/ARCHITECTURE.md @@ -34,13 +34,27 @@ continues through its playback gain to the destination. A source switch reuses t direct source and replaces the recorder; a captured-stream fallback is closed and reacquired because captured tracks can end on source changes. -The playback gain stays at unity. Native media-element source output already +The direct playback gain stays at unity. Native media-element source output already reflects the video's volume and mute; applying the same volume again would turn -50% volume into 25% output amplitude. PCM also follows the main video's volume +50% volume into 25% output amplitude. Direct-path PCM also follows the main video's volume and becomes silent when that element is muted. Native analyser tests cover the linear output ratio, mute, stop and resume on Chromium/Firefox; they do not measure physical speakers or establish Safari/device support. +Captured fallback does not own playback. Its source connects only to the recorder, +whose output connects through a zero-gain sink to the destination to keep processing +active. It must never reconnect raw captured audio to a speaker branch, including +after pause/restart. Captured PCM follows captureStream semantics and can remain +nonzero while the video is muted; that must not create audible ASR output. ASR stop +and destroy release only their contexts and captured tracks, leaving an external +owner's media graph intact. + +Chromium naturally rejects a second element-source binding. The observed Firefox +build accepts it and can leave two native owners; that separate integration issue +remains under ASR-05 review. Firefox fallback tests explicitly force only the binding +rejection, while retaining native captureStream, tracks, Worklet and analyser nodes. +They do not establish automatic fallback selection on Firefox. + `stop()` returns `Promise` and releases ASR work. An already displayed subtitle retains its original auto-hide deadline; obsolete callbacks cannot add timers. It retains playback subscriptions and allows explicit append or a later play. @@ -95,6 +109,9 @@ From the repository root: - `yarn test:browser test/browser/asr-playback.spec.js`: frozen 2.1 output attenuation versus candidate linear volume, mute and stop/resume. A native unity analyser measures the gain-to-destination route with local audio. +- `yarn test:browser test/browser/asr-fallback.spec.js`: native capture/Worklet + audio with an external media owner, silent ASR output even while muted, stop, + fresh tracks after restart/source change and independent context cleanup. - `yarn typecheck`: includes the package strict/noUncheckedIndexedAccess project. - `yarn test:asr-types-package`: packs and installs core/ASR outside the workspace, checks frozen 2.0/2.1 consumers and candidate declarations in strict compiler diff --git a/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js b/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js index 1aa5c71ce..fa6df2967 100644 --- a/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js +++ b/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";class t{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}async initialize(){this.assertOpen();const t=window,e=window.AudioContext||t.webkitAudioContext;if(!e)throw new Error("AudioContext is not supported");const i=this.context??(this.context=new e({sampleRate:this.sampleRate}));if("suspended"===i.state&&await i.resume(),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{await i.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=i.createMediaElementSource(this.video),this.direct=!0}catch(s){console.warn("[artplayerPluginAsr] Direct connection failed:",s);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=i.createMediaStreamSource(this.stream)}}prepare(){return this.preparation??(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const r=this.gain=s||e.createGain();s||(r.gain.value=1);const n=this.recorder=new AudioWorkletNode(e,"recorder-processor");n.port.onmessage=e=>{this.closed||this.recorder!==n||t(e.data)},i.connect(n),i.connect(r),r.connect(e.destination)}disconnect(){this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),this.gain?.disconnect(),this.gain=void 0,this.source?.disconnect()}async close(){if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),this.stream?.getTracks().forEach(t=>t.stop()),this.stream=void 0;const t=this.context;this.context=void 0,this.source=void 0,t&&"closed"!==t.state&&await t.close()}}class e{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const e=this.epoch,i=()=>!this.terminal&&this.epoch===e,s=(async()=>{if(await this.closing,!i())return;const{sampleRate:e,interval:s}=this.options,r=Math.floor(e*s/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(s)||s<=0)throw new Error("Audio chunk length must be positive and finite");const n=this.graph??(this.graph=new t(this.video,e));await n.prepare(),i()?(n.connect(t=>{if(this.running&&this.graph===n)return this.queue.length+t.length>Math.max(60*e,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),s)):this.running||this.graph!==n||n.idle()})().catch(async t=>{i()&&(console.error("[artplayerPluginAsr] Initialization failed:",t),await this.stop())}).finally(()=>{this.starting===s&&(this.starting=void 0)});return this.starting=s,s}async tick(t){if(!this.running||this.pending)return;const e=this.queue.take(t);if(!e)return;const i=this.epoch,s=this.pending={};try{const t=await this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function s(t={}){const{length:e=3,interval:s=100,sampleRate:r=16e3,autoHideTimeout:n=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,r=!1;function n(){clearTimeout(s),s=void 0}function o(){r||(t.style.display="none")}return{append:function(a){r||"string"!==typeof a||(n(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(/(?<=[\u3001\u3002\uff01\uff1f!?.])\s*/u).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){r=!0,n()}}}(a,e,n),c=new i(t.video,{interval:s,sampleRate:r,onAudioChunk:o},h.append),l=()=>c.start(),d=()=>c.pause(),p=()=>c.restart(),u=()=>(h.destroy(),t.off("play",l),t.off("pause",d),t.off("restart",p),t.off("destroy",u),c.stop(!0));return t.on("play",l),t.on("pause",d),t.on("restart",p),t.on("destroy",u),{name:"artplayerPluginAsr",stop:()=>c.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(s,"default",{value:s}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return s}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";class t{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}async initialize(){this.assertOpen();const t=window,e=window.AudioContext||t.webkitAudioContext;if(!e)throw new Error("AudioContext is not supported");const i=this.context??(this.context=new e({sampleRate:this.sampleRate}));if("suspended"===i.state&&await i.resume(),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{await i.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=i.createMediaElementSource(this.video),this.direct=!0}catch(s){console.warn("[artplayerPluginAsr] Direct connection failed:",s);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=i.createMediaStreamSource(this.stream)}}prepare(){return this.preparation??(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const r=this.gain=s||e.createGain();s||(r.gain.value=this.direct?1:0);const n=this.recorder=new AudioWorkletNode(e,"recorder-processor");n.port.onmessage=e=>{this.closed||this.recorder!==n||t(e.data)},i.connect(n),this.direct?i.connect(r):n.connect(r),r.connect(e.destination)}disconnect(){this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),this.gain?.disconnect(),this.gain=void 0,this.source?.disconnect()}async close(){if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),this.stream?.getTracks().forEach(t=>t.stop()),this.stream=void 0;const t=this.context;this.context=void 0,this.source=void 0,t&&"closed"!==t.state&&await t.close()}}class e{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const e=this.epoch,i=()=>!this.terminal&&this.epoch===e,s=(async()=>{if(await this.closing,!i())return;const{sampleRate:e,interval:s}=this.options,r=Math.floor(e*s/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(s)||s<=0)throw new Error("Audio chunk length must be positive and finite");const n=this.graph??(this.graph=new t(this.video,e));await n.prepare(),i()?(n.connect(t=>{if(this.running&&this.graph===n)return this.queue.length+t.length>Math.max(60*e,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),s)):this.running||this.graph!==n||n.idle()})().catch(async t=>{i()&&(console.error("[artplayerPluginAsr] Initialization failed:",t),await this.stop())}).finally(()=>{this.starting===s&&(this.starting=void 0)});return this.starting=s,s}async tick(t){if(!this.running||this.pending)return;const e=this.queue.take(t);if(!e)return;const i=this.epoch,s=this.pending={};try{const t=await this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function s(t={}){const{length:e=3,interval:s=100,sampleRate:r=16e3,autoHideTimeout:n=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,r=!1;function n(){clearTimeout(s),s=void 0}function o(){r||(t.style.display="none")}return{append:function(a){r||"string"!==typeof a||(n(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(/(?<=[\u3001\u3002\uff01\uff1f!?.])\s*/u).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){r=!0,n()}}}(a,e,n),c=new i(t.video,{interval:s,sampleRate:r,onAudioChunk:o},h.append),l=()=>c.start(),d=()=>c.pause(),p=()=>c.restart(),u=()=>(h.destroy(),t.off("play",l),t.off("pause",d),t.off("restart",p),t.off("destroy",u),c.stop(!0));return t.on("play",l),t.on("pause",d),t.on("restart",p),t.on("destroy",u),{name:"artplayerPluginAsr",stop:()=>c.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(s,"default",{value:s}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return s}); diff --git a/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js b/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js index b44f4b3f5..09aaa34ad 100644 --- a/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js +++ b/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js @@ -4,4 +4,4 @@ * (c) 2017-2026 Harvey Zhao * Released under the MIT License. */ -!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";var t=(t,e,i)=>new Promise((s,n)=>{var r=t=>{try{a(i.next(t))}catch(e){n(e)}},o=t=>{try{a(i.throw(t))}catch(e){n(e)}},a=t=>t.done?s(t.value):Promise.resolve(t.value).then(r,o);a((i=i.apply(t,e)).next())});class e{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}initialize(){return t(this,null,function*(){var t;this.assertOpen();const e=window,i=window.AudioContext||e.webkitAudioContext;if(!i)throw new Error("AudioContext is not supported");const s=null!=(t=this.context)?t:this.context=new i({sampleRate:this.sampleRate});if("suspended"===s.state&&(yield s.resume()),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{yield s.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=s.createMediaElementSource(this.video),this.direct=!0}catch(n){console.warn("[artplayerPluginAsr] Direct connection failed:",n);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=s.createMediaStreamSource(this.stream)}})}prepare(){return null!=this.preparation||(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const n=this.gain=s||e.createGain();s||(n.gain.value=1);const r=this.recorder=new AudioWorkletNode(e,"recorder-processor");r.port.onmessage=e=>{this.closed||this.recorder!==r||t(e.data)},i.connect(r),i.connect(n),n.connect(e.destination)}disconnect(){var t,e;this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),null==(t=this.gain)||t.disconnect(),this.gain=void 0,null==(e=this.source)||e.disconnect()}close(){return t(this,null,function*(){var t;if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),null==(t=this.stream)||t.getTracks().forEach(t=>t.stop()),this.stream=void 0;const e=this.context;this.context=void 0,this.source=void 0,e&&"closed"!==e.state&&(yield e.close())})}}class i{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const i=this.epoch,s=()=>!this.terminal&&this.epoch===i,n=(()=>t(this,null,function*(){var t;if(yield this.closing,!s())return;const{sampleRate:i,interval:n}=this.options,r=Math.floor(i*n/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(n)||n<=0)throw new Error("Audio chunk length must be positive and finite");const o=null!=(t=this.graph)?t:this.graph=new e(this.video,i);yield o.prepare(),s()?(o.connect(t=>{if(this.running&&this.graph===o)return this.queue.length+t.length>Math.max(60*i,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),n)):this.running||this.graph!==o||o.idle()}))().catch(e=>t(this,null,function*(){s()&&(console.error("[artplayerPluginAsr] Initialization failed:",e),yield this.stop())})).finally(()=>{this.starting===n&&(this.starting=void 0)});return this.starting=n,n}tick(e){return t(this,null,function*(){if(!this.running||this.pending)return;const t=this.queue.take(e);if(!t)return;const i=this.epoch,s=this.pending={};try{const e=yield this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function n(t={}){const{length:e=3,interval:i=100,sampleRate:n=16e3,autoHideTimeout:r=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,n=!1;function r(){clearTimeout(s),s=void 0}function o(){n||(t.style.display="none")}return{append:function(a){n||"string"!==typeof a||(r(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(new RegExp("(?<=[\u3001\u3002\uff01\uff1f!?.])\\s*","u")).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){n=!0,r()}}}(a,e,r),l=new s(t.video,{interval:i,sampleRate:n,onAudioChunk:o},h.append),c=()=>l.start(),d=()=>l.pause(),u=()=>l.restart(),p=()=>(h.destroy(),t.off("play",c),t.off("pause",d),t.off("restart",u),t.off("destroy",p),l.stop(!0));return t.on("play",c),t.on("pause",d),t.on("restart",u),t.on("destroy",p),{name:"artplayerPluginAsr",stop:()=>l.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(n,"default",{value:n}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return n}); +!function(t,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"==typeof define&&define.amd?(t.artplayerPluginAsr=e(),define(function(){return t.artplayerPluginAsr})):(t="undefined"!==typeof globalThis?globalThis:t||self).artplayerPluginAsr=e()}(this,function(){"use strict";var t=(t,e,i)=>new Promise((s,n)=>{var r=t=>{try{a(i.next(t))}catch(e){n(e)}},o=t=>{try{a(i.throw(t))}catch(e){n(e)}},a=t=>t.done?s(t.value):Promise.resolve(t.value).then(r,o);a((i=i.apply(t,e)).next())});class e{constructor(t,e){this.video=t,this.sampleRate=e,this.closed=!1,this.direct=!1,this.workletLoaded=!1}assertOpen(){if(this.closed)throw new Error("Audio capture was closed")}revokeWorklet(){void 0!==this.blobUrl&&(URL.revokeObjectURL(this.blobUrl),this.blobUrl=void 0)}initialize(){return t(this,null,function*(){var t;this.assertOpen();const e=window,i=window.AudioContext||e.webkitAudioContext;if(!i)throw new Error("AudioContext is not supported");const s=null!=(t=this.context)?t:this.context=new i({sampleRate:this.sampleRate});if("suspended"===s.state&&(yield s.resume()),this.assertOpen(),!this.workletLoaded){this.blobUrl=URL.createObjectURL(new Blob(["\nclass RecorderProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0]) {\n this.port.postMessage(input[0]);\n }\n return true;\n }\n}\nregisterProcessor('recorder-processor', RecorderProcessor);\n"],{type:"application/javascript"}));try{yield s.audioWorklet.addModule(this.blobUrl),this.workletLoaded=!0}finally{this.revokeWorklet()}}if(this.assertOpen(),!this.source)try{this.source=s.createMediaElementSource(this.video),this.direct=!0}catch(n){console.warn("[artplayerPluginAsr] Direct connection failed:",n);const t=this.video.captureStream||this.video.mozCaptureStream;if(!t)throw new Error("Could not establish audio source");this.stream=t.call(this.video),this.source=s.createMediaStreamSource(this.stream)}})}prepare(){return null!=this.preparation||(this.preparation=this.initialize().catch(t=>{throw this.preparation=void 0,t})),this.preparation}get ownsMediaConnection(){return this.direct}idle(){const t=this.gain;this.disconnect(),this.direct&&this.context&&this.source&&(this.gain=t||this.context.createGain(),t||(this.gain.gain.value=1),this.source.connect(this.gain),this.gain.connect(this.context.destination))}connect(t){this.assertOpen();const e=this.context,i=this.source;if(!e||!i)throw new Error("Audio capture is not prepared");const s=this.gain;this.disconnect();const n=this.gain=s||e.createGain();s||(n.gain.value=this.direct?1:0);const r=this.recorder=new AudioWorkletNode(e,"recorder-processor");r.port.onmessage=e=>{this.closed||this.recorder!==r||t(e.data)},i.connect(r),this.direct?i.connect(n):r.connect(n),n.connect(e.destination)}disconnect(){var t,e;this.recorder&&(this.recorder.port.onmessage=null,this.recorder.disconnect(),this.recorder=void 0),null==(t=this.gain)||t.disconnect(),this.gain=void 0,null==(e=this.source)||e.disconnect()}close(){return t(this,null,function*(){var t;if(this.closed)return;this.closed=!0,this.disconnect(),this.revokeWorklet(),null==(t=this.stream)||t.getTracks().forEach(t=>t.stop()),this.stream=void 0;const e=this.context;this.context=void 0,this.source=void 0,e&&"closed"!==e.state&&(yield e.close())})}}class i{constructor(){this.offset=0,this.size=0}get length(){return this.size}push(t){if(0===t.length)return;const e={samples:new Float32Array(t)};this.tail?this.tail.next=e:this.head=e,this.tail=e,this.size+=t.length}take(t){if(!Number.isSafeInteger(t)||t<=0)throw new RangeError("Sample count must be a positive safe integer");if(t>this.size)return null;const e=new Float32Array(t);let i=0;for(;this.head&&it?this.start():void 0):t?this.start():Promise.resolve()}start(){if(this.terminal||this.running)return Promise.resolve();if(this.starting)return this.starting;const i=this.epoch,s=()=>!this.terminal&&this.epoch===i,n=(()=>t(this,null,function*(){var t;if(yield this.closing,!s())return;const{sampleRate:i,interval:n}=this.options,r=Math.floor(i*n/1e3);if(!Number.isSafeInteger(r)||r<1||!Number.isFinite(n)||n<=0)throw new Error("Audio chunk length must be positive and finite");const o=null!=(t=this.graph)?t:this.graph=new e(this.video,i);yield o.prepare(),s()?(o.connect(t=>{if(this.running&&this.graph===o)return this.queue.length+t.length>Math.max(60*i,2*r)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queue.push(t)}),this.running=!0,this.timer=setInterval(()=>this.tick(r),n)):this.running||this.graph!==o||o.idle()}))().catch(e=>t(this,null,function*(){s()&&(console.error("[artplayerPluginAsr] Initialization failed:",e),yield this.stop())})).finally(()=>{this.starting===n&&(this.starting=void 0)});return this.starting=n,n}tick(e){return t(this,null,function*(){if(!this.running||this.pending)return;const t=this.queue.take(e);if(!t)return;const i=this.epoch,s=this.pending={};try{const e=yield this.options.onAudioChunk(function(t,e){const i=new ArrayBuffer(2*t.length),s=new DataView(i);for(let a=0;a{})),this.closing}}function n(t={}){const{length:e=3,interval:i=100,sampleRate:n=16e3,autoHideTimeout:r=1e4,onAudioChunk:o=()=>null}=t;return t=>{const a=t.layers.add({name:"asr",html:""});if(!a)throw new Error("Could not create ASR subtitle layer");const h=function(t,e,i){let s,n=!1;function r(){clearTimeout(s),s=void 0}function o(){n||(t.style.display="none")}return{append:function(a){n||"string"!==typeof a||(r(),s=setTimeout(o,i),t.style.display="",t.innerHTML=a.split(new RegExp("(?<=[\u3001\u3002\uff01\uff1f!?.])\\s*","u")).map(t=>t.trim()).filter(Boolean).slice(-e).map(t=>`
${t}
`).join(""))},hide:o,destroy(){n=!0,r()}}}(a,e,r),l=new s(t.video,{interval:i,sampleRate:n,onAudioChunk:o},h.append),c=()=>l.start(),d=()=>l.pause(),u=()=>l.restart(),p=()=>(h.destroy(),t.off("play",c),t.off("pause",d),t.off("restart",u),t.off("destroy",p),l.stop(!0));return t.on("play",c),t.on("pause",d),t.on("restart",u),t.on("destroy",p),{name:"artplayerPluginAsr",stop:()=>l.stop(),hide:h.hide,append:h.append}}}if(Object.defineProperty(n,"default",{value:n}),"undefined"!==typeof document&&!document.getElementById("artplayer-plugin-asr")){const t=document.createElement("style");t.id="artplayer-plugin-asr",t.textContent=".art-video-player .art-layer-asr{pointer-events:none!important;position:absolute;z-index:150;inset:0;display:flex;flex-direction:column;justify-content:end;padding:0 2%;font-size:1rem;gap:var(--art-subtitle-gap);padding-bottom:var(--art-subtitle-bottom);transition:padding-bottom var(--art-transition-duration) ease;text-shadow:var(--art-subtitle-border) 1px 0 1px,var(--art-subtitle-border) 0 1px 1px,var(--art-subtitle-border) -1px 0 1px,var(--art-subtitle-border) 0 -1px 1px,var(--art-subtitle-border) 1px 1px 1px,var(--art-subtitle-border) -1px -1px 1px,var(--art-subtitle-border) 1px -1px 1px,var(--art-subtitle-border) -1px 1px 1px}.art-video-player.art-control-show .art-layer-asr{padding-bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}",document.head.appendChild(t)}return n}); diff --git a/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs b/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs index fea4c7923..3fdbb9574 100644 --- a/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs +++ b/packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs @@ -99,14 +99,16 @@ class AudioGraph { this.disconnect(); const gain = this.gain = previousGain || context.createGain(); if (!previousGain) - gain.gain.value = 1; + gain.gain.value = this.direct ? 1 : 0; const recorder = this.recorder = new AudioWorkletNode(context, "recorder-processor"); recorder.port.onmessage = (event) => { if (!this.closed && this.recorder === recorder) receive(event.data); }; source.connect(recorder); - source.connect(gain); + if (this.direct) + source.connect(gain); + else recorder.connect(gain); gain.connect(context.destination); } disconnect() { diff --git a/packages/artplayer-plugin-asr/src/audio-graph.ts b/packages/artplayer-plugin-asr/src/audio-graph.ts index 013fffe5b..c6e3d7eb2 100644 --- a/packages/artplayer-plugin-asr/src/audio-graph.ts +++ b/packages/artplayer-plugin-asr/src/audio-graph.ts @@ -100,14 +100,16 @@ export class AudioGraph { this.disconnect() const gain = this.gain = previousGain || context.createGain() if (!previousGain) - gain.gain.value = 1 + gain.gain.value = this.direct ? 1 : 0 const recorder = this.recorder = new AudioWorkletNode(context, 'recorder-processor') recorder.port.onmessage = (event: MessageEvent) => { if (!this.closed && this.recorder === recorder) receive(event.data) } source.connect(recorder) - source.connect(gain) + if (this.direct) + source.connect(gain) + else recorder.connect(gain) gain.connect(context.destination) } diff --git a/refactor/baselines/asr-fallback-validation.json b/refactor/baselines/asr-fallback-validation.json new file mode 100644 index 000000000..ede7f74fa --- /dev/null +++ b/refactor/baselines/asr-fallback-validation.json @@ -0,0 +1,4631 @@ +{ + "schemaVersion": 1, + "task": "PKG-ASR-08", + "capturedAt": "2026-09-13T07:39:57.953Z", + "baseCommit": "3e3bed2b93f0dc34eeeab07d2f6deef855bfc885", + "correction": "Only captured fallback uses recorder -> gain(0) -> destination. No raw captured speaker route; external owner remains intact.", + "publicSignatureChanged": false, + "versionChanged": false, + "dependenciesAdded": [], + "red": { + "file": "refactor/.cache/asr08-fallback-red/report.json", + "sha256": "fcd1e985915fd2dd213e5d5bdd1ba2e17826a26b590d950ead1614f4085f9b98", + "failed": 2, + "observation": "Both candidate cores produced a second native ASR output while video was muted on Chromium." + }, + "branchProbe": { + "file": "refactor/.cache/asr08-native-branch-probe/report.json", + "sha256": "f17e17ca9d408ee0eb4a0e18aa21123258a61d0b90e9526fc3b0fc1bd843e9ab", + "failed": 4, + "observation": "Firefox accepted duplicate native element bindings and did not enter capture fallback. Both external and ASR branches produced signal. Remains ASR-FOREIGN-01." + }, + "browserMode": "Chromium natural duplicate-binding rejection. Firefox explicitly controlled rejection with native captureStream/Worklet/tracks/analyser. Windows WebKit lacks WebAudio.", + "browsers": [ + { + "mode": "source", + "file": "refactor/.cache/asr08-browser-source/report.json", + "sha256": "4d9ab4b2c097da308cda4ffccef599a56dba82cfe6c7fc2fce3ea0dfdeecf4ab", + "passed": 28, + "skipped": 14, + "rows": [ + { + "title": "ASR 2.0.0 + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.0.0", + "core": "published", + "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6", + "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.0.0 + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.0.0", + "core": "candidate", + "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6", + "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "integrity": null, + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "integrity": null, + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.0.0 + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.0.0", + "core": "published", + "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6", + "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.0.0 + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.0.0", + "core": "candidate", + "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6", + "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "integrity": null, + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "integrity": null, + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR 2.0.0 + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "2.0.0", + "core": "published", + "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6", + "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR 2.0.0 + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "2.0.0", + "core": "candidate", + "pluginSha256": "30c7b1fc92a262cda8c3aba63afea9c6c038f52c057039d1c6aa957baed155b6", + "integrity": "sha512-nXTf2drMSKnHRbyU9CQ+3o7Jsf1Meam2O9T8LqYakIr18ag/+i71p2x9zdFrLz16Xa6jfQgKeddmU5r3sXp4LA==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR 2.1.0 + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR 2.1.0 + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "integrity": "sha512-15gXff458PAx9LeJxKzZEvmpFhfwKUfSLgb1Mfr7Xb/ZGsnE+6EA7ezTnO5S55W5mFExjv/e95iOIXP4ijuruQ==", + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "integrity": null, + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "integrity": null, + "candidate": "workspace source", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR 2.1.0 + published: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08762481388661633, + "asr": [ + 0.08758629723718069 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0.08888106836226659 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4307, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08836963739741921, + "asr": [ + 0.08873759885352435 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4307, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + candidate: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08759580548385439, + "asr": [ + 0.08776218864356003 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4169, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0.08871286074040662 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4307, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08800560294657865, + "asr": [ + 0.08888106836226659 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4307, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08759580548385439, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08802307825410978, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "restarted": { + "external": 0.08843834429457656, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4192, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4263, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4247, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08798611880382642, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08800639246669537, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "restarted": { + "external": 0.08862216379888067, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4192, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4245, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + published: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08875186186068326, + "asr": [ + 0.08875186186068326 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4215, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0.08853254925784836 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4307, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4258, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.08854881078207408, + "asr": [ + 0.08818815547122286 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4307, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4258, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + candidate: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08774571319741767, + "asr": [ + 0.08774571319741767 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0.0887048021485559 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.08785763309746902, + "asr": [ + 0.08864678897747748 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08854340470395779, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4183, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4252, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.08892189663503984, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "restarted": { + "external": 0.08866202715264984, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4303, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4187, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.0880712446666184, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.08824565096595674, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "restarted": { + "external": 0.08790303719713795, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4303, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4187, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + published: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR 2.1.0 + candidate: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR 2.1.0 + published: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + candidate: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + published: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.1.0", + "core": "published", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + candidate: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "2.1.0", + "core": "candidate", + "pluginSha256": "7f975b131a9ea207f88a157aafec778bc4906230500db9b30f5bbfbf327b251d", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "17f0b26199a13a40338f51365c55ef4bd9c0f12edb9e4a4c2ce61904ea044edf", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR 2.1.0 + published: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + }, + { + "title": "ASR 2.1.0 + candidate: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + } + ] + }, + { + "mode": "main", + "file": "refactor/.cache/asr08-browser-main/report.json", + "sha256": "3903498d7d02c553a9b800cd9b22747eac6d4f1de5f77d772099fe391cff6eb4", + "passed": 12, + "skipped": 6, + "rows": [ + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08836286954075324, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08833319439117504, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "restarted": { + "external": 0.08869046588295236, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4192, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4263, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4250, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08798611880382642, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08811563255895567, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "restarted": { + "external": 0.08896275868833134, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4158, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4263, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4250, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08843546012492767, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.08862752847073793, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "restarted": { + "external": 0.08906111312585639, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4303, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4187, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08791119445452536, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.08812234452410372, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "restarted": { + "external": 0.08866202715264984, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4303, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4187, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + } + ] + }, + { + "mode": "legacy", + "file": "refactor/.cache/asr08-browser-legacy/report.json", + "sha256": "ceeb52df1711fcb184ee76653c65c62dec42ee8221d2734dad8a50fc3b36bc0f", + "passed": 12, + "skipped": 6, + "rows": [ + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native Worklet supplies local PCM with owned stop and destroy", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "integrity": null, + "candidate": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "media": "local mono AAC 440 Hz tone", + "networkService": false + }, + "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": 30, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08798611880382642, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08800639246669537, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "restarted": { + "external": 0.08869046588295236, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4250, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08779609911638686, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4260, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "stopped": { + "external": 0.08799690350970676, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4272, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4280, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4264, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + }, + "restarted": { + "external": 0.08869046588295236, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4290, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4192, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4263, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + }, + { + "name": "InvalidStateError", + "message": "Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode." + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08866202715264984, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4183, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4252, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.0889824932438642, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "restarted": { + "external": 0.08866202715264984, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4303, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4187, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "capabilities": { + "context": "function", + "worklet": "function", + "capture": "function", + "mozCapture": "function" + }, + "forcedException": true, + "reason": "Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "fallback": { + "output": { + "playing": { + "external": 0.08866202715264984, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4303, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4252, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "muted": { + "external": 0, + "asr": [ + 0 + ], + "contexts": [ + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "stopped": { + "external": 0.088905419835137, + "asr": [ + 0 + ], + "contexts": [ + "closed" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ] + ], + "chunks": [ + { + "max": 4190, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + }, + { + "max": 4248, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + }, + "restarted": { + "external": 0.08895963860169126, + "asr": [ + 0, + 0, + 0 + ], + "contexts": [ + "closed", + "closed", + "running" + ], + "owner": "running", + "streams": [ + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "ended" + } + ], + [ + { + "kind": "audio", + "state": "live" + } + ] + ], + "chunks": [ + { + "max": 4303, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4187, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + }, + { + "max": 4255, + "source": "http://127.0.0.1:8084/test/audio-tone.m4a?asr=fallback-second", + "samples": 1600 + } + ], + "failures": [ + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + }, + { + "name": "InvalidStateError", + "message": "Controlled duplicate binding rejection for native capture test" + } + ] + } + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native capture fallback does not create a second audible route", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "capabilities": { + "context": "undefined", + "worklet": "undefined", + "capture": "undefined", + "mozCapture": "undefined" + }, + "forcedException": false, + "reason": "Native duplicate binding rejection", + "externalContext": "Native media-element source remains owned and running", + "physicalOutput": false + }, + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit has no native WebAudio", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-fallback.spec.js", + "line": 26, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "chromium", + "status": "passed", + "browser": "153.0.8010.12", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "published", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "firefox", + "status": "passed", + "browser": "155.0", + "inputs": { + "version": "candidate", + "core": "candidate", + "pluginSha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "mediaSha256": "83e6672b880b5d1c97093b1e26bdc40221c0b279615f40e99dbf9d2ade7b4d56", + "meter": "Native unity analyser inserted between plugin gain and destination", + "physicalAudioOutput": false + }, + "annotations": [] + }, + { + "title": "ASR candidate + published: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + }, + { + "title": "ASR candidate + candidate: native playback volume and mute", + "project": "webkit", + "status": "skipped", + "browser": "26.6", + "annotations": [ + { + "type": "skip", + "description": "Windows WebKit lacks native AudioContext/AudioWorkletNode", + "location": { + "file": "D:\\github\\ArtPlayer\\test\\browser\\asr-playback.spec.js", + "line": 24, + "column": 14 + } + } + ] + } + ] + } + ], + "artifacts": [ + { + "file": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.js", + "sha256": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "bytes": 8739, + "docsCopyIdentical": true + }, + { + "file": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.legacy.js", + "sha256": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "bytes": 9216, + "docsCopyIdentical": true + }, + { + "file": "packages/artplayer-plugin-asr/dist/artplayer-plugin-asr.mjs", + "sha256": "41b06ee8f88ed3392d1c02232994d00766d553fdc0745976279669d59c3c3717", + "bytes": 13115, + "docsCopyIdentical": true + } + ], + "unit": { + "focused": 213, + "repeatedMain": 30, + "repeatedLegacy": 30, + "newFallbackRed": { + "file": "refactor/.cache/asr08-unit-red.log", + "failed": 5 + }, + "helperCorrection": "Register the actual Worklet mock object rather than a shallow copy so cleanup observes true node identity." + }, + "installed": { + "file": "refactor/.cache/asr-package-types-LFOcfJ/report.json", + "sha256": "c25ece0e7b0369545194dbf3e80442f34deb36a56f8a53b30121259935c3b38c", + "cases": 17, + "candidateConfigurations": 7, + "rejectedUsesPerCandidate": 12, + "packages": [ + { + "name": "artplayer", + "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\asr-package-types-LFOcfJ\\artplayer.tgz", + "sha256": "0c0ea52d41575261b836aefac68280733b119c75834ecad6d4bc78edb073f4c1", + "files": { + "package/ARCHITECTURE.md": "bee98fc9d93a3911d58e98ef15f2dbe07e12f2274c444d989094248a22079e46", + "package/README.md": "1e185e65f67fc2081b4182bbc6c4558ea05b6e3fa3acc5dd0c3ac7df1d542b09", + "package/THIRD_PARTY_NOTICES": "b77b5b46b79fa0cda850b0781f78c3b1112c4ed9672fa291956fe8eff53098e7", + "package/dist/artplayer.js": "6dc8ca6d6eb34aa8a056cbc31328efd634530b3c5c60f0014cb7ce719fabc335", + "package/dist/artplayer.legacy.js": "80f74a8a39df4baf64cc00c65d21cd57c011bfcdead0da38eea077fbc67c5c04", + "package/dist/artplayer.mjs": "432e2b0ad92009a09743e020bf7c928c8b8f18a52496622b16259ec9ab20059f", + "package/dist/i18n/ar.js": "bd0bef65f4f7dc3f8ed562904cc6fa9071ff4537fb64c1e0fc3d554141bcd3cb", + "package/dist/i18n/ar.mjs": "59310b7bbeea83bbb7d73aadc5ca82a6ec269c3530d19aa741c94d5c64fc5da8", + "package/dist/i18n/cs.js": "6e2c93daba24c2e48dfa206bcf4f2f2a8a02713b856e404d2e4e4ecd8309ca2f", + "package/dist/i18n/cs.mjs": "c73fba18797d62d7b2581e67fe7340c35fe84510b2250ff1ecaad253d9cabbac", + "package/dist/i18n/es.js": "f4133ac6ea06b69a474b33c4a1809a4066e2376bb9464a3f3aa8330f01f1944a", + "package/dist/i18n/es.mjs": "1390bca3def2be115e811de337df0a0c3f63998e51359f283023246245c552a7", + "package/dist/i18n/fa.js": "8d3ef2e0f0cc0ea480c2167b6fcb87cdf444e90759c3519e8daa139fec576da4", + "package/dist/i18n/fa.mjs": "ab441e8a2d79043b29ceca87c783bf06874bb9d3a149c1604bc167eb481be2a9", + "package/dist/i18n/fr.js": "89dc4194a6c0a962b9ce756b52c90401c1acf3b159f265f3bc0d7d6d7d987f03", + "package/dist/i18n/fr.mjs": "99491878430fb7b6f2b7bf93da0588a831704b412e01116f23ba1d9d45863816", + "package/dist/i18n/id.js": "4c6dc86db741bcc2857df55a9d037e82e91d375fa6fd22e0562809ff621b92db", + "package/dist/i18n/id.mjs": "383e5803a6b6686c6de4e867770241de75b6f517dcca2bdedc6f2004bf6e1fb3", + "package/dist/i18n/pl.js": "3a38828b1c3d719c0aca9906cf6a46c23555a8411f8d57645f0e0ce8147dc664", + "package/dist/i18n/pl.mjs": "079ed96732979035297af387c64a7b99519b5a43107fb86735a654ea4fe8e9b5", + "package/dist/i18n/ru.js": "62fac9dddb302eb16382e6e9301fc4b2c66f5f69d4a358a26b67026a5fa4e787", + "package/dist/i18n/ru.mjs": "a893aeaee1d79409db72e324fbff200e3864f33ed168f04d91d25f7cf2efa765", + "package/dist/i18n/tr.js": "1492c36b069a7d73db4f3913bd18085faff7afd8b3c158138fe264e31600e38c", + "package/dist/i18n/tr.mjs": "a90478cac078836b76c2a98261907610fee07f3e4b28671b8e7dbf533778f918", + "package/dist/i18n/vi.js": "c7216ef7e608a79b7e2c479a07bf310d4ab79577947ac4a62d676faaced0e9cf", + "package/dist/i18n/vi.mjs": "66270fdfa0b5da396322d1769169b7df37a030ba15ac18eb30bd87b3af8074ec", + "package/dist/i18n/zh-tw.js": "11c49857d6d40a5ffd224ffa59c9026e71621b345f63c5a086326db80a22371a", + "package/dist/i18n/zh-tw.mjs": "a38e630cb96de96c15d824686aec750e25cf6c8ada01285a292442a81566314f", + "package/package.json": "82e92699e7c29847f32ac6767ece0401290e7d7a12854722bdbcfd09b90b7954", + "package/types/COMPATIBILITY.md": "a7da9e102a116104b513df4f7451247b7759bc502d3375f30bd5b14a541826e0", + "package/types/README.md": "1bd9868e773610e6312fd48a1c5aa20772d29df0af40c3db66e75b7ab4a1aee0", + "package/types/artplayer.d.cts": "3685065e4263abcfd9d953176e461d5b3eb990b0363b54689a7f55deba770fef", + "package/types/artplayer.d.mts": "b664fec51ee5c5381f2ceab25d99d52aee56feab04a2ee7b4ea5b2145ea5fa0e", + "package/types/artplayer.d.ts": "5d0162adf7ac6d0f2bb9b5cfe361f28657c4d9c540cadbf36eba3cc954ce50f8", + "package/types/component.d.ts": "ae5bc1ddeaa1ee1ebbc62573e32423993727079205c64751a5a314170e588a32", + "package/types/config.d.ts": "36f9a84cd436f17701a0d11d3417fc1fe0414e0c52c80083b94a85dfc329e37e", + "package/types/cssVar.d.ts": "93217860fff2ebf750f606d95ddd22220a58837119e63bdf2f5f78fd7488458b", + "package/types/emitter.d.ts": "7e1f9a2a8ce65feb95734f1ed52d1334e57aa388bbef2a43d8bc53a9b6f6623c", + "package/types/events.d.ts": "f83624c9268a284477e8bd793fe3562a2363b72f2a44d5c64fe8c6a6cede8830", + "package/types/i18n.d.ts": "f4f2f05bdfda73c74fef34767cb18ee8e11b743120b1c4598e9714d7846018e5", + "package/types/icons.d.ts": "326a67ba7bdcf52862c5b2a269e540140aff98d05c7b96196485a77f84fe5960", + "package/types/language.d.cts": "f7928e108cf7b7814c2d75cb0a525333e95873fca6cd6d89a740450656b9c1cd", + "package/types/language.d.mts": "6619a26d604b7cccbe2f596243d87ae37cfaae33b8b52874ec071d9787446f9e", + "package/types/legacy/language.d.ts": "c7abda4d506fd933c350e9030290bf9e64347a921d683a66115da1995ebe0a81", + "package/types/option.d.ts": "ccf2c8ddc537dc65c9682d39f044726e23bff18f476b622512d7e6a4b949b5f6", + "package/types/playback.d.ts": "58705c273df6e76a77a6575e769a9ec6c013785c25080f2551aa22415788d870", + "package/types/player.d.ts": "92b00211b70ac71336e495899df972a0519dd138111846044fe4e0f967966177", + "package/types/plugin.d.ts": "fc3594a41c51d8248713e33c486fb1f6a518ac5c7ce31be6988a6cd8d052c5f3", + "package/types/quality.d.ts": "1ac1a48b4f74ff9ba1ce60443e1463d50d1c6a2e5d87d63dc7d2a8eb7f4c2c34", + "package/types/runtime.d.cts": "7ee988dfea9a0ac71a34a656f96f7b74e78aa92547cfdfab7ff7e1139a77d4a3", + "package/types/runtime.d.mts": "dcb85f59a8b7714918ced0c1d18c2bb24e8b80b56d8c594e019dcbdb5ad88b08", + "package/types/runtime.d.ts": "9c6ccd39a1be293f17ae46dae8ea077053e12635ee79d73a5e3b3a559230fcff", + "package/types/runtime/component.d.ts": "1b4c0cee44568649e80140fe778febfcae88af0ac85515f1cbeff78ab0c6d14f", + "package/types/runtime/construction.d.ts": "7a87bedf6f7ffff0c46078aa5f1551e0077d35574e286516121f5f935e2d9ce4", + "package/types/runtime/events.d.ts": "a1e2f9d387475453eaf95c2a5ee79e228636c0bb859bda3fcc46da46574a0c31", + "package/types/runtime/media.d.ts": "8fc89401be7fe25542c169d07dd5675f10916ed20be8493e19695966cf3864a8", + "package/types/runtime/option.d.ts": "6e334e1a76fdcc40b4665c02523bd31c3994d96078fadeb40e23422aa54c1a1d", + "package/types/runtime/player.d.ts": "8ac6383502e4872e83659838844856903031a81a0adc8e8535feb78453aa042c", + "package/types/runtime/plugin.d.ts": "8528f2b17843149f42f9cc16d6fd70baa238c9dc2c329b67d2c38096799c69bd", + "package/types/runtime/services.d.ts": "604c9f8e4b6c0da818f96576bf9f4b0e96085a9003927b2f7ba30027b1ba237e", + "package/types/runtime/setting.d.ts": "349c1fcb205ad312916bd3ee55e6bf241ea8a569fb2ab6c80822e69fc518f7d7", + "package/types/runtime/subtitle.d.ts": "269ff84678d327b3a9dc2f5bc6cc0d731d1e081b85422e474621d5d792efb153", + "package/types/runtime/template.d.ts": "7a62f968a38b4bf9109a4439557c02074eb1d62ba1f6e21a8eb194e91f0badfe", + "package/types/runtime/utils.d.ts": "6bf731705d86853e066dbaf3b8d4003b6548423fe615b7412b722bd0d3a6e48b", + "package/types/setting.d.ts": "08305a75dff96f06490430e099338b463df471b550c1d0dac3abc8995b39eb09", + "package/types/subtitle.d.ts": "b18314e2d64efdb6045830b6e12f615ae2ed585416130e562329ad3ea337f6ce", + "package/types/template.d.ts": "4792bc0c2cbcb7966e4375118bd34b326166e0831862e94ace4cf6b1bbe6a8c8", + "package/types/utils.d.ts": "fca0d3d814df94a25f9eb067c86fa0cf272e2791bac793b91229d57680797712" + } + }, + { + "name": "artplayer-plugin-asr", + "archive": "D:\\github\\ArtPlayer\\refactor\\.cache\\asr-package-types-LFOcfJ\\artplayer-plugin-asr.tgz", + "sha256": "985a048827b843f4e792f6dd96bc0491fd83a179ee2d97d620f4bf30e96fe022", + "files": { + "package/ARCHITECTURE.md": "4c7a1f0f0167b5a1341a1c3e549208422bddea696edc2d02bb8fb1ea88bcd846", + "package/README.md": "d8eec6fafd308a3f52c16548a27ea07c2cb247d950b3e155d94f98e010e6b994", + "package/dist/artplayer-plugin-asr.js": "e31dbd6225d2a703fbe7025d4d07e9434103ebf332307b7771a6139e420fb8f4", + "package/dist/artplayer-plugin-asr.legacy.js": "c3b770394ab585ee7823204b07fb586ac6c386cd60dbe465da071cd10c543cb5", + "package/dist/artplayer-plugin-asr.mjs": "41b06ee8f88ed3392d1c02232994d00766d553fdc0745976279669d59c3c3717", + "package/package.json": "781b1eebefdff5fdc234b793ff9d6d8263275115ccff4c4d90c55eca942f2444", + "package/types/artplayer-plugin-asr.d.ts": "8fce55a45c368dc95fc6169750d2157ad57f683f62b586b8162ac720e1efdf42", + "package/types/runtime.d.cts": "2965dccc49a3a75c3b16c0c13501077b8ac199e78bac2d19d57be3d811583606", + "package/types/runtime.d.mts": "ff0769d3e408d66c7d38574171ff2621456afccc50ee8191ca4ee394dc57666e", + "package/types/runtime.d.ts": "2965dccc49a3a75c3b16c0c13501077b8ac199e78bac2d19d57be3d811583606" + } + } + ] + }, + "logs": [ + { + "file": "refactor/.cache/asr08-build.log", + "sha256": "ad4f4c42c20232252e37a5e4b09fac942bf774b5323e32c20fe8edca1e1840b9" + }, + { + "file": "refactor/.cache/asr08-lint.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "file": "refactor/.cache/asr08-strict.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "file": "refactor/.cache/asr08-unit.log", + "sha256": "b96f5f42469663cca31a4972b81eb816d121532f42e9525aa0b9cd282500671a" + }, + { + "file": "refactor/.cache/asr08-unit-main.log", + "sha256": "4e07391cf520bdffac59340957b5175bda0dc422e2290ccb59203ce3c30c5a82" + }, + { + "file": "refactor/.cache/asr08-unit-legacy.log", + "sha256": "10ef2c91df5be778633a27b91dc278a6667480468aaec153ce7355b9126226a5" + }, + { + "file": "refactor/.cache/asr08-installed.log", + "sha256": "7c351a89f2e005c112b60ca67b54b2e809acc97520083564ca5facab23937463" + } + ], + "inputs": { + "packages/artplayer-plugin-asr/src/audio-graph.ts": "80e0becb48d8856b4067fd2d76e3c01235504a3948a8758f37470e9e1eddadbc", + "test/helpers/asr-audio.js": "9c973b596a61284a34dcd565e1d86b29295947c556cc5121e197005c130f5531", + "test/asr-fallback-routing.test.js": "5971941f005e0a0c5e58b5633da2c6a9661c37a749180a4780d40763e52115e9", + "test/browser/asr-fallback.spec.js": "676ad7f12b9368d14b3a748c117593302fe5c8ed3cae80b24bf6480a3b4ca455" + }, + "limitation": [ + "Firefox automatic external-owner detection is not fixed; ASR-FOREIGN-01 remains open.", + "No physical audio output, Safari/device acceptance or cross-origin/other-plugin coverage.", + "No full CI rerun; no publish." + ], + "releaseReady": false +} diff --git a/refactor/changes/2026-09-13-PKG-ASR-08-capture-output.md b/refactor/changes/2026-09-13-PKG-ASR-08-capture-output.md new file mode 100644 index 000000000..1b6e446e2 --- /dev/null +++ b/refactor/changes/2026-09-13-PKG-ASR-08-capture-output.md @@ -0,0 +1,43 @@ +# PKG-ASR-08:阻止捕获回退额外播放声音 + +子代理复核指出 captureStream 回退不拥有原播放路由,源码却把原始捕获音轨 +接到 gain=1 和 destination。主代理用外部原生 AudioContext 先绑定同一 video, +保持其播放图运行,让 Chromium 自然拒绝 ASR 的第二次媒体源绑定并进入回退。 +原生测试证明 2.1.0 和 ASR-07 候选都产生额外输出,视频静音后该分支仍非零。 +修复前两个候选核心组合在“ASR 输出必须为零”的断言失败,保留报告与 trace。 + +## 实现与契约 + +回退改为 `capture source -> recorder -> gain(0) -> destination`,保持 Worklet +处理活跃而不输出声音;直接绑定仍是自己的播放路由,使用 gain=1。暂停、停止、 +切源和销毁复用现有归属模型。回调继续收到非零 PCM,不靠静音输入掩盖问题。 +不改变媒体元素 muted/volume,不关闭外部 owner context,不修改公开 API、类型或版本。 + +captureStream 的 PCM 可不受媒体静音/音量影响,而直接路径跟随主 video。文档现在 +明确区分两者;此前未限定路径的概括已修正。这个修复阻止额外可听分支,不能解释为 +“视频静音后停止识别”;调用方仍使用 stop/pause 控制采集生命周期。 + +五项新单测由子代理独立编写并先得到全红;主代理集成后,检测出测试 helper 把 +Worklet 浅拷贝为两个对象,导致节点清理列表观察到旧连接。已修正 helper 登记真实 +对象身份,完整历史/候选测试重新验证,不以删除清理断言通过。 + +## 原生验证的边界 + +Firefox 实际允许重复创建 MediaElementAudioSource,两条原生播放分支都有信号, +因此它不会自然进入本回退。这一探测结果与四个失败报告保留,单独登记未闭环的 +ASR-FOREIGN-01。Firefox 的回退验证显式强制绑定拒绝,其余 captureStream、轨道、 +采样、Worklet 和音频 analyser 全为原生,报告标记 forcedException=true。 +不能把受控拒绝说成 Firefox 自动识别外部音频图,也不能改成 skip 后宣称通过。 + +测试覆盖采集非零、ASR 输出零、原播放图静音、stop 只关闭自己、恢复时新轨道、 +切源后旧轨道结束且新源重新输出 PCM、destroy 最终关闭自己的 context。 +Firefox close 是异步的,校验使用有界轮询等待 state=closed,保持同步 destroy API。 + +验证记录见 [捕获回退证据](../baselines/asr-fallback-validation.json)。正常三格式 +构建与副本、strict/lint、213 项定向、main/legacy 各 30 项、实际隔离安装声明矩阵 +以及 source/main/legacy 原生组均记录确切输入。原生组还重跑直接路径音量和已有 +采集生命周期。Windows WebKit 无 WebAudio 的结果仍为能力跳过,不代表 Safari。 + +本项只关闭“已进入捕获回退后额外输出”的缺陷,ASR-05 的外部双 owner、CORS、 +audio-track 组合、更多核心和设备门槛继续保留。离线 demo 与 5.3.0 对照基线也由 +ASR-05 独立验收。本次未做整仓 CI、推送或发布;回退会恢复静音仍有额外输出的缺陷。 diff --git a/refactor/plan.md b/refactor/plan.md index e8bc5b918..23a4a6a89 100644 --- a/refactor/plan.md +++ b/refactor/plan.md @@ -2,9 +2,9 @@ > 由 tasks.json 生成。请修改数据后运行 `node refactor/scripts/plan.mjs --write`,不要手改本表。 -基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 226 项,范围 22 个包及工作区/示例。 +基线:`40fcda6a37d0049d42e49c1e64e70d4fd9ba5f7f`。总任务 227 项,范围 22 个包及工作区/示例。 -状态:todo 82 / doing 16 / blocked 0 / done 128 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 +状态:todo 81 / doing 17 / blocked 0 / done 129 / deferred 0。风险 L/M/H 表示兼容风险,不表示工期。 前置依赖是启动条件;验收是完成条件。任务可以继续拆分,但不能复用或悄悄删除旧 ID。 @@ -17,7 +17,7 @@ | artplayer | 5.4.1 | CORE-01, CORE-02, CORE-03, CORE-04, CORE-05, CORE-06, CORE-07, CORE-08, CORE-09, CORE-10, CORE-11, CORE-12, CORE-13, CORE-14, CORE-15, CORE-16, CORE-17, CORE-18, CORE-19, CORE-20, CORE-21, CORE-23, CORE-22, CORE-24, CORE-25, ENG-12 | | artplayer-plugin-ads | 2.1.0 | PKG-ADS-01, PKG-ADS-02, PKG-ADS-03, PKG-ADS-04, PKG-ADS-05, PKG-ADS-06 | | artplayer-plugin-ambilight | 1.1.0 | PKG-AMBILIGHT-01, PKG-AMBILIGHT-02, PKG-AMBILIGHT-03, PKG-AMBILIGHT-04, PKG-AMBILIGHT-PROXY-01, PKG-AMBILIGHT-05, PKG-AMBILIGHT-06, PKG-FACTORY-01 | -| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06, PKG-ASR-07 | +| artplayer-plugin-asr | 2.1.0 | PKG-ASR-01, PKG-ASR-02, PKG-ASR-03, PKG-ASR-04, PKG-ASR-05, PKG-ASR-06, PKG-ASR-07, PKG-ASR-08 | | artplayer-plugin-audio-track | 1.1.0 | PKG-AUDIO-01, PKG-AUDIO-02, PKG-AUDIO-03, PKG-AUDIO-04, PKG-AUDIO-05, PKG-AUDIO-06 | | artplayer-plugin-auto-thumbnail | 1.1.0 | PKG-AUTO-THUMB-01, PKG-AUTO-THUMB-02, PKG-AUTO-THUMB-03, PKG-AUTO-THUMB-04, PKG-AUTO-THUMB-05, PKG-AUTO-THUMB-06 | | artplayer-plugin-chapter | 1.1.0 | PILOT-01, PKG-CHAPTER-01, PKG-CHAPTER-02, PKG-CHAPTER-03, PKG-CHAPTER-04, PKG-CHAPTER-05, PKG-CHAPTER-06 | @@ -255,9 +255,10 @@ | 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 | done | | PKG-ASR-04 | artplayer-plugin-asr
迁移自有源码和公开类型 | PKG-ASR-03, ENG-04, ENG-06, CORE-07 | 主线程/Worklet/音频 buffer 与异步回调类型 | 严格类型检查、旧消费样例通过;声明路径/导出和同步异步兼容 | H | done | -| PKG-ASR-05 | artplayer-plugin-asr
验证新旧核心和组合 | PKG-ASR-04, CORE-22, PKG-ASR-07 | 真实 WebAudio 分块、播放暂停/切源/销毁,不引入网络 ASR | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | todo | +| PKG-ASR-05 | artplayer-plugin-asr
验证新旧核心和组合 | PKG-ASR-04, CORE-22, PKG-ASR-07, PKG-ASR-08 | 真实 WebAudio 分块、播放暂停/切源/销毁,不引入网络 ASR | 最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成 | H | doing | | PKG-ASR-06 | artplayer-plugin-asr
验证分发并同步文档 | PKG-ASR-05, ENG-07 | asr.js、Worklet 资源、输出格式与声明证据 | tarball 入口/资源、类型、8082 demo 和 README 一致,有回退记录 | H | todo | | PKG-ASR-07 | artplayer-plugin-asr
修复原生播放音量二次衰减 | PKG-ASR-04 | 移除重复音量应用;保留新旧原生音频振幅对照及正式产物验收 | Chromium/Firefox 两核心和 source/main/legacy 输出音量线性、静音和 stop/restart 正确;旧版平方衰减单独保留;单元/类型/真实安装通过,不代替设备验收 | H | done | +| PKG-ASR-08 | artplayer-plugin-asr
修复捕获回退重复播放与静音漏声 | PKG-ASR-07 | 捕获回退静音输出图、非零PCM与外部播放归属;真实红绿证据及产物验证 | 已进入回退后不产生额外输出、静音不漏声、外部播放图不被关闭、stop/restart/换源清理正确;明确Firefox受控拒绝不代表自然选择回退 | H | done | ## 5 包迁移:artplayer-plugin-ads @@ -531,6 +532,7 @@ - PKG-ASR-02: [记录](changes/2026-09-13-PKG-ASR-02-audio-baseline.md) [记录](baselines/asr-audio-validation.json) - PKG-ASR-03: [记录](changes/2026-09-13-PKG-ASR-03-audio-ownership.md) [记录](baselines/asr-ownership-validation.json) - PKG-ASR-04: [记录](changes/2026-09-13-PKG-ASR-04-public-types.md) [记录](baselines/asr-types-validation.json) [记录](baselines/asr-type-diagnostics.json) +- PKG-ASR-05: [记录](baselines/asr-volume-validation.json) [记录](baselines/asr-fallback-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) @@ -570,3 +572,4 @@ - CORE-25: [记录](changes/2026-09-13-CORE-25-defaults-ssr.md) [记录](baselines/defaults-ssr-validation.json) - ENG-12: [记录](changes/2026-09-13-ENG-12-library-public.md) [记录](baselines/library-public-validation.json) - PKG-ASR-07: [记录](changes/2026-09-13-PKG-ASR-07-playback-volume.md) [记录](baselines/asr-volume-validation.json) +- PKG-ASR-08: [记录](changes/2026-09-13-PKG-ASR-08-capture-output.md) [记录](baselines/asr-fallback-validation.json) diff --git a/refactor/progress.md b/refactor/progress.md index 7a93db30d..e06c99846 100644 --- a/refactor/progress.md +++ b/refactor/progress.md @@ -1,5 +1,20 @@ # 进度与证据 +## PKG-ASR-08 回退漏声修复完成,ASR-05 继续 + +独立审查指出 captureStream 回退额外接扬声器;Chromium 原生两个候选核心确实在 +视频静音后仍有该分支输出。已改 recorder -> gain(0) 静音 sink,非零PCM与外部 +播放图保留;五项先红后绿单测覆盖清理/恢复/换源。最终 source/main/legacy 原生 +52过、26能力跳过,含直接音量与既有采集回归;213定向及两产物各30通过,正常 +构建、strict/lint、重新隔离安装17配置通过。见[变更](changes/2026-09-13-PKG-ASR-08-capture-output.md) +与[证据](baselines/asr-fallback-validation.json)。 +Firefox自然允许重复源绑定;受控拒绝验证了原生回退,但自动外部归属仍登记为 +ASR-FOREIGN-01 open。不得把它隐藏成回退通过,也不等于 Safari/设备验收完成。 +227项:129 done、17 doing、81 todo;208风险。ASR-05已进入doing,继续真实示例、 +相邻稳定核心、CORS与插件组合/设备。子代理负责独立测试和8082示例验收,主代理 +整合源码、浏览器与提交;没有推送或发布。 + + ## PKG-ASR-07 原生音量修复完成 ASR-05 原生验证发现旧版与候选都重复应用音量:50% 设置只有约25%输出振幅。 diff --git a/refactor/risk-table.md b/refactor/risk-table.md index ec3593c27..a6f0b2b41 100644 --- a/refactor/risk-table.md +++ b/refactor/risk-table.md @@ -210,3 +210,5 @@ | ASR-CHUNK-01 | resolved / 已复现 | ASR tick discards underfilled chunks and overrun samples in source | PKG-ASR-02, PKG-ASR-03 | | ASR-LIFE-01 | resolved / 已复现 | ASR partial initialization, pause resume and stale callback cleanup need controlled evidence | PKG-ASR-02, PKG-ASR-03, PKG-ASR-05 | | ASR-VOLUME-01 | resolved / 已复现 | ASR applies media volume twice and squares playback amplitude | PKG-ASR-07 | +| ASR-FALLBACK-01 | resolved / 已复现 | Captured fallback produces a second audible route even when the video is muted | PKG-ASR-08 | +| ASR-FOREIGN-01 | open / 已复现 | Firefox permits parallel native media-element owners instead of selecting capture fallback | PKG-ASR-05 | diff --git a/refactor/risks.json b/refactor/risks.json index 6ff0fd661..e354a701e 100644 --- a/refactor/risks.json +++ b/refactor/risks.json @@ -4637,6 +4637,41 @@ "refactor/baselines/asr-volume-validation.json" ], "resolutionRationale": "Native Chromium/Firefox source/main/legacy on published and candidate core pass relative-amplitude assertions. Formal 2.1 baseline retains squared amplitude. The fix does not establish physical device or captured-fallback acceptance." + }, + { + "id": "ASR-FALLBACK-01", + "title": "Captured fallback produces a second audible route even when the video is muted", + "confirmation": "reproduced", + "status": "resolved", + "owners": [ + "PKG-ASR-08" + ], + "evidence": [ + "test/browser/asr-fallback.spec.js", + "refactor/baselines/asr-fallback-validation.json" + ], + "compatibleResolution": "Keep recognition PCM and external media ownership; remove only the extra ASR speaker output.", + "closureCriteria": "Native fallback has zero speaker output and nonzero PCM before/after stop and source change; owned tracks/contexts close without closing the external owner.", + "resolutionEvidence": [ + "refactor/changes/2026-09-13-PKG-ASR-08-capture-output.md", + "refactor/baselines/asr-fallback-validation.json" + ], + "resolutionRationale": "Native Chromium and controlled-rejection Firefox source/main/legacy with both cores pass. Firefox natural branch selection remains separate ASR-FOREIGN-01." + }, + { + "id": "ASR-FOREIGN-01", + "title": "Firefox permits parallel native media-element owners instead of selecting capture fallback", + "confirmation": "reproduced", + "status": "open", + "owners": [ + "PKG-ASR-05" + ], + "evidence": [ + "refactor/baselines/asr-fallback-validation.json", + "refactor/changes/2026-09-13-PKG-ASR-08-capture-output.md" + ], + "compatibleResolution": "Establish an explicit compatible ownership contract for external audio graphs without silently changing default direct-path PCM/mute semantics.", + "closureCriteria": "Natural Firefox external-owner integration has no duplicate output or has a documented, validated explicit ownership mode; controlled binding rejection alone cannot close this finding." } ] } diff --git a/refactor/tasks.json b/refactor/tasks.json index 45de646c5..207abb253 100644 --- a/refactor/tasks.json +++ b/refactor/tasks.json @@ -2702,13 +2702,17 @@ "dependsOn": [ "PKG-ASR-04", "CORE-22", - "PKG-ASR-07" + "PKG-ASR-07", + "PKG-ASR-08" ], - "status": "todo", + "status": "doing", "risk": "H", "deliverable": "真实 WebAudio 分块、播放暂停/切源/销毁,不引入网络 ASR", "acceptance": "最终核心与原支持范围核心分别通过;设备/SDK 缺证据不能标完成", - "evidence": [] + "evidence": [ + "baselines/asr-volume-validation.json", + "baselines/asr-fallback-validation.json" + ] }, { "id": "PKG-ASR-06", @@ -4556,6 +4560,25 @@ "changes/2026-09-13-PKG-ASR-07-playback-volume.md", "baselines/asr-volume-validation.json" ] + }, + { + "id": "PKG-ASR-08", + "phase": "5 包迁移:artplayer-plugin-asr", + "title": "修复捕获回退重复播放与静音漏声", + "scope": [ + "artplayer-plugin-asr" + ], + "dependsOn": [ + "PKG-ASR-07" + ], + "status": "done", + "risk": "H", + "deliverable": "捕获回退静音输出图、非零PCM与外部播放归属;真实红绿证据及产物验证", + "acceptance": "已进入回退后不产生额外输出、静音不漏声、外部播放图不被关闭、stop/restart/换源清理正确;明确Firefox受控拒绝不代表自然选择回退", + "evidence": [ + "changes/2026-09-13-PKG-ASR-08-capture-output.md", + "baselines/asr-fallback-validation.json" + ] } ] } diff --git a/test/asr-fallback-routing.test.js b/test/asr-fallback-routing.test.js new file mode 100644 index 000000000..00b777670 --- /dev/null +++ b/test/asr-fallback-routing.test.js @@ -0,0 +1,130 @@ +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 -- Candidate ASR fallback uses the repository runner. +import test from 'node:test' +import { asrAudioEnvironment } 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(capabilities = {}) { + const chunks = [] + const environment = asrAudioEnvironment(implementation, { + sampleRate: 8000, + interval: 1, + onAudioChunk(chunk) { + chunks.push(chunk) + return 'Captured.' + }, + }, { directError: new Error('Video belongs to another audio context'), ...capabilities }) + return { ...environment, chunks } +} + +function assertSilentCaptureRoute(environment) { + const source = environment.nodes.filter(node => node.kind === 'stream').at(-1) + const recorder = environment.recorders.at(-1) + assert.ok(source, 'The occupied media element must use its captured stream') + assert.deepEqual(source.connections, [recorder], 'Captured audio must not have a parallel speaker route') + assert.equal(recorder.connections.length, 1) + const sink = recorder.connections[0] + assert.equal(sink.kind, 'gain') + assert.equal(sink.gain.value, 0, 'The worklet output sink must stay inaudible') + assert.deepEqual(sink.connections, [environment.contexts.at(-1).destination]) + return { source, recorder, sink } +} + +async function assertCaptures(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(chunk.wav.byteLength, 60) + assert.equal(environment.layer.innerHTML, '
Captured.
') +} + +for (const moz of [false, true]) { + test(`ASR fallback ${moz ? 'mozCaptureStream' : 'captureStream'} captures PCM through an inaudible sink`, async () => { + const environment = create({ moz }) + await environment.emit('play') + assert.equal(environment.captures(), 1) + assertSilentCaptureRoute(environment) + await assertCaptures(environment) + await environment.emit('destroy') + assert.equal(environment.contexts[0].closes, 1) + assert.equal(environment.tracks[0].stops, 1) + assert.equal(environment.intervals.size, 0) + assert.ok(environment.nodes.every(node => node.connections.length === 0)) + }) +} + +test('ASR fallback pause disconnects its sink without restoring a raw speaker route', async () => { + const environment = create() + await environment.emit('play') + const retired = assertSilentCaptureRoute(environment) + await assertCaptures(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(retired.recorder.port.onmessage, null) + for (const node of Object.values(retired)) + assert.deepEqual(node.connections, []) + await environment.emit('play') + assert.equal(environment.captures(), 1) + assert.equal(environment.contexts.length, 1) + const resumed = assertSilentCaptureRoute(environment) + assert.equal(resumed.source, retired.source) + assert.notEqual(resumed.recorder, retired.recorder) + await assertCaptures(environment) + await environment.emit('destroy') + assert.equal(environment.tracks[0].stops, 1) +}) + +test('ASR fallback explicit stop releases its graph and subsequent play captures without a speaker route', async () => { + const environment = create() + await environment.emit('play') + const retired = assertSilentCaptureRoute(environment) + await environment.plugin.stop() + assert.equal(environment.contexts[0].closes, 1) + assert.equal(environment.tracks[0].stops, 1) + assert.equal(environment.intervals.size, 0) + for (const node of Object.values(retired)) + assert.deepEqual(node.connections, []) + await environment.emit('play') + assert.equal(environment.captures(), 2) + assert.equal(environment.contexts.length, 2) + assert.notEqual(assertSilentCaptureRoute(environment).source, retired.source) + await assertCaptures(environment) + await environment.emit('destroy') + assert.equal(environment.contexts[1].closes, 1) + assert.equal(environment.tracks[0].stops, 2) + assert.ok(environment.nodes.every(node => node.connections.length === 0)) +}) + +test('ASR fallback source restart replaces its capture and keeps the new graph inaudible', async () => { + const environment = create() + await environment.emit('play') + const retired = assertSilentCaptureRoute(environment) + await environment.emit('restart') + 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) + assert.equal(retired.recorder.port.onmessage, null) + for (const node of Object.values(retired)) + assert.deepEqual(node.connections, []) + assert.notEqual(assertSilentCaptureRoute(environment).source, retired.source) + await assertCaptures(environment) + await environment.emit('destroy') + assert.equal(environment.contexts[1].closes, 1) + assert.equal(environment.tracks[0].stops, 2) + assert.equal(environment.intervals.size, 0) +}) diff --git a/test/browser/asr-fallback.spec.js b/test/browser/asr-fallback.spec.js new file mode 100644 index 000000000..47fdbad1c --- /dev/null +++ b/test/browser/asr-fallback.spec.js @@ -0,0 +1,173 @@ +import fs from 'node:fs' +import process from 'node:process' +import { verifyAsrContract } from '../../refactor/scripts/asr-contract.mjs' +import { hash, readMember } from '../../refactor/scripts/releases.mjs' +import { compilePackage } from '../helpers/load.js' +import { expect, test } from './fixtures.js' + +let contract +let candidateCode +test.beforeAll(async () => { + contract = await verifyAsrContract() + candidateCode = process.env.ARTPLAYER_ASR_ARTIFACT ? fs.readFileSync(process.env.ARTPLAYER_ASR_ARTIFACT) : await compilePackage('artplayer-plugin-asr', 'umd') +}) + +for (const version of ['2.1.0', 'candidate']) { + for (const core of ['published', 'candidate']) { + test(`ASR ${version} + ${core}: native capture fallback does not create a second audible route`, async ({ page }, testInfo) => { + const code = version === 'candidate' ? candidateCode : readMember(contract.archives.get(version), 'package/dist/artplayer-plugin-asr.js') + await page.goto(`/test/player.html?core=${core}`) + const capabilities = await page.evaluate(() => ({ context: typeof AudioContext, worklet: typeof AudioWorkletNode, capture: typeof HTMLMediaElement.prototype.captureStream, mozCapture: typeof HTMLMediaElement.prototype.mozCaptureStream })) + const forceRejection = testInfo.project.name === 'firefox' + await testInfo.attach('asr-fallback-inputs', { contentType: 'application/json', body: JSON.stringify({ version, core, pluginSha256: hash(code), capabilities, forcedException: forceRejection, reason: forceRejection ? 'Firefox permits a second native media source; force only binding rejection to reach real captureStream/Worklet' : 'Native duplicate binding rejection', externalContext: 'Native media-element source remains owned and running', physicalOutput: false }) }) + if (capabilities.context !== 'function') { + expect(process.platform).toBe('win32') + expect(testInfo.project.name).toBe('webkit') + test.skip(true, 'Windows WebKit has no native WebAudio') + } + expect(capabilities.worklet).toBe('function') + expect([capabilities.capture, capabilities.mozCapture]).toContain('function') + await page.addScriptTag({ content: code.toString() }) + await page.evaluate((forceRejection) => { + const NativeContext = window.AudioContext + window.asrContexts = [] + window.asrMeters = [] + window.asrStreams = [] + window.asrChunks = [] + window.asrDirectFailures = [] + window.AudioContext = class extends NativeContext { + constructor(option) { + super(option) + window.asrContexts.push(this) + } + + createMediaElementSource(video) { + try { + if (forceRejection) + throw new DOMException('Controlled duplicate binding rejection for native capture test', 'InvalidStateError') + return super.createMediaElementSource(video) + } + catch (error) { + window.asrDirectFailures.push({ name: error.name, message: error.message }) + throw error + } + } + + createGain() { + const gain = super.createGain() + const analyser = super.createAnalyser() + analyser.fftSize = 2048 + const connect = gain.connect.bind(gain) + const disconnect = gain.disconnect.bind(gain) + gain.connect = (destination, ...args) => { + if (destination === this.destination) { + connect(analyser) + analyser.connect(destination) + return destination + } + return connect(destination, ...args) + } + gain.disconnect = (...args) => { + analyser.disconnect() + return disconnect(...args) + } + window.asrMeters.push(analyser) + return gain + } + } + window.art = new window.Artplayer({ + container: '.player', + url: '/test/audio-tone.m4a', + volume: 1, + loop: true, + plugins: [window.artplayerPluginAsr({ onAudioChunk({ pcm }) { + const samples = new Int16Array(pcm) + const max = samples.reduce((value, sample) => Math.max(value, Math.abs(sample)), 0) + window.asrChunks.push({ max, source: window.art.video.currentSrc, samples: samples.length }) + } })], + }) + const capture = window.art.video.captureStream || window.art.video.mozCaptureStream + window.art.video.captureStream = function () { + const stream = capture.call(this) + window.asrStreams.push({ stream, tracks: stream.getTracks() }) + return stream + } + window.externalContext = new NativeContext({ sampleRate: 16000 }) + const source = window.externalContext.createMediaElementSource(window.art.video) + window.externalMeter = window.externalContext.createAnalyser() + window.externalMeter.fftSize = 2048 + source.connect(window.externalMeter) + window.externalMeter.connect(window.externalContext.destination) + document.querySelector('#play').onclick = async () => { + await window.externalContext.resume() + await window.art.play() + } + window.readFallback = () => { + const rms = (analyser) => { + const samples = new Float32Array(analyser.fftSize) + analyser.getFloatTimeDomainData(samples) + return Math.sqrt(samples.reduce((sum, value) => sum + value * value, 0) / samples.length) + } + return { external: rms(window.externalMeter), asr: window.asrMeters.map(rms), contexts: window.asrContexts.map(c => c.state), owner: window.externalContext.state, streams: window.asrStreams.map(s => s.tracks.map(t => ({ kind: t.kind, state: t.readyState }))), chunks: window.asrChunks.slice(-3), failures: window.asrDirectFailures } + } + }, forceRejection) + await expect.poll(() => page.evaluate(() => window.art.isReady)).toBe(true) + await page.locator('#play').click() + await expect.poll(() => page.evaluate(() => window.asrChunks.filter(c => c.max > 0).length)).toBeGreaterThanOrEqual(3) + const playing = await page.evaluate(() => window.readFallback()) + await page.evaluate(() => { + window.art.muted = true + }) + await page.waitForTimeout(450) + const muted = await page.evaluate(() => window.readFallback()) + await testInfo.attach('asr-fallback-output', { contentType: 'application/json', body: JSON.stringify({ playing, muted }) }) + expect(playing.failures.length).toBe(1) + expect(playing.external).toBeGreaterThan(0.01) + expect(muted.external).toBe(0) + if (version === '2.1.0') { + expect(playing.asr.at(-1)).toBeGreaterThan(0.01) + expect(muted.asr.at(-1)).toBeGreaterThan(0.01) + } + else { + expect(playing.asr.at(-1)).toBe(0) + expect(muted.asr.at(-1)).toBe(0) + } + expect(muted.chunks.every(chunk => chunk.max > 0)).toBe(true) + await page.evaluate(() => { + window.art.muted = false + }) + await page.evaluate(() => window.art.plugins.artplayerPluginAsr.stop()) + await page.waitForTimeout(200) + const stopped = await page.evaluate(() => window.readFallback()) + expect(stopped.contexts).toEqual(['closed']) + expect(stopped.owner).toBe('running') + expect(stopped.external).toBeGreaterThan(0.01) + expect(stopped.streams.flat().every(t => t.state === 'ended')).toBe(true) + await testInfo.attach('asr-fallback-stop', { contentType: 'application/json', body: JSON.stringify(stopped) }) + if (version === 'candidate') { + const count = await page.evaluate(() => window.asrChunks.length) + await page.evaluate(() => window.art.pause()) + await page.locator('#play').click() + await expect.poll(() => page.evaluate(() => window.asrChunks.filter(c => c.max > 0).length)).toBeGreaterThan(count + 2) + expect(await page.evaluate(() => window.asrStreams[1].tracks.every(track => !window.asrStreams[0].tracks.includes(track)))).toBe(true) + await page.evaluate(() => { + window.art.url = '/test/audio-tone.m4a?asr=fallback-second' + }) + await expect.poll(() => page.evaluate(() => window.art.video.currentSrc.includes('fallback-second') && window.art.video.readyState >= 2)).toBe(true) + await page.locator('#play').click() + await expect.poll(() => page.evaluate(() => window.asrChunks.filter(c => c.source.includes('fallback-second') && c.max > 0).length)).toBeGreaterThanOrEqual(3) + const restarted = await page.evaluate(() => window.readFallback()) + expect(restarted.owner).toBe('running') + expect(restarted.contexts).toEqual(['closed', 'closed', 'running']) + expect(restarted.asr.at(-1)).toBe(0) + expect(restarted.streams.slice(0, -1).flat().every(track => track.state === 'ended')).toBe(true) + expect(restarted.streams.at(-1).every(track => track.state === 'live')).toBe(true) + await testInfo.attach('asr-fallback-restart', { contentType: 'application/json', body: JSON.stringify(restarted) }) + } + await page.evaluate(() => window.art.destroy()) + await expect.poll(() => page.evaluate(() => window.asrContexts.every(context => context.state === 'closed'))).toBe(true) + expect(await page.evaluate(() => window.externalContext.state)).toBe('running') + await page.evaluate(() => window.externalContext.close()) + }) + } +} diff --git a/test/helpers/asr-audio.js b/test/helpers/asr-audio.js index 02ccd129a..a89c7a58f 100644 --- a/test/helpers/asr-audio.js +++ b/test/helpers/asr-audio.js @@ -33,8 +33,8 @@ export function asrAudioEnvironment(implementation, option = {}, capabilities = let nextUrl = 0 let captures = 0 - function node(kind) { - const value = { + function node(kind, value = {}) { + Object.assign(value, { kind, connections: [], disconnects: 0, @@ -43,7 +43,7 @@ export function asrAudioEnvironment(implementation, option = {}, capabilities = this.disconnects++ this.connections = [] }, - } + }) nodes.push(value) return value } @@ -105,7 +105,8 @@ export function asrAudioEnvironment(implementation, option = {}, capabilities = assert.ok(contexts.includes(context)) assert.equal(context.state, 'running') assert.equal(name, 'recorder-processor') - Object.assign(this, node('recorder'), { port: { onmessage: null } }) + node('recorder', this) + this.port = { onmessage: null } recorders.push(this) } }