1、了解FFmpeg库
库 | 介绍 |
---|---|
avcodec | 音视频编解码核心库 |
avformat | 音视频容器格式的封装和解析 |
avutil | 核心工具库 |
swscal | 图像格式转换的模块 |
swresampel | 音频重采样 |
avfilter | 音视频滤镜库 如视频加水印、音频变声 |
avdevice | 输入输出设备库,提供设备数据的输入与输出 |
FFmpeg 依靠以上几个库,实现了强大的音视频编码、解码、编辑、转换、采集等能力。
2、播放流程
-
解封装(Demuxing):就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。
-
软硬件解码(Decode):就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。通过解码,将压缩编码的视频数据 H.264,MPEG2 解码成为非压缩的颜色数据,例如 YUV 等等;将压缩编码的音频数据 AAC,MP3 解码成为非压缩的音频抽样数据,例如 PCM 数据。解码分为硬编码和软编码。
-
像素格式转换:将 YUV 数据格式转换成 RGB 数据格式。
-
重采样:对音频重新采样。
-
dts/pts:dts 是解码的时间戳,而 pts 是显示的时间戳。pts 用于获取当前播放进度。进度条移动需要用到
av_seek_frame
函数。 -
音视频同步:就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的音频和视频数据,并将音视频频数据送至系统的显卡和声卡播放出来(Render)。
4、ffmpeg解码流程
注意:新版本后不用再注册所有格式和编解码器 av_register_all();
5、实战
了解了这么多,接下来开始第一步,打开一个视频文件,打印出音视频流信息。
创建一个类 VideoInfo
,用于处理视频文件的打开和流信息打印。
运行环境macOS+Qt6.6+FFmpeg7.0.1+OpenGL
videoinfo.cpp
#include "videoinfo.h"
#include <QDebug>
VideoInfo::VideoInfo(QObject *parent)
: QObject(parent), formatContext(nullptr)
{
// 注册所有格式和编解码器
//av_register_all();
}
VideoInfo::~VideoInfo()
{
if (formatContext) {
avformat_close_input(&formatContext);
}
}
bool VideoInfo::openFile(const QString &filePath)
{
if (avformat_open_input(&formatContext, filePath.toStdString().c_str(), nullptr, nullptr) != 0) {
qWarning() << "Could not open file:" << filePath;
return false;
}
if (avformat_find_stream_info(formatContext, nullptr) < 0) {
qWarning() << "Could not find stream information";
return false;
}
av_dump_format(formatContext, 0, filePath.toStdString().c_str(), 0);
return true;
}
main.cpp
#include <QApplication>
#include <QPushButton>
#include <QFileDialog>
#include "videoinfo.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
VideoInfo videoInfo;
QPushButton button("Open Video File");
QObject::connect(&button, &QPushButton::clicked, [&]() {
QString filePath = QFileDialog::getOpenFileName(nullptr, "Open Video File", "", "Video Files (*.mp4 *.avi *.mkv)");
if (!filePath.isEmpty()) {
videoInfo.openFile(filePath);
}
});
button.show();
return app.exec();
}
-
av_register_all()
: 注册所有可用的文件格式和编解码器。这是 FFmpeg 初始化的一部分。在 FFmpeg 的较新版本中可能已经不需要这个调用,但为了兼容性,可以保留。 -
打开视频文件: 使用
avformat_open_input
打开视频文件,并检查是否成功。 -
查找流信息: 使用
avformat_find_stream_info
来解析视频文件中的流信息。 -
打印流信息: 使用
av_dump_format
输出文件的详细信息,包括音视频流的信息。
运行程序,打开视频文件后
获得了以下信息:
- 文件元数据: 包含标题、艺术家、编码器、版权等信息。
- 视频流: 854x480 分辨率的 H.264 编码视频,24 fps。
- 音频流: AAC 编码的音频,立体声,采样率 48000 Hz。
这些信息表明视频文件的解析和基本信息提取功能已经实现。
下一步解码视频帧并将其渲染到窗口中,拓展现有的VideoInfo
类来处理视频解码,并显示第一帧图像。
#include "videoinfo.h"
#include <QDebug>
VideoInfo::VideoInfo(QObject *parent)
: QObject(parent), formatContext(nullptr), codecContext(nullptr), frame(nullptr),
packet(nullptr), swsContext(nullptr), videoStreamIndex(-1)
{
av_register_all(); // FFmpeg 7.0.1已经不需要手动注册所有编解码器和格式
avformat_network_init(); // 如果需要支持网络流,这个初始化是必须的
}
VideoInfo::~VideoInfo()
{
if (frame) av_frame_free(&frame);
if (packet) av_packet_free(&packet);
if (codecContext) avcodec_free_context(&codecContext);
if (formatContext) avformat_close_input(&formatContext);
if (swsContext) sws_freeContext(swsContext);
}
bool VideoInfo::openFile(const QString &filePath)
{
if (avformat_open_input(&formatContext, filePath.toStdString().c_str(), nullptr, nullptr) != 0) {
qWarning() << "Could not open file:" << filePath;
return false;
}
if (avformat_find_stream_info(formatContext, nullptr) < 0) {
qWarning() << "Could not find stream information";
return false;
}
av_dump_format(formatContext, 0, filePath.toStdString().c_str(), 0);
// 查找视频流
videoStreamIndex = -1;
for (unsigned int i = 0; i < formatContext->nb_streams; i++) {
if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
break;
}
}
if (videoStreamIndex == -1) {
qWarning() << "Could not find video stream";
return false;
}
// 查找解码器
AVCodecParameters *codecParameters = formatContext->streams[videoStreamIndex]->codecpar;
AVCodec *codec = avcodec_find_decoder(codecParameters->codec_id);
if (!codec) {
qWarning() << "Unsupported codec!";
return false;
}
codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {
qWarning() << "Could not allocate codec context";
return false;
}
if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) {
qWarning() << "Could not copy codec parameters to codec context";
return false;
}
if (avcodec_open2(codecContext, codec, nullptr) < 0) {
qWarning() << "Could not open codec";
return false;
}
frame = av_frame_alloc();
packet = av_packet_alloc();
// 初始化SwsContext用于图像转换
swsContext = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, nullptr, nullptr, nullptr);
return true;
}
QImage VideoInfo::getFrame()
{
while (av_read_frame(formatContext, packet) >= 0) {
if (packet->stream_index == videoStreamIndex) {
int response = avcodec_send_packet(codecContext, packet);
if (response >= 0) {
response = avcodec_receive_frame(codecContext, frame);
if (response >= 0) {
// 使用 FFmpeg 7.0.1 API: av_image_get_buffer_size 和 av_image_fill_arrays
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecContext->width, codecContext->height, 1);
uint8_t *buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
uint8_t *dst_data[4];
int dst_linesize[4];
av_image_fill_arrays(dst_data, dst_linesize, buffer, AV_PIX_FMT_RGB24, codecContext->width, codecContext->height, 1);
sws_scale(swsContext, frame->data, frame->linesize, 0, codecContext->height, dst_data, dst_linesize);
QImage image(dst_data[0], codecContext->width, codecContext->height, QImage::Format_RGB888);
av_free(buffer); // 释放内存
// 这里缩放图像到适合显示的尺寸
return image.scaled(800, 600, Qt::KeepAspectRatio); // 假设目标尺寸为800x600
}
}
}
av_packet_unref(packet);
}
return QImage();
}
然后在主窗口代码中添加一个QLabel来显示解码的帧。运行结果如图: