尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Qt + FFmpeg 视频工具:视频一键压缩功能实现

Qt + FFmpeg 视频工具:视频一键压缩功能实现
📅 发布时间:2026/7/12 20:23:37

概述

视频文件在日常使用中经常面临体积过大的问题:手机录制的 4K 视频、屏幕录制的原始文件、从网上下载的高码率素材,动辄几百 MB 甚至几个 GB。对于非专业用户来说,打开 HandBrake 或 FFmpeg 命令行去调参数是一件有门槛的事情。

"一键压缩"功能要解决的问题是:让用户不需要理解 CRF、preset、码率、分辨率这些概念,只需在三个预设模式中选择一个,点一下按钮就能完成视频压缩。

三档预设覆盖了三类典型场景:

模式CRF分辨率音频码率适用场景
清晰优先18原始192 kbps存档、高画质需求
平衡23原始128 kbps日常推荐
小体积28720p (宽 1280)96 kbps手机分享、网盘存储

核心思路:底层是完整的 FFmpeg 转码管线(解码 → 可选 sws_scale 缩放 → H.264 重编码 → AAC 音频 → MP4 封装),上层通过VideoTranscoder纯工具类暴露 CRF、分辨率、音频码率等参数,MediaAnalyzer负责异步调度和状态管理,QML 页面只负责预设选择和结果展示。

实测数据:
原始文件 298MB,三种压缩模式输出文件分别是:151MB、52.8MB、7.8MB


功能流程

用户选择预设模式(清晰优先 / 平衡 / 小体积) │ ▼ QML 页面调用 mediaAnalyzer.compressCurrentVideo(filePath, crf, w, h, ...) │ ▼ MediaAnalyzer 入口校验 → 清空旧结果 → setBusy(true) → runAsync() │ ▼ 后台线程:VideoTranscoder::transcode() │ ├─ 打开输入容器,找到最佳视频/音频流 ├─ 创建输出容器,配置 H.264 编码器(CRF + preset medium) ├─ 配置音频编码器(AAC + 目标码率) ├─ 如目标分辨率 ≠ 源分辨率,初始化 sws_scale 缩放上下文 ├─ 如音频采样格式/采样率不匹配,初始化 SwrContext 重采样 ├─ 主循环:av_read_frame → send_packet → receive_frame → 编码 → write_frame ├─ Flush 解码器 + 编码器内部缓存 ├─ 写 trailer,关闭容器 └─ 组装 transcodeInfo(编码器、分辨率、压缩率、耗时等) │ ▼ 主线程回调:setCompressVideoInfo(info) → 状态栏 + InfoPanel 更新

核心算法

CRF 恒定质量因子

CRF(Constant Rate Factor)是 x264/x265 编码器最核心的质量控制参数,范围 0~51:

  • 0:无损编码,体积甚至可能大于源文件
  • 18:视觉无损,人眼几乎看不出与源文件的差异
  • 23:x264 默认值,画质与体积的平衡点
  • 28:明显降低体积,画质有一定损失但可接受
  • 51:最差质量,基本不可用

一键压缩通过暴露三个离散的 CRF 值,让用户在"画质"和"体积"之间做简单选择,而不是面对一个滑块不知所措。

分辨率缩放与偶数对齐

"小体积"模式会将宽度降到 1280(720p),高度按源视频宽高比自动计算。H.264 编码器要求宽高必须是偶数,因此计算结果需要& ~1对齐:

// 计算目标分辨率:宽高自动计算并对齐偶数voidcomputeTargetResolution(intsourceWidth,intsourceHeight,int&targetWidth,int&targetHeight){if(targetWidth<=0&&targetHeight<=0){// 两个维度都未指定,保留原始尺寸targetWidth=sourceWidth;targetHeight=sourceHeight;return;}if(targetWidth>0&&targetHeight>0){// 两个维度都指定了,直接使用(对齐偶数)targetWidth=targetWidth&~1;targetHeight=targetHeight&~1;return;}// 只指定了一个维度,按原始宽高比计算另一个维度constdoubleaspect=(sourceHeight>0)?static_cast<double>(sourceWidth)/sourceHeight:1.0;if(targetWidth>0){targetWidth=targetWidth&~1;targetHeight=static_cast<int>(targetWidth/aspect+0.5);targetHeight=(targetHeight>0)?(targetHeight&~1):2;}else{targetHeight=targetHeight&~1;targetWidth=static_cast<int>(targetHeight*aspect+0.5);targetWidth=(targetWidth>0)?(targetWidth&~1):2;}}

音频 AudioFIFO + SwrContext

解码出的音频帧采样格式和采样率往往与 AAC 编码器要求不一致(例如源是 fltp/48kHz,AAC 需要 fltp/44.1kHz)。处理流程是:

  1. 用SwrContext做采样格式转换和采样率重采样
  2. 转换后的样本写入AVAudioFifo
  3. FIFO 攒够一个frame_size的样本后,取出并送入编码器
  4. 尾部不足一帧的样本在 flush 阶段补齐编码

这个模式保证了无论源音频参数如何,编码器始终收到格式正确、大小一致的帧。

