Files
grown/src/hooks.server.ts
2026-03-11 00:06:51 +01:00

105 lines
2.5 KiB
TypeScript

import schedule from "node-schedule";
import { spawn } from "child_process";
import { existsSync, mkdirSync } from "fs";
import { join } from "path";
import { getAllStreams, getStreamProcess } from "$lib/server/streaming.js";
const TIMELAPSE_OUTPUT_PATH =
process.env.TIMELAPSE_OUTPUT_PATH || "./timelapse";
const CAPTURE_INTERVAL = "0 * * * *"; // Every hour at minute 0
let captureJob: any = null;
let initialized = false;
function ensureOutputDirectory() {
if (!existsSync(TIMELAPSE_OUTPUT_PATH)) {
mkdirSync(TIMELAPSE_OUTPUT_PATH, { recursive: true });
}
}
function captureTimelapseFrame() {
const streams = getAllStreams();
streams.forEach((streamInfo) => {
const stream = getStreamProcess(streamInfo.streamId);
if (!stream || !stream.process || !stream.process.stdout) {
console.error(
`[Timelapse] Stream ${streamInfo.streamId} not available for capture`,
);
return;
}
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `${streamInfo.streamId}_${timestamp}.jpg`;
const outputPath = join(TIMELAPSE_OUTPUT_PATH, filename);
const ffmpegArgs = [
"-rtsp_transport",
"tcp",
"-i",
streamInfo.rtspUrl,
"-vframes",
"1",
"-q:v",
"2",
outputPath,
];
const ffmpegProcess = spawn("ffmpeg", ffmpegArgs);
ffmpegProcess.on("error", (err: Error) => {
console.error(
`[Timelapse] FFmpeg error for ${streamInfo.streamId}:`,
err,
);
});
ffmpegProcess.on("close", (code: number | null) => {
if (code === 0) {
console.log(
`[Timelapse] Captured frame for ${streamInfo.streamId}: ${filename}`,
);
} else {
console.error(
`[Timelapse] FFmpeg exited with code ${code} for ${streamInfo.streamId}`,
);
}
});
});
}
function initializeTimelapse() {
if (initialized) {
return;
}
ensureOutputDirectory();
console.log(`[Timelapse] Output directory: ${TIMELAPSE_OUTPUT_PATH}`);
console.log("[Timelapse] Starting hourly capture job...");
captureJob = schedule.scheduleJob(CAPTURE_INTERVAL, () => {
console.log("[Timelapse] Running hourly capture job...");
captureTimelapseFrame();
});
initialized = true;
console.log("[Timelapse] Hourly capture job initialized");
}
export async function handle({ event, resolve }) {
// Initialize timelapse on first request
if (!initialized) {
initializeTimelapse();
}
return resolve(event);
}
// Cleanup on shutdown
process.on("exit", () => {
if (captureJob) {
captureJob.cancel();
console.log("[Timelapse] Capture job cancelled");
}
});