文章目录
- 项⽬简介
- 功能梳理
-
- [1. 启动页](#1. 启动页)
- [2. 主界面](#2. 主界面)
- [3. ⾸⻚](#3. ⾸⻚)
- [4. 播放⻚⾯](#4. 播放⻚⾯)
- [5. 登录⻚⾯](#5. 登录⻚⾯)
- [6. 我的⻚⾯&其他⽤⼾⻚⾯](#6. 我的⻚⾯&其他⽤⼾⻚⾯)
- [7. 上传视频页面](#7. 上传视频页面)
- [8. 系统页面](#8. 系统页面)
- [9. 视频审核⻚⾯](#9. 视频审核⻚⾯)
- [10. ⻆⾊管理⻚⾯](#10. ⻆⾊管理⻚⾯)
- 界⾯布局
-
- [1. 启动页面](#1. 启动页面)
- [2. 主界⾯布局](#2. 主界⾯布局)
- [3. 视频播放⻚布局](#3. 视频播放⻚布局)
- [4. 我的⻚⾯布局](#4. 我的⻚⾯布局)
- [5. 系统管理⻚⾯](#5. 系统管理⻚⾯)
- [6. 系统管理⻚框架](#6. 系统管理⻚框架)
- [7. 登录⻚⾯](#7. 登录⻚⾯)
- [8. Toast提⽰](#8. Toast提⽰)
- 日志系统编写
- 视频播放
-
- [1. Qt 的 QMediaPlayer](#1. Qt 的 QMediaPlayer)
- [2. mpv和libmpv](#2. mpv和libmpv)
-
- [2.1 API介绍](#2.1 API介绍)
- [2.2 mpv库封装](#2.2 mpv库封装)
- 弹幕效果
-
- [1. 弹幕区域布局 Barrage Area](#1. 弹幕区域布局 Barrage Area)
- [2. 单条弹幕控件(BulletScreenItem)](#2. 单条弹幕控件(BulletScreenItem))
- [3. 弹幕动画(从右向左滚动)](#3. 弹幕动画(从右向左滚动))
- [4. 弹幕数据结构(BulletScreenInfo)](#4. 弹幕数据结构(BulletScreenInfo))
- [5. 弹幕展示逻辑](#5. 弹幕展示逻辑)
- [6. 弹幕开关(开 / 关)](#6. 弹幕开关(开 / 关))
- [7. 发送弹幕(实时显示)](#7. 发送弹幕(实时显示))
- [8. 弹幕随播放窗⼝移动](#8. 弹幕随播放窗⼝移动)
- [9. 弹幕系统整体流程图](#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 提⽰是⼀种⽤来进⾏关键信息提⽰的弹出式消息对话框,一般只显示几秒钟。不会会⼲扰⽤⼾的正常操作。通常⽤于通知⽤⼾某些操作结果. 因为该模块是给各个模块附用的,所以采取纯代码方式改造弹出式消息对话框.
cpp
///////////////////////////// toast.h ////////////////////////////////////////
#include <QDialog>
#include <QString>
#include <QWidget>
class toast : public QDialog
{
Q_OBJECT
public:
// 构造函数,parent 默认为 nullptr
explicit 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);
};
cpp
///////////////////////////// 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. 窗口位置:水平居中、底部 100px
QScreen* 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);
}
效果图如下

日志系统编写
在运作时会产生一些日志,这些日志会记录下客户端运行过程中产生的一些事件。
本项目中的日志格式如下:
cpp
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)
cpp
LOG_INFO("文件上传成功");
写入错误日志(ERROR)
cpp
LOG_ERROR("视频上传失败:服务器无响应");
写入严重错误日志(CRITICAL)
cpp
LOG_CRITICAL("数据库文件损坏,无法继续运行");
视频播放
1. Qt 的 QMediaPlayer
QMediaPlayer 是Qt多媒体模块提供的音视频播放类。
如果使⽤QMediaPlayer播放视频时,需要导⼊Qt的多媒体模块:
cpp
CMake:
find_package(Qt6 REQUIRED COMPONENTS Multimedia)
target_link_libraries(mytarget PRIVATE Qt6::Multimedia)
qmake:
QT += multimedia
cpp
#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; // 输⼊视频路径
};
cpp
#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播放器可以在官网下载
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
bash
#...
# 设置mpv头⽂件路径
INCLUDEPATH += $$PWD/mpv/
# 设置mpv动态库库⽂件路径,注意运⾏程序时需要将该⽂件拷⻉到exe所在⽬录下
LIBS += $$PWD/mpv/libmpv-2.dll
- cmake
bash
#...
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完成。
cpp
/*
* 功能:创建⼀个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播放视频。
cpp
/////////////////////////////// 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; //当前播放时间
};
cpp
#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_NONE
mpv_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 音量太低导致听起来像静音 所以这里+10
Ratio+=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:
cpp
top (第 1 行弹幕)
middle (第 2 行弹幕)
bottom (第 3 行弹幕)
2. 单条弹幕控件(BulletScreenItem)
每条弹幕都是一个 QFrame,内部结构:
bash
[头像 QLabel] + [文本 QLabel]
默认不显示头像(普通用户弹幕)
当前用户发送的弹幕显示头像 + 蓝色边框
3. 弹幕动画(从右向左滚动)
基于 QPropertyAnimation 实现:
cpp
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, 0
connect(animal,&QPropertyAnimation::finished,this,[=]()
{
animal->stop();
this->deleteLater();
});
}
4. 弹幕数据结构(BulletScreenInfo)
- 弹幕数据组织
cpp
class BulletScreenInfo
{
public:
QString userId; // 发送弹幕⽤⼾
QString videoid; // 弹幕对应的视频I
int64_t playTime; // 发送弹幕时当前播放时间
QString text; // 弹幕内容
explicit BulletScreenInfo(const QString& userId = "", int64_t playTime = 0,const QString& text = "");
};
播放器将所有弹幕存入:
cpp
QMap<int64_t, QList<BulletScreenInfo>>
按 秒数分组,这样当播放到某一秒时:
cpp
bulletScreenLists[playTime] → 获取当秒的所有弹幕
根据播放位置精确命中弹幕数据
5. 弹幕展示逻辑
播放器每当播放时间变化(positionChanged)时执行:
cpp
showBulletScreen()
流程:
- 如果关闭弹幕 → return
- 读取当前秒数所有弹幕列表
- 根据条数分配到三行:
| 条目编号 i | 行 | 特殊规则 |
|---|---|---|
| i % 3 == 0 | top | 正常显示 |
| i % 3 == 1 | middle | 正常显示 |
| i % 3 == 2 | bottom | 向右缩进 2 字 |
- 同行多条弹幕间隔固定 4 个字宽
- 创建控件 + 设置文本 + 设置动画 + 开启动画
弹幕与视频播放进度保持同步
6. 弹幕开关(开 / 关)
-
弹幕开关(开 / 关)
用户点击弹幕开关按钮:
cppisStartBS = !isStartBS;开启 → 显示弹幕层(barrageArea->show())
关闭 → 隐藏弹幕层(barrageArea->hide())
7. 发送弹幕(实时显示)
用户可以实时发送弹幕而不影响播放
用户输入框 BarrageEdit:
-
点击发送按钮 → signal sendBulletScreen(text)
-
PlayerPage 接收 → 根据文本立即创建一条新弹幕
逻辑:
-
判断是否开启弹幕
-
构造新的 BulletScreenItem
-
强制显示头像(当前用户弹幕)
-
立即启动动画
8. 弹幕随播放窗⼝移动
当用户拖动播放器窗口时,需要让弹幕层一起移动:
在 PlayerPage::mouseMoveEvent
cpp
QPoint point = geometry().topLeft(); //播放器窗口左上角在屏幕上的位置
point.setY(point.y() + 60); // playHead 高度
barrageArea->move(point);
当播放器窗口被拖动时,让弹幕窗口紧贴在播放器窗口的播放区下面。
- point.setY(point.y() + 60) 为什么要加60
bash
┌────────────────────────────┐
│ 播放头区域 playHead │ ← 高度大约 60 px
├────────────────────────────┤
│ ⭐⭐⭐ 这里开始是视频画面 │
│ 弹幕应该在这里 │
│ │
└────────────────────────────┘
播放器窗口左上角是整个窗口顶部
弹幕应该显示在 playHead(顶部控制栏)下面
playHead 高度大概是 60 像素
播放器窗口的左上角不是视频画面的左上角。
+60 是把弹幕窗口移动到「视频区域顶部」。
-
barrageArea->move(point)
不管播放器窗口怎么拖动 弹幕总是移动到视频画面对应的位置,保证弹幕永远贴在视频上,而不是留在原地
bash
┌───────────────────────────┐
│ 播放器窗口 │ ← geometry().topLeft()
│ ┌───────────────────────┐ │
│ │ playHead(60px) │ │
│ ├───────────────────────┤ │
│ │ 视频画面 │ │
│ │ ┌───────────────────┐ │
│ │ │ 弹幕窗口(这里) │ │ ←
│ │ └───────────────────┘ │
└─┴────────────────────────┘
找到播放器窗口左上角 → geometry().topLeft()
向下移动 60 → playHead 高度
9. 弹幕系统整体流程图
bash
┌───────────────────────┐
│ 原始弹幕数据 BulletScreenInfo │
└───────────────┬───────┘
│ 按秒数分组
▼
QMap<int64_t, QList<BulletScreenInfo>>
│
▼
播放进度 positionChanged
│ playTime = 当前秒数
▼
showBulletScreen()
│
┌────────┴─────────┐
▼ ▼
分配到三行 top/mid/bottom 计算起点与速度
│ │
▼ ▼
创建 BulletScreenItem QPropertyAnimation 滚动
│ │
└──────────→ startAnimal() ─→ 自动删除