本文目录
- 1.环境配置
- 2.ffmpeg编解码的主要逻辑:
- [3. 捕获屏幕帧与写入输出文件](#3. 捕获屏幕帧与写入输出文件)
- [4. 释放资源 在录制结束时,释放所有分配的资源。](#4. 释放资源 在录制结束时,释放所有分配的资源。)
- 5.自定义I/O上下文
- 6.对于ACC编码器注意事项
1.环境配置
下载并安装FFmpeg库 在Windows上 从FFmpeg官方网站下载预编译的FFmpeg库: 解压下载的文件,并记下解压后的路径。
FFmpeg下载(windows版本)_libijkffmpeg.so 32位下载-CSDN博客
记住一定下载是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库。
dart
*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的准备工作:
第一个准备工作就是去设置编解码器:
-
设置编解码器 ---参数设置
dart
// 设置选项
AVDictionary *options = nullptr;
av_dict_set(&options, "rtbufsize", "100M", 0); // 设置实时缓冲区大小
av_dict_set(&options, "framerate", "30", 0); // 设置帧率为30 FPS
//在打开输入文件,打开编码器,写入输出文件时可以设置这些参数
通过 AVDictionary 设置选项来配置输入设备的参数,例如帧率,缓冲区大小,防止在写入时崩溃。
初始化
dart
主要是对与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
设置编解码器的主要步骤:
-
创建输出上下文
dart
avformat_alloc_output_context2(&formatContext, nullptr, nullptr, filename);
if (!formatContext) {
qDebug() << "无法创建输出上下文";
return;
}
avformat_alloc_output_context2 函数根据指定的文件名创建一个输出格式上下文。如果成功,formatContext 将指向一个新的AVFormatContext 结构体。如果失败,formatContext 将为 nullptr。
- 查找编码器
dart
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。
- 创建视频流
dart
videoStream = avformat_new_stream(formatContext, codec);
if (!videoStream) {
qDebug() << "无法创建视频流";
return;
}
avformat_new_stream 函数在 formatContext 中创建一个新的流,并将其与指定的编码器关联。如果成功,videoStream 将指向一个新的 AVStream 结构体。如果失败,videoStream 将为 nullptr。
- 分配编码器上下文
dart
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:设置像素格式。
- 设置全局头部标志
dart
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 函数打开编码器。
两者使用基本上一样,但注意及时两个类型一样,但编码器与解码器使用的是独立的上下文,注意这一点!!
以下是对于编码器的编写流程:
dart
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;
}
- 打开编码器
dart
if (avcodec_open2(codecContext, codec, nullptr) < 0) {
qDebug() << "无法打开编解码器";
return;
}
avcodec_open2 函数初始化编码器上下文并打开编码器。如果成功,返回0;如果失败,返回负值。
- 复制编码器参数
dart
if (avcodec_parameters_from_context(videoStream->codecpar, codecContext) < 0) {
qDebug() << "无法复制编解码器参数";
return;
}
avcodec_parameters_from_context 函数将编码器上下文中的参数复制到视频流的参数结构体中。如果成功,返回0;如果失败,返回负值。
- 打开输出文件
dart
if (!(formatContext->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&formatContext->pb, filename, AVIO_FLAG_WRITE) < 0) {
qDebug() << "无法打开输出文件";
return;
}
}
如果输出格式不需要文件(AVFMT_NOFILE),则跳过此步骤。否则,使用 avio_open 函数打开输出文件。如果成功,返回0;如果失败,返回负值。 修改输出文件路径为桌面路径,---修改输出上下文中的输出路径为桌面路径
dart
if (avformat_write_header(formatContext, nullptr) < 0) {
qDebug() << "打开输出文件时出错";
return;
}
avformat_write_header 函数将文件头写入输出文件。如果成功,返回0;如果失败,返回负值。
- 分配视频帧
dart
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。然后设置帧的格式、宽度和高度。
- 分配视频帧数据
dart
if (av_frame_get_buffer(frame, 32) < 0) {
qDebug() << "无法分配视频帧数据";
return;
}
av_frame_get_buffer 函数为视频帧分配数据缓冲区。如果成功,返回0;如果失败,返回负值。
- 初始化像素格式转换上下文
dart
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 分量的分辨率是原图像的一半(水平和垂直方向上各减少一半))。
完整代码:
dart
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函数捕获屏幕帧,并进行像素格式转换。具体步骤如下:
- 捕获屏幕
dart
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。
- 准备像素格式转换
uint8_t *data[1] = { image.bits() };
int linesize[1] = { static_cast(image.bytesPerLine()) };
功能:准备源图像数据和行字节数,以便进行像素格式转换。
解释:
data[1]:指向源图像数据的指针数组。
linesize[1]:源图像每行的字节数。
- 像素格式转换
dart
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:目标图像数据和行字节数。
- 记录帧的个数
frame->pts = frameCounter++;
解释:frameCounter 是一个递增的计数器。
- 发送帧到编码器
dart
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:获取详细的错误信息。
- 初始化数据包
dart
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
av_init_packet:初始化数据包。
pkt.data 和 pkt.size:设置数据包的数据指针和大小。
- 接收编码后的数据包
dart
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));
qDebug() << "接收编码后的数据包时出错: " << errbuf;
break;
}
av_interleaved_write_frame(formatContext, &pkt);
av_packet_unref(&pkt);
}
avcodec_receive_packet:从编码器接收编码后的数据包。
ret:返回值,如果等于 AVERROR(EAGAIN) 或 AVERROR_EOF 表示没有更多数据包可接收。
av_interleaved_write_frame:将数据包写入文件。
av_packet_unref:释放数据包。
注意在写入的时候,调整时间戳
dart
while (avcodec_receive_packet(codec_context_, &pkt) == 0) {
av_log(NULL, AV_LOG_DEBUG, "写入的数据的地址: %p, 大小: %d\n", static_cast<void*>(pkt.data), pkt.size);
// 重新调整时间戳
av_packet_rescale_ts(&pkt, codec_context_->time_base, format_context_->streams[0]->time_base);
pkt.stream_index = 0;
ret = av_interleaved_write_frame(format_context_, &pkt);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "写入数据包时出错\n");
av_packet_unref(&pkt);
return;
}
av_packet_unref(&pkt);
}
以下是完整的代码片段:
dart
void captureFrame() {
QPixmap originalPixmap = QGuiApplication::primaryScreen()->grabWindow(0);
QImage image = originalPixmap.toImage().convertToFormat(QImage::Format_RGB888);
uint8_t *data[1] = { image.bits() };
int linesize[1] = { static_cast<int>(image.bytesPerLine()) };
sws_scale(swsContext, data, linesize, 0, codecContext->height, frame->data, frame->linesize);
frame->pts = frameCounter++;
int ret = avcodec_send_frame(codecContext, frame);
if (ret < 0) {
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf));
qDebug() << "发送帧到编码器时出错: " << errbuf;
return;
}
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
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));
qDebug() << "接收编码后的数据包时出错: " << errbuf;
break;
}
av_interleaved_write_frame(formatContext, &pkt);
av_packet_unref(&pkt);
}
}
4. 释放资源 在录制结束时,释放所有分配的资源。
释放资源ffmpeg提供有对应的释放的方法 以下是如何释放每种资源的具体方法: 刷新缓冲区中的每一帧数据,确保每一帧都写入输出文件当中。
dart
*avcodec_send_frame(codec_context_, nullptr); // 传递空帧以刷新编码器
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
while (avcodec_receive_packet(codec_context_, &pkt) == 0) {
av_log(NULL, AV_LOG_DEBUG, "Flushing packet with data address: %p, size: %d\n", static_cast<void*>(pkt.data), pkt.size);
av_packet_rescale_ts(&pkt, codec_context_->time_base, format_context_->streams[0]->time_base);
pkt.stream_index = 0;
av_interleaved_write_frame(format_context_, &pkt);
av_packet_unref(&pkt);
}
1. 释放 AVCodecContext
if (codecContext) {
avcodec_free_context(&codecContext);
}
2. 释放 AVFrame
if (frame) {
av_frame_free(&frame);
}
3. 释放 SwsContext
if (swsContext) {
sws_freeContext(swsContext);
}
4. 释放 AVFormatContext
if (formatContext) {
avformat_close_input(&formatContext);
avformat_free_context(formatContext);
}
5. 释放 AVPacket
AVPacket packet;
av_init_packet(&packet);
// 使用 packet
av_packet_unref(&packet);
6. 释放 AVCodecParameters
if (codecParameters) {
avcodec_parameters_free(&codecParameters);
}
7. 释放 AVIOContext
if (avioContext) {
avio_context_free(&avioContext);
}
8. 释放 AVFilterGraph
if (filterGraph) {
avfilter_graph_free(&filterGraph);
}
9. 释放 AVFilterContext
if (filterContext) {
avfilter_free(filterContext);
}
10. 释放 AVFrame 中的缓冲区
if (frame) {
av_frame_unref(frame);
}*
以下是本功能的释放部分:
av_log(NULL, AV_LOG_DEBUG, "开始刷新编码器\n");
```dart
avcodec_send_frame(codec_context_, nullptr); // 传递空帧以刷新编码器
AVPacket pkt;
av_init_packet(&pkt);
pkt.data = nullptr;
pkt.size = 0;
while (avcodec_receive_packet(codec_context_, &pkt) == 0) {
av_log(NULL, AV_LOG_DEBUG, "Flushing packet with data address: %p, size: %d\n", static_cast<void*>(pkt.data), pkt.size);
av_packet_rescale_ts(&pkt, codec_context_->time_base, format_context_->streams[0]->time_base);
pkt.stream_index = 0;
av_interleaved_write_frame(format_context_, &pkt);
av_packet_unref(&pkt);
}
if (format_context_) {
av_log(NULL, AV_LOG_DEBUG, "写入文件尾部\n");
av_write_trailer(format_context_);
if (!(format_context_->oformat->flags & AVFMT_NOFILE)) {
av_log(NULL, AV_LOG_DEBUG, "关闭输出文件\n");
avio_closep(&format_context_->pb);
}
avformat_free_context(format_context_);
format_context_ = nullptr;
}
if (codec_context_) {
av_log(NULL, AV_LOG_DEBUG, "释放编码器上下文\n");
avcodec_free_context(&codec_context_);
codec_context_ = nullptr;
}
if (frame_) {
av_log(NULL, AV_LOG_DEBUG, "释放视频帧\n");
av_frame_free(&frame_);
frame_ = nullptr;
}
if (sws_context_) {
av_log(NULL, AV_LOG_DEBUG, "释放SWS上下文\n");
sws_freeContext(sws_context_);
sws_context_ = nullptr;
}
}
5.自定义I/O上下文
为什么需要同时用 AVFormatContext 和 AVIOContext
虽然 AVFormatContext 和 AVIOContext 都涉及到数据处理,但它们的职责不同,且在数据处理流程中互补:
AVFormatContext:管理文件的格式和流信息。它负责描述文件的结构,包括音频、视频、字幕等流的元数据。
AVIOContext:处理实际的数据读写操作。它负责将数据从源读取或写入目标。 在实际应用中,AVFormatContext 需要一个
AVIOContext 来执行实际的 I/O 操作。
例如,当你调用 avformat_write_header 或 av_write_frame 等函数时,AVFormatContext 会使用关联的 AVIOContext 来执行实际的写操作。
为了确保写入输出文件之后,文件是正常可以打开的,除了写入输出上下文,我们还需要去自定义一个I/O上下文来控制我们的输入:
在使用 FFmpeg 进行多媒体处理时,通常会涉及到数据的输入和输出操作。FFmpeg 提供了 AVIOContext 结构体,用于处理这些 I/O 操作。默认情况下,FFmpeg 会使用标准的文件 I/O 函数来读取和写入数据。然而,在某些情况下,你可能需要自定义这些 I/O 操作,例如:
特殊的文件格式:你可能需要处理一些特殊的文件格式或协议,这些格式或协议无法通过标准的文件 I/O 函数来处理。
内存缓冲区:你可能希望将数据写入内存缓冲区而不是文件,或者从内存缓冲区读取数据。
网络流:你可能需要处理网络流,而不是本地文件。
自定义逻辑:你可能需要在读写数据时添加一些自定义的逻辑,例如加密、解密、压缩或解压缩。
虽然我们已经将数据写入了输出上下文,但可能需要自定义 I/O 上下文来实现更复杂或特定的功能。以下是一些可能的原因:
- 更灵活的控制 通过自定义 AVIOContext,你可以完全控制数据的读写过程。例如,你可以在写入数据之前对其进行处理,或者在读取数据之后对其进行处理。
- 处理非标准数据源或目标 如果你的数据源或目标不是标准的文件,而是内存缓冲区、网络流或其他非标准的数据源或目标,自定义 AVIOContext 是必要的。
- 性能优化 在某些情况下,自定义 I/O 上下文可以带来性能上的优化。例如,你可以使用更高效的缓冲区管理策略,减少 I/O 操作的次数,从而提高性能。
- 兼容性 某些平台或环境可能不支持标准的文件 I/O 操作,通过自定义 AVIOContext,你可以实现跨平台的 I/O 操作。
一般我们可以使用hls流媒体传输协议,HLS是一种流媒体协议,通常用于视频流媒体。
//样例
dart
int main() {
// 注册所有的编解码器和格式
av_register_all();
// 输出文件名
const char *output_filename = "output.m3u8";
const char *ts_name = "segment_%03d.ts";
// 分配输出媒体上下文
AVFormatContext *oc = NULL;
avformat_alloc_output_context2(&oc, NULL, "hls", output_filename);
if (!oc) {
fprintf(stderr, "Could not allocate output context\n");
return -1;
}
// 打开输出文件
if (!(oc->oformat->flags & AVFMT_NOFILE)) {
int ret = avio_open(&oc->pb, output_filename, AVIO_FLAG_WRITE);
if (ret < 0) {
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf));
fprintf(stderr, "Could not open output file: %s\n", errbuf);
return -1;
}
}
// 设置HLS相关的选项
AVDictionary *opt = NULL;
av_dict_set(&opt, "hls_flags", "append_list", 0);
av_dict_set(&opt, "hls_time", "10", 0);
av_dict_set(&opt, "hls_list_size", "0", 0);
av_dict_set(&opt, "hls_segment_filename", ts_name, 0);
// 写文件头
int ret = avformat_write_header(oc, &opt);
av_dict_free(&opt);
if (ret < 0) {
char errbuf[128];
av_strerror(ret, errbuf, sizeof(errbuf));
fprintf(stderr, "Error occurred when opening output file: %s\n", errbuf);
return -1;
}
// 释放资源
if (!(oc->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&oc->pb);
}
avformat_free_context(oc);
return 0;
}
6.对于ACC编码器注意事项
在FFmpeg中,某些音频编解码器(例如AAC)具有固定的帧大小(样本数),这通常是由编解码器的标准和设计决定的。即使你尝试手动设置帧大小,编解码器可能会忽略你的设置并使用其默认值。
确认编解码器的帧大小
在打开编解码器之后,你可以检查 AVCodecContext 的 frame_size 字段,以确认实际使用的帧大小。
要确认编解码器是否支持某些特定的设置(例如可变帧大小),你可以查看编解码器的能力标志(capabilities)。FFmpeg 提供了一些能力标志来描述编解码器的特性和支持的功能。
检查编解码器的能力标志
AVCodec 结构体包含一个 capabilities 字段,它是一个位掩码,表示编解码器支持的功能。你可以使用这些标志来检查编解码器是否支持特定的功能。
常见的能力标志
AV_CODEC_CAP_VARIABLE_FRAME_SIZE:表示编解码器支持可变帧大小。
AV_CODEC_CAP_DELAY:表示编解码器可能会在接收到所有输入数据后才输出数据。
AV_CODEC_CAP_FRAME_THREADS:表示编解码器支持帧级多线程。
// 检查编解码器是否支持可变帧大小
if (codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) {
printf("编解码器支持可变帧大小\n");
} else {
printf("编解码器不支持可变帧大小\n");
}
在FFmpeg中,支持可变帧大小的音频编码器相对较少,但确实存在一些支持可变帧大小的编码器。一个典型的例子是Opus编码器。Opus是一种高效的音频编解码器,广泛用于实时通信和流媒体应用。
使用Opus编码器设置可变帧大小
样例:
int main() {
// 注册所有编解码器
avcodec_register_all();
// 查找Opus编码器
AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_OPUS);
if (!codec) {
fprintf(stderr, "找不到Opus编码器\n");
return -1;
}
// 分配编解码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
fprintf(stderr, "分配编解码器上下文失败\n");
return -1;
}
// 设置编码参数
codec_ctx->bit_rate = 64000;
codec_ctx->sample_rate = 48000;
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
codec_ctx->channels = av_get_channel_layout_nb_channels(codec_ctx->channel_layout);
codec_ctx->sample_fmt = codec->sample_fmts[0]; // 使用编码器支持的第一个样本格式
// 检查编解码器是否支持可变帧大小
if (codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) {
printf("Opus编码器支持可变帧大小\n");
codec_ctx->frame_size = 960; // 例如,设置为960个样本
} else {
printf("Opus编码器不支持可变帧大小\n");
codec_ctx->frame_size = 960; // 使用默认的帧大小
}
// 打开编码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
fprintf(stderr, "打开编码器失败\n");
avcodec_free_context(&codec_ctx);
return -1;
}
// 检查实际的帧大小
printf("实际的帧大小:%d\n", codec_ctx->frame_size);
// 释放编解码器上下文
avcodec_free_context(&codec_ctx);
return 0;
}
经过测试,基本上我们是无法去设定编码器的样本数,这通常取决于编码器自己设定的样本数,因此在设计帧率以及样本数,最好按照编码器来。
ACC 1024 OPUS 960