使用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不支持该缓存配置。
耗时递增的核心原因
- 随机帧定位的累积开销:每次调用
videoCapture.set(cv::CAP_PROP_POS_FRAMES, frameIndex)属于视频流的随机访问操作。FFmpeg需要不断回溯查找关键帧、重新解析流数据,随着帧索引增大,这种定位的耗时会持续累积。 - 临时文件的隐性IO损耗:将视频从内存写入临时文件再读取的流程,会依赖系统磁盘缓存。随着播放推进,缓存命中率下降,间接增加后续帧读取的IO开销。
- 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
相关产品推荐
相关产品推荐

