You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用OpenCV解码视频时帧时间逐渐增长的原因及解决方法

问题分析与解决:OpenCV视频播放耗时递增问题

问题背景

项目通过Git子模块集成OpenCV,CMake配置如下:

set(WITH_FFMPEG ON)
set(VIDEOIO_PLUGIN_LIST "ffmpeg")
set(BUILD_PERF_TESTS OFF)
set(BUILD_TESTS OFF)
set(INSTALL_TESTS OFF)
add_subdirectory(${COJE_SRC_DIR}/vendor/opencv build/build_opencv)

实现了继承自IEntity的Video结构体对接渲染系统,头文件定义:

#pragma once
#include <opencv2/opencv.hpp>
#include <coje/interfaces/IEntity.hpp>
#include <coje/enums/EFileLocation.hpp>
#include <coje/String.hpp>
#include <coje/graphics/Texture.hpp>

namespace coje::entitys
{
 struct Video : IEntity
 {
  String filePath;
  EFileLocation fileLocation;
  String tempname;
  glm::vec2 size;
  UniquePointer<cv::VideoCapture> videoCapturePointer;
  cv::Mat frame;
  cv::Mat frameConverted;
  Floating64 fps = 0;
  Floating64 frameCount = 0;
  Integer64 currentFrameIndex = -1;
  Video(const String &filePath, const EFileLocation &fileLocation, const glm::vec2 &size, const glm::vec3 &position, const glm::quat &rotation);
  ~Video();
  void updateTextureWithFrame(const uInteger64 &frameIndex, UniquePointer<Texture> &texturePointer);
  const Boolean resize(const glm::vec2 &size);
  Boolean update(const uInteger64 &elapsedTimeMs);
 };
}

核心实现代码(关键逻辑):

// 构造函数:将视频写入临时文件后打开
Video::Video(...) : IEntity(EntityType)
{
    File videoFile(filePath, fileLocation, "r");
    auto videoBytes = videoFile.toBytes();
    tempname = std::tmpnam(0);
    {
        File tempFile(tempname, EFileLocation::Relative, "w");
        tempFile & videoBytes;
    }
    videoCapturePointer = {ReleaseType::Delete, new cv::VideoCapture(tempname.c_str(), cv::CAP_FFMPEG), 1};
    // ... 初始化参数、定时器
}

// 帧更新函数:每次跳转到指定帧索引读取
void Video::updateTextureWithFrame(const uInteger64 &frameIndex, UniquePointer<Texture> &texturePointer)
{
    auto &videoCapture = *videoCapturePointer.pointer;
    videoCapture.set(cv::CAP_PROP_POS_FRAMES, frameIndex);
    Boolean frameGrabSuccess = videoCapture.grab();
    Boolean frameRetrieveSuccess = videoCapture.retrieve(frame);
    // ... 格式转换、纹理更新
}

// 定时触发更新
Boolean Video::update(const uInteger64 &elapsedTimeMs)
{
    currentFrameIndex++;
    auto &texturePointer = textures._data[0];
    updateTextureWithFrame(currentFrameIndex, texturePointer);
    return currentFrameIndex < frameCount - 1;
}

运行1280x720测试视频时,updateTextureWithFrame函数耗时从初始12ms逐渐增至100ms以上,且videoCapture.get(cv::CAP_PROP_BUFFERSIZE)返回0,说明FFmpeg不支持该缓存配置。


耗时递增的核心原因

  1. 随机帧定位的累积开销:每次调用videoCapture.set(cv::CAP_PROP_POS_FRAMES, frameIndex)属于视频流的随机访问操作。FFmpeg需要不断回溯查找关键帧、重新解析流数据,随着帧索引增大,这种定位的耗时会持续累积。
  2. 临时文件的隐性IO损耗:将视频从内存写入临时文件再读取的流程,会依赖系统磁盘缓存。随着播放推进,缓存命中率下降,间接增加后续帧读取的IO开销。
  3. Mat对象的重复内存分配:frame和frameConverted未提前预分配内存,每次retrieve和cvtColor可能触发内存重新分配,长期运行会产生内存碎片,降低内存操作效率。

解决方案

1. 改为顺序读取,移除随机帧定位

直接让VideoCapture连续读取帧,避免每次手动跳转帧索引,这是最有效的优化手段:

// 修改updateTextureWithFrame,移除帧定位逻辑
void Video::updateTextureWithFrame(UniquePointer<Texture> &texturePointer)
{
    auto start = std::chrono::high_resolution_clock::now();
    auto &videoCapture = *videoCapturePointer.pointer;
    Boolean frameGrabSuccess = videoCapture.grab();
    if (!frameGrabSuccess)
    {
        Logger(LogType::ERROR, "%s\n", "Failed to grab frame from VideoCapture");
        return;
    }
    Boolean frameRetrieveSuccess = videoCapture.retrieve(frame);
    if (!frameRetrieveSuccess)
    {
        Logger(LogType::ERROR, "%s\n", "Failed to retrieve frame from VideoCapture");
        return;
    }
    // ... 后续格式转换、纹理更新逻辑保持不变
}

// 修改update函数,不再传递帧索引
Boolean Video::update(const uInteger64 &elapsedTimeMs)
{
    currentFrameIndex++;
    Logger(LogType::INFO, "Video-elapsedTime: %llums\n", elapsedTimeMs);
    if (currentFrameIndex >= frameCount)
    {
        return false;
    }
    auto &texturePointer = textures._data[0];
    updateTextureWithFrame(texturePointer);
    return true;
}

2. 优化临时文件处理(可选)

如果视频本身存储在磁盘上,直接通过文件路径打开VideoCapture,跳过内存转临时文件的步骤:

// 构造函数中替换临时文件逻辑
videoCapturePointer = {ReleaseType::Delete, new cv::VideoCapture(filePath.c_str(), cv::CAP_FFMPEG), 1};
// 删除读取文件到内存、写入临时文件的代码

若必须从内存读取视频,可尝试基于FFmpeg自定义IO上下文实现内存流读取(复杂度较高,需结合FFmpeg底层API)。

3. 预分配Mat内存,避免重复分配

在构造函数中根据视频宽高提前分配frame和frameConverted的内存:

Video::Video(...)
{
    // ... 初始化VideoCapture并验证成功后
    uInteger64 frameWidth = videoCapture.get(cv::CAP_PROP_FRAME_WIDTH);
    uInteger64 frameHeight = videoCapture.get(cv::CAP_PROP_FRAME_HEIGHT);
    // 预分配内存,固定尺寸和格式
    frame.create(frameHeight, frameWidth, CV_8UC3);
    frameConverted.create(frameHeight, frameWidth, CV_8UC3);
    // ... 其他初始化逻辑
}

4. 禁用FFmpeg缓存(可选)

通过FFmpeg私有选项强制禁用缓存,减少内存占用和潜在的缓存失效问题:

// 在VideoCapture初始化后添加
videoCapture.set(cv::CAP_PROP_FFMPEG_FLAGS, "nobuffer");

注意:该选项可能降低播放稳定性,需测试后使用。


内容的提问来源于stack exchange,提问作者ZeunO8

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 13:50:55