librtmp 原生API做直播推流

背景

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 信息包通常会包括:

  1. 音频编码格式(如 AAC)
  2. 采样率
  3. 通道数
  4. 码率等参数
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 这两个有什么区别呢 ?

  1. 通道号 (m_nChannel) :

    • 主要用于区分数据包的类型和来源(例如音频、视频等)。
    • 在 RTMP 协议中,一个连接可以有多个通道,每个通道可以同时传输不同类型的数据。
  2. 流 ID (m_nInfoField2):

    • 表示特定的流标识符,用于识别特定的流(如某个直播流或点播流)。
    • 它是与流的生命周期相关的,通常在连接建立时分配,确保服务器知道当前包属于哪个流。

总结来说,通道号用于标识数据包类型,而流 ID 用于标识特定的流。两者共同帮助 RTMP 服务器和客户端管理和处理数据流。

m_headerType 的设置用于指示 RTMP 数据包的头部大小类型。RTMP 协议定义了几种头部类型,每种类型对应不同的头部大小。具体的判定和设置如下:

  1. RTMP_PACKET_SIZE_LARGE:头部大小为 12 字节,适用于大数据包。
  2. RTMP_PACKET_SIZE_MEDIUM:头部大小为 8 字节,适用于中等大小的数据包。
  3. 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_sizelen -= 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的地方

这是发送音频数据的地方

这是发送视频的地方

代码仓库地址

github.com/SnailCoderG...

错误信息

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;
}
相关推荐
Tiffany_Ho几秒前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常1 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀3 小时前
CSS——属性值计算
前端·css
DOKE3 小时前
VSCode终端:提升命令行使用体验
前端
xgq3 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081353 小时前
前端之路-了解原型和原型链
前端