UNPKG

mfx

Version:

In-browser video editing toolkit, with effects accelerated by WebGL

141 lines (117 loc) 3.78 kB
import { type UniformProducer } from "./effects/shaders"; import { ExtendedVideoFrame } from "./frame"; import { MFXTransformStream } from "./stream"; import { easing as tsEasing } from "ts-easing"; import parseTime from "parse-duration"; export class FrameRateAdjuster extends MFXTransformStream< ExtendedVideoFrame, ExtendedVideoFrame > { get identifier() { return "FrameRateAdjuster"; } constructor(fps: number) { // Limit to 500fps to prevent bugs const maxDuration = (1 / Math.min(fps, 500)) * 1e6; let borrowedDuration = 0; let skippedDuration = 0; super({ transform: (frame, controller) => { const duration = frame.duration; const idealFrameCount = Math.floor(duration / maxDuration); const minFrameCount = Math.floor( (duration + borrowedDuration) / maxDuration, ); // Simple path, no need for a fill if (idealFrameCount === 1 || minFrameCount === 1) { controller.enqueue(frame); borrowedDuration = 0; return; } if (idealFrameCount < 1) { // Skip this frame and store the duration borrowedDuration += frame.duration; return; } // Calculate the durations ensuring no fractional durations const baseDuration = Math.floor(duration / idealFrameCount); const remainingDuration = duration % idealFrameCount; const timestamp = frame.timestamp; let accumulatedDuration = 0; for (let i = 0; i < idealFrameCount; i++) { const frameDuration = i === idealFrameCount - 1 ? baseDuration + remainingDuration : baseDuration; const clone = ExtendedVideoFrame.revise(frame, frame.clone() as any, { timestamp: timestamp + accumulatedDuration, duration: baseDuration + borrowedDuration, }); // Reset borrowedDuration as it is consumed in the new frame's duration borrowedDuration = 0; accumulatedDuration += frameDuration; controller.enqueue(clone); } frame.close(); }, }); } } /** * * @group Advanced * @example animate("0s 100, 0.5s 200", "elastic"); */ export const animate = ( value: string, easing: string | ((number) => number) = (v) => v, ) => { const steps = value.split(","); const parsedSteps = steps.map((step) => { const [time, value] = step.trim().split(" "); return { time: parseTime(time), value: JSON.parse(value.trim()), }; }); return keyframes( parsedSteps, typeof easing === "function" ? easing : tsEasing[easing], ); }; /** * @group Advanced */ export const keyframes = <T>( defs: { time: number; easing?: (number) => number; value: T; }[], easing: (number) => number = (v) => v, ): UniformProducer<T> => { return async (frame) => { const ts = frame.timestamp / 1000; const idx = defs.findIndex((_, i) => { const nextTime = defs[i + 1]?.time || Infinity; return ts < nextTime; }); const windowStart = defs[idx]; const windowEnd = defs[idx + 1] || windowStart; // Fractional / relative value if (typeof windowStart.value === "number") { const pos = ts - windowStart.time; const duration = windowEnd.time - windowStart.time; const ease = windowStart.easing || easing || ((v) => v); if (duration <= 0) { return windowEnd.value; } const delta = ease(pos / duration); const diff = (windowEnd.value as number) - (windowStart.value as number); const value = ((windowStart.value as number) + diff * delta) as T; return value; } // Absolute value return windowStart.value; }; };