import fs from 'fs'; import { promises as fsp } from 'fs'; import path from 'path'; import { Router, Request, Response } from 'express'; import { config } from '../config'; import { runFFmpeg } from '../services/ffmpeg'; import { v4 as uuidv4 } from 'uuid'; const router = Router(); // GET /api/frame/:videoId?time=00:01:30.000 router.get('/:videoId', async (req: Request, res: Response) => { const { videoId } = req.params; const { time, frame } = req.query as { time?: string; frame?: string }; const inputPath = path.resolve(config.videosDir, videoId); if (!inputPath.startsWith(path.resolve(config.videosDir))) { res.status(400).json({ error: 'Invalid video ID' }); return; } if (!fs.existsSync(inputPath)) { res.status(404).json({ error: 'Video not found' }); return; } let seekTime = '0'; if (time) { seekTime = time; } else if (frame) { // frame number to time requires fps — use 30fps default seekTime = String(parseInt(frame, 10) / 30); } const outputFile = path.join(config.framesDir, `${uuidv4()}.jpg`); try { await runFFmpeg([ '-accurate_seek', '-ss', seekTime, '-i', inputPath, '-frames:v', '1', '-q:v', '2', outputFile, ]); res.setHeader('Content-Type', 'image/jpeg'); const stream = fs.createReadStream(outputFile); stream.pipe(res); stream.on('end', () => { fsp.unlink(outputFile).catch(() => {}); }); } catch (err) { res.status(500).json({ error: 'Frame extraction failed', detail: String(err) }); } }); export default router;