[{"data":1,"prerenderedAt":3571},["ShallowReactive",2],{"blog-ko-webcodecs-web-workers-opfs-video-processing":3,"blog-ko-webcodecs-web-workers-opfs-video-processing-surround":3560},{"id":4,"title":5,"author":6,"body":7,"category":3543,"date":3544,"description":3545,"extension":3546,"image":3547,"lastModified":3544,"meta":3548,"navigation":153,"path":3549,"readingTime":343,"seo":3550,"stem":3551,"tags":3552,"__hash__":3559},"blog_ko\u002Fko\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing.md","WebCodecs + Web Workers + OPFS: 브라우저에서 비디오 처리하는 실전 경로","FlowPick 팀",{"type":8,"value":9,"toc":3527},"minimark",[10,14,17,22,56,62,76,80,83,93,100,104,464,467,471,685,695,699,1932,1935,1966,1970,1973,1976,2000,2006,2444,2447,2451,2454,2535,2538,2552,2555,2569,2575,2579,2585,2661,2664,2667,2682,2686,2689,3206,3209,3212,3216,3228,3268,3278,3297,3314,3328,3332,3337,3351,3356,3370,3375,3383,3388,3399,3403,3444,3447,3454,3462,3465,3469,3523],[11,12,13],"p",{},"브라우저에서 비디오 처리하면 FFmpeg WASM이 기본 선택. 문제없다. 하지만 25MB 다운로드 사이즈, 기본은 싱글스레드, 블랙박스. 워크로드에 따라 새로운 브라우저 네이티브 API——WebCodecs, Web Workers, OPFS——로 더 빠르고 작고 디버깅하기 쉬운 걸 만들 수 있다.",[11,15,16],{},"이 글은 실제로 동작하는 것: 비디오 파일을 디코딩해서 지정한 타임스탬프로 프레임을 추출하고 로컬 스토리지에 쓴다. FFmpeg에 손대지 않는다. 끝에 실측 데이터.",[18,19,21],"h2",{"id":20},"_3개-api가-각자-맡은-역할","3개 API가 각자 맡은 역할",[11,23,24,28,29,33,34,33,37,33,40,43,44,47,48,51,52,55],{},[25,26,27],"strong",{},"WebCodecs","(",[30,31,32],"code",{},"VideoDecoder",", ",[30,35,36],{},"VideoEncoder",[30,38,39],{},"AudioDecoder",[30,41,42],{},"AudioEncoder",")——하드웨어 가속된 코덱 인터페이스. ",[30,45,46],{},"\u003Cvideo>","의 \"재생하고 캡처\" 경로를 우회한다. ",[30,49,50],{},"EncodedVideoChunk","(예: 날것의 H.264 NAL unit)를 주면 ",[30,53,54],{},"VideoFrame"," 객체를 반환. 캔버스에 렌더링도, 트랜스코딩도, 다운스트림에 넘긴다.",[11,57,58,61],{},[25,59,60],{},"Web Workers","——독립 스레드에서 CPU 집약 작업. 코덱 파싱, 프레임 변환, 컨테이너화 로직은 전부 여기로. 메인 스레드를 막지 않는다. Worker 안에서도 OPFS를 직접 쓸 수 있다.",[11,63,64,67,68,71,72,75],{},[25,65,66],{},"OPFS(Origin Private File System)","——진짜 고처리량의 웹용 파일 시스템. ",[30,69,70],{},"localStorage","는 동기식에 5MB 상한, IndexedDB는 비동기인데 바이너리 blob이 절망적으로 느리다. OPFS는 파일을 위해 만들어졌다. Worker 안에서 ",[30,73,74],{},"createSyncAccessHandle()","로 동기 액세스 핸들을 가져올 수 있다. 이게 성능의 목숨.",[18,77,79],{"id":78},"구축할-아키텍처","구축할 아키텍처",[11,81,82],{},"만든 것: 사용자가 비디오 파일을 페이지에 드래그하면, 매초 1프레임씩 JPEG로 추출해 OPFS에 쓰고, 마지막에 다운로드 링크를 준다. 실제 시나리오는 비디오 라이브러리의 썸네일 생성.",[84,85,90],"pre",{"className":86,"code":88,"language":89},[87],"language-text","메인 스레드:\n  - 파일 입력\n  - Worker 생성\n  - UI 업데이트(진행, 결과)\n\nWorker:\n  - OPFS 경로에서 파일 가져오기 (postMessage 경유가 아님——너무 느림)\n  - WebCodecs VideoDecoder로 디코딩\n  - Canvas 렌더링 + toBlob으로 JPEG화\n  - JPEG를 OPFS에 쓰기\n  - 진행을 메인 스레드에 보고\n","text",[30,91,88],{"__ignoreMap":92},"",[11,94,95,96,99],{},"\"OPFS 경로에서 가져오기\"가 포인트. ",[30,97,98],{},"postMessage","로 500MB 파일을 전송(Transferable로 해도)은 느리고 메모리 복사가 일어난다. 파일을 먼저 OPFS에 쓰고 경로를 Worker에 넘기는 게 훨씬 빠르다.",[18,101,103],{"id":102},"_1단계-기능-감지","1단계: 기능 감지",[84,105,109],{"className":106,"code":107,"language":108,"meta":92,"style":92},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","function checkSupport() {\n  const issues = []\n\n  if (!('VideoDecoder' in window)) {\n    issues.push('WebCodecs VideoDecoder 미지원. Chrome 94+ 이상 필요.')\n  }\n\n  if (!window.showOpenFilePicker && !window.FileSystemHandle) {\n    issues.push('File System Access API 미지원.')\n  }\n\n  if (!navigator.storage.getDirectory) {\n    issues.push('OPFS 미지원. Chrome 102+ 이상 필요.')\n  }\n\n  \u002F\u002F Worker 자체는 거의 표준이지만, Worker 안에서 동기 OPFS를 쓸 수 있는지 확인\n  \u002F\u002F (구형 Chrome은 Worker를 지원해도 동기 OPFS가 없음)\n\n  return issues\n}\n\nconst issues = checkSupport()\nif (issues.length > 0) {\n  console.warn('기능 갭:', issues)\n  \u002F\u002F FFmpeg WASM으로 폴백\n}\n","js",[30,110,111,131,148,155,190,214,220,225,260,280,285,290,316,336,341,346,353,359,364,373,379,384,401,426,453,459],{"__ignoreMap":92},[112,113,116,120,124,128],"span",{"class":114,"line":115},"line",1,[112,117,119],{"class":118},"spNyl","function",[112,121,123],{"class":122},"s2Zo4"," checkSupport",[112,125,127],{"class":126},"sMK4o","()",[112,129,130],{"class":126}," {\n",[112,132,134,137,141,144],{"class":114,"line":133},2,[112,135,136],{"class":118},"  const",[112,138,140],{"class":139},"sTEyZ"," issues",[112,142,143],{"class":126}," =",[112,145,147],{"class":146},"swJcz"," []\n",[112,149,151],{"class":114,"line":150},3,[112,152,154],{"emptyLinePlaceholder":153},true,"\n",[112,156,158,162,165,168,170,173,176,178,181,184,187],{"class":114,"line":157},4,[112,159,161],{"class":160},"s7zQu","  if",[112,163,164],{"class":146}," (",[112,166,167],{"class":126},"!",[112,169,28],{"class":146},[112,171,172],{"class":126},"'",[112,174,32],{"class":175},"sfazB",[112,177,172],{"class":126},[112,179,180],{"class":126}," in",[112,182,183],{"class":139}," window",[112,185,186],{"class":146},")) ",[112,188,189],{"class":126},"{\n",[112,191,193,196,199,202,204,206,209,211],{"class":114,"line":192},5,[112,194,195],{"class":139},"    issues",[112,197,198],{"class":126},".",[112,200,201],{"class":122},"push",[112,203,28],{"class":146},[112,205,172],{"class":126},[112,207,208],{"class":175},"WebCodecs VideoDecoder 미지원. Chrome 94+ 이상 필요.",[112,210,172],{"class":126},[112,212,213],{"class":146},")\n",[112,215,217],{"class":114,"line":216},6,[112,218,219],{"class":126},"  }\n",[112,221,223],{"class":114,"line":222},7,[112,224,154],{"emptyLinePlaceholder":153},[112,226,228,230,232,234,237,239,242,245,248,250,252,255,258],{"class":114,"line":227},8,[112,229,161],{"class":160},[112,231,164],{"class":146},[112,233,167],{"class":126},[112,235,236],{"class":139},"window",[112,238,198],{"class":126},[112,240,241],{"class":139},"showOpenFilePicker",[112,243,244],{"class":126}," &&",[112,246,247],{"class":126}," !",[112,249,236],{"class":139},[112,251,198],{"class":126},[112,253,254],{"class":139},"FileSystemHandle",[112,256,257],{"class":146},") ",[112,259,189],{"class":126},[112,261,263,265,267,269,271,273,276,278],{"class":114,"line":262},9,[112,264,195],{"class":139},[112,266,198],{"class":126},[112,268,201],{"class":122},[112,270,28],{"class":146},[112,272,172],{"class":126},[112,274,275],{"class":175},"File System Access API 미지원.",[112,277,172],{"class":126},[112,279,213],{"class":146},[112,281,283],{"class":114,"line":282},10,[112,284,219],{"class":126},[112,286,288],{"class":114,"line":287},11,[112,289,154],{"emptyLinePlaceholder":153},[112,291,293,295,297,299,302,304,307,309,312,314],{"class":114,"line":292},12,[112,294,161],{"class":160},[112,296,164],{"class":146},[112,298,167],{"class":126},[112,300,301],{"class":139},"navigator",[112,303,198],{"class":126},[112,305,306],{"class":139},"storage",[112,308,198],{"class":126},[112,310,311],{"class":139},"getDirectory",[112,313,257],{"class":146},[112,315,189],{"class":126},[112,317,319,321,323,325,327,329,332,334],{"class":114,"line":318},13,[112,320,195],{"class":139},[112,322,198],{"class":126},[112,324,201],{"class":122},[112,326,28],{"class":146},[112,328,172],{"class":126},[112,330,331],{"class":175},"OPFS 미지원. Chrome 102+ 이상 필요.",[112,333,172],{"class":126},[112,335,213],{"class":146},[112,337,339],{"class":114,"line":338},14,[112,340,219],{"class":126},[112,342,344],{"class":114,"line":343},15,[112,345,154],{"emptyLinePlaceholder":153},[112,347,349],{"class":114,"line":348},16,[112,350,352],{"class":351},"sHwdD","  \u002F\u002F Worker 자체는 거의 표준이지만, Worker 안에서 동기 OPFS를 쓸 수 있는지 확인\n",[112,354,356],{"class":114,"line":355},17,[112,357,358],{"class":351},"  \u002F\u002F (구형 Chrome은 Worker를 지원해도 동기 OPFS가 없음)\n",[112,360,362],{"class":114,"line":361},18,[112,363,154],{"emptyLinePlaceholder":153},[112,365,367,370],{"class":114,"line":366},19,[112,368,369],{"class":160},"  return",[112,371,372],{"class":139}," issues\n",[112,374,376],{"class":114,"line":375},20,[112,377,378],{"class":126},"}\n",[112,380,382],{"class":114,"line":381},21,[112,383,154],{"emptyLinePlaceholder":153},[112,385,387,390,393,396,398],{"class":114,"line":386},22,[112,388,389],{"class":118},"const",[112,391,392],{"class":139}," issues ",[112,394,395],{"class":126},"=",[112,397,123],{"class":122},[112,399,400],{"class":139},"()\n",[112,402,404,407,410,412,415,418,422,424],{"class":114,"line":403},23,[112,405,406],{"class":160},"if",[112,408,409],{"class":139}," (issues",[112,411,198],{"class":126},[112,413,414],{"class":139},"length ",[112,416,417],{"class":126},">",[112,419,421],{"class":420},"sbssI"," 0",[112,423,257],{"class":139},[112,425,189],{"class":126},[112,427,429,432,434,437,439,441,444,446,449,451],{"class":114,"line":428},24,[112,430,431],{"class":139},"  console",[112,433,198],{"class":126},[112,435,436],{"class":122},"warn",[112,438,28],{"class":146},[112,440,172],{"class":126},[112,442,443],{"class":175},"기능 갭:",[112,445,172],{"class":126},[112,447,448],{"class":126},",",[112,450,140],{"class":139},[112,452,213],{"class":146},[112,454,456],{"class":114,"line":455},25,[112,457,458],{"class":351},"  \u002F\u002F FFmpeg WASM으로 폴백\n",[112,460,462],{"class":114,"line":461},26,[112,463,378],{"class":126},[11,465,466],{},"2026년 브라우저 지원: Chrome\u002FEdge 102+, Safari 16.4+(WebCodecs 부분적), Firefox 130+(버전에 따라 아직 flag 뒤). 프로덕션에서는 항상 기능 감지하고, 안 되면 FFmpeg WASM으로 폴백.",[18,468,470],{"id":469},"_2단계-파일을-opfs에-올리기","2단계: 파일을 OPFS에 올리기",[84,472,474],{"className":106,"code":473,"language":108,"meta":92,"style":92},"async function stageFileToOpfs(file) {\n  const root = await navigator.storage.getDirectory()\n  const dir = await root.getDirectoryHandle('input', { create: true })\n  const fileHandle = await dir.getFileHandle(file.name, { create: true })\n  const writable = await fileHandle.createWritable()\n  await file.stream().pipeTo(writable)\n\n  return `${file.name}`  \u002F\u002F OPFS 루트의 'input' 폴더 내 경로\n}\n",[30,475,476,498,523,570,611,631,658,662,681],{"__ignoreMap":92},[112,477,478,481,484,487,489,493,496],{"class":114,"line":115},[112,479,480],{"class":118},"async",[112,482,483],{"class":118}," function",[112,485,486],{"class":122}," stageFileToOpfs",[112,488,28],{"class":126},[112,490,492],{"class":491},"sHdIc","file",[112,494,495],{"class":126},")",[112,497,130],{"class":126},[112,499,500,502,505,507,510,513,515,517,519,521],{"class":114,"line":133},[112,501,136],{"class":118},[112,503,504],{"class":139}," root",[112,506,143],{"class":126},[112,508,509],{"class":160}," await",[112,511,512],{"class":139}," navigator",[112,514,198],{"class":126},[112,516,306],{"class":139},[112,518,198],{"class":126},[112,520,311],{"class":122},[112,522,400],{"class":146},[112,524,525,527,530,532,534,536,538,541,543,545,548,550,552,555,558,561,565,568],{"class":114,"line":150},[112,526,136],{"class":118},[112,528,529],{"class":139}," dir",[112,531,143],{"class":126},[112,533,509],{"class":160},[112,535,504],{"class":139},[112,537,198],{"class":126},[112,539,540],{"class":122},"getDirectoryHandle",[112,542,28],{"class":146},[112,544,172],{"class":126},[112,546,547],{"class":175},"input",[112,549,172],{"class":126},[112,551,448],{"class":126},[112,553,554],{"class":126}," {",[112,556,557],{"class":146}," create",[112,559,560],{"class":126},":",[112,562,564],{"class":563},"sfNiH"," true",[112,566,567],{"class":126}," }",[112,569,213],{"class":146},[112,571,572,574,577,579,581,583,585,588,590,592,594,597,599,601,603,605,607,609],{"class":114,"line":157},[112,573,136],{"class":118},[112,575,576],{"class":139}," fileHandle",[112,578,143],{"class":126},[112,580,509],{"class":160},[112,582,529],{"class":139},[112,584,198],{"class":126},[112,586,587],{"class":122},"getFileHandle",[112,589,28],{"class":146},[112,591,492],{"class":139},[112,593,198],{"class":126},[112,595,596],{"class":139},"name",[112,598,448],{"class":126},[112,600,554],{"class":126},[112,602,557],{"class":146},[112,604,560],{"class":126},[112,606,564],{"class":563},[112,608,567],{"class":126},[112,610,213],{"class":146},[112,612,613,615,618,620,622,624,626,629],{"class":114,"line":192},[112,614,136],{"class":118},[112,616,617],{"class":139}," writable",[112,619,143],{"class":126},[112,621,509],{"class":160},[112,623,576],{"class":139},[112,625,198],{"class":126},[112,627,628],{"class":122},"createWritable",[112,630,400],{"class":146},[112,632,633,636,639,641,644,646,648,651,653,656],{"class":114,"line":216},[112,634,635],{"class":160},"  await",[112,637,638],{"class":139}," file",[112,640,198],{"class":126},[112,642,643],{"class":122},"stream",[112,645,127],{"class":146},[112,647,198],{"class":126},[112,649,650],{"class":122},"pipeTo",[112,652,28],{"class":146},[112,654,655],{"class":139},"writable",[112,657,213],{"class":146},[112,659,660],{"class":114,"line":222},[112,661,154],{"emptyLinePlaceholder":153},[112,663,664,666,669,671,673,675,678],{"class":114,"line":227},[112,665,369],{"class":160},[112,667,668],{"class":126}," `${",[112,670,492],{"class":139},[112,672,198],{"class":126},[112,674,596],{"class":139},[112,676,677],{"class":126},"}`",[112,679,680],{"class":351},"  \u002F\u002F OPFS 루트의 'input' 폴더 내 경로\n",[112,682,683],{"class":114,"line":262},[112,684,378],{"class":126},[11,686,687,688,691,692,694],{},"여기는 비동기 ",[30,689,690],{},"createWritable()",". Worker 안에서는 동기 ",[30,693,74],{},"을 쓰면 작은 쓰기가 많을 때 더 빠르다.",[18,696,698],{"id":697},"_3단계-worker","3단계: Worker",[84,700,702],{"className":106,"code":701,"language":108,"meta":92,"style":92},"\u002F\u002F worker.js\nlet decoder = null\nlet canvas = null\nlet ctx = null\n\nself.onmessage = async (e) => {\n  const { type, data } = e.data\n\n  if (type === 'init') {\n    await init(data)\n  } else if (type === 'extract') {\n    await extractFrames(data)\n  }\n}\n\nasync function init({ codec, canvasWidth, canvasHeight }) {\n  \u002F\u002F Canvas 준비 (Worker 안에서는 OffscreenCanvas)\n  canvas = new OffscreenCanvas(canvasWidth, canvasHeight)\n  ctx = canvas.getContext('2d')\n\n  \u002F\u002F WebCodecs 디코더\n  decoder = new VideoDecoder({\n    output: (frame) => handleFrame(frame),\n    error: (e) => console.error('디코더 에러:', e)\n  })\n\n  decoder.configure({\n    codec,  \u002F\u002F 예: 'avc1.640028'\n    optimizeForLatency: false\n  })\n\n  self.postMessage({ type: 'ready' })\n}\n\nlet frameCount = 0\nlet lastExtractedSecond = -1\n\nasync function handleFrame(frame) {\n  const timestampSeconds = frame.timestamp \u002F 1_000_000  \u002F\u002F WebCodecs는 마이크로초\n\n  \u002F\u002F 매초 1프레임만 추출\n  const currentSecond = Math.floor(timestampSeconds)\n  if (currentSecond !== lastExtractedSecond) {\n    lastExtractedSecond = currentSecond\n\n    ctx.drawImage(frame, 0, 0, canvas.width, canvas.height)\n    const blob = await canvas.convertToBlob({ type: 'image\u002Fjpeg', quality: 0.85 })\n    const buffer = await blob.arrayBuffer()\n\n    \u002F\u002F OPFS에 쓰기\n    const root = await navigator.storage.getDirectory()\n    const outputDir = await root.getDirectoryHandle('thumbnails', { create: true })\n    const fileHandle = await outputDir.getFileHandle(`thumb_${String(currentSecond).padStart(6, '0')}.jpg`, { create: true })\n    const syncHandle = await fileHandle.createSyncAccessHandle()\n    syncHandle.write(new Uint8Array(buffer))\n    syncHandle.close()\n\n    frameCount++\n    self.postMessage({ type: 'progress', frameCount, second: currentSecond })\n  }\n\n  frame.close()  \u002F\u002F 중요: VideoFrame은 반드시 닫기. 안 그러면 GPU 메모리 누수\n}\n\nasync function extractFrames({ chunks }) {\n  for (const chunk of chunks) {\n    decoder.decode(new EncodedVideoChunk({\n      type: chunk.keyframe ? 'key' : 'delta',\n      timestamp: chunk.timestamp,\n      data: chunk.data\n    }))\n  }\n  await decoder.flush()\n  self.postMessage({ type: 'done', frameCount })\n}\n",[30,703,704,709,722,733,744,748,775,801,805,829,844,872,885,889,893,897,926,931,955,981,985,990,1006,1034,1072,1078,1082,1096,1107,1118,1125,1130,1160,1165,1170,1183,1199,1204,1221,1248,1253,1259,1284,1304,1315,1320,1363,1412,1433,1438,1444,1467,1508,1585,1606,1633,1645,1650,1659,1702,1707,1712,1728,1733,1738,1756,1778,1800,1837,1853,1867,1875,1880,1895,1927],{"__ignoreMap":92},[112,705,706],{"class":114,"line":115},[112,707,708],{"class":351},"\u002F\u002F worker.js\n",[112,710,711,714,717,719],{"class":114,"line":133},[112,712,713],{"class":118},"let",[112,715,716],{"class":139}," decoder ",[112,718,395],{"class":126},[112,720,721],{"class":126}," null\n",[112,723,724,726,729,731],{"class":114,"line":150},[112,725,713],{"class":118},[112,727,728],{"class":139}," canvas ",[112,730,395],{"class":126},[112,732,721],{"class":126},[112,734,735,737,740,742],{"class":114,"line":157},[112,736,713],{"class":118},[112,738,739],{"class":139}," ctx ",[112,741,395],{"class":126},[112,743,721],{"class":126},[112,745,746],{"class":114,"line":192},[112,747,154],{"emptyLinePlaceholder":153},[112,749,750,753,755,758,760,763,765,768,770,773],{"class":114,"line":216},[112,751,752],{"class":139},"self",[112,754,198],{"class":126},[112,756,757],{"class":122},"onmessage",[112,759,143],{"class":126},[112,761,762],{"class":118}," async",[112,764,164],{"class":126},[112,766,767],{"class":491},"e",[112,769,495],{"class":126},[112,771,772],{"class":118}," =>",[112,774,130],{"class":126},[112,776,777,779,781,784,786,789,791,793,796,798],{"class":114,"line":222},[112,778,136],{"class":118},[112,780,554],{"class":126},[112,782,783],{"class":139}," type",[112,785,448],{"class":126},[112,787,788],{"class":139}," data",[112,790,567],{"class":126},[112,792,143],{"class":126},[112,794,795],{"class":139}," e",[112,797,198],{"class":126},[112,799,800],{"class":139},"data\n",[112,802,803],{"class":114,"line":227},[112,804,154],{"emptyLinePlaceholder":153},[112,806,807,809,811,814,817,820,823,825,827],{"class":114,"line":262},[112,808,161],{"class":160},[112,810,164],{"class":146},[112,812,813],{"class":139},"type",[112,815,816],{"class":126}," ===",[112,818,819],{"class":126}," '",[112,821,822],{"class":175},"init",[112,824,172],{"class":126},[112,826,257],{"class":146},[112,828,189],{"class":126},[112,830,831,834,837,839,842],{"class":114,"line":282},[112,832,833],{"class":160},"    await",[112,835,836],{"class":122}," init",[112,838,28],{"class":146},[112,840,841],{"class":139},"data",[112,843,213],{"class":146},[112,845,846,849,852,855,857,859,861,863,866,868,870],{"class":114,"line":287},[112,847,848],{"class":126},"  }",[112,850,851],{"class":160}," else",[112,853,854],{"class":160}," if",[112,856,164],{"class":146},[112,858,813],{"class":139},[112,860,816],{"class":126},[112,862,819],{"class":126},[112,864,865],{"class":175},"extract",[112,867,172],{"class":126},[112,869,257],{"class":146},[112,871,189],{"class":126},[112,873,874,876,879,881,883],{"class":114,"line":292},[112,875,833],{"class":160},[112,877,878],{"class":122}," extractFrames",[112,880,28],{"class":146},[112,882,841],{"class":139},[112,884,213],{"class":146},[112,886,887],{"class":114,"line":318},[112,888,219],{"class":126},[112,890,891],{"class":114,"line":338},[112,892,378],{"class":126},[112,894,895],{"class":114,"line":343},[112,896,154],{"emptyLinePlaceholder":153},[112,898,899,901,903,905,908,911,913,916,918,921,924],{"class":114,"line":348},[112,900,480],{"class":118},[112,902,483],{"class":118},[112,904,836],{"class":122},[112,906,907],{"class":126},"({",[112,909,910],{"class":491}," codec",[112,912,448],{"class":126},[112,914,915],{"class":491}," canvasWidth",[112,917,448],{"class":126},[112,919,920],{"class":491}," canvasHeight",[112,922,923],{"class":126}," })",[112,925,130],{"class":126},[112,927,928],{"class":114,"line":355},[112,929,930],{"class":351},"  \u002F\u002F Canvas 준비 (Worker 안에서는 OffscreenCanvas)\n",[112,932,933,936,938,941,944,946,949,951,953],{"class":114,"line":361},[112,934,935],{"class":139},"  canvas",[112,937,143],{"class":126},[112,939,940],{"class":126}," new",[112,942,943],{"class":122}," OffscreenCanvas",[112,945,28],{"class":146},[112,947,948],{"class":139},"canvasWidth",[112,950,448],{"class":126},[112,952,920],{"class":139},[112,954,213],{"class":146},[112,956,957,960,962,965,967,970,972,974,977,979],{"class":114,"line":366},[112,958,959],{"class":139},"  ctx",[112,961,143],{"class":126},[112,963,964],{"class":139}," canvas",[112,966,198],{"class":126},[112,968,969],{"class":122},"getContext",[112,971,28],{"class":146},[112,973,172],{"class":126},[112,975,976],{"class":175},"2d",[112,978,172],{"class":126},[112,980,213],{"class":146},[112,982,983],{"class":114,"line":375},[112,984,154],{"emptyLinePlaceholder":153},[112,986,987],{"class":114,"line":381},[112,988,989],{"class":351},"  \u002F\u002F WebCodecs 디코더\n",[112,991,992,995,997,999,1002,1004],{"class":114,"line":386},[112,993,994],{"class":139},"  decoder",[112,996,143],{"class":126},[112,998,940],{"class":126},[112,1000,1001],{"class":122}," VideoDecoder",[112,1003,28],{"class":146},[112,1005,189],{"class":126},[112,1007,1008,1011,1013,1015,1018,1020,1022,1025,1027,1029,1031],{"class":114,"line":403},[112,1009,1010],{"class":122},"    output",[112,1012,560],{"class":126},[112,1014,164],{"class":126},[112,1016,1017],{"class":491},"frame",[112,1019,495],{"class":126},[112,1021,772],{"class":118},[112,1023,1024],{"class":122}," handleFrame",[112,1026,28],{"class":146},[112,1028,1017],{"class":139},[112,1030,495],{"class":146},[112,1032,1033],{"class":126},",\n",[112,1035,1036,1039,1041,1043,1045,1047,1049,1052,1054,1057,1059,1061,1064,1066,1068,1070],{"class":114,"line":428},[112,1037,1038],{"class":122},"    error",[112,1040,560],{"class":126},[112,1042,164],{"class":126},[112,1044,767],{"class":491},[112,1046,495],{"class":126},[112,1048,772],{"class":118},[112,1050,1051],{"class":139}," console",[112,1053,198],{"class":126},[112,1055,1056],{"class":122},"error",[112,1058,28],{"class":146},[112,1060,172],{"class":126},[112,1062,1063],{"class":175},"디코더 에러:",[112,1065,172],{"class":126},[112,1067,448],{"class":126},[112,1069,795],{"class":139},[112,1071,213],{"class":146},[112,1073,1074,1076],{"class":114,"line":455},[112,1075,848],{"class":126},[112,1077,213],{"class":146},[112,1079,1080],{"class":114,"line":461},[112,1081,154],{"emptyLinePlaceholder":153},[112,1083,1085,1087,1089,1092,1094],{"class":114,"line":1084},27,[112,1086,994],{"class":139},[112,1088,198],{"class":126},[112,1090,1091],{"class":122},"configure",[112,1093,28],{"class":146},[112,1095,189],{"class":126},[112,1097,1099,1102,1104],{"class":114,"line":1098},28,[112,1100,1101],{"class":139},"    codec",[112,1103,448],{"class":126},[112,1105,1106],{"class":351},"  \u002F\u002F 예: 'avc1.640028'\n",[112,1108,1110,1113,1115],{"class":114,"line":1109},29,[112,1111,1112],{"class":146},"    optimizeForLatency",[112,1114,560],{"class":126},[112,1116,1117],{"class":563}," false\n",[112,1119,1121,1123],{"class":114,"line":1120},30,[112,1122,848],{"class":126},[112,1124,213],{"class":146},[112,1126,1128],{"class":114,"line":1127},31,[112,1129,154],{"emptyLinePlaceholder":153},[112,1131,1133,1136,1138,1140,1142,1145,1147,1149,1151,1154,1156,1158],{"class":114,"line":1132},32,[112,1134,1135],{"class":139},"  self",[112,1137,198],{"class":126},[112,1139,98],{"class":122},[112,1141,28],{"class":146},[112,1143,1144],{"class":126},"{",[112,1146,783],{"class":146},[112,1148,560],{"class":126},[112,1150,819],{"class":126},[112,1152,1153],{"class":175},"ready",[112,1155,172],{"class":126},[112,1157,567],{"class":126},[112,1159,213],{"class":146},[112,1161,1163],{"class":114,"line":1162},33,[112,1164,378],{"class":126},[112,1166,1168],{"class":114,"line":1167},34,[112,1169,154],{"emptyLinePlaceholder":153},[112,1171,1173,1175,1178,1180],{"class":114,"line":1172},35,[112,1174,713],{"class":118},[112,1176,1177],{"class":139}," frameCount ",[112,1179,395],{"class":126},[112,1181,1182],{"class":420}," 0\n",[112,1184,1186,1188,1191,1193,1196],{"class":114,"line":1185},36,[112,1187,713],{"class":118},[112,1189,1190],{"class":139}," lastExtractedSecond ",[112,1192,395],{"class":126},[112,1194,1195],{"class":126}," -",[112,1197,1198],{"class":420},"1\n",[112,1200,1202],{"class":114,"line":1201},37,[112,1203,154],{"emptyLinePlaceholder":153},[112,1205,1207,1209,1211,1213,1215,1217,1219],{"class":114,"line":1206},38,[112,1208,480],{"class":118},[112,1210,483],{"class":118},[112,1212,1024],{"class":122},[112,1214,28],{"class":126},[112,1216,1017],{"class":491},[112,1218,495],{"class":126},[112,1220,130],{"class":126},[112,1222,1224,1226,1229,1231,1234,1236,1239,1242,1245],{"class":114,"line":1223},39,[112,1225,136],{"class":118},[112,1227,1228],{"class":139}," timestampSeconds",[112,1230,143],{"class":126},[112,1232,1233],{"class":139}," frame",[112,1235,198],{"class":126},[112,1237,1238],{"class":139},"timestamp",[112,1240,1241],{"class":126}," \u002F",[112,1243,1244],{"class":420}," 1_000_000",[112,1246,1247],{"class":351},"  \u002F\u002F WebCodecs는 마이크로초\n",[112,1249,1251],{"class":114,"line":1250},40,[112,1252,154],{"emptyLinePlaceholder":153},[112,1254,1256],{"class":114,"line":1255},41,[112,1257,1258],{"class":351},"  \u002F\u002F 매초 1프레임만 추출\n",[112,1260,1262,1264,1267,1269,1272,1274,1277,1279,1282],{"class":114,"line":1261},42,[112,1263,136],{"class":118},[112,1265,1266],{"class":139}," currentSecond",[112,1268,143],{"class":126},[112,1270,1271],{"class":139}," Math",[112,1273,198],{"class":126},[112,1275,1276],{"class":122},"floor",[112,1278,28],{"class":146},[112,1280,1281],{"class":139},"timestampSeconds",[112,1283,213],{"class":146},[112,1285,1287,1289,1291,1294,1297,1300,1302],{"class":114,"line":1286},43,[112,1288,161],{"class":160},[112,1290,164],{"class":146},[112,1292,1293],{"class":139},"currentSecond",[112,1295,1296],{"class":126}," !==",[112,1298,1299],{"class":139}," lastExtractedSecond",[112,1301,257],{"class":146},[112,1303,189],{"class":126},[112,1305,1307,1310,1312],{"class":114,"line":1306},44,[112,1308,1309],{"class":139},"    lastExtractedSecond",[112,1311,143],{"class":126},[112,1313,1314],{"class":139}," currentSecond\n",[112,1316,1318],{"class":114,"line":1317},45,[112,1319,154],{"emptyLinePlaceholder":153},[112,1321,1323,1326,1328,1331,1333,1335,1337,1339,1341,1343,1345,1347,1349,1352,1354,1356,1358,1361],{"class":114,"line":1322},46,[112,1324,1325],{"class":139},"    ctx",[112,1327,198],{"class":126},[112,1329,1330],{"class":122},"drawImage",[112,1332,28],{"class":146},[112,1334,1017],{"class":139},[112,1336,448],{"class":126},[112,1338,421],{"class":420},[112,1340,448],{"class":126},[112,1342,421],{"class":420},[112,1344,448],{"class":126},[112,1346,964],{"class":139},[112,1348,198],{"class":126},[112,1350,1351],{"class":139},"width",[112,1353,448],{"class":126},[112,1355,964],{"class":139},[112,1357,198],{"class":126},[112,1359,1360],{"class":139},"height",[112,1362,213],{"class":146},[112,1364,1366,1369,1372,1374,1376,1378,1380,1383,1385,1387,1389,1391,1393,1396,1398,1400,1403,1405,1408,1410],{"class":114,"line":1365},47,[112,1367,1368],{"class":118},"    const",[112,1370,1371],{"class":139}," blob",[112,1373,143],{"class":126},[112,1375,509],{"class":160},[112,1377,964],{"class":139},[112,1379,198],{"class":126},[112,1381,1382],{"class":122},"convertToBlob",[112,1384,28],{"class":146},[112,1386,1144],{"class":126},[112,1388,783],{"class":146},[112,1390,560],{"class":126},[112,1392,819],{"class":126},[112,1394,1395],{"class":175},"image\u002Fjpeg",[112,1397,172],{"class":126},[112,1399,448],{"class":126},[112,1401,1402],{"class":146}," quality",[112,1404,560],{"class":126},[112,1406,1407],{"class":420}," 0.85",[112,1409,567],{"class":126},[112,1411,213],{"class":146},[112,1413,1415,1417,1420,1422,1424,1426,1428,1431],{"class":114,"line":1414},48,[112,1416,1368],{"class":118},[112,1418,1419],{"class":139}," buffer",[112,1421,143],{"class":126},[112,1423,509],{"class":160},[112,1425,1371],{"class":139},[112,1427,198],{"class":126},[112,1429,1430],{"class":122},"arrayBuffer",[112,1432,400],{"class":146},[112,1434,1436],{"class":114,"line":1435},49,[112,1437,154],{"emptyLinePlaceholder":153},[112,1439,1441],{"class":114,"line":1440},50,[112,1442,1443],{"class":351},"    \u002F\u002F OPFS에 쓰기\n",[112,1445,1447,1449,1451,1453,1455,1457,1459,1461,1463,1465],{"class":114,"line":1446},51,[112,1448,1368],{"class":118},[112,1450,504],{"class":139},[112,1452,143],{"class":126},[112,1454,509],{"class":160},[112,1456,512],{"class":139},[112,1458,198],{"class":126},[112,1460,306],{"class":139},[112,1462,198],{"class":126},[112,1464,311],{"class":122},[112,1466,400],{"class":146},[112,1468,1470,1472,1475,1477,1479,1481,1483,1485,1487,1489,1492,1494,1496,1498,1500,1502,1504,1506],{"class":114,"line":1469},52,[112,1471,1368],{"class":118},[112,1473,1474],{"class":139}," outputDir",[112,1476,143],{"class":126},[112,1478,509],{"class":160},[112,1480,504],{"class":139},[112,1482,198],{"class":126},[112,1484,540],{"class":122},[112,1486,28],{"class":146},[112,1488,172],{"class":126},[112,1490,1491],{"class":175},"thumbnails",[112,1493,172],{"class":126},[112,1495,448],{"class":126},[112,1497,554],{"class":126},[112,1499,557],{"class":146},[112,1501,560],{"class":126},[112,1503,564],{"class":563},[112,1505,567],{"class":126},[112,1507,213],{"class":146},[112,1509,1511,1513,1515,1517,1519,1521,1523,1525,1527,1530,1533,1536,1539,1542,1544,1547,1549,1552,1554,1556,1559,1561,1563,1566,1569,1571,1573,1575,1577,1579,1581,1583],{"class":114,"line":1510},53,[112,1512,1368],{"class":118},[112,1514,576],{"class":139},[112,1516,143],{"class":126},[112,1518,509],{"class":160},[112,1520,1474],{"class":139},[112,1522,198],{"class":126},[112,1524,587],{"class":122},[112,1526,28],{"class":146},[112,1528,1529],{"class":126},"`",[112,1531,1532],{"class":175},"thumb_",[112,1534,1535],{"class":126},"${",[112,1537,1538],{"class":122},"String",[112,1540,1541],{"class":139},"(currentSecond)",[112,1543,198],{"class":126},[112,1545,1546],{"class":122},"padStart",[112,1548,28],{"class":139},[112,1550,1551],{"class":420},"6",[112,1553,448],{"class":126},[112,1555,819],{"class":126},[112,1557,1558],{"class":175},"0",[112,1560,172],{"class":126},[112,1562,495],{"class":139},[112,1564,1565],{"class":126},"}",[112,1567,1568],{"class":175},".jpg",[112,1570,1529],{"class":126},[112,1572,448],{"class":126},[112,1574,554],{"class":126},[112,1576,557],{"class":146},[112,1578,560],{"class":126},[112,1580,564],{"class":563},[112,1582,567],{"class":126},[112,1584,213],{"class":146},[112,1586,1588,1590,1593,1595,1597,1599,1601,1604],{"class":114,"line":1587},54,[112,1589,1368],{"class":118},[112,1591,1592],{"class":139}," syncHandle",[112,1594,143],{"class":126},[112,1596,509],{"class":160},[112,1598,576],{"class":139},[112,1600,198],{"class":126},[112,1602,1603],{"class":122},"createSyncAccessHandle",[112,1605,400],{"class":146},[112,1607,1609,1612,1614,1617,1619,1622,1625,1627,1630],{"class":114,"line":1608},55,[112,1610,1611],{"class":139},"    syncHandle",[112,1613,198],{"class":126},[112,1615,1616],{"class":122},"write",[112,1618,28],{"class":146},[112,1620,1621],{"class":126},"new",[112,1623,1624],{"class":122}," Uint8Array",[112,1626,28],{"class":146},[112,1628,1629],{"class":139},"buffer",[112,1631,1632],{"class":146},"))\n",[112,1634,1636,1638,1640,1643],{"class":114,"line":1635},56,[112,1637,1611],{"class":139},[112,1639,198],{"class":126},[112,1641,1642],{"class":122},"close",[112,1644,400],{"class":146},[112,1646,1648],{"class":114,"line":1647},57,[112,1649,154],{"emptyLinePlaceholder":153},[112,1651,1653,1656],{"class":114,"line":1652},58,[112,1654,1655],{"class":139},"    frameCount",[112,1657,1658],{"class":126},"++\n",[112,1660,1662,1665,1667,1669,1671,1673,1675,1677,1679,1682,1684,1686,1689,1691,1694,1696,1698,1700],{"class":114,"line":1661},59,[112,1663,1664],{"class":139},"    self",[112,1666,198],{"class":126},[112,1668,98],{"class":122},[112,1670,28],{"class":146},[112,1672,1144],{"class":126},[112,1674,783],{"class":146},[112,1676,560],{"class":126},[112,1678,819],{"class":126},[112,1680,1681],{"class":175},"progress",[112,1683,172],{"class":126},[112,1685,448],{"class":126},[112,1687,1688],{"class":139}," frameCount",[112,1690,448],{"class":126},[112,1692,1693],{"class":146}," second",[112,1695,560],{"class":126},[112,1697,1266],{"class":139},[112,1699,567],{"class":126},[112,1701,213],{"class":146},[112,1703,1705],{"class":114,"line":1704},60,[112,1706,219],{"class":126},[112,1708,1710],{"class":114,"line":1709},61,[112,1711,154],{"emptyLinePlaceholder":153},[112,1713,1715,1718,1720,1722,1725],{"class":114,"line":1714},62,[112,1716,1717],{"class":139},"  frame",[112,1719,198],{"class":126},[112,1721,1642],{"class":122},[112,1723,1724],{"class":146},"()  ",[112,1726,1727],{"class":351},"\u002F\u002F 중요: VideoFrame은 반드시 닫기. 안 그러면 GPU 메모리 누수\n",[112,1729,1731],{"class":114,"line":1730},63,[112,1732,378],{"class":126},[112,1734,1736],{"class":114,"line":1735},64,[112,1737,154],{"emptyLinePlaceholder":153},[112,1739,1741,1743,1745,1747,1749,1752,1754],{"class":114,"line":1740},65,[112,1742,480],{"class":118},[112,1744,483],{"class":118},[112,1746,878],{"class":122},[112,1748,907],{"class":126},[112,1750,1751],{"class":491}," chunks",[112,1753,923],{"class":126},[112,1755,130],{"class":126},[112,1757,1759,1762,1764,1766,1769,1772,1774,1776],{"class":114,"line":1758},66,[112,1760,1761],{"class":160},"  for",[112,1763,164],{"class":146},[112,1765,389],{"class":118},[112,1767,1768],{"class":139}," chunk",[112,1770,1771],{"class":126}," of",[112,1773,1751],{"class":139},[112,1775,257],{"class":146},[112,1777,189],{"class":126},[112,1779,1781,1784,1786,1789,1791,1793,1796,1798],{"class":114,"line":1780},67,[112,1782,1783],{"class":139},"    decoder",[112,1785,198],{"class":126},[112,1787,1788],{"class":122},"decode",[112,1790,28],{"class":146},[112,1792,1621],{"class":126},[112,1794,1795],{"class":122}," EncodedVideoChunk",[112,1797,28],{"class":146},[112,1799,189],{"class":126},[112,1801,1803,1806,1808,1810,1812,1815,1818,1820,1823,1825,1828,1830,1833,1835],{"class":114,"line":1802},68,[112,1804,1805],{"class":146},"      type",[112,1807,560],{"class":126},[112,1809,1768],{"class":139},[112,1811,198],{"class":126},[112,1813,1814],{"class":139},"keyframe",[112,1816,1817],{"class":126}," ?",[112,1819,819],{"class":126},[112,1821,1822],{"class":175},"key",[112,1824,172],{"class":126},[112,1826,1827],{"class":126}," :",[112,1829,819],{"class":126},[112,1831,1832],{"class":175},"delta",[112,1834,172],{"class":126},[112,1836,1033],{"class":126},[112,1838,1840,1843,1845,1847,1849,1851],{"class":114,"line":1839},69,[112,1841,1842],{"class":146},"      timestamp",[112,1844,560],{"class":126},[112,1846,1768],{"class":139},[112,1848,198],{"class":126},[112,1850,1238],{"class":139},[112,1852,1033],{"class":126},[112,1854,1856,1859,1861,1863,1865],{"class":114,"line":1855},70,[112,1857,1858],{"class":146},"      data",[112,1860,560],{"class":126},[112,1862,1768],{"class":139},[112,1864,198],{"class":126},[112,1866,800],{"class":139},[112,1868,1870,1873],{"class":114,"line":1869},71,[112,1871,1872],{"class":126},"    }",[112,1874,1632],{"class":146},[112,1876,1878],{"class":114,"line":1877},72,[112,1879,219],{"class":126},[112,1881,1883,1885,1888,1890,1893],{"class":114,"line":1882},73,[112,1884,635],{"class":160},[112,1886,1887],{"class":139}," decoder",[112,1889,198],{"class":126},[112,1891,1892],{"class":122},"flush",[112,1894,400],{"class":146},[112,1896,1898,1900,1902,1904,1906,1908,1910,1912,1914,1917,1919,1921,1923,1925],{"class":114,"line":1897},74,[112,1899,1135],{"class":139},[112,1901,198],{"class":126},[112,1903,98],{"class":122},[112,1905,28],{"class":146},[112,1907,1144],{"class":126},[112,1909,783],{"class":146},[112,1911,560],{"class":126},[112,1913,819],{"class":126},[112,1915,1916],{"class":175},"done",[112,1918,172],{"class":126},[112,1920,448],{"class":126},[112,1922,1688],{"class":139},[112,1924,567],{"class":126},[112,1926,213],{"class":146},[112,1928,1930],{"class":114,"line":1929},75,[112,1931,378],{"class":126},[11,1933,1934],{},"몇 가지 함정:",[1936,1937,1938,1952,1958],"ol",{},[1939,1940,1941,1947,1948,1951],"li",{},[25,1942,1943,1946],{},[30,1944,1945],{},"frame.close()","는 필수."," VideoFrame은 GPU 버퍼에 대한 참조를 잡는다. 닫지 않으면 VRAM이 계속 누수되고, 100프레임쯤에서 디코더가 조용히 실패하기 시작. ",[30,1949,1950],{},"try\u002Ffinally","로 처리.",[1939,1953,1954,1957],{},[25,1955,1956],{},"Worker 안의 OffscreenCanvas."," Worker 안에서 렌더링하려면 필요. Chrome 69+, Safari 16.4+, Firefox 105+.",[1939,1959,1960,1965],{},[25,1961,1962,1964],{},[30,1963,74],{},"은 다른 OPFS 작업을 블록."," 열어둔 채로 비동기 작업을 넘기지 마라——쓰고 닫고 다음으로.",[18,1967,1969],{"id":1968},"_4단계-디코더에-먹일-chunk를-어디서-얻는가","4단계: 디코더에 먹일 chunk를 어디서 얻는가",[11,1971,1972],{},"여기가 제일 성가신 단계. WebCodecs는 디먹싱을 안 한다. 날것의 codec chunk를 원한다. MP4 컨테이너를 직접 파싱해서 H.264 NAL unit을 추출해야 한다.",[11,1974,1975],{},"두 가지 길:",[1936,1977,1978,1994],{},[1939,1979,1980,28,1986,1993],{},[25,1981,1982,1985],{},[30,1983,1984],{},"mp4box.js"," 사용",[1987,1988,1992],"a",{"href":1989,"rel":1990},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmp4box",[1991],"nofollow","npm",")——순수 JS MP4 파서. 샘플을 뽑아 WebCodecs에 바로 넘길 수 있다.",[1939,1995,1996,1999],{},[25,1997,1998],{},"FFmpeg WASM을 디먹싱에만 사용","——chunk를 추출해서 WebCodecs에 넘겨 디코딩. 낭비처럼 들리지만, FFmpeg WASM은 \"디코딩+렌더링\"보다 디먹싱이 훨씬 빠르므로 실제로는 합리적.",[11,2001,2002,2003,2005],{},"패턴 1의 ",[30,2004,1984],{}," 코드:",[84,2007,2009],{"className":106,"code":2008,"language":108,"meta":92,"style":92},"import mp4box from 'mp4box'\n\nasync function getCodecChunks(file) {\n  const arrayBuffer = await file.arrayBuffer()\n  arrayBuffer.fileStart = 0  \u002F\u002F mp4box.js 관례\n\n  const mp4boxFile = mp4box.createFile()\n  mp4boxFile.onReady = (info) => {\n    mp4boxFile.setExtractionOptions(info.videoTracks[0].id, null, {\n      nbSamples: 100  \u002F\u002F 배치 사이즈\n    })\n    mp4boxFile.start()\n  }\n\n  const chunks = []\n  mp4boxFile.onSamples = (trackId, ref, samples) => {\n    for (const sample of samples) {\n      chunks.push({\n        keyframe: sample.is_sync,\n        timestamp: sample.cts * 1_000_000 \u002F sample.timescale,\n        data: sample.data\n      })\n    }\n  }\n\n  mp4boxFile.appendBuffer(arrayBuffer)\n  mp4boxFile.flush()\n\n  return {\n    codec: mp4boxFile.getInfo().videoTracks[0].codec,\n    chunks\n  }\n}\n",[30,2010,2011,2030,2034,2051,2070,2087,2091,2110,2133,2172,2185,2191,2202,2206,2210,2220,2252,2272,2285,2301,2331,2344,2351,2356,2360,2364,2379,2389,2393,2399,2431,2436,2440],{"__ignoreMap":92},[112,2012,2013,2016,2019,2022,2024,2027],{"class":114,"line":115},[112,2014,2015],{"class":160},"import",[112,2017,2018],{"class":139}," mp4box ",[112,2020,2021],{"class":160},"from",[112,2023,819],{"class":126},[112,2025,2026],{"class":175},"mp4box",[112,2028,2029],{"class":126},"'\n",[112,2031,2032],{"class":114,"line":133},[112,2033,154],{"emptyLinePlaceholder":153},[112,2035,2036,2038,2040,2043,2045,2047,2049],{"class":114,"line":150},[112,2037,480],{"class":118},[112,2039,483],{"class":118},[112,2041,2042],{"class":122}," getCodecChunks",[112,2044,28],{"class":126},[112,2046,492],{"class":491},[112,2048,495],{"class":126},[112,2050,130],{"class":126},[112,2052,2053,2055,2058,2060,2062,2064,2066,2068],{"class":114,"line":157},[112,2054,136],{"class":118},[112,2056,2057],{"class":139}," arrayBuffer",[112,2059,143],{"class":126},[112,2061,509],{"class":160},[112,2063,638],{"class":139},[112,2065,198],{"class":126},[112,2067,1430],{"class":122},[112,2069,400],{"class":146},[112,2071,2072,2075,2077,2080,2082,2084],{"class":114,"line":192},[112,2073,2074],{"class":139},"  arrayBuffer",[112,2076,198],{"class":126},[112,2078,2079],{"class":139},"fileStart",[112,2081,143],{"class":126},[112,2083,421],{"class":420},[112,2085,2086],{"class":351},"  \u002F\u002F mp4box.js 관례\n",[112,2088,2089],{"class":114,"line":216},[112,2090,154],{"emptyLinePlaceholder":153},[112,2092,2093,2095,2098,2100,2103,2105,2108],{"class":114,"line":222},[112,2094,136],{"class":118},[112,2096,2097],{"class":139}," mp4boxFile",[112,2099,143],{"class":126},[112,2101,2102],{"class":139}," mp4box",[112,2104,198],{"class":126},[112,2106,2107],{"class":122},"createFile",[112,2109,400],{"class":146},[112,2111,2112,2115,2117,2120,2122,2124,2127,2129,2131],{"class":114,"line":227},[112,2113,2114],{"class":139},"  mp4boxFile",[112,2116,198],{"class":126},[112,2118,2119],{"class":122},"onReady",[112,2121,143],{"class":126},[112,2123,164],{"class":126},[112,2125,2126],{"class":491},"info",[112,2128,495],{"class":126},[112,2130,772],{"class":118},[112,2132,130],{"class":126},[112,2134,2135,2138,2140,2143,2145,2147,2149,2152,2155,2157,2160,2162,2165,2167,2170],{"class":114,"line":262},[112,2136,2137],{"class":139},"    mp4boxFile",[112,2139,198],{"class":126},[112,2141,2142],{"class":122},"setExtractionOptions",[112,2144,28],{"class":146},[112,2146,2126],{"class":139},[112,2148,198],{"class":126},[112,2150,2151],{"class":139},"videoTracks",[112,2153,2154],{"class":146},"[",[112,2156,1558],{"class":420},[112,2158,2159],{"class":146},"]",[112,2161,198],{"class":126},[112,2163,2164],{"class":139},"id",[112,2166,448],{"class":126},[112,2168,2169],{"class":126}," null,",[112,2171,130],{"class":126},[112,2173,2174,2177,2179,2182],{"class":114,"line":282},[112,2175,2176],{"class":146},"      nbSamples",[112,2178,560],{"class":126},[112,2180,2181],{"class":420}," 100",[112,2183,2184],{"class":351},"  \u002F\u002F 배치 사이즈\n",[112,2186,2187,2189],{"class":114,"line":287},[112,2188,1872],{"class":126},[112,2190,213],{"class":146},[112,2192,2193,2195,2197,2200],{"class":114,"line":292},[112,2194,2137],{"class":139},[112,2196,198],{"class":126},[112,2198,2199],{"class":122},"start",[112,2201,400],{"class":146},[112,2203,2204],{"class":114,"line":318},[112,2205,219],{"class":126},[112,2207,2208],{"class":114,"line":338},[112,2209,154],{"emptyLinePlaceholder":153},[112,2211,2212,2214,2216,2218],{"class":114,"line":343},[112,2213,136],{"class":118},[112,2215,1751],{"class":139},[112,2217,143],{"class":126},[112,2219,147],{"class":146},[112,2221,2222,2224,2226,2229,2231,2233,2236,2238,2241,2243,2246,2248,2250],{"class":114,"line":348},[112,2223,2114],{"class":139},[112,2225,198],{"class":126},[112,2227,2228],{"class":122},"onSamples",[112,2230,143],{"class":126},[112,2232,164],{"class":126},[112,2234,2235],{"class":491},"trackId",[112,2237,448],{"class":126},[112,2239,2240],{"class":491}," ref",[112,2242,448],{"class":126},[112,2244,2245],{"class":491}," samples",[112,2247,495],{"class":126},[112,2249,772],{"class":118},[112,2251,130],{"class":126},[112,2253,2254,2257,2259,2261,2264,2266,2268,2270],{"class":114,"line":355},[112,2255,2256],{"class":160},"    for",[112,2258,164],{"class":146},[112,2260,389],{"class":118},[112,2262,2263],{"class":139}," sample",[112,2265,1771],{"class":126},[112,2267,2245],{"class":139},[112,2269,257],{"class":146},[112,2271,189],{"class":126},[112,2273,2274,2277,2279,2281,2283],{"class":114,"line":361},[112,2275,2276],{"class":139},"      chunks",[112,2278,198],{"class":126},[112,2280,201],{"class":122},[112,2282,28],{"class":146},[112,2284,189],{"class":126},[112,2286,2287,2290,2292,2294,2296,2299],{"class":114,"line":366},[112,2288,2289],{"class":146},"        keyframe",[112,2291,560],{"class":126},[112,2293,2263],{"class":139},[112,2295,198],{"class":126},[112,2297,2298],{"class":139},"is_sync",[112,2300,1033],{"class":126},[112,2302,2303,2306,2308,2310,2312,2315,2318,2320,2322,2324,2326,2329],{"class":114,"line":375},[112,2304,2305],{"class":146},"        timestamp",[112,2307,560],{"class":126},[112,2309,2263],{"class":139},[112,2311,198],{"class":126},[112,2313,2314],{"class":139},"cts",[112,2316,2317],{"class":126}," *",[112,2319,1244],{"class":420},[112,2321,1241],{"class":126},[112,2323,2263],{"class":139},[112,2325,198],{"class":126},[112,2327,2328],{"class":139},"timescale",[112,2330,1033],{"class":126},[112,2332,2333,2336,2338,2340,2342],{"class":114,"line":381},[112,2334,2335],{"class":146},"        data",[112,2337,560],{"class":126},[112,2339,2263],{"class":139},[112,2341,198],{"class":126},[112,2343,800],{"class":139},[112,2345,2346,2349],{"class":114,"line":386},[112,2347,2348],{"class":126},"      }",[112,2350,213],{"class":146},[112,2352,2353],{"class":114,"line":403},[112,2354,2355],{"class":126},"    }\n",[112,2357,2358],{"class":114,"line":428},[112,2359,219],{"class":126},[112,2361,2362],{"class":114,"line":455},[112,2363,154],{"emptyLinePlaceholder":153},[112,2365,2366,2368,2370,2373,2375,2377],{"class":114,"line":461},[112,2367,2114],{"class":139},[112,2369,198],{"class":126},[112,2371,2372],{"class":122},"appendBuffer",[112,2374,28],{"class":146},[112,2376,1430],{"class":139},[112,2378,213],{"class":146},[112,2380,2381,2383,2385,2387],{"class":114,"line":1084},[112,2382,2114],{"class":139},[112,2384,198],{"class":126},[112,2386,1892],{"class":122},[112,2388,400],{"class":146},[112,2390,2391],{"class":114,"line":1098},[112,2392,154],{"emptyLinePlaceholder":153},[112,2394,2395,2397],{"class":114,"line":1109},[112,2396,369],{"class":160},[112,2398,130],{"class":126},[112,2400,2401,2403,2405,2407,2409,2412,2414,2416,2418,2420,2422,2424,2426,2429],{"class":114,"line":1120},[112,2402,1101],{"class":146},[112,2404,560],{"class":126},[112,2406,2097],{"class":139},[112,2408,198],{"class":126},[112,2410,2411],{"class":122},"getInfo",[112,2413,127],{"class":146},[112,2415,198],{"class":126},[112,2417,2151],{"class":139},[112,2419,2154],{"class":146},[112,2421,1558],{"class":420},[112,2423,2159],{"class":146},[112,2425,198],{"class":126},[112,2427,2428],{"class":139},"codec",[112,2430,1033],{"class":126},[112,2432,2433],{"class":114,"line":1127},[112,2434,2435],{"class":139},"    chunks\n",[112,2437,2438],{"class":114,"line":1132},[112,2439,219],{"class":126},[112,2441,2442],{"class":114,"line":1162},[112,2443,378],{"class":126},[11,2445,2446],{},"HLS\u002FDASH 스트림의 경우 이 단계를 완전히 건너뛸 수 있다——세그먼트를 페치하고 있으므로 날것의 chunk가 손에 있다. FlowPick은 내부적으로 일부 경로에서 이렇게 한다.",[18,2448,2450],{"id":2449},"실측-webcodecs-vs-ffmpeg-wasm-vs-네이티브","실측: WebCodecs vs FFmpeg WASM vs 네이티브",[11,2452,2453],{},"테스트 시나리오: 30분짜리 1080p H.264 비디오에서 매초 1프레임, 총 1800프레임 추출.",[2455,2456,2457,2476],"table",{},[2458,2459,2460],"thead",{},[2461,2462,2463,2467,2470,2473],"tr",{},[2464,2465,2466],"th",{},"수단",[2464,2468,2469],{},"실측 시간",[2464,2471,2472],{},"피크 메모리",[2464,2474,2475],{},"다운로드 사이즈",[2477,2478,2479,2494,2508,2521],"tbody",{},[2461,2480,2481,2485,2488,2491],{},[2482,2483,2484],"td",{},"네이티브 FFmpeg(CLI)",[2482,2486,2487],{},"18s",[2482,2489,2490],{},"80MB",[2482,2492,2493],{},"없음(설치됨)",[2461,2495,2496,2499,2502,2505],{},[2482,2497,2498],{},"FFmpeg WASM(싱글스레드)",[2482,2500,2501],{},"145s",[2482,2503,2504],{},"220MB",[2482,2506,2507],{},"25MB",[2461,2509,2510,2513,2516,2519],{},[2482,2511,2512],{},"FFmpeg WASM(멀티스레드)",[2482,2514,2515],{},"52s",[2482,2517,2518],{},"280MB",[2482,2520,2507],{},[2461,2522,2523,2526,2529,2532],{},[2482,2524,2525],{},"WebCodecs + Worker + OPFS",[2482,2527,2528],{},"31s",[2482,2530,2531],{},"95MB",[2482,2533,2534],{},"\u003C100KB",[11,2536,2537],{},"WebCodecs 승리:",[1936,2539,2540,2543,2546,2549],{},[1939,2541,2542],{},"하드웨어 가속——H.264 디코딩이 진짜 GPU에서 돈다. WASM이 아니라",[1939,2544,2545],{},"25MB짜리 WASM 바이너리가 없다",[1939,2547,2548],{},"메모리 사용량이 낮다 (FFmpeg 내부 버퍼 없음)",[1939,2550,2551],{},"Worker는 진짜 멀티스레드 (FFmpeg WASM의 pthreads는 개선했지만 여전히 오버헤드)",[11,2553,2554],{},"FFmpeg WASM 승리:",[1936,2556,2557,2560,2563,2566],{},[1939,2558,2559],{},"어떤 codec이든 먹는다 (WebCodecs는 브라우저가 지원하는 것만——대개 H.264, H.265, VP8\u002FVP9, AV1)",[1939,2561,2562],{},"컨테이너화\u002F디먹싱이 가능 (WebCodecs는 코덱만)",[1939,2564,2565],{},"엣지 케이스에 강하다",[1939,2567,2568],{},"같은 코드가 어디서든 돈다 (기능 감지 불필요)",[11,2570,2571,2574],{},[25,2572,2573],{},"내 견해:"," codec을 알고 성능이 필요하면 WebCodecs. 폭이 필요하면 FFmpeg WASM. FlowPick은 둘 다 쓴다——범용 리먹스 경로는 FFmpeg WASM, 특정 작업(썸네일 생성, 입력이 H.264로 확정하고 빠르게 하고 싶을 때)은 WebCodecs.",[18,2576,2578],{"id":2577},"opfs-성능의-미학","OPFS 성능의 미학",[11,2580,2581,2582,2584],{},"\"OPFS가 네이티브 디스크만큼 빠르다\"는 설이 있다. 거짓. IndexedDB나 ",[30,2583,70],{},"보다는 빠르지만, 오버헤드가 있다:",[2455,2586,2587,2603],{},[2458,2588,2589],{},[2461,2590,2591,2594,2597,2600],{},[2464,2592,2593],{},"작업",[2464,2595,2596],{},"OPFS(Worker 동기)",[2464,2598,2599],{},"IndexedDB",[2464,2601,2602],{},"네이티브 디스크",[2477,2604,2605,2619,2633,2647],{},[2461,2606,2607,2610,2613,2616],{},[2482,2608,2609],{},"1KB 쓰기",[2482,2611,2612],{},"0.05ms",[2482,2614,2615],{},"1.2ms",[2482,2617,2618],{},"0.01ms",[2461,2620,2621,2624,2627,2630],{},[2482,2622,2623],{},"1MB 쓰기",[2482,2625,2626],{},"1.1ms",[2482,2628,2629],{},"8.5ms",[2482,2631,2632],{},"0.4ms",[2461,2634,2635,2638,2641,2644],{},[2482,2636,2637],{},"100MB 쓰기",[2482,2639,2640],{},"95ms",[2482,2642,2643],{},"850ms",[2482,2645,2646],{},"35ms",[2461,2648,2649,2652,2655,2658],{},[2482,2650,2651],{},"1MB 읽기",[2482,2653,2654],{},"0.8ms",[2482,2656,2657],{},"4.5ms",[2482,2659,2660],{},"0.2ms",[11,2662,2663],{},"OPFS는 네이티브 디스크보다 3-5배 느리지만, IndexedDB의 바이너리 처리보다 5-10배 빠르다. 비디오 처리 워크플로에서는 \"충분\"——병목은 코덱에 있고 I\u002FO가 아니다.",[11,2665,2666],{},"몇 가지 진짜 이점:",[2668,2669,2670,2676,2679],"ul",{},[1939,2671,2672,2673,2675],{},"Worker 안의 ",[30,2674,74],{},"——동기 I\u002FO는 매 쓰기마다 await 오버헤드가 없다",[1939,2677,2678],{},"파일이 메모리보다 커도 된다——5GB 파일이 스트리밍으로 OPFS를 통과. 메모리에 다 안 올려도 된다",[1939,2680,2681],{},"세션을 넘어 영속——처리한 비디오가 리로드 후에도 그 자리에",[18,2683,2685],{"id":2684},"worker-풀로-병렬-디코딩","Worker 풀로 병렬 디코딩",[11,2687,2688],{},"여러 비디오를 배치 처리(전체 디렉토리 썸네일 만들기 등)할 때는 스레드 풀을 쓴다:",[84,2690,2692],{"className":106,"code":2691,"language":108,"meta":92,"style":92},"class WorkerPool {\n  constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {\n    this.workers = Array.from({ length: size }, () => new Worker(workerUrl))\n    this.queue = []\n    this.busy = new Set()\n  }\n\n  async run(task) {\n    const worker = await this.getIdle()\n    this.busy.add(worker)\n\n    return new Promise((resolve, reject) => {\n      worker.onmessage = (e) => {\n        this.busy.delete(worker)\n        if (e.data.type === 'done') resolve(e.data.result)\n        else if (e.data.type === 'error') reject(e.data.error)\n      }\n      worker.postMessage(task)\n    })\n  }\n\n  async getIdle() {\n    if (this.busy.size \u003C this.workers.length) {\n      return this.workers.find(w => !this.busy.has(w))\n    }\n    \u002F\u002F Worker가 빌 때까지 대기\n    return new Promise(resolve => {\n      this.queue.push(resolve)\n    })\n  }\n}\n",[30,2693,2694,2705,2739,2786,2797,2813,2817,2821,2838,2857,2875,2879,2907,2928,2946,2990,3036,3041,3055,3061,3065,3069,3080,3113,3150,3154,3159,3175,3192,3198,3202],{"__ignoreMap":92},[112,2695,2696,2699,2703],{"class":114,"line":115},[112,2697,2698],{"class":118},"class",[112,2700,2702],{"class":2701},"sBMFI"," WorkerPool",[112,2704,130],{"class":126},[112,2706,2707,2710,2712,2715,2717,2720,2722,2724,2726,2729,2732,2735,2737],{"class":114,"line":133},[112,2708,2709],{"class":118},"  constructor",[112,2711,28],{"class":126},[112,2713,2714],{"class":491},"workerUrl",[112,2716,448],{"class":126},[112,2718,2719],{"class":491}," size",[112,2721,143],{"class":126},[112,2723,512],{"class":139},[112,2725,198],{"class":126},[112,2727,2728],{"class":139},"hardwareConcurrency ",[112,2730,2731],{"class":126},"||",[112,2733,2734],{"class":420}," 4",[112,2736,495],{"class":126},[112,2738,130],{"class":126},[112,2740,2741,2744,2747,2749,2752,2754,2756,2758,2760,2763,2765,2767,2770,2773,2775,2777,2780,2782,2784],{"class":114,"line":150},[112,2742,2743],{"class":126},"    this.",[112,2745,2746],{"class":139},"workers",[112,2748,143],{"class":126},[112,2750,2751],{"class":139}," Array",[112,2753,198],{"class":126},[112,2755,2021],{"class":122},[112,2757,28],{"class":146},[112,2759,1144],{"class":126},[112,2761,2762],{"class":146}," length",[112,2764,560],{"class":126},[112,2766,2719],{"class":139},[112,2768,2769],{"class":126}," },",[112,2771,2772],{"class":126}," ()",[112,2774,772],{"class":118},[112,2776,940],{"class":126},[112,2778,2779],{"class":122}," Worker",[112,2781,28],{"class":146},[112,2783,2714],{"class":139},[112,2785,1632],{"class":146},[112,2787,2788,2790,2793,2795],{"class":114,"line":157},[112,2789,2743],{"class":126},[112,2791,2792],{"class":139},"queue",[112,2794,143],{"class":126},[112,2796,147],{"class":146},[112,2798,2799,2801,2804,2806,2808,2811],{"class":114,"line":192},[112,2800,2743],{"class":126},[112,2802,2803],{"class":139},"busy",[112,2805,143],{"class":126},[112,2807,940],{"class":126},[112,2809,2810],{"class":122}," Set",[112,2812,400],{"class":146},[112,2814,2815],{"class":114,"line":216},[112,2816,219],{"class":126},[112,2818,2819],{"class":114,"line":222},[112,2820,154],{"emptyLinePlaceholder":153},[112,2822,2823,2826,2829,2831,2834,2836],{"class":114,"line":227},[112,2824,2825],{"class":118},"  async",[112,2827,2828],{"class":146}," run",[112,2830,28],{"class":126},[112,2832,2833],{"class":491},"task",[112,2835,495],{"class":126},[112,2837,130],{"class":126},[112,2839,2840,2842,2845,2847,2849,2852,2855],{"class":114,"line":262},[112,2841,1368],{"class":118},[112,2843,2844],{"class":139}," worker",[112,2846,143],{"class":126},[112,2848,509],{"class":160},[112,2850,2851],{"class":126}," this.",[112,2853,2854],{"class":122},"getIdle",[112,2856,400],{"class":146},[112,2858,2859,2861,2863,2865,2868,2870,2873],{"class":114,"line":282},[112,2860,2743],{"class":126},[112,2862,2803],{"class":139},[112,2864,198],{"class":126},[112,2866,2867],{"class":122},"add",[112,2869,28],{"class":146},[112,2871,2872],{"class":139},"worker",[112,2874,213],{"class":146},[112,2876,2877],{"class":114,"line":287},[112,2878,154],{"emptyLinePlaceholder":153},[112,2880,2881,2884,2886,2889,2891,2893,2896,2898,2901,2903,2905],{"class":114,"line":292},[112,2882,2883],{"class":160},"    return",[112,2885,940],{"class":126},[112,2887,2888],{"class":2701}," Promise",[112,2890,28],{"class":146},[112,2892,28],{"class":126},[112,2894,2895],{"class":491},"resolve",[112,2897,448],{"class":126},[112,2899,2900],{"class":491}," reject",[112,2902,495],{"class":126},[112,2904,772],{"class":118},[112,2906,130],{"class":126},[112,2908,2909,2912,2914,2916,2918,2920,2922,2924,2926],{"class":114,"line":318},[112,2910,2911],{"class":139},"      worker",[112,2913,198],{"class":126},[112,2915,757],{"class":122},[112,2917,143],{"class":126},[112,2919,164],{"class":126},[112,2921,767],{"class":491},[112,2923,495],{"class":126},[112,2925,772],{"class":118},[112,2927,130],{"class":126},[112,2929,2930,2933,2935,2937,2940,2942,2944],{"class":114,"line":338},[112,2931,2932],{"class":126},"        this.",[112,2934,2803],{"class":139},[112,2936,198],{"class":126},[112,2938,2939],{"class":122},"delete",[112,2941,28],{"class":146},[112,2943,2872],{"class":139},[112,2945,213],{"class":146},[112,2947,2948,2951,2953,2955,2957,2959,2961,2963,2965,2967,2969,2971,2973,2975,2977,2979,2981,2983,2985,2988],{"class":114,"line":343},[112,2949,2950],{"class":160},"        if",[112,2952,164],{"class":146},[112,2954,767],{"class":139},[112,2956,198],{"class":126},[112,2958,841],{"class":139},[112,2960,198],{"class":126},[112,2962,813],{"class":139},[112,2964,816],{"class":126},[112,2966,819],{"class":126},[112,2968,1916],{"class":175},[112,2970,172],{"class":126},[112,2972,257],{"class":146},[112,2974,2895],{"class":122},[112,2976,28],{"class":146},[112,2978,767],{"class":139},[112,2980,198],{"class":126},[112,2982,841],{"class":139},[112,2984,198],{"class":126},[112,2986,2987],{"class":139},"result",[112,2989,213],{"class":146},[112,2991,2992,2995,2997,2999,3001,3003,3005,3007,3009,3011,3013,3015,3017,3019,3022,3024,3026,3028,3030,3032,3034],{"class":114,"line":348},[112,2993,2994],{"class":160},"        else",[112,2996,854],{"class":160},[112,2998,164],{"class":146},[112,3000,767],{"class":139},[112,3002,198],{"class":126},[112,3004,841],{"class":139},[112,3006,198],{"class":126},[112,3008,813],{"class":139},[112,3010,816],{"class":126},[112,3012,819],{"class":126},[112,3014,1056],{"class":175},[112,3016,172],{"class":126},[112,3018,257],{"class":146},[112,3020,3021],{"class":122},"reject",[112,3023,28],{"class":146},[112,3025,767],{"class":139},[112,3027,198],{"class":126},[112,3029,841],{"class":139},[112,3031,198],{"class":126},[112,3033,1056],{"class":139},[112,3035,213],{"class":146},[112,3037,3038],{"class":114,"line":355},[112,3039,3040],{"class":126},"      }\n",[112,3042,3043,3045,3047,3049,3051,3053],{"class":114,"line":361},[112,3044,2911],{"class":139},[112,3046,198],{"class":126},[112,3048,98],{"class":122},[112,3050,28],{"class":146},[112,3052,2833],{"class":139},[112,3054,213],{"class":146},[112,3056,3057,3059],{"class":114,"line":366},[112,3058,1872],{"class":126},[112,3060,213],{"class":146},[112,3062,3063],{"class":114,"line":375},[112,3064,219],{"class":126},[112,3066,3067],{"class":114,"line":381},[112,3068,154],{"emptyLinePlaceholder":153},[112,3070,3071,3073,3076,3078],{"class":114,"line":386},[112,3072,2825],{"class":118},[112,3074,3075],{"class":146}," getIdle",[112,3077,127],{"class":126},[112,3079,130],{"class":126},[112,3081,3082,3085,3087,3090,3092,3094,3097,3100,3102,3104,3106,3109,3111],{"class":114,"line":403},[112,3083,3084],{"class":160},"    if",[112,3086,164],{"class":146},[112,3088,3089],{"class":126},"this.",[112,3091,2803],{"class":139},[112,3093,198],{"class":126},[112,3095,3096],{"class":139},"size",[112,3098,3099],{"class":126}," \u003C",[112,3101,2851],{"class":126},[112,3103,2746],{"class":139},[112,3105,198],{"class":126},[112,3107,3108],{"class":139},"length",[112,3110,257],{"class":146},[112,3112,189],{"class":126},[112,3114,3115,3118,3120,3122,3124,3127,3129,3132,3134,3137,3139,3141,3144,3146,3148],{"class":114,"line":428},[112,3116,3117],{"class":160},"      return",[112,3119,2851],{"class":126},[112,3121,2746],{"class":139},[112,3123,198],{"class":126},[112,3125,3126],{"class":122},"find",[112,3128,28],{"class":146},[112,3130,3131],{"class":491},"w",[112,3133,772],{"class":118},[112,3135,3136],{"class":126}," !this.",[112,3138,2803],{"class":139},[112,3140,198],{"class":126},[112,3142,3143],{"class":122},"has",[112,3145,28],{"class":146},[112,3147,3131],{"class":139},[112,3149,1632],{"class":146},[112,3151,3152],{"class":114,"line":455},[112,3153,2355],{"class":126},[112,3155,3156],{"class":114,"line":461},[112,3157,3158],{"class":351},"    \u002F\u002F Worker가 빌 때까지 대기\n",[112,3160,3161,3163,3165,3167,3169,3171,3173],{"class":114,"line":1084},[112,3162,2883],{"class":160},[112,3164,940],{"class":126},[112,3166,2888],{"class":2701},[112,3168,28],{"class":146},[112,3170,2895],{"class":491},[112,3172,772],{"class":118},[112,3174,130],{"class":126},[112,3176,3177,3180,3182,3184,3186,3188,3190],{"class":114,"line":1098},[112,3178,3179],{"class":126},"      this.",[112,3181,2792],{"class":139},[112,3183,198],{"class":126},[112,3185,201],{"class":122},[112,3187,28],{"class":146},[112,3189,2895],{"class":139},[112,3191,213],{"class":146},[112,3193,3194,3196],{"class":114,"line":1109},[112,3195,1872],{"class":126},[112,3197,213],{"class":146},[112,3199,3200],{"class":114,"line":1120},[112,3201,219],{"class":126},[112,3203,3204],{"class":114,"line":1127},[112,3205,378],{"class":126},[11,3207,3208],{},"8코어 머신에 8개 Worker를 띄우면 8개 파일을 병렬 처리——각각 스레드를 독점하고 각각 하드웨어 가속으로 디코딩. 내 M2 MacBook에서 실측: 싱글코어 240프레임\u002F초, 전체 약 1900프레임\u002F초. 매초 30분짜리 비디오를 처리하는 셈.",[11,3210,3211],{},"다만 함정: 하드웨어 디코더의 동시성엔 한계가 있다. 대부분의 GPU는 동시에 2-4개 스트림만 디코딩. 그 이상은 소프트웨어 디코딩으로 폴백해서 오히려 느려진다. 타겟 하드웨어에서 테스트.",[18,3213,3215],{"id":3214},"실제로-겪은-함정","실제로 겪은 함정",[11,3217,3218,3224,3225,3227],{},[25,3219,3220,3221,3223],{},"함정 1: ",[30,3222,1945],{}," 잊기."," 닫지 않은 VideoFrame은 VRAM을 누수. 100프레임쯤 누수하면 디코더가 조용히 실패. ",[30,3226,1950],{},"로:",[84,3229,3231],{"className":106,"code":3230,"language":108,"meta":92,"style":92},"try {\n  \u002F\u002F ... frame으로 작업 ...\n} finally {\n  frame.close()\n}\n",[30,3232,3233,3240,3245,3254,3264],{"__ignoreMap":92},[112,3234,3235,3238],{"class":114,"line":115},[112,3236,3237],{"class":160},"try",[112,3239,130],{"class":126},[112,3241,3242],{"class":114,"line":133},[112,3243,3244],{"class":351},"  \u002F\u002F ... frame으로 작업 ...\n",[112,3246,3247,3249,3252],{"class":114,"line":150},[112,3248,1565],{"class":126},[112,3250,3251],{"class":160}," finally",[112,3253,130],{"class":126},[112,3255,3256,3258,3260,3262],{"class":114,"line":157},[112,3257,1717],{"class":139},[112,3259,198],{"class":126},[112,3261,1642],{"class":122},[112,3263,400],{"class":146},[112,3265,3266],{"class":114,"line":192},[112,3267,378],{"class":126},[11,3269,3270,3273,3274,3277],{},[25,3271,3272],{},"함정 2: 디코딩 순서 ≠ 표시 순서."," B 프레임이 있는 H.264는 DTS 순으로 디코딩, PTS 순으로 표시. 디코딩 순으로 프레임을 처리하면 썸네일이 순서 섞임. WebCodecs는 둘 다 준다——",[30,3275,3276],{},"frame.timestamp","는 PTS, 디코딩은 DTS로 진행.",[11,3279,3280,3283,3284,3287,3288,33,3290,33,3293,3296],{},[25,3281,3282],{},"함정 3: codec 문자열 포맷."," WebCodecs는 ",[30,3285,3286],{},"'avc1.640028'","(constraint 바이트 포함)을 요구. MP4에는 ",[30,3289,3286],{},[30,3291,3292],{},"'avc1.64.0028'",[30,3294,3295],{},"'avc1.42E01E'"," 등으로 저장되는 경우가 있다. 디코더 설정 전에 정규화.",[11,3298,3299,3302,3303,3305,3306,3309,3310,3313],{},[25,3300,3301],{},"함정 4: Worker 모듈 로드."," Worker는 모든 브라우저에서 임의 URL에서 ",[30,3304,2015],{},"할 수 있는 건 아니다. ",[30,3307,3308],{},"importScripts()","를 쓰거나 Worker 생성자에서 ",[30,3311,3312],{},"\"type\": \"module\""," 지정(Chrome 80+, Safari 15+).",[11,3315,3316,3319,3320,3323,3324,3327],{},[25,3317,3318],{},"함정 5: OPFS 쿼터."," 브라우저는 OPFS를 디스크 여유 공간의 퍼센트로 제한. 5GB 쓰기가 디스크 꽉 차서 실패할 수 있다. ",[30,3321,3322],{},"QuotaExceededError","를 catch하고 ",[30,3325,3326],{},"showSaveFilePicker()"," 스트리밍 쓰기로 폴백.",[18,3329,3331],{"id":3330},"언제-뭘-쓸까","언제 뭘 쓸까",[11,3333,3334],{},[25,3335,3336],{},"FFmpeg WASM을 쓸 때:",[2668,3338,3339,3342,3345,3348],{},[1939,3340,3341],{},"모든 codec에 대응하고 싶다 (HEVC, VP9, AV1, MPEG-TS, MKV, FLV)",[1939,3343,3344],{},"컨테이너 변환을 하고 싶다 (TS → MP4, MP4 → WebM)",[1939,3346,3347],{},"필터 체인이 필요하다 (오버레이, 스케일, 크롭, 결합)",[1939,3349,3350],{},"신뢰성이 속도보다 중요",[11,3352,3353],{},[25,3354,3355],{},"WebCodecs를 쓸 때:",[2668,3357,3358,3361,3364,3367],{},[1939,3359,3360],{},"codec이 H.264\u002FH.265\u002FVP9\u002FAV1로 앎",[1939,3362,3363],{},"프레임 단위 조작을 한다 (썸네일, 모션 감지, 프레임 추출)",[1939,3365,3366],{},"실시간 성능이 필요 (라이브 비디오 필터링, 프레임 분석)",[1939,3368,3369],{},"메모리 예산이 빠듯 (25MB짜리 WASM 다운로드하고 싶지 않다)",[11,3371,3372],{},[25,3373,3374],{},"Web Workers를 쓸 때:",[2668,3376,3377,3380],{},[1939,3378,3379],{},"CPU 무거운 작업이 있고 메인 스레드를 막고 싶지 않다",[1939,3381,3382],{},"독립 단위(파일, 세그먼트)를 병렬 처리하고 싶다",[11,3384,3385],{},[25,3386,3387],{},"OPFS를 쓸 때:",[2668,3389,3390,3393,3396],{},[1939,3391,3392],{},"5MB(localStorage 상한)를 넘는다",[1939,3394,3395],{},"중간 상태를 영속화하고 싶다 (리로드마다 재디코딩하고 싶지 않다)",[1939,3397,3398],{},"큰 파일을 스트리밍 처리하고 싶다. 메모리에 다 올리고 싶지 않다",[18,3400,3402],{"id":3401},"참고-자료","참고 자료",[2668,3404,3405,3413,3421,3429,3437],{},[1939,3406,3407,3412],{},[1987,3408,3411],{"href":3409,"rel":3410},"https:\u002F\u002Fwww.w3.org\u002FTR\u002Fwebcodecs\u002F",[1991],"WebCodecs 사양","——실제 사양. 의외로 읽기 쉽다",[1939,3414,3415,3420],{},[1987,3416,3419],{"href":3417,"rel":3418},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWebCodecs_API",[1991],"MDN WebCodecs API","——실용 문서, 예제 포함",[1939,3422,3423,3428],{},[1987,3424,3427],{"href":3425,"rel":3426},"https:\u002F\u002Fgithub.com\u002Fw3c\u002Fwebcodecs\u002Ftree\u002Fmain\u002Fsamples",[1991],"WebCodecs samples 저장소","——공식 동작 샘플",[1939,3430,3431,3436],{},[1987,3432,3435],{"href":3433,"rel":3434},"https:\u002F\u002Ffs.spec.whatwg.org\u002F",[1991],"OPFS 사양","——File System Access API, OPFS 포함",[1939,3438,3439,3443],{},[1987,3440,1984],{"href":3441,"rel":3442},"https:\u002F\u002Fgithub.com\u002Fgpac\u002Fmp4box.js",[1991],"——GPAC의 JS MP4 파서. WebCodecs 입력의 사실상 표준",[18,3445,3446],{"id":3446},"정리",[11,3448,3449,3450,198],{},"WebCodecs + Workers + OPFS는 성능 민감 시나리오에서 FFmpeg WASM의 진짜 대안. 완전한 대안은 아니다——FFmpeg WASM은 codec 폭과 엣지 케이스에서 여전히 이긴다——하지만 codec을 알고, 하드웨어 가속, 프레임 레벨 처리 워크로드에서 WebCodecs는 4-5배 빠르고 250배 작다. 다음은 ",[1987,3451,3453],{"href":3452},"\u002Fko\u002Fblog\u002Fis-it-legal-to-download-streaming-video","스트리밍 다운로드가 합법인지",[11,3455,3456,3457,3461],{},"완전한 codec, 해상도, 파일 사이즈에서의 벤치마크 데이터는 ",[1987,3458,3460],{"href":3459},"\u002Fko\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","브라우저 비디오 처리 성능 벤치마크","에서.",[3463,3464],"hr",{},[18,3466,3468],{"id":3467},"추천-글","추천 글",[2668,3470,3471,3480,3487,3496,3505,3514],{},[1939,3472,3473,3479],{},[25,3474,3475],{},[1987,3476,3478],{"href":3477},"\u002Fko\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff","브라우저에서 비디오 리먹스: fMP4, ISOBMFF, 트랜스코딩하지 않는 이유","——이 글이 codec 레이어, 저 글이 컨테이너 레이어. 보완 관계",[1939,3481,3482,3486],{},[25,3483,3484],{},[1987,3485,3460],{"href":3459},"——이 글의 데이터의 완전판",[1939,3488,3489,3495],{},[25,3490,3491],{},[1987,3492,3494],{"href":3493},"\u002Fko\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","FlowPick은 어떻게 브라우저에서 수백 개의 비디오 세그먼트를 하나의 MP4로 합치는가","——FlowPick이 어디서 FFmpeg WASM을 쓰고 어디서 WebCodecs를 쓰는가",[1939,3497,3498,3504],{},[25,3499,3500],{},[1987,3501,3503],{"href":3502},"\u002Fko\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH 딥: SegmentTemplate, ContentProtection, 시점 전환 MPD","——WebCodecs에 넘길 chunk가 어디서 오는가",[1939,3506,3507,3513],{},[25,3508,3509],{},[1987,3510,3512],{"href":3511},"\u002Fko\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack","HLS 딥: 암호화, 멀티트랙, 기억해야 할 EXT-X 태그","——세그먼트의 공급원",[1939,3515,3516,3522],{},[25,3517,3518],{},[1987,3519,3521],{"href":3520},"\u002Fko\u002Fblog\u002Fhow-i-built-flowpick-in-three-months","3개월 만에 FlowPick을 만든 전 과정","——개발 스토리. WebCodecs vs FFmpeg WASM 결정도 수록",[3524,3525,3526],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":92,"searchDepth":133,"depth":133,"links":3528},[3529,3530,3531,3532,3533,3534,3535,3536,3537,3538,3539,3540,3541,3542],{"id":20,"depth":133,"text":21},{"id":78,"depth":133,"text":79},{"id":102,"depth":133,"text":103},{"id":469,"depth":133,"text":470},{"id":697,"depth":133,"text":698},{"id":1968,"depth":133,"text":1969},{"id":2449,"depth":133,"text":2450},{"id":2577,"depth":133,"text":2578},{"id":2684,"depth":133,"text":2685},{"id":3214,"depth":133,"text":3215},{"id":3330,"depth":133,"text":3331},{"id":3401,"depth":133,"text":3402},{"id":3446,"depth":133,"text":3446},{"id":3467,"depth":133,"text":3468},"tips","2026-08-04","WebCodecs로 하드웨어 디코딩, Web Workers로 멀티스레드, OPFS로 로컬 파일 I\u002FO 고처리량화——FFmpeg WASM과의 실측 비교 포함.","md","\u002Fscreenshots\u002Fformat-conversion.png",{},"\u002Fko\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing",{"title":5,"description":3545},"ko\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing",[3553,3554,3555,3556,3557,3558],"webcodecs","web workers","opfs","성능","wasm","딥","rwSFbrW9VYTfUVf3QqWmQb6bm5VXP38yLcOxzPFxJ5o",[3561,3565],{"title":3562,"path":3452,"stem":3563,"description":3564,"date":3544,"category":3543,"children":-1},"스트리밍 비디오 다운로드는 합법인가?기술과 법적 체크리스트","ko\u002Fblog\u002Fis-it-legal-to-download-streaming-video","HLS\u002FDASH 스트림 다운로드의 합법성을 법과 기술 양쪽에서 본다——DRM vs 비보호 콘텐츠, 개인 사용 vs 재배포, 미국\u002F유럽\u002F아시아의 차이, 그리고 FlowPick이 스스로 지키는 가이드라인.",{"title":3566,"path":3567,"stem":3568,"description":3569,"date":3570,"category":3543,"children":-1},"DASH 스트리밍이란? 비개발자를 위한 MPD 파일 해설","\u002Fko\u002Fblog\u002Fwhat-is-dash-mpd-streaming","ko\u002Fblog\u002Fwhat-is-dash-mpd-streaming","MPEG-DASH와 MPD 매니페스트의 작동 방식, DASH 영상과 오디오 트랙을 다운로드하는 방법, 일반 MP4보다 저장하기 어려운 이유를 알아보세요.","2026-07-01",1787670593697]