基于微服务脚手架的视频点播系统 (仿B站) [客户端] -1
文章目录
- 项⽬简介
- 功能梳理
- 1. 启动页
- 2. 主界面
- 3. ⾸⻚
- 4. 播放⻚⾯
- 5. 登录⻚⾯
- 6. 我的⻚⾯&其他⽤⼾⻚⾯
- 7. 上传视频页面
- 8. 系统页面
- 9. 视频审核⻚⾯
- 10. ⻆⾊管理⻚⾯
- 界⾯布局
- 1. 启动页面
- 2. 主界⾯布局
- 3. 视频播放⻚布局
- 4. 我的⻚⾯布局
- 5. 系统管理⻚⾯
- 6. 系统管理⻚框架
- 7. 登录⻚⾯
- 8. Toast提⽰
- 日志系统编写
- 视频播放
- 1. Qt 的 QMediaPlayer
- 2. mpv和libmpv
- 2.1 API介绍
- 2.2 mpv库封装
- 弹幕效果
- 1. 弹幕区域布局 Barrage Area
- 2. 单条弹幕控件(BulletScreenItem)
- 3. 弹幕动画(从右向左滚动)
- 4. 弹幕数据结构(BulletScreenInfo)
- 5. 弹幕展示逻辑
- 6. 弹幕开关(开 / 关)
- 7. 发送弹幕(实时显示)
- 8. 弹幕随播放窗⼝移动
- 9. 弹幕系统整体流程图
项⽬简介
本项目是模仿B站视频播放平台客⼾端部分, 项目使用了⽤QT6框架实现多端兼容的友好桌⾯,集成libmpv多媒体内核完成视频播放与控制,借助HTTP协议完成与服务器的数据交互,支持实时弹幕,倍数,快进等功能。并可提供⼀个集视频上传的播放平台。
功能梳理
1. 启动页

2. 主界面

3. ⾸⻚

4. 播放⻚⾯

5. 登录⻚⾯

6. 我的⻚⾯&其他⽤⼾⻚⾯



7. 上传视频页面

8. 系统页面

9. 视频审核⻚⾯

10. ⻆⾊管理⻚⾯

界⾯布局
1. 启动页面

启动⻚⾮常简洁,界⾯内部只有⼀个QLabel显⽰logo。
2. 主界⾯布局
启动⻚停留2秒钟后进⼊主界⾯

3. 视频播放⻚布局

4. 我的⻚⾯布局

5. 系统管理⻚⾯
当点击主⻚中系统管理⻚⾯切换按钮时,应切换到系统管理⻚⾯。系统管理⻚⽀持两个⻚⾯:审核管
理和⻆⾊管理。审核管理⻚⾯中系统管理员对⽤⼾上传的视频进⾏审核、上架和下架处理,⻆⾊管理
⻚⾯主要是新增、编辑管理员信息,启⽤和禁⽌管理员账号等。


⻚⾯中仅仅红⾊框选的数据展⽰操作区不同外,其余基本是⼀样的,
6. 系统管理⻚框架
系统管理⻚框架⽐较简单,能让管理员选择审核视频或⻆⾊管理即可。

7. 登录⻚⾯
刚开始⽤⼾为临时⽤⼾,临时⽤⼾操作有限,只能观看视频等,如果要发送弹幕、视频点赞等操作,
必须先要登录进系统中,因此需要添加⼀个登录⻚⾯。⽀持两种登录⽅式:密码登录 和 短信登录