后端工具类:VideoTranscoder

VideoTranscoder是纯工具类,无状态、无 UI 依赖、无当前文件依赖。它只接收输入路径和编码参数,输出压缩后的视频文件。

类声明

classVideoTranscoder{public:// 对整个视频执行转码压缩//// crf: 恒定质量因子 0~51,值越小画质越高// targetWidth/targetHeight: 目标分辨率,都为 0 时保留原始尺寸// audioBitrateKbps: 音频码率(kbps),0 表示编码器默认// keepAudio: 是否保留音频流// copyMetadata: 是否复制 metadata// transcodeInfo: 成功时返回结果摘要booltranscode(constQString&inputPath,constQString&outputPath,intcrf,inttargetWidth,inttargetHeight,intaudioBitrateKbps,boolkeepAudio,boolcopyMetadata,QVariantMap*transcodeInfo,QString*errorText)const;};

编码器参数配置

视频编码器的参数配置是压缩质量的关键。preset medium保持编码速度和压缩率的平衡,max_b_frames = 0简化 PTS/DTS 顺序避免 B 帧重排问题,gop_size按实际帧率动态计算:

videoEncCtx->width=encWidth;videoEncCtx->height=encHeight;videoEncCtx->pix_fmt=chooseVideoPixelFormat(videoEncoder,videoDecCtx->pix_fmt);videoEncCtx->time_base=videoTimeBase;videoEncCtx->framerate=frameRate;videoEncCtx->gop_size=qMax(12,frameRate.num>0&&frameRate.den>0?frameRate.num/frameRate.den:12);videoEncCtx->max_b_frames=0;// CRF 和 preset 只对 x264/x265 等支持私有选项的编码器生效av_opt_set(videoEncCtx->priv_data,"preset","medium",0);av_opt_set_int(videoEncCtx->priv_data,"crf",safeCrf,0);

视频帧解码 → 缩放 → 编码

每解码一帧后,检查是否需要 sws_scale 转换。条件触发——只有当源分辨率或像素格式与编码器不一致时才初始化缩放上下文,避免不必要的内存分配:

if(videoDecCtx->width!=videoEncCtx->width||videoDecCtx->height!=videoEncCtx->height||videoDecCtx->pix_fmt!=videoEncCtx->pix_fmt){if(!swsCtx){swsCtx=sws_getContext(videoDecCtx->width,videoDecCtx->height,videoDecCtx->pix_fmt,videoEncCtx->width,videoEncCtx->height,videoEncCtx->pix_fmt,SWS_BILINEAR,nullptr,nullptr,nullptr);}// 分配可写的编码帧缓冲av_frame_make_writable(videoEncodeFrame);sws_scale(swsCtx,decodedFrame->data,decodedFrame->linesize,0,videoDecCtx->height,videoEncodeFrame->data,videoEncodeFrame->linesize);frameForEncoder=videoEncodeFrame;}

时间戳处理方面,整视频转码不需要裁剪偏移,第一帧直接作为时间轴零点:

if(firstVideoPts==AV_NOPTS_VALUE)firstVideoPts=framePts!=AV_NOPTS_VALUE?framePts:0;frameForEncoder->pts=framePts!=AV_NOPTS_VALUE?av_rescale_q(framePts-firstVideoPts,inVideoStream->time_base,videoEncCtx->time_base):encodedVideoFrames;

主读取循环

标准的 send/receive 模型:逐 packet 读取,按流索引分发到对应解码器,取出帧后立即编码。视频和音频交替处理,保证 FIFO 中的数据及时被编码写出:

while(!reachedEof){ret=av_read_frame(inCtx,packet);if(ret==AVERROR_EOF){reachedEof=true;break;}if(packet->stream_index==videoStreamIndex){avcodec_send_packet(videoDecCtx,packet);receiveVideoFrames();// 解码 → 缩放 → 编码 → write_frame}elseif(audioDecCtx&&packet->stream_index==audioStreamIndex){avcodec_send_packet(audioDecCtx,packet);receiveAudioFrames();// 解码 → 重采样 → FIFO → 编码 → write_frame}av_packet_unref(packet);}// Flush 解码器和编码器内部缓存avcodec_send_packet(videoDecCtx,nullptr);receiveVideoFrames();avcodec_send_packet(audioDecCtx,nullptr);receiveAudioFrames();// Flush SwrContext 内部延迟样本 → FIFO → 编码

导出封装:MediaAnalyzer 异步调度

MediaAnalyzer作为 QML 门面层,负责入口校验、路径处理、异步调度和状态更新。转码是耗时操作,必须放到后台线程,避免阻塞 UI:

boolMediaAnalyzer::compressCurrentVideo(constQString&filePath,intcrf,inttargetWidth,inttargetHeight,intaudioBitrateKbps,boolkeepAudio,boolcopyMetadata){if(m_currentFile.isEmpty()){setStatus(QStringLiteral("请先导入视频文件"));setCompressVideoInfo(QVariantMap());returnfalse;}// 处理输出路径:支持 file:// URL 和普通路径QString outputPath=filePath.trimmed();if(outputPath.startsWith(QStringLiteral("file:"),Qt::CaseInsensitive))outputPath=QUrl(outputPath).toLocalFile();if(outputPath.isEmpty())outputPath=defaultCompressedVideoPath();if(QFileInfo(outputPath).suffix().isEmpty())outputPath+=QStringLiteral(".mp4");setCompressVideoInfo(QVariantMap());setBusy(true);setStatus(QStringLiteral("正在压缩视频..."));// 转码是耗时操作,必须放到后台线程执行runAsync([=]()->AsyncResult{AsyncResult r;QVariantMap result;r.ok=transcoder.transcode(sourceFile,outputPath,crf,targetWidth,targetHeight,audioBitrateKbps,keepAudio,copyMetadata,&result,&r.errorText);if(r.ok){r.info=result;r.outputPath=outputPath;}returnr;},[this](constAsyncResult&r){if(r.ok){setCompressVideoInfo(r.info);setStatus(QStringLiteral("视频压缩完成:%1").arg(r.outputPath));}else{setCompressVideoInfo(QVariantMap());setStatus(QStringLiteral("视频压缩失败:%1").arg(r.errorText));}});returntrue;}

QML 页面:三档预设选择

VideoCompressPage.qml用三个可点击的预设卡片横排展示,用户点选后高亮当前预设。每个预设包含名称、CRF 标签、描述文本,底部显示当前参数摘要:

// 压缩预设定义 property var compressPresets: [ { name: "清晰优先", crf: 18, targetWidth: 0, targetHeight: 0, audioBitrateKbps: 192, description: "CRF 18,保留原始分辨率,画质接近无损。" }, { name: "平衡", crf: 23, targetWidth: 0, targetHeight: 0, audioBitrateKbps: 128, description: "CRF 23,保留原始分辨率,画质与体积均衡。" }, { name: "小体积", crf: 28, targetWidth: 1280, targetHeight: 0, audioBitrateKbps: 96, description: "CRF 28,分辨率降至 720p,适合手机分享。" } ] property int currentPresetIndex: 1 // 默认"平衡" // 开始压缩按钮 ActionButton { text: "开始压缩" accent: true onClicked: mediaAnalyzer.compressCurrentVideo( outputPathField.text.trim(), currentPreset().crf, currentPreset().targetWidth, currentPreset().targetHeight, keepAudioBox.checked ? currentPreset().audioBitrateKbps : 0, keepAudioBox.checked, copyMetadataBox.checked) }

结果面板通过InfoPanel展示压缩结果,包含编码器、分辨率、压缩率、耗时等信息,数据由MainWindow.qml的refreshVideoCompressRows()从compressVideoInfo映射生成。

状态清理

当前文件切换或重新导入时,MediaAnalyzer::openFile()会调用setCompressVideoInfo(QVariantMap())清空旧的压缩结果,避免 UI 显示过期数据。QML 页面通过Connections监听onCurrentFileChanged和onFileOpened信号,同步刷新默认输出路径。

小结

视频一键压缩的实现要点:

  1. 分层架构:VideoTranscoder(纯算法)→MediaAnalyzer(状态 + 异步调度)→VideoCompressPage.qml(交互),职责分离清晰。
  2. CRF 驱动:不暴露码率参数,用 CRF 控制质量,让编码器自行分配码率,对用户更友好。
  3. 条件缩放:只在目标分辨率与源不一致时初始化 sws_scale,原始尺寸模式零额外开销。
  4. AudioFIFO 攒帧:解决解码帧大小与编码器frame_size不匹配的问题,尾部样本在 flush 阶段补齐。
  5. 三档预设:把复杂的编码参数收敛为三个直觉化选项,降低使用门槛。

相关新闻

  • 2026年7月最新绍兴爱彼官方售后维修服务网点地址与客服电话 - 爱彼中国官方服务中心
  • OAS-Kit命令行工具完全指南:从基础到高级用法
  • V4L2 --- Linux 视频采集协议概览

最新新闻

  • Python毕业设计实战:Django搭建的电影推荐网站,含协同过滤算法与可视化界面
  • 欧盟强制新车配备驾驶员监控摄像头:技术实现与隐私博弈的深度解析
  • 无锡卖 LV 包包必看!这几个配件留好,回收多赚几千 - 全城热点
  • Unity VR开发入门:Oculus集成包核心功能与实战指南
  • 多Agent系统上线首周:我们为任务调度交的4笔学费
  • Unity NetCode位置同步实战:从权威模式到预测调和,解决联机游戏同步难题

日新闻

  • AI推荐结果怎么优化:适合深圳少儿素质培训机构的GEO服务商哪家好?全程零代码SAAS操作
  • RAG 实战教学完全指南
  • 企业级API网关架构深度解析:IBM Microgateway的技术实现与选型指南

周新闻

  • IX9104 PCIe5.0 高速交换芯片@ACP#完整规格 + 应用场景总结
  • Unity游戏集成Coze智能体:实现NPC智能对话与知识库联动
  • SAP EPIC 建行回单查询:从标准类CL_EPIC_EXAMPLE_CN_CCB_GHTD到Z类的5处关键修改

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号