0%

FFmpeg基本API:常用操作

API处理套路:

  • 方法一般返回值小于0表示失败。
  • 使用上下文连接多个API。
    • 上下文包含大量相关信息。
    • 上下文一般对应的创建与释放方法,且注释里有说明,例如:open-close、alloc-free。
  • 要复用结构体时,调用对应的unref方法,以重置信息。
  • 所有压缩包、未压缩帧操作都要循环操作。

日志系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <libavuitl/log.h>

// 设置输出的日志等级
av_log_set_level(int level);

// 打日志,参1一般为NULL,参2:AV_LOG_DEBUG等常量
void av_log (void* avcl, int level, const char* fmt, ...);

// 错误码转详细信息
#define av_err2str(errnum) \
av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, AV_ERROR_MAX_STRING_SIZE, errnum)

static inline char *av_make_error_string(char *errbuf, size_t errbuf_size, int errnum)
{
av_strerror(errnum, errbuf, errbuf_size);
return errbuf;
}

获取流信息

获取流信息基本是基于AVFormatContext进行获取的。

1
2
3
4
5
6
7
8
// 检查是否支持格式
int avformat_find_stream_info(AVFormatContext* ic, AVDictionary** options);

// 获取指定类型的流索引,只关心前两两个参数即可
int av_find_best_stream(AVFormatContext* ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec** decoder_ret, int flags);

// 打印格式信息
void av_dump_format(AVFormatContext* ic, int index, const char* url, int is_output);

