@tikagent/video-editor-cli 1.5.0-preview.2 → 1.5.0-preview.3
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/dist/lib/cli.js +1 -1
- package/dist/runtime/app.js +1 -1
- package/dist/runtime/delivery.js +1 -0
- package/dist/runtime/encode-profiles.js +1 -0
- package/dist/runtime/encoding-profiles.json +284 -0
- package/dist/runtime/export-decode.js +1 -0
- package/dist/runtime/export-delivery.js +1 -0
- package/dist/runtime/export-pipeline.js +1 -0
- package/dist/runtime/i18n.js +1 -1
- package/dist/runtime/index.html +49 -1
- package/dist/runtime/mp4-demux.js +1 -0
- package/dist/runtime/optimized-encoder-worker.js +1 -1
- package/dist/runtime/optimized-export.js +1 -1
- package/dist/runtime/server.js +1 -1
- package/dist/runtime/style.css +45 -0
- package/dist/runtime/timeline.js +1 -1
- package/dist/runtime/webcodecs-export.js +1 -0
- package/package.json +1 -1
package/dist/runtime/index.html
CHANGED
|
@@ -162,7 +162,13 @@
|
|
|
162
162
|
</div>
|
|
163
163
|
</div>
|
|
164
164
|
<div class="timeline">
|
|
165
|
-
<div class="
|
|
165
|
+
<div class="timeline-sidebar">
|
|
166
|
+
<div class="track-headers" id="trackHeaders"></div>
|
|
167
|
+
<div class="cover-control" id="coverControl" data-ui-setting="showCover">
|
|
168
|
+
<button type="button" class="cover-button" id="btnCover" title="Set cover" data-i18n-title="Set cover"><svg class="cover-pencil" viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="m5 15-1 5 5-1L20 8l-4-4L5 15Zm9-9 4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg><img id="coverThumbnail" alt="" hidden><span data-i18n="Cover">Cover</span></button>
|
|
169
|
+
<button type="button" class="cover-delete" id="btnDeleteCover" title="Remove cover" aria-label="Remove cover" data-i18n-title="Remove cover" data-i18n-aria-label="Remove cover" hidden>×</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
166
172
|
<div class="timeline-scroll" id="timelineScroll">
|
|
167
173
|
<canvas class="ruler" id="ruler"></canvas>
|
|
168
174
|
<div class="tracks-content" id="tracksContent">
|
|
@@ -176,6 +182,18 @@
|
|
|
176
182
|
</section>
|
|
177
183
|
</div>
|
|
178
184
|
|
|
185
|
+
<dialog class="cover-dialog" id="coverDialog" aria-labelledby="coverTitle">
|
|
186
|
+
<div class="cover-heading"><h2 id="coverTitle" data-i18n="Set cover">Set cover</h2><button class="btn" id="btnCloseCover" aria-label="Close" data-i18n-aria-label="Close">×</button></div>
|
|
187
|
+
<div class="cover-preview"><img id="coverPreview" alt="" hidden><p id="coverStatus" role="status"></p></div>
|
|
188
|
+
<div class="cover-source"><button class="btn" id="btnCoverFrame" data-i18n="Select video frame">Select video frame</button><button class="btn" id="btnCoverImage" data-i18n="Choose local image">Choose local image</button><input id="coverFile" type="file" accept="image/png,image/jpeg,image/webp,image/gif" hidden></div>
|
|
189
|
+
<div id="coverTimeline" class="cover-timeline" role="slider" tabindex="0" aria-valuemin="0" aria-valuemax="0" aria-valuenow="0" aria-label="Select video frame" data-i18n-aria-label="Select video frame">
|
|
190
|
+
<canvas id="coverRuler" class="ruler"></canvas>
|
|
191
|
+
<div id="coverFrames" class="cover-frames"></div>
|
|
192
|
+
<div id="coverPlayhead" class="playhead"></div>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="dialog-actions"><button class="btn" id="btnResetCover" data-i18n="Reset cover">Reset cover</button><span class="spacer"></span><button class="btn" id="btnCancelCover" data-i18n="Cancel">Cancel</button><button class="btn primary" id="btnSetCover" data-i18n="Use as cover">Use as cover</button></div>
|
|
195
|
+
</dialog>
|
|
196
|
+
|
|
179
197
|
<!-- ══════════ EXPORT OVERLAY ══════════ -->
|
|
180
198
|
<div class="overlay hidden" id="exportOverlay">
|
|
181
199
|
<div class="dialog export-dialog" id="exportDialog" role="dialog" aria-modal="true" aria-labelledby="exportTitle" tabindex="-1">
|
|
@@ -203,6 +221,7 @@
|
|
|
203
221
|
<button class="btn" id="btnCancelExport" hidden data-i18n="Cancel">Cancel</button>
|
|
204
222
|
</div>
|
|
205
223
|
</div>
|
|
224
|
+
<details id="exportChecks" class="export-checks" hidden><summary data-i18n="Delivery checks">Delivery checks</summary><div id="exportCheckResults"></div></details>
|
|
206
225
|
<p class="export-save-note" id="exportSaveNote" role="status" hidden></p>
|
|
207
226
|
</footer>
|
|
208
227
|
</div>
|
|
@@ -233,6 +252,29 @@
|
|
|
233
252
|
</select>
|
|
234
253
|
</label>
|
|
235
254
|
</div>
|
|
255
|
+
<div class="settings-section">
|
|
256
|
+
<h3><span data-i18n="Export encoding">Export encoding</span></h3>
|
|
257
|
+
<label class="settings-row language-row" for="setExportEngine">
|
|
258
|
+
<span class="settings-label"><span data-i18n="Default export method">Default export method</span></span>
|
|
259
|
+
<select id="setExportEngine" aria-describedby="exportEngineHint">
|
|
260
|
+
<option value="webcodecs" data-i18n="WebCodecs (browser encoding, default)">WebCodecs (browser encoding, default)</option>
|
|
261
|
+
<option value="optimized" data-i18n="Optimized (frame cache)">Optimized (frame cache)</option>
|
|
262
|
+
<option value="fast" data-i18n="Fast (ffmpeg)">Fast (ffmpeg)</option>
|
|
263
|
+
</select>
|
|
264
|
+
</label>
|
|
265
|
+
<p class="settings-hint" id="exportEngineHint" data-i18n="WebCodecs encodes H.264 in the browser without delivery presets or encoding profiles.">WebCodecs encodes H.264 in the browser without delivery presets or encoding profiles.</p>
|
|
266
|
+
<label class="settings-row language-row" for="setExportProfile">
|
|
267
|
+
<span class="settings-label"><span data-i18n="Encoding profile">Encoding profile</span></span>
|
|
268
|
+
<select id="setExportProfile">
|
|
269
|
+
<option value="delivery" data-i18n="Software H.264 (default)">Software H.264 (default)</option>
|
|
270
|
+
<option value="auto" data-i18n="Automatic hardware encoding">Automatic hardware encoding</option>
|
|
271
|
+
<option value="videotoolbox-h264">Apple Silicon H.264</option>
|
|
272
|
+
<option value="videotoolbox-hevc">Apple Silicon HEVC</option>
|
|
273
|
+
<option value="nvenc-h264">NVIDIA H.264</option>
|
|
274
|
+
<option value="nvenc-hevc">NVIDIA HEVC</option>
|
|
275
|
+
</select>
|
|
276
|
+
</label>
|
|
277
|
+
</div>
|
|
236
278
|
<div class="settings-section">
|
|
237
279
|
<h3><span data-i18n="Selection">Selection</span></h3>
|
|
238
280
|
<label class="settings-row">
|
|
@@ -246,6 +288,7 @@
|
|
|
246
288
|
<div class="settings-section">
|
|
247
289
|
<h3><span data-i18n="Visible modules">Visible modules</span></h3>
|
|
248
290
|
<p class="settings-hint"><span data-i18n="Hidden modules keep their functions and shortcuts.">Hidden modules keep their functions and shortcuts.</span></p>
|
|
291
|
+
<label class="settings-row"><input type="checkbox" data-visibility-setting="showCover"><span data-i18n="Cover selection">Cover selection</span></label>
|
|
249
292
|
<label class="settings-row"><input type="checkbox" data-visibility-setting="showAssets"><span data-i18n="Assets panel">Assets panel</span></label>
|
|
250
293
|
<label class="settings-row"><input type="checkbox" data-visibility-setting="showInspector"><span data-i18n="Inspector panel">Inspector panel</span></label>
|
|
251
294
|
<label class="settings-row"><input type="checkbox" data-visibility-setting="showNewProject"><span data-i18n="Create a project">Create a project</span></label>
|
|
@@ -313,6 +356,11 @@
|
|
|
313
356
|
<div id="timelineGestureHint" class="timeline-gesture-hint" role="status" hidden></div>
|
|
314
357
|
|
|
315
358
|
<script src="i18n.js"></script>
|
|
359
|
+
<script src="delivery.js"></script>
|
|
360
|
+
<script src="mp4-demux.js"></script>
|
|
361
|
+
<script src="export-decode.js"></script>
|
|
362
|
+
<script src="export-pipeline.js"></script>
|
|
363
|
+
<script src="webcodecs-export.js"></script>
|
|
316
364
|
<script src="optimized-export.js"></script>
|
|
317
365
|
<script src="timeline.js"></script>
|
|
318
366
|
<script src="app.js"></script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(t){const o=function(){"use strict";new Set(["moov","trak","mdia","minf","stbl","edts","dinf"]);const t=new Set(["avc1","avc3","hvc1","hev1","vp09","av01"]),o=(t,o)=>(t[o]<<24|t[o+1]<<16|t[o+2]<<8|t[o+3])>>>0,n=(t,o)=>t[o]<<24|t[o+1]<<16|t[o+2]<<8|t[o+3],e=(t,n)=>4294967296*o(t,n)+o(t,n+4),r=(t,e)=>4294967296*n(t,e)+o(t,e+4),s=(t,o)=>String.fromCharCode(t[o],t[o+1],t[o+2],t[o+3]),c=t=>t.toString(16).padStart(2,"0").toUpperCase();function i(t,n=0,r=t.length){const c=[];let i=n;for(;i+8<=r;){let n=o(t,i),d=i+8;const f=s(t,i+4);if(1===n){if(i+16>r)break;n=e(t,i+8),d=i+16}else 0===n&&(n=r-i);if(n<d-i||i+n>r)break;c.push({type:f,start:i,body:d,end:i+n}),i+=n}return c}const d=(t,o,n)=>i(t,o.body,o.end).find(t=>t.type===n)||null;function f(t,o,...n){let e=o;for(const o of n)if(e=e&&d(t,e,o),!e)return null;return e}function l(t,n,e){const r=n.subarray(e.body,e.end);if("avc1"===t||"avc3"===t)return`${t}.${c(r[1])}${c(r[2])}${c(r[3])}`;if("hvc1"===t||"hev1"===t){const n=["","A","B","C"][r[1]>>6],e=r[1]>>5&1?"H":"L",s=31&r[1];let c=o(r,2),i=0;for(let t=0;t<32;t++)i=i<<1|1&c,c>>>=1;const d=[];for(let t=6;t<12;t++)d.push(r[t]);for(;d.length&&0===d[d.length-1];)d.pop();return[t,n+s,(i>>>0).toString(16).toUpperCase(),e+r[12],...d.map(t=>t.toString(16).toUpperCase())].join(".")}if("vp09"===t){const t=r.subarray(4),o=t=>String(t).padStart(2,"0");return`vp09.${o(t[0])}.${o(t[1])}.${o(t[2]>>4)}`}if("av01"===t){const t=r[1]>>5,o=31&r[1],n=r[2]>>7?"H":"M",e=r[2]>>6&1?r[2]>>5&1?12:10:8;return`av01.${t}.${String(o).padStart(2,"0")}${n}.${String(e).padStart(2,"0")}`}return null}function u(t,r,s){const c=o=>d(t,r,o),i=c("stsz"),f=c("stz2"),l=c("stsc"),u=c("stts"),y=c("stco"),a=c("co64"),b=c("stss"),p=c("ctts");if(!i&&!f||!l||!u||!y&&!a)return null;const h=[];if(i){const n=o(t,i.body+4),e=o(t,i.body+8);for(let r=0;r<e;r++)h.push(n||o(t,i.body+12+4*r))}else{const n=t[f.body+7],e=o(t,f.body+8),r=f.body+12;for(let o=0;o<e;o++)4===n?h.push(1&o?15&t[r+(o>>1)]:t[r+(o>>1)]>>4):8===n?h.push(t[r+o]):h.push(t[r+2*o]<<8|t[r+2*o+1])}const v=h.length,m=[];if(y){const n=o(t,y.body+4);for(let e=0;e<n;e++)m.push(o(t,y.body+8+4*e))}else{const n=o(t,a.body+4);for(let o=0;o<n;o++)m.push(e(t,a.body+8+8*o))}const g=[];for(let n=0,e=o(t,l.body+4);n<e;n++)g.push({first:o(t,l.body+8+12*n)-1,per:o(t,l.body+12+12*n)});const S=new Array(v);let C=0;for(let t=0;t<g.length&&C<v;t++){const o=t+1<g.length?g[t+1].first:m.length;for(let n=g[t].first;n<o&&C<v;n++){let o=m[n];for(let n=0;n<g[t].per&&C<v;n++)S[C]=o,o+=h[C],C++}}if(C<v)return null;const w=new Array(v),$=new Array(v);let k=0,A=0;for(let n=0,e=o(t,u.body+4);n<e;n++){const e=o(t,u.body+8+8*n),r=o(t,u.body+12+8*n);for(let t=0;t<e&&A<v;t++,A++)w[A]=k,$[A]=r,k+=r}for(;A<v;A++)w[A]=k,$[A]=0;const M=new Array(v).fill(0);if(p){let e=0;for(let r=0,s=o(t,p.body+4);r<s;r++){const s=o(t,p.body+8+8*r),c=n(t,p.body+12+8*r);for(let t=0;t<s&&e<v;t++,e++)M[e]=c}}const x=new Array(v).fill(!b);if(b)for(let n=0,e=o(t,b.body+4);n<e;n++){const e=o(t,b.body+8+4*n)-1;e>=0&&e<v&&(x[e]=!0)}const z=new Array(v);for(let t=0;t<v;t++)z[t]={offset:S[t],size:h[t],key:x[t],cts:w[t]+M[t],pts:(w[t]+M[t])/s,dur:$[t]/s};return z}function y(t,s,c,i,d){const l=f(t,s,"edts","elst");if(!l)return;const u=1===t[l.body],y=o(t,l.body+4),a=u?20:12;let b=0,p=null;for(let s=0;s<y;s++){const c=l.body+8+s*a,i=u?e(t,c):o(t,c),d=u?r(t,c+8):n(t,c+4);if(-1!==d){p=d;break}b+=i}const h=b/d-(p||0)/i;if(h)for(const t of c)t.pts=t.cts/i+h}return{boxes:i,findMoov:async function(t,n){let r=0,c=null,i=-1;for(;r+8<=n;){(!c||r<i||r+16>i+c.length)&&(i=r,c=await t(r,Math.min(n,r+65536)));const d=r-i;let f=o(c,d),l=8;const u=s(c,d+4);if(1===f?(f=e(c,d+8),l=16):0===f&&(f=n-r),f<l)return null;if("moof"===u)return null;if("moov"===u)return c.length-d>=f?c.subarray(d,d+f):await t(r,r+f);r+=f}return null},parseMovie:function(n){const e=n,r={body:0,end:e.length,type:"root"},c=d(e,r,"moov")||("moov"===s(e,4)?i(e)[0]:null);if(!c)return null;if(d(e,c,"mvex"))return null;const a=d(e,c,"mvhd"),b=a?1===e[a.body]?o(e,a.body+20):o(e,a.body+12):1e3;for(const n of i(e,c.body,c.end).filter(t=>"trak"===t.type)){const r=f(e,n,"mdia","hdlr");if(!r||"vide"!==s(e,r.body+8))continue;const c=f(e,n,"mdia","mdhd"),a=f(e,n,"mdia","minf","stbl");if(!c||!a)continue;const p=d(e,n,"tkhd");if(p){const t=p.body+(1===e[p.body]?52:40);if(65536!==o(e,t)||0!==o(e,t+4)||0!==o(e,t+12)||65536!==o(e,t+16))return null}const h=1===e[c.body],v=o(e,h?c.body+20:c.body+12),m=d(e,a,"stsd");if(!m||!v)continue;const g=i(e,m.body+8,m.end)[0];if(!g||!t.has(g.type))continue;const S=g.type,C=e[g.body+24]<<8|e[g.body+25],w=e[g.body+26]<<8|e[g.body+27],$={avc1:"avcC",avc3:"avcC",hvc1:"hvcC",hev1:"hvcC",vp09:"vpcC",av01:"av1C"}[S],k=i(e,g.body+78,g.end).find(t=>t.type===$);if(!k)continue;const A=l(S,e,k);if(!A)continue;const M="vp09"===S?void 0:e.slice(k.body,k.end),x=u(e,a,v);if(!x)continue;y(e,n,x,v,b);const z=i(e,g.body+78,g.end).find(t=>"pasp"===t.type),H=z?o(e,z.body):0,U=z?o(e,z.body+4):0;return{codec:A,description:M,width:C,height:w,displayWidth:H&&U&&H!==U?Math.round(C*H/U):C,displayHeight:w,timescale:v,samples:x}}return null},codecString:l,frameAt:function(t,o,n){let e=0,r=o.length-1,s=0;for(;e<=r;){const c=e+r>>1;t[o[c]].pts<=n+1e-6?(s=c,e=c+1):r=c-1}return o[s]},presentationOrder:t=>t.map((t,o)=>o).sort((o,n)=>t[o].pts-t[n].pts),syncBefore:function(t,o){for(let n=o;n>=0;n--)if(t[n].key)return n;return 0}}}();"object"==typeof module&&module.exports?module.exports=o:t.FableCutMp4=o}("undefined"!=typeof self?self:this);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";let e,t,s=Promise.resolve();self.onmessage=({data:{id:a,bitmap:i}})=>{s=s.then(async()=>{try{e&&e.width===i.width&&e.height===i.height||(e=new OffscreenCanvas(i.width,i.height),t=e.getContext("2d")),t.drawImage(i,0,0);const s=await e.convertToBlob({type:"image/jpeg",quality
|
|
1
|
+
"use strict";let e,t,s=Promise.resolve();self.onmessage=({data:{id:a,bitmap:i,quality:o=.95}})=>{s=s.then(async()=>{try{e&&e.width===i.width&&e.height===i.height||(e=new OffscreenCanvas(i.width,i.height),t=e.getContext("2d")),t.drawImage(i,0,0);const s=await e.convertToBlob({type:"image/jpeg",quality:o});self.postMessage({id:a,blob:s})}catch(e){self.postMessage({id:a,error:e.message})}finally{i.close()}})};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";class OrderedFrameQueue{constructor(e,t=4,a=33554432){this.write=e,this.maxFrames=t,this.maxBytes=a,this.pending=[],this.bytes=0,this.peakBytes=0,this.error=null}async push(e){for(;this.pending.length&&(this.pending.length>=this.maxFrames||this.bytes+e.size>this.maxBytes);)await this.pending[0];if(this.error)throw this.error;this.bytes+=e.size,this.peakBytes=Math.max(this.peakBytes,this.bytes);const t=(this.pending.at(-1)||Promise.resolve()).then(()=>{if(this.error)throw this.error;return this.write(e)}).catch(e=>{this.error=e}).finally(()=>{this.bytes-=e.size,this.pending.shift()});this.pending.push(t)}async finish(){if(await Promise.all(this.pending),this.error)throw this.error}cancel(){this.error=this.error||new Error("cancelled")}}function cachedFrameIndex(e,t){let a=0,s=e.length;for(;a<s;){const r=a+s>>>1;e[r]<=t+1e-5?a=r+1:s=r}return a-1}function sourceBlockPlan(e,t){const a=[];for(const s of e){const e=Math.max(.1,Math.min(8,Number(s.props?.speed)||1)),r=s.start+s.duration;let i=Math.max(0,Math.ceil(s.start*t-1e-7));for(;i/t<r;){const r=i/t,o=s.in+(r-s.start)*e,n=Math.floor(o/5);a.push({clip:s,time:o,block:n,neededAt:r}),i=Math.max(i+1,Math.ceil((s.start+(5*(n+1)-s.in)/e)*t-1e-7))}}return a.sort((e,t)=>e.neededAt-t.neededAt)}function sourceReadAhead(e,t,a){return sourceBlockPlan(e,a).filter(e=>e.neededAt>t).slice(0,3)}class SourceReadAhead{constructor(e,t,{concurrency:a=3,syncPlan:s=async()=>{},key:r=e=>e}={}){this.load=e,this.requested=t,this.concurrency=a,this.syncPlan=s,this.key=r,this.plan=[],this.active=new Map,this.stopped=!1,this.sequence=0,this.readySequence=0,this.syncing=Promise.resolve(),this.signature=""}update(e,t=[]){if(this.stopped)return;const a=JSON.stringify([e.map(this.key),t]);if(a===this.signature)return;this.signature=a,this.plan=e;const s=++this.sequence;this.syncing=this.syncing.catch(()=>{}).then(()=>this.syncPlan(e,t,s)).then(()=>{s===this.sequence&&(this.readySequence=s,this.pump())}).catch(()=>{})}pump(){if(!this.stopped&&this.readySequence===this.sequence)for(const e of this.plan){if(this.active.size>=this.concurrency)break;const t=this.key(e);if(this.active.has(t)||this.requested(e))continue;const a=this.sequence,s=Promise.resolve().then(()=>this.load(e,a)).catch(()=>{}).finally(()=>{this.active.delete(t),this.pump()});this.active.set(t,s)}}async stop(){this.stopped=!0,this.plan=[],await this.syncing,await Promise.allSettled(this.active.values())}}class ExportWaitTracker{constructor(e,{now:t=()=>performance.now(),interval:a=500,threshold:s=250}={}){this.report=e,this.now=t,this.threshold=s,this.active=new Set,this.sequence=0,this.last=null,this.timer=setInterval(()=>this.flush(),a)}async track(e,t){const a={reason:e,started:this.now()};this.active.add(a);try{return await t()}finally{this.active.delete(a)}}flush(){const e=[...this.active].filter(e=>this.now()-e.started>=this.threshold).sort((e,t)=>["source-block","video-seek","source-image","frame-assets","audio-mix","encoder"].indexOf(e.reason)-["source-block","video-seek","source-image","frame-assets","audio-mix","encoder"].indexOf(t.reason)||e.started-t.started)[0];(e||null)!==this.last&&(this.last=e||null,Promise.resolve(this.report({sequence:++this.sequence,reason:e?.reason||null,elapsedMs:e?Math.max(0,this.now()-e.started):0})).catch(()=>{this.last===(e||null)&&(this.last=void 0)}))}stop(){clearInterval(this.timer),this.active.clear(),this.flush()}}class FrameReadAhead{constructor(e,t=2){this.load=e,this.concurrency=t,this.plan=[],this.active=new Map,this.stopped=!1}update(e){this.stopped||(this.plan=e,this.pump())}pump(){for(;!this.stopped&&this.active.size<this.concurrency&&this.plan.length;){const e=this.plan.shift();if(this.active.has(e.key))continue;const t=Promise.resolve().then(()=>this.load(e)).catch(()=>{}).finally(()=>{this.active.delete(e.key),this.pump()});this.active.set(e.key,t)}}async stop(){this.stopped=!0,this.plan=[],await Promise.all(this.active.values())}}class SnapshotPipeline{constructor(e,t,a=2){this.capture=e,this.write=t,this.depth=a,this.pending=[]}async push(e){for(;this.pending.length>=this.depth;)await this.drain();const t=await this.capture(e);t.result.catch(()=>{}),this.pending.push(t)}async drain(){const e=this.pending.shift();e&&await this.write(await e.result)}async finish(){for(;this.pending.length;)await this.drain()}async settle(){await Promise.allSettled(this.pending.splice(0).map(e=>e.result))}}function createSnapshotEncoder(e){let t,a=0;const s=new Map,r=()=>{t?.terminate(),t=null;for(const{reject:e}of s.values())e(new Error("snapshot encoder stopped"));s.clear(),e.removeEventListener("abort",r)};if("function"==typeof Worker&&"function"==typeof OffscreenCanvas&&"function"==typeof createImageBitmap)try{t=new Worker("optimized-encoder-worker.js"),t.onmessage=({data:e})=>{const t=s.get(e.id);t&&(s.delete(e.id),e.error?t.reject(new Error(e.error)):t.resolve(e.blob))},t.onerror=e=>{e.preventDefault(),r()},e.addEventListener("abort",r,{once:!0})}catch{r()}return{close:r,async capture(i){if(e.aborted)throw new Error("cancelled");if(!t)return{result:Promise.resolve(await new Promise(e=>i.toBlob(e,"image/jpeg",.95)))};const o=await createImageBitmap(i);if(e.aborted)throw o.close(),new Error("cancelled");const n=a++,c=(async()=>{const a=await createImageBitmap(o);return new Promise((r,i)=>{if(e.aborted||!t)return a.close(),void i(new Error("snapshot encoder stopped"));s.set(n,{resolve:r,reject:i});try{t.postMessage({id:n,bitmap:a},[a])}catch(e){a.close(),s.delete(n),i(e)}})})().catch(async t=>{if(e.aborted)throw t;r();const a=document.createElement("canvas");a.width=o.width,a.height=o.height;try{return a.getContext("2d").drawImage(o,0,0),await new Promise(e=>a.toBlob(e,"image/jpeg",.95))}finally{a.width=a.height=0}}).finally(()=>o.close());return c.catch(()=>{}),{result:c}}}}let optimizedSources=null,optimizedAbort=null,optimizedMaskCanvas=null;function optimizedMaskSource(e){return optimizedMaskCanvas||(optimizedMaskCanvas=document.createElement("canvas")),optimizedMaskCanvas.width!==e.width&&(optimizedMaskCanvas.width=e.width),optimizedMaskCanvas.height!==e.height&&(optimizedMaskCanvas.height=e.height),optimizedMaskCanvas.getContext("2d").drawImage(e,0,0),optimizedMaskCanvas}async function optimizedExport(e={}){if(state.exporting)return;pause(),state.exporting=!0,state.rendering=!0,renderCancelled=!1;const t=performance.now(),a=new AbortController;optimizedAbort=a;const s={engine:"optimized",frames:0,phases:{},samples:{},peakDecodedBytes:0,peakQueueBytes:0},r=(e,t)=>{const a=s.samples[e]||(s.samples[e]={count:0,totalMs:0,maxMs:0,histogram:Array(24).fill(0)});a.count++,a.totalMs+=t,a.maxMs=Math.max(a.maxMs,t),a.histogram[Math.min(23,Math.max(0,Math.ceil(Math.log2(Math.max(1,t)))))]++};let i,o=!1,n=!1;const c=()=>{if(i)throw i;if(renderCancelled||a.signal.aborted)throw new Error("cancelled")},d=async(e,t,s)=>{c();const r=await fetch(e,{method:s||(void 0===t?"GET":"POST"),body:void 0===t?void 0:t instanceof Blob?t:JSON.stringify(t),signal:a.signal}),i=await r.json();if(!r.ok||i.error)throw Object.assign(new Error(i.error||"Export request failed"),{status:r.status});return i};let p,l,h,m,u,f,w,g,y,k,x,v=0,b=new Set;const M=new Map,S=new Map,E=new Map,P=new Set,A=new Set,z=(e,t="")=>projectApi(`/api/export/cache/${e}`)+`&id=${p}${t}`,j=()=>{p&&fetch(z("release"),{method:"POST",keepalive:!0}).catch(()=>{}),l&&fetch(`/api/export/end?id=${l}&discard=1`,{method:"POST",keepalive:!0}).catch(()=>{})},C=()=>{a.abort(),j()},T=e=>{"Escape"===e.key&&(renderCancelled=!0,a.abort()),e.stopImmediatePropagation(),e.preventDefault()};window.addEventListener("pagehide",C),window.addEventListener("keydown",T,!0),els.exportOverlay.classList.remove("hidden"),els.exportProgress.style.width="0%",els.exportTitle.textContent=uiText("Preparing source frames…"),els.exportNote.textContent=uiText("Pre-decoding video and caching frames. You can switch tabs.");try{if(e.requestId&&"function"==typeof readBrowserGpu){try{runtime.browserGpu=readBrowserGpu()}catch{}fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e.requestId,diagnostic:!0,browserGpu:runtime.browserGpu})}).catch(()=>{})}const j=performance.now()+5e3;for(;;){c();const e=await d(projectApi("/api/project"));if(!runtime.saveTimer&&e.revision===project.revision)break;if(performance.now()>j)throw new Error("Project is not saved or changed externally. Reload/save before exporting.");await new Promise(e=>setTimeout(e,100))}u={...project},Object.assign(project,JSON.parse(JSON.stringify(project)));const C=await d("/api/export/ffmpeg");if(!C.available||!C.ffprobe)throw new Error("Optimized export needs ffmpeg and ffprobe");p=(await d(projectApi("/api/export/cache/begin"),{revision:project.revision})).id,x=new ExportWaitTracker(e=>d(z("wait"),e)),h=setInterval(async()=>{if(!o&&!a.signal.aborted){o=!0;try{if(f=await d(z("status")),"error"===f.state)throw Object.assign(new Error(f.error),{status:400})}catch(e){!n&&e.status>=400&&e.status<500&&(i=e,a.abort())}finally{o=!1}}},15e3);do{if(f=await d(z("status")),"error"===f.state)throw new Error(f.error);"ready"!==f.state&&await new Promise(e=>setTimeout(e,100))}while("ready"!==f.state);const T=new Set(f.clips),I=(e,t)=>{P.add(e.id),s.fallbacks||={},s.fallbacks[e.id]=t},O=(e,t,a=!1,s=0,r)=>{const i=Math.floor(t/5),o=e.id+":"+i,n=!a&&!A.has(o);if(n&&A.add(o),!S.has(o)||n){const t=E.get(o);t?.skipped&&E.delete(o);const n=d(z("block"),{clipId:e.id,block:i,priority:a?"prefetch":"demand",neededAt:s,sequence:r}).then(t=>(t.fallback&&!a&&I(e,t.fallback),S.get(o)===n&&E.set(o,t),t)).catch(t=>a?{skipped:t.message}:(I(e,t.message),{fallback:t.message}));S.set(o,n);for(const t of S.keys())t.startsWith(e.id+":")&&!a&&Number(t.slice(e.id.length+1))<i-1&&(S.delete(t),E.delete(t),A.delete(t))}return S.get(o)},q=(e,t=0)=>{for(const[a,s]of M){if(v+t<=134217728)break;!e.has(a)&&s.img&&(s.img.close(),v-=s.bytes,M.delete(a))}},B=async(e,t,i,o=!1)=>{const n=e.id+":"+Math.floor(t/5);if(o&&!E.has(n))return null;!o&&!E.has(n)&&s.frames>0&&(els.exportTitle.textContent=uiText("Preparing video frames…"),els.exportNote.textContent=uiText("Preparing the next part of your video. Export will continue automatically."));const d=!E.has(n)||E.get(n)?.skipped,p=performance.now(),l=o?E.get(n):await x.track("source-block",()=>O(e,t));if(!o&&d&&r("blockWait",performance.now()-p),l.fallback||l.skipped)return null;const h=cachedFrameIndex(l.times,t);if(h<0||t>l.times.at(-1)+(l.times.length>1?l.times[1]-l.times[0]:.1)+1e-5)return o||I(e,"source time outside cached coverage"),null;const m=l.key+":"+h;o||i.add(m);let u=M.get(m);if(u)M.delete(m),M.set(m,u);else{if(q(i,l.width*l.height*4),o&&v+l.width*l.height*4>134217728)return null;u={bytes:l.width*l.height*4,img:null},v+=u.bytes,s.peakDecodedBytes=Math.max(s.peakDecodedBytes,v),u.promise=(async()=>{let e;try{let t=performance.now();const s=await fetch(z("frame",`&key=${l.key}&index=${h}`),{signal:a.signal});if(!s.ok)throw new Error("Cached source frame unavailable");const i=await s.blob();return r("sourceFetch",performance.now()-t),t=performance.now(),e=await createImageBitmap(i),r("sourceDecode",performance.now()-t),c(),u.img=e,e}catch(t){throw e?.close(),v-=u.bytes,M.delete(m),t}})(),M.set(m,u)}return o?u.promise:x.track("source-image",()=>u.promise)},F=project.fps,R=Math.max(1/F,projDur()),N=Math.max(1,Math.round(R*F)),$=project.clips.filter(e=>"video"===e.kind&&isTrackEnabled(e.track)),W=sourceBlockPlan($.filter(e=>T.has(e.id)),F),D=e=>e.clip.id+":"+e.block;g=new SourceReadAhead((e,t)=>O(e.clip,e.time,!0,e.neededAt,t),e=>P.has(e.clip.id)||S.has(D(e)),{key:D,syncPlan:async(e,t,a)=>{const s=new Set([...e.map(D),...t.map(e=>e.clipId+":"+e.block)]);for(const e of S.keys())A.has(e)||s.has(e)||(S.delete(e),E.delete(e));await d(z("retain"),{sequence:a,blocks:e.map(e=>({clipId:e.clip.id,block:e.block})),current:t})}});const G=new Set,L=async e=>{const t=$.filter(t=>activeAt(t,e));g.update(W.filter(t=>t.neededAt>e&&!P.has(t.clip.id)).slice(0,3),t.filter(e=>T.has(e.id)&&!P.has(e.id)).map(t=>({clipId:t.id,block:Math.floor(mediaTimeAt(t,e)/5)})));const a=$.filter(t=>t.start+t.duration<=e&&!G.has(t.id));if(a.length){await d(z("drop"),{clipIds:a.map(e=>e.id)});for(const e of a)G.add(e.id)}optimizedSources=new Map;const s=b=new Set;for(const a of t)stealExportVideo(a,e);return releaseFinishedExportVideos(e),await Promise.all(t.map(async t=>{const a=mediaTimeAt(t,e);if(T.has(t.id)&&!P.has(t.id))try{const e=await B(t,a,s);if(e)return void optimizedSources.set(t.id,e)}catch(e){c(),I(t,e.message)}await x.track("video-seek",()=>prepareVideoFrame(t,acquireExportVideo(t,e),a))})),q(s),s};await L(0),s.phases.prepareMs=performance.now()-t,els.exportTitle.textContent=uiText("Mixing audio…");let J=performance.now();const Q=await x.track("audio-mix",()=>renderAudioMix(R,a.signal));c(),s.phases.audioMs=performance.now()-J;const V=e.name||project.name||"export";l=(await d(projectApi("/api/export/begin"),{fps:F,name:V,requestId:e.requestId,totalFrames:N,engine:"optimized",cacheId:p})).id,Q&&await d(`/api/export/audio?id=${l}`,Q),await document.fonts.ready,project.clips.some(e=>isTrackEnabled(e.track)&&e.props?.bgRemove)&&await ensureBgSeg(),m=new OrderedFrameQueue(async e=>{const t=performance.now();await d(`/api/export/frame?id=${l}`,e),r("upload",performance.now()-t)}),w=createSnapshotEncoder(a.signal);const U=Math.max(1,Math.min(2,Math.floor(67108864/(project.width*project.height*4))));s.snapshotDepth=U,k=new SnapshotPipeline(async e=>{const t=performance.now(),a=await w.capture(e);return r("snapshot",performance.now()-t),a.result=a.result.then(e=>(r("jpeg",performance.now()-t),e)),a},async e=>{if(!e?.size)throw new Error("Canvas returned an empty frame");const t=performance.now();await x.track("encoder",()=>m.push(e)),r("queueWait",performance.now()-t)},U),y=new FrameReadAhead(async({clip:e,time:t})=>{if(P.has(e.id)||G.has(e.id))return;const a=performance.now();await B(e,t,b,!0),r("prefetch",performance.now()-a)});const Y=performance.now();for(let e=0;e<N;e++){c();const t=e/F;state.time=t,J=performance.now();const a=await L(t);r("source",performance.now()-J),$.some(a=>a.start>(e-1)/F&&a.start<=t)&&r("cutSourceWait",performance.now()-J),J=performance.now(),await x.track("frame-assets",()=>prepareFrameAssets(t)),r("assets",performance.now()-J),J=performance.now(),drawFrame(t),r("composite",performance.now()-J),J=performance.now();const i=[];for(let t=1;t<=4&&e+t<N;t++){const a=(e+t)/F;for(const s of $)activeAt(s,a)&&T.has(s.id)&&!P.has(s.id)&&i.push({key:`${s.id}:${e+t}`,clip:s,time:mediaTimeAt(s,a)})}y.update(i),await x.track("encoder",()=>k.push(els.preview)),r("pipeline",performance.now()-J),s.frames++,q(a);const o=(e+1)/N*100;els.exportProgress.style.width=o.toFixed(1)+"%",els.exportTitle.textContent=uiText("Rendering… {percent}%",{percent:o.toFixed(0)}),els.exportNote.textContent=uiText("Cached video frames · {count} compatibility fallbacks · You can switch tabs.",{count:new Set([...Object.keys(f.stats.fallbacks),...P]).size})}await x.track("encoder",async()=>{await k.finish(),await m.finish()}),await y.stop(),s.peakQueueBytes=m.peakBytes,s.phases.renderMs=performance.now()-Y,f=await d(z("status")),s.cache=f.stats;for(const e of Object.values(s.samples)){let t=0;e.meanMs=e.totalMs/e.count;for(let a=0;a<e.histogram.length;a++)if(t+=e.histogram[a],t>=.95*e.count){e.p95UpperMs=2**a;break}}els.exportTitle.textContent=uiText("Encoding…"),J=performance.now(),s.totalMs=performance.now()-t,n=!0;const K=await d(`/api/export/end?id=${l}`,{metrics:s});if(l=null,s.phases.finalizeMs=performance.now()-J,s.totalMs=performance.now()-t,s.fps=N/(s.totalMs/1e3),!e.requestId){const e=document.createElement("a");e.href=K.src,e.download=decodeURIComponent(K.src.split("/").pop()),e.click(),toast(uiText("Optimized export complete: {seconds}s · {hits} cache hits",{seconds:(s.totalMs/1e3).toFixed(1),hits:s.cache.hits}))}console.info("Optimized export",s)}catch(t){m?.cancel(),a.abort(),await(m?.finish().catch(()=>{}));const r=i?.message||(renderCancelled||"AbortError"===t.name?"cancelled":t.message);if(e.requestId){let a;try{a=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,error:r,stack:t.stack,frame:s.frames,timeSec:state.time,browserGpu:runtime.browserGpu,clipIds:a})}).catch(()=>{})}else"cancelled"!==r&&alert(uiText("Optimized export failed: ")+r)}finally{clearInterval(h),x?.stop(),j(),w?.close(),a.abort(),await(g?.stop()),await(y?.stop()),await(k?.settle()),optimizedAbort=null,optimizedSources=null;for(const e of M.values())e.img&&e.img.close();M.clear(),optimizedMaskCanvas&&(optimizedMaskCanvas.width=0,optimizedMaskCanvas.height=0,optimizedMaskCanvas=null),u&&Object.assign(project,u),state.exporting=!1,state.rendering=!1,window.removeEventListener("pagehide",C),window.removeEventListener("keydown",T,!0),els.exportOverlay.classList.add("hidden"),els.exportNote.textContent=uiText("Rendering your sequence in real time. Keep this tab focused."),runtime.pendingSync&&syncFromServer()}}"undefined"!=typeof module&&(module.exports={ExportWaitTracker,OrderedFrameQueue,cachedFrameIndex,sourceBlockPlan,sourceReadAhead,SourceReadAhead,FrameReadAhead,SnapshotPipeline,createSnapshotEncoder});
|
|
1
|
+
"use strict";class OrderedFrameQueue{constructor(e,t=4,a=33554432){this.write=e,this.maxFrames=t,this.maxBytes=a,this.pending=[],this.bytes=0,this.peakBytes=0,this.error=null}async push(e){for(;this.pending.length&&(this.pending.length>=this.maxFrames||this.bytes+e.size>this.maxBytes);)await this.pending[0];if(this.error)throw this.error;this.bytes+=e.size,this.peakBytes=Math.max(this.peakBytes,this.bytes);const t=(this.pending.at(-1)||Promise.resolve()).then(()=>{if(this.error)throw this.error;return this.write(e)}).catch(e=>{this.error=e}).finally(()=>{this.bytes-=e.size,this.pending.shift()});this.pending.push(t)}async finish(){if(await Promise.all(this.pending),this.error)throw this.error}cancel(){this.error=this.error||new Error("cancelled")}}function cachedFrameIndex(e,t){let a=0,s=e.length;for(;a<s;){const i=a+s>>>1;e[i]<=t+1e-5?a=i+1:s=i}return a-1}function sourceBlockPlan(e,t){const a=[];for(const s of e){const e=Math.max(.1,Math.min(8,Number(s.props?.speed)||1)),i=s.start+s.duration;let r=Math.max(0,Math.ceil(s.start*t-1e-7));for(;r/t<i;){const i=r/t,o=s.in+(i-s.start)*e,n=Math.floor(o/5);a.push({clip:s,time:o,block:n,neededAt:i}),r=Math.max(r+1,Math.ceil((s.start+(5*(n+1)-s.in)/e)*t-1e-7))}}return a.sort((e,t)=>e.neededAt-t.neededAt)}function sourceReadAhead(e,t,a){return sourceBlockPlan(e,a).filter(e=>e.neededAt>t).slice(0,3)}class SourceReadAhead{constructor(e,t,{concurrency:a=3,syncPlan:s=async()=>{},key:i=e=>e}={}){this.load=e,this.requested=t,this.concurrency=a,this.syncPlan=s,this.key=i,this.plan=[],this.active=new Map,this.stopped=!1,this.sequence=0,this.readySequence=0,this.syncing=Promise.resolve(),this.signature=""}update(e,t=[]){if(this.stopped)return;const a=JSON.stringify([e.map(this.key),t]);if(a===this.signature)return;this.signature=a,this.plan=e;const s=++this.sequence;this.syncing=this.syncing.catch(()=>{}).then(()=>this.syncPlan(e,t,s)).then(()=>{s===this.sequence&&(this.readySequence=s,this.pump())}).catch(()=>{})}pump(){if(!this.stopped&&this.readySequence===this.sequence)for(const e of this.plan){if(this.active.size>=this.concurrency)break;const t=this.key(e);if(this.active.has(t)||this.requested(e))continue;const a=this.sequence,s=Promise.resolve().then(()=>this.load(e,a)).catch(()=>{}).finally(()=>{this.active.delete(t),this.pump()});this.active.set(t,s)}}async stop(){this.stopped=!0,this.plan=[],await this.syncing,await Promise.allSettled(this.active.values())}}class ExportWaitTracker{constructor(e,{now:t=()=>performance.now(),interval:a=500,threshold:s=250}={}){this.report=e,this.now=t,this.threshold=s,this.active=new Set,this.sequence=0,this.last=null,this.timer=setInterval(()=>this.flush(),a)}async track(e,t){const a={reason:e,started:this.now()};this.active.add(a);try{return await t()}finally{this.active.delete(a)}}flush(){const e=[...this.active].filter(e=>this.now()-e.started>=this.threshold).sort((e,t)=>["source-block","video-seek","source-image","frame-assets","audio-mix","encoder"].indexOf(e.reason)-["source-block","video-seek","source-image","frame-assets","audio-mix","encoder"].indexOf(t.reason)||e.started-t.started)[0];(e||null)!==this.last&&(this.last=e||null,Promise.resolve(this.report({sequence:++this.sequence,reason:e?.reason||null,elapsedMs:e?Math.max(0,this.now()-e.started):0})).catch(()=>{this.last===(e||null)&&(this.last=void 0)}))}stop(){clearInterval(this.timer),this.active.clear(),this.flush()}}class FrameReadAhead{constructor(e,t=2){this.load=e,this.concurrency=t,this.plan=[],this.active=new Map,this.stopped=!1}update(e){this.stopped||(this.plan=e,this.pump())}pump(){for(;!this.stopped&&this.active.size<this.concurrency&&this.plan.length;){const e=this.plan.shift();if(this.active.has(e.key))continue;const t=Promise.resolve().then(()=>this.load(e)).catch(()=>{}).finally(()=>{this.active.delete(e.key),this.pump()});this.active.set(e.key,t)}}async stop(){this.stopped=!0,this.plan=[],await Promise.all(this.active.values())}}class SnapshotPipeline{constructor(e,t,a=2){this.capture=e,this.write=t,this.depth=a,this.pending=[]}async push(e){for(;this.pending.length>=this.depth;)await this.drain();const t=await this.capture(e);t.result.catch(()=>{}),this.pending.push(t)}async drain(){const e=this.pending.shift();e&&await this.write(await e.result)}async finish(){for(;this.pending.length;)await this.drain()}async settle(){await Promise.allSettled(this.pending.splice(0).map(e=>e.result))}}function createSnapshotEncoder(e,t=.95){let a,s=0;const i=new Map,r=()=>{a?.terminate(),a=null;for(const{reject:e}of i.values())e(new Error("snapshot encoder stopped"));i.clear(),e.removeEventListener("abort",r)};if("function"==typeof Worker&&"function"==typeof OffscreenCanvas&&"function"==typeof createImageBitmap)try{a=new Worker("optimized-encoder-worker.js"),a.onmessage=({data:e})=>{const t=i.get(e.id);t&&(i.delete(e.id),e.error?t.reject(new Error(e.error)):t.resolve(e.blob))},a.onerror=e=>{e.preventDefault(),r()},e.addEventListener("abort",r,{once:!0})}catch{r()}return{close:r,async capture(o){if(e.aborted)throw new Error("cancelled");if(!a)return{result:Promise.resolve(await new Promise(e=>o.toBlob(e,"image/jpeg",t)))};const n=await createImageBitmap(o);if(e.aborted)throw n.close(),new Error("cancelled");const c=s++,d=(async()=>{const s=await createImageBitmap(n);return new Promise((r,o)=>{if(e.aborted||!a)return s.close(),void o(new Error("snapshot encoder stopped"));i.set(c,{resolve:r,reject:o});try{a.postMessage({id:c,bitmap:s,quality:t},[s])}catch(e){s.close(),i.delete(c),o(e)}})})().catch(async a=>{if(e.aborted)throw a;r();const s=document.createElement("canvas");s.width=n.width,s.height=n.height;try{return s.getContext("2d").drawImage(n,0,0),await new Promise(e=>s.toBlob(e,"image/jpeg",t))}finally{s.width=s.height=0}}).finally(()=>n.close());return d.catch(()=>{}),{result:d}}}}let optimizedSources=null,optimizedAbort=null,optimizedMaskCanvas=null;function optimizedMaskSource(e){return optimizedMaskCanvas||(optimizedMaskCanvas=document.createElement("canvas")),optimizedMaskCanvas.width!==e.width&&(optimizedMaskCanvas.width=e.width),optimizedMaskCanvas.height!==e.height&&(optimizedMaskCanvas.height=e.height),optimizedMaskCanvas.getContext("2d").drawImage(e,0,0),optimizedMaskCanvas}async function optimizedExport(e={}){if(state.exporting)return;pause(),state.exporting=!0,state.rendering=!0,renderCancelled=!1;const t=performance.now(),a=new AbortController;optimizedAbort=a;const s={engine:"optimized",frames:0,phases:{},samples:{},peakDecodedBytes:0,peakQueueBytes:0},i=(e,t)=>{const a=s.samples[e]||(s.samples[e]={count:0,totalMs:0,maxMs:0,histogram:Array(24).fill(0)});a.count++,a.totalMs+=t,a.maxMs=Math.max(a.maxMs,t),a.histogram[Math.min(23,Math.max(0,Math.ceil(Math.log2(Math.max(1,t)))))]++};let r,o=!1,n=!1;const c=()=>{if(r)throw r;if(renderCancelled||a.signal.aborted)throw new Error("cancelled")},d=async(e,t,s)=>{c();const i=await fetch(e,{method:s||(void 0===t?"GET":"POST"),body:void 0===t?void 0:t instanceof Blob?t:JSON.stringify(t),signal:a.signal}),r=await i.json();if(!i.ok||r.error)throw Object.assign(new Error(r.error||"Export request failed"),{status:i.status});return r};let p,l,h,m,u,f,w,g,y,k,x,v=0,b=new Set;const M=new Map,E=new Map,S=new Map,P=new Set,A=new Set,z=(e,t="")=>projectApi(`/api/export/cache/${e}`)+`&id=${p}${t}`,T=()=>{p&&fetch(z("release"),{method:"POST",keepalive:!0}).catch(()=>{}),l&&fetch(`/api/export/end?id=${l}&discard=1`,{method:"POST",keepalive:!0}).catch(()=>{})},j=()=>{a.abort(),T()},C=e=>{"Escape"===e.key&&(renderCancelled=!0,a.abort()),e.stopImmediatePropagation(),e.preventDefault()};window.addEventListener("pagehide",j),window.addEventListener("keydown",C,!0),els.exportOverlay.classList.remove("hidden"),els.exportProgress.style.width="0%",els.exportTitle.textContent=uiText("Preparing source frames…"),els.exportNote.textContent=uiText("Pre-decoding video and caching frames. You can switch tabs.");try{if(e.requestId&&"function"==typeof readBrowserGpu){try{runtime.browserGpu=readBrowserGpu()}catch{}fetch(projectApi("/api/export/report"),{method:"POST",body:JSON.stringify({requestId:e.requestId,diagnostic:!0,browserGpu:runtime.browserGpu})}).catch(()=>{})}const T=performance.now()+5e3;for(;;){c();const e=await d(projectApi("/api/project"));if(!runtime.saveTimer&&e.revision===project.revision)break;if(performance.now()>T)throw new Error("Project is not saved or changed externally. Reload/save before exporting.");await new Promise(e=>setTimeout(e,100))}u={...project},Object.assign(project,JSON.parse(JSON.stringify(project)));const j=await d("/api/export/ffmpeg");if(!j.available||!j.ffprobe)throw new Error("Optimized export needs ffmpeg and ffprobe");p=(await d(projectApi("/api/export/cache/begin"),{revision:project.revision})).id,x=new ExportWaitTracker(e=>d(z("wait"),e)),h=setInterval(async()=>{if(!o&&!a.signal.aborted){o=!0;try{if(f=await d(z("status")),"error"===f.state)throw Object.assign(new Error(f.error),{status:400})}catch(e){!n&&e.status>=400&&e.status<500&&(r=e,a.abort())}finally{o=!1}}},15e3);do{if(f=await d(z("status")),"error"===f.state)throw new Error(f.error);"ready"!==f.state&&await new Promise(e=>setTimeout(e,100))}while("ready"!==f.state);const C=new Set("webcodecs"===e.decode||"video"===e.decode?[]:f.clips);exportDecodeBegin(a.signal,e.decode);const I=(e,t)=>{P.add(e.id),s.fallbacks||={},s.fallbacks[e.id]=t},O=(e,t,a=!1,s=0,i)=>{const r=Math.floor(t/5),o=e.id+":"+r,n=!a&&!A.has(o);if(n&&A.add(o),!E.has(o)||n){const t=S.get(o);t?.skipped&&S.delete(o);const n=d(z("block"),{clipId:e.id,block:r,priority:a?"prefetch":"demand",neededAt:s,sequence:i}).then(t=>(t.fallback&&!a&&I(e,t.fallback),E.get(o)===n&&S.set(o,t),t)).catch(t=>a?{skipped:t.message}:(I(e,t.message),{fallback:t.message}));E.set(o,n);for(const t of E.keys())t.startsWith(e.id+":")&&!a&&Number(t.slice(e.id.length+1))<r-1&&(E.delete(t),S.delete(t),A.delete(t))}return E.get(o)},B=(e,t=0)=>{for(const[a,s]of M){if(v+t<=134217728)break;!e.has(a)&&s.img&&(s.img.close(),v-=s.bytes,M.delete(a))}},q=async(e,t,r,o=!1)=>{const n=e.id+":"+Math.floor(t/5);if(o&&!S.has(n))return null;!o&&!S.has(n)&&s.frames>0&&(els.exportTitle.textContent=uiText("Preparing video frames…"),els.exportNote.textContent=uiText("Preparing the next part of your video. Export will continue automatically."));const d=!S.has(n)||S.get(n)?.skipped,p=performance.now(),l=o?S.get(n):await x.track("source-block",()=>O(e,t));if(!o&&d&&i("blockWait",performance.now()-p),l.fallback||l.skipped)return null;const h=cachedFrameIndex(l.times,t);if(h<0||t>l.times.at(-1)+(l.times.length>1?l.times[1]-l.times[0]:.1)+1e-5)return o||I(e,"source time outside cached coverage"),null;const m=l.key+":"+h;o||r.add(m);let u=M.get(m);if(u)M.delete(m),M.set(m,u);else{if(B(r,l.width*l.height*4),o&&v+l.width*l.height*4>134217728)return null;u={bytes:l.width*l.height*4,img:null},v+=u.bytes,s.peakDecodedBytes=Math.max(s.peakDecodedBytes,v),u.promise=(async()=>{let e;try{let t=performance.now();const s=await fetch(z("frame",`&key=${l.key}&index=${h}`),{signal:a.signal});if(!s.ok)throw new Error("Cached source frame unavailable");const r=await s.blob();return i("sourceFetch",performance.now()-t),t=performance.now(),e=await createImageBitmap(r),i("sourceDecode",performance.now()-t),c(),u.img=e,e}catch(t){throw e?.close(),v-=u.bytes,M.delete(m),t}})(),M.set(m,u)}return o?u.promise:x.track("source-image",()=>u.promise)},F=e.fps||project.fps,R=Math.max(1/F,projDur()),N=(Math.max(1,Math.round(R*F)),project.clips.filter(e=>"video"===e.kind&&isTrackEnabled(e.track))),$=sourceBlockPlan(N.filter(e=>C.has(e.id)),F),D=e=>e.clip.id+":"+e.block;g=new SourceReadAhead((e,t)=>O(e.clip,e.time,!0,e.neededAt,t),e=>P.has(e.clip.id)||E.has(D(e)),{key:D,syncPlan:async(e,t,a)=>{const s=new Set([...e.map(D),...t.map(e=>e.clipId+":"+e.block)]);for(const e of E.keys())A.has(e)||s.has(e)||(E.delete(e),S.delete(e));await d(z("retain"),{sequence:a,blocks:e.map(e=>({clipId:e.clip.id,block:e.block})),current:t})}});const W=new Set,L=async e=>{const t=N.filter(t=>activeAt(t,e));g.update($.filter(t=>t.neededAt>e&&!P.has(t.clip.id)).slice(0,3),t.filter(e=>C.has(e.id)&&!P.has(e.id)).map(t=>({clipId:t.id,block:Math.floor(mediaTimeAt(t,e)/5)})));const a=N.filter(t=>t.start+t.duration<=e&&!W.has(t.id));if(a.length){await d(z("drop"),{clipIds:a.map(e=>e.id)});for(const e of a)W.add(e.id)}optimizedSources=new Map;const s=b=new Set;for(const a of t)stealExportVideo(a,e);return releaseFinishedExportVideos(e),await Promise.all(t.map(async t=>{const a=mediaTimeAt(t,e);if(C.has(t.id)&&!P.has(t.id))try{const e=await q(t,a,s);if(e)return void optimizedSources.set(t.id,e)}catch(e){c(),I(t,e.message)}await x.track("video-seek",async()=>{await decodeExportVideo(t,e)||await prepareVideoFrame(t,acquireExportVideo(t,e),a)})})),B(s),s},G=exportWindow(e);await L(G.start),s.phases.prepareMs=performance.now()-t,els.exportTitle.textContent=uiText("Mixing audio…");let J=performance.now();const Q=await x.track("audio-mix",()=>renderExportAudio(G.start,G.dur,a.signal));c(),s.phases.audioMs=performance.now()-J,e.name||project.name;const V=await d(projectApi("/api/export/begin"),{...exportBeginOptions(e,G,Q,"optimized"),cacheId:p});l=V.id,Q&&await d(`/api/export/audio?id=${l}`,Q),await loadLibraryFonts(),await document.fonts.ready,project.clips.some(e=>isTrackEnabled(e.track)&&e.props?.bgRemove)&&await ensureBgSeg();const U=createExportPipeline(V,a.signal,async(e,t)=>{const a=performance.now();await d(`/api/export/frame?id=${l}&frames=${t}`,e),i("upload",performance.now()-a)},i);({queue:m,encoder:w,pipeline:k}=U),s.snapshotDepth=U.depth,y=new FrameReadAhead(async({clip:e,time:t})=>{if(P.has(e.id)||W.has(e.id))return;const a=performance.now();await q(e,t,b,!0),i("prefetch",performance.now()-a)});const Y=performance.now();for(let e=0;e<G.frames;e++){c();const t=G.start+e/G.fps;state.time=t,J=performance.now();const a=await L(t);i("source",performance.now()-J),N.some(a=>a.start>G.start+(e-1)/G.fps&&a.start<=t)&&i("cutSourceWait",performance.now()-J),J=performance.now(),await x.track("frame-assets",()=>prepareFrameAssets(t)),i("assets",performance.now()-J),J=performance.now(),drawFrame(t),i("composite",performance.now()-J),J=performance.now();const r=[];for(let t=1;t<=4&&e+t<G.frames;t++){const a=G.start+(e+t)/G.fps;for(const s of N)activeAt(s,a)&&C.has(s.id)&&!P.has(s.id)&&r.push({key:`${s.id}:${e+t}`,clip:s,time:mediaTimeAt(s,a)})}y.update(r),await x.track("encoder",()=>k.push(els.preview)),i("pipeline",performance.now()-J),s.frames++,B(a);const o=(e+1)/G.frames*100;els.exportProgress.style.width=o.toFixed(1)+"%",els.exportTitle.textContent=uiText("Rendering… {percent}%",{percent:o.toFixed(0)}),els.exportNote.textContent=uiText("Cached video frames · {count} compatibility fallbacks · You can switch tabs.",{count:new Set([...Object.keys(f.stats.fallbacks),...P]).size})}await x.track("encoder",async()=>{await k.finish(),await m.finish()}),await y.stop(),s.peakQueueBytes=m.peakBytes,s.phases.renderMs=performance.now()-Y,f=await d(z("status")),s.cache=f.stats;for(const e of Object.values(s.samples)){let t=0;e.meanMs=e.totalMs/e.count;for(let a=0;a<e.histogram.length;a++)if(t+=e.histogram[a],t>=.95*e.count){e.p95UpperMs=2**a;break}}els.exportTitle.textContent=uiText("Encoding…"),J=performance.now(),s.totalMs=performance.now()-t,n=!0;const K=await d(`/api/export/end?id=${l}`,{metrics:s});if(l=null,s.phases.finalizeMs=performance.now()-J,s.totalMs=performance.now()-t,s.fps=G.frames/(s.totalMs/1e3),!e.requestId){const e=document.createElement("a");e.href=K.src,e.download=decodeURIComponent(K.src.split("/").pop()),e.click(),toast(uiText("Optimized export complete: {seconds}s · {hits} cache hits",{seconds:(s.totalMs/1e3).toFixed(1),hits:s.cache.hits}))}console.info("Optimized export",s)}catch(t){m?.cancel(),a.abort(),await(m?.finish().catch(()=>{}));const i=r?.message||(renderCancelled||"AbortError"===t.name?"cancelled":t.message);if(e.requestId){let a;try{a=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:i,stack:t.stack,frame:s.frames,timeSec:state.time,browserGpu:runtime.browserGpu,clipIds:a})}).catch(()=>{})}else"cancelled"!==i&&alert(uiText("Optimized export failed: ")+i)}finally{clearInterval(h),x?.stop(),T(),w?.close(),a.abort(),await(g?.stop()),await(y?.stop()),await(k?.settle()),optimizedAbort=null,optimizedSources=null,exportDecodeEnd();for(const e of M.values())e.img&&e.img.close();M.clear(),optimizedMaskCanvas&&(optimizedMaskCanvas.width=0,optimizedMaskCanvas.height=0,optimizedMaskCanvas=null),u&&Object.assign(project,u),state.exporting=!1,state.rendering=!1,window.removeEventListener("pagehide",j),window.removeEventListener("keydown",C,!0),els.exportOverlay.classList.add("hidden"),els.exportNote.textContent=uiText("Rendering your sequence in real time. Keep this tab focused."),runtime.pendingSync&&syncFromServer()}}"undefined"!=typeof module&&(module.exports={ExportWaitTracker,OrderedFrameQueue,cachedFrameIndex,sourceBlockPlan,sourceReadAhead,SourceReadAhead,FrameReadAhead,SnapshotPipeline,createSnapshotEncoder});
|
package/dist/runtime/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const e=require("http"),r=require("fs"),t=require("path"),o=require("os"),{spawn:i,fork:a,spawnSync:s}=require("./cli/lib/processes"),{writeFrame:n,stopProcess:c,run:d}=require("./export-encoder"),{createExportSaver:p,resolveExportSource:l,validVideoName:m}=require("./export-save"),f=p(),{analyze:u}=require("./analyze"),h=require("./project-store"),{ExportCache:g}=require("./export-cache"),{Waveforms:w}=require("./waveform"),{ExportAudio:y}=require("./export-audio"),{pipeline:S}=require("node:stream/promises"),{APP_DIR:v,DATA_DIR:b,PROJECTS_DIR:x,LIBRARY_DIR:j,LIBRARY_SUBDIRS:I,DEFAULT_PROJECT_ID:E,normalizeProjectId:P,projectPaths:T,listProjects:O,ensureDirs:D,projectRoots:k}=require("./paths"),q=v,C=new y({libraryDir:j}),F=process.env.HOST||"127.0.0.1",N=new Set(["localhost","127.0.0.1","[::1]","::1",F.toLowerCase()]);for(const e of(process.env.FABLECUT_ALLOWED_HOSTS||"").split(","))e.trim()&&N.add(e.trim().toLowerCase());function R(e){if(!e)return!1;const r=e.replace(/^(\[[^\]]*\]|[^:]+)(:\d+)?$/,"$1").toLowerCase();return N.has(r)}let $=!1;try{$=0===s("ffmpeg",["-version"],{stdio:"ignore"}).status}catch{}let z=!1;try{z=0===s("ffprobe",["-version"],{stdio:"ignore"}).status}catch{}const _=new g({libraryDir:j}),A=new w({libraryDir:j}),U={".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"};D();const G=new Set;function J(e,r="change"){for(const t of G)e&&t.projectId!==e||t.res.write(`data: ${r}\n\n`)}const L=new Map;function H(e){clearTimeout(L.get(e)),L.set(e,setTimeout(()=>J(e),150))}const M=new Map;function W(e){const r=M.get(e);if(M.delete(e),r)for(const e of r.watchers)e.close()}function B(e){const t=T(e),o=M.get(e);if(o?.dir===t.dir)return;W(e);const i=[],a={dir:t.dir,watchers:i},s=(t,o)=>{try{const s=r.watch(t,o);s.on("error",()=>{M.get(e)===a&&W(e),H(e)}),i.push(s)}catch{}};s(t.dir,(r,t)=>{"project.json"===t&&H(e)}),s(t.mediaDir,()=>H(e)),i.length&&M.set(e,a)}for(const e of O())B(e.id);let V=null;for(const e of[x,k().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]:[...M.keys()];for(const e of o){W(e);try{B(e)}catch{}H(e)}clearTimeout(V),V=setTimeout(()=>J(null,"projects"),150)}).on("error",()=>{for(const[r,o]of M)t.dirname(o.dir)===e&&W(r);J(null,"projects")})}catch{}for(const e of I)try{r.watch(t.join(j,e),()=>J(null))}catch{}function Y(e,r,t){const o=JSON.stringify(t);e.writeHead(r,{"Content-Type":"application/json","Cache-Control":"no-store"}),e.end(o)}function K(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 X(e,o){let i=t.join(e,o);const a=t.extname(o),s=t.basename(o,a);let n=1;for(;r.existsSync(i);)i=t.join(e,`${s}_${n++}${a}`);return i}function Q(e){const r=P(e.searchParams.get("project")||E),t=h.context(r);return B(r),t}function Z(e,r){return"/projects/"+encodeURIComponent(e)+"/media/"+encodeURIComponent(r)}const ee=new Set([".mp4",".mov",".m4v"]),re=new Map,te=new Map,oe=new Map;function ie(){const e=new Set;for(const[r,t]of oe){const o=te.get(r)?.state;o&&["complete","error","cancelled"].includes(o)||e.add(t.projectId)}for(const r of re.values())r.projectId&&e.add(r.projectId);return[...e]}function ae(e){const r=oe.get(e);if(!r||!r.cleanupDone||r.child&&!r.childClosed)return;clearTimeout(r.timer),oe.delete(e);const t=te.get(e);t&&te.set(e,{...t,workerDone:!0}),queueMicrotask(se)}function se(){let e=[...oe.values()].filter(e=>e.child).length;for(const[r,t]of oe){if(e>=2)break;t.child||t.stopping||(ne(r,t),t.child&&e++)}}function ne(e,r){const{projectId:o,options:i}=r;try{const s=a(t.join(q,"export-worker.js"),[],{stdio:["ignore","ignore","pipe","ipc"]});r.child=s,le(e,{state:"starting",projectId:o});let n="";s.stderr.on("data",e=>{n=(n+e).slice(-4e3),r.stderrTail=n}),s.on("message",r=>{const t=te.get(e);(["starting","preparing-browser","preparing-media"].includes(t?.state)||"error"===r.state)&&le(e,{...r,projectId:o})}),s.on("error",r=>le(e,{state:"error",projectId:o,error:r.message})),s.once("close",()=>{r.childClosed=!0,le(e,{state:"error",projectId:o,error:"Export worker exited: "+n.slice(-800)}),ae(e)}),r.timer=setTimeout(()=>le(e,{state:"error",projectId:o,error:"Export timed out"}),1e3*i.timeoutSeconds);const c=he.address(),d="0.0.0.0"===F?"127.0.0.1":"::"===F?"[::1]":F.includes(":")&&!F.startsWith("[")?`[${F}]`:F;s.send({id:e,projectId:o,engine:i.engine,name:i.name,browser:i.browser,url:`http://${d}:${c.port}/`})}catch(r){le(e,{state:"error",projectId:o,error:r.message})}}function ce(e,r){const t=e.searchParams.get("exportJob");if(!t)return;const o=oe.get(t);if(!o||o.stopping||o.projectId!==r.id)throw new Error("no such export job");return o.snapshot}function de(){if(!de.monitor)try{const e=require("./cli/lib/telemetry");de.monitor=e.createExportMonitor({enrich(e){const r=oe.get(e),t=te.get(e);let o,i,a=t?.wait;for(const e of r?.cacheIds||[]){const r=_.sessions.get(e);r&&(o=r.stats,i=r.activity||i,r.wait&&(a={reason:r.wait.reason,elapsedMs:Math.max(0,Date.now()-r.wait.since)}))}return{stats:o,activity:i,wait:a,stderr:r?.stderrTail,detail:t?.detail,browser:t?.browser}}})}catch{de.monitor={stop(){},upsert(){},note(){},cancel:()=>!1,fail(){},complete(){},scan(){}}}return de.monitor}function pe(e,o){let i=0;const a=`/projects/${e.id}/media/`;for(const s of o?.media||[])try{if("string"!=typeof s?.src||!s.src.startsWith(a))continue;const o=decodeURIComponent(s.src.slice(a.length));if(!o||o.includes("..")||/[/\\]/.test(o))continue;i+=r.statSync(t.join(e.mediaDir,o)).size}catch{}return i}function le(e,r){if(!e||!/^[a-f0-9]{32}$/.test(e))return;const t=te.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})`}),te.set(e,{...t,...r,updated:Date.now()});try{!function(e,r,t){const o=de();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=oe.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(_.release(e,r.projectId));for(const[r,o]of re)o.requestId===e&&t.push(fe(r));r.child&&!r.childClosed&&r.child.kill();const o=()=>{r.cleanupDone=!0,ae(e)};t.some(Boolean)?Promise.allSettled(t).then(o):o()}(e),te.size>100){const e=[...te].sort((e,r)=>e[1].updated-r[1].updated).slice(0,20);for(const[r]of e)oe.has(r)||te.delete(r)}}}function me(e,a,s,n,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"),f=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 u="";f.stderr.on("data",e=>{u=(u+e).slice(-2e3)});const h={controller:new AbortController,proc:f,dir:l,videoPath:m,name:(a||"export").replace(/[<>:"/\\|?*\x00-\x1f]+/g,"_").slice(0,120).trim()||"export",projectId:s,requestId:n,cacheId:c,totalFrames:Number.isSafeInteger(d)&&d>0?d:void 0,wav:null,frames:0,frameBytes:0,err:()=>u,done:new Promise(e=>f.on("close",e))};re.set(p,h);const g=e=>{h.cleaning||(le(n,{state:"error",projectId:s,error:e.message}),fe(p))};return f.on("error",g),f.stdin.on("error",g),f.once("exit",(e,r)=>{h.finalizing&&0===e||g(new Error(`ffmpeg exited (${r||e}): ${h.err()}`))}),le(n,{state:"rendering",projectId:s,frames:0,totalFrames:h.totalFrames}),p}function fe(e){const t=re.get(e);if(t)return t.cleaning||(t.controller.abort(),t.cleaning=(async()=>{await Promise.allSettled([c(t.proc,t.done),t.muxDone,t.cacheId&&_.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(()=>re.delete(e))),t.cleaning}function ue(e,o,i){r.stat(i,(a,s)=>{if(a||!s.isFile())return o.writeHead(404),void o.end("Not found");"HEAD"===e.method&&o.setHeader("ETag",`W/"${s.size}-${s.mtimeMs}"`);const n=U[t.extname(i).toLowerCase()]||"application/octet-stream",c=e.headers.range;if(c){const t=/bytes=(\d*)-(\d*)/.exec(c);let a=t&&t[1]?parseInt(t[1]):0,d=t&&t[2]?parseInt(t[2]):s.size-1;if(a=Math.min(a,s.size-1),d=Math.min(d,s.size-1),o.writeHead(206,{"Content-Type":n,"Accept-Ranges":"bytes","Content-Range":`bytes ${a}-${d}/${s.size}`,"Content-Length":d-a+1}),"HEAD"===e.method)return void o.end();r.createReadStream(i,{start:a,end:d}).pipe(o)}else{if(o.writeHead(200,{"Content-Type":n,"Content-Length":s.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 re)r.cacheId&&!_.sessions.has(r.cacheId)&&(le(r.requestId,{state:"error",projectId:r.projectId,error:"Optimized export session expired"}),fe(e))},15e3).unref();const he=e.createServer(async(e,o)=>{if(!function(e){if(!R(e.headers.host))return!1;const r=e.headers.origin;if(r)try{return R(new URL(r).host)}catch{return!1}return!0}(e))return void Y(o,403,{error:"forbidden: request must come from this machine (bad Host or Origin header)"});const i=new URL(e.url,"http://localhost"),a=decodeURIComponent(i.pathname);if(a.split(/[\\/]/).some(e=>e.startsWith(".")))return o.writeHead(403),void o.end();if("/api/status"===a&&"GET"===e.method)return void Y(o,200,{service:"fablecut",pid:process.pid,version:ge(),dataDir:r.realpathSync(b)});if("/api/export/active"===a&&"GET"===e.method){const e=ie(),r=i.searchParams.get("project");return void Y(o,200,{active:r?e.includes(r):e.length>0,projects:e})}if("/api/server/stop"===a&&"POST"===e.method){if(re.size>0||ie().length>0)return void Y(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,()=>{Se(),he.close(()=>process.exit(0))})}if("/api/projects"===a&&"GET"===e.method)return void Y(o,200,O());if("/api/projects"===a&&"POST"===e.method){try{const r=JSON.parse((await K(e)).toString("utf8")||"{}"),t=String(r.name||"Untitled Project").trim()||"Untitled Project",{id:i}=h.create(t,r.id);B(i),J(null,"projects"),Y(o,201,{id:i,name:t})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/project"===a&&"GET"===e.method){try{const e=Q(i);Y(o,200,ce(i,e)||h.read(e.id))}catch(e){if("PROJECT_NOT_FOUND"!==e.code)try{require("./cli/lib/telemetry").reportError("ProjectReadFailed",e)}catch{}Y(o,"PROJECT_NOT_FOUND"===e.code?404:500,{error:String(e)})}return}if("/api/project"===a&&"PUT"===e.method){try{const r=Q(i),t=await K(e),a=JSON.parse(t.toString("utf8"));Y(o,200,{ok:!0,revision:h.save(r.id,a,"1"===i.searchParams.get("force")).revision})}catch(e){Y(o,e.status||400,{error:String(e),revision:e.revision})}return}if("/api/media"===a&&"GET"===e.method){try{const e=Q(i);Y(o,200,r.readdirSync(e.mediaDir).filter(o=>r.statSync(t.join(e.mediaDir,o)).isFile()).map(o=>({name:o,src:Z(e.id,o),size:r.statSync(t.join(e.mediaDir,o)).size})))}catch(e){Y(o,500,{error:String(e)})}return}if("/api/library"===a&&"GET"===e.method){const e=i.searchParams.get("dir");if(!I.includes(e))return void Y(o,400,{error:"dir must be one of "+I.join("|")});try{const i=t.join(j,e),a=[],s=(o,i)=>{for(const n of r.readdirSync(o)){const c=t.join(o,n),d=i?i+"/"+n:n,p=r.statSync(c);p.isDirectory()?s(c,d):a.push({name:n,rel:d,size:p.size,src:"/library/"+e+"/"+d.split("/").map(encodeURIComponent).join("/")})}};s(i,""),Y(o,200,a)}catch(e){Y(o,500,{error:String(e)})}return}if("/api/upload"===a&&"POST"===e.method){try{const a=Q(i);let s=function(e){return e.replace(/[^\w.\- ()\[\]]+/g,"_").slice(0,120)||"file"}(i.searchParams.get("name")||"upload.bin"),n=t.join(a.mediaDir,s),c=1;const p=t.extname(s),l=t.basename(s,p),m=await K(e);for(;;)try{r.writeFileSync(n,m,{flag:"wx"});break}catch(e){if("EEXIST"!==e.code)throw e;n=t.join(a.mediaDir,`${l}_${c++}${p}`)}await async function(e){if(!$||!ee.has(t.extname(e).toLowerCase()))return;const o=e+".fs"+t.extname(e);try{await d("ffmpeg",["-y","-i",e,"-c","copy","-movflags","+faststart",o]),r.rmSync(e),r.renameSync(o,e)}catch{try{r.rmSync(o)}catch{}}}(n),Y(o,200,{ok:!0,src:Z(a.id,t.basename(n))})}catch(e){Y(o,500,{error:String(e)})}return}if(a.startsWith("/api/export/cache/")){try{if(!$||!z)throw new Error("Optimized export needs ffmpeg and ffprobe");const r=Q(i),t=i.searchParams.get("id");if("/api/export/cache/begin"===a&&"POST"===e.method){const t=JSON.parse((await K(e)).toString()),a=ce(i,r),s=oe.get(i.searchParams.get("exportJob"));if(s&&!s.child)throw new Error("Export job has not started");const n=_.create(r,t.revision,a,{managed:!!s});s&&(s.cacheIds||=new Set).add(n),Y(o,200,{id:n})}else if("/api/export/cache/status"===a&&"GET"===e.method)Y(o,200,_.status(t,r.id));else if("/api/export/cache/block"===a&&"POST"===e.method){const i=JSON.parse((await K(e)).toString());Y(o,200,await _.block(t,r.id,i.clipId,i.block,{priority:i.priority,neededAt:i.neededAt,sequence:i.sequence}))}else if("/api/export/cache/retain"===a&&"POST"===e.method){const i=JSON.parse((await K(e)).toString());_.retain(t,r.id,i.sequence,i.blocks,i.current),Y(o,200,{ok:!0})}else if("/api/export/cache/wait"===a&&"POST"===e.method)_.reportWait(t,r.id,JSON.parse((await K(e)).toString())),Y(o,200,{ok:!0});else if("/api/export/cache/frame"===a&&"GET"===e.method)ue(e,o,_.frame(t,r.id,i.searchParams.get("key"),Number(i.searchParams.get("index"))));else if("/api/export/cache/drop"===a&&"POST"===e.method){const i=JSON.parse((await K(e)).toString());_.drop(t,r.id,i.clipIds),Y(o,200,{ok:!0})}else"/api/export/cache/release"===a&&"POST"===e.method?(await _.release(t,r.id),Y(o,200,{ok:!0})):Y(o,404,{error:"unknown cache operation"})}catch(e){Y(o,400,{error:String(e.message||e)})}return}if("/api/export/source-audio"===a&&"GET"===e.method){const e=new AbortController,t=()=>e.abort();let a;o.once("close",t);try{if(!$)throw Object.assign(new Error("Audio export needs ffmpeg"),{status:503});if(a=await C.prepare(Q(i),i.searchParams.get("src"),e.signal),e.signal.aborted)return;if(!a)return o.writeHead(204),void o.end();o.writeHead(200,{"Content-Type":"audio/wav","Cache-Control":"no-store"}),await S(r.createReadStream(a.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():Y(o,r.status||400,{error:String(r.message||r)})}finally{o.removeListener("close",t),a&&await a.cleanup()}return}if(("/api/waveform"===a||"/api/audio-info"===a)&&"GET"===e.method){try{if(!$||!z)throw Object.assign(new Error("Waveforms need ffmpeg and ffprobe"),{status:503});const e=Q(i),r=i.searchParams.get("src");Y(o,200,"/api/waveform"===a?await A.peaks(e,r):{channels:await A.channels(e,r)})}catch(e){if(!e.status||e.status>=500)try{require("./cli/lib/telemetry").reportError("/api/waveform"===a?"WaveformFailed":"AudioInfoFailed",e)}catch{}Y(o,e.status||400,{error:String(e.message||e)})}return}if("/api/export/save"===a&&"POST"===e.method){const r=["127.0.0.1","::1","::ffff:127.0.0.1"].includes(e.socket.remoteAddress);let a=!1;try{a=new URL(e.headers.origin).origin===`http://${e.headers.host}`}catch{}if(!r||!a)return e.resume(),void Y(o,403,{error:"System folder selection requires a local, same-origin editor page",browserSave:!0});const s=new AbortController,n=()=>s.abort();o.once("close",n);try{const r=Q(i),a=i.searchParams.get("src");let n,c;if(a)n=await l(r,a),c=t.basename(n),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 f({source:n,name:c,stream:e,signal:s.signal});e.resume(),o.destroyed||Y(o,200,d)}catch(r){e.resume(),o.destroyed||Y(o,r.status||500,{error:r.message,browserSave:!0===r.browserSave||501===r.status})}finally{o.off("close",n)}return}if("/api/export/start"===a&&"POST"===e.method){try{const r=Q(i),t=JSON.parse((await K(e)).toString("utf8")||"{}"),a=t.engine||"optimized";if(!["fast","optimized","realtime"].includes(a))throw new Error("Unknown export engine");if("realtime"!==a&&(!$||"optimized"===a&&!z))throw new Error("Export needs ffmpeg"+("optimized"===a?" and ffprobe":""));if(void 0!==t.browser&&("string"!=typeof t.browser||!t.browser))throw new Error("browser must be a nonempty executable path");const s=void 0===t.timeoutSeconds?3600:t.timeoutSeconds;if(!Number.isFinite(s)||s<=0||s>86400)throw new Error("timeoutSeconds must be between 0 and 86400");const n=h.read(r.id);if(void 0!==t.revision&&t.revision!==n.revision)return void Y(o,409,{error:"Project changed; save/reload before exporting"});if(!n.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(te.has(c))return void Y(o,409,{error:"Export request id already used"});oe.set(c,{projectId:r.id,snapshot:n,options:{engine:a,name:String(t.name||n.name||"export"),browser:t.browser,timeoutSeconds:s}});try{const o=require("./cli/lib/telemetry");de().upsert(c,{state:"queued",engine:a,projectId:r.id,project:o.summarizeExportProject(n,{mediaBytes:pe(r,n)}),startedAt:Date.now(),traceparent:e.headers.traceparent}),!0===t.fallback&&o.reportCustom({name:"export_ui_fallback",type:"export",properties:{engine:a,reason:"ui-fallback"}})}catch{}le(c,{state:"queued",projectId:r.id,engine:a}),se(),Y(o,202,{id:c,engine:a})}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{}Y(o,400,{error:String(e.message||e)})}return}if("/api/export/cancel"===a&&"POST"===e.method){try{const e=Q(i),r=i.searchParams.get("id");if(te.get(r)?.projectId!==e.id)return void Y(o,404,{error:"no such export request"});le(r,{state:"cancelled",projectId:e.id}),Y(o,200,{ok:!0})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/export/ffmpeg"===a&&"GET"===e.method)return void Y(o,200,{available:$,ffprobe:z});if("/api/export/status"===a&&"GET"===e.method){try{const e=Q(i),r=te.get(i.searchParams.get("id"));if(!r||r.projectId!==e.id)return void Y(o,404,{error:"no such export request"});let t;if(!["complete","error","cancelled","finalizing"].includes(r.state)){const e=oe.get(i.searchParams.get("id")),r=e?.cacheIds||[...re.values()].filter(e=>e.requestId===i.searchParams.get("id")).map(e=>e.cacheId);for(const e of r){const r=_.sessions.get(e)?.wait;if(r){t={reason:r.reason,elapsedMs:Math.max(0,Date.now()-r.since)};break}}}Y(o,200,{...r,...t?{wait:t}:{}})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/export/recording"===a&&"POST"===e.method){let a,s;try{const n=Q(i),c=i.searchParams.get("exportJob");if(s=oe.get(c),!s?.child||s.stopping||s.projectId!==n.id||"realtime"!==s.options.engine)return void Y(o,404,{error:"No active recording for this project"});if(s.recordingUpload)return void Y(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");le(c,{state:"rendering",frames:e,totalFrames:r})}else{const o=i.searchParams.get("name");if(!m(o)||!["video/mp4","video/webm"].includes(e.headers["content-type"]))throw new Error("Invalid recording");s.recordingUpload=new AbortController,le(c,{state:"finalizing"}),r.mkdirSync(n.exportsDir,{recursive:!0}),a=t.join(t.dirname(n.projectFile),".recording-"+c+".part");const d=r.createWriteStream(a,{flags:"wx"});if(await S(e,d,{signal:s.recordingUpload.signal}),s.recordingUpload.signal.throwIfAborted(),!d.bytesWritten)throw new Error("Empty recording");const p=X(n.exportsDir,o);r.renameSync(a,p),a=null,le(c,{state:"complete",src:"/projects/"+n.id+"/exports/"+encodeURIComponent(t.basename(p))})}Y(o,200,{ok:!0})}catch(e){Y(o,400,{error:e.message})}finally{a&&r.rmSync(a,{force:!0})}return}if("/api/export/report"===a&&"POST"===e.method){try{const r=Q(i),t=JSON.parse((await K(e)).toString("utf8")||"{}");if(!0===t.diagnostic){try{const e=require("./cli/lib/telemetry");t.browserGpu&&e.noteBrowserGpu(t.browserGpu),de().note(t.requestId,{browserGpu:t.browserGpu,wait:t.wait,stack:t.stack,clipIds:t.clipIds,timeSec:t.timeSec}),Number.isFinite(Number(t.frame))&&de().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 Y(o,200,{ok:!0})}const a=te.get(t.requestId);a&&"error"===a.state||le(t.requestId,{state:"error",projectId:r.id,error:String(t.error||"browser export failed"),stack:t.stack,clipIds:t.clipIds,browserGpu:t.browserGpu,timeSec:t.timeSec,...Number.isFinite(Number(t.frame))?{frames:Number(t.frame)}:{}}),Y(o,200,{ok:!0})}catch(e){Y(o,400,{error:String(e)})}return}if("/api/export/begin"===a&&"POST"===e.method){if(!$)return void Y(o,400,{error:"ffmpeg not found on PATH"});try{const r=Q(i),t=JSON.parse((await K(e)).toString("utf8")||"{}"),a=te.get(t.requestId);if(a&&(["complete","error","cancelled"].includes(a.state)||a.projectId!==r.id))throw new Error("Export request is no longer active");"optimized"===t.engine&&_.get(t.cacheId,r.id),Y(o,200,{id:me(t.fps||30,t.name,r.id,t.requestId,"optimized"===t.engine?t.cacheId:void 0,t.totalFrames)})}catch(e){Y(o,500,{error:String(e)})}return}if("/api/export/frame"===a&&"POST"===e.method){const r=re.get(i.searchParams.get("id"));if(!r)return void Y(o,404,{error:"no such export session"});try{const t=await K(e);if(t.length<4||255!==t[0]||216!==t[1])throw new Error("export frame is not a valid JPEG");await n(r,t),r.frames++,r.frameBytes+=t.length,le(r.requestId,{state:"rendering",projectId:r.projectId,frames:r.frames,totalFrames:r.totalFrames}),Y(o,200,{ok:!0})}catch(e){le(r.requestId,{state:"error",projectId:r.projectId,error:e.message}),fe(i.searchParams.get("id")),Y(o,500,{error:String(e)})}return}if("/api/export/audio"===a&&"POST"===e.method){const a=re.get(i.searchParams.get("id"));if(!a)return void Y(o,404,{error:"no such export session"});try{a.wav=t.join(a.dir,"audio.wav"),r.writeFileSync(a.wav,await K(e)),Y(o,200,{ok:!0})}catch(e){Y(o,500,{error:String(e)})}return}if("/api/export/end"===a&&"POST"===e.method){const a=i.searchParams.get("id"),s=re.get(a);if(!s)return void Y(o,404,{error:"no such export session"});let n;try{if(i.searchParams.get("discard"))return await fe(a),void Y(o,200,{ok:!0});if(s.finalizing||s.cleaning)throw new Error("Export session is already ending");const c=Date.now(),p=await K(e),l=p.length?JSON.parse(p.toString()).metrics:void 0;if(!s.frames)throw new Error("export received no video frames");if(s.finalizing=!0,le(s.requestId,{state:"finalizing",projectId:s.projectId,frames:s.frames,totalFrames:s.totalFrames}),s.proc.stdin.end(),0!==await s.done)throw new Error("ffmpeg encode failed: "+s.err());if(s.controller.signal.aborted)throw new Error("export cancelled");const m=h.context(s.projectId);r.mkdirSync(m.exportsDir,{recursive:!0}),n=s.output=X(m.exportsDir,s.name.replace(/\.mp4$/i,"")+".mp4");const f=["-colorspace","bt709","-color_primaries","bt709","-color_trc","bt709","-color_range","tv"];if(s.wav&&r.existsSync(s.wav)?s.muxDone=d("ffmpeg",["-y","-i",s.videoPath,"-i",s.wav,"-c:v","copy","-c:a","aac","-b:a","192k",...s.cacheId?[]:["-shortest"],...f,"-movflags","+faststart",n],s.controller.signal):s.muxDone=d("ffmpeg",["-y","-i",s.videoPath,"-c","copy",...f,"-movflags","+faststart",n],s.controller.signal),await s.muxDone,s.controller.signal.aborted)throw new Error("export cancelled");l&&"object"==typeof l&&(l.phases||={},l.phases.finalizeMs=Date.now()-c,l.totalMs=(Number(l.totalMs)||0)+l.phases.finalizeMs,l.fps=s.frames/(l.totalMs/1e3),l.serverRssBytes=process.memoryUsage().rss),s.committed=!0,le(s.requestId,{state:"complete",projectId:m.id,metrics:l,src:"/projects/"+encodeURIComponent(m.id)+"/exports/"+encodeURIComponent(t.basename(n))}),await fe(a),Y(o,200,{ok:!0,metrics:l,src:"/projects/"+encodeURIComponent(m.id)+"/exports/"+encodeURIComponent(t.basename(n))})}catch(e){const r=`${String(e)} (frames=${s.frames}, bytes=${s.frameBytes})`;le(s.requestId,{state:"error",projectId:s.projectId,error:r}),await fe(a),Y(o,500,{error:r})}return}if("/api/analyze"===a&&"GET"===e.method){let e;try{e=Q(i)}catch(e){return void Y(o,400,{error:String(e)})}const a=decodeURIComponent(i.searchParams.get("src")||""),s=t.join(e.analysisDir,t.basename(a,t.extname(a))+".json");if(!a||!r.existsSync(s))return void Y(o,404,{error:"no cached analysis for that src — POST /api/analyze first"});try{Y(o,200,JSON.parse(r.readFileSync(s,"utf8")))}catch(e){Y(o,500,{error:String(e)})}return}if("/api/analyze"===a&&"POST"===e.method){if(!$)return void Y(o,400,{error:"ffmpeg not found on PATH"});try{const a=Q(i),s=JSON.parse((await K(e)).toString("utf8")||"{}"),n=t.basename(decodeURIComponent(s.src||"")),c=t.join(a.mediaDir,n);if(!n||!r.existsSync(c))return void Y(o,404,{error:"src must name an existing file under /media/"});const d=await u(c,{threshold:s.threshold,music:!1!==s.music,musicDir:a.mediaDir,srcUrl:Z(a.id,n)});d.music&&(d.music.src=Z(a.id,d.music.name)),r.writeFileSync(t.join(a.analysisDir,t.basename(n,t.extname(n))+".json"),JSON.stringify(d,null,2)),Y(o,200,d)}catch(e){try{require("./cli/lib/telemetry").reportError("AnalyzeFailed",e)}catch{}Y(o,500,{error:String(e)})}return}if("/api/events"===a){let r;try{r=Q(i)}catch(e){return void Y(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 G.add(t),void e.on("close",()=>G.delete(t))}const s=/^\/projects\/([a-z0-9_-]+)\/(media|exports)\/(.+)$/.exec(a);if(s){try{const i=T(s[1]);if(!r.existsSync(i.projectFile))throw new Error("no such project");const a="media"===s[2]?i.mediaDir:i.exportsDir;ue(e,o,t.join(a,t.basename(s[3])))}catch{o.writeHead(404),o.end("Not found")}return}if(a.startsWith("/media/")||a.startsWith("/exports/")){try{const r=h.context(E),i=a.startsWith("/media/")?r.mediaDir:r.exportsDir;ue(e,o,t.join(i,t.basename(a)))}catch{o.writeHead(404),o.end("Not found")}return}if(a.startsWith("/library/")){const r=t.normalize(t.join(j,a.slice(9)));return r.startsWith(j+t.sep)?void ue(e,o,r):(o.writeHead(403),void o.end())}let c="/"===a?"/index.html":a;if(c=t.normalize(t.join(q,c)),!c.startsWith(q+t.sep))return o.writeHead(403),void o.end();ue(e,o,c)});function ge(){const e=process.env.FABLECUT_VERSION;return"string"==typeof e&&e.trim()?e.trim():""}function we(e){const r="0.0.0.0"===F?"127.0.0.1":"::"===F?"::1":F;return`http://${r.includes(":")&&!r.startsWith("[")?`[${r}]`:r}:${e}`}function ye(){return t.join(b,"server.json")}function Se(){try{JSON.parse(r.readFileSync(ye(),"utf8")).pid===process.pid&&r.rmSync(ye(),{force:!0})}catch{}}process.on("exit",Se);let ve=!1;function be(e,r){if(!ve){ve=!0;try{require("./cli/lib/telemetry").reportFatalSync(e,r,{command:"server"})}catch{}console.error(r),process.exit(1)}}process.on("uncaughtException",e=>{be("UncaughtException",e)}),process.on("unhandledRejection",e=>{be("UnhandledRejection",e instanceof Error?e:new Error(String(e)))});try{require("./cli/lib/telemetry").initTelemetry({role:"server",command:"server"})}catch{}const xe=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)}};he.once("error",i),he.listen(t,F,()=>{he.removeListener("error",i);const e=he.address().port;!function(e){const t={pid:process.pid,host:F,port:e,url:we(e)+"/",version:ge(),dataDir:r.realpathSync(b),startedAt:(new Date).toISOString()},o=ye(),i=o+".tmp";r.writeFileSync(i,JSON.stringify(t)+"\n"),r.renameSync(i,o)}(e),console.log(`\n FableCut running → ${we(e)}/\n`),["127.0.0.1","localhost","::1"].includes(F)||console.log(` ⚠ WARNING: HOST=${F} exposes the editor (and its file APIs) to the network.\n`),console.log(` projects : ${x} (${O().length})`),console.log(` library : ${j} (${I.join(", ")})`),b!==v&&console.log(` app files : ${v}`),console.log(` ffmpeg : ${$?"found (fast export + faststart remux on)":"not found (real-time export only)"}\n`)})}(xe.port,xe.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: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);
|
package/dist/runtime/style.css
CHANGED
|
@@ -2124,3 +2124,48 @@ body.file-drag::after {
|
|
|
2124
2124
|
.reorder-preview { transition: transform 240ms cubic-bezier(.25, .1, .25, 1); }
|
|
2125
2125
|
.tracks-content[data-gesture="move"], .tracks-content[data-gesture="move"] .clip { cursor: grabbing; }
|
|
2126
2126
|
@media (prefers-reduced-motion: reduce) { .reorder-preview { transition: none; } }
|
|
2127
|
+
|
|
2128
|
+
/* 封面入口固定在时间轴左侧,不占用轨道时间。 */
|
|
2129
|
+
.timeline-sidebar { position: relative; width: 88px; flex: none; min-height: 0; }
|
|
2130
|
+
.timeline-sidebar .track-headers { height: 100%; width: 88px; box-sizing: border-box; }
|
|
2131
|
+
.cover-control { position: absolute; top: calc(50% + 13px); left: 93px; width: 54px; height: 60px; transform: translateY(-50%); }
|
|
2132
|
+
.cover-button { position: relative; width: 100%; height: 100%; border: 1px dashed var(--dim); border-radius: 6px; background: var(--panel); color: var(--text); cursor: pointer; overflow: hidden; }
|
|
2133
|
+
.cover-button:hover { border-color: var(--accent); }
|
|
2134
|
+
.cover-button img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
|
|
2135
|
+
.cover-button span { position: absolute; inset: auto 0 0; padding: 4px; background: #0009; font-size: 11px; }
|
|
2136
|
+
.cover-dialog { margin: auto; width: min(960px, calc(100vw - 48px)); max-height: calc(100dvh - 48px); box-sizing: border-box; overflow: auto; border: 1px solid var(--border); border-radius: 12px; padding: 20px; background: var(--panel); color: var(--text); }
|
|
2137
|
+
.cover-dialog::backdrop { background: #000b; }
|
|
2138
|
+
.cover-heading, .cover-source { display: flex; align-items: center; gap: 12px; }
|
|
2139
|
+
.cover-heading { justify-content: space-between; margin-bottom: 16px; }
|
|
2140
|
+
.cover-heading h2 { margin: 0; font-size: 18px; }
|
|
2141
|
+
.cover-preview { height: min(48dvh, 460px); display: flex; align-items: center; justify-content: center; background: #101012; position: relative; border-radius: 6px; }
|
|
2142
|
+
.cover-preview img { width: 100%; height: 100%; object-fit: contain; }
|
|
2143
|
+
.cover-preview p { position: absolute; bottom: 8px; background: #000b; padding: 6px 12px; border-radius: 4px; }
|
|
2144
|
+
.cover-preview p:empty { display: none; }
|
|
2145
|
+
.cover-source { margin: 16px 0; }
|
|
2146
|
+
.cover-timeline { margin-bottom: 20px; position: relative; overflow: hidden; border: 1px solid var(--border); border-radius: 6px; background: var(--panel-2); touch-action: none; cursor: ew-resize; }
|
|
2147
|
+
.cover-timeline.inactive { opacity: .45; cursor: default; }
|
|
2148
|
+
.cover-timeline .clip { min-width: 0; cursor: inherit; }
|
|
2149
|
+
.cover-timeline .playhead { top: 26px; pointer-events: none; }
|
|
2150
|
+
.cover-frames { min-height: 64px; max-height: 192px; overflow-y: auto; overflow-x: hidden; scrollbar-width: thin; }
|
|
2151
|
+
.cover-frames .clip-body img { width: 100%; height: 100%; object-fit: cover; }
|
|
2152
|
+
|
|
2153
|
+
.cover-source .btn[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
|
|
2154
|
+
.cover-pencil { margin-bottom: 12px; }
|
|
2155
|
+
|
|
2156
|
+
/* 时间轴禁止滚动,轨道在区域中整体居中。 */
|
|
2157
|
+
.timeline-scroll { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: clip; scrollbar-width: none; }
|
|
2158
|
+
.timeline-scroll .ruler { flex: none; }
|
|
2159
|
+
.timeline-scroll .tracks-content { flex: 1; min-height: 0; padding-bottom: 0; display: flow-root; }
|
|
2160
|
+
.track-head { justify-content: center; }
|
|
2161
|
+
.timeline-sidebar:has(.cover-control[data-ui-visible]) { width: 152px; }
|
|
2162
|
+
.timeline-scroll::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
|
2163
|
+
.cover-delete { position: absolute; top: -6px; right: -6px; width: 18px; height: 18px; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid var(--panel); border-radius: 50%; background: #e1e1e6; color: #29292e; font-size: 15px; line-height: 1; cursor: pointer; opacity: 0; pointer-events: none; }
|
|
2164
|
+
.cover-delete[hidden] { display: none; }
|
|
2165
|
+
.cover-control:hover .cover-delete, .cover-control:focus-within .cover-delete { opacity: 1; pointer-events: auto; }
|
|
2166
|
+
.cover-delete:hover { background: var(--danger); color: white; }
|
|
2167
|
+
|
|
2168
|
+
/* 成片报告折叠显示,多规格结果可分别下载。 */
|
|
2169
|
+
.export-checks { font-size: 12px; max-height: 150px; overflow: auto; margin: 8px 0; color: var(--muted, #9ca3af); }
|
|
2170
|
+
.export-checks p { margin: 6px 0; overflow-wrap: anywhere; }
|
|
2171
|
+
.export-checks a { color: inherit; }
|
package/dist/runtime/timeline.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
!function(t){const r=function(){"use strict";const t=1e-9,r=t=>"audio"===t.kind?"audio":"video",
|
|
1
|
+
!function(t){const r=function(){"use strict";const t=1e-9,r=t=>"audio"===t.kind?"audio":"video",e=t=>t.start+t.duration,i=(r,i)=>r.start<e(i)-t&&i.start<e(r)-t,o=t=>"string"==typeof t&&/^[VA][1-9]\d*$/.test(t)&&Number.isSafeInteger(+t.slice(1)),s=t=>"A"===t[0]?"audio":"video";function n(t,r){const e="audio"===r?"A":"V",i=t.reduce((t,e)=>e.kind===r?Math.max(t,+e.id.slice(1)):t,0)+1;if(!Number.isSafeInteger(i))throw new Error("Track identifier exhausted");return e+i}function a(t,r,e){t.tracks||=[];const i={id:n(t.tracks,r),kind:r};let o=t.tracks.findIndex(t=>t.id===e);return o<0&&(o="video"===r?t.tracks.findIndex(t=>"audio"===t.kind):t.tracks.length),o<0&&(o=t.tracks.length),t.tracks.splice(o,0,i),i.id}return{EPS:t,kindOf:r,overlaps:i,normalize:function(n){if(!n||!Array.isArray(n.clips))return n;if(null!=n.cover){const t=n.cover;if(!t||"object"!=typeof t||Array.isArray(t)||("frame"===t.type?!Number.isFinite(t.time)||t.time<0:"image"!==t.type||"string"!=typeof t.src||!/^(\/projects\/|\/library\/|https?:\/\/)/.test(t.src)))throw new Error("Invalid cover: use {type:'frame',time:non-negative seconds} or {type:'image',src}")}if(null!=n.delivery||null!=n.outputs){const t="object"==typeof module&&module.exports?require("./delivery"):globalThis.FableCutDelivery;if(null!=n.delivery&&(n.delivery=t.normalize(n.delivery).spec),null!=n.outputs){if(!Array.isArray(n.outputs)||n.outputs.length>16)throw new Error("outputs must be an array of at most 16 deliveries");const r=new Set;n.outputs=n.outputs.map((e,i)=>{if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("invalid output");const o=e.id||"output-"+(i+1);if("string"!=typeof o||!/^[a-zA-Z0-9_-]{1,64}$/.test(o)||r.has(o))throw new Error("invalid or duplicate output id");r.add(o);const s=e.range||"entire";if(!["entire","in-out"].includes(s))throw new Error("invalid output range");return{...t.normalize(e.delivery||e).spec,id:o,name:"string"==typeof e.name?e.name.slice(0,120):void 0,on:!1!==e.on&&!1!==e.enabled,range:s}})}}const d=[],c=new Set;if(void 0!==n.tracks&&!Array.isArray(n.tracks))throw new Error("tracks must be an array");for(const t of n.tracks||[]){if(!t||!o(t.id)||t.kind!==s(t.id)||c.has(t.id))throw new Error("Invalid or duplicate track");if(null!=t.name&&"string"!=typeof t.name)throw new Error("Track name must be a string");c.add(t.id);const r={id:t.id,kind:t.kind};"string"==typeof t.name&&t.name.trim()&&(r.name=t.name.trim()),d.push(r)}const l=new Set;for(const t of n.clips){if(!o(t.track)||s(t.track)!==r(t))throw new Error(`Invalid track for clip ${t.id||"?"}`);if(!Number.isFinite(t.start)||t.start<0||!Number.isFinite(t.duration)||t.duration<=0)throw new Error(`Invalid timing for clip ${t.id||"?"}`);c.has(t.track)||l.add(t.track)}for(const t of n.disabledTracks||[])o(t)&&!c.has(t)&&l.add(t);const u=[...l].sort((t,r)=>t[0]===r[0]?"V"===t[0]?+r.slice(1)-+t.slice(1):+t.slice(1)-+r.slice(1):"V"===t[0]?-1:1);for(const t of u){const r=s(t);let e=d.findIndex(e=>"video"===r&&("audio"===e.kind||+e.id.slice(1)<+t.slice(1)));e<0&&(e=d.length),d.splice(e,0,{id:t,kind:r}),c.add(t)}if(d.some((t,r)=>"video"===t.kind&&d.slice(0,r).some(t=>"audio"===t.kind)))throw new Error("Video tracks must precede audio tracks");n.tracks=d;const f=new Set(n.disabledTracks||[]),p=new Map;for(const t of n.clips)p.has(t.track)||p.set(t.track,[]),p.get(t.track).push(t);for(const r of[...d]){const o=(p.get(r.id)||[]).sort((t,r)=>t.start-r.start);let s=[];const d=[r.id];for(const c of o){s=s.filter(r=>e(r.clip)>c.start+t);let o=0;for(const t of s)i(c,t.clip)&&(o=Math.max(o,t.level+1));for(;d.length<=o;){const t=a(n,r.kind,"video"===r.kind?d[d.length-1]:void 0);d.push(t),f.has(r.id)&&f.add(t)}c.track=d[o],s.push({clip:c,level:o})}}return(void 0!==n.disabledTracks||f.size)&&(n.disabledTracks=[...f].filter(t=>c.has(t)||d.some(r=>r.id===t))),n},insertTrack:a,firstGap:function(r,i,o,s,n=new Set){let a=Math.max(0,o);const d=r.filter(t=>t.track===i&&!n.has(t.id)).sort((t,r)=>t.start-r.start);for(const r of d){if(a+s<=r.start+t)break;a<e(r)-t&&(a=e(r))}return a},freeTrack:function(t,r,e,o,s,n=new Set){const d=r=>!t.clips.some(t=>t.track===r&&!n.has(t.id)&&i(t,{start:e,duration:o}));if(t.tracks.some(t=>t.id===s&&t.kind===r)&&d(s))return s;const c=t.tracks.find(e=>e.kind===r&&!(t.disabledTracks||[]).includes(e.id)&&d(e.id));return c?c.id:a(t,r)}}}();"object"==typeof module&&module.exports?module.exports=r:t.FableCutTimeline=r}("object"==typeof globalThis?globalThis:this);
|
|
@@ -0,0 +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()}}
|
package/package.json
CHANGED