目录

1.介绍

2.低分辨率编码码流推流的流程

2.1. 初始化RKMEDIA_FFMPEG_CONFIG结构体

2.调用init_rkmedia_ffmpeg_context来初始化1280* 720推流器

2.3. 创建low_video_push_thread线程

2.4. 从LOW_VIDEO_QUEUE获取每一帧H264数据码流并且赋值到AVPacket

2.4.1. AVPacket缓冲区的赋值

代码讲解

2.4.2. AVPacket缓冲区长度的赋值

2.4.3. AVPacket关键帧标识符的赋值

2.5. 每一帧AVPacket计算PTS时间戳

2.6. 把每一帧视频数据传输到流媒体服务器


1.介绍

如何通过低分辨率队列的每一帧数据,并且通过FFMPEG推流器传输到低码流分辨率流媒体服务器,本章节代码在rkmedia_assignment_manage.cpp和rkmedia_data_process.cpp里面。低分辨率编码码流推送的过程和高分辨率编码码流的推送的过程基本上一致,唯一的区别在于分辨率的设置。

2.低分辨率编码码流推流的流程

上面是低分辨率推流的过程,总共分成6个步骤。分别是初始化RKMEDIA_FFMPEG_CONFIG结构体、调用init_rkmedia_ffmpeg_context设置1280* 720推流器、创建low_video_push_thread线程、从 LOW_VIDEO_QUEUE 队列获取每一帧视频数据 、每一帧AVPacket计算PTS并进行时间基转换、利用FFMPEG的API推送每一帧视频数据到流媒体服务器,下面我们来具体看每一个步骤的实现过程:

2.1. 初始化RKMEDIA_FFMPEG_CONFIG结构体

我们来看看RKMEDIA_FFMPEG_CONFIG的成员变量

2.2.1. width推流器的width,width和rv1126编码器的width一致

2.2.2. height推流器的height,height和rv1126编码器的height一致

2.2.3. config_idconfig_id,暂时没用到

2.2.4. protocol_type流媒体的类型

2.2.5.network_addr流媒体地址

2.2.6.video_codec视频编码器ID

2.2.7.audio_codec音频编码器ID

2.2.8.video_stream自定义VIDEO的STREAM结构体配置

2.2.9.audio_stream自定义AUDIO的STREAM结构体配置

上面是低分辨率rkmedia_ffmpeg_config的设置

2.调用init_rkmedia_ffmpeg_context来初始化1280* 720推流器

init_rkmedia_ffmpeg_context是初始化rkmedia_ffmpeg_config的设置,关于这个函数的内容在之前的课程已经说了。这里不做过多的介绍。

2.3. 创建low_video_push_thread线程

Low_video_push_thread最主要作用是在LOW_VIDEO_QUEUE队列获取每一帧1280* 720的H264编码视频流,然后再把每一帧H264的码流数据先赋值到AVPacket,再调用FFMPEG的API把视频流传输到流媒体服务器。

2.4. LOW_VIDEO_QUEUE获取每一帧H264数据码流并且赋值到AVPacket

上面的代码是从LOW_VIDEO_QUEUE队列里面取出每一帧1280* 720的H264数据,并且赋值到AVPacket的过程。整个函数封装到deal_low_video_packet里面。在deal_high_video_packet主要是实现从LOW_VIDEO_QUEUE队列获取每一帧数据并赋值到AVPacket的具体实现过程,具体如上图:

这里面有几个比较核心的地方:video_data_packet的视频数据包赋值到AVPacket,这里要赋值两部分:一部分是AVPacket缓冲区数据的赋值,另外一个是AVPacket的长度赋值。

2.4.1. AVPacket缓冲区的赋值

首先用av_buffer_realloc分配每一个缓冲区数据。要注意的是AVPacket中缓冲区的buf是不能直接赋值的,如: memcpy(pkt->data, video_data_packet->buffer, video_data_packet->frame_size)否则程序就会出现core_dump情况。我们需要先把video_data_packet_t的视频数据(video_data_packet->buffer)先拷贝到pkt->buf->data,然后再把pkt->buf->data的数据赋值到pkt->data

代码讲解

int av_buffer_realloc(AVBufferRef **pbuf, size_t size);

FFmpeg中重新分配AVBufferRef缓冲区的函数

参数1:AVBufferRef **pbuf,这是一个双重指针,指向AVBufferRef指针的地址。

typedef struct AVBufferRef {
    AVBuffer *buffer;        // 实际缓冲区
    uint8_t *data;           // 数据指针
    int      size;           // 缓冲区大小
} AVBufferRef;

参数2:size_t size,需要的新缓冲区大小(字节)。

调整AVPacket的缓冲区大小,确保能容纳即将放入的数据。

int ret = av_buffer_realloc(&pkt->buf, video_data_packet->video_frame_size + 70);

+70 是预留的头部空间

// 可能的用途:
- H264的SPS/PPS头部(如果需要插入)
- FLV Tag头部(11字节)
- 其他封装格式的头部信息
- 编码器可能添加的额外数据

if (pkt->buf == NULL) {
    // 首次分配
    pkt->buf = av_buffer_alloc(new_size);
} else {
    // 重新分配
    if (pkt->buf->size < new_size) {
        // 创建更大的新缓冲区
        // 复制旧数据
        // 释放旧缓冲区
        // 更新pkt->buf指向新缓冲区
    }
}

2.4.2. AVPacket缓冲区长度的赋值

把video_data_packet的video_frame_size长度直接赋值给AVPacket的pkt->size。

2.4.3. AVPacket关键帧标识符的赋值

添加了这个标识符后,每个AVPacket中都进行关键帧设置,这个标识符必须要加,否则播放器则无法正常解码出视频。

