[{"data":1,"prerenderedAt":3586},["ShallowReactive",2],{"blog-en-webcodecs-web-workers-opfs-video-processing":3,"blog-en-webcodecs-web-workers-opfs-video-processing-surround":3575},{"id":4,"title":5,"author":6,"body":7,"category":3558,"date":3559,"description":3560,"extension":3561,"image":3562,"lastModified":3559,"meta":3563,"navigation":157,"path":3564,"readingTime":347,"seo":3565,"stem":3566,"tags":3567,"__hash__":3574},"blog\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing.md","WebCodecs + Web Workers + OPFS: Practical Video Processing in the Browser","FlowPick Team",{"type":8,"value":9,"toc":3541},"minimark",[10,14,17,22,56,62,76,80,83,93,104,108,468,471,475,689,699,703,1936,1939,1966,1970,1973,1976,2000,2005,2443,2446,2450,2453,2534,2537,2551,2554,2568,2574,2578,2584,2660,2663,2666,2680,2684,2687,3204,3207,3210,3214,3225,3265,3275,3295,3312,3325,3329,3334,3348,3353,3367,3372,3380,3385,3396,3400,3441,3445,3453,3460,3463,3467,3504,3508,3537],[11,12,13],"p",{},"FFmpeg WASM is the right default for browser-side video processing. But it's a 25MB download, runs in a single thread by default, and is a black box. For some workflows, the newer browser-native APIs — WebCodecs, Web Workers, and OPFS — let you build something faster, smaller, and more debuggable.",[11,15,16],{},"This article is a working example: decode a video file, extract frames at specific timestamps, write them to local storage, all without FFmpeg. With real benchmarks at the end.",[18,19,21],"h2",{"id":20},"the-three-apis","The three APIs",[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",") — hardware-accelerated codec access. Bypasses the ",[30,45,46],{},"\u003Cvideo>"," element's \"play and capture\" model. You feed it an ",[30,49,50],{},"EncodedVideoChunk"," (a raw H.264 NAL unit, for instance), it returns ",[30,53,54],{},"VideoFrame"," objects you can render to a canvas, encode to a different format, or pipe downstream.",[11,57,58,61],{},[25,59,60],{},"Web Workers"," — separate threads for CPU-heavy work. Codec parsing, frame transformations, muxing logic all belong here so the main thread stays responsive. Workers can also use OPFS directly.",[11,63,64,67,68,71,72,75],{},[25,65,66],{},"OPFS (Origin Private File System)"," — a real, high-throughput filesystem accessible to web content. Unlike ",[30,69,70],{},"localStorage"," (synchronous, 5MB cap) or IndexedDB (async but slow for binary blobs), OPFS is designed for files. Workers get synchronous access via ",[30,73,74],{},"createSyncAccessHandle()",", which is critical for performance.",[18,77,79],{"id":78},"the-setup","The setup",[11,81,82],{},"Here's the architecture I'll build: a user drops a video file on the page, we extract one frame per second, store frames as JPEGs in OPFS, and provide a download link. Real-world use case: generating thumbnails for a video library.",[84,85,90],"pre",{"className":86,"code":88,"language":89},[87],"language-text","Main thread:\n  - File input\n  - Worker creation\n  - UI updates (progress, results)\n\nWorker:\n  - Receives file via OPFS path (not postMessage — too slow)\n  - Uses WebCodecs VideoDecoder to decode frames\n  - Canvas to render + toBlob for JPEG\n  - Writes JPEGs to OPFS\n  - Posts progress back to main thread\n","text",[30,91,88],{"__ignoreMap":92},"",[11,94,95,96,99,100,103],{},"The \"receive file via OPFS path\" is important. Posting a 500MB file via ",[30,97,98],{},"postMessage"," (even as a ",[30,101,102],{},"Transferable",") is slow and copies memory. Writing the file to OPFS first, then passing the path to the worker, is dramatically faster.",[18,105,107],{"id":106},"step-1-detect-support","Step 1: Detect support",[84,109,113],{"className":110,"code":111,"language":112,"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 not supported. Need Chrome 94+ or equivalent.')\n  }\n\n  if (!window.showOpenFilePicker && !window.FileSystemHandle) {\n    issues.push('File System Access API not supported.')\n  }\n\n  if (!navigator.storage.getDirectory) {\n    issues.push('OPFS not supported. Need Chrome 102+ or equivalent.')\n  }\n\n  \u002F\u002F Worker support is universal, but check if Workers can use OPFS sync access\n  \u002F\u002F (Older Chrome had Workers but no sync OPFS in Workers)\n\n  return issues\n}\n\nconst issues = checkSupport()\nif (issues.length > 0) {\n  console.warn('Feature gaps:', issues)\n  \u002F\u002F Fall back to FFmpeg WASM\n}\n","js",[30,114,115,135,152,159,194,218,224,229,264,284,289,294,320,340,345,350,357,363,368,377,383,388,405,430,457,463],{"__ignoreMap":92},[116,117,120,124,128,132],"span",{"class":118,"line":119},"line",1,[116,121,123],{"class":122},"spNyl","function",[116,125,127],{"class":126},"s2Zo4"," checkSupport",[116,129,131],{"class":130},"sMK4o","()",[116,133,134],{"class":130}," {\n",[116,136,138,141,145,148],{"class":118,"line":137},2,[116,139,140],{"class":122},"  const",[116,142,144],{"class":143},"sTEyZ"," issues",[116,146,147],{"class":130}," =",[116,149,151],{"class":150},"swJcz"," []\n",[116,153,155],{"class":118,"line":154},3,[116,156,158],{"emptyLinePlaceholder":157},true,"\n",[116,160,162,166,168,171,174,177,180,182,185,188,191],{"class":118,"line":161},4,[116,163,165],{"class":164},"s7zQu","  if",[116,167,28],{"class":150},[116,169,170],{"class":130},"!",[116,172,173],{"class":150},"(",[116,175,176],{"class":130},"'",[116,178,32],{"class":179},"sfazB",[116,181,176],{"class":130},[116,183,184],{"class":130}," in",[116,186,187],{"class":143}," window",[116,189,190],{"class":150},")) ",[116,192,193],{"class":130},"{\n",[116,195,197,200,203,206,208,210,213,215],{"class":118,"line":196},5,[116,198,199],{"class":143},"    issues",[116,201,202],{"class":130},".",[116,204,205],{"class":126},"push",[116,207,173],{"class":150},[116,209,176],{"class":130},[116,211,212],{"class":179},"WebCodecs VideoDecoder not supported. Need Chrome 94+ or equivalent.",[116,214,176],{"class":130},[116,216,217],{"class":150},")\n",[116,219,221],{"class":118,"line":220},6,[116,222,223],{"class":130},"  }\n",[116,225,227],{"class":118,"line":226},7,[116,228,158],{"emptyLinePlaceholder":157},[116,230,232,234,236,238,241,243,246,249,252,254,256,259,262],{"class":118,"line":231},8,[116,233,165],{"class":164},[116,235,28],{"class":150},[116,237,170],{"class":130},[116,239,240],{"class":143},"window",[116,242,202],{"class":130},[116,244,245],{"class":143},"showOpenFilePicker",[116,247,248],{"class":130}," &&",[116,250,251],{"class":130}," !",[116,253,240],{"class":143},[116,255,202],{"class":130},[116,257,258],{"class":143},"FileSystemHandle",[116,260,261],{"class":150},") ",[116,263,193],{"class":130},[116,265,267,269,271,273,275,277,280,282],{"class":118,"line":266},9,[116,268,199],{"class":143},[116,270,202],{"class":130},[116,272,205],{"class":126},[116,274,173],{"class":150},[116,276,176],{"class":130},[116,278,279],{"class":179},"File System Access API not supported.",[116,281,176],{"class":130},[116,283,217],{"class":150},[116,285,287],{"class":118,"line":286},10,[116,288,223],{"class":130},[116,290,292],{"class":118,"line":291},11,[116,293,158],{"emptyLinePlaceholder":157},[116,295,297,299,301,303,306,308,311,313,316,318],{"class":118,"line":296},12,[116,298,165],{"class":164},[116,300,28],{"class":150},[116,302,170],{"class":130},[116,304,305],{"class":143},"navigator",[116,307,202],{"class":130},[116,309,310],{"class":143},"storage",[116,312,202],{"class":130},[116,314,315],{"class":143},"getDirectory",[116,317,261],{"class":150},[116,319,193],{"class":130},[116,321,323,325,327,329,331,333,336,338],{"class":118,"line":322},13,[116,324,199],{"class":143},[116,326,202],{"class":130},[116,328,205],{"class":126},[116,330,173],{"class":150},[116,332,176],{"class":130},[116,334,335],{"class":179},"OPFS not supported. Need Chrome 102+ or equivalent.",[116,337,176],{"class":130},[116,339,217],{"class":150},[116,341,343],{"class":118,"line":342},14,[116,344,223],{"class":130},[116,346,348],{"class":118,"line":347},15,[116,349,158],{"emptyLinePlaceholder":157},[116,351,353],{"class":118,"line":352},16,[116,354,356],{"class":355},"sHwdD","  \u002F\u002F Worker support is universal, but check if Workers can use OPFS sync access\n",[116,358,360],{"class":118,"line":359},17,[116,361,362],{"class":355},"  \u002F\u002F (Older Chrome had Workers but no sync OPFS in Workers)\n",[116,364,366],{"class":118,"line":365},18,[116,367,158],{"emptyLinePlaceholder":157},[116,369,371,374],{"class":118,"line":370},19,[116,372,373],{"class":164},"  return",[116,375,376],{"class":143}," issues\n",[116,378,380],{"class":118,"line":379},20,[116,381,382],{"class":130},"}\n",[116,384,386],{"class":118,"line":385},21,[116,387,158],{"emptyLinePlaceholder":157},[116,389,391,394,397,400,402],{"class":118,"line":390},22,[116,392,393],{"class":122},"const",[116,395,396],{"class":143}," issues ",[116,398,399],{"class":130},"=",[116,401,127],{"class":126},[116,403,404],{"class":143},"()\n",[116,406,408,411,414,416,419,422,426,428],{"class":118,"line":407},23,[116,409,410],{"class":164},"if",[116,412,413],{"class":143}," (issues",[116,415,202],{"class":130},[116,417,418],{"class":143},"length ",[116,420,421],{"class":130},">",[116,423,425],{"class":424},"sbssI"," 0",[116,427,261],{"class":143},[116,429,193],{"class":130},[116,431,433,436,438,441,443,445,448,450,453,455],{"class":118,"line":432},24,[116,434,435],{"class":143},"  console",[116,437,202],{"class":130},[116,439,440],{"class":126},"warn",[116,442,173],{"class":150},[116,444,176],{"class":130},[116,446,447],{"class":179},"Feature gaps:",[116,449,176],{"class":130},[116,451,452],{"class":130},",",[116,454,144],{"class":143},[116,456,217],{"class":150},[116,458,460],{"class":118,"line":459},25,[116,461,462],{"class":355},"  \u002F\u002F Fall back to FFmpeg WASM\n",[116,464,466],{"class":118,"line":465},26,[116,467,382],{"class":130},[11,469,470],{},"Browser support as of 2026: Chrome\u002FEdge 102+, Safari 16.4+ (partial WebCodecs), Firefox 130+ (gated behind flag in some versions). For anything production-facing, feature-detect and fall back to FFmpeg WASM.",[18,472,474],{"id":473},"step-2-write-the-file-to-opfs","Step 2: Write the file to OPFS",[84,476,478],{"className":110,"code":477,"language":112,"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 path within OPFS root's 'input' dir\n}\n",[30,479,480,502,527,574,615,635,662,666,685],{"__ignoreMap":92},[116,481,482,485,488,491,493,497,500],{"class":118,"line":119},[116,483,484],{"class":122},"async",[116,486,487],{"class":122}," function",[116,489,490],{"class":126}," stageFileToOpfs",[116,492,173],{"class":130},[116,494,496],{"class":495},"sHdIc","file",[116,498,499],{"class":130},")",[116,501,134],{"class":130},[116,503,504,506,509,511,514,517,519,521,523,525],{"class":118,"line":137},[116,505,140],{"class":122},[116,507,508],{"class":143}," root",[116,510,147],{"class":130},[116,512,513],{"class":164}," await",[116,515,516],{"class":143}," navigator",[116,518,202],{"class":130},[116,520,310],{"class":143},[116,522,202],{"class":130},[116,524,315],{"class":126},[116,526,404],{"class":150},[116,528,529,531,534,536,538,540,542,545,547,549,552,554,556,559,562,565,569,572],{"class":118,"line":154},[116,530,140],{"class":122},[116,532,533],{"class":143}," dir",[116,535,147],{"class":130},[116,537,513],{"class":164},[116,539,508],{"class":143},[116,541,202],{"class":130},[116,543,544],{"class":126},"getDirectoryHandle",[116,546,173],{"class":150},[116,548,176],{"class":130},[116,550,551],{"class":179},"input",[116,553,176],{"class":130},[116,555,452],{"class":130},[116,557,558],{"class":130}," {",[116,560,561],{"class":150}," create",[116,563,564],{"class":130},":",[116,566,568],{"class":567},"sfNiH"," true",[116,570,571],{"class":130}," }",[116,573,217],{"class":150},[116,575,576,578,581,583,585,587,589,592,594,596,598,601,603,605,607,609,611,613],{"class":118,"line":161},[116,577,140],{"class":122},[116,579,580],{"class":143}," fileHandle",[116,582,147],{"class":130},[116,584,513],{"class":164},[116,586,533],{"class":143},[116,588,202],{"class":130},[116,590,591],{"class":126},"getFileHandle",[116,593,173],{"class":150},[116,595,496],{"class":143},[116,597,202],{"class":130},[116,599,600],{"class":143},"name",[116,602,452],{"class":130},[116,604,558],{"class":130},[116,606,561],{"class":150},[116,608,564],{"class":130},[116,610,568],{"class":567},[116,612,571],{"class":130},[116,614,217],{"class":150},[116,616,617,619,622,624,626,628,630,633],{"class":118,"line":196},[116,618,140],{"class":122},[116,620,621],{"class":143}," writable",[116,623,147],{"class":130},[116,625,513],{"class":164},[116,627,580],{"class":143},[116,629,202],{"class":130},[116,631,632],{"class":126},"createWritable",[116,634,404],{"class":150},[116,636,637,640,643,645,648,650,652,655,657,660],{"class":118,"line":220},[116,638,639],{"class":164},"  await",[116,641,642],{"class":143}," file",[116,644,202],{"class":130},[116,646,647],{"class":126},"stream",[116,649,131],{"class":150},[116,651,202],{"class":130},[116,653,654],{"class":126},"pipeTo",[116,656,173],{"class":150},[116,658,659],{"class":143},"writable",[116,661,217],{"class":150},[116,663,664],{"class":118,"line":226},[116,665,158],{"emptyLinePlaceholder":157},[116,667,668,670,673,675,677,679,682],{"class":118,"line":231},[116,669,373],{"class":164},[116,671,672],{"class":130}," `${",[116,674,496],{"class":143},[116,676,202],{"class":130},[116,678,600],{"class":143},[116,680,681],{"class":130},"}`",[116,683,684],{"class":355},"  \u002F\u002F path within OPFS root's 'input' dir\n",[116,686,687],{"class":118,"line":266},[116,688,382],{"class":130},[11,690,691,692,695,696,698],{},"This uses the async ",[30,693,694],{},"createWritable()"," API. Workers can use the synchronous ",[30,697,74],{}," instead, which is faster for many small writes.",[18,700,702],{"id":701},"step-3-the-worker","Step 3: The worker",[84,704,706],{"className":110,"code":705,"language":112,"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 Set up canvas (OffscreenCanvas in worker)\n  canvas = new OffscreenCanvas(canvasWidth, canvasHeight)\n  ctx = canvas.getContext('2d')\n\n  \u002F\u002F WebCodecs decoder\n  decoder = new VideoDecoder({\n    output: (frame) => handleFrame(frame),\n    error: (e) => console.error('Decoder error:', e)\n  })\n\n  decoder.configure({\n    codec,  \u002F\u002F e.g., '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 uses microseconds\n\n  \u002F\u002F Only extract one frame per second\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 Write to 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 IMPORTANT: VideoFrame must be closed to free memory\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,707,708,713,726,737,748,752,779,805,809,833,848,876,889,893,897,901,930,935,959,985,989,994,1010,1038,1076,1082,1086,1100,1111,1122,1129,1134,1164,1169,1174,1187,1203,1208,1225,1252,1257,1263,1288,1308,1319,1324,1367,1416,1437,1442,1448,1471,1512,1589,1610,1637,1649,1654,1663,1706,1711,1716,1732,1737,1742,1760,1782,1804,1841,1857,1871,1879,1884,1899,1931],{"__ignoreMap":92},[116,709,710],{"class":118,"line":119},[116,711,712],{"class":355},"\u002F\u002F worker.js\n",[116,714,715,718,721,723],{"class":118,"line":137},[116,716,717],{"class":122},"let",[116,719,720],{"class":143}," decoder ",[116,722,399],{"class":130},[116,724,725],{"class":130}," null\n",[116,727,728,730,733,735],{"class":118,"line":154},[116,729,717],{"class":122},[116,731,732],{"class":143}," canvas ",[116,734,399],{"class":130},[116,736,725],{"class":130},[116,738,739,741,744,746],{"class":118,"line":161},[116,740,717],{"class":122},[116,742,743],{"class":143}," ctx ",[116,745,399],{"class":130},[116,747,725],{"class":130},[116,749,750],{"class":118,"line":196},[116,751,158],{"emptyLinePlaceholder":157},[116,753,754,757,759,762,764,767,769,772,774,777],{"class":118,"line":220},[116,755,756],{"class":143},"self",[116,758,202],{"class":130},[116,760,761],{"class":126},"onmessage",[116,763,147],{"class":130},[116,765,766],{"class":122}," async",[116,768,28],{"class":130},[116,770,771],{"class":495},"e",[116,773,499],{"class":130},[116,775,776],{"class":122}," =>",[116,778,134],{"class":130},[116,780,781,783,785,788,790,793,795,797,800,802],{"class":118,"line":226},[116,782,140],{"class":122},[116,784,558],{"class":130},[116,786,787],{"class":143}," type",[116,789,452],{"class":130},[116,791,792],{"class":143}," data",[116,794,571],{"class":130},[116,796,147],{"class":130},[116,798,799],{"class":143}," e",[116,801,202],{"class":130},[116,803,804],{"class":143},"data\n",[116,806,807],{"class":118,"line":231},[116,808,158],{"emptyLinePlaceholder":157},[116,810,811,813,815,818,821,824,827,829,831],{"class":118,"line":266},[116,812,165],{"class":164},[116,814,28],{"class":150},[116,816,817],{"class":143},"type",[116,819,820],{"class":130}," ===",[116,822,823],{"class":130}," '",[116,825,826],{"class":179},"init",[116,828,176],{"class":130},[116,830,261],{"class":150},[116,832,193],{"class":130},[116,834,835,838,841,843,846],{"class":118,"line":286},[116,836,837],{"class":164},"    await",[116,839,840],{"class":126}," init",[116,842,173],{"class":150},[116,844,845],{"class":143},"data",[116,847,217],{"class":150},[116,849,850,853,856,859,861,863,865,867,870,872,874],{"class":118,"line":291},[116,851,852],{"class":130},"  }",[116,854,855],{"class":164}," else",[116,857,858],{"class":164}," if",[116,860,28],{"class":150},[116,862,817],{"class":143},[116,864,820],{"class":130},[116,866,823],{"class":130},[116,868,869],{"class":179},"extract",[116,871,176],{"class":130},[116,873,261],{"class":150},[116,875,193],{"class":130},[116,877,878,880,883,885,887],{"class":118,"line":296},[116,879,837],{"class":164},[116,881,882],{"class":126}," extractFrames",[116,884,173],{"class":150},[116,886,845],{"class":143},[116,888,217],{"class":150},[116,890,891],{"class":118,"line":322},[116,892,223],{"class":130},[116,894,895],{"class":118,"line":342},[116,896,382],{"class":130},[116,898,899],{"class":118,"line":347},[116,900,158],{"emptyLinePlaceholder":157},[116,902,903,905,907,909,912,915,917,920,922,925,928],{"class":118,"line":352},[116,904,484],{"class":122},[116,906,487],{"class":122},[116,908,840],{"class":126},[116,910,911],{"class":130},"({",[116,913,914],{"class":495}," codec",[116,916,452],{"class":130},[116,918,919],{"class":495}," canvasWidth",[116,921,452],{"class":130},[116,923,924],{"class":495}," canvasHeight",[116,926,927],{"class":130}," })",[116,929,134],{"class":130},[116,931,932],{"class":118,"line":359},[116,933,934],{"class":355},"  \u002F\u002F Set up canvas (OffscreenCanvas in worker)\n",[116,936,937,940,942,945,948,950,953,955,957],{"class":118,"line":365},[116,938,939],{"class":143},"  canvas",[116,941,147],{"class":130},[116,943,944],{"class":130}," new",[116,946,947],{"class":126}," OffscreenCanvas",[116,949,173],{"class":150},[116,951,952],{"class":143},"canvasWidth",[116,954,452],{"class":130},[116,956,924],{"class":143},[116,958,217],{"class":150},[116,960,961,964,966,969,971,974,976,978,981,983],{"class":118,"line":370},[116,962,963],{"class":143},"  ctx",[116,965,147],{"class":130},[116,967,968],{"class":143}," canvas",[116,970,202],{"class":130},[116,972,973],{"class":126},"getContext",[116,975,173],{"class":150},[116,977,176],{"class":130},[116,979,980],{"class":179},"2d",[116,982,176],{"class":130},[116,984,217],{"class":150},[116,986,987],{"class":118,"line":379},[116,988,158],{"emptyLinePlaceholder":157},[116,990,991],{"class":118,"line":385},[116,992,993],{"class":355},"  \u002F\u002F WebCodecs decoder\n",[116,995,996,999,1001,1003,1006,1008],{"class":118,"line":390},[116,997,998],{"class":143},"  decoder",[116,1000,147],{"class":130},[116,1002,944],{"class":130},[116,1004,1005],{"class":126}," VideoDecoder",[116,1007,173],{"class":150},[116,1009,193],{"class":130},[116,1011,1012,1015,1017,1019,1022,1024,1026,1029,1031,1033,1035],{"class":118,"line":407},[116,1013,1014],{"class":126},"    output",[116,1016,564],{"class":130},[116,1018,28],{"class":130},[116,1020,1021],{"class":495},"frame",[116,1023,499],{"class":130},[116,1025,776],{"class":122},[116,1027,1028],{"class":126}," handleFrame",[116,1030,173],{"class":150},[116,1032,1021],{"class":143},[116,1034,499],{"class":150},[116,1036,1037],{"class":130},",\n",[116,1039,1040,1043,1045,1047,1049,1051,1053,1056,1058,1061,1063,1065,1068,1070,1072,1074],{"class":118,"line":432},[116,1041,1042],{"class":126},"    error",[116,1044,564],{"class":130},[116,1046,28],{"class":130},[116,1048,771],{"class":495},[116,1050,499],{"class":130},[116,1052,776],{"class":122},[116,1054,1055],{"class":143}," console",[116,1057,202],{"class":130},[116,1059,1060],{"class":126},"error",[116,1062,173],{"class":150},[116,1064,176],{"class":130},[116,1066,1067],{"class":179},"Decoder error:",[116,1069,176],{"class":130},[116,1071,452],{"class":130},[116,1073,799],{"class":143},[116,1075,217],{"class":150},[116,1077,1078,1080],{"class":118,"line":459},[116,1079,852],{"class":130},[116,1081,217],{"class":150},[116,1083,1084],{"class":118,"line":465},[116,1085,158],{"emptyLinePlaceholder":157},[116,1087,1089,1091,1093,1096,1098],{"class":118,"line":1088},27,[116,1090,998],{"class":143},[116,1092,202],{"class":130},[116,1094,1095],{"class":126},"configure",[116,1097,173],{"class":150},[116,1099,193],{"class":130},[116,1101,1103,1106,1108],{"class":118,"line":1102},28,[116,1104,1105],{"class":143},"    codec",[116,1107,452],{"class":130},[116,1109,1110],{"class":355},"  \u002F\u002F e.g., 'avc1.640028'\n",[116,1112,1114,1117,1119],{"class":118,"line":1113},29,[116,1115,1116],{"class":150},"    optimizeForLatency",[116,1118,564],{"class":130},[116,1120,1121],{"class":567}," false\n",[116,1123,1125,1127],{"class":118,"line":1124},30,[116,1126,852],{"class":130},[116,1128,217],{"class":150},[116,1130,1132],{"class":118,"line":1131},31,[116,1133,158],{"emptyLinePlaceholder":157},[116,1135,1137,1140,1142,1144,1146,1149,1151,1153,1155,1158,1160,1162],{"class":118,"line":1136},32,[116,1138,1139],{"class":143},"  self",[116,1141,202],{"class":130},[116,1143,98],{"class":126},[116,1145,173],{"class":150},[116,1147,1148],{"class":130},"{",[116,1150,787],{"class":150},[116,1152,564],{"class":130},[116,1154,823],{"class":130},[116,1156,1157],{"class":179},"ready",[116,1159,176],{"class":130},[116,1161,571],{"class":130},[116,1163,217],{"class":150},[116,1165,1167],{"class":118,"line":1166},33,[116,1168,382],{"class":130},[116,1170,1172],{"class":118,"line":1171},34,[116,1173,158],{"emptyLinePlaceholder":157},[116,1175,1177,1179,1182,1184],{"class":118,"line":1176},35,[116,1178,717],{"class":122},[116,1180,1181],{"class":143}," frameCount ",[116,1183,399],{"class":130},[116,1185,1186],{"class":424}," 0\n",[116,1188,1190,1192,1195,1197,1200],{"class":118,"line":1189},36,[116,1191,717],{"class":122},[116,1193,1194],{"class":143}," lastExtractedSecond ",[116,1196,399],{"class":130},[116,1198,1199],{"class":130}," -",[116,1201,1202],{"class":424},"1\n",[116,1204,1206],{"class":118,"line":1205},37,[116,1207,158],{"emptyLinePlaceholder":157},[116,1209,1211,1213,1215,1217,1219,1221,1223],{"class":118,"line":1210},38,[116,1212,484],{"class":122},[116,1214,487],{"class":122},[116,1216,1028],{"class":126},[116,1218,173],{"class":130},[116,1220,1021],{"class":495},[116,1222,499],{"class":130},[116,1224,134],{"class":130},[116,1226,1228,1230,1233,1235,1238,1240,1243,1246,1249],{"class":118,"line":1227},39,[116,1229,140],{"class":122},[116,1231,1232],{"class":143}," timestampSeconds",[116,1234,147],{"class":130},[116,1236,1237],{"class":143}," frame",[116,1239,202],{"class":130},[116,1241,1242],{"class":143},"timestamp",[116,1244,1245],{"class":130}," \u002F",[116,1247,1248],{"class":424}," 1_000_000",[116,1250,1251],{"class":355},"  \u002F\u002F WebCodecs uses microseconds\n",[116,1253,1255],{"class":118,"line":1254},40,[116,1256,158],{"emptyLinePlaceholder":157},[116,1258,1260],{"class":118,"line":1259},41,[116,1261,1262],{"class":355},"  \u002F\u002F Only extract one frame per second\n",[116,1264,1266,1268,1271,1273,1276,1278,1281,1283,1286],{"class":118,"line":1265},42,[116,1267,140],{"class":122},[116,1269,1270],{"class":143}," currentSecond",[116,1272,147],{"class":130},[116,1274,1275],{"class":143}," Math",[116,1277,202],{"class":130},[116,1279,1280],{"class":126},"floor",[116,1282,173],{"class":150},[116,1284,1285],{"class":143},"timestampSeconds",[116,1287,217],{"class":150},[116,1289,1291,1293,1295,1298,1301,1304,1306],{"class":118,"line":1290},43,[116,1292,165],{"class":164},[116,1294,28],{"class":150},[116,1296,1297],{"class":143},"currentSecond",[116,1299,1300],{"class":130}," !==",[116,1302,1303],{"class":143}," lastExtractedSecond",[116,1305,261],{"class":150},[116,1307,193],{"class":130},[116,1309,1311,1314,1316],{"class":118,"line":1310},44,[116,1312,1313],{"class":143},"    lastExtractedSecond",[116,1315,147],{"class":130},[116,1317,1318],{"class":143}," currentSecond\n",[116,1320,1322],{"class":118,"line":1321},45,[116,1323,158],{"emptyLinePlaceholder":157},[116,1325,1327,1330,1332,1335,1337,1339,1341,1343,1345,1347,1349,1351,1353,1356,1358,1360,1362,1365],{"class":118,"line":1326},46,[116,1328,1329],{"class":143},"    ctx",[116,1331,202],{"class":130},[116,1333,1334],{"class":126},"drawImage",[116,1336,173],{"class":150},[116,1338,1021],{"class":143},[116,1340,452],{"class":130},[116,1342,425],{"class":424},[116,1344,452],{"class":130},[116,1346,425],{"class":424},[116,1348,452],{"class":130},[116,1350,968],{"class":143},[116,1352,202],{"class":130},[116,1354,1355],{"class":143},"width",[116,1357,452],{"class":130},[116,1359,968],{"class":143},[116,1361,202],{"class":130},[116,1363,1364],{"class":143},"height",[116,1366,217],{"class":150},[116,1368,1370,1373,1376,1378,1380,1382,1384,1387,1389,1391,1393,1395,1397,1400,1402,1404,1407,1409,1412,1414],{"class":118,"line":1369},47,[116,1371,1372],{"class":122},"    const",[116,1374,1375],{"class":143}," blob",[116,1377,147],{"class":130},[116,1379,513],{"class":164},[116,1381,968],{"class":143},[116,1383,202],{"class":130},[116,1385,1386],{"class":126},"convertToBlob",[116,1388,173],{"class":150},[116,1390,1148],{"class":130},[116,1392,787],{"class":150},[116,1394,564],{"class":130},[116,1396,823],{"class":130},[116,1398,1399],{"class":179},"image\u002Fjpeg",[116,1401,176],{"class":130},[116,1403,452],{"class":130},[116,1405,1406],{"class":150}," quality",[116,1408,564],{"class":130},[116,1410,1411],{"class":424}," 0.85",[116,1413,571],{"class":130},[116,1415,217],{"class":150},[116,1417,1419,1421,1424,1426,1428,1430,1432,1435],{"class":118,"line":1418},48,[116,1420,1372],{"class":122},[116,1422,1423],{"class":143}," buffer",[116,1425,147],{"class":130},[116,1427,513],{"class":164},[116,1429,1375],{"class":143},[116,1431,202],{"class":130},[116,1433,1434],{"class":126},"arrayBuffer",[116,1436,404],{"class":150},[116,1438,1440],{"class":118,"line":1439},49,[116,1441,158],{"emptyLinePlaceholder":157},[116,1443,1445],{"class":118,"line":1444},50,[116,1446,1447],{"class":355},"    \u002F\u002F Write to OPFS\n",[116,1449,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469],{"class":118,"line":1450},51,[116,1452,1372],{"class":122},[116,1454,508],{"class":143},[116,1456,147],{"class":130},[116,1458,513],{"class":164},[116,1460,516],{"class":143},[116,1462,202],{"class":130},[116,1464,310],{"class":143},[116,1466,202],{"class":130},[116,1468,315],{"class":126},[116,1470,404],{"class":150},[116,1472,1474,1476,1479,1481,1483,1485,1487,1489,1491,1493,1496,1498,1500,1502,1504,1506,1508,1510],{"class":118,"line":1473},52,[116,1475,1372],{"class":122},[116,1477,1478],{"class":143}," outputDir",[116,1480,147],{"class":130},[116,1482,513],{"class":164},[116,1484,508],{"class":143},[116,1486,202],{"class":130},[116,1488,544],{"class":126},[116,1490,173],{"class":150},[116,1492,176],{"class":130},[116,1494,1495],{"class":179},"thumbnails",[116,1497,176],{"class":130},[116,1499,452],{"class":130},[116,1501,558],{"class":130},[116,1503,561],{"class":150},[116,1505,564],{"class":130},[116,1507,568],{"class":567},[116,1509,571],{"class":130},[116,1511,217],{"class":150},[116,1513,1515,1517,1519,1521,1523,1525,1527,1529,1531,1534,1537,1540,1543,1546,1548,1551,1553,1556,1558,1560,1563,1565,1567,1570,1573,1575,1577,1579,1581,1583,1585,1587],{"class":118,"line":1514},53,[116,1516,1372],{"class":122},[116,1518,580],{"class":143},[116,1520,147],{"class":130},[116,1522,513],{"class":164},[116,1524,1478],{"class":143},[116,1526,202],{"class":130},[116,1528,591],{"class":126},[116,1530,173],{"class":150},[116,1532,1533],{"class":130},"`",[116,1535,1536],{"class":179},"thumb_",[116,1538,1539],{"class":130},"${",[116,1541,1542],{"class":126},"String",[116,1544,1545],{"class":143},"(currentSecond)",[116,1547,202],{"class":130},[116,1549,1550],{"class":126},"padStart",[116,1552,173],{"class":143},[116,1554,1555],{"class":424},"6",[116,1557,452],{"class":130},[116,1559,823],{"class":130},[116,1561,1562],{"class":179},"0",[116,1564,176],{"class":130},[116,1566,499],{"class":143},[116,1568,1569],{"class":130},"}",[116,1571,1572],{"class":179},".jpg",[116,1574,1533],{"class":130},[116,1576,452],{"class":130},[116,1578,558],{"class":130},[116,1580,561],{"class":150},[116,1582,564],{"class":130},[116,1584,568],{"class":567},[116,1586,571],{"class":130},[116,1588,217],{"class":150},[116,1590,1592,1594,1597,1599,1601,1603,1605,1608],{"class":118,"line":1591},54,[116,1593,1372],{"class":122},[116,1595,1596],{"class":143}," syncHandle",[116,1598,147],{"class":130},[116,1600,513],{"class":164},[116,1602,580],{"class":143},[116,1604,202],{"class":130},[116,1606,1607],{"class":126},"createSyncAccessHandle",[116,1609,404],{"class":150},[116,1611,1613,1616,1618,1621,1623,1626,1629,1631,1634],{"class":118,"line":1612},55,[116,1614,1615],{"class":143},"    syncHandle",[116,1617,202],{"class":130},[116,1619,1620],{"class":126},"write",[116,1622,173],{"class":150},[116,1624,1625],{"class":130},"new",[116,1627,1628],{"class":126}," Uint8Array",[116,1630,173],{"class":150},[116,1632,1633],{"class":143},"buffer",[116,1635,1636],{"class":150},"))\n",[116,1638,1640,1642,1644,1647],{"class":118,"line":1639},56,[116,1641,1615],{"class":143},[116,1643,202],{"class":130},[116,1645,1646],{"class":126},"close",[116,1648,404],{"class":150},[116,1650,1652],{"class":118,"line":1651},57,[116,1653,158],{"emptyLinePlaceholder":157},[116,1655,1657,1660],{"class":118,"line":1656},58,[116,1658,1659],{"class":143},"    frameCount",[116,1661,1662],{"class":130},"++\n",[116,1664,1666,1669,1671,1673,1675,1677,1679,1681,1683,1686,1688,1690,1693,1695,1698,1700,1702,1704],{"class":118,"line":1665},59,[116,1667,1668],{"class":143},"    self",[116,1670,202],{"class":130},[116,1672,98],{"class":126},[116,1674,173],{"class":150},[116,1676,1148],{"class":130},[116,1678,787],{"class":150},[116,1680,564],{"class":130},[116,1682,823],{"class":130},[116,1684,1685],{"class":179},"progress",[116,1687,176],{"class":130},[116,1689,452],{"class":130},[116,1691,1692],{"class":143}," frameCount",[116,1694,452],{"class":130},[116,1696,1697],{"class":150}," second",[116,1699,564],{"class":130},[116,1701,1270],{"class":143},[116,1703,571],{"class":130},[116,1705,217],{"class":150},[116,1707,1709],{"class":118,"line":1708},60,[116,1710,223],{"class":130},[116,1712,1714],{"class":118,"line":1713},61,[116,1715,158],{"emptyLinePlaceholder":157},[116,1717,1719,1722,1724,1726,1729],{"class":118,"line":1718},62,[116,1720,1721],{"class":143},"  frame",[116,1723,202],{"class":130},[116,1725,1646],{"class":126},[116,1727,1728],{"class":150},"()  ",[116,1730,1731],{"class":355},"\u002F\u002F IMPORTANT: VideoFrame must be closed to free memory\n",[116,1733,1735],{"class":118,"line":1734},63,[116,1736,382],{"class":130},[116,1738,1740],{"class":118,"line":1739},64,[116,1741,158],{"emptyLinePlaceholder":157},[116,1743,1745,1747,1749,1751,1753,1756,1758],{"class":118,"line":1744},65,[116,1746,484],{"class":122},[116,1748,487],{"class":122},[116,1750,882],{"class":126},[116,1752,911],{"class":130},[116,1754,1755],{"class":495}," chunks",[116,1757,927],{"class":130},[116,1759,134],{"class":130},[116,1761,1763,1766,1768,1770,1773,1776,1778,1780],{"class":118,"line":1762},66,[116,1764,1765],{"class":164},"  for",[116,1767,28],{"class":150},[116,1769,393],{"class":122},[116,1771,1772],{"class":143}," chunk",[116,1774,1775],{"class":130}," of",[116,1777,1755],{"class":143},[116,1779,261],{"class":150},[116,1781,193],{"class":130},[116,1783,1785,1788,1790,1793,1795,1797,1800,1802],{"class":118,"line":1784},67,[116,1786,1787],{"class":143},"    decoder",[116,1789,202],{"class":130},[116,1791,1792],{"class":126},"decode",[116,1794,173],{"class":150},[116,1796,1625],{"class":130},[116,1798,1799],{"class":126}," EncodedVideoChunk",[116,1801,173],{"class":150},[116,1803,193],{"class":130},[116,1805,1807,1810,1812,1814,1816,1819,1822,1824,1827,1829,1832,1834,1837,1839],{"class":118,"line":1806},68,[116,1808,1809],{"class":150},"      type",[116,1811,564],{"class":130},[116,1813,1772],{"class":143},[116,1815,202],{"class":130},[116,1817,1818],{"class":143},"keyframe",[116,1820,1821],{"class":130}," ?",[116,1823,823],{"class":130},[116,1825,1826],{"class":179},"key",[116,1828,176],{"class":130},[116,1830,1831],{"class":130}," :",[116,1833,823],{"class":130},[116,1835,1836],{"class":179},"delta",[116,1838,176],{"class":130},[116,1840,1037],{"class":130},[116,1842,1844,1847,1849,1851,1853,1855],{"class":118,"line":1843},69,[116,1845,1846],{"class":150},"      timestamp",[116,1848,564],{"class":130},[116,1850,1772],{"class":143},[116,1852,202],{"class":130},[116,1854,1242],{"class":143},[116,1856,1037],{"class":130},[116,1858,1860,1863,1865,1867,1869],{"class":118,"line":1859},70,[116,1861,1862],{"class":150},"      data",[116,1864,564],{"class":130},[116,1866,1772],{"class":143},[116,1868,202],{"class":130},[116,1870,804],{"class":143},[116,1872,1874,1877],{"class":118,"line":1873},71,[116,1875,1876],{"class":130},"    }",[116,1878,1636],{"class":150},[116,1880,1882],{"class":118,"line":1881},72,[116,1883,223],{"class":130},[116,1885,1887,1889,1892,1894,1897],{"class":118,"line":1886},73,[116,1888,639],{"class":164},[116,1890,1891],{"class":143}," decoder",[116,1893,202],{"class":130},[116,1895,1896],{"class":126},"flush",[116,1898,404],{"class":150},[116,1900,1902,1904,1906,1908,1910,1912,1914,1916,1918,1921,1923,1925,1927,1929],{"class":118,"line":1901},74,[116,1903,1139],{"class":143},[116,1905,202],{"class":130},[116,1907,98],{"class":126},[116,1909,173],{"class":150},[116,1911,1148],{"class":130},[116,1913,787],{"class":150},[116,1915,564],{"class":130},[116,1917,823],{"class":130},[116,1919,1920],{"class":179},"done",[116,1922,176],{"class":130},[116,1924,452],{"class":130},[116,1926,1692],{"class":143},[116,1928,571],{"class":130},[116,1930,217],{"class":150},[116,1932,1934],{"class":118,"line":1933},75,[116,1935,382],{"class":130},[11,1937,1938],{},"The key gotchas:",[1940,1941,1942,1952,1958],"ol",{},[1943,1944,1945,1951],"li",{},[25,1946,1947,1950],{},[30,1948,1949],{},"frame.close()"," is mandatory."," VideoFrame holds a reference to a GPU buffer. Forget to close it and you leak GPU memory until the tab crashes.",[1943,1953,1954,1957],{},[25,1955,1956],{},"OffscreenCanvas in workers."," This is what lets you render in a worker. Available in Chrome 69+, Safari 16.4+, Firefox 105+.",[1943,1959,1960,1965],{},[25,1961,1962,1964],{},[30,1963,74],{}," blocks other OPFS operations."," Don't hold it open across async operations — write, close, move on.",[18,1967,1969],{"id":1968},"step-4-getting-chunks-to-feed-the-decoder","Step 4: Getting chunks to feed the decoder",[11,1971,1972],{},"This is the annoying part. WebCodecs doesn't demux — it expects raw codec chunks. You still need to parse the MP4 container to extract H.264 NAL units.",[11,1974,1975],{},"Two options:",[1940,1977,1978,1994],{},[1943,1979,1980,28,1986,1993],{},[25,1981,1982,1983],{},"Use ",[30,1984,1985],{},"mp4box.js",[1987,1988,1992],"a",{"href":1989,"rel":1990},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fmp4box",[1991],"nofollow","npm",") — a pure-JS MP4 parser that emits samples ready for WebCodecs.",[1943,1995,1996,1999],{},[25,1997,1998],{},"Use FFmpeg WASM just for demuxing"," — extract chunks, then hand them to WebCodecs for decode. This sounds wasteful but FFmpeg WASM is faster at demuxing than at decode+render.",[11,2001,2002,2003,564],{},"Here's option 1 with ",[30,2004,1985],{},[84,2006,2008],{"className":110,"code":2007,"language":112,"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 convention\n\n  const mp4boxFile = mp4box.createFile()\n  mp4boxFile.onReady = (info) => {\n    mp4boxFile.setExtractionOptions(info.videoTracks[0].id, null, {\n      nbSamples: 100  \u002F\u002F batch size\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,2009,2010,2029,2033,2050,2069,2086,2090,2109,2132,2171,2184,2190,2201,2205,2209,2219,2251,2271,2284,2300,2330,2343,2350,2355,2359,2363,2378,2388,2392,2398,2430,2435,2439],{"__ignoreMap":92},[116,2011,2012,2015,2018,2021,2023,2026],{"class":118,"line":119},[116,2013,2014],{"class":164},"import",[116,2016,2017],{"class":143}," mp4box ",[116,2019,2020],{"class":164},"from",[116,2022,823],{"class":130},[116,2024,2025],{"class":179},"mp4box",[116,2027,2028],{"class":130},"'\n",[116,2030,2031],{"class":118,"line":137},[116,2032,158],{"emptyLinePlaceholder":157},[116,2034,2035,2037,2039,2042,2044,2046,2048],{"class":118,"line":154},[116,2036,484],{"class":122},[116,2038,487],{"class":122},[116,2040,2041],{"class":126}," getCodecChunks",[116,2043,173],{"class":130},[116,2045,496],{"class":495},[116,2047,499],{"class":130},[116,2049,134],{"class":130},[116,2051,2052,2054,2057,2059,2061,2063,2065,2067],{"class":118,"line":161},[116,2053,140],{"class":122},[116,2055,2056],{"class":143}," arrayBuffer",[116,2058,147],{"class":130},[116,2060,513],{"class":164},[116,2062,642],{"class":143},[116,2064,202],{"class":130},[116,2066,1434],{"class":126},[116,2068,404],{"class":150},[116,2070,2071,2074,2076,2079,2081,2083],{"class":118,"line":196},[116,2072,2073],{"class":143},"  arrayBuffer",[116,2075,202],{"class":130},[116,2077,2078],{"class":143},"fileStart",[116,2080,147],{"class":130},[116,2082,425],{"class":424},[116,2084,2085],{"class":355},"  \u002F\u002F mp4box.js convention\n",[116,2087,2088],{"class":118,"line":220},[116,2089,158],{"emptyLinePlaceholder":157},[116,2091,2092,2094,2097,2099,2102,2104,2107],{"class":118,"line":226},[116,2093,140],{"class":122},[116,2095,2096],{"class":143}," mp4boxFile",[116,2098,147],{"class":130},[116,2100,2101],{"class":143}," mp4box",[116,2103,202],{"class":130},[116,2105,2106],{"class":126},"createFile",[116,2108,404],{"class":150},[116,2110,2111,2114,2116,2119,2121,2123,2126,2128,2130],{"class":118,"line":231},[116,2112,2113],{"class":143},"  mp4boxFile",[116,2115,202],{"class":130},[116,2117,2118],{"class":126},"onReady",[116,2120,147],{"class":130},[116,2122,28],{"class":130},[116,2124,2125],{"class":495},"info",[116,2127,499],{"class":130},[116,2129,776],{"class":122},[116,2131,134],{"class":130},[116,2133,2134,2137,2139,2142,2144,2146,2148,2151,2154,2156,2159,2161,2164,2166,2169],{"class":118,"line":266},[116,2135,2136],{"class":143},"    mp4boxFile",[116,2138,202],{"class":130},[116,2140,2141],{"class":126},"setExtractionOptions",[116,2143,173],{"class":150},[116,2145,2125],{"class":143},[116,2147,202],{"class":130},[116,2149,2150],{"class":143},"videoTracks",[116,2152,2153],{"class":150},"[",[116,2155,1562],{"class":424},[116,2157,2158],{"class":150},"]",[116,2160,202],{"class":130},[116,2162,2163],{"class":143},"id",[116,2165,452],{"class":130},[116,2167,2168],{"class":130}," null,",[116,2170,134],{"class":130},[116,2172,2173,2176,2178,2181],{"class":118,"line":286},[116,2174,2175],{"class":150},"      nbSamples",[116,2177,564],{"class":130},[116,2179,2180],{"class":424}," 100",[116,2182,2183],{"class":355},"  \u002F\u002F batch size\n",[116,2185,2186,2188],{"class":118,"line":291},[116,2187,1876],{"class":130},[116,2189,217],{"class":150},[116,2191,2192,2194,2196,2199],{"class":118,"line":296},[116,2193,2136],{"class":143},[116,2195,202],{"class":130},[116,2197,2198],{"class":126},"start",[116,2200,404],{"class":150},[116,2202,2203],{"class":118,"line":322},[116,2204,223],{"class":130},[116,2206,2207],{"class":118,"line":342},[116,2208,158],{"emptyLinePlaceholder":157},[116,2210,2211,2213,2215,2217],{"class":118,"line":347},[116,2212,140],{"class":122},[116,2214,1755],{"class":143},[116,2216,147],{"class":130},[116,2218,151],{"class":150},[116,2220,2221,2223,2225,2228,2230,2232,2235,2237,2240,2242,2245,2247,2249],{"class":118,"line":352},[116,2222,2113],{"class":143},[116,2224,202],{"class":130},[116,2226,2227],{"class":126},"onSamples",[116,2229,147],{"class":130},[116,2231,28],{"class":130},[116,2233,2234],{"class":495},"trackId",[116,2236,452],{"class":130},[116,2238,2239],{"class":495}," ref",[116,2241,452],{"class":130},[116,2243,2244],{"class":495}," samples",[116,2246,499],{"class":130},[116,2248,776],{"class":122},[116,2250,134],{"class":130},[116,2252,2253,2256,2258,2260,2263,2265,2267,2269],{"class":118,"line":359},[116,2254,2255],{"class":164},"    for",[116,2257,28],{"class":150},[116,2259,393],{"class":122},[116,2261,2262],{"class":143}," sample",[116,2264,1775],{"class":130},[116,2266,2244],{"class":143},[116,2268,261],{"class":150},[116,2270,193],{"class":130},[116,2272,2273,2276,2278,2280,2282],{"class":118,"line":365},[116,2274,2275],{"class":143},"      chunks",[116,2277,202],{"class":130},[116,2279,205],{"class":126},[116,2281,173],{"class":150},[116,2283,193],{"class":130},[116,2285,2286,2289,2291,2293,2295,2298],{"class":118,"line":370},[116,2287,2288],{"class":150},"        keyframe",[116,2290,564],{"class":130},[116,2292,2262],{"class":143},[116,2294,202],{"class":130},[116,2296,2297],{"class":143},"is_sync",[116,2299,1037],{"class":130},[116,2301,2302,2305,2307,2309,2311,2314,2317,2319,2321,2323,2325,2328],{"class":118,"line":379},[116,2303,2304],{"class":150},"        timestamp",[116,2306,564],{"class":130},[116,2308,2262],{"class":143},[116,2310,202],{"class":130},[116,2312,2313],{"class":143},"cts",[116,2315,2316],{"class":130}," *",[116,2318,1248],{"class":424},[116,2320,1245],{"class":130},[116,2322,2262],{"class":143},[116,2324,202],{"class":130},[116,2326,2327],{"class":143},"timescale",[116,2329,1037],{"class":130},[116,2331,2332,2335,2337,2339,2341],{"class":118,"line":385},[116,2333,2334],{"class":150},"        data",[116,2336,564],{"class":130},[116,2338,2262],{"class":143},[116,2340,202],{"class":130},[116,2342,804],{"class":143},[116,2344,2345,2348],{"class":118,"line":390},[116,2346,2347],{"class":130},"      }",[116,2349,217],{"class":150},[116,2351,2352],{"class":118,"line":407},[116,2353,2354],{"class":130},"    }\n",[116,2356,2357],{"class":118,"line":432},[116,2358,223],{"class":130},[116,2360,2361],{"class":118,"line":459},[116,2362,158],{"emptyLinePlaceholder":157},[116,2364,2365,2367,2369,2372,2374,2376],{"class":118,"line":465},[116,2366,2113],{"class":143},[116,2368,202],{"class":130},[116,2370,2371],{"class":126},"appendBuffer",[116,2373,173],{"class":150},[116,2375,1434],{"class":143},[116,2377,217],{"class":150},[116,2379,2380,2382,2384,2386],{"class":118,"line":1088},[116,2381,2113],{"class":143},[116,2383,202],{"class":130},[116,2385,1896],{"class":126},[116,2387,404],{"class":150},[116,2389,2390],{"class":118,"line":1102},[116,2391,158],{"emptyLinePlaceholder":157},[116,2393,2394,2396],{"class":118,"line":1113},[116,2395,373],{"class":164},[116,2397,134],{"class":130},[116,2399,2400,2402,2404,2406,2408,2411,2413,2415,2417,2419,2421,2423,2425,2428],{"class":118,"line":1124},[116,2401,1105],{"class":150},[116,2403,564],{"class":130},[116,2405,2096],{"class":143},[116,2407,202],{"class":130},[116,2409,2410],{"class":126},"getInfo",[116,2412,131],{"class":150},[116,2414,202],{"class":130},[116,2416,2150],{"class":143},[116,2418,2153],{"class":150},[116,2420,1562],{"class":424},[116,2422,2158],{"class":150},[116,2424,202],{"class":130},[116,2426,2427],{"class":143},"codec",[116,2429,1037],{"class":130},[116,2431,2432],{"class":118,"line":1131},[116,2433,2434],{"class":143},"    chunks\n",[116,2436,2437],{"class":118,"line":1136},[116,2438,223],{"class":130},[116,2440,2441],{"class":118,"line":1166},[116,2442,382],{"class":130},[11,2444,2445],{},"For HLS\u002FDASH streams, you can skip this entirely — you already have the raw chunks, since you fetched them as segments. This is the path FlowPick uses internally for some operations.",[18,2447,2449],{"id":2448},"benchmarks-webcodecs-vs-ffmpeg-wasm-vs-native","Benchmarks: WebCodecs vs FFmpeg WASM vs native",[11,2451,2452],{},"Test setup: extracting one frame per second from a 30-minute 1080p H.264 video (1800 frames total).",[2454,2455,2456,2475],"table",{},[2457,2458,2459],"thead",{},[2460,2461,2462,2466,2469,2472],"tr",{},[2463,2464,2465],"th",{},"Approach",[2463,2467,2468],{},"Wall-clock time",[2463,2470,2471],{},"Memory peak",[2463,2473,2474],{},"Download size",[2476,2477,2478,2493,2507,2520],"tbody",{},[2460,2479,2480,2484,2487,2490],{},[2481,2482,2483],"td",{},"Native FFmpeg (command line)",[2481,2485,2486],{},"18s",[2481,2488,2489],{},"80MB",[2481,2491,2492],{},"n\u002Fa (installed)",[2460,2494,2495,2498,2501,2504],{},[2481,2496,2497],{},"FFmpeg WASM (single thread)",[2481,2499,2500],{},"145s",[2481,2502,2503],{},"220MB",[2481,2505,2506],{},"25MB",[2460,2508,2509,2512,2515,2518],{},[2481,2510,2511],{},"FFmpeg WASM (multi-threaded)",[2481,2513,2514],{},"52s",[2481,2516,2517],{},"280MB",[2481,2519,2506],{},[2460,2521,2522,2525,2528,2531],{},[2481,2523,2524],{},"WebCodecs + Worker + OPFS",[2481,2526,2527],{},"31s",[2481,2529,2530],{},"95MB",[2481,2532,2533],{},"\u003C100KB",[11,2535,2536],{},"WebCodecs wins because:",[1940,2538,2539,2542,2545,2548],{},[1943,2540,2541],{},"Hardware acceleration — the actual H.264 decode happens on the GPU, not in WASM",[1943,2543,2544],{},"No 25MB WASM payload",[1943,2546,2547],{},"Lower memory (no FFmpeg's internal buffering)",[1943,2549,2550],{},"Real multithreading via workers (FFmpeg WASM's pthreads support is improving but still has overhead)",[11,2552,2553],{},"FFmpeg WASM wins because:",[1940,2555,2556,2559,2562,2565],{},[1943,2557,2558],{},"Handles every codec (WebCodecs is limited to what the browser supports — typically H.264, H.265, VP8\u002FVP9, AV1)",[1943,2560,2561],{},"Handles muxing\u002Fdemuxing (WebCodecs is decode\u002Fencode only)",[1943,2563,2564],{},"Battle-tested on edge cases",[1943,2566,2567],{},"Same code works everywhere (no feature detection)",[11,2569,2570,2573],{},[25,2571,2572],{},"Original opinion:"," Use WebCodecs when you know your codec and need performance. Use FFmpeg WASM when you need breadth. FlowPick uses both — FFmpeg WASM for the general remux path, WebCodecs for specific operations like thumbnail generation where speed matters and the input codec is known to be H.264.",[18,2575,2577],{"id":2576},"the-opfs-performance-myth","The OPFS performance myth",[11,2579,2580,2581,2583],{},"There's a claim floating around that OPFS is \"as fast as native disk.\" It isn't. It's faster than IndexedDB and ",[30,2582,70],{},", but still has overhead:",[2454,2585,2586,2602],{},[2457,2587,2588],{},[2460,2589,2590,2593,2596,2599],{},[2463,2591,2592],{},"Operation",[2463,2594,2595],{},"OPFS (sync, in worker)",[2463,2597,2598],{},"IndexedDB",[2463,2600,2601],{},"Native disk",[2476,2603,2604,2618,2632,2646],{},[2460,2605,2606,2609,2612,2615],{},[2481,2607,2608],{},"1KB write",[2481,2610,2611],{},"0.05ms",[2481,2613,2614],{},"1.2ms",[2481,2616,2617],{},"0.01ms",[2460,2619,2620,2623,2626,2629],{},[2481,2621,2622],{},"1MB write",[2481,2624,2625],{},"1.1ms",[2481,2627,2628],{},"8.5ms",[2481,2630,2631],{},"0.4ms",[2460,2633,2634,2637,2640,2643],{},[2481,2635,2636],{},"100MB write",[2481,2638,2639],{},"95ms",[2481,2641,2642],{},"850ms",[2481,2644,2645],{},"35ms",[2460,2647,2648,2651,2654,2657],{},[2481,2649,2650],{},"1MB read",[2481,2652,2653],{},"0.8ms",[2481,2655,2656],{},"4.5ms",[2481,2658,2659],{},"0.2ms",[11,2661,2662],{},"OPFS is roughly 3-5x slower than native disk, but 5-10x faster than IndexedDB for binary data. For most video processing workflows, OPFS is \"fast enough\" — the bottleneck is decode\u002Fencode, not I\u002FO.",[11,2664,2665],{},"The big wins:",[2667,2668,2669,2674,2677],"ul",{},[1943,2670,2671,2673],{},[30,2672,74],{}," in workers — synchronous I\u002FO means no await overhead per write",[1943,2675,2676],{},"File can be larger than RAM — you can stream a 5GB file through OPFS without loading it all into memory",[1943,2678,2679],{},"Persists across sessions — your processed video is still there after a refresh",[18,2681,2683],{"id":2682},"worker-thread-pool-for-parallel-decode","Worker thread pool for parallel decode",[11,2685,2686],{},"If you're processing multiple video files (say, batch thumbnailing a directory), you want a worker pool:",[84,2688,2690],{"className":110,"code":2689,"language":112,"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 Wait for one to become idle\n    return new Promise(resolve => {\n      this.queue.push(resolve)\n    })\n  }\n}\n",[30,2691,2692,2703,2737,2784,2795,2811,2815,2819,2836,2855,2873,2877,2905,2926,2944,2988,3034,3039,3053,3059,3063,3067,3078,3111,3148,3152,3157,3173,3190,3196,3200],{"__ignoreMap":92},[116,2693,2694,2697,2701],{"class":118,"line":119},[116,2695,2696],{"class":122},"class",[116,2698,2700],{"class":2699},"sBMFI"," WorkerPool",[116,2702,134],{"class":130},[116,2704,2705,2708,2710,2713,2715,2718,2720,2722,2724,2727,2730,2733,2735],{"class":118,"line":137},[116,2706,2707],{"class":122},"  constructor",[116,2709,173],{"class":130},[116,2711,2712],{"class":495},"workerUrl",[116,2714,452],{"class":130},[116,2716,2717],{"class":495}," size",[116,2719,147],{"class":130},[116,2721,516],{"class":143},[116,2723,202],{"class":130},[116,2725,2726],{"class":143},"hardwareConcurrency ",[116,2728,2729],{"class":130},"||",[116,2731,2732],{"class":424}," 4",[116,2734,499],{"class":130},[116,2736,134],{"class":130},[116,2738,2739,2742,2745,2747,2750,2752,2754,2756,2758,2761,2763,2765,2768,2771,2773,2775,2778,2780,2782],{"class":118,"line":154},[116,2740,2741],{"class":130},"    this.",[116,2743,2744],{"class":143},"workers",[116,2746,147],{"class":130},[116,2748,2749],{"class":143}," Array",[116,2751,202],{"class":130},[116,2753,2020],{"class":126},[116,2755,173],{"class":150},[116,2757,1148],{"class":130},[116,2759,2760],{"class":150}," length",[116,2762,564],{"class":130},[116,2764,2717],{"class":143},[116,2766,2767],{"class":130}," },",[116,2769,2770],{"class":130}," ()",[116,2772,776],{"class":122},[116,2774,944],{"class":130},[116,2776,2777],{"class":126}," Worker",[116,2779,173],{"class":150},[116,2781,2712],{"class":143},[116,2783,1636],{"class":150},[116,2785,2786,2788,2791,2793],{"class":118,"line":161},[116,2787,2741],{"class":130},[116,2789,2790],{"class":143},"queue",[116,2792,147],{"class":130},[116,2794,151],{"class":150},[116,2796,2797,2799,2802,2804,2806,2809],{"class":118,"line":196},[116,2798,2741],{"class":130},[116,2800,2801],{"class":143},"busy",[116,2803,147],{"class":130},[116,2805,944],{"class":130},[116,2807,2808],{"class":126}," Set",[116,2810,404],{"class":150},[116,2812,2813],{"class":118,"line":220},[116,2814,223],{"class":130},[116,2816,2817],{"class":118,"line":226},[116,2818,158],{"emptyLinePlaceholder":157},[116,2820,2821,2824,2827,2829,2832,2834],{"class":118,"line":231},[116,2822,2823],{"class":122},"  async",[116,2825,2826],{"class":150}," run",[116,2828,173],{"class":130},[116,2830,2831],{"class":495},"task",[116,2833,499],{"class":130},[116,2835,134],{"class":130},[116,2837,2838,2840,2843,2845,2847,2850,2853],{"class":118,"line":266},[116,2839,1372],{"class":122},[116,2841,2842],{"class":143}," worker",[116,2844,147],{"class":130},[116,2846,513],{"class":164},[116,2848,2849],{"class":130}," this.",[116,2851,2852],{"class":126},"getIdle",[116,2854,404],{"class":150},[116,2856,2857,2859,2861,2863,2866,2868,2871],{"class":118,"line":286},[116,2858,2741],{"class":130},[116,2860,2801],{"class":143},[116,2862,202],{"class":130},[116,2864,2865],{"class":126},"add",[116,2867,173],{"class":150},[116,2869,2870],{"class":143},"worker",[116,2872,217],{"class":150},[116,2874,2875],{"class":118,"line":291},[116,2876,158],{"emptyLinePlaceholder":157},[116,2878,2879,2882,2884,2887,2889,2891,2894,2896,2899,2901,2903],{"class":118,"line":296},[116,2880,2881],{"class":164},"    return",[116,2883,944],{"class":130},[116,2885,2886],{"class":2699}," Promise",[116,2888,173],{"class":150},[116,2890,173],{"class":130},[116,2892,2893],{"class":495},"resolve",[116,2895,452],{"class":130},[116,2897,2898],{"class":495}," reject",[116,2900,499],{"class":130},[116,2902,776],{"class":122},[116,2904,134],{"class":130},[116,2906,2907,2910,2912,2914,2916,2918,2920,2922,2924],{"class":118,"line":322},[116,2908,2909],{"class":143},"      worker",[116,2911,202],{"class":130},[116,2913,761],{"class":126},[116,2915,147],{"class":130},[116,2917,28],{"class":130},[116,2919,771],{"class":495},[116,2921,499],{"class":130},[116,2923,776],{"class":122},[116,2925,134],{"class":130},[116,2927,2928,2931,2933,2935,2938,2940,2942],{"class":118,"line":342},[116,2929,2930],{"class":130},"        this.",[116,2932,2801],{"class":143},[116,2934,202],{"class":130},[116,2936,2937],{"class":126},"delete",[116,2939,173],{"class":150},[116,2941,2870],{"class":143},[116,2943,217],{"class":150},[116,2945,2946,2949,2951,2953,2955,2957,2959,2961,2963,2965,2967,2969,2971,2973,2975,2977,2979,2981,2983,2986],{"class":118,"line":347},[116,2947,2948],{"class":164},"        if",[116,2950,28],{"class":150},[116,2952,771],{"class":143},[116,2954,202],{"class":130},[116,2956,845],{"class":143},[116,2958,202],{"class":130},[116,2960,817],{"class":143},[116,2962,820],{"class":130},[116,2964,823],{"class":130},[116,2966,1920],{"class":179},[116,2968,176],{"class":130},[116,2970,261],{"class":150},[116,2972,2893],{"class":126},[116,2974,173],{"class":150},[116,2976,771],{"class":143},[116,2978,202],{"class":130},[116,2980,845],{"class":143},[116,2982,202],{"class":130},[116,2984,2985],{"class":143},"result",[116,2987,217],{"class":150},[116,2989,2990,2993,2995,2997,2999,3001,3003,3005,3007,3009,3011,3013,3015,3017,3020,3022,3024,3026,3028,3030,3032],{"class":118,"line":352},[116,2991,2992],{"class":164},"        else",[116,2994,858],{"class":164},[116,2996,28],{"class":150},[116,2998,771],{"class":143},[116,3000,202],{"class":130},[116,3002,845],{"class":143},[116,3004,202],{"class":130},[116,3006,817],{"class":143},[116,3008,820],{"class":130},[116,3010,823],{"class":130},[116,3012,1060],{"class":179},[116,3014,176],{"class":130},[116,3016,261],{"class":150},[116,3018,3019],{"class":126},"reject",[116,3021,173],{"class":150},[116,3023,771],{"class":143},[116,3025,202],{"class":130},[116,3027,845],{"class":143},[116,3029,202],{"class":130},[116,3031,1060],{"class":143},[116,3033,217],{"class":150},[116,3035,3036],{"class":118,"line":359},[116,3037,3038],{"class":130},"      }\n",[116,3040,3041,3043,3045,3047,3049,3051],{"class":118,"line":365},[116,3042,2909],{"class":143},[116,3044,202],{"class":130},[116,3046,98],{"class":126},[116,3048,173],{"class":150},[116,3050,2831],{"class":143},[116,3052,217],{"class":150},[116,3054,3055,3057],{"class":118,"line":370},[116,3056,1876],{"class":130},[116,3058,217],{"class":150},[116,3060,3061],{"class":118,"line":379},[116,3062,223],{"class":130},[116,3064,3065],{"class":118,"line":385},[116,3066,158],{"emptyLinePlaceholder":157},[116,3068,3069,3071,3074,3076],{"class":118,"line":390},[116,3070,2823],{"class":122},[116,3072,3073],{"class":150}," getIdle",[116,3075,131],{"class":130},[116,3077,134],{"class":130},[116,3079,3080,3083,3085,3088,3090,3092,3095,3098,3100,3102,3104,3107,3109],{"class":118,"line":407},[116,3081,3082],{"class":164},"    if",[116,3084,28],{"class":150},[116,3086,3087],{"class":130},"this.",[116,3089,2801],{"class":143},[116,3091,202],{"class":130},[116,3093,3094],{"class":143},"size",[116,3096,3097],{"class":130}," \u003C",[116,3099,2849],{"class":130},[116,3101,2744],{"class":143},[116,3103,202],{"class":130},[116,3105,3106],{"class":143},"length",[116,3108,261],{"class":150},[116,3110,193],{"class":130},[116,3112,3113,3116,3118,3120,3122,3125,3127,3130,3132,3135,3137,3139,3142,3144,3146],{"class":118,"line":432},[116,3114,3115],{"class":164},"      return",[116,3117,2849],{"class":130},[116,3119,2744],{"class":143},[116,3121,202],{"class":130},[116,3123,3124],{"class":126},"find",[116,3126,173],{"class":150},[116,3128,3129],{"class":495},"w",[116,3131,776],{"class":122},[116,3133,3134],{"class":130}," !this.",[116,3136,2801],{"class":143},[116,3138,202],{"class":130},[116,3140,3141],{"class":126},"has",[116,3143,173],{"class":150},[116,3145,3129],{"class":143},[116,3147,1636],{"class":150},[116,3149,3150],{"class":118,"line":459},[116,3151,2354],{"class":130},[116,3153,3154],{"class":118,"line":465},[116,3155,3156],{"class":355},"    \u002F\u002F Wait for one to become idle\n",[116,3158,3159,3161,3163,3165,3167,3169,3171],{"class":118,"line":1088},[116,3160,2881],{"class":164},[116,3162,944],{"class":130},[116,3164,2886],{"class":2699},[116,3166,173],{"class":150},[116,3168,2893],{"class":495},[116,3170,776],{"class":122},[116,3172,134],{"class":130},[116,3174,3175,3178,3180,3182,3184,3186,3188],{"class":118,"line":1102},[116,3176,3177],{"class":130},"      this.",[116,3179,2790],{"class":143},[116,3181,202],{"class":130},[116,3183,205],{"class":126},[116,3185,173],{"class":150},[116,3187,2893],{"class":143},[116,3189,217],{"class":150},[116,3191,3192,3194],{"class":118,"line":1113},[116,3193,1876],{"class":130},[116,3195,217],{"class":150},[116,3197,3198],{"class":118,"line":1124},[116,3199,223],{"class":130},[116,3201,3202],{"class":118,"line":1131},[116,3203,382],{"class":130},[11,3205,3206],{},"With 8 cores and 8 workers, you can process 8 files in parallel — each on a dedicated thread, each with hardware-accelerated decode. Real-world throughput on my M2 MacBook: 240 frames\u002Fsec\u002Fcore, ~1900 frames\u002Fsec total. That's roughly 30 minutes of video processed per second.",[11,3208,3209],{},"The catch: hardware decoders have limited concurrency. Most GPUs handle 2-4 simultaneous decode sessions. Beyond that, you fall back to software decode, which is slower. Test on your target hardware.",[18,3211,3213],{"id":3212},"common-pitfalls","Common pitfalls",[11,3215,3216,3221,3222,564],{},[25,3217,3218,3219,202],{},"Pitfall 1: forgetting ",[30,3220,1949],{}," Every unclosed VideoFrame leaks GPU memory. After ~100 leaked frames, decode fails silently. Use ",[30,3223,3224],{},"try\u002Ffinally",[84,3226,3228],{"className":110,"code":3227,"language":112,"meta":92,"style":92},"try {\n  \u002F\u002F ... use frame ...\n} finally {\n  frame.close()\n}\n",[30,3229,3230,3237,3242,3251,3261],{"__ignoreMap":92},[116,3231,3232,3235],{"class":118,"line":119},[116,3233,3234],{"class":164},"try",[116,3236,134],{"class":130},[116,3238,3239],{"class":118,"line":137},[116,3240,3241],{"class":355},"  \u002F\u002F ... use frame ...\n",[116,3243,3244,3246,3249],{"class":118,"line":154},[116,3245,1569],{"class":130},[116,3247,3248],{"class":164}," finally",[116,3250,134],{"class":130},[116,3252,3253,3255,3257,3259],{"class":118,"line":161},[116,3254,1721],{"class":143},[116,3256,202],{"class":130},[116,3258,1646],{"class":126},[116,3260,404],{"class":150},[116,3262,3263],{"class":118,"line":196},[116,3264,382],{"class":130},[11,3266,3267,3270,3271,3274],{},[25,3268,3269],{},"Pitfall 2: decode order != display order."," H.264 with B-frames decodes in DTS order but displays in PTS order. If you process frames in decode order, your thumbnails are out of order. WebCodecs gives you both — ",[30,3272,3273],{},"frame.timestamp"," is PTS, decode happens in DTS order.",[11,3276,3277,3280,3281,3284,3285,3287,3288,3287,3291,3294],{},[25,3278,3279],{},"Pitfall 3: codec string format."," WebCodecs wants ",[30,3282,3283],{},"'avc1.640028'"," (with the constraint byte). MP4 sometimes stores ",[30,3286,3283],{},", sometimes ",[30,3289,3290],{},"'avc1.64.0028'",[30,3292,3293],{},"'avc1.42E01E'",". Normalize before configuring the decoder.",[11,3296,3297,3300,3301,3303,3304,3307,3308,3311],{},[25,3298,3299],{},"Pitfall 4: worker module loading."," Workers can't ",[30,3302,2014],{}," from arbitrary URLs in all browsers. Use ",[30,3305,3306],{},"importScripts()"," or set ",[30,3309,3310],{},"\"type\": \"module\""," in the Worker constructor (Chrome 80+, Safari 15+).",[11,3313,3314,3317,3318,3321,3322,202],{},[25,3315,3316],{},"Pitfall 5: OPFS quota."," Browsers limit OPFS to a percentage of free disk space. A 5GB write might fail if the user has a full disk. Catch ",[30,3319,3320],{},"QuotaExceededError"," and fall back to streaming via ",[30,3323,3324],{},"showSaveFilePicker()",[18,3326,3328],{"id":3327},"when-to-use-what","When to use what",[11,3330,3331],{},[25,3332,3333],{},"Use FFmpeg WASM when:",[2667,3335,3336,3339,3342,3345],{},[1943,3337,3338],{},"You need to handle every codec (HEVC, VP9, AV1, MPEG-TS, MKV, FLV)",[1943,3340,3341],{},"You need to remux\u002Fmux containers (TS → MP4, MP4 → WebM)",[1943,3343,3344],{},"You need filter chains (overlay, scale, trim, concatenate)",[1943,3346,3347],{},"Reliability matters more than speed",[11,3349,3350],{},[25,3351,3352],{},"Use WebCodecs when:",[2667,3354,3355,3358,3361,3364],{},[1943,3356,3357],{},"You know the codec is H.264\u002FH.265\u002FVP9\u002FAV1",[1943,3359,3360],{},"You're doing per-frame operations (thumbnails, motion detection, frame extraction)",[1943,3362,3363],{},"You need real-time performance (filtering live video, frame-by-frame analysis)",[1943,3365,3366],{},"Memory budget is tight (avoid 25MB WASM download)",[11,3368,3369],{},[25,3370,3371],{},"Use Web Workers for:",[2667,3373,3374,3377],{},[1943,3375,3376],{},"Anything CPU-heavy that shouldn't block the main thread",[1943,3378,3379],{},"Parallel processing of independent units (files, segments)",[11,3381,3382],{},[25,3383,3384],{},"Use OPFS for:",[2667,3386,3387,3390,3393],{},[1943,3388,3389],{},"Anything bigger than 5MB (the localStorage cap)",[1943,3391,3392],{},"Persistent intermediate state (you don't want to re-decode on refresh)",[1943,3394,3395],{},"Streaming large files without loading into memory",[18,3397,3399],{"id":3398},"references","References",[2667,3401,3402,3410,3418,3426,3434],{},[1943,3403,3404,3409],{},[1987,3405,3408],{"href":3406,"rel":3407},"https:\u002F\u002Fwww.w3.org\u002FTR\u002Fwebcodecs\u002F",[1991],"WebCodecs specification"," — The actual spec, surprisingly readable",[1943,3411,3412,3417],{},[1987,3413,3416],{"href":3414,"rel":3415},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWebCodecs_API",[1991],"MDN WebCodecs API"," — Practical docs with examples",[1943,3419,3420,3425],{},[1987,3421,3424],{"href":3422,"rel":3423},"https:\u002F\u002Fgithub.com\u002Fw3c\u002Fwebcodecs\u002Ftree\u002Fmain\u002Fsamples",[1991],"WebCodecs samples repo"," — Official working examples",[1943,3427,3428,3433],{},[1987,3429,3432],{"href":3430,"rel":3431},"https:\u002F\u002Ffs.spec.whatwg.org\u002F",[1991],"OPFS specification"," — File System Access API, includes OPFS",[1943,3435,3436,3440],{},[1987,3437,1985],{"href":3438,"rel":3439},"https:\u002F\u002Fgithub.com\u002Fgpac\u002Fmp4box.js",[1991]," — GPAC's JS MP4 parser, the standard for WebCodecs input",[18,3442,3444],{"id":3443},"summary","Summary",[11,3446,3447,3448,3452],{},"WebCodecs + Workers + OPFS is a real alternative to FFmpeg WASM for performance-sensitive video processing. It's not a replacement — FFmpeg WASM still wins on codec breadth and edge case handling — but for known-codec, hardware-accelerated, frame-level work, WebCodecs is 4-5x faster and 250x smaller. The next article in this series is ",[1987,3449,3451],{"href":3450},"\u002Fblog\u002Fis-it-legal-to-download-streaming-video","the legality of streaming video download",", which covers when you're allowed to use these tools at all.",[11,3454,3455,3456,202],{},"For full benchmark numbers across codecs, resolutions, and file sizes, see ",[1987,3457,3459],{"href":3458},"\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","the browser video processing performance benchmarks article",[3461,3462],"hr",{},[18,3464,3466],{"id":3465},"related-articles","Related articles",[2667,3468,3469,3478,3486,3495],{},[1943,3470,3471,3477],{},[25,3472,3473],{},[1987,3474,3476],{"href":3475},"\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff","Browser-Side Video Remuxing: fMP4, ISOBMFF, Why We Don't Transcode"," — The container-format deep dive that complements this codec-level article",[1943,3479,3480,3485],{},[25,3481,3482],{},[1987,3483,3484],{"href":3458},"Browser Video Processing Performance Benchmarks"," — Full benchmarks including the data in this article",[1943,3487,3488,3494],{},[25,3489,3490],{},[1987,3491,3493],{"href":3492},"\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","How FlowPick Merges Hundreds of Video Segments in Your Browser"," — Where FlowPick uses FFmpeg WASM vs WebCodecs",[1943,3496,3497,3503],{},[25,3498,3499],{},[1987,3500,3502],{"href":3501},"\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH Deep Dive: SegmentTemplate, ContentProtection"," — The chunks you'd feed to WebCodecs",[18,3505,3507],{"id":3506},"recommended-reading","Recommended reading",[2667,3509,3510,3519,3528],{},[1943,3511,3512,3518],{},[25,3513,3514],{},[1987,3515,3517],{"href":3516},"\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack","HLS Deep Dive: Encryption, Multi-Track, EXT-X Tags"," — Where the segments come from",[1943,3520,3521,3527],{},[25,3522,3523],{},[1987,3524,3526],{"href":3525},"\u002Fblog\u002Fhow-i-built-flowpick-in-three-months","How I Built FlowPick in Three Months"," — The development story, including the WebCodecs vs FFmpeg WASM decision",[1943,3529,3530,3536],{},[25,3531,3532],{},[1987,3533,3535],{"href":3534},"\u002Fblog\u002Fflowpick-v1-1-0-smarter-media-detection","FlowPick v1.1.0: Smarter Media Detection"," — MSE recording, which uses WebCodecs internally",[3538,3539,3540],"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":137,"depth":137,"links":3542},[3543,3544,3545,3546,3547,3548,3549,3550,3551,3552,3553,3554,3555,3556,3557],{"id":20,"depth":137,"text":21},{"id":78,"depth":137,"text":79},{"id":106,"depth":137,"text":107},{"id":473,"depth":137,"text":474},{"id":701,"depth":137,"text":702},{"id":1968,"depth":137,"text":1969},{"id":2448,"depth":137,"text":2449},{"id":2576,"depth":137,"text":2577},{"id":2682,"depth":137,"text":2683},{"id":3212,"depth":137,"text":3213},{"id":3327,"depth":137,"text":3328},{"id":3398,"depth":137,"text":3399},{"id":3443,"depth":137,"text":3444},{"id":3465,"depth":137,"text":3466},{"id":3506,"depth":137,"text":3507},"tips","2026-08-04","A working example of using WebCodecs for hardware-accelerated video decode, Web Workers for parallel processing, and OPFS for high-throughput local file I\u002FO — with benchmarks against FFmpeg WASM.","md","\u002Fscreenshots\u002Fformat-conversion.png",{},"\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing",{"title":5,"description":3560},"blog\u002Fwebcodecs-web-workers-opfs-video-processing",[3568,3569,3570,3571,3572,3573],"webcodecs","web workers","opfs","performance","wasm","deep-dive","xZdR2LbRcE_DquTkX62mxQHEzLtpmkCnUBMxD2RTOGI",[3576,3580],{"title":3577,"path":3450,"stem":3578,"description":3579,"date":3559,"category":3558,"children":-1},"Is It Legal to Download Streaming Video? A Technical and Legal Checklist","blog\u002Fis-it-legal-to-download-streaming-video","Not 'consult a lawyer' hand-waving — actual signals you can check in the network tab, the actual laws that matter in major jurisdictions, and where FlowPick draws the line.",{"title":3581,"path":3582,"stem":3583,"description":3584,"date":3585,"category":3558,"children":-1},"What Is DASH Streaming? MPD File Explained for Non-Developers","\u002Fblog\u002Fwhat-is-dash-mpd-streaming","blog\u002Fwhat-is-dash-mpd-streaming","Learn what MPEG-DASH and MPD manifest files are, how to download DASH video and audio tracks, and why MPD streams are harder to save than regular MP4 files.","2026-07-01",1787670589116]