我不生产代码,我只是代码的搬运工,相信我,看完这个文章你的图片一定能变成流媒体推出去。
诉求:使用opencv拉流,转成bgr数据,需要把处理后的数据(BGR)编码成264,然后推流推出去,相当于直播(实时编码)
播放器
超低延迟的RTSP播放器
https://github.com/tsingsee/EasyPlayer-RTSP-Win
青犀的一个播放器,直接下他的EasyPlayer-RTSP-Win用来测试就行。划重点,超低延时,我整体方案的延时大概是600-700ms,使用海康相机,rtsp拉流,做了yolo处理,再推出去,有编码,有解码,vlc的延时设置低了就回卡帧,Gop已经改成5了还是卡帧,没有测试Gop改成1的情况,但是vlc的延时和流畅,整体看是不太兼容的。ffmpeg使用nobuffer也会卡帧。直观感受卡的就是Gop的P帧。
服务器
live555 方案
如果你不着急的话。。。 可以试试这个方案,这方面的参考文献给列下面了,因为确实正经研究了几天还看了不少代码,认真想了应该怎么处理,但是确实不太想写,而且对我的需求来讲live555冗余了很多功能,再加上网上确实没有写好的,我又很着急要结果,确定方案能用,所以也没有用这个方案。
官方demo
live555自己的测试文件是有推流demo的,主要是根据实时需求推264文件,以及无脑做一个推264文件的服务器,当时看代码的时候一头雾水加上着急,也没太认真看,主要在live555\testProgs下面,testOnDemandRTSPServer,testH264VideoStreamer ,第二个是无脑推,第一个是你来一个请求,我从头开始给你播放一次视频文件。
这个东西的底层是向一个fTo指针里面拷贝264码流。
如果你Cpp、coding能力强的话,应该是能看懂直接改的,也就不用往后看了。
参考demo 零声 usb相机推流
网上基本上和我需求最接近的live555方案下的代码是国内的做音视频开发教学的一个零声出的视频还有他们传的这个代码。
主要功能是 v4l2相机读取mjpeg,然后ffmpeg的avcodec相关库编码,然后送live555,然后推实时流,像是改的testOnDemandRTSPServer,结构很清晰,除了不能用我也找不到原因外都挺好的。另外他的课是5K的,有点贵。
视频的话去B站带关键词,基本都能搜到,这个代码我加了那个联系QQ要到的。但是在我本地没有推成功,我也不确定是哪里的问题,编译过了,放在这里
https://gitee.com/qingfuliao/v4l2_ipc_live555?_from=gitee_search
在我这是下面这个demo 实现了一个相似的功能,编译实测是可以读取usb相机然后推流成功的。但是代码结构没有上面那个清晰。
参考demo
https://github.com/mpromonet/v4l2rtspserver
这个功能是基于linux的v4l2,使用264的方式读取相机视频流(如果你的usb相机不支持264输出,会驱动失败),然后直接拆帧把流发出去
需要自己下一个libv4l2cpp的代码放进来就能编译了
这个现在看稍微改一改就能用了,不过当时对整体没有概念,改了一阵子不知道怎么下手把我自己的码流变成demo的输入,码流送进去了但是没有推成功,定位了一会儿很难定位问题,也就搁置了。有兴趣的可以基于这个改一改。
https://blog.csdn.net/qq_43418269/article/details/122488866
这个方案我是成功了的,不过延时不太能满足我的需求,这个复现很快。他是用一个管道文件做的,我把编码之后264文件直接写到live555的testOnDemandRTSPServer.cpp这边的读取文件里面,然后逻辑是live555这边接收到请求,创建管道,相机程序初始化后阻塞住,管道被创建后往管道里写,然后另一边就开始播。。
这个就相当于是运行在一个demo里的两个程序。流倒是推出来了,只不过,我这样实现,延时很大,他说的百毫秒量级我做不到,我是2s左右。感觉也可能是我操作不当。
https://blog.csdn.net/lifexx/article/details/52823777
live555读文件改为内存读取实现,确实C++不太行,这个文章对我理解Live555,还有改成内存中的数据方向给了很大启发,但是没有按照他的做,而且他的参考代码无法运行。对我理解另一个推相机的demo有帮助
其他开源服务器框架
这个也是一个很容易就编译成功的服务器,可以使用这个做服务器,然后调用ffmpeg推流,在RK3588上也推成功了,基本没改make相关的配置,需要按照他给的快速开始流程使用git下附加库,功能很强大,但是对我的需求来讲,这个功能我进行二次开发比较慢。不排除我太菜。
https://github.com/ZLMediaKit/ZLMediaKit/tree/master
这里写这个的主要原因是他的一些文章对我的启发和快速上手有很大的参考意义,比如下面这个。
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都把发送文件的部分注释掉了,需要结合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 调试配置文件,可以单步调试,全程在板上编译,没配置交叉编译环境
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/xop src/net src/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: SEI if (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个彩条,你可以根据需要调整 // 生成彩条 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) /* 会话有客户端在线, 发送音视频数据 */ { { xop::AVFrame videoFrame = {0}; // printf("width is %d, height is %d",colorBar.rows,colorBar.cols); // 编码 发包 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); //送到服务器进行转发, 接口线程安全 /* //获取一帧 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); //送到服务器进行转发, 接口线程安全 */ } { /* //获取一帧 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); // 送到服务器进行转发, 接口线程安全 */ } } // xop::Timer::Sleep(20); /* 实际使用需要根据帧率计算延时! 我这里处理延时很大,就不人工延迟了*/ } 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就是压缩标准,压缩中需要做两种压缩 帧内压缩和 帧间压缩。
帧内压缩:使用一定方法使用尽量小的空间存一帧数据。
帧间压缩:利用帧和前后帧的关联来进一步的压缩视频。
这里我们重点关注帧间压缩。给一个参考文献
https://zhuanlan.zhihu.com/p/409527359
说的挺透彻的,这里我粗略说一下,看下面这个图
I帧 ,是帧间压缩里面的一帧完整图像,P帧是前向预测帧,B帧是双向预测帧
IDR帧是特殊的I帧,在编解码时候P、B帧可以参考I帧前面的帧进行复原,但是不能参考IDR帧前面的帧进行复原
而一个上面这样的循环,被编码成一组,我们指定了h264的gop大小,就确定了多少帧中有一个I帧
而我们在做直播,就导致为了低延时,不要B帧,gop 也要尽量的不要太大,如果要解码P帧,前面就一定要缓存I帧,
监测工具
wireshark, 这个感觉还是蛮必要的,至少你能看见客户端和服务器之间说没说话。如果你有耐心开RTSP 或者网络协议握手说明的话,你甚至能看到他们之间的握手流程。
杂记
中间遇到了一个问题,因为我这个地方没有公网,所以只能自己给开一个热点给电脑,PC和RK3588之间通过路由器连接,然后使用前面的某个demo的时候遇到的,主机向服务器(RK3588)发出视频请求时,服务器并没有直接给客户端发rtcp包,而是给一个200多的地址发包,而这个包在客户端收不到,但是他能收到rtp的协议包,所以vlc这边也不提示打不开,就是没有图像显示。
后来使用的方法是,和板子通过路由器网线连接,然后电脑PC wifi 连接路由器,然后路由器没有公网,再使用一个手机usb给电脑共享网络,让我的调试环境稳定可以接受到3588的流,和路由器网线连接的时候接受不到包,感觉是因为路由器没有网,使用网线连接时候被屏蔽了服务器功能,所以交给路由器的包转发请求没有被PC识别到,但是wifi连接的话,就算他没有网,也是不能忽略的。 目前是这样理解的。