前言
这是ffmpeg的第二篇,这篇主要实现一下使用ffmpeg的API实现解码,不使用和上篇的解析器做解析。
流程图
代码流程即如流程图所示,下面讲解一下当中部分函数的作用)。
- avformat_open_input
打开输入文件,并读取文件头相关信息 - avformat_find_stream_info
读取媒体文件信息。 - av_find_best_stream
获取视频流序号(因为文件当中可能既有音频也有视频,字幕等流,我们这里使用这个函数获取视频流的序号)。 - avcodec_find_decoder
获取解码器 - avcodec_parameters_to_context
我们自己构建的解码器并没有设置一些解码相关的参数,此时我们拷贝视频流的参数到里面即可。 - av_read_frame
从视频当中读取数据(一帧),不用和上篇一样,还需要我们使用解析器解析成一帧。 - avcodec_send_packet
发送我们刚刚得到的解析数据到解码器做解码。 - avcodec_receive_frame
获取解码之后的数据。
源码
#pragma once
#define __STDC_CONSTANT_MACROS
#define _CRT_SECURE_NO_WARNINGS
extern "C"
{
#include <libavformat/avformat.h>
#include "libavcodec/avcodec.h"
}
#define INBUF_SIZE 4096
using namespace std;
#define INPUT_FILE_NAME "lh_online.h264"
#define OUTPUT_FILE_NAME "lh_online.yuv"
static void decode(AVCodecContext* dec_ctx, AVFrame* frame, AVPacket* pkt,
FILE* ofile)
{
int ret;
int y_size;
ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "发送数据包到解码器出错。\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) {
av_log(NULL, AV_LOG_ERROR, "Error sending a packet for decoding.\n");
exit(1);
}
//此时一帧视频已经保存到frame中了
//
//打印输出的视频帧的帧数
av_log(NULL, AV_LOG_INFO, "saving frame:%d , width: %d ,height: %d.\n", dec_ctx->frame_number, frame->width, frame->height);
//获取一帧视频数据大小
y_size = frame->width * frame->height;
fwrite(frame->data[0], 1, y_size, ofile); //Y
fwrite(frame->data[1], 1, y_size / 4, ofile); //U
fwrite(frame->data[2], 1, y_size / 4, ofile); //V
}
}
int main(int argc, char* argv[])
{
const AVCodec* codec;
AVFormatContext* fmt_ctx = NULL;
AVCodecContext* c = NULL;
AVStream* st;
AVFrame* frame;
AVPacket* pkt;
int ret;
int stream_index;
FILE* ofile;
//打开输入文件,并为fmt_ctx分配空间
if (avformat_open_input(&fmt_ctx, INPUT_FILE_NAME, NULL, NULL)) {
av_log(NULL, AV_LOG_ERROR, "Codec not open source file.\n");
exit(1);
}
//获取流信息
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not find stream information.\n");
exit(1);
}
//获取视频流序号(这里我们明确要解码的是视频,也只处理视频)
stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (stream_index < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot find stream index\n");
exit(1);
}
//获取文件流
st = fmt_ctx->streams[stream_index];
//获取解码器(这里不需要我们显示的指定了)
codec = avcodec_find_decoder(st->codecpar->codec_id);
if (!codec) {
av_log(NULL, AV_LOG_ERROR, "Codec not found.\n");
exit(1);
}
//分配解析器上下文
c = avcodec_alloc_context3(codec);
if (!c) {
av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context.\n");
exit(1);
}
//把输入流的编解码参数复制到我们的解码器上
if (avcodec_parameters_to_context(c, st->codecpar) < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to copy %s codec parameters to decoder context\n");
exit(1);
}
//打开解码器
if (avcodec_open2(c, codec, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open codec.\n");
exit(1);
}
//分配AVPacket
pkt = av_packet_alloc();
if (!pkt) {
exit(1);
}
//分配AVFrame
frame = av_frame_alloc();
if (!frame) {
exit(1);
}
//打开输出文件
ofile = fopen(OUTPUT_FILE_NAME, "wb+");
if (!ofile) {
av_log(NULL, AV_LOG_ERROR, "Could not open \s.\n", OUTPUT_FILE_NAME);
exit(1);
}
//从文件读取帧
while (av_read_frame(fmt_ctx, pkt) >= 0) {
//只处理视频流
if (pkt->stream_index==stream_index) {
decode(c, frame, pkt, ofile);
}
av_packet_unref(pkt);
}
//flush 解码器
decode(c, frame, NULL, ofile);
//资源释放
fclose(ofile);
avcodec_free_context(&c);
av_frame_free(&frame);
av_packet_free(&pkt);
return 0;
}
这个示例中和上篇文章一样,还是把一个H264编码的 lh_online.h264 的文件解码成原始YUV视频文件 lh_online.yuv。
播放还是使用ffplay来实现,下面是播放的代码。
ffplay -i D:\project\ffmpeg_demo\decode_video_by_api\lh_online.yuv -pix_fmt yuv420p -s 512*288
注意两点:
- 此时我们播放的数据格式是 yuv 的(此处可设置可不设置,ffplay默认可播放)。
- 我们需要知道视频的原始宽高,代码里有个位置获取了宽高的(dec_ctx->frame_number, frame->width, frame->height),我们需要把快其设置到命令里,否者是不能播放的(我这个文件是512*288的)。
下面看下效果。
到此,视频解码相关的API即介绍完了。
相对于上篇文章基于parser的方式来的话,这篇文章的相对来说代码逻辑是比较直观的,也不需要我们自己去考虑分割视频帧相关的问题(平常我们使用最多的应该也是这种方式)。
需要注意的就是我们需要 avformat.h这个文件头,这个文件封装的里面即是媒体文件格式相关的API。