[{"data":1,"prerenderedAt":1808},["ShallowReactive",2],{"blog-zh-Hant-browser-streaming-remux-mp4-isobmff":3,"blog-zh-Hant-browser-streaming-remux-mp4-isobmff-surround":1795},{"id":4,"title":5,"author":6,"body":7,"category":1780,"date":1781,"description":1782,"extension":1783,"image":1784,"lastModified":1781,"meta":1785,"navigation":243,"path":1786,"readingTime":441,"seo":1787,"stem":1788,"tags":1789,"__hash__":1794},"blog_zh_Hant\u002Fzh-Hant\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff.md","瀏覽器端影片重封裝：fMP4、ISOBMFF 和不轉碼的理由","FlowPick 團隊",{"type":8,"value":9,"toc":1766},"minimark",[10,14,17,20,25,28,35,41,49,59,62,66,69,79,90,100,104,111,117,126,539,542,546,549,588,599,613,617,1147,1153,1161,1165,1168,1171,1412,1415,1418,1434,1438,1441,1470,1476,1483,1486,1489,1556,1559,1567,1570,1580,1593,1599,1613,1616,1657,1660,1667,1670,1683,1703,1706,1709,1762],[11,12,13],"p",{},"下載串流影片的最後一步——把 TS 分片或 fMP4 分片合成一個能播的 MP4——有兩種做法。一種對、一種一種錯。",[11,15,16],{},"錯的是轉碼：解碼、重新編碼。慢、耗 CPU、有質量損失、在瀏覽器裡基本不可行。",[11,18,19],{},"對的是重封裝：解封裝容器、拿原始 codec 資料、封裝進新容器。快、不耗 CPU、無質量損失。這篇文章就是講重封裝在瀏覽器裡怎麼做，為什麼能這樣做，以及為什麼很多人沒意識到這是對的選擇。",[21,22,24],"h2",{"id":23},"容器-vs-codec","容器 vs codec",[11,26,27],{},"先把概念分清楚，因為 90% 的困惑來自這裡混淆。",[11,29,30,34],{},[31,32,33],"strong",{},"Codec"," 是壓縮演算法。H.264、H.265、VP9、AV1——這些是 codec。它定義了「如何把畫面序列壓成位元流」。",[11,36,37,40],{},[31,38,39],{},"Container"," 是檔案格式。MP4、MKV、WebM、MPEG-TS——這些是 container。它定義了「如何把 codec 位元流、音訊軌道、字幕、時間戳等打包成一個檔案」。",[11,42,43,44,48],{},"一個 ",[45,46,47],"code",{},".mp4"," 檔案裡可能裝 H.264 影片 + AAC 音訊，也可能裝 H.265 影片 + Opus 音訊。container 不關心 codec 是什麼——它只負責打包。",[11,50,51,54,55,58],{},[31,52,53],{},"轉碼"," 是改 codec（H.264 → H.265）。",[31,56,57],{},"重封裝"," 是改 container（TS → MP4）但保留 codec。轉碼要解碼再編碼；重封裝不解碼——它直接搬 codec 資料。",[11,60,61],{},"HLS 下載的場景：來源是 TS 分片，內含 H.264\u002FAAC。要做的是 TS → MP4 重封裝。H.264 和 AAC 原封不動搬過去。這就是為什麼 30 分鐘影片能在 10 秒內完成——不解碼就沒有計算量。",[21,63,65],{"id":64},"isobmffmp4-的結構","ISOBMFF：MP4 的結構",[11,67,68],{},"MP4 是 ISO Base Media File Format（ISOBMFF，ISO 14496-12）的實例。結構是 box（或 atom）的遞迴巢狀。",[70,71,76],"pre",{"className":72,"code":74,"language":75},[73],"language-text","ftyp    檔案類型 box\nmoov    電影 box（元資料）\n├── mvhd    電影標頭（時長、timescale）\n├── trak    軌道 1（影片）\n│   ├── tkhd    軌道標頭\n│   ├── mdia    媒體資訊\n│   │   ├── mdhd    媒體標頭（codec 的 timescale）\n│   │   ├── hdlr    處理器（'vide' = 影片）\n│   │   └── minf    媒體資訊\n│   │       ├── stbl   樣本表（關鍵！）\n│   │       │   ├── stsd   樣本描述（codec 詳情）\n│   │       │   ├── stts   解碼時間到顯示時間\n│   │       │   ├── stsc   樣本到 chunk\n│   │       │   ├── stsz   樣本大小\n│   │       │   └── stco   chunk 偏移\n│   │       └── ...\n│   └── ...\n├── trak    軌道 2（音訊）\n│   └── ...\n└── ...\nmdat    媒體資料（實際的 codec 位元流）\n","text",[45,77,74],{"__ignoreMap":78},"",[11,80,81,82,85,86,89],{},"關鍵是 ",[45,83,84],{},"stbl","（樣本表）——它告訴播放器「mdat 裡的位元組哪段是哪一幀、每幀多大、什麼時候顯示」。沒有這個表，",[45,87,88],{},"mdat"," 就是亂七八糟的位元組。",[11,91,92,93,96,97,99],{},"重封裝要做的事：建一個正確的 ",[45,94,95],{},"moov","，把 ",[45,98,88],{}," 裡的 codec 資料原樣放進去。",[21,101,103],{"id":102},"為什麼-fmp4-能拼接","為什麼 fMP4 能拼接",[11,105,106,107,110],{},"Fragmented MP4（fMP4）是 ISOBMFF 的變體，每個軌道被切成多個帶 ",[45,108,109],{},"moof","（影片片段）box 的片段。",[70,112,115],{"className":113,"code":114,"language":75},[73],"ftyp\nmoov        （軌道佈局，但沒有樣本表）\nmoof        （片段 1 的樣本表）\nmdat        （片段 1 的媒體資料）\nmoof        （片段 2 的樣本表）\nmdat        （片段 2 的媒體資料）\n...\n",[45,116,114],{"__ignoreMap":78},[11,118,119,120,122,123,125],{},"每對 ",[45,121,109],{},"+",[45,124,88],{}," 自描述——它自己帶樣本表和媒體資料。所以拼接 fMP4 片段就是位元組級串聯：",[70,127,131],{"className":128,"code":129,"language":130,"meta":78,"style":78},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","async function mergeFmp4(initUrl, segmentUrls) {\n  const init = new Uint8Array(await (await fetch(initUrl)).arrayBuffer())\n  const chunks = [init]\n\n  for (const url of segmentUrls) {\n    chunks.push(new Uint8Array(await (await fetch(url)).arrayBuffer()))\n  }\n\n  const total = chunks.reduce((s, c) => s + c.byteLength, 0)\n  const out = new Uint8Array(total)\n  let offset = 0\n\n  for (const chunk of chunks) {\n    out.set(chunk, offset)\n    offset += chunk.byteLength\n  }\n\n  return new Blob([out], { type: 'video\u002Fmp4' })\n}\n","js",[45,132,133,169,219,238,245,270,312,318,323,379,400,414,419,439,461,477,482,487,533],{"__ignoreMap":78},[134,135,138,142,145,149,153,157,160,163,166],"span",{"class":136,"line":137},"line",1,[134,139,141],{"class":140},"spNyl","async",[134,143,144],{"class":140}," function",[134,146,148],{"class":147},"s2Zo4"," mergeFmp4",[134,150,152],{"class":151},"sMK4o","(",[134,154,156],{"class":155},"sHdIc","initUrl",[134,158,159],{"class":151},",",[134,161,162],{"class":155}," segmentUrls",[134,164,165],{"class":151},")",[134,167,168],{"class":151}," {\n",[134,170,172,175,179,182,185,188,191,195,198,200,203,205,207,210,213,216],{"class":136,"line":171},2,[134,173,174],{"class":140},"  const",[134,176,178],{"class":177},"sTEyZ"," init",[134,180,181],{"class":151}," =",[134,183,184],{"class":151}," new",[134,186,187],{"class":147}," Uint8Array",[134,189,152],{"class":190},"swJcz",[134,192,194],{"class":193},"s7zQu","await",[134,196,197],{"class":190}," (",[134,199,194],{"class":193},[134,201,202],{"class":147}," fetch",[134,204,152],{"class":190},[134,206,156],{"class":177},[134,208,209],{"class":190},"))",[134,211,212],{"class":151},".",[134,214,215],{"class":147},"arrayBuffer",[134,217,218],{"class":190},"())\n",[134,220,222,224,227,229,232,235],{"class":136,"line":221},3,[134,223,174],{"class":140},[134,225,226],{"class":177}," chunks",[134,228,181],{"class":151},[134,230,231],{"class":190}," [",[134,233,234],{"class":177},"init",[134,236,237],{"class":190},"]\n",[134,239,241],{"class":136,"line":240},4,[134,242,244],{"emptyLinePlaceholder":243},true,"\n",[134,246,248,251,253,256,259,262,264,267],{"class":136,"line":247},5,[134,249,250],{"class":193},"  for",[134,252,197],{"class":190},[134,254,255],{"class":140},"const",[134,257,258],{"class":177}," url",[134,260,261],{"class":151}," of",[134,263,162],{"class":177},[134,265,266],{"class":190},") ",[134,268,269],{"class":151},"{\n",[134,271,273,276,278,281,283,286,288,290,292,294,296,298,300,303,305,307,309],{"class":136,"line":272},6,[134,274,275],{"class":177},"    chunks",[134,277,212],{"class":151},[134,279,280],{"class":147},"push",[134,282,152],{"class":190},[134,284,285],{"class":151},"new",[134,287,187],{"class":147},[134,289,152],{"class":190},[134,291,194],{"class":193},[134,293,197],{"class":190},[134,295,194],{"class":193},[134,297,202],{"class":147},[134,299,152],{"class":190},[134,301,302],{"class":177},"url",[134,304,209],{"class":190},[134,306,212],{"class":151},[134,308,215],{"class":147},[134,310,311],{"class":190},"()))\n",[134,313,315],{"class":136,"line":314},7,[134,316,317],{"class":151},"  }\n",[134,319,321],{"class":136,"line":320},8,[134,322,244],{"emptyLinePlaceholder":243},[134,324,326,328,331,333,335,337,340,342,344,347,349,352,354,357,360,363,365,367,370,372,376],{"class":136,"line":325},9,[134,327,174],{"class":140},[134,329,330],{"class":177}," total",[134,332,181],{"class":151},[134,334,226],{"class":177},[134,336,212],{"class":151},[134,338,339],{"class":147},"reduce",[134,341,152],{"class":190},[134,343,152],{"class":151},[134,345,346],{"class":155},"s",[134,348,159],{"class":151},[134,350,351],{"class":155}," c",[134,353,165],{"class":151},[134,355,356],{"class":140}," =>",[134,358,359],{"class":177}," s",[134,361,362],{"class":151}," +",[134,364,351],{"class":177},[134,366,212],{"class":151},[134,368,369],{"class":177},"byteLength",[134,371,159],{"class":151},[134,373,375],{"class":374},"sbssI"," 0",[134,377,378],{"class":190},")\n",[134,380,382,384,387,389,391,393,395,398],{"class":136,"line":381},10,[134,383,174],{"class":140},[134,385,386],{"class":177}," out",[134,388,181],{"class":151},[134,390,184],{"class":151},[134,392,187],{"class":147},[134,394,152],{"class":190},[134,396,397],{"class":177},"total",[134,399,378],{"class":190},[134,401,403,406,409,411],{"class":136,"line":402},11,[134,404,405],{"class":140},"  let",[134,407,408],{"class":177}," offset",[134,410,181],{"class":151},[134,412,413],{"class":374}," 0\n",[134,415,417],{"class":136,"line":416},12,[134,418,244],{"emptyLinePlaceholder":243},[134,420,422,424,426,428,431,433,435,437],{"class":136,"line":421},13,[134,423,250],{"class":193},[134,425,197],{"class":190},[134,427,255],{"class":140},[134,429,430],{"class":177}," chunk",[134,432,261],{"class":151},[134,434,226],{"class":177},[134,436,266],{"class":190},[134,438,269],{"class":151},[134,440,442,445,447,450,452,455,457,459],{"class":136,"line":441},14,[134,443,444],{"class":177},"    out",[134,446,212],{"class":151},[134,448,449],{"class":147},"set",[134,451,152],{"class":190},[134,453,454],{"class":177},"chunk",[134,456,159],{"class":151},[134,458,408],{"class":177},[134,460,378],{"class":190},[134,462,464,467,470,472,474],{"class":136,"line":463},15,[134,465,466],{"class":177},"    offset",[134,468,469],{"class":151}," +=",[134,471,430],{"class":177},[134,473,212],{"class":151},[134,475,476],{"class":177},"byteLength\n",[134,478,480],{"class":136,"line":479},16,[134,481,317],{"class":151},[134,483,485],{"class":136,"line":484},17,[134,486,244],{"emptyLinePlaceholder":243},[134,488,490,493,495,498,501,504,507,509,512,515,518,521,525,528,531],{"class":136,"line":489},18,[134,491,492],{"class":193},"  return",[134,494,184],{"class":151},[134,496,497],{"class":147}," Blob",[134,499,500],{"class":190},"([",[134,502,503],{"class":177},"out",[134,505,506],{"class":190},"]",[134,508,159],{"class":151},[134,510,511],{"class":151}," {",[134,513,514],{"class":190}," type",[134,516,517],{"class":151},":",[134,519,520],{"class":151}," '",[134,522,524],{"class":523},"sfazB","video\u002Fmp4",[134,526,527],{"class":151},"'",[134,529,530],{"class":151}," }",[134,532,378],{"class":190},[134,534,536],{"class":136,"line":535},19,[134,537,538],{"class":151},"}\n",[11,540,541],{},"這就是 FlowPick DASH 下載路徑的核心邏輯——大概 20 行 JS，跑得跟原生工具一樣快，因為沒有什麼計算可做，就是位元組拼接。",[21,543,545],{"id":544},"為什麼-ts-不能這樣","為什麼 TS 不能這樣",[11,547,548],{},"MPEG-TS 沒有「每個分片獨立可拼接」的屬性。TS 分片是一串 188 位元組的封包，每個帶 PID、PTS、DTS。要把多個 TS 分片合成一個 MP4：",[550,551,552,556,559,562,568,573,578],"ol",{},[553,554,555],"li",{},"解析 TS 封包頭，按 PID 分流到影片\u002F音訊",[553,557,558],{},"從 PES 封包抽 codec 資料",[553,560,561],{},"記錄每個畫面的 PTS\u002FDTS 和大小",[553,563,564,565,567],{},"建 ",[45,566,84],{}," box（樣本表）",[553,569,570,571],{},"建完整的 ",[45,572,95],{},[553,574,575,576],{},"重新交錯影片\u002F音訊樣本，寫入 ",[45,577,88],{},[553,579,580,581,584,585,587],{},"回填 ",[45,582,583],{},"stco","（chunk 偏移），因為寫 ",[45,586,95],{}," 之前不知道最終偏移",[11,589,590,591,594,595,598],{},"這工作量重，但不需要解碼。FFmpeg 的 ",[45,592,593],{},"mpegts"," 解封裝器 + ",[45,596,597],{},"mp4"," 封裝器幹的就是這個。在瀏覽器裡用 FFmpeg WASM 是最常見方案，因為 FFmpeg 實作過這個邏輯、各種邊界情況都處理過。",[11,600,601,604,605,612],{},[31,602,603],{},"但有個純 JS 路徑。"," ",[606,607,611],"a",{"href":608,"rel":609},"https:\u002F\u002Fgithub.com\u002FitsKaynine\u002Fjayer",[610],"nofollow","jayer"," 之類的庫做了 TS 解封裝，然後你手動建 MP4 box。好處是不需要 25MB 的 WASM 下載。壞處是邊界情況可能翻車——FFmpeg 在各種 codec、各種 TS 變體下都測過幾十年了。",[21,614,616],{"id":615},"ffmpeg-wasm-重封裝","FFmpeg WASM 重封裝",[70,618,620],{"className":128,"code":619,"language":130,"meta":78,"style":78},"import { FFmpeg } from '@ffmpeg\u002Fffmpeg'\nimport { fetchFile } from '@ffmpeg\u002Futil'\n\nconst ffmpeg = new FFmpeg()\nawait ffmpeg.load({\n  coreURL: '\u002Fffmpeg-core.js',\n  wasmURL: '\u002Fffmpeg-core.wasm'\n})\n\n\u002F\u002F 把所有 TS 分片寫進記憶體 FS\nawait ffmpeg.writeFile('list.txt', segmentUrls.map((_, i) => `file 'seg_${i}.ts'`).join('\\n'))\nfor (let i = 0; i \u003C segmentUrls.length; i++) {\n  await ffmpeg.writeFile(`seg_${i}.ts`, await fetchFile(segmentUrls[i]))\n}\n\n\u002F\u002F 重封裝\nawait ffmpeg.exec([\n  '-f', 'concat',\n  '-i', 'list.txt',\n  '-c', 'copy',         \u002F\u002F 不轉碼！只重封裝\n  '-f', 'mp4',\n  '-movflags', '+faststart',\n  'out.mp4'\n])\n\nconst data = await ffmpeg.readFile('out.mp4')\nconst blob = new Blob([data.buffer], { type: 'video\u002Fmp4' })\n",[45,621,622,645,665,669,686,702,719,733,740,744,750,834,877,926,930,934,939,953,974,993,1017,1036,1057,1067,1073,1078,1107],{"__ignoreMap":78},[134,623,624,627,629,632,634,637,639,642],{"class":136,"line":137},[134,625,626],{"class":193},"import",[134,628,511],{"class":151},[134,630,631],{"class":177}," FFmpeg",[134,633,530],{"class":151},[134,635,636],{"class":193}," from",[134,638,520],{"class":151},[134,640,641],{"class":523},"@ffmpeg\u002Fffmpeg",[134,643,644],{"class":151},"'\n",[134,646,647,649,651,654,656,658,660,663],{"class":136,"line":171},[134,648,626],{"class":193},[134,650,511],{"class":151},[134,652,653],{"class":177}," fetchFile",[134,655,530],{"class":151},[134,657,636],{"class":193},[134,659,520],{"class":151},[134,661,662],{"class":523},"@ffmpeg\u002Futil",[134,664,644],{"class":151},[134,666,667],{"class":136,"line":221},[134,668,244],{"emptyLinePlaceholder":243},[134,670,671,673,676,679,681,683],{"class":136,"line":240},[134,672,255],{"class":140},[134,674,675],{"class":177}," ffmpeg ",[134,677,678],{"class":151},"=",[134,680,184],{"class":151},[134,682,631],{"class":147},[134,684,685],{"class":177},"()\n",[134,687,688,690,693,695,698,700],{"class":136,"line":247},[134,689,194],{"class":193},[134,691,692],{"class":177}," ffmpeg",[134,694,212],{"class":151},[134,696,697],{"class":147},"load",[134,699,152],{"class":177},[134,701,269],{"class":151},[134,703,704,707,709,711,714,716],{"class":136,"line":272},[134,705,706],{"class":190},"  coreURL",[134,708,517],{"class":151},[134,710,520],{"class":151},[134,712,713],{"class":523},"\u002Fffmpeg-core.js",[134,715,527],{"class":151},[134,717,718],{"class":151},",\n",[134,720,721,724,726,728,731],{"class":136,"line":314},[134,722,723],{"class":190},"  wasmURL",[134,725,517],{"class":151},[134,727,520],{"class":151},[134,729,730],{"class":523},"\u002Fffmpeg-core.wasm",[134,732,644],{"class":151},[134,734,735,738],{"class":136,"line":320},[134,736,737],{"class":151},"}",[134,739,378],{"class":177},[134,741,742],{"class":136,"line":325},[134,743,244],{"emptyLinePlaceholder":243},[134,745,746],{"class":136,"line":381},[134,747,749],{"class":748},"sHwdD","\u002F\u002F 把所有 TS 分片寫進記憶體 FS\n",[134,751,752,754,756,758,761,763,765,768,770,772,774,776,779,781,783,786,788,791,793,795,798,801,804,807,809,812,815,817,819,822,824,826,829,831],{"class":136,"line":402},[134,753,194],{"class":193},[134,755,692],{"class":177},[134,757,212],{"class":151},[134,759,760],{"class":147},"writeFile",[134,762,152],{"class":177},[134,764,527],{"class":151},[134,766,767],{"class":523},"list.txt",[134,769,527],{"class":151},[134,771,159],{"class":151},[134,773,162],{"class":177},[134,775,212],{"class":151},[134,777,778],{"class":147},"map",[134,780,152],{"class":177},[134,782,152],{"class":151},[134,784,785],{"class":155},"_",[134,787,159],{"class":151},[134,789,790],{"class":155}," i",[134,792,165],{"class":151},[134,794,356],{"class":140},[134,796,797],{"class":151}," `",[134,799,800],{"class":523},"file 'seg_",[134,802,803],{"class":151},"${",[134,805,806],{"class":177},"i",[134,808,737],{"class":151},[134,810,811],{"class":523},".ts'",[134,813,814],{"class":151},"`",[134,816,165],{"class":177},[134,818,212],{"class":151},[134,820,821],{"class":147},"join",[134,823,152],{"class":177},[134,825,527],{"class":151},[134,827,828],{"class":177},"\\n",[134,830,527],{"class":151},[134,832,833],{"class":177},"))\n",[134,835,836,839,841,844,847,849,851,854,856,859,861,863,866,868,870,873,875],{"class":136,"line":416},[134,837,838],{"class":193},"for",[134,840,197],{"class":177},[134,842,843],{"class":140},"let",[134,845,846],{"class":177}," i ",[134,848,678],{"class":151},[134,850,375],{"class":374},[134,852,853],{"class":151},";",[134,855,846],{"class":177},[134,857,858],{"class":151},"\u003C",[134,860,162],{"class":177},[134,862,212],{"class":151},[134,864,865],{"class":177},"length",[134,867,853],{"class":151},[134,869,790],{"class":177},[134,871,872],{"class":151},"++",[134,874,266],{"class":177},[134,876,269],{"class":151},[134,878,879,882,884,886,888,890,892,895,897,899,901,904,906,908,911,913,915,918,921,923],{"class":136,"line":421},[134,880,881],{"class":193},"  await",[134,883,692],{"class":177},[134,885,212],{"class":151},[134,887,760],{"class":147},[134,889,152],{"class":190},[134,891,814],{"class":151},[134,893,894],{"class":523},"seg_",[134,896,803],{"class":151},[134,898,806],{"class":177},[134,900,737],{"class":151},[134,902,903],{"class":523},".ts",[134,905,814],{"class":151},[134,907,159],{"class":151},[134,909,910],{"class":193}," await",[134,912,653],{"class":147},[134,914,152],{"class":190},[134,916,917],{"class":177},"segmentUrls",[134,919,920],{"class":190},"[",[134,922,806],{"class":177},[134,924,925],{"class":190},"]))\n",[134,927,928],{"class":136,"line":441},[134,929,538],{"class":151},[134,931,932],{"class":136,"line":463},[134,933,244],{"emptyLinePlaceholder":243},[134,935,936],{"class":136,"line":479},[134,937,938],{"class":748},"\u002F\u002F 重封裝\n",[134,940,941,943,945,947,950],{"class":136,"line":484},[134,942,194],{"class":193},[134,944,692],{"class":177},[134,946,212],{"class":151},[134,948,949],{"class":147},"exec",[134,951,952],{"class":177},"([\n",[134,954,955,958,961,963,965,967,970,972],{"class":136,"line":489},[134,956,957],{"class":151},"  '",[134,959,960],{"class":523},"-f",[134,962,527],{"class":151},[134,964,159],{"class":151},[134,966,520],{"class":151},[134,968,969],{"class":523},"concat",[134,971,527],{"class":151},[134,973,718],{"class":151},[134,975,976,978,981,983,985,987,989,991],{"class":136,"line":535},[134,977,957],{"class":151},[134,979,980],{"class":523},"-i",[134,982,527],{"class":151},[134,984,159],{"class":151},[134,986,520],{"class":151},[134,988,767],{"class":523},[134,990,527],{"class":151},[134,992,718],{"class":151},[134,994,996,998,1001,1003,1005,1007,1010,1012,1014],{"class":136,"line":995},20,[134,997,957],{"class":151},[134,999,1000],{"class":523},"-c",[134,1002,527],{"class":151},[134,1004,159],{"class":151},[134,1006,520],{"class":151},[134,1008,1009],{"class":523},"copy",[134,1011,527],{"class":151},[134,1013,159],{"class":151},[134,1015,1016],{"class":748},"         \u002F\u002F 不轉碼！只重封裝\n",[134,1018,1020,1022,1024,1026,1028,1030,1032,1034],{"class":136,"line":1019},21,[134,1021,957],{"class":151},[134,1023,960],{"class":523},[134,1025,527],{"class":151},[134,1027,159],{"class":151},[134,1029,520],{"class":151},[134,1031,597],{"class":523},[134,1033,527],{"class":151},[134,1035,718],{"class":151},[134,1037,1039,1041,1044,1046,1048,1050,1053,1055],{"class":136,"line":1038},22,[134,1040,957],{"class":151},[134,1042,1043],{"class":523},"-movflags",[134,1045,527],{"class":151},[134,1047,159],{"class":151},[134,1049,520],{"class":151},[134,1051,1052],{"class":523},"+faststart",[134,1054,527],{"class":151},[134,1056,718],{"class":151},[134,1058,1060,1062,1065],{"class":136,"line":1059},23,[134,1061,957],{"class":151},[134,1063,1064],{"class":523},"out.mp4",[134,1066,644],{"class":151},[134,1068,1070],{"class":136,"line":1069},24,[134,1071,1072],{"class":177},"])\n",[134,1074,1076],{"class":136,"line":1075},25,[134,1077,244],{"emptyLinePlaceholder":243},[134,1079,1081,1083,1086,1088,1090,1092,1094,1097,1099,1101,1103,1105],{"class":136,"line":1080},26,[134,1082,255],{"class":140},[134,1084,1085],{"class":177}," data ",[134,1087,678],{"class":151},[134,1089,910],{"class":193},[134,1091,692],{"class":177},[134,1093,212],{"class":151},[134,1095,1096],{"class":147},"readFile",[134,1098,152],{"class":177},[134,1100,527],{"class":151},[134,1102,1064],{"class":523},[134,1104,527],{"class":151},[134,1106,378],{"class":177},[134,1108,1110,1112,1115,1117,1119,1121,1124,1126,1129,1131,1133,1135,1137,1139,1141,1143,1145],{"class":136,"line":1109},27,[134,1111,255],{"class":140},[134,1113,1114],{"class":177}," blob ",[134,1116,678],{"class":151},[134,1118,184],{"class":151},[134,1120,497],{"class":147},[134,1122,1123],{"class":177},"([data",[134,1125,212],{"class":151},[134,1127,1128],{"class":177},"buffer]",[134,1130,159],{"class":151},[134,1132,511],{"class":151},[134,1134,514],{"class":190},[134,1136,517],{"class":151},[134,1138,520],{"class":151},[134,1140,524],{"class":523},[134,1142,527],{"class":151},[134,1144,530],{"class":151},[134,1146,378],{"class":177},[11,1148,1149,1152],{},[45,1150,1151],{},"-c copy"," 是這裡的魔法。它告訴 FFmpeg「不解碼、不重新編碼，codec 資料原樣搬」。這就是重封裝。",[11,1154,1155,1157,1158,1160],{},[45,1156,1052],{}," 把 ",[45,1159,95],{}," box 移到檔案開頭——這樣播放器拿到檔案前幾 KB 就能開始播，不用下載整個檔案。串流和漸進式下載必備。",[21,1162,1164],{"id":1163},"記憶體管理大檔案的分塊處理","記憶體管理：大檔案的分塊處理",[11,1166,1167],{},"上面那段在 30 分鐘 1080p 影片上沒問題——500MB 的分片 + 輸出還裝得進瀏覽器標籤頁 2GB 的軟上限。但 4K 30 分鐘大約 2.5GB，4K 2 小時電影就 10GB+。",[11,1169,1170],{},"分塊策略：",[70,1172,1174],{"className":128,"code":1173,"language":130,"meta":78,"style":78},"async function chunkedRemux(segmentUrls, chunkSize = 50) {\n  const parts = []\n\n  for (let i = 0; i \u003C segmentUrls.length; i += chunkSize) {\n    const chunk = segmentUrls.slice(i, i + chunkSize)\n    const partBlob = await remuxChunk(chunk, i === 0)\n    parts.push(partBlob)\n    \u002F\u002F 讓記憶體回收\n    await new Promise(r => setTimeout(r, 0))\n  }\n\n  return new Blob(parts, { type: 'video\u002Fmp4' })\n}\n",[45,1175,1176,1203,1215,1219,1258,1288,1317,1333,1338,1369,1373,1377,1408],{"__ignoreMap":78},[134,1177,1178,1180,1182,1185,1187,1189,1191,1194,1196,1199,1201],{"class":136,"line":137},[134,1179,141],{"class":140},[134,1181,144],{"class":140},[134,1183,1184],{"class":147}," chunkedRemux",[134,1186,152],{"class":151},[134,1188,917],{"class":155},[134,1190,159],{"class":151},[134,1192,1193],{"class":155}," chunkSize",[134,1195,181],{"class":151},[134,1197,1198],{"class":374}," 50",[134,1200,165],{"class":151},[134,1202,168],{"class":151},[134,1204,1205,1207,1210,1212],{"class":136,"line":171},[134,1206,174],{"class":140},[134,1208,1209],{"class":177}," parts",[134,1211,181],{"class":151},[134,1213,1214],{"class":190}," []\n",[134,1216,1217],{"class":136,"line":221},[134,1218,244],{"emptyLinePlaceholder":243},[134,1220,1221,1223,1225,1227,1229,1231,1233,1235,1237,1240,1242,1244,1246,1248,1250,1252,1254,1256],{"class":136,"line":240},[134,1222,250],{"class":193},[134,1224,197],{"class":190},[134,1226,843],{"class":140},[134,1228,790],{"class":177},[134,1230,181],{"class":151},[134,1232,375],{"class":374},[134,1234,853],{"class":151},[134,1236,790],{"class":177},[134,1238,1239],{"class":151}," \u003C",[134,1241,162],{"class":177},[134,1243,212],{"class":151},[134,1245,865],{"class":177},[134,1247,853],{"class":151},[134,1249,790],{"class":177},[134,1251,469],{"class":151},[134,1253,1193],{"class":177},[134,1255,266],{"class":190},[134,1257,269],{"class":151},[134,1259,1260,1263,1265,1267,1269,1271,1274,1276,1278,1280,1282,1284,1286],{"class":136,"line":247},[134,1261,1262],{"class":140},"    const",[134,1264,430],{"class":177},[134,1266,181],{"class":151},[134,1268,162],{"class":177},[134,1270,212],{"class":151},[134,1272,1273],{"class":147},"slice",[134,1275,152],{"class":190},[134,1277,806],{"class":177},[134,1279,159],{"class":151},[134,1281,790],{"class":177},[134,1283,362],{"class":151},[134,1285,1193],{"class":177},[134,1287,378],{"class":190},[134,1289,1290,1292,1295,1297,1299,1302,1304,1306,1308,1310,1313,1315],{"class":136,"line":272},[134,1291,1262],{"class":140},[134,1293,1294],{"class":177}," partBlob",[134,1296,181],{"class":151},[134,1298,910],{"class":193},[134,1300,1301],{"class":147}," remuxChunk",[134,1303,152],{"class":190},[134,1305,454],{"class":177},[134,1307,159],{"class":151},[134,1309,790],{"class":177},[134,1311,1312],{"class":151}," ===",[134,1314,375],{"class":374},[134,1316,378],{"class":190},[134,1318,1319,1322,1324,1326,1328,1331],{"class":136,"line":314},[134,1320,1321],{"class":177},"    parts",[134,1323,212],{"class":151},[134,1325,280],{"class":147},[134,1327,152],{"class":190},[134,1329,1330],{"class":177},"partBlob",[134,1332,378],{"class":190},[134,1334,1335],{"class":136,"line":320},[134,1336,1337],{"class":748},"    \u002F\u002F 讓記憶體回收\n",[134,1339,1340,1343,1345,1349,1351,1354,1356,1359,1361,1363,1365,1367],{"class":136,"line":325},[134,1341,1342],{"class":193},"    await",[134,1344,184],{"class":151},[134,1346,1348],{"class":1347},"sBMFI"," Promise",[134,1350,152],{"class":190},[134,1352,1353],{"class":155},"r",[134,1355,356],{"class":140},[134,1357,1358],{"class":147}," setTimeout",[134,1360,152],{"class":190},[134,1362,1353],{"class":177},[134,1364,159],{"class":151},[134,1366,375],{"class":374},[134,1368,833],{"class":190},[134,1370,1371],{"class":136,"line":381},[134,1372,317],{"class":151},[134,1374,1375],{"class":136,"line":402},[134,1376,244],{"emptyLinePlaceholder":243},[134,1378,1379,1381,1383,1385,1387,1390,1392,1394,1396,1398,1400,1402,1404,1406],{"class":136,"line":416},[134,1380,492],{"class":193},[134,1382,184],{"class":151},[134,1384,497],{"class":147},[134,1386,152],{"class":190},[134,1388,1389],{"class":177},"parts",[134,1391,159],{"class":151},[134,1393,511],{"class":151},[134,1395,514],{"class":190},[134,1397,517],{"class":151},[134,1399,520],{"class":151},[134,1401,524],{"class":523},[134,1403,527],{"class":151},[134,1405,530],{"class":151},[134,1407,378],{"class":190},[134,1409,1410],{"class":136,"line":421},[134,1411,538],{"class":151},[11,1413,1414],{},"每塊獨立重封裝，FFmpeg WASM 實例在塊之間重設，記憶體峰值壓在「一個塊的資料 + FFmpeg 內部緩衝」——約 100MB。",[11,1416,1417],{},"問題：合出來的 MP4 是多個獨立檔案串起來的，不是一個 seek 連續的 MP4。對「播放\u002F下載」場景夠用；對「匯出到剪輯軟體」不夠，因為時間軸會斷。",[11,1419,1420,1421,1424,1425,1427,1428,1430,1431,1433],{},"解法是重封裝後做一次 ",[45,1422,1423],{},"mp4box.js"," 的「fixup」——讀所有塊的 ",[45,1426,95],{},"，合成一個連續的 ",[45,1429,95],{},"，重寫 ",[45,1432,583],{}," 偏移。複雜但能做。FlowPick 對超過 2GB 的檔案走這條路。",[21,1435,1437],{"id":1436},"什麼時候用-webcodecs-而不是-ffmpeg-wasm","什麼時候用 WebCodecs 而不是 FFmpeg WASM",[11,1439,1440],{},"有個替代路徑，不需要 FFmpeg：WebCodecs + 手寫封裝。",[1442,1443,1444,1455,1461],"ul",{},[553,1445,1446,1447,1450,1451,1454],{},"用 WebCodecs 的 ",[45,1448,1449],{},"VideoDecoder","\u002F",[45,1452,1453],{},"AudioDecoder"," 解碼",[553,1456,1457,1458,1460],{},"用 ",[45,1459,1423],{}," 或手寫封裝器建 MP4",[553,1462,1446,1463,1450,1466,1469],{},[45,1464,1465],{},"VideoEncoder",[45,1467,1468],{},"AudioEncoder"," 重新編碼（如果要轉碼的話）",[11,1471,1472,1473,1475],{},"對",[31,1474,57],{},"不需要解碼器——沒有轉碼發生。但 WebCodecs 在這裡另有用途：解析 TS 分片抽 codec chunk。這部分 FFmpeg WASM 也能做，但 WebCodecs 在某些 codec 上硬體加速得更快。",[11,1477,1478,1482],{},[606,1479,1481],{"href":1480},"\u002Fzh-Hant\u002Fblog\u002Fwebcodecs-web-workers-opfs-video-processing","WebCodecs 那篇"," 講完整的故事。我的看法：對純重封裝（TS → MP4），FFmpeg WASM 仍是更穩健的選擇；WebCodecs 適合需要逐幀操作（縮圖、濾鏡、分析）的場景。",[21,1484,1485],{"id":1485},"效能實測",[11,1487,1488],{},"30 分鐘 1080p HLS（300 個 TS 分片、500MB）重封裝成 MP4：",[1490,1491,1492,1508],"table",{},[1493,1494,1495],"thead",{},[1496,1497,1498,1502,1505],"tr",{},[1499,1500,1501],"th",{},"方案",[1499,1503,1504],{},"實際耗時",[1499,1506,1507],{},"記憶體峰值",[1509,1510,1511,1523,1534,1545],"tbody",{},[1496,1512,1513,1517,1520],{},[1514,1515,1516],"td",{},"原生 FFmpeg",[1514,1518,1519],{},"8s",[1514,1521,1522],{},"80MB",[1496,1524,1525,1528,1531],{},[1514,1526,1527],{},"FFmpeg WASM 單執行緒",[1514,1529,1530],{},"75s",[1514,1532,1533],{},"220MB",[1496,1535,1536,1539,1542],{},[1514,1537,1538],{},"FFmpeg WASM 多執行緒",[1514,1540,1541],{},"22s",[1514,1543,1544],{},"280MB",[1496,1546,1547,1550,1553],{},[1514,1548,1549],{},"純 JS TS 解封裝 + mp4box.js",[1514,1551,1552],{},"45s",[1514,1554,1555],{},"180MB",[11,1557,1558],{},"原生是基線，瀏覽器選項都在它的 3-10 倍內。多執行緒 FFmpeg WASM 對即時使用夠用——30 分鐘影片 22 秒處理完。",[11,1560,1561,1562,1566],{},"純 JS 路徑比 FFmpeg WASM 單執行緒快——沒有 WASM 呼叫開銷——但不那麼穩健，某些 codec 變體會出問題。完整基準資料在 ",[606,1563,1565],{"href":1564},"\u002Fzh-Hant\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","效能基準那篇","。",[21,1568,1569],{"id":1569},"踩過的坑",[11,1571,1572,1575,1576,1579],{},[31,1573,1574],{},"坑 1：PTS\u002FDTS 亂序。"," 帶 B 幀的 H.264，解碼順序（DTS）和顯示順序（PTS）不同。",[45,1577,1578],{},"stts"," box（解碼時間到顯示時間）記錄這個映射。重封裝時要正確記錄——錯了，播放器會頓、時間軸會錯、有些播放器直接拒絕。",[11,1581,1582,604,1585,1588,1589,1592],{},[31,1583,1584],{},"坑 2：編輯清單。",[45,1586,1587],{},"elst"," box 讓 container 層修剪——例如「跳過前 2 秒」。HLS 清單有 ",[45,1590,1591],{},"#EXT-X-START"," 標籤時要建編輯清單。忘了，播放器從 0 開始播，可能從廣告或黑幀開始。",[11,1594,1595,1598],{},[31,1596,1597],{},"坑 3：H.265\u002FHEVC profile。"," 不同瀏覽器和裝置支援的 HEVC profile 不同。重封裝不改變 codec，所以如果來源是裝置不支援的 profile，輸出也不支援。這不是重封裝的問題——但使用者會抱怨「下的影片播不了」。",[11,1600,1601,1604,1605,1608,1609,1612],{},[31,1602,1603],{},"坑 4：AAC 編輯清單差異。"," AAC 封包有附加資料（AudioSpecificConfig）。",[45,1606,1607],{},"stsd"," 裡的 ",[45,1610,1611],{},"esds"," box 要正確含這個。忘了，播放器可能播放但音訊有問題——常見症狀是「有影片沒聲音」。",[21,1614,1615],{"id":1615},"參考資料",[1442,1617,1618,1626,1634,1642,1649],{},[553,1619,1620,1625],{},[606,1621,1624],{"href":1622,"rel":1623},"https:\u002F\u002Fwww.iso.org\u002Fstandard\u002F79330.html",[610],"ISO\u002FIEC 14496-12 — ISOBMFF 規範"," — 正式規範，付費但權威",[553,1627,1628,1633],{},[606,1629,1632],{"href":1630,"rel":1631},"https:\u002F\u002Fwww.ftyps.com\u002F",[610],"MP4 File Format 規範"," — 各種 ftyp 和 brand 的資料庫",[553,1635,1636,1641],{},[606,1637,1640],{"href":1638,"rel":1639},"https:\u002F\u002Fffmpeg.org\u002Fffmpeg-formats.html#mpegts",[610],"FFmpeg MPEG-TS 文件"," — 理解 TS 封包結構",[553,1643,1644,1648],{},[606,1645,1423],{"href":1646,"rel":1647},"https:\u002F\u002Fgithub.com\u002Fgpac\u002Fmp4box.js",[610]," — GPAC 的 JS MP4 工具，能寫也能讀",[553,1650,1651,1656],{},[606,1652,1655],{"href":1653,"rel":1654},"https:\u002F\u002Fgithub.com\u002Faxiomatic-systems\u002FBento4",[610],"Bento4"," — 開源 MP4 工具，用來驗證輸出正確性",[21,1658,1659],{"id":1659},"小結",[11,1661,1662,1663,1666],{},"瀏覽器端影片合併的對的哲學：",[31,1664,1665],{},"重封裝，不轉碼。"," 你下載的 TS\u002FfMP4 分片已經含編碼好的 codec 資料。你的工作只是改變 container。這個操作是 O(n) 位元組操作——不涉及解碼、不涉及編碼、沒有質量損失、沒有 CPU 重活。",[11,1668,1669],{},"fMP4 場景下這就是字面意義的位元組拼接。TS 場景下需要解封裝——FFmpeg WASM 是最穩健的實作，純 JS 也行但邊界情況要小心。",[11,1671,1672,1673,1675,1676,1675,1679,1682],{},"效能差異是「10 秒還是 5 分鐘」——如果「合併」步驟明顯慢，說明你的工具在轉碼而不是重封裝。查 ",[45,1674,1151],{}," \u002F ",[45,1677,1678],{},"copy_audio",[45,1680,1681],{},"copy_video"," 標誌有沒有設。",[11,1684,1685,1686,1694,1695,1698,1699,1702],{},"下一篇講 ",[606,1687,1689,1690,1693],{"href":1688},"\u002Fzh-Hant\u002Fblog\u002Fhls-m3u8-deep-dive-encryption-multitrack","HLS 清單裡 ",[45,1691,1692],{},"#EXT-X-KEY"," 標籤的深度","，以及為什麼 ",[45,1696,1697],{},"METHOD=SAMPLE-AES"," 跟 ",[45,1700,1701],{},"METHOD=AES-128"," 是兩個世界——後者你能處理，前者碰不得。",[1704,1705],"hr",{},[21,1707,1708],{"id":1708},"推薦閱讀",[1442,1710,1711,1720,1728,1736,1745,1753],{},[553,1712,1713,1719],{},[31,1714,1715],{},[606,1716,1718],{"href":1717},"\u002Fzh-Hant\u002Fblog\u002Fhow-flowpick-merges-video-segments-in-browser","FlowPick 是怎麼在瀏覽器裡把幾百個影片分片合成 MP4 的"," — 這篇的淺版本",[553,1721,1722,1727],{},[31,1723,1724],{},[606,1725,1726],{"href":1480},"WebCodecs + Web Workers + OPFS：瀏覽器影片處理實戰"," — FFmpeg WASM 不適合時的更底層 API",[553,1729,1730,1735],{},[31,1731,1732],{},[606,1733,1734],{"href":1688},"HLS 深度：加密、多音軌、值得記住的 EXT-X 標籤"," — 你重封裝的那些 TS 分片是哪來的",[553,1737,1738,1744],{},[31,1739,1740],{},[606,1741,1743],{"href":1742},"\u002Fzh-Hant\u002Fblog\u002Fdash-mpd-deep-dive-segmenttemplate-contentprotection","DASH 深度：SegmentTemplate、ContentProtection 與多視角 MPD"," — fMP4 分片的來源",[553,1746,1747,1752],{},[31,1748,1749],{},[606,1750,1751],{"href":1564},"瀏覽器影片處理效能基準"," — 本文方法的完整效能資料",[553,1754,1755,1761],{},[31,1756,1757],{},[606,1758,1760],{"href":1759},"\u002Fzh-Hant\u002Fblog\u002Fflowpick-vs-yt-dlp","FlowPick vs. yt-dlp"," — 瀏覽器重封裝 vs 原生重封裝的差異",[1763,1764,1765],"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 .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 .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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .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 .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":78,"searchDepth":171,"depth":171,"links":1767},[1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779],{"id":23,"depth":171,"text":24},{"id":64,"depth":171,"text":65},{"id":102,"depth":171,"text":103},{"id":544,"depth":171,"text":545},{"id":615,"depth":171,"text":616},{"id":1163,"depth":171,"text":1164},{"id":1436,"depth":171,"text":1437},{"id":1485,"depth":171,"text":1485},{"id":1569,"depth":171,"text":1569},{"id":1615,"depth":171,"text":1615},{"id":1659,"depth":171,"text":1659},{"id":1708,"depth":171,"text":1708},"tips","2026-08-04","把 TS 分片拼成 MP4 不是轉碼——是重封裝。講清楚 fMP4 結構、為什麼 init 分片是命脈、純 JS 怎麼實作，以及和 FFmpeg WASM 的效能對比。","md","\u002Fscreenshots\u002Fformat-conversion.png",{},"\u002Fzh-hant\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff",{"title":5,"description":1782},"zh-Hant\u002Fblog\u002Fbrowser-streaming-remux-mp4-isobmff",[1790,597,1791,1792,1793],"remux","isobmff","wasm","深度","E19q3_2ZzNW8xZSonLW-xY_nEGfs5Z8oLSmfrIFzR_4",[1796,1803],{"title":1797,"path":1798,"stem":1799,"description":1800,"date":1801,"category":1802,"children":-1},"如何批次下載網頁上的圖片","\u002Fzh-hant\u002Fblog\u002Fbatch-download-images-from-any-website","zh-Hant\u002Fblog\u002Fbatch-download-images-from-any-website","用 FlowPick 內建圖片下載器一次抓取網頁上的幾十張圖，支援按檔案大小和格式篩選，不用裝任何桌面軟體。","2026-07-01","tutorials",{"title":1804,"path":1805,"stem":1806,"description":1807,"date":1781,"category":1780,"children":-1},"瀏覽器影片處理效能基準：FFmpeg WASM vs WebCodecs vs 原生","\u002Fzh-hant\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","zh-Hant\u002Fblog\u002Fbrowser-video-processing-performance-benchmarks","真實硬體上的真實資料——1080p\u002F4K\u002F8K 重封裝和解碼基準，橫跨 FFmpeg WASM、WebCodecs、原生 FFmpeg。附方法論、原始資料、數字到底意味著什麼。",1787670589703]