다운로드 엔진 아키텍처
FlowPick의 다운로드 엔진은 전체 시스템의 핵심으로, 스트리밍 세그먼트를 다운로드, 복호화, 병합하여 디스크에 기록하는 역할을 담당합니다. 본 문서는 내부 메커니즘을 깊이 이해하고자 하는 개발자와 고급 사용자를 대상으로 합니다.

아키텍처 개요
다운로드 엔진은 세 개의 핵심 모듈로 구성되며, 데이터가 파이프라인 방식으로 각 모듈을 순차적으로 통과합니다:
┌─────────────────────────────────────────────────┐
│ 다운로드 엔진 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 세그먼트 │→│ 세그먼트 │→│ 파일 쓰기 │ │
│ │ 다운로드 │ │ 처리 │ │ 모듈 │ │
│ │ 모듈 │ │ 모듈 │ │ │ │
│ │ │ │ │ │ │ │
│ │ · 동시 │ │ · 복호화 │ │ · FSA 스트림 │ │
│ │ · 재시도 │ │ · 연결 │ │ · StreamSaver │ │
│ │ · 속도 제한│ │ · 리무싱 │ │ · Blob 최후 │ │
│ └──────────┘ └──────────┘ └───────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 메모리 안전 관리자 │ │
│ │ · 크기 예측 · 임계값 검사 · 전략 선택 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
데이터 흐름 전경
사용자가 다운로드 클릭부터 파일 디스크 기록까지의 완전한 체인:
사용자 다운로드 클릭
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. 플레이리스트 │ ──→ │ 2. 세그먼트 │ ──→ │ 3. 세그먼트 │
│ 파싱 │ │ 다운로드 │ │ 처리 │
│ │ │ │ │ │
│ · M3U8 다운로드│ │ · Worker Pool│ │ · AES 복호화 │
│ · 세그먼트 리스트│ │ · 동시 제어 │ │ · TS 연결 │
│ 파싱 │ │ · 지수 백오프 │ │ · FFmpeg 리무 │
│ · 키 추출 │ │ · 진행률 보고 │ │ · 오디오/비디오│
│ · 크기 예측 │ │ │ │ 병합 │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 6. 완료 알림 │ ←── │ 5. 정리 회수 │ ←── │ 4. 파일 쓰기 │
│ │ │ │ │ │
│ · 데스크톱 알림│ │ · 메모리 해제 │ │ · FSA 스트림 │
│ · 경로 복사 │ │ · 임시 파일 │ │ · StreamSaver│
│ · 큐 진행 │ │ 정리 │ │ · Blob 최후 │
│ │ │ · 상태 리셋 │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
엔진 생명주기
다운로드 엔진의 한 번의 완전한 실행은 다음 상태 전환을 거칩니다:
[유휴]
│ 사용자 다운로드 트리거
▼
[초기화] ──── WASM 로딩, API 가용성 확인, 쓰기 전략 선택
│
▼
[플레이리스트 파싱] ── M3U8/MPD 다운로드 및 파싱, 세그먼트 리스트와 키 추출
│
▼
[세그먼트 다운로드] ── Worker Pool 동시 다운로드, 실시간 진행률 보고
│
▼
[세그먼트 처리] ── 복호화 → 연결/리무싱 → 디스크 쓰기
│
├── 성공 → [정리] → [완료] → [유휴]
│
├── 취소 → [정리] → [유휴]
│
└── 실패 → [정리] → [에러] → [유휴]
각 상태 전환은 해당하는 생명주기 훅을 트리거하며, UI 계층은 이 훅들을 감시하여 인터페이스 상태를 업데이트합니다. UI와 엔진의 상호작용 상세는 프로젝트 아키텍처 — 데이터 흐름을 참조하세요.
세그먼트 다운로드 모듈
동시 다운로더
세그먼트 다운로드는 Worker Pool 패턴으로 동시 제어를 구현합니다:
const downloadSegmentsConcurrently = async (
segments: SegmentInfo[],
onSegmentDownloaded: (buffer: ArrayBuffer, index: number) => void
) => {
const concurrency = Math.min(config.concurrency, 8)
let nextIndex = 0
const worker = async () => {
while (nextIndex < segments.length) {
const index = nextIndex++
const buffer = await downloadWithRetry(segments[index]!)
onSegmentDownloaded(buffer, index)
}
}
const workers = Array.from(
{ length: Math.min(concurrency, segments.length) },
() => worker()
)
await Promise.all(workers)
}
설계 포인트:
- 공유
nextIndex카운터로 작업 할당, 미리 분할导致的 부하 불균형 방지 - Worker 수가 세그먼트 총수를 초과하지 않아 유휴 Worker 생성 방지
- 각 Worker가 독립 실행, 단일 세그먼트 실패가 다른 Worker에 영향 없음
Worker Pool 심층
Worker Pool 패턴의 핵심 장점은 동적 부하 균형에 있습니다. 미리 세그먼트 배열을 N등분하는 것과 달리, 공유 카운터는 다음을 보장합니다:
미리 분할(비권장):
Worker 1: [세그먼트 0-49] ← 이 세그먼트들이 크면 Worker 1이 병목이 됨
Worker 2: [세그먼트 50-99] ← 조기 완료 후 유휴 대기
공유 카운터(FlowPick 채택):
Worker 1: 세그먼트 0 → 세그먼트 3 → 세그먼트 5 → ...
Worker 2: 세그먼트 1 → 세그먼트 4 → 세그먼트 7 → ...
Worker 3: 세그먼트 2 → 세그먼트 6 → 세그먼트 8 → ...
각 Worker는 현재 세그먼트를 완료한 즉시 다음 미할당 세그먼트를 획득하며, 모든 세그먼트 처리 완료까지 반복합니다. 이 패턴은 세그먼트 크기가 불균일한 시나리오(HLS 스트림의 처음/끝 세그먼트는 보통 작고, 중간 세그먼트는 큼)에 자연스럽게 적응합니다.
config.concurrency로 제어되며, 상한은 8입니다. 사용자는 설정 참조에서 기본값을 조정하거나, 다운로드 인터페이스에서 실시간 수정할 수 있습니다. 일괄 다운로드 시나리오의 큐 스케줄링에 대해서는 일괄 다운로드 — 동시 수 선택 가이드를 참조하세요.비동기 생성자 패턴
초대용량 파일(세그먼트 수 > 500)의 경우, 엔진은 비동기 생성자(AsyncGenerator) 패턴을 사용하여 모든 세그먼트 데이터를 한 번에 메모리에 로드하는 것을 피합니다:
async function* segmentGenerator(
segments: SegmentInfo[],
signal?: AbortSignal
): AsyncGenerator<ArrayBuffer> {
for (const segment of segments) {
if (signal?.aborted) break
const buffer = await downloadWithRetry(segment)
yield buffer
}
}
배열 패턴과의 차이:
| 차원 | 배열 패턴 | 생성자 패턴 |
|---|---|---|
| 메모리 피크 | 모든 세그먼트가 동시에 메모리에 존재 | 현재 처리 중인 세그먼트만 메모리에 존재 |
| 적합 시나리오 | 세그먼트 수 < 500 | 세그먼트 수 > 500 |
| 진행률 추적 | 총수 알려짐, 정확한 퍼센트 | 총수 알려짐, 정확한 퍼센트 |
| 취소 응답 | 현재 배치 완료 대기 필요 | 즉시 응답 |
엔진은 세그먼트 수에 따라 자동으로 패턴을 선택하며, 사용자 개입 불필요합니다. 초대용량 파일 다운로드의 실제 시나리오는 라이브 방송 재생 저장를 참조하세요.
재시도 메커니즘
세그먼트 다운로드 실패 시 지수 백오프 재시도를 채택합니다:
const downloadWithRetry = async (
segment: SegmentInfo,
maxRetries = 3
): Promise<ArrayBuffer> => {
let lastError: unknown
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await downloadAndDecryptSegment(segment)
} catch (e) {
lastError = e
if (attempt < maxRetries - 1) {
// 지수 백오프: 400ms, 800ms, 1600ms
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 400))
}
}
}
throw lastError
}
재시도 전략:
| 시도 횟수 | 지연 | 누적 대기 |
|---|---|---|
| 1차 실패 | 400ms | 400ms |
| 2차 실패 | 800ms | 1200ms |
| 3차 실패 | 1600ms | 2800ms |
재시도 트리거하지 않는 경우:
- HTTP 4xx 클라이언트 에러(403, 404 등) — 재시도 무의미
- CORS 에러 — 정책 문제, 재시도로 결과 변경 불가
- 암호화 미지원 에러 — 재시도로 해결 불가능
에러 분류
class FetchError extends Error {
constructor(
message: string,
public status: number,
public url: string
) {
super(message)
this.name = 'FetchError'
}
}
에러는 HTTP 상태 코드별로 분류 처리됩니다:
| 상태 코드 | 에러 유형 | 사용자 프롬프트 |
|---|---|---|
| 403 | 인증/인증 실패 | "접근 거부됨, URL 유효성 확인하세요" |
| 404 | 리소스 없음 | "세그먼트를 찾을 수 없음, 스트림이 만료되었을 수 있음" |
| 502/503/504 | 서버 에러 | "서버를 일시적으로 사용할 수 없음, 나중에 다시 시도하세요" |
| CORS | 크로스 오리진 제한 | "크로스 오리진 요청이 차단됨, 확장 버전 사용 권장" |
| Network | 네트워크 단절 | "네트워크 연결 실패, 네트워크 확인하세요" |
webRequest 권한으로 동일 출처 정책을 우회할 수 있습니다. 더 많은 해결 방법은 일반적인 문제 해결를 참조하세요. CORS의 기술적 원리와 제한에 대해서는 알려진 제한 — 브라우저 제한를 참조하세요.세그먼트 처리 모듈
AES-128 복호화
암호화된 HLS 스트림의 경우, FlowPick은 Web Crypto API를 사용하여 브라우저에서 복호화합니다:
const decryptAES128 = async (
encryptedData: ArrayBuffer,
key: ArrayBuffer,
iv?: Uint8Array
): Promise<ArrayBuffer> => {
const keyBytes = await crypto.subtle.importKey(
'raw', key,
{ name: 'AES-CBC' },
false,
['decrypt']
)
const ivBuffer = iv ? new Uint8Array(iv) : new Uint8Array(16)
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv: ivBuffer },
keyBytes,
encryptedData
)
return decrypted
}
복호화 흐름:
- M3U8의
#EXT-X-KEY태그에서 키 URI와 IV 추출 - 키 파일 다운로드(보통 16바이트 이진 파일)
- AES-128-CBC 모드로 각 세그먼트 복호화
- IV가 지정되지 않은 경우, 세그먼트 순번을 IV로 사용(HLS 규격 기본 동작)
성능 고려사항:
- Web Crypto API는 하드웨어 가속을 사용하므로, 복호화 속도는 보통 병목이 아님
- 각 세그먼트가 독립적으로 복호화되므로, 다운로드와 병행 가능
- 키는 한 번만 다운로드하면 되며, 메모리에 캐싱
TS 세그먼트 연결
TS 출력 포맷의 경우, 세그먼트를 직접 이진 연결합니다:
// 의사 코드
const merged = new Uint8Array(totalSize)
let offset = 0
for (const segment of segments) {
merged.set(new Uint8Array(segment), offset)
offset += segment.byteLength
}
이 방식은 CPU 오버헤드가 제로이며, 속도는 메모리 복사 속도에만 의존합니다.
FFmpeg WASM 리무싱
MP4 출력 포맷의 경우, FFmpeg WASM을 사용하여 컨테이너 변환을 수행합니다. 상세 내용은 포맷 변환 문서를 참조하세요.
DASH 스트림의 특수 처리
DASH 스트림은 HLS 스트림과 세그먼트 처리에서 현저한 차이가 있습니다:
HLS 스트림 처리:
세그먼트 0 → 세그먼트 1 → 세그먼트 2 → ... → 연결 → 출력
DASH 스트림 처리(오디오/비디오 분리):
초기화 세그먼트 → 비디오 세그먼트 0 → 비디오 세그먼트 1 → ... ─┐
├→ FFmpeg 병합 → 출력
초기화 세그먼트 → 오디오 세그먼트 0 → 오디오 세그먼트 1 → ... ─┘
DASH의 FMP4(Fragmented MP4) 포맷은 특수 처리가 필요합니다:
- 초기화 세그먼트(
ftyp+moovbox)는 반드시 파일 선두에 위치해야 함 - 미디어 세그먼트(
moof+mdatbox)는 순서대로 추가 - 오디오/비디오 분리 시, 비디오와 오디오 트랙을 각각 다운로드한 후 마지막에 FFmpeg로 병합
파일 쓰기 모듈
쓰기 모듈은 우선순위로 전략을 선택하여, 가능한 많은 브라우저에서 작동하도록 보장합니다.

