[{"data":1,"prerenderedAt":3568},["ShallowReactive",2],{"blog-ja-webcodecs-web-workers-opfs-video-processing":3,"blog-ja-webcodecs-web-workers-opfs-video-processing-surround":3557},{"id":4,"title":5,"author":6,"body":7,"category":3540,"date":3541,"description":3542,"extension":3543,"image":3544,"lastModified":3541,"meta":3545,"navigation":152,"path":3546,"readingTime":343,"seo":3547,"stem":3548,"tags":3549,"__hash__":3556},"blog_ja\u002Fja\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing.md","WebCodecs + Web Workers + OPFS：ブラウザで動画処理をする実戦パス","FlowPick チーム",{"type":8,"value":9,"toc":3524},"minimark",[10,14,17,22,56,62,76,79,82,92,99,103,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,3215,3227,3267,3277,3296,3313,3327,3330,3335,3349,3354,3368,3373,3381,3386,3397,3400,3441,3444,3452,3460,3463,3466,3520],[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"," オブジェクトを返す。Canvas にレンダリングでも、トランスコードでも、下流に渡せる。",[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,78],{"id":78},"搭載するアーキテクチャ",[11,80,81],{},"作ったもの：ユーザーが動画ファイルをページにドラッグし、毎秒 1 フレームで JPEG として抽出して OPFS に書き込み、最後にダウンロードリンクを出す。実際のシナリオは動画ライブラリのサムネイル生成。",[83,84,89],"pre",{"className":85,"code":87,"language":88},[86],"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,90,87],{"__ignoreMap":91},"",[11,93,94,95,98],{},"「OPFS パスから取得」がポイント。",[30,96,97],{},"postMessage"," で 500MB のファイルを転送（Transferable にしても）は遅くてメモリコピーが走る。ファイルを先に OPFS に書いて、パスを Worker に渡す方がずっと速い。",[18,100,102],{"id":101},"ステップ-1機能検出","ステップ 1：機能検出",[83,104,108],{"className":105,"code":106,"language":107,"meta":91,"style":91},"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,109,110,130,147,154,190,214,220,225,260,280,285,290,316,336,341,346,353,359,364,373,379,384,401,426,453,459],{"__ignoreMap":91},[111,112,115,119,123,127],"span",{"class":113,"line":114},"line",1,[111,116,118],{"class":117},"spNyl","function",[111,120,122],{"class":121},"s2Zo4"," checkSupport",[111,124,126],{"class":125},"sMK4o","()",[111,128,129],{"class":125}," {\n",[111,131,133,136,140,143],{"class":113,"line":132},2,[111,134,135],{"class":117},"  const",[111,137,139],{"class":138},"sTEyZ"," issues",[111,141,142],{"class":125}," =",[111,144,146],{"class":145},"swJcz"," []\n",[111,148,150],{"class":113,"line":149},3,[111,151,153],{"emptyLinePlaceholder":152},true,"\n",[111,155,157,161,164,167,170,173,176,178,181,184,187],{"class":113,"line":156},4,[111,158,160],{"class":159},"s7zQu","  if",[111,162,163],{"class":145}," (",[111,165,166],{"class":125},"!",[111,168,169],{"class":145},"(",[111,171,172],{"class":125},"'",[111,174,32],{"class":175},"sfazB",[111,177,172],{"class":125},[111,179,180],{"class":125}," in",[111,182,183],{"class":138}," window",[111,185,186],{"class":145},")) ",[111,188,189],{"class":125},"{\n",[111,191,193,196,199,202,204,206,209,211],{"class":113,"line":192},5,[111,194,195],{"class":138},"    issues",[111,197,198],{"class":125},".",[111,200,201],{"class":121},"push",[111,203,169],{"class":145},[111,205,172],{"class":125},[111,207,208],{"class":175},"WebCodecs VideoDecoder が未サポート。Chrome 94+ または同等のブラウザが必要。",[111,210,172],{"class":125},[111,212,213],{"class":145},")\n",[111,215,217],{"class":113,"line":216},6,[111,218,219],{"class":125},"  }\n",[111,221,223],{"class":113,"line":222},7,[111,224,153],{"emptyLinePlaceholder":152},[111,226,228,230,232,234,237,239,242,245,248,250,252,255,258],{"class":113,"line":227},8,[111,229,160],{"class":159},[111,231,163],{"class":145},[111,233,166],{"class":125},[111,235,236],{"class":138},"window",[111,238,198],{"class":125},[111,240,241],{"class":138},"showOpenFilePicker",[111,243,244],{"class":125}," &&",[111,246,247],{"class":125}," !",[111,249,236],{"class":138},[111,251,198],{"class":125},[111,253,254],{"class":138},"FileSystemHandle",[111,256,257],{"class":145},") ",[111,259,189],{"class":125},[111,261,263,265,267,269,271,273,276,278],{"class":113,"line":262},9,[111,264,195],{"class":138},[111,266,198],{"class":125},[111,268,201],{"class":121},[111,270,169],{"class":145},[111,272,172],{"class":125},[111,274,275],{"class":175},"File System Access API が未サポート。",[111,277,172],{"class":125},[111,279,213],{"class":145},[111,281,283],{"class":113,"line":282},10,[111,284,219],{"class":125},[111,286,288],{"class":113,"line":287},11,[111,289,153],{"emptyLinePlaceholder":152},[111,291,293,295,297,299,302,304,307,309,312,314],{"class":113,"line":292},12,[111,294,160],{"class":159},[111,296,163],{"class":145},[111,298,166],{"class":125},[111,300,301],{"class":138},"navigator",[111,303,198],{"class":125},[111,305,306],{"class":138},"storage",[111,308,198],{"class":125},[111,310,311],{"class":138},"getDirectory",[111,313,257],{"class":145},[111,315,189],{"class":125},[111,317,319,321,323,325,327,329,332,334],{"class":113,"line":318},13,[111,320,195],{"class":138},[111,322,198],{"class":125},[111,324,201],{"class":121},[111,326,169],{"class":145},[111,328,172],{"class":125},[111,330,331],{"class":175},"OPFS が未サポート。Chrome 102+ または同等のブラウザが必要。",[111,333,172],{"class":125},[111,335,213],{"class":145},[111,337,339],{"class":113,"line":338},14,[111,340,219],{"class":125},[111,342,344],{"class":113,"line":343},15,[111,345,153],{"emptyLinePlaceholder":152},[111,347,349],{"class":113,"line":348},16,[111,350,352],{"class":351},"sHwdD","  \u002F\u002F Worker 自体はほぼ標準だが、Worker 内で同期 OPFS が使えるかをチェック\n",[111,354,356],{"class":113,"line":355},17,[111,357,358],{"class":351},"  \u002F\u002F （古い Chrome は Worker をサポートしていても同期 OPFS がない）\n",[111,360,362],{"class":113,"line":361},18,[111,363,153],{"emptyLinePlaceholder":152},[111,365,367,370],{"class":113,"line":366},19,[111,368,369],{"class":159},"  return",[111,371,372],{"class":138}," issues\n",[111,374,376],{"class":113,"line":375},20,[111,377,378],{"class":125},"}\n",[111,380,382],{"class":113,"line":381},21,[111,383,153],{"emptyLinePlaceholder":152},[111,385,387,390,393,396,398],{"class":113,"line":386},22,[111,388,389],{"class":117},"const",[111,391,392],{"class":138}," issues ",[111,394,395],{"class":125},"=",[111,397,122],{"class":121},[111,399,400],{"class":138},"()\n",[111,402,404,407,410,412,415,418,422,424],{"class":113,"line":403},23,[111,405,406],{"class":159},"if",[111,408,409],{"class":138}," (issues",[111,411,198],{"class":125},[111,413,414],{"class":138},"length ",[111,416,417],{"class":125},">",[111,419,421],{"class":420},"sbssI"," 0",[111,423,257],{"class":138},[111,425,189],{"class":125},[111,427,429,432,434,437,439,441,444,446,449,451],{"class":113,"line":428},24,[111,430,431],{"class":138},"  console",[111,433,198],{"class":125},[111,435,436],{"class":121},"warn",[111,438,169],{"class":145},[111,440,172],{"class":125},[111,442,443],{"class":175},"機能ギャップ：",[111,445,172],{"class":125},[111,447,448],{"class":125},",",[111,450,139],{"class":138},[111,452,213],{"class":145},[111,454,456],{"class":113,"line":455},25,[111,457,458],{"class":351},"  \u002F\u002F FFmpeg WASM にフォールバック\n",[111,460,462],{"class":113,"line":461},26,[111,463,378],{"class":125},[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 に置く",[83,472,474],{"className":105,"code":473,"language":107,"meta":91,"style":91},"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":91},[111,477,478,481,484,487,489,493,496],{"class":113,"line":114},[111,479,480],{"class":117},"async",[111,482,483],{"class":117}," function",[111,485,486],{"class":121}," stageFileToOpfs",[111,488,169],{"class":125},[111,490,492],{"class":491},"sHdIc","file",[111,494,495],{"class":125},")",[111,497,129],{"class":125},[111,499,500,502,505,507,510,513,515,517,519,521],{"class":113,"line":132},[111,501,135],{"class":117},[111,503,504],{"class":138}," root",[111,506,142],{"class":125},[111,508,509],{"class":159}," await",[111,511,512],{"class":138}," navigator",[111,514,198],{"class":125},[111,516,306],{"class":138},[111,518,198],{"class":125},[111,520,311],{"class":121},[111,522,400],{"class":145},[111,524,525,527,530,532,534,536,538,541,543,545,548,550,552,555,558,561,565,568],{"class":113,"line":149},[111,526,135],{"class":117},[111,528,529],{"class":138}," dir",[111,531,142],{"class":125},[111,533,509],{"class":159},[111,535,504],{"class":138},[111,537,198],{"class":125},[111,539,540],{"class":121},"getDirectoryHandle",[111,542,169],{"class":145},[111,544,172],{"class":125},[111,546,547],{"class":175},"input",[111,549,172],{"class":125},[111,551,448],{"class":125},[111,553,554],{"class":125}," {",[111,556,557],{"class":145}," create",[111,559,560],{"class":125},":",[111,562,564],{"class":563},"sfNiH"," true",[111,566,567],{"class":125}," }",[111,569,213],{"class":145},[111,571,572,574,577,579,581,583,585,588,590,592,594,597,599,601,603,605,607,609],{"class":113,"line":156},[111,573,135],{"class":117},[111,575,576],{"class":138}," fileHandle",[111,578,142],{"class":125},[111,580,509],{"class":159},[111,582,529],{"class":138},[111,584,198],{"class":125},[111,586,587],{"class":121},"getFileHandle",[111,589,169],{"class":145},[111,591,492],{"class":138},[111,593,198],{"class":125},[111,595,596],{"class":138},"name",[111,598,448],{"class":125},[111,600,554],{"class":125},[111,602,557],{"class":145},[111,604,560],{"class":125},[111,606,564],{"class":563},[111,608,567],{"class":125},[111,610,213],{"class":145},[111,612,613,615,618,620,622,624,626,629],{"class":113,"line":192},[111,614,135],{"class":117},[111,616,617],{"class":138}," writable",[111,619,142],{"class":125},[111,621,509],{"class":159},[111,623,576],{"class":138},[111,625,198],{"class":125},[111,627,628],{"class":121},"createWritable",[111,630,400],{"class":145},[111,632,633,636,639,641,644,646,648,651,653,656],{"class":113,"line":216},[111,634,635],{"class":159},"  await",[111,637,638],{"class":138}," file",[111,640,198],{"class":125},[111,642,643],{"class":121},"stream",[111,645,126],{"class":145},[111,647,198],{"class":125},[111,649,650],{"class":121},"pipeTo",[111,652,169],{"class":145},[111,654,655],{"class":138},"writable",[111,657,213],{"class":145},[111,659,660],{"class":113,"line":222},[111,661,153],{"emptyLinePlaceholder":152},[111,663,664,666,669,671,673,675,678],{"class":113,"line":227},[111,665,369],{"class":159},[111,667,668],{"class":125}," `${",[111,670,492],{"class":138},[111,672,198],{"class":125},[111,674,596],{"class":138},[111,676,677],{"class":125},"}`",[111,679,680],{"class":351},"  \u002F\u002F OPFS ルート下の 'input' フォルダ内のパス\n",[111,682,683],{"class":113,"line":262},[111,684,378],{"class":125},[11,686,687,688,691,692,694],{},"ここは非同期 ",[30,689,690],{},"createWritable()","。Worker 内では同期版 ",[30,693,74],{}," を使えば、小さな書き込みが多い場合は速い。",[18,696,698],{"id":697},"ステップ-3worker","ステップ 3：Worker",[83,700,702],{"className":105,"code":701,"language":107,"meta":91,"style":91},"\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":91},[111,705,706],{"class":113,"line":114},[111,707,708],{"class":351},"\u002F\u002F worker.js\n",[111,710,711,714,717,719],{"class":113,"line":132},[111,712,713],{"class":117},"let",[111,715,716],{"class":138}," decoder ",[111,718,395],{"class":125},[111,720,721],{"class":125}," null\n",[111,723,724,726,729,731],{"class":113,"line":149},[111,725,713],{"class":117},[111,727,728],{"class":138}," canvas ",[111,730,395],{"class":125},[111,732,721],{"class":125},[111,734,735,737,740,742],{"class":113,"line":156},[111,736,713],{"class":117},[111,738,739],{"class":138}," ctx ",[111,741,395],{"class":125},[111,743,721],{"class":125},[111,745,746],{"class":113,"line":192},[111,747,153],{"emptyLinePlaceholder":152},[111,749,750,753,755,758,760,763,765,768,770,773],{"class":113,"line":216},[111,751,752],{"class":138},"self",[111,754,198],{"class":125},[111,756,757],{"class":121},"onmessage",[111,759,142],{"class":125},[111,761,762],{"class":117}," async",[111,764,163],{"class":125},[111,766,767],{"class":491},"e",[111,769,495],{"class":125},[111,771,772],{"class":117}," =>",[111,774,129],{"class":125},[111,776,777,779,781,784,786,789,791,793,796,798],{"class":113,"line":222},[111,778,135],{"class":117},[111,780,554],{"class":125},[111,782,783],{"class":138}," type",[111,785,448],{"class":125},[111,787,788],{"class":138}," data",[111,790,567],{"class":125},[111,792,142],{"class":125},[111,794,795],{"class":138}," e",[111,797,198],{"class":125},[111,799,800],{"class":138},"data\n",[111,802,803],{"class":113,"line":227},[111,804,153],{"emptyLinePlaceholder":152},[111,806,807,809,811,814,817,820,823,825,827],{"class":113,"line":262},[111,808,160],{"class":159},[111,810,163],{"class":145},[111,812,813],{"class":138},"type",[111,815,816],{"class":125}," ===",[111,818,819],{"class":125}," '",[111,821,822],{"class":175},"init",[111,824,172],{"class":125},[111,826,257],{"class":145},[111,828,189],{"class":125},[111,830,831,834,837,839,842],{"class":113,"line":282},[111,832,833],{"class":159},"    await",[111,835,836],{"class":121}," init",[111,838,169],{"class":145},[111,840,841],{"class":138},"data",[111,843,213],{"class":145},[111,845,846,849,852,855,857,859,861,863,866,868,870],{"class":113,"line":287},[111,847,848],{"class":125},"  }",[111,850,851],{"class":159}," else",[111,853,854],{"class":159}," if",[111,856,163],{"class":145},[111,858,813],{"class":138},[111,860,816],{"class":125},[111,862,819],{"class":125},[111,864,865],{"class":175},"extract",[111,867,172],{"class":125},[111,869,257],{"class":145},[111,871,189],{"class":125},[111,873,874,876,879,881,883],{"class":113,"line":292},[111,875,833],{"class":159},[111,877,878],{"class":121}," extractFrames",[111,880,169],{"class":145},[111,882,841],{"class":138},[111,884,213],{"class":145},[111,886,887],{"class":113,"line":318},[111,888,219],{"class":125},[111,890,891],{"class":113,"line":338},[111,892,378],{"class":125},[111,894,895],{"class":113,"line":343},[111,896,153],{"emptyLinePlaceholder":152},[111,898,899,901,903,905,908,911,913,916,918,921,924],{"class":113,"line":348},[111,900,480],{"class":117},[111,902,483],{"class":117},[111,904,836],{"class":121},[111,906,907],{"class":125},"({",[111,909,910],{"class":491}," codec",[111,912,448],{"class":125},[111,914,915],{"class":491}," canvasWidth",[111,917,448],{"class":125},[111,919,920],{"class":491}," canvasHeight",[111,922,923],{"class":125}," })",[111,925,129],{"class":125},[111,927,928],{"class":113,"line":355},[111,929,930],{"class":351},"  \u002F\u002F Canvas を用意（Worker 内では OffscreenCanvas）\n",[111,932,933,936,938,941,944,946,949,951,953],{"class":113,"line":361},[111,934,935],{"class":138},"  canvas",[111,937,142],{"class":125},[111,939,940],{"class":125}," new",[111,942,943],{"class":121}," OffscreenCanvas",[111,945,169],{"class":145},[111,947,948],{"class":138},"canvasWidth",[111,950,448],{"class":125},[111,952,920],{"class":138},[111,954,213],{"class":145},[111,956,957,960,962,965,967,970,972,974,977,979],{"class":113,"line":366},[111,958,959],{"class":138},"  ctx",[111,961,142],{"class":125},[111,963,964],{"class":138}," canvas",[111,966,198],{"class":125},[111,968,969],{"class":121},"getContext",[111,971,169],{"class":145},[111,973,172],{"class":125},[111,975,976],{"class":175},"2d",[111,978,172],{"class":125},[111,980,213],{"class":145},[111,982,983],{"class":113,"line":375},[111,984,153],{"emptyLinePlaceholder":152},[111,986,987],{"class":113,"line":381},[111,988,989],{"class":351},"  \u002F\u002F WebCodecs デコーダ\n",[111,991,992,995,997,999,1002,1004],{"class":113,"line":386},[111,993,994],{"class":138},"  decoder",[111,996,142],{"class":125},[111,998,940],{"class":125},[111,1000,1001],{"class":121}," VideoDecoder",[111,1003,169],{"class":145},[111,1005,189],{"class":125},[111,1007,1008,1011,1013,1015,1018,1020,1022,1025,1027,1029,1031],{"class":113,"line":403},[111,1009,1010],{"class":121},"    output",[111,1012,560],{"class":125},[111,1014,163],{"class":125},[111,1016,1017],{"class":491},"frame",[111,1019,495],{"class":125},[111,1021,772],{"class":117},[111,1023,1024],{"class":121}," handleFrame",[111,1026,169],{"class":145},[111,1028,1017],{"class":138},[111,1030,495],{"class":145},[111,1032,1033],{"class":125},",\n",[111,1035,1036,1039,1041,1043,1045,1047,1049,1052,1054,1057,1059,1061,1064,1066,1068,1070],{"class":113,"line":428},[111,1037,1038],{"class":121},"    error",[111,1040,560],{"class":125},[111,1042,163],{"class":125},[111,1044,767],{"class":491},[111,1046,495],{"class":125},[111,1048,772],{"class":117},[111,1050,1051],{"class":138}," console",[111,1053,198],{"class":125},[111,1055,1056],{"class":121},"error",[111,1058,169],{"class":145},[111,1060,172],{"class":125},[111,1062,1063],{"class":175},"デコーダエラー：",[111,1065,172],{"class":125},[111,1067,448],{"class":125},[111,1069,795],{"class":138},[111,1071,213],{"class":145},[111,1073,1074,1076],{"class":113,"line":455},[111,1075,848],{"class":125},[111,1077,213],{"class":145},[111,1079,1080],{"class":113,"line":461},[111,1081,153],{"emptyLinePlaceholder":152},[111,1083,1085,1087,1089,1092,1094],{"class":113,"line":1084},27,[111,1086,994],{"class":138},[111,1088,198],{"class":125},[111,1090,1091],{"class":121},"configure",[111,1093,169],{"class":145},[111,1095,189],{"class":125},[111,1097,1099,1102,1104],{"class":113,"line":1098},28,[111,1100,1101],{"class":138},"    codec",[111,1103,448],{"class":125},[111,1105,1106],{"class":351},"  \u002F\u002F 例：'avc1.640028'\n",[111,1108,1110,1113,1115],{"class":113,"line":1109},29,[111,1111,1112],{"class":145},"    optimizeForLatency",[111,1114,560],{"class":125},[111,1116,1117],{"class":563}," false\n",[111,1119,1121,1123],{"class":113,"line":1120},30,[111,1122,848],{"class":125},[111,1124,213],{"class":145},[111,1126,1128],{"class":113,"line":1127},31,[111,1129,153],{"emptyLinePlaceholder":152},[111,1131,1133,1136,1138,1140,1142,1145,1147,1149,1151,1154,1156,1158],{"class":113,"line":1132},32,[111,1134,1135],{"class":138},"  self",[111,1137,198],{"class":125},[111,1139,97],{"class":121},[111,1141,169],{"class":145},[111,1143,1144],{"class":125},"{",[111,1146,783],{"class":145},[111,1148,560],{"class":125},[111,1150,819],{"class":125},[111,1152,1153],{"class":175},"ready",[111,1155,172],{"class":125},[111,1157,567],{"class":125},[111,1159,213],{"class":145},[111,1161,1163],{"class":113,"line":1162},33,[111,1164,378],{"class":125},[111,1166,1168],{"class":113,"line":1167},34,[111,1169,153],{"emptyLinePlaceholder":152},[111,1171,1173,1175,1178,1180],{"class":113,"line":1172},35,[111,1174,713],{"class":117},[111,1176,1177],{"class":138}," frameCount ",[111,1179,395],{"class":125},[111,1181,1182],{"class":420}," 0\n",[111,1184,1186,1188,1191,1193,1196],{"class":113,"line":1185},36,[111,1187,713],{"class":117},[111,1189,1190],{"class":138}," lastExtractedSecond ",[111,1192,395],{"class":125},[111,1194,1195],{"class":125}," -",[111,1197,1198],{"class":420},"1\n",[111,1200,1202],{"class":113,"line":1201},37,[111,1203,153],{"emptyLinePlaceholder":152},[111,1205,1207,1209,1211,1213,1215,1217,1219],{"class":113,"line":1206},38,[111,1208,480],{"class":117},[111,1210,483],{"class":117},[111,1212,1024],{"class":121},[111,1214,169],{"class":125},[111,1216,1017],{"class":491},[111,1218,495],{"class":125},[111,1220,129],{"class":125},[111,1222,1224,1226,1229,1231,1234,1236,1239,1242,1245],{"class":113,"line":1223},39,[111,1225,135],{"class":117},[111,1227,1228],{"class":138}," timestampSeconds",[111,1230,142],{"class":125},[111,1232,1233],{"class":138}," frame",[111,1235,198],{"class":125},[111,1237,1238],{"class":138},"timestamp",[111,1240,1241],{"class":125}," \u002F",[111,1243,1244],{"class":420}," 1_000_000",[111,1246,1247],{"class":351},"  \u002F\u002F WebCodecs はマイクロ秒\n",[111,1249,1251],{"class":113,"line":1250},40,[111,1252,153],{"emptyLinePlaceholder":152},[111,1254,1256],{"class":113,"line":1255},41,[111,1257,1258],{"class":351},"  \u002F\u002F 毎秒 1 フレームだけ抽出\n",[111,1260,1262,1264,1267,1269,1272,1274,1277,1279,1282],{"class":113,"line":1261},42,[111,1263,135],{"class":117},[111,1265,1266],{"class":138}," currentSecond",[111,1268,142],{"class":125},[111,1270,1271],{"class":138}," Math",[111,1273,198],{"class":125},[111,1275,1276],{"class":121},"floor",[111,1278,169],{"class":145},[111,1280,1281],{"class":138},"timestampSeconds",[111,1283,213],{"class":145},[111,1285,1287,1289,1291,1294,1297,1300,1302],{"class":113,"line":1286},43,[111,1288,160],{"class":159},[111,1290,163],{"class":145},[111,1292,1293],{"class":138},"currentSecond",[111,1295,1296],{"class":125}," !==",[111,1298,1299],{"class":138}," lastExtractedSecond",[111,1301,257],{"class":145},[111,1303,189],{"class":125},[111,1305,1307,1310,1312],{"class":113,"line":1306},44,[111,1308,1309],{"class":138},"    lastExtractedSecond",[111,1311,142],{"class":125},[111,1313,1314],{"class":138}," currentSecond\n",[111,1316,1318],{"class":113,"line":1317},45,[111,1319,153],{"emptyLinePlaceholder":152},[111,1321,1323,1326,1328,1331,1333,1335,1337,1339,1341,1343,1345,1347,1349,1352,1354,1356,1358,1361],{"class":113,"line":1322},46,[111,1324,1325],{"class":138},"    ctx",[111,1327,198],{"class":125},[111,1329,1330],{"class":121},"drawImage",[111,1332,169],{"class":145},[111,1334,1017],{"class":138},[111,1336,448],{"class":125},[111,1338,421],{"class":420},[111,1340,448],{"class":125},[111,1342,421],{"class":420},[111,1344,448],{"class":125},[111,1346,964],{"class":138},[111,1348,198],{"class":125},[111,1350,1351],{"class":138},"width",[111,1353,448],{"class":125},[111,1355,964],{"class":138},[111,1357,198],{"class":125},[111,1359,1360],{"class":138},"height",[111,1362,213],{"class":145},[111,1364,1366,1369,1372,1374,1376,1378,1380,1383,1385,1387,1389,1391,1393,1396,1398,1400,1403,1405,1408,1410],{"class":113,"line":1365},47,[111,1367,1368],{"class":117},"    const",[111,1370,1371],{"class":138}," blob",[111,1373,142],{"class":125},[111,1375,509],{"class":159},[111,1377,964],{"class":138},[111,1379,198],{"class":125},[111,1381,1382],{"class":121},"convertToBlob",[111,1384,169],{"class":145},[111,1386,1144],{"class":125},[111,1388,783],{"class":145},[111,1390,560],{"class":125},[111,1392,819],{"class":125},[111,1394,1395],{"class":175},"image\u002Fjpeg",[111,1397,172],{"class":125},[111,1399,448],{"class":125},[111,1401,1402],{"class":145}," quality",[111,1404,560],{"class":125},[111,1406,1407],{"class":420}," 0.85",[111,1409,567],{"class":125},[111,1411,213],{"class":145},[111,1413,1415,1417,1420,1422,1424,1426,1428,1431],{"class":113,"line":1414},48,[111,1416,1368],{"class":117},[111,1418,1419],{"class":138}," buffer",[111,1421,142],{"class":125},[111,1423,509],{"class":159},[111,1425,1371],{"class":138},[111,1427,198],{"class":125},[111,1429,1430],{"class":121},"arrayBuffer",[111,1432,400],{"class":145},[111,1434,1436],{"class":113,"line":1435},49,[111,1437,153],{"emptyLinePlaceholder":152},[111,1439,1441],{"class":113,"line":1440},50,[111,1442,1443],{"class":351},"    \u002F\u002F OPFS に書き込む\n",[111,1445,1447,1449,1451,1453,1455,1457,1459,1461,1463,1465],{"class":113,"line":1446},51,[111,1448,1368],{"class":117},[111,1450,504],{"class":138},[111,1452,142],{"class":125},[111,1454,509],{"class":159},[111,1456,512],{"class":138},[111,1458,198],{"class":125},[111,1460,306],{"class":138},[111,1462,198],{"class":125},[111,1464,311],{"class":121},[111,1466,400],{"class":145},[111,1468,1470,1472,1475,1477,1479,1481,1483,1485,1487,1489,1492,1494,1496,1498,1500,1502,1504,1506],{"class":113,"line":1469},52,[111,1471,1368],{"class":117},[111,1473,1474],{"class":138}," outputDir",[111,1476,142],{"class":125},[111,1478,509],{"class":159},[111,1480,504],{"class":138},[111,1482,198],{"class":125},[111,1484,540],{"class":121},[111,1486,169],{"class":145},[111,1488,172],{"class":125},[111,1490,1491],{"class":175},"thumbnails",[111,1493,172],{"class":125},[111,1495,448],{"class":125},[111,1497,554],{"class":125},[111,1499,557],{"class":145},[111,1501,560],{"class":125},[111,1503,564],{"class":563},[111,1505,567],{"class":125},[111,1507,213],{"class":145},[111,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":113,"line":1510},53,[111,1512,1368],{"class":117},[111,1514,576],{"class":138},[111,1516,142],{"class":125},[111,1518,509],{"class":159},[111,1520,1474],{"class":138},[111,1522,198],{"class":125},[111,1524,587],{"class":121},[111,1526,169],{"class":145},[111,1528,1529],{"class":125},"`",[111,1531,1532],{"class":175},"thumb_",[111,1534,1535],{"class":125},"${",[111,1537,1538],{"class":121},"String",[111,1540,1541],{"class":138},"(currentSecond)",[111,1543,198],{"class":125},[111,1545,1546],{"class":121},"padStart",[111,1548,169],{"class":138},[111,1550,1551],{"class":420},"6",[111,1553,448],{"class":125},[111,1555,819],{"class":125},[111,1557,1558],{"class":175},"0",[111,1560,172],{"class":125},[111,1562,495],{"class":138},[111,1564,1565],{"class":125},"}",[111,1567,1568],{"class":175},".jpg",[111,1570,1529],{"class":125},[111,1572,448],{"class":125},[111,1574,554],{"class":125},[111,1576,557],{"class":145},[111,1578,560],{"class":125},[111,1580,564],{"class":563},[111,1582,567],{"class":125},[111,1584,213],{"class":145},[111,1586,1588,1590,1593,1595,1597,1599,1601,1604],{"class":113,"line":1587},54,[111,1589,1368],{"class":117},[111,1591,1592],{"class":138}," syncHandle",[111,1594,142],{"class":125},[111,1596,509],{"class":159},[111,1598,576],{"class":138},[111,1600,198],{"class":125},[111,1602,1603],{"class":121},"createSyncAccessHandle",[111,1605,400],{"class":145},[111,1607,1609,1612,1614,1617,1619,1622,1625,1627,1630],{"class":113,"line":1608},55,[111,1610,1611],{"class":138},"    syncHandle",[111,1613,198],{"class":125},[111,1615,1616],{"class":121},"write",[111,1618,169],{"class":145},[111,1620,1621],{"class":125},"new",[111,1623,1624],{"class":121}," Uint8Array",[111,1626,169],{"class":145},[111,1628,1629],{"class":138},"buffer",[111,1631,1632],{"class":145},"))\n",[111,1634,1636,1638,1640,1643],{"class":113,"line":1635},56,[111,1637,1611],{"class":138},[111,1639,198],{"class":125},[111,1641,1642],{"class":121},"close",[111,1644,400],{"class":145},[111,1646,1648],{"class":113,"line":1647},57,[111,1649,153],{"emptyLinePlaceholder":152},[111,1651,1653,1656],{"class":113,"line":1652},58,[111,1654,1655],{"class":138},"    frameCount",[111,1657,1658],{"class":125},"++\n",[111,1660,1662,1665,1667,1669,1671,1673,1675,1677,1679,1682,1684,1686,1689,1691,1694,1696,1698,1700],{"class":113,"line":1661},59,[111,1663,1664],{"class":138},"    self",[111,1666,198],{"class":125},[111,1668,97],{"class":121},[111,1670,169],{"class":145},[111,1672,1144],{"class":125},[111,1674,783],{"class":145},[111,1676,560],{"class":125},[111,1678,819],{"class":125},[111,1680,1681],{"class":175},"progress",[111,1683,172],{"class":125},[111,1685,448],{"class":125},[111,1687,1688],{"class":138}," frameCount",[111,1690,448],{"class":125},[111,1692,1693],{"class":145}," second",[111,1695,560],{"class":125},[111,1697,1266],{"class":138},[111,1699,567],{"class":125},[111,1701,213],{"class":145},[111,1703,1705],{"class":113,"line":1704},60,[111,1706,219],{"class":125},[111,1708,1710],{"class":113,"line":1709},61,[111,1711,153],{"emptyLinePlaceholder":152},[111,1713,1715,1718,1720,1722,1725],{"class":113,"line":1714},62,[111,1716,1717],{"class":138},"  frame",[111,1719,198],{"class":125},[111,1721,1642],{"class":121},[111,1723,1724],{"class":145},"()  ",[111,1726,1727],{"class":351},"\u002F\u002F 重要：VideoFrame は必ず閉じる。さもないと GPU メモリがリークする\n",[111,1729,1731],{"class":113,"line":1730},63,[111,1732,378],{"class":125},[111,1734,1736],{"class":113,"line":1735},64,[111,1737,153],{"emptyLinePlaceholder":152},[111,1739,1741,1743,1745,1747,1749,1752,1754],{"class":113,"line":1740},65,[111,1742,480],{"class":117},[111,1744,483],{"class":117},[111,1746,878],{"class":121},[111,1748,907],{"class":125},[111,1750,1751],{"class":491}," chunks",[111,1753,923],{"class":125},[111,1755,129],{"class":125},[111,1757,1759,1762,1764,1766,1769,1772,1774,1776],{"class":113,"line":1758},66,[111,1760,1761],{"class":159},"  for",[111,1763,163],{"class":145},[111,1765,389],{"class":117},[111,1767,1768],{"class":138}," chunk",[111,1770,1771],{"class":125}," of",[111,1773,1751],{"class":138},[111,1775,257],{"class":145},[111,1777,189],{"class":125},[111,1779,1781,1784,1786,1789,1791,1793,1796,1798],{"class":113,"line":1780},67,[111,1782,1783],{"class":138},"    decoder",[111,1785,198],{"class":125},[111,1787,1788],{"class":121},"decode",[111,1790,169],{"class":145},[111,1792,1621],{"class":125},[111,1794,1795],{"class":121}," EncodedVideoChunk",[111,1797,169],{"class":145},[111,1799,189],{"class":125},[111,1801,1803,1806,1808,1810,1812,1815,1818,1820,1823,1825,1828,1830,1833,1835],{"class":113,"line":1802},68,[111,1804,1805],{"class":145},"      type",[111,1807,560],{"class":125},[111,1809,1768],{"class":138},[111,1811,198],{"class":125},[111,1813,1814],{"class":138},"keyframe",[111,1816,1817],{"class":125}," ?",[111,1819,819],{"class":125},[111,1821,1822],{"class":175},"key",[111,1824,172],{"class":125},[111,1826,1827],{"class":125}," :",[111,1829,819],{"class":125},[111,1831,1832],{"class":175},"delta",[111,1834,172],{"class":125},[111,1836,1033],{"class":125},[111,1838,1840,1843,1845,1847,1849,1851],{"class":113,"line":1839},69,[111,1841,1842],{"class":145},"      timestamp",[111,1844,560],{"class":125},[111,1846,1768],{"class":138},[111,1848,198],{"class":125},[111,1850,1238],{"class":138},[111,1852,1033],{"class":125},[111,1854,1856,1859,1861,1863,1865],{"class":113,"line":1855},70,[111,1857,1858],{"class":145},"      data",[111,1860,560],{"class":125},[111,1862,1768],{"class":138},[111,1864,198],{"class":125},[111,1866,800],{"class":138},[111,1868,1870,1873],{"class":113,"line":1869},71,[111,1871,1872],{"class":125},"    }",[111,1874,1632],{"class":145},[111,1876,1878],{"class":113,"line":1877},72,[111,1879,219],{"class":125},[111,1881,1883,1885,1888,1890,1893],{"class":113,"line":1882},73,[111,1884,635],{"class":159},[111,1886,1887],{"class":138}," decoder",[111,1889,198],{"class":125},[111,1891,1892],{"class":121},"flush",[111,1894,400],{"class":145},[111,1896,1898,1900,1902,1904,1906,1908,1910,1912,1914,1917,1919,1921,1923,1925],{"class":113,"line":1897},74,[111,1899,1135],{"class":138},[111,1901,198],{"class":125},[111,1903,97],{"class":121},[111,1905,169],{"class":145},[111,1907,1144],{"class":125},[111,1909,783],{"class":145},[111,1911,560],{"class":125},[111,1913,819],{"class":125},[111,1915,1916],{"class":175},"done",[111,1918,172],{"class":125},[111,1920,448],{"class":125},[111,1922,1688],{"class":138},[111,1924,567],{"class":125},[111,1926,213],{"class":145},[111,1928,1930],{"class":113,"line":1929},75,[111,1931,378],{"class":125},[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],{},"2 つの道：",[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],{}," を使ったコード：",[83,2007,2009],{"className":105,"code":2008,"language":107,"meta":91,"style":91},"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":91},[111,2012,2013,2016,2019,2022,2024,2027],{"class":113,"line":114},[111,2014,2015],{"class":159},"import",[111,2017,2018],{"class":138}," mp4box ",[111,2020,2021],{"class":159},"from",[111,2023,819],{"class":125},[111,2025,2026],{"class":175},"mp4box",[111,2028,2029],{"class":125},"'\n",[111,2031,2032],{"class":113,"line":132},[111,2033,153],{"emptyLinePlaceholder":152},[111,2035,2036,2038,2040,2043,2045,2047,2049],{"class":113,"line":149},[111,2037,480],{"class":117},[111,2039,483],{"class":117},[111,2041,2042],{"class":121}," getCodecChunks",[111,2044,169],{"class":125},[111,2046,492],{"class":491},[111,2048,495],{"class":125},[111,2050,129],{"class":125},[111,2052,2053,2055,2058,2060,2062,2064,2066,2068],{"class":113,"line":156},[111,2054,135],{"class":117},[111,2056,2057],{"class":138}," arrayBuffer",[111,2059,142],{"class":125},[111,2061,509],{"class":159},[111,2063,638],{"class":138},[111,2065,198],{"class":125},[111,2067,1430],{"class":121},[111,2069,400],{"class":145},[111,2071,2072,2075,2077,2080,2082,2084],{"class":113,"line":192},[111,2073,2074],{"class":138},"  arrayBuffer",[111,2076,198],{"class":125},[111,2078,2079],{"class":138},"fileStart",[111,2081,142],{"class":125},[111,2083,421],{"class":420},[111,2085,2086],{"class":351},"  \u002F\u002F mp4box.js の慣習\n",[111,2088,2089],{"class":113,"line":216},[111,2090,153],{"emptyLinePlaceholder":152},[111,2092,2093,2095,2098,2100,2103,2105,2108],{"class":113,"line":222},[111,2094,135],{"class":117},[111,2096,2097],{"class":138}," mp4boxFile",[111,2099,142],{"class":125},[111,2101,2102],{"class":138}," mp4box",[111,2104,198],{"class":125},[111,2106,2107],{"class":121},"createFile",[111,2109,400],{"class":145},[111,2111,2112,2115,2117,2120,2122,2124,2127,2129,2131],{"class":113,"line":227},[111,2113,2114],{"class":138},"  mp4boxFile",[111,2116,198],{"class":125},[111,2118,2119],{"class":121},"onReady",[111,2121,142],{"class":125},[111,2123,163],{"class":125},[111,2125,2126],{"class":491},"info",[111,2128,495],{"class":125},[111,2130,772],{"class":117},[111,2132,129],{"class":125},[111,2134,2135,2138,2140,2143,2145,2147,2149,2152,2155,2157,2160,2162,2165,2167,2170],{"class":113,"line":262},[111,2136,2137],{"class":138},"    mp4boxFile",[111,2139,198],{"class":125},[111,2141,2142],{"class":121},"setExtractionOptions",[111,2144,169],{"class":145},[111,2146,2126],{"class":138},[111,2148,198],{"class":125},[111,2150,2151],{"class":138},"videoTracks",[111,2153,2154],{"class":145},"[",[111,2156,1558],{"class":420},[111,2158,2159],{"class":145},"]",[111,2161,198],{"class":125},[111,2163,2164],{"class":138},"id",[111,2166,448],{"class":125},[111,2168,2169],{"class":125}," null,",[111,2171,129],{"class":125},[111,2173,2174,2177,2179,2182],{"class":113,"line":282},[111,2175,2176],{"class":145},"      nbSamples",[111,2178,560],{"class":125},[111,2180,2181],{"class":420}," 100",[111,2183,2184],{"class":351},"  \u002F\u002F バッチサイズ\n",[111,2186,2187,2189],{"class":113,"line":287},[111,2188,1872],{"class":125},[111,2190,213],{"class":145},[111,2192,2193,2195,2197,2200],{"class":113,"line":292},[111,2194,2137],{"class":138},[111,2196,198],{"class":125},[111,2198,2199],{"class":121},"start",[111,2201,400],{"class":145},[111,2203,2204],{"class":113,"line":318},[111,2205,219],{"class":125},[111,2207,2208],{"class":113,"line":338},[111,2209,153],{"emptyLinePlaceholder":152},[111,2211,2212,2214,2216,2218],{"class":113,"line":343},[111,2213,135],{"class":117},[111,2215,1751],{"class":138},[111,2217,142],{"class":125},[111,2219,146],{"class":145},[111,2221,2222,2224,2226,2229,2231,2233,2236,2238,2241,2243,2246,2248,2250],{"class":113,"line":348},[111,2223,2114],{"class":138},[111,2225,198],{"class":125},[111,2227,2228],{"class":121},"onSamples",[111,2230,142],{"class":125},[111,2232,163],{"class":125},[111,2234,2235],{"class":491},"trackId",[111,2237,448],{"class":125},[111,2239,2240],{"class":491}," ref",[111,2242,448],{"class":125},[111,2244,2245],{"class":491}," samples",[111,2247,495],{"class":125},[111,2249,772],{"class":117},[111,2251,129],{"class":125},[111,2253,2254,2257,2259,2261,2264,2266,2268,2270],{"class":113,"line":355},[111,2255,2256],{"class":159},"    for",[111,2258,163],{"class":145},[111,2260,389],{"class":117},[111,2262,2263],{"class":138}," sample",[111,2265,1771],{"class":125},[111,2267,2245],{"class":138},[111,2269,257],{"class":145},[111,2271,189],{"class":125},[111,2273,2274,2277,2279,2281,2283],{"class":113,"line":361},[111,2275,2276],{"class":138},"      chunks",[111,2278,198],{"class":125},[111,2280,201],{"class":121},[111,2282,169],{"class":145},[111,2284,189],{"class":125},[111,2286,2287,2290,2292,2294,2296,2299],{"class":113,"line":366},[111,2288,2289],{"class":145},"        keyframe",[111,2291,560],{"class":125},[111,2293,2263],{"class":138},[111,2295,198],{"class":125},[111,2297,2298],{"class":138},"is_sync",[111,2300,1033],{"class":125},[111,2302,2303,2306,2308,2310,2312,2315,2318,2320,2322,2324,2326,2329],{"class":113,"line":375},[111,2304,2305],{"class":145},"        timestamp",[111,2307,560],{"class":125},[111,2309,2263],{"class":138},[111,2311,198],{"class":125},[111,2313,2314],{"class":138},"cts",[111,2316,2317],{"class":125}," *",[111,2319,1244],{"class":420},[111,2321,1241],{"class":125},[111,2323,2263],{"class":138},[111,2325,198],{"class":125},[111,2327,2328],{"class":138},"timescale",[111,2330,1033],{"class":125},[111,2332,2333,2336,2338,2340,2342],{"class":113,"line":381},[111,2334,2335],{"class":145},"        data",[111,2337,560],{"class":125},[111,2339,2263],{"class":138},[111,2341,198],{"class":125},[111,2343,800],{"class":138},[111,2345,2346,2349],{"class":113,"line":386},[111,2347,2348],{"class":125},"      }",[111,2350,213],{"class":145},[111,2352,2353],{"class":113,"line":403},[111,2354,2355],{"class":125},"    }\n",[111,2357,2358],{"class":113,"line":428},[111,2359,219],{"class":125},[111,2361,2362],{"class":113,"line":455},[111,2363,153],{"emptyLinePlaceholder":152},[111,2365,2366,2368,2370,2373,2375,2377],{"class":113,"line":461},[111,2367,2114],{"class":138},[111,2369,198],{"class":125},[111,2371,2372],{"class":121},"appendBuffer",[111,2374,169],{"class":145},[111,2376,1430],{"class":138},[111,2378,213],{"class":145},[111,2380,2381,2383,2385,2387],{"class":113,"line":1084},[111,2382,2114],{"class":138},[111,2384,198],{"class":125},[111,2386,1892],{"class":121},[111,2388,400],{"class":145},[111,2390,2391],{"class":113,"line":1098},[111,2392,153],{"emptyLinePlaceholder":152},[111,2394,2395,2397],{"class":113,"line":1109},[111,2396,369],{"class":159},[111,2398,129],{"class":125},[111,2400,2401,2403,2405,2407,2409,2412,2414,2416,2418,2420,2422,2424,2426,2429],{"class":113,"line":1120},[111,2402,1101],{"class":145},[111,2404,560],{"class":125},[111,2406,2097],{"class":138},[111,2408,198],{"class":125},[111,2410,2411],{"class":121},"getInfo",[111,2413,126],{"class":145},[111,2415,198],{"class":125},[111,2417,2151],{"class":138},[111,2419,2154],{"class":145},[111,2421,1558],{"class":420},[111,2423,2159],{"class":145},[111,2425,198],{"class":125},[111,2427,2428],{"class":138},"codec",[111,2430,1033],{"class":125},[111,2432,2433],{"class":113,"line":1127},[111,2434,2435],{"class":138},"    chunks\n",[111,2437,2438],{"class":113,"line":1132},[111,2439,219],{"class":125},[111,2441,2442],{"class":113,"line":1162},[111,2443,378],{"class":125},[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],{},"複数の動画をバッチ処理する（ディレクトリ全体のサムネイルを作るなど）場合は、スレッドプールを使う：",[83,2690,2692],{"className":105,"code":2691,"language":107,"meta":91,"style":91},"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":91},[111,2695,2696,2699,2703],{"class":113,"line":114},[111,2697,2698],{"class":117},"class",[111,2700,2702],{"class":2701},"sBMFI"," WorkerPool",[111,2704,129],{"class":125},[111,2706,2707,2710,2712,2715,2717,2720,2722,2724,2726,2729,2732,2735,2737],{"class":113,"line":132},[111,2708,2709],{"class":117},"  constructor",[111,2711,169],{"class":125},[111,2713,2714],{"class":491},"workerUrl",[111,2716,448],{"class":125},[111,2718,2719],{"class":491}," size",[111,2721,142],{"class":125},[111,2723,512],{"class":138},[111,2725,198],{"class":125},[111,2727,2728],{"class":138},"hardwareConcurrency ",[111,2730,2731],{"class":125},"||",[111,2733,2734],{"class":420}," 4",[111,2736,495],{"class":125},[111,2738,129],{"class":125},[111,2740,2741,2744,2747,2749,2752,2754,2756,2758,2760,2763,2765,2767,2770,2773,2775,2777,2780,2782,2784],{"class":113,"line":149},[111,2742,2743],{"class":125},"    this.",[111,2745,2746],{"class":138},"workers",[111,2748,142],{"class":125},[111,2750,2751],{"class":138}," Array",[111,2753,198],{"class":125},[111,2755,2021],{"class":121},[111,2757,169],{"class":145},[111,2759,1144],{"class":125},[111,2761,2762],{"class":145}," length",[111,2764,560],{"class":125},[111,2766,2719],{"class":138},[111,2768,2769],{"class":125}," },",[111,2771,2772],{"class":125}," ()",[111,2774,772],{"class":117},[111,2776,940],{"class":125},[111,2778,2779],{"class":121}," Worker",[111,2781,169],{"class":145},[111,2783,2714],{"class":138},[111,2785,1632],{"class":145},[111,2787,2788,2790,2793,2795],{"class":113,"line":156},[111,2789,2743],{"class":125},[111,2791,2792],{"class":138},"queue",[111,2794,142],{"class":125},[111,2796,146],{"class":145},[111,2798,2799,2801,2804,2806,2808,2811],{"class":113,"line":192},[111,2800,2743],{"class":125},[111,2802,2803],{"class":138},"busy",[111,2805,142],{"class":125},[111,2807,940],{"class":125},[111,2809,2810],{"class":121}," Set",[111,2812,400],{"class":145},[111,2814,2815],{"class":113,"line":216},[111,2816,219],{"class":125},[111,2818,2819],{"class":113,"line":222},[111,2820,153],{"emptyLinePlaceholder":152},[111,2822,2823,2826,2829,2831,2834,2836],{"class":113,"line":227},[111,2824,2825],{"class":117},"  async",[111,2827,2828],{"class":145}," run",[111,2830,169],{"class":125},[111,2832,2833],{"class":491},"task",[111,2835,495],{"class":125},[111,2837,129],{"class":125},[111,2839,2840,2842,2845,2847,2849,2852,2855],{"class":113,"line":262},[111,2841,1368],{"class":117},[111,2843,2844],{"class":138}," worker",[111,2846,142],{"class":125},[111,2848,509],{"class":159},[111,2850,2851],{"class":125}," this.",[111,2853,2854],{"class":121},"getIdle",[111,2856,400],{"class":145},[111,2858,2859,2861,2863,2865,2868,2870,2873],{"class":113,"line":282},[111,2860,2743],{"class":125},[111,2862,2803],{"class":138},[111,2864,198],{"class":125},[111,2866,2867],{"class":121},"add",[111,2869,169],{"class":145},[111,2871,2872],{"class":138},"worker",[111,2874,213],{"class":145},[111,2876,2877],{"class":113,"line":287},[111,2878,153],{"emptyLinePlaceholder":152},[111,2880,2881,2884,2886,2889,2891,2893,2896,2898,2901,2903,2905],{"class":113,"line":292},[111,2882,2883],{"class":159},"    return",[111,2885,940],{"class":125},[111,2887,2888],{"class":2701}," Promise",[111,2890,169],{"class":145},[111,2892,169],{"class":125},[111,2894,2895],{"class":491},"resolve",[111,2897,448],{"class":125},[111,2899,2900],{"class":491}," reject",[111,2902,495],{"class":125},[111,2904,772],{"class":117},[111,2906,129],{"class":125},[111,2908,2909,2912,2914,2916,2918,2920,2922,2924,2926],{"class":113,"line":318},[111,2910,2911],{"class":138},"      worker",[111,2913,198],{"class":125},[111,2915,757],{"class":121},[111,2917,142],{"class":125},[111,2919,163],{"class":125},[111,2921,767],{"class":491},[111,2923,495],{"class":125},[111,2925,772],{"class":117},[111,2927,129],{"class":125},[111,2929,2930,2933,2935,2937,2940,2942,2944],{"class":113,"line":338},[111,2931,2932],{"class":125},"        this.",[111,2934,2803],{"class":138},[111,2936,198],{"class":125},[111,2938,2939],{"class":121},"delete",[111,2941,169],{"class":145},[111,2943,2872],{"class":138},[111,2945,213],{"class":145},[111,2947,2948,2951,2953,2955,2957,2959,2961,2963,2965,2967,2969,2971,2973,2975,2977,2979,2981,2983,2985,2988],{"class":113,"line":343},[111,2949,2950],{"class":159},"        if",[111,2952,163],{"class":145},[111,2954,767],{"class":138},[111,2956,198],{"class":125},[111,2958,841],{"class":138},[111,2960,198],{"class":125},[111,2962,813],{"class":138},[111,2964,816],{"class":125},[111,2966,819],{"class":125},[111,2968,1916],{"class":175},[111,2970,172],{"class":125},[111,2972,257],{"class":145},[111,2974,2895],{"class":121},[111,2976,169],{"class":145},[111,2978,767],{"class":138},[111,2980,198],{"class":125},[111,2982,841],{"class":138},[111,2984,198],{"class":125},[111,2986,2987],{"class":138},"result",[111,2989,213],{"class":145},[111,2991,2992,2995,2997,2999,3001,3003,3005,3007,3009,3011,3013,3015,3017,3019,3022,3024,3026,3028,3030,3032,3034],{"class":113,"line":348},[111,2993,2994],{"class":159},"        else",[111,2996,854],{"class":159},[111,2998,163],{"class":145},[111,3000,767],{"class":138},[111,3002,198],{"class":125},[111,3004,841],{"class":138},[111,3006,198],{"class":125},[111,3008,813],{"class":138},[111,3010,816],{"class":125},[111,3012,819],{"class":125},[111,3014,1056],{"class":175},[111,3016,172],{"class":125},[111,3018,257],{"class":145},[111,3020,3021],{"class":121},"reject",[111,3023,169],{"class":145},[111,3025,767],{"class":138},[111,3027,198],{"class":125},[111,3029,841],{"class":138},[111,3031,198],{"class":125},[111,3033,1056],{"class":138},[111,3035,213],{"class":145},[111,3037,3038],{"class":113,"line":355},[111,3039,3040],{"class":125},"      }\n",[111,3042,3043,3045,3047,3049,3051,3053],{"class":113,"line":361},[111,3044,2911],{"class":138},[111,3046,198],{"class":125},[111,3048,97],{"class":121},[111,3050,169],{"class":145},[111,3052,2833],{"class":138},[111,3054,213],{"class":145},[111,3056,3057,3059],{"class":113,"line":366},[111,3058,1872],{"class":125},[111,3060,213],{"class":145},[111,3062,3063],{"class":113,"line":375},[111,3064,219],{"class":125},[111,3066,3067],{"class":113,"line":381},[111,3068,153],{"emptyLinePlaceholder":152},[111,3070,3071,3073,3076,3078],{"class":113,"line":386},[111,3072,2825],{"class":117},[111,3074,3075],{"class":145}," getIdle",[111,3077,126],{"class":125},[111,3079,129],{"class":125},[111,3081,3082,3085,3087,3090,3092,3094,3097,3100,3102,3104,3106,3109,3111],{"class":113,"line":403},[111,3083,3084],{"class":159},"    if",[111,3086,163],{"class":145},[111,3088,3089],{"class":125},"this.",[111,3091,2803],{"class":138},[111,3093,198],{"class":125},[111,3095,3096],{"class":138},"size",[111,3098,3099],{"class":125}," \u003C",[111,3101,2851],{"class":125},[111,3103,2746],{"class":138},[111,3105,198],{"class":125},[111,3107,3108],{"class":138},"length",[111,3110,257],{"class":145},[111,3112,189],{"class":125},[111,3114,3115,3118,3120,3122,3124,3127,3129,3132,3134,3137,3139,3141,3144,3146,3148],{"class":113,"line":428},[111,3116,3117],{"class":159},"      return",[111,3119,2851],{"class":125},[111,3121,2746],{"class":138},[111,3123,198],{"class":125},[111,3125,3126],{"class":121},"find",[111,3128,169],{"class":145},[111,3130,3131],{"class":491},"w",[111,3133,772],{"class":117},[111,3135,3136],{"class":125}," !this.",[111,3138,2803],{"class":138},[111,3140,198],{"class":125},[111,3142,3143],{"class":121},"has",[111,3145,169],{"class":145},[111,3147,3131],{"class":138},[111,3149,1632],{"class":145},[111,3151,3152],{"class":113,"line":455},[111,3153,2355],{"class":125},[111,3155,3156],{"class":113,"line":461},[111,3157,3158],{"class":351},"    \u002F\u002F Worker が空くまで待つ\n",[111,3160,3161,3163,3165,3167,3169,3171,3173],{"class":113,"line":1084},[111,3162,2883],{"class":159},[111,3164,940],{"class":125},[111,3166,2888],{"class":2701},[111,3168,169],{"class":145},[111,3170,2895],{"class":491},[111,3172,772],{"class":117},[111,3174,129],{"class":125},[111,3176,3177,3180,3182,3184,3186,3188,3190],{"class":113,"line":1098},[111,3178,3179],{"class":125},"      this.",[111,3181,2792],{"class":138},[111,3183,198],{"class":125},[111,3185,201],{"class":121},[111,3187,169],{"class":145},[111,3189,2895],{"class":138},[111,3191,213],{"class":145},[111,3193,3194,3196],{"class":113,"line":1109},[111,3195,1872],{"class":125},[111,3197,213],{"class":145},[111,3199,3200],{"class":113,"line":1120},[111,3201,219],{"class":125},[111,3203,3204],{"class":113,"line":1127},[111,3205,378],{"class":125},[11,3207,3208],{},"8 コアマシンで 8 つの Worker を立てれば、8 ファイルを並列処理——それぞれがスレッドを独占し、それぞれがハードウェアアクセラレーションでデコードする。手元の M2 MacBook で実測：シングルコア 240 フレーム\u002F秒、全体で約 1900 フレーム\u002F秒。毎秒 30 分の動画を処理する計算。",[11,3210,3211],{},"ただし落とし穴：ハードウェアデコーダの並発数には限界がある。多くの GPU は同時に 2-4 ストリームしかデコードできない。それを超えるとソフトウェアデコードにフォールバックして逆に遅くなる。ターゲットハードウェアでテストすること。",[18,3213,3214],{"id":3214},"実際に踏んだ抜け",[11,3216,3217,3223,3224,3226],{},[25,3218,3219,3220,3222],{},"抜け 1：",[30,3221,1945],{}," の忘れ。"," 閉じていない VideoFrame は VRAM をリークする。100 フレームくらいリークするとデコーダが黙って失敗する。",[30,3225,1950],{}," で：",[83,3228,3230],{"className":105,"code":3229,"language":107,"meta":91,"style":91},"try {\n  \u002F\u002F ... frame を使って処理 ...\n} finally {\n  frame.close()\n}\n",[30,3231,3232,3239,3244,3253,3263],{"__ignoreMap":91},[111,3233,3234,3237],{"class":113,"line":114},[111,3235,3236],{"class":159},"try",[111,3238,129],{"class":125},[111,3240,3241],{"class":113,"line":132},[111,3242,3243],{"class":351},"  \u002F\u002F ... frame を使って処理 ...\n",[111,3245,3246,3248,3251],{"class":113,"line":149},[111,3247,1565],{"class":125},[111,3249,3250],{"class":159}," finally",[111,3252,129],{"class":125},[111,3254,3255,3257,3259,3261],{"class":113,"line":156},[111,3256,1717],{"class":138},[111,3258,198],{"class":125},[111,3260,1642],{"class":121},[111,3262,400],{"class":145},[111,3264,3265],{"class":113,"line":192},[111,3266,378],{"class":125},[11,3268,3269,3272,3273,3276],{},[25,3270,3271],{},"抜け 2：デコード順序 ≠ 表示順序。"," B フレームのある H.264 は DTS 順にデコードし、PTS 順に表示する。デコード順にフレームを処理すると、サムネイルが順不同になる。WebCodecs は両方を返す——",[30,3274,3275],{},"frame.timestamp"," は PTS、デコードは DTS で行われる。",[11,3278,3279,3282,3283,3286,3287,33,3289,33,3292,3295],{},[25,3280,3281],{},"抜け 3：codec 文字列のフォーマット。"," WebCodecs は ",[30,3284,3285],{},"'avc1.640028'","（constraint バイト込み）を要求する。MP4 には ",[30,3288,3285],{},[30,3290,3291],{},"'avc1.64.0028'",[30,3293,3294],{},"'avc1.42E01E'"," などで格納されていることがある。デコーダを設定する前に正規化する。",[11,3297,3298,3301,3302,3304,3305,3308,3309,3312],{},[25,3299,3300],{},"抜け 4：Worker モジュールのロード。"," Worker はどのブラウザでも任意の URL から ",[30,3303,2015],{}," できるわけではない。",[30,3306,3307],{},"importScripts()"," を使うか、Worker コンストラクタで ",[30,3310,3311],{},"\"type\": \"module\""," を指定する（Chrome 80+、Safari 15+）。",[11,3314,3315,3318,3319,3322,3323,3326],{},[25,3316,3317],{},"抜け 5：OPFS のクォータ。"," ブラウザは OPFS をディスクの空き容量のパーセンテージに制限する。5GB の書き込みがディスク一杯で失敗することがある。",[30,3320,3321],{},"QuotaExceededError"," を catch して、",[30,3324,3325],{},"showSaveFilePicker()"," のストリーミング書き出しにフォールバックする。",[18,3328,3329],{"id":3329},"何に何を使うか",[11,3331,3332],{},[25,3333,3334],{},"FFmpeg WASM を使う場面：",[2668,3336,3337,3340,3343,3346],{},[1939,3338,3339],{},"あらゆる codec に対応したい（HEVC、VP9、AV1、MPEG-TS、MKV、FLV）",[1939,3341,3342],{},"コンテナ変換をしたい（TS → MP4、MP4 → WebM）",[1939,3344,3345],{},"フィルタチェーンが要る（オーバーレイ、スケール、クロップ、結合）",[1939,3347,3348],{},"信頼性が速度より重要",[11,3350,3351],{},[25,3352,3353],{},"WebCodecs を使う場面：",[2668,3355,3356,3359,3362,3365],{},[1939,3357,3358],{},"codec が H.264\u002FH.265\u002FVP9\u002FAV1 と分かっている",[1939,3360,3361],{},"フレーム単位の操作をする（サムネイル、モーション検出、フレーム抽出）",[1939,3363,3364],{},"リアルタイム性能が要る（ライブ動画のフィルタリング、フレーム分析）",[1939,3366,3367],{},"メモリ予算が厳しい（25MB の WASM をダウンロードしたくない）",[11,3369,3370],{},[25,3371,3372],{},"Web Workers を使う場面：",[2668,3374,3375,3378],{},[1939,3376,3377],{},"CPU 重い処理があって、メインスレッドをブロックしたくない",[1939,3379,3380],{},"独立した単位（ファイル、セグメント）を並列処理したい",[11,3382,3383],{},[25,3384,3385],{},"OPFS を使う場面：",[2668,3387,3388,3391,3394],{},[1939,3389,3390],{},"5MB（localStorage の上限）を超える",[1939,3392,3393],{},"中間状態を永続化したい（リロードごとに再デコードしたくない）",[1939,3395,3396],{},"大きなファイルをストリーミング処理したい。メモリに全部載せたくない",[18,3398,3399],{"id":3399},"参考資料",[2668,3401,3402,3410,3418,3426,3434],{},[1939,3403,3404,3409],{},[1987,3405,3408],{"href":3406,"rel":3407},"https:\u002F\u002Fwww.w3.org\u002FTR\u002Fwebcodecs\u002F",[1991],"WebCodecs 仕様","——実際の仕様。意外と読みやすい",[1939,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","——実用的なドキュメント、サンプル付き",[1939,3419,3420,3425],{},[1987,3421,3424],{"href":3422,"rel":3423},"https:\u002F\u002Fgithub.com\u002Fw3c\u002Fwebcodecs\u002Ftree\u002Fmain\u002Fsamples",[1991],"WebCodecs samples リポジトリ","——公式の動くサンプル",[1939,3427,3428,3433],{},[1987,3429,3432],{"href":3430,"rel":3431},"https:\u002F\u002Ffs.spec.whatwg.org\u002F",[1991],"OPFS 仕様","——File System Access API、OPFS を含む",[1939,3435,3436,3440],{},[1987,3437,1984],{"href":3438,"rel":3439},"https:\u002F\u002Fgithub.com\u002Fgpac\u002Fmp4box.js",[1991],"——GPAC の JS MP4 パーサー、WebCodecs への入力の事実上の標準",[18,3442,3443],{"id":3443},"まとめ",[11,3445,3446,3447,3451],{},"WebCodecs + Workers + OPFS はパフォーマンス重視のシナリオで FFmpeg WASM の真の代替になる。完全な代替ではない——FFmpeg WASM は codec の幅とエッジケースで勝つ——が、codec が分かっている、ハードウェアアクセラレーション、フレームレベルの処理ワークロードでは、WebCodecs は 4-5 倍速く 250 倍小さい。次は ",[1987,3448,3450],{"href":3449},"\u002Fja\u002Fblog\u002Fis-it-legal-to-download-streaming-video","ストリーミングのダウンロードは合法か","。",[11,3453,3454,3455,3459],{},"完全な codec、解像度、ファイルサイズでのベンチマークデータは ",[1987,3456,3458],{"href":3457},"\u002Fja\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","ブラウザ動画処理パフォーマンスベンチマーク"," で。",[3461,3462],"hr",{},[18,3464,3465],{"id":3465},"おすすめ記事",[2668,3467,3468,3477,3484,3493,3502,3511],{},[1939,3469,3470,3476],{},[25,3471,3472],{},[1987,3473,3475],{"href":3474},"\u002Fja\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff","ブラウザでの動画リマックス：fMP4、ISOBMFF、トランスコードしない理由","——この記事が codec 層、あちらがコンテナ層。補完関係",[1939,3478,3479,3483],{},[25,3480,3481],{},[1987,3482,3458],{"href":3457},"——この記事のデータの完全版",[1939,3485,3486,3492],{},[25,3487,3488],{},[1987,3489,3491],{"href":3490},"\u002Fja\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","FlowPick はどうやってブラウザだけで数百の動画セグメントを 1 つの MP4 にまとめるのか","——FlowPick がどこで FFmpeg WASM を使い、どこで WebCodecs を使うか",[1939,3494,3495,3501],{},[25,3496,3497],{},[1987,3498,3500],{"href":3499},"\u002Fja\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH ディープ：SegmentTemplate、ContentProtection、視点切り替え MPD","——WebCodecs に渡す chunk がどこから来るか",[1939,3503,3504,3510],{},[25,3505,3506],{},[1987,3507,3509],{"href":3508},"\u002Fja\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack","HLS ディープ：暗号化、マルチトラック、覚えておくべき EXT-X タグ","——セグメントの供給元",[1939,3512,3513,3519],{},[25,3514,3515],{},[1987,3516,3518],{"href":3517},"\u002Fja\u002Fblog\u002Fhow-i-built-flowpick-in-three-months","3 カ月で FlowPick を作った全過程","——開発ストーリー。WebCodecs vs FFmpeg WASM の決断も収録",[3521,3522,3523],"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":91,"searchDepth":132,"depth":132,"links":3525},[3526,3527,3528,3529,3530,3531,3532,3533,3534,3535,3536,3537,3538,3539],{"id":20,"depth":132,"text":21},{"id":78,"depth":132,"text":78},{"id":101,"depth":132,"text":102},{"id":469,"depth":132,"text":470},{"id":697,"depth":132,"text":698},{"id":1968,"depth":132,"text":1969},{"id":2449,"depth":132,"text":2450},{"id":2577,"depth":132,"text":2578},{"id":2684,"depth":132,"text":2685},{"id":3214,"depth":132,"text":3214},{"id":3329,"depth":132,"text":3329},{"id":3399,"depth":132,"text":3399},{"id":3443,"depth":132,"text":3443},{"id":3465,"depth":132,"text":3465},"tips","2026-08-04","WebCodecs でハードウェアデコード、Web Workers でマルチスレッド、OPFS でローカルファイル I\u002FO を高スループット化——FFmpeg WASM との実測比較付き。","md","\u002Fscreenshots\u002Fformat-conversion.png",{},"\u002Fja\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing",{"title":5,"description":3542},"ja\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing",[3550,3551,3552,3553,3554,3555],"webcodecs","web workers","opfs","パフォーマンス","wasm","ディープ","6vDBwnyEk_C4k5qyJFlTALckmE2zlBxknd7rzcVwiNM",[3558,3562],{"title":3559,"path":3449,"stem":3560,"description":3561,"date":3541,"category":3540,"children":-1},"ストリーミングのダウンロードは合法か？技術と法律のチェックリスト","ja\u002Fblog\u002Fis-it-legal-to-download-streaming-video","「弁護士に相談を」的な曖昧な話ではない——Network パネルで実際に確認できる信号、主要司法管轄区の実際の法律、そして FlowPick がどこで線を引くか。",{"title":3563,"path":3564,"stem":3565,"description":3566,"date":3567,"category":3540,"children":-1},"DASHストリーミングとは？非開発者向けMPDファイル解析","\u002Fja\u002Fblog\u002Fwhat-is-dash-mpd-streaming","ja\u002Fblog\u002Fwhat-is-dash-mpd-streaming","MPEG-DASHとMPDマニフェストの仕組み、DASH動画の映像・音声トラックを保存する方法、MP4より扱いにくい理由を解説します。","2026-07-01",1787670591971]