stories

코드 오픈소스로 공개했습니다! 3개월 만에 FlowPick을 만든 이야기

불편해서 직접 만들기로 한 독립 개발자가 문제 발견부터 오픈소스 공개까지의 전 과정을 기록합니다. webRequest 감지, FFmpeg WASM 삽질기, TSToMP4Muxer 자체 개발, 3단계 디스크 쓰기 전략까지 실제 개발 스토리입니다.
FlowPick 팀
16분 분량
# 오픈소스 # 독립 개발 # wasm # 아키텍처 # 개발 로그

오늘 코드를 GitHub에 올렸습니다. 저장소는 공개 상태입니다.

이 글은 자랑하려는 게 아니라, 지난 3개월 동안 겪었던 문제들을 기록해두려는 목적이 큽니다. 나중에 제가 까먹었을 때 찾아보기도 하고, 비슷한 고민을 하는 분들께 참고가 되길 바랍니다. 기술적인 내용도 다루지만, 지나치게 딱딱하게 쓰진 않겠습니다. 사람 말로 풀어낸 이야기입니다.


계기: 몇 달간 참았던 불편함

작년 말, 온라인 강의 플랫폼에서 기술 강의 시리즈를 듣고 있었습니다. 플랫폼은 다운로드를 막아놨지만, 저는 오프라인으로 보고 싶었습니다. 지하철에선 신호가 안 잡히고, 카페 와이파이는 불안정하고, 집에 가는 길에 특정 구간만 다시 보고 싶은데 매번进度条를 끌기도 귀찮았거든요.

Video DownloadHelper를 써봤습니다. HLS 스트림은 CoApp 설치가 필요하더군요. CoApp을 설치했더니, 암호화된 강의에서 바로 에러가 났습니다. 온라인 툴도 몇 개 찾아봤는데, 쓸만한 건 속도 제한이 있거나 파일이 커지면 터지거나, 서버를 경유하는 방식이라 찜찜했습니다.

youtube-dl로 일부는 처리됐지만, 그 플랫폼은 yt-dlp가 아예 인식하지 못했습니다. extractor가 없거나, 쿠키를 전달할 수 없었습니다.

이렇게 몇 달을 끌다가, 어느 주말 문득 생각이 들었습니다. "M3U8 파싱하고, 세그먼트 다운로드하고, 파일 합치는 게 전부잖아? 이걸 브라우저에서 할 수 있을까?"

MDN을 뒤져보니 Web Crypto API, File System Access API, WebAssembly 이 세 가지가 최근 몇 년 사이에 안정화됐고, Chrome 호환성도 문제없었습니다. 그럼 만들어보자.

첫째 주: "감지가 되나?"

어떤 도구든 가장 먼저 해결해야 할 문제는: 페이지에 어떤 영상이 있는지 알아내는 것입니다.

브라우저 확장 프로그램에는 webRequest라는 API가 있습니다. 탭에서 발생하는 모든 네트워크 요청을 감시할 수 있습니다. DOM 레벨이 아니라 네트워크 레벨에서 말이죠. 플레이어가 M3U8이나 MPD 요청을 보내는 순간, 확장 프로그램이 가로챌 수 있습니다.

