FFmpeg的自定义AVIO—FFmpeg API教程

作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音

ffmpeg 支持从网络流 或者本地文件读取数据,然后拿去丢给解码器解码,但是有一种特殊情况,就是数据不是从网络来的,也不在本地文件里面,而是在某块内存里面的。

这时候 av_read_frame() 函数怎样才能从内存AVPacket 读出来呢?

FFmpeg 的开发者一早就考虑到这个问题了,所以他们提供了 自定义 AVIO 的功能。利用这个功能,你可以自定义 AVIO 的输入函数,也可以自定义 AVIO 的输出函数。没错,输出你也可以输出到一块内存里面,而不是保存到本地文件。

AVIOContext 实际上就是一个中间层,负责连接 用户层的 API 函数 与 Demuxer/Muxer ,如下:

0-2

上图中我没有把 Muxer 复用的图画出来,省略了。

用户层 API 会调用 AVIOContextread_packet 函数从输入文件读取数据,然后放进去 缓存 buffer 里面,再调用 Demuxer 层的函数来解析 或者 拷贝 缓存 buffer 的数据。


本文的代码下载地址:GitHub,编译环境是 Qt 5.15.2 跟 MSVC2019_64bit 。

1-1

avio 项目 是从 output 项目改造来的,所以一些代码是类似的。

avio 项目的整个流程是这样的,先用了 readFile() 函数读取本地的 mp4 文件到内存,来模拟内存场景。然后调 avio_alloc_context() 自定义输入跟 seek 函数。

重点讲一下 avio_alloc_context() 函数的用法,定义如下:

AVIOContext *avio_alloc_context(
                  unsigned char *buffer,
                  int buffer_size,
                  int write_flag,
                  void *opaque,
                  int (*read_packet)(void *opaque, uint8_t *buf, int buf_size),
                  int (*write_packet)(void *opaque, uint8_t *buf, int buf_size),
                  int64_t (*seek)(void *opaque, int64_t offset, int whence));

参数如下:

1,unsigned char *buffer,这是一个指针,指向一块 av_malloc() 申请的内存。这是我们的自定义函数跟 FFmpegAPI 函数沟通的桥梁。

write_flag 为 0 时,由 自定义的回调函数 向 buffer 填充数据,FFmpeg API 函数取走数据。

write_flag 为 1 时,由 FFmpeg API 函数 向 buffer 填充数据,自定义的回调函数 取走数据。

补充,我测试的时候,这里的 buffer 指针跟 回调函数 里面的 buf 指针,好像不是同一块内存,FFmpeg 注释说,buffer 可能会被替换成另一块内存。

2,int buffer_sizebuffer 内存的大小,通常设置为 4kb 大小即可,对于一些有固定块大小的格式,例如 TS 格式,TS流的包结构是固定长度188字节的,所以你需要设置为 188 字节大小。如果这个值设置得不对,性能会下降得比较厉害,但是不会报错。

又例如,如果输入数据是 yuv,你最好把 buffer_size 设置成一帧 yuv 的大小,这样上层处理起来更加方便。

3,int write_flagwrite_flag 可以是 0 或者 1,作用是标记 buffer 内存的用途。

4,void *opaque,传递给我们自定义函数用的。

5,int (*read_packet)(...),输入函数的指针。

6,int (*write_packet)(...),输出函数的指针。

7,int (*seek)(...),seek 函数的指针。


再来看一下我们自定义的输入函数 read_packet(),如下:

1-2

bufbuf_size 都是 FFmpeg 告诉我们的,告诉我们的自定义函数,要往哪里(buf)写数据,写多少(buf_size)数据。

read_packet() 函数的返回值是实际写入的数据大小,如果是一些网络流,在某个时间确实没从网络读到数据,不能返回 0 ,需要阻塞等待读到数据。如果是已经到结尾了或者网络出错了,可以返回 AVERROR_EOF,代表没有更多输入了


再来看一下自定义的 seek 函数 seek_in_buffer(),如下:

1-3

这个 seek 函数是必须实现,如果不实现这个函数,直接传 NULLavio_alloc_context() ,在 mp4 格式下会导致 av_read_frame() 读不出来 AVPacket

但是在 flv 格式下是可以不实现 seek 函数的,需不需要实现 seek 是由封装格式决定的。我测试下来,发现 yuv 的输入源,只要指定了 format 格式,就不需要实现 seek 函数。

seek_in_buffer 函数的 offsetwhence 参数特别重要。

whence 代表 seek 的类型,主要有 2 个值,如下:

1,AVSEEK_SIZE,不进行 seek 操作,而是返回 视频 buffer 整体的大小。也就是文件大小。

2,SEEK_SET,要进行 seek 操作,seek 到 offset 参数的位置,也就是需要 seek 到 第 offset 个字节。需要把 bd->ptr 指向第 offset 个字节


后面的逻辑就是 av_read_frame() 从内存读到 AVPacket 之后,就丢给解码器,然后再重新编码,输出成另一个 mp4。

最后需要调 avio_context_free(&avio_ctx) 来释放这个 自定义的 avio 上下文。


main.c 里面只定义了输入函数,输出函数是在另一个文件 main_write.c 里面的。你只需要修改一下 avio.pro 文件即可调试,如下:

SOURCES += main.c 
改成
SOURCES += main_write.c

main_write.c 里面的重点代码如下:

1-4

main_write.c 的逻辑是申请了 100M 的内存,然后把 av_interleaved_write_frame() 输出的数据,全部保存到这 100M 内存里面,可能没用完100M内存。

上图中我新申请了一个 avio 上下文实例来给输出用,不能跟输入用同一个 avio 实例。然后封装格式指定位 flv 了,因为 flv 格式可以不实现 seek

mp4 格式的输出对应的 seek 函数我也不知道怎么实现。可能跟输入一样吧,这里埋个坑,后面填。

然后直接赋值 fmt_ctx_out->pb 即可

fmt_ctx_out->pb = avio_ctx_out;

内存IO模式输出,是不需要调 avio_open2() 的了。


参考文章:

1,ffmpeg AVIOContext 自定义 IO 及 seek

2,FFmpeg内存IO模式(内存区作输入或输出) - 叶余



感谢 NETINT(镕铭微电子) 赞助《FFmpeg原理》免费版一书的服务器费用,下面是 VPU 产品介绍

版权所属 xianwanzhiyin.net 罗上文 2024 all right reserved,powered by Gitbook该文件修订时间: 2023-05-17 11:39:54

results matching ""

    No results matching ""