踩坑ffmpeg錄製的mp4無法在瀏覽器上播放

前言

使用ffmpeg編譯好的程序在電腦上進行音視頻轉換,可以參考這篇:《windows電腦FFmpeg安裝教程手把手詳解_windows安裝ffmpeg》,而我們要做的是在遊戲引擎中集成ffmpeg源碼用來錄製遊戲視頻。

我們遊戲目前只支持錄製avi格式的視頻,但是近期有個運營需求:在上架商品的時候在遊戲內錄製一段視頻提供給網頁端進行播放。

首先簡單的瞭解了一下,ffmpeg是支持錄製mp4格式的,於是簡短地改了幾行代碼就實現了錄製mp4,然後把錄製出來的視頻發給網頁同學部署測試。

第二天收到反饋我們錄製的視頻無法在網頁上播放,由於我也是第一次接觸ffmpeg,不知道爲什麼mp4無法在瀏覽器上播放,整個過程就是不斷通過chatgpt查閱資料,不斷修改代碼調試,最終在某個夜晚跑通了。

問題:瀏覽器上無法播放mp4

我們遊戲錄製出來的mp4,右鍵 - 打開方式,選擇瀏覽器,或者直接拖動mp4文件到瀏覽器裏面,讓它打開,表現爲無法播放

image-20231227151352847

查看視頻詳細信息

通過ffmpeg工具提供的一些指令用來查看視頻的詳細信息,有助於調試

ffprobe -v quiet -print_format json -show_format -show_streams 你的文件名

ffprobe -v quiet -print_format json -show_format -show_streams video.mp4

查看視頻每一幀的信息:

ffprobe -show_packets -of xml -i video.mp4

使用ffmpeg將mp4轉爲h264文件

我的第一個驗證想法,使用ffmpeg把遊戲錄製的視頻轉換看看轉換之後的視頻是否在瀏覽器上播放,結果:轉換後就可以在瀏覽器中就可以播放了

指令:ffmpeg -i video.mp4 -vcodec h264 -crf 10 test.mp4

說明:-crf 的數值是0~51,代表壓縮等級,值越大畫質越差,體積越小

使用h264編碼

通過chatgpt查閱資料,瞭解到需要把mp4使用h264編碼,於是就改了這個接口,這樣來看格式雖然是h264了,但是仍然無法在網頁上播放

avformat_alloc_output_context2(&oc, NULL, NULL, file_name);
//把第三個參數,輸出格式強制指定爲H264
avformat_alloc_output_context2(&oc, NULL, "h264", file_name);

對比差異

既然通過格式轉換是可以播放的,那就對比一下兩個視頻文件的詳細對比差異,差異如下所示:

image-20240107141519087

可以播放的視頻 遊戲錄製的視頻
"codec_tag_string": "avc1", "codec_tag_string": "[0][0][0][0]",
"is_avc": "true", "is_avc": "false",

遊戲錄製的缺少了以下部分字段

"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 45824,
"duration": "3.580000",
"bit_rate": "2161030",
"nb_frames": "179",

手動設置codec_tag

一開始我的重點方向是:codec_tag

在 MP4 文件中,codec_tag 是一個用於標識視頻和音頻編解碼器的標籤。它通常是一個四個字母的代碼,例如“avc1”表示 H.264 視頻編解碼器,“mp4a”表示 AAC 音頻編解碼器。codec_tag 可以幫助播放器確定正確的解碼器來解碼視頻和音頻流。在使用 MP4 文件時,確保你的播放器支持所使用的 codec_tag。

然後使用chatgpt查到示例代碼加到遊戲內但代碼編譯不通過,原因我們是自己編譯的ffmpeg.lib,還需要修改export才能用某些接口,這個問題後面再說

但是手動設置tag之後,問題依賴存在

斷點查證

在斷點的時候發現調用:avcodec_find_encoder傳入的格式並不是h264,而是mpeg4

image-20231227144727599

於是手動在上面添加了一 行用來修改編碼格式,但是還是一樣的結果

其實在這個時候,我還是有些分不清楚codec_tag和codec的關係

查一些正確的示例

所以讓chatgpt給我舉例一些使用ffmpeg來編碼h264的視頻,然後對照我們的代碼來分析是問題出在那裏,後面瞭解到某位同事對ffmpeg比較懂,已於向他請教,大大加速了查證過程。

視頻每一幀的數據中無pts和dst

ffprobe -show_packets -of xml -i video.mp4,使用這個指令來查看視頻中每幀的數據,發現錄製出來的視頻沒有pts和dst

