OpenCV C/C++ 视频播放器 (支持调速和进度控制)
OpenCV C/C++ 视频播放器 (支持调速和进度控制)
本文将引导你使用 C++ 和 OpenCV 库创建一个功能稍复杂的视频播放器。该播放器不仅能播放视频,还允许用户通过滑动条来调整播放速度(加速/减速)以及控制视频的播放进度。
使用opencv打开不会压缩画质,你看起来的效果和其他播放器打开的不一样,会觉得很高清
目录
- 功能简介
- 先决条件
- 核心组件
- 代码实现
- 全局变量与结构
- 滑动条回调函数
- 主函数
main
- 完整代码
- 编译和运行
- 使用说明
- 代码解释
- 速度控制
- 进度控制
- 暂停/播放
- 可能的改进
- 总结
功能简介
- 播放本地视频文件。
- 通过滑动条调整播放速度(例如,0.1x 到 4.0x)。
- 通过滑动条跳转到视频的任意位置。
- 显示当前播放进度。
- 支持暂停和继续播放。
先决条件
- C++ 编译器: 如 GCC (MinGW for Windows), Clang, 或 MSVC。
- OpenCV 库: 版本 3.x 或 4.x 已安装并正确配置。
- 基本的 C++ 知识: 函数、循环、变量、指针等。
- 基本的 OpenCV 知识:
cv::VideoCapture
,cv::Mat
,cv::imshow
,cv::createTrackbar
,cv::waitKey
。
核心组件
cv::VideoCapture
: 用于打开和读取视频帧。cv::imshow
: 用于在窗口中显示视频帧。cv::createTrackbar
: 用于创建速度控制和进度控制的滑动条。- 回调函数: 用于响应滑动条数值的变化。
- 主循环: 控制视频的读取、显示、延迟和用户输入。
代码实现
我们将逐步构建代码。
全局变量与结构
为了在回调函数和主循环之间共享数据,我们会使用一些全局变量。对于更大型的应用,通常会封装在一个类中。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <algorithm> // For std::max and std::min// --- 全局变量 ---
cv::VideoCapture cap;
std::string window_name = "OpenCV Video Player";
int g_slider_progress = 0; // 进度条的当前位置
int g_total_frames = 0; // 视频总帧数
bool g_user_is_dragging_progress_slider = false; // 标记用户是否正在拖动进度条// 速度控制相关
int g_speed_trackbar_val = 100; // 速度条的值 (例如100代表1.0x)
const int g_speed_trackbar_max = 400; // 速度条最大值 (例如400代表4.0x)
const int g_speed_trackbar_min = 10; // 速度条最小值 (例如10代表0.1x)
double g_current_fps_multiplier = 1.0; // 当前播放速度倍率bool g_paused = false; // 暂停状态
cv::Mat g_current_frame_for_pause; // 用于暂停时显示的当前帧
滑动条回调函数
我们需要两个回调函数:一个用于进度条,一个用于速度条。
// --- 进度条回调函数 ---
void on_trackbar_progress(int pos, void* userdata) {if (g_user_is_dragging_progress_slider && cap.isOpened()) {// 只有当用户实际改变滑块位置时才跳转// (避免程序自身更新滑块位置时触发不必要的跳转)if (std::abs(pos - (int)cap.get(cv::CAP_PROP_POS_FRAMES)) > 1) { // 容差,避免微小抖动cap.set(cv::CAP_PROP_POS_FRAMES, pos);}g_slider_progress = pos; // 确保全局变量也更新}
}// --- 速度条回调函数 ---
void on_trackbar_speed(int pos, void* userdata) {if (pos < g_speed_trackbar_min) { // 防止速度过低或为0g_speed_trackbar_val = g_speed_trackbar_min;cv::setTrackbarPos("Speed x0.01", window_name, g_speed_trackbar_min);} else {g_speed_trackbar_val = pos;}g_current_fps_multiplier = static_cast<double>(g_speed_trackbar_val) / 100.0;
}// OpenCV的createTrackbar在内部处理鼠标事件,
// 我们需要一个通用的鼠标回调来检测用户是否开始/停止拖动进度条
void on_mouse_event_progress(int event, int x, int y, int flags, void* userdata) {// 这个函数可以用来更精确地控制 g_user_is_dragging_progress_slider// 但OpenCV的Trackbar没有直接提供这种拖动状态,这里简化处理// 简单地假设,如果回调被调用且值改变,就是用户操作// 更稳健的方法需要自己绘制滑动条或使用更高级的UI库// 这里我们依赖于一个简化的逻辑:在设置进度条位置前,先设置 g_user_is_dragging_progress_slider// 并在 on_trackbar_progress 中检查。// 对于进度条,更常见的是,当用户按下鼠标左键在滑动条上时,我们设置一个标志,// 释放时清除标志。OpenCV 的 createTrackbar 没有直接暴露这些事件。// 替代方案:on_trackbar_progress 被调用时,我们认为可能是用户操作。// 主循环中程序更新进度条时,我们不希望 on_trackbar_progress 错误地认为用户在拖动。// 这就是为什么我们在主循环中更新进度条时要小心。// 为了简化,我们直接在 on_trackbar_progress 中处理,并接受程序更新也可能调用它。// 如果 pos 与当前 cap.get(cv::CAP_PROP_POS_FRAMES) 不同,则认为是用户或程序触发的有效seek。// 在本例中,我们将在主循环中设置一个标志,表明是程序在更新。
}
注意: 精确检测用户是否正在 拖动 OpenCV 原生滑动条是比较棘手的。createTrackbar
的回调是在值 改变后 触发的。更理想的方案是,程序更新滑动条时不应触发 cap.set
。
一个更可行的简化方案是在 on_trackbar_progress
中,仅当值与视频当前帧显著不同时才 cap.set
,并在主循环中更新 g_slider_progress
和 cv::setTrackbarPos
。
// 简化的进度条回调
void on_trackbar_progress_simplified(int pos, void* userdata) {if (!cap.isOpened()) return;// 只有当滑动条的位置与视频当前帧位置有显著差异时,才认为是用户拖动并执行跳转// 或者我们引入一个外部标志来区分用户拖动和程序更新// 这里假设回调是用户操作的结果cap.set(cv::CAP_PROP_POS_FRAMES, pos);g_slider_progress = pos; // 同步全局变量
}
主函数 main
这里是所有逻辑的汇集处。
int main(int argc, char** argv) {std::string video_path;if (argc > 1) {video_path = argv[1];} else {std::cout << "请输入视频文件路径: ";std::cin >> video_path;}if (!cap.open(video_path)) {std::cerr << "错误: 无法打开视频文件: " << video_path << std::endl;return -1;}g_total_frames = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_COUNT));double fps_original = cap.get(cv::CAP_PROP_FPS);if (fps_original <= 0) { // 防止除以0或负数std::cout << "警告: 无法获取视频的有效FPS,将使用默认值30 FPS。" << std::endl;fps_original = 30.0;}int base_delay_ms = static_cast<int>(1000.0 / fps_original);cv::namedWindow(window_name, cv::WINDOW_AUTOSIZE);// 创建进度条// 注意:传递 &g_slider_progress 使滑动条直接绑定到该变量// 当用户拖动时,g_slider_progress 会被OpenCV更新,然后回调被触发cv::createTrackbar("Progress", window_name, &g_slider_progress, g_total_frames > 0 ? g_total_frames - 1 : 0, on_trackbar_progress_simplified);// 创建速度条cv::createTrackbar("Speed x0.01", window_name, &g_speed_trackbar_val, g_speed_trackbar_max, on_trackbar_speed);cv::setTrackbarMin("Speed x0.01", window_name, g_speed_trackbar_min); // 设置最小值cv::setTrackbarPos("Speed x0.01", window_name, 100); // 初始速度1.0x (对应值100)on_trackbar_speed(100, 0); // 初始化速度倍率cv::Mat frame;int current_frame_number = 0;while (true) {if (!g_paused) {bool success = cap.read(frame);if (!success) { // 如果读取失败(视频结束或错误)std::cout << "视频结束或读取帧失败。" << std::endl;// 可以选择重置播放或退出cap.set(cv::CAP_PROP_POS_FRAMES, 0); // 重置到开头cv::setTrackbarPos("Progress", window_name, 0);g_slider_progress = 0;// continue; // 如果想循环播放g_paused = true; // 暂停在最后一帧或黑屏if(frame.empty() && !g_current_frame_for_pause.empty()){frame = g_current_frame_for_pause.clone(); // 显示暂停前的最后一帧} else if (frame.empty()){break; // 如果一开始就没帧,则退出}}if (!frame.empty()) {g_current_frame_for_pause = frame.clone(); // 保存当前帧用于暂停}} else { // 如果是暂停状态if (g_current_frame_for_pause.empty() && cap.isOpened()) { // 如果暂停时没有缓存帧,尝试读取一帧cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress); //确保位置正确cap.read(g_current_frame_for_pause);}// 如果仍然为空,可能视频有问题或已结束if(g_current_frame_for_pause.empty()){std::cout << "暂停时无有效帧可显示。" << std::endl;frame = cv::Mat::zeros(cv::Size(640,480), CV_8UC3); // 显示黑屏} else {frame = g_current_frame_for_pause.clone(); // 使用暂停时缓存的帧}}if (frame.empty()){// 如果所有尝试后帧仍然为空,可能真的无法播放了std::cout << "错误:帧为空,无法继续播放。" << std::endl;break;}// 更新进度条的当前位置 (非用户拖动时)current_frame_number = static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES));if (current_frame_number != g_slider_progress) { // 避免不必要的setTrackbarPos调用g_slider_progress = current_frame_number;cv::setTrackbarPos("Progress", window_name, g_slider_progress);}cv::imshow(window_name, frame);int delay = static_cast<int>(static_cast<double>(base_delay_ms) / g_current_fps_multiplier);if (delay <= 0) delay = 1; // waitKey至少需要1mschar key = (char)cv::waitKey(delay);if (key == 27 || key == 'q' || key == 'Q') { // ESC 或 q/Q 退出break;} else if (key == ' ') { // 空格键 暂停/播放g_paused = !g_paused;if (!g_paused && !g_current_frame_for_pause.empty()) {// 从暂停状态恢复时,确保视频捕获对象的位置与滑块一致// 防止因暂停期间滑块被拖动而导致的播放位置不匹配if(std::abs(g_slider_progress - (int)cap.get(cv::CAP_PROP_POS_FRAMES)) > 1) {cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress);}}}}cap.release();cv::destroyAllWindows();return 0;
}
完整代码
将上述所有部分(全局变量、回调函数、main
函数)合并到一个 .cpp
文件中。
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
#include <algorithm> // For std::max and std::min// --- 全局变量 ---
cv::VideoCapture cap;
std::string window_name = "OpenCV Video Player";
int g_slider_progress = 0; // 进度条的当前位置
int g_total_frames = 0; // 视频总帧数// 速度控制相关
int g_speed_trackbar_val = 100; // 速度条的值 (例如100代表1.0x)
const int g_speed_trackbar_max = 400; // 速度条最大值 (例如400代表4.0x)
const int g_speed_trackbar_min = 10; // 速度条最小值 (例如10代表0.1x)
double g_current_fps_multiplier = 1.0; // 当前播放速度倍率bool g_paused = false; // 暂停状态
cv::Mat g_current_frame_for_pause; // 用于暂停时显示的当前帧// --- 进度条回调函数 ---
// 当用户拖动进度条时,此函数被调用
void on_trackbar_progress(int pos, void* userdata) {if (!cap.isOpened()) return;// `pos` 是滑动条的新位置// 我们只在 `pos` 与视频捕获对象的内部帧计数器显著不同时才设置帧位置,// 以避免在程序更新滑动条时(非用户拖动)产生循环调用或抖动。// `userdata` 在这里没有使用。if (std::abs(pos - static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES))) > 1) {cap.set(cv::CAP_PROP_POS_FRAMES, pos);}g_slider_progress = pos; // 确保全局变量与滑动条同步
}// --- 速度条回调函数 ---
// 当用户拖动速度条时,此函数被调用
void on_trackbar_speed(int pos, void* userdata) {if (pos < g_speed_trackbar_min) {g_speed_trackbar_val = g_speed_trackbar_min;// 如果用户尝试设置低于最小值,强制将滑动条也设回最小值cv::setTrackbarPos("Speed x0.01", window_name, g_speed_trackbar_min);} else {g_speed_trackbar_val = pos;}// 将滑动条的值 (例如 10 到 400) 转换为速度倍率 (例如 0.1x 到 4.0x)g_current_fps_multiplier = static_cast<double>(g_speed_trackbar_val) / 100.0;
}int main(int argc, char** argv) {std::string video_path;if (argc > 1) {video_path = argv[1];} else {std::cout << "使用方法: " << argv[0] << " <视频文件路径>" << std::endl;std::cout << "请输入视频文件路径: ";std::getline(std::cin, video_path); // 使用getline读取可能带空格的路径}if (video_path.empty()){std::cerr << "错误: 未提供视频文件路径。" << std::endl;return -1;}// 移除路径两端可能存在的引号(常见于拖拽文件到命令行)if (video_path.front() == '"' && video_path.back() == '"') {video_path = video_path.substr(1, video_path.length() - 2);}if (!cap.open(video_path)) {std::cerr << "错误: 无法打开视频文件: \"" << video_path << "\"" << std::endl;return -1;}g_total_frames = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_COUNT));double fps_original = cap.get(cv::CAP_PROP_FPS);if (fps_original <= 0) {std::cout << "警告: 无法获取视频的有效FPS,将使用默认值 30 FPS。" << std::endl;fps_original = 30.0;}int base_delay_ms = static_cast<int>(1000.0 / fps_original);cv::namedWindow(window_name, cv::WINDOW_AUTOSIZE);// 创建进度条if (g_total_frames > 0) {cv::createTrackbar("Progress", window_name, &g_slider_progress, g_total_frames - 1, on_trackbar_progress);}// 创建速度条cv::createTrackbar("Speed x0.01", window_name, &g_speed_trackbar_val, g_speed_trackbar_max, on_trackbar_speed);cv::setTrackbarMin("Speed x0.01", window_name, g_speed_trackbar_min);cv::setTrackbarPos("Speed x0.01", window_name, 100); // 初始速度1.0xon_trackbar_speed(100, 0); // 手动调用一次以初始化g_current_fps_multipliercv::Mat frame;int current_frame_display_number = 0; // 用于UI显示的帧号std::cout << "按 '空格键' 暂停/播放, 'ESC' 或 'q' 退出." << std::endl;while (true) {if (!g_paused) {bool success = cap.read(frame);if (!success) {std::cout << "视频结束或读取帧失败。" << std::endl;// 视频结束时可以选择暂停在最后一帧g_paused = true; if (g_current_frame_for_pause.empty()) { // 如果从未成功读取过帧std::cout << "没有帧可以显示,退出。" << std::endl;break;}frame = g_current_frame_for_pause.clone(); // 显示最后一帧} else {g_current_frame_for_pause = frame.clone(); // 保存当前有效帧}} else { // 暂停状态if (g_current_frame_for_pause.empty()) {// 尝试在当前进度条位置读取一帧作为暂停帧// 这通常在视频开始就暂停,或跳转后暂停时发生if (cap.isOpened() && g_total_frames > 0) {cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress);cap.read(g_current_frame_for_pause);}}// 如果仍然没有可显示的暂停帧,则显示黑屏或之前的帧if (!g_current_frame_for_pause.empty()) {frame = g_current_frame_for_pause.clone();} else {// 作为最后手段,显示一个黑屏cv::Size frame_size = cap.isOpened() ? cv::Size(static_cast<int>(cap.get(cv::CAP_PROP_FRAME_WIDTH)), static_cast<int>(cap.get(cv::CAP_PROP_FRAME_HEIGHT))) :cv::Size(640, 480); // 默认大小if(frame_size.width <= 0 || frame_size.height <=0) frame_size = cv::Size(640,480);frame = cv::Mat::zeros(frame_size, CV_8UC3);std::cout << "暂停时无有效帧可显示,显示黑屏。" << std::endl;}}if (frame.empty()){std::cout << "错误:帧为空,无法继续播放。" << std::endl;break;}// 更新进度条的当前位置 (仅当未被用户拖动时)// cv::VideoCapture::get(cv::CAP_PROP_POS_FRAMES) 获取的是下一帧的索引current_frame_display_number = static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES));if (current_frame_display_number > 0 && !g_paused) { // 通常 POS_FRAMES 指向下一帧current_frame_display_number--; // 显示的是当前帧的编号}if (g_paused) { // 暂停时,进度条应显示当前暂停帧的编号current_frame_display_number = g_slider_progress;}// 避免在回调函数中再次设置,导致可能的抖动// 我们只在程序逻辑前进时更新滑动条// 而用户拖动滑动条时,回调函数会更新 g_slider_progress 和视频位置if (g_total_frames > 0 && std::abs(g_slider_progress - current_frame_display_number) > 0 && !g_paused) {// 只有当程序播放导致帧号变化时,才更新滑动条绑定的g_slider_progress// 并且确保不是因为回调函数刚刚设置了cap.set而立即又被这里的cap.get更新回来// 这是一个简化模型,最理想的是区分用户拖动和程序更新g_slider_progress = current_frame_display_number;cv::setTrackbarPos("Progress", window_name, g_slider_progress);} else if (g_total_frames > 0 && g_paused) {// 暂停时,如果用户拖动了滑块,g_slider_progress会被回调更新// 我们也需要确保滑块视觉位置正确cv::setTrackbarPos("Progress", window_name, g_slider_progress);}cv::imshow(window_name, frame);int delay = static_cast<int>(static_cast<double>(base_delay_ms) / g_current_fps_multiplier);if (delay <= 0) delay = 1;char key = (char)cv::waitKey(delay);if (key == 27 || key == 'q' || key == 'Q') {break;} else if (key == ' ') {g_paused = !g_paused;if (!g_paused) { // 从暂停恢复播放// 确保视频从滑动条指示的位置开始播放if (cap.isOpened() && std::abs(g_slider_progress - static_cast<int>(cap.get(cv::CAP_PROP_POS_FRAMES))) > 1) {cap.set(cv::CAP_PROP_POS_FRAMES, g_slider_progress);}std::cout << "播放中..." << std::endl;} else { // 进入暂停std::cout << "已暂停." << std::endl;// g_current_frame_for_pause 应该已经被保存了最新的帧}}}cap.release();cv::destroyAllWindows();return 0;
}
编译和运行
使用 CMake (推荐):
- 将上述代码保存为
video_player.cpp
。 - 创建
CMakeLists.txt
文件:cmake_minimum_required(VERSION 3.10) project(VideoPlayer)set(CMAKE_CXX_STANDARD 14) # C++14 或更高 set(CMAKE_CXX_STANDARD_REQUIRED True)find_package(OpenCV REQUIRED)include_directories(${OpenCV_INCLUDE_DIRS}) add_executable(VideoPlayer video_player.cpp) target_link_libraries(VideoPlayer PRIVATE ${OpenCV_LIBS})
- 编译:
mkdir build cd build cmake .. make # 或者ninja, 或者在Visual Studio中构建项目
- 运行:
如果没有在命令行提供路径,程序会提示你输入。./VideoPlayer /path/to/your/video.mp4 # 或者在Windows上 (如果构建在Debug目录): # .\Debug\VideoPlayer.exe C:\path\to\your\video.mp4
使用 g++ (Linux/macOS):
g++ video_player.cpp -o VideoPlayer $(pkg-config --cflags --libs opencv4) # 或 opencv
./VideoPlayer /path/to/your/video.mp4
使用说明
- 启动程序时,如果未在命令行参数中指定视频路径,程序会提示你输入。
- Progress 滑动条: 显示当前播放进度,拖动它可以跳转到视频的不同位置。
- Speed x0.01 滑动条: 调整播放速度。值为 100 表示 1.0x (正常速度),50 表示 0.5x (半速),200 表示 2.0x (两倍速)。范围是 0.1x 到 4.0x。
- 空格键: 暂停或继续播放。
- ESC 或 q/Q键: 退出播放器。
代码解释
速度控制
- 视频的原始
fps_original
(每秒帧数) 决定了每帧的基础延迟base_delay_ms = 1000 / fps_original
。 - 速度滑动条的值
g_speed_trackbar_val
(例如范围 10-400) 被转换为一个倍率g_current_fps_multiplier
(例如 0.1 - 4.0)。 - 实际的帧间延迟
delay
通过base_delay_ms / g_current_fps_multiplier
计算。- 倍率 > 1.0 (加速):
delay
减小,播放加快。 - 倍率 < 1.0 (减速):
delay
增大,播放减慢。
- 倍率 > 1.0 (加速):
cv::waitKey(delay)
不仅提供延迟,还处理 GUI 事件(如滑动条的拖动)。
进度控制
- 进度条的最大值设置为视频的总帧数
g_total_frames - 1
。 - 当用户拖动进度条时,
on_trackbar_progress
回调函数被触发。 - 在该回调中,
cap.set(cv::CAP_PROP_POS_FRAMES, pos)
用于将视频的当前读取位置跳转到滑动条指定的新位置pos
。 - 在主循环中,程序会读取视频的当前帧号
cap.get(cv::CAP_PROP_POS_FRAMES)
,并用cv::setTrackbarPos
更新滑动条的显示位置,以反映实际的播放进度。
暂停/播放
- 一个布尔变量
g_paused
跟踪播放状态。 - 按下空格键时,
g_paused
的状态会切换。 - 如果
g_paused
为true
:- 主循环不读取新的视频帧 (
cap.read()
不被调用)。 cv::imshow
持续显示暂停时捕获的最后一帧g_current_frame_for_pause
。
- 主循环不读取新的视频帧 (
- 如果
g_paused
为false
:- 正常读取和显示视频帧。
可能的改进
- 更精确的进度条拖动检测: 当前实现中,程序更新进度条也可能触发回调。可以通过更复杂的事件处理或自定义UI控件来区分用户拖动和程序更新。
- 显示时间码: 在窗口上显示 “当前时间 / 总时间” 而不是帧号。
- 音量控制和音频播放: OpenCV 主要处理视频。播放音频需要额外的库,如 SDL、PortAudio,或者使用 FFmpeg 更底层的 API。
- 逐帧步进: 添加按钮或快捷键实现向前/向后逐帧播放。
- 错误处理: 更详细的错误报告和处理。
- UI美化: 使用 Qt 或其他 GUI 框架替换 OpenCV HighGUI 可以创建更美观和功能丰富的界面。
- 播放列表: 支持加载和管理多个视频文件。
总结
这个项目展示了如何使用 OpenCV 创建一个具有基本播放控制功能的视频播放器。通过滑动条,用户可以方便地控制播放速度和进度,并通过键盘快捷键进行暂停和退出。这是一个很好的练习,可以帮助你熟悉 OpenCV 的视频处理和简单的 GUI 交互。