(req, res)
| 752 | } |
| 753 | |
| 754 | async function handleExport(req, res) { |
| 755 | try { |
| 756 | const body = JSON.parse((await readBody(req)).toString() || "{}"); |
| 757 | const session = sessions.get(body.id); |
| 758 | if (!session) throw new Error("Session not found. Analyse videos first."); |
| 759 | |
| 760 | const fps = session.meta1.fps || 24; |
| 761 | const trimEnd1 = clampNumber(body.trimEnd1, 0, session.meta1.frames - 1); |
| 762 | const trimStart2 = clampNumber(body.trimStart2, 0, session.meta2.frames - 1); |
| 763 | const blendFrames = clampNumber(body.blendFrames, 0, 48); |
| 764 | const interpolate = Boolean(body.interpolate); |
| 765 | const mode = body.mode === "blend" && blendFrames > 0 ? "blend" : "cut"; |
| 766 | const toneFrames = clampNumber(body.toneFrames, 0, 48); |
| 767 | const toneScope = body.toneScope === "full" ? "full" : "join"; |
| 768 | const toneMatch = Boolean(body.toneMatch) && (toneScope === "full" || toneFrames > 0); |
| 769 | const audioMode = ["micro_crossfade", "clean_cut", "hard_cut"].includes(body.audioMode) ? body.audioMode : "micro_crossfade"; |
| 770 | const output = uniqueOutputPath(outputName(session, { |
| 771 | trimEnd1, |
| 772 | trimStart2, |
| 773 | mode, |
| 774 | blendFrames, |
| 775 | toneMatch, |
| 776 | toneScope, |
| 777 | toneFrames, |
| 778 | interpolate, |
| 779 | audioMode |
| 780 | })); |
| 781 | const end1 = Math.max(1 / fps, (session.meta1.frames - trimEnd1) / fps); |
| 782 | const start2 = trimStart2 / fps; |
| 783 | const blendDuration = Math.min(blendFrames / fps, Math.max(0, end1 - 1 / fps)); |
| 784 | const toneSource = session.analysis.tone; |
| 785 | const v2ToneFilter = toneMatch ? toneFilter(toneSource, toneScope === "full" ? 1 : toneFrames, fps, toneScope) : ""; |
| 786 | |
| 787 | let filter; |
| 788 | if (mode === "blend") { |
| 789 | const offset = Math.max(0, end1 - blendDuration); |
| 790 | const expr = `A*(1-T/${blendDuration.toFixed(6)})+B*(T/${blendDuration.toFixed(6)})`; |
| 791 | filter = [ |
| 792 | `[0:v]trim=start=0:end=${end1.toFixed(6)},setpts=PTS-STARTPTS,format=yuv420p[v0base]`, |
| 793 | `[1:v]trim=start=${start2.toFixed(6)},setpts=PTS-STARTPTS,${v2ToneFilter}format=yuv420p[v1base]`, |
| 794 | `[v0base]split=2[v0pre][v0fade]`, |
| 795 | `[v1base]split=2[v1fade][v1post]`, |
| 796 | `[v0pre]trim=start=0:end=${offset.toFixed(6)},setpts=PTS-STARTPTS[pre]`, |
| 797 | `[v0fade]trim=start=${offset.toFixed(6)}:end=${end1.toFixed(6)},setpts=PTS-STARTPTS[v0b]`, |
| 798 | `[v1fade]trim=start=0:end=${blendDuration.toFixed(6)},setpts=PTS-STARTPTS[v1b]`, |
| 799 | `[v1post]trim=start=${blendDuration.toFixed(6)},setpts=PTS-STARTPTS[post]`, |
| 800 | `[v0b][v1b]blend=all_expr='${expr}'[mix]`, |
| 801 | `[pre][mix][post]concat=n=3:v=1:a=0[vout]` |
| 802 | ].join(";"); |
| 803 | } else { |
| 804 | filter = [ |
| 805 | `[0:v]trim=start=0:end=${end1.toFixed(6)},setpts=PTS-STARTPTS,format=yuv420p[v0]`, |
| 806 | `[1:v]trim=start=${start2.toFixed(6)},setpts=PTS-STARTPTS,${v2ToneFilter}format=yuv420p[v1]`, |
| 807 | `[v0][v1]concat=n=2:v=1:a=0[vout]` |
| 808 | ].join(";"); |
| 809 | } |
| 810 | |
| 811 | if (interpolate) { |
no test coverage detected