- 텍스트(측점/POI) 전 프레임 사전 계산 Map (requestIdleCallback 백그라운드) - 드론 데이터 이동 평균 스무딩 (smoothFrame ±N프레임) - 30fps→60fps 프레임 간 선형 보간 (performance.now() 기반) - EMA(지수이동평균) 표시 위치 스무딩 (α=0.01 기본값) - 글씨 2배 크기, bold, strokeText 테두리, 배경 박스 제거 - 카메라 파라미터 패널에 smooth/EMA α 슬라이더 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
23 KiB
23 KiB
CLAUDE.md — abcvideo 프로젝트
이 파일은 Claude Code가 abcvideo 프로젝트에서 작업할 때 참조하는 가이드입니다.
작업 완료 시 필수 — 히스토리 기록
작업(대화 세션)이 끝날 때 반드시 아래 형식으로 히스토리 파일을 작성한다.
파일 경로: docs/history/YYYY-MM-DD_{작업명}.md
필수 항목 (누락 시 Stop 훅이 응답을 차단함):
**소요 시간**: X분
**Context 사용량**: input Xk / output Xk tokens
- 날짜는 오늘 날짜(YYYY-MM-DD), 작업명은 작업 내용을 한글 또는 영문으로 간략히
- 같은 날 여러 작업이면 각각 별도 파일 (예:
2026-04-01_StationOverlay-분석.md) - 소요 시간: 대화 시작부터 작업 완료까지의 실제 경과 시간
- Context 사용량: 이 대화의 토큰 사용량 (모를 경우 추정값 기재)
에이전트 협업 규칙
필수 파일
| 파일 | 용도 | 누가 관리 |
|---|---|---|
CLAUDE.md |
프로젝트 가이드 (아키텍처, 규칙, 기술 스택) | 사용자/아키텍트 |
PLAN.md |
전체 구현 계획 + 각 단계별 세부 태스크 | 계획 수립 시 작성, 에이전트가 필요 시 업데이트 |
PROGRESS.md |
각 단계의 진행 상태 + 완료/미완료/이슈 기록 | 작업하는 에이전트가 실시간 업데이트 |
에이전트 작업 시작 프로토콜
모든 에이전트는 작업 시작 시 반드시 아래 순서를 따른다:
1. CLAUDE.md 읽기 → 프로젝트 규칙/아키텍처 파악
2. PLAN.md 읽기 → 전체 계획에서 자신이 담당할 단계 확인
3. PROGRESS.md 읽기 → 이전 단계 완료 여부, 현재 상태, 알려진 이슈 확인
4. 작업 시작 전 PROGRESS.md에 자신의 단계를 "🔄 진행 중"으로 업데이트
5. 작업 완료 후 PROGRESS.md에 결과 기록 (완료 항목, 남은 이슈, 다음 단계 참고사항)
PLAN.md 구조
# PLAN.md
## 단계 N: 단계 이름
### 목표
### 세부 태스크
- [ ] 태스크 1
- [ ] 태스크 2
### 산출물 (완료 기준)
### 다음 단계 의존성
- 각 단계는 독립적으로 실행 가능한 단위로 분할
- 태스크는 체크박스(
- [ ]/- [x])로 관리 - 완료 기준(산출물)이 명확해야 다음 에이전트가 판단 가능
PROGRESS.md 구조
# PROGRESS.md
## 현재 상태 요약
- 마지막 완료 단계: N
- 현재 진행 단계: N+1
- 블로커: (있으면 기록)
## 단계별 진행 기록
### 단계 N: 단계 이름
- 상태: ✅ 완료 | 🔄 진행 중 | ⏳ 대기 | ❌ 실패
- 완료일: YYYY-MM-DD
- 완료 항목:
- 항목 1
- 항목 2
- 미완료/이슈:
- 이슈 설명
- 다음 단계 참고:
- 후속 에이전트가 알아야 할 사항
- 에이전트는 작업 중 의미 있는 마일스톤마다 PROGRESS.md를 업데이트
- 실패 시 원인과 시도한 방법을 기록하여 다음 에이전트가 같은 실수를 반복하지 않도록 함
- 이전 에이전트가 남긴 "다음 단계 참고"를 반드시 확인 후 작업 시작
에이전트 간 인계 규칙
- 이전 단계 산출물 검증: 코드가 빌드/실행 가능한 상태인지 확인 후 작업 시작
- 기존 코드 존중: 이전 에이전트가 작성한 코드를 이유 없이 대폭 변경하지 않음
- 충돌 방지: 같은 파일을 동시에 수정하는 병렬 에이전트 실행 금지
- 이슈 에스컬레이션: 블로커 발견 시 PROGRESS.md에 기록하고 사용자에게 보고
프로젝트 개요
웹 브라우저(PC)에서 2GB 이상의 대용량 동영상을 안정적으로 재생하는 특화 기능 탑재 플레이어. 단순 재생을 넘어 프레임 추출, 단위 이동, 텍스트 오버레이 등 영상 분석/편집 보조 도구로 기능.
핵심 요구사항
- 2GB 이상 동영상 안정 재생 (서버 파일 + 로컬 파일 모두)
- 프레임 단위 추출 (정확도 우선)
- 입력 단위 기준 앞뒤 이동 (프레임 / 초 / 장면)
- 영상 위 텍스트 입력 (자막형 + 메모형 병행 지원)
- 확장 가능한 플러그인 아키텍처
설계 결정 (2026-03-24 확정)
| 항목 | 결정 | 비고 |
|---|---|---|
| 사용자 모델 | 단일 사용자 도구 | 인증/세션/큐 불필요, 구현 단순화 |
| 로컬 파일 | 업로드 없이 직접 재생 | File API → URL.createObjectURL() |
| 프레임 정확도 | 표준 수준 | CFR: 1/fps 산술, VFR: best-effort |
| HLS 변환 전 재생 | 원본 즉시 재생 | Range Request로 바로 재생, HLS는 백그라운드 변환 |
| 브라우저 지원 | PC 웹 우선 | Chrome/Firefox/Edge, iOS Safari 무시 |
아키텍처 결정 (ADR)
재생 이중 경로
서버 파일: GET /api/stream/:videoId (Range Request) → 즉시 재생
POST /api/hls/:videoId/convert → 백그라운드 HLS 변환
HLS 준비 완료 시 → hls.js로 전환 (seek 안정성 향상)
로컬 파일: File API → URL.createObjectURL() → <video src> 직접 재생
프레임 추출 필요 시 → 서버 업로드 또는 Canvas 폴백
- 로컬 파일은 HLS 변환 없이 브라우저가 직접 재생
- 서버 파일은 Range Request로 즉시 재생 후, HLS 변환 완료 시 전환
- 2GB 이상 로컬 파일은
URL.createObjectURL()이 메모리 효율적 (Blob URL은 참조만 유지)
스트리밍 방식: HLS
- Progressive Download는 2GB+ seek 불안정으로 제외
- HLS 세그먼트 분할로 파일 크기 제한 완전 우회
- 세그먼트 크기: 6초 (분석 도구 특성상 seek 반응성 우선)
- 키프레임 간격: 2초 (
-force_key_frames "expr:gte(t,n_forced*2)") - hls.js v1.6.15로 Chrome/Firefox/Edge 지원 (iOS Safari 네이티브는 비대상)
hls.js 필수 설정
const hls = new Hls({
maxBufferLength: 30, // 전방 버퍼: 30초
maxMaxBufferLength: 600, // 최대 10분
maxBufferSize: 60 * 1024 * 1024, // 60MB
backBufferLength: 30, // *** 필수: 재생 완료 버퍼 30초 후 해제 ***
// (기본값 Infinity → 장시간 재생 시 메모리 누수)
enableWorker: true, // Web Worker 트랜스먹싱
});
backBufferLength: 30미설정 시 2GB 영상 30분 재생 후 브라우저 크래시 위험.
플레이어: Video.js 8.23.x
- 플러그인 200개+ 생태계 → 확장성 우선
@videojs/http-streaming내장으로 HLS 기본 지원- 전체화면 시 오버레이 유지를 위해 컨테이너 전체를 fullscreen으로 처리
- Video.js v10 (2026 중반 GA 예정)으로 향후 마이그레이션 가능하도록 플레이어 로직을 훅/서비스 레이어로 분리
프레임 추출: 서버 FFmpeg (child_process.spawn)
- Canvas API는 키프레임 의존으로 정확도 불충분
- ffmpeg.wasm은 ~30MB 로딩 + iOS 미지원 + VFR 불안정
- 서버 FFmpeg +
-accurate_seek플래그로 정확한 프레임 추출 - fluent-ffmpeg 아카이브됨 (2025-05) →
child_process.spawn()+ 얇은 TypeScript 래퍼 사용 - 로컬 파일 프레임 추출: Canvas API 즉시 미리보기 + 정확 추출 시 서버 업로드
백엔드: Node.js + Express
- 서버 영상 스트리밍 (Range Request)
- HLS 변환 API (FFmpeg child_process.spawn)
- 프레임 추출 API
- 대용량 업로드 (@tus/server v2.3.0 — 자동 재개/재시도)
- 주석/메모 저장 (better-sqlite3 v12.8.0)
- FFmpeg 변환 진행률: SSE (Server-Sent Events)
기술 스택
프론트엔드
React 18 + TypeScript
Video.js 8.23.x + @videojs/http-streaming (HLS 내장)
hls.js 1.6.x (Video.js VHS 보완/대체 가능)
Tailwind CSS
Vite
Zustand (상태 관리)
interact.js (메모형 오버레이 드래그/리사이즈)
subsrt (자막 포맷 변환: SRT, VTT, ASS 등)
백엔드
Node.js 20+ LTS + Express
child_process.spawn (FFmpeg 래퍼 — fluent-ffmpeg 대체)
@tus/server 2.3.x + @tus/file-store (대용량 업로드)
better-sqlite3 12.8.x (주석 저장)
check-disk-space 3.4.x (디스크 모니터링)
인프라/개발 환경
FFmpeg 6.x+ (시스템 설치 필요 — PATH 등록)
Node.js 20+ LTS
프로젝트 구조
abcvideo/
├── client/ # React 프론트엔드
│ ├── src/
│ │ ├── components/
│ │ │ ├── player/ # Video.js 플레이어 컴포넌트
│ │ │ ├── overlay/ # 텍스트 오버레이 (자막형/메모형)
│ │ │ ├── controls/ # 커스텀 컨트롤 (단위 이동, 속도 등)
│ │ │ └── toolbar/ # 프레임 추출, 주석 관리 툴바
│ │ ├── hooks/
│ │ │ ├── useVideoPlayer.ts # Video.js 초기화/제어
│ │ │ ├── useFrameStep.ts # 프레임 단위 이동
│ │ │ ├── useAnnotations.ts # 주석 CRUD
│ │ │ └── useHls.ts # HLS 로드/에러 처리
│ │ ├── store/ # Zustand 스토어
│ │ ├── types/ # 공유 타입 정의
│ │ ├── utils/
│ │ │ ├── timecode.ts # 타임코드 변환 유틸
│ │ │ ├── vtt.ts # WebVTT 생성/파싱
│ │ │ └── frameCapture.ts # Canvas 프레임 캡처 (로컬 파일용)
│ │ └── App.tsx
│ ├── index.html
│ ├── vite.config.ts
│ └── tsconfig.json
│
├── server/ # Node.js 백엔드
│ ├── src/
│ │ ├── routes/
│ │ │ ├── stream.ts # Range Request 스트리밍
│ │ │ ├── hls.ts # HLS 변환/서빙
│ │ │ ├── frame.ts # 프레임 추출 API
│ │ │ ├── upload.ts # tus 업로드 연동
│ │ │ ├── local.ts # 로컬 파일 프록시 (선택)
│ │ │ └── annotations.ts # 주석 CRUD API
│ │ ├── services/
│ │ │ ├── ffmpeg.ts # FFmpeg spawn 래퍼 (HLS 변환, 프레임 추출)
│ │ │ ├── streaming.ts # Range Request 로직
│ │ │ └── storage.ts # 파일/DB 접근
│ │ ├── middleware/
│ │ │ ├── security.ts # Path traversal 방어, CORS
│ │ │ └── upload.ts # tus 서버 설정
│ │ ├── db/
│ │ │ └── schema.sql # SQLite 스키마
│ │ └── app.ts
│ └── tsconfig.json
│
├── shared/ # 클라이언트/서버 공유 타입
│ └── types.ts
│
├── storage/ # 런타임 생성 (gitignore)
│ ├── videos/ # 업로드된 원본 영상
│ ├── hls/ # HLS 세그먼트 (.m3u8, .ts)
│ ├── frames/ # 추출된 프레임 이미지
│ └── thumbnails/ # 탐색바 썸네일 스프라이트
│
├── CLAUDE.md # 프로젝트 가이드 (아키텍처, 규칙)
├── PLAN.md # 전체 구현 계획 + 단계별 세부 태스크
├── PROGRESS.md # 진행 상태 추적 (에이전트 실시간 업데이트)
├── README.md
└── package.json # 루트 (워크스페이스)
API 설계
스트리밍
GET /api/stream/:videoId # Range Request 스트리밍 (원본 즉시 재생)
GET /api/hls/:videoId/index.m3u8 # HLS 플레이리스트
GET /api/hls/:videoId/:segment # HLS 세그먼트 (.ts)
POST /api/hls/:videoId/convert # HLS 변환 시작 (비동기)
GET /api/hls/:videoId/progress # 변환 진행 상태 (SSE)
프레임 추출
GET /api/frame/:videoId?time=00:01:30.000 # 특정 시간 프레임
GET /api/frame/:videoId?frame=1234 # 프레임 번호로 추출
GET /api/thumbnails/:videoId/sprite.jpg # 탐색바 썸네일 스프라이트
GET /api/thumbnails/:videoId/sprite.vtt # 썸네일 위치 VTT
업로드 (tus 프로토콜)
# tus 엔드포인트 — @tus/server가 자동 처리
POST /api/upload # 업로드 생성 (tus Creation)
PATCH /api/upload/:uploadId # 청크 전송 (tus 자동 재개)
HEAD /api/upload/:uploadId # 업로드 상태 조회
DELETE /api/upload/:uploadId # 업로드 취소
주석/메모
GET /api/annotations/:videoId # 영상별 주석 목록
POST /api/annotations/:videoId # 주석 추가
PUT /api/annotations/:videoId/:id # 주석 수정
DELETE /api/annotations/:videoId/:id # 주석 삭제
GET /api/annotations/:videoId/export?format=vtt|srt|json|csv # 내보내기
메타데이터
GET /api/meta/:videoId # 영상 메타데이터 (fps, duration, resolution 등)
GET /api/videos # 업로드된 영상 목록
DELETE /api/videos/:videoId # 영상 삭제 (원본 + HLS + 프레임 + 썸네일 일괄)
핵심 구현 규칙
대용량 파일 처리
- Range Request:
fs.createReadStream({ start, end, highWaterMark: 1024 * 1024 })사용 - 응답 시 반드시
Accept-Ranges: bytes헤더 포함 (Safari seek 호환) stream/promises의pipeline()사용 (에러 핸들링 + 백프레셔 자동 처리)- 4GB 이상 파일: JavaScript
Number는2^53-1까지 안전 → BigInt 불필요 - HLS 변환은 원본 파일을 직접 수정하지 않고
storage/hls/{videoId}/에 출력 - 소스가 이미 H.264인 경우
-c copy로 재인코딩 없이 세그먼트 분할만 수행 (수 초 내 완료)
Range Request 구현 패턴
// 핵심: 한 응답에 전체 파일을 보내지 않고 청크 크기 제한
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
const start = requestedStart;
const end = Math.min(requestedEnd || (start + CHUNK_SIZE - 1), fileSize - 1);
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': end - start + 1,
'Content-Type': 'video/mp4',
});
HLS 변환 FFmpeg 명령
# H.264 소스 → 재인코딩 없이 세그먼트 분할 (빠름)
ffmpeg -i input.mp4 -c copy \
-f hls -hls_time 6 -hls_list_size 0 \
-hls_segment_filename 'output/segment%03d.ts' \
-hls_playlist_type vod \
output/index.m3u8
# 비-H.264 소스 → 트랜스코딩 (느림)
ffmpeg -i input.mkv \
-c:v libx264 -preset medium -crf 23 -profile:v high -level 4.1 \
-c:a aac -b:a 128k -ar 48000 \
-f hls -hls_time 6 -hls_list_size 0 \
-hls_segment_filename 'output/segment%03d.ts' \
-hls_playlist_type vod \
-force_key_frames "expr:gte(t,n_forced*2)" \
output/index.m3u8
FFmpeg spawn 래퍼 패턴
import { spawn } from 'child_process';
function runFFmpeg(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
let stderr = '';
proc.stderr.on('data', (chunk) => {
stderr += chunk.toString();
// 진행률 파싱: "time=00:01:30.00" 패턴
});
proc.on('close', (code) =>
code === 0 ? resolve() : reject(new Error(`FFmpeg exit ${code}: ${stderr.slice(-500)}`))
);
});
}
프레임 정확도
requestVideoFrameCallbackAPI로 실제 FPS 동적 감지 (fallback: 30fps)- 프레임 번호 → 시간 변환:
frameNumber / fps(currentTime 누적 연산 금지 — 부동소수점 드리프트) - 서버 FFmpeg 프레임 추출:
-accurate_seek -ss {time} -i {file} -frames:v 1순서 준수 - VFR 영상은 best-effort (FFprobe 메타데이터 기반 평균 FPS 사용)
타임코드 표현
- 내부 저장: 초 단위 float (예:
90.033) - 표시:
HH:MM:SS.mmm형식 - VTT 내보내기:
HH:MM:SS.mmm --> HH:MM:SS.mmm(점 사용) - SRT 내보내기:
HH:MM:SS,mmm --> HH:MM:SS,mmm(콤마 사용)
보안
- 서버 파일 경로:
path.resolve()정규화 후 허용 디렉토리(storage/videos/) 내부 검증 (Path Traversal 방어) - 업로드 파일 검증: MIME 타입 체크 + FFprobe로 실제 코덱/컨테이너 확인
- 허용 MIME:
video/mp4,video/quicktime,video/x-matroska,video/webm - 업로드 크기 상한: 환경변수
MAX_UPLOAD_SIZE(기본 20GB) - CORS 설정: 개발 시
localhost:5173↔localhost:3001허용 Cross-Origin-Opener-Policy/Cross-Origin-Embedder-Policy헤더 (SharedArrayBuffer 대비)
전체화면
document.fullscreenElement가<video>가 아닌 플레이어 컨테이너 div여야 오버레이 유지- Video.js
fluid: true모드로 컨테이너 기준 비율 유지 - Safari webkit 접두사:
webkitRequestFullscreen분기 불필요 (PC 웹 대상)
오버레이 성능
timeupdate이벤트에서 직접 DOM 조작 금지 (~250ms 간격, 부정확)requestVideoFrameCallback또는requestAnimationFrame루프로 현재 시간 읽고 표시할 주석 필터링- 주석 배열은
time기준 정렬 유지 → 이진 탐색 O(log n) 조회 - 오버레이 요소에
will-change: transform적용 (GPU 가속) - 동시 표시 오버레이 50개 미만 유지 (DOM 레이아웃 스래싱 방지)
메모리 관리
URL.createObjectURL()사용 후 반드시URL.revokeObjectURL()호출- Canvas 프레임 캡처 시 단일 Canvas 재사용 (매번 새로 생성 금지)
- hls.js
backBufferLength: 30필수 설정 - QuotaExceededError 발생 시:
abort()→remove(0, currentTime - 30)→ 재시도
자막형 vs 메모형 동작 정의
자막형 (Subtitle Mode)
- WebVTT
<track kind="subtitles">요소 사용 - 시작 시간 ~ 종료 시간 범위 표시
- 위치: 하단 고정 (WebVTT
position,line속성으로 조정 가능) - 내보내기:
.vtt,.srt
메모형 (Memo Mode)
- 커스텀 DOM 오버레이 레이어 + interact.js 드래그/리사이즈
- 특정 시점(포인트) + 표시 지속 시간 (기본 3초)
- 위치: 드래그로 자유 배치 (x%, y% 정규화 좌표 — 해상도 독립적)
- 크기: width%, height% 정규화 (리사이즈 가능)
- 내보내기:
.json,.csv
주석 데이터 모델 (SQLite)
{
"id": "uuid",
"videoId": "string",
"type": "subtitle | memo",
"timeStart": 90.033,
"timeEnd": 95.500,
"position": { "x": 50.0, "y": 75.0 },
"size": { "width": 30.0, "height": 10.0 },
"text": "주석 내용",
"style": {
"fontSize": 16,
"color": "#ffffff",
"backgroundColor": "rgba(0,0,0,0.7)"
},
"createdAt": "ISO-8601",
"updatedAt": "ISO-8601"
}
키보드 단축키
| 키 | 동작 | 비고 |
|---|---|---|
Space |
재생 / 일시정지 | 플레이어 포커스 시에만 |
← / → |
5초 뒤로 / 앞으로 | |
J / L |
10초 뒤로 / 앞으로 | |
, / . |
이전 프레임 / 다음 프레임 | 일시정지 상태에서만 |
[ / ] |
이전 장면 / 다음 장면 | |
F |
전체화면 토글 | |
M |
음소거 토글 | |
0~9 |
10% 단위 탐색 | |
Shift + S |
현재 프레임 캡처 | |
Shift + M |
현재 시간에 메모 추가 | |
+ / - |
재생 속도 증가 / 감소 |
텍스트 입력 모드(메모 작성 중)에서는 모든 플레이어 단축키 비활성화.
Video.js React 통합 패턴
// ref + useEffect 패턴 (Video.js 공식 권장)
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);
useEffect(() => {
if (!playerRef.current) {
const videoElement = document.createElement('video-js');
videoElement.classList.add('vjs-big-play-centered');
videoRef.current!.appendChild(videoElement);
playerRef.current = videojs(videoElement, options, onReady);
}
}, [options]);
useEffect(() => {
return () => {
if (playerRef.current && !playerRef.current.isDisposed()) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, []);
return <div data-vjs-player ref={videoRef} />;
data-vjs-player래퍼로 Video.js 추가 DOM 래핑 방지. React 18 Strict Mode에서playerRef.current가드 필수 (이중 초기화 방지).
환경변수 (server/.env)
PORT=3001
VIDEOS_DIR=../storage/videos
HLS_DIR=../storage/hls
FRAMES_DIR=../storage/frames
THUMBNAILS_DIR=../storage/thumbnails
DB_PATH=../storage/annotations.db
MAX_UPLOAD_SIZE=21474836480 # 20GB (bytes)
FFMPEG_PATH=ffmpeg # 시스템 PATH에 등록된 경우 기본값
HLS_SEGMENT_TIME=6 # HLS 세그먼트 길이 (초)
THUMBNAIL_INTERVAL=10 # 썸네일 추출 간격 (초)
개발 단계 (에이전트 계획)
세부 태스크와 완료 기준은
PLAN.md참조. 진행 상태는PROGRESS.md참조.
| 단계 | 내용 | 의존성 |
|---|---|---|
| 1. 프로젝트 구조 | 모노레포 세팅, TypeScript 설정, 폴더 골격, 패키지 설치 | — |
| 2. 백엔드 서버 | Express + Range Request 스트리밍 + FFmpeg spawn 래퍼 + tus 업로드 + HLS 변환 + SSE 진행률 | FFmpeg 설치 |
| 3. 기본 플레이어 | Video.js + 이중 경로 재생 (로컬 File API + 서버 HLS) + 기본 UI | 단계 2 |
| 4. 프레임 추출 | Canvas 즉시 미리보기 + 서버 FFmpeg API (정확 추출) | 단계 2, 3 |
| 5. 단위 이동 | 프레임/초/장면 seek + requestVideoFrameCallback FPS 감지 + 키보드 단축키 | 단계 3, 4 |
| 6. 텍스트 오버레이 | 자막형(WebVTT track) + 메모형(interact.js DOM 오버레이) + 내보내기(subsrt) | 단계 3 |
| 7. UI/UX 통합 | 전체 통합, 반응형 레이아웃, 썸네일 미리보기, 디스크 정리 | 단계 2~6 |
단계 1 → 단계 2 → 단계 4 ──┐
└──→ 단계 3 → 단계 5 ──┤→ 단계 7
→ 단계 6 ──┘
※ 단계 5와 6은 병렬 진행 가능
파일 정리 정책
- 업로드 실패/중단 임시 파일: 24시간 후 자동 삭제 (서버 시작 시 + 주기적 스캔)
- 영상 삭제 시: 원본 + HLS 세그먼트 + 프레임 + 썸네일 + 주석 DB 레코드 일괄 삭제
- 디스크 잔여 공간 10GB 미만 시 경고 (check-disk-space)
- 영상 1개당 예상 디스크 사용량: 원본 + HLS ≈ 원본 x 2 + 프레임/썸네일 ~50MB
알려진 제약 / 주의사항
- VFR 영상: 가변 프레임율 영상은 프레임 번호 계산 부정확 → FFprobe 평균 FPS 사용 (best-effort)
- CORS + Canvas: 크로스 오리진 영상에서
toDataURL()보안 오류 →crossorigin="anonymous"필수 - HLS 변환 시간: 2GB 영상 트랜스코딩 시 수 분~십수 분 소요 → SSE 진행률로 클라이언트 통보
- H.264 소스 감지: FFprobe로 코덱 확인 후
-c copy(재인코딩 불필요) vs 트랜스코딩 분기 - WebVTT 리전: Chrome/Edge에서 미지원 → 리전 기능 사용 금지, 기본 positioning만 사용
- Video.js v10: 2026 중반 GA 예정, 플러그인 호환 불확실 → 플레이어 로직을 훅으로 분리하여 마이그레이션 비용 최소화