refactor(asr): [PKG-ASR-03] own typed audio capture and playback lifetimes

This commit is contained in:
Harvey Zhao committed 2026-09-13 14:44:23 +08:00
1 parent 53c295819d
commit 0cf2af4840
33 files changed
+3492 -809

No files matched your search

+1 -1
View File
@@ -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";if("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 function(t={}){const{length:e=3,interval:n=100,sampleRate:r=16e3,autoHideTimeout:a=1e4,onAudioChunk:o=()=>null}=t;return t=>{let i=!1,l=null,s=null,u=null,c=null,d=[],p=null,f=!1,y=null,m=null,g=null;const h=t.layers.add({name:"asr",html:""});function w(){h.style.display="none"}function b(t){var n;"string"===typeof t&&(clearTimeout(y),y=setTimeout(w,a),h.style.display="",h.innerHTML=(n=t,n.split(/(?<=[\u3001\u3002\uff01\uff1f!?.])\s*/u).map(t=>t.trim()).filter(Boolean).slice(-e)).map(t=>`<div class="art-asr-line">${t}</div>`).join(""))}async function v(){if(!s&&t.video)try{return s=l.createMediaElementSource(t.video),s}catch(e){console.warn("[artplayerPluginAsr] Direct connection failed:",e)}try{if(m||(m=await async function(t){try{return t.captureStream?t.captureStream():t.mozCaptureStream?t.mozCaptureStream():(console.warn("[artplayerPluginAsr] captureStream not supported"),null)}catch(e){return console.warn("[artplayerPluginAsr] Error capturing stream:",e),null}}(t.video)),m)return g=l.createMediaStreamSource(m),g}catch(e){console.warn("[artplayerPluginAsr] MediaStream fallback failed:",e)}return null}async function x(){i&&(i=!1,clearInterval(p),p=null,u&&(u.disconnect(),u.port.onmessage=null,u=null),c&&(c.disconnect(),c=null),d=[])}async function A(){await x(),g&&(g.disconnect(),g=null),s&&(s.disconnect(),s=null),m&&(m.getTracks().forEach(t=>t.stop()),m=null),l&&(await l.close(),l=null),f=!1}return t.on("video:volumechange",()=>{c&&(c.gain.value=t.volume)}),t.on("play",async function(){if(!i)try{await async function(){return l||(l=new(window.AudioContext||window.webkitAudioContext)({sampleRate:r}),"suspended"===l.state&&await l.resume(),l)}();const t=await v();if(!t)throw new Error("Could not establish audio source");if(!f){const t=(()=>{const t=new Blob(["\n class 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 }\n registerProcessor('recorder-processor', RecorderProcessor);\n "],{type:"application/javascript"});return URL.createObjectURL(t)})();await l.audioWorklet.addModule(t),URL.revokeObjectURL(t),f=!0}c=l.createGain(),c.gain.value=1,u=new AudioWorkletNode(l,"recorder-processor"),u.port.onmessage=t=>{d.push(new Float32Array(t.data))},t.connect(u),t.connect(c),c.connect(l.destination);const e=r*(n/1e3);p=setInterval(async()=>{if(0===d.length)return;let t=new Float32Array(0);for(;t.length<e&&d.length;){const e=d.shift(),n=new Float32Array(t.length+e.length);n.set(t,0),n.set(e,t.length),t=n}if(t.length<e)return;const n=function(t){const e=t.length,n=new ArrayBuffer(2*e),r=new DataView(n);for(let a=0;a<e;a++){const e=Math.max(-1,Math.min(1,t[a]));r.setInt16(2*a,e<0?32768*e:32767*e,!0)}return n}(t.slice(0,e)),a=function(t,e){const n=t.byteLength,r=new ArrayBuffer(44+n),a=new DataView(r),o=(t,e)=>{for(let n=0;n<e.length;n++)a.setUint8(t+n,e.charCodeAt(n))};return o(0,"RIFF"),a.setUint32(4,36+n,!0),o(8,"WAVE"),o(12,"fmt "),a.setUint32(16,16,!0),a.setUint16(20,1,!0),a.setUint16(22,1,!0),a.setUint32(24,e,!0),a.setUint32(28,2*e,!0),a.setUint16(32,2,!0),a.setUint16(34,16,!0),o(36,"data"),a.setUint32(40,n,!0),new Uint8Array(r).set(new Uint8Array(t),44),r}(n,r);b(await o({pcm:n,wav:a}))},n),i=!0}catch(t){console.error("[artplayerPluginAsr] Initialization failed:",t),await x()}}),t.on("pause",x),t.on("destroy",A),{name:"artplayerPluginAsr",stop:A,hide:w,append:b}}}});
!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 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)}volume(t){this.gain&&(this.gain.gain.value=t)}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&&i<t;){const s=this.head.samples,n=Math.min(s.length-this.offset,t-i);e.set(s.subarray(this.offset,this.offset+n),i),i+=n,this.offset+=n,this.offset===s.length&&(this.head=this.head.next,this.offset=0)}return this.head||(this.tail=void 0),this.size-=t,e}clear(){this.head=void 0,this.tail=void 0,this.offset=0,this.size=0}}class i{constructor(t,i,s){this.video=t,this.options=i,this.append=s,this.queue=new e,this.closing=Promise.resolve(),this.running=!1,this.terminal=!1,this.epoch=0}reset(){this.epoch++,this.pending=void 0,this.queue.clear()}pause(){this.reset(),this.running=!1,clearInterval(this.timer),this.timer=void 0,this.starting=void 0,this.graph?.idle()}restart(){const t=this.running||Boolean(this.starting);return this.pause(),this.graph&&!this.graph.ownsMediaConnection?this.stop().then(()=>t?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,n=Math.floor(e*s/1e3);if(!Number.isSafeInteger(n)||n<1||!Number.isFinite(s)||s<=0)throw new Error("Audio chunk length must be positive and finite");const r=this.graph??(this.graph=new t(this.video,e));await r.prepare(),i()?(r.connect(t=>{if(this.running&&this.graph===r)return this.queue.length+t.length>Math.max(60*e,2*n)?(console.error("[artplayerPluginAsr] Audio callback backlog exceeded its capture limit"),void this.pause()):void this.queLine truncated