8. Toast提⽰
Toast 提⽰是⼀种⽤来进⾏关键信息提⽰的弹出式消息对话框,一般只显示几秒钟。不会会⼲扰⽤⼾的正常操作。通常⽤于通知⽤⼾某些操作结果. 因为该模块是给各个模块附用的,所以采取纯代码方式改造弹出式消息对话框.
///////////////////////////// toast.h ////////////////////////////////////////#include <QDialog>
#include <QString>
#include <QWidget>class toast : public QDialog
{Q_OBJECT
public:// 构造函数,parent 默认为 nullptrexplicit toast(const QString& text, QWidget *parent = nullptr);// 静态函数,直接显示消息static void showMessage(const QString& text);static void showMessage(const QString& text, QWidget* parent);private:void initUI(const QString& text);
};
///////////////////////////// toast.cpp ////////////////////////////////////////#include "toast.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QApplication>
#include <QScreen>
#include <QTimer>toast::toast(const QString& text, QWidget *pWidget): QDialog(pWidget)
{initUI(text);// 2 秒后自动关闭QTimer* timer = new QTimer(this);connect(timer, &QTimer::timeout, this, [=]() {timer->stop();this->close();this->deleteLater(); // 延迟删除对象// if (pWidget)// pWidget->show(); // 如果有父窗口,重新显示它});timer->start(2000);
}// 静态函数,直接弹出消息
void toast::showMessage(const QString &text)
{toast* t = new toast(text);t->show();
}void toast::showMessage(const QString &text, QWidget *pWidget)
{toast* t = new toast(text, pWidget);t->show();
}// 初始化 UI
void toast::initUI(const QString &text)
{// 1. 设置窗口属性this->setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); // 无边框小工具窗口this->setAttribute(Qt::WA_TranslucentBackground); // 背景透明setFixedSize(800, 60); // 固定大小// 2. 背景 QWidget + 圆角QWidget* background = new QWidget(this);background->setFixedSize(800, 60);background->setStyleSheet("background-color: rgba(102, 102, 102, 0.5);" // 半透明灰色"border-radius: 4px;");// 3. 布局管理QVBoxLayout* layout = new QVBoxLayout(background);layout->setSpacing(0);layout->setContentsMargins(0, 0, 0, 0);background->setLayout(layout);// 4. Label 显示文字QLabel* label = new QLabel();label->setText(text);label->setAlignment(Qt::AlignCenter);label->setStyleSheet("font-family: 微软雅黑;""font-size: 14px;""color: white;");label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);layout->addWidget(label);// 5. 窗口位置:水平居中、底部 100pxQScreen* screen = QApplication::primaryScreen();int screenWidth = screen->size().width();int screenHeight = screen->size().height();int x = (screenWidth - this->width()) / 2;int y = screenHeight - this->height() - 100;this->move(x, y);
}
效果图如下

