概述
项目中遇到的一个难题,即需要从RTSP流地址中解析出来Nalu,然后对Nalu进行封包发送到GB28181平台。本文仅仅总结如何利用FFmpeg库中的函数通过AVpacket解析出来Nalu的可行性以及方法。
如果是非嵌入式设备,也可以自己搭建一个小型RTSP服务器来实现,其中各种功能也可以自行控制,后面文章再进行总结
源码分析
可行性分析
FFmpeg库中没有找到直接解析出来的Nalu的方法,如果无法从AVpacket中解析出来Nalu,那么对于H264 H265格式的视频就没有办法正确的进行解析
打开FFmpeg源码可以找到parse_packet 函数和 av_parser_parse2 函数
-
av_parser_parse2 的作用是使用解码器解析器 (AVCodecParserContext) 解析输入数据,并将解析后的数据输出到新的 AVPacket 中
-
所以AVpacket中肯定是存储了解析出来的Nalu,但是直接按照Nalu的方法没有成功提取出来,证明其中AVpacket中的data加了一些特殊的处理
进一步分析av_parser_parse2函数逻辑
- 首先根据 AVCodecParserContext 中设置的解析器,识别输入数据流中的结构化单元
- 然后针对于视频流,FFmpeg 会根据 codec_id 初始化相应的解析器。这些解析器的目的就是将原始的码流数据分解成解码器可以处理的单元
-
循环处理 parse_packet 的输出
-
parse_packet 函数内部的 while 循环结构表明,一个输入的 AVPacket 可能被 av_parser_parse2 分割成多个 out_pkt (输出的 AVPacket)
-
上述功能也证明其正在提取Nalu,解析器的作用就是将其拆分出来,然后单个或者一组Nalu会被放入AVpacket中
-
总结其基本逻辑
源码中并没有直接给出Nalu的提取代码,也许是我没有找到,根据相关源码的实现逻辑,其提取Nalu的大概逻辑应该如下
-
识别 NALU 的起始码: 解析器会扫描码流,寻找 NALU 的起始码 (例如 H.264 的 0x00 00 01 或 0x00 00 00 01)
-
提取 NALU 数据: 一旦找到起始码,解析器会提取从起始码开始到下一个起始码之前的数据,这就是一个 NALU 的负载
-
封装成 AVPacket (内部处理): 在内部,FFmpeg 可能会将提取出的 NALU 数据封装成 AVPacket 结构,以便后续的解码器处理
提取方法总结
主要流程总结
-
访问 pkt->data 和 pkt->size
-
pkt->data 是一个指向 NALU 数据的 uint8_t* 指针
-
pkt->size 是 NALU 数据的字节数
-
-
Nalu格式:此时需要根据RTSP中拉取的视频流格式的不同,解析方式需要进行相应的调整
-
遍历和解析Nalu
-
手动解析Nalu的边界,然后根据编码规范,提取出来每个独立的Nalu
-
同时还需要解析出来Nalu的头部信息
-
H265解析事例
// 解析出Nalu的类型
NaluType Device::parse_nalu_type(uint8_t *nalu) {
// H.265 NALU 类型位于头两个字节的后6位
return static_cast<NaluType>((nalu[0] >> 1) & 0x3F);
}
// 处理AVpacke中的Nalu数据
void Device::process_packet(AVPacket *pkt) {
uint8_t *data = pkt->data;
int size = pkt->size;
int startcode_len = 0;
while (size > 0) {
// 查找起始码 (0x00000001 或 0x000001)
if (size >= 4 && data[0] == 0 && data[1] == 0 && data[2] == 0 && data[3] == 1) {
startcode_len = 4;
} else if (size >= 3 && data[0] == 0 && data[1] == 0 && data[2] == 1) {
startcode_len = 3;
} else {
data++;
size--;
continue;
}
// 定位到下一个起始码或结尾
uint8_t *next_startcode = NULL;
for (int i = startcode_len; i < size - 3; i++) {
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
next_startcode = data + i;
break;
}
}
int nalu_size = (next_startcode != NULL) ?
(next_startcode - (data + startcode_len)) :
(size - startcode_len);
// 提取 NALU 数据 (排除起始码)
uint8_t *nalu_data = data + startcode_len;
NaluType nalu_type = parse_nalu_type(nalu_data);
// 处理 NALU
handle_nalu(nalu_data, nalu_size, nalu_type, pkt->pts, pkt->dts);
// 移动到下一个 NALU
data += startcode_len + nalu_size;
size -= startcode_len + nalu_size;
}
}
int Device::h265_parser(const char *url,std::vector<Nalu *> &nalu_vector){
avformat_network_init();
//日志输出等级
set_ffmpeg_log_level();
AVFormatContext *fmt_ctx = NULL;
AVPacket *pkt = av_packet_alloc();
if (avformat_open_input(&fmt_ctx, "rtsp://127.0.0.1/live/test", NULL, NULL) < 0) {
fprintf(stderr, "无法打开输入文件\n");
return -1;
}
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
fprintf(stderr, "无法获取流信息\n");
return -1;
}
int video_stream_idx = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_idx = i;
break;
}
}
if (video_stream_idx == -1) {
fprintf(stderr, "未找到视频流\n");
return -1;
}
while (av_read_frame(fmt_ctx, pkt) >= 0) {
if (pkt->stream_index == video_stream_idx) {
process_packet(pkt);
}
av_packet_unref(pkt);
}
av_packet_free(&pkt);
avformat_close_input(&fmt_ctx);
}