目录
- 1. 整体流程
- 2. 代码
- 2.1 头文件
- 2.2 c文件
- 3. 测试
RTMP协议相关:
【流媒体】RTMP协议概述
【流媒体】RTMP协议的数据格式
【流媒体】RTMP协议的消息类型
【流媒体】RTMPDump—主流程简单分析
【流媒体】RTMPDump—RTMP_Connect函数(握手、网络连接)
【流媒体】RTMPDump—RTMP_ConnectStream(创建流连接)
【流媒体】RTMPDump—Download(接收流媒体信息)
【流媒体】RTMPDump—AMF编码
【流媒体】基于libRTMP的H264推流器
参考:
- libRTMP推流开源工程(附带github工程链接)
最简单的基于librtmp的示例:发布H.264(H.264通过RTMP发布) - RTMP服务器的搭建
Windows搭建RTMP服务器+OBS推流+VLC拉流 - 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