【流媒体】基于libRTMP的H264推流器

RTMP协议相关:
【流媒体】RTMP协议概述
【流媒体】RTMP协议的数据格式
【流媒体】RTMP协议的消息类型
【流媒体】RTMPDump—主流程简单分析
【流媒体】RTMPDump—RTMP_Connect函数(握手、网络连接)
【流媒体】RTMPDump—RTMP_ConnectStream(创建流连接)
【流媒体】RTMPDump—Download(接收流媒体信息)
【流媒体】RTMPDump—AMF编码
【流媒体】基于libRTMP的H264推流器

参考:

  1. libRTMP推流开源工程(附带github工程链接)
    最简单的基于librtmp的示例:发布H.264(H.264通过RTMP发布)
  2. RTMP服务器的搭建
    Windows搭建RTMP服务器+OBS推流+VLC拉流
  3. RTMP封装H264头部的格式
    rtmp传输h.264视频的必备知识(一)

前面记录了RTMP协议相关的内容,本文记录如何使用libRTMP库进行推流,推流的内容是本地H264码流,向RTMP服务器(nginx)进行推流,并且在传输之后使用ffplay进行拉流播放,主要参考了雷博的实现方式,但进行了代码简化,加上了自己的理解和注释。在修改过程中,直接使用了雷博提供的libRTMP库,没有自己编译,后续再了解

1. 整体流程

工程代码结构:
在这里插入图片描述
代码实现的流程步骤为:
(1)创建RTMP上下文结构体(RTMP_Alloc)
(2)进行连接(librtmp_h264_connect)
 (a)初始化网络sockets(InitSockets)
 (b)初始化RTMP结构体(RTMP_Init)
 (c)设置URL(RTMP_SetupURL)
 (d)设置链接可写(RTMP_EnableWrite)
 (e)RTMP连接(RTMP_Connect)
 (f)RTMP流连接(RTMP_ConnectStream)
(3)发送数据报(librtmp_h264_send)
 (a)寻找H264码流中的单个NAL(find_nal_unit)
 (b)内部发送函数(h264_send_internal)
  (i)获取NAL到SPS或PPS(copy_data)
  (ii)发送SPS和PPS信息(h264_send_metadata)
  (iii)发送frame信息(h264_send_frame)
(4)关闭连接(librtmp_h264_close)
(5)释放RTMP结构体(RTMP_Close)

PS:在传输H264码流时,有一个小问题,发送H264码流时,雷博的代码中会将SEI信息跳过,暂时还不理解

2. 代码

2.1 头文件

#pragma once
#include <stdint.h>

typedef enum {
	NALU_TYPE_SLICE = 1,
	NALU_TYPE_DPA = 2,
	NALU_TYPE_DPB = 3,
	NALU_TYPE_DPC = 4,
	NALU_TYPE_IDR = 5,
	NALU_TYPE_SEI = 6,
	NALU_TYPE_SPS = 7,
	NALU_TYPE_PPS = 8,
	NALU_TYPE_AUD = 9,
	NALU_TYPE_EOSEQ = 10,
	NALU_TYPE_EOSTREAM = 11,
	NALU_TYPE_FILL = 12,
} NALU_TYPE;

typedef struct RTMPMetadata {
	int sps_size;
	uint8_t* sps_data;

	int pps_size;
	uint8_t* pps_data;
}RTMPMetadata_t;

2.2 c文件

c文件基本上是参考雷博的思想实现的,里面涉及到了如何封装SPS、PPS和frame的信息,可以看参考目录的第3条,有详细的解释。代码中关键位置有注解

#pragma warning(disable:4996)

#include <stdio.h>
#include <stdlib.h>
#include "librtmp_sendH264.h"
#include "librtmp\rtmp.h"   
#include "librtmp\rtmp_sys.h"   
#include "librtmp\amf.h"  
//#include "sps_decode.h" // 这里没有去解析fps

#ifdef WIN32     
#include <windows.h>  
#pragma comment(lib,"WS2_32.lib")   
#pragma comment(lib,"winmm.lib")  
#endif 

#define BASE_SIZE		1024
#define MAX_BUF_SIZE	32 * 1024 * 1024
#define RTMP_HEAD_SIZE   (sizeof(RTMPPacket) + RTMP_MAX_HEADER_SIZE)

