本文目录
- 1.环境配置
- 2.ffmpeg编解码的主要逻辑:
- 3. 捕获屏幕帧与写入输出文件
- 4. 释放资源 在录制结束时,释放所有分配的资源。
- 5.自定义I/O上下文
- 6.对于ACC编码器注意事项
1.环境配置
下载并安装FFmpeg库 在Windows上 从FFmpeg官方网站下载预编译的FFmpeg库: 解压下载的文件,并记下解压后的路径。
FFmpeg下载(windows版本)_libijkffmpeg.so 32位下载-****博客
记住一定下载是32位,否则使用msvc32位时无法进行链接对应库的。
将下载后的 static 中动态库拿出来,将shared的include与lib拿出来,放在当前项目目录下。
#FFmpeg库的路径
INCLUDEPATH += /path/to/ffmpeg/include
LIBS += -L/path/to/ffmpeg/lib -lavcodec -lavformat -lavutil -lswscale
#如果你在Windows上使用预编译的FFmpeg库,可能还需要添加以下内容
LIBS += -lavdevice -lavfilter -lswresample -lpostproc
重要的一点:指定可执行程序的路径,这里就是我们动态库的目录下:
DESTDIR =$$PWD/bin #指定可执行的生成路径--到库的位置
2.ffmpeg编解码的主要逻辑:
- 初始化FFmpeg库在使用FFmpeg之前,需要初始化FFmpeg库。
*extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
}
void initializeFFmpeg() {
av_register_all();
avcodec_register_all();
avformat_network_init();
}*
首先,在使用ffmpeg的上下文,音视频流,编解码…之前,都需要先进性初始化,
在使用FFmpeg进行多媒体处理时,初始化库是非常重要的步骤。以下是对 av_register_all、avcodec_register_all 和 avformat_network_init 三个初始化函数的详细解释:
-
av_register_all()
含义
av_register_all 函数用于注册所有的编解码器、文件格式和协议。它是FFmpeg库的初始化函数之一,确保在使用FFmpeg的任何功能之前,所有的编解码器和格式都已注册。
2.av_register_all();
作用:
注册所有的编解码器(如H.264、AAC等)。
注册所有的文件格式(如MP4、AVI等)。
注册所有的协议(如HTTP、RTMP等)。 -
avcodec_register_all()
avcodec_register_all 函数用于注册所有的编解码器。虽然 av_register_all 已经包含了这个步骤,但在某些情况下,你可能只需要注册编解码器而不需要注册其他组件。
avcodec_register_all();
主要作用:注册所有的编解码器(如H.264、AAC等)。 -
avformat_network_init()
avformat_network_init 函数用于初始化网络组件。它在使用任何网络协议(如HTTP、RTMP等)之前调用,以确保网络功能已正确初始化。
主要作用:初始化网络组件,确保网络协议(如HTTP、RTMP等)可以正常使用。
一般我们在构造进行库的初始化,之后,我们回去在一个初始化接口中去进行ffmpeg的准备工作:
第一个准备工作就是去设置编解码器: -
设置编解码器 —参数设置
// 设置选项
AVDictionary *options = nullptr;
av_dict_set(&options, "rtbufsize", "100M", 0); // 设置实时缓冲区大小
av_dict_set(&options, "framerate", "30", 0); // 设置帧率为30 FPS
//在打开输入文件,打开编码器,写入输出文件时可以设置这些参数
通过 AVDictionary 设置选项来配置输入设备的参数,例如帧率,缓冲区大小,防止在写入时崩溃。
初始化
主要是对与FFmpeg的以下成员进行设置初始化:
AVFormatContext)格式上下文(用于管理输出文件的格式信息
AVCodecContext:FFmpeg的编解码器上下文,用于管理视频编码器的参数和状态
AVStream :FFmpeg的视频流,用于表示输出文件中的视频流
SwsContext :FFmpeg的图像转换上下文,用于将捕获的图像转换为编码器需要的格式
AVFrame :FFmpeg的视频帧,用于存储要编码的视频数据
int frame:帧计数器,用于跟踪已捕获的帧数
常见的资源设置有如下这些:
AVCodecContext
资源:AVCodecContext 是编码器或解码器的上下文,包含了编解码器的所有状态信息和参数。
释放函数:avcodec_free_context
-
AVFrame
资源:AVFrame 用于存储解码后的帧数据或编码前的帧数据。
释放函数:av_frame_free -
SwsContext
资源:SwsContext 是用于图像缩放和像素格式转换的上下文。
释放函数:sws_freeContext -
AVFormatContext
资源:AVFormatContext 是多媒体文件的上下文,包含了文件格式、流信息等。
释放函数:avformat_free_context -
AVPacket
资源:AVPacket 用于存储编码后的数据包。
释放函数:av_packet_unref -
AVCodecParameters
资源:AVCodecParameters 用于存储编解码器的参数。
释放函数:avcodec_parameters_free -
AVIOContext
资源:AVIOContext 是用于输入输出操作的上下文。
释放函数:avio_context_free -
AVFilterGraph
资源:AVFilterGraph 是用于音视频过滤操作的图。
释放函数:avfilter_graph_free -
AVFilterContext
资源:AVFilterContext 是用于音视频过滤操作的上下文。
释放函数:avfilter_free -
AVDictionary
资源:AVDictionary 是用于存储键值对的字典,常用于传递选项。
释放函数:av_dict_free
设置编解码器的主要步骤: -
创建输出上下文
avformat_alloc_output_context2(&formatContext, nullptr, nullptr, filename);
if (!formatContext) {
qDebug() << "无法创建输出上下文";
return;
}
avformat_alloc_output_context2 函数根据指定的文件名创建一个输出格式上下文。如果成功,formatContext 将指向一个新的AVFormatContext 结构体。如果失败,formatContext 将为 nullptr。
2. 查找编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
qDebug() << "找不到编码器";
return;
}
avcodec_find_encoder 函数根据编码器ID(这里是AV_CODEC_ID_H264)查找对应的编码器。如果成功,codec 将指向一个 AVCodec 结构体。如果失败,codec 将为 nullptr。
3. 创建视频流
videoStream = avformat_new_stream(formatContext, codec);
if (!videoStream) {
qDebug() << "无法创建视频流";
return;
}
avformat_new_stream 函数在 formatContext 中创建一个新的流,并将其与指定的编码器关联。如果成功,videoStream 将指向一个新的 AVStream 结构体。如果失败,videoStream 将为 nullptr。
4. 分配编码器上下文
codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {
qDebug() << "无法分配视频编解码器上下文";
return;
}
//设置编码器参数
codecContext->bit_rate = 400000;
codecContext->width = width;
codecContext->height = height;
codecContext->time_base = {1, 25};
codecContext->framerate = {25, 1};
codecContext->gop_size = 10;
codecContext->max_b_frames = 1;
codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
avcodec_alloc_context3 函数为指定的编码器分配并初始化一个新的编码器上下文。如果成功,codecContext 将指向一个新的 AVCodecContext 结构体。如果失败,codecContext 将为 nullptr。
参数设置:
bit_rate:设置编码器的比特率。
width 和 height:设置视频的宽度和高度。
time_base:设置时间基,表示每秒25帧。
framerate:设置帧率。
gop_size:设置GOP(Group of Pictures)大小。
max_b_frames:设置最大B帧数。
pix_fmt:设置像素格式。
6. 设置全局头部标志
if (formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
如果输出格式需要全局头部(AVFMT_GLOBALHEADER),则在编码器上下文中设置 AV_CODEC_FLAG_GLOBAL_HEADER 标志。
注意:
在使用FFmpeg进行解码和编码操作时,都需要打开对应的编解码器。具体来说,你需要为解码器和编码器分别初始化和打开它们的上下文。(demo这里只涉及编码)
解码器和编码器的初始化和打开
解码器
查找解码器:使用 avcodec_find_decoder 函数查找解码器。
分配解码器上下文:使用 avcodec_alloc_context3 函数分配解码器上下文。
打开解码器:使用 avcodec_open2 函数打开解码器。
编码器
查找编码器:使用 avcodec_find_encoder 函数查找编码器。
分配编码器上下文:使用 avcodec_alloc_context3 函数分配编码器上下文。
设置编码器参数:设置编码器的必要参数,如比特率、宽度、高度、像素格式等。
打开编码器:使用 avcodec_open2 函数打开编码器。
两者使用基本上一样,但注意及时两个类型一样,但编码器与解码器使用的是独立的上下文,注意这一点!!
以下是对于编码器的编写流程:
int main() {
// 初始化FFmpeg库
avcodec_register_all();
avformat_network_init();
// 查找H.264编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
std::cerr << "找不到H.264编码器" << std::endl;
return -1;
}
// 分配编码器上下文
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {
std::cerr << "无法分配编码器上下文" << std::endl;
return -1;
}
// 设置编码器参数
codecContext->bit_rate = 400000;
codecContext->width = 640;
codecContext->height = 480;
codecContext->time_base = {1, 25};
codecContext->framerate = {25, 1};
codecContext->gop_size = 10;
codecContext->max_b_frames = 1;
codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
if (avcodec_open2(codecContext, codec, nullptr) < 0) {
std::cerr << "无法打开编解码器" << std::endl;
avcodec_free_context(&codecContext);
return -1;
}
// 发送一个空帧到编码器,以便刷新编码器的内部缓冲区
int ret = avcodec_send_frame(codecContext, nullptr);
if (ret < 0) {
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf));
std::cerr << "发送空帧到编码器时出错: " << errbuf << std::endl;
avcodec_free_context(&codecContext);
return -1;
}
// 接收所有剩余的编码数据包
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
while (true) {
ret = avcodec_receive_packet(codecContext, &pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf));
std::cerr << "接收编码后的数据包时出错: " << errbuf << std::endl;
break;
}
// 处理编码后的数据包
std::cout << "接收到一个编码数据包,大小: " << pkt.size << " 字节" << std::endl;
// 释放数据包
av_packet_unref(&pkt);
}
// 释放资源
avcodec_free_context(&codecContext);
return 0;
}
- 打开编码器
if (avcodec_open2(codecContext, codec, nullptr) < 0) {
qDebug() << "无法打开编解码器";
return;
}
avcodec_open2 函数初始化编码器上下文并打开编码器。如果成功,返回0;如果失败,返回负值。
8. 复制编码器参数
if (avcodec_parameters_from_context(videoStream->codecpar, codecContext) < 0) {
qDebug() << "无法复制编解码器参数";
return;
}
avcodec_parameters_from_context 函数将编码器上下文中的参数复制到视频流的参数结构体中。如果成功,返回0;如果失败,返回负值。
9. 打开输出文件
if (!(formatContext->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&formatContext->pb, filename, AVIO_FLAG_WRITE) < 0) {
qDebug() << "无法打开输出文件";
return;
}
}
如果输出格式不需要文件(AVFMT_NOFILE),则跳过此步骤。否则,使用 avio_open 函数打开输出文件。如果成功,返回0;如果失败,返回负值。 修改输出文件路径为桌面路径,—修改输出上下文中的输出路径为桌面路径
if (avformat_write_header(formatContext, nullptr) < 0) {
qDebug() << "打开输出文件时出错";
return;
}
avformat_write_header 函数将文件头写入输出文件。如果成功,返回0;如果失败,返回负值。
11. 分配视频帧
frame = av_frame_alloc();
if (!frame) {
qDebug() << "无法分配视频帧";
return;
}
frame->format = codecContext->pix_fmt;
frame->width = codecContext->width;
frame->height = codecContext->height;
分配并初始化一个视频帧。
av_frame_alloc 函数分配一个新的视频帧。如果成功,frame 将指向一个新的 AVFrame 结构体。如果失败,frame 将为 nullptr。然后设置帧的格式、宽度和高度。
12. 分配视频帧数据
if (av_frame_get_buffer(frame, 32) < 0) {
qDebug() << "无法分配视频帧数据";
return;
}
av_frame_get_buffer 函数为视频帧分配数据缓冲区。如果成功,返回0;如果失败,返回负值。
13. 初始化像素格式转换上下文
swsContext = sws_getContext(codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
codecContext->width, codecContext->height, codecContext->pix_fmt,
SWS_BICUBIC, nullptr, nullptr, nullptr);
if (!swsContext) {
qDebug() << "无法初始化转换上下文";
return;
}
sws_getContext 函数创建一个用于像素格式转换的上下文。它将源图像的格式(AV_PIX_FMT_RGB24)转换为目标图像的格式(codecContext->pix_fmt)。如果成功,swsContext 将指向一个新的 SwsContext 结构体。如果失败,swsContext 将为 nullptr。
参数为:输入图像的高度,宽度,像素格式,输出图像的宽度,高度,像素格式,(缩放算法,过滤器
输入格式:RGB24(每个像素由三个字节表示,分别表示红色、绿色和蓝色分量)。
输出格式:通常是 YUV420P(每个像素由三个分量表示,分别是亮度(Y)和两个色度(U 和 V)。Y 分量的分辨率与原图像相同,而 U 和 V 分量的分辨率是原图像的一半(水平和垂直方向上各减少一半))。
完整代码:
avformat_alloc_output_context2(&formatContext, nullptr, nullptr, filename);
if (!formatContext) {
qDebug() << "无法创建输出上下文";
return;
}
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
qDebug() << "找不到编码器";
return;
}
videoStream = avformat_new_stream(formatContext, codec);
if (!videoStream) {
qDebug() << "无法创建视频流";
return;
}
codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {
qDebug() << "无法分配视频编解码器上下文";
return;
}
codecContext->bit_rate = 400000;
codecContext->width = width;
codecContext->height = height;
codecContext->time_base = {1, 25};
codecContext->framerate = {25, 1};
codecContext->gop_size = 10;
codecContext->max_b_frames = 1;
codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
if (formatContext->oformat->flags & AVFMT_GLOBALHEADER) {
codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
if (avcodec_open2(codecContext, codec, nullptr) < 0) {
qDebug() << "无法打开编解码器";
return;
}
if (avcodec_parameters_from_context(videoStream->codecpar, codecContext) < 0) {
qDebug() << "无法复制编解码器参数";
return;
}
if (!(formatContext->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&formatContext->pb, filename, AVIO_FLAG_WRITE) < 0) {
qDebug() << "无法打开输出文件";
return;
}
}
if (avformat_write_header(formatContext, nullptr) < 0) {
qDebug() << "打开输出文件时出错";
return;
}
frame = av_frame_alloc();
if (!frame) {
qDebug() << "无法分配视频帧";
return;
}
frame->format = codecContext->pix_fmt;
frame->width = codecContext->width;
frame->height = codecContext->height;
if (av_frame_get_buffer(frame, 32) < 0) {
qDebug() << "无法分配视频帧数据";
return;
}
swsContext = sws_getContext(codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
codecContext->width, codecContext->height, codecContext->pix_fmt,
SWS_BICUBIC, nullptr, nullptr, nullptr);
if (!swsContext) {
qDebug() << "无法初始化转换上下文";
return;
}
}
3. 捕获屏幕帧与写入输出文件
这里主要是使用Qt的grab函数捕获屏幕帧,并进行像素格式转换。具体步骤如下:
- 捕获屏幕
QPixmap originalPixmap = QGuiApplication::primaryScreen()->grabWindow(0);
QImage image = originalPixmap.toImage().convertToFormat(QImage::Format_RGB888);
QGuiApplication::primaryScreen()->grabWindow(0):捕获整个屏幕(窗口ID为0表示整个屏幕)。
originalPixmap.toImage().convertToFormat(QImage::Format_RGB888):将捕获的图像转换为 QImage 格式,并将其像素格式转换为 RGB888。
2. 准备像素格式转换
uint8_t *data[1] = { image.bits() };
int linesize[1] = { static_cast(image.bytesPerLine()) };
功能:准备源图像数据和行字节数,以便进行像素格式转换。
解释:
data[1]:指向源图像数据的指针数组。
linesize[1]:源图像每行的字节数。
3. 像素格式转换
sws_scale(swsContext, data, linesize, 0, codecContext->height, frame->data, frame->linesize);
swsContext:像素格式转换上下文。data 和 linesize:源图像数据和行字节数。–根据格式转化上下文转换对应的输入到输出----
输入数据属性:swsContext,data为图像数据,RGB24,
输出数据属性:codecContext->height, frame->data, frame->linesize
0:源图像的起始行。
codecContext->height:源图像的行数。
frame->data 和 frame->linesize:目标图像数据和行字节数。
4. 记录帧的个数
frame->pts = frameCounter++;
解释:frameCounter 是一个递增的计数器。
5. 发送帧到编码器
int ret = avcodec_send_frame(codecContext, frame);
if (ret < 0) {
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "发送帧到编码器时出错: " << errbuf;
return;
}
功能:将帧发送到编码器。
解释:
avcodec_send_frame:将帧发送到编码器。
ret:返回值,如果小于0表示出错。
av_strerror:获取详细的错误信息。
6. 初始化数据包
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
av_init_packet:初始化数据包。
pkt.data 和 pkt.size:设置数据包的数据指针和大小。
7. 接收编码后的数据包
while (ret >= 0) {
ret = avcodec_receive_packet(codecContext, &pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf