摘要:本文從NDI 提供的SDK(Android)出發(fā),通過Android Studio進(jìn)行開發(fā)粗悯,實現(xiàn)了Android手機端對NDI視頻流的發(fā)送和接收俄占,并在局域網(wǎng)里測試辐烂,打通了Android端和PC端通過NDI互相串流及鏡像顯示。
關(guān)鍵字:NDI验残、JNI捞附、C++、Cmake胚膊、NDK故俐、NSD、Service紊婉;
一药版、前言
????????NDI是Newtech公司(目前被Vizrt收購)基于IP網(wǎng)絡(luò)里面?zhèn)鬏敎\壓縮視頻流的方案,NDI作為一款純軟件標(biāo)準(zhǔn)喻犁,相比于RTMP槽片、RTS等視頻流協(xié)議,它具有更低的延遲(理論延遲只有16行)肢础、4K和高清的實時兼容还栓、支持Alpha通道的傳輸、以及Tally传轰、元素?fù)?jù)剩盒、控制協(xié)議等、能夠快速的構(gòu)建信號的擴展和連接慨蛙,任意NDI流辽聊、任意分辨率纪挎,只要在一個局域網(wǎng)里,就能被發(fā)現(xiàn)和接收,并統(tǒng)一的進(jìn)行輸出。
????????相比于ST2110的標(biāo)準(zhǔn)舶掖,NDI是能夠更有效的降低制作成本,對于廣電制作烤蜕,NDI有“Full NDI”支持輸出更高的碼率,對于手機端和PC端迹冤,NDI有“NDI HX”支持輸出較低的碼率讽营,兩者在局域網(wǎng)內(nèi)互相兼容,都能被發(fā)送和接收到叁巨。
????????本次項目開發(fā)的時候還是基于NDI 4.0的SDK版本斑匪,目前NDI已經(jīng)支持5.0,NDI 5.0向下兼容4.0,此次介紹的一些方法和庫锋勺,同樣適用于5.0版本,具體功能可參考官方文檔NDI SDK文檔蚀瘸。
二、Android端NDI框架的設(shè)計與搭建
2.1庶橱、導(dǎo)入NDI SDK并構(gòu)建依賴項
????????NDI SDK是用C++原生代碼進(jìn)行開發(fā)贮勃,想要在Android端使用C++的原生代碼,需要下載NDK和CMake兩個SDK工具包苏章,其中NDK工具集能夠允許Andoroid端調(diào)用C++代碼寂嘉,且通過JNI這類平臺庫,我們可以在MainActivity直接調(diào)用C++的函數(shù)枫绅,CMake是一款外部構(gòu)建工具泉孩,搭配“build.gradle”一起用,用來導(dǎo)入和關(guān)聯(lián)第三方C++庫并淋,具體操作方法如下:
1寓搬、在“Tools->SDK Manager->Android SDK->SDK tool”下,下載CMake和NDK工具县耽;
2句喷、如果在新建項目的時候,默認(rèn)勾選了“include C++ support”,會在項目文件的“/src/main/"目錄下多一個cpp的文件夾兔毙,cpp文件夾的根目錄有CMake文件的模板唾琼,同時關(guān)于C++程序的頭文件和編譯好的*.so數(shù)據(jù)庫文件,放到對應(yīng)include和jniLibs文件夾下澎剥,如下圖所示锡溯,這些文件夾也可以通過自建的方式添加到項目里;
3、“CMake.txt”文件關(guān)聯(lián)本地庫和第三方C++庫的方式如下圖所示祭饭,其中“native-lib”為本地庫涌乳,本地庫的名稱和路徑在CMake文件內(nèi)指定,默認(rèn)首選路徑是cpp文件夾的根目錄甜癞,當(dāng)cpp的根目錄找不到“native-lib.cpp”文件時,再到include文件夾下找宛乃;ndi-lib是導(dǎo)入的第三方庫悠咱,文件名是“l(fā)ibndi.so”,此處NDI SDK針對不同版本的CPU設(shè)定有不同的“l(fā)ibndi.so”文件征炼,系統(tǒng)會根據(jù)實際手機的CPU去優(yōu)選最匹配的“l(fā)ibndi.so”文件析既,當(dāng)匹配上第三方庫之后,就能在include文件夾下谆奥,調(diào)用第三方庫的C++頭文件眼坏;
4、“native-lib.cpp”文件默認(rèn)在關(guān)聯(lián)C++項目的時候自動生成酸些,里面通過JNI接口的方式宰译,打通C++和JAVA的對接,如下圖所示魄懂,我在此處用C++規(guī)范了一些NDI的初始化沿侈、推流、拉流的函數(shù)市栗,然后通過JNI接口把對應(yīng)的函數(shù)和參數(shù)映射到JAVA程序缀拭,圖中舉例是NDI初始化的接口函數(shù),返回類型為“空”填帽,函數(shù)名為“initialNDI”蛛淋,在JAVA文件中對應(yīng)的調(diào)取路徑為“Java_com_example_ndkdemo”項目,“RecordService”文件下,這些命名方式都是在“RecordService.java”中引入“native-lib”關(guān)聯(lián)項和函數(shù)以后篡腌,在”native-lib.cpp“中自動生成的接口函數(shù)褐荷;
????????對應(yīng)“RecordService.java”文件中,首先需要引入“native-lib.cpp”關(guān)聯(lián)項哀蘑,如下圖所示诚卸,然后通過自定義的方式創(chuàng)建接口文件,接口文件可以創(chuàng)建很多個绘迁,“native-lib.cpp”文件中合溺,也可以引用之前導(dǎo)入好的第三方庫文件的頭文件,只要是涉及到C++語法的缀台,都可以在“native-lib.cpp”文件中操作棠赛,并通過JNI接口轉(zhuǎn)成JAVA函數(shù),具體可以參照J(rèn)NI的相關(guān)教程;
5睛约、”build.gradle“為Android程序的構(gòu)建文件鼎俘,其中包括第三方JAVA庫的依賴項,還有CMake文件的關(guān)聯(lián)項目辩涝,如下圖所示贸伐,在cppFlags項中,”-frtti“希望Android程序支持RTTI怔揩,”-fexceptions“表示Android程序啟動對C++異常處理的功能捉邢,這兩項功能不填,也不影響最終的編譯商膊,只是為了有助于調(diào)試程序而打開伏伐;
6、”AndroidManifest.xml“文件中規(guī)范了Android程序的主進(jìn)程”MainActivity“啟動入口晕拆、規(guī)范程序的權(quán)限以及注冊主進(jìn)程中運行的服務(wù)藐翎,如下圖所示,主進(jìn)程中注冊了”RecordService“服務(wù)实幕,該服務(wù)可以被實例化吝镣,并且服務(wù)可以以”mediaProjection“接口服務(wù)的方式運行于前臺服務(wù)(服務(wù)優(yōu)先級最高);
????????原先Android5.0之前昆庇,AndroidManifest.xml文件中赤惊,能夠規(guī)范Android程序的所有權(quán)限,自5.0以后凰锡,某些Android程序的權(quán)限未舟,如調(diào)取屏幕、攝像頭掂为、麥克風(fēng)等操作裕膀,均通過在Activity進(jìn)程中,動態(tài)授權(quán)的方式獲取勇哗,這樣做同時也是為了保障Android程序的健康安全昼扛,下圖中顯示AndroidManifest.xml中打開的權(quán)限主要為網(wǎng)絡(luò)訪問和前臺服務(wù)的權(quán)限;
2.2欲诺、NDI發(fā)送端實現(xiàn)方式
NDI發(fā)送端程序流程框圖如下圖所示:
1抄谐、在MainActivity主進(jìn)程建立時,開啟一個按鈕監(jiān)聽扰法,用于開啟錄屏服務(wù)(RecordService)并推送為前臺服務(wù)蛹含,同時綁定錄屏服務(wù)并返回一個錄屏服務(wù)的實例對象;
2塞颁、此處把bindService和startForegroundService兩種服務(wù)的開啟方式進(jìn)行了混用浦箱,混用后實現(xiàn)了Service服務(wù)與主線程的通信吸耿,同時把Service服務(wù)實例化,兩種服務(wù)方式的開啟并不會產(chǎn)生兩個單獨的錄屏服務(wù)酷窥,首先通過bindService綁定并連接上RecordService服務(wù)咽安,覆寫RecordService內(nèi)”onCreate“和”onBind“兩種方法,具體程序如下蓬推,綁定RecordService后使用“ServiceConnection”對服務(wù)進(jìn)行連接妆棒,連接前根據(jù)具體需求,可以申請一些動態(tài)權(quán)限沸伏,如存儲的讀寫募逞、手機狀態(tài)的讀取和錄制音頻等,連接成功后馋评,返回RecordService實力對象;
????????在”RecordService“內(nèi)的”onCreate"重寫函數(shù)中刺啦,”getDisplayMetrics“為獲取手機顯示畫布留特,進(jìn)而得到手機顯示的長、寬玛瘸、dpi等參數(shù)蜕青;后臺線程”serviceThread“,用做后臺錄屏的子線程容器糊渊;“NSDServer”為網(wǎng)絡(luò)服務(wù)發(fā)現(xiàn)右核,為Android自帶的,用于在局域網(wǎng)內(nèi)網(wǎng)絡(luò)服務(wù)發(fā)現(xiàn)渺绒;initialNDI為初始化NDI的JNI接口程序贺喝。
????????在”RecordService“內(nèi)的在“onBind”重寫函數(shù)中,主要為了在主進(jìn)程中連接上錄屏服務(wù)(RecordService)后宗兼,返回一個服務(wù)對象躏鱼,實現(xiàn)主進(jìn)程和服務(wù)之間的通信,如停止服務(wù)的操作殷绍。
3染苛、當(dāng)在MainActivity主進(jìn)程中按下按鈕以后,打開錄屏接口(MediaProjectionManager)并觸發(fā)“startForegroundService”主到,將RecordService服務(wù)開啟為前臺服務(wù)茶行,具體程序如下;
????????獲取MediaProjectionManager服務(wù)實例登钥,并調(diào)用錄屏權(quán)限“createScreenCaptureIntent”;
????????通過"startActivityForResult",將錄屏權(quán)限(captureIntent)和固定參數(shù)(RECORD_REQUEST_CODE)傳入其中畔师,等待動態(tài)權(quán)限的申請結(jié)果;
????????等待動態(tài)錄屏權(quán)限申請通過牧牢,之后將通過成功后返回的參數(shù)傳遞給RecordService服務(wù)茉唉,并將RecordService開啟為前臺服務(wù)固蛾;
4、之前在bindService的時候度陆,已經(jīng)生成并返回了RecordService服務(wù)的實例對象艾凯,因此“startForegroundService”開啟以后,直接在RecordService服務(wù)中懂傀,重寫“onStartCommand”和“onDestroy”兩個方法趾诗。
????????在”RecordService“內(nèi)的”onStartCommand"重寫函數(shù)中,創(chuàng)建RecordService前臺服務(wù)的通知欄和ID號(必須創(chuàng)建)蹬蚁,將動態(tài)申請錄屏服務(wù)時返回的參數(shù)獲取到恃泪,并依據(jù)這些創(chuàng)建“MeidaProjection”錄屏服務(wù)實例,并開始錄屏犀斋;
5贝乎、錄屏和NDI推流通過“startCapture”函數(shù)來實現(xiàn),首先通過"createVirtualDisplay"創(chuàng)建一塊虛擬畫布叽粹,傳入之前讀取的手機長览效、寬、dpi參數(shù)并通過“ImageReader”實例化對象虫几,將手機桌面讀取并放到虛擬畫布上锤灿;
????????然后啟動之前在RecordService服務(wù)“onCreate”時就初始化的線程“serviceThread”,等待“createVirtualDisplay”中創(chuàng)建對應(yīng)尺寸的畫布辆脸,程序里等了1000ms但校,實際創(chuàng)建畫布不需要這么長;
????????最后通過子線程“backgroundHandler”中放入循環(huán)的“serviceThread”線程啡氢,每次循環(huán)后自動去調(diào)取回調(diào)函數(shù)ChildCallback,用于后臺錄屏數(shù)據(jù)實時推流NDI状囱;
6、在“ChildCallback”回調(diào)函數(shù)中倘是,每次回調(diào)觸發(fā)以后浪箭,讀取最新手機錄屏圖像,讀取圖像以RGBA的方式進(jìn)行排布辨绊,每個像素點占4個byte奶栖,分別為RGBA,將圖像從左往右门坷、從上往下宣鄙,按照像素點的方式,以16進(jìn)制數(shù)據(jù)存入到byte數(shù)組中默蚌,得到的“b”數(shù)組是正向手機錄屏數(shù)據(jù)冻晤;
????????同時將這些像素點,以鏡像的方式重排绸吸,存入"mirrorByte"數(shù)組中鼻弧,與“b”數(shù)組一起设江,通過JNI接口推送給C++函數(shù),并一起推流攘轩,最終實現(xiàn)正向畫面和鏡像畫面同時出現(xiàn)在NDI網(wǎng)絡(luò)中叉存;
2.3、NSD(Network Service Discovery)服務(wù)的使用
????????為了使得發(fā)送的NDI流能夠被局域網(wǎng)里的其他設(shè)備發(fā)現(xiàn)度帮,需要在Android端打開Google原生的NSD服務(wù)歼捏,并且在Android端NDI目前不支持第三方的網(wǎng)絡(luò)發(fā)現(xiàn)服務(wù)(蘋果端使用的是Bonjour)。NSD是Andorid SDK內(nèi)部自帶的類庫笨篷,它的作用是為下一步的連接提供準(zhǔn)備瞳秽,如提供IP地址和端口號等,我們此次發(fā)送端率翅,作為NSD服務(wù)端發(fā)布到局域網(wǎng)中练俐,它定義了主機的名字、端口號冕臭,并進(jìn)行了注冊腺晾,為NSD客戶端的連接做準(zhǔn)備。
????我在Android發(fā)送端程序里浴韭,建立了一個“NSDServer”的類,專門用于NSD服務(wù)端的注冊監(jiān)聽脯宿,具體步驟如下:
1念颈、設(shè)置注冊監(jiān)聽函數(shù),實例化監(jiān)聽服務(wù)器连霉,用于監(jiān)聽之后的注冊服務(wù)是否成功榴芳,如注冊失敗后的操作(onRegistrationFailed)、注冊成功后的操作(onServiceRegistered)跺撼、解除注冊失敗后的操作(onUnregistrationFailed)等等窟感;
2、設(shè)置注冊NSD服務(wù)端函數(shù)歉井,設(shè)置服務(wù)端口號柿祈、服務(wù)端名稱、服務(wù)類型哩至,服務(wù)端的端口號是5960(NDI SDK文檔規(guī)定)躏嚎;
3、設(shè)置取消注冊函數(shù)菩貌,用于停止NSD服務(wù)端的操作卢佣;
4、NSD服務(wù)端狀態(tài)的監(jiān)聽接口箭阶,并設(shè)置實例化對象虚茶;
5戈鲁、當(dāng)“NSDServer”類寫完以后,需要在“RecordService”服務(wù)中嘹叫,實例化該類婆殿,并調(diào)用"startNSDServer"接口,實現(xiàn)整個NSD服務(wù)端的初始化待笑、注冊及監(jiān)聽鸣皂;關(guān)于NSD客戶端的掃描、發(fā)現(xiàn)和解析NSD服務(wù)器暮蹂,主要應(yīng)用在NDI接收端寞缝,在此不做介紹。
2.4仰泻、NDI SDK中JNI接口的實現(xiàn)方式
????????在“RecordService”服務(wù)中荆陆,調(diào)用了兩個NDI的JNI接口函數(shù),分別為“initialNDI”和“publishNDI”集侯,在“RecordService”服務(wù)中以JAVA方式的寫法如下被啼,分別表示初始化NDI流和推送NDI留,這類函數(shù)的用法棠枉,跟“RecordService”服務(wù)中其他函數(shù)的使用方法一樣浓体。
? “nativie-lib.cpp”的C++文件中,對應(yīng)有initialNDI和publishNDI的C++函數(shù)實現(xiàn)方式辈讶,在了解C++函數(shù)前命浴,首先需要知道NDI SDK中規(guī)范的NDI流發(fā)送流程,具體步驟如下:
1贱除、初始化NDI相關(guān)庫(NDIlib_initialize)生闲,官方要求開始NDI之前一定要做初始化工作;
2月幌、創(chuàng)建NDI發(fā)送端的配置指針(NDIlib_send_create_t )碍讯,根據(jù)實際情況,設(shè)定NDI發(fā)送流的名字扯躺、組等信息捉兴;
3、依據(jù)創(chuàng)建好的NDI配置指針录语,實例化對應(yīng)的發(fā)送對象(NDIlib_send_instance_t)轴术,并返回一個發(fā)送對象的指針;
4钦无、把圖像數(shù)據(jù)動態(tài)分配給一個空間內(nèi)逗栽,并返回一個指針,如果圖像采用1920*1080的分辨率失暂,且使用BGRA的顏色空間彼宠,那么實際給每一幀圖像分配的內(nèi)存空間為1920*1080*4鳄虱,單位為Byte;
5凭峡、初始化NDI視頻幀的指針(NDIlib_video_frame_v2_t)拙已,設(shè)置NDI視頻幀對應(yīng)的分辨率、色彩空間摧冀、寬高倍踪、幀率等,可根據(jù)項目實際情況具體設(shè)置索昂;
6建车、將視頻幀的指針,放入到NDI發(fā)送的緩存里(NDIlib_send_send_video_v2)椒惨,NDI自動發(fā)送視頻數(shù)據(jù)缤至;
7、之后清空視頻幀里對應(yīng)的視頻數(shù)據(jù)康谆,重新放入新的數(shù)據(jù)并發(fā)送领斥,并進(jìn)入循環(huán);
8沃暗、發(fā)送音頻的原理跟發(fā)送視頻一樣月洛,需要指定音頻幀的指針(NDIlib_audio_frame_v3_t),分配空間孽锥,并放入發(fā)送音頻的數(shù)據(jù)(NDIlib_send_send_audio_v3)嚼黔;
????????在Android Studio中使用JNI接口具體通過“nativie-lib.cpp”實現(xiàn),在程序編譯之初忱叭,“nativie-lib.cpp”本地就運行了一遍C++程序隔崎,因此“nativie-lib.cpp”本地的全局參數(shù)和對象今艺,都可以直接被應(yīng)用到函數(shù)中韵丑,使用JNI接口發(fā)送NDI留的具體實現(xiàn)方式如下:
1、“nativie-lib.cpp”本地實例化兩個NDI發(fā)送端的配置指針(NDIlib_send_create_t?)虚缎、NDI視頻幀的指針(NDIlib_video_frame_v2_t)和發(fā)送對象(NDIlib_send_instance_t)撵彻,主要用作正常視頻和鏡像視頻的發(fā)送;
2实牡、在“nativie-lib.cpp”的"initialNDI"函數(shù)中陌僵,初始化一些發(fā)送配置,如設(shè)定NDI流的名字创坞,色彩空間信息碗短;
3、在"RecordService"服務(wù)中题涨,調(diào)用“initialNDI”的JNI接口后偎谁,實際跳轉(zhuǎn)運行如下总滩,初始化時,創(chuàng)建了兩個發(fā)送對象指針“p_send”和“p_send2”;
4巡雨、在“nativie-lib.cpp”的"publishNDI2"函數(shù)中闰渔,設(shè)定好發(fā)送視頻的分辨率,根據(jù)視頻數(shù)據(jù)的指針铐望,發(fā)送正常和鏡像兩個NDI流冈涧;
5、在"RecordService"服務(wù)中正蛙,調(diào)用“publishNDI”的JNI接口后督弓,實際跳轉(zhuǎn)運行如下,"RecordService"服務(wù)中跟畅,將已經(jīng)生成的正常咽筋、鏡像數(shù)據(jù)的數(shù)組發(fā)送給JNI接口,JNI接口里再將這些數(shù)組轉(zhuǎn)成jbyte指針徊件,就能供“publishNDI2”函數(shù)調(diào)用奸攻;
6、在"RecordService"服務(wù)中循環(huán)讀取最新桌面的錄屏圖像虱痕,并轉(zhuǎn)成byte數(shù)組睹耐,發(fā)送給“publishNDI”函數(shù)進(jìn)行推流。
三部翘、NDI SDK在使用中的分析
????NDI SDK雖然支持蘋果端和Android的開發(fā)應(yīng)用硝训,但在底層,套用的還是一套C++的庫文件新思,到手機端需要通過不同的接口轉(zhuǎn)換窖梁,因此在實際開發(fā)應(yīng)用中,還會遇到不少的瓶頸和疑惑夹囚,我把優(yōu)缺點進(jìn)行了總結(jié)和分析纵刘。
NDI SDK的優(yōu)點:
1、基于C++開發(fā)荸哟,對外只暴露一些接口調(diào)用函數(shù)假哎,可通過特定接口(JNI)實現(xiàn)跨平臺應(yīng)用的開發(fā),代碼統(tǒng)一鞍历,更新迭代方便舵抹;
2、NDI的收發(fā)對流進(jìn)行自適應(yīng)匹配劣砍,發(fā)送端不管是什么分辨率惧蛹、寬高比、幀率,在接收端都能收到并進(jìn)行匹配香嗓,NDI SDK中爵政,不需要對流的編解碼,做深入的處理陶缺;
3钾挟、NDI SDK代碼開源,有很好的收發(fā)流例子饱岸,低成本的特性掺出,使得國內(nèi)外開發(fā)應(yīng)用案例比較多。
NDI SDK的缺點:(主要針對在Android端的開發(fā))
1苫费、NDI內(nèi)部視頻流的編解碼不開放汤锨,NDI SDK里面只是把像素數(shù)據(jù)發(fā)送給NDI發(fā)送端,NDI 4的不支持H.264/265相關(guān)應(yīng)用設(shè)置百框,NDI 5支持闲礼,但有限制,并且調(diào)用SDK只能發(fā)送HX的流铐维;
2柬泽、NDI SDK中,不支持流的實施輸入和輸出嫁蛇,如Android端使用錄屏接口“mediaProjection”的流锨并,沒法送給NDI,只能需要通過截屏的方法睬棚,獲取像素信息后第煮,在發(fā)送給NDI接收,這樣效率比較低抑党,延遲大包警,NDI 5中還沒有給出很好的支持方法,只能依賴Android端“mediaProjection”的流的轉(zhuǎn)換底靠;
3害晦、Android端本身由于安全性考慮,要把本地的音頻流串流到NDI里面苛骨,需要改復(fù)雜的權(quán)限設(shè)置篱瞎,目前暫時只支持麥克風(fēng)的調(diào)用苟呐;
4痒芝、沒有對于高清、4K流的一些詳細(xì)規(guī)范牵素,比如說4K制作中最常見的HLG和BT2020的設(shè)置严衬,NDI為了降低應(yīng)用沒看,把很多涉及編解碼的東西都放到了后臺處理笆呆,這就使得我們在做一些專業(yè)視音頻項目開發(fā)的時候很痛苦请琳,得不到更好的支持粱挡;
四、總結(jié)
????此次應(yīng)用NDI SDK開發(fā)俄精,主要是想用于NDI在便攜式提詞器上的一些應(yīng)用询筏,比如說攝像出去外拍,只需要帶1太攝像機和Android平板竖慧,Android平板端為NDI接收嫌套,攝像手機端為NDI發(fā)送,通過無線的方式圾旨,把稿件信息直接串流到Android平板上踱讨,省去的復(fù)雜的提詞器系統(tǒng)搭建和設(shè)置過程;因此在程序設(shè)計時砍的,除了輸出NDI主畫面以外痹筛,還輸出了一個鏡像畫面,使用人員可以根據(jù)實際應(yīng)用廓鞠,使用主畫面或鏡像畫面帚稠。
????NDI SDK開發(fā)的時候是在3月份,當(dāng)時還只有NDI 4的支持床佳,很多NDI的工具原理還不清楚翁锡,只能在開發(fā)過程中一步步嘗試,開發(fā)出來的產(chǎn)品幀率也比較低夕土,今后基于NDI 5,尤其是NDI Bridge和H265編解碼方面馆衔,還要做更深入的研究,下圖為實際的NDI輸出效果怨绣。