
簡介MP4Box.js 是一款基于開源多媒體處理框架的 JavaScript 工具庫主要面向 Web 前端與音視頻開發者。它能夠在瀏覽器端完成 MP4 文件的元數據解析讀取視頻分辨率、編碼方式、音頻采樣率與聲道等關鍵信息也可以對文件進行切片配合媒體源擴展機制實現分段加載與平滑播放同時支持抽取視頻幀并生成文本軌道用于添加字幕或輔助說明從而降低對服務器端轉碼的依賴。壓縮包內共 45 個文件整體大小約 407KB以 js 核心腳本為主體包含 mp4box.js、mp4box.all.js 等庫文件其余文件覆蓋網頁示例頁面、樣式表、配置文件、說明文檔以及樣例數據從調用示例到構建配置均有提供目錄結構清晰便于開發者快速定位并驗證功能。目前已有 667 人學習下載。借助這些源碼和配套文件讀者可以深入理解 MP4 盒結構、分片流程與文本軌生成原理并直接改造示例代碼在真實項目中實現視頻信息讀取、按需切片、字幕添加等功能是希望掌握瀏覽器端視頻處理能力的中高級前端工程師值得參考的工具包。1. mp4box.js 做 MP4 切片先把重封裝和轉碼分開MP4 切片這個詞經常被理解成「把視頻剪成幾段」實際上在播放器場景里它指的是把一整塊 moovmdat 的普通 MP4 重封裝成 fragmented MP4fMP4拆出一小段 init segmentstypmoovsidx和一段段 media segmentmoofmdat。干這件事最省事的前端方案是 mp4box.js——GPAC 的 MP4Box 工具用 Emscripten 編譯成的 JavaScript 版本瀏覽器和 Node 都能跑。它不做轉碼不碰像素數據只做容器層面的拆裝所以速度遠快于后端 FFmpeg 轉 HLS。適合的場景包括前端視頻預覽、上傳前分片、去服務端化的推送和播放鏈路以及 Electron 工具里的離線視頻處理。切分質量取決于輸入是不是合法的 MP4最好 H.264/AAC以及你對 track 和關鍵幀這兩個概念的理解。下面從最小可用流程講起。2. 用 mp4box.all.js 跑通「讀文件→切段」的最小流程2.1 引入 mp4box.all.js瀏覽器 script 與 Node requiremp4box.js 實際發布的核心文件就是mp4box.all.js。這是一個 UMD 打包產物內部把 BoxParser、ISOFile、DataStream、Log 全部掛在一個全局對象上。瀏覽器里直接script srcmp4box.all.js/script script const MB window.MP4Box; const file MB.createFile(); /scriptNode 或打包器環境里npm 包名是mp4box實際加載的還是 dist 下同一個文件const { MP4Box } require(mp4box);注意這里 MP4Box 是一個命名屬性不是默認導出個別構建工具因為 UMD 的 global 掛載方式會拿成{ MP4Box: ... }的默認對象解構時留意一下即可。2.2 完整切片代碼一次 appendBuffer 的最小工程下面這段代碼可以直接跑輸入一個普通 MP4輸出 init.m4s 和按序排列的 media segmentconst fs require(fs); const { MP4Box } require(mp4box); const input fs.readFileSync(input.mp4); const mp4 MP4Box.createFile(); const outDir ./out; fs.mkdirSync(outDir, { recursive: true }); // Node 的 Buffer 轉成 ArrayBuffer注意 byteOffset 的偏移 const box input.buffer.slice(input.byteOffset, input.byteOffset input.byteLength); mp4.onError (e) console.error([mp4box], e); mp4.onReady (info) { console.log(duration(ms):, info.duration); console.log(mimeCodec:, info.mimeCodec); // init segment 必須最先落盤 fs.writeFileSync(${outDir}/init.m4s, Buffer.from(mp4.getInitSegment())); // 給視頻 track 開切片目標 2 秒一段并貼合關鍵幀 info.videoTracks.forEach((t) { mp4.setSegmentOptions(t.id, { type: video }, { rapAlignement: true, duration: 2000 }); }); info.audioTracks.forEach((t) { mp4.setSegmentOptions(t.id, { type: audio }, { duration: 2000 }); }); mp4.start(); }; let videoCount 0; let audioCount 0; mp4.onSegment (id, user, buffer, sampleNum, isLast) { const ext String(sampleNum).padStart(6, 0); const name user.type video ? ${outDir}/video-${ext}.m4s : ${outDir}/audio-${ext}.m4s; fs.writeFileSync(name, Buffer.from(buffer)); if (user.type video) videoCount; else audioCount; console.log([${user.type}] ${name}, ${buffer.byteLength} bytes, isLast${isLast}); }; mp4.appendBuffer(box); mp4.flush(); console.log(segments: video videoCount , audio audioCount);這段代碼的邏輯是createFile 先創建一個解析實例它不直接讀文件而是接收你喂進來的 ArrayBuffer/Uint8ArrayonReady 在 moov 解析完成后觸發一次此時能拿到時長、編碼、track 列表接著調用 getInitSegment() 把播放必需的頭部數據導出再用 setSegmentOptions 給每個要切的 track 聲明分片規則最后 start() 啟動切片輸出。數據喂完后再 flush()把緩沖區內尚未輸出的末尾 sample 強制推完否則最后一段可能丟在內存里。幾個參數的說明duration: 2000 表示目標段時長 2 秒mp4box 內部會換算成該 track 的 timescale 下的樣本數所以你不用關心視頻 90k 和音頻 48k 的時間基差異。rapAlignement: true 讓段邊界貼合關鍵幀這是視頻 track 必開項具體行為見 3.2。user 對象{ type: video }原樣透傳給 onSegment用于區分輸出來自哪條 track。onSegment 的 sampleNum 是同一條 track 內累計的 sample 數不是段序號用它拼文件名只是方便isLast 表示是不是該 track 最后一段。提示Node 里Buffer.from(uint8Array)會復制數據這是安全的但如果你把 onSegment 的 buffer 塞進隊列等異步處理先復制再傳避免后續 buffer 被內部復用時覆蓋。對于瀏覽器端把 fs 換成收集 Uint8Array 數組最后new Blob(parts, { type: video/mp4 })再觸發下載邏輯完全一樣。2.3 init segment 和 media segment 的區別先記住三句話很多人在這一步容易栽onSegment 回調永遠只會收到 media segmentinit segment 必須主動調 getInitSegment() 拿一次順序還要排在所有 media segment 之前。三句話init segment 是地圖stypmoovsidxmedia segment 是路上的車廂moofmdat沒有地圖車廂就是一串無法解碼的二進制地圖重復發比如每次 append 前都重新 getInitSegmentMSE 也會報錯只發一次。后面第 4 章接入 MSE 時順序錯了就是最常見的黑屏原因。2.4 大文件的讀入方式別一次 arrayBuffer 到底fetch 場景下新手最容易寫const buf await res.arrayBuffer(); mp4.appendBuffer(buf);。文件在幾十 MB 內沒問題超過 200MB 就是一次內存翻倍原始響應 mp4box 內部解析緩沖。更穩的做法是把讀取和 append 串起來const reader res.body.getReader(); let done false; while (!done) { const r await reader.read(); done r.done; if (r.value) mp4.appendBuffer(r.value); } mp4.flush();注意不要為了等 onReady 而把數據攢著不 append。moov 在文件尾時前 N 個 chunk 可能一直不觸發 onReady等 onReady 之后再 start()前面的數據已經在 mp4box 內部緩沖排隊了。也就是說「先收集后處理」由 appendBuffer 內置完成代碼不需要自己攢。3. 切片參數怎么調時長、關鍵幀對齊與 track 選擇3.1 setSegmentOptions 的三個有效參數mp4.setSegmentOptions(trackId, user, { duration: 2000, // 目標段時長毫秒 nbSamples: undefined, // 精確 sample 數 rapAlignement: true, // 對齊隨機訪問點 });參數類型作用建議值durationnumber目標段時長毫秒按 track.timescale 換算為樣本數視頻 1000~6000按 GOP 整數倍nbSamplesnumber每段固定包含的 sample 數適合 sample 時長不規律的流視頻一般不設rapAlignementboolean段首是否必須是關鍵幀RAP視頻必開音頻可關duration 換算的底層邏輯每段樣本數 duration × track.timescale / 1000。如果這個數除不盡mp4box 向下取整所以實際段長會比設定值略短如果你的源視頻 GOP 是 2 秒duration 卻是 1500ms開了 rapAlignement 后段長會被吸附到 2 秒的整數倍邊界出來的段是 2 秒而不是 1.5 秒這不是 bug是「段首必須是關鍵幀」優先級高于「精確時長」的設計。3.2 rapAlignement 到底對齊的是什么普通 MP4 里每個 sample 是否關鍵幀記錄在 stss box。mp4box 切段時先按樣本數把軌道切成 sample 區間再檢查區間第一個 sample 的 sync 標志如果它不是關鍵幀就往回挪到最近的關鍵幀作為段起點。這個「往回挪」的行為就叫 rapAlignement。關掉它的后果是段首畫面解碼不出來MSE 的第一幀要么黑屏要么花屏而且會延續到后面所有依賴該關鍵幀的段。所以視頻 track 永遠設 true。一個常見誤判是ffprobe 看到第一個段開頭是 K就認為所有段都對齊了。實際上只有段首 sample 的 flags 帶 K 才有意義所有 media segment 的首 sample 都必須帶 K否則單段播放會失敗。檢查當前段是否對齊的驗證方式我一般這樣看把 init.m4s 和某個 video-xxx.m4s 拼一起用 ffprobe 看 packet flags段邊界應該是 K具體命令見 4.4。3.3 只切需要的 trackid 不是索引項目里常見只做預覽、不需要音頻或者反過來只抽音頻做波形。onReady 里拿到的是完整 track 列表你只對需要的 track 調 setSegmentOptions其余 track 不會輸出任何 segment。注意info.videoTracks[0].id通常是 1但不要寫成setSegmentOptions(0, ...)id 是 MP4 容器內的 track_id和數組下標不是一個東西一個視頻文件里如果有多個視頻軌比如主片和預告片合并的文件靠 id 選擇才是對的。3.4 音頻 track 的段邊界與視頻不必強對齊音頻AAC每個 sample 獨立可解碼不需要 RAP 概念所以音頻的 segment 常常比視頻多一段或少一段邊界對不上是正常的。MSE 里音視頻同步靠 PTS不是靠段序號只要每個段內 PTS 連續播放就不會亂。真要嚴格對齊比如做 CMAF得保證兩個 track 的段邊界都在同一時間點做法是把 duration 設成視頻 GOP 的整數倍并且音頻與視頻都開 rapAlignement——對音頻來說這個開關只是讓段首為可解碼樣本無副作用。mp4box 本身不保證跨 track 強對齊這是它的一個邊界需要強對齊時你得在 onSegment 回調里自己按 PTS 判斷是否再合并相鄰段。順便提一句樣本級操作如果要做更細的「提取某段內所有 sample」的分析工作用 setExtractionOptions onSamples而不是 setSegmentOptions。前者輸出 sample 數組每個含 data、cts、dts、is_sync適合做樣本級校驗后者輸出的是可直接播的 segment。兩個 API 可以同時工作但同一個 track 別同時開否則 onSegment 和 onSamples 會互相干擾。4. 把切片結果接入 MSE驗證切片能不能播4.1 用 info.mimeCodec 拿 addSourceBuffer 的 mime 參數MSE 的 addSourceBuffer 需要一個完整 type形如video/mp4; codecsavc1.64001f,mp4a.40.2。mp4box 的 onReady 已經把 codec 字符串拼好放在info.mimeCodec直接用不要自己從文件名推斷擴展名。實際項目里這一步經常踩坑的點是codec 字符串里的 profile/level比如avc1.64001f必須和實際樣本嚴格一致手拼容易少寫逗號或者寫錯 leveladdSourceBuffer 直接拋 NotSupportedError。4.2 SourceBuffer 追加順序與 updateend 隊列const mediaSource new MediaSource(); video.src URL.createObjectURL(mediaSource); mediaSource.addEventListener(sourceopen, () { const sb mediaSource.addSourceBuffer(mimeCodec); sb.appendBuffer(initSegment); // 先地圖 const pending mediaSegments.slice(); // 車廂按順序排隊 const pump () { if (!pending.length) return; sb.appendBuffer(pending.shift()); }; sb.addEventListener(updateend, pump, { once: true }); pump(); });注意SourceBuffer 同一時刻只能有一個 append 在進行第二次 appendBuffer 必須等上一次 updateend 事件觸發上傳場景里常見的是把用戶拖進來的 File 用 FileReader 讀成 ArrayBuffer逐個 append事件鏈不要漏。另外 SourceBuffer 有內存配額Chrome 默認約 80%~85% 可用內存把所有段一次全 append 進去會拋 QuotaExceededError正確做法是按需 append播到第 N 段時再往隊列里加第 N1 段或者移除已播放段用sourceBuffer.remove(0, buffered.end(0))配合 timeupdate 事件維護一個滑動窗口窗口外的時間區間及時清掉長時間直播場景尤其需要。4.3 用「拼回完整文件」交叉驗證切片一致性這是我最常用的驗證把 init.m4s 和所有 media segment 按序 cat 成一個 mp4然后丟回 mp4box 再解析一次比較前后 duration 和 sample 總數。如果一致說明切片過程沒丟幀如果后者的 duration 比源文件小多半是最后一段沒有 flush第 2 章的坑或者 setSegmentOptions 的 duration 沒覆蓋到尾部余量。cat init.m4s video-*.m4s audio-*.m4s joined.mp4 ffprobe -show_entries formatduration -v quiet joined.mp4cat 順序里 audio/video 段交錯與否不影響 mp4box 解析但會影響部分播放器驗證用無所謂實際播放交給 MSE 按 track 分開處理。4.4 ffprobe 檢查段邊界關鍵幀標志ffprobe -select_streams v:0 -show_packets -show_entries packetpts,dts,flags -of csv joined.mp4看每個段開頭的 packet flags 是否含K。如果沒有 K說明 rapAlignement 沒生效。再補一個判斷打印每段首尾 dts連續兩段之間 dts 應該連續沒有跳變或重疊。dts 出現跳變通常是 B 幀導致的解碼順序問題看 pts 與 dts 差值是否穩定在一個 max_b_frames 數量上排查時比看黑屏直觀得多。5. MP4 切片常見的三個高頻坑和一條經驗驗證法5.1 moov 在尾部時 onReady 延遲點播 MP4 的 moov 可以在文件頭也可以文件尾。在線服務給的下載 URL 如果支持 range很多直接就把 moov 放末尾導致 onReady 到最后一個 chunk 才觸發。此時不要提前調用 start()start 只會處理當前已進內存的 box正確順序是 appendBuffer 一直喂到 onReadyonReady 里再 setSegmentOptions start。如果你在 onReady 之前調了 start()它只會把 moov 之前的數據當空 track 處理輸出結果為空。5.2 長生命周期頁面的內存釋放Electron 或長時間不刷新的 SPA 里反復 createFile舊 file 實例的數據不會被 GC 完全回收因為內部 DataStream 還持有 ArrayBuffer。處理完記得先mp4.stop()再mp4.releaseUsedSamples()然后把引用置空。stop 是停止后續分片輸出releaseUsedSamples 釋放已經處理完的 sample 緩沖配合 2.4 的分塊讀取這個組合能穩定控制內存峰值。5.3 輸入不是 MP4從 mpkg 到各種偽裝擴展名這個標題附近經常跟著「mpkg 文件怎么轉化 mp4」這類熱搜。mpkg 不是視頻格式它是 macOS 的安裝包里面可能封裝了 mp4但直接用 mp4box 解析會報 onError。遇到解析失敗先查魔數不用猜擴展名head -c 16 input.mp4 | xxd合法 MP4 的前 4 字節是 box size第 5 到 8 字節是ftyp比如isom、iso2、mp41、avc1。不是 ftyp 開頭就說明文件被包裝過mpkg 里要先解包取出真實 mp4dmg 里的文件要先掛載提取這類容器轉換不是 mp4box 的職責先做解包再談切片。5.4 單段播放驗證法把問題壓縮到某一個 segment排查「花屏/黑屏/卡某一段」時一個技巧是一次只播一段把 init.m4s 和某一段 media segment 拼成一個 Blob單獨用 video 標簽播放。這一段能播就說明該段以關鍵幀開頭、moof 內 sample 完整不能播優先查 rapAlignement 和此段的 PTS/DTS 是否連續或者用 4.4 的命令看 flags。const blob new Blob([initSegment, segments[3]], { type: video/mp4 }); const url URL.createObjectURL(blob); video.src url;一段一段試下去問題段的索引一般就是出錯樣本所在的段再配合 setExtractionOptions 的 onSamples 回調打印該段每個 sample 的 is_sync 和 dts就能精確定位是邊界吸附還是樣本缺失。這是沒有現成調試器時最快定位切片問題的路徑。本文還有配套的精品資源點擊獲取