Files
dronevideoplayer/CLAUDE.md
minsung 2aae3d1c0d feat: StationOverlay 렌더링 최적화 및 스무딩 적용 close #1
- 텍스트(측점/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>
2026-04-01 15:11:39 +09:00

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를 업데이트
  • 실패 시 원인과 시도한 방법을 기록하여 다음 에이전트가 같은 실수를 반복하지 않도록 함
  • 이전 에이전트가 남긴 "다음 단계 참고"를 반드시 확인 후 작업 시작

에이전트 간 인계 규칙

  1. 이전 단계 산출물 검증: 코드가 빌드/실행 가능한 상태인지 확인 후 작업 시작
  2. 기존 코드 존중: 이전 에이전트가 작성한 코드를 이유 없이 대폭 변경하지 않음
  3. 충돌 방지: 같은 파일을 동시에 수정하는 병렬 에이전트 실행 금지
  4. 이슈 에스컬레이션: 블로커 발견 시 PROGRESS.md에 기록하고 사용자에게 보고

프로젝트 개요

웹 브라우저(PC)에서 2GB 이상의 대용량 동영상을 안정적으로 재생하는 특화 기능 탑재 플레이어. 단순 재생을 넘어 프레임 추출, 단위 이동, 텍스트 오버레이 등 영상 분석/편집 보조 도구로 기능.

핵심 요구사항

  1. 2GB 이상 동영상 안정 재생 (서버 파일 + 로컬 파일 모두)
  2. 프레임 단위 추출 (정확도 우선)
  3. 입력 단위 기준 앞뒤 이동 (프레임 / 초 / 장면)
  4. 영상 위 텍스트 입력 (자막형 + 메모형 병행 지원)
  5. 확장 가능한 플러그인 아키텍처

설계 결정 (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/promisespipeline() 사용 (에러 핸들링 + 백프레셔 자동 처리)
  • 4GB 이상 파일: JavaScript Number2^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)}`))
    );
  });
}

프레임 정확도

  • requestVideoFrameCallback API로 실제 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:5173localhost: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 예정, 플러그인 호환 불확실 → 플레이어 로직을 훅으로 분리하여 마이그레이션 비용 최소화