Extension → webRequest.onBeforeRequest
         → Content-Type 필터링
         → application/x-mpegurl 매칭 → HLS로 기록
         → application/dash+xml 매칭  → DASH로 기록
         → video/* 매칭               → 직접 링크 영상으로 기록

이 부분은 예상보다 빨리 끝났습니다. 이틀 만에 현재 페이지의 모든 영상 스트림을 확장 프로그램 팝업에 나열하는 프로토타입이 완성됐습니다. 빌리빌리에서 테스트해보니, 영상 스트림, 오디오 스트림, 광고 스트림이 전부 뜨더군요. URL과 파일 크기까지 표시됐습니다.

세부 사항 하나가 저를 꽤 괴롭혔습니다. M3U8에는 Master Playlist와 Media Playlist 두 종류가 있습니다. Master는 디렉터리 역할로, 사용 가능한 모든 화질을 나열하고 각각이 Media Playlist를 가리킵니다. Media Playlist가 진짜 세그먼트 목록입니다. 처음에는 둘 다 표시했더니, 사용자 입장에선 뭘 골라야 할지 모르는 항목만 잔뜩 보였습니다. 나중에 Master Playlist를 인식하면 자동으로 화질 옵션을 파싱해서, 사람이 이해할 수 있는 해상도 목록만 보여주고 원시 URL 계층은 노출하지 않도록 바꿨습니다.

셋째 주: 첫 번째 진짜 난제 — 파일을 어디에 쓸 것인가

수백 개의 세그먼트를 다운로드하는 건 비동기 동시성 문제라 어렵지 않았습니다. 진짜 문제는 이 데이터를 어디에 쓸 것인가였습니다.

브라우저 JavaScript는 파일 시스템에 직접 접근할 권한이 없습니다. 가장 간단한 방법은 모든 데이터를 하나의 Blob으로 모아서 다운로드를 트리거하는 것입니다. URL.createObjectURL(blob)과 숨겨진 <a> 태그를 조합하는 방식이죠.

이 방법으로 프로토타입을 먼저 돌려봤습니다. 500MB 영상은 괜찮았습니다. 800MB도 버텼습니다. 1GB에서 Chrome이 살짝 버벅이기 시작했습니다. 이유는 명확합니다. Blob 모드는 전체 파일을 메모리에 올려야 합니다. 1GB 영상이면 브라우저가 1GB의 ArrayBuffer를 동시에 들고 있어야 하고, 페이지와 확장 프로그램 오버헤드까지 더하면 메모리 압박이 상당합니다. 1.5GB 이상에서는 그냥 멈춰버렸습니다.

Blob 모드에 하드 리밋을 걸었습니다. 1.5GB는 처리 거부, 800MB부터 경고. 이건 문제를 해결한 게 아니라, 문제의 경계선을 표시한 것뿐입니다.

진짜 해결책은 File System Access API(FSA) 였습니다. Chrome 86부터 지원하며, JavaScript가 파일 핸들을 얻어 WritableStream으로 지속적으로 써넣을 수 있게 해줍니다. 마치 네이티브 프로그램이 파일을 쓰는 것처럼요. 메모리에는 현재 쓰고 있는 데이터 블록만 존재하고, 파일 총 크기와는 무관합니다.

const writable = await handle.createWritable()
// 16MB 버퍼 설정
const stream = new WritableStream({
  async write(chunk) { await writable.write(chunk) },
  async close() { await writable.close() }
}, { highWaterMark: 16 * 1024 * 1024 })

이렇게 하면 5GB 영상이나 500MB 영상이나 메모리 사용량은 거의 차이가 없습니다.

문제는 Firefox가 FSA API를 지원하지 않고, Safari도 마찬가지라는 점입니다. 폴백 방안이 필요했습니다.

결국 3단계 디스크 쓰기 전략으로 정리했습니다:

  1. 우선 FSA API (Chrome/Edge, 크기 제한 없음)
  2. 폴백 StreamSaver.js (Service Worker 의존, 호환성 약간 더 넓음)
  3. 최후 Blob 모드 (모든 브라우저, 단 1.5GB 제한)

어느 단계로 진입할지는 런타임 감지로 결정하며, 사용자는 뒤에서 무슨 일이 일어나는지 알 필요가 없습니다.

다섯째 주: 진짜 보스전 — 세그먼트를 MP4로 합치기

세그먼트 다운로드는 됐고, 파일 쓰기도 됐습니다. 그런데 HLS 세그먼트는 TS 포맷(MPEG Transport Stream)이고, 대부분의 사람이 원하는 건 MP4입니다.

수백 개의 .ts 파일을 바이너리로 그냥 이어붙이면 .ts 대용량 파일이 됩니다. VLC에서는 재생되지만, Windows 기본 플레이어는 인식 못 하고, 캡컷에서 임포트 안 되고, 빌리빌리에 업로드하면 포맷 에러가 납니다. 실제로 재생은 되지만 사용자 경험이 너무 나쁩니다.

TS를 MP4로 바꾸는 건 본질적으로 리먹싱(remux) 입니다. TS 안의 비디오 스트림과 오디오 스트림을 꺼내서 MP4 컨테이너 구조에 담는 거죠. 재인코딩이 아니므로 이론적으로 속도도 빠르고 품질 손실도 없습니다.

문제는: 브라우저 안에서 누가 이걸 할 것인가?

답은 이미 1년 전에 나와 있었습니다. FFmpeg이 WebAssembly로 컴파일된 것입니다. 몇몇 오픈소스 프로젝트가 이 작업을 해냈고, 가장 유명한 건 @ffmpeg/ffmpeg입니다. WASM은 브라우저 JavaScript 환경에서 네이티브에 가까운 속도로 바이너리 코드를 실행할 수 있고, FFmpeg은 원래 리먹싱을 지원합니다.

이론적으로 제가 해야 할 일은:

  1. FFmpeg WASM 로드
  2. 세그먼트를 FFmpeg 가상 파일 시스템에 쓰기 (메모리 안에 파일 시스템을 에뮬레이션합니다)
  3. 다음 명령 실행:
ffmpeg -f concat -safe 0 -i filelist.txt \
  -c copy \
  -movflags +faststart \
  -bsf:a aac_adtstoasc \
  output.mp4

-c copy는 스트림 복사로, 재인코딩 없이 컨테이너만 바꿉니다. -bsf:a aac_adtstoasc는 AAC 오디오를 TS용 ADTS 패키징에서 MP4용 ASC 패키징으로 변환합니다. 이걸 빼먹으면 일부 플레이어에서 오디오 문제가 생깁니다. -movflags +faststart는 MP4의 인덱스(moov atom)를 파일 앞쪽으로 옮겨서, 스트리밍 재생에 유리하게 만듭니다.

듣기엔 좋습니다. 실제로는 여러 함정을 밟았습니다:

함정 1: FFmpeg WASM 첫 로딩에 10~30초 걸림.

WASM 코어 파일이 약 30MB라, 네트워크에서 다운로드하고 컴파일해야 합니다. 사용자가 다운로드 버튼을 눌렀는데 20초 동안 아무 일도 안 일어나면 경험 최악입니다.

해법: 다운로드 시작과 동시에 FFmpeg 로딩을 트리거해서, 세그먼트 다운로드와 병렬로 진행합니다. 세그먼트가 다 받아질 때쯤이면 FFmpeg도 보통 초기화가 끝나 있어서, 체감 대기 시간이 거의 없습니다. 브라우저 캐시 덕분에 두 번째부터는 로딩이 2~5초면 됩니다.

함정 2: WASM 가상 파일 시스템이 메모리 위에 있다.

FFmpeg이 파일을 처리하려면, 먼저 세그먼트를 가상 파일 시스템에 써넣어야 합니다. 1080P 1시간짜리 영상이면 600800MB의 세그먼트를 전부 WASM 메모리에 올려야 하고, FFmpeg 자체 런타임 메모리(200400MB)까지 더하면 WASM의 4GB 주소 공간 제한을 쉽게 넘겨서 그냥 크래시납니다.

이 문제는 대용량 파일 시나리오에서 간헐적이 아니라 필연적입니다. FFmpeg WASM에 의존하지 않는 대용량 파일 솔루션이 필요했습니다.

일곱째 주: TSToMP4Muxer를 만들다

거의 2주를 들여 순수 JavaScript TS → MP4 스트리밍 리먹서를 작성했습니다. 이름은 TSToMP4Muxer입니다.

원리는 이렇습니다: 모든 세그먼트가 다운로드될 때까지 기다리지 않고, 다운로드하면서 파싱하고 디스크에 쓰는 방식입니다. TS 포맷의 각 데이터 패킷은 고정 188바이트입니다. 패킷 단위로 읽으면서 PES(Packetized Elementary Stream) 데이터를 추출하고, 실시간으로 MP4 Box 포맷으로 변환하여 Streams API로 디스크에 지속적으로 써넣습니다.

세그먼트 0 다운로드 완료
  → 188바이트 TS 패킷 파싱
  → 비디오/오디오 PES 추출
  → MP4 mdat Box에 쓰기
세그먼트 1 다운로드 완료
  → 계속 추가
...
세그먼트 N 쓰기 완료
  → moov Box(인덱스) 쓰기
  → 파일 닫기

이렇게 하면 메모리에는 현재 처리 중인 세그먼트 하나만 존재하고, 파일 크기와 완전히 분리됩니다. 게다가 순수 JS이므로 FFmpeg WASM 파일을 로드할 필요가 없어, 그 30MB 로딩 오버헤드와 초기화 시간을 아낄 수 있습니다.

대가는 커버리지가 좁다는 점입니다. 단일 TS → MP4만 처리할 수 있고, DASH의 오디오/비디오 병합(두 입력을 동기화 처리해야 함)이나 다양한 엣지 포맷은 처리하지 못합니다. 하지만 HLS 다운로드라는 가장 메인스트림 시나리오에서는 TSToMP4Muxer가 더 나은 솔루션입니다.

현재 로직은: 일반 HLS는 TSToMP4Muxer 우선, 스트리밍 처리로 빠르고 메모리 절약; DASH나 두 스트림 병합이 필요한 경우 FFmpeg WASM 사용; 2GB를 초과하는 HLS도 TSToMP4Muxer로 처리하여 WASM 메모리 오버플로 방지.

이걸 만드는 데 정말 오래 걸렸습니다. MP4 포맷의 Box 구조는 스펙이 있는데, 그 스펙이 600페이지가 넘습니다. 저는 리먹싱과 관련된 수십 페이지만 봤습니다. ftyp, moov, mvhd, trak, mdia, minf, stbl — 이 Box들의 중첩 계층 관계가 하나라도 틀리면 플레이어가 읽지 못합니다. 디버깅 과정은: 만들어서 VLC로 열고, 어떤 에러가 나는지 보고, 스펙 찾아보고, 고치고, 반복. 스무 번쯤 돌았습니다.

가장 멘탈이 나갔던 순간: 영상이 재생은 되는데 타임라인이 엉망이었습니다.进度条를 드래그하면 랜덤 위치로 점프하고, 원하는 시간으로 가지 않았습니다. 원인은 stts(Sample to Time)와 ctts(Composition Time Offset) 두 Box의 타임스탬프 계산에서 B프레임의 DTS/PTS 오프셋을 처리하지 않은 것이었습니다. H.264에는 B프레임이 있어서, 디코딩 순서(DTS)와 표시 순서(PTS)가 다릅니다. ctts에 각 프레임의 표시 시간 오프셋을 따로 기록해야 합니다. 문제를 찾고 고치는 데 이틀이 걸렸습니다.

아홉째 주: AES-128 암호화 — 생각보다 어렵지 않았다

암호화된 영상 처리가 가장 어려울 거라고 생각했는데, TSToMP4Muxer보다 훨씬 수월했습니다.

HLS의 암호화 포맷은 M3U8 안에 아주 명확하게 적혀 있습니다:

#EXT-X-KEY:METHOD=AES-128,URI="https://api.example.com/key?token=abc",IV=0x00000000000000000000000000000001

로직은:

  1. #EXT-X-KEY 발견, Key URI와 IV 파싱
  2. Key URI에 요청해서 16바이트 키 획득 (브라우저 확장 프로그램이 보내는 요청이므로, 사용자의 현재 쿠키가 함께 전달됩니다. 로그인이 필요한 키도 여기서 받을 수 있습니다)
  3. 다운로드 완료된 각 세그먼트에 대해 Web Crypto API로 AES-128-CBC 복호화
const keyBytes = await crypto.subtle.importKey('raw', key, { name: 'AES-CBC' }, false, ['decrypt'])
const decrypted = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivBuffer }, keyBytes, encryptedData)

crypto.subtle은 브라우저 내장 Web Crypto API로, 하드웨어 가속을 지원합니다. 복호화 속도는 병목이 아니며, 세그먼트 하나당 몇 밀리초면 처리됩니다.

실제로 시간이 걸린 건 IV 처리였습니다. HLS 스펙에 따르면: M3U8에 IV가 명시되지 않은 경우, 각 세그먼트의 IV는 세그먼트 시퀀스 번호(MSB 포맷, 16바이트)를 사용해야 합니다. 저는 처음에 이 규칙을 처리하지 않아서, 모든 세그먼트에 첫 번째 IV를 적용했습니다. 암호화된 강의를 복호화하니 오디오는 맞는데 비디오가 깨져 나왔습니다. RFC 8216을 뒤져서 이 규칙을 찾아내고, 추가하고 나서야 해결됐습니다.

처리 불가: Widevine, PlayReady, FairPlay. 이것들은 DRM으로, 키가 운영체제나 하드웨어에 있어 JavaScript가 접근할 수 없습니다. 이런 콘텐츠를 만나면 FlowPick은 솔직히 "지원하지 않음"이라고 알려줄 뿐, 우회할 방법은 전혀 없습니다. Netflix, Disney+가 이런 플랫폼이며, 설계 범위에 없습니다.

열한째 주: DASH의 오디오/비디오 병합

DASH는 또 다른 주류 스트리밍 포맷으로, YouTube, 빌리빌리 일반 영상에서 사용됩니다. HLS와의 핵심 차이: 오디오와 비디오가 완전히 독립된 두 개의 스트림입니다.

이건 설계 결함이 아니라 의도된 것입니다. 이렇게 하면 플랫폼이 비디오 트랙 한 벌만 저장하고, 여러 언어의 오디오 트랙을 조합할 수 있어 저장 비용이 몇 배로 줄어듭니다. 하지만 다운로드 도구 입장에서는 두 경로의 데이터를 동시에 받아서 합쳐야 한다는 뜻입니다.

두 데이터를 합치는 건 FFmpeg WASM을 사용합니다:

ffmpeg
  -f concat -safe 0 -i video_list.txt
  -f concat -safe 0 -i audio_list.txt
  -c:v copy -c:a copy
  -movflags +faststart
  output.mp4

여기서 중요한 디테일이 있습니다. DASH의 세그먼트 포맷은 FMP4(Fragmented MP4)로, HLS의 TS와 다릅니다. FMP4의 특이점은 각 세그먼트 파일이 개별적으로 재생 가능하지만, 올바르게 파싱하려면 반드시 init segment가 먼저 필요하다는 점입니다. 코덱 파라미터를 담은 초기화 세그먼트(ftyp + moov Box)입니다. init segment가 없으면 FFmpeg이 영상의 해상도, 프레임 레이트, 코덱을 알 수 없어서 후속 데이터 세그먼트를 처리할 수 없습니다.

그래서 DASH 처리 흐름은 HLS보다 한 단계 더 많습니다. 먼저 init segment를 받고, 그다음 모든 데이터 세그먼트를 받으며, 병합 시 init segment를 반드시 맨 앞에 둬야 합니다. 처음에 init segment 처리를 빼먹었더니, FFmpeg이 합친 MP4를 열면 "파일 손상" 에러가 났습니다. 이틀 동안 원인을 못 찾다가 여기라는 걸 깨달았습니다.

DASH의 병렬 다운로드 전략도 바꿔야 했습니다. 비디오 세그먼트와 오디오 세그먼트를 동시에 받아야 합니다. 비디오를 전부 받고 오디오를 받는 게 아니라요. 이렇게 하면 총 다운로드 시간이 거의 절반으로 줄어듭니다. Worker Pool 설계가 이를 지원합니다. 두 경로의 작업이 병렬로 돌고, 하나의 스레드 풀을 공유하며, 놀고 있는 워커가 다음 세그먼트를 가져갑니다.

동시 다운로드: 사전 분할이 아닌 Worker Pool

동시 다운로드에 대해 따로 설명하고 싶습니다. 구현 디테일 하나 때문에 두 번이나 수정했거든요.

가장 직관적인 방법은: 200개 세그먼트를 4등분해서, 4개의 Worker가 각자 한 몫씩 처리하는 겁니다. 이 방법의 문제는 로드 불균형입니다. HLS 세그먼트는 크기가 균일하지 않습니다. 영상 앞뒤 세그먼트는 작고, 중간은 큽니다. 사전 분할하면 어떤 Worker는 큰 세그먼트 더미에 갇히고, 다른 세 Worker는 이미 놀고 있는 상황이 생깁니다.

FlowPick은 공유 카운터 방식을 씁니다:

let nextIndex = 0
const worker = async () => {
  while (nextIndex < segments.length) {
    const index = nextIndex++  // 원자적으로 다음 작업 확보
    await downloadAndProcess(segments[index])
  }
}
// N개의 worker를 시작하고, 모두 nextIndex를 공유
await Promise.all(Array.from({ length: concurrency }, () => worker()))

각 Worker는 현재 세그먼트를 끝내는 즉시 다음 세그먼트를 가져갑니다. 다른 Worker를 기다리지 않고, 큰 세그먼트든 작은 세그먼트든 상관없습니다. 이렇게 하면 자연스럽게 동적 로드 밸런싱이 이뤄지고, 총 완료 시간은 이론적 최적에 가까워집니다.

동시성 수는 기본 2, 최대 8까지 조정 가능합니다. 왜 기본값이 8이 아닐까요? 일부 CDN에서는 높은 동시성이 속도 제한이나 429 에러를 유발해서 오히려 더 느려지기 때문입니다. 4~6개 동시성이 보통 가성비 최적 구간이고, 8개부터는 수확 체감이 시작됩니다.

재시도 메커니즘은 지수 백오프입니다. 실패 후 400ms 대기, 또 실패하면 800ms, 또 실패하면 1600ms, 3회 모두 실패해야 포기합니다. 대부분의 네트워크 변동은 첫 번째나 두 번째 재시도에서 복구됩니다. 4xx 에러(403, 404)는 재시도하지 않습니다. 서버가 명시적으로 거부한 거라 재시도해도 의미가 없습니다.

메모리 관리: 브라우저 크래시 방지

다운로드 막바지 단계에서 메모리 관리가 가장 신경 쓰이는 부분이었습니다.

문제는 다운로드할 파일이 얼마나 클지 모른다는 점입니다. 사용자가 1080P 라이브 다시보기를 선택하면, 2GB일 수도 있고 20GB일 수도 있습니다. 모든 세그먼트를 다 받기 전까지는 알 수 없습니다.

다운로드 전 샘플링 추정을 넣었습니다:

// 먼저 max(1, total × 10%) 개의 세그먼트를 다운로드, 평균 크기를 구하고 총 개수로 곱해서 추정
const sampleCount = Math.min(5, Math.max(1, Math.floor(totalCount * 0.1)))

추정 후, 크기에 따라 디스크 쓰기 전략을 선택합니다. 800MB 이하는 자유, 800MB~1.5GB는 반드시 스트리밍 쓰기, 1.5GB 초과는 Blob 모드 거부.

하지만 추정만으로는 부족해서, 런타임에도 모니터링합니다. 다운로드 중 할당된 ArrayBuffer 총량을 지속적으로 집계하여, 600MB 초과 시 경고, 1200MB 초과 시 새 세그먼트 다운로드를 일시 중지하고 이미 받은 세그먼트를 우선 디스크에 쓰고 메모리를 해제한 후 계속합니다. 이렇게 하면 추정이 크게 빗나가도 대비책이 있습니다.

만들었다가 지운 기능들

3개월 동안 몇 가지 기능을 반쯤 만들었다가 지웠습니다:

트랜스코딩(transcoding): 한때 "다운로드할 때 720P로 변환"을 지원해서 파일 크기를 줄이려 했습니다. 그런데 브라우저에서 트랜스코딩이 너무 느리다는 걸 깨달았습니다. FFmpeg WASM은 리먹싱 정도는 괜찮지만, 재인코딩은 매 프레임을 디코딩하고 다시 인코딩해야 해서 CPU 사용률이 치솟고, 1GB 영상이 30분 걸릴 수도 있습니다. 사용자 경험으로 용납할 수 없는 수준입니다. 결국 FlowPick은 리먹싱(컨테이너만 변경, 코덱 변경 없음)만 하고, 트랜스코딩이 필요하면 커맨드라인 FFmpeg을 쓰라고 안내합니다.

실시간 진행률 ETA(남은 시간 추정): 첫 버전은 순간 속도를 썼더니, 진행률 표시줄이 미친 듯이 흔들렸습니다. CDN 세그먼트 크기가 불균일해서, 순간 속도가 0.5MB/s와 50MB/s 사이를 왔다 갔다 했습니다. 5초 슬라이딩 윈도 평균으로 바꾸니 훨씬 안정적이었습니다.

여러 파일 일괄 다운로드: 원래는 안 넣으려다가, 누군가 모든 에피소드를 한 번에 받을 수 있냐고 물어봐서 간단한 큐를 추가했습니다. 각 다운로드 작업이 순차적으로 실행되며, 병렬은 지원하지 않습니다(두 영상을 동시에 받으면 대역폭과 FFmpeg 메모리가 모두 터집니다).

3개월의 결과

코드량은 약 1.2만 줄 TypeScript, 확장 프로그램 + 온라인 툴 + 랜딩 페이지입니다.

할 수 있는 것:

  • HLS/M3U8 다운로드, AES-128 자동 복호화
  • DASH/MPD 다운로드, 오디오/비디오 자동 병합
  • 직접 링크 영상 일괄 감지
  • 이미지 일괄 감지 및 다운로드
  • 오디오 리소스 감지
  • MP4 출력, 크기 제한 없음 (Chrome/Edge)

할 수 없는 것 (게으름이 아니라 진짜 못 하는 것):

  • Widevine/PlayReady/FairPlay DRM — 하드웨어 레벨 보호, JS가 키에 접근 불가
  • 영상 트랜스코딩 — 브라우저 연산 능력 부족, 경험 너무 나쁨
  • MKV/AVI로 포맷 변환 — FFmpeg WASM 통합이 현재 MP4/TS 경로만 되어 있음

코드는 오픈소스로 공개했고, 저장소는 GitHub에 있습니다. 확장 프로그램은 세 브라우저에 등록되어 있습니다: Chrome 웹 스토어, Edge 추가 기능, Firefox 부가 기능. 버그를 발견하거나 아이디어가 있다면, 이슈나 PR을 환영합니다.


추천 글