深入理解帧率

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秒

封装格式的帧率兼容性

封装格式帧率支持特点
MP4CFR/VFR支持可变帧率但兼容性一般
MKVVFR优秀原生支持可变帧率
FLVCFR only只支持固定帧率
MPEG-TSCFR优先广播标准,要求严格

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 fps66.67 ms±33 ms
24 fps41.67 ms±20 ms中高
30 fps33.33 ms±16 ms
60 fps16.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 fps20-25 fps< 200ms
操作引导(探头定位)20 fps30 fps< 100ms
介入手术(针头定位)30 fps60 fps< 50ms
心动图(快速运动)30 fps60-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-200ms100-200msWindows GDI API调用
提取像素50-100ms150-300ms8MB数据复制
格式转换50-100ms200-400ms逐像素BGRA→BGR
PNG编码+写盘50-150ms250-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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dylan55_you

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值