void debug_byte(uint8_t* data, int size)
{
	for (int m = 0; m < size; m++)
	{
		printf("data[%d]:%x ", m, data[m]);
		if (m % 10 == 0 && m != 0)
		{
			printf("\n");
		}
	}
}

void debug_rtmp_packet(RTMPPacket* rtmp_packet)
{
	printf("m_headerType:%d\n", rtmp_packet->m_headerType);
	printf("m_packetType:%d\n", rtmp_packet->m_packetType);
	printf("m_hasAbsTimestamp:%d\n", rtmp_packet->m_hasAbsTimestamp);
	printf("m_nChannel:%d\n", rtmp_packet->m_nChannel);
	printf("m_nTimeStamp:%d\n", rtmp_packet->m_nTimeStamp);
	printf("m_nInfoField2:%d\n", rtmp_packet->m_nInfoField2);
	printf("m_nBodySize:%d\n", rtmp_packet->m_nBodySize);
	printf("m_nBytesRead:%d\n", rtmp_packet->m_nBytesRead);
	printf("m_nBody\n");
	debug_byte((uint8_t*)rtmp_packet->m_body, rtmp_packet->m_nBodySize);
}
// 查找起始码,能够找出一个NALU的起始位置和结束位置
int find_nal_unit(uint8_t* buf, int size, int* nal_start, int* nal_end)
{
	int i;
	// find start
	*nal_start = 0;
	*nal_end = 0;

	i = 0;
	while (   //( next_bits( 24 ) != 0x000001 && next_bits( 32 ) != 0x00000001 )
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) &&
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0 || buf[i + 3] != 0x01)
		)
	{
		i++; // skip leading zero
		if (i + 4 >= size) { return 0; } // did not find nal start
	}

	if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) // ( next_bits( 24 ) != 0x000001 )
	{
		i++;
	}

	if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) { /* error, should never happen */ return 0; }
	i += 3;
	*nal_start = i;

	while (   //( next_bits( 24 ) != 0x000000 && next_bits( 24 ) != 0x000001 )
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0) &&
		(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01)
		)
	{
		i++;
		// FIXME the next line fails when reading a nal that ends exactly at the end of the data
		if (i + 3 >= size) { *nal_end = size; return -1; } // did not find nal end, stream ended first
	}

	*nal_end = i;
	return (*nal_end - *nal_start);
}
// 初始化sockets
int InitSockets()
{
	WORD version;
	WSADATA wsaData;
	version = MAKEWORD(2, 2); // RTMPDump中使用1.1,目前应尽量使用2.2
	return (WSAStartup(version, &wsaData) == 0);
}

// 网络连接
int librtmp_h264_connect(RTMP* rtmp, const char* url)
{
	int ret = 0;
	// 初始化socket
	ret = InitSockets();
	if (ret < 0)
	{
		printf("Init sockets failed\n");
		return -1;
	}
	RTMP_Init(rtmp);

	ret = RTMP_SetupURL(rtmp, (char*)url);
	if (ret < 0)
	{
		printf("Setup URL failed\n");
		return -1;
	}

	// 设置可写
	RTMP_EnableWrite(rtmp);

	// RTMP连接
	ret = RTMP_Connect(rtmp, NULL);
	if (ret < 0)
	{
		printf("RTMP Connect failed\n");
		return -1;
	}
	// RTMP网络连接
	ret = RTMP_ConnectStream(rtmp, NULL);
	if (ret < 0)
	{
		printf("RTMP Connect Stream failed\n");
		return -1;
	}

	return 0;
}

int copy_data(uint8_t** dst, uint8_t* src, int size)
{
	*dst = (uint8_t*)malloc(size * sizeof(uint8_t));
	memcpy(*dst, src, size);
	return size;
}

