@TOC 项目源码链接: https://gitee.com/ly121381/cpp-videoplayer 因为代码量已经比较大了,所以这里我们先提交一部分,可以给读者一个参考。从此文章开始项目每更新一篇文章我都会在文章结束之后合并一次分支并提交代码。 界面布局部分已经基本完成,那么接下来就是让我们的客户端能够播放视频。首先我们先改一下原来的播放窗口,将之前的playerPage.cpp中的initPlayer部分的下列代码:
//把标题栏中的文字拿去 setWindowTitle("LimePlayer"); //Qt::CustomizeWindowHint → 开启自定义窗口装饰 Qt::WindowTitleHint → 保留标题栏 setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint); //将窗口设置为模态 setAttribute(Qt::WA_ShowModal);改为:
xxxxxxxxxx //设置为无边框 setWindowFlags(Qt::FramelessWindowHint); //将窗口设置为模态 setAttribute(Qt::WA_ShowModal); setAttribute(Qt::WA_TranslucentBackground);也就是说拿掉它的边框,同时我们重写其鼠标点击与拖动函数,让我们的窗口支持拖拽移动:
xvoid PlayerPage::mousePressEvent(QMouseEvent *event){ // 获取鼠标按下时相对于窗口的位置,检测位置是否在窗口的playHead内部 QPoint point = event->position().toPoint(); if(ui->playHead->geometry().contains(point) && !isMax){ if(event->button() == Qt::LeftButton){ // 计算鼠标按下之后的全局坐标和窗口左上角的相对偏移 // 在整个移动过程中,该偏移不能发生改变 dragPos = event->globalPosition().toPoint() - geometry().topLeft(); return; } }
QWidget::mousePressEvent(event);}
void PlayerPage::mouseMoveEvent(QMouseEvent *event){ QPoint point = event->position().toPoint(); if(ui->playHead->geometry().contains(point) && !isMax){ if(event->buttons() == Qt::LeftButton){ move(event->globalPosition().toPoint() - dragPos); } }
QWidget::mousePressEvent(event);}dragPos是新增的私有成员QPoint类型的成员变量。
接下来在ui界面进行布局,将原来的playControlBox与playHead从screen中拖出来将二者置于screen的下上方。修改后的控件嵌套关系如下:

