@tikagent/video-editor-cli 1.5.0-preview.3 → 1.5.0-preview.5

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.
@@ -1 +1 @@
1
- "use strict";const e=require("http"),r=require("fs"),t=require("path"),o=require("os"),{spawn:i,fork:s,spawnSync:n}=require("./cli/lib/processes"),{DeliverySession:a,encoders:c}=require("./export-delivery"),d=require("./encode-profiles"),p=require("./delivery"),{writeFrame:l,stopProcess:m,run:u}=require("./export-encoder"),{createExportSaver:f,resolveExportSource:g,validVideoName:h}=require("./export-save"),w=f(),{analyze:y}=require("./analyze"),v=require("./project-store"),{ExportCache:b}=require("./export-cache"),{Waveforms:S}=require("./waveform"),{ExportAudio:x}=require("./export-audio"),{pipeline:I}=require("node:stream/promises"),{APP_DIR:j,DATA_DIR:E,PROJECTS_DIR:P,LIBRARY_DIR:T,LIBRARY_SUBDIRS:q,DEFAULT_PROJECT_ID:O,normalizeProjectId:D,projectPaths:k,listProjects:C,ensureDirs:F,projectRoots:N}=require("./paths"),R=j,z=new x({libraryDir:T}),$=process.env.HOST||"127.0.0.1",U=new Set(["localhost","127.0.0.1","[::1]","::1",$.toLowerCase()]);for(const e of(process.env.FABLECUT_ALLOWED_HOSTS||"").split(","))e.trim()&&U.add(e.trim().toLowerCase());function A(e){if(!e)return!1;const r=e.replace(/^(\[[^\]]*\]|[^:]+)(:\d+)?$/,"$1").toLowerCase();return U.has(r)}let _=!1;try{_=0===n("ffmpeg",["-version"],{stdio:"ignore"}).status}catch{}let G=!1;try{G=0===n("ffprobe",["-version"],{stdio:"ignore"}).status}catch{}const M=new b({libraryDir:T}),J=new S({libraryDir:T}),L={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".json":"application/json",".mp4":"video/mp4",".webm":"video/webm",".mov":"video/quicktime",".mkv":"video/x-matroska",".m4v":"video/mp4",".mp3":"audio/mpeg",".wav":"audio/wav",".ogg":"audio/ogg",".m4a":"audio/mp4",".aac":"audio/aac",".flac":"audio/flac",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".ico":"image/x-icon",".ttf":"font/ttf",".otf":"font/otf",".woff":"font/woff",".woff2":"font/woff2"};F();const H=new Set;function B(e,r="change"){for(const t of H)e&&t.projectId!==e||t.res.write(`data: ${r}\n\n`)}const W=new Map;function Q(e){clearTimeout(W.get(e)),W.set(e,setTimeout(()=>B(e),150))}const V=new Map;function Y(e){const r=V.get(e);if(V.delete(e),r)for(const e of r.watchers)e.close()}function K(e){const t=k(e),o=V.get(e);if(o?.dir===t.dir)return;Y(e);const i=[],s={dir:t.dir,watchers:i},n=(t,o)=>{try{const n=r.watch(t,o);n.on("error",()=>{V.get(e)===s&&Y(e),Q(e)}),i.push(n)}catch{}};n(t.dir,(r,t)=>{"project.json"===t&&Q(e)}),n(t.mediaDir,()=>Q(e)),i.length&&V.set(e,s)}for(const e of C())K(e.id);let X=null;for(const e of[P,N().legacyDir].filter(Boolean))try{r.watch(e,(e,r)=>{const t=r&&String(r),o=t&&/^[a-z0-9][a-z0-9_-]{0,63}$/.test(t)?[t]:[...V.keys()];for(const e of o){Y(e);try{K(e)}catch{}Q(e)}clearTimeout(X),X=setTimeout(()=>B(null,"projects"),150)}).on("error",()=>{for(const[r,o]of V)t.dirname(o.dir)===e&&Y(r);B(null,"projects")})}catch{}for(const e of q)try{r.watch(t.join(T,e),()=>B(null))}catch{}function Z(e,r,t){const o=JSON.stringify(t);e.writeHead(r,{"Content-Type":"application/json","Cache-Control":"no-store"}),e.end(o)}function ee(e){return new Promise((r,t)=>{const o=[];e.on("data",e=>o.push(e)),e.on("end",()=>r(Buffer.concat(o))),e.on("error",t)})}function re(e,o){let i=t.join(e,o);const s=t.extname(o),n=t.basename(o,s);let a=1;for(;r.existsSync(i);)i=t.join(e,`${n}_${a++}${s}`);return i}function te(e){const r=D(e.searchParams.get("project")||O),t=v.context(r);return K(r),t}function oe(e,r){return"/projects/"+encodeURIComponent(e)+"/media/"+encodeURIComponent(r)}const ie=new Set([".mp4",".mov",".m4v"]),se=new Map,ne=new Map,ae=new Map;function ce(){const e=new Set;for(const[r,t]of ae){const o=ne.get(r)?.state;o&&["complete","error","cancelled"].includes(o)||e.add(t.projectId)}for(const r of se.values())r.projectId&&e.add(r.projectId);return[...e]}function de(e){const r=ae.get(e);if(!r||!r.cleanupDone||r.child&&!r.childClosed)return;clearTimeout(r.timer),ae.delete(e);const t=ne.get(e);t&&ne.set(e,{...t,workerDone:!0}),queueMicrotask(pe)}function pe(){let e=[...ae.values()].filter(e=>e.child).length;for(const[r,t]of ae){if(e>=2)break;t.child||t.stopping||(le(r,t),t.child&&e++)}}function le(e,r){const{projectId:o,options:i}=r;try{const n=s(t.join(R,"export-worker.js"),[],{stdio:["ignore","ignore","pipe","ipc"]});r.child=n,ge(e,{state:"starting",projectId:o});let a="";n.stderr.on("data",e=>{a=(a+e).slice(-4e3),r.stderrTail=a}),n.on("message",r=>{const t=ne.get(e);(["starting","preparing-browser","preparing-media"].includes(t?.state)||"error"===r.state)&&ge(e,{...r,projectId:o})}),n.on("error",r=>ge(e,{state:"error",projectId:o,error:r.message})),n.once("close",()=>{r.childClosed=!0,ge(e,{state:"error",projectId:o,error:"Export worker exited: "+a.slice(-800)}),de(e)}),r.timer=setTimeout(()=>ge(e,{state:"error",projectId:o,error:"Export timed out"}),1e3*i.timeoutSeconds);const c=be.address(),d="0.0.0.0"===$?"127.0.0.1":"::"===$?"[::1]":$.includes(":")&&!$.startsWith("[")?`[${$}]`:$;n.send({id:e,projectId:o,engine:i.engine,name:i.name,browser:i.browser,url:`http://${d}:${c.port}/`})}catch(r){ge(e,{state:"error",projectId:o,error:r.message})}}function me(e,r){const t=e.searchParams.get("exportJob");if(!t)return;const o=ae.get(t);if(!o||o.stopping||o.projectId!==r.id)throw new Error("no such export job");return o.snapshot}function ue(){if(!ue.monitor)try{const e=require("./cli/lib/telemetry");ue.monitor=e.createExportMonitor({enrich(e){const r=ae.get(e),t=ne.get(e);let o,i,s=t?.wait;for(const e of r?.cacheIds||[]){const r=M.sessions.get(e);r&&(o=r.stats,i=r.activity||i,r.wait&&(s={reason:r.wait.reason,elapsedMs:Math.max(0,Date.now()-r.wait.since)}))}return{stats:o,activity:i,wait:s,stderr:r?.stderrTail,detail:t?.detail,browser:t?.browser}}})}catch{ue.monitor={stop(){},upsert(){},note(){},cancel:()=>!1,fail(){},complete(){},scan(){}}}return ue.monitor}function fe(e,o){let i=0;const s=`/projects/${e.id}/media/`;for(const n of o?.media||[])try{if("string"!=typeof n?.src||!n.src.startsWith(s))continue;const o=decodeURIComponent(n.src.slice(s.length));if(!o||o.includes("..")||/[/\\]/.test(o))continue;i+=r.statSync(t.join(e.mediaDir,o)).size}catch{}return i}function ge(e,r){if(!e||!/^[a-f0-9]{32}$/.test(e))return;const t=ne.get(e);if(!t||!["complete","error","cancelled"].includes(t.state)){"error"===r.state&&(r={...r,error:`${r.error} (stage=${t?.state||"starting"}, frames=${t?.frames||0})`}),ne.set(e,{...t,...r,updated:Date.now()});try{!function(e,r,t){const o=ue();if("error"===t.state)return o.note(e,{stack:t.stack,clipIds:t.clipIds,browserGpu:t.browserGpu,timeSec:t.timeSec}),void o.fail(e,t.error,{stage:r?.state,frames:t.frames??r?.frames,totalFrames:t.totalFrames??r?.totalFrames,origin:"server"});"cancelled"!==t.state?"complete"!==t.state?o.upsert(e,{projectId:t.projectId||r?.projectId,state:t.state||r?.state,frames:t.frames??r?.frames,totalFrames:t.totalFrames??r?.totalFrames,detail:t.detail,engine:t.engine||r?.engine,browser:t.browser}):o.complete(e,{metrics:t.metrics,frames:t.frames??r?.frames,totalFrames:t.totalFrames??r?.totalFrames}):o.cancel(e)}(e,t,r)}catch{}if(["complete","error","cancelled"].includes(r.state)&&function(e){const r=ae.get(e);if(!r||r.stopping)return;r.stopping=!0,r.recordingUpload?.abort(),clearTimeout(r.timer);const t=[];for(const e of r.cacheIds||[])t.push(M.release(e,r.projectId));for(const[r,o]of se)o.requestId===e&&t.push(ye(r));r.child&&!r.childClosed&&r.child.kill();const o=()=>{r.cleanupDone=!0,de(e)};t.some(Boolean)?Promise.allSettled(t).then(o):o()}(e),ne.size>100){const e=[...ne].sort((e,r)=>e[1].updated-r[1].updated).slice(0,20);for(const[r]of e)ae.has(r)||ne.delete(r)}}}function he(e,r){const t=ae.get(e.requestId);(!e.modern||!t||t.options.groups.length<=1)&&ge(e.requestId,{state:"error",projectId:e.projectId,error:r})}function we(e,s,n,a,c,d){const p=Date.now().toString(36)+Math.random().toString(36).slice(2,7),l=r.mkdtempSync(t.join(o.tmpdir(),"fablecut-")),m=t.join(l,"video.mp4"),u=i("ffmpeg",[...c?["-filter_threads","1"]:[],"-y","-f","image2pipe","-vcodec","mjpeg","-framerate",String(e),"-i","-","-vf","scale=in_range=full:in_color_matrix=bt601:out_range=tv:out_color_matrix=bt709","-c:v","libx264","-preset","fast","-crf","18","-pix_fmt","yuv420p","-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv",m],{stdio:["pipe","ignore","pipe"]});let f="";u.stderr.on("data",e=>{f=(f+e).slice(-2e3)});const g={controller:new AbortController,proc:u,dir:l,videoPath:m,name:(s||"export").replace(/[<>:"/\\|?*\x00-\x1f]+/g,"_").slice(0,120).trim()||"export",projectId:n,requestId:a,cacheId:c,totalFrames:Number.isSafeInteger(d)&&d>0?d:void 0,wav:null,frames:0,frameBytes:0,err:()=>f,done:new Promise(e=>u.on("close",e))};se.set(p,g);const h=e=>{g.cleaning||(ge(a,{state:"error",projectId:n,error:e.message}),ye(p))};return u.on("error",h),u.stdin.on("error",h),u.once("exit",(e,r)=>{g.finalizing&&0===e||h(new Error(`ffmpeg exited (${r||e}): ${g.err()}`))}),ge(a,{state:"rendering",projectId:n,frames:0,totalFrames:g.totalFrames}),p}function ye(e){const t=se.get(e);if(t)return t.cleaning||(t.controller.abort(),t.cleaning=(async()=>{await Promise.allSettled([t.modern?t.modern.close():m(t.proc,t.done),t.muxDone,t.cacheId&&M.release(t.cacheId,t.projectId)]),t.output&&!t.committed&&await r.promises.rm(t.output,{force:!0}),await r.promises.rm(t.dir,{recursive:!0,force:!0})})().catch(e=>console.error("Export cleanup failed:",e.message)).finally(()=>se.delete(e))),t.cleaning}function ve(e,o,i){r.stat(i,(s,n)=>{if(s||!n.isFile())return o.writeHead(404),void o.end("Not found");"HEAD"===e.method&&o.setHeader("ETag",`W/"${n.size}-${n.mtimeMs}"`);const a=L[t.extname(i).toLowerCase()]||"application/octet-stream",c=e.headers.range;if(c){const t=/bytes=(\d*)-(\d*)/.exec(c);let s=t&&t[1]?parseInt(t[1]):0,d=t&&t[2]?parseInt(t[2]):n.size-1;if(s=Math.min(s,n.size-1),d=Math.min(d,n.size-1),o.writeHead(206,{"Content-Type":a,"Accept-Ranges":"bytes","Content-Range":`bytes ${s}-${d}/${n.size}`,"Content-Length":d-s+1}),"HEAD"===e.method)return void o.end();r.createReadStream(i,{start:s,end:d}).pipe(o)}else{if(o.writeHead(200,{"Content-Type":a,"Content-Length":n.size,"Accept-Ranges":"bytes","Cache-Control":"no-cache"}),"HEAD"===e.method)return void o.end();r.createReadStream(i).pipe(o)}})}setInterval(()=>{for(const[e,r]of se)r.cacheId&&!M.sessions.has(r.cacheId)&&(ge(r.requestId,{state:"error",projectId:r.projectId,error:"Optimized export session expired"}),ye(e))},15e3).unref(),setInterval(()=>{for(const[e,r]of se)r.modern&&!ae.has(r.requestId)&&Date.now()-r.modern.lastTouch>6e5&&ye(e)},6e4).unref();const be=e.createServer(async(e,o)=>{if(!function(e){if(!A(e.headers.host))return!1;const r=e.headers.origin;if(r)try{return A(new URL(r).host)}catch{return!1}return!0}(e))return void Z(o,403,{error:"forbidden: request must come from this machine (bad Host or Origin header)"});const i=new URL(e.url,"http://localhost"),s=decodeURIComponent(i.pathname);if(s.split(/[\\/]/).some(e=>e.startsWith(".")))return o.writeHead(403),void o.end();if("/api/status"===s&&"GET"===e.method)return void Z(o,200,{service:"fablecut",pid:process.pid,version:Se(),dataDir:r.realpathSync(E)});if("/api/export/active"===s&&"GET"===e.method){const e=ce(),r=i.searchParams.get("project");return void Z(o,200,{active:r?e.includes(r):e.length>0,projects:e})}if("/api/server/stop"===s&&"POST"===e.method){if(se.size>0||ce().length>0)return void Z(o,409,{error:"Export in progress"});const e=JSON.stringify({ok:!0,stopped:!0});return o.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-store","Content-Length":Buffer.byteLength(e)}),void o.end(e,()=>{je(),be.close(()=>process.exit(0))})}if("/api/projects"===s&&"GET"===e.method)return void Z(o,200,C());if("/api/projects"===s&&"POST"===e.method){try{const r=JSON.parse((await ee(e)).toString("utf8")||"{}"),t=String(r.name||"Untitled Project").trim()||"Untitled Project",{id:i}=v.create(t,r.id);K(i),B(null,"projects"),Z(o,201,{id:i,name:t})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/project"===s&&"GET"===e.method){try{const e=te(i);Z(o,200,me(i,e)||v.read(e.id))}catch(e){if("PROJECT_NOT_FOUND"!==e.code)try{require("./cli/lib/telemetry").reportError("ProjectReadFailed",e)}catch{}Z(o,"PROJECT_NOT_FOUND"===e.code?404:500,{error:String(e)})}return}if("/api/project"===s&&"PUT"===e.method){try{const r=te(i),t=await ee(e),s=JSON.parse(t.toString("utf8"));Z(o,200,{ok:!0,revision:v.save(r.id,s,"1"===i.searchParams.get("force")).revision})}catch(e){Z(o,e.status||400,{error:String(e),revision:e.revision})}return}if("/api/media"===s&&"GET"===e.method){try{const e=te(i);Z(o,200,r.readdirSync(e.mediaDir).filter(o=>r.statSync(t.join(e.mediaDir,o)).isFile()).map(o=>({name:o,src:oe(e.id,o),size:r.statSync(t.join(e.mediaDir,o)).size})))}catch(e){Z(o,500,{error:String(e)})}return}if("/api/library"===s&&"GET"===e.method){const e=i.searchParams.get("dir");if(!q.includes(e))return void Z(o,400,{error:"dir must be one of "+q.join("|")});try{const i=t.join(T,e),s=[],n=(o,i)=>{for(const a of r.readdirSync(o)){const c=t.join(o,a),d=i?i+"/"+a:a,p=r.statSync(c);p.isDirectory()?n(c,d):s.push({name:a,rel:d,size:p.size,src:"/library/"+e+"/"+d.split("/").map(encodeURIComponent).join("/")})}};n(i,""),Z(o,200,s)}catch(e){Z(o,500,{error:String(e)})}return}if("/api/upload"===s&&"POST"===e.method){try{const s=te(i);let n=function(e){return e.replace(/[^\w.\- ()\[\]]+/g,"_").slice(0,120)||"file"}(i.searchParams.get("name")||"upload.bin"),a=t.join(s.mediaDir,n),c=1;const d=t.extname(n),p=t.basename(n,d),l=await ee(e);for(;;)try{r.writeFileSync(a,l,{flag:"wx"});break}catch(e){if("EEXIST"!==e.code)throw e;a=t.join(s.mediaDir,`${p}_${c++}${d}`)}await async function(e){if(!_||!ie.has(t.extname(e).toLowerCase()))return;const o=e+".fs"+t.extname(e);try{await u("ffmpeg",["-y","-i",e,"-c","copy","-movflags","+faststart",o]),r.rmSync(e),r.renameSync(o,e)}catch{try{r.rmSync(o)}catch{}}}(a),Z(o,200,{ok:!0,src:oe(s.id,t.basename(a))})}catch(e){Z(o,500,{error:String(e)})}return}if(s.startsWith("/api/export/cache/")){try{if(!_||!G)throw new Error("Optimized export needs ffmpeg and ffprobe");const r=te(i),t=i.searchParams.get("id");if("/api/export/cache/begin"===s&&"POST"===e.method){const t=JSON.parse((await ee(e)).toString()),s=me(i,r),n=ae.get(i.searchParams.get("exportJob"));if(n&&!n.child)throw new Error("Export job has not started");const a=M.create(r,t.revision,s,{managed:!!n});n&&(n.cacheIds||=new Set).add(a),Z(o,200,{id:a})}else if("/api/export/cache/status"===s&&"GET"===e.method)Z(o,200,M.status(t,r.id));else if("/api/export/cache/block"===s&&"POST"===e.method){const i=JSON.parse((await ee(e)).toString());Z(o,200,await M.block(t,r.id,i.clipId,i.block,{priority:i.priority,neededAt:i.neededAt,sequence:i.sequence}))}else if("/api/export/cache/retain"===s&&"POST"===e.method){const i=JSON.parse((await ee(e)).toString());M.retain(t,r.id,i.sequence,i.blocks,i.current),Z(o,200,{ok:!0})}else if("/api/export/cache/wait"===s&&"POST"===e.method)M.reportWait(t,r.id,JSON.parse((await ee(e)).toString())),Z(o,200,{ok:!0});else if("/api/export/cache/frame"===s&&"GET"===e.method)ve(e,o,M.frame(t,r.id,i.searchParams.get("key"),Number(i.searchParams.get("index"))));else if("/api/export/cache/drop"===s&&"POST"===e.method){const i=JSON.parse((await ee(e)).toString());M.drop(t,r.id,i.clipIds),Z(o,200,{ok:!0})}else"/api/export/cache/release"===s&&"POST"===e.method?(await M.release(t,r.id),Z(o,200,{ok:!0})):Z(o,404,{error:"unknown cache operation"})}catch(e){Z(o,400,{error:String(e.message||e)})}return}if("/api/export/source-audio"===s&&"GET"===e.method){const e=new AbortController,t=()=>e.abort();let s;o.once("close",t);try{if(!_)throw Object.assign(new Error("Audio export needs ffmpeg"),{status:503});if(s=await z.prepare(te(i),i.searchParams.get("src"),e.signal),e.signal.aborted)return;if(!s)return o.writeHead(204),void o.end();o.writeHead(200,{"Content-Type":"audio/wav","Cache-Control":"no-store"}),await I(r.createReadStream(s.file),o,{signal:e.signal})}catch(r){if(!e.signal.aborted&&(!r.status||r.status>=500))try{require("./cli/lib/telemetry").reportError("ExportAudioFailed",r)}catch{}o.destroyed||o.headersSent?o.destroyed||o.destroy():Z(o,r.status||400,{error:String(r.message||r)})}finally{o.removeListener("close",t),s&&await s.cleanup()}return}if(("/api/waveform"===s||"/api/audio-info"===s)&&"GET"===e.method){try{if(!_||!G)throw Object.assign(new Error("Waveforms need ffmpeg and ffprobe"),{status:503});const e=te(i),r=i.searchParams.get("src");Z(o,200,"/api/waveform"===s?await J.peaks(e,r):{channels:await J.channels(e,r)})}catch(e){if(!e.status||e.status>=500)try{require("./cli/lib/telemetry").reportError("/api/waveform"===s?"WaveformFailed":"AudioInfoFailed",e)}catch{}Z(o,e.status||400,{error:String(e.message||e)})}return}if("/api/export/save"===s&&"POST"===e.method){const r=["127.0.0.1","::1","::ffff:127.0.0.1"].includes(e.socket.remoteAddress);let s=!1;try{s=new URL(e.headers.origin).origin===`http://${e.headers.host}`}catch{}if(!r||!s)return e.resume(),void Z(o,403,{error:"System folder selection requires a local, same-origin editor page",browserSave:!0});const n=new AbortController,a=()=>n.abort();o.once("close",a);try{const r=te(i),s=i.searchParams.get("src");let a,c;if(s)a=await g(r,s),c=t.basename(a),e.resume();else if(c=i.searchParams.get("name"),!["video/mp4","video/webm"].includes(e.headers["content-type"]))throw Object.assign(new Error("Expected a completed video"),{status:400});const d=await w({source:a,name:c,stream:e,signal:n.signal});e.resume(),o.destroyed||Z(o,200,d)}catch(r){e.resume(),o.destroyed||Z(o,r.status||500,{error:r.message,browserSave:!0===r.browserSave||501===r.status})}finally{o.off("close",a)}return}if("/api/export/start"===s&&"POST"===e.method){try{const r=te(i),t=JSON.parse((await ee(e)).toString("utf8")||"{}"),s=t.engine||"optimized";if(!["fast","optimized","webcodecs","realtime"].includes(s))throw new Error("Unknown export engine");if("realtime"!==s&&(!_||"optimized"===s&&!G))throw new Error("Export needs ffmpeg"+("optimized"===s?" and ffprobe":""));if(void 0!==t.browser&&("string"!=typeof t.browser||!t.browser))throw new Error("browser must be a nonempty executable path");const n=void 0===t.timeoutSeconds?3600:t.timeoutSeconds;if(!Number.isFinite(n)||n<=0||n>86400)throw new Error("timeoutSeconds must be between 0 and 86400");const a=v.read(r.id);if(void 0!==t.revision&&t.revision!==a.revision)return void Z(o,409,{error:"Project changed; save/reload before exporting"});if(!a.clips?.length)throw new Error("Timeline is empty");const c=t.requestId??require("crypto").randomBytes(16).toString("hex");if("string"!=typeof c||!/^[a-f0-9]{32}$/.test(c))throw new Error("Invalid export request id");if(ne.has(c))return void Z(o,409,{error:"Export request id already used"});if("realtime"===s&&["profile","preset","delivery","outputs","range","decode"].some(e=>void 0!==t[e]))throw new Error("Realtime does not support offline delivery settings");const d="realtime"===s?[{fps:a.fps,range:"entire",deliveries:[]}]:function(e,r){if(void 0!==e.profile&&"string"!=typeof e.profile)throw new Error("profile must be a string");if(void 0!==e.decode&&!["auto","video","webcodecs","cache"].includes(e.decode))throw new Error("unknown decode backend");if(void 0!==e.range&&!["entire","in-out"].includes(e.range))throw new Error("range must be entire or in-out");if(e.profile&&(e.delivery||e.preset||e.outputs))throw new Error("profile and delivery settings cannot be combined");if("webcodecs"===e.engine&&(e.profile||e.delivery||e.preset||e.outputs))throw new Error("WebCodecs encoding uses bitrate settings, not delivery profiles");const t=!0===e.outputs?r.outputs:e.outputs;if(e.outputs&&(!Array.isArray(t)||!t.length||t.length>16))throw new Error("outputs must contain 1 to 16 deliveries");const o=t?t.filter(e=>!1!==e.on&&!1!==e.enabled):[{delivery:e.delivery||e.preset||!e.profile&&"webcodecs"!==e.engine&&r.delivery,name:e.name}];if(!o.length)throw new Error("no enabled outputs");const i=new Map;for(const s of o){const o=s.delivery||(t?s:s.preset||null),n=o?p.normalize(o).spec:null,a=n?.fps||r.fps,c=s.range||e.range||"entire";if(!["entire","in-out"].includes(c))throw new Error("invalid output range");const d=a+":"+c,l=i.get(d)||{fps:a,range:c,deliveries:[]};if(n&&l.deliveries.push({delivery:n,name:s.name||(t?p.projectFilename(n,r):e.name||r.name)}),l.deliveries.length>8)throw new Error("at most 8 outputs per frame rate and range");i.set(d,l)}return[...i.values()]}({...t,engine:s},a);ae.set(c,{projectId:r.id,snapshot:a,completedOutputs:[],groupIndex:0,options:{groups:d,profile:t.profile,decode:t.decode||"auto",bitrate:t.bitrate,bitrateMode:t.bitrateMode,engine:s,name:String(t.name||a.name||"export"),browser:t.browser,timeoutSeconds:n}});try{const o=require("./cli/lib/telemetry");ue().upsert(c,{state:"queued",engine:s,projectId:r.id,project:o.summarizeExportProject(a,{mediaBytes:fe(r,a)}),startedAt:Date.now(),traceparent:e.headers.traceparent}),!0===t.fallback&&o.reportCustom({name:"export_ui_fallback",type:"export",properties:{engine:s,reason:"ui-fallback"}})}catch{}ge(c,{state:"queued",projectId:r.id,engine:s}),pe(),Z(o,202,{id:c,engine:s})}catch(e){try{const r=String(e.message||e);/Timeline is empty|Unknown export engine|timeoutSeconds|browser must|Invalid export request|already used/.test(r)||require("./cli/lib/telemetry").reportError("ExportStartFailed",e,{origin:"server"})}catch{}Z(o,400,{error:String(e.message||e)})}return}if("/api/export/options"===s&&"GET"===e.method){try{const e=te(i),r=i.searchParams.get("exportJob"),t=ae.get(r);if(!t||t.stopping||t.projectId!==e.id)throw new Error("no such export job");Z(o,200,t.options)}catch(e){Z(o,404,{error:e.message})}return}if("/api/export/profiles"===s&&"GET"===e.method){try{Z(o,200,{...d.listProfilesPublic(!0),encoders:[...await c()],presets:p.PRESETS})}catch(e){Z(o,503,{error:e.message})}return}if("/api/export/cancel"===s&&"POST"===e.method){try{const e=te(i),r=i.searchParams.get("id");if(ne.get(r)?.projectId!==e.id)return void Z(o,404,{error:"no such export request"});ge(r,{state:"cancelled",projectId:e.id}),Z(o,200,{ok:!0})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/export/ffmpeg"===s&&"GET"===e.method)return void Z(o,200,{available:_,ffprobe:G});if("/api/export/status"===s&&"GET"===e.method){try{const e=te(i),r=ne.get(i.searchParams.get("id"));if(!r||r.projectId!==e.id)return void Z(o,404,{error:"no such export request"});let t;if(!["complete","error","cancelled","finalizing"].includes(r.state)){const e=ae.get(i.searchParams.get("id")),r=e?.cacheIds||[...se.values()].filter(e=>e.requestId===i.searchParams.get("id")).map(e=>e.cacheId);for(const e of r){const r=M.sessions.get(e)?.wait;if(r){t={reason:r.reason,elapsedMs:Math.max(0,Date.now()-r.since)};break}}}Z(o,200,{...r,...t?{wait:t}:{}})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/export/recording"===s&&"POST"===e.method){let s,n;try{const a=te(i),c=i.searchParams.get("exportJob");if(n=ae.get(c),!n?.child||n.stopping||n.projectId!==a.id||"realtime"!==n.options.engine)return void Z(o,404,{error:"No active recording for this project"});if(n.recordingUpload)return void Z(o,409,{error:"Recording upload already started"});if(i.searchParams.has("frames")){const e=Number(i.searchParams.get("frames")),r=Number(i.searchParams.get("totalFrames"));if(!Number.isSafeInteger(e)||!Number.isSafeInteger(r)||e<0||r<=0||e>r)throw new Error("Invalid recording progress");ge(c,{state:"rendering",frames:e,totalFrames:r})}else{const o=i.searchParams.get("name");if(!h(o)||!["video/mp4","video/webm"].includes(e.headers["content-type"]))throw new Error("Invalid recording");n.recordingUpload=new AbortController,ge(c,{state:"finalizing"}),r.mkdirSync(a.exportsDir,{recursive:!0}),s=t.join(t.dirname(a.projectFile),".recording-"+c+".part");const d=r.createWriteStream(s,{flags:"wx"});if(await I(e,d,{signal:n.recordingUpload.signal}),n.recordingUpload.signal.throwIfAborted(),!d.bytesWritten)throw new Error("Empty recording");const p=re(a.exportsDir,o);r.renameSync(s,p),s=null,ge(c,{state:"complete",src:"/projects/"+a.id+"/exports/"+encodeURIComponent(t.basename(p))})}Z(o,200,{ok:!0})}catch(e){Z(o,400,{error:e.message})}finally{s&&r.rmSync(s,{force:!0})}return}if("/api/export/report"===s&&"POST"===e.method){try{const r=te(i),t=JSON.parse((await ee(e)).toString("utf8")||"{}");if(!0===t.diagnostic){try{const e=require("./cli/lib/telemetry");t.browserGpu&&e.noteBrowserGpu(t.browserGpu),ue().note(t.requestId,{browserGpu:t.browserGpu,wait:t.wait,stack:t.stack,clipIds:t.clipIds,timeSec:t.timeSec}),Number.isFinite(Number(t.frame))&&ue().upsert(t.requestId,{frames:Number(t.frame)}),t.error&&e.reportError("ExportPageError",t.error,{stage:t.stage||"browser",...Number.isFinite(Number(t.frame))?{frame:Number(t.frame)}:{}},{stack:t.stack,clipIds:t.clipIds,browserGpu:t.browserGpu})}catch{}return void Z(o,200,{ok:!0})}const s=ne.get(t.requestId);if(s&&s.projectId!==r.id)throw new Error("no such export job");s&&"error"===s.state||(s||ge(t.requestId,{projectId:r.id}),await async function(e,r,t=0,o={}){const i=ae.get(e);if(!i||i.options.groups.length<=1)return void ge(e,{...o,state:"error",error:r});if(i.stopping||t!==i.groupIndex)return;if(await Promise.allSettled([...se].filter(([,r])=>r.requestId===e).map(([e])=>ye(e))),i.stopping)return;for(const e of i.options.groups[t].deliveries)i.completedOutputs.push({name:e.name,error:r});i.groupIndex++;const s=i.groupIndex<i.options.groups.length,n=i.completedOutputs.find(e=>e.src)?.src;ge(e,{...o,state:s?"preparing-media":n?"complete":"error",src:n,outputs:i.completedOutputs,error:s||n?void 0:r})}(t.requestId,String(t.error||"browser export failed"),t.group||0,{stack:t.stack,clipIds:t.clipIds,browserGpu:t.browserGpu,timeSec:t.timeSec,...Number.isFinite(Number(t.frame))?{frames:Number(t.frame)}:{}})),Z(o,200,{ok:!0})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/export/begin"===s&&"POST"===e.method){if(!_)return void Z(o,400,{error:"ffmpeg not found on PATH"});try{const r=te(i),t=JSON.parse((await ee(e)).toString("utf8")||"{}"),s=ne.get(t.requestId);if(s&&(["complete","error","cancelled"].includes(s.state)||s.projectId!==r.id))throw new Error("Export request is no longer active");if("optimized"===t.engine&&M.get(t.cacheId,r.id),t.mode||t.profile||t.delivery||t.deliveries){const e=ae.get(t.requestId);if(e&&(e.stopping||(t.group||0)!==e.groupIndex))throw new Error("export group is no longer active");const i=await a.create({...t,width:t.width??t.canvasW,height:t.height??t.canvasH},{exportsDir:r.exportsDir,url:e=>"/projects/"+encodeURIComponent(r.id)+"/exports/"+encodeURIComponent(e)});if(e?.stopping)throw await i.close(),new Error("export cancelled");const s=require("crypto").randomBytes(16).toString("hex"),n={modern:i,controller:i.controller,dir:i.dir,projectId:r.id,requestId:t.requestId,cacheId:t.cacheId,totalFrames:t.totalFrames,frames:0,frameBytes:0,group:t.group||0};se.set(s,n),ge(t.requestId,{state:"rendering",projectId:r.id,frames:0,totalFrames:t.totalFrames,group:n.group+1,totalGroups:e?.options.groups.length||1}),Z(o,200,{id:s,input:i.input,jpegQuality:i.jpegQuality})}else Z(o,200,{id:we(t.fps||30,t.name,r.id,t.requestId,"optimized"===t.engine?t.cacheId:void 0,t.totalFrames)})}catch(e){Z(o,500,{error:String(e)})}return}if("/api/export/frame"===s&&"POST"===e.method){const r=se.get(i.searchParams.get("id"));if(!r)return void Z(o,404,{error:"no such export session"});try{const t=await ee(e);if(r.modern){const e=Math.max(67108864,"rgba"===r.modern.input?r.modern.options.width*r.modern.options.height*4:0);if(t.length>e)throw new Error("frame batch exceeds upload limit");const s=i.searchParams.has("frames")?Number(i.searchParams.get("frames")):void 0;return await r.modern.write(t,s),r.frames=r.modern.frames,r.frameBytes=r.modern.frameBytes,ge(r.requestId,{state:"rendering",projectId:r.projectId,frames:r.frames,totalFrames:r.totalFrames}),void Z(o,200,{ok:!0})}if(t.length<4||255!==t[0]||216!==t[1])throw new Error("export frame is not a valid JPEG");await l(r,t),r.frames++,r.frameBytes+=t.length,ge(r.requestId,{state:"rendering",projectId:r.projectId,frames:r.frames,totalFrames:r.totalFrames}),Z(o,200,{ok:!0})}catch(e){he(r,e.message),ye(i.searchParams.get("id")),Z(o,500,{error:String(e)})}return}if("/api/export/audio"===s&&"POST"===e.method){const s=se.get(i.searchParams.get("id"));if(!s)return void Z(o,404,{error:"no such export session"});try{s.modern?await s.modern.audio(await ee(e)):(s.wav=t.join(s.dir,"audio.wav"),r.writeFileSync(s.wav,await ee(e))),Z(o,200,{ok:!0})}catch(e){Z(o,500,{error:String(e)})}return}if("/api/export/end"===s&&"POST"===e.method){const s=i.searchParams.get("id"),n=se.get(s);if(!n)return void Z(o,404,{error:"no such export session"});let a;try{if(i.searchParams.get("discard"))return await ye(s),void Z(o,200,{ok:!0});if(n.finalizing||n.cleaning)throw new Error("Export session is already ending");const c=Date.now(),d=await ee(e),p=d.length?JSON.parse(d.toString()).metrics:void 0;if(!n.frames)throw new Error("export received no video frames");if(n.finalizing=!0,ge(n.requestId,{state:"finalizing",projectId:n.projectId,frames:n.frames,totalFrames:n.totalFrames}),n.modern){const e=await n.modern.end();p&&"object"==typeof p&&(p.phases||={},p.phases.finalizeMs=Date.now()-c,p.totalMs=(Number(p.totalMs)||0)+p.phases.finalizeMs,p.serverRssBytes=process.memoryUsage().rss);const r=ae.get(n.requestId);r&&(r.completedOutputs.push(...e.outputs),r.groupIndex++);const t=r?.completedOutputs||e.outputs,i=r&&r.groupIndex<r.options.groups.length;return n.committed=!0,ge(n.requestId,{state:i?"preparing-media":"complete",projectId:n.projectId,...e,outputs:t,src:t.find(e=>e.src)?.src,metrics:p}),await ye(s),void Z(o,200,{ok:!0,...e,metrics:p})}if(n.proc.stdin.end(),0!==await n.done)throw new Error("ffmpeg encode failed: "+n.err());if(n.controller.signal.aborted)throw new Error("export cancelled");const l=v.context(n.projectId);r.mkdirSync(l.exportsDir,{recursive:!0}),a=n.output=re(l.exportsDir,n.name.replace(/\.mp4$/i,"")+".mp4");const m=["-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv"];if(n.wav&&r.existsSync(n.wav)?n.muxDone=u("ffmpeg",["-y","-i",n.videoPath,"-i",n.wav,"-c:v","copy","-c:a","aac","-b:a","192k",...n.cacheId?[]:["-shortest"],...m,"-movflags","+faststart",a],n.controller.signal):n.muxDone=u("ffmpeg",["-y","-i",n.videoPath,"-c","copy",...m,"-movflags","+faststart",a],n.controller.signal),await n.muxDone,n.controller.signal.aborted)throw new Error("export cancelled");p&&"object"==typeof p&&(p.phases||={},p.phases.finalizeMs=Date.now()-c,p.totalMs=(Number(p.totalMs)||0)+p.phases.finalizeMs,p.fps=n.frames/(p.totalMs/1e3),p.serverRssBytes=process.memoryUsage().rss),n.committed=!0,ge(n.requestId,{state:"complete",projectId:l.id,metrics:p,src:"/projects/"+encodeURIComponent(l.id)+"/exports/"+encodeURIComponent(t.basename(a))}),await ye(s),Z(o,200,{ok:!0,metrics:p,src:"/projects/"+encodeURIComponent(l.id)+"/exports/"+encodeURIComponent(t.basename(a))})}catch(e){const r=`${String(e)} (frames=${n.frames}, bytes=${n.frameBytes})`;he(n,r),await ye(s),Z(o,500,{error:r})}return}if("/api/analyze"===s&&"GET"===e.method){let e;try{e=te(i)}catch(e){return void Z(o,400,{error:String(e)})}const s=decodeURIComponent(i.searchParams.get("src")||""),n=t.join(e.analysisDir,t.basename(s,t.extname(s))+".json");if(!s||!r.existsSync(n))return void Z(o,404,{error:"no cached analysis for that src — POST /api/analyze first"});try{Z(o,200,JSON.parse(r.readFileSync(n,"utf8")))}catch(e){Z(o,500,{error:String(e)})}return}if("/api/analyze"===s&&"POST"===e.method){if(!_)return void Z(o,400,{error:"ffmpeg not found on PATH"});try{const s=te(i),n=JSON.parse((await ee(e)).toString("utf8")||"{}"),a=t.basename(decodeURIComponent(n.src||"")),c=t.join(s.mediaDir,a);if(!a||!r.existsSync(c))return void Z(o,404,{error:"src must name an existing file under /media/"});const d=await y(c,{threshold:n.threshold,music:!1!==n.music,musicDir:s.mediaDir,srcUrl:oe(s.id,a)});d.music&&(d.music.src=oe(s.id,d.music.name)),r.writeFileSync(t.join(s.analysisDir,t.basename(a,t.extname(a))+".json"),JSON.stringify(d,null,2)),Z(o,200,d)}catch(e){try{require("./cli/lib/telemetry").reportError("AnalyzeFailed",e)}catch{}Z(o,500,{error:String(e)})}return}if("/api/events"===s){let r;try{r=te(i)}catch(e){return void Z(o,400,{error:String(e)})}o.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-store",Connection:"keep-alive"}),o.write("data: hello\n\n");const t={res:o,projectId:r.id};return H.add(t),void e.on("close",()=>H.delete(t))}const n=/^\/projects\/([a-z0-9_-]+)\/(media|exports)\/(.+)$/.exec(s);if(n){try{const i=k(n[1]);if(!r.existsSync(i.projectFile))throw new Error("no such project");const s="media"===n[2]?i.mediaDir:i.exportsDir;ve(e,o,t.join(s,t.basename(n[3])))}catch{o.writeHead(404),o.end("Not found")}return}if(s.startsWith("/media/")||s.startsWith("/exports/")){try{const r=v.context(O),i=s.startsWith("/media/")?r.mediaDir:r.exportsDir;ve(e,o,t.join(i,t.basename(s)))}catch{o.writeHead(404),o.end("Not found")}return}if(s.startsWith("/library/")){const r=t.normalize(t.join(T,s.slice(9)));return r.startsWith(T+t.sep)?void ve(e,o,r):(o.writeHead(403),void o.end())}let m="/"===s?"/index.html":s;if(m=t.normalize(t.join(R,m)),!m.startsWith(R+t.sep))return o.writeHead(403),void o.end();ve(e,o,m)});function Se(){const e=process.env.FABLECUT_VERSION;return"string"==typeof e&&e.trim()?e.trim():""}function xe(e){const r="0.0.0.0"===$?"127.0.0.1":"::"===$?"::1":$;return`http://${r.includes(":")&&!r.startsWith("[")?`[${r}]`:r}:${e}`}function Ie(){return t.join(E,"server.json")}function je(){try{JSON.parse(r.readFileSync(Ie(),"utf8")).pid===process.pid&&r.rmSync(Ie(),{force:!0})}catch{}}process.on("exit",je);let Ee=!1;function Pe(e,r){if(!Ee){Ee=!0;try{require("./cli/lib/telemetry").reportFatalSync(e,r,{command:"server"})}catch{}console.error(r),process.exit(1)}}process.on("uncaughtException",e=>{Pe("UncaughtException",e)}),process.on("unhandledRejection",e=>{Pe("UnhandledRejection",e instanceof Error?e:new Error(String(e)))});try{require("./cli/lib/telemetry").initTelemetry({role:"server",command:"server"})}catch{}const Te=function(){if("auto"===process.env.PORT)return{port:7777,fallback:!0};const e=Number(process.env.PORT||7777);return(!Number.isInteger(e)||e<0||e>65535)&&(console.error("Invalid PORT"),process.exit(1)),{port:e,fallback:"1"===process.env.FABLECUT_PORT_FALLBACK&&0!==e}}();!function e(t,o){const i=r=>{if(o&&r&&"EADDRINUSE"===r.code&&0!==t)e(0,!1);else{try{require("./cli/lib/telemetry").reportFatalSync("ServerListenFailed",r,{port:String(t)})}catch{}console.error(r&&r.message?r.message:r),process.exit(1)}};be.once("error",i),be.listen(t,$,()=>{be.removeListener("error",i);const e=be.address().port;!function(e){const t={pid:process.pid,host:$,port:e,url:xe(e)+"/",version:Se(),dataDir:r.realpathSync(E),startedAt:(new Date).toISOString()},o=Ie(),i=o+".tmp";r.writeFileSync(i,JSON.stringify(t)+"\n"),r.renameSync(i,o)}(e),console.log(`\n FableCut running → ${xe(e)}/\n`),["127.0.0.1","localhost","::1"].includes($)||console.log(` ⚠ WARNING: HOST=${$} exposes the editor (and its file APIs) to the network.\n`),console.log(` projects : ${P} (${C().length})`),console.log(` library : ${T} (${q.join(", ")})`),E!==j&&console.log(` app files : ${j}`),console.log(` ffmpeg : ${_?"found (fast export + faststart remux on)":"not found (real-time export only)"}\n`)})}(Te.port,Te.fallback);
1
+ "use strict";const e=require("http"),r=require("fs"),t=require("path"),o=require("os"),{spawn:i,fork:s,spawnSync:n}=require("./cli/lib/processes"),{DeliverySession:a,encoders:c}=require("./export-delivery"),d=require("./encode-profiles"),p=require("./delivery"),{writeFrame:l,stopProcess:m,run:u}=require("./export-encoder"),{createExportSaver:f,resolveExportSource:g,validVideoName:h}=require("./export-save"),w=f(),{analyze:y}=require("./analyze"),v=require("./project-store"),{ExportCache:b}=require("./export-cache"),{Waveforms:S}=require("./waveform"),{ExportAudio:x}=require("./export-audio"),{pipeline:I}=require("node:stream/promises"),{APP_DIR:j,DATA_DIR:E,PROJECTS_DIR:P,LIBRARY_DIR:T,LIBRARY_SUBDIRS:q,DEFAULT_PROJECT_ID:k,normalizeProjectId:O,projectPaths:D,listProjects:F,ensureDirs:C,projectRoots:N}=require("./paths"),R=j,z=new x({libraryDir:T}),A=process.env.HOST||"127.0.0.1",$=new Set(["localhost","127.0.0.1","[::1]","::1",A.toLowerCase()]);for(const e of(process.env.FABLECUT_ALLOWED_HOSTS||"").split(","))e.trim()&&$.add(e.trim().toLowerCase());function U(e){if(!e)return!1;const r=e.replace(/^(\[[^\]]*\]|[^:]+)(:\d+)?$/,"$1").toLowerCase();return $.has(r)}let _=!1;try{_=0===n("ffmpeg",["-version"],{stdio:"ignore"}).status}catch{}let M=!1;try{M=0===n("ffprobe",["-version"],{stdio:"ignore"}).status}catch{}const G=new b({libraryDir:T}),J=new S({libraryDir:T}),L={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".json":"application/json",".mp4":"video/mp4",".webm":"video/webm",".mov":"video/quicktime",".mkv":"video/x-matroska",".m4v":"video/mp4",".mp3":"audio/mpeg",".wav":"audio/wav",".ogg":"audio/ogg",".m4a":"audio/mp4",".aac":"audio/aac",".flac":"audio/flac",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".webp":"image/webp",".svg":"image/svg+xml",".ico":"image/x-icon",".ttf":"font/ttf",".otf":"font/otf",".woff":"font/woff",".woff2":"font/woff2"};C();const H=new Set;function B(e,r="change"){for(const t of H)e&&t.projectId!==e||t.res.write(`data: ${r}\n\n`)}const W=new Map;function Q(e){clearTimeout(W.get(e)),W.set(e,setTimeout(()=>B(e),150))}const V=new Map;function Y(e){const r=V.get(e);if(V.delete(e),r)for(const e of r.watchers)e.close()}function K(e){const t=D(e),o=V.get(e);if(o?.dir===t.dir)return;Y(e);const i=[],s={dir:t.dir,watchers:i},n=(t,o)=>{try{const n=r.watch(t,o);n.on("error",()=>{V.get(e)===s&&Y(e),Q(e)}),i.push(n)}catch{}};n(t.dir,(r,t)=>{"project.json"===t&&Q(e)}),n(t.mediaDir,()=>Q(e)),i.length&&V.set(e,s)}for(const e of F())K(e.id);let X=null;for(const e of[P,N().legacyDir].filter(Boolean))try{r.watch(e,(e,r)=>{const t=r&&String(r),o=t&&/^[a-z0-9][a-z0-9_-]{0,63}$/.test(t)?[t]:[...V.keys()];for(const e of o){Y(e);try{K(e)}catch{}Q(e)}clearTimeout(X),X=setTimeout(()=>B(null,"projects"),150)}).on("error",()=>{for(const[r,o]of V)t.dirname(o.dir)===e&&Y(r);B(null,"projects")})}catch{}for(const e of q)try{r.watch(t.join(T,e),()=>B(null))}catch{}function Z(e,r,t){const o=JSON.stringify(t);e.writeHead(r,{"Content-Type":"application/json","Cache-Control":"no-store"}),e.end(o)}function ee(e){return new Promise((r,t)=>{const o=[];e.on("data",e=>o.push(e)),e.on("end",()=>r(Buffer.concat(o))),e.on("error",t)})}function re(e,o){let i=t.join(e,o);const s=t.extname(o),n=t.basename(o,s);let a=1;for(;r.existsSync(i);)i=t.join(e,`${n}_${a++}${s}`);return i}function te(e){const r=O(e.searchParams.get("project")||k),t=v.context(r);return K(r),t}function oe(e,r){return"/projects/"+encodeURIComponent(e)+"/media/"+encodeURIComponent(r)}const ie=new Set([".mp4",".mov",".m4v"]),se=new Map,ne=new Map,ae=new Map;function ce(){const e=new Set;for(const[r,t]of ae){const o=ne.get(r)?.state;o&&["complete","error","cancelled"].includes(o)||e.add(t.projectId)}for(const r of se.values())r.projectId&&e.add(r.projectId);return[...e]}function de(e){const r=ae.get(e);if(!r||!r.cleanupDone||r.child&&!r.childClosed)return;clearTimeout(r.timer),ae.delete(e);const t=ne.get(e);t&&ne.set(e,{...t,workerDone:!0}),queueMicrotask(pe)}function pe(){let e=[...ae.values()].filter(e=>e.child).length;for(const[r,t]of ae){if(e>=2)break;t.child||t.stopping||(le(r,t),t.child&&e++)}}function le(e,r){const{projectId:o,options:i}=r;try{const n=s(t.join(R,"export-worker.js"),[],{stdio:["ignore","ignore","pipe","ipc"]});r.child=n,he(e,{state:"starting",projectId:o});let a="";n.stderr.on("data",e=>{a=(a+e).slice(-4e3),r.stderrTail=a}),n.on("message",r=>{const t=ne.get(e);(["starting","preparing-browser","preparing-media"].includes(t?.state)||"error"===r.state)&&he(e,{...r,projectId:o})}),n.on("error",r=>he(e,{state:"error",projectId:o,error:r.message})),n.once("close",()=>{r.childClosed=!0,he(e,{state:"error",projectId:o,error:"Export worker exited: "+a.slice(-800)}),de(e)}),r.timer=setTimeout(()=>he(e,{state:"error",projectId:o,error:"Export timed out"}),1e3*i.timeoutSeconds);const c=Se.address(),d="0.0.0.0"===A?"127.0.0.1":"::"===A?"[::1]":A.includes(":")&&!A.startsWith("[")?`[${A}]`:A;n.send({id:e,projectId:o,engine:i.engine,name:i.name,browser:i.browser,url:`http://${d}:${c.port}/`})}catch(r){he(e,{state:"error",projectId:o,error:r.message})}}function me(e,r){const t=e.searchParams.get("exportJob");if(!t)return;const o=ae.get(t);if(!o||o.stopping||o.projectId!==r.id)throw new Error("no such export job");return o.snapshot}function ue(){if(!ue.monitor)try{const e=require("./cli/lib/telemetry");ue.monitor=e.createExportMonitor({enrich(e){const r=ae.get(e),t=ne.get(e);let o,i,s=t?.wait;for(const e of r?.cacheIds||[]){const r=G.sessions.get(e);r&&(o=r.stats,i=r.activity||i,r.wait&&(s={reason:r.wait.reason,elapsedMs:Math.max(0,Date.now()-r.wait.since)}))}return{stats:o,activity:i,wait:s,stderr:r?.stderrTail,detail:t?.detail,browser:t?.browser}}})}catch{ue.monitor={stop(){},upsert(){},note(){},cancel:()=>!1,fail(){},complete(){},scan(){}}}return ue.monitor}function fe(e,o){let i=0;const s=`/projects/${e.id}/media/`;for(const n of o?.media||[])try{if("string"!=typeof n?.src||!n.src.startsWith(s))continue;const o=decodeURIComponent(n.src.slice(s.length));if(!o||o.includes("..")||/[/\\]/.test(o))continue;i+=r.statSync(t.join(e.mediaDir,o)).size}catch{}return i}function ge(e,r,t){const o=(r||[]).flatMap(e=>e.deliveries||[]);let i="";if("string"==typeof e.preset&&e.preset.trim())i=e.preset.trim();else if(e.delivery&&"object"==typeof e.delivery)i="string"==typeof e.delivery.preset&&e.delivery.preset.trim()?e.delivery.preset.trim():"custom";else if("string"==typeof e.delivery&&e.delivery.trim())i=e.delivery.trim();else if(o.length){const e=[...new Set(o.map(e=>e.delivery?.preset).filter(e=>"string"==typeof e&&e))];i=1===e.length?e[0]:"custom"}const s="string"==typeof e.range&&e.range?e.range:r?.[0]?.range||"",n={};"realtime"!==t&&(n.decode="string"==typeof e.decode&&e.decode?e.decode:"auto"),"string"==typeof e.profile&&e.profile&&(n.profile=e.profile),i&&(n.preset=i),s&&(n.range=s),Number.isFinite(Number(e.bitrate))&&(n.bitrate=Number(e.bitrate)),"string"==typeof e.bitrateMode&&e.bitrateMode&&(n.bitrateMode=e.bitrateMode);const a=o.length||("realtime"===t?0:1);return a&&(n.outputs=a),n}function he(e,r){if(!e||!/^[a-f0-9]{32}$/.test(e))return;const t=ne.get(e);if(!t||!["complete","error","cancelled"].includes(t.state)){"error"===r.state&&(r={...r,error:`${r.error} (stage=${t?.state||"starting"}, frames=${t?.frames||0})`}),ne.set(e,{...t,...r,updated:Date.now()});try{!function(e,r,t){const o=ue();if("error"===t.state)return o.note(e,{stack:t.stack,clipIds:t.clipIds,browserGpu:t.browserGpu,timeSec:t.timeSec}),void o.fail(e,t.error,{stage:r?.state,frames:t.frames??r?.frames,totalFrames:t.totalFrames??r?.totalFrames,origin:"server"});if("cancelled"!==t.state){if("complete"===t.state){const i=function(e){const r=Array.isArray(e?.outputs)?e.outputs:[],t=[],o=new Set,i=e=>{e&&"object"==typeof e&&!o.has(e)&&(o.add(e),t.push(e))};i(e?.report);for(const e of r)i(e?.report);if(!t.length)return;let s=0;for(const e of t)Array.isArray(e.checks)?s+=e.checks.filter(e=>e&&!1===e.ok).length:!1===e.ok&&(s+=1);return{ok:t.every(e=>!1!==e.ok)&&r.every(e=>!e?.error),failed:s,...r.length?{outputs:r.length}:{}}}(t);return void o.complete(e,{metrics:t.metrics,frames:t.frames??r?.frames,totalFrames:t.totalFrames??r?.totalFrames,...i?{delivery:i}:{}})}o.upsert(e,{projectId:t.projectId||r?.projectId,state:t.state||r?.state,frames:t.frames??r?.frames,totalFrames:t.totalFrames??r?.totalFrames,detail:t.detail,engine:t.engine||r?.engine,browser:t.browser})}else o.cancel(e)}(e,t,r)}catch{}if(["complete","error","cancelled"].includes(r.state)&&function(e){const r=ae.get(e);if(!r||r.stopping)return;r.stopping=!0,r.recordingUpload?.abort(),clearTimeout(r.timer);const t=[];for(const e of r.cacheIds||[])t.push(G.release(e,r.projectId));for(const[r,o]of se)o.requestId===e&&t.push(ve(r));r.child&&!r.childClosed&&r.child.kill();const o=()=>{r.cleanupDone=!0,de(e)};t.some(Boolean)?Promise.allSettled(t).then(o):o()}(e),ne.size>100){const e=[...ne].sort((e,r)=>e[1].updated-r[1].updated).slice(0,20);for(const[r]of e)ae.has(r)||ne.delete(r)}}}function we(e,r){const t=ae.get(e.requestId);(!e.modern||!t||t.options.groups.length<=1)&&he(e.requestId,{state:"error",projectId:e.projectId,error:r})}function ye(e,s,n,a,c,d){const p=Date.now().toString(36)+Math.random().toString(36).slice(2,7),l=r.mkdtempSync(t.join(o.tmpdir(),"fablecut-")),m=t.join(l,"video.mp4"),u=i("ffmpeg",[...c?["-filter_threads","1"]:[],"-y","-f","image2pipe","-vcodec","mjpeg","-framerate",String(e),"-i","-","-vf","scale=in_range=full:in_color_matrix=bt601:out_range=tv:out_color_matrix=bt709","-c:v","libx264","-preset","fast","-crf","18","-pix_fmt","yuv420p","-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv",m],{stdio:["pipe","ignore","pipe"]});let f="";u.stderr.on("data",e=>{f=(f+e).slice(-2e3)});const g={controller:new AbortController,proc:u,dir:l,videoPath:m,name:(s||"export").replace(/[<>:"/\\|?*\x00-\x1f]+/g,"_").slice(0,120).trim()||"export",projectId:n,requestId:a,cacheId:c,totalFrames:Number.isSafeInteger(d)&&d>0?d:void 0,wav:null,frames:0,frameBytes:0,err:()=>f,done:new Promise(e=>u.on("close",e))};se.set(p,g);const h=e=>{g.cleaning||(he(a,{state:"error",projectId:n,error:e.message}),ve(p))};return u.on("error",h),u.stdin.on("error",h),u.once("exit",(e,r)=>{g.finalizing&&0===e||h(new Error(`ffmpeg exited (${r||e}): ${g.err()}`))}),he(a,{state:"rendering",projectId:n,frames:0,totalFrames:g.totalFrames}),p}function ve(e){const t=se.get(e);if(t)return t.cleaning||(t.controller.abort(),t.cleaning=(async()=>{await Promise.allSettled([t.modern?t.modern.close():m(t.proc,t.done),t.muxDone,t.cacheId&&G.release(t.cacheId,t.projectId)]),t.output&&!t.committed&&await r.promises.rm(t.output,{force:!0}),await r.promises.rm(t.dir,{recursive:!0,force:!0})})().catch(e=>console.error("Export cleanup failed:",e.message)).finally(()=>se.delete(e))),t.cleaning}function be(e,o,i){r.stat(i,(s,n)=>{if(s||!n.isFile())return o.writeHead(404),void o.end("Not found");"HEAD"===e.method&&o.setHeader("ETag",`W/"${n.size}-${n.mtimeMs}"`);const a=L[t.extname(i).toLowerCase()]||"application/octet-stream",c=e.headers.range;if(c){const t=/bytes=(\d*)-(\d*)/.exec(c);let s=t&&t[1]?parseInt(t[1]):0,d=t&&t[2]?parseInt(t[2]):n.size-1;if(s=Math.min(s,n.size-1),d=Math.min(d,n.size-1),o.writeHead(206,{"Content-Type":a,"Accept-Ranges":"bytes","Content-Range":`bytes ${s}-${d}/${n.size}`,"Content-Length":d-s+1}),"HEAD"===e.method)return void o.end();r.createReadStream(i,{start:s,end:d}).pipe(o)}else{if(o.writeHead(200,{"Content-Type":a,"Content-Length":n.size,"Accept-Ranges":"bytes","Cache-Control":"no-cache"}),"HEAD"===e.method)return void o.end();r.createReadStream(i).pipe(o)}})}setInterval(()=>{for(const[e,r]of se)r.cacheId&&!G.sessions.has(r.cacheId)&&(he(r.requestId,{state:"error",projectId:r.projectId,error:"Optimized export session expired"}),ve(e))},15e3).unref(),setInterval(()=>{for(const[e,r]of se)r.modern&&!ae.has(r.requestId)&&Date.now()-r.modern.lastTouch>6e5&&ve(e)},6e4).unref();const Se=e.createServer(async(e,o)=>{if(!function(e){if(!U(e.headers.host))return!1;const r=e.headers.origin;if(r)try{return U(new URL(r).host)}catch{return!1}return!0}(e))return void Z(o,403,{error:"forbidden: request must come from this machine (bad Host or Origin header)"});const i=new URL(e.url,"http://localhost"),s=decodeURIComponent(i.pathname);if(s.split(/[\\/]/).some(e=>e.startsWith(".")))return o.writeHead(403),void o.end();if("/api/status"===s&&"GET"===e.method)return void Z(o,200,{service:"fablecut",pid:process.pid,version:xe(),dataDir:r.realpathSync(E)});if("/api/export/active"===s&&"GET"===e.method){const e=ce(),r=i.searchParams.get("project");return void Z(o,200,{active:r?e.includes(r):e.length>0,projects:e})}if("/api/server/stop"===s&&"POST"===e.method){if(se.size>0||ce().length>0)return void Z(o,409,{error:"Export in progress"});const e=JSON.stringify({ok:!0,stopped:!0});return o.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-store","Content-Length":Buffer.byteLength(e)}),void o.end(e,()=>{Ee(),Se.close(()=>process.exit(0))})}if("/api/projects"===s&&"GET"===e.method)return void Z(o,200,F());if("/api/projects"===s&&"POST"===e.method){try{const r=JSON.parse((await ee(e)).toString("utf8")||"{}"),t=String(r.name||"Untitled Project").trim()||"Untitled Project",{id:i}=v.create(t,r.id);K(i),B(null,"projects"),Z(o,201,{id:i,name:t})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/project"===s&&"GET"===e.method){try{const e=te(i);Z(o,200,me(i,e)||v.read(e.id))}catch(e){if("PROJECT_NOT_FOUND"!==e.code)try{require("./cli/lib/telemetry").reportError("ProjectReadFailed",e)}catch{}Z(o,"PROJECT_NOT_FOUND"===e.code?404:500,{error:String(e)})}return}if("/api/project"===s&&"PUT"===e.method){try{const r=te(i),t=await ee(e),s=JSON.parse(t.toString("utf8"));Z(o,200,{ok:!0,revision:v.save(r.id,s,"1"===i.searchParams.get("force")).revision})}catch(e){Z(o,e.status||400,{error:String(e),revision:e.revision})}return}if("/api/media"===s&&"GET"===e.method){try{const e=te(i);Z(o,200,r.readdirSync(e.mediaDir).filter(o=>r.statSync(t.join(e.mediaDir,o)).isFile()).map(o=>({name:o,src:oe(e.id,o),size:r.statSync(t.join(e.mediaDir,o)).size})))}catch(e){Z(o,500,{error:String(e)})}return}if("/api/library"===s&&"GET"===e.method){const e=i.searchParams.get("dir");if(!q.includes(e))return void Z(o,400,{error:"dir must be one of "+q.join("|")});try{const i=t.join(T,e),s=[],n=(o,i)=>{for(const a of r.readdirSync(o)){const c=t.join(o,a),d=i?i+"/"+a:a,p=r.statSync(c);p.isDirectory()?n(c,d):s.push({name:a,rel:d,size:p.size,src:"/library/"+e+"/"+d.split("/").map(encodeURIComponent).join("/")})}};n(i,""),Z(o,200,s)}catch(e){Z(o,500,{error:String(e)})}return}if("/api/upload"===s&&"POST"===e.method){try{const s=te(i);let n=function(e){return e.replace(/[^\w.\- ()\[\]]+/g,"_").slice(0,120)||"file"}(i.searchParams.get("name")||"upload.bin"),a=t.join(s.mediaDir,n),c=1;const d=t.extname(n),p=t.basename(n,d),l=await ee(e);for(;;)try{r.writeFileSync(a,l,{flag:"wx"});break}catch(e){if("EEXIST"!==e.code)throw e;a=t.join(s.mediaDir,`${p}_${c++}${d}`)}await async function(e){if(!_||!ie.has(t.extname(e).toLowerCase()))return;const o=e+".fs"+t.extname(e);try{await u("ffmpeg",["-y","-i",e,"-c","copy","-movflags","+faststart",o]),r.rmSync(e),r.renameSync(o,e)}catch{try{r.rmSync(o)}catch{}}}(a),Z(o,200,{ok:!0,src:oe(s.id,t.basename(a))})}catch(e){Z(o,500,{error:String(e)})}return}if(s.startsWith("/api/export/cache/")){try{if(!_||!M)throw new Error("Optimized export needs ffmpeg and ffprobe");const r=te(i),t=i.searchParams.get("id");if("/api/export/cache/begin"===s&&"POST"===e.method){const t=JSON.parse((await ee(e)).toString()),s=me(i,r),n=ae.get(i.searchParams.get("exportJob"));if(n&&!n.child)throw new Error("Export job has not started");const a=G.create(r,t.revision,s,{managed:!!n});n&&(n.cacheIds||=new Set).add(a),Z(o,200,{id:a})}else if("/api/export/cache/status"===s&&"GET"===e.method)Z(o,200,G.status(t,r.id));else if("/api/export/cache/block"===s&&"POST"===e.method){const i=JSON.parse((await ee(e)).toString());Z(o,200,await G.block(t,r.id,i.clipId,i.block,{priority:i.priority,neededAt:i.neededAt,sequence:i.sequence}))}else if("/api/export/cache/retain"===s&&"POST"===e.method){const i=JSON.parse((await ee(e)).toString());G.retain(t,r.id,i.sequence,i.blocks,i.current),Z(o,200,{ok:!0})}else if("/api/export/cache/wait"===s&&"POST"===e.method)G.reportWait(t,r.id,JSON.parse((await ee(e)).toString())),Z(o,200,{ok:!0});else if("/api/export/cache/frame"===s&&"GET"===e.method)be(e,o,G.frame(t,r.id,i.searchParams.get("key"),Number(i.searchParams.get("index"))));else if("/api/export/cache/drop"===s&&"POST"===e.method){const i=JSON.parse((await ee(e)).toString());G.drop(t,r.id,i.clipIds),Z(o,200,{ok:!0})}else"/api/export/cache/release"===s&&"POST"===e.method?(await G.release(t,r.id),Z(o,200,{ok:!0})):Z(o,404,{error:"unknown cache operation"})}catch(e){Z(o,400,{error:String(e.message||e)})}return}if("/api/export/source-audio"===s&&"GET"===e.method){const e=new AbortController,t=()=>e.abort();let s;o.once("close",t);try{if(!_)throw Object.assign(new Error("Audio export needs ffmpeg"),{status:503});if(s=await z.prepare(te(i),i.searchParams.get("src"),e.signal),e.signal.aborted)return;if(!s)return o.writeHead(204),void o.end();o.writeHead(200,{"Content-Type":"audio/wav","Cache-Control":"no-store"}),await I(r.createReadStream(s.file),o,{signal:e.signal})}catch(r){if(!e.signal.aborted&&(!r.status||r.status>=500))try{require("./cli/lib/telemetry").reportError("ExportAudioFailed",r)}catch{}o.destroyed||o.headersSent?o.destroyed||o.destroy():Z(o,r.status||400,{error:String(r.message||r)})}finally{o.removeListener("close",t),s&&await s.cleanup()}return}if(("/api/waveform"===s||"/api/audio-info"===s)&&"GET"===e.method){try{if(!_||!M)throw Object.assign(new Error("Waveforms need ffmpeg and ffprobe"),{status:503});const e=te(i),r=i.searchParams.get("src");Z(o,200,"/api/waveform"===s?await J.peaks(e,r):{channels:await J.channels(e,r)})}catch(e){if(!e.status||e.status>=500)try{require("./cli/lib/telemetry").reportError("/api/waveform"===s?"WaveformFailed":"AudioInfoFailed",e)}catch{}Z(o,e.status||400,{error:String(e.message||e)})}return}if("/api/export/save"===s&&"POST"===e.method){const r=["127.0.0.1","::1","::ffff:127.0.0.1"].includes(e.socket.remoteAddress);let s=!1;try{s=new URL(e.headers.origin).origin===`http://${e.headers.host}`}catch{}if(!r||!s)return e.resume(),void Z(o,403,{error:"System folder selection requires a local, same-origin editor page",browserSave:!0});const n=new AbortController,a=()=>n.abort();o.once("close",a);try{const r=te(i),s=i.searchParams.get("src");let a,c;if(s)a=await g(r,s),c=t.basename(a),e.resume();else if(c=i.searchParams.get("name"),!["video/mp4","video/webm"].includes(e.headers["content-type"]))throw Object.assign(new Error("Expected a completed video"),{status:400});const d=await w({source:a,name:c,stream:e,signal:n.signal});e.resume(),o.destroyed||Z(o,200,d)}catch(r){e.resume(),o.destroyed||Z(o,r.status||500,{error:r.message,browserSave:!0===r.browserSave||501===r.status})}finally{o.off("close",a)}return}if("/api/export/start"===s&&"POST"===e.method){try{const r=te(i),t=JSON.parse((await ee(e)).toString("utf8")||"{}"),s=t.engine||"optimized";if(!["fast","optimized","webcodecs","realtime"].includes(s))throw new Error("Unknown export engine");if("realtime"!==s&&(!_||"optimized"===s&&!M))throw new Error("Export needs ffmpeg"+("optimized"===s?" and ffprobe":""));if(void 0!==t.browser&&("string"!=typeof t.browser||!t.browser))throw new Error("browser must be a nonempty executable path");const n=void 0===t.timeoutSeconds?3600:t.timeoutSeconds;if(!Number.isFinite(n)||n<=0||n>86400)throw new Error("timeoutSeconds must be between 0 and 86400");const a=v.read(r.id);if(void 0!==t.revision&&t.revision!==a.revision)return void Z(o,409,{error:"Project changed; save/reload before exporting"});if(!a.clips?.length)throw new Error("Timeline is empty");const c=t.requestId??require("crypto").randomBytes(16).toString("hex");if("string"!=typeof c||!/^[a-f0-9]{32}$/.test(c))throw new Error("Invalid export request id");if(ne.has(c))return void Z(o,409,{error:"Export request id already used"});if("realtime"===s&&["profile","preset","delivery","outputs","range","decode"].some(e=>void 0!==t[e]))throw new Error("Realtime does not support offline delivery settings");const d="realtime"===s?[{fps:a.fps,range:"entire",deliveries:[]}]:function(e,r){if(void 0!==e.profile&&"string"!=typeof e.profile)throw new Error("profile must be a string");if(void 0!==e.decode&&!["auto","video","webcodecs","cache"].includes(e.decode))throw new Error("unknown decode backend");if(void 0!==e.range&&!["entire","in-out"].includes(e.range))throw new Error("range must be entire or in-out");if(e.profile&&(e.delivery||e.preset||e.outputs))throw new Error("profile and delivery settings cannot be combined");if("webcodecs"===e.engine&&(e.profile||e.delivery||e.preset||e.outputs))throw new Error("WebCodecs encoding uses bitrate settings, not delivery profiles");const t=!0===e.outputs?r.outputs:e.outputs;if(e.outputs&&(!Array.isArray(t)||!t.length||t.length>16))throw new Error("outputs must contain 1 to 16 deliveries");const o=t?t.filter(e=>!1!==e.on&&!1!==e.enabled):[{delivery:e.delivery||e.preset||!e.profile&&"webcodecs"!==e.engine&&r.delivery,name:e.name}];if(!o.length)throw new Error("no enabled outputs");const i=new Map;for(const s of o){const o=s.delivery||(t?s:s.preset||null),n=o?p.normalize(o).spec:null,a=n?.fps||r.fps,c=s.range||e.range||"entire";if(!["entire","in-out"].includes(c))throw new Error("invalid output range");const d=a+":"+c,l=i.get(d)||{fps:a,range:c,deliveries:[]};if(n&&l.deliveries.push({delivery:n,name:s.name||(t?p.projectFilename(n,r):e.name||r.name)}),l.deliveries.length>8)throw new Error("at most 8 outputs per frame rate and range");i.set(d,l)}return[...i.values()]}({...t,engine:s},a);ae.set(c,{projectId:r.id,snapshot:a,completedOutputs:[],groupIndex:0,options:{groups:d,profile:t.profile,decode:t.decode||"auto",bitrate:t.bitrate,bitrateMode:t.bitrateMode,engine:s,name:String(t.name||a.name||"export"),browser:t.browser,timeoutSeconds:n}});try{const o=require("./cli/lib/telemetry");ue().upsert(c,{state:"queued",engine:s,projectId:r.id,project:o.summarizeExportProject(a,{mediaBytes:fe(r,a)}),startedAt:Date.now(),traceparent:e.headers.traceparent,...ge(t,d,s)}),!0===t.fallback&&o.reportCustom({name:"export_ui_fallback",type:"export",properties:{engine:s,reason:"ui-fallback"}})}catch{}he(c,{state:"queued",projectId:r.id,engine:s}),pe(),Z(o,202,{id:c,engine:s})}catch(e){try{const r=String(e.message||e);/Timeline is empty|Unknown export engine|timeoutSeconds|browser must|Invalid export request|already used/.test(r)||require("./cli/lib/telemetry").reportError("ExportStartFailed",e,{origin:"server"})}catch{}Z(o,400,{error:String(e.message||e)})}return}if("/api/export/options"===s&&"GET"===e.method){try{const e=te(i),r=i.searchParams.get("exportJob"),t=ae.get(r);if(!t||t.stopping||t.projectId!==e.id)throw new Error("no such export job");Z(o,200,t.options)}catch(e){Z(o,404,{error:e.message})}return}if("/api/export/profiles"===s&&"GET"===e.method){try{Z(o,200,{...d.listProfilesPublic(!0),encoders:[...await c()],presets:p.PRESETS})}catch(e){Z(o,503,{error:e.message})}return}if("/api/export/cancel"===s&&"POST"===e.method){try{const e=te(i),r=i.searchParams.get("id");if(ne.get(r)?.projectId!==e.id)return void Z(o,404,{error:"no such export request"});he(r,{state:"cancelled",projectId:e.id}),Z(o,200,{ok:!0})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/export/ffmpeg"===s&&"GET"===e.method)return void Z(o,200,{available:_,ffprobe:M});if("/api/export/status"===s&&"GET"===e.method){try{const e=te(i),r=ne.get(i.searchParams.get("id"));if(!r||r.projectId!==e.id)return void Z(o,404,{error:"no such export request"});let t;if(!["complete","error","cancelled","finalizing"].includes(r.state)){const e=ae.get(i.searchParams.get("id")),r=e?.cacheIds||[...se.values()].filter(e=>e.requestId===i.searchParams.get("id")).map(e=>e.cacheId);for(const e of r){const r=G.sessions.get(e)?.wait;if(r){t={reason:r.reason,elapsedMs:Math.max(0,Date.now()-r.since)};break}}}Z(o,200,{...r,...t?{wait:t}:{}})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/export/recording"===s&&"POST"===e.method){let s,n;try{const a=te(i),c=i.searchParams.get("exportJob");if(n=ae.get(c),!n?.child||n.stopping||n.projectId!==a.id||"realtime"!==n.options.engine)return void Z(o,404,{error:"No active recording for this project"});if(n.recordingUpload)return void Z(o,409,{error:"Recording upload already started"});if(i.searchParams.has("frames")){const e=Number(i.searchParams.get("frames")),r=Number(i.searchParams.get("totalFrames"));if(!Number.isSafeInteger(e)||!Number.isSafeInteger(r)||e<0||r<=0||e>r)throw new Error("Invalid recording progress");he(c,{state:"rendering",frames:e,totalFrames:r})}else{const o=i.searchParams.get("name");if(!h(o)||!["video/mp4","video/webm"].includes(e.headers["content-type"]))throw new Error("Invalid recording");n.recordingUpload=new AbortController,he(c,{state:"finalizing"}),r.mkdirSync(a.exportsDir,{recursive:!0}),s=t.join(t.dirname(a.projectFile),".recording-"+c+".part");const d=r.createWriteStream(s,{flags:"wx"});if(await I(e,d,{signal:n.recordingUpload.signal}),n.recordingUpload.signal.throwIfAborted(),!d.bytesWritten)throw new Error("Empty recording");const p=re(a.exportsDir,o);r.renameSync(s,p),s=null,he(c,{state:"complete",src:"/projects/"+a.id+"/exports/"+encodeURIComponent(t.basename(p))})}Z(o,200,{ok:!0})}catch(e){Z(o,400,{error:e.message})}finally{s&&r.rmSync(s,{force:!0})}return}if("/api/export/report"===s&&"POST"===e.method){try{const r=te(i),t=JSON.parse((await ee(e)).toString("utf8")||"{}");if(!0===t.diagnostic){try{const e=require("./cli/lib/telemetry");t.browserGpu&&e.noteBrowserGpu(t.browserGpu),ue().note(t.requestId,{browserGpu:t.browserGpu,wait:t.wait,stack:t.stack,clipIds:t.clipIds,timeSec:t.timeSec,decodeFallback:t.decodeFallback}),Number.isFinite(Number(t.frame))&&ue().upsert(t.requestId,{frames:Number(t.frame)}),t.error&&e.reportError("ExportPageError",t.error,{stage:t.stage||"browser",...Number.isFinite(Number(t.frame))?{frame:Number(t.frame)}:{}},{stack:t.stack,clipIds:t.clipIds,browserGpu:t.browserGpu})}catch{}return void Z(o,200,{ok:!0})}const s=ne.get(t.requestId);if(s&&s.projectId!==r.id)throw new Error("no such export job");s&&"error"===s.state||(s||he(t.requestId,{projectId:r.id}),await async function(e,r,t=0,o={}){const i=ae.get(e);if(!i||i.options.groups.length<=1)return void he(e,{...o,state:"error",error:r});if(i.stopping||t!==i.groupIndex)return;if(await Promise.allSettled([...se].filter(([,r])=>r.requestId===e).map(([e])=>ve(e))),i.stopping)return;for(const e of i.options.groups[t].deliveries)i.completedOutputs.push({name:e.name,error:r});i.groupIndex++;const s=i.groupIndex<i.options.groups.length,n=i.completedOutputs.find(e=>e.src)?.src;he(e,{...o,state:s?"preparing-media":n?"complete":"error",src:n,outputs:i.completedOutputs,error:s||n?void 0:r})}(t.requestId,String(t.error||"browser export failed"),t.group||0,{stack:t.stack,clipIds:t.clipIds,browserGpu:t.browserGpu,timeSec:t.timeSec,...Number.isFinite(Number(t.frame))?{frames:Number(t.frame)}:{}})),Z(o,200,{ok:!0})}catch(e){Z(o,400,{error:String(e)})}return}if("/api/export/begin"===s&&"POST"===e.method){if(!_)return void Z(o,400,{error:"ffmpeg not found on PATH"});try{const r=te(i),t=JSON.parse((await ee(e)).toString("utf8")||"{}"),s=ne.get(t.requestId);if(s&&(["complete","error","cancelled"].includes(s.state)||s.projectId!==r.id))throw new Error("Export request is no longer active");if("optimized"===t.engine&&G.get(t.cacheId,r.id),t.mode||t.profile||t.delivery||t.deliveries){const e=ae.get(t.requestId);if(e&&(e.stopping||(t.group||0)!==e.groupIndex))throw new Error("export group is no longer active");const i=await a.create({...t,width:t.width??t.canvasW,height:t.height??t.canvasH},{exportsDir:r.exportsDir,url:e=>"/projects/"+encodeURIComponent(r.id)+"/exports/"+encodeURIComponent(e)});if(e?.stopping)throw await i.close(),new Error("export cancelled");const s=require("crypto").randomBytes(16).toString("hex"),n={modern:i,controller:i.controller,dir:i.dir,projectId:r.id,requestId:t.requestId,cacheId:t.cacheId,totalFrames:t.totalFrames,frames:0,frameBytes:0,group:t.group||0};se.set(s,n),he(t.requestId,{state:"rendering",projectId:r.id,frames:0,totalFrames:t.totalFrames,group:n.group+1,totalGroups:e?.options.groups.length||1}),Z(o,200,{id:s,input:i.input,jpegQuality:i.jpegQuality})}else Z(o,200,{id:ye(t.fps||30,t.name,r.id,t.requestId,"optimized"===t.engine?t.cacheId:void 0,t.totalFrames)})}catch(e){Z(o,500,{error:String(e)})}return}if("/api/export/frame"===s&&"POST"===e.method){const r=se.get(i.searchParams.get("id"));if(!r)return void Z(o,404,{error:"no such export session"});try{const t=await ee(e);if(r.modern){const e=Math.max(67108864,"rgba"===r.modern.input?r.modern.options.width*r.modern.options.height*4:0);if(t.length>e)throw new Error("frame batch exceeds upload limit");const s=i.searchParams.has("frames")?Number(i.searchParams.get("frames")):void 0;return await r.modern.write(t,s),r.frames=r.modern.frames,r.frameBytes=r.modern.frameBytes,he(r.requestId,{state:"rendering",projectId:r.projectId,frames:r.frames,totalFrames:r.totalFrames}),void Z(o,200,{ok:!0})}if(t.length<4||255!==t[0]||216!==t[1])throw new Error("export frame is not a valid JPEG");await l(r,t),r.frames++,r.frameBytes+=t.length,he(r.requestId,{state:"rendering",projectId:r.projectId,frames:r.frames,totalFrames:r.totalFrames}),Z(o,200,{ok:!0})}catch(e){we(r,e.message),ve(i.searchParams.get("id")),Z(o,500,{error:String(e)})}return}if("/api/export/audio"===s&&"POST"===e.method){const s=se.get(i.searchParams.get("id"));if(!s)return void Z(o,404,{error:"no such export session"});try{s.modern?await s.modern.audio(await ee(e)):(s.wav=t.join(s.dir,"audio.wav"),r.writeFileSync(s.wav,await ee(e))),Z(o,200,{ok:!0})}catch(e){Z(o,500,{error:String(e)})}return}if("/api/export/end"===s&&"POST"===e.method){const s=i.searchParams.get("id"),n=se.get(s);if(!n)return void Z(o,404,{error:"no such export session"});let a;try{if(i.searchParams.get("discard"))return await ve(s),void Z(o,200,{ok:!0});if(n.finalizing||n.cleaning)throw new Error("Export session is already ending");const c=Date.now(),d=await ee(e),p=d.length?JSON.parse(d.toString()).metrics:void 0;if(!n.frames)throw new Error("export received no video frames");if(n.finalizing=!0,he(n.requestId,{state:"finalizing",projectId:n.projectId,frames:n.frames,totalFrames:n.totalFrames}),n.modern){const e=await n.modern.end();p&&"object"==typeof p&&(p.phases||={},p.phases.finalizeMs=Date.now()-c,p.totalMs=(Number(p.totalMs)||0)+p.phases.finalizeMs,p.serverRssBytes=process.memoryUsage().rss);const r=ae.get(n.requestId);r&&(r.completedOutputs.push(...e.outputs),r.groupIndex++);const t=r?.completedOutputs||e.outputs,i=r&&r.groupIndex<r.options.groups.length;return n.committed=!0,he(n.requestId,{state:i?"preparing-media":"complete",projectId:n.projectId,...e,outputs:t,src:t.find(e=>e.src)?.src,metrics:p}),await ve(s),void Z(o,200,{ok:!0,...e,metrics:p})}if(n.proc.stdin.end(),0!==await n.done)throw new Error("ffmpeg encode failed: "+n.err());if(n.controller.signal.aborted)throw new Error("export cancelled");const l=v.context(n.projectId);r.mkdirSync(l.exportsDir,{recursive:!0}),a=n.output=re(l.exportsDir,n.name.replace(/\.mp4$/i,"")+".mp4");const m=["-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv"];if(n.wav&&r.existsSync(n.wav)?n.muxDone=u("ffmpeg",["-y","-i",n.videoPath,"-i",n.wav,"-c:v","copy","-c:a","aac","-b:a","192k",...n.cacheId?[]:["-shortest"],...m,"-movflags","+faststart",a],n.controller.signal):n.muxDone=u("ffmpeg",["-y","-i",n.videoPath,"-c","copy",...m,"-movflags","+faststart",a],n.controller.signal),await n.muxDone,n.controller.signal.aborted)throw new Error("export cancelled");p&&"object"==typeof p&&(p.phases||={},p.phases.finalizeMs=Date.now()-c,p.totalMs=(Number(p.totalMs)||0)+p.phases.finalizeMs,p.fps=n.frames/(p.totalMs/1e3),p.serverRssBytes=process.memoryUsage().rss),n.committed=!0,he(n.requestId,{state:"complete",projectId:l.id,metrics:p,src:"/projects/"+encodeURIComponent(l.id)+"/exports/"+encodeURIComponent(t.basename(a))}),await ve(s),Z(o,200,{ok:!0,metrics:p,src:"/projects/"+encodeURIComponent(l.id)+"/exports/"+encodeURIComponent(t.basename(a))})}catch(e){const r=`${String(e)} (frames=${n.frames}, bytes=${n.frameBytes})`;we(n,r),await ve(s),Z(o,500,{error:r})}return}if("/api/analyze"===s&&"GET"===e.method){let e;try{e=te(i)}catch(e){return void Z(o,400,{error:String(e)})}const s=decodeURIComponent(i.searchParams.get("src")||""),n=t.join(e.analysisDir,t.basename(s,t.extname(s))+".json");if(!s||!r.existsSync(n))return void Z(o,404,{error:"no cached analysis for that src — POST /api/analyze first"});try{Z(o,200,JSON.parse(r.readFileSync(n,"utf8")))}catch(e){Z(o,500,{error:String(e)})}return}if("/api/analyze"===s&&"POST"===e.method){if(!_)return void Z(o,400,{error:"ffmpeg not found on PATH"});try{const s=te(i),n=JSON.parse((await ee(e)).toString("utf8")||"{}"),a=t.basename(decodeURIComponent(n.src||"")),c=t.join(s.mediaDir,a);if(!a||!r.existsSync(c))return void Z(o,404,{error:"src must name an existing file under /media/"});const d=await y(c,{threshold:n.threshold,music:!1!==n.music,musicDir:s.mediaDir,srcUrl:oe(s.id,a)});d.music&&(d.music.src=oe(s.id,d.music.name)),r.writeFileSync(t.join(s.analysisDir,t.basename(a,t.extname(a))+".json"),JSON.stringify(d,null,2)),Z(o,200,d)}catch(e){try{require("./cli/lib/telemetry").reportError("AnalyzeFailed",e)}catch{}Z(o,500,{error:String(e)})}return}if("/api/events"===s){let r;try{r=te(i)}catch(e){return void Z(o,400,{error:String(e)})}o.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-store",Connection:"keep-alive"}),o.write("data: hello\n\n");const t={res:o,projectId:r.id};return H.add(t),void e.on("close",()=>H.delete(t))}const n=/^\/projects\/([a-z0-9_-]+)\/(media|exports)\/(.+)$/.exec(s);if(n){try{const i=D(n[1]);if(!r.existsSync(i.projectFile))throw new Error("no such project");const s="media"===n[2]?i.mediaDir:i.exportsDir;be(e,o,t.join(s,t.basename(n[3])))}catch{o.writeHead(404),o.end("Not found")}return}if(s.startsWith("/media/")||s.startsWith("/exports/")){try{const r=v.context(k),i=s.startsWith("/media/")?r.mediaDir:r.exportsDir;be(e,o,t.join(i,t.basename(s)))}catch{o.writeHead(404),o.end("Not found")}return}if(s.startsWith("/library/")){const r=t.normalize(t.join(T,s.slice(9)));return r.startsWith(T+t.sep)?void be(e,o,r):(o.writeHead(403),void o.end())}let m="/"===s?"/index.html":s;if(m=t.normalize(t.join(R,m)),!m.startsWith(R+t.sep))return o.writeHead(403),void o.end();be(e,o,m)});function xe(){const e=process.env.FABLECUT_VERSION;return"string"==typeof e&&e.trim()?e.trim():""}function Ie(e){const r="0.0.0.0"===A?"127.0.0.1":"::"===A?"::1":A;return`http://${r.includes(":")&&!r.startsWith("[")?`[${r}]`:r}:${e}`}function je(){return t.join(E,"server.json")}function Ee(){try{JSON.parse(r.readFileSync(je(),"utf8")).pid===process.pid&&r.rmSync(je(),{force:!0})}catch{}}process.on("exit",Ee);let Pe=!1;function Te(e,r){if(!Pe){Pe=!0;try{require("./cli/lib/telemetry").reportFatalSync(e,r,{command:"server"})}catch{}console.error(r),process.exit(1)}}process.on("uncaughtException",e=>{Te("UncaughtException",e)}),process.on("unhandledRejection",e=>{Te("UnhandledRejection",e instanceof Error?e:new Error(String(e)))});try{require("./cli/lib/telemetry").initTelemetry({role:"server",command:"server"})}catch{}const qe=function(){if("auto"===process.env.PORT)return{port:7777,fallback:!0};const e=Number(process.env.PORT||7777);return(!Number.isInteger(e)||e<0||e>65535)&&(console.error("Invalid PORT"),process.exit(1)),{port:e,fallback:"1"===process.env.FABLECUT_PORT_FALLBACK&&0!==e}}();!function e(t,o){const i=r=>{if(o&&r&&"EADDRINUSE"===r.code&&0!==t)e(0,!1);else{try{require("./cli/lib/telemetry").reportFatalSync("ServerListenFailed",r,{port:String(t)})}catch{}console.error(r&&r.message?r.message:r),process.exit(1)}};Se.once("error",i),Se.listen(t,A,()=>{Se.removeListener("error",i);const e=Se.address().port;!function(e){const t={pid:process.pid,host:A,port:e,url:Ie(e)+"/",version:xe(),dataDir:r.realpathSync(E),startedAt:(new Date).toISOString()},o=je(),i=o+".tmp";r.writeFileSync(i,JSON.stringify(t)+"\n"),r.renameSync(i,o)}(e),console.log(`\n FableCut running → ${Ie(e)}/\n`),["127.0.0.1","localhost","::1"].includes(A)||console.log(` ⚠ WARNING: HOST=${A} exposes the editor (and its file APIs) to the network.\n`),console.log(` projects : ${P} (${F().length})`),console.log(` library : ${T} (${q.join(", ")})`),E!==j&&console.log(` app files : ${j}`),console.log(` ffmpeg : ${_?"found (fast export + faststart remux on)":"not found (real-time export only)"}\n`)})}(qe.port,qe.fallback);
@@ -1 +1 @@
1
- "use strict";async function webCodecsExport(e={}){if(state.exporting)return;pause(),state.exporting=!0,state.rendering=!0,renderCancelled=!1;const t=new AbortController,r=t.signal;fastExportAbort=t;let o,a,n,i,s=Promise.resolve(),d=0,c=0;const p=()=>{if(renderCancelled||r.aborted)throw new Error("cancelled");if(i)throw i},l=async(e,t)=>{p();const o=await fetch(e,{method:"POST",body:t instanceof Blob?t:void 0===t?void 0:JSON.stringify(t),signal:r}),a=await o.json();if(!o.ok||a.error)throw new Error(a.error||"export request failed");return a};els.exportOverlay.classList.remove("hidden");try{const t=exportWindow(e);if("function"!=typeof VideoEncoder)throw new Error("WebCodecs VideoEncoder is unavailable");const f=e.bitrate??Math.max(1e5,Math.min(2e8,Math.round(project.width*project.height*t.fps*.15)));if(!Number.isSafeInteger(f)||f<1e5||f>2e8)throw new Error("bitrate must be between 100000 and 200000000 bits/s");const w=e.bitrateMode||"variable";if(!["variable","constant"].includes(w))throw new Error("bitrateMode must be variable or constant");let h;for(const e of["avc1.640034","avc1.640033","avc1.420028"]){const r={codec:e,width:project.width,height:project.height,bitrate:f,bitrateMode:w,framerate:t.fps,avc:{format:"annexb"},latencyMode:"quality"};if((await VideoEncoder.isConfigSupported(r).catch(()=>({}))).supported){h=r;break}}if(!h)throw new Error("H.264 WebCodecs encoding is unsupported at this resolution");exportDecodeBegin(r,e.decode);const u=await renderExportAudio(t.start,t.dur,r),b=await l(projectApi("/api/export/begin"),exportBeginOptions(e,t,u,"webcodecs"));a=b.id,u&&await l("/api/export/audio?id="+a,u),n=new BatchFrameUploader((e,t)=>l(`/api/export/frame?id=${a}&frames=${t}`,e)),o=new VideoEncoder({output:e=>{if(r.aborted||i)return;const t=new Uint8Array(e.byteLength);e.copyTo(t),d++,c+=t.length,s=s.then(()=>n.push(new Blob([t]))).catch(e=>{i||=e}).finally(()=>{d--,c-=t.length})},error:e=>{i||=e}}),o.configure(h),await loadLibraryFonts(),await document.fonts.ready;for(let e=0;e<t.frames;e++){p();const r=performance.now()+3e4;for(;o.encodeQueueSize>4||d>=8||c>33554432;){if(p(),performance.now()>r)throw new Error("WebCodecs encoder stalled");await new Promise(e=>setTimeout(e,10))}const a=t.start+e/t.fps;state.time=a,await seekVideosTo(a),await prepareFrameAssets(a),drawFrame(a);const n=new VideoFrame(els.preview,{timestamp:Math.round(1e6*e/t.fps),duration:Math.round(1e6*(e+1)/t.fps)-Math.round(1e6*e/t.fps)});try{o.encode(n,{keyFrame:e%Math.max(1,Math.round(2*t.fps))===0})}finally{n.close()}const i=(e+1)/t.frames*100;els.exportProgress.style.width=i.toFixed(1)+"%",els.exportTitle.textContent=uiText("Rendering… {percent}%",{percent:i.toFixed(0)})}await waitExportTask(o.flush(),r,3e4,"VideoEncoder flush"),await s,p(),await n.finish();const m=await l("/api/export/end?id="+a);if(a=null,!e.requestId){const e=document.createElement("a");e.href=m.src,e.download=decodeURIComponent(m.src.split("/").pop()),e.click()}}catch(t){const o=r.aborted||renderCancelled?"cancelled":t.message;e.requestId?await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e.requestId,group:e.group||0,error:o})}).catch(()=>{}):"cancelled"!==o&&alert(uiText("Export failed: ")+o)}finally{t.abort(),n?.cancel(),o&&"closed"!==o.state&&o.close(),await s,a&&await fetch(`/api/export/end?id=${a}&discard=1`,{method:"POST"}).catch(()=>{}),exportDecodeEnd(),fastExportAbort=null,state.exporting=!1,state.rendering=!1,els.exportOverlay.classList.add("hidden"),runtime.pendingSync&&syncFromServer()}}
1
+ "use strict";async function webCodecsExport(e={}){if(state.exporting)return;pause(),state.exporting=!0,state.rendering=!0,renderCancelled=!1;const t=new AbortController,r=t.signal;fastExportAbort=t;let o,a,n,i,s=Promise.resolve(),c=0,d=0;const p=()=>{if(renderCancelled||r.aborted)throw new Error("cancelled");if(i)throw i},l=async(e,t)=>{p();const o=await fetch(e,{method:"POST",body:t instanceof Blob?t:void 0===t?void 0:JSON.stringify(t),signal:r}),a=await o.json();if(!o.ok||a.error)throw new Error(a.error||"export request failed");return a};els.exportOverlay.classList.remove("hidden");const f=(t,r)=>"function"==typeof trackExportWait?trackExportWait(e.requestId,t,r):r();let w=0;try{const t=performance.now(),u=exportWindow(e);if("function"!=typeof VideoEncoder)throw new Error("WebCodecs VideoEncoder is unavailable");const h=e.bitrate??Math.max(1e5,Math.min(2e8,Math.round(project.width*project.height*u.fps*.15)));if(!Number.isSafeInteger(h)||h<1e5||h>2e8)throw new Error("bitrate must be between 100000 and 200000000 bits/s");const m=e.bitrateMode||"variable";if(!["variable","constant"].includes(m))throw new Error("bitrateMode must be variable or constant");let b;for(const e of["avc1.640034","avc1.640033","avc1.420028"]){const t={codec:e,width:project.width,height:project.height,bitrate:h,bitrateMode:m,framerate:u.fps,avc:{format:"annexb"},latencyMode:"quality"};if((await VideoEncoder.isConfigSupported(t).catch(()=>({}))).supported){b=t;break}}if(!b)throw new Error("H.264 WebCodecs encoding is unsupported at this resolution");exportDecodeBegin(r,e.decode,e.requestId);const x=await f("audio-mix",()=>renderExportAudio(u.start,u.dur,r)),g=await l(projectApi("/api/export/begin"),exportBeginOptions(e,u,x,"webcodecs"));a=g.id,x&&await l("/api/export/audio?id="+a,x),n=new BatchFrameUploader((e,t)=>l(`/api/export/frame?id=${a}&frames=${t}`,e)),o=new VideoEncoder({output:e=>{if(r.aborted||i)return;const t=new Uint8Array(e.byteLength);e.copyTo(t),c++,d+=t.length,s=s.then(()=>n.push(new Blob([t]))).catch(e=>{i||=e}).finally(()=>{c--,d-=t.length})},error:e=>{i||=e}}),o.configure(b),await loadLibraryFonts(),await document.fonts.ready;const y=performance.now();for(let e=0;e<u.frames;e++){p(),await f("encoder",async()=>{const e=performance.now()+3e4;for(;o.encodeQueueSize>4||c>=8||d>33554432;){if(p(),performance.now()>e)throw new Error("WebCodecs encoder stalled");await new Promise(e=>setTimeout(e,10))}});const t=u.start+e/u.fps;state.time=t,await f("video-seek",()=>seekVideosTo(t)),await f("frame-assets",()=>prepareFrameAssets(t)),drawFrame(t);const r=new VideoFrame(els.preview,{timestamp:Math.round(1e6*e/u.fps),duration:Math.round(1e6*(e+1)/u.fps)-Math.round(1e6*e/u.fps)});try{o.encode(r,{keyFrame:e%Math.max(1,Math.round(2*u.fps))===0})}finally{r.close()}w=e+1;const a=(e+1)/u.frames*100;els.exportProgress.style.width=a.toFixed(1)+"%",els.exportTitle.textContent=uiText("Rendering… {percent}%",{percent:a.toFixed(0)})}const E=performance.now()-y;await f("encoder",()=>waitExportTask(o.flush(),r,3e4,"VideoEncoder flush")),await s,p(),await n.finish();const v=await l("/api/export/end?id="+a,{metrics:{engine:"webcodecs",frames:u.frames,totalMs:performance.now()-t,phases:{renderMs:E},codec:b.codec,bitrate:h,bitrateMode:m,decode:e.decode||"auto"}});if(a=null,!e.requestId){const e=document.createElement("a");e.href=v.src,e.download=decodeURIComponent(v.src.split("/").pop()),e.click()}}catch(t){const o=r.aborted||renderCancelled?"cancelled":t.message;if(e.requestId){let r;try{r=project.clips.filter(e=>activeAt(e,state.time)).map(e=>e.id).slice(0,12)}catch{}await fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e.requestId,group:e.group||0,error:o,stack:t.stack,timeSec:state.time,frame:w,browserGpu:runtime.browserGpu,clipIds:r})}).catch(()=>{})}else"cancelled"!==o&&alert(uiText("Export failed: ")+o)}finally{t.abort(),n?.cancel(),o&&"closed"!==o.state&&o.close(),await s,a&&await fetch(`/api/export/end?id=${a}&discard=1`,{method:"POST"}).catch(()=>{}),exportDecodeEnd(),fastExportAbort=null,state.exporting=!1,state.rendering=!1,els.exportOverlay.classList.add("hidden"),runtime.pendingSync&&syncFromServer()}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikagent/video-editor-cli",
3
- "version": "1.5.0-preview.3",
3
+ "version": "1.5.0-preview.5",
4
4
  "description": "Local video editing, on-demand preview, and headless",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",