// Independent cleanup attempts: a broken optional recorder or audio node must // never keep the camera/microphone open or prevent the remaining owners closing. const attempt=operation=>{try{operation();}catch{/* Continue releasing other owners. */}}; export function stopTracks(stream) { if(!stream)return; for(const track of stream.getTracks())attempt(()=>{track.onended=null;track.stop();}); } export function releaseCapture(run,preview,finishRecording) { if(run.released)return; run.released=true; attempt(()=>stopTracks(run.stream)); for(const timer of [run.timer,run.setupTimer,run.signalTimer])clearTimeout(timer); clearInterval(run.updateTimer); if(run.callback!=null)attempt(()=>preview.cancelVideoFrameCallback(run.callback)); if(run.recording)attempt(finishRecording); if(run.node) { run.node.onprocessorerror=null; run.node.port.onmessage=null; attempt(()=>run.node.port.postMessage('stop')); attempt(()=>run.node.disconnect()); } attempt(()=>run.source?.disconnect()); // A rejected or unresolved AudioContext.close never postpones track release. attempt(()=>run.context?.close().catch(()=>{})); }