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