<ffprobe>
Input #0, h264, from 'video.mp4':
  Duration: N/A, bitrate: N/A
  Stream #0:0: Video: h264 (Main), yuv420p(progressive), 1904x1002 [SAR 1:1 DAR 952:501], 25 fps, 100 tbr, 1200k tbn
    <packets>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="34080" pos="0" flags="K__"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="4995" pos="34080" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="817" pos="39075" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="428" pos="39892" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="89" pos="40320" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="169" pos="40409" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="75" pos="40578" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="71" pos="40653" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="71" pos="40724" flags="___"/>
        <packet codec_type="video" stream_index="0" duration="24000" duration_time="0.020000" size="672" pos="40795" flags="___"/>
    </packets>
</ffprobe>

正常情況下每幀的視頻數據中是有pts和dst的,於是在寫入每幀數據時手動給pts和dst賦值

av_packet_rescale_ts(pkt, *time_base, st->time_base);
pkt->stream_index = st->index;
this->video_st.write_size += pkt->size;
int step = 1000 / this->fps;//ptk.pts每幀加的數值=1000/幀率
pkt->pts = step * m_frameNum;
pkt->dts = step * m_frameNum;
/* Write the compressed frame to the media file. */
return av_interleaved_write_frame(fmt_ctx, pkt);

在av_interleaved_write_frame之前寫入pts,之後pts的值就無效了

手動計算pts播放起來非常卡

這樣改完之後pts的值是有了,但是播放速度不正常,表現會非常卡,原因就是pts計算錯誤

image-20240107142856666

assert中斷

試過強制修改codec_id,但是碰到問題:在結束錄製的時候會中多線程的Assert導致錄製得到的視頻數據是空的,代碼:lib\cstdmf\concurrency.hpp void grab() MF_ASSERT(id_ != gid);

讓ffmpeg自動選擇輸出格式

反覆閱讀代碼然後不斷嘗試,發現在調用avformat_alloc_output_context2接口不指定格式,而只給輸出文件的後綴,錄製出來的視頻是有pts和dts的!!!

這一下就回到最初的地方,在最早的時候我就是沒有設置這個選項的。

image-20240105200613571

接口定義和解釋如下:

int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename);

ctx:輸出格式上下文的指針,函數執行成功後會將創建的上下文賦值給該指針。
oformat:輸出格式,可以爲 NULL,表示讓 FFmpeg 自動選擇輸出格式。
format_name:輸出格式的名稱,可以爲 NULL,表示讓 FFmpeg 自動選擇輸出格式。
filename:輸出文件名,可以爲 NULL,表示不需要輸出到文件。

pts有了但格式是mpeg4

然後修改接口,再次編譯驗證,這一次pts在視頻幀數據中有了,但爲啥視頻格式會變成mpeg4???

測試了一下mpeg4在瀏覽器上也無法播放,但是可以在win10自帶的播放上可以播放。

mp4好了但avi壞了

查看代碼,確實有一處地方指定了mpeg4,代碼:video_codec = avcodec_find_encoder(AV_CODEC_ID_MPEG4);

於是把它改爲video_codec = avcodec_find_encoder(AV_CODEC_ID_H264);,這樣終於好了。

再來驗證一下以前的錄製avi格式,結果發現壞了,以前的avi格式錄製不了,斷點一下,發現中上面提到的assert了。盲猜是avi不能使用h264,於是修改了一下mp4使用h264,其它格式使用mpeg4,再編譯驗證,這樣就好了。

總結一下:

  1. 初始化avformat_alloc_output_context2不要指定格式,讓ffmpeg自動調用,但需要輸出的文件後綴爲mp4
  2. 調用avcodec_find_encoder把mp4設置h246格式,但是對於avi改爲AV_CODEC_ID_MPEG4
  3. 指定視頻的文件頭
  4. 其它地方不要再手動去修改codec_id,否則在結束錄製的時候會出錯,導致視頻爲空
  5. 多觀察ffmpeg每一個接口的返回值,特別是非成功的情況下要進行處理

這幾個接口需要關注返回值是否成功:

  1. avformat_alloc_output_context2 初始化
  2. avcodec_find_encoder 查找編碼器的函數
  3. avformat_write_header 寫入視頻的header
  4. av_interleaved_write_frame 寫入視頻每一幀的數據
  5. av_write_trailer 結束錄製

參考內容

ffmpeg實現將H264裸流封裝成.mp4或.avi文件_ffmpeg對裸流封裝-CSDN博客

[原]零基礎學習視頻解碼之FFMpeg中比較重要的函數以及數據結構 - 雪夜&流星 - 博客園 (cnblogs.com)

FFmpeg從入門到入魔(3):提取MP4中的H.264和AAC - 掘金 (juejin.cn)

YUV編碼爲H264 H264封裝爲MP4 - 知乎 (zhihu.com)

使用工具

MP4封裝格式—音視頻基礎知識 · FFmpeg原理 (xianwaizhiyin.net)

下載 Mp4 Explorer (apponic.com)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章