// 发送metadata(SPS和PPS)
int h264_send_metadata(RTMP* rtmp, RTMPMetadata_t* rtmp_meta)
{
	RTMPPacket* packet = NULL;
	unsigned char* body = NULL;
	int i;
	uint8_t* sps = rtmp_meta->sps_data;
	uint8_t* pps = rtmp_meta->pps_data;
	int sps_len = rtmp_meta->sps_size;
	int pps_len = rtmp_meta->pps_size;

	packet = (RTMPPacket*)malloc(RTMP_HEAD_SIZE + 1024);
	if (!packet)
	{
		printf("malloc packet failed\n");
		return -1;
	}
	memset(packet, 0, RTMP_HEAD_SIZE + 1024);
	packet->m_body = (char*)packet + RTMP_HEAD_SIZE;
	body = (unsigned char*)packet->m_body;
	i = 0;
	/*
		下面对SPS和PPS信息进行了封装,封装的格式参考
		https://blog.csdn.net/dqxiaoxiao/article/details/94820599
	*/
	body[i++] = 0x17;	// 1: keyframe, 7: AVC
	body[i++] = 0x00;	// AVPacketType

	body[i++] = 0x00;	// Composition Time (3 Bytes)
	body[i++] = 0x00;
	body[i++] = 0x00;

	/*AVCDecoderConfigurationRecord*/ // 
	body[i++] = 0x01;	// 版本号固定为1
	body[i++] = sps[1];	// AVCProfileIndication
	body[i++] = sps[2];	// profile_compability
	body[i++] = sps[3];	// AVCLevelIndication
	body[i++] = 0xff;	// reserved

	/*sps*/
	body[i++] = 0xe1;					// 0xe1 : 1110 0001,高3位为保留位,默认全为1,低5位表示SPS的数量,这里设置为1
	body[i++] = (sps_len >> 8) & 0xff;	// 下面两个字节都是,SPS的长度
	body[i++] = sps_len & 0xff;			// SPS的长度
	memcpy(&body[i], sps, sps_len);		// 将长度为sps_len的SPS信息赋值到body中
	i += sps_len;

	/*pps*/
	body[i++] = 0x01;					// PPS的个数
	body[i++] = (pps_len >> 8) & 0xff;	// 下面两个字节都是PPS的长度
	body[i++] = (pps_len) & 0xff;
	memcpy(&body[i], pps, pps_len);		// 将长度为pps_len的PPS信息赋值到body中
	i += pps_len;

	// 处理头信息
	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 = rtmp->m_stream_id;

	//printf("own test\n");
	//debug_byte(body, i);
	//debug_rtmp_packet(packet);

	/*调用发送接口*/
	int ret = RTMP_SendPacket(rtmp, packet, TRUE);
	free(packet);    //释放内存
	return ret;
}

int rtmp_send_packet(RTMP* rtmp, uint8_t* data, int size, int timestamp)
{
	RTMPPacket* packet;
	packet = (RTMPPacket*)malloc(RTMP_HEAD_SIZE + size);
	if (!packet)
	{
		printf("malloc packet failed\n");
		return -1;
	}
	memset(packet, 0, RTMP_HEAD_SIZE);
	packet->m_body = (char*)packet + RTMP_HEAD_SIZE;
	packet->m_nBodySize = size;
	memcpy(packet->m_body, data, size);
	packet->m_hasAbsTimestamp = 0;
	packet->m_packetType = RTMP_PACKET_TYPE_VIDEO; // Video
	packet->m_nInfoField2 = rtmp->m_stream_id;
	packet->m_nChannel = 0x04;
	packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
	packet->m_nTimeStamp = timestamp;
	int ret = 0;
	if (RTMP_IsConnected(rtmp))
	{
		ret = RTMP_SendPacket(rtmp, packet, TRUE); /*TRUE为放进发送队列,FALSE是不放进发送队列,直接发送*/
	}
	/*释放内存*/
	free(packet);
	return ret;
}

int h264_send_frame(RTMP* rtmp, RTMPMetadata_t* rtmp_meta, int is_keyframe, uint8_t* data, int size, int timestamp)
{
	unsigned char* body = (unsigned char*)malloc(size + 9);
	if (!body)
	{
		printf("malloc body failed\n");
		return -1;
	}
	memset(body, 0, size + 9);
	int i = 0;
	/*
		下面对frame进行了封装,封装的格式参考
		https://blog.csdn.net/dqxiaoxiao/article/details/94820599
	*/
	if (is_keyframe)
	{
		body[i++] = 0x17;// 1: keyframe  7:AVC
	}
	else
	{
		body[i++] = 0x27;// 2: p-frame  7:AVC
	}
	body[i++] = 0x01;// AVC NALU   
	body[i++] = 0x00;
	body[i++] = 0x00;
	body[i++] = 0x00;

	// NALU size
	body[i++] = size >> 24 & 0xff;
	body[i++] = size >> 16 & 0xff;
	body[i++] = size >> 8 & 0xff;
	body[i++] = size & 0xff;
	// NALU data
	memcpy(&body[i], data, size);

	int ret = rtmp_send_packet(rtmp, body, i + size, timestamp);
	free(body);

	return ret;
}

