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_H
RTMPPusher.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还有很多研究空间,实际使用的时候具体情况具体分析。