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"); } });