用OpenCV写个视频播放器可还行?(C++版)

引言

提到OpenCV,大家首先想到的可能是图像处理、目标检测,但你是否想过------用OpenCV实现一个带进度条、倍速播放、暂停功能的视频播放器?本文将通过一个实战项目,带你深入掌握OpenCV的视频处理能力,并解锁以下功能:

  • 基础播放/暂停
  • 动态倍速调节(0.5x~4x)
  • 交互式进度条
  • 实时时间戳显示

文末提供完整代码,可直接运行!

一、环境准备

安装OpenCV

请参考其他博客,C++版本的OpenCV安装,每个操作系统都有自己的玩法。比如我在Ubuntu中下载了OpenCV 4.11版本的源码自行编译。可以参考我的上一篇文章

准备测试视频

准备一个MP4或AVI格式的视频文件(示例代码路径为/home/user/video.mp4,读者自行替换)。

二、核心功能实现

1. 基础播放器

cpp 复制代码
#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的延迟时间实现变速:

cpp 复制代码
#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的滑动条控件实现交互:

cpp 复制代码
#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", &currentFrame, 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. 实时信息叠加

在视频帧上绘制进度条和时间戳:

cpp 复制代码
    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%的问题,当然,也将程序引入了复杂的困境。

cpp 复制代码
#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初探中的第二个程序:视频。

相关推荐
zephyr_zeng12 分钟前
VsCode + EIDE + OpenOCD + STM32(野火DAP) 开发环境配置
c语言·c++·vscode·stm32·单片机·嵌入式硬件·编辑器
Stack Overflow?Tan901 小时前
c++实现在同一台主机两个程序实现实时通信
开发语言·c++
@@永恒2 小时前
map&set
c++
小鹏编程2 小时前
【C++教程】C++中的基本数据类型
开发语言·c++·教程·少儿编程
熊峰峰2 小时前
C++第十节:map和set的介绍与使用
开发语言·c++
Antonio9153 小时前
【网络编程】事件选择模型
网络·c++
jndingxin3 小时前
OpenCV计算摄影学(15)无缝克隆(Seamless Cloning)调整图像颜色的函数colorChange()
人工智能·opencv·计算机视觉
kimi-2223 小时前
plt和cv2有不同的图像表示方式和颜色通道顺序
人工智能·opencv·计算机视觉
春末的南方城市3 小时前
阿里发布新开源视频生成模型Wan-Video,支持文生图和图生图,最低6G就能跑,ComFyUI可用!
人工智能·计算机视觉·自然语言处理·开源·aigc·音视频