基本流操作框架

  1. 创建并打开输入上下文。avformat_open_input
  2. 检查输入格式。avformat_find_stream_infoav_dump_format
  3. 根据路径创建输出上下文。avformat_alloc_output_context2
  4. 创建流,因为不参与编解码过程,所以拷贝编解码参数。`avformat_new_streamavcodec_parameters_copyout_stream->codecpar->codec_tag = 0
  5. 检查输出格式。av_dump_format
  6. 打开输出IO。avio_open
  7. 写入头部。avformat_write_header
  8. ==从输入流中读取并写入输出数据包。==
  9. 写入尾部。av_write_trailer
  10. 释放上面创建的上下文。avformat_close_inputavio_closeavformat_free_context

流的操作按照FFmpeg的API流程大多只需要更改高亮的步骤,其余的基本是固定流程。数据包读取与写入的过程一般是这样的:

  1. 创建数据包结构体。av_packet_alloc
  2. 循环读取帧,进入帧处理。av_read_frame
    1. 处理数据包细节:pkt.ptspkt.dtspkt.durationpkt.pos
    2. 写入。av_interleaved_write_frame
    3. 减少数据包引用。av_packet_unref
  3. 释放数据包结构体。av_packet_unref

要点:

AVFormatContext

从输入URL得出格式信息:

  1. 建立输入格式上下文。avformat_open_input
  2. 检查格式是否支持。avformat_find_stream_infoav_dump_format

从输出URL得出格式信息:

  1. 从输出URL猜测得出。avformat_alloc_output_context2

AVStream

输入流信息是输入格式上下文的信息,且是完整的:

  1. 找到指定格式的流索引:av_find_best_stream
  2. 直接取出:fmt_ctx->streams[audio_idx]
  3. 外加异步格式检查:assert_condition(in_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO, "媒体类型不匹配");

输出流信息则是要自己创建的:

  1. 根据输出格式上下文创建输出流:avformat_new_stream
  2. 从输入流拷贝编解码参数到输出流:avcodec_parameters_copy

AVIOContext

存在输出格式上下文中,只需要在写入前后开启和关闭即可。avio_openavio_close

AVPacket

  1. 从输入格式上下文读取数据包:av_read_frame
  2. 交错写入数据包到输出格式上下文:av_interleaved_write_frame

应用:导出音频流/视频流

可以用两种方式:

  • 读取数据包,写入数据包到文件,补充文件头(这里存在要自己实现文件头的逻辑)。
  • 走FFmpeg整个流程。

在以上的基本流程上做修改:

  • 获取指定媒体类型,并获取输入流。
    1. av_find_best_stream -> stream_id
    2. in_stream = fmt_ctx->streams[stream_idx]
  • 循环读取帧,只处理stream_idx匹配的数据包。

应用:时间裁剪

  • 读取数据包之前进行跳转,并获取跳转后pts、dts。av_seek_frame
  • 读取数据包时:
    • 减去起始的pts、dts。
    • 不处理结束时间之后的数据包。av_q2d(in_stream->time_base) * pts <= end_time

编解码

编码

基本步骤:

  1. 打开编码器。avcodec_find_encoder_by_name
  2. 设置编码参数。须手动设置,因为没有参照的来源。
  3. 打开编码器。avcodec_open2
  4. 编码。avcodec_encode_video2

具体步骤:

  1. 查询编码器,并创建编码上下文。avcodec_find_encoder_by_nameavcodec_alloc_context3
  2. 设置编码参数。
  3. 打开编码器。avcodec_open2
  4. 创建文件、AVFrame并把编码上下文的参数设置到AVFrame中。
  5. AVFrame分配缓冲区空间。av_frame_get_buffer
  6. 编码并写入数据。
    1. 发送帧。avcodec_send_frame
    2. 循环接收数据包,并写入文件。avcodec_send_framefwrite
  7. 编码空的AVFrame以刷新编码器。
  8. 按需写入结束码。
  9. 关闭文件、释放相关资源。

编码细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void encode_to_file(AVCodecContext* enc_ctx,
AVFrame* frame,
AVPacket* pkt,
FILE* outfile)
{
int ret;
if (!!frame) {
av_log(NULL, AV_LOG_INFO, "Send frame %3" PRId64 "\n", frame->pts);
}

ret = avcodec_send_frame(enc_ctx, frame);
assert_errnum(ret, "发送编码帧失败");

while (ret >= 0) {
ret = avcodec_receive_packet(enc_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return;
}
assert_errnum(ret, "编码失败");

av_log(NULL, AV_LOG_INFO, "Write frame %3" PRId64 " (size=%5d)\n",
pkt->pts, pkt->size);
fwrite(pkt->data, 1, pkt->size, outfile);
av_packet_unref(pkt);
}
}

注意:这里是直接把编码后的数据包数据直接写到文件中,并没有写头尾。即没有使用格式上下文的AVIOContext。

要点:

AVCodec

通过id和名称查找,后续通过AVCodecContext进行管理。avcodec_find_encoder_by_name

AVCodecContext

  1. 通过AVCodec创建:avcodec_alloc_context3
  2. 重点在于格式配置,都设置到该上下文中。
  3. 打开后才能使用编解码器:avcodec_open2
  4. 使用完毕后释放:avcodec_free_context

编码,两层循环:

  1. 给编码器上下文塞帧:avcodec_send_frame
  2. 循环从编码器上下文获取数据包:avcodec_receive_packet

AVFrame

  • 创建与释放:av_frame_allocav_frame_free
  • 若是自己填充数据,则要先从codec上下文获取格式设置:pix_fmtwidthheight
  • 填充数据前要分配空间:av_frame_get_buffer
  • 填充数据前要确认帧是否可写入:av_frame_make_writable

设置了格式决定了分配空间的大小以及后续填充数据的方式。

解码

基本步骤:

  1. 查找解码器。avcodec_find_decoder
  2. 从输入拷贝相关解码参数。avcodec_parameters_to_context
  3. 打开解码器。avcodec_open2
  4. 解码。avcodec_decode_video2

具体步骤:

  1. 打开文件。avformat_open_input
  2. 检查格式,获取视频流。avformat_find_stream_infoav_dump_formatav_find_best_stream
  3. 根据读取的流信息查询编解码器并创建对应上下文。avcodec_find_decoderavcodec_alloc_context3
  4. 从读取的流信息中拷贝相关的编解码器参数。avcodec_parameters_to_context
  5. 打开编解码器。avcodec_open2
  6. 若要转换图像格式,则创建SwsContext
  7. 循环读取帧。av_read_frame
  8. 循环解码。
    1. 发送数据包。avcodec_send_packet
    2. 循环获得解码帧。avcodec_receive_frame

解码细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt,
const char *filename)
{
int ret;

// 发送数据包
ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
fprintf(stderr, "Error sending a packet for decoding\n");
exit(1);
}

while (ret >= 0) {
// 获得解码帧
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
fprintf(stderr, "Error during decoding\n");
exit(1);
}

printf("saving frame %3d\n", dec_ctx->frame_number);
fflush(stdout);

// 帧处理业务
}
}

与编码不同,解码的参数是由之前编码的决定的,所以这里直接把读取的流中拷贝编码器参数。

欢迎关注我的其它发布渠道