本文将详细介绍如何使用ffmpeg 4.4在C++中解码多种格式的媒体文件,这些媒体文件可以是视频、视频流、图片,或是桌面截屏或USB摄像头的实时图片。解码文件后,还将每帧图片转换为OpenCV的Mat格式以供后续使用。
文章目录
- 1 基于ffmpeg的媒体文件解码
- 1.1 简介
- 1.2 详细代码
- 2 ffmpeg函数解释
- 2.1 解封装
- AVFormatContext
- AVInputFormat
- 2.2 解码
- AVStream
- AVCodecContext
- AVCodec
- AVCodecParameters
- 2.3 数据存储
- AVPacket
- AVFrame
- 2.4 功能结构
- AVRational
- SwsContext
- AVDictionary
- 其他函数
- 3 参考
- 3.1 参考文章
- 3.2 ffmpeg结构体
- 3.3 ffmpeg函数
1 基于ffmpeg的媒体文件解码
1.1 简介
在开始之前,需要先安装FFmpeg。对于Windows用户,可以参考FFmpeg + Visual studio 开发环境搭建;对于Linux用户,可以参考FFmpeg4.4编译。
本文主要参考了ffmpeg-libav-tutorial/0_hello_world.c提供的代码。值得注意的是,由于FFmpeg版本变化较大,本文所使用的FFmpeg接口和以往有所不同。如果想进一步学习FFmpeg代码的使用,可以阅读FFmpeg-libav-tutorial、ffmpeg-learning-indexes和视音频编解码技术零基础学习方法(由于作者雷霄骅不幸英年早逝,哀悼!该文主要基于旧ffmpeg版本,但是仍然有很好的学习价值)。
涉及的步骤如下图所示:
解封装
在音视频处理过程中,解封装是指将输入的音视频文件进行解析,提取出音频流和视频流等多种流媒体数据,以便后续的数据处理和解码。在解封装过程中,首先需要判断输入源的格式,即判断输入的音视频文件是属于哪种格式。然后打开文件,查找流信息和视频索引。
解码
解码是指将音视频数据进行解码,将压缩后的数据转换成原始的音视频数据,以便后续的数据处理和播放。在解码过程中,需要初始化解码器,并打开解码器。本文只解码视频,音频则不进行处理。
取数据
在取数据过程中,需要初始化数据结构,读取视频帧,并将视频帧发送给解码器。随后,从解码器获取解码结果。
数据处理
数据处理是指对音视频数据进行各种处理,比如色彩空间转换、图像尺寸变换、图像格式转换等。
释放资源
在完成解码和数据处理后,需要释放结构体,以释放资源。释放资源是指对音视频处理过程中占用的各种资源进行释放,包括解码器、数据结构、缓冲区等。
1.2 详细代码
详细代码如下:
/**
* @brief 代码主要参考https://github.com/leandromoreira/ffmpeg-libav-tutorial/blob/master/0_hello_world.c
*
*/
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavfilter/avfilter.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libavutil/ffversion.h"
#include "libavutil/opt.h"
#include "libavutil/imgutils.h"
#include "libavutil/time.h"
#include "libswresample/swresample.h"
#include "libswscale/swscale.h"
#include "libavdevice/avdevice.h"
}
#include <stdio.h>
#include <stdlib.h>
#include <chrono>
#include <iostream>
#include <opencv2/opencv.hpp>
#include <stdarg.h>
#include <string.h>
#include <inttypes.h>
// 日志打印宏
#define LOG(msg, ...)\
fprintf(stderr,"LOG [line %d] ",__LINE__);\
fprintf(stderr,msg, ##__VA_ARGS__);\
fprintf(stderr, "\n");
// 支持的输入文件形式
enum URLType { file, usbcam, desktop, yuvfile };
// usb摄像头读取函数
void usbcam_get(const char * url, AVInputFormat ** ifmt);
// 解码函数
static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame, int skip = 0);
/**
* 涉及到结构体
* AVFormatContext 存储媒体文件所有信息的结构体
* AVInputFormat 存储媒体文件的格式信息
* AVStream 表示音视频流信息的结构体
* AVCodecContext 存储解码音视频所有信息的结构体
* AVCodec 存储视频或音频的编解码器的结构体
* AVCodecParameters 存储音视频编解码器的相关参数信息的结构体
* AVPacket 储存解码前数据的结构体
* AVFrame 存储解码后数据的结构体
* AVRational 表示有理数的结构体
* SwsContext 用于图像转换的结构体
*/
int main()
{
// 设置数据类型
URLType urltype = yuvfile;
// 初始化结构体
const char *url = NULL;
AVFormatContext *pFormatContext = NULL;
AVInputFormat *ifmt = NULL;
AVDictionary *options = NULL;
// AVCodec负责编解码音视频流
AVCodecContext *pCodecContext = NULL;
AVCodec *pCodec = NULL;
AVCodecParameters *pCodecParameters = NULL;
// 负责保存数据
AVPacket *pPacket = NULL;
AVFrame *pFrame = NULL;
LOG("FFMPEG VERSION: %s", av_version_info());
LOG("开始运行");
// 存储音视频封装格式中包含的信息
// avformat_alloc_context初始化AVFormatContext结构体
pFormatContext = avformat_alloc_context();
if (!pFormatContext)
{
LOG("pFormatContext分配内存失败");
return -1;
}
// 注册能操作的输入输出设备
avdevice_register_all();
if (urltype == file)
{
// rtsp流
//url = "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4";
// 输入图片
// url = "demo.png";
// 输入视频
url = "demo.mp4";
// 设置超时时间为5秒
av_dict_set(&options, "stimeout", "5000000", 0);
}
else if (urltype == usbcam)
{
url = "0";
// 如果使用以下方式读取本机摄像头,需要自行获得摄像头名称
// 使用指令:ffmpeg -list_devices true -f dshow -i dummy
//url = "video=HD WebCam";
// 输出ffmpeg版本
usbcam_get(url, &ifmt);
// 设置图片尺寸
av_dict_set(&options, "video_size", "640x480", 0);
av_dict_set(&options, "framerate", "30", 0);
}
else if (urltype == desktop)
{
// Windows
#ifdef _WIN32
// 根据不同的url选择不同的格式
url = "desktop";
ifmt = av_find_input_format("gdigrab");
// linux处理
#elif defined linux
// linux命令行输入echo $DISPALY获得
url = ":1";
ifmt = av_find_input_format("x11grab");
#endif
av_dict_set(&options, "video_size", "1920x1080", 0);
av_dict_set(&options, "framerate", "15", 0);
}
else if (urltype == yuvfile)
{
url = "akiyo_cif.yuv";
// yuv图像尺寸需要提前设置
av_dict_set(&options, "video_size", "352x288", 0);
}
// avformat_open_input打开输入的媒体文件
if (avformat_open_input(&pFormatContext, url, ifmt, &options) != 0)
{
LOG("打开文件失败");
return -1;
}
LOG("打开文件 %s", url);
// 读取文件音视频编解码器的信息
LOG("文件格式 %s, 文件时长 %lld us, 比特率 %lld bit/s",
pFormatContext->iformat->name,
pFormatContext->duration,
pFormatContext->bit_rate);
LOG("获取输入音视频文件的流信息");
// avformat_find_stream_info获取输入音视频文件的流信息
if (avformat_find_stream_info(pFormatContext, NULL) < 0)
{
LOG("无法获取流信息");
return -1;
}
// 设置是否读取到视频流
int video_stream_index = -1;
// 循环浏览所有流并打印其主要信息
for (int i = 0; i < int(pFormatContext->nb_streams); i++)
{
AVCodecParameters *pLocalCodecParameters = NULL;
// 提取当前流的编解码器参数
pLocalCodecParameters = pFormatContext->streams[i]->codecpar;
AVCodec *pLocalCodec = NULL;
// 查找指定编解码器的解码器
pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);
if (pLocalCodec == NULL)
{
LOG("不支持该解码器!");
continue;
}
// 当流是视频时,我们存储其索引、解码器和编解码器参数
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO)
{
if (video_stream_index == -1)
{
video_stream_index = i;
pCodec = pLocalCodec;
pCodecParameters = pLocalCodecParameters;
}
LOG("视频编解码器类型: %s ID: %d", pLocalCodec->name, pLocalCodec->id);
LOG("视频流帧率为:%f", av_q2d(pFormatContext->streams[i]->r_frame_rate));
LOG("视频流共有:%d帧", pFormatContext->streams[i]->nb_frames);
LOG("视频图像分辨率为:(%d,%d)", pLocalCodecParameters->width, pLocalCodecParameters->height);
}
else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO)
{
LOG("音频编解码器类型: %s ID: %d", pLocalCodec->name, pLocalCodec->id);
LOG("音频通道数:%d channels, 采样率:%d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
}
if (video_stream_index == -1)
{
LOG("%s文件不包含视频流!", url);
return -1;
}
// 分配AVCodecContext结构体并进行初始化
pCodecContext = avcodec_alloc_context3(pCodec);
if (!pCodecContext)
{
LOG("AVCodecContext初始失败");
return -1;
}
// 将AVCodecParameters中的参数设置到AVCodecContext中
if (avcodec_parameters_to_context(pCodecContext, pCodecParameters) < 0)
{
LOG("AVCodecParameters参数拷贝失败");
return -1;
}
// 打开解码器
if (avcodec_open2(pCodecContext, pCodec, NULL) < 0)
{
LOG("打开解码器失败");
return -1;
}
// 创建AVPacket
pPacket = av_packet_alloc();
if (!pPacket)
{
LOG("AVPacket初始化失败");
return -1;
}
// 创建AVFrame
pFrame = av_frame_alloc();
if (!pFrame)
{
LOG("AVFrame初始化失败");
return -1;
}
int response = 0;
// 最多读取帧数
int how_many_packets_to_process = 500;
// 帧处理跨度
int skip_span = 50;
// 读取媒体文件中的音视频帧
while (av_read_frame(pFormatContext, pPacket) >= 0)
{
// 判断是否为视频帧
if (pPacket->stream_index == video_stream_index)
{
// 只解码关键帧,关键帧不依赖于其他帧进行解码,所以可以跳过其他帧
// 关键帧间隔由媒体流数据源决定
// if (!(pPacket->flags & AV_PKT_FLAG_KEY)) {
// continue;
//}
int skip = 1;
// 如果已读取帧数除以skip_span为0,则下一帧进行处理
if (pCodecContext->frame_number % skip_span == 0)
{
skip = 0;
}
// 计算时间
auto start = std::chrono::system_clock::now();
// 图像解码函数
response = decode_packet(pPacket, pCodecContext, pFrame, skip);
auto end = std::chrono::system_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
if (skip == 0)
{
LOG("解码和处理一帧图像耗时:%d ms", duration);
}
else
{
LOG("仅解码一帧图像耗时:%d ms", duration);
}
// 图像解码状态判定
if (response < 0)
break;
// 超过读取图像上限
if (--how_many_packets_to_process <= 0)
{
LOG("读图完毕!");
break;
}
}
// 释放AVPacket结构体中的内存
av_packet_unref(pPacket);
}
LOG("销毁所有结构体");
// 销毁结构体
avformat_close_input(&pFormatContext);
av_packet_free(&pPacket);
av_frame_free(&pFrame);
avcodec_free_context(&pCodecContext);
av_dict_free(&options);
system("pause");
return 0;
}
void usbcam_get(const char * url, AVInputFormat ** ifmt)
{
// Windows
#ifdef _WIN32
// 根据不同的url选择不同的格式
if (url == "0")
*ifmt = av_find_input_format("vfwcap");
else
*ifmt = av_find_input_format("dshow");
// linux
#elif defined linux
url = "/dev/video0";
*ifmt = av_find_input_format("video4linux2");
#endif
}
static int decode_packet(AVPacket *pPacket, AVCodecContext *pCodecContext, AVFrame *pFrame, int skip)
{
// 将pPacket数据送入pCodecContext进行解码
int response = avcodec_send_packet(pCodecContext, pPacket);
if (response < 0)
{
return response;
}
while (response >= 0)
{
// 用于从解码器中获取解码后的视频帧
response = avcodec_receive_frame(pCodecContext, pFrame);
if (response == AVERROR(EAGAIN) || response == AVERROR_EOF)
{
break;
}
else if (response < 0)
{
LOG("读图出错: %d", response);
return response;
}
// 仅读取当前帧
if (skip != 0)
{
return 0;
}
if (response >= 0)
{
LOG("Frame %d,帧类型=%c,视频格式=%d,pts=%d,是否为关键帧=%d",
pCodecContext->frame_number,
av_get_picture_type_char(pFrame->pict_type),
pFrame->format,
pFrame->pts,
pFrame->key_frame);
// 图像保存名
char frame_filename[1024];
snprintf(frame_filename, sizeof(frame_filename), "%s-%d.jpg", "frame", pCodecContext->frame_number);
// 将解码后的帧转换为BGR格式
// 创建图像转换器,设置图像尺寸缩小一倍
int dst_w = int(pCodecContext->width / 2);
int dst_h = int(pCodecContext->height / 2);
SwsContext *swsCtx = sws_getContext(
pCodecContext->width, pCodecContext->height, (AVPixelFormat)pCodecContext->pix_fmt,
dst_w, dst_h, AV_PIX_FMT_BGR24,
SWS_POINT, NULL, NULL, NULL);
cv::Mat bgrMat(dst_h, dst_w, CV_8UC3);
// 拿出opencv的数据
uint8_t *dest[1] = { bgrMat.data };
int destStride[1] = { int(bgrMat.step) };
// 执行格式转换
sws_scale(swsCtx, pFrame->data, pFrame->linesize, 0, pFrame->height, dest, destStride);
// 保存图片
cv::imwrite(frame_filename, bgrMat);
// 释放swsCtx数据
sws_freeContext(swsCtx);
}
}
return 0;
}
以上代码参考下图阅读最好。图片来自ffmpeg-libav-tutorial/decoding.png。
如果是linux下使用该代码文件还需编写CMakeLists.txt,CMakeLists.txt内容如下:
# 最低cmake版本
cmake_minimum_required(VERSION 3.2)
# 工程名
project(ffmpeg_demo)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR})
# --- opencv
find_package(OpenCV REQUIRED)
# --- ffmpeg
set(FFMPEG_INCLUDE_DIRS "/usr/local/include/")
set(FFMPEG_LIB_DIRS "/usr/local/lib/")
set(FFMPEG_LIBS "avcodec;avformat;avutil;swresample;avdevice;swscale")
include_directories(${FFMPEG_INCLUDE_DIRS})
link_directories(${FFMPEG_LIB_DIRS})
# 生成可执行文件
add_executable(ffmpeg_demo demo.cpp)
target_link_libraries(ffmpeg_demo ${FFMPEG_LIBS} ${OpenCV_LIBS} pthread)
2 ffmpeg函数解释
2.1 解封装
解封装的作用是从输入的封装格式数据(例如MP4、AVI、MKV)中提取视频流压缩编码数据和音频流压缩编码数据。封装格式的作用是将已经压缩编码的视频数据和音频数据按照一定的格式放在一起。例如,将MP4封装格式的数据输出H.264编码格式的视频流和AAC格式的音频流。一般解封装的流程如下:
- 在使用FFmpeg解码音视频文件时,需要通过AVFormatContext来获取文件信息和流信息。AVFormatContext中包含AVInputFormat结构体指针,指向当前媒体文件的输入格式。
- AVInputFormat结构体描述了媒体文件的封装格式,如MP4、AVI、MKV等。
- avformat_alloc_context用于创建并初始化AVFormatContext结构体,为后续的音视频文件解码或编码做好准备。
- avformat_open_input用于打开音视频文件并读取文件信息到AVFormatContext结构体中。
- avformat_find_stream_info用于获取音视频流信息并存储到AVFormatContext结构体中。
- avformat_close_input用于关闭音视频文件并释放AVFormatContext结构体占用的内存空间。
AVFormatContext
AVFormatContext是一个存储流媒体相关信息的上下文结构体(统领相关操作全局的结构体)。几乎所有的音视频操作都需要先创建一个AVFormatContext对象。AVFormatContext使用完毕需要手动释放内存。AVFormatContext的主要属性及使用说明:
AVInputFormat *iformat
:输入格式结构体指针,用于指定输入文件的格式,一般由FFmpeg自动探测获取。AVOutputFormat *oformat
:输出封装格式的结构体指针。AVIOContext *pb
:输入输出的AVIOContext结构体指针。unsigned int nb_streams
:音视频流个数。int64_t duration
:音视频文件的时长,单位为微秒(μs),一般由FFmpeg解析后赋值。int64_t bit_rate
:音视频文件的码率,单位为bit/s,一般由FFmpeg解析后赋值。AVStream **streams
:音视频流列表的指针数组。AVDictionary *metadata
:元数据信息,例如标题、作者、描述等等。
该结构体涉及以下函数:
avformat_alloc_context
avformat_alloc_context用于分配AVFormatContext结构体并初始化。该函数返回一个指向AVFormatContext结构体的指针,如果分配失败则返回NULL。
AVFormatContext *avformat_alloc_context(void);
avformat_open_input
avformat_open_input用于打开输入的媒体文件,将音视频文件的元数据信息读取到AVFormatContext结构体中。函数返回0表示成功打开文件。
int avformat_open_input(AVFormatContext ** ps,
const char * url,
const AVInputFormat * fmt,
AVDictionary ** options )
**ps
:指向AVFormatContext结构体指针的指针,用于存放打开的媒体文件的相关信息。*url
:输入媒体文件的URL地址。可以是本地文件路径或者网络地址。*fmt
:输入媒体文件的格式,如果为NULL,则根据URL自动探测输入媒体的格式。**options
:输入媒体文件的选项参数。
avformat_find_stream_info
avformat_find_stream_info用于获取输入文件的流信息。它会读取输入文件的所有数据包,并尝试从中获取流的参数,如流的编解码器、帧率、分辨率等等。在调用avformat_find_stream_info之后,可以通过 AVFormatContext结构体中的streams字段访问到每个流的详细信息。函数返回值大于等于0表示成功。
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
ic
:指向AVFormatContext
结构体的指针。options
:指向AVDictionary
结构体指针的指针,用于传递选项给解复用器。
avformat_close_input
avformat_close_input用于关闭AVFormatContext文件并释放相关资源。一般情况下avformat_close_input和avformat_open_input成对使用,该函数也在内部会调用avformat_free_context函数释放AVFormatContext结构体。
void avformat_close_input(AVFormatContext **ps);
ps
:指向AVFormatContext结构体指针的指针,该指针会在函数执行完毕后被置为NULL。
avformat_free_context
avformat_free_context用于释放AVFormatContext结构体。
void avformat_free_context(AVFormatContext * s)
ps
:指向AVFormatContext结构体的指针。
AVInputFormat
AVInputFormat用于表示输入的媒体文件的格式。主要作用为通过解析输入的媒体文件,并将其转换成FFmpeg内部所使用的数据结构。AVInputFormat结构体的内存由FFmpeg库自动分配和释放,在调用avformat_close_input函数后,FFmpeg库将自动释放AVInputFormat结构体。AVInputFormat的主要属性及使用说明:
const char * name
:输入文件类型的名称。const char * long_name
:输入文件类型的详细描述。const char * extensions
:输入文件类型的扩展名列表。
2.2 解码
解码的作用是将视频或音频压缩编码数据转换成为非压缩的视频或音频原始数据。例如将H.264的视频压缩数据解码为逐帧YUV图像数据。一般解码的流程如下:
- 每个AVStream结构都存储一个视频/音频流的相关数据,例如流的编号、流的类型、流的码率等等。AVStream结构中还包含一个指向对应AVCodecContext结构的指针,该结构用于存储该视频/音频流解码方式的所有信息,如编码器的名称、编码器的属性、编码器的状态等等。
- AVCodecContext结构中又包含一个指向对应AVCodec结构的指针,AVCodec结构包含该视频/音频对应的解码器的基本信息,如编码器的名称、编码器的类型、编码器的能力等。当需要使用某个编解码器时,需要先通过编解码器的名称来查找对应的AVCodec结构体,然后再将这个结构体中的信息赋值给AVCodecContext结构体中的相应字段。
- AVCodecParameters结构体是一个描述编解码器参数结构体,它包含了一个编解码器的参数信息,如编码器的宽度、编码器的高度、编码器的码率等等。对一个视频或音频流进行编解码时,需要使用AVCodecParameters结构体来描述这个流的参数信息,然后再将这个结构体中的信息赋值给AVCodecContext结构体中的相应字段。
AVStream
AVStream是FFmpeg中表示音视频流的结构体,每个AVStream结构体都对应一个视频或音频流的相关数据。AVStream结构体的内存由FFmpeg库自动分配和释放。AVStream的主要属性及使用说明:
AVCodecParameters *codecpar
:指向AVCodecParameters结构体的指针,存储了该流的编解码器参数。AVRational ime_base
:时间基准,表示每个采样的持续时间,以分数形式表示。int64_t start_time
:流的开始时间,以时间戳的形式表示。int64_t duration
:流的持续时间,以时间戳的形式表示。int64_t nb_frames
:该流中的帧数。AVRational r_frame_rate
:用于表示实际帧率的AVRational结构体。AVRational avg_frame_rate
: 用于表示平均帧率的AVRational结构体。
AVCodecContext
AVCodecContext包含解码音视频所有信息的上下文结构体。在进行音视频编解码时,通过对AVCodecContext的相关参数进行设置,来控制编解码器的行为。AVCodecContext使用完毕需要手动释放内存。AVCodecContext的一些常用参数包括:
enum AVCodecID codec_id
:指定音视频编解码器的ID。AVRational time_base
:音视频帧的时间基准,用于计算时间戳等。int64_t bit_rate
:音视频的比特率,影响编码后的文件大小和质量。int width
和int height
:视频的宽高。int sample_rate
和int channels
:音频的采样率和声道数。enum AVPixelFormat pix_fmt
:视频的像素格式,如YUV420P、RGB24等。attribute_deprecated int frame_number
:获得已处理帧数,但是该属性已经被废弃,因为如果编码/解码导致错误,则计数器不递增。
该结构体涉及以下函数:
avcodec_alloc_context3
avcodec_alloc_context3函数用于分配AVCodecContext结构体并进行初始化。该函数返回一个指向AVCodecContext结构体的指针,如果分配失败则返回NULL。
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
codec
:指向AVCodec结构体的指针,表示要使用的解码器。
avcodec_parameters_to_context
avcodec_parameters_to_context函数的作用是将AVCodecParameters中的参数设置到AVCodecContext中。返回值小于0表示设置失败。
int avcodec_parameters_to_context(AVCodecContext *codec, const AVCodecParameters *par);
AVCodecContext *codec
:需要设置参数的AVCodecContext结构体指针。const AVCodecParameters *par
:需要从中获取参数的AVCodecParameters结构体指针。
avcodec_open2
avcodec_open2用于打开AVCodec并初始化AVCodecContext。返回0表示成功,否则表示失败。
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
avctx
:指向一个已经分配好内存的AVCodecContext
结构体。codec
:指向一个已经注册好的编码器或解码器的AVCodec
结构体。options
:指向一个AVDictionary
类型的指针,用于传递打开编码器或解码器时的参数,可以为NULL。
avcodec_send_packet
avcodec_send_packet函数用于将一个未解码的AVPacket数据送入解码器AVCodecContext进行解码。该函数执行成功后,解码器AVCodecContext内部的缓存将会被填充上相应的数据,可以通过调用avcodec_receive_frame函数来获取解码结果。返回值为0表示成功,否则表示失败。
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
avctx
:已经被打开的编解码器。avpkt
:待解码的AVPacket。
avcodec_receive_frame
avcodec_receive_frame用于从解码器中获取解码后的视频帧。avcodec_receive_frame一般会外嵌while循环,可以保证在没有接收到可用帧之前不会退出循环,从而避免数据包丢失或者解码错误的情况发生。
int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
avctx
:解码器对象。frame
:存放解码后的视频帧的AVFrame对象。
返回值表示获取到的视频帧的状态,具体取值如下:
0
:成功获取到一帧视频帧。AVERROR(EAGAIN)
:缓冲区中没有可用的视频帧,需要再次调用该函数。AVERROR_EOF
:解码器中的所有视频帧都已经读取完成。
avcodec_free_context
avcodec_free_context函数用于释放AVCodecContext结构体所占用的内存。
void avcodec_free_context(AVCodecContext **avctx);
avctx
: 指向需要释放的AVCodecContext结构体指针。`
AVCodec
AVCodec用于表示视频或音频的编解码器。AVCodec数据结构的内存由FFmpeg库自动分配和释放。AVCodec的主要属性及使用说明:
const char * name
:编解码器的名称。const char * long_name
:编解码器的详细描述。enum AVMediaType type
:表示编解码器的类型,可以是视频、音频或其他类型。enum AVCodecID id
:表示编解码器的ID。int capabilities
:表示编解码器的功能特性,例如是否支持多线程等。const AVRational * supported_framerates
:表示编解码器支持的帧率列表。enum AVPixelFormat * pix_fmts
:表示编解码器支持的像素格式列表。const int * supported_samplerates
:表示编解码器支持的采样率列表。enum AVSampleFormat * sample_fmts
:表示编解码器支持的采样格式列表。
该结构体涉及以下函数:
avcodec_find_decoder
avcodec_find_decoder用于通过codec_id查找指定已经注册的解码器。 如果找到了指定的解码器,返回指向该解码器的AVCodec指针。如果未找到指定的解码器,返回 NULL。
AVCodec *avcodec_find_decoder(enum AVCodecID id);
id
:要查找的解码器的AVCodecID格式codec_id,AVCodecID 是一个枚举类型表示不同的编解码器。。
AVCodecParameters
AVCodecParameters主要用于存储音视频编解码器的相关参数信息。AVCodecParameters数据结构的内存由FFmpeg库自动分配和释放。AVCodecParameters常用属性介绍:
enum AVMediaType codec_type
:音视频流类型。enum AVCodecID codec_id
:指定解码器的ID,如AV_CODEC_ID_H264表示使用H.264解码器。int64_t bit_rate
:指定音视频的比特率,单位为bps。int width
和int height
:指定视频的宽度和高度。int channels
:表示声道数int sample_rate
:指定音频采样率,单位为Hz。uint8_t * extradata/int extradata_size
:指定音视频流的附加数据和附加数据的大小。
2.3 数据存储
AVPacket用于保存解码前的数据,AVFrame则用于保存解码后的数据。在解码器中,AVPacket中的数据会被解码成AVFrame。在编码器中,AVFrame中的数据会被编码成AVPacket。
AVPacket
AVPacket是用于存储压缩音频或视频数据的结构体。它包含了一段压缩后的数据和对应的时间戳信息,以及一些其他的附加信息,如数据流索引、关键帧标识等。在解码过程中,AVPacket会被送到解码器中进行解码,得到AVFrame。AVPacket使用完毕需要手动释放内存。AVPacket的主要属性如下:
uint8_t* data
:指向音视频数据帧的指针。int size
:音视频数据帧的大小。int64_t pts
:音视频数据帧的显示时间。int64_t dts
:音视频数据帧的解码时间。int stream_index
:音视频数据帧所属的流的索引。int flags
:用于描述AVPacket的一些特性。常见选项如下:- AV_PKT_FLAG_KEY:表示该AVPacket所包含的数据是一个关键帧。
- AV_PKT_FLAG_CORRUPT:表示该AVPacket所包含的数据可能已经损坏。当解码器无法正确解码一个AVPacket时,就会设置该标志位,通知应用程序此AVPacket已经损坏。
- AV_PKT_FLAG_DISCARD:表示该AVPacket所包含的数据可以被丢弃。当解码器对于某些时刻无法解码出正确的图像时,就会设置该标志位。可以选择丢弃该AVPacket,以保证视频的流畅性。
- AV_PKT_FLAG_TRUSTED:表示该AVPacket所包含的数据是可信的。当解码器在解码AVPacket时,会校验AVPacket的CRC校验码,如果校验码正确,则会设置该标志位。这个标志位通常用于保证视频的完整性,以防止篡改或者损坏。
该结构体涉及以下函数:
av_packet_alloc
av_packet_alloc用于创建AVPacket结构体并为其分配内存空间。函数返回一个指向新分配的AVPacket结构体的指针。如果分配失败,则返回NULL。
AVPacket *av_packet_alloc(void);
av_packet_unref
av_packet_unref函数用于清除AVPacket结构体中的数据,但是并不会释放这个结构体本身,以便可以重新使用或销毁AVPacket结构体。
void av_packet_unref(AVPacket *pkt);
pkt
:AVPacket结构体指针。
av_packet_free
av_packet_free用于释放AVPacket结构体所占用内存。
void av_packet_free(AVPacket **pkt);
pkt
:指向AVPacket结构体指针的指针。
AVFrame
AVFrame是用于存储解码后的数据的结构体。它包含了一帧图像或音频解码后的数据,以及一些相关的信息,如宽度、高度、像素格式等。在解码过程中,AVFrame是解码器输出的数据,它可以被送到渲染器中进行渲染,也可以被编码器编码成新的AVPacket。AVFrame使用完毕需要手动释放内存。AVFrame结构体中常用的属性介绍和说明:
uint8_t * data
:指向一个指针数组,其中包含了这一帧的所有数据。对于视频帧,通常包含了YUV或RGB数据;对于音频帧,通常包含了PCM数据。具体的数据格式和分布,可以通过其他参数进行描述。int linesize
:指向一个整型数组,用于描述每个数据平面的行大小(即每一行占用的字节数)。对于视频帧,通常会有三个数据平面(分别对应Y、U、V或R、G、B三个分量);对于音频帧,通常只有一个数据平面。linesize数组的大小应该与data数组的大小相同。uint8_t ** extended_data
:指向一个指针数组,其中包含了所有数据平面的指针。对于一些特殊的数据格式,data数组可能无法直接描述所有数据平面。这时,extended_data可以用于补充缺失的数据平面。int width
和int height
:分别表示这一帧的宽度和高度。对于音频帧,这两个参数均为0。int format
:表示这一帧的数据格式。对于视频帧,常用的格式有YUV420、YUV422、YUV444、RGB24等;对于音频帧,常用的格式有PCM_S16LE、PCM_S16BE、PCM_F32LE等。int64_t pts
:表示这一帧在整个多媒体流中的时间戳(Presentation Time Stamp)。它通常以视频帧率或音频采样率为单位,用于确定这一帧的播放时间。int64_t pkt_pts
和int64_t pkt_dts
:分别表示这一帧所属的AVPacket中的时间戳和解码时间戳(Decode Time Stamp)。它们与pts的含义类似,但是它们是从AVPacket中直接获取的,可能会存在一些偏差或不准确的情况。int sample_rate
和int channel_layout
:仅用于音频帧,分别表示采样率和声道布局。其中,channel_layout可以用于指定声道数和声道位置的具体信息。enum AVPictureType pict_type
:表示帧的图像类型是I帧、P帧、B帧还是S帧。关键帧(I帧)是一种特殊的帧,它包含完整的图像信息,不依赖于前面或后面的帧。P帧(预测帧)和B帧(双向预测帧)则只包含部分图像信息,需要参考前面或后面的帧才能正确解码。通过使用关键帧,可以提高视频的压缩比以及解码效率。S帧是跳帧,它直接复制前一帧的图像,用于视频压缩。int key_frame
:表示当前帧是否为关键帧。
该结构体涉及以下函数:
av_frame_alloc
av_frame_alloc用于创建AVFrame结构体并为其分配内存空间。函数返回一个指向新分配的AVFrame结构体的指针。如果分配失败,则返回NULL。
AVFrame *av_frame_alloc(void);
av_read_frame
av_read_frame用于从AVFormatContext中读取媒体文件中的音频或视频帧,并将数据存储到AVPacket中。返回值为0表示读取成功,为负数表示读取失败。
int av_read_frame(AVFormatContext *s, AVPacket *pkt);
s
: 指向表示媒体文件AVFormatContext结构体的指针。pkt
: 指向AVPacket结构体的指针,用于存储读取到的音视频帧的数据。
av_frame_free
av_frame_free函数用于释放AVFrame结构体占用的内存空间。
void av_frame_free(AVFrame **frame);
frame
:需要释放的AVFrame结构体指针的地址。
2.4 功能结构
AVRational
AVRational结构体是FFmpeg中表示有理数的结构体,用于表示时间戳、帧率、采样率等一些基本的时间和频率相关的属性。AVRational结构体包含两个整型成员,num和den,分别表示分子和分母。用AVRational结构体表示的有理数值为num/den。AVRational结构体中的成员变量由FFmpeg内部自动分配和释放。AVRational属性如下:
int num
: 有理数值的分子int den
: 有理数值的分母
该结构体涉及以下函数:
av_q2d
av_q2d是一个用于将AVRational转换为double类型的函数,也就是将AVRational中的分子和分母相除。
double av_q2d(AVRational a);
a
:需要转换的AVRational类型的数值。
SwsContext
SwsContext是FFmpeg中用于图像转换的数据结构,它包含了图像转换所需的所有参数。要注意的是该结构体的原始定义在swscale_internal.h
文件中,普通编译的ffmpeg工程没有该文件。所以该结构体一般仅仅是使用。SwsContext使用完毕需要手动释放内存。SwsContext的主要参数如下:
int srcW
和int srcH
:源图像的宽度和高度。int dstW
和int dstH
:目标图像的宽度和高度。enum AVPixelFormat srcFormat
和enum AVPixelFormat dstFormat
:源图像和目标图像的像素格式。int flags
:图像转换时的一些特殊选项,如是否进行区间缩放等。double param
:一些额外的参数,如亮度、对比度等。
在使用FFmpeg解码时,默认解码后图像的颜色格式为YUV420p,关于YUV420p介绍见YUV图像处理入门1。
该结构体涉及以下函数:
sws_getContext
sws_getContext的作用是创建一个用于图像转换的SwsContext结构体。如果创建成功,返回一个指向SwsContext结构体的指针,否则返回NULL。函数原型如下:
struct SwsContext *sws_getContext(
int srcW,
int srcH,
enum AVPixelFormat srcFormat,
int dstW,
int dstH,
enum AVPixelFormat dstFormat,
int flags,
SwsFilter *srcFilter,
SwsFilter *dstFilter,
const double *param
);
srcW
:输入图像宽度。srcH
:输入图像高度。srcFormat
:输入图像像素格式。dstW
:输出图像宽度。dstH
:输出图像高度。dstFormat
:输出图像像素格式。flags
:转换标志,用于指定转换算法和参数。常用设置如下:- SWS_FAST_BILINEAR:较快的双线性转换,适用于实时应用,但可能会有些失真。
- SWS_BILINEAR:双线性转换,速度较快,但输出质量较低。
- SWS_BICUBIC:双三次转换,速度较慢,但输出质量较高。
- SWS_X:可自定义的转换算法,速度和质量取决于具体实现。
- SWS_POINT:转换的速度非常快的最近邻插值算法,但是转换后的图像质量相对其他方法低。因为SWS_POINT将目标像素点映射到图像时,直接使用最近的像素点来进行映射,会导致转换后的图像出现锯齿状的边缘,而且图像的细节信息也会丢失。
srcFilter
:输入图像过滤器,用于图像缩放和裁剪。dstFilter
:输出图像过滤器,用于图像缩放和裁剪。param
:转换参数,用于指定转换算法的参数。
sws_scale
sws_scale用于执行多种不同的像素格式转换。sws_scale函数的返回值为输出图像的高度,返回值小于等于0表示转换失败。
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
c
:可以通过sws_getContext函数获取。srcSlice[]
:输入图像数据指针数组。srcStride[]
:输入图像每行的字节数组。srcSliceY
:输入图像的起始行。srcSliceH
:输入图像的高度。dst[]
:输出图像数据指针数组。dstStride[]
:输出图像每行的字节数组。
sws_freeContext
sws_freeContext的作用是释放SwsContext结构体占用的内存空间,避免内存泄露。
void sws_freeContext(struct SwsContext *context);
context
:要释放的SwsContext结构体指针。
AVDictionary
AVDictionary是FFmpeg中的一个字典结构体,用于存储键值对数据。AVDictionary使用后一般不需要手动释放内存,但是建议手动释放内存。以下是AVDictionary属性的说明:
int count
:AVDictionary中键值对的数量。AVDictionaryEntry * elems
:指向AVDictionaryEntry结构体数组的指针,每个元素包含一个键值对。
该结构体涉及以下函数:
av_dict_set
av_dict_set用于向字典中添加或修改键值对。该函数的返回值为0表示成功,否则表示失败。av_dict_set可以设置的有效键值对需要参阅。
int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags)
pm
:指向字典指针的指针。key
:要添加或修改的键名。value
:要添加或修改的键值。flags
:标志位,控制键名是否可以覆盖已存在的键名。
av_dict_free
av_dict_free用于释放字典(dictionary)结构体占用的内存空间。
void av_dict_free(AVDictionary **m)
m
:指向AVDictionary指针的指针。
其他函数
avdevice_register_all
avdevice_register_all用于注册所有可用的音视频输入/输出设备,以方便进行数据采集。
void avdevice_register_all(void);
av_find_input_format
av_find_input_format用于查找输入视频流格式。该函数返回值是一个指向AVInputFormat结构体的指针。
AVInputFormat *av_find_input_format(const char *short_name);
short_name
是待查找的输入流格式的短名称。短名称是该格式的简称,例如:- mp4:表示MP4格式。
- vfwcap:是一个视频捕获设备的输入格式,用于Windows平台。它使用VFW(Video for Windows)API来捕获视频数据。
- dshow:是一个视频捕获设备的输入格式,用于Windows平台。它使用DirectShow API来捕获视频数据。
- video4linux2:是一个视频捕获设备的输入格式,用于Linux平台。它使用Video4Linux2 API来捕获视频数据。
- gdigrab:用于在Windows上捕获屏幕的输入格式。
- x11grab:用于在Linux上捕获屏幕的输入格式。
3 参考
3.1 参考文章
- FFmpeg + Visual studio 开发环境搭建
- ffmpeg4.4编译
- ffmpeg-libav-tutorial/0_hello_world.c
- ffmpeg-libav-tutorial
- ffmpeg-learning-indexes
- 视音频编解码技术零基础学习方法
- ffmpeg-libav-tutorial/decoding.png
- YUV图像处理入门1
3.2 ffmpeg结构体
- AVFormatContext
- AVInputFormat
- AVStream
- AVCodecContext
- AVCodec
- AVCodecParameters
- AVPacket
- AVFrame
- AVRational
- SwsContext
- AVDictionary
3.3 ffmpeg函数
- avformat_alloc_context
- avformat_open_input
- avformat_find_stream_info
- avformat_close_input
- avformat_free_context
- avcodec_find_decoder
- avcodec_parameters_to_context
- avcodec_open2
- avcodec_send_packet
- avcodec_receive_frame
- avcodec_free_context
- av_packet_alloc
- av_packet_unref
- av_packet_free
- av_frame_alloc
- av_read_frame
- av_frame_free
- av_q2d
- sws_getContext
- sws_scale
- sws_freeContext
- av_dict_set
- av_dict_free
- avdevice_register_all
- av_find_input_format