芯SDK對接實戰(zhàn):從Linux到Android的集成與優(yōu)化)
2. 對接前要先搞明白夜視機(jī)芯SDK到底是啥很多第一次接觸夜視機(jī)芯的兄弟拿到廠商給的SDK壓縮包時都是一臉懵。一個幾百兆的包里面既有.so動態(tài)庫又有.jar、.aar還夾著一堆文檔和Demo工程乍一看根本不知道從哪下手。先把這個事說清楚所謂夜視機(jī)芯本質(zhì)就是一個集成了圖像傳感器、ISP圖像信號處理器、紅外照明控制、鏡頭驅(qū)動和視頻編碼模塊的嵌入式相機(jī)模組。廠商開放SDK目的就是讓你不碰硬件底層的寄存器操作直接通過一套標(biāo)準(zhǔn)接口拿到視頻流、控制變焦、切換日夜模式、調(diào)節(jié)圖像參數(shù)。我這次接的機(jī)芯主控跑的是海思平臺對外提供了兩條通道一條是RTSP推流拿來傳視頻另一條是私有TCP協(xié)議用來發(fā)控制命令。SDK在這中間起的作用就是封裝了底層的網(wǎng)絡(luò)通信、設(shè)備發(fā)現(xiàn)、固件升級這些臟活對外暴露一套統(tǒng)一API避免你去直接解析海思的私有協(xié)議。1.1 打開SDK壓縮包你會看到什么廠商SDK的目錄結(jié)構(gòu)通常大同小異以我這次拿到的版本為例核心就這幾塊lib/按平臺區(qū)分的動態(tài)庫一般有armv7a、arm64和x86_64三種Linux平臺下還會單獨給一份.soAndroid平臺則放在對應(yīng)的jniLibs目錄里。include/C/C頭文件這是整個SDK的使用說明書。demo/官方示例工程一般有Linux命令行版本和Android Studio版本。doc/PDF或CHM格式的API文檔我強(qiáng)烈建議你先把目錄結(jié)構(gòu)過一遍尤其是“快速開始”章節(jié)。第一件事不是急著寫代碼而是確認(rèn)你目標(biāo)平臺的架構(gòu)和SDK的匹配度。Android設(shè)備要用arm64-v8a如果是老設(shè)備可能還要保留armeabi-v7a千萬別一股腦全塞進(jìn)去APK體積會無謂膨脹而且某些機(jī)芯SDK在不同ABI下表現(xiàn)還不一樣后面排查起來很頭疼。1.2 常見傳輸方式與協(xié)議選型夜視機(jī)芯SDK的傳輸方式基本就三條路SDK內(nèi)置的拉流接口部分高端機(jī)芯SDK直接提供GetVideoStream()這類接口內(nèi)部封裝了RTSP/RTP解包邏輯你拿到的是一個解碼后的YUV或RGB幀。好處是鏈路短、延遲低壞處是和SDK強(qiáng)耦合想換機(jī)芯得重寫。單獨走RTSP/ONVIF拉流機(jī)芯上有獨立的網(wǎng)絡(luò)服務(wù)你直接拉流SDK只用來控制。這種方式最靈活而且方便和現(xiàn)有NVR、視頻平臺對接。私有協(xié)議SDK直連主要用在純控制場景比如改參數(shù)、觸發(fā)報警、升級固件。多數(shù)情況下是TCP長連接加自定義報文。這次項目我選了方案二加方案三的組合視頻流走RTSP控制走SDK的TCP通道。這么做的好處是Android端視頻顯示可以直接用系統(tǒng)自帶的MediaCodec硬解性能壓力小控制命令單獨走SDK又不會干擾視頻流出問題也好定位。2. Linux端集成先把底層鏈路跑通Linux端是整個項目的地基。我習(xí)慣先把所有功能在Linux上驗證一遍再搬到Android因為Linux下調(diào)試工具豐富抓包、打日志、分析崩潰都方便得多。如果你一上來就直接弄Android遇到問題很難說清是SDK的問題、JNI層的問題還是你應(yīng)用層的問題。2.1 開發(fā)環(huán)境與交叉編譯要點環(huán)境是老生常談但還是列一下避免新人踩坑Ubuntu 20.04 / 22.04 LTS64位CMake 3.10以上NDK r21e或更高版本用于Android交叉編譯GCC/G 9.xLinux本地編譯沒什么說的直接cmake -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j$(nproc)如果要交叉編譯給嵌入式Linux或ARM開發(fā)板用記得配置工具鏈cmake -B build_arm \ -DCMAKE_TOOLCHAIN_FILE/path/to/aarch64-linux-gnu.toolchain.cmake \ -DCMAKE_BUILD_TYPERelease這里有一個非常容易出問題的地方SDK動態(tài)庫的依賴。你用ldd查看廠商.so時會發(fā)現(xiàn)它依賴了一些系統(tǒng)庫比如libstdc.so.6、libpthread等。在開發(fā)機(jī)上運(yùn)行沒問題一部署到精簡的嵌入式根文件系統(tǒng)上就報找不到庫。我的習(xí)慣是把ldd libxxxx.so的輸出完整保留部署的時候?qū)φ罩讶笔У囊蕾囈黄鹂竭^去。別看這一步簡單能省下后面大量的現(xiàn)場救火時間。2.2 起流與解碼從RTSP到Y(jié)UV/BGRLinux端拉RTSP流我用了FFmpeg這算是萬能解了。核心代碼邏輯不多但有幾個關(guān)鍵點值得說透AVFormatContext *fmt_ctx NULL; // 打開輸入流設(shè)置超時和緩存大小 AVDictionary *opts NULL; av_dict_set(opts, rtsp_transport, tcp, 0); // 強(qiáng)制走TCP避免UDP丟包花屏 av_dict_set(opts, stimeout, 5000000, 0); // 5秒超時 av_dict_set(opts, buffer_size, 2048000, 0); // 2MB緩沖防止弱網(wǎng)卡頓 if (avformat_open_input(fmt_ctx, url, NULL, opts) ! 0) { fprintf(stderr, open failed\n); return -1; }rtsp_transport設(shè)成tcp是我反復(fù)確認(rèn)過的最優(yōu)解。夜視機(jī)芯在低照度環(huán)境下碼率往往會突然拉升因為噪點變多、編碼器要分配更多碼率UDP模式在這種場景下丟包特別嚴(yán)重畫面直接花掉。TCP慢是慢點但穩(wěn)。打開成功后取出視頻流參數(shù)喂給解碼器AVCodecParameters *codec_params fmt_ctx-streams[video_idx]-codecpar; AVCodec *decoder avcodec_find_decoder(codec_params-codec_id); AVCodecContext *dec_ctx avcodec_alloc_context3(decoder); avcodec_parameters_to_context(dec_ctx, codec_params); avcodec_open2(dec_ctx, decoder, NULL);解碼出來的幀是AV_PIX_FMT_YUV420P如果你的算法庫需要BGR可以用sws_scale轉(zhuǎn)一下。這一步的性能優(yōu)化放到第4節(jié)講這里先提個醒千萬別每幀都重新初始化SwsContext這玩意兒創(chuàng)建一次后面復(fù)用。2.3 命令控制與參數(shù)調(diào)節(jié)SDK協(xié)議封裝技巧視頻流起來后最核心的部分就是控制通道了。工業(yè)級機(jī)芯的控制協(xié)議一般包括以下區(qū)間設(shè)備信息查詢型號、固件版本、序列號日夜模式切換自動、強(qiáng)制彩色、強(qiáng)制黑白紅外燈控制開關(guān)、亮度等級鏡頭控制變倍Z、聚焦F、光圈I圖像參數(shù)亮度、對比度、飽和度、銳度、增益上限、3D降噪等級我用的是一個典型的SDK調(diào)用模式同步請求加回調(diào)。先封裝一個DeviceControl類class DeviceControl { public: // 初始化SDK網(wǎng)絡(luò)庫 bool init(const char* ip, uint16_t port); // 設(shè)置日夜模式1白天2夜晚0自動 int setDayNightMode(int mode); // 查詢當(dāng)前參數(shù)返回結(jié)構(gòu)體攜帶各個參數(shù)值 DeviceStatus queryStatus(); private: void* sdk_handle_; int sequence_; };每一個SDK調(diào)用都有對應(yīng)的響應(yīng)數(shù)據(jù)包務(wù)必做超時處理。我曾經(jīng)偷懶沒加超時結(jié)果機(jī)芯偶發(fā)不響應(yīng)控制代碼直接卡死在等待響應(yīng)的地方連帶整個采集線程全堵死了。后來統(tǒng)一加了3秒超時加自動重連機(jī)制問題迎刃而解。關(guān)于SDK的私有報文格式廠商一般會給你一個結(jié)構(gòu)體定義常見的就是包頭魔數(shù)長度命令字 正文 校驗。這個部分的調(diào)試強(qiáng)烈建議用Wireshark抓包把SDK發(fā)出的報文和文檔里的定義對比一遍你很快就能理解每條指令在干嘛。我這邊的經(jīng)驗是先花半天時間把報文格式吃透后面排查能省三天。2.4 實時分析管線的搭建思路拿到Y(jié)UV幀以后我們項目要做的是把畫面送入自研的輕量檢測算法基于NCNN檢測結(jié)果要疊加到預(yù)覽畫面上。這就涉及到一個經(jīng)典問題解碼線程、算法線程、顯示線程的耦合關(guān)系。我的做法是生產(chǎn)者-消費者模型三層緩沖解碼線程只負(fù)責(zé)把RTSP的包解成YUV幀丟進(jìn)環(huán)形緩沖。算法線程從緩沖取幀跑推理把結(jié)果目標(biāo)框坐標(biāo)、置信度寫入一個原子結(jié)構(gòu)體。顯示線程只管從緩沖取最新幀渲染同時讀算法結(jié)果覆層繪制。緩沖大小我設(shè)為5幀這樣既能平滑抖動又不至于延遲太高。實測延遲大約140ms左右含解碼算法渲染對于監(jiān)控類場景完全夠用。這里有一個小坑提醒夜視機(jī)芯在低照度模式下幀率可能自動下降到15fps甚至更低。算法線程要注意處理這點不然你后面做目標(biāo)跟蹤時相同間隔下目標(biāo)位移量會變大需要按實際幀間隔調(diào)整匹配閾值。3. Android端集成從JNI到網(wǎng)絡(luò)拉流兩條路Android端是我這次項目的重點。接入方式有三條路可選我分別說下利弊。3.1 方案一JNI直接封裝廠商庫把廠商的.so通過JNI封裝成Java接口這是很多SDK Demo默認(rèn)的做法。Android Studio里配置jniLibs目錄把對應(yīng)ABI的.so文件放進(jìn)去app/src/main/jniLibs/ ├── arm64-v8a/ │ └── libsdk_detector.so ├── armeabi-v7a/ │ └── libsdk_detector.so然后在Java層聲明一個native方法public class NightVisionSDK { static { System.loadLibrary(sdk_detector); } public static native int nativeInit(String ip, int port); public static native int nativeSetDayNightMode(int mode); }這種方式的優(yōu)點是延遲最低、功能最完整SDK所有接口都能暴露出來缺點是JNI層代碼量大而且一旦SDK庫崩潰整個App進(jìn)程直接掛掉錯誤信息還不好定位。除非項目對延遲有極致要求否則我不推薦把控制SDK直接接在App進(jìn)程里。3.2 方案二Linux服務(wù)端轉(zhuǎn)發(fā)Android拉流推薦我自己最終采用的是服務(wù)端中繼方案。具體是把夜視機(jī)芯接在Linux網(wǎng)關(guān)設(shè)備上由Linux進(jìn)程負(fù)責(zé)SDK控制邏輯對外提供兩個能力標(biāo)準(zhǔn)RTSP轉(zhuǎn)發(fā)現(xiàn)拉流就是第2節(jié)提到的FFmpeg那套。一個輕量HTTP/WebSocket接口App通過JSON格式下發(fā)控制命令服務(wù)端轉(zhuǎn)發(fā)給SDK。Android端變成單純的應(yīng)用層開發(fā)只需要MediaCodec或ExoPlayer直接解碼RTSP流。HttpURLConnection/OkHttp發(fā)送控制請求。這個方案的最大好處是隔離異常。機(jī)芯SDK是C/C寫的內(nèi)存管理嚴(yán)苛偶發(fā)崩潰是常態(tài)。放在獨立進(jìn)程里最壞情況是重啟這個進(jìn)程App主進(jìn)程毫發(fā)無損。另外將來要接機(jī)芯本身帶的AI功能人形檢測、區(qū)域入侵報警也都在服務(wù)端統(tǒng)一處理Android端不用頻繁升級。3.3 動態(tài)權(quán)限與生命周期處理不管走哪條路Android端有幾個雷區(qū)必須先排掉。如果你用SurfaceView或TextureView渲染視頻需要注意網(wǎng)絡(luò)拉流不需要相機(jī)權(quán)限但一定不能申請相機(jī)權(quán)限否則部分機(jī)型的系統(tǒng)會強(qiáng)制拉起攝像頭導(dǎo)致后置夜視畫面變黑或者系統(tǒng)殺進(jìn)程。Android 6.0以上動態(tài)權(quán)限不多提反正網(wǎng)絡(luò)權(quán)限記得在AndroidManifest.xml里聲明uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE /生命周期處理是我差點翻車的地方。Activity在onPause()時必須停止解碼和渲染onResume()時重新連接。如果你用ExoPlayer這個邏輯本身就幫你管了如果你自己寫了解碼循環(huán)別忘了在onPause()里發(fā)一個中斷信號別直接調(diào)用stop()否則解碼線程可能會阻塞在讀取網(wǎng)絡(luò)流的地方無法退出。我的常見寫法是用AtomicBoolean作為運(yùn)行標(biāo)志private AtomicBoolean running new AtomicBoolean(false); Override protected void onPause() { super.onPause(); running.set(false); // 中斷阻塞調(diào)用 if (decoder ! null) { decoder.interrupt(); } } Override protected void onResume() { super.onResume(); running.set(true); // 重新啟動解碼線程 startDecoder(); }4. 高頻報錯與排查技巧這部分是我最想分享的。項目做下來90%的時間都花在排查各種奇怪問題上。4.1 常見錯誤速查表錯誤現(xiàn)象可能原因排查思路調(diào)用SDK初始化返回-1IP地址或端口不對用adb或ping確認(rèn)設(shè)備在線查看機(jī)芯后臺的TCP端口配置打開RTSP流超時機(jī)芯帶寬打滿或防火墻攔截554端口降低編碼碼率測試檢查防火墻規(guī)則用VLC先試?yán)饕曨l畫面全黑但有時間戳機(jī)芯處于彩色模式但環(huán)境光過暗或IR-CUT未切換調(diào)用SDK強(qiáng)制切到黑白模式確認(rèn)紅外燈亮起花屏/馬賽克RTSP走UDP丟包或機(jī)芯碼率突增超出解碼能力強(qiáng)制TCP傳輸降低分辨率到720p測試增大緩沖App崩潰但日志無Java堆棧JNI層C/C異常在關(guān)鍵Native函數(shù)入口加日志觀察logcat中SIGSEGV標(biāo)記用ndk-stack定位控制命令偶發(fā)無響應(yīng)機(jī)芯SDK請求和響應(yīng)異步未正確處理加報文重發(fā)和超時機(jī)制檢查命令序號是否沖突晝夜切換后圖像變暗攝像頭的自動曝光參數(shù)沒同步更新切換后延遲300ms再查詢曝光參數(shù)必要時手動設(shè)置曝光時間4.2 幾個容易忽略的坑第一個坑是機(jī)芯時間同步。夜視機(jī)芯的SDK在觸發(fā)報警錄像時時間戳用的是機(jī)芯內(nèi)部時鐘。如果機(jī)芯沒做NTP同步錄像回放的時間戳?xí)y得一塌糊涂。我在Linux服務(wù)端每次啟動時主動把系統(tǒng)時間通過SDK設(shè)置到機(jī)芯里問題就解決了。第二個坑是網(wǎng)絡(luò)抖動下的重建策略。RTSP流斷開之后FFmpeg默認(rèn)不會自動重連我踩過這個坑。必須顯式處理if (ret 0) { avformat_close_input(fmt_ctx); // 重建注意指數(shù)退避5秒、10秒、20秒... sleep(retry_count * 5); retry_count; goto reconnect; }重連邏輯要加個上限否則機(jī)芯斷電期間會瘋狂重建把系統(tǒng)資源打滿。第三個坑和Android的硬件解碼有關(guān)。部分機(jī)型的MediaCodec解碼器對分辨率變化非常敏感。機(jī)芯SDK在用戶調(diào)整分辨率后SPS/PPS會發(fā)生變化如果App沒重新初始化解碼器就會出現(xiàn)綠屏或解碼失敗。穩(wěn)妥的處理是拉流成功后監(jiān)聽SPS變化一旦發(fā)現(xiàn)分辨率改變立即重建解碼器。ExoPlayer在這一塊做得比較完善但如果你是自己寫的解碼循環(huán)必須自己接管這部分的邏輯。第四個坑很隱蔽多路相機(jī)同時接入時容易發(fā)生。OpenCV、FFmpeg、NCNN這些組件內(nèi)部會創(chuàng)建自己的線程池如果每路視頻都加載一份FFmpeg庫實例內(nèi)存占用會爆炸。我最后是用單例模式共享FFmpeg的全局初始化各路由共用一個AVIOBufferPool實際測試下來8路1080p同時接入內(nèi)存穩(wěn)定在1.2GB以內(nèi)。4.3 性能優(yōu)化三板斧性能這個東西沒有最優(yōu)化只有符合場景。我這次項目在Linux網(wǎng)關(guān)設(shè)備和Android端分別做了優(yōu)化思路大致如下解碼側(cè)優(yōu)先硬解。Linux網(wǎng)關(guān)設(shè)備用的是瑞芯微RK3588平臺MMP硬件解碼能力很強(qiáng)FFmpeg只需要設(shè)置-c:v h264_rkmpp就能啟用。Android端用MediaCodec硬解比軟解在同等分辨率下CPU占用能少60%左右。色彩空間轉(zhuǎn)換軟解出來的YUV轉(zhuǎn)RGB/BGR最好用NEON指令優(yōu)化OpenCV的cvtColor在ARM上其實有優(yōu)化但如果你只需要灰度圖完全可以把Y通道直接提出來用省掉一次轉(zhuǎn)換的開銷。夜視場景下很多算法比如基礎(chǔ)的煙霧檢測、區(qū)域入侵根本不關(guān)注顏色。網(wǎng)絡(luò)傳輸Android端如果直接用RTSP Player比如Vitamio、ijkplayer要注意開啟硬解和自動緩沖配置。ijkPlayer有一個參數(shù)framedrop設(shè)為1在網(wǎng)絡(luò)抖動時可以丟幀保實時實測卡頓感明顯改善。5. 復(fù)盤總結(jié)與項目心得整個項目從接到需求到全部跑通前后花了兩周半。第一天在確認(rèn)SDK文檔、搭環(huán)境后面三天都在各種排查底層問題。我自己最大的體會是第三方SDK集成項目真正的難點不在“調(diào)用接口”而在于“理解約定”。這里的“約定”包括很多層面?zhèn)鬏攨f(xié)議約定是TCP還是UDP是長連接還是短連接命令響應(yīng)是同步還是異步。平臺約定庫的ABI是否匹配依賴的系統(tǒng)庫是否存在。生命周期約定設(shè)備斷開后數(shù)據(jù)結(jié)構(gòu)是否還保留重復(fù)初始化的規(guī)則是什么。時間約定命令發(fā)出后多久算超時重發(fā)的冪等性怎樣。每一個約定沒弄明白都可能在后面某個時段跳出來咬你一口。我的建議是拿到SDK后的前半天不要急著聯(lián)調(diào)先把文檔通讀一遍把接口清單列出來再寫一個小的demo程序驗證每個接口的返回值。這個過程能幫你過濾掉大部分后期要踩的坑。再分享一個小技巧我這邊在調(diào)機(jī)芯的時候把每個接口的入?yún)⒊鰠ⅰ嶋H返回值和耗時都記錄下來整理成一個離線表格。后來做Android端的時候照著表格快速核對避免重復(fù)采坑。而且這個表格最終也成了交接給客戶的文檔基礎(chǔ)一舉兩得。最后說一句夜視機(jī)芯SDK對接這個方向的坑雖然多但套路其實是固定的。只要把鏈路分層清理清楚采集層、傳輸層、解碼層、應(yīng)用層每一層的職責(zé)劃分明確之后換任何品牌機(jī)芯也只是換一個SDK殼子的事。希望這篇實戰(zhàn)記錄能幫你少走點彎路。