还记得我们之前有隐藏动画吗,这部分我们不要了,将隐藏动画的所有相关语句删除,然后在之前我们给动画设置的定时器的timeOut的槽函数修改为如下内容:
xxxxxxxxxxvoid PlayerPage::onTimerTimeout(){ //配合淡入淡出达到定时隐藏效果 if (!ui->playControlBox->underMouse() && !ui->videoSilder->underMouse()) { isHide = !isHide; hideTimer->stop(); ui->playHead->hide(); ui->playControlBox->hide(); } else{ //重置定时器 hideTimer->start(); }}也就是说当定时器时间到了之后直接将二者隐藏以尽可能的让screen占多的空间当用户不进行任何操作时。明明原来的效果更好,为什么要这样改,等到下面我们再来解释。
我们先来看一下本文结束时最终的播放器效果:
图中带有头像的是我们自己发的弹幕。下面正式开始本文的内容。
一.mpv库的封装
我们之前做音乐播放器的时候,其实有一个严重的问题没有解决,因为qt的音视频播放是基于系统自身自带的播放器进行解码播放的,就会导致部分类型文件无法播放,比如流式播放中常见的m3u8文件。不了解m3u8文件格式的读者可以去搜一下就知道为什么我们之后要用这种格式的文件进行视频播放了。
那么这时我们最好是不用qt自带的媒体播放类,而是借助mpv来实现我们的播放器。首先我们需要将mpv官方提供的头文件与动态库文件下载到我们的工程目录底下(这组文件可以到我提交的源码中获取):
然后再我们的项目中添加新文件,除了上图中的dll其他文件全部添加到工程中(当然除了mpvplayer.cpp与mpvplayer.h-这是我们后面自己简单对mpv进行了封装的类文件)。
接下来在CMakeLists.txt中添加如下代码:
xxxxxxxxxxset(MPV_DLL ${CMAKE_CURRENT_SOURCE_DIR}/mpv/dll/libmpv-2.dll) #设置mpv库⽬录target_link_libraries(LimePlayer PRIVATE Qt${QT_VERSION_MAJOR}::Widgets ${MPV_DLL})最后将上图dll文件夹下的libmpv-2.dll拷⻉到exe所在⽬录下那么我们的mpv环境配置就结束了。 大家可以去mpv的官网: https://mpv.io/ 它下面有github源码的链接,大家可以看一下人家官方给的mpv在qt中使用的示例,看个七七八八就ok了,我们这里只是简单的使用,接下来我们新添加一个普通类在mpv文件夹下,类名为MpvPlayer,下面是对mpv库的简单封装:
xxxxxxxxxx/////////////////////////////mpvplayer.h
// 包含mpv客户端库头文件
// MpvPlayer类,继承自QObject,用于封装mpv播放器的功能class MpvPlayer : public QObject{ Q_OBJECT // Qt元对象系统宏,启用信号槽机制public: // 构造函数 // videoWindow: 视频渲染窗口,mpv将视频输出到此窗口 // parent: 父对象,用于Qt对象树管理 explicit MpvPlayer(QWidget* videoWindow = nullptr, QObject *parent = nullptr); // 开始播放指定视频文件 void startPlay(const QString &videoPath); // 播放视频(从暂停状态恢复) void play(); // 暂停视频播放 void pause(); // 设置播放速率 // speed: 播放速度倍数(1.0为正常速度) void setSpeed(double speed); // 设置音量 // volume: 音量大小(0.0-100.0) void setVolume(double volume); // 设置当前播放位置 // seconds: 要跳转到的位置(单位:秒) void setCurrentPlayPosition(int64_t seconds); // 析构函数,清理资源 ~MpvPlayer();
signals: // mpv事件信号,用于通知有mpv事件需要处理 void mpvEvents(); // 播放进度改变信号 // seconds: 当前播放时间(单位:秒) void timePosChanged(int64_t seconds);
private: // 处理mpv事件的槽函数 void onMpvEvents(); // 处理具体的mpv事件 // event: mpv事件指针 void handleMpvEvent(mpv_event* event);
private: // mpv句柄,用于与mpv实例交互 mpv_handle* mpv = nullptr;};
// MPVPLAYER_H
//////////////////////////////mpvplayer.cpp// 包含工具函数,如LOG()宏
// 唤醒回调函数,当mpv有事件时需要调用此函数// ctx: 用户自定义上下文指针,这里指向MpvPlayer对象static void wakeUp(void* ctx){ MpvPlayer* mpvPlayer = (MpvPlayer*)ctx; // 发射信号,通知有mpv事件需要处理 emit mpvPlayer->mpvEvents();}
// 构造函数实现MpvPlayer::MpvPlayer(QWidget* videoWindow, QObject *parent) : QObject{parent}{ // 设置程序区域为C语言区域,确保数字格式等的一致性 // mpv内部使用C语言区域设置,避免本地化设置导致的问题 std::setlocale(LC_NUMERIC, "C"); // 创建mpv对象实例 mpv = mpv_create(); if(!mpv) { LOG() << "mpv对象创建失败!"; return; } // 获取视频窗口的窗口句柄(Windows为HWND,X11为Window等) int64_t wid = videoWindow->winId(); // 将窗口ID传给mpv的wid选项,让mpv将视频渲染到指定窗口 // MPV_FORMAT_INT64表示传递的是64位整数类型 mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid); // 观察time-pos属性变化,当播放位置改变时会收到通知 // 参数说明:mpv句柄,观察ID(可自定义),属性名,格式 mpv_observe_property(mpv, 0, "time-pos", MPV_FORMAT_INT64); // 设置mpv唤醒回调函数,当mpv有事件时调用wakeUp函数 mpv_set_wakeup_callback(mpv, wakeUp, this); // 连接信号槽:当mpvEvents信号发射时,调用onMpvEvents槽函数 // Qt::QueuedConnection表示异步连接,确保线程安全 connect(this, &MpvPlayer::mpvEvents, this, &MpvPlayer::onMpvEvents, Qt::QueuedConnection); // 初始化mpv实例 if(mpv_initialize(mpv) < 0){ LOG() << "初始化mpv失败"; mpv_destroy(mpv); mpv = nullptr; // 确保指针置空 }}
// 处理mpv事件的槽函数void MpvPlayer::onMpvEvents(){ // 循环处理mpv中的所有事件 while(mpv) // 确保mpv实例仍然有效 { // 等待mpv事件,0表示立即返回(非阻塞) // 如果有事件则返回事件指针,没有事件则返回特殊事件MPV_EVENT_NONE mpv_event* event = mpv_wait_event(mpv, 0); // 如果没有更多事件,退出循环 if(event->event_id == MPV_EVENT_NONE) { break; } // 处理具体的事件 handleMpvEvent(event); }}
// 处理具体的mpv事件void MpvPlayer::handleMpvEvent(mpv_event *event){ switch (event->event_id) { case MPV_EVENT_PROPERTY_CHANGE: // 属性改变事件 { // 获取属性事件数据 mpv_event_property* eventPropery = (mpv_event_property*)event->data; // 检查数据是否为空(程序刚启动时可能没有视频播放) if(eventPropery->data == nullptr) { return; } // 判断是否为time-pos属性改变(播放位置改变) if (strcmp(eventPropery->name, "time-pos") == 0) { // 提取播放时间(秒) int64_t seconds = *(int64_t*)(eventPropery->data); // 发射信号通知播放进度改变 emit timePosChanged(seconds); } break; } case MPV_EVENT_SHUTDOWN: // mpv关闭事件 { // 清理mpv资源 if(mpv) { // 终止并销毁mpv实例 mpv_terminate_destroy(mpv); mpv = nullptr; // 指针置空,避免野指针 } break; } default: // 可以在这里处理其他类型的事件 // 如:MPV_EVENT_FILE_LOADED, MPV_EVENT_END_FILE, MPV_EVENT_PLAYBACK_RESTART等 break; }}
// 开始播放视频文件void MpvPlayer::startPlay(const QString& videoPath){ // 将QString转换为UTF-8编码的字节数组(mpv需要UTF-8字符串) const QByteArray c_filename = videoPath.toUtf8(); // 构建mpv命令参数数组 // "loadfile": 加载文件命令 // c_filename.data(): 文件路径 // NULL: 参数数组结束标记 const char *args[] = {"loadfile", c_filename.data(), NULL}; // 异步执行加载文件命令,避免阻塞UI线程 // 第二个参数0表示不使用回复ID mpv_command_async(mpv, 0, args);}
// 设置当前播放位置void MpvPlayer::setCurrentPlayPosition(int64_t seconds){ // 异步设置time-pos属性,跳转到指定时间位置 mpv_set_property_async(mpv, 0, "time-pos", MPV_FORMAT_INT64, &seconds);}
// 播放视频(从暂停状态恢复)void MpvPlayer::play(){ int pause = 0; // 0表示播放状态 // 异步设置pause属性为0(取消暂停) mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &pause);}
// 暂停视频播放void MpvPlayer::pause(){ int pause = 1; // 1表示暂停状态 // 异步设置pause属性为1(暂停播放) mpv_set_property_async(mpv, 0, "pause", MPV_FORMAT_FLAG, &pause);}
// 设置播放速率void MpvPlayer::setSpeed(double speed){ // 异步设置speed属性,改变播放速度 // 1.0 = 正常速度,2.0 = 2倍速,0.5 = 半速等 mpv_set_property_async(mpv, 0, "speed", MPV_FORMAT_DOUBLE, &speed);}
// 设置音量void MpvPlayer::setVolume(double volume){ // 异步设置volume属性,调整音量大小 // 通常范围:0.0(静音)到100.0(最大音量) mpv_set_property_async(mpv, 0, "volume", MPV_FORMAT_DOUBLE, &volume);}
// 析构函数实现MpvPlayer::~MpvPlayer(){ // 清理mpv资源 if(mpv) { // 终止并销毁mpv实例 mpv_terminate_destroy(mpv); mpv = nullptr; // 指针置空 }}这部分其实大家可以直接照抄就行,可以看看或搜索下去理解这部分封装中的某些点。注意我们只是简单使用,了解即可。
二.实现播放器中的各项功能
其实有了我们之前做音乐播放器的经验,再加上上面对mpv库的封装,实现播放器的各项功能其实就很简单了。我们这里就不再详细的介绍什么地方需要添加什么了,主要说明几个需要注意的地方,其余详情可以参考我提交的源码:
2.1close导致的mpv对象释放问题
还记得我们之前给关闭按钮绑定了close槽函数了吗,当qt的窗口被close之后,qt会将当前窗口的句柄归还给操作系统,此时mpv_handle对象感知到原来的渲染窗口句柄丢失了,就会触发MPV_EVENT_SHUTDOWN事件,而我们重新打开窗口时因为mpv对象被释放了,所以我们再掉startPlayer方法就会引起空指针的解引用导致程序崩溃,而解决办法就是让其窗口在关闭时不要去close,而是使用hide。
xxxxxxxxxx//让原来绑定close变为绑定playerClosevoid PlayerPage::playerClose(){ this->hide(); bulletScreen->hide(); //暂停视频播放 if(isPlay) onPlayBtnClicked();}2.2弹幕框跟随问题
因为我们的弹幕框是一个透明的QFrame,当窗口位置移动时你的弹幕框自然需要跟随我的播放窗口。但是如果全屏的时候,他就没办法去通过鼠标拖拽事件去跟随了,所以我们需要自己去在最大化函数中改变它的位置,当然还需要去改变它的宽度:
xxxxxxxxxxvoid PlayerPage::onMaxBtnClicked(){ if(!isMax) { showFullScreen(); bulletScreen->setGeometry(0,ui->playHead->height(),width(),bulletScreen->height()); } else { showNormal(); QPoint point = mapToGlobal(QPoint(0,ui->playHead->height())); bulletScreen->setGeometry(point.x(),point.y(),width(),bulletScreen->height()); } isMax = !isMax;}2.3弹幕移动的问题
我们这里给三个弹幕框,然后让每个弹幕只能在当前窗口中停留10s。所以就可以这样去设计所有弹幕的移动逻辑:
xxxxxxxxxxvoid PlayerPage::showBullet(){ QList<BulletScreenInfo> bulletScreenList = bulletScreenLists[playTime]; int topX,middleX,bottomX; topX = middleX = bottomX = width(); for(int i = 0;i < bulletScreenList.size();i++) { //每条弹幕轨道与其上面一列错开一个字 BulletScreenItem* bulletItem; BulletScreenInfo& bulletInfo = bulletScreenList[i]; if(i % 3 == 0) { //显示到第一列 bulletItem = new BulletScreenItem(top); if(bulletInfo.userId == "1") bulletItem->setImage(QPixmap(":/images/homePage/touxiang.png")); bulletItem->setText(bulletInfo.text); int duration = 10000 / (double)width() * (topX + bulletItem->width());//让其在窗口中存在的时间大约为10s左右 bulletItem->initAnimation(topX, duration); //每两个弹幕之间隔四个字 topX += bulletItem->width() + 4 * 18; } else if(i % 3 == 1) { //显示到第二列 bulletItem = new BulletScreenItem(middle); if(bulletInfo.userId == "1") bulletItem->setImage(QPixmap(":/images/homePage/touxiang.png")); bulletItem->setText(bulletInfo.text); int duration = 10000 / (double)width() * (middleX + bulletItem->width());//让其在窗口中存在的时间大约为10s左右 bulletItem->initAnimation(middleX + 18,duration); //每两个弹幕之间隔四个字 middleX += bulletItem->width() + 4 * 18; } else { //显示到第三列 bulletItem = new BulletScreenItem(bottom); if(bulletInfo.userId == "1") bulletItem->setImage(QPixmap(":/images/homePage/touxiang.png")); bulletItem->setText(bulletInfo.text); int duration = 10000 / (double)width() * (bottomX + bulletItem->width());//让其在窗口中存在的时间大约为10s左右 bulletItem->initAnimation(bottomX + 2 * 18,duration); //每两个弹幕之间隔四个字 bottomX += bulletItem->width() + 4 * 18; } bulletItem->startAnimation(); }}2.4弹幕运动与暂停跟随视频的播放暂停
因为我们在初始化弹幕项的时候将他们分别挂到了不同轨道的对象树上,所以我们可以通过findChildren去找到他们并且控制他们的运动,暂停与释放:
xxxxxxxxxxvoid PlayerPage::pauseAllBullet(){ //暂停所有弹幕 QList<BulletScreenItem*> topBulletItems = top->findChildren<BulletScreenItem*>(); for(auto& item : topBulletItems) item->stopAnimation(); QList<BulletScreenItem*> middleBulletItems = middle->findChildren<BulletScreenItem*>(); for(auto& item : middleBulletItems) item->stopAnimation(); QList<BulletScreenItem*> bottomBulletItems = bottom->findChildren<BulletScreenItem*>(); for(auto& item : bottomBulletItems) item->stopAnimation();}
void PlayerPage::startAllBullet(){ //开始所有弹幕 QList<BulletScreenItem*> topBulletItems = top->findChildren<BulletScreenItem*>(); for(auto& item : topBulletItems) item->startAnimation(); QList<BulletScreenItem*> middleBulletItems = middle->findChildren<BulletScreenItem*>(); for(auto& item : middleBulletItems) item->startAnimation(); QList<BulletScreenItem*> bottomBulletItems = bottom->findChildren<BulletScreenItem*>(); for(auto& item : bottomBulletItems) item->startAnimation();}
void PlayerPage::delAllBullet(){ //释放当前的所有弹幕 QList<BulletScreenItem*> topBulletItems = top->findChildren<BulletScreenItem*>(); for(auto& item : topBulletItems) delete item; QList<BulletScreenItem*> middleBulletItems = middle->findChildren<BulletScreenItem*>(); for(auto& item : middleBulletItems) delete item; QList<BulletScreenItem*> bottomBulletItems = bottom->findChildren<BulletScreenItem*>(); for(auto& item : bottomBulletItems) delete item;}2.5弹幕关闭时不允许发射弹幕
xxxxxxxxxxvoid PlayerPage::onBulletLaunch(const QString &bullet){ if(isShowBullet) { //当开启弹幕时才允许发射弹幕 BulletScreenItem* bs = new BulletScreenItem(bottom); bs->setText(bullet); bs->setImage(QPixmap(":/images/homePage/touxiang.png")); int duration = 10000 / (double)width() * (width() + bs->width()); bs->initAnimation(width() + bs->width(),duration); bs->startAnimation(); bulletScreenLists[playTime].append(BulletScreenInfo("1",playTime,bullet)); }}总之,因为播放器功能实现这部分比较简单,所以我们就不详细每一处实现方法了。

评论(已关闭)
评论已关闭