전략 1: File System Access API(최적)
async function createFSAStream(
filename: string,
dirHandle?: FileSystemDirectoryHandle | null
): Promise<WritableStream<Uint8Array>> {
let handle: FileSystemFileHandle
if (dirHandle) {
// 이미 디렉토리 권한 있음, 바로 파일 생성
handle = await dirHandle.getFileHandle(filename, { create: true })
} else {
// 팝업으로 사용자에게 저장 위치 선택 요청
handle = await window.showSaveFilePicker!({
suggestedName: filename,
types: [{
description: 'Video',
accept: { 'video/mp4': ['.mp4'] }
}]
})
}
const writable = await handle.createWritable()
return new WritableStream<Uint8Array>({
async write(chunk) { await writable.write(chunk) },
async close() { await writable.close() },
async abort(reason) { await writable.abort(reason) }
}, {
highWaterMark: 16 * 1024 * 1024 // 16MB 버퍼
})
}
장점:
- 스트리밍 쓰기, 메모리 점유 일정(단 16MB 버퍼만)
- 임의 크기의 파일 지원
- 디렉토리 영속화 후 반복 팝업 불필요
제한:
- Chrome 86+ 및 Edge 86+ 만 지원
- 사용자 제스처 필요(
showDirectoryPicker는 사용자 클릭 이벤트에서 호출해야 함)
전략 2: StreamSaver.js(대안)
async function createStreamSaverStream(
filename: string,
fileSize?: number
): Promise<WritableStream<Uint8Array>> {
const ss = await getStreamSaver()
const fileStream = ss.createWriteStream(filename, { size: fileSize })
return fileStream
}
장점:
- 스트리밍 쓰기, 메모리 점유 낮음
- FSA API보다 호환성 양호
제한:
- Service Worker 지원 필요
mitm.html과streamsaver-sw.js올바른 배포 필요- 일부 기업 네트워크 환경에서 Service Worker 차단 가능
전략 3: Blob 최후 수단
// 의사 코드
const blob = new Blob([mergedData], { type: 'video/mp4' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
제한:
- 전체 파일이 메모리에 로드됨
- 하드 제한 1.5GB(
MEMORY_CONFIG.maxBlobSize) - 800MB 초과 시 콘솔 경고(
MEMORY_CONFIG.warnBlobSize)
전략 비교 요약
| 차원 | FSA API | StreamSaver.js | Blob |
|---|---|---|---|
| 메모리 점유 | 16MB(일정) | 낮음(스트리밍) | 파일 크기와 동일 |
| 파일 크기 상한 | 무제한 | 무제한 | 1.5GB |
| 브라우저 요구사항 | Chrome/Edge 86+ | Service Worker 지원 | 모든 브라우저 |
| 디렉토리 영속화 | 지원 | 지원 안 함 | 지원 안 함 |
| 다운로드 경험 | 최상 | 양호 | 일반 |
메모리 안전 관리

크기 예측
다운로드 시작 전, 엔진은 샘플링으로 총 파일 크기를 예측합니다:
async function estimateSegmentsSize(
segments: ArrayBuffer[] | AsyncGenerator<ArrayBuffer>,
totalCount: number
): Promise<{ estimatedSize: number; isEstimate: boolean }> {
// 샘플링 수량: min(5, max(1, totalCount * 0.1))
const sampleCount = Math.min(5, Math.max(1, Math.floor(totalCount * 0.1)))
// 처음 몇 세그먼트 다운로드 및 평균 크기 계산
let sampledTotal = 0
for (let i = 0; i < sampleCount; i++) {
sampledTotal += sampleSegment.byteLength
}
const avgSample = sampledTotal / sampleCount
return {
estimatedSize: Math.round(avgSample * totalCount),
isEstimate: true
}
}
샘플링 전략 설명:
- 샘플링 수량은
min(5, max(1, 세그먼트 총수 × 10%))를 취함, 작은 스트림은 최소 1개 샘플링, 큰 스트림은 최대 5개 샘플링 보장 - 샘플링 결과는
isEstimate: true로 표시, UI 계층은 이에 따라 "약 XX MB"를 정확한 값 대신 표시 - HLS 스트림의 경우, Master Playlist에
BANDWIDTH속성이 선언되어 있다면, 엔진은 이 값을 우선 예측 참조로 사용
전략 선택 논리
예상 크기 < 800MB → 임의 전략 가능
예상 크기 800MB-1.5GB → 우선 스트리밍 쓰기, Blob 모드 경고 표시
예상 크기 > 1.5GB → 강제 스트리밍 쓰기, Blob 모드 거부
메모리 설정 상수
const MEMORY_CONFIG = {
maxBlobSize: 1500 * 1024 * 1024, // 1.5GB 하드 제한
warnBlobSize: 800 * 1024 * 1024, // 800MB 경고 임계값
}
런타임 메모리 모니터링
다운로드 전 예측 외에, 엔진은 실행 중에도 지속적으로 메모리 압력을 모니터링합니다:
const checkMemoryPressure = (): 'normal' | 'warning' | 'critical' => {
// 현재 할당된 세그먼트 버퍼 총 크기 확인
const allocatedMB = allocatedBuffers.reduce((sum, buf) => sum + buf.byteLength, 0) / 1048576
if (allocatedMB > 1200) return 'critical'
if (allocatedMB > 600) return 'warning'
return 'normal'
}
메모리 압력이 critical 레벨에 도달하면, 엔진은:
- 새로운 세그먼트 다운로드 요청 일시 중지
- 이미 다운로드된 세그먼트를 우선 디스크에 기록
- 쓰기 완료된 세그먼트 버퍼 해제
진행률 추적
진행률 계산 모델
엔진은 다단계 진행률 모델을 사용하여 사용자에게 정확한 피드백을 제공합니다:
총 진행률 = (파싱 단계 % × 파싱 가중치)
+ (다운로드 단계 % × 다운로드 가중치)
+ (처리 단계 % × 처리 가중치)
+ (쓰기 단계 % × 쓰기 가중치)
기본 가중치 배분:
| 단계 | 가중치 | 비율 | 설명 |
|---|---|---|---|
| 플레이리스트 파싱 | 0.05 | 5% | 빠르게 완료, 상대적으로 적은 시간 소요 |
| 세그먼트 다운로드 | 0.80 | 80% | 가장 시간 소요, 네트워크 대역폭 의존 |
| 세그먼트 처리 | 0.10 | 10% | 복호화/병합, CPU 성능 의존 |
| 파일 쓰기 | 0.05 | 5% | 스트리밍 쓰기는 빠름, Blob은 일시적 정체 가능 |
실시간 속도 추정
다운로드 속도는 슬라이딩 윈도우(Sliding Window) 알고리즘으로 추정합니다:
class SpeedEstimator {
private samples: Array<{ timestamp: number; bytes: number }> = []
private windowSize = 10000 // 10초 윈도우
addSample(bytes: number): void {
const now = Date.now()
this.samples.push({ timestamp: now, bytes })
// 윈도우 외 샘플 제거
while (this.samples.length > 0 && now - this.samples[0]!.timestamp > this.windowSize) {
this.samples.shift()
}
}
getSpeed(): number {
if (this.samples.length < 2) return 0
const oldest = this.samples[0]!
const newest = this.samples[this.samples.length - 1]!
const timeDiff = newest.timestamp - oldest.timestamp
const totalBytes = newest.bytes - oldest.bytes
return (totalBytes / timeDiff) * 1000 // bytes/sec
}
}
알고리즘 특징:
- 최근 10초 데이터만 사용, 과거 데이터의 영향 제거
- 순간 급증/급감 평활화
- 네트워크 변동에 민감하게 반응
남은 시간 계산
남은 시간(ETA)은 현재 속도와 남은 데이터량으로 계산:
ETA = (총 바이트 - 다운로드된 바이트) / 현재 속도
특수 처리:
- 초기 다운로드 단계(샘플 부족): "속도 산출 중..." 표시
- 속도가 0에 가까울 때: ETA 표시 안 함("계산 중...")
- 네트워크 중단 시: 마지막 유효 ETA 유지, "네트워크 불안정" 표시
UI 업데이트 주기
진행률 데이터의 UI 업데이트는 요청 애니메이션(requestAnimationFrame)으로 제어:
function updateProgressUI(progress: ProgressData): void {
requestAnimationFrame(() => {
progressBar.value = progress.percentage
speedEl.textContent = formatSpeed(progress.speed)
etaEl.textContent = formatTime(progress.eta)
})
}
설계 이유:
- UI 새로고침 빈도 제어(보통 60fps)
- 과도한 DOM 조작 방지
- 배터리 절약(특히 노트북)
취소 및 정리
취소 메커니즘
사용자가 다운로드를 취소하면, 엔진은 즉시 정리 프로세스를 시작합니다:
async function cancelDownload(abortController: AbortController): Promise<void> {
// 1. AbortSignal 발신
abortController.abort()
// 2. 진행 중인 fetch 요청 취소
// (AbortSignal이 fetch에 전달되어 있음)
// 3. Worker Pool 중지
// (다음 세그먼트 획득 시 aborted 체크)
// 4. 임시 파일 정리
await cleanupTempFiles()
// 5. 메모리 해제
releaseBuffers()
}
취소 보장:
AbortController를 통해 진행 중인 네트워크 요청 즉시 취소- Worker Pool은 다음 작업 획득 시 취소 상태 확인
- 이미 다운로드된 세그먼트는 폐기(미완성 파일로 남지 않음)
- 디스크 공간 회수
정리 프로세스
다운로드 완료 또는 취소 후, 엔진은 다음 정리를 실행:
- 메모리 해제: 모든 세그먼트 버퍼 배열 참조 삭제(GC 허용)
- 임시 파일 삭제: 부분 다운로드된 파일 제거
- WASM 인스턴스 해제: FFmpeg WASM 메모리 회수
- 상태 리셋: 엔진 상태를
[유휴]로 복귀 - 이벤트 발생: UI 계층에 완료/취소/실패 이벤트 통지
성능 최적화
네트워크 계층 최적화
HTTP 연결 재사용
엔진은 동일 출처 세그먼트에 대해 HTTP/2 멀티플렉싱을 활용하여 연결 오버헤드를 줄입니다:
// 같은 도메인의 세그먼트는 자동으로 연결 공유
// (브라우저 fetch API 내장 최적화)
병렬 다운로드 제어
동시 수는 자동 조정됩니다:
- 기본값: 6개 동시 연결
- 상한: 8개(브라우저 동일 도메인 제한 고려)
- 사용자 설정 가능: 설정 참조에서 수정
속도 제한
선택적 속도 제한 기능:
const throttleDownload = async (
url: string,
maxBytesPerSec: number
): Promise<ArrayBuffer> => {
const response = await fetch(url)
const reader = response.body!.getReader()
const chunks: Uint8Array[] = []
let totalBytes = 0
const startTime = Date.now()
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value!)
totalBytes += value!.length
// 속도 초과 시 대기
const elapsed = Date.now() - startTime
const expected = (totalBytes / maxBytesPerSec) * 1000
if (elapsed < expected) {
await sleep(expected - elapsed)
}
}
return concatenate(chunks)
}
메모리 최적화
스트리밍 처리
가능한 한 세그먼트를 스트리밍 방식으로 처리:
다운로드 → [버퍼] → 복호화 → [버퍼] → 디스크 쓰기 → 버퍼 해제
각 세그먼트 처리 완료 후 즉시 메모리 해제, 누적 방지.
WASM 메모리 관리
FFmpeg WASM의 메모리 사용 최적화:
// FFmpeg 입력용 원시 버퍼 직접 사용
// (불필요한 복사 방지)
const ffmpeg = createFFmpeg({ log: true })
// TS → MP4 변환 시:
// 1. 세그먼트를 FFmpeg의 virtual filesystem에 쓰기
// 2. FFmpeg 실행
// 3. 출력 파일 읽기
// 4. virtual filesystem 정리
CPU 최적화
Web Worker 오프로ading
무거운 작업을 메인 스레드에서 분리:
| 작업 | 실행 위치 | 이유 |
|---|---|---|
| 세그먼트 다운로드 | Web Worker | 메인 스레드 차단 방지 |
| AES 복호화 | Web Worker | Crypto API는 Worker에서도 사용 가능 |
| FFmpeg 실행 | Web Worker (WASM) | WASM은 기본적으로 Worker에서 실행 |
| 진행률 계산 | 메인 스레드 | UI 업데이드 필요 |
비차단 UI
모든 장기 작업은 await와 비동기 패턴을 사용하여 UI 응답성 유지:
// 좋음: 비차단
async function startDownload() {
updateUI('downloading')
await engine.download()
updateUI('complete')
}
// 나쁨: 차단
function startDownload() {
updateUI('downloading')
engine.download() // 메인 스레드 차단!
updateUI('complete')
}
로깅 및 디버깅
로그 레벨
엔진은 다음 로그 레벨을 지원합니다:
| 레벨 | 용도 | 예시 |
|---|---|---|
error | 복구 불가능한 에러 | 세그먼트 다운로드 3회 실패 |
warn | 복구 가능한 문제 | 세그먼트 재시도 1회 |
info | 중요 이벤트 | 다운로드 시작/완료 |
debug | 상세 정보 | 각 세그먼트 다운로드 시작 |
개발자 도구 통합
브라우저 개발자 도구 Console에서 엔진 상태 조회 가능:
// FlowPick DevTools 확장(있는 경우)
window.__FLOWPICK_ENGINE__.getStatus()
// => { state: 'downloading', progress: 45, speed: '2.3 MB/s' }
// 또는 Console 직접 로그 확인
// [FlowPick] [INFO] Download started: video.m3u8
// [FlowPick] [WARN] Segment #42 retry 1/3
// [FlowPick] [ERROR] Segment #42 failed after 3 retries
관련 문서
- 설치 가이드 — 환경 요구사항 및 종속성
- 온라인 도구 — 웹 버전의 엔진 사용 방식
- 브라우저 호환성 — API 지원 및 강등 행동
- 포맷 변환 — FFmpeg WASM 및 리무싱 상세
- 비디오 스니핑 — 플레이리스트 파싱 및 암호화 감지
- 일괄 다운로드 — 큐 스케줄링 및 동시 제어
- 개인정보 보호와 보안 — 로컬 처리 및 데이터 보안
- 프로젝트 아키텍처 — 시스템 전체 구조 및 데이터 흐름
- 라이브 방송 재생 저장 — 초대용량 파일 시나리오
- 일반적인 문서 해결 — 다운로드 실패 진단
- 알려진 문제 — 엔진 제한 및 알려진 버그
- 자주 묻는 질문 — 다운로드 관련 고빈도 질문