1.avio介绍
avio是FFmpeg中的一个模块,用于实现多种输入输出方式的封装。
avio提供了一系列API,可以将数据从内存读取到缓冲区中,也可以将缓冲区中的数据写入到内存中。其实现依赖于IOContext结构体,该结构体定义了当前输入/输出事件的状态、数据、回调函数等信息,并支持通过自定义回调函数实现不同的输入/输出方式。
内存输入(Memory Input)是指将数据从内存中读取到缓冲区中,常见的应用场景包括:从内存中读取音视频数据进行解码或处理。在使用avio实现内存输入时,需要首先创建一个AVIOContext结构体,并将内存数据缓冲区作为参数传递给avio_open函数进行初始化。之后,可以使用avio_read函数从缓冲区中读取数据,直至读取完成。
内存输出(Memory Output)是指将数据从缓冲区中写入到内存中,常见的应用场景包括:将音视频数据编码并保存到内存中。在使用avio实现内存输出时,需要首先创建一个AVIOContext结构体,并将内存数据缓冲区和缓冲区大小作为参数传递给avio_open函数进行初始化。之后,可以使用avio_write函数将数据写入缓冲区中,并在完成输出后调用avio_close函数关闭AVIOContext结构体。
总的来说,内存输入和输出是指在使用FFmpeg进行音视频处理时,将数据从内存中读取或写入到内存中的一种方式。使用avio模块可以方便地实现这种输入输出方式,并支持自定义回调函数以满足不同的应用需求。
2.为什么要用avio?
使用FFmpeg的avio模块实现内存输入和输出有以下几个优点:
2.1.灵活性高
传统的音视频处理方式往往需要将音视频数据保存到文件中,然后再进行读取和处理。而使用avio模块可以将数据直接读取或写入到内存中,从而提高了音视频处理的灵活性。这种方式可以避免繁琐的文件IO操作,节省磁盘空间。
2.2.扩展性强
内存输入和输出功能可以方便地扩展为其他更高级的应用程序,例如:流媒体服务器、实时音视频采集与处理等。这是因为内存输出能够较为轻松地将音视频数据编码并存储到内存缓冲区中,进而交由网络传输;内存输入则可直接从内存缓冲区获取音视频数据,快速响应用户请求。
2.3.可定制性好
FFmpeg的avio模块提供了一系列API,可以通过设置回调函数实现各种自定义功能。例如:自定义网络协议传输方式、增加错误重试机制、实现多路复用等。这使得处理器可以根据自己的需求对avio模块进行灵活配置,以最大限度地满足不同场景下的业务需求。
因此,使用FFmpeg的avio模块实现内存输入和输出可以提高音视频处理的效率,增加程序的灵活性和扩展性,同时还具有良好的可定制性。
3.内存区作为输入
3.1.回调函数何时被回调呢?
所有需要从输入源中读取数据的时刻,都将调用回调函数。和输入源是普通文件相比,只不过输入源变成了内存区,其他各种外在表现并无不同。
如下各函数在不同的阶段从输入源读数据,都会调用回调函数:
函数一:打开媒体文件并获取媒体文件信息的函数
第二个参数是要打开的音影文件,或者媒体文件的URL。
如果第一个参数 AVFormatContext 中 pb(类型是AVIOContext) 是有值的话,第二个参数可以传递NULL
int avformat_open_input(AVFormatContext **ps, const char *url,
const AVInputFormat *fmt, AVDictionary **options);
AVFormatContext里面有一个参数 AVIOContext *pb;
函数二,为了获取 每个音视频流。
avformat_find_stream_info()函数是用于获取媒体文件中每个音视频流的详细信息的函数,包括解码器类型、采样率、声道数、码率、关键帧等信息。该函数定义在libavformat/avformat.h中。
函数原型为:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
同样, AVFormatContext里面有一个参数 AVIOContext *pb;
函数三,从输入源读取数据包
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
同样, AVFormatContext里面有一个参数 AVIOContext *pb;
3.2.该示例作用是统计mp4文件的视频帧数,代码如下:
4.内存区作为输出
4.1.回调函数何时被回调呢?
所有输出数据的时刻,都将调用回调函数。和输出是普通文件相比,只不过输出变成了内存区,其他各种外在表现并无不同。
如下各函数在不同的阶段会输出数据,都会调用回调函数:
avformat_write_header() 将流头部信息写入输出区
av_interleaved_write_frame() 将数据包写入输出区
av_write_trailer() 将流尾部信息写入输出区
5.内存IO模式非常重要的一个函数:avio_alloc_context()
/**
* Allocate and initialize an AVIOContext for buffered I/O. It must be later
* freed with avio_context_free().
*
* @param buffer Memory block for input/output operations via AVIOContext.
* The buffer must be allocated with av_malloc() and friends.
* It may be freed and replaced with a new buffer by libavformat.
* AVIOContext.buffer holds the buffer currently in use,
* which must be later freed with av_free().
* @param buffer_size The buffer size is very important for performance.
* For protocols with fixed blocksize it should be set to this blocksize.
* For others a typical size is a cache page, e.g. 4kb.
* @param write_flag Set to 1 if the buffer should be writable, 0 otherwise.
* @param opaque An opaque pointer to user-specific data.
* @param read_packet A function for refilling the buffer, may be NULL.
* For stream protocols, must never return 0 but rather
* a proper AVERROR code.
* @param write_packet A function for writing the buffer contents, may be NULL.
* The function may not change the input buffers content.
* @param seek A function for seeking to specified byte position, may be NULL.
*
* @return Allocated AVIOContext or NULL on failure.
*/
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));
该函数具有以下参数:
-
buffer:存储音视频数据的内存缓冲区指针,必须通过 av_malloc() 等函数分配。该内存块会被 AVIOContext 结构体引用,不能在生命周期内被释放。
-
buffer ⽤作FFmpeg输⼊时,由⽤户负责向 buffer 中填充数据,FFmpeg取⾛数据。
-
buffer ⽤作FFmpeg输出时,由FFmpeg负责向 buffer 中填充数据,⽤户取⾛数据。
-
-
buffer_size:缓冲区大小,对于固定块大小的协议需要设置为固定块大小,对于其他协议可以设置为典型缓存页大小,例如 4KB。
read_packet和write_packet是函数指针,指向⽤户编写的回调函数。
seek也是函数指针,需要⽀持seek时使⽤。 可以类⽐fseek的机制
6.代码范例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavutil/frame.h>
#include <libavutil/mem.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#define BUF_SIZE 20480
static char* av_get_err(int errnum)
{
static char err_buf[128] = {0};
av_strerror(errnum, err_buf, 128);
return err_buf;
}
static void print_sample_format(const AVFrame *frame)
{
printf("ar-samplerate: %uHz\n", frame->sample_rate);
printf("ac-channel: %u\n", frame->channels);
printf("f-format: %u\n", frame->format);// 格式需要注意,实际存储到本地文件时已经改成交错模式
}
static int read_packet(void *opaque, uint8_t *buf, int buf_size)
{
FILE *in_file = (FILE *)opaque;
int read_size = fread(buf, 1, buf_size, in_file);
printf("read_packet read_size:%d, buf_size:%d\n", read_size, buf_size);
if(read_size <=0) {
return AVERROR_EOF; // 数据读取完毕
}
return read_size;
}
static void decode(AVCodecContext *dec_ctx, AVPacket *packet, AVFrame *frame,
FILE *outfile)
{
int ret = 0;
ret = avcodec_send_packet(dec_ctx, packet);
if(ret == AVERROR(EAGAIN)) {
printf("Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
} else if(ret < 0) {
printf("Error submitting the packet to the decoder, err:%s\n",
av_get_err(ret));
return;
}
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return;
} else if (ret < 0) {
printf("Error during decoding\n");
exit(1);
}
if(!packet) {
printf("get flush frame\n");
}
int data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);
// print_sample_format(frame);
/**
P表示Planar(平面),其数据格式排列方式为 :
LLLLLLRRRRRRLLLLLLRRRRRRLLLLLLRRRRRRL...(每个LLLLLLRRRRRR为一个音频帧)
而不带P的数据格式(即交错排列)排列方式为:
LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRL...(每个LR为一个音频样本)
播放范例: ffplay -ar 48000 -ac 2 -f f32le believe.pcm
并不是每一种都是这样的格式
*/
// 这里的写法不是通用,通用要调用重采样的函数去实现
// 这里只是针对解码出来是planar格式的转换
for(int i = 0; i < frame->nb_samples; i++) {
for(int ch = 0; ch < dec_ctx->channels; ch++) {
fwrite(frame->data[ch] + data_size *i, 1, data_size, outfile);
}
}
}
}
int main(int argc, char **argv)
{
if(argc != 3) {
printf("usage: %s <intput file> <out file>\n", argv[0]);
return -1;
}
const char *in_file_name = argv[1];
const char *out_file_name = argv[2];
FILE *in_file = NULL;
FILE *out_file = NULL;
// 1. 打开参数文件
in_file = fopen(in_file_name, "rb");
if(!in_file) {
printf("open file %s failed\n", in_file_name);
return -1;
}
out_file = fopen(out_file_name, "wb");
if(!out_file) {
printf("open file %s failed\n", out_file_name);
return -1;
}
// 2自定义 io
uint8_t *io_buffer = av_malloc(BUF_SIZE);
AVIOContext *avio_ctx = avio_alloc_context(io_buffer, BUF_SIZE, 0, (void *)in_file, \
read_packet, NULL, NULL);
AVFormatContext *format_ctx = avformat_alloc_context();
format_ctx->pb = avio_ctx;
int ret = avformat_open_input(&format_ctx, NULL, NULL, NULL);
if(ret < 0) {
printf("avformat_open_input failed:%s\n", av_err2str(ret));
return -1;
}
// 编码器查找
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_AAC);
if(!codec) {
printf("avcodec_find_decoder failed\n");
return -1;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if(!codec_ctx) {
printf("avcodec_alloc_context3 failed\n");
return -1;
}
ret = avcodec_open2(codec_ctx, codec, NULL);
if(ret < 0) {
printf("avcodec_open2 failed:%s\n", av_err2str(ret));
return -1;
}
AVPacket *packet = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
while (1) {
ret = av_read_frame(format_ctx, packet);
if(ret < 0) {
printf("av_read_frame failed:%s\n", av_err2str(ret));
break;
}
decode(codec_ctx, packet, frame, out_file);
}
printf("read file finish\n");
decode(codec_ctx, NULL, frame, out_file);
fclose(in_file);
fclose(out_file);
av_free(io_buffer);
av_frame_free(&frame);
av_packet_free(&packet);
avformat_close_input(&format_ctx);
avio_context_free(&avio_ctx);
// avcodec_free_context(&codec_ctx); 调用这一行在 ffmpeg 6.0上,会有问题,目前原因不明,需要进一步todo
printf("main finish\n");
return 0;
}
7.代码测试:
ffplay -ar 48000 -ac 2 -f f32le believe.pcm