fix: EDL export uses sequence frame_rate for timecode (29.97/59.94 DF, others non-drop)
This commit is contained in:
parent
d21c61a8b2
commit
d382c6b559
1 changed files with 69 additions and 31 deletions
|
|
@ -7,39 +7,77 @@ import { requireAuth } from '../middleware/auth.js';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(requireAuth);
|
router.use(requireAuth);
|
||||||
|
|
||||||
// ── 59.94 DF timecode helpers (for EDL export) ────────────────────────────────
|
// ── Timecode helpers ──────────────────────────────────────────────────────────
|
||||||
const NOM = 60; // nominal integer fps
|
//
|
||||||
const DROP = 4; // frames dropped per minute (except every 10th)
|
// generateEDL emits CMX3600 timecode using the sequence's frame_rate.
|
||||||
const FRAMES_FIRST_MIN = NOM * 60; // 3600 — first (non-drop) minute per 10-min group
|
//
|
||||||
const FRAMES_PER_MIN = NOM * 60 - DROP; // 3596
|
// 29.97 fps → NTSC drop-frame (30 NOM, drop 2/min except every 10th) → ";"
|
||||||
const FRAMES_PER_10MIN = FRAMES_PER_MIN * 10 + DROP; // 35964
|
// 59.94 fps → double-NTSC DF (60 NOM, drop 4/min except every 10th) → ";"
|
||||||
const FRAMES_PER_HOUR = FRAMES_PER_10MIN * 6; // 215784
|
// all others → non-drop integer (24/25/30/50/60 …) → ":"
|
||||||
|
//
|
||||||
|
|
||||||
function pad2(n) { return String(Math.floor(n)).padStart(2, '0'); }
|
function pad2(n) { return String(Math.floor(n)).padStart(2, '0'); }
|
||||||
|
|
||||||
function framesToTC(totalFrames) {
|
function framesToTC(totalFrames, fps) {
|
||||||
|
fps = fps || 59.94;
|
||||||
const fc = Math.max(0, Math.round(totalFrames));
|
const fc = Math.max(0, Math.round(totalFrames));
|
||||||
const h = Math.floor(fc / FRAMES_PER_HOUR);
|
|
||||||
let rem = fc % FRAMES_PER_HOUR;
|
// 29.97 DF ─ drop 2 frames per minute except every 10th
|
||||||
const tm = Math.floor(rem / FRAMES_PER_10MIN);
|
if (Math.abs(fps - 29.97) < 0.02) {
|
||||||
rem = rem % FRAMES_PER_10MIN;
|
const NOM = 30, DROP = 2;
|
||||||
let m, ss, ff;
|
const FPM = NOM * 60 - DROP; // 1798
|
||||||
// The first minute of each 10-minute group is non-drop (rem < 3600).
|
const FP10M = FPM * 10 + DROP; // 17982
|
||||||
// Minutes 1-9 are drop-frame and start at frame label :00;04.
|
const FPH = FP10M * 6; // 107892
|
||||||
if (rem < FRAMES_FIRST_MIN) {
|
const h = Math.floor(fc / FPH);
|
||||||
m = 0; ss = Math.floor(rem / NOM); ff = rem % NOM;
|
let rem = fc % FPH;
|
||||||
} else {
|
const tm = Math.floor(rem / FP10M);
|
||||||
rem -= FRAMES_FIRST_MIN;
|
rem = rem % FP10M;
|
||||||
m = Math.floor(rem / FRAMES_PER_MIN) + 1;
|
let m, ss, ff;
|
||||||
const remInMin = rem % FRAMES_PER_MIN;
|
if (rem < NOM * 60) {
|
||||||
const adj = remInMin + DROP; // shift so label starts at :00;04
|
m = 0; ss = Math.floor(rem / NOM); ff = rem % NOM;
|
||||||
ss = Math.floor(adj / NOM); ff = adj % NOM;
|
} else {
|
||||||
|
rem -= NOM * 60;
|
||||||
|
m = Math.floor(rem / FPM) + 1;
|
||||||
|
const adj = (rem % FPM) + DROP;
|
||||||
|
ss = Math.floor(adj / NOM); ff = adj % NOM;
|
||||||
|
}
|
||||||
|
return `${pad2(h)}:${pad2(tm * 10 + m)}:${pad2(ss)};${pad2(ff)}`;
|
||||||
}
|
}
|
||||||
const M = tm * 10 + m;
|
|
||||||
return `${pad2(h)}:${pad2(M)}:${pad2(ss)};${pad2(ff)}`;
|
// 59.94 DF ─ drop 4 frames per minute except every 10th
|
||||||
|
if (Math.abs(fps - 59.94) < 0.02) {
|
||||||
|
const NOM = 60, DROP = 4;
|
||||||
|
const FPM = NOM * 60 - DROP; // 3596
|
||||||
|
const FP10M = FPM * 10 + DROP; // 35964
|
||||||
|
const FPH = FP10M * 6; // 215784
|
||||||
|
const h = Math.floor(fc / FPH);
|
||||||
|
let rem = fc % FPH;
|
||||||
|
const tm = Math.floor(rem / FP10M);
|
||||||
|
rem = rem % FP10M;
|
||||||
|
let m, ss, ff;
|
||||||
|
if (rem < NOM * 60) {
|
||||||
|
m = 0; ss = Math.floor(rem / NOM); ff = rem % NOM;
|
||||||
|
} else {
|
||||||
|
rem -= NOM * 60;
|
||||||
|
m = Math.floor(rem / FPM) + 1;
|
||||||
|
const adj = (rem % FPM) + DROP;
|
||||||
|
ss = Math.floor(adj / NOM); ff = adj % NOM;
|
||||||
|
}
|
||||||
|
return `${pad2(h)}:${pad2(tm * 10 + m)}:${pad2(ss)};${pad2(ff)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-drop frame (24, 23.976→24, 25, 30, 50, 60 …) ─ colon separator
|
||||||
|
const nomFps = Math.round(fps);
|
||||||
|
const ff = fc % nomFps;
|
||||||
|
const totalSec = Math.floor(fc / nomFps);
|
||||||
|
const ss = totalSec % 60;
|
||||||
|
const mm = Math.floor(totalSec / 60) % 60;
|
||||||
|
const hh = Math.floor(totalSec / 3600);
|
||||||
|
return `${pad2(hh)}:${pad2(mm)}:${pad2(ss)}:${pad2(ff)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateEDL(seqName, clips) {
|
function generateEDL(seqName, clips, fps) {
|
||||||
|
fps = fps || 59.94;
|
||||||
const lines = [`TITLE: ${seqName}`, ''];
|
const lines = [`TITLE: ${seqName}`, ''];
|
||||||
clips.forEach((c, i) => {
|
clips.forEach((c, i) => {
|
||||||
const num = String(i + 1).padStart(3, '0');
|
const num = String(i + 1).padStart(3, '0');
|
||||||
|
|
@ -49,10 +87,10 @@ function generateEDL(seqName, clips) {
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.substring(0, 32)
|
.substring(0, 32)
|
||||||
.padEnd(8);
|
.padEnd(8);
|
||||||
const srcIn = framesToTC(c.source_in_frames);
|
const srcIn = framesToTC(c.source_in_frames, fps);
|
||||||
const srcOut = framesToTC(c.source_out_frames);
|
const srcOut = framesToTC(c.source_out_frames, fps);
|
||||||
const recIn = framesToTC(c.timeline_in_frames);
|
const recIn = framesToTC(c.timeline_in_frames, fps);
|
||||||
const recOut = framesToTC(c.timeline_out_frames);
|
const recOut = framesToTC(c.timeline_out_frames, fps);
|
||||||
lines.push(`${num} ${reel} V C ${srcIn} ${srcOut} ${recIn} ${recOut}`);
|
lines.push(`${num} ${reel} V C ${srcIn} ${srcOut} ${recIn} ${recOut}`);
|
||||||
});
|
});
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
|
|
@ -228,7 +266,7 @@ router.post('/:id/export/edl', async (req, res, next) => {
|
||||||
[req.params.id]
|
[req.params.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const edl = generateEDL(seq.name, clipsR.rows);
|
const edl = generateEDL(seq.name, clipsR.rows, seq.frame_rate);
|
||||||
const filename = `${seq.name.replace(/[^a-z0-9]/gi, '_')}.edl`;
|
const filename = `${seq.name.replace(/[^a-z0-9]/gi, '_')}.edl`;
|
||||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue