초기 커밋: 스테이션(측점) 기반 주행영상 플레이어
- 클라이언트(React/Vite): Video.js 플레이어, 하단 스테이션바, POI/구조물 영상 오버레이, 카메라 파라미터 보정, 미니맵/RoutePanel - 서버(Express): Range 스트리밍, HLS 변환, 프레임 추출, tus 업로드, 주석 API - 최근 작업: 스테이션바 종점역 표출/양끝 정렬/미도착(재생불가) 표시, POI 라벨 '구분' 우선·컴팩트 팝업·겹침제외 토글·동일좌표 다중행, 진행방향(상/하) 우선 표출, 커서 배지 크기·픽셀 떨림 개선 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user