基于微服务脚手架的视频点播系统 (仿B站) [客户端] -1

文章目录

  • 项⽬简介
  • 功能梳理
    • [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] 文件上传成功

日志格式分为 四个部分:

  1. ✅ 时间戳(Time)

    yyyy-MM-dd hh:mm:ss 格式

  2. ✅ 日志等级(Log Level)

    支持 5 种等级:
    DEBUG
    INFO
    WARNING
    ERROR
    CRITICAL

    当日志等级超过等于 WARNING 就会自动保存进去文件。 该文件按每天的日期进行命名.

  3. ✅ 代码位置 ( File : Line @ Function)

    由宏 __FILE__ / __LINE__ / __FUNCTION__ 自动采集。

  4. ✅ 日志正文(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优点:

    1. 有完善的API文档说明和使用⽰例
    2. ⽀持基本的播放控制功能,如播放、暂停、⾳量控制等功能
    3. 提供了丰富的信号,⽤于监听播放状态和错误信息
    4. Qt 自带,无需额外编译第三方库
  • Qt⾃带的QMediaPlayer缺点:

    1. API升级带来兼容性问题。从Qt 5升级到Qt 6,QMediaPlayer的API发⽣了变化,⽐如setMedia()⽅法被移除,取⽽代之的是setSource()⽅法。
    2. 操作系统⾃带的解码器,有些视频可能⽆法解析,需要⾃⼰安装对应平台的视频解码器。
    3. 播放视频时,有可能会遇到较⾼的CPU占⽤率,或者出现延迟较⾼的问题
    4. 播放某些特定格式视频时可能会出现卡顿,尤其是播放⾼清视频。估计和底层的解码器性能有关。
    5. 某些更高级的视频功能受限,如鼠标显示当前画面帧功能

因此本项目使用 libmpv库 来实现视频播放功能。

2. mpv和libmpv

mpv是一个免费开源的媒体播放器。

具有以下特点:

  • 跨平台:⽀持 Windows、Linux、macOS 和 Android 等操作系统
  • 播放功能强⼤:⽀持多种⾳视频⽂件格式以及多种编码器
  • 脚本控制:⽀持 Lua 脚本,⽤⼾可通过编写脚本来⾃定义播放器功能.如自动截图
  • 配置灵活:⽀持通过配置(mpv.conf)和命令选项进⾏⾼度⾃定义,以及运⾏时调整等。
  • 开发友好:通过libmpv提供强⼤的嵌⼊式功能,⽅便开发者将mpv集成到桌⾯应⽤、移动应⽤和嵌
    ⼊式设备中

mpv官网
mpv源码

mpv播放器可以在官网下载

mpvlib是⼀个⽤于嵌⼊mpv播放器功能的库,提供了C API,允许开发者将mpv的功能集成到⾃⼰的应

⽤程序中。

  • libmpv库进⾏⾳视频开发的流程如下:

    1. mpv_create():创建mpv实例
    2. mpv_set_option():进⾏选项设置
    3. mpv_observe_property():进⾏事件订阅
    4. mpv_initialize():初始化mpv实例
    5. mpv_set_wakeup_callback():设置回调函数
    6. mpv_wait_event():循环等待订阅的事件触发,处理该事件
    7. mpv_terminate_destroy():结束播放,销毁mpv实例
  • 【注意事项】

    1. 使⽤mpv库时需包含"client.h"头⽂件
    2. 如果使⽤qmake编译套件,需要修改.pro⽂件;如果使⽤cmake编译套件,需要修改CMakeLists.txt⽂件
    3. 运⾏程序时需要将 "libmpv-2.dll"库拷⻉到exe所在⽬录
  • 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()

流程:

  1. 如果关闭弹幕 → return
  2. 读取当前秒数所有弹幕列表
  3. 根据条数分配到三行:
条目编号 i 特殊规则
i % 3 == 0 top 正常显示
i % 3 == 1 middle 正常显示
i % 3 == 2 bottom 向右缩进 2 字
  1. 同行多条弹幕间隔固定 4 个字宽
  2. 创建控件 + 设置文本 + 设置动画 + 开启动画

弹幕与视频播放进度保持同步

6. 弹幕开关(开 / 关)

  • 弹幕开关(开 / 关)

    用户点击弹幕开关按钮:

    cpp 复制代码
    isStartBS = !isStartBS;

    开启 → 显示弹幕层(barrageArea->show())

    关闭 → 隐藏弹幕层(barrageArea->hide())


7. 发送弹幕(实时显示)

用户可以实时发送弹幕而不影响播放

用户输入框 BarrageEdit:

  • 点击发送按钮 → signal sendBulletScreen(text)

  • PlayerPage 接收 → 根据文本立即创建一条新弹幕

逻辑:

  1. 判断是否开启弹幕

  2. 构造新的 BulletScreenItem

  3. 强制显示头像(当前用户弹幕)

  4. 立即启动动画


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() ─→ 自动删除
相关推荐
悠闲蜗牛�2 小时前
智能时代技术融合之道:大模型、微服务与数据安全的系统化实践
微服务·云原生·架构
胡耀超3 小时前
通往AGI的模块化路径:一个可能的技术架构(同时解答微调与RAG之争)
人工智能·python·ai·架构·大模型·微调·agi
落羽的落羽3 小时前
【C++】现代C++的新特性constexpr,及其在C++14、C++17、C++20中的进化
linux·c++·人工智能·学习·机器学习·c++20·c++40周年
CAU界编程小白3 小时前
数据结构系列之十大排序算法
数据结构·c++·算法·排序算法
头发还没掉光光4 小时前
Linux网络初始及网络通信基本原理
linux·运维·开发语言·网络·c++
m0_748248024 小时前
揭开 C++ vector 底层面纱:从三指针模型到手写完整实现
开发语言·c++·算法
海盗猫鸥4 小时前
「C++」string类(2)常用接口
开发语言·c++
序属秋秋秋4 小时前
《Linux系统编程之开发工具》【实战:倒计时 + 进度条】
linux·运维·服务器·c语言·c++·ubuntu·系统编程
yugi9878384 小时前
基于Qt框架开发多功能视频播放器
开发语言·qt