背景
librtmp是rtmpdum项目提供的一个rtmp协议推流库。本篇博客,就是简单的使用librtmp库在window环境推送rtmp数据(H64+AAC)到srs服务器。
还会借此简单介绍一下rtmp协议的封装。
关于librmtp编译,请参考我的博客 window 使用 vs 编译 librtmp 静态库
API 介绍
- RTMP_Alloc()
用于创建一个RTMP会话的句柄
- RTMP_Init()
初始化句柄。
- Link.timeout
rmtp 连接超时时间,单位秒
- RTMP_LogSetLevel()
设置日志级别,头文件在 'log.h'
ini
RTMP_LogLevel lvl = RTMP_LogLevel::RTMP_LOGDEBUG;
RTMP_LogSetLevel(lvl);
- RTMP_SetupURL(RTMP *r, char *url);
设置rtmp地址
- RTMP_EnableWrite()
设置支持写入,也就是支持publish推流。否则只能play拉流
- RTMP_Connect(RTMP *r, RTMPPacket *cp)
建立RTMP连接,创建一个RTMP协议规范中的NetConnection。第二个参数,如果不是null的话,会在连接成功额后,发送这个rtmp包
- RTMP_ConnectStream(RTMP *r, int seekTime)
创建一个RTMP协议规范中的NetStream。第二个参数,可以指定到流的时间,如果是推流,这里设置0.
- RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
rtmp 发送数据包
初始化一个librtmp对象
c++
RtmpPushLibrtmp::RtmpPushLibrtmp()
{
//创建一个RTMP会话的句柄
m_pRtmp = RTMP_Alloc();
//初始化句柄
RTMP_Init(m_pRtmp);
m_pRtmp->Link.timeout = 30;
RTMP_LogLevel lvl = RTMP_LogLevel::RTMP_LOGALL;
RTMP_LogSetLevel(lvl);
}
创建rtmp连接,并发布流
RTMP_ConnectStream(...) 如果是推流,这里局势publish方法。publish方法会指定流id。
C++
int RtmpPushLibrtmp::Init(std::string url,int width,
int height,
int fps,
int sample_rate,
int channel)
{
width_ = width;
height_ = height;
fps_ = fps;
sample_rate_ = sample_rate;
channel_ = channel;
if (!RTMP_SetupURL(m_pRtmp, (char*)url.c_str()))
{
std::cout << "rtmp setupurl faild: " << std::endl;
return -1;
}
RTMP_EnableWrite(m_pRtmp);
/////// window 环境需要的
WSADATA wsaData;
int nRet;
if ((nRet = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) {
return nRet;
}
////////////////////
if (!RTMP_Connect(m_pRtmp, NULL))
{
std::cout << "rtmp connect faild: " << url << std::endl;
return -1;
}
if (!RTMP_ConnectStream(m_pRtmp, 0))
{
std::cout << "rtmp connect stream faild: " << url << std::endl;
return -2;
}
RTMPMetadata metadta;
metadta.nWidth = width_;
metadta.nHeight = height_;
metadta.nFrameRate = fps_;
metadta.nAudioSampleRate = sample_rate_;
metadta.nAudioChannels = channel_;
metadta.bHasAudio = true;
SendMetadata(metadta); //发送metadata信息
SendAacSpec(); //发送音频spec信息
}
发送 metadata
然后就是要发送metadata,这里面是一些键值对。包含了一些音视频信息。这里面可以做一些自定义的信息。
上面的一段代码里面sendMeatadata有调用发送meta的。具体的方法如下
c++
bool RtmpPushLibrtmp::SendMetadata(RTMPMetadata& lpMetaData)
{
char body[1024] = { 0 };
char* p = (char*)body;
p = put_byte(p, AMF_STRING);
p = put_amf_string(p, "@setDataFrame");
p = put_byte(p, AMF_STRING);
p = put_amf_string(p, "onMetaData");
p = put_byte(p, AMF_OBJECT);
p = put_amf_string(p, "copyright");
p = put_byte(p, AMF_STRING);
p = put_amf_string(p, "gupan");
p = put_amf_string(p, "width");
p = put_amf_double(p, lpMetaData.nWidth);
p = put_amf_string(p, "height");
p = put_amf_double(p, lpMetaData.nHeight);
p = put_amf_string(p, "framerate");
p = put_amf_double(p, lpMetaData.nFrameRate);
p = put_amf_string(p, "videocodecid");
p = put_amf_double(p, FLV_CODECID_H264);
p = put_amf_string(p, "audiocodecid");
p = put_amf_double(p, 10);
p = put_amf_string(p, "audiodatarate");
if (lpMetaData.nAudioSampleRate == 44100) {
p = put_amf_double(p, 64);
}
else if (lpMetaData.nAudioSampleRate == 48000)
{
p = put_amf_double(p, 512);
}
p = put_amf_string(p, "audiochannels");
p = put_amf_double(p, 2);
p = put_amf_string(p, "audiosamplesize");
p = put_amf_double(p, 16);
p = put_amf_string(p, "audiosamplerate");
if (lpMetaData.nAudioSampleRate == 44100) {
p = put_amf_double(p, 44100);
}
else if (lpMetaData.nAudioSampleRate == 48000) {
p = put_amf_double(p, 48000);
}
p = put_amf_string(p, "");
p = put_byte(p, AMF_OBJECT_END);
int size = p - body;
RTMPPacket packet;
RTMPPacket_Reset(&packet);
RTMPPacket_Alloc(&packet, size);
packet.m_packetType = RTMP_PACKET_TYPE_INFO;
packet.m_nChannel = 0x04;
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_nTimeStamp = 0;
packet.m_nInfoField2 = m_pRtmp->m_stream_id;
packet.m_nBodySize = size;
memcpy(packet.m_body, body, size);
int nRet = RTMP_SendPacket(m_pRtmp, &packet, 0);
RTMPPacket_Free(&packet);
return true;
}
这是我使用librmtp发送的一个meta的抓包
发送音频aac spec信息
int SendAacSpec(const uint8_t* extradata,int extextradata_size);
音频的spec包就是一些配置信息。这些信息在第一个音频包里面发送。这个数据的来源是ffmpeg编译出来的
在 AAC 流中,Spec 信息包通常会包括:
- 音频编码格式(如 AAC)
- 采样率
- 通道数
- 码率等参数
C++
int RtmpPushLibrtmp::SendAacSpec(const uint8_t* extradata,
int extextradata_size)
{
RTMPPacket packet;
RTMPPacket_Reset(&packet);
RTMPPacket_Alloc(&packet, 2+ extextradata_size);
packet.m_body[0] = 0xAF;
packet.m_body[1] = 0x00;
memcpy(&(packet.m_body[2]), extradata, extextradata_size);
packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM; // 设置数据包头的大小类型。
packet.m_packetType = RTMP_PACKET_TYPE_AUDIO; // 指定数据包类型为音频。
packet.m_hasAbsTimestamp = 0; // 设置是否使用绝对时间戳,0 表示使用相对时间戳。
packet.m_nChannel = 0x04; // 设置 RTMP 通道号。
packet.m_nTimeStamp = 0; // 设置时间戳,通常第一个包时间戳为 0。
packet.m_nInfoField2 = m_pRtmp->m_stream_id // 设置流 ID,以便 RTMP 服务器识别这是哪个流。
packet.m_nBodySize = 2+ extextradata_size; //设置数据包的有效载荷大小,这里是 2 字节 加上实体数据长度。
//调用发送接口
int nRet = RTMP_SendPacket(m_pRtmp, &packet, 0 // 调用 RTMP 的发送接口,将数据包发送到服务器。
RTMPPacket_Free(&packet);//释放内存
return 1;
}
主要代码行的基本功能如上注释
上面有一个m_nChannel和m_nInfoField2 这两个有什么区别呢 ?
-
通道号 (
m_nChannel
) :- 主要用于区分数据包的类型和来源(例如音频、视频等)。
- 在 RTMP 协议中,一个连接可以有多个通道,每个通道可以同时传输不同类型的数据。
-
流 ID (
m_nInfoField2
):- 表示特定的流标识符,用于识别特定的流(如某个直播流或点播流)。
- 它是与流的生命周期相关的,通常在连接建立时分配,确保服务器知道当前包属于哪个流。
总结来说,通道号用于标识数据包类型,而流 ID 用于标识特定的流。两者共同帮助 RTMP 服务器和客户端管理和处理数据流。
m_headerType 的设置用于指示 RTMP 数据包的头部大小类型。RTMP 协议定义了几种头部类型,每种类型对应不同的头部大小。具体的判定和设置如下:
- RTMP_PACKET_SIZE_LARGE:头部大小为 12 字节,适用于大数据包。
- RTMP_PACKET_SIZE_MEDIUM:头部大小为 8 字节,适用于中等大小的数据包。
- RTMP_PACKET_SIZE_SMALL:头部大小为 1 字节,适用于小数据包。
m_body[0] 字段:这个字段通常用于指示音频的编码类型。例如,值为
0xAF
表示这是一个 AAC 编码的音频数据包。
m_body[1] 表示是音频sequence header信息包
上面是一个抓包信息,下面结合代码,给一些解释:
-
RTMP Header : 占用8个字节
arduino包含了时间搓(timestamp: 0),通道号(Chunk Steam ID:4),包类型(Type ID: Audio Data (0x08)) > 关于Format 在RTMP协议中,消息格式的标识如下:
- Format 0:无消息头,直接使用完整的消息。
- Format 1:包含时间戳和消息长度,适用于较短的消息。
- Format 2:只包含时间戳,适用于已知长度的消息。
- Format 3:没有时间戳或长度信息,适用于高频率的消息。
m_nInfoField2字段在librtmp中主要用于维护流ID的信息。在RTMP协议中,这个字段在消息的上下文中会被使用,但在实际的协议数据包中并不直接体现。它主要是一个内部管理字段,用于帮助librtmp库在处理不同的流时进行区分和管理
-
RTMP Body :占用输
body就很简单了,就是我们代码里面写那样
发送sps,pps
int SendSpsPps(unsigned int timeoffset);
C++
int RtmpPushLibrtmp::SendSpsPps(const uint8_t* extradata,
int extextradata_size)
{
int nal_size = isNalTail(extradata, extextradata_size); //计算nalu头长度,4个字节或者3个字节
uint8_t* m_pSps = nullptr;
int m_pSpslen = 0;
uint8_t* m_pPps = nullptr;
int m_pPpslen = 0;
//根据nal头找到pps的位置
for (int i = 0;i < extextradata_size - 4;i++)
{
if (i!=0&&extradata[i] == 0 && extradata[i + 1] == 0 && extradata[i + 2] == 0 && extradata[i + 3] == 1)
{
m_pSps = const_cast<uint8_t*>(&extradata[nal_size]);
m_pSpslen = i + 1 - nal_size;
m_pPps = const_cast<uint8_t*>(&extradata[i+nal_size]);
m_pPpslen = extextradata_size - m_pSpslen - nal_size;
break;
}
}
if (m_pPps == nullptr || m_pPps == nullptr)
{
std::cout << "parse sps,pps error " << std::endl;
return -1;
}
RTMPPacket packet;
RTMPPacket_Reset(&packet);
RTMPPacket_Alloc(&packet, 1024);
unsigned char* body = (unsigned char*)packet.m_body;
int i = 0;
body[i++] = 0x17;
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
/*AVCDecoderConfigurationRecord,根据sps,设置了一些配置信息*/
body[i++] = 0x01;
body[i++] = m_pSps[1];
body[i++] = m_pSps[2];
body[i++] = m_pSps[3];
body[i++] = 0xff;
/*m_pSps 添加数据*/
body[i++] = 0xe1;
body[i++] = (m_pSpslen >> 8) & 0xff;
body[i++] = m_pSpslen & 0xff;
memcpy(&body[i], m_pSps, m_pSpslen);
i += m_pSpslen;
/*m_pPps 数据添加*/
body[i++] = 0x01;
body[i++] = (m_pPpslen >> 8) & 0xff;
body[i++] = (m_pPpslen) & 0xff;
memcpy(&body[i], m_pPps, m_pPpslen);
i += m_pPpslen;
packet.m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet.m_nBodySize = i;
packet.m_nChannel = 0x04;
packet.m_nTimeStamp = 0;
packet.m_hasAbsTimestamp = 0;
packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
packet.m_nInfoField2 = m_pRtmp->m_stream_id;
int nRet = RTMP_SendPacket(m_pRtmp, &packet, 0); //调用api,发送数据包
RTMPPacket_Free(&packet);
return 1;
}
输入参数 extradata 是带nal头的(也就是0,0,0.1),这里,我做了一些的操作,把nalu头去掉,然后按照一定格式组合。这里面包含了sps,pps。我会把指针分别取出来,然后按照格式存储。
如果你的sps,pps没有这些nalu头,那就不需要去掉nalu头这个步骤了
发送的时间搓是0
下面是这个sps,pps抓包截图:
可以对照着抓包理解一下协议
发送音频包
int RtmpPushLibrtmp::SendAudio(unsigned char* buf, int len)
c++
int RtmpPushLibrtmp::SendAudio(unsigned char* buf, int len)
{
std::lock_guard<std::mutex> lock(librmtp_mutex);
RTMPPacket packet;
RTMPPacket_Reset(&packet);
RTMPPacket_Alloc(&packet, len + 2);
packet.m_nBodySize = len + 2;
unsigned char* body = (unsigned char*)packet.m_body;
memset(body, 0, len + 2);
/*AF 01 + AAC RAW data*/
body[0] = 0xAF;
body[1] = 0x01;
memcpy(&body[2], buf, len);
packet.m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet.m_nChannel = 0x04;
auto now = std::chrono::steady_clock::now();
int64_t now_time = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
unsigned int timeoffset = now_time - start_time;
packet.m_nTimeStamp = timeoffset;
packet.m_hasAbsTimestamp = 0;
packet.m_headerType = RTMP_PACKET_SIZE_MEDIUM;
packet.m_nInfoField2 = m_pRtmp->m_stream_id;
/*调用发送接口*/
int nRet = RTMP_SendPacket(m_pRtmp, &packet, 0);
if (nRet != 1)
{
std::cout << "send audio error " << nRet << std::endl;
}
RTMPPacket_Free(&packet);
return 1;
}
std::lock_guard<std::mutex> lock(librmtp_mutex);
加锁是为了保证时间搓的递增,在librmtp里面是不允许时间搓非递增的。
body[0] = 0xAF;
:设置第一个字节为 0xAF。这个字节代表音频数据的类型和编码方式:
0xA
表示 AAC 音频。0xF
表示音频采样率为 44.1kHz,且是立体声
body[1] = 0x01;
:表示音频数据的格式,这里 0x01
代表 AAC 的 RAW 数据。
packet.m_nTimeStamp = timeoffset;
这个时间搓,是从推流开始计算经过的时间,但是是ms
其他的就没有特别的,就是发送加入raw的编码aac数据
很多网上的例子是假adts的,注意我这里没有没有加入adts的。我是在第一个音频的数据前发送了spec的音频信息。用于解码
由于音频数据包,在wiresharek里面抓包看起来有一些问题。所以我这里就不展示音频的抓包了。免得误导
发送视频包
int SendVideo(unsigned char* buf, int len)
c++
int RtmpPushLibrtmp::SendVideo(unsigned char* buf, int len)
{
std::lock_guard<std::mutex> lock(librmtp_mutex);
int nal_size = isNalTail(extradata, extextradata_size); //计算nalu头长度,4个字节或者3个字节
// 去掉nalu头的界定符号
buf += nal_size;
len -= nal_size;
int type = buf[0] & 0x1f;
RTMPPacket packet;
RTMPPacket_Reset(&packet);
RTMPPacket_Alloc(&packet, len + 9);
packet.m_nBodySize = len + 9;
/*send video packet*/
unsigned char* body = (unsigned char*)packet.m_body;
memset(body, 0, len + 9);
/*key frame*/
body[0] = 0x27;
if (type == 5) {
body[0] = 0x17;
}
body[1] = 0x01; /*nal unit*/
body[2] = 0x00;
body[3] = 0x00;
body[4] = 0x00;
// raw 数据的长度
body[5] = (len >> 24) & 0xff;
body[6] = (len >> 16) & 0xff;
body[7] = (len >> 8) & 0xff;
body[8] = (len) & 0xff;
/*copy data*/
memcpy(&body[9], buf, len);
packet.m_hasAbsTimestamp = 0;
packet.m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet.m_nInfoField2 = m_pRtmp->m_stream_id;
packet.m_nChannel = 0x04;
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
auto now = std::chrono::steady_clock::now();
int64_t now_time = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
unsigned int timeoffset = now_time - start_time;
packet.m_nTimeStamp = timeoffset;
/*调用发送接口*/
int nRet = RTMP_SendPacket(m_pRtmp, &packet, 0);
if (nRet != 1)
{
std::cout << "send video error " << nRet << std::endl;
}
RTMPPacket_Free(&packet);
return 1;
}
}
ini
int nal_size = isNalTail(extradata, extextradata_size); //计算nalu头长度,4个字节或者3个字节
buf += nal_size;
len -= nal_size;
isNalTail
计算 NAL 单元头的长度,通常为 3 或 4 个字节。NAL 单元头用于标识视频帧的边界,但它不属于实际的编码数据。- 通过
buf += nal_size
和len -= nal_size
,跳过 NAL 单元头,只保留实际的视频数据。
ini
int type = buf[0] & 0x1f;
-
这里从
buf
中的第一个字节获取视频帧的类型,buf[0] & 0x1f
提取低 5 位,这些位通常包含 NAL 单元的类型。- 类型
5
表示关键帧(I-frame)。 - 类型
1
表示非关键帧(P-frame)。
- 类型
ini
body[0] = 0x27;
if (type == 5) {
body[0] = 0x17;
}
body[1] = 0x01; /*nal unit*/
body[2] = 0x00;
body[3] = 0x00;
body[4] = 0x00;
-
body[0]
:设置第一个字节为0x27
,表示这个视频帧是非关键帧。如果视频帧类型是 5(关键帧),则设置为0x17
。0x1
表示关键帧,0x7
表示非关键帧。
-
body[1] = 0x01;
:表示这个包中包含的是 NAL 单元数据。 -
body[2] ~ body[4]
:这三个字节为 0,通常用于表示时间戳的扩展部分。
css
body[5] = (len >> 24) & 0xff;
body[6] = (len >> 16) & 0xff;
body[7] = (len >> 8) & 0xff;
body[8] = (len) & 0xff;
- 这几行代码将
len
(视频数据的长度)写入body[5]
到body[8]
,使用大端序的方式,按字节顺序存储数据长度。每个字节都包含视频数据长度的一部分。
关闭操作
void Close();
c++
void RtmpPushLibrtmp::Close()
{
if (m_pRtmp)
{
RTMP_Close(m_pRtmp);
RTMP_Free(m_pRtmp);
m_pRtmp = NULL;
}
}
结合MediaPush工程的调用
我这一个代码的调用是放在我的主工程MediaPush里面,它和FFmpeg的api实现了相同的推流功能。下面展示一下,我使用的地方。
这里用一个宏(RTMP_PUSH_USER_FFMPEG)来区分是ffmpeg实现的推流,还是librmtp
这是调用初始化,并且发送音频spec,视频sps,pps的地方
这是发送音频数据的地方
这是发送视频的地方
代码仓库地址
错误信息
RTMP_Connect() 连接返回错误
vbnet
//错误信息日志
DEBUG: Parsing...
DEBUG: Parsed protocol: 0
DEBUG: Parsed host : 192.168.109.128
DEBUG: Parsed app : live
ERROR: RTMP_Connect0, failed to create socket. Error: 10093
rtmp connect faild: rtmp://192.168.109.128:1935/live/test
原因是使用winsocket需要初始化网络,librtmp中没有这部分代码。需要加上一下部分
ini
WSADATA wsaData;
int nRet;
if ((nRet = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) {
return nRet;
}