日志系统编写
在运作时会产生一些日志,这些日志会记录下客户端运行过程中产生的一些事件。
本项目中的日志格式如下:
2025-11-07 14:23:51 [INFO] [UploadVideoPage.cpp:132@onCommitBtnClicked] 文件上传成功
日志格式分为 四个部分:
-
✅ 时间戳(Time)
yyyy-MM-dd hh:mm:ss 格式
-
✅ 日志等级(Log Level)
支持 5 种等级:
DEBUG
INFO
WARNING
ERROR
CRITICAL
当日志等级超过等于 WARNING 就会自动保存进去文件。 该文件按每天的日期进行命名. -
✅ 代码位置 ( File : Line @ Function)
由宏
__FILE__ / __LINE__ / __FUNCTION__自动采集。 -
✅ 日志正文(Message)
这是用户传递给日志系统的具体内容,如业务信息、错误描述等。
- 如何调用日志系统
只需要引入头文件:
如
写入普通信息日志(INFO)
LOG_INFO("文件上传成功");
写入错误日志(ERROR)
LOG_ERROR("视频上传失败:服务器无响应");
写入严重错误日志(CRITICAL)
LOG_CRITICAL("数据库文件损坏,无法继续运行");
视频播放
1. Qt 的 QMediaPlayer
QMediaPlayer 是Qt多媒体模块提供的音视频播放类。
如果使⽤QMediaPlayer播放视频时,需要导⼊Qt的多媒体模块:
CMake:
find_package(Qt6 REQUIRED COMPONENTS Multimedia)
target_link_libraries(mytarget PRIVATE Qt6::Multimedia)
qmake:
QT += multimedia
#include <QWidget>
#include <QMediaPlayer>
#include <QVideoWidget>
#include <QLineEdit>
class MyPlayerQt : public QWidget
{
Q_OBJECT
public:explicit MyPlayerQt(QWidget *parent = nullptr);
private slots:void onPlayBtnClicked();
private:QMediaPlayer *player; // Qt多媒体模块中的⼀个类,⽤于播放⾳频和视频QVideoWidget *videoWidget; // Qt多媒体模块中的⼀个类,⽤于渲染视频,与QMediaPlayer配合使⽤播放视频QLineEdit* videoPathEdit; // 输⼊视频路径
};
#include "myplayerqt.h"
#include <QDebug>
#include <QUrl>
#include <QVBoxLayout>
#include <QLineEdit>
#include <QPushButton>
#include <QDir>MyPlayerQt::MyPlayerQt(QWidget *parent)
: QWidget{parent}
{setFixedSize(1024, 860);// 创建视频渲染窗⼝对象,将视频渲染到当前窗⼝上videoWidget = new QVideoWidget(this);videoWidget->setFixedSize(1024, 800);videoWidget->move(0, 0);// 创建⽤⼾输⼊视频路径的编辑框videoPathEdit = new QLineEdit(this);videoPathEdit->setGeometry(5, 820, 800, 20);QDir currentDir = QDir::current();currentDir.cdUp();currentDir.cdUp();QString videoPath = currentDir.path();videoPathEdit->setText(videoPath + "/videos/111.mp4");// 创建播放按钮QPushButton* playBtn = new QPushButton(this);playBtn->setGeometry(810, 820, 60, 20);playBtn->setText("点击播放");// 创建媒体播放对象player = new QMediaPlayer(this);// 将视频数据输出到videoWidget中player->setVideoOutput(videoWidget);connect(playBtn, &QPushButton::clicked, this,&MyPlayerQt::onPlayBtnClicked);
}
void MyPlayerQt::onPlayBtnClicked()
{qDebug()<<videoPathEdit->text();// 设置视频源player->setSource(QUrl::fromLocalFile(videoPathEdit->text()));// 播放视频player->play();player->pause();
}
-
Qt⾃带的QMediaPlayer优点:
- 有完善的API文档说明和使用⽰例
- ⽀持基本的播放控制功能,如播放、暂停、⾳量控制等功能
- 提供了丰富的信号,⽤于监听播放状态和错误信息
- Qt 自带,无需额外编译第三方库
-
Qt⾃带的QMediaPlayer缺点:
- API升级带来兼容性问题。从Qt 5升级到Qt 6,QMediaPlayer的API发⽣了变化,⽐如setMedia()⽅法被移除,取⽽代之的是setSource()⽅法。
- 操作系统⾃带的解码器,有些视频可能⽆法解析,需要⾃⼰安装对应平台的视频解码器。
- 播放视频时,有可能会遇到较⾼的CPU占⽤率,或者出现延迟较⾼的问题
- 播放某些特定格式视频时可能会出现卡顿,尤其是播放⾼清视频。估计和底层的解码器性能有关。
- 某些更高级的视频功能受限,如鼠标显示当前画面帧功能
因此本项目使用 libmpv库 来实现视频播放功能。
2. mpv和libmpv
mpv是一个免费开源的媒体播放器。
具有以下特点:
- 跨平台:⽀持 Windows、Linux、macOS 和 Android 等操作系统
- 播放功能强⼤:⽀持多种⾳视频⽂件格式以及多种编码器
- 脚本控制:⽀持 Lua 脚本,⽤⼾可通过编写脚本来⾃定义播放器功能.如自动截图
- 配置灵活:⽀持通过配置(mpv.conf)和命令选项进⾏⾼度⾃定义,以及运⾏时调整等。
- 开发友好:通过libmpv提供强⼤的嵌⼊式功能,⽅便开发者将mpv集成到桌⾯应⽤、移动应⽤和嵌
⼊式设备中
mpv官网
mpv源码
mpv播放器可以在官网下载
mpvlib是⼀个⽤于嵌⼊mpv播放器功能的库,提供了C API,允许开发者将mpv的功能集成到⾃⼰的应
⽤程序中。
-
libmpv库进⾏⾳视频开发的流程如下:
mpv_create():创建mpv实例mpv_set_option():进⾏选项设置mpv_observe_property():进⾏事件订阅mpv_initialize():初始化mpv实例mpv_set_wakeup_callback():设置回调函数mpv_wait_event():循环等待订阅的事件触发,处理该事件mpv_terminate_destroy():结束播放,销毁mpv实例
-
【注意事项】
- 使⽤mpv库时需包含
"client.h"头⽂件 - 如果使⽤qmake编译套件,需要修改
.pro⽂件;如果使⽤cmake编译套件,需要修改CMakeLists.txt⽂件 - 运⾏程序时需要将
"libmpv-2.dll"库拷⻉到exe所在⽬录
- 使⽤mpv库时需包含
-
qmake
#...
# 设置mpv头⽂件路径
INCLUDEPATH += $$PWD/mpv/
# 设置mpv动态库库⽂件路径,注意运⾏程序时需要将该⽂件拷⻉到exe所在⽬录下
LIBS += $$PWD/mpv/libmpv-2.dll
- cmake
#...
set(MPV_DLL ${CMAKE_CURRENT_SOURCE_DIR}/mpv/libmpv-2.dll)
target_link_libraries(bitPlayer PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets Qt6::Network ${MPV_DLL})
2.1 API介绍
mpv官⽅并未对C API提供专⻔的官⽅⽂档详细说明,⽽是在"client.h"⽂件中说明,与mpv播放器⼤
部分交互都是通过选项/命令/属性完成的,这些都可以通过C API完成。
/*
* 功能:创建⼀个mpv实例
* 返回值:成功返回实例句柄,失败返回NULL
* 注意事项:在下⾯情况下可能会失败
* 1. 内存不⾜
* 2. LC_NUMERIC未被设置成"C"
*/
mpv_handle *mpv_create(void);/*
* 功能:销毁mpv实例并终⽌播放器,它会发送⼀个退出命令给播放器,
* 并等待播放器完全关闭后在销毁实例
* 参数:ctx要销毁的mpv实例(通过mpv_create()创建)
*/
void mpv_terminate_destroy(mpv_handle *ctx)/*
* 功能:在mpv初始化之前,设置mpv选项,这些选项通常在播放器启动时⽣效,并且在初始化后⽆法
修改
* 参数:
* ctx:mpv实例
* name:选项名称
* format:选项值格式
* data:选型值指针
* 返回值:0或正数表⽰成功,负数表⽰失败
* ⽐如:input-default-bindings选项表⽰是否启⽤默认输⼊绑定,1启⽤,0不启⽤
* input-vo-keyboard 是否在视频输出窗⼝中启⽤键盘输⼊,1启⽤,0不启⽤
*/
int mpv_set_option(mpv_handle *ctx, const char *name, mpv_format format, void *data);/*
* 功能:⽤于订阅mpv播放器的某个属性变化。当指定的属性变化时,mpv会发送
* MPV_EVENT_PROPERTY_CHANGE事件通知⽤⼾属性值已发⽣变化,⽤⼾根据需求进⾏特定处理
* 参数:
* ctx:mpv实例
* name:要订阅的属性名称
* reply_userdata: ⽤⼾定义的数据,会在属性变化事件中返回,可以⽤于标识特定的属性变
化事件
* format: 指定属性值的格式
* 返回值:0或正数表⽰成功,负数表⽰失败
*/
int mpv_observe_property(mpv_handle *mpv, uint64_t reply_userdata,const char *name, mpv_format format);// 事件数据结构
typedef struct mpv_event_property
{const char *name; // 属性名称mpv_format format; // 属性值的格式void *data; // 属性值的指针
} mpv_event_property;/*
* 功能:设置回调函数,该回调函数会在mpv有事件需要处理时被调⽤,以通知应⽤程序的
主事件循序处理mpv事件
* 参数:
* ctx:mpv实例
* cb: 回调函数指针,回调函数带⼀个void*类型参数
* d:回调函数参数
* 注意:Qt中通常使⽤信号槽机制将事件处理委托给主线程,从⽽确保线程安全和⾼效的事件处理
* 回调函数原型:static void wakeup(void *ctx)
*/
void mpv_set_wakeup_callback(mpv_handle *ctx, void (*cb)(void *d), void *d);/*
* 功能:等待下⼀个事件发⽣,或者直到超时时间到期,或者另⼀个线程调⽤了mpv_wakeup
* 参数:
* ctx:mpv实例
* time_out: 超时时间,单位为秒。如果指定事件内没有事件发⽣,函数将返回
MPV_EVENT_NONE
* 如果超时时间为0,则表⽰不会等待,适⽤于轮询
* 负值则表⽰⽆限等待
*/
mpv_event *mpv_wait_event(mpv_handle *ctx, double timeout)/*
* 功能:初始化mpv实例,该函数会设置播放器的内部状态,使其准备好接收命令和处理事件
* 参数:
* ctx:mpv实例
* 注意:在初始化前可以设置mpv选项或订阅属性,在初始化后才能够加载和播放视频
*/
int mpv_initialize(mpv_handle *ctx);/*
* 功能:异步执⾏mpv命令,该函数会⽴即返回,不会等待命令执⾏完成,命令的执⾏结束
* 会触发MPV_EVENT_COMMAND_REPLY事件
* 参数:
* ctx:mpv实例
* reply_userdata:⽤⼾⾃定义数据,会在命令执⾏完成时通过事件返回,可标识特殊异步请
求
* args:⼀个以NULL结果的字符串数组,包含命令及命令参数
* 返回值:0或正数表⽰成功,负数表⽰失败,事件的error字段会包含错误码
*/
int mpv_command_async(mpv_handle *ctx, uint64_t reply_userdata, const char
**args);/** 功能:异步设置mpv播放器的属性,该函数不等待属性设置完成⽽是直接返回,设置结果通过* MPV_EVENT_SET_PROPERTY_REPLY事件返回* * 参数:* ctx:mpv实例* reply_userdata:⽤⼾⾃定义数据,属性设置完后随事件返回,⽤于标识特定异步请求* name:属性名称* foramt: 属性值格式* data:属性值指针* 返回值:0或正数表⽰成功,负数表⽰失败,事件的error字段包含错误码* 常⻅属性:* time-pos:视频当前播放位置* pause:播放器的暂停状态,可设置为暂停或播放,0表⽰播放,1表⽰暂停* mute:播放器的静⾳状态,0表⽰取消静⾳,1表⽰静⾳* volume:播放器⾳量* loop:控制播放⽂件的循环⾏为no:不循环播放inf:⽆限循环file: 当前播放⽂件循环playlist:列表循环* speed:倍速播放 1.0:正常速度 2.0:两倍速度 0.5半倍速度 0.0:暂停播放
*/
int mpv_set_property_async(mpv_handle *ctx, uint64_t reply_userdata,const char *name, mpv_format format, void *data);// 该结构体作⽤:在事件中传递属性变化的相关信息
// ⽐如当MPV_EVENT_PROPERTY_CHANGE事件触发时,该结构体会被填充并传递给事件处理函数
typedef struct mpv_event_property
{const char *name; // 属性名称mpv_format format; // 属性值的格式void *data; // 属性值指针
} mpv_event_property;// 指定属性值或参数的数值格式,明确说明API需要处理的数据类型
typedef enum mpv_format
{MPV_FORMAT_NONE = 0, // ⽆效或空值MPV_FORMAT_STRING = 1, // 字符串 (char*),不会进⾏任何格式化,"123.45"秒MPV_FORMAT_OSD_STRING = 2, // 屏幕显⽰的格式化后的字符串,⽐如:"00:02:03"MPV_FORMAT_FLAG = 3, // 布尔值 (int)MPV_FORMAT_INT64 = 4, // 64位整数 (int64_t)MPV_FORMAT_DOUBLE = 5, // 双精度浮点数 (double)MPV_FORMAT_NODE = 6, // 复杂数据结构 (mpv_node)MPV_FORMAT_NODE_ARRAY = 7, // 节点数组 (mpv_node_list)MPV_FORMAT_NODE_MAP = 8, // 节点映射 (mpv_node_list)MPV_FORMAT_BYTE_ARRAY = 9 // 字节数组 (mpv_byte_array)
} mpv_format;
2.2 mpv库封装
对mpv库进⾏简单封装,⽅便后序使⽤mpv播放视频。
/////////////////////////////// mpvplayer.h ///////////////////////////////
#include <QObject>
#include "mpv-dev-x86_64-20250924-git-be98b35/include/mpv/client.h"class MpvPlayer : public QObject
{Q_OBJECT
public:explicit MpvPlayer(QObject *parent = nullptr,QWidget* videoRenderWnd = nullptr);~MpvPlayer();// 加载视频void startPlay(const QString& videoPath);// 播放和暂停void play();void pause();//倍速播放设置void setSpeed(double speed);//获取当前播放速度double getSpeed();//设置静音void setMuted(bool muted);//音量调节void setRatio(int64_t Ratio);//调节视频播放位置void setCurrentPlayPosition(double seconds);// 使⽤ffmpeg⼯具获取视频⾸帧static QString getVideoFirstFrame(const QString& videoPath);//获取当前播放时间int64_t getPlayTime();private:// 处理mpv的事件void handleMpvEvent(mpv_event* event);signals:// 当订阅的事件发⽣时,触发该信号,利⽤Qt的信号槽机制处理void mpvEvents();void playPositionChanged(int64_t);void sendendVideoPlay();// 视频总时⻓发⽣改变时void durationChanged(int64_t duration);private slots:void onMpvEvents();private:mpv_handle* mpv;double currentSpeed; //当前播放速度int64_t currentPlayTime; //当前播放时间};
#include "mpvplayer.h"
#include <QWidget>
#include "Logger.h"
#include <QProcess>
#include <QDir>static void wakeup(void* ctx)
{MpvPlayer* mvpPlayer = static_cast<MpvPlayer*>(ctx);emit mvpPlayer->mpvEvents();
}MpvPlayer::MpvPlayer(QObject *parent,QWidget* videoRenderWnd): QObject{parent}
{//libmpv需要将LC_NUMBER类别设置为默认的C语言环境,即使用标准的ASCII编码和格式化规则//区域设置决定了程序在运⾏时如何处理各种本地化相关操作,⽐如:⽇期格式、数字格式、货币符号等std::setlocale(LC_NUMERIC,"C");// 创建mpv实例mpv = mpv_create();if(mpv == nullptr){LOG_ERROR("创造mpv实例失败");throw std::runtime_error("can't create mpv instance!!!");}// 设置视频渲染窗⼝--将窗⼝的id告知给mpv// 如果设置了视频渲染窗⼝,就告知mpv,否则就不渲染视频画⾯和声⾳输出if(videoRenderWnd){//设置视频渲染窗⼝int64_t wid = videoRenderWnd->winId();mpv_set_option(mpv,"wid",MPV_FORMAT_INT64,&wid);}else{// 此处不需要视频播放,让视频在后台加载成功即可// vo 表⽰视频输出 ao表⽰⾳频输出// vo null:表⽰禁⽌视频输出,视频不会被渲染到任何设备上// ao null:表⽰禁⽌⾳频输出,⾳频不会被播放到任何设备上mpv_set_option_string(mpv,"vo","null");mpv_set_option_string(mpv,"ao","null");}// 设置mpv内部事件触发时的回到函数wakeup// 通过应⽤程序的主事件循环处理mpv事件// 注册的回调函数 typedef void (*mpv_wakeup_callback)(void *d);//初始化mpv实例mpv_set_wakeup_callback(mpv,wakeup,this);if(mpv_initialize(mpv) < 0){LOG_ERROR("初始化mpv失败");mpv_destroy(mpv);throw std::runtime_error("init mpv instance failed!!!");}//订阅时间mpv_observe_property(mpv,0,"time-pos",MPV_FORMAT_INT64);// 订阅 duration 属性变化mpv_observe_property(mpv,0,"duration",MPV_FORMAT_DOUBLE);connect(this,&MpvPlayer::mpvEvents,this,&MpvPlayer::onMpvEvents);
}MpvPlayer::~MpvPlayer()
{// 释放mpv实例if (mpv){mpv_terminate_destroy(mpv);mpv = nullptr;}}void MpvPlayer::onMpvEvents()
{// 处理所有事件,直到事件队列为空while(mpv){//0:立即返回(非阻塞模式/-1:无限期等待,直到有事件发生/正数:最多等待指定秒数,超时后返回MPV_EVENT_NONEmpv_event* event = mpv_wait_event(mpv,0);if(event->event_id == MPV_EVENT_NONE)break;handleMpvEvent(event);}
}void MpvPlayer::handleMpvEvent(mpv_event *event)
{switch(event->event_id){case MPV_EVENT_PROPERTY_CHANGE:{mpv_event_property* eventProperty = (mpv_event_property*)event->data;if(eventProperty->data == nullptr)break;if(strcmp(eventProperty->name,"time-pos") == 0){// 播放进度发生改变,发出信号,让界面更新进度条和时间//当前分片的全局起始时间double segmentStartTime = 0;mpv_get_property(mpv,"demuxer-start-time",MPV_FORMAT_DOUBLE,&segmentStartTime);//获取当前分片内的播放时间double segmentCurrentTime = 0;mpv_get_property(mpv,"time-pos",MPV_FORMAT_DOUBLE,&segmentCurrentTime);// 全局播放时间 = 分片起始时间 + 分片内播放时间// -1 是为了一开始进度条对齐到0的位置currentPlayTime = static_cast<int64_t>(segmentStartTime + segmentCurrentTime) - 1;emit playPositionChanged(currentPlayTime);}else if(strcmp(eventProperty->name,"duration") == 0 && eventProperty->format == MPV_FORMAT_DOUBLE){int64_t duration = (int64_t)*(double*)eventProperty->data;emit durationChanged(duration);}break;}case MPV_EVENT_SHUTDOWN: //关闭{mpv_terminate_destroy(mpv);mpv = nullptr;break;}case MPV_EVENT_END_FILE: //音量播放完毕{mpv_event_end_file* endFile = (mpv_event_end_file*)event->data;if(endFile->reason == MPV_END_FILE_REASON_EOF){//检测是否最后一个视频分片播放结束int64_t playlistCount = -1;int64_t playlistPos = -1;mpv_get_property(mpv,"playlist-count",MPV_FORMAT_INT64,&playlistCount);mpv_get_property(mpv,"playlist-pos", MPV_FORMAT_INT64,&playlistPos);if(playlistCount > 0 && playlistPos == playlistCount-1){LOG_INFO("整个视频播放结束");emit sendendVideoPlay();}else{LOG_INFO("单个分片播放结束");}}}default:{break;}}}// mpv_command_async
// ↓
// mpv 内部处理命令
// ↓
// mpv 生成事件
// ↓
// wakeup 回调被触发
// ↓
// 主线程/事件循环里处理事件
void MpvPlayer::startPlay(const QString &videoPath)
{// 与mpv_command相同,但异步运⾏命令来避免阻塞,直到进程终⽌const QByteArray c_filename = videoPath.toUtf8();//loadfile 加载并播放一个媒体文件const char* args[] = {"loadfile", c_filename.data(),NULL};mpv_command_async(mpv,0,args);
}void MpvPlayer::play()
{int pause = 0;mpv_set_property_async(mpv,0,"pause",MPV_FORMAT_FLAG,&pause);
}void MpvPlayer::pause()
{int pause = 1;mpv_set_property_async(mpv, 0,"pause",MPV_FORMAT_FLAG, &pause);
}void MpvPlayer::setSpeed(double speed)
{currentSpeed = speed;mpv_set_property_async(mpv,0,"speed",MPV_FORMAT_DOUBLE,&speed);
}double MpvPlayer::getSpeed()
{return currentSpeed;
}void MpvPlayer::setMuted(bool muted)
{int flag = muted? 0 : 1;mpv_set_property_async(mpv, 0, "mute", MPV_FORMAT_FLAG, &flag);
}void MpvPlayer::setRatio(int64_t Ratio)
{//mpv 音量太低导致听起来像静音 所以这里+10Ratio+=10;mpv_set_property_async(mpv,0,"volume",MPV_FORMAT_INT64,&Ratio);
}void MpvPlayer::setCurrentPlayPosition(double seconds)
{mpv_set_property_async(mpv,0,"time-pos",MPV_FORMAT_DOUBLE,&seconds);
}QString MpvPlayer::getVideoFirstFrame(const QString &videoPath)
{// 使⽤ffmpeg⼯具获取视频⾸帧// 获取ffmpeg⼯具的路径QString ffmpegPath = QDir::currentPath() + "/ffmpeg/ffmpeg.exe";if (!QFile::exists(ffmpegPath)){LOG_ERROR("ffmpeg 路径不存在: " + ffmpegPath);return "";}// 获取保存提取的⾸帧图⽚路径QString fristFrame = QDir::currentPath() + "/firstFrame.png";// 设置命令⾏参数QStringList cmd;cmd << "-y" // ✅ 自动覆盖<< "-ss" << "00:00:00" \<< "-i" << videoPath \<< "-vframes" << "1" \<< fristFrame;//启动一个进程 管理ffmpeg⼯具QProcess ffmpegProgress;ffmpegProgress.start(ffmpegPath,cmd);// 等待5秒 防止死等if(!ffmpegProgress.waitForFinished(5000)){LOG_INFO("ffmpeg 进程执⾏失败");return "";}//如果 ffmpeg 进程执行失败 或者 输出文件不存在,就认为提取首帧失败if (ffmpegProgress.exitCode() != 0 || !QFile::exists(fristFrame)){// 打印 ffmpeg 的错误输出,方便调试LOG_ERROR("ffmpeg 提取首帧失败: " + ffmpegProgress.readAllStandardError());return "";}return fristFrame;
}int64_t MpvPlayer::getPlayTime()
{return currentPlayTime;
}
弹幕效果
本项目实现了一个支持 三行布局、滚动动画、与播放时间绑定 的完整弹幕系统。
1. 弹幕区域布局 Barrage Area
播放器界面创建一个 透明、无边框的 QDialog 作为弹幕承载层。
该层包含三条 QFrame:
top (第 1 行弹幕)
middle (第 2 行弹幕)
bottom (第 3 行弹幕)
2. 单条弹幕控件(BulletScreenItem)
每条弹幕都是一个 QFrame,内部结构:
[头像 QLabel] + [文本 QLabel]
默认不显示头像(普通用户弹幕)
当前用户发送的弹幕显示头像 + 蓝色边框
3. 弹幕动画(从右向左滚动)
基于 QPropertyAnimation 实现:
void BulletScreenItem::setBulletScreenAnimal(int x, int duration)
{animal = new QPropertyAnimation(this,"pos"); //弹幕控件自身animal->setDuration(duration); //根据文字长度计算速度animal->setStartValue(QPoint(x,0)); //一开始在窗口右边animal->setEndValue(QPoint(0 - this->width(),0)); //完全超出屏幕左边 比如窗口宽度 800,那结束位置就是 (-800, 0connect(animal,&QPropertyAnimation::finished,this,[=](){animal->stop();this->deleteLater();});
}
4. 弹幕数据结构(BulletScreenInfo)
- 弹幕数据组织
class BulletScreenInfo
{
public:QString userId; // 发送弹幕⽤⼾QString videoid; // 弹幕对应的视频Iint64_t playTime; // 发送弹幕时当前播放时间QString text; // 弹幕内容explicit BulletScreenInfo(const QString& userId = "", int64_t playTime = 0,const QString& text = "");
};
播放器将所有弹幕存入:
QMap<int64_t, QList<BulletScreenInfo>>
按 秒数分组,这样当播放到某一秒时:
bulletScreenLists[playTime] → 获取当秒的所有弹幕
根据播放位置精确命中弹幕数据
5. 弹幕展示逻辑
播放器每当播放时间变化(positionChanged)时执行:
showBulletScreen()
流程:
- 如果关闭弹幕 → return
- 读取当前秒数所有弹幕列表
- 根据条数分配到三行:
| 条目编号 i | 行 | 特殊规则 |
|---|---|---|
| i % 3 == 0 | top | 正常显示 |
| i % 3 == 1 | middle | 正常显示 |
| i % 3 == 2 | bottom | 向右缩进 2 字 |
- 同行多条弹幕间隔固定 4 个字宽
- 创建控件 + 设置文本 + 设置动画 + 开启动画
弹幕与视频播放进度保持同步
6. 弹幕开关(开 / 关)
-
弹幕开关(开 / 关)
用户点击弹幕开关按钮:isStartBS = !isStartBS;开启 → 显示弹幕层(barrageArea->show())
关闭 → 隐藏弹幕层(barrageArea->hide())
7. 发送弹幕(实时显示)
用户可以实时发送弹幕而不影响播放
用户输入框 BarrageEdit:
-
点击发送按钮 → signal sendBulletScreen(text)
-
PlayerPage 接收 → 根据文本立即创建一条新弹幕
逻辑:
-
判断是否开启弹幕
-
构造新的 BulletScreenItem
-
强制显示头像(当前用户弹幕)
-
立即启动动画
8. 弹幕随播放窗⼝移动
当用户拖动播放器窗口时,需要让弹幕层一起移动:
在 PlayerPage::mouseMoveEvent
QPoint point = geometry().topLeft(); //播放器窗口左上角在屏幕上的位置
point.setY(point.y() + 60); // playHead 高度
barrageArea->move(point);
当播放器窗口被拖动时,让弹幕窗口紧贴在播放器窗口的播放区下面。
- point.setY(point.y() + 60) 为什么要加60
┌────────────────────────────┐
│ 播放头区域 playHead │ ← 高度大约 60 px
├────────────────────────────┤
│ ⭐⭐⭐ 这里开始是视频画面 │
│ 弹幕应该在这里 │
│ │
└────────────────────────────┘
播放器窗口左上角是整个窗口顶部弹幕应该显示在 playHead(顶部控制栏)下面playHead 高度大概是 60 像素
播放器窗口的左上角不是视频画面的左上角。
+60 是把弹幕窗口移动到「视频区域顶部」。
-
barrageArea->move(point)
不管播放器窗口怎么拖动弹幕总是移动到视频画面对应的位置,保证弹幕永远贴在视频上,而不是留在原地
┌───────────────────────────┐
│ 播放器窗口 │ ← geometry().topLeft()
│ ┌───────────────────────┐ │
│ │ playHead(60px) │ │
│ ├───────────────────────┤ │
│ │ 视频画面 │ │
│ │ ┌───────────────────┐ │
│ │ │ 弹幕窗口(这里) │ │ ←
│ │ └───────────────────┘ │
└─┴────────────────────────┘
找到播放器窗口左上角 → geometry().topLeft()
向下移动 60 → playHead 高度
9. 弹幕系统整体流程图
┌───────────────────────┐
│ 原始弹幕数据 BulletScreenInfo │
└───────────────┬───────┘│ 按秒数分组▼QMap<int64_t, QList<BulletScreenInfo>>│▼播放进度 positionChanged│ playTime = 当前秒数▼showBulletScreen()│┌────────┴─────────┐▼ ▼分配到三行 top/mid/bottom 计算起点与速度│ │▼ ▼创建 BulletScreenItem QPropertyAnimation 滚动│ │└──────────→ startAnimal() ─→ 自动删除