@lumen5/framefusion 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/framefusion.cjs +1 -1
- package/dist/framefusion.cjs.map +1 -1
- package/dist/framefusion.es.js +254 -406
- package/dist/framefusion.es.js.map +1 -1
- package/dist/src/backends/beamcoder.d.ts +34 -159
- package/dist/src/backends/beamcoder.js +254 -542
- package/dist/src/backends/beamcoder.js.map +1 -1
- package/dist/test/downloadurl.test.js +2 -2
- package/dist/test/downloadurl.test.js.map +1 -1
- package/dist/test/framefusion.test.js +154 -332
- package/dist/test/framefusion.test.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,3 +92,11 @@ We also want to build the library in a way that we can provide different backend
|
|
|
92
92
|
Only point 1 is implemented so far (Extracting frames from videos).
|
|
93
93
|
|
|
94
94
|
We also only provide a beamcoder backend and we still have to figure out how we can swap backends and skip compiling beamcoder. This could be through different npm packages.
|
|
95
|
+
|
|
96
|
+
# Contributing
|
|
97
|
+
|
|
98
|
+
Make sure you follow our [code of conduct](CODE_OF_CONDUCT.md).
|
|
99
|
+
|
|
100
|
+
Additionally, make sure that any code and video samples you add to the repo are in the public domain or compatible with our [GPLv3 license](LICENSE).
|
|
101
|
+
|
|
102
|
+
It's preferable to build new test videos to avoid committing copyrighted videos that might have been encountered in production environments.
|
package/dist/framefusion.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var re=Object.defineProperty;var ie=(i,r,e)=>r in i?re(i,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[r]=e;var f=(i,r,e)=>(ie(i,typeof r!="symbol"?r+"":r,e),e),z=(i,r,e)=>{if(!r.has(i))throw TypeError("Cannot "+e)};var a=(i,r,e)=>(z(i,r,"read from private field"),e?e.call(i):r.get(i)),c=(i,r,e)=>{if(r.has(i))throw TypeError("Cannot add the same private member more than once");r instanceof WeakSet?r.add(i):r.set(i,e)},u=(i,r,e,t)=>(z(i,r,"write to private field"),t?t.call(i,e):r.set(i,e),e);var d=(i,r,e)=>(z(i,r,"access private method"),e);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=require("@antoinemopa/beamcoder"),ae=require("canvas"),ne=require("path"),oe=require("node:https"),he=require("http"),ce=require("tmp"),me=require("fs-extra");class de{static async create(r){throw new Error("Not implemented")}async init({inputFileOrUrl:r,outputFile:e,threadCount:t=8,endTime:s,interpolateFps:n,interpolateMode:h}){throw new Error("Not implemented")}get duration(){throw new Error("Not implemented")}get width(){throw new Error("Not implemented")}get height(){throw new Error("Not implemented")}async seekToPTS(r){throw new Error("Not implemented")}async getFrameAtTime(r){throw new Error("Not implemented")}async getImageDataAtTime(r){throw new Error("Not implemented")}async getFrameAtPts(r){throw new Error("Not implemented")}async seekToTime(r){throw new Error("Not implemented")}ptsToTime(r){throw new Error("Not implemented")}async readFrames({onFrameAvailable:r,flush:e=!0}={flush:!0,onFrameAvailable:()=>!0}){throw new Error("Not implemented")}async dispose(){throw new Error("Not implemented")}}class le extends Error{}var T,F,k,x;class ue{constructor(r){c(this,T,void 0);c(this,F,void 0);c(this,k,void 0);c(this,x,void 0);u(this,T,r)}get filepath(){return a(this,k)}async download(){await new Promise((r,e)=>{const t=a(this,T),s=ne.extname(t);u(this,x,ce.fileSync({postfix:s}));try{const n=t.startsWith("https://")?oe:he;u(this,F,n.get(t,h=>{const o=h.headers["content-type"];if(!o.includes("video")){const p=new Error(`Source ${t}, returned unsupported content type ${o}`);e(p);return}const g=me.createWriteStream(a(this,x).name);h.pipe(g),g.on("finish",()=>{g.close(),u(this,k,a(this,x).name),r()}),g.on("error",p=>{e(p)})})),a(this,F).on("error",h=>{h instanceof le||e(h)})}catch(n){e(n)}})}clear(){a(this,x)&&a(this,x).removeCallback(),a(this,T)&&u(this,T,void 0),a(this,F)&&u(this,F,null),a(this,k)&&u(this,k,void 0)}}T=new WeakMap,F=new WeakMap,k=new WeakMap,x=new WeakMap;const fe="video",we=async({stream:i,outputPixelFormat:r,interpolateFps:e,interpolateMode:t="fast"})=>{if(!i.codecpar.format)return null;let s=[`[in0:v]format=${i.codecpar.format}`];if(e)if(t==="high-quality")s=[...s,`minterpolate=fps=${e}`];else if(t==="fast")s=[...s,`fps=${e}`];else throw new Error(`Unexpected interpolation mode: ${t}`);s=[...s];const n=s.join(", ")+"[out0:v]";return console.log(`filterSpec: ${n}`),b.filterer({filterType:"video",inputParams:[{name:"in0:v",width:i.codecpar.width,height:i.codecpar.height,pixelFormat:i.codecpar.format,timeBase:i.time_base,pixelAspect:i.sample_aspect_ratio}],outputParams:[{name:"out0:v",pixelFormat:r}],filterSpec:n})};var E,_,M,A,m,D,H,S,v,N,V,j,X,q,Y,R,K,I,G,L,Z,$,J,C,ee,O,te;const Q=class extends de{constructor(){super(...arguments);c(this,D);c(this,S);c(this,N);c(this,j);c(this,q);c(this,R);c(this,I);c(this,L);c(this,$);c(this,C);c(this,O);f(this,"decoder",null);f(this,"demuxer",null);f(this,"encoder",null);f(this,"muxer",null);f(this,"filterer",null);f(this,"packet");f(this,"endTime");c(this,E,void 0);c(this,_,0);f(this,"demuxerStream");f(this,"readStream");f(this,"lastFrame",null);f(this,"filteredFrames",[]);c(this,M,!1);c(this,A,!1);c(this,m,-1)}static async create(e){const t=new Q;return await t.init(e),t}async init({inputFileOrUrl:e,outputFile:t,threadCount:s=8,endTime:n,interpolateFps:h,interpolateMode:o}){if(!e)throw new Error("Can only use file OR url");let g,p="rgba";console.log("init",{inputFileOrUrl:e,outputFile:t,threadCount:s,endTime:n,interpolateFps:h,interpolateMode:o,outputPixelFormat:p});let U;if(e.startsWith("http")){console.log("downloading url");const w=new ue(e);await w.download(),e=w.filepath,console.log("finished downloading")}console.log("file mode");const l=await b.demuxer("file:"+e);if(u(this,m,l.streams.findIndex(w=>w.codecpar.codec_type===fe)),a(this,m)===-1)throw new Error("File has no video stream!");console.log({time_base:l.streams[a(this,m)].time_base,stream:a(this,m)});const P=b.decoder({demuxer:l,width:l.streams[a(this,m)].codecpar.width,height:l.streams[a(this,m)].codecpar.height,stream_index:a(this,m),pix_fmt:l.streams[a(this,m)].codecpar.format,thread_count:s});let y=null;if(t&&(["png","tiff","mjpeg"].forEach(w=>{t.endsWith(w)&&(y=w,y==="mjpeg"&&(p="yuvj422p"),y==="png"&&(p="rgb24"))}),!y))throw new Error("Output format could not be determined");const W=await we({stream:l.streams[a(this,m)],outputPixelFormat:p,interpolateFps:h,interpolateMode:o});if(y){const w=pe({decoder:P,outputFormat:y,thread_count:s});this.encoder=w;const se=await ge({thread_count:s,filename:t,sourceDemuxer:l,streamIndex:a(this,m)});this.muxer=se}this.decoder=P,this.demuxer=l,this.filterer=W,this.endTime=n,this.demuxerStream=U,this.readStream=g}get duration(){const e=this.demuxer.streams[a(this,m)].time_base,t=this.demuxer.streams.map(s=>s.duration*e[0]/e[1]);return Math.max(...t)}get width(){return this.demuxer.streams[a(this,m)].codecpar.width}get height(){return this.demuxer.streams[a(this,m)].codecpar.height}async seekToPTS(e){if(!(e===0&&!this.packet))for(this.lastFrame=null,u(this,_,e),console.log(`Seeking to PTS=${e}`),await d(this,N,V).call(this),await this.demuxer.seek({stream_index:a(this,m),timestamp:e}),await d(this,N,V).call(this);this.packet&&this.packet.pts>e;)this.packet.flags={DISCARD:!0},await d(this,S,v).call(this)}async getFrameAtTime(e){const t=Math.floor(this.timeToPts(e));return this.getFrameAtPts(t)}async getImageDataAtTime(e){const t=Math.floor(this.timeToPts(e)),s=await this.getFrameAtPts(t),n=4,h=s.width*s.height*n,o=new Uint8ClampedArray(h),g=s.linesize,p=s.data[0];for(let l=0;l<s.height;l++){const P=l*g,y=P+s.width*n,W=p.slice(P,y),w=l*s.width*n;o.set(W,w)}return ae.createImageData(o,s.width,s.height)}async getFrameAtPts(e){if(this.packet){const n=this.ptsToTime(e),h=this.ptsToTime(this.lastFrame.pts),o=2;Math.abs(n-h)>o&&await this.seekToPTS(e)}if(this.packet===null&&this.filteredFrames.length===0&&e>=this.lastFrame.pts)return this.lastFrame;if(this.filteredFrames.length>0&&e<this.filteredFrames[0].pts)return this.lastFrame;let t=null;const s=await new Promise(async n=>{const h=async o=>o.pts===e?(this.lastFrame=o,n(o),!1):o.pts>e?this.lastFrame?(n(this.lastFrame),this.filteredFrames=[o,...this.filteredFrames],!1):(this.filteredFrames=[o,...this.filteredFrames],this.lastFrame=o,n(o),!1):this.packet===null&&this.filteredFrames.length===0&&e>=this.lastFrame.pts?(n(o),!1):(this.lastFrame=o,!0);t=this.readFrames({onFrameAvailable:h,flush:!0}),await t});return await t,s}async seekToTime(e){await this.seekToPTS(this.timeToPts(e))}timeToPts(e){const t=this.demuxer.streams[a(this,m)].time_base;return e*t[1]/t[0]}ptsToTime(e){const t=this.demuxer.streams[a(this,m)].time_base;return e*t[0]/t[1]}async withLock(e){if(a(this,E))throw new Error("Multiple attempts to use beamcoder at the same time.");u(this,E,!0),await e(),u(this,E,!1)}async readFrames({onFrameAvailable:e,flush:t=!0}={flush:!0,onFrameAvailable:()=>!0}){await this.withLock(async()=>{const s=e;if(a(this,$,J)&&(e=async h=>{const o=await this.encoder.encode(h);return await d(this,R,K).call(this,o),await s(h)}),!!d(this,q,Y).call(this,[],{onFrameAvailable:e})&&this.packet!==null){{const{needsMore:h}=await d(this,L,Z).call(this,{onFrameAvailable:e});if(!h)return}t&&await d(this,O,te).call(this,{onFrameAvailable:e})}})}async dispose(){await new Promise(e=>{setTimeout(()=>{this.readStream&&(this.readStream.unpipe(this.demuxerStream),this.readStream.destroy())},30)})}};let B=Q;E=new WeakMap,_=new WeakMap,M=new WeakMap,A=new WeakMap,m=new WeakMap,D=new WeakSet,H=async function(){if(a(this,M))throw new Error("Error: Trying to read after stream has stopped.");try{const e=await this.demuxer.read();return e||u(this,M,!0),e}catch(e){throw e}},S=new WeakSet,v=async function(){if(this.packet===null)throw new Error("Stream is over!");for(this.packet=await d(this,D,H).call(this);this.packet&&this.packet.stream_index!==a(this,m);)if(this.packet=await d(this,D,H).call(this),this.packet===null)return},N=new WeakSet,V=async function(){for(;!this.packet||this.packet.stream_index!==a(this,m);)await d(this,S,v).call(this)},j=new WeakSet,X=async function(e){const t=await this.filterer.filter([{name:"in0:v",frames:e}]);return this.filteredFrames=t.flatMap(s=>s.frames),this.filteredFrames},q=new WeakSet,Y=async function(e,{onFrameAvailable:t=()=>!0}){let s=!0;for(;this.filteredFrames.length>0&&s;){const n=this.filteredFrames.shift();s=await t(n)}return{needsMore:s}},R=new WeakSet,K=async function(e){for(let t=0;t<e.packets.length;t++)(!a(this,_)||e.packets[t].pts>=a(this,_))&&await this.muxer.writeFrame(e.packets[t])},I=new WeakSet,G=async function({decoderResult:e,onFrameAvailable:t}){const s=await d(this,j,X).call(this,e.frames),{needsMore:n}=await d(this,q,Y).call(this,s,{onFrameAvailable:t});return{needsMore:n}},L=new WeakSet,Z=async function({onFrameAvailable:e}){for(await d(this,S,v).call(this);this.packet;){const t=await this.decoder.decode(this.packet),{needsMore:s}=await d(this,I,G).call(this,{decoderResult:t,onFrameAvailable:e});if(!s)return{needsMore:s};await d(this,S,v).call(this)}return{needsMore:!0}},$=new WeakSet,J=function(){return this.encoder!==null},C=new WeakSet,ee=async function(){if(!a(this,$,J))return;const e=await this.encoder.flush();await d(this,R,K).call(this,e)},O=new WeakSet,te=async function({onFrameAvailable:e}){if(a(this,A))throw new Error("Already flushed");u(this,A,!0);const t=await this.decoder.flush(),{needsMore:s}=await d(this,I,G).call(this,{decoderResult:t,onFrameAvailable:e});s&&await d(this,C,ee).call(this)};const pe=({decoder:i,outputFormat:r,thread_count:e})=>{let t="rgb24";return r==="mjpeg"&&(t="yuvj422p"),b.encoder({name:r,width:i.width,height:i.height,pix_fmt:t,thread_count:e,time_base:[1,1]})},ge=async({filename:i,sourceDemuxer:r,thread_count:e,streamIndex:t})=>{const s=b.demuxers(),n=s[Object.keys(s).find(o=>o.indexOf("mp4")>-1)],h=b.muxer({name:"image2",filename:i,thread_count:e,iformat:n});return h.newStream(r.streams[t]),await h.writeHeader(),h};exports.BeamcoderExtractor=B;
|
|
1
|
+
"use strict";var b=(r,s,t)=>{if(!s.has(r))throw TypeError("Cannot "+t)};var e=(r,s,t)=>(b(r,s,"read from private field"),t?t.call(r):s.get(r)),l=(r,s,t)=>{if(s.has(r))throw TypeError("Cannot add the same private member more than once");s instanceof WeakSet?s.add(r):s.set(r,t)},n=(r,s,t,i)=>(b(r,s,"write to private field"),i?i.call(r,t):s.set(r,t),t),N=(r,s,t,i)=>({set _(a){n(r,s,a,t)},get _(){return e(r,s,i)}}),q=(r,s,t)=>(b(r,s,"access private method"),t);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const D=require("@antoinemopa/beamcoder"),I=require("canvas"),O=require("path"),V=require("node:https"),B=require("http"),L=require("tmp"),U=require("fs-extra");class W{static async create(s){throw new Error("Not implemented")}async init({inputFileOrUrl:s,outputFile:t,threadCount:i=8,endTime:a,interpolateFps:o,interpolateMode:u}){throw new Error("Not implemented")}get duration(){throw new Error("Not implemented")}get width(){throw new Error("Not implemented")}get height(){throw new Error("Not implemented")}async seekToPTS(s){throw new Error("Not implemented")}async getFrameAtTime(s){throw new Error("Not implemented")}async getImageDataAtTime(s){throw new Error("Not implemented")}async getFrameAtPts(s){throw new Error("Not implemented")}async seekToTime(s){throw new Error("Not implemented")}ptsToTime(s){throw new Error("Not implemented")}async readFrames({onFrameAvailable:s,flush:t=!0}={flush:!0,onFrameAvailable:()=>!0}){throw new Error("Not implemented")}async dispose(){throw new Error("Not implemented")}}class G extends Error{}var _,E,x,g;class H{constructor(s){l(this,_,void 0);l(this,E,void 0);l(this,x,void 0);l(this,g,void 0);n(this,_,s)}get filepath(){return e(this,x)}async download(){await new Promise((s,t)=>{const i=e(this,_),a=O.extname(i);n(this,g,L.fileSync({postfix:a}));try{const o=i.startsWith("https://")?V:B;n(this,E,o.get(i,u=>{const p=u.headers["content-type"];if(!p.includes("video")){const F=new Error(`Source ${i}, returned unsupported content type ${p}`);t(F);return}const d=U.createWriteStream(e(this,g).name);u.pipe(d),d.on("finish",()=>{d.close(),n(this,x,e(this,g).name),s()}),d.on("error",F=>{t(F)})})),e(this,E).on("error",u=>{u instanceof G||t(u)})}catch(o){t(o)}})}clear(){e(this,g)&&e(this,g).removeCallback(),e(this,_)&&n(this,_,void 0),e(this,E)&&n(this,E,null),e(this,x)&&n(this,x,void 0)}}_=new WeakMap,E=new WeakMap,x=new WeakMap,g=new WeakMap;const Y=({demuxer:r,streamIndex:s,threadCount:t})=>D.decoder({demuxer:r,width:r.streams[s].codecpar.width,height:r.streams[s].codecpar.height,stream_index:s,pix_fmt:r.streams[s].codecpar.format,thread_count:t}),j=async({stream:r,outputPixelFormat:s,interpolateFps:t,interpolateMode:i="fast"})=>{if(!r.codecpar.format)return null;let a=[`[in0:v]format=${r.codecpar.format}`];if(t)if(i==="high-quality")a=[...a,`minterpolate=fps=${t}`];else if(i==="fast")a=[...a,`fps=${t}`];else throw new Error(`Unexpected interpolation mode: ${i}`);const o=a.join(", ")+"[out0:v]";return D.filterer({filterType:"video",inputParams:[{name:"in0:v",width:r.codecpar.width,height:r.codecpar.height,pixelFormat:r.codecpar.format,timeBase:r.time_base,pixelAspect:r.sample_aspect_ratio}],outputParams:[{name:"out0:v",pixelFormat:s}],filterSpec:o})},C="video",J="rgba";var h,c,v,T,f,w,y,P,m,A,R;const S=class extends W{constructor(){super(...arguments);l(this,A);l(this,h,null);l(this,c,null);l(this,v,null);l(this,T,[]);l(this,f,[]);l(this,w,null);l(this,y,null);l(this,P,8);l(this,m,0)}static async create(t){const i=new S;return await i.init(t),i}async init({inputFileOrUrl:t,threadCount:i=8}){if(n(this,P,i),t.startsWith("http")){const a=new H(t);await a.download(),t=a.filepath}if(n(this,h,await D.demuxer("file:"+t)),n(this,m,e(this,h).streams.findIndex(a=>a.codecpar.codec_type===C)),e(this,m)===-1)throw new Error(`File has no ${C} stream!`);n(this,v,await j({stream:e(this,h).streams[e(this,m)],outputPixelFormat:J}))}get duration(){const t=e(this,h).streams[e(this,m)].time_base,i=e(this,h).streams.map(a=>a.duration*t[0]/t[1]);return Math.max(...i)}get width(){return e(this,h).streams[e(this,m)].codecpar.width}get height(){return e(this,h).streams[e(this,m)].codecpar.height}async getFrameAtTime(t){const i=Math.floor(this._timeToPTS(t));return this._getFrameAtPts(i)}async getImageDataAtTime(t){const i=Math.floor(this._timeToPTS(t)),a=await this._getFrameAtPts(i);if(!a)return null;const o=this._resizeFrameData(a);return I.createImageData(o,a.width,a.height)}_timeToPTS(t){const i=e(this,h).streams[e(this,m)].time_base;return t*i[1]/i[0]}ptsToTime(t){const i=e(this,h).streams[e(this,m)].time_base;return t*i[0]/i[1]}async _getFrameAtPts(t){if((!e(this,y)||e(this,y)>t)&&(await e(this,h).seek({stream_index:0,timestamp:t,any:!1}),await q(this,A,R).call(this),n(this,w,null),n(this,f,[])),!e(this,c)){if(e(this,T).length>0){const u=e(this,T).find(p=>p.pts<=t);return n(this,y,t),u}throw Error("Unexpected condition: no decoder and no frames")}let i=null,a=-1,o=null;for(!e(this,w)&&e(this,f).length===0&&({packet:N(this,w)._,frames:N(this,f)._}=await this._getNextPacketAndDecodeFrames());(e(this,w)||e(this,f).length!==0)&&a<t;){if(e(this,f).length!==0){i=(await e(this,v).filter([{name:"in0:v",frames:e(this,f)}])).flatMap(d=>d.frames);const p=i.reverse().find(d=>d.pts<=t);if(!p)return o;if(n(this,T,i),a=p==null?void 0:p.pts,!o||a<t)o=p;else break}({packet:N(this,w)._,frames:N(this,f)._}=await this._getNextPacketAndDecodeFrames())}if(!o)throw Error("No matching frame found");return n(this,y,t),o}async _getNextPacketAndDecodeFrames(){const t=await this._getNextVideoStreamPacket();let i=null;t!==null&&e(this,c)?i=await e(this,c).decode(t):e(this,c)&&(i=await e(this,c).flush(),n(this,c,null));let a=[];return i&&i.frames.length!==0&&(a=i.frames),{packet:t,frames:a}}async _getNextVideoStreamPacket(){let t=await e(this,h).read();for(;t&&t.stream_index!==e(this,m);)if(t=await e(this,h).read(),t===null)return null;return t}_resizeFrameData(t){const a=t.width*t.height*4,o=new Uint8ClampedArray(a),u=t.linesize,p=t.data[0];for(let d=0;d<t.height;d++){const F=d*u,$=F+t.width*4,z=p.slice(F,$),M=d*t.width*4;o.set(z,M)}return o}async dispose(){e(this,c)&&(await e(this,c).flush(),n(this,c,null)),e(this,h).forceClose(),n(this,v,null),n(this,T,void 0),n(this,f,[]),n(this,w,null),n(this,y,null),n(this,m,0)}};let k=S;h=new WeakMap,c=new WeakMap,v=new WeakMap,T=new WeakMap,f=new WeakMap,w=new WeakMap,y=new WeakMap,P=new WeakMap,m=new WeakMap,A=new WeakSet,R=async function(){e(this,c)&&(await e(this,c).flush(),n(this,c,null)),n(this,c,Y({demuxer:e(this,h),streamIndex:e(this,m),threadCount:e(this,P)}))};exports.BeamcoderExtractor=k;
|
|
2
2
|
//# sourceMappingURL=framefusion.cjs.map
|
package/dist/framefusion.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"framefusion.cjs","sources":["../src/BaseExtractor.ts","../src/DownloadVideoURL.ts","../src/backends/beamcoder.ts"],"sourcesContent":["import type { ImageData } from 'canvas';\n\nimport type {\n ExtractorArgs,\n Frame,\n Extractor\n} from '../framefusion';\n\nexport class BaseExtractor implements Extractor {\n static async create(args: ExtractorArgs): Promise<Extractor> {\n throw new Error('Not implemented');\n }\n\n async init({\n inputFileOrUrl,\n outputFile,\n threadCount = 8,\n endTime,\n interpolateFps,\n interpolateMode,\n }: ExtractorArgs): Promise<void> {\n throw new Error('Not implemented');\n }\n\n get duration(): number {\n throw new Error('Not implemented');\n }\n\n get width(): number {\n throw new Error('Not implemented');\n }\n\n get height(): number {\n throw new Error('Not implemented');\n }\n\n async seekToPTS(targetPts: number) {\n throw new Error('Not implemented');\n }\n\n async getFrameAtTime(targetTime: number): Promise<Frame> {\n throw new Error('Not implemented');\n }\n\n async getImageDataAtTime(targetTime: number): Promise<ImageData> {\n throw new Error('Not implemented');\n }\n\n async getFrameAtPts(targetPts: number): Promise<Frame> {\n throw new Error('Not implemented');\n }\n\n async seekToTime(targetTime: number) {\n throw new Error('Not implemented');\n }\n\n /**\n * Convert a PTS (based on timebase) to PTS (in seconds)\n */\n ptsToTime(pts: number) {\n throw new Error('Not implemented');\n }\n\n async readFrames({\n onFrameAvailable,\n flush = true,\n }: {\n /**\n * Return true if we need to read more frames.\n */\n onFrameAvailable?: (frame: Frame) => Promise<boolean> | boolean;\n flush?: boolean;\n } = {\n flush: true,\n onFrameAvailable: () => true,\n }) {\n throw new Error('Not implemented');\n }\n\n async dispose() {\n throw new Error('Not implemented');\n }\n}\n","import path from 'path';\nimport https from 'node:https';\nimport type { ClientRequest } from 'http';\nimport http from 'http';\nimport tmp from 'tmp';\nimport fs from 'fs-extra';\n\nclass CancelRequestError extends Error { }\n\n/**\n * Downloads a video file from a given URL as a temporary file. When the object is cleared, the temporary file is\n * deleted.\n */\nexport class DownloadVideoURL {\n #url: string | undefined;\n #httpRequest: ClientRequest | undefined = undefined;\n #filepath: string | undefined = undefined;\n #tmpObj: tmp.SynchrounousResult | undefined = undefined;\n\n constructor(url) {\n this.#url = url;\n }\n\n /**\n * returns the filepath of the downloaded file. If the file has not been downloaded yet, it will be undefined\n */\n get filepath() {\n return this.#filepath;\n }\n\n /**\n * Downloads the file from the given URL. The file will be downloaded to a temporary file.\n */\n async download() {\n await new Promise<void>((resolve, reject) => {\n const source = this.#url;\n const extension = path.extname(source);\n this.#tmpObj = tmp.fileSync({ postfix: extension });\n try {\n const connectionHandler = source.startsWith('https://') ? https : http;\n this.#httpRequest = connectionHandler.get(source, (res) => {\n const contentType = res.headers['content-type'];\n if (!contentType.includes('video')) {\n const err = new Error(`Source ${source}, returned unsupported content type ${contentType}`);\n reject(err);\n return;\n }\n const writeStream = fs.createWriteStream(this.#tmpObj.name);\n res.pipe(writeStream);\n writeStream.on('finish', () => {\n writeStream.close();\n this.#filepath = this.#tmpObj.name;\n resolve();\n });\n writeStream.on('error', (e) => {\n reject(e);\n });\n });\n this.#httpRequest.on('error', (e) => {\n if (e instanceof CancelRequestError) {\n return;\n }\n reject(e);\n });\n }\n catch (e) {\n reject(e);\n }\n });\n }\n\n clear() {\n if (this.#tmpObj) this.#tmpObj.removeCallback();\n if (this.#url) this.#url = undefined;\n if (this.#httpRequest) this.#httpRequest = null;\n if (this.#filepath) this.#filepath = undefined;\n }\n}\n","import type { Stream } from 'stream';\nimport type { ReadStream } from 'fs';\nimport type {\n Frame\n} from '@antoinemopa/beamcoder';\nimport beamcoder from '@antoinemopa/beamcoder';\nimport { createImageData } from 'canvas';\nimport type { ImageData } from 'canvas';\nimport type {\n InterpolateMode,\n Extractor,\n ExtractorArgs\n} from '../../framefusion.js';\nimport {\n BaseExtractor\n} from '../BaseExtractor.js';\nimport { DownloadVideoURL } from '../DownloadVideoURL.js';\n\nconst LOG_PACKET_FLOW = false;\nconst LOG_SINGLE_FRAME_DUMP_FLOW = false;\nconst STREAM_TYPE_VIDEO = 'video';\n\n/**\n * Assumptions made by this library:\n *\n * - The input is always a mp4\n * It would be nice to support other formats. One thing to change is the iformat, hardcoded for mp4.\n *\n */\nexport const probeCodecPar = codecpar => ({\n type: codecpar.type,\n codec_type: codecpar.codec_type,\n codec_id: codecpar.codec_id,\n name: codecpar.name,\n codec_tag: codecpar.codec_tag,\n extradata: codecpar.extradata,\n format: codecpar.format,\n bit_rate: codecpar.bit_rate,\n bits_per_coded_sample: codecpar.bits_per_coded_sample,\n bits_per_raw_sample: codecpar.bits_per_raw_sample,\n profile: codecpar.profile,\n level: codecpar.level,\n width: codecpar.width,\n height: codecpar.height,\n sample_aspect_ratio: codecpar.sample_aspect_ratio,\n field_order: codecpar.field_order,\n color_range: codecpar.color_range,\n color_primaries: codecpar.color_primaries,\n color_trc: codecpar.color_trc,\n color_space: codecpar.color_space,\n chroma_location: codecpar.chroma_location,\n video_delay: codecpar.video_delay,\n channel_layout: codecpar.channel_layout,\n channels: codecpar.channels,\n sample_rate: codecpar.sample_rate,\n block_align: codecpar.block_align,\n frame_size: codecpar.frame_size,\n initial_padding: codecpar.initial_padding,\n trailing_padding: codecpar.trailing_padding,\n seek_preroll: codecpar.seek_preroll,\n});\n\nexport const probeStream = stream => ({\n type: stream.type,\n index: stream.index,\n id: stream.id,\n time_base: stream.time_base,\n start_time: stream.start_time,\n duration: stream.duration,\n nb_frames: stream.nb_frames,\n disposition: stream.disposition,\n discard: stream.discard,\n sample_aspect_ratio: stream.sample_aspect_ratio,\n metadata: stream.metadata,\n avg_frame_rate: stream.avg_frame_rate,\n attached_pic: stream.attached_pic,\n side_data: stream.side_data,\n event_flags: stream.event_flags,\n r_frame_rate: stream.r_frame_rate,\n codecpar: probeCodecPar(stream.codecpar),\n});\n\n\n/**\n * A filter to convert between color spaces.\n * An example would be YUV to RGB, for mp4 to png conversion.\n */\nconst getFilter = async({\n stream,\n outputPixelFormat,\n interpolateFps,\n interpolateMode = 'fast',\n}: {\n stream: beamcoder.Stream;\n outputPixelFormat: string;\n interpolateFps?: number;\n interpolateMode?: InterpolateMode;\n}): Promise<beamcoder.Filterer> => {\n if (!stream.codecpar.format) {\n return null;\n }\n\n let filterSpec = [`[in0:v]format=${stream.codecpar.format}`];\n\n if (interpolateFps) {\n if (interpolateMode === 'high-quality') {\n filterSpec = [...filterSpec, `minterpolate=fps=${interpolateFps}`];\n }\n else if (interpolateMode === 'fast') {\n filterSpec = [...filterSpec, `fps=${interpolateFps}`];\n }\n else {\n throw new Error(`Unexpected interpolation mode: ${interpolateMode}`);\n }\n }\n\n filterSpec = [...filterSpec];\n\n const filterSpecStr = filterSpec.join(', ') + '[out0:v]';\n\n console.log(`filterSpec: ${filterSpecStr}`);\n\n return beamcoder.filterer({\n filterType: 'video',\n inputParams: [\n {\n name: 'in0:v',\n width: stream.codecpar.width,\n height: stream.codecpar.height,\n pixelFormat: stream.codecpar.format,\n timeBase: stream.time_base,\n pixelAspect: stream.sample_aspect_ratio,\n },\n ],\n outputParams: [\n {\n name: 'out0:v',\n pixelFormat: outputPixelFormat,\n },\n ],\n filterSpec: filterSpecStr,\n });\n};\n\n/**\n * Class to keep track of the muxer/demuxer/encoder/muxer used while extracting a video to frames.\n */\nexport class BeamcoderExtractor extends BaseExtractor implements Extractor {\n decoder: beamcoder.Decoder = null;\n demuxer: beamcoder.Demuxer = null;\n encoder: beamcoder.Encoder = null;\n muxer: beamcoder.Muxer = null;\n filterer: beamcoder.Filterer = null;\n packet: beamcoder.Packet;\n endTime: number;\n #locked: boolean;\n #targetPts = 0;\n demuxerStream: beamcoder.WritableDemuxerStream;\n readStream: ReadStream;\n lastFrame: beamcoder.Frame = null;\n filteredFrames: beamcoder.Frame[] = [];\n #streamStopped = false;\n #flushed = false;\n /**\n * mp4 files can contain multiple streams of different type (audio, video, text, etc.).\n * It could be that stream 0 is not a video stream and we will work with the first video stream in the file.\n */\n #streamIndex = -1;\n\n /**\n * Encoder/Decoder constuction is async, so it can't be put in a regular constructor.\n *\n * Use and await this method to generate an extactor.\n */\n static async create(args: ExtractorArgs): Promise<Extractor> {\n const extractor = new BeamcoderExtractor();\n await extractor.init(args);\n return extractor as unknown as Extractor;\n }\n\n async init({\n inputFileOrUrl,\n outputFile,\n threadCount = 8,\n endTime,\n interpolateFps,\n interpolateMode,\n }: ExtractorArgs): Promise<void> {\n if (!inputFileOrUrl) {\n throw new Error('Can only use file OR url');\n }\n let readStream: Stream;\n let outputPixelFormat = 'rgba';\n\n console.log('init', { inputFileOrUrl, outputFile, threadCount, endTime, interpolateFps, interpolateMode, outputPixelFormat });\n\n //\n // - Demuxing -\n //\n // The demuxer reads the file and outputs packet streams\n //\n\n let demuxerStream;\n\n if (inputFileOrUrl.startsWith('http')) {\n console.log('downloading url');\n const downloadUrl = new DownloadVideoURL(inputFileOrUrl);\n await downloadUrl.download();\n inputFileOrUrl = downloadUrl.filepath;\n console.log('finished downloading');\n }\n\n console.log('file mode');\n const demuxer = await beamcoder.demuxer('file:' + inputFileOrUrl);\n\n // Find out which is the first video stream\n this.#streamIndex = demuxer.streams.findIndex(stream => stream.codecpar.codec_type === STREAM_TYPE_VIDEO);\n if (this.#streamIndex === -1) {\n throw new Error('File has no video stream!');\n }\n console.log({ time_base: demuxer.streams[this.#streamIndex].time_base, stream: this.#streamIndex });\n\n //\n // - Decoding -\n //\n // The decoder reads packets and can output raw frame data\n //\n\n const decoder = beamcoder.decoder({\n demuxer: demuxer,\n width: demuxer.streams[this.#streamIndex].codecpar.width,\n height: demuxer.streams[this.#streamIndex].codecpar.height,\n stream_index: this.#streamIndex,\n pix_fmt: demuxer.streams[this.#streamIndex].codecpar.format,\n thread_count: threadCount,\n });\n\n let outputFormat: OutputFormats | null = null;\n\n if (outputFile) {\n ['png', 'tiff', 'mjpeg'].forEach(format => {\n if (outputFile.endsWith(format)) {\n outputFormat = format as OutputFormats;\n\n // TODO: build a map of outputFormats to outputPixelFormats and remove this from this loop.\n if (outputFormat === 'mjpeg') {\n outputPixelFormat = 'yuvj422p';\n }\n\n if (outputFormat === 'png') {\n outputPixelFormat = 'rgb24';\n }\n }\n });\n\n if (!outputFormat) {\n throw new Error('Output format could not be determined');\n }\n }\n\n //\n // - Filtering -\n //\n // Packets can be filtered to change colorspace, fps and add various effects\n // If there are no color space changes or filters, filter might not be necessary.\n //\n\n const filterer = await getFilter({\n stream: demuxer.streams[this.#streamIndex],\n outputPixelFormat,\n interpolateFps,\n interpolateMode,\n });\n\n if (outputFormat) {\n //\n // - Encoding -\n //\n // Encode frames in packets of the output format\n //\n // This process is usually slower than decoding!\n const encoder = createFrameDumpingEncoder({\n decoder,\n outputFormat,\n thread_count: threadCount,\n });\n this.encoder = encoder;\n\n //\n // - Muxing -\n //\n // Assemble packets in a format that can be sent to the outside world!\n //\n\n // This process is usually slower than decoding!\n const muxer = await createFrameDumpingMuxer({\n thread_count: threadCount,\n filename: outputFile,\n sourceDemuxer: demuxer,\n streamIndex: this.#streamIndex,\n });\n\n this.muxer = muxer;\n }\n\n this.decoder = decoder;\n this.demuxer = demuxer;\n this.filterer = filterer;\n this.endTime = endTime;\n this.demuxerStream = demuxerStream;\n this.readStream = readStream as ReadStream;\n }\n\n get duration(): number {\n const time_base = this.demuxer.streams[this.#streamIndex].time_base;\n const durations = this.demuxer.streams.map(\n stream => stream.duration * time_base[0] / time_base[1]\n );\n\n return Math.max(...durations);\n }\n\n get width(): number {\n return this.demuxer.streams[this.#streamIndex].codecpar.width;\n }\n\n get height(): number {\n return this.demuxer.streams[this.#streamIndex].codecpar.height;\n }\n\n /**\n * Seek to a given PTS in the stream\n *\n * PTS stands for presentation time stamp. It's expressed in time_base units.\n * If timebase is 1/10000,\n * and a frame PTS is 10000,\n * Then the frame must be displayed at exactly 1 second.\n */\n async seekToPTS(targetPts: number) {\n if (targetPts === 0 && !this.packet) {\n // No need to seek, we haven't started reading yet.\n return;\n }\n this.lastFrame = null;\n this.#targetPts = targetPts;\n console.log(`Seeking to PTS=${targetPts}`);\n await this.#goToCurrentStream();\n await this.demuxer.seek({ stream_index: this.#streamIndex, timestamp: targetPts });\n await this.#goToCurrentStream();\n\n // When seeking to the past:\n // Skip any packets which were read before.\n // at a PTS bigger than the target PTS.\n while (this.packet && this.packet.pts > targetPts) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.packet.flags as any) = { DISCARD: true };\n await this.#nextPacket();\n }\n }\n\n\n /**\n * Dump one frame at a specific time\n *\n * This method can seek as required, but generally, it is designed to be\n * performant in the cases where we progressively read a video frame by frame.\n *\n * So the implementation in here should not seek all the time, but rather\n * read packets as they come, when possible,\n *\n * @see getFrameAtPts()\n */\n async getFrameAtTime(targetTime: number): Promise<beamcoder.Frame> {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Requesting to dump a frame at Time(s)=${targetTime}`);\n const targetPts = Math.floor(this.timeToPts(targetTime));\n return this.getFrameAtPts(targetPts);\n }\n\n /**\n * Dump one frame's image data (for canvas) at a specific time\n *\n * @see getFrameAtTime()\n */\n async getImageDataAtTime(targetTime: number): Promise<ImageData> {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Requesting to dump a frame at Time(s)=${targetTime}`);\n const targetPts = Math.floor(this.timeToPts(targetTime));\n const frame = await this.getFrameAtPts(targetPts);\n\n const components = 4; // 4 components: r, g, b and a\n const size = frame.width * frame.height * components;\n const rawData = new Uint8ClampedArray(size);\n const sourceLineSize = frame.linesize as unknown as number;\n const pixels = frame.data[0] as Uint8Array;\n\n // libav creates larger buffers because it makes their internal code simpler.\n // we have to trim a part at the right of each pixel row.\n for (let i = 0; i < frame.height; i++) {\n const sourceStart = i * sourceLineSize;\n const sourceEnd = sourceStart + frame.width * components;\n const sourceData = pixels.slice(sourceStart, sourceEnd);\n const targetOffset = i * frame.width * components;\n rawData.set(sourceData, targetOffset);\n }\n\n const image = createImageData(\n rawData,\n frame.width,\n frame.height\n );\n\n return image;\n }\n\n /**\n * Dump one frame at a specific pts\n *\n * This method can seek as required, but generally, it is designed to be\n * performant in the cases where we progressively read a video frame by frame.\n *\n * So the implementation in here should not seek all the time, but rather\n * read packets as they come, when possible,\n */\n async getFrameAtPts(targetPts: number): Promise<beamcoder.Frame> {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Requesting to dump a frame at PTS=${targetPts}`);\n\n if (this.packet) {\n const newTime = this.ptsToTime(targetPts);\n const currentTime = this.ptsToTime(this.lastFrame.pts);\n\n // Heuristic: if moving more than `SEEK_DELAY` away, seek instead\n // of processing packets until target. Note that this logic is not\n // good if playback rate is faster.\n const SEEK_DELAY = 2.0;\n if (Math.abs(newTime - currentTime) > SEEK_DELAY) {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Time difference is bigger than SEEK_DELAY=${SEEK_DELAY}. Seeking.`);\n await this.seekToPTS(targetPts);\n }\n }\n\n if (this.packet === null && this.filteredFrames.length === 0 && targetPts >= this.lastFrame.pts) {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Last frame has been reached, resolving with last frame, pts=${this.lastFrame.pts}`);\n return this.lastFrame;\n }\n\n if (this.filteredFrames.length > 0 && targetPts < this.filteredFrames[0].pts) {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Next frame is to be presented later (at PTS=${this.filteredFrames[0].pts}), resolving with last frame again, pts=${this.lastFrame.pts} [video fps is probably low]`);\n return this.lastFrame;\n }\n\n // TODO:\n // - Seek if we are:\n // - Seeking to the past;\n // - Seeking beyond the next keyframe. AND; - or some other heuristic,\n // such as seeking more than 1 seconds in the future.\n // - Clear last image when seeking.\n // - Clear extra frames when seeking\n\n // Read packets and dump a single frame.\n let finishReadingCleanlyPromise = null;\n\n const frame = await new Promise<beamcoder.Frame>(async resolve => {\n const onFrameAvailable = async(frame) => {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Frame available: pts=${frame.pts}`);\n\n // When we decoded the next frame after targetPts,\n // that's when we know the last frame was the one\n // that had to be shown at targetPts\n if (frame.pts === targetPts) {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log('frame.pts === targetPts | ', `${frame.pts} === ${targetPts} - resolving with this frame!`);\n this.lastFrame = frame;\n resolve(frame);\n return false;\n }\n\n if (frame.pts > targetPts) {\n if (this.lastFrame) {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log('frame.pts > targetPts | ', `${frame.pts} > ${targetPts} - resolving with previous frame (PTS=${this.lastFrame.pts})!`);\n resolve(this.lastFrame);\n // Add this frame back to the buffer - we don't need it yet.\n this.filteredFrames = [frame, ...this.filteredFrames];\n return false;\n }\n else {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log('frame.pts > targetPts | ', `${frame.pts} > ${targetPts} - but no last frame available - resolving with frame!`);\n // Add this frame back to the buffer - we don't need it yet.\n this.filteredFrames = [frame, ...this.filteredFrames];\n this.lastFrame = frame;\n resolve(frame);\n return false;\n }\n }\n\n if (this.packet === null && this.filteredFrames.length === 0 && targetPts >= this.lastFrame.pts) {\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log(`Last frame has been reached, resolving with last frame, pts=${this.lastFrame.pts}`);\n resolve(frame);\n return false;\n }\n\n LOG_SINGLE_FRAME_DUMP_FLOW && console.log('frame.pts < targetPts | ', `${frame.pts} < ${targetPts} - continuing`);\n\n this.lastFrame = frame;\n\n return true;\n };\n\n finishReadingCleanlyPromise = this.readFrames({\n onFrameAvailable,\n flush: true,\n });\n\n await finishReadingCleanlyPromise;\n });\n\n await finishReadingCleanlyPromise;\n\n return frame;\n }\n\n\n /**\n * Seek to a given time in the stream\n */\n async seekToTime(targetTime: number) {\n await this.seekToPTS(this.timeToPts(targetTime));\n }\n\n /**\n * Convert a time (in seconds) to PTS (based on timebase)\n */\n timeToPts(time: number) {\n const time_base = this.demuxer.streams[this.#streamIndex].time_base;\n return time * time_base[1] / time_base[0];\n }\n\n /**\n * Convert a PTS (based on timebase) to PTS (in seconds)\n */\n ptsToTime(pts: number) {\n const time_base = this.demuxer.streams[this.#streamIndex].time_base;\n return pts * time_base[0] / time_base[1];\n }\n\n /**\n * This just calls `this.demuxer.read()` with additional logging in case of errors.\n */\n async #readPacketWrapped() {\n if (this.#streamStopped) {\n throw new Error('Error: Trying to read after stream has stopped.');\n }\n try {\n const packet = await this.demuxer.read();\n if (!packet) {\n this.#streamStopped = true;\n }\n return packet;\n }\n catch (e) {\n throw e;\n }\n }\n\n async #nextPacket() {\n if (this.packet === null) {\n throw new Error('Stream is over!');\n }\n this.packet = await this.#readPacketWrapped();\n while (this.packet && this.packet.stream_index !== this.#streamIndex) {\n this.packet = await this.#readPacketWrapped();\n if (this.packet === null) {\n return;\n }\n }\n }\n\n async #goToCurrentStream() {\n while (!this.packet || this.packet.stream_index !== this.#streamIndex) {\n await this.#nextPacket();\n }\n }\n\n async #filterFrames(frames) {\n const result = await this.filterer.filter([{\n name: 'in0:v',\n frames: frames,\n }]);\n this.filteredFrames = result.flatMap(r => r.frames);\n return this.filteredFrames;\n }\n\n /**\n * Note: the promise returned is for the first frame only.\n */\n async #processFilteredFrames(frames: Frame[] | null, {\n onFrameAvailable = () => true,\n }: {\n onFrameAvailable?: (frame: beamcoder.Frame) => boolean | Promise<boolean>;\n }) {\n let needsMore = true;\n\n while (this.filteredFrames.length > 0 && needsMore) {\n const frame = this.filteredFrames.shift();\n LOG_PACKET_FLOW && console.log(`Sending filter result to encoder PTS=${frame.pts}`);\n needsMore = await onFrameAvailable(frame);\n }\n\n return { needsMore };\n }\n\n async #muxImageResults(imageResult): Promise<void> {\n for (let j = 0; j < imageResult.packets.length; j++) {\n const shouldSave = !this.#targetPts || imageResult.packets[j].pts >= this.#targetPts;\n\n if (shouldSave) {\n await this.muxer.writeFrame(imageResult.packets[j]);\n LOG_PACKET_FLOW && console.log(`wrote image ${JSON.stringify(imageResult.packets[j])}`);\n }\n else {\n LOG_PACKET_FLOW && console.log(`skipped image ${JSON.stringify(imageResult.packets[j])}`);\n }\n }\n }\n\n async #filterAndProcessFrames({\n decoderResult,\n onFrameAvailable,\n }: {\n decoderResult?: beamcoder.DecodedFrames;\n /**\n * Return true if we need to read more frames.\n */\n onFrameAvailable: (frame: Frame) => Promise<boolean> | boolean;\n }) {\n const filteredFrames = await this.#filterFrames(decoderResult.frames);\n const { needsMore } = await this.#processFilteredFrames(filteredFrames, {\n onFrameAvailable,\n });\n\n return { needsMore };\n }\n\n async #readPacketLoop({\n onFrameAvailable,\n }: {\n /**\n * Return true if we need to read more frames.\n */\n onFrameAvailable: (frame: Frame) => Promise<boolean> | boolean;\n }): Promise<{ needsMore: boolean; }> {\n await this.#nextPacket();\n while (this.packet) {\n const decoderResult = await this.decoder.decode(this.packet);\n LOG_PACKET_FLOW && console.log(`Received ${decoderResult.frames.length} decoder frames`);\n const { needsMore } = await this.#filterAndProcessFrames({\n decoderResult,\n onFrameAvailable,\n });\n\n if (!needsMore) {\n return { needsMore };\n }\n\n await this.#nextPacket();\n }\n\n return { needsMore: true };\n }\n\n /**\n * #hasEncoder\n *\n * Sometimes, we don't need to encode so we'll have no encoder.\n * (when reading raw frames, and no outputFile is given)\n * Otherwise, here we'll want to pass frames to the encoder as well\n * as running onFrameAvailable.\n */\n get #hasEncoder() {\n return this.encoder !== null;\n }\n\n async #flushEncoder() {\n if (!this.#hasEncoder) {\n return;\n }\n LOG_PACKET_FLOW && console.log(`\\n Flushing encoder`);\n const imageResult = await this.encoder.flush();\n await this.#muxImageResults(imageResult);\n }\n\n async #flushFilterAndProcessFrames({\n onFrameAvailable,\n }: {\n onFrameAvailable: (frame: Frame) => Promise<boolean> | boolean;\n }) {\n LOG_PACKET_FLOW && console.log(`\\n Flushing decoder`);\n\n if (this.#flushed) {\n throw new Error('Already flushed');\n }\n\n this.#flushed = true;\n const decoderResult = await this.decoder.flush();\n const { needsMore } = await this.#filterAndProcessFrames({\n decoderResult,\n onFrameAvailable,\n });\n\n if (!needsMore) {\n return;\n }\n\n await this.#flushEncoder();\n }\n\n /**\n * Detect multiple access to beamcoder which could put our system in an unexpected state.\n */\n async withLock(callback) {\n if (this.#locked) {\n throw new Error('Multiple attempts to use beamcoder at the same time.');\n }\n this.#locked = true;\n await callback();\n this.#locked = false;\n }\n\n /**\n * Dump all frames from the current point.\n * Stops processing packets after target is reached.\n */\n async readFrames({\n onFrameAvailable,\n flush = true,\n }: {\n /**\n * Return true if we need to read more frames.\n */\n onFrameAvailable?: (frame: Frame) => Promise<boolean> | boolean;\n flush?: boolean;\n } = {\n flush: true,\n onFrameAvailable: () => true,\n }) {\n await this.withLock(async() => {\n const originalOnFrameAvailable = onFrameAvailable;\n\n if (this.#hasEncoder) {\n onFrameAvailable = async(frame): Promise<boolean> => {\n LOG_PACKET_FLOW && console.log(`About to encode ${frame.pts}.\\n`);\n const imageResult = await this.encoder.encode(frame);\n await this.#muxImageResults(imageResult);\n\n LOG_PACKET_FLOW && console.log(`About to run originalOnFrameAvailable.\\n`);\n const needsMore = await originalOnFrameAvailable(frame);\n\n return needsMore;\n };\n }\n\n // First output successively frames which were already decoded and filtered\n const needsMore = this.#processFilteredFrames([], { onFrameAvailable });\n if (!needsMore) {\n LOG_PACKET_FLOW && console.log('No need to read more after reading buffered frames.');\n return;\n }\n\n // Maybe after processing extra frames. there are no frames left, because\n // we already read the last packet the previous time around.\n if (this.packet === null) {\n return;\n }\n\n // Main reading loop\n // Most frame reading happens here.\n // We read packets, filter them and call onFrameAvailable to let the calling code\n // perform it's work.\n {\n const { needsMore } = await this.#readPacketLoop({ onFrameAvailable });\n if (!needsMore) {\n return;\n }\n }\n\n // Flushing.\n // After every packet has been read by the main loop, we need to get the\n // last packets from the decoder (flush) and repeat filtering and calling onFrameAvailable like the main loop.\n if (flush) {\n await this.#flushFilterAndProcessFrames({ onFrameAvailable });\n }\n\n LOG_PACKET_FLOW && console.log(`\\nRead frame ended.\\n`);\n });\n }\n\n async dispose() {\n await new Promise((resolve) => {\n setTimeout(() => {\n if (this.readStream) {\n // These might be creating memory issues.\n this.readStream.unpipe(this.demuxerStream);\n this.readStream.destroy();\n }\n }, 30);\n });\n }\n}\n\ntype OutputFormats = 'mjpeg' | 'png' | 'tiff';\n\nconst createFrameDumpingEncoder = ({\n decoder,\n outputFormat,\n thread_count,\n}: {\n decoder: beamcoder.Decoder;\n outputFormat: OutputFormats;\n thread_count: number;\n}) => {\n let colorspace = 'rgb24';\n if (outputFormat === 'mjpeg') {\n colorspace = 'yuvj422p';\n }\n\n return beamcoder.encoder({\n name: outputFormat,\n width: decoder.width,\n height: decoder.height,\n pix_fmt: colorspace,\n thread_count: thread_count,\n time_base: [1, 1],\n });\n};\n\nconst createFrameDumpingMuxer = async({\n filename,\n sourceDemuxer,\n thread_count,\n streamIndex,\n}: {\n filename: string;\n sourceDemuxer: beamcoder.Demuxer;\n thread_count: number;\n streamIndex: number;\n}) => {\n const demuxers = beamcoder.demuxers();\n // The iformat is necessary to have a working probe when using a stream\n const iformat = demuxers[Object.keys(demuxers).find(key => key.indexOf('mp4') > -1)];\n\n const muxer = beamcoder.muxer({\n name: 'image2',\n filename,\n thread_count: thread_count,\n iformat,\n });\n\n muxer.newStream(sourceDemuxer.streams[streamIndex]);\n await muxer.writeHeader();\n return muxer;\n};\n"],"names":["BaseExtractor","args","inputFileOrUrl","outputFile","threadCount","endTime","interpolateFps","interpolateMode","targetPts","targetTime","pts","onFrameAvailable","flush","CancelRequestError","DownloadVideoURL","url","__privateAdd","_url","_httpRequest","_filepath","_tmpObj","__privateSet","__privateGet","resolve","reject","source","extension","path","tmp","connectionHandler","https","http","res","contentType","err","writeStream","fs","e","STREAM_TYPE_VIDEO","getFilter","stream","outputPixelFormat","filterSpec","filterSpecStr","beamcoder","_BeamcoderExtractor","_readPacketWrapped","_nextPacket","_goToCurrentStream","_filterFrames","_processFilteredFrames","_muxImageResults","_filterAndProcessFrames","_readPacketLoop","_hasEncoder","_flushEncoder","_flushFilterAndProcessFrames","__publicField","_locked","_targetPts","_streamStopped","_flushed","_streamIndex","extractor","readStream","demuxerStream","downloadUrl","demuxer","decoder","outputFormat","format","filterer","encoder","createFrameDumpingEncoder","muxer","createFrameDumpingMuxer","time_base","durations","__privateMethod","goToCurrentStream_fn","nextPacket_fn","frame","components","size","rawData","sourceLineSize","pixels","i","sourceStart","sourceEnd","sourceData","targetOffset","createImageData","newTime","currentTime","SEEK_DELAY","finishReadingCleanlyPromise","time","callback","originalOnFrameAvailable","hasEncoder_get","imageResult","muxImageResults_fn","processFilteredFrames_fn","needsMore","readPacketLoop_fn","flushFilterAndProcessFrames_fn","BeamcoderExtractor","readPacketWrapped_fn","packet","filterFrames_fn","frames","result","r","j","filterAndProcessFrames_fn","decoderResult","filteredFrames","flushEncoder_fn","thread_count","colorspace","filename","sourceDemuxer","streamIndex","demuxers","iformat","key"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"framefusion.cjs","sources":["../src/BaseExtractor.ts","../src/DownloadVideoURL.ts","../src/backends/beamcoder.ts"],"sourcesContent":["import type { ImageData } from 'canvas';\n\nimport type {\n ExtractorArgs,\n Frame,\n Extractor\n} from '../framefusion';\n\nexport class BaseExtractor implements Extractor {\n static async create(args: ExtractorArgs): Promise<Extractor> {\n throw new Error('Not implemented');\n }\n\n async init({\n inputFileOrUrl,\n outputFile,\n threadCount = 8,\n endTime,\n interpolateFps,\n interpolateMode,\n }: ExtractorArgs): Promise<void> {\n throw new Error('Not implemented');\n }\n\n get duration(): number {\n throw new Error('Not implemented');\n }\n\n get width(): number {\n throw new Error('Not implemented');\n }\n\n get height(): number {\n throw new Error('Not implemented');\n }\n\n async seekToPTS(targetPts: number) {\n throw new Error('Not implemented');\n }\n\n async getFrameAtTime(targetTime: number): Promise<Frame> {\n throw new Error('Not implemented');\n }\n\n async getImageDataAtTime(targetTime: number): Promise<ImageData> {\n throw new Error('Not implemented');\n }\n\n async getFrameAtPts(targetPts: number): Promise<Frame> {\n throw new Error('Not implemented');\n }\n\n async seekToTime(targetTime: number) {\n throw new Error('Not implemented');\n }\n\n /**\n * Convert a PTS (based on timebase) to PTS (in seconds)\n */\n ptsToTime(pts: number) {\n throw new Error('Not implemented');\n }\n\n async readFrames({\n onFrameAvailable,\n flush = true,\n }: {\n /**\n * Return true if we need to read more frames.\n */\n onFrameAvailable?: (frame: Frame) => Promise<boolean> | boolean;\n flush?: boolean;\n } = {\n flush: true,\n onFrameAvailable: () => true,\n }) {\n throw new Error('Not implemented');\n }\n\n async dispose() {\n throw new Error('Not implemented');\n }\n}\n","import path from 'path';\nimport https from 'node:https';\nimport type { ClientRequest } from 'http';\nimport http from 'http';\nimport tmp from 'tmp';\nimport fs from 'fs-extra';\n\nclass CancelRequestError extends Error { }\n\n/**\n * Downloads a video file from a given URL as a temporary file. When the object is cleared, the temporary file is\n * deleted.\n */\nexport class DownloadVideoURL {\n #url: string | undefined;\n #httpRequest: ClientRequest | undefined = undefined;\n #filepath: string | undefined = undefined;\n #tmpObj: tmp.SynchrounousResult | undefined = undefined;\n\n constructor(url) {\n this.#url = url;\n }\n\n /**\n * returns the filepath of the downloaded file. If the file has not been downloaded yet, it will be undefined\n */\n get filepath() {\n return this.#filepath;\n }\n\n /**\n * Downloads the file from the given URL. The file will be downloaded to a temporary file.\n */\n async download() {\n await new Promise<void>((resolve, reject) => {\n const source = this.#url;\n const extension = path.extname(source);\n this.#tmpObj = tmp.fileSync({ postfix: extension });\n try {\n const connectionHandler = source.startsWith('https://') ? https : http;\n this.#httpRequest = connectionHandler.get(source, (res) => {\n const contentType = res.headers['content-type'];\n if (!contentType.includes('video')) {\n const err = new Error(`Source ${source}, returned unsupported content type ${contentType}`);\n reject(err);\n return;\n }\n const writeStream = fs.createWriteStream(this.#tmpObj.name);\n res.pipe(writeStream);\n writeStream.on('finish', () => {\n writeStream.close();\n this.#filepath = this.#tmpObj.name;\n resolve();\n });\n writeStream.on('error', (e) => {\n reject(e);\n });\n });\n this.#httpRequest.on('error', (e) => {\n if (e instanceof CancelRequestError) {\n return;\n }\n reject(e);\n });\n }\n catch (e) {\n reject(e);\n }\n });\n }\n\n clear() {\n if (this.#tmpObj) this.#tmpObj.removeCallback();\n if (this.#url) this.#url = undefined;\n if (this.#httpRequest) this.#httpRequest = null;\n if (this.#filepath) this.#filepath = undefined;\n }\n}\n","import type {\n DecodedFrames,\n Packet,\n Demuxer,\n Decoder,\n Filterer\n} from '@antoinemopa/beamcoder';\nimport beamcoder from '@antoinemopa/beamcoder';\nimport type { ImageData } from 'canvas';\nimport { createImageData } from 'canvas';\nimport { BaseExtractor } from '../BaseExtractor';\nimport type { Extractor, ExtractorArgs, InterpolateMode } from '../../framefusion';\nimport { DownloadVideoURL } from '../DownloadVideoURL';\n\nconst VERBOSE = false;\n\nconst createDecoder = ({\n demuxer,\n streamIndex,\n threadCount,\n}: {\n demuxer: Demuxer;\n streamIndex: number;\n threadCount: number;\n}): Decoder => {\n return beamcoder.decoder({\n demuxer: demuxer,\n width: demuxer.streams[streamIndex].codecpar.width,\n height: demuxer.streams[streamIndex].codecpar.height,\n stream_index: streamIndex,\n pix_fmt: demuxer.streams[streamIndex].codecpar.format,\n thread_count: threadCount,\n });\n};\n\n/**\n * A filter to convert between color spaces.\n * An example would be YUV to RGB, for mp4 to png conversion.\n */\nconst createFilter = async({\n stream,\n outputPixelFormat,\n interpolateFps,\n interpolateMode = 'fast',\n}: {\n stream: beamcoder.Stream;\n outputPixelFormat: string;\n interpolateFps?: number;\n interpolateMode?: InterpolateMode;\n}): Promise<beamcoder.Filterer> => {\n if (!stream.codecpar.format) {\n return null;\n }\n\n let filterSpec = [`[in0:v]format=${stream.codecpar.format}`];\n\n if (interpolateFps) {\n if (interpolateMode === 'high-quality') {\n filterSpec = [...filterSpec, `minterpolate=fps=${interpolateFps}`];\n }\n else if (interpolateMode === 'fast') {\n filterSpec = [...filterSpec, `fps=${interpolateFps}`];\n }\n else {\n throw new Error(`Unexpected interpolation mode: ${interpolateMode}`);\n }\n }\n\n const filterSpecStr = filterSpec.join(', ') + '[out0:v]';\n\n VERBOSE && console.log(`filterSpec: ${filterSpecStr}`);\n\n return beamcoder.filterer({\n filterType: 'video',\n inputParams: [\n {\n name: 'in0:v',\n width: stream.codecpar.width,\n height: stream.codecpar.height,\n pixelFormat: stream.codecpar.format,\n timeBase: stream.time_base,\n pixelAspect: stream.sample_aspect_ratio,\n },\n ],\n outputParams: [\n {\n name: 'out0:v',\n pixelFormat: outputPixelFormat,\n },\n ],\n filterSpec: filterSpecStr,\n });\n};\n\nconst STREAM_TYPE_VIDEO = 'video';\nconst COLORSPACE_RGBA = 'rgba';\n\n/**\n * A simple extractor that uses beamcoder to extract frames from a video file.\n */\nexport class BeamcoderExtractor extends BaseExtractor implements Extractor {\n /**\n * The demuxer reads the file and outputs packet streams\n */\n #demuxer: Demuxer = null;\n\n /**\n * The decoder reads packets and can output raw frame data\n */\n #decoder: Decoder = null;\n\n /**\n * Packets can be filtered to change colorspace, fps and add various effects. If there are no colorspace changes or\n * filters, filter might not be necessary.\n */\n #filterer: Filterer = null;\n\n /**\n * This is where we store available filtered frames which have not been requested yet.\n * We keep these in chronological order. We hang on to them for two reasons:\n * 1. so we can return them if we get a request for the same time again\n * 2. so we can return frames close the end of the stream. When such a frame is requested we have to flush (destroy)\n * the encoder to get the last few frames. This avoids having to re-create an encoder.\n */\n #filteredFrames: undefined[] | DecodedFrames = [];\n\n /**\n * This contains the last raw frames we read from the demuxer. We use it as a starting point for each new query. We\n * do this ensure we don't skip any frames.\n */\n #frames = [];\n\n /**\n * This contains the last packet we read from the demuxer. We use it as a starting point for each new query. We do\n * this ensure we don't skip any frames.\n */\n #packet: null | Packet = null;\n\n /**\n * The last target presentation timestamp (PTS) we requested. If we never requested a time(stamp) then this\n * value is null\n */\n #previousTargetPTS: null | number = null;\n\n /**\n * The number of threads to use for decoding\n */\n #threadCount = 8;\n\n /**\n * The index of the video stream in the demuxer\n */\n #streamIndex = 0;\n\n /**\n * Encoder/Decoder construction is async, so it can't be put in a regular constructor.\n * Use and await this method to generate an extractor.\n */\n static async create(args: ExtractorArgs): Promise<BeamcoderExtractor> {\n const extractor = new BeamcoderExtractor();\n await extractor.init(args);\n return extractor;\n }\n\n async init({\n inputFileOrUrl,\n threadCount = 8,\n }: ExtractorArgs): Promise<void> {\n this.#threadCount = threadCount;\n if (inputFileOrUrl.startsWith('http')) {\n VERBOSE && console.log('downloading url', inputFileOrUrl);\n const downloadUrl = new DownloadVideoURL(inputFileOrUrl);\n await downloadUrl.download();\n inputFileOrUrl = downloadUrl.filepath;\n VERBOSE && console.log('finished downloading');\n }\n this.#demuxer = await beamcoder.demuxer('file:' + inputFileOrUrl);\n this.#streamIndex = this.#demuxer.streams.findIndex(stream => stream.codecpar.codec_type === STREAM_TYPE_VIDEO);\n if (this.#streamIndex === -1) {\n throw new Error(`File has no ${STREAM_TYPE_VIDEO} stream!`);\n }\n this.#filterer = await createFilter({\n stream: this.#demuxer.streams[this.#streamIndex],\n outputPixelFormat: COLORSPACE_RGBA,\n });\n }\n\n async #createDecoder() {\n // It's possible that we need to create decoder multiple times during the lifecycle of this extractor so we\n // need to make sure we destroy the old one first if it exists\n if (this.#decoder) {\n await this.#decoder.flush();\n this.#decoder = null;\n }\n this.#decoder = createDecoder({\n demuxer: this.#demuxer as Demuxer,\n streamIndex: this.#streamIndex,\n threadCount: this.#threadCount,\n });\n }\n\n /**\n * Duration in seconds\n */\n get duration(): number {\n const time_base = this.#demuxer.streams[this.#streamIndex].time_base;\n const durations = this.#demuxer.streams.map(\n stream => stream.duration * time_base[0] / time_base[1]\n );\n\n return Math.max(...durations);\n }\n\n /**\n * Width in pixels\n */\n get width(): number {\n return this.#demuxer.streams[this.#streamIndex].codecpar.width;\n }\n\n /**\n * Height in pixels\n */\n get height(): number {\n return this.#demuxer.streams[this.#streamIndex].codecpar.height;\n }\n\n /**\n * Get the beamcoder Frame for a given time in seconds\n * @param targetTime\n */\n async getFrameAtTime(targetTime: number): Promise<beamcoder.Frame> {\n VERBOSE && console.log(`getFrameAtTime time(s)=${targetTime}`);\n const targetPts = Math.floor(this._timeToPTS(targetTime));\n return this._getFrameAtPts(targetPts);\n }\n\n /**\n * Get imageData for a given time in seconds\n * @param targetTime\n */\n async getImageDataAtTime(targetTime: number): Promise<ImageData> {\n const targetPts = Math.floor(this._timeToPTS(targetTime));\n VERBOSE && console.log('targetTime', targetTime, '-> targetPts', targetPts);\n const frame = await this._getFrameAtPts(targetPts);\n if (!frame) {\n VERBOSE && console.log('no frame found');\n return null;\n }\n const rawData = this._resizeFrameData(frame);\n const image = createImageData(\n rawData,\n frame.width,\n frame.height\n ) as ImageData;\n return image;\n }\n\n /**\n * Get the presentation timestamp (PTS) for a given time in seconds\n */\n _timeToPTS(time: number) {\n const time_base = this.#demuxer.streams[this.#streamIndex].time_base;\n return time * time_base[1] / time_base[0];\n }\n\n /**\n * Get the time in seconds from a given presentation timestamp (PTS)\n */\n ptsToTime(pts: number) {\n const time_base = this.#demuxer.streams[this.#streamIndex].time_base;\n return pts * time_base[0] / time_base[1];\n }\n\n /**\n * Get the frame at the given presentation timestamp (PTS)\n */\n async _getFrameAtPts(targetPTS: number) {\n VERBOSE && console.log('_getFrameAtPts', targetPTS, '-> duration', this.duration);\n let packetReadCount = 0;\n\n // seek and create a decoder when retrieving a frame for the first time or when seeking backwards\n // we have to create a new decoder when seeking backwards as the decoder can only process frames in\n // chronological order.\n if (!this.#previousTargetPTS || this.#previousTargetPTS > targetPTS) {\n await this.#demuxer.seek({\n stream_index: 0, // even though we specify the stream index, it still seeks all streams\n timestamp: targetPTS,\n any: false,\n });\n await this.#createDecoder();\n this.#packet = null;\n this.#frames = [];\n }\n\n // the decoder has been previously flushed while retrieving frames at the end of the stream and has thus been\n // destroyed. See if the requested targetPTS is part of the last few frames we decoded. If so, return it.\n if (!this.#decoder) {\n VERBOSE && console.log('no decoder');\n if ((this.#filteredFrames as any).length > 0) {\n const closestFrame = (this.#filteredFrames as any).find(f => f.pts <= targetPTS);\n // we should probably check the delta between the targetPTS and the closestFrame. If it's too big, we\n // should return null or something.\n VERBOSE && console.log('returning closest frame with pts', closestFrame.pts);\n VERBOSE && console.log('read', packetReadCount, 'packets');\n this.#previousTargetPTS = targetPTS;\n return closestFrame;\n }\n throw Error('Unexpected condition: no decoder and no frames');\n }\n\n // Read packets until we have a frame which is closest to targetPTS\n let filteredFrames = null;\n let closestFramePTS = -1;\n let outputFrame = null;\n\n // This is the first time we're decoding frames. Get the first packet and decode it.\n if (!this.#packet && this.#frames.length === 0) {\n ({ packet: this.#packet, frames: this.#frames } = await this._getNextPacketAndDecodeFrames());\n packetReadCount++;\n }\n while ((this.#packet || this.#frames.length !== 0) && closestFramePTS < targetPTS) {\n VERBOSE && console.log('packet si:', this.#packet?.stream_index, 'pts:', this.#packet?.pts, 'frames:', this.#frames?.length);\n VERBOSE && console.log('frames', this.#frames?.length, 'frames.pts:', this.#frames?.map(f => f.pts), '-> target.pts:', targetPTS);\n\n // packet contains frames\n if (this.#frames.length !== 0) {\n // filter the frames\n const filteredResult = await this.#filterer.filter([{ name: 'in0:v', frames: this.#frames }]);\n filteredFrames = filteredResult.flatMap(r => r.frames);\n VERBOSE && console.log('filteredFrames', filteredFrames.length, 'filteredFrames.pts:', filteredFrames.map(f => f.pts), '-> target.pts:', targetPTS);\n\n // get the closest frame to our target presentation timestamp (PTS)\n // Beamcoder returns decoded packet frames as follows: [1000, 2000, 3000, 4000]\n // If we're looking for a frame at 2500, we want to return the frame at 2000\n const closestFrame = filteredFrames.reverse().find(f => f.pts <= targetPTS);\n if (!closestFrame) {\n return outputFrame;\n }\n this.#filteredFrames = filteredFrames as beamcoder.DecodedFrames;\n\n closestFramePTS = closestFrame?.pts;\n VERBOSE && console.log('closestFramePTS', closestFramePTS, 'targetPTS', targetPTS);\n if (!outputFrame || closestFramePTS < targetPTS) {\n VERBOSE && console.log('assigning outputFrame', closestFrame?.pts);\n outputFrame = closestFrame;\n }\n else {\n // break out of the loop if we've found the closest frame (and ensure we don't move to the next\n // packet by calling _getNextPacketAndDecodeFrames again) as this risks us getting a frame that is\n // after our targetPTS\n VERBOSE && console.log('breaking');\n break;\n }\n }\n // get the next packet and frames\n ({ packet: this.#packet, frames: this.#frames } = await this._getNextPacketAndDecodeFrames());\n\n // keep track of how many packets we've read\n packetReadCount++;\n }\n\n if (!outputFrame) {\n throw Error('No matching frame found');\n }\n VERBOSE && console.log('read', packetReadCount, 'packets');\n\n this.#previousTargetPTS = targetPTS;\n return outputFrame;\n }\n\n /**\n * Get the next packet from the video stream and decode it into frames. Each frame has a presentation time stamp\n * (PTS). If we've reached the end of the stream and no more packets are available, we'll extract the last frames\n * from the decoder and destroy it.\n */\n async _getNextPacketAndDecodeFrames() {\n const packet = await this._getNextVideoStreamPacket();\n VERBOSE && console.log('packet pts:', packet?.pts);\n\n // NOTE: maybe we should only decode frames when we're relatively close to our targetPTS?\n // extract frames from the packet\n let decodedFrames = null;\n if (packet !== null && this.#decoder) {\n decodedFrames = await this.#decoder.decode(packet as Packet);\n VERBOSE && console.log('decodedFrames', decodedFrames.frames.length, decodedFrames.frames.map(f => f.pts));\n }\n // we've reached the end of the stream\n else {\n if (this.#decoder) {\n VERBOSE && console.log('getting the last frames from the decoder');\n // flush the decoder -- this will return the last frames and destroy the decoder\n decodedFrames = await this.#decoder.flush();\n this.#decoder = null;\n }\n else {\n // we don't have a decoder, so we can't decode any more frames\n VERBOSE && console.log('no more frames to decode');\n }\n }\n\n let frames = [];\n if (decodedFrames && decodedFrames.frames.length !== 0) {\n frames = decodedFrames.frames;\n }\n\n return { packet, frames };\n }\n\n async _getNextVideoStreamPacket(): Promise<null | Packet> {\n VERBOSE && console.log('_getNextVideoStreamPacket');\n\n let packet = await this.#demuxer.read();\n while (packet && packet.stream_index !== this.#streamIndex) {\n packet = await this.#demuxer.read();\n if (packet === null) {\n VERBOSE && console.log('no more packets');\n return null;\n }\n }\n VERBOSE && console.log('returning packet', !!packet, 'pts', packet?.pts, 'si', packet?.stream_index);\n return packet as Packet;\n }\n\n _resizeFrameData(frame): Uint8ClampedArray {\n const components = 4; // 4 components: r, g, b and a\n const size = frame.width * frame.height * components;\n const rawData = new Uint8ClampedArray(size); // we should probably reuse this buffer\n const sourceLineSize = frame.linesize as unknown as number;\n // frame.data can contain multiple \"planes\" in other colorspaces, but in rgba, there is just one \"plane\", so\n // our data is in frame.data[0]\n const pixels = frame.data[0] as Uint8Array;\n\n // libav creates larger buffers because it makes their internal code simpler.\n // we have to trim a part at the right of each pixel row.\n for (let i = 0; i < frame.height; i++) {\n const sourceStart = i * sourceLineSize;\n const sourceEnd = sourceStart + frame.width * components;\n const sourceData = pixels.slice(sourceStart, sourceEnd);\n const targetOffset = i * frame.width * components;\n rawData.set(sourceData, targetOffset);\n }\n return rawData;\n }\n\n async dispose() {\n if (this.#decoder) {\n await this.#decoder.flush();\n this.#decoder = null;\n }\n this.#demuxer.forceClose();\n this.#filterer = null;\n this.#filteredFrames = undefined;\n this.#frames = [];\n this.#packet = null;\n this.#previousTargetPTS = null;\n this.#streamIndex = 0;\n }\n}\n"],"names":["BaseExtractor","args","inputFileOrUrl","outputFile","threadCount","endTime","interpolateFps","interpolateMode","targetPts","targetTime","pts","onFrameAvailable","flush","CancelRequestError","DownloadVideoURL","url","__privateAdd","_url","_httpRequest","_filepath","_tmpObj","__privateSet","__privateGet","resolve","reject","source","extension","path","tmp","connectionHandler","https","http","res","contentType","err","writeStream","fs","e","createDecoder","demuxer","streamIndex","beamcoder","createFilter","stream","outputPixelFormat","filterSpec","filterSpecStr","STREAM_TYPE_VIDEO","COLORSPACE_RGBA","_BeamcoderExtractor","_createDecoder","_demuxer","_decoder","_filterer","_filteredFrames","_frames","_packet","_previousTargetPTS","_threadCount","_streamIndex","extractor","downloadUrl","time_base","durations","frame","rawData","createImageData","time","targetPTS","__privateMethod","createDecoder_fn","closestFrame","f","filteredFrames","closestFramePTS","outputFrame","__privateWrapper","r","packet","decodedFrames","frames","size","sourceLineSize","pixels","i","sourceStart","sourceEnd","sourceData","targetOffset","BeamcoderExtractor"],"mappings":"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"}
|