MCPcopy
hub / github.com/cpaczek/skylight / stabilizeClip

Function stabilizeClip

tracker/src/video/stabilize.ts:233–291  ·  view source on GitHub ↗
(
  src: string,
  out: string,
  opts: StabilizeOpts = {},
)

Source from the content-addressed store, hash-verified

231
232/** Stabilize a clip: detect the plane, smooth, crop locked onto it, encode. */
233export async function stabilizeClip(
234 src: string,
235 out: string,
236 opts: StabilizeOpts = {},
237): Promise<void> {
238 const cropFrac = Math.min(0.95, Math.max(0.3, opts.cropFrac ?? 0.62));
239 const probe = await ffprobe(src);
240 const { width: W, height: H, fps } = probe;
241 const nFrames = probe.frames || Math.ceil((probe.duration || 30) * fps) + 15;
242 opts.onProgress?.(0.1);
243
244 // Prefer the live track logged during recording (the tracker knew exactly
245 // where the plane was); fall back to offline motion-comp detection.
246 const sidecar = `${src}.track.json`;
247 let raw: ({ cx: number; cy: number } | null)[];
248 if (existsSync(sidecar)) {
249 const sc = JSON.parse(readFileSync(sidecar, "utf8"));
250 raw = trackFromSidecar(sc, nFrames, fps, opts.alignMs ?? 0);
251 } else {
252 raw = await detectTrack(src);
253 }
254 opts.onProgress?.(0.5);
255 const track = smoothTrack(raw);
256
257 // Crop window (even dims), 16:9, clamped inside the frame as it follows the plane.
258 const cw = Math.round((Math.round(H * cropFrac) * 16) / 9 / 2) * 2;
259 const ch = Math.round((Math.round(H * cropFrac)) / 2) * 2;
260 const cmds: string[] = [];
261 for (let i = 0; i < track.length; i++) {
262 const t = (i / fps).toFixed(4);
263 const x = Math.max(0, Math.min(W - cw, Math.round(track[i].cx * W - cw / 2)));
264 const y = Math.max(0, Math.min(H - ch, Math.round(track[i].cy * H - ch / 2)));
265 cmds.push(`${t} crop x ${x}, crop y ${y};`);
266 }
267 const cmdFile = `${out}.cmds.txt`;
268 await writeFile(cmdFile, cmds.join("\n"));
269
270 const outH = opts.outH ?? 720;
271 const outW = Math.round((outH * 16) / 9 / 2) * 2;
272 await new Promise<void>((resolve, reject) => {
273 // niced + thread-capped so the offline encode yields to the live tracker on
274 // the already-busy Pi (no hardware H.264 encoder, so this is software x264).
275 const ff = spawn("nice", [
276 "-n", "19", "ffmpeg",
277 "-hide_banner", "-loglevel", "error", "-y", "-threads", "2", "-i", src,
278 "-vf", `sendcmd=f=${cmdFile},crop=${cw}:${ch}:0:0,scale=${outW}:${outH}`,
279 "-an", "-c:v", "libx264", "-preset", "veryfast", "-crf", "21",
280 "-movflags", "+faststart", out,
281 ]);
282 let err = "";
283 ff.stderr.on("data", (c) => (err = (err + c.toString()).slice(-500)));
284 ff.on("error", reject);
285 ff.on("close", (code) =>
286 code === 0 ? resolve() : reject(new Error(`ffmpeg ${code}: ${err}`)),
287 );
288 });
289 await unlink(cmdFile).catch(() => {});
290 opts.onProgress?.(1);

Callers 2

autoStabilizeMethod · 0.85
stabilize-cli.tsFile · 0.85

Calls 5

ffprobeFunction · 0.85
trackFromSidecarFunction · 0.85
detectTrackFunction · 0.85
smoothTrackFunction · 0.85
pushMethod · 0.80

Tested by

no test coverage detected