UNPKG

timecode-converter

Version:

Modern TypeScript library for broadcast timecode conversions with full SMPTE drop-frame support

265 lines (258 loc) 10 kB
'use strict'; // src/dropFrameHelpers.ts var isDropFrameTimecode = (tc) => { return tc.includes(";"); }; var isDropFrameRate = (fps) => { return Math.abs(fps - 29.97) < 0.01 || Math.abs(fps - 59.94) < 0.01; }; var calculateDroppedFrames = (hours, minutes, frameRate) => { if (!isDropFrameRate(frameRate)) return 0; const dropFramesPerMinute = Math.abs(frameRate - 29.97) < 0.01 ? 2 : 4; const totalMinutes = hours * 60 + minutes; const droppedMinutes = totalMinutes - Math.floor(totalMinutes / 10); return droppedMinutes * dropFramesPerMinute; }; var dropFrameTimecodeToFrames = (hours, minutes, seconds, frames, frameRate) => { const nominalFps = Math.round(frameRate); let totalFrames = frames; totalFrames += seconds * nominalFps; totalFrames += minutes * nominalFps * 60; totalFrames += hours * nominalFps * 60 * 60; const droppedFrames = calculateDroppedFrames(hours, minutes, frameRate); return totalFrames - droppedFrames; }; var framesToDropFrameTimecode = (frameCount, frameRate) => { const nominalFps = Math.round(frameRate); const dropFramesPerMinute = Math.abs(frameRate - 29.97) < 0.01 ? 2 : 4; const actualFramesPerMinute = nominalFps * 60 - dropFramesPerMinute; const framesPer10Minutes = actualFramesPerMinute * 9 + nominalFps * 60; const tenMinuteSegments = Math.floor(frameCount / framesPer10Minutes); let remainingFrames = frameCount % framesPer10Minutes; let additionalMinutes = 0; if (remainingFrames >= nominalFps * 60) { remainingFrames -= nominalFps * 60; additionalMinutes = 1 + Math.floor(remainingFrames / actualFramesPerMinute); remainingFrames = remainingFrames % actualFramesPerMinute; } const totalMinutes = tenMinuteSegments * 10 + additionalMinutes; const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; let displayFrames = remainingFrames; if (minutes % 10 !== 0 && additionalMinutes > 0) { displayFrames += dropFramesPerMinute; } const seconds = Math.floor(displayFrames / nominalFps); const frames = displayFrames % nominalFps; return { hours, minutes, seconds, frames }; }; // src/secondsToTimecode.ts var normalisePlayerTime = (seconds, fps) => { const totalFrames = Math.floor(Number((fps * seconds).toPrecision(12))); return Number((totalFrames / fps).toFixed(2)); }; var padZero = (n) => { if (n < 10) return `0${Math.floor(n)}`; return String(Math.floor(n)); }; var secondsToTimecode = (seconds, frameRate, dropFrame) => { let useDropFrame; if (dropFrame !== void 0) { useDropFrame = dropFrame; } else if (isDropFrameRate(frameRate)) { useDropFrame = seconds >= 60; } else { useDropFrame = false; } if (dropFrame === false && isDropFrameRate(frameRate) && seconds >= 3600) { console.warn( `Warning: Converting ${seconds} seconds at ${frameRate} fps without drop-frame. For durations over 1 hour, consider using drop-frame format for accurate broadcast timecode. After ${Math.floor(seconds / 3600)} hour(s), drift is approximately ${Math.round(seconds / 3600 * 3.6)} seconds.` ); } if (useDropFrame && isDropFrameRate(frameRate)) { const totalFrames = Math.round(seconds * frameRate); const { hours: hours2, minutes: minutes2, seconds: secs2, frames: frames2 } = framesToDropFrameTimecode(totalFrames, frameRate); return `${padZero(hours2)}:${padZero(minutes2)}:${padZero(secs2)};${padZero(frames2)}`; } if (seconds === 0) { return "00:00:00:00"; } const fps = frameRate; const normalisedSeconds = normalisePlayerTime(seconds, fps); const wholeSeconds = Math.floor(normalisedSeconds); const framesPrecise = (normalisedSeconds - wholeSeconds) * fps; const frames = Math.round(framesPrecise * 100) / 100; const hours = Math.floor(wholeSeconds / 3600); const minutes = Math.floor(wholeSeconds % 3600 / 60); const secs = wholeSeconds % 60; return `${padZero(hours)}:${padZero(minutes)}:${padZero(secs)}:${padZero(frames)}`; }; var secondsToTimecode_default = secondsToTimecode; // src/timecodeToSeconds.ts var parseTimecode = (tc) => { const isDropFrame = isDropFrameTimecode(tc); const normalized = tc.replace(";", ":"); const parts = normalized.split(":"); return { hours: parseInt(parts[0], 10), minutes: parseInt(parts[1], 10), seconds: parseInt(parts[2], 10), frames: parseInt(parts[3], 10), isDropFrame }; }; var timecodeToFrames = (tc, fps) => { const { hours, minutes, seconds, frames, isDropFrame } = parseTimecode(tc); if (isDropFrame && isDropFrameRate(fps)) { return dropFrameTimecodeToFrames(hours, minutes, seconds, frames, fps); } let totalFrames = frames; totalFrames += seconds * fps; totalFrames += minutes * fps * 60; totalFrames += hours * fps * 60 * 60; return totalFrames; }; var timecodeToSecondsHelper = (tc, frameRate) => { const fps = frameRate; if (isDropFrameTimecode(tc) && !isDropFrameRate(fps)) { console.warn( `Warning: Drop-frame timecode format (${tc}) used with non-drop-frame rate ${fps} fps. Drop-frame is only valid for 29.97 and 59.94 fps.` ); } const frames = timecodeToFrames(tc, fps); return Number((frames / fps).toFixed(2)); }; var timecodeToSeconds_default = timecodeToSecondsHelper; // src/padTimeToTimecode.ts var countColon = (timecode) => timecode.split(":").length; var includesFullStop = (timecode) => timecode.includes("."); var isOneDigit = (str) => str.length === 1; var padTimeToTimecode = (time) => { if (typeof time === "string") { const isDropFrame = time.includes(";"); const separatorCount = isDropFrame ? time.split(/[:;]/).length : countColon(time); switch (separatorCount) { case 4: return time; case 2: { const parts = time.split(":"); if (isOneDigit(parts[0])) { return `00:0${time}:00`; } return `00:${time}:00`; } case 3: return `${time}:00`; default: { if (includesFullStop(time)) { const parts = time.split("."); if (isOneDigit(parts[0])) { return `00:0${parts[0]}:${parts[1]}:00`; } return `00:${time.replace(".", ":")}:00`; } if (isOneDigit(time)) { return `00:00:0${time}:00`; } return `00:00:${time}:00`; } } } return time; }; var padTimeToTimecode_default = padTimeToTimecode; // src/validateTimecode.ts function validateTimecode(timecode, frameRate) { const errors = []; const warnings = []; const dropFrameFormat = isDropFrameTimecode(timecode); const format = dropFrameFormat ? "drop-frame" : "non-drop"; const normalized = timecode.replace(";", ":"); const parts = normalized.split(":"); if (parts.length !== 4) { errors.push(`Invalid timecode format. Expected hh:mm:ss:ff or hh:mm:ss;ff, got ${timecode}`); return { valid: false, errors, warnings }; } const hours = parts[0] ? parseInt(parts[0], 10) : NaN; const minutes = parts[1] ? parseInt(parts[1], 10) : NaN; const seconds = parts[2] ? parseInt(parts[2], 10) : NaN; const frames = parts[3] ? parseInt(parts[3], 10) : NaN; if (isNaN(hours) || hours < 0) { errors.push("Hours must be a valid non-negative number"); } else if (hours > 23) { errors.push("Hours cannot exceed 23"); } if (isNaN(minutes) || minutes < 0) { errors.push("Minutes must be a valid non-negative number"); } else if (minutes > 59) { errors.push("Minutes cannot exceed 59"); } if (isNaN(seconds) || seconds < 0) { errors.push("Seconds must be a valid non-negative number"); } else if (seconds > 59) { errors.push("Seconds cannot exceed 59"); } if (isNaN(frames) || frames < 0) { errors.push("Frames must be a valid non-negative number"); } if (frameRate !== void 0) { const maxFrames = Math.round(frameRate) - 1; if (frames > maxFrames) { errors.push(`Frames cannot exceed ${maxFrames} for ${frameRate} fps`); } if (dropFrameFormat) { if (!isDropFrameRate(frameRate)) { warnings.push( `Drop-frame format used with non-drop-frame rate ${frameRate} fps. Drop-frame is only valid for 29.97 and 59.94 fps.` ); } else { if (seconds === 0 && minutes % 10 !== 0 && frames < 2) { errors.push(`Invalid drop-frame timecode. Frames 00 and 01 don't exist at minute ${minutes}`); } } } } if (dropFrameFormat && frameRate === void 0) { warnings.push("Drop-frame format detected but no frame rate provided for validation"); } return { valid: errors.length === 0, errors, warnings, format, components: errors.length === 0 ? { hours, minutes, seconds, frames } : void 0 }; } // src/index.ts var timecodeToSeconds = (time, frameRate) => { if (typeof time === "string") { if (frameRate === void 0) { throw new Error("Frame rate must be specified when converting timecode strings to seconds"); } const resultPadded = padTimeToTimecode_default(time); if (typeof resultPadded === "string") { const resultConverted = timecodeToSeconds_default(resultPadded, frameRate); return resultConverted; } return resultPadded; } return parseFloat(String(time)); }; var shortTimecode = (time, frameRate, dropFrame) => { if (time === 0) { return "00:00:00"; } else { const useDropFrame = dropFrame !== void 0 ? dropFrame : typeof time === "string" && time.includes(";"); const seconds = typeof time === "string" ? timecodeToSeconds(time, frameRate) : time; const timecode = secondsToTimecode_default(seconds, frameRate, useDropFrame); return timecode.slice(0, -3); } }; exports.isDropFrameRate = isDropFrameRate; exports.isDropFrameTimecode = isDropFrameTimecode; exports.secondsToTimecode = secondsToTimecode_default; exports.shortTimecode = shortTimecode; exports.timecodeToSeconds = timecodeToSeconds; exports.validateTimecode = validateTimecode; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map