2.5. 每一帧AVPacket计算PTS时间戳

根据AVPacket的数据去计算视频的PTS,若AVPacket的数据不为空。则让视频pts = ost->next_timestamp++。

把视频PTS进行时间基的转换,调用av_packet_rescale_ts把采集的视频时间基转换成复合流的时间基。

2.6. 把每一帧视频数据传输到流媒体服务器

时间基转换完成之后,就把视频数据写入到复合流文件里面,调用的API是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。

void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);

  • pkt:指向要转换时间戳的 AVPacket 的指针。该包应已包含有效的 ptsdts 和 duration 字段。转换后,这些字段将被修改为新的时间基下的值。
  • tb_src:源时间基,即包当前时间戳所使用的时间单位。通常为编码器时间基(AVCodecContext.time_base)。
  • tb_dst:目标时间基,即转换后包时间戳应使用的时间单位。通常为流时间基(AVStream.time_base)。

av_packet_rescale_ts 执行以下转换:

  • 将 pkt->pts 从 tb_src 单位转换为 tb_dst 单位。
  • 将 pkt->dts 从 tb_src 单位转换为 tb_dst 单位。
  • 将 pkt->duration 从 tb_src 单位转换为 tb_dst 单位。

注:

  • 必须在使用包时间戳的任何地方之前进行转换:例如,在调用 av_interleaved_write_frame 前,必须确保包时间戳已处于流时间基下。
  • 源时间基和目标时间基的确定:通常源时间基是编码器的时间基,目标时间基是流的时间基。但具体取决于使用阶段。务必查阅相关上下文的文档或代码注释。
  • 持续时间也要转换duration 字段也必须转换,以保证播放时长正确。
  • 转换是就地的:函数直接修改传入的 AVPacket,因此如果需要保留原值,应先复制一份。
  • 不会修改 stream_index:该字段保持不变,仍指向所属的流。

int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);

  • s:指向 AVFormatContext 的指针,该上下文已通过 avformat_alloc_output_context2 创建,并且已打开输出(通过 avio_open 等)。它包含了输出格式、流信息、IO 上下文等。
  • pkt:指向要写入的 AVPacket 的指针。该数据包必须包含编码后的媒体数据(如 H.264 视频帧或 AAC 音频帧),并且已正确设置了 pts/dtsstream_indexsizedata 等字段。

特殊用法:若 pkt 为 NULL,则函数用于刷新内部缓冲区,强制将所有已缓存但尚未写入的包写入输出。

  1. 按交错顺序写入:函数内部维护一个包队列,根据包的 pts(或 dts,取决于具体实现)进行排序,确保写入容器的包是严格按照时间顺序交错的。这样生成的音视频文件在播放时,不同流的数据能够交替出现,便于解码器连续读取。
  2. 自动缓冲与延迟写入:它不会立即将每个包写入输出,而是先缓存,直到确定后续不会有时间戳更早的包出现(例如收到了另一个流的包,或缓存队列达到一定大小)。这种机制可以容忍输入包的顺序稍有混乱,并自动纠正。
  3. 不进行时间基转换:重要:av_interleaved_write_frame 不会对包中的 ptsdts 进行任何缩放或转换。它直接使用包中当前的值。调用者必须在调用此函数之前,通过 av_packet_rescale_ts 将包的时间戳从编码器时间基转换到流时间基(AVStream.time_base)。流时间基通常应与输出格式要求的单位一致(例如 FLV 要求毫秒,即 {1,1000})。
  4. 处理包的资源:写入成功后,函数内部会调用 av_packet_unref 来释放包内部的缓冲区引用,但不会释放包结构本身。调用者仍需负责 av_packet_free 或重用该包。

  • 时间戳必须正确:包的 pts/dts 必须已经处于流时间基下,且数值合理(非负、单调递增,符合编码器 GOP 结构)。

  • 必须调用 avformat_write_header 之前:在写入任何数据包之前,必须先调用 avformat_write_header 写入文件头。

  • 最后调用 av_write_trailer:在所有数据包写入完毕后,需要调用 av_write_trailer 写入文件尾,并关闭输出。

  • 刷新缓冲区:在调用 av_write_trailer 之前,可以用 av_interleaved_write_frame(s, NULL) 刷新内部缓存,确保所有包都已写入。

  • 线程安全:该函数不是线程安全的,如果多个线程同时操作同一个 AVFormatContext,需要外部加锁。

注:

  • 时间戳必须正确:包的 pts/dts 必须已经处于流时间基下,且数值合理(非负、单调递增,符合编码器 GOP 结构)。
  • 必须调用 avformat_write_header :在写入任何数据包之前,必须先调用 avformat_write_header 写入文件头。
  • 最后调用 av_write_trailer:在所有数据包写入完毕后,需要调用 av_write_trailer 写入文件尾,并关闭输出。
  • 刷新缓冲区:在调用 av_write_trailer 之前,可以用 av_interleaved_write_frame(s, NULL) 刷新内部缓存,确保所有包都已写入。
  • 线程安全:该函数不是线程安全的,如果多个线程同时操作同一个 AVFormatContext,需要外部加锁。

注:

【AVCodecContext】c->time_base = 1/25
├─ 存放位置:AVCodecContext 结构体
├─ 对应什么:VENC硬件编码器输出的原始H.264数据
├─ 实际含义:VENC以25fps的速率输出帧
└─ 本质:这是"硬件编码器的时间基的软件表示"

【AVStream】st->time_base = 1/1000  
├─ 存放位置:AVStream 结构体
├─ 对应什么:RTMP/FLV推流协议的要求
├─ 实际含义:RTMP协议规定时间戳必须用毫秒
└─ 本质:这是"推流协议的时间基"

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