s)break;if(l.nextFragment){u.pos=l.nextFragment.moofOffset+l.nextFragment.moofSize,l=l.nextFragment;continue}}await u.reader.loadRange(u.pos,u.pos+16);let p=u.pos,x=u.readBoxHeader();if(x.name==="moof"){let w=z(c.fragments,p,y=>y.moofOffset);if(w===-1){u.pos=p;let y=await c.readFragment();l&&(l.nextFragment=y),l=y;let{fragmentIndex:N,sampleIndex:Z,correctSampleFound:V}=e();if(V){let A=this.internalTrack.fragments[N];return this.fetchChunkInFragment(A,Z)}N!==-1&&(g=N,h=Z)}else{let y=c.fragments[w];l&&(l.nextFragment=y),l=y}}u.pos=p+x.totalSize}if(g!==-1){let p=this.internalTrack.fragments[g];return this.fetchChunkInFragment(p,h)}return null}finally{o()}}},Tt=class extends We{internalTrack;constructor(e){super(e),this.internalTrack=e}async getCodec(){return this.internalTrack.info.codec}async getNumberOfChannels(){return this.internalTrack.info.numberOfChannels}async getSampleRate(){return this.internalTrack.info.sampleRate}async getDecoderConfig(){return{codec:Gt(this.internalTrack.info.codec,this.internalTrack.info.codecDescription),numberOfChannels:this.internalTrack.info.numberOfChannels,sampleRate:this.internalTrack.info.sampleRate,description:this.internalTrack.info.codecDescription??void 0}}},ir=(r,e)=>{let t=I(r.presentationTimestamps,e,s=>s.presentationTimestamp);return t===-1?-1:r.presentationTimestamps[t].sampleIndex},pi=(r,e)=>{let t=I(r.sampleTimingEntries,e,p=>p.startIndex),s=r.sampleTimingEntries[t];if(!s||s.startIndex+s.count<=e)return null;let a=s.startDecodeTimestamp+(e-s.startIndex)*s.delta,n=I(r.sampleCompositionTimeOffsets,e,p=>p.startIndex),c=r.sampleCompositionTimeOffsets[n];c&&(a+=c.offset);let o=r.sampleSizes[Math.min(e,r.sampleSizes.length-1)],u=I(r.sampleToChunk,e,p=>p.startSampleIndex),d=r.sampleToChunk[u];m(d);let l=d.startChunkIndex+Math.floor((e-d.startSampleIndex)/d.samplesPerChunk),g=r.chunkOffsets[l],h=0,b=g;if(r.sampleSizes.length===1)b+=o*(e-d.startSampleIndex),h+=o*d.samplesPerChunk;else{let p=d.startSampleIndex+(l-d.startChunkIndex)*d.samplesPerChunk;for(let x=p;xp)!==-1:!0}},hi=(r,e)=>{if(!r.keySampleIndices)return e;let t=I(r.keySampleIndices,e,s=>s);return r.keySampleIndices[t]??-1},bi=(r,e)=>{if(!r.keySampleIndices)return e+1;let t=I(r.keySampleIndices,e,s=>s);return r.keySampleIndices[t+1]??-1},sr=(r,e)=>{r.startTimestamp+=e,r.endTimestamp+=e;for(let t of r.samples)t.presentationTimestamp+=e;for(let t of r.presentationTimestamps)t.presentationTimestamp+=e};var He=class extends ce{};var Le=class{},xt=class extends Le{async _canReadInput(e){if(await e._mainReader.source._getSize()<8)return!1;let s=new Y(e._mainReader);return s.pos=4,s.readAscii(4)==="ftyp"}_createDemuxer(e){return new Ne(e)}},St=class extends Le{async _canReadInput(){return!1}_createDemuxer(e){return new He(e)}},$e=new xt,gi=$e,ki=$e,Ct=new St,ar=Ct,Ti=Ct,xi=[$e,ar];var wt=class{_source;_formats;_mainReader;_demuxerPromise=null;_format=null;constructor(e){this._formats=e.formats,this._source=e.source,this._mainReader=new ue(e.source)}_getDemuxer(){return this._demuxerPromise??=(async()=>{await this._mainReader.loadRange(0,4096);for(let e of this._formats)if(await e._canReadInput(this))return this._format=e,e._createDemuxer(this);throw new Error("Input has an unrecognizable format.")})()}async getFormat(){return await this._getDemuxer(),m(this._format),this._format}async getDuration(){return(await this._getDemuxer()).getDuration()}async getTracks(){return(await this._getDemuxer()).getTracks()}async getVideoTracks(){return(await this.getTracks()).filter(t=>t.isVideoTrack())}async getPrimaryVideoTrack(){return(await this.getTracks()).find(t=>t.isVideoTrack())??null}async getAudioTracks(){return(await this.getTracks()).filter(t=>t.isAudioTrack())}async getPrimaryAudioTrack(){return(await this.getTracks()).find(t=>t.isAudioTrack())??null}async getMimeType(){return(await this._getDemuxer()).getMimeType()}};var Qe=class{constructor(e){this.videoTrack=e}getFirstChunk(){return this.videoTrack._backing.getFirstChunk()}getChunk(e){return this.videoTrack._backing.getChunk(e)}getNextChunk(e){return this.videoTrack._backing.getNextChunk(e)}getKeyChunk(e){return this.videoTrack._backing.getKeyChunk(e)}getNextKeyChunk(e){return this.videoTrack._backing.getNextKeyChunk(e)}async*chunks(e,t=1/0){let s=[],{promise:i,resolve:a}=j(),{promise:n,resolve:c}=j(),o=!1,u=[],d=()=>Math.max(2,u.length);(async()=>{let l=e??await this.getFirstChunk();for(;l&&!o&&!(l.timestamp/1e6>=t);){if(s.length>d()){({promise:n,resolve:c}=j()),await n;continue}s.push(l),a(),{promise:i,resolve:a}=j(),l=await this.getNextChunk(l)}o=!0,a()})();try{for(;;)if(s.length>0){yield s.shift();let l=performance.now();for(u.push(l);u.length>0&&l-u[0]>=1e3;)u.shift();c()}else if(!o)await i;else break}finally{o=!0,c()}}},yt=class{constructor(e){this.videoTrack=e}decoderConfig=null;async createDecoder(e){this.decoderConfig||(this.decoderConfig=await this.videoTrack.getDecoderConfig());let t=new VideoDecoder({output:e,error:s=>console.error(s)});return t.configure(this.decoderConfig),t}async getKeyFrame(e){let t=null,s=await this.createDecoder(a=>t=a),i=await this.videoTrack._backing.getKeyChunk(e);return i?(s.decode(i),await s.flush(),s.close(),t):null}async getFrame(e){let t=null,s=await this.createDecoder(c=>{c.timestamp/1e6<=e?(t?.close(),t=c):c.close()}),i=await this.videoTrack._backing.getKeyChunk(e);if(!i)return null;let a=await this.videoTrack._backing.getChunk(e);m(a),s.decode(i);let n=i;for(;n!==a;){let c=await this.videoTrack._backing.getNextChunk(n);m(c),n=c,s.decode(c),s.decodeQueueSize>=10&&await new Promise(o=>s.addEventListener("dequeue",o,{once:!0}))}return await s.flush(),s.close(),t}async*frames(e=0,t=1/0){let s=[],i=!1,a=null,{promise:n,resolve:c}=j(),o=!1,u=await this.createDecoder(g=>{let h=g.timestamp/1e6;if(h>=t&&(o=!0),o){g.close();return}a&&(h>e?(s.push(a),i=!0):a.close()),h>=e&&(s.push(g),i=!0),a=i?null:g,s.length>0&&(c(),{promise:n,resolve:c}=j())}),d=await this.videoTrack._backing.getKeyChunk(e)??await this.videoTrack._backing.getFirstChunk();if(!d)return;let l=!1;(async()=>{let g=d,h=1/0;if(t<1/0){let x=await this.videoTrack._backing.getChunk(t),w=x?x.type==="key"&&x.timestamp/1e6===t?x:await this.videoTrack._backing.getNextKeyChunk(x):null;w&&(h=w.timestamp/1e6)}let p=new Qe(this.videoTrack).chunks(d,h);for(await p.next();g&&!o;){u.decode(g),u.decodeQueueSize>=10&&await new Promise(w=>u.addEventListener("dequeue",w,{once:!0}));let x=await p.next();if(x.done)break;g=x.value}await p.return(),await u.flush(),u.close(),!i&&a&&s.push(a),l=!0,c()})();try{for(;;)if(s.length>0)yield s.shift();else if(!l)await n;else break}finally{o=!0}}};export{xi as ALL_FORMATS,ie as AUDIO_CODECS,bt as ArrayBufferSource,pe as ArrayBufferTarget,lt as AudioBufferSource,dt as AudioDataSource,D as AudioSource,gt as BlobSource,ct as CanvasSource,mt as EncodedAudioChunkSource,Qe as EncodedVideoChunkDrain,nt as EncodedVideoChunkSource,$e as ISOBMFF,wt as Input,Ct as MATROSKA,ar as MKV,ki as MOV,gi as MP4,ne as MediaSource,ft as MediaStreamAudioTrackSource,ut as MediaStreamVideoTrackSource,Ue as MkvOutputFormat,at as Mp4OutputFormat,ht as Output,G as OutputFormat,Ve as SUBTITLE_CODECS,Te as Source,tt as StreamTarget,oe as SubtitleSource,X as Target,pt as TextSubtitleSource,re as VIDEO_CODECS,yt as VideoFrameDrain,ot as VideoFrameSource,P as VideoSource,Ti as WEBM,ae as WebMOutputFormat};
+${t.notes??""}`,l=this.createInternalChunk(u,a,t.duration,"key",m.trim()?M.encode(m):null);n.chunkQueue.push(l),await this.interleaveChunks()}finally{s()}}async interleaveChunks(){for(let e of this.output._tracks)if(!e.source._closed&&!this.trackDatas.some(t=>t.track===e))return;e:for(;;){let e=null,t=1/0;for(let s of this.trackDatas){if(s.chunkQueue.length===0&&!s.track.source._closed)break e;s.chunkQueue.length>0&&s.chunkQueue[0].timestamp=2&&r++;let o={rgb:7,bt709:2,bt470bg:1,smpte170m:3}[e.info.decoderConfig.colorSpace.matrix];Et(t.data,r+0,r+3,o)}createInternalChunk(e,t,r,s,n=null){return{data:e,type:s,timestamp:t,duration:r,additions:n}}writeBlock(e,t){this.segment||(this.createTracks(),this.createSegment());let r=Math.floor(1e3*t.timestamp),s=this.trackDatas.every(m=>{if(m.track.source._closed)return!0;if(e===m)return t.type==="key";let l=m.chunkQueue[0];return l&&l.type==="key"});(!this.currentCluster||s&&r-this.currentClusterMsTimestamp>=1e3)&&this.createNewCluster(r);let n=r-this.currentClusterMsTimestamp;if(n<0)return;if(n>=ut)throw new Error(`Current Matroska cluster exceeded its maximum allowed length of ${ut} milliseconds. In order to produce a correct WebM file, you must pass in a key frame at least every ${ut} milliseconds.`);let c=new Uint8Array(4),o=new DataView(c.buffer);o.setUint8(0,128|e.track.id),o.setInt16(1,n,!1);let u=Math.floor(1e3*t.duration);if(u===0&&!t.additions){o.setUint8(3,+(t.type==="key")<<7);let m={id:163,data:[c,t.data]};this.writeEBML(m)}else{let m={id:160,data:[{id:161,data:[c,t.data]},t.type==="delta"?{id:251,data:new ke(e.lastWrittenMsTimestamp-r)}:null,t.additions?{id:30113,data:[{id:166,data:[{id:165,data:t.additions},{id:238,data:1}]}]}:null,u>0?{id:155,data:u}:null]};this.writeEBML(m)}this.duration=Math.max(this.duration,r+u),e.lastWrittenMsTimestamp=r,this.trackDatasInCurrentCluster.add(e)}createNewCluster(e){this.currentCluster&&!this.format._options.streamable&&this.finalizeCurrentCluster(),this.currentCluster={id:524531317,size:this.format._options.streamable?-1:cr,data:[{id:231,data:e}]},this.writeEBML(this.currentCluster),this.currentClusterMsTimestamp=e,this.trackDatasInCurrentCluster.clear()}finalizeCurrentCluster(){d(this.currentCluster);let e=this.writer.getPos()-this.dataOffsets.get(this.currentCluster),t=this.writer.getPos();this.writer.seek(this.offsets.get(this.currentCluster)+4),this.writeEBMLVarInt(e,cr),this.writer.seek(t);let r=this.offsets.get(this.currentCluster)-this.segmentDataOffset;d(this.cues),this.cues.data.push({id:187,data:[{id:179,data:this.currentClusterMsTimestamp},...[...this.trackDatasInCurrentCluster].map(s=>({id:183,data:[{id:247,data:s.track.id},{id:241,data:r}]}))]})}async onTrackClose(){let e=await this.mutex.acquire();await this.interleaveChunks(),e()}async finalize(){let e=await this.mutex.acquire();this.segment||(this.createTracks(),this.createSegment());for(let t of this.trackDatas)for(;t.chunkQueue.length>0;)this.writeBlock(t,t.chunkQueue.shift());if(!this.format._options.streamable&&this.currentCluster&&this.finalizeCurrentCluster(),d(this.cues),this.writeEBML(this.cues),!this.format._options.streamable){let t=this.writer.getPos(),r=this.writer.getPos()-this.segmentDataOffset;this.writer.seek(this.offsets.get(this.segment)+4),this.writeEBMLVarInt(r,or),this.segmentDuration.data=new ne(this.duration),this.writer.seek(this.offsets.get(this.segmentDuration)),this.writeEBML(this.segmentDuration),this.seekHead.data[0].data[1].data=this.offsets.get(this.cues)-this.segmentDataOffset,this.seekHead.data[1].data[1].data=this.offsets.get(this.segmentInfo)-this.segmentDataOffset,this.seekHead.data[2].data[1].data=this.offsets.get(this.tracksElement)-this.segmentDataOffset,this.writer.seek(this.offsets.get(this.seekHead)),this.writeEBML(this.seekHead),this.writer.seek(t)}e()}};var Y=class{},mt=class extends Y{_options;constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.fastStart!==void 0&&![!1,"in-memory","fragmented"].includes(e.fastStart))throw new TypeError('options.fastStart, when provided, must be false, "in-memory", or "fragmented".');super(),this._options=e}_createMuxer(e){return new De(e,this)}},Ue=class extends Y{_options;constructor(e={}){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(e.streamable!==void 0&&typeof e.streamable!="boolean")throw new TypeError("options.streamable, when provided, must be a boolean.");super(),this._options=e}_createMuxer(e){return new ze(e,this)}},ae=class extends Ue{};var oe=class{_connectedTrack=null;_closed=!1;_offsetTimestamps=!1;_ensureValidDigest(){if(!this._connectedTrack)throw new Error("Cannot call digest without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call digest before output has been started.");if(this._connectedTrack.output._finalizing)throw new Error("Cannot call digest after output has started finalizing.");if(this._closed)throw new Error("Cannot call digest after source has been closed.")}_start(){}async _flush(){}close(){if(this._closed)throw new Error("Source already closed.");if(!this._connectedTrack)throw new Error("Cannot call close without connecting the source to an output track.");if(!this._connectedTrack.output._started)throw new Error("Cannot call close before output has been started.");this._closed=!0,!this._connectedTrack.output._finalizing&&this._connectedTrack.output._muxer.onTrackClose(this._connectedTrack)}},U=class extends oe{_connectedTrack=null;_codec;constructor(e){if(super(),!ie.includes(e))throw new TypeError(`Invalid video codec '${e}'. Must be one of: ${ie.join(", ")}.`);this._codec=e}},dt=class extends U{constructor(e){super(e)}digest(e,t){if(!(e instanceof EncodedVideoChunk))throw new TypeError("chunk must be an EncodedVideoChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedVideoChunk(this._connectedTrack,e,t)}},bi=5,ki=i=>{if(!i||typeof i!="object")throw new TypeError("Codec config must be an object.");if(!ie.includes(i.codec))throw new TypeError(`Invalid video codec '${i.codec}'. Must be one of: ${ie.join(", ")}.`);if(!Number.isInteger(i.bitrate)||i.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.");if(i.latencyMode!==void 0&&!["quality","realtime"].includes(i.latencyMode))throw new TypeError("config.latencyMode, when provided, must be 'quality' or 'realtime'.")},ge=class{constructor(e,t){this.source=e;this.codecConfig=t;ki(t)}encoder=null;muxer=null;lastMultipleOfKeyFrameInterval=-1;lastWidth=null;lastHeight=null;async digest(e){if(this.source._ensureValidDigest(),this.lastWidth!==null&&this.lastHeight!==null){if(e.codedWidth!==this.lastWidth||e.codedHeight!==this.lastHeight)throw new Error(`Video frame size must remain constant. Expected ${this.lastWidth}x${this.lastHeight}, got ${e.codedWidth}x${e.codedHeight}.`)}else this.lastWidth=e.codedWidth,this.lastHeight=e.codedHeight;this.ensureEncoder(e),d(this.encoder);let t=Math.floor(e.timestamp/1e6/bi);this.encoder.encode(e,{keyFrame:t!==this.lastMultipleOfKeyFrameInterval}),this.lastMultipleOfKeyFrameInterval=t,this.encoder.encodeQueueSize>=4&&await new Promise(r=>this.encoder.addEventListener("dequeue",r,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(e){this.encoder||(this.encoder=new VideoEncoder({output:(t,r)=>void this.muxer.addEncodedVideoChunk(this.source._connectedTrack,t,r),error:t=>console.error("Video encode error:",t)}),this.encoder.configure({codec:Jt(this.codecConfig.codec,e.codedWidth,e.codedHeight,this.codecConfig.bitrate),width:e.codedWidth,height:e.codedHeight,bitrate:this.codecConfig.bitrate,framerate:this.source._connectedTrack?.metadata.frameRate,latencyMode:this.codecConfig.latencyMode,...ir(this.codecConfig.codec)}),d(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},lt=class extends U{_encoder;constructor(e){super(e.codec),this._encoder=new ge(this,e)}digest(e){if(!(e instanceof VideoFrame))throw new TypeError("videoFrame must be a VideoFrame.");return this._encoder.digest(e)}_flush(){return this._encoder.flush()}},ft=class extends U{_encoder;_canvas;constructor(e,t){if(!(e instanceof HTMLCanvasElement))throw new TypeError("canvas must be an HTMLCanvasElement.");super(t.codec),this._encoder=new ge(this,t),this._canvas=e}digest(e,t=0){if(!Number.isFinite(e)||e<0)throw new TypeError("timestamp must be a non-negative number.");if(!Number.isFinite(t)||t<0)throw new TypeError("duration must be a non-negative number.");let r=new VideoFrame(this._canvas,{timestamp:Math.round(1e6*e),duration:Math.round(1e6*t),alpha:"discard"}),s=this._encoder.digest(r);return r.close(),s}_flush(){return this._encoder.flush()}},pt=class extends U{_encoder;_abortController=null;_track;_offsetTimestamps=!0;constructor(e,t){if(!(e instanceof MediaStreamTrack)||e.kind!=="video")throw new TypeError("track must be a video MediaStreamTrack.");t={...t,latencyMode:"realtime"},super(t.codec),this._encoder=new ge(this,t),this._track=e}_start(){this._abortController=new AbortController;let e=new MediaStreamTrackProcessor({track:this._track}),t=new WritableStream({write:r=>{this._encoder.digest(r),r.close()}});e.readable.pipeTo(t,{signal:this._abortController.signal}).catch(r=>{r instanceof DOMException&&r.name==="AbortError"||console.error("Pipe error:",r)})}async _flush(){this._abortController&&(this._abortController.abort(),this._abortController=null),await this._encoder.flush()}},B=class extends oe{_connectedTrack=null;_codec;constructor(e){if(super(),!se.includes(e))throw new TypeError(`Invalid audio codec '${e}'. Must be one of: ${se.join(", ")}.`);this._codec=e}},ht=class extends B{constructor(e){super(e)}digest(e,t){if(!(e instanceof EncodedAudioChunk))throw new TypeError("chunk must be an EncodedAudioChunk.");return this._ensureValidDigest(),this._connectedTrack.output._muxer.addEncodedAudioChunk(this._connectedTrack,e,t)}},gi=i=>{if(!i||typeof i!="object")throw new TypeError("Codec config must be an object.");if(!se.includes(i.codec))throw new TypeError(`Invalid audio codec '${i.codec}'. Must be one of: ${se.join(", ")}.`);if(!Number.isInteger(i.bitrate)||i.bitrate<=0)throw new TypeError("config.bitrate must be a positive integer.")},Te=class{constructor(e,t){this.source=e;this.codecConfig=t;gi(t)}encoder=null;muxer=null;lastNumberOfChannels=null;lastSampleRate=null;async digest(e){if(this.source._ensureValidDigest(),this.lastNumberOfChannels!==null&&this.lastSampleRate!==null){if(e.numberOfChannels!==this.lastNumberOfChannels||e.sampleRate!==this.lastSampleRate)throw new Error(`Audio parameters must remain constant. Expected ${this.lastNumberOfChannels} channels at ${this.lastSampleRate} Hz, got ${e.numberOfChannels} channels at ${e.sampleRate} Hz.`)}else this.lastNumberOfChannels=e.numberOfChannels,this.lastSampleRate=e.sampleRate;this.ensureEncoder(e),d(this.encoder),this.encoder.encode(e),this.encoder.encodeQueueSize>=4&&await new Promise(t=>this.encoder.addEventListener("dequeue",t,{once:!0})),await this.muxer.mutex.currentPromise}ensureEncoder(e){this.encoder||(this.encoder=new AudioEncoder({output:(t,r)=>void this.muxer.addEncodedAudioChunk(this.source._connectedTrack,t,r),error:t=>console.error("Audio encode error:",t)}),this.encoder.configure({codec:tr(this.codecConfig.codec,e.numberOfChannels,e.sampleRate),numberOfChannels:e.numberOfChannels,sampleRate:e.sampleRate,bitrate:this.codecConfig.bitrate,...sr(this.codecConfig.codec)}),d(this.source._connectedTrack),this.muxer=this.source._connectedTrack.output._muxer)}async flush(){this.encoder&&(await this.encoder.flush(),this.encoder.close())}},bt=class extends B{_encoder;constructor(e){super(e.codec),this._encoder=new Te(this,e)}digest(e){if(!(e instanceof AudioData))throw new TypeError("audioData must be an AudioData.");return this._encoder.digest(e)}_flush(){return this._encoder.flush()}},kt=class extends B{_encoder;_accumulatedFrameCount=0;constructor(e){super(e.codec),this._encoder=new Te(this,e)}digest(e){if(!(e instanceof AudioBuffer))throw new TypeError("audioBuffer must be an AudioBuffer.");let t=e.numberOfChannels,r=e.sampleRate,s=e.length,n=new Float32Array(t*s);for(let o=0;o{this._encoder.digest(r),r.close()}});e.readable.pipeTo(t,{signal:this._abortController.signal}).catch(r=>{r instanceof DOMException&&r.name==="AbortError"||console.error("Pipe error:",r)})}async _flush(){this._abortController&&(this._abortController.abort(),this._abortController=null),await this._encoder.flush()}},ce=class extends oe{_connectedTrack=null;_codec;constructor(e){if(super(),!Fe.includes(e))throw new TypeError(`Invalid subtitle codec '${e}'. Must be one of: ${Fe.join(", ")}.`);this._codec=e}},Tt=class extends ce{_parser;constructor(e){super(e),this._parser=new ve({codec:e,output:(t,r)=>this._connectedTrack?.output._muxer.addSubtitleCue(this._connectedTrack,t,r),error:t=>console.error("Subtitle parse error:",t)})}digest(e){if(typeof e!="string")throw new TypeError("text must be a string.");return this._ensureValidDigest(),this._parser.parse(e),this._connectedTrack.output._muxer.mutex.currentPromise}};var xt=class{_muxer;_writer;_tracks=[];_started=!1;_finalizing=!1;_mutex=new N;constructor(e){if(!e||typeof e!="object")throw new TypeError("options must be an object.");if(!(e.format instanceof Y))throw new TypeError("options.format must be an OutputFormat.");if(!(e.target instanceof G))throw new TypeError("options.target must be a Target.");if(e.target._output)throw new Error("Target is already used for another output.");e.target._output=this,this._writer=e.target._createWriter(),this._muxer=e.format._createMuxer(this)}addVideoTrack(e,t={}){if(!(e instanceof U))throw new TypeError("source must be a VideoSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");if(typeof t.rotation=="number"&&![0,90,180,270].includes(t.rotation))throw new TypeError(`Invalid video rotation: ${t.rotation}. Has to be 0, 90, 180 or 270.`);if(Array.isArray(t.rotation)&&(t.rotation.length!==9||t.rotation.some(r=>!Number.isFinite(r))))throw new TypeError(`Invalid video transformation matrix: ${t.rotation.join()}`);if(t.frameRate!==void 0&&(!Number.isInteger(t.frameRate)||t.frameRate<=0))throw new TypeError(`Invalid video frame rate: ${t.frameRate}. Must be a positive integer.`);this._addTrack("video",e,t)}addAudioTrack(e,t={}){if(!(e instanceof B))throw new TypeError("source must be an AudioSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");this._addTrack("audio",e,t)}addSubtitleTrack(e,t={}){if(!(e instanceof ce))throw new TypeError("source must be a SubtitleSource.");if(!t||typeof t!="object")throw new TypeError("metadata must be an object.");this._addTrack("subtitle",e,t)}_addTrack(e,t,r){if(this._started)throw new Error("Cannot add track after output has started.");if(t._connectedTrack)throw new Error("Source is already used for a track.");let s={id:this._tracks.length+1,output:this,type:e,source:t,metadata:r};this._muxer.beforeTrackAdd(s),this._tracks.push(s),t._connectedTrack=s}async start(){if(this._started)throw new Error("Output already started.");this._started=!0,this._writer.start();let e=await this._mutex.acquire();await this._muxer.start();for(let t of this._tracks)t.source._start();e()}async finalize(){if(!this._started)throw new Error("Cannot finalize before starting.");if(this._finalizing)throw new Error("Cannot call finalize twice.");this._finalizing=!0;let e=await this._mutex.acquire(),t=this._tracks.map(r=>r.source._flush());await Promise.all(t),await this._muxer.finalize(),await this._writer.flush(),await this._writer.finalize(),e()}};var xe=class{_sizePromise=null;_getSize(){return this._sizePromise??=this._retrieveSize()}},Ct=class extends xe{constructor(t){super();this.buffer=t}async _read(t,r){return new Uint8Array(this.buffer,t,r-t)}async _retrieveSize(){return this.buffer.byteLength}},St=class extends xe{constructor(t){super();this.blob=t}async _read(t,r){let n=await this.blob.slice(t,r).arrayBuffer();return new Uint8Array(n)}async _retrieveSize(){return this.blob.size}};var ue=class{input;constructor(e){this.input=e}};var Be=class{_backing;constructor(e){this._backing=e}isVideoTrack(){return this instanceof Ce}isAudioTrack(){return this instanceof Se}computeDuration(){return this._backing.computeDuration()}},Ce=class extends Be{_backing;constructor(e){super(e),this._backing=e}getCodec(){return this._backing.getCodec()}getWidth(){return this._backing.getWidth()}getHeight(){return this._backing.getHeight()}getRotation(){return this._backing.getRotation()}getDecoderConfig(){return this._backing.getDecoderConfig()}async getCodecMimeType(){return(await this.getDecoderConfig()).codec}},Se=class extends Be{_backing;constructor(e){super(e),this._backing=e}getCodec(){return this._backing.getCodec()}getNumberOfChannels(){return this._backing.getNumberOfChannels()}getSampleRate(){return this._backing.getSampleRate()}getDecoderConfig(){return this._backing.getDecoderConfig()}async getCodecMimeType(){return(await this.getDecoderConfig()).codec}};var me=class{constructor(e,t=1/0){this.source=e;this.maxStorableBytes=t}loadedSegments=[];loadingSegments=[];sourceSizePromise=null;nextAge=0;totalStoredBytes=0;async loadRange(e,t){t=Math.min(t,await this.source._getSize());let r=this.loadingSegments.find(o=>o.start<=e&&o.end>=t);if(r){await r.promise;return}if(this.loadedSegments.some(o=>o.start<=e&&o.end>=t))return;let n=this.source._read(e,t),a={start:e,end:t,promise:n};this.loadingSegments.push(a);let c=await n;zt(this.loadingSegments,a),this.insertIntoLoadedSegments(e,c)}insertIntoLoadedSegments(e,t){let r={start:e,end:e+t.byteLength,bytes:t,view:new DataView(t.buffer),age:this.nextAge++},s=I(this.loadedSegments,e,n=>n.start);(s===-1||this.loadedSegments[s].start=r.end)break;r.start<=a.start&&a.end<=r.end&&(this.loadedSegments.splice(n,1),n--)}for(;this.totalStoredBytes>this.maxStorableBytes&&this.loadedSegments.length>1;){let n=null,a=-1;for(let c=0;cn.start),s=null;if(r!==-1)for(let n=r;ne)break;if(t<=a.end){s=a;break}}if(!s)throw new Error(`No segment loaded for range [${e}, ${t}).`);return s.age=this.nextAge++,{view:s.view,offset:s.bytes.byteOffset+e-s.start}}forgetRange(e,t){if(t<=e)return;let r=I(this.loadedSegments,e,n=>n.start);if(r===-1)return;let s=this.loadedSegments[r];s.start!==e||s.end!==t||(this.loadedSegments.splice(r,1),this.totalStoredBytes-=s.bytes.byteLength)}};var Z=class{constructor(e){this.reader=e}pos=0;readRange(e,t){let{view:r,offset:s}=this.reader.getViewAndOffset(e,t);return new Uint8Array(r.buffer,s,t-e)}readU8(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+1);return this.pos++,e.getUint8(t)}readU16(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+2);return this.pos+=2,e.getUint16(t,!1)}readU24(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+3);this.pos+=3;let r=e.getUint16(t,!1),s=e.getUint8(t+2);return r*256+s}readS32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getInt32(t,!1)}readU32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getUint32(t,!1)}readI32(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+4);return this.pos+=4,e.getInt32(t,!1)}readU64(){let e=this.readU32(),t=this.readU32();return e*4294967296+t}readF64(){let{view:e,offset:t}=this.reader.getViewAndOffset(this.pos,this.pos+8);return this.pos+=8,e.getFloat64(t,!1)}readFixed_16_16(){return this.readS32()/65536}readFixed_2_30(){return this.readS32()/1073741824}readAscii(e){let{view:t,offset:r}=this.reader.getViewAndOffset(this.pos,this.pos+e);this.pos+=e;let s="";for(let n=0;nr.computeDuration()));return Math.max(0,...t)}async getTracks(){return await this.readMetadata(),this.tracks.map(e=>e.inputTrack)}async getMimeType(){await this.readMetadata();let e="video/mp4";if(this.tracks.length>0){let t=await Promise.all(this.tracks.map(s=>s.inputTrack.getCodecMimeType())),r=[...new Set(t)];e+=`; codecs="${r.join(", ")}"`}return e}readMetadata(){return this.metadataPromise??=(async()=>{let e=await this.isobmffReader.reader.source._getSize();for(;this.isobmffReader.pos=0&&rr.presentationTimestamp-s.presentationTimestamp),e.sampleTable}async readFragment(){let e=this.isobmffReader.pos;await this.isobmffReader.reader.loadRange(this.isobmffReader.pos,this.isobmffReader.pos+16);let t=this.isobmffReader.readBoxHeader();d(t.name==="moof"),await this.isobmffReader.reader.loadRange(e,e+t.totalSize),this.isobmffReader.pos=e,this.traverseBox();let r=z(this.fragments,e,n=>n.moofOffset);d(r!==-1);let s=this.fragments[r];d(s.moofOffset===e),this.isobmffReader.reader.forgetRange(e,e+t.totalSize);for(let[n,a]of s.trackData){if(a.startTimestampIsFinal)continue;let c=this.tracks.find(l=>l.id===n);this.isobmffReader.pos=0;let o=null,u=null,m=I(c.fragments,e-1,l=>l.moofOffset);for(m!==-1&&(o=c.fragments[m],u=o,this.isobmffReader.pos=o.moofOffset+o.moofSize);this.isobmffReader.posb.moofOffset);if(h===-1){this.isobmffReader.pos=l;let b=await this.readFragment();o&&(o.nextFragment=b),o=b}else{let b=this.fragments[h];o&&(o.nextFragment=b),o=b}}this.isobmffReader.pos=l+p.totalSize}o&&o.trackData.has(n)&&(u=o)}if(u){let l=u.trackData.get(n);d(l.startTimestampIsFinal),mr(a,l.endTimestamp)}a.startTimestampIsFinal=!0}return s}readContiguousBoxes(e){let t=this.isobmffReader.pos;for(;this.isobmffReader.pos-tm.every((l,p)=>l===o[p]));u===-1?s.rotation=0:s.rotation=90*u}break;case"mdhd":{let s=this.currentTrack;d(s);let n=this.isobmffReader.readU8();this.isobmffReader.pos+=3,n===0?(this.isobmffReader.pos+=8,s.timescale=this.isobmffReader.readU32(),s.durationInTimescale=this.isobmffReader.readU32()):n===1&&(this.isobmffReader.pos+=16,s.timescale=this.isobmffReader.readU32(),s.durationInTimescale=this.isobmffReader.readU64())}break;case"hdlr":{let s=this.currentTrack;d(s),this.isobmffReader.pos+=8;let n=this.isobmffReader.readAscii(4);n==="vide"?s.info={type:"video",width:-1,height:-1,codec:null,codecDescription:null,colorSpace:null}:n==="soun"&&(s.info={type:"audio",numberOfChannels:-1,sampleRate:-1,codec:null,codecDescription:null})}break;case"stbl":{let s=this.currentTrack;d(s),s.sampleTableOffset=e,this.readContiguousBoxes(t.contentSize)}break;case"stsd":{let s=this.currentTrack;if(d(s),s.info===null||s.sampleTable)break;let n=this.isobmffReader.readU8();this.isobmffReader.pos+=3;let a=this.isobmffReader.readU32();for(let c=0;c0){if(u===1)this.isobmffReader.pos+=4*4;else if(u===2){this.isobmffReader.pos+=4,l=this.isobmffReader.readF64(),m=this.isobmffReader.readU32(),this.isobmffReader.pos+=4;let p=this.isobmffReader.readU32(),h=this.isobmffReader.readU32(),b=this.isobmffReader.readU32(),k=this.isobmffReader.readU32()}}s.info.numberOfChannels=m,s.info.sampleRate=l,this.readContiguousBoxes(e+o.totalSize-this.isobmffReader.pos)}}}break;case"avcC":{let s=this.currentTrack;d(s&&s.info),s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+t.contentSize)}break;case"hvcC":{let s=this.currentTrack;d(s&&s.info),s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+t.contentSize)}break;case"colr":{let s=this.currentTrack;if(d(s&&s.info?.type==="video"),this.isobmffReader.readAscii(4)!=="nclx")break;let a=this.isobmffReader.readU16(),c=this.isobmffReader.readU16(),o=this.isobmffReader.readU16(),u=!!(this.isobmffReader.readU8()&128);s.info.colorSpace={primaries:Ft[a],transfer:Mt[c],matrix:Vt[o],fullRange:u}}break;case"wave":t.totalSize>8&&this.readContiguousBoxes(t.contentSize);break;case"esds":{let s=this.currentTrack;d(s&&s.info),this.isobmffReader.pos+=4;let n=this.isobmffReader.readU8();d(n===3),this.isobmffReader.readIsomVariableInteger(),this.isobmffReader.pos+=2;let a=this.isobmffReader.readU8(),c=(a&128)!==0,o=(a&64)!==0,u=(a&32)!==0;if(c&&(this.isobmffReader.pos+=2),o){let b=this.isobmffReader.readU8();this.isobmffReader.pos+=b}u&&(this.isobmffReader.pos+=2);let m=this.isobmffReader.readU8();d(m===4),this.isobmffReader.readIsomVariableInteger();let l=this.isobmffReader.readU8();d(l===64),this.isobmffReader.pos+=12;let p=this.isobmffReader.readU8();d(p===5);let h=this.isobmffReader.readIsomVariableInteger();s.info.codecDescription=this.isobmffReader.readRange(this.isobmffReader.pos,this.isobmffReader.pos+h)}break;case"stts":{let s=this.currentTrack;if(d(s),!s.sampleTable)break;this.isobmffReader.pos+=4;let n=this.isobmffReader.readU32(),a=0,c=0;for(let o=0;ow.id===n);if(!a)break;a.fragmentLookupTable=[];let c=this.isobmffReader.readU32(),o=(c&48)>>4,u=(c&12)>>2,m=c&3,l=this.isobmffReader,p=[l.readU8.bind(l),l.readU16.bind(l),l.readU24.bind(l),l.readU32.bind(l)],h=p[o],b=p[u],k=p[m],g=this.isobmffReader.readU32();for(let w=0;wn.moofOffset);this.fragments.splice(s+1,0,this.currentFragment);for(let[,n]of this.currentFragment.trackData){let a=n.samples[0],c=A(n.samples);this.currentFragment.dataStart=Math.min(this.currentFragment.dataStart,a.byteOffset),this.currentFragment.dataEnd=Math.max(this.currentFragment.dataEnd,c.byteOffset+c.byteSize)}this.currentFragment=null}break;case"traf":if(d(this.currentFragment),this.readContiguousBoxes(t.contentSize),this.currentTrack){let s=this.currentFragment.trackData.get(this.currentTrack.id);if(s){let n=I(this.currentTrack.fragments,this.currentFragment.moofOffset,c=>c.moofOffset);this.currentTrack.fragments.splice(n+1,0,this.currentFragment);let{currentFragmentState:a}=this.currentTrack;d(a),a.startTimestamp!==null&&(mr(s,a.startTimestamp),s.startTimestampIsFinal=!0)}this.currentTrack.currentFragmentState=null,this.currentTrack=null}break;case"tfhd":{d(this.currentFragment),this.isobmffReader.pos+=1;let s=this.isobmffReader.readU24(),n=!!(s&1),a=!!(s&2),c=!!(s&8),o=!!(s&16),u=!!(s&32),m=!!(s&65536),l=!!(s&131072),p=this.isobmffReader.readU32(),h=this.tracks.find(k=>k.id===p);if(!h)break;let b=this.fragmentTrackDefaults.find(k=>k.trackId===p);this.currentTrack=h,h.currentFragmentState={baseDataOffset:this.currentFragment.implicitBaseDataOffset,sampleDescriptionIndex:b?.defaultSampleDescriptionIndex??null,defaultSampleDuration:b?.defaultSampleDuration??null,defaultSampleSize:b?.defaultSampleSize??null,defaultSampleFlags:b?.defaultSampleFlags??null,startTimestamp:null},n?h.currentFragmentState.baseDataOffset=this.isobmffReader.readU64():l&&(h.currentFragmentState.baseDataOffset=this.currentFragment.moofOffset),a&&(h.currentFragmentState.sampleDescriptionIndex=this.isobmffReader.readU32()),c&&(h.currentFragmentState.defaultSampleDuration=this.isobmffReader.readU32()),o&&(h.currentFragmentState.defaultSampleSize=this.isobmffReader.readU32()),u&&(h.currentFragmentState.defaultSampleFlags=this.isobmffReader.readU32()),m&&(h.currentFragmentState.defaultSampleDuration=0)}break;case"tfdt":{let s=this.currentTrack;if(!s)break;d(s.currentFragmentState);let n=this.isobmffReader.readU8();this.isobmffReader.pos+=3;let a=n===0?this.isobmffReader.readU32():this.isobmffReader.readU64();s.currentFragmentState.startTimestamp=a}break;case"trun":{let s=this.currentTrack;if(!s)break;if(d(this.currentFragment),d(s.currentFragmentState),this.currentFragment.trackData.has(s.id))throw new Error("Can't have two trun boxes for the same track in one fragment.");let n=this.isobmffReader.readU8(),a=this.isobmffReader.readU24(),c=!!(a&1),o=!!(a&4),u=!!(a&256),m=!!(a&512),l=!!(a&1024),p=!!(a&2048),h=this.isobmffReader.readU32(),b=s.currentFragmentState.baseDataOffset;c&&(b+=this.isobmffReader.readI32());let k=null;o&&(k=this.isobmffReader.readU32());let g=b;if(h===0){this.currentFragment.implicitBaseDataOffset=g;break}let w=0,v={startTimestamp:0,endTimestamp:0,samples:[],presentationTimestamps:[],startTimestampIsFinal:!1};this.currentFragment.trackData.set(s.id,v);for(let E=0;E({presentationTimestamp:E.presentationTimestamp,sampleIndex:F})).sort((E,F)=>E.presentationTimestamp-F.presentationTimestamp);let _=v.samples[v.presentationTimestamps[0].sampleIndex],H=v.samples[A(v.presentationTimestamps).sampleIndex];v.startTimestamp=_.presentationTimestamp,v.endTimestamp=H.presentationTimestamp+H.duration,this.currentFragment.implicitBaseDataOffset=g}break}this.isobmffReader.pos=r}},We=class{constructor(e){this.internalTrack=e}chunkToSampleIndex=new WeakMap;sampleIndexToChunk=new Map;chunkToFragmentLocation=new WeakMap;fragmentLocationToChunk=new Map;getCodec(){throw new Error("Not implemented on base class.")}async computeDuration(){let t=(await this.getChunk(1/0,{metadataOnly:!0}))?.timestamp;return t?t/1e6:0}async getFirstChunk(e){return this.internalTrack.demuxer.isFragmented?this.performFragmentedLookup(()=>{let t=this.internalTrack.fragments[0];return{fragmentIndex:t?0:-1,sampleIndex:t?0:-1,correctSampleFound:!!t}},0,1/0,e):this.fetchChunkForSampleIndex(0,e)}async getChunk(e,t){let r=e*this.internalTrack.timescale;if(this.internalTrack.demuxer.isFragmented)return this.performFragmentedLookup(()=>this.findSampleInFragmentsForTimestamp(r),r,r,t);{let s=this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack),n=ur(s,r);return this.fetchChunkForSampleIndex(n,t)}}async getNextChunk(e,t){if(this.internalTrack.demuxer.isFragmented){let s=this.chunkToFragmentLocation.get(e);if(s===void 0)throw new Error("Chunk was not created from this track.");let n=s.fragment.trackData.get(this.internalTrack.id),a=n.samples[s.sampleIndex],c=z(this.internalTrack.fragments,s.fragment.moofOffset,o=>o.moofOffset);return d(c!==-1),this.performFragmentedLookup(()=>{if(s.sampleIndex+1l.moofOffset);return d(m!==-1),{fragmentIndex:m,sampleIndex:0,correctSampleFound:!0}}return{fragmentIndex:c,sampleIndex:-1,correctSampleFound:!1}}},a.presentationTimestamp,1/0,t)}let r=this.chunkToSampleIndex.get(e);if(r===void 0)throw new Error("Chunk was not created from this track.");return this.fetchChunkForSampleIndex(r+1,t)}async getKeyChunk(e,t){let r=e*this.internalTrack.timescale;if(this.internalTrack.demuxer.isFragmented)return this.performFragmentedLookup(()=>this.findKeySampleInFragmentsForTimestamp(r),r,r,t);let s=this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack),n=ur(s,r),a=n===-1?-1:Ci(s,n);return this.fetchChunkForSampleIndex(a,t)}async getNextKeyChunk(e,t){if(this.internalTrack.demuxer.isFragmented){let a=this.chunkToFragmentLocation.get(e);if(a===void 0)throw new Error("Chunk was not created from this track.");let c=a.fragment.trackData.get(this.internalTrack.id),o=c.samples[a.sampleIndex],u=z(this.internalTrack.fragments,a.fragment.moofOffset,m=>m.moofOffset);return d(u!==-1),this.performFragmentedLookup(()=>{let m=c.samples.findIndex((l,p)=>l.isKeyFrame&&p>a.sampleIndex);if(m!==-1)return{fragmentIndex:u,sampleIndex:m,correctSampleFound:!0};{let l=a.fragment;for(;l.nextFragment;){l=l.nextFragment;let p=l.trackData.get(this.internalTrack.id);if(p){let h=z(this.internalTrack.fragments,l.moofOffset,k=>k.moofOffset);d(h!==-1);let b=p.samples.findIndex(k=>k.isKeyFrame);if(b===-1)throw new Error("Not supported: Fragment does not contain key sample.");return{fragmentIndex:h,sampleIndex:b,correctSampleFound:!0}}}return{fragmentIndex:u,sampleIndex:-1,correctSampleFound:!1}}},o.presentationTimestamp,1/0,t)}let r=this.chunkToSampleIndex.get(e);if(r===void 0)throw new Error("Chunk was not created from this track.");let s=this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack),n=Si(s,r);return this.fetchChunkForSampleIndex(n,t)}async fetchChunkForSampleIndex(e,t){if(e===-1)return null;let r=this.sampleIndexToChunk.get(e)?.deref();if(r)return r;let s=this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack),n=xi(s,e);if(!n)return null;let a;t.metadataOnly?a=new Uint8Array(0):(await this.internalTrack.demuxer.chunkReader.reader.loadRange(n.chunkOffset,n.chunkOffset+n.chunkSize),a=this.internalTrack.demuxer.chunkReader.readRange(n.sampleOffset,n.sampleOffset+n.sampleSize));let c=1e6*n.presentationTimestamp/this.internalTrack.timescale,o=1e6*n.duration/this.internalTrack.timescale,u=this.createChunk(a,c,o,n.isKeyFrame);return this.chunkToSampleIndex.set(u,e),this.sampleIndexToChunk.set(e,new WeakRef(u)),u}async fetchChunkInFragment(e,t,r){if(t===-1)return null;let s=`${e.moofOffset}:${t}`,n=this.fragmentLocationToChunk.get(s)?.deref();if(n)return n;let c=e.trackData.get(this.internalTrack.id).samples[t];d(c);let o;r.metadataOnly?o=new Uint8Array(0):(await this.internalTrack.demuxer.chunkReader.reader.loadRange(e.dataStart,e.dataEnd),o=this.internalTrack.demuxer.chunkReader.readRange(c.byteOffset,c.byteOffset+c.byteSize));let u=1e6*c.presentationTimestamp/this.internalTrack.timescale,m=1e6*c.duration/this.internalTrack.timescale,l=this.createChunk(o,u,m,c.isKeyFrame);return this.chunkToFragmentLocation.set(l,{fragment:e,sampleIndex:t}),this.fragmentLocationToChunk.set(s,new WeakRef(l)),l}findSampleInFragmentsForTimestamp(e){let t=I(this.internalTrack.fragments,e,n=>n.trackData.get(this.internalTrack.id).startTimestamp),r=-1,s=!1;if(t!==-1){let a=this.internalTrack.fragments[t].trackData.get(this.internalTrack.id),c=I(a.presentationTimestamps,e,o=>o.presentationTimestamp);d(c!==-1),r=a.presentationTimestamps[c].sampleIndex,s=en.trackData.get(this.internalTrack.id).startTimestamp),r=-1,s=!1;if(t!==-1){let a=this.internalTrack.fragments[t].trackData.get(this.internalTrack.id),c=a.presentationTimestamps.findLastIndex(u=>a.samples[u.sampleIndex].isKeyFrame&&u.presentationTimestamp<=e);if(c===-1)throw new Error("Not supported: Fragment does not begin with a key sample.");r=a.presentationTimestamps[c].sampleIndex,s=ew.timestamp);g!==-1&&(k=this.internalTrack.fragmentLookupTable[g])}if(c===-1)m.pos=k?.moofOffset??0;else{let g=this.internalTrack.fragments[c];!k||g.moofOffset>=g.moofOffset?(m.pos=g.moofOffset+g.moofSize,p=g):m.pos=k.moofOffset}for(;m.posr)break;if(p.nextFragment){m.pos=p.nextFragment.moofOffset+p.nextFragment.moofSize,p=p.nextFragment;continue}}await m.reader.loadRange(m.pos,m.pos+16);let g=m.pos,w=m.readBoxHeader();if(w.name==="moof"){let v=z(n.fragments,g,_=>_.moofOffset);if(v===-1){m.pos=g;let _=await n.readFragment();p&&(p.nextFragment=_),p=_;let{fragmentIndex:H,sampleIndex:E,correctSampleFound:F}=e();if(F){let J=this.internalTrack.fragments[H];return this.fetchChunkInFragment(J,E,s)}H!==-1&&(h=H,b=E)}else{let _=n.fragments[v];p&&(p.nextFragment=_),p=_}}m.pos=g+w.totalSize}if(h!==-1){let g=this.internalTrack.fragments[h];return this.fetchChunkInFragment(g,b,s)}return null}finally{a()}}},wt=class extends We{internalTrack;constructor(e){super(e),this.internalTrack=e}async getCodec(){return this.internalTrack.info.codec}async getWidth(){return this.internalTrack.info.width}async getHeight(){return this.internalTrack.info.height}async getRotation(){return this.internalTrack.rotation}async getDecoderConfig(){return{codec:er(this.internalTrack.info.codec,this.internalTrack.info.codecDescription),codedWidth:this.internalTrack.info.width,codedHeight:this.internalTrack.info.height,description:this.internalTrack.info.codecDescription??void 0,colorSpace:this.internalTrack.info.colorSpace??void 0}}createChunk(e,t,r,s){return new EncodedVideoChunk({data:e,timestamp:t,duration:r,type:s?"key":"delta"})}},yt=class extends We{internalTrack;constructor(e){super(e),this.internalTrack=e}async getCodec(){return this.internalTrack.info.codec}async getNumberOfChannels(){return this.internalTrack.info.numberOfChannels}async getSampleRate(){return this.internalTrack.info.sampleRate}async getDecoderConfig(){return{codec:rr(this.internalTrack.info.codec,this.internalTrack.info.codecDescription),numberOfChannels:this.internalTrack.info.numberOfChannels,sampleRate:this.internalTrack.info.sampleRate,description:this.internalTrack.info.codecDescription??void 0}}createChunk(e,t,r,s){return new EncodedAudioChunk({data:e,timestamp:t,duration:r,type:s?"key":"delta"})}},ur=(i,e)=>{let t=I(i.presentationTimestamps,e,r=>r.presentationTimestamp);return t===-1?-1:i.presentationTimestamps[t].sampleIndex},xi=(i,e)=>{let t=I(i.sampleTimingEntries,e,k=>k.startIndex),r=i.sampleTimingEntries[t];if(!r||r.startIndex+r.count<=e)return null;let n=r.startDecodeTimestamp+(e-r.startIndex)*r.delta,a=I(i.sampleCompositionTimeOffsets,e,k=>k.startIndex),c=i.sampleCompositionTimeOffsets[a];c&&(n+=c.offset);let o=i.sampleSizes[Math.min(e,i.sampleSizes.length-1)],u=I(i.sampleToChunk,e,k=>k.startSampleIndex),m=i.sampleToChunk[u];d(m);let l=m.startChunkIndex+Math.floor((e-m.startSampleIndex)/m.samplesPerChunk),p=i.chunkOffsets[l],h=0,b=p;if(i.sampleSizes.length===1)b+=o*(e-m.startSampleIndex),h+=o*m.samplesPerChunk;else{let k=m.startSampleIndex+(l-m.startChunkIndex)*m.samplesPerChunk;for(let g=k;gk)!==-1:!0}},Ci=(i,e)=>{if(!i.keySampleIndices)return e;let t=I(i.keySampleIndices,e,r=>r);return i.keySampleIndices[t]??-1},Si=(i,e)=>{if(!i.keySampleIndices)return e+1;let t=I(i.keySampleIndices,e,r=>r);return i.keySampleIndices[t+1]??-1},mr=(i,e)=>{i.startTimestamp+=e,i.endTimestamp+=e;for(let t of i.samples)t.presentationTimestamp+=e;for(let t of i.presentationTimestamps)t.presentationTimestamp+=e};var He=class extends ue{};var Le=class{},vt=class extends Le{async _canReadInput(e){if(await e._mainReader.source._getSize()<8)return!1;let r=new Z(e._mainReader);return r.pos=4,r.readAscii(4)==="ftyp"}_createDemuxer(e){return new Ne(e)}},It=class extends Le{async _canReadInput(){return!1}_createDemuxer(e){return new He(e)}},Ke=new vt,wi=Ke,yi=Ke,At=new It,dr=At,vi=At,Ii=[Ke,dr];var Rt=class{_source;_formats;_mainReader;_demuxerPromise=null;_format=null;constructor(e){this._formats=e.formats,this._source=e.source,this._mainReader=new me(e.source)}_getDemuxer(){return this._demuxerPromise??=(async()=>{await this._mainReader.loadRange(0,4096);for(let e of this._formats)if(await e._canReadInput(this))return this._format=e,e._createDemuxer(this);throw new Error("Input has an unrecognizable format.")})()}async getFormat(){return await this._getDemuxer(),d(this._format),this._format}async computeDuration(){return(await this._getDemuxer()).computeDuration()}async getTracks(){return(await this._getDemuxer()).getTracks()}async getVideoTracks(){return(await this.getTracks()).filter(t=>t.isVideoTrack())}async getPrimaryVideoTrack(){return(await this.getTracks()).find(t=>t.isVideoTrack())??null}async getAudioTracks(){return(await this.getTracks()).filter(t=>t.isAudioTrack())}async getPrimaryAudioTrack(){return(await this.getTracks()).find(t=>t.isAudioTrack())??null}async getMimeType(){return(await this._getDemuxer()).getMimeType()}};var $e=class{async*chunks(e,t=1/0){let r=[],{promise:s,resolve:n}=q(),{promise:a,resolve:c}=q(),o=!1,u=[],m=()=>Math.max(2,u.length);(async()=>{let l=e??await this.getFirstChunk();for(;l&&!o&&!(l.timestamp/1e6>=t);){if(r.length>m()){({promise:a,resolve:c}=q()),await a;continue}r.push(l),n(),{promise:s,resolve:n}=q(),l=await this.getNextChunk(l)}o=!0,n()})();try{for(;;)if(r.length>0){yield r.shift();let l=performance.now();for(u.push(l);u.length>0&&l-u[0]>=1e3;)u.shift();c()}else if(!o)await s;else break}finally{o=!0,c()}}},Qe=class{async getKeyMediaFrame(e){let t=null,r=await this.createDecoder(a=>t=a),n=await this.createChunkDrain().getKeyChunk(e);return n?(r.decode(n),await r.flush(),r.close(),t):null}async getMediaFrame(e){let t=null,r=await this.createDecoder(o=>{o.timestamp/1e6<=e?(t?.close(),t=o):o.close()}),s=this.createChunkDrain(),n=await s.getKeyChunk(e);if(!n)return null;let a=await s.getChunk(e);d(a),r.decode(n);let c=n;for(;c!==a;){let o=await s.getNextChunk(c);d(o),c=o,r.decode(o),r.decodeQueueSize>=10&&await new Promise(u=>r.addEventListener("dequeue",u,{once:!0}))}return await r.flush(),r.close(),t}async*mediaFrames(e=0,t=1/0){let r=[],s=!1,n=null,{promise:a,resolve:c}=q(),o=!1,u=await this.createDecoder(h=>{let b=h.timestamp/1e6;if(b>=t&&(o=!0),o){h.close();return}n&&(b>e?(r.push(n),s=!0):n.close()),b>=e&&(r.push(h),s=!0),n=s?null:h,r.length>0&&(c(),{promise:a,resolve:c}=q())}),m=this.createChunkDrain(),l=await m.getKeyChunk(e)??await m.getFirstChunk();if(!l)return;let p=!1;(async()=>{let h=l,b=1/0;if(t<1/0){let g=await m.getChunk(t),w=g?g.type==="key"&&g.timestamp/1e6===t?g:await m.getNextKeyChunk(g):null;w&&(b=w.timestamp/1e6)}let k=m.chunks(l,b);for(await k.next();h&&!o;){u.decode(h),u.decodeQueueSize>=10&&await new Promise(w=>u.addEventListener("dequeue",w,{once:!0}));let g=await k.next();if(g.done)break;h=g.value}await k.return(),await u.flush(),u.close(),!s&&n&&r.push(n),p=!0,c()})();try{for(;;)if(r.length>0)yield r.shift();else if(!p)await a;else break}finally{o=!0}}},je=class extends $e{constructor(t){super();this.videoTrack=t}getFirstChunk(t={}){return this.videoTrack._backing.getFirstChunk(t)}getChunk(t,r={}){return this.videoTrack._backing.getChunk(t,r)}getNextChunk(t,r={}){return this.videoTrack._backing.getNextChunk(t,r)}getKeyChunk(t,r={}){return this.videoTrack._backing.getKeyChunk(t,r)}getNextKeyChunk(t,r={}){return this.videoTrack._backing.getNextKeyChunk(t,r)}},Ot=class extends Qe{constructor(t){super();this.videoTrack=t}decoderConfig=null;async createDecoder(t){this.decoderConfig||(this.decoderConfig=await this.videoTrack.getDecoderConfig());let r=new VideoDecoder({output:t,error:s=>console.error(s)});return r.configure(this.decoderConfig),r}createChunkDrain(){return new je(this.videoTrack)}getKeyFrame(t){return this.getKeyMediaFrame(t)}getFrame(t){return this.getMediaFrame(t)}frames(t=0,r=1/0){return this.mediaFrames(t,r)}},qe=class extends $e{constructor(t){super();this.audioTrack=t}getFirstChunk(t={}){return this.audioTrack._backing.getFirstChunk(t)}getChunk(t,r={}){return this.audioTrack._backing.getChunk(t,r)}getNextChunk(t,r={}){return this.audioTrack._backing.getNextChunk(t,r)}getKeyChunk(t,r={}){return this.audioTrack._backing.getKeyChunk(t,r)}getNextKeyChunk(t,r={}){return this.audioTrack._backing.getNextKeyChunk(t,r)}},Xe=class extends Qe{constructor(t){super();this.audioTrack=t}decoderConfig=null;async createDecoder(t){this.decoderConfig||(this.decoderConfig=await this.audioTrack.getDecoderConfig());let r=new AudioDecoder({output:t,error:s=>console.error(s)});return r.configure(this.decoderConfig),r}createChunkDrain(){return new qe(this.audioTrack)}getKeyData(t){return this.getKeyMediaFrame(t)}getData(t){return this.getMediaFrame(t)}data(t=0,r=1/0){return this.mediaFrames(t,r)}},_t=class{constructor(e){this.audioTrack=e;this.audioDataDrain=new Xe(e)}audioDataDrain;audioDataToWrappedArrayBuffer(e){if(!e)return null;let t=new AudioBuffer({numberOfChannels:e.numberOfChannels,length:e.numberOfFrames,sampleRate:e.sampleRate});for(let r=0;r {
};
var extractAudioCodecString = (codec, description) => {
if (codec === "aac") {
- if (!description || description.byteLength < 2) {
- throw new TypeError("AAC description must be at least 2 bytes long.");
- }
- const mpeg4AudioObjectType = description[0] >> 3;
- return `mp4a.40.${mpeg4AudioObjectType}`;
+ const audioSpecificConfig = parseAacAudioSpecificConfig(description);
+ return `mp4a.40.${audioSpecificConfig.objectType}`;
} else if (codec === "opus") {
return "opus";
} else if (codec === "vorbis") {
@@ -1761,6 +1758,66 @@ var extractAudioCodecString = (codec, description) => {
}
throw new TypeError(`Unhandled codec '${codec}'.`);
};
+var parseAacAudioSpecificConfig = (bytes2) => {
+ if (!bytes2 || bytes2.byteLength < 2) {
+ throw new TypeError("AAC description must be at least 2 bytes long.");
+ }
+ let bitOffset = 0;
+ let objectType = readBits(bytes2, bitOffset, bitOffset + 5);
+ bitOffset += 5;
+ if (objectType === 31) {
+ objectType = 32 + readBits(bytes2, bitOffset, bitOffset + 6);
+ bitOffset += 6;
+ }
+ const frequencyIndex = readBits(bytes2, bitOffset, bitOffset + 4);
+ bitOffset += 4;
+ let sampleRate = null;
+ if (frequencyIndex === 15) {
+ sampleRate = readBits(bytes2, bitOffset, bitOffset + 24);
+ bitOffset += 24;
+ } else {
+ const freqTable = [
+ 96e3,
+ 88200,
+ 64e3,
+ 48e3,
+ 44100,
+ 32e3,
+ 24e3,
+ 22050,
+ 16e3,
+ 12e3,
+ 11025,
+ 8e3,
+ 7350
+ ];
+ if (frequencyIndex < freqTable.length) {
+ sampleRate = freqTable[frequencyIndex];
+ }
+ }
+ const channelConfiguration = readBits(bytes2, bitOffset, bitOffset + 4);
+ bitOffset += 4;
+ let numberOfChannels = null;
+ if (channelConfiguration >= 1 && channelConfiguration <= 7) {
+ const channelMap = {
+ 1: 1,
+ 2: 2,
+ 3: 3,
+ 4: 4,
+ 5: 5,
+ 6: 6,
+ 7: 8
+ };
+ numberOfChannels = channelMap[channelConfiguration];
+ }
+ return {
+ objectType,
+ frequencyIndex,
+ sampleRate,
+ channelConfiguration,
+ numberOfChannels
+ };
+};
var getVideoEncoderConfigExtension = (codec) => {
if (codec === "avc") {
return {
@@ -4025,8 +4082,8 @@ var InputTrack = class {
isAudioTrack() {
return this instanceof InputAudioTrack;
}
- getDuration() {
- return this._backing.getDuration();
+ computeDuration() {
+ return this._backing.computeDuration();
}
};
var InputVideoTrack = class extends InputTrack {
@@ -4304,12 +4361,10 @@ var IsobmffDemuxer = class extends Demuxer {
this.isobmffReader = new IsobmffReader(input._mainReader);
this.chunkReader = new IsobmffReader(new Reader(input._source, 64 * 2 ** 20));
}
- async getDuration() {
- await this.readMetadata();
- if (this.movieDurationInTimescale === -1) {
- throw new Error("Could not read movie duration.");
- }
- return this.movieDurationInTimescale / this.movieTimescale;
+ async computeDuration() {
+ const tracks = await this.getTracks();
+ const trackDurations = await Promise.all(tracks.map((x) => x.computeDuration()));
+ return Math.max(0, ...trackDurations);
}
async getTracks() {
await this.readMetadata();
@@ -4527,6 +4582,15 @@ var IsobmffDemuxer = class extends Demuxer {
const audioTrack = track;
track.inputTrack = new InputAudioTrack(new IsobmffAudioTrackBacking(audioTrack));
this.tracks.push(track);
+ if (track.info.codec === "aac") {
+ const audioSpecificConfig = parseAacAudioSpecificConfig(track.info.codecDescription);
+ if (audioSpecificConfig.numberOfChannels !== null) {
+ track.info.numberOfChannels = audioSpecificConfig.numberOfChannels;
+ }
+ if (audioSpecificConfig.sampleRate !== null) {
+ track.info.sampleRate = audioSpecificConfig.sampleRate;
+ }
+ }
}
}
this.currentTrack = null;
@@ -5233,38 +5297,12 @@ var IsobmffTrackBacking = class {
getCodec() {
throw new Error("Not implemented on base class.");
}
- async getDuration() {
- return this.internalTrack.durationInTimescale / this.internalTrack.timescale;
+ async computeDuration() {
+ const lastChunk = await this.getChunk(Infinity, { metadataOnly: true });
+ const timestamp = lastChunk?.timestamp;
+ return timestamp ? timestamp / 1e6 : 0;
}
-};
-var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
- internalTrack;
- constructor(internalTrack) {
- super(internalTrack);
- this.internalTrack = internalTrack;
- }
- async getCodec() {
- return this.internalTrack.info.codec;
- }
- async getWidth() {
- return this.internalTrack.info.width;
- }
- async getHeight() {
- return this.internalTrack.info.height;
- }
- async getRotation() {
- return this.internalTrack.rotation;
- }
- async getDecoderConfig() {
- return {
- codec: extractVideoCodecString(this.internalTrack.info.codec, this.internalTrack.info.codecDescription),
- codedWidth: this.internalTrack.info.width,
- codedHeight: this.internalTrack.info.height,
- description: this.internalTrack.info.codecDescription ?? void 0,
- colorSpace: this.internalTrack.info.colorSpace ?? void 0
- };
- }
- async getFirstChunk() {
+ async getFirstChunk(options) {
if (this.internalTrack.demuxer.isFragmented) {
return this.performFragmentedLookup(
() => {
@@ -5276,26 +5314,28 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
};
},
0,
- Infinity
+ Infinity,
+ options
);
}
- return this.fetchChunkForSampleIndex(0);
+ return this.fetchChunkForSampleIndex(0, options);
}
- async getChunk(timestamp) {
+ async getChunk(timestamp, options) {
const timestampInTimescale = timestamp * this.internalTrack.timescale;
if (this.internalTrack.demuxer.isFragmented) {
return this.performFragmentedLookup(
() => this.findSampleInFragmentsForTimestamp(timestampInTimescale),
timestampInTimescale,
- timestampInTimescale
+ timestampInTimescale,
+ options
);
} else {
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
- return this.fetchChunkForSampleIndex(sampleIndex);
+ return this.fetchChunkForSampleIndex(sampleIndex, options);
}
}
- async getNextChunk(chunk) {
+ async getNextChunk(chunk, options) {
if (this.internalTrack.demuxer.isFragmented) {
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
if (locationInFragment === void 0) {
@@ -5344,30 +5384,32 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
}
},
sample.presentationTimestamp,
- Infinity
+ Infinity,
+ options
);
}
const sampleIndex = this.chunkToSampleIndex.get(chunk);
if (sampleIndex === void 0) {
throw new Error("Chunk was not created from this track.");
}
- return this.fetchChunkForSampleIndex(sampleIndex + 1);
+ return this.fetchChunkForSampleIndex(sampleIndex + 1, options);
}
- async getKeyChunk(timestamp) {
+ async getKeyChunk(timestamp, options) {
const timestampInTimescale = timestamp * this.internalTrack.timescale;
if (this.internalTrack.demuxer.isFragmented) {
return this.performFragmentedLookup(
() => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale),
timestampInTimescale,
- timestampInTimescale
+ timestampInTimescale,
+ options
);
}
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
const keyFrameSampleIndex = sampleIndex === -1 ? -1 : getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
- return this.fetchChunkForSampleIndex(keyFrameSampleIndex);
+ return this.fetchChunkForSampleIndex(keyFrameSampleIndex, options);
}
- async getNextKeyChunk(chunk) {
+ async getNextKeyChunk(chunk, options) {
if (this.internalTrack.demuxer.isFragmented) {
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
if (locationInFragment === void 0) {
@@ -5423,7 +5465,8 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
}
},
sample.presentationTimestamp,
- Infinity
+ Infinity,
+ options
);
}
const sampleIndex = this.chunkToSampleIndex.get(chunk);
@@ -5432,9 +5475,9 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
}
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex);
- return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex);
+ return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex, options);
}
- async fetchChunkForSampleIndex(sampleIndex) {
+ async fetchChunkForSampleIndex(sampleIndex, options) {
if (sampleIndex === -1) {
return null;
}
@@ -5447,25 +5490,27 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
if (!sampleInfo) {
return null;
}
- await this.internalTrack.demuxer.chunkReader.reader.loadRange(
- sampleInfo.chunkOffset,
- sampleInfo.chunkOffset + sampleInfo.chunkSize
- );
- const data = this.internalTrack.demuxer.chunkReader.readRange(
- sampleInfo.sampleOffset,
- sampleInfo.sampleOffset + sampleInfo.sampleSize
- );
- const chunk = new EncodedVideoChunk({
- data,
- timestamp: 1e6 * sampleInfo.presentationTimestamp / this.internalTrack.timescale,
- duration: 1e6 * sampleInfo.duration / this.internalTrack.timescale,
- type: sampleInfo.isKeyFrame ? "key" : "delta"
- });
+ let data;
+ if (options.metadataOnly) {
+ data = new Uint8Array(0);
+ } else {
+ await this.internalTrack.demuxer.chunkReader.reader.loadRange(
+ sampleInfo.chunkOffset,
+ sampleInfo.chunkOffset + sampleInfo.chunkSize
+ );
+ data = this.internalTrack.demuxer.chunkReader.readRange(
+ sampleInfo.sampleOffset,
+ sampleInfo.sampleOffset + sampleInfo.sampleSize
+ );
+ }
+ const timestamp = 1e6 * sampleInfo.presentationTimestamp / this.internalTrack.timescale;
+ const duration = 1e6 * sampleInfo.duration / this.internalTrack.timescale;
+ const chunk = this.createChunk(data, timestamp, duration, sampleInfo.isKeyFrame);
this.chunkToSampleIndex.set(chunk, sampleIndex);
this.sampleIndexToChunk.set(sampleIndex, new WeakRef(chunk));
return chunk;
}
- async fetchChunkInFragment(fragment, sampleIndex) {
+ async fetchChunkInFragment(fragment, sampleIndex, options) {
if (sampleIndex === -1) {
return null;
}
@@ -5477,17 +5522,19 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
const trackData = fragment.trackData.get(this.internalTrack.id);
const sample = trackData.samples[sampleIndex];
assert(sample);
- await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd);
- const data = this.internalTrack.demuxer.chunkReader.readRange(
- sample.byteOffset,
- sample.byteOffset + sample.byteSize
- );
- const chunk = new EncodedVideoChunk({
- data,
- timestamp: 1e6 * sample.presentationTimestamp / this.internalTrack.timescale,
- duration: 1e6 * sample.duration / this.internalTrack.timescale,
- type: sample.isKeyFrame ? "key" : "delta"
- });
+ let data;
+ if (options.metadataOnly) {
+ data = new Uint8Array(0);
+ } else {
+ await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd);
+ data = this.internalTrack.demuxer.chunkReader.readRange(
+ sample.byteOffset,
+ sample.byteOffset + sample.byteSize
+ );
+ }
+ const timestamp = 1e6 * sample.presentationTimestamp / this.internalTrack.timescale;
+ const duration = 1e6 * sample.duration / this.internalTrack.timescale;
+ const chunk = this.createChunk(data, timestamp, duration, sample.isKeyFrame);
this.chunkToFragmentLocation.set(chunk, { fragment, sampleIndex });
this.fragmentLocationToChunk.set(compositeKey, new WeakRef(chunk));
return chunk;
@@ -5527,10 +5574,10 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
const trackData = fragment.trackData.get(this.internalTrack.id);
const index = trackData.presentationTimestamps.findLastIndex((x) => {
const sample = trackData.samples[x.sampleIndex];
- return sample.isKeyFrame;
+ return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale;
});
if (index === -1) {
- throw new Error("Not supported: Fragment does not contain key sample.");
+ throw new Error("Not supported: Fragment does not begin with a key sample.");
}
const entry = trackData.presentationTimestamps[index];
sampleIndex = entry.sampleIndex;
@@ -5539,15 +5586,15 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
return { fragmentIndex, sampleIndex, correctSampleFound };
}
/** Looks for a sample in the fragments while trying to load as few fragments as possible to retrieve it. */
- async performFragmentedLookup(getBestMatch, earliestTimestamp, latestTimestamp) {
- const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
- if (correctSampleFound) {
- const fragment = this.internalTrack.fragments[fragmentIndex];
- return this.fetchChunkInFragment(fragment, sampleIndex);
- }
+ async performFragmentedLookup(getBestMatch, searchTimestamp, latestTimestamp, options) {
const demuxer = this.internalTrack.demuxer;
const release = await demuxer.fragmentLookupMutex.acquire();
try {
+ const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
+ if (correctSampleFound) {
+ const fragment = this.internalTrack.fragments[fragmentIndex];
+ return this.fetchChunkInFragment(fragment, sampleIndex, options);
+ }
const isobmffReader = demuxer.isobmffReader;
const sourceSize = await isobmffReader.reader.source._getSize();
let prevFragment = null;
@@ -5557,7 +5604,7 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
if (this.internalTrack.fragmentLookupTable) {
const index = binarySearchLessOrEqual(
this.internalTrack.fragmentLookupTable,
- earliestTimestamp,
+ searchTimestamp,
(x) => x.timestamp
);
if (index !== -1) {
@@ -5600,7 +5647,7 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
const { fragmentIndex: fragmentIndex2, sampleIndex: sampleIndex2, correctSampleFound: correctSampleFound2 } = getBestMatch();
if (correctSampleFound2) {
const fragment2 = this.internalTrack.fragments[fragmentIndex2];
- return this.fetchChunkInFragment(fragment2, sampleIndex2);
+ return this.fetchChunkInFragment(fragment2, sampleIndex2, options);
}
if (fragmentIndex2 !== -1) {
bestFragmentIndex = fragmentIndex2;
@@ -5616,7 +5663,7 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
}
if (bestFragmentIndex !== -1) {
const fragment = this.internalTrack.fragments[bestFragmentIndex];
- return this.fetchChunkInFragment(fragment, bestSampleIndex);
+ return this.fetchChunkInFragment(fragment, bestSampleIndex, options);
}
return null;
} finally {
@@ -5624,6 +5671,42 @@ var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
}
}
};
+var IsobmffVideoTrackBacking = class extends IsobmffTrackBacking {
+ internalTrack;
+ constructor(internalTrack) {
+ super(internalTrack);
+ this.internalTrack = internalTrack;
+ }
+ async getCodec() {
+ return this.internalTrack.info.codec;
+ }
+ async getWidth() {
+ return this.internalTrack.info.width;
+ }
+ async getHeight() {
+ return this.internalTrack.info.height;
+ }
+ async getRotation() {
+ return this.internalTrack.rotation;
+ }
+ async getDecoderConfig() {
+ return {
+ codec: extractVideoCodecString(this.internalTrack.info.codec, this.internalTrack.info.codecDescription),
+ codedWidth: this.internalTrack.info.width,
+ codedHeight: this.internalTrack.info.height,
+ description: this.internalTrack.info.codecDescription ?? void 0,
+ colorSpace: this.internalTrack.info.colorSpace ?? void 0
+ };
+ }
+ createChunk(data, timestamp, duration, isKeyFrame) {
+ return new EncodedVideoChunk({
+ data,
+ timestamp,
+ duration,
+ type: isKeyFrame ? "key" : "delta"
+ });
+ }
+};
var IsobmffAudioTrackBacking = class extends IsobmffTrackBacking {
internalTrack;
constructor(internalTrack) {
@@ -5647,6 +5730,14 @@ var IsobmffAudioTrackBacking = class extends IsobmffTrackBacking {
description: this.internalTrack.info.codecDescription ?? void 0
};
}
+ createChunk(data, timestamp, duration, isKeyFrame) {
+ return new EncodedAudioChunk({
+ data,
+ timestamp,
+ duration,
+ type: isKeyFrame ? "key" : "delta"
+ });
+ }
};
var getSampleIndexForTimestamp = (sampleTable, timescaleUnits) => {
const index = binarySearchLessOrEqual(
@@ -5808,9 +5899,9 @@ var Input = class {
assert(this._format);
return this._format;
}
- async getDuration() {
+ async computeDuration() {
const demuxer = await this._getDemuxer();
- return demuxer.getDuration();
+ return demuxer.computeDuration();
}
async getTracks() {
const demuxer = await this._getDemuxer();
@@ -5839,25 +5930,7 @@ var Input = class {
};
// src/media-drain.ts
-var EncodedVideoChunkDrain = class {
- constructor(videoTrack) {
- this.videoTrack = videoTrack;
- }
- getFirstChunk() {
- return this.videoTrack._backing.getFirstChunk();
- }
- getChunk(timestamp) {
- return this.videoTrack._backing.getChunk(timestamp);
- }
- getNextChunk(chunk) {
- return this.videoTrack._backing.getNextChunk(chunk);
- }
- getKeyChunk(timestamp) {
- return this.videoTrack._backing.getKeyChunk(timestamp);
- }
- getNextKeyChunk(chunk) {
- return this.videoTrack._backing.getNextKeyChunk(chunk);
- }
+var BaseChunkDrain = class {
async *chunks(startChunk, endTimestamp = Infinity) {
const chunkQueue = [];
let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers();
@@ -5906,26 +5979,12 @@ var EncodedVideoChunkDrain = class {
}
}
};
-var VideoFrameDrain = class {
- constructor(videoTrack) {
- this.videoTrack = videoTrack;
- }
- decoderConfig = null;
- async createDecoder(onFrame) {
- if (!this.decoderConfig) {
- this.decoderConfig = await this.videoTrack.getDecoderConfig();
- }
- const decoder = new VideoDecoder({
- output: onFrame,
- error: (error) => console.error(error)
- });
- decoder.configure(this.decoderConfig);
- return decoder;
- }
- async getKeyFrame(timestamp) {
+var BaseMediaFrameDrain = class {
+ async getKeyMediaFrame(timestamp) {
let result = null;
const decoder = await this.createDecoder((frame) => result = frame);
- const chunk = await this.videoTrack._backing.getKeyChunk(timestamp);
+ const chunkDrain = this.createChunkDrain();
+ const chunk = await chunkDrain.getKeyChunk(timestamp);
if (!chunk) {
return null;
}
@@ -5934,7 +5993,7 @@ var VideoFrameDrain = class {
decoder.close();
return result;
}
- async getFrame(timestamp) {
+ async getMediaFrame(timestamp) {
let result = null;
const decoder = await this.createDecoder((frame) => {
if (frame.timestamp / 1e6 <= timestamp) {
@@ -5944,16 +6003,17 @@ var VideoFrameDrain = class {
frame.close();
}
});
- const keyChunk = await this.videoTrack._backing.getKeyChunk(timestamp);
+ const chunkDrain = this.createChunkDrain();
+ const keyChunk = await chunkDrain.getKeyChunk(timestamp);
if (!keyChunk) {
return null;
}
- const targetChunk = await this.videoTrack._backing.getChunk(timestamp);
+ const targetChunk = await chunkDrain.getChunk(timestamp);
assert(targetChunk);
decoder.decode(keyChunk);
let currentChunk = keyChunk;
while (currentChunk !== targetChunk) {
- const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk);
+ const nextChunk = await chunkDrain.getNextChunk(currentChunk);
assert(nextChunk);
currentChunk = nextChunk;
decoder.decode(nextChunk);
@@ -5965,7 +6025,7 @@ var VideoFrameDrain = class {
decoder.close();
return result;
}
- async *frames(startTimestamp = 0, endTimestamp = Infinity) {
+ async *mediaFrames(startTimestamp = 0, endTimestamp = Infinity) {
const frameQueue = [];
let firstFrameQueued = false;
let lastFrame = null;
@@ -5998,7 +6058,8 @@ var VideoFrameDrain = class {
({ promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers());
}
});
- const keyChunk = await this.videoTrack._backing.getKeyChunk(startTimestamp) ?? await this.videoTrack._backing.getFirstChunk();
+ const chunkDrain = this.createChunkDrain();
+ const keyChunk = await chunkDrain.getKeyChunk(startTimestamp) ?? await chunkDrain.getFirstChunk();
if (!keyChunk) {
return;
}
@@ -6007,13 +6068,12 @@ var VideoFrameDrain = class {
let currentChunk = keyChunk;
let chunksEndTimestamp = Infinity;
if (endTimestamp < Infinity) {
- const endFrame = await this.videoTrack._backing.getChunk(endTimestamp);
- const endKeyFrame = !endFrame ? null : endFrame.type === "key" && endFrame.timestamp / 1e6 === endTimestamp ? endFrame : await this.videoTrack._backing.getNextKeyChunk(endFrame);
+ const endFrame = await chunkDrain.getChunk(endTimestamp);
+ const endKeyFrame = !endFrame ? null : endFrame.type === "key" && endFrame.timestamp / 1e6 === endTimestamp ? endFrame : await chunkDrain.getNextKeyChunk(endFrame);
if (endKeyFrame) {
chunksEndTimestamp = endKeyFrame.timestamp / 1e6;
}
}
- const chunkDrain = new EncodedVideoChunkDrain(this.videoTrack);
const chunks = chunkDrain.chunks(keyChunk, chunksEndTimestamp);
await chunks.next();
while (currentChunk && !ended) {
@@ -6051,16 +6111,158 @@ var VideoFrameDrain = class {
}
}
};
+var EncodedVideoChunkDrain = class extends BaseChunkDrain {
+ constructor(videoTrack) {
+ super();
+ this.videoTrack = videoTrack;
+ }
+ getFirstChunk(options = {}) {
+ return this.videoTrack._backing.getFirstChunk(options);
+ }
+ getChunk(timestamp, options = {}) {
+ return this.videoTrack._backing.getChunk(timestamp, options);
+ }
+ getNextChunk(chunk, options = {}) {
+ return this.videoTrack._backing.getNextChunk(chunk, options);
+ }
+ getKeyChunk(timestamp, options = {}) {
+ return this.videoTrack._backing.getKeyChunk(timestamp, options);
+ }
+ getNextKeyChunk(chunk, options = {}) {
+ return this.videoTrack._backing.getNextKeyChunk(chunk, options);
+ }
+};
+var VideoFrameDrain = class extends BaseMediaFrameDrain {
+ constructor(videoTrack) {
+ super();
+ this.videoTrack = videoTrack;
+ }
+ decoderConfig = null;
+ async createDecoder(onFrame) {
+ if (!this.decoderConfig) {
+ this.decoderConfig = await this.videoTrack.getDecoderConfig();
+ }
+ const decoder = new VideoDecoder({
+ output: onFrame,
+ error: (error) => console.error(error)
+ });
+ decoder.configure(this.decoderConfig);
+ return decoder;
+ }
+ createChunkDrain() {
+ return new EncodedVideoChunkDrain(this.videoTrack);
+ }
+ getKeyFrame(timestamp) {
+ return this.getKeyMediaFrame(timestamp);
+ }
+ getFrame(timestamp) {
+ return this.getMediaFrame(timestamp);
+ }
+ frames(startTimestamp = 0, endTimestamp = Infinity) {
+ return this.mediaFrames(startTimestamp, endTimestamp);
+ }
+};
+var EncodedAudioChunkDrain = class extends BaseChunkDrain {
+ constructor(audioTrack) {
+ super();
+ this.audioTrack = audioTrack;
+ }
+ getFirstChunk(options = {}) {
+ return this.audioTrack._backing.getFirstChunk(options);
+ }
+ getChunk(timestamp, options = {}) {
+ return this.audioTrack._backing.getChunk(timestamp, options);
+ }
+ getNextChunk(chunk, options = {}) {
+ return this.audioTrack._backing.getNextChunk(chunk, options);
+ }
+ getKeyChunk(timestamp, options = {}) {
+ return this.audioTrack._backing.getKeyChunk(timestamp, options);
+ }
+ getNextKeyChunk(chunk, options = {}) {
+ return this.audioTrack._backing.getNextKeyChunk(chunk, options);
+ }
+};
+var AudioDataDrain = class extends BaseMediaFrameDrain {
+ constructor(audioTrack) {
+ super();
+ this.audioTrack = audioTrack;
+ }
+ decoderConfig = null;
+ async createDecoder(onData) {
+ if (!this.decoderConfig) {
+ this.decoderConfig = await this.audioTrack.getDecoderConfig();
+ }
+ const decoder = new AudioDecoder({
+ output: onData,
+ error: (error) => console.error(error)
+ });
+ decoder.configure(this.decoderConfig);
+ return decoder;
+ }
+ createChunkDrain() {
+ return new EncodedAudioChunkDrain(this.audioTrack);
+ }
+ getKeyData(timestamp) {
+ return this.getKeyMediaFrame(timestamp);
+ }
+ getData(timestamp) {
+ return this.getMediaFrame(timestamp);
+ }
+ data(startTimestamp = 0, endTimestamp = Infinity) {
+ return this.mediaFrames(startTimestamp, endTimestamp);
+ }
+};
+var AudioBufferDrain = class {
+ constructor(audioTrack) {
+ this.audioTrack = audioTrack;
+ this.audioDataDrain = new AudioDataDrain(audioTrack);
+ }
+ audioDataDrain;
+ audioDataToWrappedArrayBuffer(data) {
+ if (!data) {
+ return null;
+ }
+ const audioBuffer = new AudioBuffer({
+ numberOfChannels: data.numberOfChannels,
+ length: data.numberOfFrames,
+ sampleRate: data.sampleRate
+ });
+ for (let i = 0; i < data.numberOfChannels; i++) {
+ const dataBytes = new Float32Array(data.allocationSize({ planeIndex: i }));
+ data.copyTo(dataBytes, { planeIndex: i });
+ audioBuffer.copyToChannel(dataBytes, i);
+ }
+ return {
+ buffer: audioBuffer,
+ timestamp: data.timestamp / 1e6
+ };
+ }
+ async getBuffer(timestamp) {
+ const data = await this.audioDataDrain.getData(timestamp);
+ return this.audioDataToWrappedArrayBuffer(data);
+ }
+ async *buffers(startTimestamp = 0, endTimestamp = Infinity) {
+ for await (const data of this.audioDataDrain.data(startTimestamp, endTimestamp)) {
+ const result = this.audioDataToWrappedArrayBuffer(data);
+ data.close();
+ yield result;
+ }
+ }
+};
export {
ALL_FORMATS,
AUDIO_CODECS,
ArrayBufferSource,
ArrayBufferTarget,
+ AudioBufferDrain,
AudioBufferSource,
+ AudioDataDrain,
AudioDataSource,
AudioSource,
BlobSource,
CanvasSource,
+ EncodedAudioChunkDrain,
EncodedAudioChunkSource,
EncodedVideoChunkDrain,
EncodedVideoChunkSource,
diff --git a/package.json b/package.json
index eb5abb6..542011f 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,8 @@
"build": "node build.mjs && tsc && api-extractor run && node append-namespace.mjs",
"build-local": "node build.mjs && tsc && api-extractor run --local --verbose && node append-namespace.mjs",
"watch": "node build.mjs --watch",
- "lint": "eslint ."
+ "lint": "eslint .",
+ "check": "tsc --noEmit"
},
"author": "",
"license": "MIT",
diff --git a/src/codec.ts b/src/codec.ts
index a39a786..cb1528a 100644
--- a/src/codec.ts
+++ b/src/codec.ts
@@ -5,6 +5,7 @@ import {
bytesToHexString,
isAllowSharedBufferSource,
last,
+ readBits,
reverseBitsU32,
} from './misc';
import { SubtitleMetadata } from './subtitles';
@@ -268,13 +269,8 @@ export const buildAudioCodecString = (codec: AudioCodec, numberOfChannels: numbe
export const extractAudioCodecString = (codec: AudioCodec, description: Uint8Array | null) => {
if (codec === 'aac') {
- if (!description || description.byteLength < 2) {
- throw new TypeError('AAC description must be at least 2 bytes long.');
- }
-
- // TODO: Is this correct? Give a source/reason
- const mpeg4AudioObjectType = description[0]! >> 3;
- return `mp4a.40.${mpeg4AudioObjectType}`;
+ const audioSpecificConfig = parseAacAudioSpecificConfig(description);
+ return `mp4a.40.${audioSpecificConfig.objectType}`;
} else if (codec === 'opus') {
return 'opus';
} else if (codec === 'vorbis') {
@@ -285,6 +281,63 @@ export const extractAudioCodecString = (codec: AudioCodec, description: Uint8Arr
throw new TypeError(`Unhandled codec '${codec}'.`);
};
+export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null) => {
+ if (!bytes || bytes.byteLength < 2) {
+ throw new TypeError('AAC description must be at least 2 bytes long.');
+ }
+
+ let bitOffset = 0;
+
+ // 1) Audio Object Type (5 bits)
+ let objectType = readBits(bytes, bitOffset, bitOffset + 5);
+ bitOffset += 5;
+ if (objectType === 31) {
+ // (Escape value) -> 6 bits + 32
+ objectType = 32 + readBits(bytes, bitOffset, bitOffset + 6);
+ bitOffset += 6;
+ }
+
+ // 2) Sampling Frequency Index (4 bits)
+ const frequencyIndex = readBits(bytes, bitOffset, bitOffset + 4);
+ bitOffset += 4;
+ let sampleRate: number | null = null;
+ if (frequencyIndex === 15) {
+ // Explicit sample rate (24 bits)
+ sampleRate = readBits(bytes, bitOffset, bitOffset + 24);
+ bitOffset += 24;
+ } else {
+ const freqTable = [
+ 96000, 88200, 64000, 48000, 44100,
+ 32000, 24000, 22050, 16000, 12000,
+ 11025, 8000, 7350,
+ ];
+ if (frequencyIndex < freqTable.length) {
+ sampleRate = freqTable[frequencyIndex]!;
+ }
+ }
+
+ // 3) Channel Configuration (4 bits)
+ const channelConfiguration = readBits(bytes, bitOffset, bitOffset + 4);
+ bitOffset += 4;
+ let numberOfChannels: number | null = null;
+ if (channelConfiguration >= 1 && channelConfiguration <= 7) {
+ const channelMap = {
+ 1: 1, 2: 2, 3: 3,
+ 4: 4, 5: 5, 6: 6,
+ 7: 8,
+ };
+ numberOfChannels = channelMap[channelConfiguration as keyof typeof channelMap];
+ }
+
+ return {
+ objectType,
+ frequencyIndex,
+ sampleRate,
+ channelConfiguration,
+ numberOfChannels,
+ };
+};
+
export const getVideoEncoderConfigExtension = (codec: VideoCodec) => {
if (codec === 'avc') {
return {
diff --git a/src/demuxer.ts b/src/demuxer.ts
index 7d66b09..dfba130 100644
--- a/src/demuxer.ts
+++ b/src/demuxer.ts
@@ -8,7 +8,7 @@ export abstract class Demuxer {
this.input = input;
}
- abstract getDuration(): Promise;
+ abstract computeDuration(): Promise;
abstract getTracks(): Promise;
abstract getMimeType(): Promise;
}
diff --git a/src/index.ts b/src/index.ts
index 5d3e7b5..bf59d2d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -38,6 +38,12 @@ export { TransformationMatrix } from './misc';
export { Source, ArrayBufferSource, BlobSource } from './source';
export { ALL_FORMATS, ISOBMFF, MP4, MOV, MATROSKA, MKV, WEBM } from './input-format';
export { Input, InputOptions } from './input';
-export { EncodedVideoChunkDrain, VideoFrameDrain } from './media-drain';
+export {
+ EncodedVideoChunkDrain,
+ VideoFrameDrain,
+ EncodedAudioChunkDrain,
+ AudioDataDrain,
+ AudioBufferDrain,
+} from './media-drain';
// 🐡🦔
diff --git a/src/input-track.ts b/src/input-track.ts
index aa1ad0a..84799c6 100644
--- a/src/input-track.ts
+++ b/src/input-track.ts
@@ -3,7 +3,7 @@ import { Rotation } from './misc';
export interface InputTrackBacking {
getCodec(): Promise;
- getDuration(): Promise;
+ computeDuration(): Promise;
}
export abstract class InputTrack {
@@ -26,22 +26,26 @@ export abstract class InputTrack {
return this instanceof InputAudioTrack;
}
- getDuration() {
- return this._backing.getDuration();
+ computeDuration() {
+ return this._backing.computeDuration();
}
}
+export type ChunkRetrievalOptions = {
+ metadataOnly?: boolean;
+};
+
export interface InputVideoTrackBacking extends InputTrackBacking {
getCodec(): Promise;
getWidth(): Promise;
getHeight(): Promise;
getRotation(): Promise;
getDecoderConfig(): Promise;
- getFirstChunk(): Promise;
- getChunk(timestamp: number): Promise;
- getNextChunk(chunk: EncodedVideoChunk): Promise;
- getKeyChunk(timestamp: number): Promise;
- getNextKeyChunk(chunk: EncodedVideoChunk): Promise;
+ getFirstChunk(options: ChunkRetrievalOptions): Promise;
+ getChunk(timestamp: number, options: ChunkRetrievalOptions): Promise;
+ getNextChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions): Promise;
+ getKeyChunk(timestamp: number, options: ChunkRetrievalOptions): Promise;
+ getNextKeyChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions): Promise;
}
export class InputVideoTrack extends InputTrack {
@@ -86,6 +90,11 @@ export interface InputAudioTrackBacking extends InputTrackBacking {
getNumberOfChannels(): Promise;
getSampleRate(): Promise;
getDecoderConfig(): Promise;
+ getFirstChunk(options: ChunkRetrievalOptions): Promise;
+ getChunk(timestamp: number, options: ChunkRetrievalOptions): Promise;
+ getNextChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions): Promise;
+ getKeyChunk(timestamp: number, options: ChunkRetrievalOptions): Promise;
+ getNextKeyChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions): Promise;
}
export class InputAudioTrack extends InputTrack {
diff --git a/src/input.ts b/src/input.ts
index 3d48aa9..837ac04 100644
--- a/src/input.ts
+++ b/src/input.ts
@@ -50,9 +50,9 @@ export class Input {
return this._format;
}
- async getDuration() {
+ async computeDuration() {
const demuxer = await this._getDemuxer();
- return demuxer.getDuration();
+ return demuxer.computeDuration();
}
async getTracks() {
diff --git a/src/isobmff/isobmff-demuxer.ts b/src/isobmff/isobmff-demuxer.ts
index fa6fdc9..17694e0 100644
--- a/src/isobmff/isobmff-demuxer.ts
+++ b/src/isobmff/isobmff-demuxer.ts
@@ -1,7 +1,15 @@
-import { AudioCodec, extractAudioCodecString, extractVideoCodecString, MediaCodec, VideoCodec } from '../codec';
+import {
+ AudioCodec,
+ extractAudioCodecString,
+ extractVideoCodecString,
+ MediaCodec,
+ parseAacAudioSpecificConfig,
+ VideoCodec,
+} from '../codec';
import { Demuxer } from '../demuxer';
import { Input } from '../input';
import {
+ ChunkRetrievalOptions,
InputAudioTrack,
InputAudioTrackBacking,
InputTrack,
@@ -166,14 +174,10 @@ export class IsobmffDemuxer extends Demuxer {
this.chunkReader = new IsobmffReader(new Reader(input._source, 64 * 2 ** 20)); // Max 64 MiB of stored chunks
}
- override async getDuration() {
- await this.readMetadata();
-
- if (this.movieDurationInTimescale === -1) {
- throw new Error('Could not read movie duration.');
- }
-
- return this.movieDurationInTimescale / this.movieTimescale;
+ override async computeDuration() {
+ const tracks = await this.getTracks();
+ const trackDurations = await Promise.all(tracks.map(x => x.computeDuration()));
+ return Math.max(0, ...trackDurations);
}
override async getTracks() {
@@ -447,6 +451,18 @@ export class IsobmffDemuxer extends Demuxer {
const audioTrack = track as InternalAudioTrack;
track.inputTrack = new InputAudioTrack(new IsobmffAudioTrackBacking(audioTrack));
this.tracks.push(track);
+
+ if (track.info.codec === 'aac') {
+ // Some videos have metadata mismatch, so let's try to deduce more accurate values directly
+ // from the AudioSpecificConfig:
+ const audioSpecificConfig = parseAacAudioSpecificConfig(track.info.codecDescription);
+ if (audioSpecificConfig.numberOfChannels !== null) {
+ track.info.numberOfChannels = audioSpecificConfig.numberOfChannels;
+ }
+ if (audioSpecificConfig.sampleRate !== null) {
+ track.info.sampleRate = audioSpecificConfig.sampleRate;
+ }
+ }
}
}
@@ -1287,16 +1303,16 @@ export class IsobmffDemuxer extends Demuxer {
}
}
-abstract class IsobmffTrackBacking implements InputTrackBacking {
- chunkToSampleIndex = new WeakMap();
- sampleIndexToChunk = new Map>();
+abstract class IsobmffTrackBacking implements InputTrackBacking {
+ chunkToSampleIndex = new WeakMap();
+ sampleIndexToChunk = new Map>();
- chunkToFragmentLocation = new WeakMap();
- fragmentLocationToChunk = new Map>();
+ fragmentLocationToChunk = new Map>();
constructor(public internalTrack: InternalTrack) {
@@ -1306,47 +1322,20 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
throw new Error('Not implemented on base class.');
}
- async getDuration() {
- // TODO: Make this produce a proper result in fragmented files
- return this.internalTrack.durationInTimescale / this.internalTrack.timescale;
- }
-}
-
-class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideoTrackBacking {
- override internalTrack: InternalVideoTrack;
-
- constructor(internalTrack: InternalVideoTrack) {
- super(internalTrack);
- this.internalTrack = internalTrack;
+ async computeDuration() {
+ const lastChunk = await this.getChunk(Infinity, { metadataOnly: true });
+ const timestamp = lastChunk?.timestamp;
+ return timestamp ? timestamp / 1e6 : 0;
}
- override async getCodec(): Promise {
- return this.internalTrack.info.codec!;
- }
+ abstract createChunk(
+ data: Uint8Array,
+ timestamp: number,
+ duration: number,
+ isKeyFrame: boolean,
+ ): Chunk;
- async getWidth() {
- return this.internalTrack.info.width;
- }
-
- async getHeight() {
- return this.internalTrack.info.height;
- }
-
- async getRotation() {
- return this.internalTrack.rotation;
- }
-
- async getDecoderConfig(): Promise {
- return {
- codec: extractVideoCodecString(this.internalTrack.info.codec!, this.internalTrack.info.codecDescription),
- codedWidth: this.internalTrack.info.width,
- codedHeight: this.internalTrack.info.height,
- description: this.internalTrack.info.codecDescription ?? undefined,
- colorSpace: this.internalTrack.info.colorSpace ?? undefined,
- };
- }
-
- async getFirstChunk() {
+ async getFirstChunk(options: ChunkRetrievalOptions) {
if (this.internalTrack.demuxer.isFragmented) {
return this.performFragmentedLookup(
() => {
@@ -1359,13 +1348,14 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
},
0,
Infinity,
+ options,
);
}
- return this.fetchChunkForSampleIndex(0);
+ return this.fetchChunkForSampleIndex(0, options);
}
- async getChunk(timestamp: number) {
+ async getChunk(timestamp: number, options: ChunkRetrievalOptions) {
const timestampInTimescale = timestamp * this.internalTrack.timescale;
if (this.internalTrack.demuxer.isFragmented) {
@@ -1373,15 +1363,16 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
() => this.findSampleInFragmentsForTimestamp(timestampInTimescale),
timestampInTimescale,
timestampInTimescale,
+ options,
);
} else {
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const sampleIndex = getSampleIndexForTimestamp(sampleTable, timestampInTimescale);
- return this.fetchChunkForSampleIndex(sampleIndex);
+ return this.fetchChunkForSampleIndex(sampleIndex, options);
}
}
- async getNextChunk(chunk: EncodedVideoChunk) {
+ async getNextChunk(chunk: Chunk, options: ChunkRetrievalOptions) {
if (this.internalTrack.demuxer.isFragmented) {
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
if (locationInFragment === undefined) {
@@ -1438,6 +1429,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
},
sample.presentationTimestamp,
Infinity,
+ options,
);
}
@@ -1445,10 +1437,10 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
if (sampleIndex === undefined) {
throw new Error('Chunk was not created from this track.');
}
- return this.fetchChunkForSampleIndex(sampleIndex + 1);
+ return this.fetchChunkForSampleIndex(sampleIndex + 1, options);
}
- async getKeyChunk(timestamp: number) {
+ async getKeyChunk(timestamp: number, options: ChunkRetrievalOptions) {
const timestampInTimescale = timestamp * this.internalTrack.timescale;
if (this.internalTrack.demuxer.isFragmented) {
@@ -1456,6 +1448,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
() => this.findKeySampleInFragmentsForTimestamp(timestampInTimescale),
timestampInTimescale,
timestampInTimescale,
+ options,
);
}
@@ -1464,10 +1457,10 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
const keyFrameSampleIndex = sampleIndex === -1
? -1
: getRelevantKeyframeIndexForSample(sampleTable, sampleIndex);
- return this.fetchChunkForSampleIndex(keyFrameSampleIndex);
+ return this.fetchChunkForSampleIndex(keyFrameSampleIndex, options);
}
- async getNextKeyChunk(chunk: EncodedVideoChunk) {
+ async getNextKeyChunk(chunk: Chunk, options: ChunkRetrievalOptions) {
if (this.internalTrack.demuxer.isFragmented) {
const locationInFragment = this.chunkToFragmentLocation.get(chunk);
if (locationInFragment === undefined) {
@@ -1534,6 +1527,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
},
sample.presentationTimestamp,
Infinity,
+ options,
);
}
@@ -1543,10 +1537,10 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
}
const sampleTable = this.internalTrack.demuxer.getSampleTableForTrack(this.internalTrack);
const nextKeyFrameSampleIndex = getNextKeyframeIndexForSample(sampleTable, sampleIndex);
- return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex);
+ return this.fetchChunkForSampleIndex(nextKeyFrameSampleIndex, options);
}
- private async fetchChunkForSampleIndex(sampleIndex: number) {
+ private async fetchChunkForSampleIndex(sampleIndex: number, options: ChunkRetrievalOptions) {
if (sampleIndex === -1) {
return null;
}
@@ -1562,23 +1556,25 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
return null;
}
- // Load the entire chunk
- await this.internalTrack.demuxer.chunkReader.reader.loadRange(
- sampleInfo.chunkOffset,
- sampleInfo.chunkOffset + sampleInfo.chunkSize,
- );
+ let data: Uint8Array;
+ if (options.metadataOnly) {
+ data = new Uint8Array(0); // Placeholder buffer
+ } else {
+ // Load the entire chunk
+ await this.internalTrack.demuxer.chunkReader.reader.loadRange(
+ sampleInfo.chunkOffset,
+ sampleInfo.chunkOffset + sampleInfo.chunkSize,
+ );
- const data = this.internalTrack.demuxer.chunkReader.readRange(
- sampleInfo.sampleOffset,
- sampleInfo.sampleOffset + sampleInfo.sampleSize,
- );
+ data = this.internalTrack.demuxer.chunkReader.readRange(
+ sampleInfo.sampleOffset,
+ sampleInfo.sampleOffset + sampleInfo.sampleSize,
+ );
+ }
- const chunk = new EncodedVideoChunk({
- data,
- timestamp: (1e6 * sampleInfo.presentationTimestamp) / this.internalTrack.timescale,
- duration: (1e6 * sampleInfo.duration) / this.internalTrack.timescale,
- type: sampleInfo.isKeyFrame ? 'key' : 'delta',
- });
+ const timestamp = (1e6 * sampleInfo.presentationTimestamp) / this.internalTrack.timescale;
+ const duration = (1e6 * sampleInfo.duration) / this.internalTrack.timescale;
+ const chunk = this.createChunk(data, timestamp, duration, sampleInfo.isKeyFrame);
this.chunkToSampleIndex.set(chunk, sampleIndex);
this.sampleIndexToChunk.set(sampleIndex, new WeakRef(chunk));
@@ -1586,7 +1582,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
return chunk;
}
- private async fetchChunkInFragment(fragment: Fragment, sampleIndex: number) {
+ private async fetchChunkInFragment(fragment: Fragment, sampleIndex: number, options: ChunkRetrievalOptions) {
if (sampleIndex === -1) {
return null;
}
@@ -1601,20 +1597,22 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
const sample = trackData.samples[sampleIndex];
assert(sample);
- // Load the entire fragment
- await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd);
+ let data: Uint8Array;
+ if (options.metadataOnly) {
+ data = new Uint8Array(0); // Placeholder buffer
+ } else {
+ // Load the entire fragment
+ await this.internalTrack.demuxer.chunkReader.reader.loadRange(fragment.dataStart, fragment.dataEnd);
- const data = this.internalTrack.demuxer.chunkReader.readRange(
- sample.byteOffset,
- sample.byteOffset + sample.byteSize,
- );
+ data = this.internalTrack.demuxer.chunkReader.readRange(
+ sample.byteOffset,
+ sample.byteOffset + sample.byteSize,
+ );
+ }
- const chunk = new EncodedVideoChunk({
- data,
- timestamp: (1e6 * sample.presentationTimestamp) / this.internalTrack.timescale,
- duration: (1e6 * sample.duration) / this.internalTrack.timescale,
- type: sample.isKeyFrame ? 'key' : 'delta',
- });
+ const timestamp = (1e6 * sample.presentationTimestamp) / this.internalTrack.timescale;
+ const duration = (1e6 * sample.duration) / this.internalTrack.timescale;
+ const chunk = this.createChunk(data, timestamp, duration, sample.isKeyFrame);
this.chunkToFragmentLocation.set(chunk, { fragment, sampleIndex });
this.fragmentLocationToChunk.set(compositeKey, new WeakRef(chunk));
@@ -1663,11 +1661,11 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
const trackData = fragment.trackData.get(this.internalTrack.id)!;
const index = trackData.presentationTimestamps.findLastIndex((x) => {
const sample = trackData.samples[x.sampleIndex]!;
- return sample.isKeyFrame;
+ return sample.isKeyFrame && x.presentationTimestamp <= timestampInTimescale;
});
if (index === -1) {
- throw new Error('Not supported: Fragment does not contain key sample.');
+ throw new Error('Not supported: Fragment does not begin with a key sample.');
}
const entry = trackData.presentationTimestamps[index]!;
@@ -1683,22 +1681,23 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
// This function returns the best-matching sample that is currently loaded. Based on this information, we know
// which fragments we need to load to find the actual match.
getBestMatch: () => { fragmentIndex: number; sampleIndex: number; correctSampleFound: boolean },
- // The timestamp for which we know the correct sample will not come before it
- earliestTimestamp: number,
+ // The timestamp with which we can search the lookup table
+ searchTimestamp: number,
// The timestamp for which we know the correct sample will not come after it
latestTimestamp: number,
+ options: ChunkRetrievalOptions,
) {
- const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
- if (correctSampleFound) {
- // The correct sample already exists, easy path.
- const fragment = this.internalTrack.fragments[fragmentIndex]!;
- return this.fetchChunkInFragment(fragment, sampleIndex);
- }
-
const demuxer = this.internalTrack.demuxer;
const release = await demuxer.fragmentLookupMutex.acquire(); // The algorithm requires exclusivity
try {
+ const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
+ if (correctSampleFound) {
+ // The correct sample already exists, easy path.
+ const fragment = this.internalTrack.fragments[fragmentIndex]!;
+ return this.fetchChunkInFragment(fragment, sampleIndex, options);
+ }
+
const isobmffReader = demuxer.isobmffReader;
const sourceSize = await isobmffReader.reader.source._getSize();
@@ -1712,7 +1711,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
// but can jump right into the correct fragment (or at least nearby).
const index = binarySearchLessOrEqual(
this.internalTrack.fragmentLookupTable,
- earliestTimestamp,
+ searchTimestamp,
x => x.timestamp,
);
@@ -1769,7 +1768,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
const { fragmentIndex, sampleIndex, correctSampleFound } = getBestMatch();
if (correctSampleFound) {
const fragment = this.internalTrack.fragments[fragmentIndex]!;
- return this.fetchChunkInFragment(fragment, sampleIndex);
+ return this.fetchChunkInFragment(fragment, sampleIndex, options);
}
if (fragmentIndex !== -1) {
bestFragmentIndex = fragmentIndex;
@@ -1789,7 +1788,7 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
if (bestFragmentIndex !== -1) {
// If we finished looping but didn't find a perfect match, still return the best match we found
const fragment = this.internalTrack.fragments[bestFragmentIndex]!;
- return this.fetchChunkInFragment(fragment, bestSampleIndex);
+ return this.fetchChunkInFragment(fragment, bestSampleIndex, options);
}
return null;
@@ -1799,7 +1798,51 @@ class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideo
}
}
-class IsobmffAudioTrackBacking extends IsobmffTrackBacking implements InputAudioTrackBacking {
+class IsobmffVideoTrackBacking extends IsobmffTrackBacking implements InputVideoTrackBacking {
+ override internalTrack: InternalVideoTrack;
+
+ constructor(internalTrack: InternalVideoTrack) {
+ super(internalTrack);
+ this.internalTrack = internalTrack;
+ }
+
+ override async getCodec(): Promise {
+ return this.internalTrack.info.codec!;
+ }
+
+ async getWidth() {
+ return this.internalTrack.info.width;
+ }
+
+ async getHeight() {
+ return this.internalTrack.info.height;
+ }
+
+ async getRotation() {
+ return this.internalTrack.rotation;
+ }
+
+ async getDecoderConfig(): Promise {
+ return {
+ codec: extractVideoCodecString(this.internalTrack.info.codec!, this.internalTrack.info.codecDescription),
+ codedWidth: this.internalTrack.info.width,
+ codedHeight: this.internalTrack.info.height,
+ description: this.internalTrack.info.codecDescription ?? undefined,
+ colorSpace: this.internalTrack.info.colorSpace ?? undefined,
+ };
+ }
+
+ createChunk(data: Uint8Array, timestamp: number, duration: number, isKeyFrame: boolean) {
+ return new EncodedVideoChunk({
+ data,
+ timestamp,
+ duration,
+ type: isKeyFrame ? 'key' : 'delta',
+ });
+ }
+}
+
+class IsobmffAudioTrackBacking extends IsobmffTrackBacking implements InputAudioTrackBacking {
override internalTrack: InternalAudioTrack;
constructor(internalTrack: InternalAudioTrack) {
@@ -1827,6 +1870,15 @@ class IsobmffAudioTrackBacking extends IsobmffTrackBacking implements InputAudio
description: this.internalTrack.info.codecDescription ?? undefined,
};
}
+
+ createChunk(data: Uint8Array, timestamp: number, duration: number, isKeyFrame: boolean) {
+ return new EncodedAudioChunk({
+ data,
+ timestamp,
+ duration,
+ type: isKeyFrame ? 'key' : 'delta',
+ });
+ }
}
const getSampleIndexForTimestamp = (sampleTable: SampleTable, timescaleUnits: number) => {
diff --git a/src/media-drain.ts b/src/media-drain.ts
index 5477ffd..e5f5546 100644
--- a/src/media-drain.ts
+++ b/src/media-drain.ts
@@ -1,31 +1,15 @@
-import { InputVideoTrack } from './input-track';
+import { ChunkRetrievalOptions, InputAudioTrack, InputVideoTrack } from './input-track';
import { assert, promiseWithResolvers } from './misc';
-export class EncodedVideoChunkDrain {
- constructor(public videoTrack: InputVideoTrack) {}
+abstract class BaseChunkDrain {
+ abstract getFirstChunk(options?: ChunkRetrievalOptions): Promise;
+ abstract getChunk(timestamp: number, options?: ChunkRetrievalOptions): Promise;
+ abstract getNextChunk(chunk: Chunk, options?: ChunkRetrievalOptions): Promise;
+ abstract getKeyChunk(timestamp: number, options?: ChunkRetrievalOptions): Promise;
+ abstract getNextKeyChunk(chunk: Chunk, options?: ChunkRetrievalOptions): Promise;
- getFirstChunk() {
- return this.videoTrack._backing.getFirstChunk();
- }
-
- getChunk(timestamp: number) {
- return this.videoTrack._backing.getChunk(timestamp);
- }
-
- getNextChunk(chunk: EncodedVideoChunk) {
- return this.videoTrack._backing.getNextChunk(chunk);
- }
-
- getKeyChunk(timestamp: number) {
- return this.videoTrack._backing.getKeyChunk(timestamp);
- }
-
- getNextKeyChunk(chunk: EncodedVideoChunk) {
- return this.videoTrack._backing.getNextKeyChunk(chunk);
- }
-
- async* chunks(startChunk?: EncodedVideoChunk, endTimestamp = Infinity) {
- const chunkQueue: EncodedVideoChunk[] = [];
+ async* chunks(startChunk?: Chunk, endTimestamp = Infinity) {
+ const chunkQueue: Chunk[] = [];
let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers();
let { promise: queueDequeue, resolve: onQueueDequeue } = promiseWithResolvers();
@@ -87,30 +71,19 @@ export class EncodedVideoChunkDrain {
}
}
-export class VideoFrameDrain {
- decoderConfig: VideoDecoderConfig | null = null;
+abstract class BaseMediaFrameDrain<
+ Chunk extends EncodedVideoChunk | EncodedAudioChunk,
+ MediaFrame extends VideoFrame | AudioData,
+> {
+ abstract createDecoder(onMedia: (media: MediaFrame) => unknown): Promise;
+ abstract createChunkDrain(): BaseChunkDrain;
- constructor(public videoTrack: InputVideoTrack) {}
-
- async createDecoder(onFrame: (frame: VideoFrame) => unknown) {
- if (!this.decoderConfig) {
- this.decoderConfig = await this.videoTrack.getDecoderConfig();
- }
-
- const decoder = new VideoDecoder({
- output: onFrame,
- error: error => console.error(error),
- });
- decoder.configure(this.decoderConfig);
-
- return decoder;
- }
-
- async getKeyFrame(timestamp: number) {
- let result: VideoFrame | null = null;
+ protected async getKeyMediaFrame(timestamp: number): Promise {
+ let result: MediaFrame | null = null;
const decoder = await this.createDecoder(frame => result = frame);
- const chunk = await this.videoTrack._backing.getKeyChunk(timestamp);
+ const chunkDrain = this.createChunkDrain();
+ const chunk = await chunkDrain.getKeyChunk(timestamp);
if (!chunk) {
return null;
}
@@ -123,8 +96,8 @@ export class VideoFrameDrain {
return result;
}
- async getFrame(timestamp: number) {
- let result: VideoFrame | null = null;
+ protected async getMediaFrame(timestamp: number): Promise {
+ let result: MediaFrame | null = null;
const decoder = await this.createDecoder((frame) => {
if (frame.timestamp / 1e6 <= timestamp) {
@@ -134,19 +107,20 @@ export class VideoFrameDrain {
frame.close();
}
});
- const keyChunk = await this.videoTrack._backing.getKeyChunk(timestamp);
+ const chunkDrain = this.createChunkDrain();
+ const keyChunk = await chunkDrain.getKeyChunk(timestamp);
if (!keyChunk) {
return null;
}
- const targetChunk = await this.videoTrack._backing.getChunk(timestamp);
+ const targetChunk = await chunkDrain.getChunk(timestamp);
assert(targetChunk);
decoder.decode(keyChunk);
let currentChunk = keyChunk;
while (currentChunk !== targetChunk) {
- const nextChunk = await this.videoTrack._backing.getNextChunk(currentChunk);
+ const nextChunk = await chunkDrain.getNextChunk(currentChunk);
assert(nextChunk);
currentChunk = nextChunk;
@@ -163,10 +137,10 @@ export class VideoFrameDrain {
return result;
}
- async* frames(startTimestamp = 0, endTimestamp = Infinity) {
- const frameQueue: VideoFrame[] = [];
+ protected async* mediaFrames(startTimestamp = 0, endTimestamp = Infinity) {
+ const frameQueue: MediaFrame[] = [];
let firstFrameQueued = false;
- let lastFrame: VideoFrame | null = null;
+ let lastFrame: MediaFrame | null = null;
let { promise: queueNotEmpty, resolve: onQueueNotEmpty } = promiseWithResolvers();
let ended = false;
@@ -184,10 +158,10 @@ export class VideoFrameDrain {
if (lastFrame) {
if (frameTimestamp > startTimestamp) {
- // We don't know ahead of time what the first frame is. This is because the first frame is the last
- // frame whose timestamp is less than or equal to the start timestamp. Therefore we need to wait
- // for the first frame after the start timestamp, and then we'll know that the previous frame was
- // the first frame.
+ // We don't know ahead of time what the first first is. This is because the first first is the last
+ // first whose timestamp is less than or equal to the start timestamp. Therefore we need to wait
+ // for the first first after the start timestamp, and then we'll know that the previous first was
+ // the first first.
frameQueue.push(lastFrame);
firstFrameQueued = true;
} else {
@@ -208,8 +182,8 @@ export class VideoFrameDrain {
}
});
- const keyChunk = await this.videoTrack._backing.getKeyChunk(startTimestamp)
- ?? await this.videoTrack._backing.getFirstChunk();
+ const chunkDrain = this.createChunkDrain();
+ const keyChunk = await chunkDrain.getKeyChunk(startTimestamp) ?? await chunkDrain.getFirstChunk();
if (!keyChunk) {
return;
}
@@ -218,7 +192,7 @@ export class VideoFrameDrain {
// The following is the "pump" process that keeps pumping chunks into the decoder
void (async () => {
- let currentChunk: EncodedVideoChunk | null = keyChunk;
+ let currentChunk: Chunk | null = keyChunk;
let chunksEndTimestamp = Infinity;
if (endTimestamp < Infinity) {
@@ -226,23 +200,25 @@ export class VideoFrameDrain {
// frames (B-frames). Instead, we'll need to keep decoding chunks until we get a frame that exceeds
// this end time. However, we can still put a bound on it: Since key frames are by definition never
// out of order, we can stop at the first key frame after the end timestamp.
- const endFrame = await this.videoTrack._backing.getChunk(endTimestamp);
+ const endFrame = await chunkDrain.getChunk(endTimestamp);
const endKeyFrame = !endFrame
? null
: endFrame.type === 'key' && endFrame.timestamp / 1e6 === endTimestamp
? endFrame
- : await this.videoTrack._backing.getNextKeyChunk(endFrame);
+ : await chunkDrain.getNextKeyChunk(endFrame);
if (endKeyFrame) {
chunksEndTimestamp = endKeyFrame.timestamp / 1e6;
}
}
- const chunkDrain = new EncodedVideoChunkDrain(this.videoTrack);
const chunks = chunkDrain.chunks(keyChunk, chunksEndTimestamp);
await chunks.next();
while (currentChunk && !ended) {
+ // TODO: Some sort of queue size limit for the frames? Right now this code relies on the pace at which
+ // the consumer calls .close()
+
decoder.decode(currentChunk);
if (decoder.decodeQueueSize >= 10) {
@@ -285,3 +261,181 @@ export class VideoFrameDrain {
}
}
}
+
+export class EncodedVideoChunkDrain extends BaseChunkDrain {
+ constructor(public videoTrack: InputVideoTrack) {
+ super();
+ }
+
+ getFirstChunk(options: ChunkRetrievalOptions = {}) {
+ return this.videoTrack._backing.getFirstChunk(options);
+ }
+
+ getChunk(timestamp: number, options: ChunkRetrievalOptions = {}) {
+ return this.videoTrack._backing.getChunk(timestamp, options);
+ }
+
+ getNextChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions = {}) {
+ return this.videoTrack._backing.getNextChunk(chunk, options);
+ }
+
+ getKeyChunk(timestamp: number, options: ChunkRetrievalOptions = {}) {
+ return this.videoTrack._backing.getKeyChunk(timestamp, options);
+ }
+
+ getNextKeyChunk(chunk: EncodedVideoChunk, options: ChunkRetrievalOptions = {}) {
+ return this.videoTrack._backing.getNextKeyChunk(chunk, options);
+ }
+}
+
+export class VideoFrameDrain extends BaseMediaFrameDrain {
+ decoderConfig: VideoDecoderConfig | null = null;
+
+ constructor(public videoTrack: InputVideoTrack) {
+ super();
+ }
+
+ async createDecoder(onFrame: (frame: VideoFrame) => unknown) {
+ if (!this.decoderConfig) {
+ this.decoderConfig = await this.videoTrack.getDecoderConfig();
+ }
+
+ const decoder = new VideoDecoder({
+ output: onFrame,
+ error: error => console.error(error),
+ });
+ decoder.configure(this.decoderConfig);
+
+ return decoder;
+ }
+
+ createChunkDrain() {
+ return new EncodedVideoChunkDrain(this.videoTrack);
+ }
+
+ getKeyFrame(timestamp: number) {
+ return this.getKeyMediaFrame(timestamp);
+ }
+
+ getFrame(timestamp: number) {
+ return this.getMediaFrame(timestamp);
+ }
+
+ frames(startTimestamp = 0, endTimestamp = Infinity) {
+ return this.mediaFrames(startTimestamp, endTimestamp);
+ }
+}
+
+export class EncodedAudioChunkDrain extends BaseChunkDrain {
+ constructor(public audioTrack: InputAudioTrack) {
+ super();
+ }
+
+ getFirstChunk(options: ChunkRetrievalOptions = {}) {
+ return this.audioTrack._backing.getFirstChunk(options);
+ }
+
+ getChunk(timestamp: number, options: ChunkRetrievalOptions = {}) {
+ return this.audioTrack._backing.getChunk(timestamp, options);
+ }
+
+ getNextChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions = {}) {
+ return this.audioTrack._backing.getNextChunk(chunk, options);
+ }
+
+ getKeyChunk(timestamp: number, options: ChunkRetrievalOptions = {}) {
+ return this.audioTrack._backing.getKeyChunk(timestamp, options);
+ }
+
+ getNextKeyChunk(chunk: EncodedAudioChunk, options: ChunkRetrievalOptions = {}) {
+ return this.audioTrack._backing.getNextKeyChunk(chunk, options);
+ }
+}
+
+export class AudioDataDrain extends BaseMediaFrameDrain {
+ decoderConfig: AudioDecoderConfig | null = null;
+
+ constructor(public audioTrack: InputAudioTrack) {
+ super();
+ }
+
+ async createDecoder(onData: (data: AudioData) => unknown) {
+ if (!this.decoderConfig) {
+ this.decoderConfig = await this.audioTrack.getDecoderConfig();
+ }
+
+ const decoder = new AudioDecoder({
+ output: onData,
+ error: error => console.error(error),
+ });
+ decoder.configure(this.decoderConfig);
+
+ return decoder;
+ }
+
+ createChunkDrain() {
+ return new EncodedAudioChunkDrain(this.audioTrack);
+ }
+
+ getKeyData(timestamp: number) {
+ return this.getKeyMediaFrame(timestamp);
+ }
+
+ getData(timestamp: number) {
+ return this.getMediaFrame(timestamp);
+ }
+
+ data(startTimestamp = 0, endTimestamp = Infinity) {
+ return this.mediaFrames(startTimestamp, endTimestamp);
+ }
+}
+
+type WrappedAudioBuffer = {
+ buffer: AudioBuffer;
+ timestamp: number;
+};
+
+export class AudioBufferDrain {
+ audioDataDrain: AudioDataDrain;
+
+ constructor(public audioTrack: InputAudioTrack) {
+ this.audioDataDrain = new AudioDataDrain(audioTrack);
+ }
+
+ private audioDataToWrappedArrayBuffer(data: AudioData | null): WrappedAudioBuffer | null {
+ if (!data) {
+ return null;
+ }
+
+ const audioBuffer = new AudioBuffer({
+ numberOfChannels: data.numberOfChannels,
+ length: data.numberOfFrames,
+ sampleRate: data.sampleRate,
+ });
+
+ for (let i = 0; i < data.numberOfChannels; i++) {
+ const dataBytes = new Float32Array(data.allocationSize({ planeIndex: i }));
+ data.copyTo(dataBytes, { planeIndex: i });
+ audioBuffer.copyToChannel(dataBytes, i);
+ }
+
+ return {
+ buffer: audioBuffer,
+ timestamp: data.timestamp / 1e6,
+ };
+ }
+
+ async getBuffer(timestamp: number) {
+ const data = await this.audioDataDrain.getData(timestamp);
+ return this.audioDataToWrappedArrayBuffer(data);
+ }
+
+ async* buffers(startTimestamp = 0, endTimestamp = Infinity) {
+ for await (const data of this.audioDataDrain.data(startTimestamp, endTimestamp)) {
+ const result = this.audioDataToWrappedArrayBuffer(data);
+ data.close();
+
+ yield result;
+ }
+ }
+}