發(fā):編碼選型與音畫(huà)同步工程實(shí)踐)
簡(jiǎn)介面向從零開(kāi)始學(xué)習(xí)音視頻編程的開(kāi)發(fā)者這套最終完善版的錄屏軟件開(kāi)發(fā)工程基于Qt 4.8.4與FFmpeg 2.5.2演示如何用FFmpeg完成屏幕采集、編碼與封裝并集成到界面中。壓縮包共289個(gè)文件含226個(gè)頭文件、8個(gè)C源文件、2個(gè)UI界面文件以及14個(gè)靜態(tài)庫(kù)、12個(gè)動(dòng)態(tài)庫(kù)和若干SDL依賴整體約17.43MB頭文件與源碼負(fù)責(zé)界面邏輯靜態(tài)庫(kù)、動(dòng)態(tài)庫(kù)處理音視頻編解碼與播放依賴目錄結(jié)構(gòu)清晰可配合Qt Creator直接打開(kāi)工程梳理代碼。已有1075人學(xué)習(xí)下載適合需要參考完整錄屏實(shí)現(xiàn)、研究FFmpeg調(diào)用方式或希望基于現(xiàn)成項(xiàng)目二次開(kāi)發(fā)的入門(mén)與中級(jí)開(kāi)發(fā)者。工程內(nèi)附界面布局與核心邏輯頭文件標(biāo)注了主要接口庫(kù)文件覆蓋了FFmpeg、SDL等常用依賴既能用于理解音視頻數(shù)據(jù)流也可作為環(huán)境配置與排錯(cuò)的對(duì)照樣本是一份難得的完整學(xué)習(xí)示例。1. 錄屏軟件開(kāi)發(fā)沒(méi)有想象中簡(jiǎn)單ffmpeg 只是起點(diǎn)封裝與同步才是分水嶺錄屏軟件這個(gè)項(xiàng)目做出來(lái)的“能跑”版本和“能交付”版本之間隔著一條很寬的溝。用 ffmpeg 拉桌面、推編碼器半小時(shí)能出一個(gè) Demo但真正到了需要長(zhǎng)時(shí)間錄制、音畫(huà)同步、文件可修復(fù)、CPU 占用可控的階段問(wèn)題會(huì)一個(gè)接一個(gè)冒出來(lái)。這個(gè)系列的最后一篇就把錄屏開(kāi)發(fā)里那些“就差最后一步”的完善工作講清楚從采集參數(shù)怎么定、編碼器怎么選到音畫(huà)不同步怎么排查、錄制文件損壞怎么兜底。適合已經(jīng)能用 ffmpeg 命令行錄出視頻、但想把工具做成產(chǎn)品的開(kāi)發(fā)者也適合正在調(diào)試錄屏功能卻總被詭異問(wèn)題卡住的從業(yè)者。2. 錄屏鏈路選型先定采集方案和編碼器再談功能完善2.1 各種采集方式的適用邊界gdigrab、x11grab、avfoundation 怎么選錄屏開(kāi)發(fā)第一步不是寫(xiě)代碼是確認(rèn)目標(biāo)平臺(tái)和畫(huà)面源類(lèi)型。常見(jiàn)的采集方案各有限制選錯(cuò)后面全是坑。Windows 上ffmpeg 的 gdigrab 是默認(rèn)選擇它基于 GDI 抓取屏幕簡(jiǎn)單、兼容性好但有兩個(gè)明顯短板一是對(duì) DirectX 游戲或者硬件加速渲染的畫(huà)面抓不到表現(xiàn)為黑屏或花屏二是幀率不穩(wěn)定尤其在多顯示器、動(dòng)態(tài)壁紙這類(lèi)場(chǎng)景下時(shí)間戳跳動(dòng)明顯。如果你只錄普通桌面、PPT、瀏覽器窗口gdigrab 夠用但你得把-framerate參數(shù)顯式傳給它否則默認(rèn)只有 25fps而且它抓取的是經(jīng)過(guò)合成后的桌面畫(huà)面跟用戶看到的是一致的。macOS 上對(duì)應(yīng)的是 avfoundation可以直接指定屏幕索引和麥克風(fēng)索引。Linux 上常見(jiàn)的是 x11grab配合 X server 的 MIT-SHM 擴(kuò)展能拿到較高性能的抓取。坦率講跨平臺(tái)錄屏如果想省事先按平臺(tái)分別封裝采集封裝器不建議一上來(lái)就搞統(tǒng)一抽象層因?yàn)槿齻€(gè)后端的參數(shù)語(yǔ)義差別不小強(qiáng)行統(tǒng)一的代價(jià)是調(diào)用鏈變長(zhǎng)出了問(wèn)題反而難排。如果你的目標(biāo)是錄制游戲或者高幀率畫(huà)面gdigrab 這條路就走不通了。Windows 上比較可靠的做法是用 DirectX 的 Desktop Duplication APIffmpeg 里對(duì)應(yīng)的設(shè)備是 dshow 配合采集 filter或者直接走 N 卡提供的 Capture SDK。但要注意這條路牽涉到幀同步和 GPU 資源管理復(fù)雜度明顯上了一個(gè)臺(tái)階。在“最終完善”這個(gè)階段我一般建議先把桌面采集做穩(wěn)游戲錄制作為獨(dú)立模塊往后放。2.2 編碼器選型libx264 與 nvenc 的取舍以及預(yù)設(shè)和碼率控制編碼器選型直接決定錄屏軟件的 CPU 占用和產(chǎn)物質(zhì)量。頭號(hào)選擇是 libx264軟編兼容性最好。代價(jià)是 CPU 占用高1080p30 下presetmedium時(shí)一顆 6 核處理器要吃掉大約 30%50% 的算力而且這是平均占用瞬時(shí)波動(dòng)更嚇人。preset參數(shù)越往后slow、slower走CPU 越吃緊壓縮率也越高。錄屏場(chǎng)景下我一般不用默認(rèn) medium而是用presetveryfast或faster視頻不太復(fù)雜時(shí)質(zhì)量損失很小但 CPU 壓力能降一個(gè)檔位。如果你的目標(biāo)機(jī)器有 N 卡或者 Intel 核顯、AMD 顯卡硬件編碼是更合理的選擇。ffmpeg 里用-c:v h264_nvenc或-c:v hevc_nvenc走硬編。硬編的好處不只是 CPU 占用低還在于“編碼耗時(shí)”穩(wěn)定這對(duì)長(zhǎng)時(shí)間錄制很重要。軟編在畫(huà)面復(fù)雜度突然升高時(shí)編碼器可能會(huì)因來(lái)不及處理而產(chǎn)生延遲導(dǎo)致時(shí)間戳異常硬編的吞吐量遠(yuǎn)高于實(shí)時(shí)要求所以幀間隔更均勻。碼率控制方面錄屏不推薦固定碼率。屏幕內(nèi)容大多是靜止或局部變化的靜態(tài)畫(huà)面用固定碼率純屬浪費(fèi)帶寬。我習(xí)慣用-crf模式libx264 給 1822nvenc 給 1824 之間再設(shè)置一個(gè)-maxrate做上限避免畫(huà)面劇烈變化時(shí)碼率失控。還有一個(gè)容易被忽略的參數(shù)是-gGOP 大小錄屏?xí)r建議設(shè)成幀率的整數(shù)倍比如 30fps 下-g 60這樣關(guān)鍵幀間隔穩(wěn)定后期剪輯時(shí)拖動(dòng)進(jìn)度條不那么卡。提示如果你不確定目標(biāo)機(jī)器有沒(méi)有硬件編碼能力發(fā)布前先在用戶機(jī)器上跑一次ffmpeg -encoders | findstr nvenc把它作為安裝包的“環(huán)境檢測(cè)”步驟。硬件編碼頻繁翻車(chē)多半是驅(qū)動(dòng)版本太老而不是代碼邏輯有問(wèn)題。2.3 顏色與像素格式為什么錄出來(lái)的顏色發(fā)灰或者飽和度不對(duì)錄屏過(guò)程里有個(gè)坑特別隱蔽畫(huà)面發(fā)灰、顏色偏淡。原因大多出在像素格式的轉(zhuǎn)換上。默認(rèn)情況下ffmpeg 會(huì)把采集到的畫(huà)面轉(zhuǎn)成 yuv420p這個(gè)格式是視頻編碼的標(biāo)準(zhǔn)輸入但它在色彩范圍的理解上有個(gè)細(xì)節(jié)——TV rangelimited range數(shù)值范圍 16235與 PC rangefull range0255。屏幕內(nèi)容是 PC range編碼器卻按 TV range 處理時(shí)暗部被裁掉變成黑白墻變成灰看起來(lái)就是整體發(fā)蒙。處理方式有兩個(gè)思路。其一是在編碼前把 pixel format 顯式標(biāo)成 yuv420p 并追加-color_range pc讓編碼器按完整范圍理解數(shù)據(jù)。這個(gè)改動(dòng)的風(fēng)險(xiǎn)是播放器不一定尊重這個(gè) flag有的播放器照樣按 TV range 渲染反而更糟。更穩(wěn)的做法是在采集端設(shè)置 GPU 或系統(tǒng)級(jí)色彩配置但這對(duì)錄屏工具不現(xiàn)實(shí)。所以在實(shí)際項(xiàng)目里我一般默認(rèn)輸出 yuv420p保持兼容性如果用戶反饋顏色不對(duì)再提供一個(gè)“增強(qiáng)顏色”開(kāi)關(guān)內(nèi)部做一次 colorspace 轉(zhuǎn)換而不是讓這個(gè)轉(zhuǎn)換默認(rèn)開(kāi)啟。還有一個(gè)相關(guān)的點(diǎn)不要在采集端就用 RGB 直接編碼。h264 等主流編碼器對(duì) RGB 的支持是有的但兼容性差播放器支持參差不齊。硬編碼進(jìn) RGB 視頻用戶拿到文件可能直接花屏。3. 搬運(yùn)工與總管把 ffmpeg 當(dāng)模塊用而不是當(dāng)命令行工具用3.1 進(jìn)程 vs 動(dòng)態(tài)庫(kù)錄屏軟件承載方式的現(xiàn)實(shí)選擇很多剛接觸這個(gè)方向的開(kāi)發(fā)者會(huì)把 ffmpeg 理解成一個(gè)“可執(zhí)行的命令行工具”。這沒(méi)錯(cuò)但從軟件開(kāi)發(fā)的角度看你要考慮的是你的錄屏程序是殼它調(diào)度 ffmpeg 進(jìn)程還是 ffmpeg 是庫(kù)你的程序把采集、編碼、封裝邏輯全部揉進(jìn)自己的進(jìn)程里。進(jìn)程方案的優(yōu)勢(shì)非常明顯解耦。ffmpeg 崩潰不會(huì)帶崩你的主程序最多是錄制中斷升級(jí) ffmpeg 只要替換二進(jìn)制文件不需要重新編譯。通過(guò)解析標(biāo)準(zhǔn)輸出來(lái)監(jiān)控進(jìn)度也簡(jiǎn)單-progress pipe:1就能把幀數(shù)、碼率、時(shí)間戳推給父進(jìn)程。缺點(diǎn)是進(jìn)程間通信有開(kāi)銷(xiāo)以及控制粒度稍粗比如想動(dòng)態(tài)切換編碼參數(shù)就得重建進(jìn)程。動(dòng)態(tài)庫(kù)方案SDK 方式控制粒度細(xì)能拿到每一幀的回調(diào)可以做自定義水印、動(dòng)態(tài)字幕、局部錄屏高亮適合功能深度集成。代價(jià)是 ffmpeg 的 API 復(fù)雜度高內(nèi)存管理、線程同步、異常處理都得自己兜底。我見(jiàn)過(guò)不少項(xiàng)目在 “ffmpeg 庫(kù)方式接入” 上卡了兩三周最后退回進(jìn)程方式實(shí)現(xiàn)速度反而更快。我的建議是第一版錄屏工具用進(jìn)程方案把精力集中在錄制邏輯本身等功能穩(wěn)定后再按需把高頻路徑切到動(dòng)態(tài)庫(kù)。真到了非庫(kù)不可的場(chǎng)景你往往會(huì)發(fā)現(xiàn)你要的不是庫(kù)而是要對(duì)視頻流做實(shí)時(shí)處理那正確路線是引入 filter 或 GPU 算子而不是把整個(gè) ffmpeg 請(qǐng)進(jìn)內(nèi)存。3.2 命令行拼裝的工程化參數(shù)模板與錯(cuò)誤識(shí)別進(jìn)程方案里最容易埋雷的兩個(gè)地方一個(gè)是參數(shù)拼裝一個(gè)是錯(cuò)誤解析。參數(shù)拼裝方面我強(qiáng)烈建議用結(jié)構(gòu)化方式組織 ffmpeg 命令行而不是用字符串拼接。比如用std::vectorstd::string或 Python 的list把每個(gè)參數(shù)作為一個(gè)獨(dú)立元素最后統(tǒng)一exec。這樣做的好處是能避免路徑里出現(xiàn)空格時(shí)參數(shù)斷裂也能方便在 debug 模式打印出完整命令復(fù)現(xiàn)問(wèn)題。以下是一個(gè)典型的進(jìn)程式錄屏封裝段偽代碼示意import subprocess def build_record_cmd( output_file: str, framerate: int 30, preset: str veryfast, crf: int 20, audio_device: str | None None, ) - list[str]: cmd [ ffmpeg, -hide_banner, -loglevel, error, -f, gdigrab, -framerate, str(framerate), -video_size, 1920x1080, -i, desktop, ] if audio_device: cmd [ -f, dshow, -i, faudio{audio_device}, ] cmd [ -c:v, libx264, -preset, preset, -crf, str(crf), -pix_fmt, yuv420p, -g, str(framerate * 2), -c:a, aac, -b:a, 128k, -y, output_file, ] return cmdcmd 是一個(gè)列表每項(xiàng)都是一個(gè)獨(dú)立參數(shù)包括輸入設(shè)備名、輸入路徑、編碼設(shè)置和輸出路徑。關(guān)鍵在于不使用 shellTrue直接用列表傳參從而繞開(kāi) shell 轉(zhuǎn)義導(dǎo)致路徑含空格或特殊字符時(shí)命令被拆散的問(wèn)題。-hide_banner可以避免輸出里混入版本信息干擾解析-loglevel error讓錯(cuò)誤輸出干凈方便程序捕獲。錯(cuò)誤解析方面最核心的一條經(jīng)驗(yàn)是不要用返回值判斷成功要用輸出文件的存在性和時(shí)長(zhǎng)來(lái)兜底。ffmpeg 進(jìn)程退出碼為 0 只能說(shuō)明它“自己認(rèn)為正常結(jié)束了”但錄制時(shí)間過(guò)短、音頻設(shè)備斷流、磁盤(pán)空間不足導(dǎo)致尾部文件寫(xiě)失敗這類(lèi)情況退出碼很可能依然是 0。所以我的檢查邏輯是先看進(jìn)程返回碼再看文件是否存在、時(shí)長(zhǎng)是否超過(guò)預(yù)期閾值兩者同時(shí)通過(guò)才算成功。3.3 錄制的生命周期控制開(kāi)始、暫停、停止的進(jìn)程管理進(jìn)程方式下“暫停”是個(gè)需要小心的功能。用戶點(diǎn)了暫停你以為只是不寫(xiě)幀了但 ffmpeg 進(jìn)程還活著緩沖還在漲時(shí)間戳還在走。如果直接停掉進(jìn)程再起一個(gè)新的輸出文件會(huì)有一個(gè)斷層后續(xù)拼接時(shí)音畫(huà)可能對(duì)不上。比較務(wù)實(shí)的處理方式是暫停時(shí)不中斷進(jìn)程而是“凍結(jié)畫(huà)面”后讓編碼器繼續(xù)工作或者干脆把暫停實(shí)現(xiàn)為“停止本次錄制生成片段文件”恢復(fù)時(shí)另起一個(gè)分段最后統(tǒng)一用 concat demuxer 合并。后者雖然多了一個(gè)合并步驟但邏輯清晰出錯(cuò)后更容易定位。很多成熟的錄屏工具也是這么做的——它們?cè)谖募飳?xiě)的是分段索引而不是真的去拼接底層流。停止流程同樣要看錄像時(shí)間和寫(xiě)入緩沖。ffmpeg 收到 SIGINT 或要求正常結(jié)束時(shí)會(huì)先把編碼器 flush把緩沖里的幀寫(xiě)完再進(jìn)行文件 box 的收尾。有些開(kāi)發(fā)者直接 kill 進(jìn)程結(jié)果文件時(shí)長(zhǎng)短一截播放到尾部直接卡死。正確做法是先向進(jìn)程發(fā)送終止指令再等待超時(shí)超時(shí)未退出才強(qiáng)制清理。4. 音頻采集與混音從靜音到回聲問(wèn)題往往不在編碼而在設(shè)備4.1 音頻設(shè)備鏈路的正確姿勢(shì)麥克風(fēng)、立體聲混音與系統(tǒng)聲音錄屏如果不做音頻采集那功能是不完整的。但音頻恰恰是錄屏軟件里最容易出現(xiàn)“偽成功”的模塊——錄出來(lái)的文件能播、有聲音可聲音是空的或者只有麥克風(fēng)沒(méi)有系統(tǒng)聲用戶一用就會(huì)發(fā)現(xiàn)。在 Windows 上系統(tǒng)聲音的采集通常依賴“立體聲混音”設(shè)備。這個(gè)設(shè)備默認(rèn)是禁用的需要用戶在錄音設(shè)置里手動(dòng)開(kāi)啟程序?qū)用鏇](méi)法繞過(guò)。如果開(kāi)發(fā)時(shí)直接用-f dshow -i audio立體聲混音去錄而用戶沒(méi)開(kāi)混音ffmpeg 會(huì)直接報(bào)錯(cuò)或無(wú)限等待。比較穩(wěn)妥的做法有兩條路第一安裝驅(qū)動(dòng)時(shí)加一個(gè)虛擬音頻設(shè)備如 VB-Audio 類(lèi)的驅(qū)動(dòng)把系統(tǒng)輸出重定向到虛擬設(shè)備上再采集第二也是更常見(jiàn)的選擇用 WASAPI 的 loopback 模式直接采集系統(tǒng)輸出不需要依賴混音設(shè)備是否開(kāi)啟。ffmpeg在 Windows 下可以通過(guò) dshow 的audiovirtual-audio-capturer等虛擬設(shè)備來(lái)采集但這屬于依賴第三方驅(qū)動(dòng)。就平臺(tái)原生能力而言我的建議是Windows 錄屏工具的音頻采集走 WASAPI loopback 實(shí)現(xiàn)ffmpeg 只負(fù)責(zé)把 PC 的采集回調(diào)數(shù)據(jù)寫(xiě)進(jìn)編碼器。這樣繞開(kāi)了“用戶沒(méi)開(kāi)啟立體聲混音”這個(gè)變量行為可預(yù)期。macOS 上簡(jiǎn)單一些avfoundation 原生能枚舉出屏幕和音頻輸入設(shè)備但系統(tǒng)聲音默認(rèn)拿不到需要安裝一個(gè)虛擬聲卡驅(qū)動(dòng)做路由。Linux 這邊則依賴 PulseAudio 的 monitor 源pactl list sources能看到名為.monitor的虛擬源采集它就是采集聲卡輸出。4.2 雙音軌策略麥克風(fēng)與系統(tǒng)聲音分開(kāi)錄的意義很多錄屏軟件默認(rèn)把系統(tǒng)聲和麥克風(fēng)混成一條音軌輸出。這個(gè)設(shè)計(jì)對(duì)最終觀眾來(lái)說(shuō)是合理的但對(duì)錄制者來(lái)說(shuō)存在隱患如果某段系統(tǒng)聲里有通知提示音而麥克風(fēng)恰好錄到了不該出現(xiàn)的內(nèi)容后期想去掉就沒(méi)辦法了。稍微講究一點(diǎn)的方案是在錄制時(shí)就輸出雙音軌音軌一是系統(tǒng)聲音音軌二是麥克風(fēng)。編碼方面用 AAC 支持多音軌是沒(méi)問(wèn)題的封裝成 mkv 或 mp4 都行。ffmpeg 命令行里做雙音軌錄制核心是給每個(gè)音頻輸入顯式分配輸出流順序ffmpeg -f gdigrab -framerate 30 -i desktop \ -f dshow -i audio麥克風(fēng)輸入 \ -f dshow -i audio立體聲混音 \ -map 0:v -map 1:a -map 2:a \ -c:v libx264 -preset veryfast -crf 20 \ -c:a aac -b:a 128k \ -metadata:s:a:0 titleMicrophone \ -metadata:s:a:1 titleSystem \ -y output.mkv命令里-map 0:v表示取第一個(gè)輸入桌面的視頻流-map 1:a取麥克風(fēng)音頻-map 2:a取系統(tǒng)聲音三路流同時(shí)被送進(jìn)輸出容器。封裝的 mkv 文件里會(huì)有三條獨(dú)立的流播放器里可以手動(dòng)切換音軌。-metadata:s:a:0 title給每條音軌打上標(biāo)簽在后期剪輯軟件里一目了然。這樣設(shè)計(jì)還有一個(gè)隱形好處發(fā)布時(shí)只需要把麥克風(fēng)音軌刪掉就能生成一版不含隱私信息的版本比混音后再做聲音分離省事太多。4.3 回聲與音量平衡聲學(xué)問(wèn)題的軟件兜底方案如果錄制對(duì)象是“自己在講話 屏幕上有視頻或游戲聲”回聲幾乎是躲不掉的。系統(tǒng)聲音通過(guò)音箱播放麥克風(fēng)再把音箱的聲音采集回來(lái)形成回聲。ffmpeg 有一個(gè)afftdn降噪濾鏡和一個(gè)aresample重采樣工具但對(duì)回聲的處理并不理想真正能用的回聲消除器在 ffmpeg 里幾乎不存在。實(shí)踐中常用的兜底做法是在混音之前給麥克風(fēng)加一個(gè)高通濾波切除 80Hz 以下低頻和一個(gè)動(dòng)態(tài)壓縮器限制峰值再?gòu)南到y(tǒng)聲音那條軌上做側(cè)鏈衰減。這屬于民用級(jí)的“半回聲消除”效果有限但對(duì)大多數(shù)錄屏場(chǎng)景已經(jīng)夠看了。如果你的目標(biāo)用戶是專(zhuān)業(yè)內(nèi)容創(chuàng)作者那恐怕需要接入聲學(xué)算法庫(kù)這是另一個(gè)量級(jí)的工作量錄屏軟件一般不做。音量平衡方面我有兩個(gè)數(shù)字可以分享麥克風(fēng)增益到 -6dB 到 -3dB 之間是比較穩(wěn)的起點(diǎn)系統(tǒng)聲音建議不做增益而只做衰減限制因?yàn)橄到y(tǒng)音量本身不受控用戶隨時(shí)可能把播放器音量拉到 100%這時(shí)你再不做限制爆音直接削頂。ffmpeg 里處理這個(gè)用alimiter濾鏡limit0.9可以防止超過(guò)閾值的部分削波。5. 錄屏開(kāi)發(fā)避坑指南黑屏、音畫(huà)不同步、文件損壞的排查路徑5.1 現(xiàn)象一畫(huà)面是黑的但 ffmpeg 進(jìn)程顯示幀數(shù)在正常增長(zhǎng)這個(gè)現(xiàn)象最容易誤導(dǎo)人。幀數(shù)在漲說(shuō)明采集鏈路確實(shí)有數(shù)據(jù)流出編碼器也確實(shí)在工作可輸出文件就是黑屏。原因八成不在 ffmpeg而在采集源本身的格式。gdigrab 在部分機(jī)器上抓取的是帶硬件加速的合成畫(huà)面如果顯示驅(qū)動(dòng)是“混合模式”比如老版本 NVIDIA 驅(qū)動(dòng)配合 Windows 的混合 GPU 方案gdigrab 拿到的可能是 GPU 加速層的黑幀。解決辦法是在錄制開(kāi)始前檢查系統(tǒng)顯示設(shè)置強(qiáng)制目標(biāo)程序使用集成顯卡渲染然后讓 gdigrab 采集獨(dú)顯輸出或者把 GPU 加速模式臨時(shí)切到“僅獨(dú)顯”再錄。此外檢查-pix_fmt是否正確某些顯示器色深是 10bitgdigrab 可能輸出的是 bgra 或 rgb 序列編碼器按 yuv420p 轉(zhuǎn)出來(lái)的畫(huà)面也可能異常。但幀數(shù)正常增長(zhǎng)本身就是最重要的信息不要懷疑編碼器先懷疑源的格式。5.2 現(xiàn)象二音畫(huà)不同步而且不同步的程度隨時(shí)間推移越來(lái)越嚴(yán)重音畫(huà)不同步最常見(jiàn)的原因有兩個(gè)一是音頻和視頻的時(shí)間戳基準(zhǔn)不一致二是丟幀后時(shí)間戳沒(méi)有補(bǔ)齊。前者典型場(chǎng)景是音頻設(shè)備以采樣率為時(shí)間基準(zhǔn)而視頻以系統(tǒng)單調(diào)時(shí)鐘為基準(zhǔn)兩者偏差隨錄制時(shí)長(zhǎng)累積。后者更隱蔽比如系統(tǒng)負(fù)載高導(dǎo)致 gdigrab 丟了幾幀但 ffmpeg 的-vsync行為沒(méi)有按預(yù)期做時(shí)間戳補(bǔ)償出來(lái)的視頻就“缺時(shí)間”隨著錄制時(shí)間拉長(zhǎng)音畫(huà)差距越來(lái)越大。排查思路分成兩步走。第一步先查幀率是不是真在跑。用ffprobe分別讀輸出文件里視頻流的nb_frames和duration再用公式fps_actual nb_frames / duration推一下實(shí)際幀率如果跟預(yù)期 30fps 差 2fps 以上那說(shuō)明采集端丟幀了跟音頻端無(wú)關(guān)。第二步如果幀率正常那就查音頻的時(shí)間戳補(bǔ)償。ffmpeg 里比較穩(wěn)的參數(shù)組合是給輸出加-af aresampleasync1:first_pts0這個(gè)濾鏡會(huì)把音頻流的時(shí)間基點(diǎn)對(duì)齊到視頻流的起始時(shí)間并把采樣率偏差帶來(lái)的微小時(shí)差做自動(dòng)拉伸或壓縮。在輸入側(cè)同時(shí)給視頻源加-fflags genpts讓 ffmpeg 在輸入缺失時(shí)間戳?xí)r自動(dòng)生成能解決一部分源設(shè)備不給時(shí)間戳的問(wèn)題ffmpeg -fflags genpts \ -f gdigrab -framerate 30 -i desktop \ -f dshow -i audio麥克風(fēng)輸入 \ -af aresampleasync1:first_pts0 \ -c:v libx264 -preset veryfast -crf 20 \ -c:a aac -b:a 128k \ -y output.mp4aresample的參數(shù)async1表示允許音頻采樣率存在動(dòng)態(tài)微調(diào)first_pts0強(qiáng)制音頻時(shí)間戳從 0 開(kāi)始避免出現(xiàn)幾百毫秒的偏移。-fflags genpts是輸入側(cè) FLAG讓 ffmpeg 在輸入包沒(méi)有 PTS 時(shí)自動(dòng)推算時(shí)間戳。兩者配合后絕大多數(shù)“錄得越久不同步越嚴(yán)重”的問(wèn)題都能被按下去。提示用了aresampleasync1之后音頻理論上可能被拉伸或壓縮幾個(gè)毫秒對(duì)正常聽(tīng)感無(wú)影響。如果你做的是音樂(lè)現(xiàn)場(chǎng)錄制這類(lèi)對(duì)音質(zhì)錙銖必較的場(chǎng)景這個(gè)方案要斟酌但錄屏幾乎不會(huì)受影響。5.3 現(xiàn)象三錄制中途程序崩潰后產(chǎn)物文件無(wú)法播放或播放到尾部卡死這是長(zhǎng)時(shí)間錄制的命門(mén)。MP4 是“moov box 在文件頭部”的格式嚴(yán)格說(shuō)是 FastStart 變體但是 ffmpeg 默認(rèn)會(huì)在文件末尾寫(xiě)索引或把索引放在結(jié)尾如果程序在中途崩潰索引寫(xiě)不進(jìn)去文件就變成了“不可播”的狀態(tài)。MKV 的設(shè)計(jì)對(duì)這種情況要寬容得多它的索引是分散的流式結(jié)構(gòu)邊錄邊寫(xiě)索引所以即便中途斷電前面已經(jīng)寫(xiě)進(jìn)去的 mkv 數(shù)據(jù)也能播放。我服務(wù)的那個(gè)模擬項(xiàng)目 X 里就把“緊急保存”功能直接落到了“分段輸出 mkv”上。這個(gè)問(wèn)題的標(biāo)準(zhǔn)解法是錄制數(shù)據(jù)輸出成 mkv 或 ts 流兩者都是對(duì)寫(xiě)入中斷更寬容的容器錄完后再做一次“轉(zhuǎn)封裝”把它變成 mp4而不是直接錄 mp4。轉(zhuǎn)封裝就是只改容器不動(dòng)編碼速度快一條命令搞定ffmpeg -i input.mkv -c copy -movflags faststart output.mp4其中-c copy意思是視頻音頻流不做重編碼原樣拷進(jìn)新容器所以速度接近磁盤(pán)讀寫(xiě)上限。faststart會(huì)把 moov box 挪到文件頭部好處是用戶下載文件時(shí)不用等整個(gè)文件下載完只要頭部拉下來(lái)就能開(kāi)始拖動(dòng)播放。這條命令本身在錄屏工具的“完成錄制”階段跑一次比在錄制階段硬上 mp4 要穩(wěn)得多。5.4 現(xiàn)象四硬件編碼突然失效換回軟編就正常但 CPU 拉滿硬編很“玄學(xué)”同一套代碼在某些機(jī)器上跑幾小時(shí)沒(méi)問(wèn)題在另一些機(jī)器上錄 20 分鐘就報(bào)錯(cuò)或者畫(huà)面卡住。翻車(chē)原因幾乎都指向兩點(diǎn)顯卡驅(qū)動(dòng)版本太舊或者顯存已滿尤其在 4GB 顯存的入門(mén)卡上錄屏 游戲同吃顯存編碼器直接罷工。排查方法倒是很簡(jiǎn)單先跑一個(gè)最簡(jiǎn)硬編命令比如ffmpeg -f gdigrab -i desktop -c:v h264_nvenc -b:v 5M test.mp4如果這個(gè)命令也失敗或持續(xù)報(bào)錯(cuò)就確定是編碼器或驅(qū)動(dòng)問(wèn)題。解決路徑是升級(jí)驅(qū)動(dòng)并且不要用“最新驅(qū)動(dòng)”里的 Preview 版用穩(wěn)定分支。如果你是面向最終用戶發(fā)布工具建議在首次運(yùn)行硬編前調(diào)用 NVAPI 或 DXVA 做一次能力檢測(cè)直接把不支持硬編的機(jī)器切到軟編模式別硬來(lái)。5.5 現(xiàn)象五錄出來(lái)的文件超大或者 1 小時(shí)錄制占了 8GB 空間體積問(wèn)題通常露餡于-crf和-preset配錯(cuò)。用 libx264 時(shí)preset 越慢同等 CRF 下體積越小preset 越快體積越大。如果你用veryfast配crf20錄制大量動(dòng)態(tài)畫(huà)面時(shí)體積會(huì)大得驚人。修正方向有兩個(gè)把 preset 降到medium或slow或者改碼率上限模式。更符合錄屏場(chǎng)景的做法是在“演示模式”下用-crf 24在“高質(zhì)量模式”下用-crf 18并額外加-maxrate 8M -bufsize 16M做上限控制ffmpeg -f gdigrab -framerate 30 -i desktop \ -c:v libx264 -preset medium -crf 22 \ -maxrate 8M -bufsize 16M \ -c:a aac -b:a 128k -y output.mp4maxrate限制了視頻流的瞬間峰值碼率不能超過(guò) 8Mbpsbufsize是編碼器做碼率控制的緩沖區(qū)間兩個(gè)值一般按 2 倍關(guān)系設(shè)置。這個(gè)組合的效果是靜止畫(huà)面時(shí)碼率自動(dòng)降到極低動(dòng)態(tài)畫(huà)面時(shí)最高不超過(guò) 8Mbps1 小時(shí) 1080p30 的視頻基本能壓在 2GB 上下。6. 上線前的一小時(shí)用 ffprobe 做產(chǎn)物校驗(yàn)與自動(dòng)化驗(yàn)收錄屏工具開(kāi)發(fā)進(jìn)入尾聲時(shí)你會(huì)需要一個(gè)“驗(yàn)收腳本”。它的意義不只是檢查錄制是否成功而是幫你把那些用戶側(cè)才會(huì)暴露的坑提前踩掉。我自己的習(xí)慣是每次發(fā)布前強(qiáng)制走一遍完整的錄制驗(yàn)收流程跑一段 5 分鐘的標(biāo)準(zhǔn)測(cè)試錄制然后從四個(gè)維度檢查產(chǎn)物流信息、時(shí)長(zhǎng)偏差、關(guān)鍵幀間隔、音畫(huà)時(shí)間差。第一個(gè)維度用ffprobe讀流信息ffprobe -v error -show_entries \ streamindex,codec_type,codec_name,width,height,r_frame_rate,avg_frame_rate \ -of json output.mp4-show_entries指定了只輸出這些字段-of json把結(jié)果序列化為 JSON 方便腳本解析。重點(diǎn)看幾個(gè)值視頻流的avg_frame_rate是否約等于采集幀率音頻流是否有兩條如果你的產(chǎn)品是雙音軌設(shè)計(jì)寬度高度是否跟預(yù)期一致。這個(gè)命令不校驗(yàn)內(nèi)容只是確認(rèn)文件結(jié)構(gòu)沒(méi)有畸形。第二個(gè)維度是時(shí)長(zhǎng)偏差。用ffprobe -v error -show_entries formatduration -of csvp0 output.mp4拿到封裝時(shí)長(zhǎng)然后跟錄制的實(shí)際秒數(shù)做比較。錄了 300 秒文件時(shí)長(zhǎng)在 295305 秒之間都算正常范圍。偏差超過(guò) 5 秒說(shuō)明錄制過(guò)程有丟幀或音頻流錯(cuò)位需要回頭查采集端或aresample的參數(shù)。第三個(gè)維度是關(guān)鍵幀間隔。用ffprobe -v error -select_streams v -show_entries framekey_frame -of csvp0 output.mp4輸出每一幀的關(guān)鍵幀標(biāo)記然后寫(xiě)一段小腳本統(tǒng)計(jì)相鄰關(guān)鍵幀的最大間隔。如果最大間隔超過(guò)-g設(shè)定值的兩倍那說(shuō)明編碼器沒(méi)有按預(yù)期插入關(guān)鍵幀這對(duì)剪輯工具和播放器拖動(dòng)進(jìn)度條影響極大。第四個(gè)維度音畫(huà)時(shí)間差是文本類(lèi)資源最難自動(dòng)驗(yàn)證的我的做法是取一個(gè)已知畫(huà)面特征——比如屏幕錄制的開(kāi)始畫(huà)面是特定顏色的全屏色塊——然后從音頻波形里定位對(duì)應(yīng)的起始點(diǎn)比對(duì)兩者時(shí)間偏移。這個(gè)用 ffmpeg 的showwaves或者astats濾鏡可以做但實(shí)現(xiàn)成本偏高小型項(xiàng)目里可以退一步只驗(yàn)證音頻首包的 PTS 是否為 0配合aresamplefirst_pts0就已經(jīng)能規(guī)避 95% 的同步問(wèn)題ffprobe -v error -select_streams a -show_entries \ packetpts_time,flags -of csvp0 -read_intervals %#2 output.mp4-read_intervals %#2表示只讀前 2 秒的包。輸出的第一行如果是 0.000000說(shuō)明音頻起點(diǎn)正確如果第一行是 0.5 或者更大那就算畫(huà)面沒(méi)問(wèn)題也要檢查輸入設(shè)備或aresample配置。自動(dòng)化這套驗(yàn)收流程我用的是一個(gè)很簡(jiǎn)單的 Python 腳本每次 CI 或發(fā)布前跑一遍先從錄制目錄里找最新的產(chǎn)物文件依次跑上述四條 ffprobe 命令然后把結(jié)果解析后與閾值對(duì)比最后輸出 PASS/FAIL 報(bào)告。腳本不復(fù)雜但它幫我抓到了至少三類(lèi)真實(shí)缺陷某次更新后音頻設(shè)備枚舉順序變了導(dǎo)致錄到空音軌某驅(qū)動(dòng)升級(jí)后關(guān)鍵幀間隔異常增大以及一次-g參數(shù)被誤改小后文件體積暴漲的問(wèn)題。從那以后我每次發(fā)版都強(qiáng)制走一遍這個(gè)驗(yàn)收流程錄屏工具的“最后一公里”才算真正踏實(shí)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取