
1 帧率的本质
1.1 什么是帧率?
帧率(Frame Rate) 是指单位时间内显示、捕获或处理的图像帧数,通常以 FPS(Frames Per Second,帧/秒)为单位。帧率的本质是时间与图像序列的映射关系,它定义了动态图像的时间维度。
从数学角度看,帧率可以表示为:
帧率 = 总帧数 / 时间跨度
时间间隔 = 1 / 帧率
例如:30 fps 意味着每帧间隔约 33.33 毫秒,60 fps 则每帧间隔约 16.67 毫秒。
1.2 帧率的三个维度
采样维度:从连续信号中提取离散样本的频率
播放维度:离散样本被重建为连续感知的速度
同步维度:多个数据流(音视频)在时间轴上的对齐基准
1.3 不同帧率的感知差异
| 帧率 | 应用场景 | 视觉感受 | 典型用途 |
|---|---|---|---|
| 24 fps | 电影标准 | 略有抖动,有"电影感" | 好莱坞电影、艺术片 |
| 25/30 fps | 视频标准 | 基本流畅 | 电视广播、YouTube视频 |
| 50/60 fps | 高清视频 | 明显流畅 | 体育直播、高质量网络视频 |
| 120 fps | 高帧率 | 极度流畅 | 慢动作素材、电竞游戏 |
| 144/240 fps | 超高帧率 | 丝般顺滑 | 专业电竞、高刷新率显示器 |
人眼的帧率感知能力:
- 人眼能感知的最低"流畅"帧率约为 24 fps
- 专业游戏玩家可以分辨 60 fps 与 144 fps 的差异
- 理论上人眼可以感知 500-1000 fps 的差异(在特定条件下)
2 帧率在各场景中的关键作用
2.1 场景一:视频采集 - 帧率是"捕捉现实的速度"
1. 屏幕捕获
帧率的作用:决定能否流畅记录屏幕上的动态内容
应用场景:
├─ 教学录制:30fps(足够展示操作)
├─ 游戏直播:60fps(流畅展示游戏画面)
└─ 演示文稿:15fps(静态内容为主,节省资源)
关键考量因素:
-
屏幕刷新率与采集帧率的关系
显示器:144Hz 采集设置:30fps → 每采集一帧,显示器已经刷新了约5次 结果:可能丢失快速运动的细节 建议:采集帧率 ≥ 显示器刷新率的 1/2 -
CPU负载平衡
低帧率策略(如15fps): ✓ CPU占用低(约5-10%) ✓ 文件体积小 ✗ 鼠标移动不流畅 高帧率策略(如60fps): ✓ 画面极度流畅 ✗ CPU占用高(可达30-50%) ✗ 文件体积大4倍 -
实际案例分析
场景:录制代码编辑过程 错误配置: - 60fps 录制 - 1小时素材 = 50GB - 90%的时间只是静态文字 优化配置: - 15fps 录制 - 1小时素材 = 5GB - 视觉效果几乎无差别(打字、滚动都很慢)
2. 摄像头采集
帧率的作用:平衡画质与传输带宽
USB带宽限制的实际影响:
USB 2.0(480 Mbps 理论值,实际约 35 MB/s)
├─ 1080p@30fps YUV422 ≈ 124 MB/s → 不可能
├─ 1080p@30fps MJPEG ≈ 15 MB/s → 可行但质量损失
├─ 720p@60fps YUV422 ≈ 55 MB/s → 不可能
└─ 720p@60fps MJPEG ≈ 8 MB/s → 可行
USB 3.0(5 Gbps 理论值,实际约 400 MB/s)
├─ 1080p@60fps YUV422 ≈ 248 MB/s → 可行
└─ 4K@30fps YUV422 ≈ 497 MB/s → 勉强可行
帧率选择的实际考量:
| 应用场景 | 推荐帧率 | 原因 |
|---|---|---|
| 视频会议 | 15-24 fps | 人脸运动慢,节省带宽 |
| 直播聊天 | 30 fps | 平衡流畅度与传输 |
| 运动分析 | 60-120 fps | 捕捉快速动作细节 |
| 安防监控 | 5-15 fps | 长时间存储,事件触发 |
多摄像头的帧率协调:
场景:双摄像头直播系统
配置A(失败案例):
├─ 主摄像头:1080p@60fps
├─ 特写摄像头:1080p@60fps
└─ 共享USB控制器 → 带宽不足 → 实际降到30fps且卡顿
配置B(成功案例):
├─ 主摄像头:1080p@30fps(主画面)
├─ 特写摄像头:720p@30fps(小窗口)
└─ 分别使用不同USB控制器 → 稳定运行
3. 游戏捕获
帧率的作用:决定能否真实还原游戏体验
游戏渲染帧率 vs 捕获帧率的关系:
情况1:游戏120fps,捕获60fps
→ 每2帧选1帧捕获
→ 观众看到:流畅但略逊于玩家体验
情况2:游戏60fps,捕获30fps
→ 每2帧选1帧捕获
→ 观众看到:明显不流畅,快速转身会卡顿
情况3:游戏30fps(性能不足),捕获60fps
→ 需要重复帧填充
→ 观众看到:帧率数字高,但实际不流畅
帧率与游戏类型的匹配:
| 游戏类型 | 推荐捕获帧率 | 关键考量 |
|---|---|---|
| FPS竞技游戏 | 60 fps | 快速转身、瞄准需要高帧率 |
| MOBA游戏 | 30-60 fps | 团战时帧率波动大 |
| RPG/剧情游戏 | 30 fps | 运动相对缓慢 |
| 赛车游戏 | 60 fps | 高速运动需要流畅感 |
| 回合制游戏 | 15-30 fps | 大量静态画面 |
2.2 场景二:视频编码 - 帧率是"时间的标尺"
1. H.264/HEVC 编码中的帧率
帧率的三个关键作用:
A. 码率分配的基准
编码原理:
- 可用码率 = 5 Mbps
- 视频时长 = 10 秒
30fps配置:
├─ 总帧数 = 300 帧
├─ 每帧平均码率 = 5 Mbps ÷ 300 = 16.7 Kbps/帧
└─ 画质:较好
60fps配置:
├─ 总帧数 = 600 帧
├─ 每帧平均码率 = 5 Mbps ÷ 600 = 8.3 Kbps/帧
└─ 画质:下降(每帧分配的数据量减半)
结论:相同码率下,帧率越高,每帧质量越低
B. GOP(关键帧组)结构的控制
GOP结构示例(30fps):
标准配置:GOP Size = 60
├─ I帧间隔 = 60帧 = 2秒
└─ 结构:I-B-B-P-B-B-P-...-P(重复)
帧率翻倍(60fps)但GOP不变:
├─ I帧间隔 = 60帧 = 1秒
└─ 问题:I帧太频繁,码率浪费
正确做法(60fps):
├─ GOP Size = 120
├─ I帧间隔 = 120帧 = 2秒
└─ 保持相同的关键帧时间间隔
C. 运动估计的精度
运动补偿原理:
低帧率(15fps):
帧间隔 = 66.7ms
├─ 快速运动物体位移大
├─ 编码器难以预测
└─ 需要更多码率描述差异
高帧率(60fps):
帧间隔 = 16.7ms
├─ 相邻帧差异小
├─ 运动预测准确
└─ 压缩效率高
实际数据对比(相同场景):
├─ 15fps:需要 8 Mbps 达到良好画质
└─ 60fps:只需 10 Mbps 达到相同画质
(虽然帧数4倍,但压缩效率提升)
2. MPEG-TS 封装中的帧率
帧率的作用:定义时间戳的解释规则
时间戳机制:
配置:30 fps
├─ time_base = 1/30 秒
└─ 每个时间戳单位 = 33.33ms
实际编码过程:
帧1:timestamp = 0 → PTS = 0
帧2:timestamp = 33.33ms → PTS = 1
帧3:timestamp = 66.67ms → PTS = 2
...
播放器解析:
PTS = 60 → 播放时刻 = 60 × (1/30) = 2秒
封装格式的帧率兼容性:
| 封装格式 | 帧率支持 | 特点 |
|---|---|---|
| MP4 | CFR/VFR | 支持可变帧率但兼容性一般 |
| MKV | VFR优秀 | 原生支持可变帧率 |
| FLV | CFR only | 只支持固定帧率 |
| MPEG-TS | CFR优先 | 广播标准,要求严格 |
2.3 场景三:视频处理 - 帧率是"处理节奏"
1. 滤镜处理中的帧率
帧率决定了处理负载和效果质量
实时滤镜处理的性能考量:
场景:添加美颜滤镜
30fps配置:
├─ 每帧处理时间预算 = 33.33ms
├─ 美颜算法耗时 = 15ms
├─ 剩余时间 = 18.33ms(充裕)
└─ 结果:流畅运行
60fps配置:
├─ 每帧处理时间预算 = 16.67ms
├─ 美颜算法耗时 = 15ms
├─ 剩余时间 = 1.67ms(紧张)
└─ 结果:偶尔丢帧
120fps配置:
├─ 每帧处理时间预算 = 8.33ms
├─ 美颜算法耗时 = 15ms
└─ 结果:无法实时处理
滤镜复杂度与帧率的权衡:
处理策略选择:
策略A:降低帧率
30fps → 15fps
优点:处理时间翻倍(66.67ms/帧)
缺点:画面不够流畅
策略B:降低滤镜质量
保持30fps,简化算法
优点:保持流畅度
缺点:效果打折扣
策略C:选择性处理
检测场景复杂度
├─ 简单场景(静态)→ 30fps全处理
└─ 复杂场景(运动)→ 15fps处理 + 帧插值
策略D:异步处理
解耦采集与处理帧率
├─ 采集:30fps
├─ 处理:实际10fps
└─ 显示:重复上一个处理结果
2. 同步调整中的帧率
帧率是音视频对齐的时间基准
音视频同步的帧率影响:
视频:30fps(帧间隔 33.33ms)
音频:48kHz(采样间隔 0.021ms)
同步精度分析:
├─ 理论最小调整单位 = 1视频帧 = 33.33ms
├─ 音频可以精确到 1采样 = 0.021ms
└─ 同步精度受限于视频帧率
实际问题:
场景:音频延迟 50ms
30fps视频调整:
├─ 延迟1帧 = 33.33ms(不够)
├─ 延迟2帧 = 66.67ms(过头)
└─ 无法精确补偿
60fps视频调整:
├─ 延迟3帧 = 50ms
└─ 可以精确补偿
帧率与同步容差:
| 视频帧率 | 帧间隔 | 可接受音画偏差 | 同步难度 |
|---|---|---|---|
| 15 fps | 66.67 ms | ±33 ms | 高 |
| 24 fps | 41.67 ms | ±20 ms | 中高 |
| 30 fps | 33.33 ms | ±16 ms | 中 |
| 60 fps | 16.67 ms | ±8 ms | 低 |
为什么高帧率更容易同步?
低帧率(15fps)场景:
├─ 音频延迟 60ms
├─ 调整1帧 = 66.67ms(过头)
├─ 需要额外音频拉伸/压缩技术
└─ 复杂度高
高帧率(60fps)场景:
├─ 音频延迟 60ms
├─ 调整3或4帧即可精确匹配
└─ 简单有效
2.4 场景四:实时渲染 - 帧率是"流畅度保证"
1. 图形界面刷新
帧率决定了用户交互的响应感受
界面响应的帧率阶梯:
10 fps:
├─ 鼠标光标移动有明显拖影
├─ 滚动列表有跳跃感
└─ 用户感受:非常卡顿
30 fps:
├─ 鼠标光标流畅
├─ 滚动基本顺滑
└─ 用户感受:可接受
60 fps:
├─ 界面响应即时
├─ 动画丝般顺滑
└─ 用户感受:流畅舒适
120 fps:
├─ 在高刷新率显示器上有优势
├─ 普通显示器看不出差别
└─ 用户感受:与60fps差别不大(普通场景)
实际应用中的帧率策略:
Windows资源管理器的帧率策略:
空闲状态:
├─ 刷新率:0 fps(完全静止)
└─ CPU占用:0%
窗口滚动:
├─ 刷新率:60 fps
└─ CPU占用:5-10%
文件拖拽:
├─ 刷新率:30 fps(足够流畅)
└─ CPU占用:3-5%
策略总结:按需刷新,节省资源
2. 游戏引擎渲染
帧率是游戏体验的核心指标
游戏类型与帧率需求:
第一人称射击游戏(FPS):
├─ 最低可玩:30 fps
├─ 流畅体验:60 fps
├─ 竞技标准:144 fps
└─ 原因:瞄准精度受帧率影响大
角色扮演游戏(RPG):
├─ 最低可玩:30 fps
├─ 流畅体验:30-60 fps
└─ 原因:运动速度慢,对帧率不敏感
格斗游戏:
├─ 标准:60 fps(固定)
└─ 原因:帧数直接影响判定(如3帧必杀)
音乐节奏游戏:
├─ 标准:60 fps
└─ 原因:节奏判定需要稳定帧时间
帧率稳定性比绝对值更重要:
场景对比:
配置A(不稳定高帧率):
├─ 平均帧率:60 fps
├─ 帧时间波动:16-50ms
└─ 玩家感受:卡顿、掉帧
配置B(稳定中帧率):
├─ 固定帧率:30 fps
├─ 帧时间稳定:33.33ms±0.5ms
└─ 玩家感受:流畅,无卡顿
结论:稳定的30fps > 波动的60fps
V-Sync(垂直同步)与帧率:
V-Sync的作用机制:
关闭V-Sync:
├─ 游戏渲染:75 fps
├─ 显示器刷新:60Hz
└─ 结果:画面撕裂(tearing)
开启V-Sync:
├─ 游戏渲染被限制在:60 fps
├─ 与显示器刷新同步
└─ 结果:无撕裂,但可能增加延迟
自适应V-Sync(G-Sync/FreeSync):
├─ 显示器刷新率匹配游戏帧率
├─ 范围:30-144Hz动态调整
└─ 结果:既无撕裂,延迟也低
3. 医疗设备图像显示(超声设备案例)
帧率直接影响诊断准确性和医生操作效率
超声成像的帧率链条:
探头采集 → 信号处理 → 图像重建 → 屏幕显示
↓ ↓ ↓ ↓
30ms 25ms 20ms 5ms
帧率不足的实际影响:
场景:心脏超声检查
15 fps配置:
├─ 帧间隔:66.7ms
├─ 心跳周期:800ms(75 bpm)
├─ 每个心跳捕获:12帧
└─ 问题:心脏瓣膜的快速开合可能漏掉
30 fps配置:
├─ 帧间隔:33.3ms
├─ 每个心跳捕获:24帧
└─ 效果:可以清晰看到瓣膜运动
60 fps配置(理想):
├─ 帧间隔:16.7ms
├─ 每个心跳捕获:48帧
└─ 效果:细节完美,但设备成本高
帧率与图像质量的权衡:
超声设备的参数配置矛盾:
高帧率模式(30fps):
├─ 探测深度:浅(10cm)
├─ 图像分辨率:中等
├─ 噪声水平:高
└─ 适用:体表器官、血管
高质量模式(15fps):
├─ 探测深度:深(20cm)
├─ 图像分辨率:高
├─ 噪声水平:低
└─ 适用:腹部深部器官
原因:
- 每帧需要发射多次超声波
- 更多次发射 = 更好的图像质量
- 但会延长单帧采集时间
- 导致帧率下降
实时性要求的分级:
| 应用场景 | 最低帧率 | 推荐帧率 | 延迟容忍 |
|---|---|---|---|
| 诊断模式(静态观察) | 15 fps | 20-25 fps | < 200ms |
| 操作引导(探头定位) | 20 fps | 30 fps | < 100ms |
| 介入手术(针头定位) | 30 fps | 60 fps | < 50ms |
| 心动图(快速运动) | 30 fps | 60-120 fps | < 50ms |
3 帧率问题的根本原因分析
下面针对工作中真实遇到的问题及其解决方案进行说明。
3.1 采集与编码的帧率脱节
问题本质:数据生产速度与消费速度不匹配
典型场景:屏幕录制
实际情况:
采集线程:获取屏幕 → 受系统负载影响 → 不稳定
编码器:期望固定帧率 → 严格的时间戳要求
矛盾:
├─ 采集说:"我这次用了 45ms 才拿到帧"
└─ 编码器说:"我需要每 33.33ms 一帧,你给我的是什么?"
结果:
├─ 时长错乱(60秒变30秒)
├─ 播放速度异常(忽快忽慢)
└─ 音画不同步
核心问题分析
问题链路图:
屏幕采集(不稳定/慢) 编码器(固定帧率)
↓ ↓
实际:20-50ms/帧 ──────→ 期望:33.33ms/帧(30fps)
↓ ↓
时间戳混乱 ──────→ 播放异常/时长错误
3.1.1 原始桌面录屏实现
原始实现将帧采集和视频编码完全分离,采用"先截图存PNG,后统一编码"的两阶段处理模式。
详细流程:
阶段一:录制过程(实时运行)
录制线程循环 {
1. 屏幕捕获 (100-200ms)
- 调用系统API截取屏幕区域
- 获得32位BGRA格式的位图句柄
2. 提取像素数据 (50-100ms)
- 从位图句柄中读取原始像素
- 复制到内存缓冲区
- 数据大小:1920×1080×4 = 8MB
3. 格式转换 (50-100ms)
- 将32位BGRA转换为24位BGR
- 去除Alpha通道
- 数据大小压缩到:1920×1080×3 = 6MB
4. 保存为PNG (50-150ms)
- 检查系统可用内存
- 如果内存不足(<7GB):
* 使用图像压缩库
* 将BGR数据编码为PNG格式
* 写入临时文件:FrameBuf_0.png, FrameBuf_1.png...
* 文件大小:约500KB/帧(经过PNG压缩)
- 如果内存充足:
* 直接缓存原始数据到内存
5. 等待下一帧
- 根据设定帧率计算等待时间
- 但由于前4步已耗时300-500ms
- 实际根本无法等待,立即处理下一帧
}
磁盘临时文件示例:
C:\\Users\\xxx\\AppData\\Local\\Temp\\ApexVideoTmp\\
├── FrameBuf_0.png (~500KB)
├── FrameBuf_1.png (~500KB)
├── FrameBuf_2.png (~500KB)
...
└── FrameBuf_249.png (~500KB)
总计:250帧 × 500KB ≈ 125MB 磁盘占用
阶段二:保存过程(用户点击保存后)
保存线程 {
1. 初始化视频编码器
- 创建MP4容器
- 配置H.264编码器
- 设置参数:分辨率、帧率(10 FPS)、比特率
2. 遍历所有帧 {
对于每个PNG文件:
a. 加载PNG (50-100ms)
- 从磁盘读取FrameBuf_N.png
- 解压PNG数据
b. 解码为原始数据 (30-50ms)
- PNG → BGR24格式
- 恢复到6MB原始像素数据
c. 色彩空间转换 (50-100ms)
- BGR24 → YUV420P
- 视频编码器需要的格式
d. H.264编码 (100-200ms)
- YUV420P → 压缩的H.264数据包
- 写入MP4文件
e. 更新进度条
- 通知UI当前处理进度
}
3. 完成编码
- 写入MP4文件索引
- 关闭文件
- 删除临时PNG文件
}
总耗时:250帧 × (50+30+50+100)ms ≈ 57秒
性能瓶颈分析(1920×1080分辨率)
| 步骤 | 耗时 | 累计耗时 | 说明 |
|---|---|---|---|
| 屏幕捕获 | 100-200ms | 100-200ms | Windows GDI API调用 |
| 提取像素 | 50-100ms | 150-300ms | 8MB数据复制 |
| 格式转换 | 50-100ms | 200-400ms | 逐像素BGRA→BGR |
| PNG编码+写盘 | 50-150ms | 250-550ms | 压缩+磁盘IO |
| 总计 | 250-550ms | - | 理论最高2-4 FPS |
代码示例:
// pngmain.cpp - PNG缓存方式示例
#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <FreeImage.h>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
}
#ifdef _WIN32
#include <Windows.h>
#else
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#define MAX_PATH 260
inline unsigned long GetTempPathW(unsigned long nBufferLength, wchar_t* lpBuffer) {
if (lpBuffer && nBufferLength > 0) {
wcscpy(lpBuffer, L"/tmp/");
return 5;
}
return 5;
}
inline int CreateDirectoryW(const wchar_t* path, void*) {
char mbPath[MAX_PATH];
wcstombs(mbPath, path, MAX_PATH);
return mkdir(mbPath, 0755) == 0 ? 1 : 0;
}
inline int DeleteFileW(const wchar_t* path) {
char mbPath[MAX_PATH];
wcstombs(mbPath, path, MAX_PATH);
return unlink(mbPath) == 0 ? 1 : 0;
}
#endif
// ========== 数据结构定义 ==========
struct ImageRawByte {
unsigned pixelWidth = 0;
unsigned pixelHeight = 0;
unsigned bitsPerPixel = 24;
unsigned bytesPerLine = 0;
std::vector<unsigned char> byteArray;
};
enum VideoFrameCachePolicy {
CACHE_TO_MEMORY,
CACHE_TO_DISK
};
// ========== PNG录制器类 ==========
class PNGCacheRecorder {
private:
unsigned int m_frameCount;
unsigned int m_frameRate;
unsigned int m_width;
unsigned int m_height;
std::wstring m_tempDirectory;
std::vector<ImageRawByte> m_memoryCache;
std::map<int, VideoFrameCachePolicy> m_frameCacheMap;
uint64_t m_memoryThresholdMB;
public:
PNGCacheRecorder(int width, int height, int fps)
: m_frameCount(0), m_frameRate(fps), m_width(width), m_height(height),
m_memoryThresholdMB(7168) // 7GB
{
// 创建临时目录
wchar_t tempPath[MAX_PATH];
GetTempPathW(MAX_PATH, tempPath);
m_tempDirectory = std::wstring(tempPath) + L"VideoTempFrames\\\\";
CreateDirectoryW(m_tempDirectory.c_str(), nullptr);
std::wcout << L"临时目录: " << m_tempDirectory << std::endl;
}
~PNGCacheRecorder() {
cleanup();
}
// ========== 添加帧(核心方法)==========
void addFrame(const ImageRawByte& frame) {
auto startTime = std::chrono::high_resolution_clock::now();
// 步骤1: 检查可用内存
uint64_t availableMemMB = checkAvailableMemory();
VideoFrameCachePolicy cachePolicy;
if (availableMemMB >= m_memoryThresholdMB) {
// 内存充足:缓存到内存
m_memoryCache.push_back(frame);
cachePolicy = CACHE_TO_MEMORY;
std::cout << "帧 " << m_frameCount << ": 缓存到内存 (可用: "
<< availableMemMB << " MB)" << std::endl;
} else {
// 内存不足:保存为PNG
std::wstring pngPath = m_tempDirectory + L"FrameBuf_"
+ std::to_wstring(m_frameCount) + L".png";
// 步骤2: 转换为FreeImage格式
FIBITMAP* fiBitmap = FreeImage_ConvertFromRawBits(
(BYTE*)frame.byteArray.data(),
frame.pixelWidth, frame.pixelHeight,
frame.bytesPerLine, frame.bitsPerPixel,
0, 0, 0, false
);
// 步骤3: 保存为PNG(压缩)
if (fiBitmap) {
FreeImage_SaveU(FIF_PNG, fiBitmap, pngPath.c_str(), PNG_DEFAULT);
FreeImage_Unload(fiBitmap);
cachePolicy = CACHE_TO_DISK;
std::wcout << L"帧 " << m_frameCount << L": 保存为 " << pngPath << std::endl;
} else {
std::cerr << "❌ FreeImage转换失败!" << std::endl;
return;
}
}
m_frameCacheMap[m_frameCount] = cachePolicy;
m_frameCount++;
auto endTime = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
endTime - startTime).count();
std::cout << " → 耗时: " << elapsed << " ms" << std::endl;
}
// ========== 保存为MP4(核心方法)==========
bool saveToMP4(const char* outputPath) {
std::cout << "\\n========== 开始保存MP4 ==========" << std::endl;
auto totalStartTime = std::chrono::high_resolution_clock::now();
// 1. 初始化FFmpeg编码器
AVFormatContext* formatCtx = nullptr;
avformat_alloc_output_context2(&formatCtx, nullptr, nullptr, outputPath);
if (!formatCtx) {
std::cerr << "❌ 创建输出上下文失败!" << std::endl;
return false;
}
const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec) {
std::cerr << "❌ 找不到H.264编码器!" << std::endl;
avformat_free_context(formatCtx);
return false;
}
AVCodecContext* codecCtx = avcodec_alloc_context3(codec);
codecCtx->width = m_width;
codecCtx->height = m_height;
codecCtx->time_base = {1, (int)m_frameRate}; // ⚠️ 设定帧率
codecCtx->framerate = {(int)m_frameRate, 1};
codecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
codecCtx->bit_rate = 5000000;
avcodec_open2(codecCtx, codec, nullptr);
AVStream* stream = avformat_new_stream(formatCtx, nullptr);
stream->time_base = codecCtx->time_base;
avcodec_parameters_from_context(stream->codecpar, codecCtx);
avio_open(&formatCtx->pb, outputPath, AVIO_FLAG_WRITE);
avformat_write_header(formatCtx, nullptr);
// 2. 创建帧缓冲区
AVFrame* frame = av_frame_alloc();
frame->format = AV_PIX_FMT_YUV420P;
frame->width = m_width;
frame->height = m_height;
av_frame_get_buffer(frame, 0);
int yuvBufferSize = m_width * m_height * 3 / 2;
uint8_t* yuvBuffer = new uint8_t[yuvBufferSize];
AVPacket* packet = av_packet_alloc();
// 3. 遍历所有帧
for (auto& [frameID, cachePolicy] : m_frameCacheMap) {
auto frameStartTime = std::chrono::high_resolution_clock::now();
ImageRawByte frameBGR24;
if (cachePolicy == CACHE_TO_MEMORY) {
// ✅ 从内存读取
frameBGR24 = m_memoryCache[frameID];
std::cout << "帧 " << frameID << ": 从内存读取" << std::endl;
} else {
// ❌ 从磁盘读取PNG(慢!)
std::wstring pngPath = m_tempDirectory + L"FrameBuf_"
+ std::to_wstring(frameID) + L".png";
// a. 加载PNG文件
FIBITMAP* fiBitmap = FreeImage_LoadU(FIF_PNG, pngPath.c_str());
if (!fiBitmap) {
std::cerr << "❌ 加载PNG失败: " << frameID << std::endl;
continue;
}
// b. 转换为原始BGR24数据
frameBGR24.pixelWidth = FreeImage_GetWidth(fiBitmap);
frameBGR24.pixelHeight = FreeImage_GetHeight(fiBitmap);
frameBGR24.bitsPerPixel = 24;
frameBGR24.bytesPerLine = frameBGR24.pixelWidth * 3;
frameBGR24.byteArray.resize(frameBGR24.bytesPerLine * frameBGR24.pixelHeight);
FreeImage_ConvertToRawBits(
frameBGR24.byteArray.data(), fiBitmap,
frameBGR24.bytesPerLine, 24, 0, 0, 0, false
);
FreeImage_Unload(fiBitmap);
std::wcout << L"帧 " << frameID << L": 从磁盘读取 " << pngPath << std::endl;
}
// c. BGR24 → YUV420P转换
convertBGR24ToYUV420(frameBGR24, yuvBuffer);
// d. 填充AVFrame
av_image_fill_arrays(
frame->data, frame->linesize,
yuvBuffer, AV_PIX_FMT_YUV420P,
m_width, m_height, 1
);
// e. H.264编码
frame->pts = frameID; // ⚠️ 时间戳=帧序号
avcodec_send_frame(codecCtx, frame);
while (avcodec_receive_packet(codecCtx, packet) == 0) {
av_packet_rescale_ts(packet, codecCtx->time_base, stream->time_base);
packet->stream_index = stream->index;
av_write_frame(formatCtx, packet);
av_packet_unref(packet);
}
auto frameEndTime = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
frameEndTime - frameStartTime).count();
int progress = (frameID + 1) * 100 / m_frameCount;
std::cout << " → 进度: " << progress << "% (耗时: " << elapsed << " ms)" << std::endl;
}
// 4. 完成编码
av_write_trailer(formatCtx);
// 5. 清理资源
delete[] yuvBuffer;
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&codecCtx);
avio_closep(&formatCtx->pb);
avformat_free_context(formatCtx);
auto totalEndTime = std::chrono::high_resolution_clock::now();
auto totalElapsed = std::chrono::duration_cast<std::chrono::seconds>(
totalEndTime - totalStartTime).count();
std::cout << "\\n✅ 保存完成! 总耗时: " << totalElapsed << " 秒" << std::endl;
return true;
}
unsigned int getFrameCount() const { return m_frameCount; }
private:
uint64_t checkAvailableMemory() {
#ifdef _WIN32
MEMORYSTATUSEX statex;
statex.dwLength = sizeof(statex);
GlobalMemoryStatusEx(&statex);
return statex.ullAvailPhys / (1024 * 1024);
#else
return 8192; // 默认8GB
#endif
}
void convertBGR24ToYUV420(const ImageRawByte& bgr, uint8_t* yuv) {
unsigned char* bgrData = (unsigned char*)bgr.byteArray.data();
unsigned int w = bgr.pixelWidth;
int h = (int)bgr.pixelHeight;
uint8_t *ptrY = yuv;
uint8_t *ptrU = yuv + w * h;
uint8_t *ptrV = ptrU + (w * h / 4);
for (int j = h - 1; j >= 0; j--) {
uint8_t* ptrBGR = bgrData + bgr.bytesPerLine * j;
for (unsigned int i = 0; i < w; i++) {
uint8_t b = *(ptrBGR++);
uint8_t g = *(ptrBGR++);
uint8_t r = *(ptrBGR++);
uint8_t y = (66 * r + 129 * g + 25 * b + 128) >> 8;
uint8_t u = (-38 * r - 74 * g + 112 * b + 128) >> 8;
uint8_t v = (112 * r - 94 * g - 18 * b + 128) >> 8;
*(ptrY++) = std::min(std::max(y + 16, 0), 255);
if (j % 2 == 0 && i % 2 == 0) {
*(ptrU++) = std::min(std::max(u + 128, 0), 255);
} else if (i % 2 == 0) {
*(ptrV++) = std::min(std::max(v + 128, 0), 255);
}
}
}
}
void cleanup() {
m_memoryCache.clear();
m_frameCacheMap.clear();
// 删除临时PNG文件
for (int i = 0; i < (int)m_frameCount; i++) {
std::wstring pngPath = m_tempDirectory + L"FrameBuf_"
+ std::to_wstring(i) + L".png";
DeleteFileW(pngPath.c_str());
}
}
};
// ========== 模拟屏幕捕获 ==========
ImageRawByte simulateScreenCapture(int width, int height, int frameIndex) {
ImageRawByte frame;
frame.pixelWidth = width;
frame.pixelHeight = height;
frame.bitsPerPixel = 24;
frame.bytesPerLine = width * 3;
frame.byteArray.resize(frame.bytesPerLine * height);
// 填充渐变色(模拟真实图像)
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int offset = y * frame.bytesPerLine + x * 3;
frame.byteArray[offset + 0] = (frameIndex * 10 + x) % 256; // B
frame.byteArray[offset + 1] = (frameIndex * 5 + y) % 256; // G
frame.byteArray[offset + 2] = (frameIndex * 3) % 256; // R
}
}
return frame;
}
// ========== 主函数 ==========
int main() {
FreeImage_Initialise();
std::cout << "========== PNG缓存方式录制演示 ==========" << std::endl;
const int WIDTH = 1920;
const int HEIGHT = 1080;
const int FPS = 10;
const int DURATION_SEC = 20;
const int TARGET_FRAME_INTERVAL_MS = 1000 / FPS;
PNGCacheRecorder recorder(WIDTH, HEIGHT, FPS);
auto recordStartTime = std::chrono::steady_clock::now();
// 模拟录制过程
int actualFramesCaptured = 0;
for (int sec = 0; sec < DURATION_SEC; sec++) {
std::cout << "\\n--- 第 " << (sec + 1) << " 秒 ---" << std::endl;
auto secStartTime = std::chrono::steady_clock::now();
while (true) {
auto frameStartTime = std::chrono::steady_clock::now();
// 步骤1-3: 屏幕捕获 + 格式转换(模拟耗时300ms)
std::this_thread::sleep_for(std::chrono::milliseconds(150)); // 模拟捕获
ImageRawByte frame = simulateScreenCapture(WIDTH, HEIGHT, actualFramesCaptured);
std::this_thread::sleep_for(std::chrono::milliseconds(150)); // 模拟转换
// 步骤4: 保存PNG(实际执行)
recorder.addFrame(frame);
actualFramesCaptured++;
auto frameEndTime = std::chrono::steady_clock::now();
auto frameElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
frameEndTime - frameStartTime).count();
std::cout << " 总耗时: " << frameElapsed << " ms" << std::endl;
// 检查是否超过1秒
auto secElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
frameEndTime - secStartTime).count();
if (secElapsed >= 1000) break;
}
}
auto recordEndTime = std::chrono::steady_clock::now();
int recordElapsed = std::chrono::duration_cast<std::chrono::seconds>(
recordEndTime - recordStartTime).count();
std::cout << "\\n========== 录制统计 ==========" << std::endl;
std::cout << "实际录制时长: " << recordElapsed << " 秒" << std::endl;
std::cout << "实际捕获帧数: " << actualFramesCaptured << " 帧" << std::endl;
std::cout << "实际帧率: " << (float)actualFramesCaptured / recordElapsed << " FPS" << std::endl;
std::cout << "配置帧率: " << FPS << " FPS" << std::endl;
// 保存为MP4
std::cout << "\\n========== 开始保存MP4 ==========" << std::endl;
recorder.saveToMP4("output_png.mp4");
// ⚠️ 问题分析
std::cout << "\\n========== 问题分析 ==========" << std::endl;
std::cout << "期望视频时长: " << DURATION_SEC << " 秒" << std::endl;
std::cout << "实际视频时长: " << (float)actualFramesCaptured / FPS << " 秒 ❌" << std::endl;
std::cout << "原因: 实际帧率(" << (float)actualFramesCaptured / recordElapsed
<< " FPS) != 配置帧率(" << FPS << " FPS)" << std::endl;
FreeImage_DeInitialise();
return 0;
}
3.1.2 提高采集效率的方案:
将"帧采集"与"视频编码"合并到同一流程,采集到的每一帧立即编码写入MP4文件,彻底消除帧率脱节问题。
实现原理 初始化阶段(录制开始前,仅执行一次)
初始化实时编码器 {
1. 创建临时MP4文件
- 文件路径:C:\\Temp\\ApexVideoTmp\\realtime_temp.mp4
- 直接创建MP4容器,不使用中间格式
2. 搜索最优编码器(按优先级)
优先级列表:
[1] NVENC (NVIDIA GPU硬件加速)
[2] QSV (Intel核显硬件加速)
[3] AMF (AMD GPU硬件加速)
[4] libx264 (CPU软件编码,兜底方案)
3. 配置编码器参数
- 分辨率:1920×1080
- 帧率:10 FPS(用户设定)
- 比特率:5-8 Mbps(根据分辨率自动计算)
- 质量预设:
* 硬件加速:平衡模式
* 软件编码:veryfast预设 + CRF 20
4. 创建像素格式转换器
- 输入格式:BGR24(屏幕捕获的格式)
- 输出格式:YUV420P(H.264编码器需要的格式)
- 转换算法:SWS_BICUBIC(高质量)
- 针对Windows DIB特殊处理:
* Windows:使用负步长翻转图像(bottom-up存储)
* Linux:使用正步长(top-down存储)
5. 分配帧缓冲区
- 格式:YUV420P
- 大小:1920×1080×1.5 ≈ 3MB
- 只分配一次,后续重复使用
6. 写入MP4文件头
- 包含视频流信息
- 预留索引空间
}
录制阶段(每一帧的处理流程)
录制线程循环 {
记录帧开始时间
1. 屏幕捕获 (100-200ms) ← 不优化
- 调用系统API截取屏幕
- 获得32位BGRA位图句柄
2. 提取像素数据 (50-100ms) ← 不优化
- 从句柄读取原始像素
- 复制到内存缓冲区(8MB)
3. 格式转换 (50-100ms) ← 不优化
- BGRA → BGR(去除Alpha通道)
- 数据量:8MB → 6MB
4. 实时编码 (40-80ms) ← 核心优化点
a. 快速格式转换 (20-30ms)
- 使用预初始化的SwsContext
- BGR24 → YUV420P
- 硬件加速的SIMD指令
b. H.264编码 (10-60ms)
硬件加速(NVENC/QSV):10-20ms
软件编码(libx264):40-60ms
编码流程:
- 将YUV帧送入编码器
- 设置时间戳:PTS = 帧序号
- 接收压缩后的H.264数据包
c. 写入MP4文件 (5-10ms)
- 直接写入临时MP4文件
- 顺序写入,磁盘IO高效
- 不产生任何中间文件
记录帧结束时间
计算本帧耗时 = 结束时间 - 开始时间
5. 帧率控制(关键逻辑)
目标帧间隔 = 1000ms / 设定帧率
实际耗时 = 步骤1-4的总时间
需要等待 = 目标帧间隔 - 实际耗时
if (需要等待 > 0) {
// 情况A:处理速度快于目标帧率
// 例如:10 FPS目标 = 100ms/帧
// 实际耗时 = 60ms
// 等待 = 40ms
sleep(需要等待的时间)
→ 确保精确的10 FPS输出
}
else {
// 情况B:处理速度慢于目标帧率
// 例如:30 FPS目标 = 33ms/帧
// 实际耗时 = 260ms
// 等待 = -227ms (负数)
不等待,立即处理下一帧
}
}
保存阶段(用户点击保存后),这个阶段的时间不影响帧率。
保存流程 {
1. 完成编码
- 刷新编码器缓冲区
* 将编码器内部缓存的帧全部输出
* 确保最后几帧不丢失
- 写入MP4文件尾
* 包含视频索引(moov atom)
* 使视频可正常播放
2. 关闭临时文件
- 释放文件句柄
- 确保数据写入磁盘
3. 文件复制
- 源文件:C:\\Temp\\realtime_temp.mp4
- 目标:用户指定路径
- 文件大小:15-25MB(25秒1080p视频)
4. 清理临时文件
- 删除realtime_temp.mp4
- 释放磁盘空间
5. 通知用户完成
- 显示保存成功消息
- 可选:打开文件所在目录
}
代码示例:
// realtimemain.cpp - 实时编码方式示例
#include <iostream>
#include <vector>
#include <string>
#include <chrono>
#include <thread>
#include <fstream>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
}
#ifdef _WIN32
#include <Windows.h>
inline int CopyFileA(const char* src, const char* dst, int) {
std::ifstream srcFile(src, std::ios::binary);
std::ofstream dstFile(dst, std::ios::binary);
dstFile << srcFile.rdbuf();
return 1;
}
inline int DeleteFileA(const char* path) {
return remove(path) == 0 ? 1 : 0;
}
#else
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#define MAX_PATH 260
inline unsigned long GetTempPathW(unsigned long nBufferLength, wchar_t* lpBuffer) {
if (lpBuffer && nBufferLength > 0) {
wcscpy(lpBuffer, L"/tmp/");
return 5;
}
return 5;
}
inline int CreateDirectoryW(const wchar_t* path, void*) {
char mbPath[MAX_PATH];
wcstombs(mbPath, path, MAX_PATH);
return mkdir(mbPath, 0755) == 0 ? 1 : 0;
}
inline int CopyFileA(const char* src, const char* dst, int) {
std::ifstream srcFile(src, std::ios::binary);
std::ofstream dstFile(dst, std::ios::binary);
dstFile << srcFile.rdbuf();
return 1;
}
inline int DeleteFileA(const char* path) {
return unlink(path) == 0 ? 1 : 0;
}
#endif
// ========== 数据结构定义 ==========
struct ImageRawByte {
unsigned pixelWidth = 0;
unsigned pixelHeight = 0;
unsigned bytesPerLine = 0;
std::vector<unsigned char> byteArray;
};
// ========== 实时编码录制器类 ==========
class RealtimeRecorder {
private:
AVFormatContext* m_formatCtx;
AVCodecContext* m_codecCtx;
AVStream* m_stream;
SwsContext* m_swsCtx;
AVFrame* m_frame;
AVPacket* m_packet;
unsigned int m_frameCount;
unsigned int m_frameRate;
unsigned int m_width;
unsigned int m_height;
std::string m_tempFilePath;
bool m_initialized;
public:
RealtimeRecorder(int width, int height, int fps)
: m_formatCtx(nullptr), m_codecCtx(nullptr), m_stream(nullptr),
m_swsCtx(nullptr), m_frame(nullptr), m_packet(nullptr),
m_frameCount(0), m_frameRate(fps), m_width(width), m_height(height),
m_initialized(false)
{
}
~RealtimeRecorder() {
cleanup();
}
// ========== 初始化编码器(一次性)==========
bool init() {
std::cout << "========== 初始化实时编码器 ==========" << std::endl;
// 1. 创建临时目录和文件路径
wchar_t tempPathW[MAX_PATH];
GetTempPathW(MAX_PATH, tempPathW);
std::wstring tempDirW = std::wstring(tempPathW) + L"VideoTempFrames\\\\";
CreateDirectoryW(tempDirW.c_str(), nullptr);
char tempDir[MAX_PATH];
wcstombs(tempDir, tempDirW.c_str(), MAX_PATH);
m_tempFilePath = std::string(tempDir) + "realtime_temp.mp4";
std::cout << "临时文件: " << m_tempFilePath << std::endl;
// 2. 创建输出上下文
int ret = avformat_alloc_output_context2(&m_formatCtx, nullptr, "mp4",
m_tempFilePath.c_str());
if (ret < 0) {
std::cerr << "❌ 创建输出上下文失败!" << std::endl;
return false;
}
// 3. 搜索H.264编码器(优先硬件加速)
const AVCodec* codec = nullptr;
const char* encoderNames[] = {
"h264_nvenc", // NVIDIA GPU
"h264_qsv", // Intel QSV
"h264_amf", // AMD GPU
"libx264" // CPU软件
};
for (const char* name : encoderNames) {
codec = avcodec_find_encoder_by_name(name);
if (codec) {
std::cout << "✅ 找到编码器: " << name << std::endl;
break;
}
}
if (!codec) {
std::cerr << "❌ 找不到H.264编码器!" << std::endl;
cleanup();
return false;
}
// 4. 创建编码器上下文
m_codecCtx = avcodec_alloc_context3(codec);
if (!m_codecCtx) {
std::cerr << "❌ 创建编码器上下文失败!" << std::endl;
cleanup();
return false;
}
// 5. 配置编码器参数
m_codecCtx->width = m_width;
m_codecCtx->height = m_height;
m_codecCtx->time_base = {1, (int)m_frameRate};
m_codecCtx->framerate = {(int)m_frameRate, 1};
m_codecCtx->gop_size = m_frameRate;
m_codecCtx->max_b_frames = 0;
m_codecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
// 计算比特率
int pixelCount = m_width * m_height;
int baseBitrate = (pixelCount >= 1920*1080) ? 8000000 : 5000000;
m_codecCtx->bit_rate = baseBitrate * m_frameRate / 30;
std::cout << "编码器配置:" << std::endl;
std::cout << " 分辨率: " << m_width << "×" << m_height << std::endl;
std::cout << " 帧率: " << m_frameRate << " FPS" << std::endl;
std::cout << " 比特率: " << m_codecCtx->bit_rate / 1000000.0 << " Mbps" << std::endl;
// 6. 编码器优化
if (strstr(codec->name, "nvenc")) {
std::cout << "🚀 使用NVIDIA NVENC加速" << std::endl;
av_opt_set(m_codecCtx->priv_data, "preset", "p4", 0);
av_opt_set(m_codecCtx->priv_data, "tune", "ll", 0);
} else if (strstr(codec->name, "x264")) {
std::cout << "⚡ 使用x264软件编码" << std::endl;
av_opt_set(m_codecCtx->priv_data, "preset", "ultrafast", 0);
av_opt_set(m_codecCtx->priv_data, "tune", "zerolatency", 0);
m_codecCtx->thread_count = 4;
}
// 7. 打开编码器
ret = avcodec_open2(m_codecCtx, codec, nullptr);
if (ret < 0) {
std::cerr << "❌ 打开编码器失败!" << std::endl;
cleanup();
return false;
}
// 8. 创建视频流
m_stream = avformat_new_stream(m_formatCtx, nullptr);
if (!m_stream) {
std::cerr << "❌ 创建视频流失败!" << std::endl;
cleanup();
return false;
}
m_stream->time_base = m_codecCtx->time_base;
avcodec_parameters_from_context(m_stream->codecpar, m_codecCtx);
// 9. 打开输出文件
ret = avio_open(&m_formatCtx->pb, m_tempFilePath.c_str(), AVIO_FLAG_WRITE);
if (ret < 0) {
std::cerr << "❌ 打开输出文件失败!" << std::endl;
cleanup();
return false;
}
// 10. 写入文件头
ret = avformat_write_header(m_formatCtx, nullptr);
if (ret < 0) {
std::cerr << "❌ 写入文件头失败!" << std::endl;
cleanup();
return false;
}
// 11. 创建YUV帧缓冲区
m_frame = av_frame_alloc();
if (!m_frame) {
std::cerr << "❌ 创建帧缓冲区失败!" << std::endl;
cleanup();
return false;
}
m_frame->format = AV_PIX_FMT_YUV420P;
m_frame->width = m_width;
m_frame->height = m_height;
av_frame_get_buffer(m_frame, 0);
// 12. 创建像素格式转换器
m_swsCtx = sws_getContext(
m_width, m_height, AV_PIX_FMT_BGR24,
m_width, m_height, AV_PIX_FMT_YUV420P,
SWS_BICUBIC, nullptr, nullptr, nullptr
);
if (!m_swsCtx) {
std::cerr << "❌ 创建格式转换器失败!" << std::endl;
cleanup();
return false;
}
// 13. 创建数据包
m_packet = av_packet_alloc();
if (!m_packet) {
std::cerr << "❌ 创建数据包失败!" << std::endl;
cleanup();
return false;
}
m_initialized = true;
std::cout << "✅ 编码器初始化完成!" << std::endl;
return true;
}
// ========== 实时编码帧(核心方法)==========
bool encodeFrame(const ImageRawByte& frame) {
if (!m_initialized) {
return false;
}
auto startTime = std::chrono::high_resolution_clock::now();
// 步骤1: BGR24 → YUV420P格式转换
#ifdef _WIN32
const uint8_t* srcData = frame.byteArray.data()
+ frame.bytesPerLine * (frame.pixelHeight - 1);
int srcStride = -(int)frame.bytesPerLine;
#else
const uint8_t* srcData = frame.byteArray.data();
int srcStride = (int)frame.bytesPerLine;
#endif
const uint8_t* srcSlice[1] = { srcData };
int srcStrides[1] = { srcStride };
int ret = sws_scale(
m_swsCtx,
srcSlice, srcStrides, 0, frame.pixelHeight,
m_frame->data, m_frame->linesize
);
if (ret != (int)frame.pixelHeight) {
std::cerr << "❌ 格式转换失败!" << std::endl;
return false;
}
// 步骤2: 设置时间戳
m_frame->pts = m_frameCount;
// 步骤3: H.264编码
ret = avcodec_send_frame(m_codecCtx, m_frame);
if (ret < 0) {
std::cerr << "❌ 发送帧到编码器失败!" << std::endl;
return false;
}
// 步骤4: 接收编码后的数据包并写入文件
while (ret >= 0) {
ret = avcodec_receive_packet(m_codecCtx, m_packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
return false;
}
av_packet_rescale_ts(m_packet, m_codecCtx->time_base, m_stream->time_base);
m_packet->stream_index = m_stream->index;
av_interleaved_write_frame(m_formatCtx, m_packet);
av_packet_unref(m_packet);
}
m_frameCount++;
auto endTime = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
endTime - startTime).count();
std::cout << "帧 " << m_frameCount - 1 << ": 编码完成 (耗时: "
<< elapsed << " ms)" << std::endl;
return true;
}
// ========== 完成编码 ==========
bool finalize() {
if (!m_initialized) {
return true;
}
std::cout << "\\n========== 刷新编码器缓冲区 ==========" << std::endl;
// 刷新编码器
avcodec_send_frame(m_codecCtx, nullptr);
int ret;
while ((ret = avcodec_receive_packet(m_codecCtx, m_packet)) == 0) {
av_packet_rescale_ts(m_packet, m_codecCtx->time_base, m_stream->time_base);
m_packet->stream_index = m_stream->index;
av_interleaved_write_frame(m_formatCtx, m_packet);
av_packet_unref(m_packet);
}
// 写入文件尾
av_write_trailer(m_formatCtx);
std::cout << "✅ 编码完成!" << std::endl;
return true;
}
// ========== 保存到文件(快速复制)==========
bool saveToFile(const char* outputPath) {
std::cout << "\\n========== 保存文件 ==========" << std::endl;
finalize();
cleanup();
// 快速文件复制(1秒内完成)
auto startTime = std::chrono::high_resolution_clock::now();
if (!CopyFileA(m_tempFilePath.c_str(), outputPath, false)) {
std::cerr << "❌ 复制文件失败!" << std::endl;
return false;
}
DeleteFileA(m_tempFilePath.c_str());
auto endTime = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
endTime - startTime).count();
std::cout << "✅ 文件已保存: " << outputPath << std::endl;
std::cout << " 保存耗时: " << elapsed << " ms" << std::endl;
return true;
}
unsigned int getFrameCount() const { return m_frameCount; }
private:
void cleanup() {
if (m_packet) av_packet_free(&m_packet);
if (m_frame) av_frame_free(&m_frame);
if (m_swsCtx) sws_freeContext(m_swsCtx);
if (m_codecCtx) avcodec_free_context(&m_codecCtx);
if (m_formatCtx) {
if (!(m_formatCtx->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&m_formatCtx->pb);
}
avformat_free_context(m_formatCtx);
}
m_formatCtx = nullptr;
m_codecCtx = nullptr;
m_stream = nullptr;
m_swsCtx = nullptr;
m_frame = nullptr;
m_packet = nullptr;
m_initialized = false;
}
};
// ========== 模拟屏幕捕获 ==========
ImageRawByte simulateScreenCapture(int width, int height, int frameIndex) {
ImageRawByte frame;
frame.pixelWidth = width;
frame.pixelHeight = height;
frame.bytesPerLine = width * 3;
frame.byteArray.resize(frame.bytesPerLine * height);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int offset = y * frame.bytesPerLine + x * 3;
frame.byteArray[offset + 0] = (frameIndex * 10 + x) % 256;
frame.byteArray[offset + 1] = (frameIndex * 5 + y) % 256;
frame.byteArray[offset + 2] = (frameIndex * 3) % 256;
}
}
return frame;
}
// ========== 主函数 ==========
int main() {
std::cout << "========== 实时编码方式录制演示 ==========" << std::endl;
const int WIDTH = 1920;
const int HEIGHT = 1080;
const int FPS = 10;
const int DURATION_SEC = 20;
const int TARGET_FRAME_INTERVAL_MS = 1000 / FPS;
RealtimeRecorder recorder(WIDTH, HEIGHT, FPS);
// 初始化编码器(只执行一次)
if (!recorder.init()) {
return -1;
}
auto recordStartTime = std::chrono::steady_clock::now();
// 模拟录制过程
int actualFramesCaptured = 0;
for (int sec = 0; sec < DURATION_SEC; sec++) {
std::cout << "\\n--- 第 " << (sec + 1) << " 秒 ---" << std::endl;
auto secStartTime = std::chrono::steady_clock::now();
while (true) {
auto frameStartTime = std::chrono::steady_clock::now();
// 步骤1-3: 屏幕捕获 + 格式转换(模拟耗时300ms)
std::this_thread::sleep_for(std::chrono::milliseconds(150));
ImageRawByte frame = simulateScreenCapture(WIDTH, HEIGHT, actualFramesCaptured);
std::this_thread::sleep_for(std::chrono::milliseconds(150));
// 步骤4: 实时编码(实际执行)
recorder.encodeFrame(frame);
actualFramesCaptured++;
auto frameEndTime = std::chrono::steady_clock::now();
auto frameElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
frameEndTime - frameStartTime).count();
std::cout << " 总耗时: " << frameElapsed << " ms" << std::endl;
// 帧率控制
int sleepTime = TARGET_FRAME_INTERVAL_MS - frameElapsed;
if (sleepTime > 0) {
std::cout << " ⏸️ 等待 " << sleepTime << " ms 以维持 " << FPS << " FPS" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(sleepTime));
} else {
std::cout << " ⚠️ 处理过慢 (" << frameElapsed << " ms > "
<< TARGET_FRAME_INTERVAL_MS << " ms)" << std::endl;
}
// 检查是否超过1秒
auto secElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
frameEndTime - secStartTime).count();
if (secElapsed >= 1000) break;
}
}
auto recordEndTime = std::chrono::steady_clock::now();
int recordElapsed = std::chrono::duration_cast<std::chrono::seconds>(
recordEndTime - recordStartTime).count();
std::cout << "\\n========== 录制统计 ==========" << std::endl;
std::cout << "实际录制时长: " << recordElapsed << " 秒" << std::endl;
std::cout << "实际捕获帧数: " << actualFramesCaptured << " 帧" << std::endl;
std::cout << "实际帧率: " << (float)actualFramesCaptured / recordElapsed << " FPS" << std::endl;
std::cout << "配置帧率: " << FPS << " FPS" << std::endl;
// 保存为MP4(快速复制)
recorder.saveToFile("output_realtime.mp4");
// ⚠️ 问题分析
std::cout << "\\n========== 问题分析 ==========" << std::endl;
std::cout << "期望视频时长: " << DURATION_SEC << " 秒" << std::endl;
std::cout << "实际视频时长: " << (float)actualFramesCaptured / FPS << " 秒";
if (abs((float)actualFramesCaptured / FPS - DURATION_SEC) < 1.0) {
std::cout << " ✅ 时长正确!" << std::endl;
} else {
std::cout << " ❌ 时长错误!" << std::endl;
}
std::cout << "\\n优势:" << std::endl;
std::cout << "✅ 保存速度极快(约1秒)" << std::endl;
std::cout << "✅ 不产生临时PNG文件" << std::endl;
std::cout << "✅ 内存占用固定(约30MB)" << std::endl;
return 0;
}
3.1.3 矫正时间戳的方案:
针对采集效率确定无法提高到用户设定的帧率(例如:60fps)。(参考开源库OBS-STUDIO实现)
1. 时间戳管理器(参考 async-delay-filter.c)
问题:为什么需要重构时间戳?
// 错误示例(直接使用系统时间戳)
采集时刻 系统时间戳 问题
Frame 1: 15:30:01.000 ✓ 正常
Frame 2: 15:30:01.045 ✓ 间隔45ms
Frame 3: 15:30:02.500 ✗ 跳变1.5秒(系统挂起/负载)
Frame 4: 15:30:02.530 ✓ 恢复正常
后果:视频在第3帧处突然快进/卡顿
解决思路:构建理想时间线
#pragma once
#include <cstdint>
// 参考 obs-filters/async-delay-filter.c:20
#define MAX_TS_VAR 2000000000ULL // 2秒时间戳跳变阈值
class TimestampManager {
public:
TimestampManager()
: base_timestamp_(0)
, last_timestamp_(0)
, frame_count_(0)
, target_frame_duration_ns_(0)
, first_frame_(true) {}
// 设置目标帧率
// 参考 decklink/decklink-device-instance.cpp:673 的 frameDuration 计算
void SetTargetFrameRate(uint32_t fps_num, uint32_t fps_den) {
// frameDuration = (frameTimescale * fps_den) / fps_num
// 转换为纳秒: (1000000000 * fps_den) / fps_num
target_frame_duration_ns_ = (1000000000ULL * fps_den) / fps_num;
}
// 处理输入帧的时间戳,返回标准化时间戳
// 参考 obs-filters/async-delay-filter.c:182 的 is_timestamp_jump
uint64_t ProcessFrame(uint64_t input_timestamp) {
// 首帧初始化
if (first_frame_) {
base_timestamp_ = input_timestamp;
last_timestamp_ = input_timestamp;
frame_count_ = 0;
first_frame_ = false;
return 0; // 相对时间戳从0开始
}
// 检测时间戳跳变
// 参考 async-delay-filter.c:45 的 is_timestamp_jump 函数
// static inline bool is_timestamp_jump(uint64_t ts, uint64_t prev_ts)
// {
// return ts < prev_ts || (ts - prev_ts) > MAX_TS_VAR;
// }
if (input_timestamp < last_timestamp_ ||
(input_timestamp - last_timestamp_) > MAX_TS_VAR) {
// 参考 async-delay-filter.c:182 的重置逻辑
// free_audio_data(filter);
// filter->audio_delay_reached = false;
base_timestamp_ = input_timestamp;
frame_count_ = 0;
last_timestamp_ = input_timestamp;
return 0;
}
last_timestamp_ = input_timestamp;
// 使用帧计数器生成理想时间戳
// 参考 decklink/decklink-device-instance.cpp:673
// output->ScheduleVideoFrame(frame,
// totalFramesScheduled * frameDuration,
// frameDuration, frameTimescale);
uint64_t ideal_timestamp = frame_count_ * target_frame_duration_ns_;
frame_count_++;
return ideal_timestamp;
}
void Reset() {
first_frame_ = true;
base_timestamp_ = 0;
last_timestamp_ = 0;
frame_count_ = 0;
}
uint64_t GetFrameCount() const { return frame_count_; }
private:
uint64_t base_timestamp_; // 基准时间戳
uint64_t last_timestamp_; // 上一帧时间戳
uint64_t frame_count_; // 累积帧计数
uint64_t target_frame_duration_ns_; // 目标帧间隔
bool first_frame_;
};
类比:就像音乐节拍器
- 实际采集:乐手演奏(可能快慢不均)
- 时间戳管理器:节拍器(提供稳定节奏)
- 编码器:录音棚(需要精确时间线)
2. 帧缓冲队列(参考 nvidia-audiofx-filter.c)
问题:生产者-消费者速率不匹配
生产者(采集线程) 缓冲区 消费者(编码线程)
快 ──────→ [■■■■■] ←────── 慢
每20ms产生 满时丢弃 每33ms消费
解决思路:FIFO队列 + 溢出保护
#pragma once
#include <deque>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <cstdint>
// 参考 nv-filters/nvidia-audiofx-filter.c 的 deque 使用模式
template<typename FrameType>
class FrameBuffer {
public:
explicit FrameBuffer(size_t max_size = 10)
: max_size_(max_size)
, last_timestamp_(0) {}
// 生产者推入帧
// 参考 nvidia-audiofx-filter.c:851 的 deque_push_back
bool Push(std::unique_ptr<FrameType> frame) {
std::lock_guard<std::mutex> lock(mutex_);
// 参考 nvidia-audiofx-filter.c:843 的时间戳检测
// if (ng->last_timestamp) {
// int64_t diff = llabs((int64_t)ng->last_timestamp -
// (int64_t)audio->timestamp);
// if (diff > 1000000000LL)
// reset_data(ng);
// }
if (last_timestamp_ != 0) {
int64_t diff = std::abs(static_cast<int64_t>(last_timestamp_) -
static_cast<int64_t>(frame->timestamp));
if (diff > 1000000000LL) { // 1秒差异
// 清空缓冲区(重置)
std::deque<std::unique_ptr<FrameType>>().swap(buffer_);
}
}
last_timestamp_ = frame->timestamp;
// 参考 nvidia-audiofx-filter.c:851
// deque_push_back(&ng->info_buffer, &info, sizeof(info));
if (buffer_.size() >= max_size_) {
// 队列满时丢弃最旧帧
buffer_.pop_front();
}
buffer_.push_back(std::move(frame));
cv_.notify_one();
return true;
}
// 消费者取帧
// 参考 nvidia-audiofx-filter.c:893 的 deque_pop_front
std::unique_ptr<FrameType> Pop(uint32_t timeout_ms = 100) {
std::unique_lock<std::mutex> lock(mutex_);
if (!cv_.wait_for(lock, std::chrono::milliseconds(timeout_ms),
[this] { return !buffer_.empty(); })) {
return nullptr; // 超时返回
}
auto frame = std::move(buffer_.front());
buffer_.pop_front();
return frame;
}
size_t Size() const {
std::lock_guard<std::mutex> lock(mutex_);
return buffer_.size();
}
void Clear() {
std::lock_guard<std::mutex> lock(mutex_);
std::deque<std::unique_ptr<FrameType>>().swap(buffer_);
last_timestamp_ = 0;
}
private:
mutable std::mutex mutex_;
std::condition_variable cv_;
std::deque<std::unique_ptr<FrameType>> buffer_; // 参考 nvidia-audiofx-filter.c 的 deque
size_t max_size_;
uint64_t last_timestamp_;
};
类比:传送带系统
- 采集线程:工人A放物品
- 缓冲区:传送带(长度有限)
- 编码线程:工人B取物品
- 满载策略:物品掉落(丢弃旧帧)
3. 帧率转换器(参考 aja-output.cpp) 问题:帧率转换的三种场景
场景1:过采样(采集>输出)
采集40fps → 输出30fps
策略:每4帧中跳过1帧
场景2:欠采样(采集<输出)
采集25fps → 输出30fps
策略:每5帧中复制1帧
场景3:精确匹配
采集30fps → 输出30fps
策略:直通
解决思路:累积误差补偿算法
#pragma once
#include <cstdint>
// 参考 aja/aja-output.cpp:419 的 mVideoAdjust 机制
class FramerateConverter {
public:
FramerateConverter(uint32_t input_fps_num, uint32_t input_fps_den,
uint32_t output_fps_num, uint32_t output_fps_den)
: input_frame_count_(0)
, output_frame_count_(0)
, video_adjust_(0) // 参考 aja-output.cpp 的 mVideoAdjust
, ratio_(0.0) {
// 计算帧率比例
ratio_ = (static_cast<double>(output_fps_num) * input_fps_den) /
(static_cast<double>(input_fps_num) * output_fps_den);
}
// 参考 aja/aja-output.cpp:419 的帧调度策略
enum Action {
SKIP, // 跳过此帧(过采样)
COPY_ONCE, // 正常输出
COPY_TWICE // 复制帧(欠采样)
};
Action ProcessInputFrame() {
input_frame_count_++;
// 参考 aja-output.cpp:419 的 mVideoAdjust 处理
// if (mVideoAdjust != 0) {
// if (mVideoAdjust > 0) {
// writeFrame = false; // 跳过
// } else {
// freeFrame = false; // 复制(不释放)
// }
// mVideoAdjust = 0;
// }
if (video_adjust_ > 0) {
video_adjust_ = 0;
return SKIP;
} else if (video_adjust_ < 0) {
video_adjust_ = 0;
output_frame_count_++; // 只计数一次(第二次复制不计数)
return COPY_TWICE;
}
// 计算期望的输出帧数
double expected_output = input_frame_count_ * ratio_;
double diff = expected_output - output_frame_count_;
// 决策逻辑
if (diff >= 1.5) {
// 需要复制帧(慢动作补偿)
video_adjust_ = -1;
output_frame_count_ += 2;
return COPY_TWICE;
} else if (diff >= 0.5) {
// 正常输出
output_frame_count_++;
return COPY_ONCE;
} else {
// 跳过帧(快动作补偿)
video_adjust_ = 1;
return SKIP;
}
}
void Reset() {
input_frame_count_ = 0;
output_frame_count_ = 0;
video_adjust_ = 0;
}
double GetRatio() const { return ratio_; }
private:
uint64_t input_frame_count_;
uint64_t output_frame_count_;
int video_adjust_; // 参考 aja-output.cpp 的 mVideoAdjust
double ratio_;
};
工作示例(40fps → 30fps):
输入帧 期望输出 实际输出 误差 决策
Frame 1: 0.75 0 0.75 → COPY_ONCE (输出1帧,累计1)
Frame 2: 1.50 1 0.50 → COPY_ONCE (输出1帧,累计2)
Frame 3: 2.25 2 0.25 → SKIP (跳过,累计2)
Frame 4: 3.00 2 1.00 → COPY_ONCE (输出1帧,累计3)
Frame 5: 3.75 3 0.75 → COPY_ONCE (输出1帧,累计4)
...
类比:水龙头调节
- 输入水流:采集帧率(不可控)
- 输出水杯:编码器容量(固定)
- 调节策略:
- 水流太快 → 关小龙头(跳帧)
- 水流太慢 → 开大龙头(复帧)
4. 视频帧结构
#pragma once
#include <cstdint>
#include <memory>
struct VideoFrame {
uint8_t* data;
uint32_t width;
uint32_t height;
uint64_t timestamp; // 纳秒
uint32_t linesize[4];
VideoFrame(uint32_t w, uint32_t h)
: width(w), height(h), timestamp(0) {
// 简化:仅分配RGBA数据
data = new uint8_t[w * h * 4];
linesize[0] = w * 4;
}
~VideoFrame() {
delete[] data;
}
// 禁止拷贝
VideoFrame(const VideoFrame&) = delete;
VideoFrame& operator=(const VideoFrame&) = delete;
};
5. 完整的采集管道
#pragma once
#include "timestamp_manager.h"
#include "frame_buffer.h"
#include "framerate_converter.h"
#include "video_frame.h"
#include <thread>
#include <atomic>
#include <chrono>
#include <functional>
#include <iostream>
class CapturePipeline {
public:
using EncodeCallback = std::function<void(const VideoFrame*)>;
CapturePipeline(uint32_t capture_fps, uint32_t output_fps,
uint32_t width, uint32_t height)
: frame_buffer_(5) // 5帧缓冲(参考OBS默认配置)
, converter_(capture_fps, 1, output_fps, 1)
, timestamp_mgr_()
, width_(width)
, height_(height)
, running_(false)
, frames_captured_(0)
, frames_encoded_(0)
, frames_dropped_(0) {
// 设置目标帧率
timestamp_mgr_.SetTargetFrameRate(output_fps, 1);
}
~CapturePipeline() {
Stop();
}
void SetEncodeCallback(EncodeCallback callback) {
encode_callback_ = callback;
}
void Start() {
if (running_) return;
running_ = true;
// 采集线程(模拟不稳定的屏幕捕获)
// 参考 win-capture/game-capture.c 的捕获逻辑
capture_thread_ = std::thread([this] {
CaptureThreadFunc();
});
// 编码线程(稳定的帧率输出)
// 参考 aja/aja-output.cpp 的输出逻辑
encode_thread_ = std::thread([this] {
EncodeThreadFunc();
});
}
void Stop() {
if (!running_) return;
running_ = false;
if (capture_thread_.joinable()) capture_thread_.join();
if (encode_thread_.joinable()) encode_thread_.join();
// 打印统计信息
std::cout << "=== Pipeline Statistics ===" << std::endl;
std::cout << "Frames Captured: " << frames_captured_ << std::endl;
std::cout << "Frames Encoded: " << frames_encoded_ << std::endl;
std::cout << "Frames Dropped: " << frames_dropped_ << std::endl;
}
private:
void CaptureThreadFunc() {
auto frame_duration = std::chrono::milliseconds(33); // 约30fps
while (running_) {
auto start = std::chrono::steady_clock::now();
// 模拟实际采集(带随机延迟)
auto frame = CaptureScreen();
if (frame) {
// 获取系统时间戳(纳秒)
uint64_t sys_timestamp = std::chrono::steady_clock::now()
.time_since_epoch().count();
// 标准化时间戳(参考 async-delay-filter.c)
frame->timestamp = timestamp_mgr_.ProcessFrame(sys_timestamp);
// 推入缓冲区(参考 nvidia-audiofx-filter.c)
if (!frame_buffer_.Push(std::move(frame))) {
frames_dropped_++;
}
frames_captured_++;
}
// 控制采集帧率
auto elapsed = std::chrono::steady_clock::now() - start;
auto sleep_time = frame_duration - elapsed;
if (sleep_time.count() > 0) {
std::this_thread::sleep_for(sleep_time);
}
}
}
void EncodeThreadFunc() {
while (running_) {
// 从缓冲区取帧(参考 nvidia-audiofx-filter.c:893)
auto frame = frame_buffer_.Pop(100);
if (!frame) continue;
// 帧率转换决策(参考 aja-output.cpp:419)
auto action = converter_.ProcessInputFrame();
switch (action) {
case FramerateConverter::COPY_ONCE:
EncodeFrame(frame.get());
frames_encoded_++;
break;
case FramerateConverter::COPY_TWICE:
// 参考 aja-output.cpp:419 的复制逻辑
// freeFrame = false; // 不释放帧
EncodeFrame(frame.get());
EncodeFrame(frame.get()); // 复制帧
frames_encoded_ += 2;
break;
case FramerateConverter::SKIP:
// 跳过此帧
frames_dropped_++;
break;
}
}
}
std::unique_ptr<VideoFrame> CaptureScreen() {
// 模拟不稳定的采集延迟(15-50ms)
std::this_thread::sleep_for(
std::chrono::milliseconds(15 + rand() % 35)
);
auto frame = std::make_unique<VideoFrame>(width_, height_);
// 填充测试图案(可选)
// memset(frame->data, 0x80, width_ * height_ * 4);
return frame;
}
void EncodeFrame(const VideoFrame* frame) {
if (encode_callback_) {
encode_callback_(frame);
} else {
// 默认输出(用于调试)
std::cout << "Encoding frame at ts=" << frame->timestamp
<< " ns" << std::endl;
}
}
TimestampManager timestamp_mgr_;
FrameBuffer<VideoFrame> frame_buffer_;
FramerateConverter converter_;
uint32_t width_;
uint32_t height_;
std::thread capture_thread_;
std::thread encode_thread_;
std::atomic<bool> running_;
EncodeCallback encode_callback_;
// 统计信息
std::atomic<uint64_t> frames_captured_;
std::atomic<uint64_t> frames_encoded_;
std::atomic<uint64_t> frames_dropped_;
};
6. 使用示例
#include "capture_pipeline.h"
#include <iostream>
#include <iomanip>
void EncodeCallback(const VideoFrame* frame) {
// 实际编码逻辑(例如调用FFmpeg)
static uint64_t last_ts = 0;
uint64_t delta = frame->timestamp - last_ts;
last_ts = frame->timestamp;
std::cout << std::fixed << std::setprecision(2)
<< "Encode: ts=" << (frame->timestamp / 1000000.0) << "ms, "
<< "delta=" << (delta / 1000000.0) << "ms" << std::endl;
}
int main() {
// 场景1:60fps采集 -> 30fps编码
std::cout << "=== Test: 60fps -> 30fps ===" << std::endl;
{
CapturePipeline pipeline(60, 30, 1920, 1080);
pipeline.SetEncodeCallback(EncodeCallback);
pipeline.Start();
std::this_thread::sleep_for(std::chrono::seconds(5));
pipeline.Stop();
}
std::cout << "\\n=== Test: 30fps -> 60fps ===" << std::endl;
{
CapturePipeline pipeline(30, 60, 1920, 1080);
pipeline.SetEncodeCallback(EncodeCallback);
pipeline.Start();
std::this_thread::sleep_for(std::chrono::seconds(5));
pipeline.Stop();
}
return 0;
}

4282

被折叠的 条评论
为什么被折叠?



