用OpenCV写个视频播放器可还行?(C++版)
引言
提到OpenCV,大家首先想到的可能是图像处理、目标检测,但你是否想过——用OpenCV实现一个带进度条、倍速播放、暂停功能的视频播放器?本文将通过一个实战项目,带你深入掌握OpenCV的视频处理能力,并解锁以下功能:
- 基础播放/暂停
- 动态倍速调节(0.5x~4x)
- 交互式进度条
- 实时时间戳显示
文末提供完整代码,可直接运行!
一、环境准备
安装OpenCV
请参考其他博客,C++版本的OpenCV安装,每个操作系统都有自己的玩法。比如我在Ubuntu中下载了OpenCV 4.11版本的源码自行编译。可以参考我的上一篇文章
准备测试视频
准备一个MP4或AVI格式的视频文件(示例代码路径为/home/user/video.mp4,读者自行替换)。
二、核心功能实现
1. 基础播放器
#include <opencv2/opencv.hpp>
int main() {
// 1. 创建视频捕获对象(核心对象)
cv::VideoCapture cap("video.mp4");
// 2. 主循环(核心逻辑)
while(true) {
cv::Mat frame;
// 3. 读取帧(核心方法)
if (!cap.read(frame))
break;
// 4. 显示帧(核心输出)
cv::imshow("Video", frame);
// 5. 控制逻辑(核心交互)
int key = cv::waitKey(25); // 控制播放速度
if(key == 27) break; // ESC退出
}
// 6. 资源释放(核心清理)
cap.release();
cv::destroyAllWindows();
return 0;
}
代码解析
- VideoCapture:支持文件、摄像头、网络流多种输入源。
- waitKey(25):控制播放速度(25ms对应约40 FPS)。
三、功能扩展:让播放器更强大
1. 倍速播放
通过调整waitKey的延迟时间实现变速:
#include <opencv2/opencv.hpp>
int main() {
cv::VideoCapture cap("video.mp4");
double fps = cap.get(cv::CAP_PROP_FPS);
int base_delay = 1000 / fps; // 基准延迟
double speed = 1.0; // 初始速度
while(true) {
cv::Mat frame;
if(!cap.read(frame)) break;
cv::imshow("Video", frame);
// 核心控制逻辑
int delay = static_cast<int>(base_delay / speed);
int key = cv::waitKey(std::max(1, delay));
if(key == 27) break; // ESC退出
else if(key == '+') speed = std::min(4.0, speed + 0.5); // 加速
else if(key == '-') speed = std::max(0.5, speed - 0.5); // 减速
}
cap.release();
cv::destroyAllWindows();
return 0;
}
按+加速,按-减速,速度范围限制在0.5x~4x。
2. 进度条与跳转
利用OpenCV的滑动条控件实现交互:
#include <opencv2/opencv.hpp>
using namespace cv;
// 全局变量
VideoCapture cap;
int currentFrame = 0;
int totalFrames = 0;
// 进度条回调函数
void onTrackbar(int pos, void* userdata) {
cap.set(CAP_PROP_POS_FRAMES, pos);
currentFrame = pos;
}
int main() {
cap.open("video.mp4");
totalFrames = cap.get(CAP_PROP_FRAME_COUNT);
namedWindow("Video Player");
createTrackbar("Progress", "Video Player", ¤tFrame, totalFrames, onTrackbar);
Mat frame;
while(true) {
cap.read(frame);
if(frame.empty()) break;
// 更新当前帧位置
currentFrame = cap.get(CAP_PROP_POS_FRAMES);
setTrackbarPos("Progress", "Video Player", currentFrame);
imshow("Video Player", frame);
if(waitKey(30) == 27) break; // ESC退出
}
cap.release();
destroyAllWindows();
return 0;
}
3. 实时信息叠加
在视频帧上绘制进度条和时间戳:
void drawOverlay(Mat& frame) {
int videoWidth = frame.cols;
float progressRatio = static_cast<float>(currentFrame) / totalFrames;
int progressWidth = static_cast<int>(videoWidth * progressRatio);
rectangle(frame, Point(0, 10), Point(progressWidth, 30), Scalar(0, 255, 0), FILLED);
double currentTime = currentFrame / fps;
ostringstream timeText;
timeText << "Time: " << fixed << setprecision(2) << currentTime << "s";
putText(frame, timeText.str(), Point(10, 60), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
ostringstream speedText;
speedText << "Speed: " << fixed << setprecision(1) << speed << "x";
putText(frame, speedText.str(), Point(10, 100), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
}
四、完整代码
注意,完整代码中头文件是分开引入的。只引入了opencv两个模块:highgui和imgproc。并优化了快进时CPU占用100%的问题,当然,也将程序引入了复杂的困境。
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <iostream>
#include <iomanip>
#include <chrono>
#include <string>
#include <sstream>
using namespace cv;
using namespace std;
using namespace chrono;
class VideoPlayer {
private:
VideoCapture cap;
int totalFrames;
double fps;
int baseDelay;
bool pause;
int currentFrame;
double speed;
string windowName;
high_resolution_clock::time_point lastFrameTime;
static void onTrackbar(int pos, void* userdata) {
VideoPlayer* self = static_cast<VideoPlayer*>(userdata);
if (self->currentFrame != pos) {
self->cap.set(CAP_PROP_POS_FRAMES, pos);
self->currentFrame = pos;
}
}
void drawOverlay(Mat& frame) {
int videoWidth = frame.cols;
float progressRatio = static_cast<float>(currentFrame) / totalFrames;
int progressWidth = static_cast<int>(videoWidth * progressRatio);
rectangle(frame, Point(0, 10), Point(progressWidth, 30), Scalar(0, 255, 0), FILLED);
double currentTime = currentFrame / fps;
ostringstream timeText;
timeText << "Time: " << fixed << setprecision(2) << currentTime << "s";
putText(frame, timeText.str(), Point(10, 60), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
ostringstream speedText;
speedText << "Speed: " << fixed << setprecision(1) << speed << "x";
putText(frame, speedText.str(), Point(10, 100), FONT_HERSHEY_SIMPLEX, 0.7, Scalar(0, 255, 0), 2);
}
public:
VideoPlayer(const string& videoPath) :
windowName("OpenCV Video Player"),
pause(false),
currentFrame(0),
speed(1.0) {
cap.open(videoPath);
if (!cap.isOpened()) {
throw runtime_error("无法打开视频文件");
}
totalFrames = static_cast<int>(cap.get(CAP_PROP_FRAME_COUNT));
if (totalFrames <= 0) {
throw runtime_error("无效的视频文件");
}
fps = cap.get(CAP_PROP_FPS);
baseDelay = static_cast<int>(1000.0 / fps);
namedWindow(windowName, WINDOW_AUTOSIZE);
createTrackbar("Progress", windowName, nullptr, totalFrames, onTrackbar, this);
setTrackbarPos("Progress", windowName, 0);
lastFrameTime = high_resolution_clock::now();
}
void run() {
Mat frame;
int64 frameCounter = 0;
double accumulatedDelay = 0;
while (true) {
auto startTime = high_resolution_clock::now();
if (!pause) {
// 动态帧处理控制
double speedFactor = max(0.5, min(4.0, speed));
int framesToProcess = static_cast<int>(speedFactor);
int actuallyProcessed = 0;
for (int i = 0; i < framesToProcess; ++i) {
bool ret = cap.read(frame);
if (!ret) {
cap.set(CAP_PROP_POS_FRAMES, 0);
currentFrame = 0;
setTrackbarPos("Progress", windowName, currentFrame);
break;
}
currentFrame = static_cast<int>(cap.get(CAP_PROP_POS_FRAMES));
actuallyProcessed++;
}
if (actuallyProcessed > 0) {
// 更新进度条(每5帧更新一次以降低开销)
if (frameCounter++ % 5 == 0) {
setTrackbarPos("Progress", windowName, currentFrame);
}
drawOverlay(frame);
imshow(windowName, frame);
}
}
// 智能延迟控制
auto processTime = duration_cast<milliseconds>(high_resolution_clock::now() - startTime).count();
int targetDelay = static_cast<int>((baseDelay / speed) - processTime);
int actualDelay = max(1, targetDelay);
int key = waitKeyEx(actualDelay);
handleKeyInput(key);
// 自动跳帧机制(当处理落后时)
if (targetDelay < 0 && speed > 1.0) {
int skipFrames = static_cast<int>(-targetDelay / baseDelay * speed);
currentFrame = min(currentFrame + skipFrames, totalFrames - 1);
cap.set(CAP_PROP_POS_FRAMES, currentFrame);
}
}
}
private:
void handleKeyInput(int key) {
switch (key) {
case 27: // ESC
cap.release();
destroyAllWindows();
exit(0);
case 32: // Space
pause = !pause;
break;
case '+':
speed = min(4.0, speed + 0.5);
cout << "Speed: " << speed << "x" << endl;
break;
case '-':
speed = max(0.5, speed - 0.5);
cout << "Speed: " << speed << "x" << endl;
break;
}
}
};
int main() {
try {
VideoPlayer player("C:\\Users\\lebro\\Videos\\video_test.mp4");
player.run();
}
catch (const exception& e) {
cerr << "Error: " << e.what() << endl;
return -1;
}
return 0;
}
五、后记
请参考《学习OpenCV3》,第二章 OpenCV初探中的第二个程序:视频。