1、背景
由于项目有需求在一个现有的产品上增加RTMP推流的功能,目前只推视频流。
2、方案选择
由于是在现有的产品上新增功能,那么为了减少总的成本,故选择只动应用软件的来实现需求。
现有的产品中的第三方库比较有限,连个ffmpeg都没,所以要选择可以直接集成代码进来的第三方库,最后选中了srs_librtmp。虽然它已经停止维护了,但是主要功能没问题,使用简单,且可以直接集成代码。
3、实现代码
step1:去github上面先把源码下下来。
GitHub - ossrs/srs-librtmp at master
step2:把对应的代码文件集成到项目里。
这里只需要src/srs目录下的srs_librtmp.h和srs_librtmp.cpp就行了,如下图
step3:封装成工具类
封装过程中使用了另一个第三方库POCO,这个库只要用来实现线程,不想要的话可以直接改掉。
RTMPPusher.h
// // Created by zhengqiuxu on 2023/8/5. // #ifndef VIS_G3_SOFTWARE_RTMPPUSHER_H #define VIS_G3_SOFTWARE_RTMPPUSHER_H #include <Poco/Runnable.h> #include <Poco/Thread.h> #include <mutex> #include "srs_librtmp.h" class RTMPPusher : public Poco::Runnable{ public: // h264 nalu struct NaluHead { unsigned type : 5; unsigned nal_ref_idc : 2; unsigned forbidden_zero_bit : 1; }; RTMPPusher(); /** * 初始化 * * @param url : 推流地址 * @return 0:成功 其他:失败 */ int init(const std::string url); /** * 启动线程 */ void start(); /** * 设置一帧H264数据帧 * * @param h264Data : 一帧H264数据的指针 * @param dataLen : 一帧H264数据的指针的长度 */ void setH264Data(uint8_t *h264Data, const int dataLen); /** * 停止推流 */ void stop(); void run() override; int getCameraId() const; void setCameraId(int cameraId); const std::string &getRtmpUrl() const; void setRtmpUrl(const std::string &rtmpUrl); bool isInited() const; void setInited(bool inited); private: /** * 推送一帧H264数据帧(真实推送到RTMP) * * @param h264Data : 一帧H264数据的指针 * @param dataLen : 一帧H264数据的指针的长度 */ void pushH264Data(char *h264Data, const int dataLen); /* 对应的相机ID */ int cameraId = -1; /* RTMP的推送地址 */ std::string rtmpUrl; /* 是不是需要停止推送 */ bool isNeedStop = true; /* 是否初始化了 */ bool inited = false; /* 是否可以发送了?需要第一帧是sps才行 */ bool canSen = false; Poco::Thread pushThread; srs_rtmp_t rtmp; uint64_t pts = 0; uint64_t dts = 0; const int MAX_H264CACHE_SIZE = 1024*1024*4; /* 缓冲起来的h264数据 */ char *h264DataCache; /* 缓冲起来的h264数据的长度 */ int curH264DataLen = 0; /* 读写H264数据的互斥锁 */ std::mutex h264DataLock; }; #endif //VIS_G3_SOFTWARE_RTMPPUSHER_HRTMPPusher.cpp
// // Created by zhengqiuxu on 2023/8/5. // #include "RTMPPusher.h" #include <cstring> #include <unistd.h> RTMPPusher::RTMPPusher() { } /** * 初始化 * * @param url : 推流地址 * @return 0:成功 其他:失败 */ int RTMPPusher::init(const std::string url) { int ret = -1; rtmpUrl = url; inited = true; h264DataCache = (char *)malloc(MAX_H264CACHE_SIZE); ret = 0; return ret; } /** * 推送一帧H264数据帧(真实推送到RTMP) * * @param h264Data : 一帧H264数据的指针 * @param dataLen : 一帧H264数据的指针的长度 */ void RTMPPusher::pushH264Data(char *h264Data, const int dataLen) { try { printf("RTMPPusher::pushH264Data size=%d \n",dataLen); /* 推流到RTMP */ pts += 40; /* 如果是B帧的话,PTS应该等于离它最近的P帧或者I帧的的PTS 一般都是选择填上一帧数据的PTS */ dts = pts; int ret = srs_h264_write_raw_frames(rtmp, h264Data, dataLen, dts, pts); if (ret != 0) { if (srs_h264_is_dvbsp_error(ret)) { srs_human_trace("ignore drop video error, code=%d", ret); } else if (srs_h264_is_duplicated_sps_error(ret)) { srs_human_trace("ignore duplicated sps, code=%d", ret); } else if (srs_h264_is_duplicated_pps_error(ret)) { srs_human_trace("ignore duplicated pps, code=%d", ret); } else { srs_human_trace("send h264 raw data failed. ret=%d", ret); } } } catch (...) { printf("push H264Data to %s failed! %s \n",rtmpUrl.c_str(),strerror(errno)); } } /** * 停止推流 */ void RTMPPusher::stop() { isNeedStop = true; srs_human_trace("h264 raw data completed"); srs_rtmp_destroy(rtmp); free(h264DataCache); inited = false; } /** * 启动线程 */ void RTMPPusher::start() { pushThread.start(*this); } void RTMPPusher::run() { std::string pthreadName = "RTMPPusher_"; pthreadName.append(rtmpUrl); pthread_setname_np(pthread_self(), pthreadName.c_str()); isNeedStop = false; /* 创建一个RTMP客户端对象 */ rtmp = srs_rtmp_create(rtmpUrl.c_str()); /* 开始跟RTMP服务器握手 */ if (srs_rtmp_handshake(rtmp) != 0) { srs_human_trace("simple handshake failed."); }else{ srs_human_trace("simple handshake success"); /* 连接RTMP流 */ if (srs_rtmp_connect_app(rtmp) != 0) { srs_human_trace("connect vhost/app failed."); }else{ srs_human_trace("connect vhost/app success"); /* 看看RTMP流是否可以推流 */ if (srs_rtmp_publish_stream(rtmp) != 0) { srs_human_trace("publish stream failed."); }else{ srs_human_trace("publish stream success"); canSen = false; /* 循环从内存里读出H264并推到RTMP服务器 */ while (!isNeedStop){ h264DataLock.lock(); if(curH264DataLen > 0){ if(canSen){ pushH264Data(h264DataCache,curH264DataLen); curH264DataLen = 0; }else{ /* 拿出NALU头用来后面判断NALU类型 */ struct NaluHead curNaluHead = *(struct NaluHead *)(h264DataCache+4); /* 从SPSPPS开始推,有些服务器做的不好,不是从SPSPPS开始推的话会报错 */ if(curNaluHead.type == 7){ canSen = true; pushH264Data(h264DataCache,curH264DataLen); curH264DataLen = 0; } } } h264DataLock.unlock(); usleep(10000); } } } } } /** * 设置一帧H264数据帧 * * @param h264Data : 一帧H264数据的指针 * @param dataLen : 一帧H264数据的指针的长度 */ void RTMPPusher::setH264Data(uint8_t *h264Data, const int dataLen) { if(dataLen > 0){ h264DataLock.lock(); memcpy(h264DataCache,h264Data,dataLen); curH264DataLen = dataLen; h264DataLock.unlock(); } } int RTMPPusher::getCameraId() const { return cameraId; } void RTMPPusher::setCameraId(int cameraId) { RTMPPusher::cameraId = cameraId; } const std::string &RTMPPusher::getRtmpUrl() const { return rtmpUrl; } void RTMPPusher::setRtmpUrl(const std::string &rtmpUrl) { RTMPPusher::rtmpUrl = rtmpUrl; } bool RTMPPusher::isInited() const { return inited; } void RTMPPusher::setInited(bool inited) { RTMPPusher::inited = inited; }到这里我们就已经实现完成且封装成一个方便调用的工具类了。调用的时候只要需要先调用init()函数初始化,再调用start()函数,让发送线程跑起来,一有H264数据就通过setH264Data()函数设置给工具类的就行了。这样工具类就会循环读取设置过来的H264数据并推送到RTMP服务器了。
4、其他
1、由于这个库的pts和dts是需要自己赋值的,所以有时候推送上去的数据要么是播放速度变快,要么是卡顿,都很有可能是pts和dts的问题。由于我这里是固定25帧的,所有我直接pts固定每帧都比上一帧+40ms。pts和dts还有很多研究空间,实际使用的时候具体情况具体分析。