作者:billy
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处
前言
之前做了一个功能,从采集卡获取数据然后录制成视频,结果发现录制的视频内存占用非常大,1分钟的视频大概有 800MB 内存。在帧率和分辨率已确定的情况下,只能通过调整比特率来减少内存占用,但是设置比特率在不同编码器和平台支持情况都有所不同,有些编码器甚至不支持直接设置比特率,所以博主想起了 ffmpeg 这个神器。
这里先介绍一下视频的相关参数:
在用户视角:
清晰度 = 比特率(码率) / 分辨率
流畅度 = 帧率
在开发者视角:
影响内存的:主要是分辨率
影响 CPU 的:码率和编码格式
影响 GPU 的:分辨率和编码格式
影响体积大小和带宽:码率
ffmpeg 库功能测试
首先在网上找到了 ffmpeg 库的 windows 安装包来做下测试
百度网盘下载链接:ffmpeg(windows安装包)
提取码:fkmn
下载完成之后可以直接使用 bin\ffmpeg.exe 在命令行做测试,把 ffmpeg\bin 路径添加到环境变量中
-
确认 ffmpeg 版本和配置:
ffmpeg -version
-
列举所有设备:
ffmpeg -list_devices true -f dshow -i dummy
对已录制的内存占用较大的视频进行压缩:
- 设置码率:
ffmpeg -i input.mp4 -b:v 1000k output.mp4
- 设置分辨率:
ffmpeg -i input.mp4 -s 640x360 output.mp4
- 设置帧率:
ffmpeg -i input.mp4 -r 30 output.mp4
- 综合调整:
ffmpeg -i input.mp4 -b:v 800k -s 640x360 -r 30 output.mp4
也可以用 python 跑脚本:
import subprocess
def compress_video(input_file, output_file, bitrate='1000k'):
command = [
'ffmpeg',
'-i', input_file,
'-b:v', bitrate,
output_file
]
try:
subprocess.run(command, check=True)
print(f"视频压缩成功,输出文件为: {output_file}")
except subprocess.CalledProcessError as e:
print(f"视频压缩失败: {e}")
# 使用示例
input_file = 'input.mp4'
output_file = 'output.mp4'
compress_video(input_file, output_file, bitrate='800k')
再测试一下直接打开设备录制视频
ffmpeg -f dshow -i video="@device_pnp_\\?\usb#vid_2b89&pid_5647&mi_00#7&223c07ce&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global" -c:v libx264 -max_delay 10000 -b:v 1000k -bufsize 200k -r 30 output.mp4 -y
命令解释
-c:v libx264: 指定使用libx264编码器进行视频编码。
-max_delay 10000: 设置编码器的最大延迟为10000毫秒(10秒)。这有助于控制视频编码的缓冲延迟。
-b:v 1000k: 设置视频比特率为1000 kbps(千比特每秒)。
-bufsize 200k: 设置编码器的缓冲区大小为200 kbps。这个参数用于控制编码器在遇到高负载或低负载时的比特率变化。
-r 30: 设置帧率为30帧每秒。
output.mp4: 输出文件名。
-y: 覆盖输出文件,如果文件已存在。
测试结果为用 ffmpeg 压缩过的视频内存占用率非常小,码率越小内存就越小。但是对于客户来说他不会使用命令行去做压缩,所以最终方案还是直接使用 ffmpeg 库来录制视频。
Qt 中集成 ffmpeg 库来录制视频
首先在网上下载编译完成的 ffmpeg 库,博主用的是 Qt 5.15.2 和 vs2019
百度网盘下载链接:ffmpeg(库文件)
提取码: cqwx
把库集成到 Qt 中:
INCLUDEPATH += $$PWD/ffmpeg/include
LIBS += -L$$PWD/ffmpeg/lib/ -lavcodec
LIBS += -L$$PWD/ffmpeg/lib/ -lavdevice
LIBS += -L$$PWD/ffmpeg/lib/ -lavfilter
LIBS += -L$$PWD/ffmpeg/lib/ -lavformat
LIBS += -L$$PWD/ffmpeg/lib/ -lavutil
LIBS += -L$$PWD/ffmpeg/lib/ -lpostproc
LIBS += -L$$PWD/ffmpeg/lib/ -lswresample
LIBS += -L$$PWD/ffmpeg/lib/ -lswscale
实现拍照和录屏的功能:
extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
}
void showErrorInfo(int ret)
{
static char errbuf[AV_ERROR_MAX_STRING_SIZE];
memset(errbuf, 0, AV_ERROR_MAX_STRING_SIZE);
av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE);
qDebug() << "Error info:" << errbuf;
}
void videoRecord()
{
avformat_network_init();
avdevice_register_all();
// 遍历所有设备类型
// const AVInputFormat *iformat = nullptr;
// while ((iformat = av_input_audio_device_next(iformat))) {
// qDebug() << "Audio input device: " << iformat->name << " - " << iformat->long_name;
// }
// iformat = nullptr;
// while ((iformat = av_input_video_device_next(iformat))) {
// qDebug() << "Video input device: " << iformat->name << " - " << iformat->long_name;
// }
const AVInputFormat *iformat_dshow = av_find_input_format("dshow");
if (!iformat_dshow)
{
qDebug() << "Could not find input format !";
return;
}
// 用于存储设备列表的上下文
AVDeviceInfoList *deviceList = nullptr;
int ret = avdevice_list_input_sources(iformat_dshow, nullptr, nullptr, &deviceList);
if (ret < 0) {
qDebug() << "Could not list input sources !";
showErrorInfo(ret);
return;
}
// 设备名称
std::string deviceName = "";
// 遍历设备列表
// qDebug() << "Available DirectShow devices:";
for (int i = 0; i < deviceList->nb_devices; ++i)
{
AVDeviceInfo *device = deviceList->devices[i];
// qDebug() << "Device" << i << ":" << device->device_description << "(" << device->device_name << ")";
// 获取绿联采集卡的设备名称
QString description = QString(device->device_description);
if ( description.contains("UGREEN") ) {
deviceName = "video=" + description.toStdString();
break;
}
}
// 释放设备列表
avdevice_free_list_devices(&deviceList);
//------------------------------
// 创建 AVFormatContext
AVFormatContext *inputFormatContext = avformat_alloc_context();
if (!inputFormatContext) {
qDebug() << "Could not allocate AVFormatContext !";
return;
}
// 设置附加参数
AVDictionary *options = nullptr;
int framerate = 30;
av_dict_set(&options, "rtbufsize", "100M", 0); // 设置缓冲区大小
av_dict_set(&options, "framerate", "30", 0); // 设置帧率
// 打开输入设备
ret = avformat_open_input(&inputFormatContext, deviceName.c_str(), iformat_dshow, &options);
if (ret < 0) {
qDebug() << "Could not open input device !";
showErrorInfo(ret);
return;
}
// 打印输入设备的信息
// av_dump_format(inputFormatContext, 0, deviceName.c_str(), 0);
// 查找输入流信息
ret = avformat_find_stream_info(inputFormatContext, nullptr);
if (ret < 0) {
qDebug() << "Could not find stream information !";
showErrorInfo(ret);
return;
}
// 查找视频流
int videoStreamIndex = -1;
for (unsigned int i = 0; i < inputFormatContext->nb_streams; i++)
{
if (inputFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoStreamIndex = i;
break;
}
}
if (videoStreamIndex == -1) {
qDebug() << "Could not find video stream !";
return;
}
// 获取视频流参数
AVCodecParameters *inputCodecParameters = inputFormatContext->streams[videoStreamIndex]->codecpar;
// 查找输入解码器
const AVCodec *inputCodec = avcodec_find_decoder(inputCodecParameters->codec_id);
if (!inputCodec) {
qDebug() << "Could not find codec !";
return ;
}
// 创建输入解码器上下文
AVCodecContext *inputCodecContext = avcodec_alloc_context3(inputCodec);
if (!inputCodecContext) {
qDebug() << "Could not allocate codec context !";
return;
}
// 将视频流参数复制到输入解码器上下文
ret = avcodec_parameters_to_context(inputCodecContext, inputCodecParameters);
if (ret < 0) {
qDebug() << "Could not copy codec parameters to context !";
showErrorInfo(ret);
return;
}
// 打开输入解码器
ret = avcodec_open2(inputCodecContext, inputCodec, nullptr);
if (ret < 0) {
qDebug() << "Could not open codec !";
showErrorInfo(ret);
return;
}
//------------------------------
// 分配帧和数据包
AVFrame *frame = av_frame_alloc();
AVPacket *packet = av_packet_alloc();
if (!frame || !packet) {
qDebug() << "Could not alloc frame and packet !";
return;
}
// 分配图像转换上下文
SwsContext *swsContext = sws_getContext(inputCodecContext->width, inputCodecContext->height, inputCodecContext->pix_fmt,
inputCodecContext->width, inputCodecContext->height, AV_PIX_FMT_RGB24,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!swsContext) {
qDebug() << "Could not initialize SwsContext !";
return;
}
// 分配 RGB 帧
AVFrame *rgbFrame = av_frame_alloc();
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, inputCodecContext->width, inputCodecContext->height, 1);
uint8_t *buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, buffer, AV_PIX_FMT_RGB24,
inputCodecContext->width, inputCodecContext->height, 1);
// 读取帧数据(保存10张图片)
int frameCount = 0;
while (av_read_frame(inputFormatContext, packet) >= 0 && frameCount < 11)
{
// 确保帧的数据类型和解码器的数据类型一致
if (packet->stream_index == videoStreamIndex)
{
// 发送数据包到解码器
ret = avcodec_send_packet(inputCodecContext, packet);
if (ret < 0) {
qDebug() << "Error sending packet to decoder !";
showErrorInfo(ret);
continue;
}
// 接收解码后的帧
while (avcodec_receive_frame(inputCodecContext, frame) == 0)
{
// 舍弃第一帧
if ( frameCount == 0 ) {
frameCount++;
continue;
}
// 转换图像格式
sws_scale(swsContext, frame->data, frame->linesize, 0, inputCodecContext->height,
rgbFrame->data, rgbFrame->linesize);
// 创建 QImage
QImage image(rgbFrame->data[0], inputCodecContext->width, inputCodecContext->height, QImage::Format_RGB888);
// 保存图像
QString fileName = QString("frame_%1.jpg").arg(frameCount++);
if (!image.save(fileName)) {
qDebug() << "Error saving image:" << fileName;
} else {
qDebug() << "Image saved:" << fileName;
}
}
}
av_packet_unref(packet);
}
//------------------------------
// 输出格式上下文
AVFormatContext *outputFormatContext = nullptr;
const char *outputFileName = "output.mp4";
// 创建输出格式上下文
ret = avformat_alloc_output_context2(&outputFormatContext, nullptr, nullptr, outputFileName);
if (!outputFormatContext) {
qDebug() << "Could not create output context !";
showErrorInfo(ret);
return;
}
// 查找输出编码器
const AVCodec *outputCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!outputCodec) {
qDebug() << "Could not find output codec !";
return;
}
// 创建输出流
AVStream *outputStream = avformat_new_stream(outputFormatContext, outputCodec);
if (!outputStream) {
qDebug() << "Could not create output stream !";
return;
}
// 打开输出编码器上下文
AVCodecContext *outputCodecContext = avcodec_alloc_context3(outputCodec);
if (!outputCodecContext) {
qDebug() << "Could not allocate output codec context !";
return;
}
// 设置输出编码器参数
outputCodecContext->codec_id = outputCodec->id;
outputCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
outputCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
outputCodecContext->width = 1920; // 设置分辨率
outputCodecContext->height = 1080;
outputCodecContext->time_base = {1, framerate}; // 设置帧率
outputCodecContext->framerate = {framerate, 1};
outputCodecContext->bit_rate = 2000000; // 设置比特率
outputCodecContext->rc_buffer_size = 2 * outputCodecContext->bit_rate; // 设置缓冲区大小为码率的两倍
outputCodecContext->gop_size = 10; // 设置关键帧间隔
outputCodecContext->max_b_frames = 1;
if (outputFormatContext->oformat->flags & AVFMT_GLOBALHEADER) {
outputCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
// 打开输出编码器
ret = avcodec_open2(outputCodecContext, outputCodec, nullptr);
if (ret < 0) {
qDebug() << "Could not open output codec !";
showErrorInfo(ret);
return;
}
// 复制输出编码器参数到输出流
avcodec_parameters_from_context(outputStream->codecpar, outputCodecContext);
outputStream->time_base = outputCodecContext->time_base;
//------------------------------
// 打开输出文件
if (!(outputFormatContext->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&outputFormatContext->pb, outputFileName, AVIO_FLAG_WRITE);
if (ret < 0) {
qDebug() << "Could not open output file !";
showErrorInfo(ret);
return;
}
}
// 写入文件头
ret = avformat_write_header(outputFormatContext, nullptr);
if (ret < 0) {
qDebug() << "Could not write header !";
showErrorInfo(ret);
return;
}
// 分配帧和数据包
AVFrame *inputFrame = av_frame_alloc();
AVPacket *inputPacket = av_packet_alloc();
AVFrame *outputFrame = av_frame_alloc();
AVPacket *outputPacket = av_packet_alloc();
if (!inputFrame || !inputPacket || !outputFrame || !outputPacket) {
qDebug() << "Could not alloc frame and packet !";
return;
}
// 分配图像转换上下文
SwsContext *swsContext2 = sws_getContext(inputCodecContext->width, inputCodecContext->height, inputCodecContext->pix_fmt,
outputCodecContext->width, outputCodecContext->height, outputCodecContext->pix_fmt,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!swsContext2) {
qDebug() << "Could not initialize SwsContext !";
return;
}
// 分配输出帧数据
outputFrame->format = AV_PIX_FMT_YUV420P;
outputFrame->width = inputCodecContext->width;
outputFrame->height = inputCodecContext->height;
ret = av_frame_get_buffer(outputFrame, 0);
if (ret < 0) {
qDebug() << "Could not get frame buffer !";
showErrorInfo(ret);
return;
}
// 录屏60秒,帧率30,保存1800帧数据
int frameCount2 = 0;
while (av_read_frame(inputFormatContext, inputPacket) >= 0 && frameCount2 < 1800)
{
// 确保帧的数据类型和解码器的数据类型一致
if (inputPacket->stream_index == videoStreamIndex)
{
// 发送输入数据包到解码器
ret = avcodec_send_packet(inputCodecContext, inputPacket);
if (ret < 0) {
qDebug() << "Error sending packet to decoder !";
showErrorInfo(ret);
continue;
}
// 接收解码后的帧
while (avcodec_receive_frame(inputCodecContext, inputFrame) == 0)
{
// 转换图像格式
sws_scale(swsContext2, inputFrame->data, inputFrame->linesize, 0, inputCodecContext->height,
outputFrame->data, outputFrame->linesize);
outputFrame->pts = frameCount2++;
qDebug() << "frameCount2: " << frameCount2;
// 发送输出帧到输出编码器
ret = avcodec_send_frame(outputCodecContext, outputFrame);
if (ret < 0) {
qDebug() << "Error sending frame to encoder !";
showErrorInfo(ret);
continue;
}
// 接收编码后的数据包
while (avcodec_receive_packet(outputCodecContext, outputPacket) == 0)
{
av_packet_rescale_ts(outputPacket, outputCodecContext->time_base, outputStream->time_base);
outputPacket->stream_index = outputStream->index;
// 写入数据包到输出文件
ret = av_interleaved_write_frame(outputFormatContext, outputPacket);
if (ret < 0) {
qDebug() << "Error writing packet to output file";
showErrorInfo(ret);
continue;
}
av_packet_unref(outputPacket);
}
}
}
av_packet_unref(inputPacket);
}
// 刷新编码器
avcodec_send_frame(outputCodecContext, nullptr);
while (avcodec_receive_packet(outputCodecContext, outputPacket) == 0)
{
av_packet_rescale_ts(outputPacket, outputCodecContext->time_base, outputStream->time_base);
outputPacket->stream_index = outputStream->index;
ret = av_interleaved_write_frame(outputFormatContext, outputPacket);
if (ret < 0) {
qDebug() << "Error writing packet to output file";
showErrorInfo(ret);
}
av_packet_unref(outputPacket);
}
// 写入文件尾
av_write_trailer(outputFormatContext);
// 释放资源
av_frame_free(&inputFrame);
av_frame_free(&outputFrame);
av_packet_free(&inputPacket);
av_packet_free(&outputPacket);
sws_freeContext(swsContext2);
avcodec_free_context(&outputCodecContext);
avformat_free_context(outputFormatContext);
if (outputFormatContext && !(outputFormatContext->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&outputFormatContext->pb);
}
av_free(buffer);
av_frame_free(&rgbFrame);
av_frame_free(&frame);
av_packet_free(&packet);
sws_freeContext(swsContext);
avcodec_free_context(&inputCodecContext);
avformat_close_input(&inputFormatContext);
avformat_free_context(inputFormatContext);
av_dict_free(&options);
avformat_network_deinit();
}