戰(zhàn):原生庫(kù)加載與依賴沖突避坑指南)
前陣子我做一個(gè)小工具需要在界面里加一塊屏幕錄制能力。技術(shù)選型時(shí)圖省事直接用JavaCV里的FFmpeg來(lái)做這類庫(kù)發(fā)展了這么多年心想無(wú)非就是抓屏、編碼、輸出文件三步。結(jié)果代碼寫(xiě)完了Maven工程連運(yùn)行都沒(méi)跑起來(lái)直接卡死在啟動(dòng)階段。那幾天我先后見(jiàn)到至少五種不同的報(bào)錯(cuò)從類找不到到原生庫(kù)加載失敗再到“偽成功”空文件最后一項(xiàng)一項(xiàng)排查下來(lái)才發(fā)現(xiàn)問(wèn)題幾乎都不在業(yè)務(wù)代碼上而是依賴體系和JavaCPP原生庫(kù)加載機(jī)制沒(méi)搞透。這篇就把這次的完整經(jīng)歷寫(xiě)下來(lái)什么現(xiàn)象、什么原因、怎么解決以及最終穩(wěn)定錄屏的一套方案給同樣打算用JavaCV做錄屏的朋友避避坑。1. 問(wèn)題現(xiàn)場(chǎng)連“運(yùn)行”都失敗的三種形態(tài)先說(shuō)結(jié)論JavaCV錄屏程序的“運(yùn)行失敗”和普通Java程序的報(bào)錯(cuò)路徑不太一樣。普通程序掛了多半是空指針、業(yè)務(wù)異常JavaCV這類帶原生庫(kù)的庫(kù)掛了往往是類路徑缺失、動(dòng)態(tài)庫(kù)加載失敗、編碼器初始化異常。下面是我這次以及身邊朋友高頻遇到的三類現(xiàn)場(chǎng)。1.1 啟動(dòng)就報(bào)NoClassDefFoundError類去哪了第一次報(bào)錯(cuò)出現(xiàn)在命令行啟動(dòng)階段。我把工程用Maven打成一個(gè)普通jar然后執(zhí)行java -jarJVM直接拋了這么一段Exception in thread main java.lang.NoClassDefFoundError: org/bytedeco/javacv/FFmpegFrameRecorder at com.example.screencap.ScreenCapMain.main(ScreenCapMain.java:12)當(dāng)時(shí)我第一反應(yīng)是依賴沒(méi)配全但打開(kāi)pom.xml明明寫(xiě)了javacv依賴IDE里import org.bytedeco.javacv.FFmpegFrameRecorder;也能正常編譯怎么運(yùn)行時(shí)就找不到類后來(lái)才反應(yīng)過(guò)來(lái)Maven默認(rèn)打出來(lái)的jar是“瘦包”只包含自己的class文件根本不會(huì)把你引用的第三方依賴jar塞進(jìn)去。IDE里能跑是因?yàn)镮DE會(huì)自動(dòng)用Maven計(jì)算出來(lái)的完整classpath但用java -jar啟動(dòng)時(shí)如果沒(méi)把依賴一起放到classpath里JVM自然找不到FFmpegFrameRecorder這個(gè)類。這個(gè)報(bào)錯(cuò)其實(shí)是“運(yùn)行時(shí)依賴缺失”問(wèn)題不是代碼問(wèn)題很多人會(huì)因此浪費(fèi)半天時(shí)間。1.2 UnsatisfiedLinkErrorJava類在本地庫(kù)沒(méi)了類路徑問(wèn)題解決之后我又遇到第二種報(bào)錯(cuò)。這次是在IDE里直接跑代碼能進(jìn)入main方法卻在創(chuàng)建FFmpegFrameRecorder實(shí)例或者調(diào)用start()時(shí)拋出了java.lang.UnsatisfiedLinkError: no ffmpeg-java in java.library.path或者類似這樣Exception in thread main java.lang.UnsatisfiedLinkError: Cant load library: D:\path\to\ffmpeg.dll當(dāng)時(shí)我很困惑Java包都在為什么要去加載一個(gè)叫ffmpeg.dll的東西這就是JavaCV和普通Java庫(kù)最大的不同它只是“遙控器”真正干活的FFmpeg是C/C寫(xiě)的原生庫(kù)運(yùn)行時(shí)要加載對(duì)應(yīng)平臺(tái)Windows是dllLinux是somacOS是dylib的本地庫(kù)文件。如果這些本地庫(kù)沒(méi)有正確地被加載JVM就會(huì)報(bào)UnsatisfiedLinkError。1.3 更隱蔽的“偽成功”程序沒(méi)報(bào)錯(cuò)輸出文件卻是0字節(jié)第三種情況最氣人程序一路暢通控制臺(tái)沒(méi)有任何紅字但錄了十幾秒之后發(fā)現(xiàn)目錄里要么沒(méi)有輸出文件要么生成了一個(gè)0字節(jié)的mp4。這種“偽成功”比直接報(bào)錯(cuò)更難定位因?yàn)镴ava層面完全不知道發(fā)生了什么。我后來(lái)在系統(tǒng)里逐個(gè)排查才發(fā)現(xiàn)問(wèn)題可能出在幾個(gè)地方幀率參數(shù)與實(shí)際的抓幀速度不匹配導(dǎo)致錄制器遲遲等不到關(guān)鍵幀像素格式設(shè)置和編碼器不對(duì)付或者壓根忘了調(diào)用stop()和close()文件沒(méi)有收尾寫(xiě)入。FFmpeg的報(bào)錯(cuò)通常不會(huì)通過(guò)Java異常拋出來(lái)而是寫(xiě)到FFmpeg自己的日志流里控制臺(tái)里出現(xiàn)一堆[h264 0x...]開(kāi)頭的信息但你如果不認(rèn)識(shí)這些日志很容易忽略。2. 先搞懂JavaCV和FFmpeg的運(yùn)行機(jī)制再談改代碼一連串的失敗讓我意識(shí)到不能只在代碼層打轉(zhuǎn)轉(zhuǎn)得先把JavaCV這套體系怎么運(yùn)轉(zhuǎn)搞明白。搞懂機(jī)制之后很多報(bào)錯(cuò)其實(shí)是可以提前預(yù)判的。2.1 JavaCV不是FFmpeg本身它是中間的“膠水層”很多人會(huì)把JavaCV和FFmpeg混為一談其實(shí)它們是兩層?xùn)|西。FFmpeg是一套強(qiáng)大的多媒體處理庫(kù)負(fù)責(zé)解碼、編碼、封裝、推流JavaCV則是在Java層面對(duì)FFmpeg、OpenCV等原生庫(kù)做了封裝讓Java開(kāi)發(fā)者可以用對(duì)象和方法的方式來(lái)調(diào)用C庫(kù)的能力。JavaCV里和我們錄屏相關(guān)的核心類有三個(gè)FFmpegFrameGrabber負(fù)責(zé)從視頻源抓取幀F(xiàn)FmpegFrameRecorder負(fù)責(zé)把幀編碼并寫(xiě)入文件或流Java2DFrameConverter負(fù)責(zé)把Java的BufferedImage轉(zhuǎn)成JavaCV通用幀對(duì)象Frame。整個(gè)流程就是抓屏得到圖片 - 轉(zhuǎn)成Frame - 交給FFmpegFrameRecorder編碼 - 輸出mp4。打個(gè)比方FFmpeg是一臺(tái)專業(yè)攝像機(jī)JavaCV是它的遙控器JavaCPP則是連接遙控器和攝像機(jī)的一根數(shù)據(jù)線。你寫(xiě)的是Java代碼但真正干活的是C語(yǔ)言寫(xiě)的攝像機(jī)本體。2.2 JavaCPP與預(yù)制原生庫(kù)為什么“沒(méi)庫(kù)”會(huì)報(bào)錯(cuò)JavaCV底層依賴JavaCPP技術(shù)。JavaCPP做的事情簡(jiǎn)單說(shuō)就是讓Java程序能直接加載并調(diào)用C/C原生庫(kù)。JavaCV在發(fā)布時(shí)會(huì)把FFmpeg等原生庫(kù)按平臺(tái)分別打包成預(yù)制庫(kù)文件放進(jìn)對(duì)應(yīng)的jar里。運(yùn)行時(shí)JavaCPP的Loader類會(huì)根據(jù)當(dāng)前操作系統(tǒng)和CPU架構(gòu)從jar里解壓出對(duì)應(yīng)的dll/so/dylib放到臨時(shí)目錄然后執(zhí)行加載。這個(gè)過(guò)程看似自動(dòng)實(shí)際上對(duì)運(yùn)行環(huán)境非常敏感臨時(shí)目錄沒(méi)有寫(xiě)權(quán)限、殺毒軟件把解壓出來(lái)的dll隔離了、系統(tǒng)缺少C運(yùn)行庫(kù)、磁盤(pán)滿了一類的環(huán)境問(wèn)題都會(huì)讓它在加載這一步失敗。而且失敗信息往往不直觀包裝成UnsatisfiedLinkError或者直接JVM退出很容易讓人誤判成代碼問(wèn)題。2.3 版本錯(cuò)位靜默翻車的第一大元兇除了加載機(jī)制版本問(wèn)題也是JavaCV運(yùn)行失敗的“重災(zāi)區(qū)”。JavaCV不是單一jar它分成了多個(gè)模塊比如javacv、ffmpeg-platform、opencv-platform等等。最關(guān)鍵的是javacv的版本號(hào)必須和ffmpeg-platform的版本號(hào)后半段保持一致。舉個(gè)例子如果你在pom.xml里引入dependency groupIdorg.bytedeco/groupId artifactIdjavacv/artifactId version1.5.9/version /dependency那么對(duì)應(yīng)的FFmpeg封裝包版本也應(yīng)該是6.1.1-1.5.9這種后半段相同的版本。如果誤把javacv 1.5.9和ffmpeg-platform 6.1.1-1.5.7混著用Java層和C層的方法簽名對(duì)不上運(yùn)行期就會(huì)冒出NoSuchMethodError而且這種錯(cuò)還不一定立刻出現(xiàn)在第一行調(diào)用上可能是在編碼器初始化時(shí)才爆出來(lái)排查成本很高。3. Maven依賴的正確打開(kāi)方式經(jīng)歷了前兩輪的“磨難”我決定徹底把依賴這塊收拾干凈。這里直接給出我認(rèn)為最穩(wěn)妥的配置方式以及排查依賴沖突的具體做法。3.1 推薦的最小依賴組合只做錄屏不需要OpenCV等附加能力的話最小依賴方案是兩個(gè)jarproperties javacv.version1.5.9/javacv.version ffmpeg.version6.1.1-1.5.9/ffmpeg.version /properties dependencies dependency groupIdorg.bytedeco/groupId artifactIdjavacv/artifactId version${javacv.version}/version /dependency dependency groupIdorg.bytedeco/groupId artifactIdffmpeg-platform/artifactId version${ffmpeg.version}/version /dependency /dependenciesffmpeg-platform這個(gè)包已經(jīng)包含了各平臺(tái)預(yù)編譯的FFmpeg原生庫(kù)JavaCV在運(yùn)行時(shí)會(huì)自動(dòng)選擇當(dāng)前平臺(tái)的那一份。如果你不放心可以再加一行顯式依賴javacpp但通常JavaCV會(huì)傳遞引入不用手動(dòng)寫(xiě)。注意版本號(hào)不要亂動(dòng)。JavaCV和ffmpeg-platform的版本對(duì)應(yīng)關(guān)系以你實(shí)際使用的版本為準(zhǔn)最穩(wěn)妥的辦法是去Maven中央倉(cāng)庫(kù)看同名版本是否真的存在。我用過(guò)1.5.8、1.5.9、1.5.10這幾個(gè)組合整體表現(xiàn)穩(wěn)定。3.2 聚合包與按需引入的取舍很多教程會(huì)直接推薦引入javacv-platform這是一個(gè)聚合包會(huì)把JavaCV相關(guān)的所有能封裝的庫(kù)全部帶進(jìn)來(lái)包括OpenCV、OpenBLAS、FFmpeg等。它的優(yōu)點(diǎn)是省心缺點(diǎn)也很明顯體積大打進(jìn)最終產(chǎn)物體積可能膨脹幾百M(fèi)B里頭的OpenCV等模塊如果版本和項(xiàng)目里其他依賴沖突也會(huì)引發(fā)更多問(wèn)題。我這次的小工具對(duì)體積有要求所以只用了javacv ffmpeg-platform的組合。兩者的權(quán)衡可以看下面這張表依賴方案優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景javacv-platform配置簡(jiǎn)單全量能力體積巨大依賴復(fù)雜易沖突快速原型驗(yàn)證、不在乎包體積javacv ffmpeg-platform體積適中按需引入需要手動(dòng)維護(hù)版本對(duì)應(yīng)關(guān)系只用到FFmpeg能力、對(duì)包體有要求javacv 具體ffmpeg模塊更小的體積需要精確匹配各模塊版本非常容易錯(cuò)對(duì)體積極度敏感且有經(jīng)驗(yàn)的團(tuán)隊(duì)3.3 用dependency:tree一次性看清依賴沖突如果你啟動(dòng)時(shí)碰到NoSuchMethodError或者LinkageError多半是依賴樹(shù)里有多個(gè)版本的JavaCV相關(guān)jar在打架。排查方法很簡(jiǎn)單Maven項(xiàng)目跑mvn dependency:tree -Dincludesorg.bytedeco:* -Dverbose看輸出里有沒(méi)有同一個(gè)坐標(biāo)出現(xiàn)多個(gè)版本。比如我這次就在輸出里發(fā)現(xiàn)項(xiàng)目里某個(gè)模塊傳遞引用了舊版javacv 1.5.7而我自己又顯式引入了javacv 1.5.9兩個(gè)jar同時(shí)出現(xiàn)在classpath里編譯期不報(bào)錯(cuò)運(yùn)行期就會(huì)有一堆老方法簽名對(duì)不上。解決方法是在引入自身依賴時(shí)排除舊版本傳遞或者用dependencyManagement統(tǒng)一指定版本。排除寫(xiě)法參考dependency groupIdcom.somegroup/groupId artifactIdsome-internal-module/artifactId version1.0.0/version exclusions exclusion groupIdorg.bytedeco/groupId artifactIdjavacv/artifactId /exclusion exclusion groupIdorg.bytedeco/groupId artifactIdffmpeg-platform/artifactId /exclusion /exclusions /dependency記住一個(gè)原則全項(xiàng)目只允許出現(xiàn)一組JavaCV相關(guān)版本要么全用1.5.8要么全用1.5.9不要混搭。4. 錄屏功能的核心代碼與參數(shù)取舍依賴問(wèn)題解決后錄屏代碼本身其實(shí)不復(fù)雜。下面給出一個(gè)能直接跑通的核心流程以及我在參數(shù)調(diào)優(yōu)方面的一些實(shí)測(cè)經(jīng)驗(yàn)。4.1 主流程Robot抓幀 FrameRecorder編碼Java里抓取屏幕最快的方式是java.awt.Robot它可以精確截取某個(gè)矩形區(qū)域的內(nèi)容。得到BufferedImage之后用Java2DFrameConverter轉(zhuǎn)成Frame再交給FFmpegFrameRecorder編碼寫(xiě)入文件。import org.bytedeco.javacv.*; import org.bytedeco.ffmpeg.global.avcodec; import org.bytedeco.ffmpeg.global.avutil; import java.awt.*; import java.awt.image.BufferedImage; public class ScreenRecorderDemo { public static void main(String[] args) throws Exception { int width 1920; int height 1080; int fps 15; String outputFile record.mp4; Robot robot new Robot(); Rectangle screenRect new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()); FFmpegFrameRecorder recorder new FFmpegFrameRecorder(outputFile, width, height); recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P); recorder.setFormat(mp4); recorder.setFrameRate(fps); recorder.setVideoBitrate(2_000_000); recorder.start(); Java2DFrameConverter converter new Java2DFrameConverter(); long start System.currentTimeMillis(); long durationMs 10_000L; int gapMs 1000 / fps; while (System.currentTimeMillis() - start durationMs) { BufferedImage image robot.createScreenCapture(screenRect); Frame frame converter.convert(image); recorder.record(frame); frame.close(); Thread.sleep(gapMs); } recorder.stop(); recorder.close(); converter.close(); } }這段代碼的邏輯很直白每gapMs毫秒抓一幀屏幕轉(zhuǎn)成Frame后寫(xiě)入錄制器持續(xù)10秒后結(jié)束。生產(chǎn)中建議把screenRect和寬高改成動(dòng)態(tài)獲取避免寫(xiě)死分辨率。4.2 編碼器、像素格式、幀率、比特率怎么配錄屏最容易踩的坑是參數(shù)之間不兼容。下面這幾組參數(shù)是我實(shí)測(cè)下來(lái)最穩(wěn)的組合編碼器使用H.264也就是avcodec.AV_CODEC_ID_H264。播放兼容性最好幾乎所有終端都能解。如果你的運(yùn)行環(huán)境CPU很強(qiáng)且需要更高壓縮率可以試AV_CODEC_ID_H265但要確認(rèn)播放端支持。像素格式AV_PIX_FMT_YUV420P。這是H.264最常用的輸入格式兼容性極高。如果你錄制的畫(huà)面有透明通道或者特殊顏色需求才需要考慮其他格式否則不要亂改。幀率屏幕錄制我個(gè)人建議10到15。錄PPT、操作教程這種靜態(tài)內(nèi)容比較多的場(chǎng)景15幀完全夠體積也不會(huì)太大要錄游戲或快速動(dòng)畫(huà)再考慮30幀。比特率720p建議1Mbps到2Mbps1080p建議2Mbps到4Mbps。設(shè)太高了文件體積瘋長(zhǎng)設(shè)太低了畫(huà)面會(huì)出現(xiàn)明顯的壓縮模糊。另外setFormat(mp4)和setVideoCodec的配合別忽略兩者不一致可能導(dǎo)致封裝器初始化失敗。如果輸出record.flv把格式對(duì)應(yīng)改成flv即可。4.3 結(jié)束錄制時(shí)必須做的收尾動(dòng)作很多朋友錄完直接結(jié)束進(jìn)程結(jié)果發(fā)現(xiàn)文件打不開(kāi)。原因是FFmpegFrameRecorder在寫(xiě)入mp4時(shí)需要正常的收尾流程否則文件缺少moov元數(shù)據(jù)播放器無(wú)法定位音視頻軌道。正確的收尾順序是recorder.stop(); recorder.close();如果用了Java2DFrameConverter也要記得關(guān)閉。手動(dòng)調(diào)用的方式不夠優(yōu)雅但勝在可控尤其是錄制循環(huán)可能提前中斷的場(chǎng)景你可以在finally塊里確保執(zhí)行這兩個(gè)方法。5. 我這次踩過(guò)的坑和最終解決方案前面講的偏通用知識(shí)這節(jié)才是我這次最想分享的“實(shí)戰(zhàn)排雷”記錄。每個(gè)坑都有真實(shí)的現(xiàn)象、排查思路和最終處理方式。5.1 坑一依賴版本分裂導(dǎo)致NoSuchMethodError現(xiàn)象程序能啟動(dòng)FFmpegFrameRecorder對(duì)象也創(chuàng)建成功了但在調(diào)用start()時(shí)拋NoSuchMethodError棧信息指向org.bytedeco.ffmpeg包下的某個(gè)方法。排查思路我第一反應(yīng)是代碼用錯(cuò)了API對(duì)著文檔看了半天也沒(méi)問(wèn)題。后來(lái)冷靜下來(lái)用mvn dependency:tree -Dincludesorg.bytedeco:*一看發(fā)現(xiàn)項(xiàng)目里有個(gè)內(nèi)部工具模塊傳遞引用了javacv 1.5.7而我主模塊用的是javacv 1.5.9。兩個(gè)不同版本的原生庫(kù)同時(shí)被裝載JavaCPP直接錯(cuò)亂。最終解決排除掉內(nèi)部模塊傳遞過(guò)來(lái)的舊版javacv然后在dependencyManagement里統(tǒng)一聲明JavaCV及其相關(guān)包的版本確保全項(xiàng)目只有一套依賴。5.2 坑二殺毒軟件把解壓出來(lái)的原生庫(kù)隔離了現(xiàn)象在我本地機(jī)器上程序運(yùn)行正常但把錄制模塊放到另一臺(tái)Windows機(jī)器上剛啟動(dòng)就報(bào)UnsatisfiedLinkError而且時(shí)不時(shí)報(bào)錯(cuò)重啟又能好一會(huì)兒。排查思路這類“環(huán)境相關(guān)”的加載失敗優(yōu)先懷疑JavaCPP解壓原生庫(kù)時(shí)被外部因素干擾。打開(kāi)Windows的安全中心一看果然歷史保護(hù)記錄里躺著被隔離的ffmpeg.dll。JavaCPP默認(rèn)會(huì)把原生庫(kù)解壓到系統(tǒng)臨時(shí)目錄殺毒軟件對(duì)臨時(shí)目錄里的dll格外敏感誤刪或隔離是常有的事。最終解決兩種辦法。一是給jvm加參數(shù)指定一個(gè)專門的可信目錄例如-Djava.io.tmpdir/path/to/your/tmp二是把項(xiàng)目編碼目錄加入殺毒軟件白名單。兩個(gè)方案我后來(lái)都用上了開(kāi)發(fā)環(huán)境加白名單生產(chǎn)環(huán)境指定獨(dú)立臨時(shí)目錄再?zèng)]出現(xiàn)過(guò)離奇消失的dll。5.3 坑三Windows系統(tǒng)缺少C運(yùn)行庫(kù)導(dǎo)致64位庫(kù)加載直接崩現(xiàn)象有一臺(tái)同事的機(jī)器Java環(huán)境完全正常但錄屏程序一跑就崩連標(biāo)準(zhǔn)的Java異常棧都沒(méi)有彈窗報(bào)錯(cuò)或直接退出有時(shí)控制臺(tái)出現(xiàn)0xc000007b這樣的錯(cuò)誤碼。排查思路0xc000007b在Windows上通常代表應(yīng)用程序需要系統(tǒng)運(yùn)行庫(kù)支持。JavaCV解壓出來(lái)的FFmpeg dll依賴微軟的C運(yùn)行庫(kù)如果目標(biāo)機(jī)器沒(méi)有安裝對(duì)應(yīng)的運(yùn)行庫(kù)動(dòng)態(tài)庫(kù)加載就會(huì)失敗而且失敗方式非常粗暴可能直接導(dǎo)致進(jìn)程退出。最終解決在目標(biāo)機(jī)器安裝最新版的Visual C Redistributable x64運(yùn)行庫(kù)重啟之后程序就能正常啟動(dòng)。后來(lái)我再部署這類帶原生庫(kù)的Java工具都會(huì)把運(yùn)行庫(kù)檢查寫(xiě)進(jìn)環(huán)境準(zhǔn)備清單而不是默認(rèn)所有機(jī)器都齊備。5.4 最終能穩(wěn)定跑的完整方案在所有坑都填平之后我把錄屏模塊收斂成了一套穩(wěn)定方案基本規(guī)則如下依賴固定javacv 1.5.9ffmpeg-platform 6.1.1-1.5.9全項(xiàng)目通過(guò)dependencyManagement鎖死版本。啟動(dòng)參數(shù)顯式指定-Djava.io.tmpdir到用戶可寫(xiě)目錄避免系統(tǒng)臨時(shí)目錄權(quán)限和殺軟干擾。編碼參數(shù)H.264 yuv420p mp4 15fps 2Mbps兼容性和畫(huà)質(zhì)都在可接受范圍。收尾流程錄制循環(huán)結(jié)束后先stop()再close()保證文件完整。這套方案我用下來(lái)在Windows和Linux上都能穩(wěn)定錄屏CPU占用也不算夸張1080p下大約占一個(gè)單核左右。6. 常見(jiàn)問(wèn)題速查表與救急兜底手段最后把這次經(jīng)歷中遇到的典型問(wèn)題整理成一張速查表方便以后直接對(duì)號(hào)入座。如果你也卡在JavaCV錄屏運(yùn)行階段可以先看一眼這張表再動(dòng)手查。6.1 常見(jiàn)報(bào)錯(cuò)與對(duì)應(yīng)處理現(xiàn)象可能原因處理方式NoClassDefFoundError: FFmpegFrameRecorder運(yùn)行時(shí)依賴缺失打包時(shí)沒(méi)帶上javacv相關(guān)jar用maven-shade打包或把依賴jar放進(jìn)classpathNoSuchMethodErrorJavaCV系列依賴版本分裂檢查依賴樹(shù)統(tǒng)一javacv和ffmpeg-platform版本UnsatisfiedLinkError 指向ffmpeg/local庫(kù)原生庫(kù)解壓或加載失敗檢查臨時(shí)目錄權(quán)限、殺毒軟件隔離、VC運(yùn)行庫(kù)0xc000007b錯(cuò)誤碼Windows缺C運(yùn)行庫(kù)或架構(gòu)不匹配安裝對(duì)應(yīng)x64架構(gòu)的運(yùn)行庫(kù)RuntimeException: No encoder found編碼器不匹配或平臺(tái)包未包含相應(yīng)編碼器顯式指定libx264/H264檢查像素格式和封裝格式錄制文件0字節(jié)未正常調(diào)用stop/close或錄制幀率太低錄制結(jié)束后按順序stop再close適當(dāng)提高fps日志顯示幀時(shí)間戳異常抓幀間隔不穩(wěn)定線程調(diào)度抖動(dòng)用nanoTime高精度計(jì)時(shí)或使用定時(shí)調(diào)度框架6.2 實(shí)在不行時(shí)的兜底方案如果上面這些排查完仍然無(wú)法在期限內(nèi)穩(wěn)定跑通JavaCV內(nèi)嵌錄屏還有一個(gè)工程上常見(jiàn)的兜底方案先用Robot把屏幕幀落盤(pán)成臨時(shí)PNG序列錄制結(jié)束后再調(diào)用外部FFmpeg命令行把圖片序列合并成視頻。ffmpeg -framerate 15 -i frame_%05d.png -c:v libx264 -pix_fmt yuv420p output.mp4這條路的好處是把JavaCV的原生庫(kù)加載問(wèn)題整個(gè)繞開(kāi)代價(jià)是落盤(pán)圖片占空間、后期編碼耗時(shí)。它適合作為項(xiàng)目初期的應(yīng)急預(yù)案不建議長(zhǎng)期使用。長(zhǎng)期來(lái)看JavaCV內(nèi)存內(nèi)錄制的效率確實(shí)更好文件也只在最終階段寫(xiě)一次體驗(yàn)更順滑。這次踩坑的經(jīng)歷給我的最大感受是JavaCV這類帶原生庫(kù)的Java庫(kù)真正決定能不能跑起來(lái)的往往不是Java代碼本身而是依賴版本、原生庫(kù)加載環(huán)境、系統(tǒng)運(yùn)行庫(kù)這些“看不見(jiàn)的底座”。代碼半天就能寫(xiě)完跑通往往要折騰一兩天這是常態(tài)。所以我后來(lái)做類似項(xiàng)目都會(huì)先跑JavaCV官方自帶的示例確認(rèn)環(huán)境沒(méi)問(wèn)題再動(dòng)自己的業(yè)務(wù)代碼。版本依賴固定成一張“已知可用矩陣”全項(xiàng)目統(tǒng)一執(zhí)行真的能避免一大半的啟動(dòng)失敗。希望這篇記錄能幫你少走幾個(gè)彎路。