int h264_send_internal(RTMP* rtmp, RTMPMetadata_t* rtmp_meta, uint8_t* data, int nal_start, int nal_end, int timestamp)
{
	int nal_size = nal_end - nal_start;
	int nal_type = data[nal_start] & 0x0F;
	int is_keyframe = (nal_type == NALU_TYPE_IDR) ? TRUE : FALSE;
	switch (nal_type)
	{
	case NALU_TYPE_SPS:
		rtmp_meta->sps_size = copy_data(&rtmp_meta->sps_data, data + nal_start, nal_size);
		break;
	case NALU_TYPE_PPS:
		rtmp_meta->pps_size = copy_data(&rtmp_meta->pps_data, data + nal_start, nal_size);
		break;
	case NALU_TYPE_SEI: // SEI信息不作处理,直接跳过
		break;
	case NALU_TYPE_IDR:
	case NALU_TYPE_SLICE:
		if (is_keyframe)
		{
			h264_send_metadata(rtmp, rtmp_meta); // 在发送keyframe之前必须发送metadata
			printf("send metadata\n");
		}
		h264_send_frame(rtmp, rtmp_meta, is_keyframe, data, nal_size, timestamp);
		printf("send frame\n");
		break;
	default:
		break;
	}
	

	return 0;
}

int librtmp_h264_send(RTMP* rtmp)
{
	FILE* file;
	file = fopen("test.h264", "rb");
	if (!file)
	{
		printf("read stream file failed\n");
		return -1;
	}

	uint8_t* buf = (uint8_t*)malloc(MAX_BUF_SIZE);
	if (!buf)
	{
		printf("malloc buf failed\n");
		return -1;
	}

	RTMPMetadata_t rtmp_meta = { 0 };
	size_t rsz = 0;
	size_t sz = 0;
	int64_t off = 0;
	uint8_t* p = buf;
	int nal_start = 0;
	int nal_end = 0;
	int timestamp = 0;
	int frame_rate = 25;

	while (1)
	{
		rsz = fread(buf + sz, 1, MAX_BUF_SIZE - sz, file);
		if (!rsz)
		{
			printf("EOF\n");
			break;
		}
		sz += rsz;
		while (find_nal_unit(p, sz, &nal_start, &nal_end) > 0)
		{
			h264_send_internal(rtmp, &rtmp_meta, p, nal_start, nal_end, timestamp);
			timestamp += 1000 / frame_rate;
			msleep(80); // 这里简化了一下,使用固定的时间间隔

			p += nal_end;
			sz -= nal_end;
		}

		memmove(buf, p, sz);
		off += p - buf;
		p = buf;
	}

	if (rtmp_meta.sps_data)
	{
		free(rtmp_meta.sps_data);
	}
	if (rtmp_meta.pps_data)
	{
		free(rtmp_meta.pps_data);
	}

	return 0;
}

int CleanupSockets()
{
	WSACleanup();
	return 0;
}

int librtmp_h264_close(RTMP* rtmp)
{
	CleanupSockets();
	return 0;
}

int main(int argc, char* argv[])
{
	RTMP* rtmp = RTMP_Alloc();
	int ret;

	const char* url = "rtmp://127.0.0.1:1935/live/stream";
	// 1. 网络连接
	librtmp_h264_connect(rtmp, url);
	// 2. 发送数据报
	librtmp_h264_send(rtmp);
	// 3. 关闭连接
	librtmp_h264_close(rtmp);

	RTMP_Close(rtmp);
	return 0;
}

3. 测试

测试的步骤为:
(1)启动RTMP服务器(nginx)
(2)使用libRTMP进行推流
 (a)码流为Crew序列
 (b)因为是将本机作为RTMP服务器,可以在浏览器中输入如下地址来查看服务器的情况

http://localhost/stat

如下所示,这里因为来回测试了很多遍,所以持续Time比较久。从stream中一栏可以看到video的相关信息,主要包括codec和size
在这里插入图片描述

(3)使用ffplay进行拉流查看,命令行为

.\ffplay.exe rtmp://127.0.0.1:1935/live/stream

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值