如何開發(fā)一個app建設一個網站關鍵詞搜索熱度查詢
我不生產代碼,我只是代碼的搬運工,相信我,看完這個文章你的圖片一定能變成流媒體推出去。
訴求:使用opencv拉流,轉成bgr數(shù)據(jù),需要把處理后的數(shù)據(jù)(BGR)編碼成264,然后推流推出去,相當于直播(實時編碼)
播放器
超低延遲的RTSP播放器
https://github.com/tsingsee/EasyPlayer-RTSP-Win
青犀的一個播放器,直接下他的EasyPlayer-RTSP-Win用來測試就行。劃重點,超低延時,我整體方案的延時大概是600-700ms,使用??迪鄼C,rtsp拉流,做了yolo處理,再推出去,有編碼,有解碼,vlc的延時設置低了就回卡幀,Gop已經改成5了還是卡幀,沒有測試Gop改成1的情況,但是vlc的延時和流暢,整體看是不太兼容的。ffmpeg使用nobuffer也會卡幀。直觀感受卡的就是Gop的P幀。
服務器
live555 方案
如果你不著急的話。。。 可以試試這個方案,這方面的參考文獻給列下面了,因為確實正經研究了幾天還看了不少代碼,認真想了應該怎么處理,但是確實不太想寫,而且對我的需求來講live555冗余了很多功能,再加上網上確實沒有寫好的,我又很著急要結果,確定方案能用,所以也沒有用這個方案。
官方demo
live555自己的測試文件是有推流demo的,主要是根據(jù)實時需求推264文件,以及無腦做一個推264文件的服務器,當時看代碼的時候一頭霧水加上著急,也沒太認真看,主要在live555\testProgs下面,testOnDemandRTSPServer,testH264VideoStreamer ,第二個是無腦推,第一個是你來一個請求,我從頭開始給你播放一次視頻文件。
這個東西的底層是向一個fTo指針里面拷貝264碼流。
如果你Cpp、coding能力強的話,應該是能看懂直接改的,也就不用往后看了。
參考demo 零聲 usb相機推流
網上基本上和我需求最接近的live555方案下的代碼是國內的做音視頻開發(fā)教學的一個零聲出的視頻還有他們傳的這個代碼。
主要功能是 v4l2相機讀取mjpeg,然后ffmpeg的avcodec相關庫編碼,然后送live555,然后推實時流,像是改的testOnDemandRTSPServer,結構很清晰,除了不能用我也找不到原因外都挺好的。另外他的課是5K的,有點貴。
視頻的話去B站帶關鍵詞,基本都能搜到,這個代碼我加了那個聯(lián)系QQ要到的。但是在我本地沒有推成功,我也不確定是哪里的問題,編譯過了,放在這里
https://gitee.com/qingfuliao/v4l2_ipc_live555?_from=gitee_search
在我這是下面這個demo 實現(xiàn)了一個相似的功能,編譯實測是可以讀取usb相機然后推流成功的。但是代碼結構沒有上面那個清晰。
參考demo
https://github.com/mpromonet/v4l2rtspserver
這個功能是基于linux的v4l2,使用264的方式讀取相機視頻流(如果你的usb相機不支持264輸出,會驅動失敗),然后直接拆幀把流發(fā)出去
需要自己下一個libv4l2cpp的代碼放進來就能編譯了
這個現(xiàn)在看稍微改一改就能用了,不過當時對整體沒有概念,改了一陣子不知道怎么下手把我自己的碼流變成demo的輸入,碼流送進去了但是沒有推成功,定位了一會兒很難定位問題,也就擱置了。有興趣的可以基于這個改一改。
https://blog.csdn.net/qq_43418269/article/details/122488866
這個方案我是成功了的,不過延時不太能滿足我的需求,這個復現(xiàn)很快。他是用一個管道文件做的,我把編碼之后264文件直接寫到live555的testOnDemandRTSPServer.cpp這邊的讀取文件里面,然后邏輯是live555這邊接收到請求,創(chuàng)建管道,相機程序初始化后阻塞住,管道被創(chuàng)建后往管道里寫,然后另一邊就開始播。。
這個就相當于是運行在一個demo里的兩個程序。流倒是推出來了,只不過,我這樣實現(xiàn),延時很大,他說的百毫秒量級我做不到,我是2s左右。感覺也可能是我操作不當。
https://blog.csdn.net/lifexx/article/details/52823777
live555讀文件改為內存讀取實現(xiàn),確實C++不太行,這個文章對我理解Live555,還有改成內存中的數(shù)據(jù)方向給了很大啟發(fā),但是沒有按照他的做,而且他的參考代碼無法運行。對我理解另一個推相機的demo有幫助
其他開源服務器框架
這個也是一個很容易就編譯成功的服務器,可以使用這個做服務器,然后調用ffmpeg推流,在RK3588上也推成功了,基本沒改make相關的配置,需要按照他給的快速開始流程使用git下附加庫,功能很強大,但是對我的需求來講,這個功能我進行二次開發(fā)比較慢。不排除我太菜。
https://github.com/ZLMediaKit/ZLMediaKit/tree/master
這里寫這個的主要原因是他的一些文章對我的啟發(fā)和快速上手有很大的參考意義,比如下面這個。
https://github.com/ZLMediaKit/ZLMediaKit/wiki/%E6%80%8E%E4%B9%88%E6%B5%8B%E8%AF%95ZLMediaKit%E7%9A%84%E5%BB%B6%E6%97%B6%EF%BC%9F
RK3399 參考
網上有瑞芯微其他方案的rtsp推流,我只能幫忙排除錯誤選項
https://t.rock-chips.com/forum.php?mod=viewthread&tid=749&extra=page%3D1
如果你是在看這個帖子,可以不用看了,這個貓頭的代碼雖然推出去了,但是他的Rtsp是調庫,這個庫是閉源的,3588沒有,這個網站注冊要兩三天才能通過,不必等這個,用不了。
rtsp推流
在github上搜索rtsp,排名最高的那個結果,就是那個小烏龜,
https://github.com/PHZ76/RtspServer
這個功能比較單一,但是足夠滿足我的需求了,他還使用他自己的庫做了一個windows下的應用,windows上編譯成功了,但是不太好用,不過對我理解他的Demo運行有一定幫助。因為用的是一套庫。另外他主頁還有一個rtmp ,我沒進去看也沒試能不能用。
https://github.com/PHZ76/DesktopSharing
下載安裝
下載下來直接在3588上面make就可以,編譯的是RtspServer-master/example里面的main 文件,這里的rtsp_h264_file.cpp是可以直接推運行推264文件的,一般不需要修改就能直接用,如果不能用,有可能是554端口被占用,改一個大點的就好了
std::string suffix = "live";std::string ip = "127.0.0.1";std::string port = "5543";// 改這里 不要改那個"0.0.0.0" 那個是對的不用改std::string rtsp_url = "rtsp://" + ip + ":" + port + "/" + suffix;
然后rtsp_pusher.cpp還有rtsp_server.cpp都把發(fā)送文件的部分注釋掉了,需要結合h264那個文件來對比把264碼流寫進去。
運行測試
編譯出來之后運行 ./rtsp_h264_file ./test.264 就能推出來了
工程已經被我魔改過了,重新生成一個sample 來演示下結果
這是RK3588 服務器
這是VLC的界面IP
下面的是顯示的結果
改cmake 支持opencv 、rknn,mpp
因為我使用的RKNN之間是在Qt里面編譯的,工程使用的都是Cmake的cmakelists,他的makefile也不難改,主要的問題是我自己寫的解碼器,在使用makefile指定mpp庫之后編譯出來的mpp庫運行不正常,具體報錯找不到了,然后我qt上用是沒問題的,定位到是makefile沒寫好,改成cmakelist就可以正常編譯了
-g是為了支持gdb調試,配合我的vscode 調試配置文件,可以單步調試,全程在板上編譯,沒配置交叉編譯環(huán)境
cmake_minimum_required(VERSION 3.5)
project(rtspserver)
set(CMAKE_INCLUDE_CURRENT_DIR ON)set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSOCKLEN_T=socklen_t -g ")find_package(OpenCV 4.5.5 REQUIRED)
find_package(OpenSSL REQUIRED)# MPP
set(MPP_PATH /home/orangepi/code/mpp-develop/inc)
set(MPP_LIBS /home/orangepi/code/mpp-develop/mpp/librockchip_mpp.so)
include_directories(${MPP_PATH})# OSAL
set(OSAL_PATH /home/orangepi/code/mpp-develop/osal/inc/ /home/orangepi/code/mpp-develop/utils)
set(OSAL_LIBS /home/orangepi/code/mpp-develop/osal/libosal.a /home/orangepi/code/mpp-develop/utils/libutils.a)
include_directories(${OSAL_PATH})# RKNN lib
set(RKNN_API_PATH ${CMAKE_SOURCE_DIR}/lib)
set(RKNN_RT_LIB ${RKNN_API_PATH}/aarch64/librknnrt.so)
include_directories(${RKNN_API_PATH}/include)
include_directories(${CMAKE_SOURCE_DIR}/3rdparty)# RGA
set(RGA_PATH ${CMAKE_SOURCE_DIR}/3rdparty/rga/RK3588)
set(RGA_LIB ${RGA_PATH}/lib/Linux/aarch64/librga.so)
include_directories(${RGA_PATH}/include)aux_source_directory(src/xop SOURCE1)
aux_source_directory(src/net SOURCE2)include_directories( src src/xopsrc/netsrc/3rdpart)# add_executable(rtsp
# example/rtsp_server.cpp
# ${SOURCE1}
# ${SOURCE2}
# src/3rdpart/md5/md5.hpp
# )
# target_link_libraries(rtsp ${MPP_LIBS} ${OSAL_LIBS} ${OpenCV_LIBS} ${RKNN_RT_LIB} ${RGA_LIB} OpenSSL::SSL OpenSSL::Crypto )
# add_executable(rh264
# example/rtsp_h264_file.cpp
# ${SOURCE1}
# ${SOURCE2}
# src/3rdpart/md5/md5.hpp
# )
# target_link_libraries(rh264 ${MPP_LIBS} ${OSAL_LIBS} ${OpenCV_LIBS} OpenSSL::SSL OpenSSL::Crypto )
add_executable(sample
example/sample.cpp
${SOURCE1}
${SOURCE2}
src/3rdpart/md5/md5.hpp
)
target_link_libraries(sample ${MPP_LIBS} ${OSAL_LIBS} ${OpenCV_LIBS} ${RKNN_RT_LIB} ${RGA_LIB} OpenSSL::SSL OpenSSL::Crypto )
代碼修改
代碼是基于他的rtsp_server來修改的,主要修改的內容是sendFrameThread,大概思路是這樣的,還差一個問題,怎么把你的原始mat圖像轉成264碼流
bool IsKeyFrame(const char* data, uint32_t size)
{if (size > 4) {//0x67:sps ,0x65:IDR, 0x6: SEIif (data[4] == 0x67 || data[4] == 0x65 || data[4] == 0x6 || data[4] == 0x27) {return true;}}return false;
}void SendFrameThread(xop::RtspServer* rtsp_server, xop::MediaSessionId session_id, int& clients)
{ encoder e;// encoder相關 內存拷貝int size = 0;char* buffer ;// 編碼標志位int i = 0;// 生成圖像int width = 1920;int height = 1080;cv::Mat colorBar= cv::Mat::zeros(height, width, CV_8UC3);// 設置彩條的寬度int barWidth = width / 8; // 8個彩條,你可以根據(jù)需要調整// 生成彩條for (int i = 0; i < 8; ++i) {// 計算彩條的起始和結束位置int startX = i * barWidth;int endX = (i + 1) * barWidth;// 設置彩條顏色(BGR格式)cv::Vec3b color;if (i % 2 == 0) {color = cv::Vec3b(255, 0, 155); // 藍色} else {color = cv::Vec3b(0, 255, 0); // 綠色}// 在colorBar上畫出彩條colorBar(cv::Rect(startX, 0, barWidth, height)) = color;}while(1){if(clients > 0) /* 會話有客戶端在線, 發(fā)送音視頻數(shù)據(jù) */{{ xop::AVFrame videoFrame = {0};// printf("width is %d, height is %d",colorBar.rows,colorBar.cols);// 編碼 發(fā)包if(0==i){// 第一幀有sps信息 給他兩幀拼一起char *buffer1;int size1;e.init(buffer1,size1);videoFrame.size = size1;e.postAframe(colorBar,buffer,size);videoFrame.size += size; videoFrame.buffer.reset(new uint8_t[videoFrame.size]);memcpy(videoFrame.buffer.get(), buffer1, size1);memcpy(videoFrame.buffer.get()+size1, buffer, size);i++;} else{e.postAframe(colorBar,buffer,size);videoFrame.size = size; // 視頻幀大小 videoFrame.buffer.reset(new uint8_t[videoFrame.size]);memcpy(videoFrame.buffer.get(), buffer, videoFrame.size);}videoFrame.type = IsKeyFrame(buffer, size) ? xop::VIDEO_FRAME_I : xop::VIDEO_FRAME_P;// videoFrame.type = 0; // 建議確定幀類型。I幀(xop::VIDEO_FRAME_I) P幀(xop::VIDEO_FRAME_P)videoFrame.timestamp = xop::H264Source::GetTimestamp(); // 時間戳, 建議使用編碼器提供的時間戳// writeCharPointerToFile((char *)videoFrame.buffer.get(), videoFrame.size, "filename.txt"); rtsp_server->PushFrame(session_id, xop::channel_0, videoFrame); //送到服務器進行轉發(fā), 接口線程安全/*//獲取一幀 H264, 打包xop::AVFrame videoFrame = {0};videoFrame.type = 0; // 建議確定幀類型。I幀(xop::VIDEO_FRAME_I) P幀(xop::VIDEO_FRAME_P)videoFrame.size = video frame size; // 視頻幀大小 videoFrame.timestamp = xop::H264Source::GetTimestamp(); // 時間戳, 建議使用編碼器提供的時間戳videoFrame.buffer.reset(new uint8_t[videoFrame.size]); memcpy(videoFrame.buffer.get(), video frame data, videoFrame.size); rtsp_server->PushFrame(session_id, xop::channel_0, videoFrame); //送到服務器進行轉發(fā), 接口線程安全*/}{ /*//獲取一幀 AAC, 打包xop::AVFrame audioFrame = {0};audioFrame.type = xop::AUDIO_FRAME;audioFrame.size = audio frame size; /* 音頻幀大小 audioFrame.timestamp = xop::AACSource::GetTimestamp(44100); // 時間戳audioFrame.buffer.reset(new uint8_t[audioFrame.size]); memcpy(audioFrame.buffer.get(), audio frame data, audioFrame.size);rtsp_server->PushFrame(session_id, xop::channel_1, audioFrame); // 送到服務器進行轉發(fā), 接口線程安全*/} }// xop::Timer::Sleep(20); /* 實際使用需要根據(jù)幀率計算延時! 我這里處理延時很大,就不人工延遲了*/}videocapture->release();e.deinit((MPP_RET)0);yolo5.deinit();}
rk3588編碼
這方面網上的文章不太多,但是官方給了demo,都是中文,認真看看,功能都是能用的。
主要參考他的mpp-develop/test/mpi_enc_test 以及Rk3588-linux-v002\linux\docs\Linux\Multimedia\Rockchip_Developer_Guide_MPP_CN.pdf進行配置和使用。這相關的東西我之后再另開一個文章單獨說,總之參考這部分可以做一個BGR888轉成264存儲的demo。
這里重點說一個概念,I、P、B幀
這個東西是264流編碼的一個概念,正常你每一幀的圖像都很大,比如一個1920*1080,每個像素點存一個BGR888的話,就是1920*1080*3 Byte =6220800 Byte ≈ 6M ,然后一秒30幀的話,一秒就要傳180M,局域網或許勉強可以,但是對帶寬壓力也很大。所以這里就涉及到了壓縮,264、265就是壓縮標準,壓縮中需要做兩種壓縮 幀內壓縮和 幀間壓縮。
-
幀內壓縮:使用一定方法使用盡量小的空間存一幀數(shù)據(jù)。
-
幀間壓縮:利用幀和前后幀的關聯(lián)來進一步的壓縮視頻。
這里我們重點關注幀間壓縮。給一個參考文獻
https://zhuanlan.zhihu.com/p/409527359
說的挺透徹的,這里我粗略說一下,看下面這個圖
I幀 ,是幀間壓縮里面的一幀完整圖像,P幀是前向預測幀,B幀是雙向預測幀
IDR幀是特殊的I幀,在編解碼時候P、B幀可以參考I幀前面的幀進行復原,但是不能參考IDR幀前面的幀進行復原
而一個上面這樣的循環(huán),被編碼成一組,我們指定了h264的gop大小,就確定了多少幀中有一個I幀
而我們在做直播,就導致為了低延時,不要B幀,gop 也要盡量的不要太大,如果要解碼P幀,前面就一定要緩存I幀,
監(jiān)測工具
wireshark, 這個感覺還是蠻必要的,至少你能看見客戶端和服務器之間說沒說話。如果你有耐心開RTSP 或者網絡協(xié)議握手說明的話,你甚至能看到他們之間的握手流程。
雜記
中間遇到了一個問題,因為我這個地方沒有公網,所以只能自己給開一個熱點給電腦,PC和RK3588之間通過路由器連接,然后使用前面的某個demo的時候遇到的,主機向服務器(RK3588)發(fā)出視頻請求時,服務器并沒有直接給客戶端發(fā)rtcp包,而是給一個200多的地址發(fā)包,而這個包在客戶端收不到,但是他能收到rtp的協(xié)議包,所以vlc這邊也不提示打不開,就是沒有圖像顯示。
后來使用的方法是,和板子通過路由器網線連接,然后電腦PC wifi 連接路由器,然后路由器沒有公網,再使用一個手機usb給電腦共享網絡,讓我的調試環(huán)境穩(wěn)定可以接受到3588的流,和路由器網線連接的時候接受不到包,感覺是因為路由器沒有網,使用網線連接時候被屏蔽了服務器功能,所以交給路由器的包轉發(fā)請求沒有被PC識別到,但是wifi連接的話,就算他沒有網,也是不能忽略的。 目前是這樣理解的。