Qt+FFmpeg视频播放器开发(四)
简述:上文叙述了ffmpeg的一些核心类的概念及功能,今天说一下解析视频流的流程,以及ffmpeg解析出一帧图像后,要呈现到Qt需要进行怎么样的转换。最后叙述理论知识,让大家可以进一步理解这些代码背后存在哪些知识点。
一、视频流解析
1.1 视频流基本信息解析
代码流程:
void ParseVideoStreamBasicInfo(const char* filename) {
// 打开输入视频文件,创建 AVFormatContext 结构体
// 该结构体包含视频文件的格式、流信息等
AVFormatContext* formatContext = nullptr;
if (avformat_open_input(&formatContext, filename, nullptr, nullptr) != 0) {
std::cerr << "无法打开文件: " << filename << std::endl;
return;
}
// 读取流信息(包括视频、音频、字幕等),并填充 AVFormatContext 结构体
if (avformat_find_stream_info(formatContext, nullptr) < 0) {
std::cerr << "无法获取流信息" << std::endl;
avformat_close_input(&formatContext);
return;
}
// 遍历所有流
AVCodecContext* pVideoCodecContext = nullptr;
AVStream* pVideoStream = nullptr;
for (unsigned int i = 0; i < formatContext->nb_streams; i++) {
// 检查当前流是否为视频流
AVStream* stream = formatContext->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
pVideoStream = stream;
// 查找对应的解码器
const AVCodec* codec = avcodec_find_decoder(stream->codecpar->codec_id);
// 分配编解码上下文
pVideoCodecContext = avcodec_alloc_context3(codec);
// 将流的参数复制到编解码上下文
avcodec_parameters_to_context(pVideoCodecContext, formatContext->streams[i]->codecpar);
// 打开编解码器
avcodec_open2(pVideoCodecContext, codec, NULL);
}
}
// 读取视频流基本信息
VideoStreamBasicInfo info = {};
if (pVideoStream && pVideoStream->codecpar) {
// 视频编码格式
info.codec_id = pVideoStream->codecpar->codec_id;
// 视频宽度
info.width = pVideoStream->codecpar->width;
// 视频高度
info.height = pVideoStream->codecpar->height;
// 像素格式
info.pix_fmt = static_cast<AVPixelFormat>(pVideoStream->codecpar->format);
// 帧率
info.framerate = pVideoStream->avg_frame_rate;
// 视频码率
info.bit_rate = pVideoStream->codecpar->bit_rate;
// 采样宽高比
info.sample_aspect_ratio = pVideoStream->codecpar->sample_aspect_ratio;
}
}
1.2 解析视频帧
1.2.1 读取视频帧
AVFrame* ReadFrame()
{
// 参照之前的代码流程获取格式上下文和编解码上下文
AVFormatContext* formatContext; // 视频文件的格式上下文
AVCodecContext* pVideoCodecContext; // 视频流的解码上下文
// 分配一个 AVPacket 结构体,用于存储编码数据(压缩数据)
// 从文件中读取一个数据包(Packet),存储到 _pPacket
AVPacket* _pPacket = av_packet_alloc();
if (av_read_frame(formatContext, _pPacket) < 0)
{
av_packet_unref(_pPacket); // 释放数据包的引用计数,防止内存泄漏
return nullptr; // 读取失败,返回空指针
}
// 将读取到的数据包发送给解码器
int _nRet = avcodec_send_packet(pVideoCodecContext, _pPacket);
if (_nRet < 0) {
av_packet_unref(_pPacket); // 释放数据包
return nullptr; // 发送失败,返回空指针
}
// 分配一个 AVFrame 结构体,用于存储解码后的帧(像素数据)
// 从解码器获取解码后的帧数据
AVFrame* _pAVFrame = av_frame_alloc();
_nRet = avcodec_receive_frame(pVideoCodecContext, _pAVFrame);
if (_nRet < 0) {
av_frame_free(&_pAVFrame); // 释放帧内存
av_packet_unref(_pPacket); // 释放数据包
return nullptr; // 获取失败,返回空指针
}
// 检查解码后的帧格式是否有效
if (_pAVFrame->format == -1) {
av_frame_free(&_pAVFrame); // 释放帧内存
av_packet_unref(_pPacket); // 释放数据包
return nullptr; // 无效格式,返回空指针
}
// 释放数据包的引用计数,避免内存泄漏
av_packet_unref(_pPacket);
return _pAVFrame;
}
1.2.2 将视频帧转换为Qt的QImage
// 包含缩放图片依赖的头文件和链接库
extern "C" {
#include <libswscale/swscale.h>
#pragma comment(lib, "swscale.lib")
}
// 将AVFrame转换到能够直接显示的QImage数据
QImage convertFrameToQImage(AVFrame* pAVFrame_, int width, int height)
{
// 创建一个 QImage 对象,使用 32 位 RGB 格式存储像素数据
QImage img(width, height, QImage::Format_RGB32);
// 创建一个 SwsContext,用于像素格式转换
// - 源格式: pAVFrame_->format (原始帧格式)
// - 目标格式: AV_PIX_FMT_RGB32 (Qt 兼容的 RGB32 格式)
// - 采用 SWS_BILINEAR 进行缩放
SwsContext* sws_ctx = sws_getContext(
width, height, static_cast<AVPixelFormat>(pAVFrame_->format),
width, height, AV_PIX_FMT_RGB32,
SWS_BILINEAR, nullptr, nullptr, nullptr
);
if (!sws_ctx) {
return QImage(); // 转换上下文创建失败,返回空图像
}
// 设置目标图像的数据缓冲区,QImage 的 bits() 方法返回底层数据指针
uint8_t* data[1] = { reinterpret_cast<uint8_t*>(img.bits()) };
int linesize[1] = { static_cast<int>(img.bytesPerLine()) };
// 执行像素格式转换
sws_scale(
sws_ctx, // SwsContext 上下文
pAVFrame_->data, // 源图像数据
pAVFrame_->linesize, // 源图像行宽
0, height, // 转换的起始行和高度
data, // 目标图像数据
linesize // 目标图像行宽
);
// 释放 SwsContext 资源,防止内存泄漏
sws_freeContext(sws_ctx);
// 返回转换后的 QImage
return img;
}
二、理论知识补充说明
2.1 视频帧解析理论说明
2.1.1 什么是视频帧?
视频帧是视频中的单张图像,是视频的基本组成单位。视频是由一系列按时间顺序排列的静态图像(即帧)组成的,每一帧都有特定的图像数据。当这些帧以一定的速度连续播放时,人眼会将其感知为流畅的视频画面。
视频的每一帧包含了图像的颜色、亮度、尺寸等信息。视频中的帧通常会根据编码格式不同,有不同的存储方式。
2.1.2 解码与视频帧
视频流是经过压缩的,压缩后的数据无法直接显示为图像,因此需要解码才能将其转换为可以显示的图像数据。解码的过程是指将压缩的编码数据(例如 H.264 或 HEVC)还原成图像数据(通常是 RGB 或 YUV 格式)。
在 FFmpeg 中,解码过程是通过 avcodec_send_packet()
和 avcodec_receive_frame()
完成的:
- avcodec_send_packet():将压缩的编码数据(AVPacket)发送给解码器。这个步骤是将压缩视频帧输入解码器的第一步。
- avcodec_receive_frame():从解码器中获取解码后的图像数据(AVFrame)。这一步会返回解码后的帧(图像数据),这些图像数据是以某种像素格式(如 YUV420P)表示的。
2.1.3 帧的组成
解码后的每一帧(AVFrame)通常包含以下几个主要部分:
- data:这是一个指向图像数据的指针数组,图像的每个平面(如 Y、U、V 平面或 R、G、B 平面)会占用一个数组。
- linesize:指每个平面的一行像素所占的字节数。由于视频帧中的每一行的长度可能因图像宽度对齐的要求而有所不同,
linesize
会存储这个值。 - width 和 height:图像的宽度和高度,以像素为单位。
- format:帧的像素格式,表示帧中的颜色空间和存储方式。常见的像素格式包括 YUV420P、RGB24 等。
2.1.4 视频帧的类型
视频帧可以分为以下几种类型:
- I帧(Intra-coded Frame):全帧,是最完整的帧,不依赖其他帧进行解码。I帧的质量最高,但是数据量也最大。
- P帧(Predicted Frame):预测帧,依赖前面的I帧或P帧,通过预测技术编码,数据量比I帧小。
- B帧(Bidirectional Predicted Frame):双向预测帧,既可以参考前面的I帧或P帧,也可以参考后面的帧,通常比P帧更小。
每个视频流是由这些帧按照时间顺序排列组成的,解码器根据这些帧的信息来进行图像的恢复。
2.1.5 解码过程中的帧丢失和同步问题
在解码过程中,由于网络问题或者文件损坏等原因,可能会出现某些帧无法解码的情况。通常,这时解码器会丢弃那些无法解码的帧并尝试同步下一帧。为了保持视频播放的平滑,FFmpeg 提供了很多功能来检测和修复丢帧问题。
2.2 视频帧数据转换理论说明
2.2.1 为什么需要进行数据转换?
视频帧在解码后通常是以特定的像素格式(如 YUV)存储的,而显示设备(例如电脑屏幕)通常使用 RGB 格式来表示图像。YUV 和 RGB 是两种不同的颜色表示模型,因此我们需要将解码后的帧从 YUV 格式转换为 RGB 格式,这样才能显示在屏幕上。
2.2.2 YUV 到 RGB 的转换
YUV 是一种颜色空间,其中 Y 分量表示亮度信息,U 和 V 分量表示色度信息。它主要用于视频编码中,因为它与人眼对颜色的感知方式较为接近。
在 RGB 中,每个像素有三个分量,分别表示红色、绿色和蓝色。因此,YUV 到 RGB 的转换涉及到将亮度和色度信息合并,生成红、绿、蓝三种颜色的分量。
2.2.3 FFmpeg 提供的转换工具
FFmpeg 提供了 libswscale
库来执行像素格式转换,它可以将 YUV 格式的图像数据转换为 RGB 格式,并且支持各种不同的像素格式之间的转换。转换过程中,通常需要用到 SwsContext 结构体来保存转换的参数。
sws_getContext()
函数
该函数用于创建一个 SwsContext
,它保存了源格式和目标格式之间的转换参数,包括源图像的尺寸、像素格式、目标图像的尺寸和像素格式等。它的返回值是一个指向 SwsContext
结构体的指针。
sws_scale()
函数
在创建好 SwsContext
后,可以使用 sws_scale()
函数来执行实际的像素数据转换。该函数会将源图像数据(如 YUV 格式)转换为目标图像数据(如 RGB 格式)。转换后的数据将存储在目标图像的数据缓冲区中。
2.2.4 如何优化转换过程?
- 避免频繁的转换:像素格式转换是一个比较消耗计算资源的过程,因此最好只在需要显示的时候进行转换,不要在每一帧的处理中都进行转换。
- 硬件加速:FFmpeg 也支持一些硬件加速技术(如通过 CUDA 或 OpenCL),可以在显卡上进行像素格式转换,从而提高转换效率。
2.2.5 从 AVFrame 到 QImage 的转换
为了在 Qt 中显示视频帧,我们需要将解码后的 AVFrame(通常是 YUV 格式)转换成 QImage 对象。QImage 是 Qt 提供的图像处理类,它可以将图像数据显示到界面上。为了实现这一转换,可以使用 libswscale
提供的功能将 AVFrame 中的 YUV 数据转换为 RGB 格式,然后将转换后的数据填充到 QImage 中进行显示。