/* Run only on a local teaching page at http://127.0.0.1 or http://localhost. Own canvas + oscillator input, no microphone, no remote fetch, no real voice. Decoding support is browser/format dependent; this does not promise all video formats. The output is decoded/re-encoded PCM16 WAV, not a lossless copy of compressed bytes. */ function requireTeachingOrigin() { if (!['127.0.0.1','localhost'].includes(location.hostname)) throw new Error('Use a dedicated local teaching page'); } async function teachingBytesHash(bytes) { return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),x=>x.toString(16).padStart(2,'0')).join(''); } function teachingPcm16Wav(buffer) { const channels=buffer.numberOfChannels,frames=buffer.length; if (![1,2].includes(channels)) throw new Error('This example supports only mono/stereo'); const bytes=new ArrayBuffer(44+frames*channels*2),d=new DataView(bytes); const text=(offset,s)=>{for(let i=0;ibuffer.getChannelData(c));let pos=44; for(let n=0;n8*1024*1024) throw new Error('Nonempty Blob up to 8 MiB required; actual bytes='+blob?.size); const input=await blob.arrayBuffer(),inputSha256=await teachingBytesHash(input); const decoder=new OfflineAudioContext(1,1,16000); const decoded=await decoder.decodeAudioData(input.slice(0)); const wavBlob=teachingPcm16Wav(decoded),wav=await wavBlob.arrayBuffer(),d=new DataView(wav); if (d.getUint32(4,true)+8!==wav.byteLength||d.getUint32(40,true)!==decoded.length*decoded.numberOfChannels*2)throw new Error('WAV byte contract mismatch'); let squares=0,peak=0; for(let i=44;i{diagnostics.events.push({kind:'data',bytes:e.data.size});if(e.data.size)chunks.push(e.data)}; let timer; try { await audio.resume();diagnostics.audioState=audio.state;diagnostics.trackStates=mixed.getTracks().map(t=>({kind:t.kind,readyState:t.readyState,muted:t.muted}));osc.start(); const stopped=new Promise((resolve,reject)=>{recorder.onstop=resolve;recorder.onerror=e=>reject(new Error(e.error?.message||'recording failed'))}); recorder.start(100);let frame=0; timer=setInterval(()=>{ctx.fillStyle=frame++%2?'#A39BDB':'#4290C6';ctx.fillRect(0,0,160,90)},100); await new Promise(resolve=>setTimeout(resolve,1200));recorder.stop();await stopped; diagnostics.recorderState=recorder.state;diagnostics.chunks=chunks.length; const videoBlob=new Blob(chunks,{type:recorder.mimeType});diagnostics.videoBytes=videoBlob.size; const video=document.createElement('video'),url=URL.createObjectURL(videoBlob);video.muted=true;video.preload='metadata'; let videoMetadata; try { await new Promise((resolve,reject)=>{const timeout=setTimeout(()=>reject(new Error('encoded video metadata timeout')),5000);video.onloadedmetadata=()=>{clearTimeout(timeout);resolve()};video.onerror=()=>{clearTimeout(timeout);reject(new Error('encoded video metadata failed'))};video.src=url;video.load()}); videoMetadata={width:video.videoWidth,height:video.videoHeight,containerDurationSeconds:Number.isFinite(video.duration)?video.duration:null,unknownDurationIsNotZero:true,playbackPerformed:false}; } finally {video.removeAttribute('src');video.load();URL.revokeObjectURL(url)} const result=await decodeTeachingVideoBlob(videoBlob);result.report.encodedVideoMetadata=videoMetadata; result.report.fixture={kind:'own alternating-color canvas plus 440-Hz oscillator',recordedVideoTracks:mixed.getVideoTracks().length,recordedAudioTracks:mixed.getAudioTracks().length,requestedRecordingMilliseconds:1200,recordingMime:recorder.mimeType,actualCapturedDurationNotGuaranteedByTimer:true,microphoneUsed:false,zeroGainSpeakerSinkConnected:true,audibleSignalSentToSpeakers:false,realSpeech:false}; let invalidRejected=false; try{await decodeTeachingVideoBlob(new Blob([new Uint8Array([1,2,3,4])],{type:'video/webm'}))}catch(e){invalidRejected=true} result.report.invalidFourByteContainerRejected=invalidRejected; return result; } finally { clearInterval(timer);if(recorder.state!=='inactive')recorder.stop(); osc.stop();mixed.getTracks().forEach(t=>t.stop());visual.getTracks().forEach(t=>t.stop());await audio.close();canvas.remove(); } } // Explicit local use from a real button click: // button.onclick=async()=>{ window.localVideoAudioResult=await teachingVideoAudioExtractDemo(); console.log(window.localVideoAudioResult.report); }; // localVideoAudioResult.wavBlob contains the generated WAV; this script never auto-uploads or auto-downloads it.