├── res ├── c.jpeg ├── red.png ├── al2l.png ├── close.png ├── icon.png ├── image.png ├── loading.gif ├── 杨任东竹石体-Bold.ttf ├── fa-solid-900.ttf ├── 杨任东竹石体-Medium.ttf ├── fa-v4compatibility.ttf ├── qss │ ├── show.css │ ├── title.css │ ├── playlist.css │ ├── ctrlbar.css │ └── mainwid.css ├── menu.json └── gear-solid.svg ├── swscale-5.dll ├── 更新日志1月5日.txt ├── main.cpp ├── CustomSlider.h ├── QTD.vcxproj.user ├── mainwid.qrc ├── medialist.h ├── QTD.ui ├── CustomSlider.cpp ├── show.ui ├── QTD.sln ├── playlist.h ├── medialist.cpp ├── title.h ├── ctrlbar.h ├── globalhelper.h ├── show.h ├── playlist.ui ├── mainwid.h ├── mainwid.ui ├── README.md ├── QTD.vcxproj.filters ├── title.ui ├── sonic.h ├── globalhelper.cpp ├── show.cpp ├── VideoCtl.h ├── QTD.vcxproj ├── playlist.cpp ├── ctrlbar.ui ├── ctrlbar.cpp ├── title.cpp ├── mainwid.cpp └── data.h /res/c.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/c.jpeg -------------------------------------------------------------------------------- /res/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/red.png -------------------------------------------------------------------------------- /res/al2l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/al2l.png -------------------------------------------------------------------------------- /res/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/close.png -------------------------------------------------------------------------------- /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/icon.png -------------------------------------------------------------------------------- /res/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/image.png -------------------------------------------------------------------------------- /swscale-5.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/swscale-5.dll -------------------------------------------------------------------------------- /res/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/loading.gif -------------------------------------------------------------------------------- /res/杨任东竹石体-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/杨任东竹石体-Bold.ttf -------------------------------------------------------------------------------- /res/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/fa-solid-900.ttf -------------------------------------------------------------------------------- /res/杨任东竹石体-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/杨任东竹石体-Medium.ttf -------------------------------------------------------------------------------- /res/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lwh1019/VideoPlayer/HEAD/res/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /res/qss/show.css: -------------------------------------------------------------------------------- 1 | QWidget { 2 | background-color: #1a1a20; 3 | color: white; 4 | border: 1px solid black; 5 | border-bottom: none; 6 | } 7 | 8 | QLabel { 9 | background: transparent; 10 | color: white; 11 | } -------------------------------------------------------------------------------- /res/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "打开文件": "OpenFile/F", 3 | "退出": "OnCloseBtnClicked/ESC", 4 | "截图": "OnSaveBmp/S", 5 | "全屏/退出全屏": "OnFullScreenPlay/Enter", 6 | "录屏": "OnSavePeriod/A", 7 | "循环方式": { 8 | "循环播放": "OnLoop", 9 | "列表播放": "OnList", 10 | "随机播放": "OnRandom" 11 | }, 12 | "点 击 Bilibili 试 试":"Onchekc" 13 | } -------------------------------------------------------------------------------- /更新日志1月5日.txt: -------------------------------------------------------------------------------- 1 | Bug修复: 2 | 1.切换视频后菜单难以切出 3 | 4 | 具体修改:因为貌似涉及到SDL的窗口绑定,将原先的QT的Label锁定,但在很多线程上还是用到了SDL,所以无法直接调用SDL_Exit()函数来进行初始化;所以退而求其次,新增一个快捷键M来快速打开对应的菜单 5 | 6 | 2.视频条切换只能1秒之间跳转 7 | 8 | 具体修改:将原先使用int型的变量替换为double,并修改对应的SliderMAX,使视频条能够平滑过渡移动 9 | 10 | 11 | 12 | 功能修改: 13 | 14 | 1.添加音乐播放功能并设置对应的播放界面(去除原先黑色底)、封面设置为唱片 (已完成) 15 | 16 | 2.修改原先的视频质量调整功能 (完成一半但受阻) -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwid.h" 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | #undef main 8 | 9 | int main(int argc, char *argv[]) 10 | { 11 | QApplication a(argc, argv); 12 | 13 | MainWid w; 14 | if (w.Init() == false) 15 | { 16 | return -1; 17 | } 18 | w.show(); 19 | return a.exec(); 20 | } 21 | -------------------------------------------------------------------------------- /CustomSlider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class CustomSlider : public QSlider 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | CustomSlider(QWidget* parent); 12 | ~CustomSlider(); 13 | protected: 14 | void mousePressEvent(QMouseEvent *ev); 15 | void mouseReleaseEvent(QMouseEvent *ev); 16 | void mouseMoveEvent(QMouseEvent *ev); 17 | signals: 18 | void SigCustomSliderValueChanged(); 19 | 20 | private: 21 | bool IsPressed = false; 22 | }; 23 | -------------------------------------------------------------------------------- /QTD.vcxproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /res/qss/title.css: -------------------------------------------------------------------------------- 1 | * { 2 | background-color: #202129; 3 | border: none; 4 | color: white; 5 | } 6 | 7 | QPushButton:hover { 8 | color: Cyan; 9 | } 10 | 11 | QPushButton:pressed { 12 | color: CadetBlue; 13 | } 14 | 15 | QPushButton#CloseBtn:hover { 16 | color: Tomato; 17 | } 18 | 19 | QPushButton#CloseBtn:pressed { 20 | color: red; 21 | /*background:#FF0000;*/ 22 | } 23 | 24 | QLabel#MovieNameLab { 25 | color: #c4c6d2; 26 | } 27 | 28 | /**********提示**********/ 29 | QToolTip { 30 | border: none; 31 | background-color: #2e2f37; 32 | color: white; 33 | } -------------------------------------------------------------------------------- /mainwid.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | res/qss/ctrlbar.css 4 | res/qss/mainwid.css 5 | res/qss/playlist.css 6 | res/qss/show.css 7 | res/qss/title.css 8 | res/menu.json 9 | res/fa-solid-900.ttf 10 | res/fa-v4compatibility.ttf 11 | res/杨任东竹石体-Medium.ttf 12 | res/杨任东竹石体-Bold.ttf 13 | res/image.png 14 | res/c.jpeg 15 | 16 | 17 | -------------------------------------------------------------------------------- /medialist.h: -------------------------------------------------------------------------------- 1 | // 播放列表菜单 2 | // Editor: Liwh 2024/12/22 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | class MediaList : public QListWidget 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | MediaList(QWidget *parent = 0); 16 | ~MediaList(); 17 | bool Init(); 18 | protected: 19 | void contextMenuEvent(QContextMenuEvent* event); 20 | private: 21 | void AddFile(); 22 | void RemoveFile(); 23 | signals: 24 | void SigAddFile(QString strFileName); 25 | 26 | private: 27 | QMenu Menu; 28 | 29 | QAction ActAdd; //添加文件 30 | QAction ActRemove; //移除文件 31 | QAction ActClearList;//清空列表 32 | }; 33 | -------------------------------------------------------------------------------- /QTD.ui: -------------------------------------------------------------------------------- 1 | 2 | QTDClass 3 | 4 | 5 | QTDClass 6 | 7 | 8 | 9 | 0 10 | 0 11 | 600 12 | 400 13 | 14 | 15 | 16 | QTD 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /CustomSlider.cpp: -------------------------------------------------------------------------------- 1 | #include "CustomSlider.h" 2 | #include "globalhelper.h" 3 | 4 | CustomSlider::CustomSlider(QWidget *parent) 5 | : QSlider(parent) 6 | { 7 | this->setMaximum(MAX_SLIDER_VALUE); 8 | } 9 | 10 | CustomSlider::~CustomSlider() 11 | { 12 | } 13 | 14 | void CustomSlider::mousePressEvent(QMouseEvent *ev) 15 | { 16 | QSlider::mousePressEvent(ev); 17 | double pos = ev->pos().x() / (double)width(); 18 | setValue(pos * (maximum() - minimum()) + minimum()); 19 | 20 | emit SigCustomSliderValueChanged(); 21 | IsPressed = true; 22 | } 23 | 24 | void CustomSlider::mouseReleaseEvent(QMouseEvent *ev) 25 | { 26 | QSlider::mouseReleaseEvent(ev); 27 | IsPressed = false; 28 | } 29 | 30 | void CustomSlider::mouseMoveEvent(QMouseEvent *ev) 31 | { 32 | QSlider::mouseMoveEvent(ev); 33 | if (IsPressed) 34 | { 35 | double pos = ev->pos().x() / (double)width(); 36 | setValue(pos * (maximum() - minimum()) + minimum()); 37 | emit SigCustomSliderValueChanged(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /show.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Show 4 | 5 | 6 | 7 | 0 8 | 0 9 | 5000 10 | 5000 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 0 20 | 0 21 | 5000 22 | 5000 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 5000 33 | 5000 34 | 40 35 | 12 36 | 37 | 38 | 39 | 0 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /QTD.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34902.84 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QTD", "QTD.vcxproj", "{96CD23A5-AFFF-4692-89D9-B54F983A2CC7}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Release|x64 = Release|x64 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {96CD23A5-AFFF-4692-89D9-B54F983A2CC7}.Debug|x64.ActiveCfg = Debug|x64 15 | {96CD23A5-AFFF-4692-89D9-B54F983A2CC7}.Debug|x64.Build.0 = Debug|x64 16 | {96CD23A5-AFFF-4692-89D9-B54F983A2CC7}.Release|x64.ActiveCfg = Release|x64 17 | {96CD23A5-AFFF-4692-89D9-B54F983A2CC7}.Release|x64.Build.0 = Release|x64 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {0F71F19D-D433-4270-80E7-FE9542BC51D6} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /res/gear-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playlist.h: -------------------------------------------------------------------------------- 1 | // 播放列表界面 2 | // Editor: Liwh 2024/12/22 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "ui_playlist.h" 11 | 12 | class Playlist : public QWidget,public Ui::Playlist 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | QString GenerateThumbnail(const QString& videoPath); 18 | explicit Playlist(QWidget *parent = 0); 19 | ~Playlist(); 20 | 21 | bool Init(); 22 | bool GetPlaylistStatus(); //列表状态 23 | 24 | public: 25 | 26 | void OnAddFile(QString strFileName); 27 | void OnAddFileAndPlay(QString strFileName); 28 | 29 | void OnBackwardPlay(); 30 | void OnForwardPlay(); 31 | void OnRandomPlay(); 32 | QSize sizeHint() const 33 | { 34 | return QSize(150, 900); 35 | } 36 | protected: 37 | 38 | void dropEvent(QDropEvent *event); 39 | void dragEnterEvent(QDragEnterEvent *event); 40 | 41 | signals: 42 | void SigUpdateUi(); 43 | void SigPlay(QString strFile); 44 | 45 | private: 46 | bool InitUi(); 47 | bool ConnectSignalSlots(); 48 | 49 | private slots: 50 | 51 | void on_List_itemDoubleClicked(QListWidgetItem *item); 52 | 53 | private: 54 | Ui::Playlist *ui; 55 | 56 | int CurrentPlayListIndex; 57 | }; 58 | -------------------------------------------------------------------------------- /medialist.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "medialist.h" 5 | 6 | #pragma execution_character_set("utf-8") 7 | 8 | MediaList::MediaList(QWidget *parent) 9 | : QListWidget(parent), 10 | Menu(this), 11 | ActAdd(this), 12 | ActRemove(this), 13 | ActClearList(this) 14 | { 15 | } 16 | 17 | MediaList::~MediaList() 18 | { 19 | } 20 | 21 | bool MediaList::Init() 22 | { 23 | ActAdd.setText("添加"); 24 | Menu.addAction(&ActAdd); 25 | ActRemove.setText("移除所选项"); 26 | QMenu* stRemoveMenu = Menu.addMenu("移除"); 27 | stRemoveMenu->addAction(&ActRemove); 28 | ActClearList.setText("清空列表"); 29 | Menu.addAction(&ActClearList); 30 | Menu.setStyleSheet("QMenu { background-color: #2C2C2C; color: white; border: 1px solid #5C5C5C; }" 31 | "QMenu::item { padding: 5px 20px; }" 32 | "QMenu::item:selected { background-color: #4C9AFF; }"); 33 | 34 | connect(&ActAdd, &QAction::triggered, this, &MediaList::AddFile); 35 | connect(&ActRemove, &QAction::triggered, this, &MediaList::RemoveFile); 36 | connect(&ActClearList, &QAction::triggered, this, &QListWidget::clear); 37 | 38 | return true; 39 | } 40 | 41 | void MediaList::contextMenuEvent(QContextMenuEvent* event) 42 | { 43 | Menu.popup(event->globalPos()); 44 | } 45 | 46 | void MediaList::AddFile() 47 | { 48 | QStringList listFileName = QFileDialog::getOpenFileNames(this, "打开文件", QDir::homePath(),"音视频文件(*.wav *.ogg *.mp3 *.mkv *.rmvb *.mp4 *.avi *.flv *.wmv *.3gp *.mov *.yuv)"); 49 | for (QString strFileName : listFileName) 50 | { 51 | emit SigAddFile(strFileName); 52 | } 53 | } 54 | void MediaList::RemoveFile() 55 | { 56 | takeItem(currentRow()); 57 | } 58 | 59 | -------------------------------------------------------------------------------- /title.h: -------------------------------------------------------------------------------- 1 | // 标题界面:显示名称和视频名 2 | // Editor: Liwh 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #include "ui_title.h" 29 | 30 | 31 | class Title : public QWidget,public Ui::Title 32 | { 33 | Q_OBJECT 34 | 35 | public: 36 | explicit Title(QWidget *parent = 0); 37 | ~Title(); 38 | bool Init(); 39 | private: 40 | void paintEvent(QPaintEvent *event); 41 | void mouseDoubleClickEvent(QMouseEvent *event); 42 | void resizeEvent(QResizeEvent *event); 43 | 44 | void ChangeMovieNameShow(); 45 | bool InitUi(); 46 | 47 | void OpenFile(); 48 | public: 49 | 50 | void OnChangeMaxBtnStyle(bool bIfMax); 51 | void OnPlay(QString strMovieName); 52 | void OnStopFinished(); 53 | 54 | void OnMenuBtnClicked(); 55 | signals: 56 | void SigCloseBtnClicked(); //< 点击关闭按钮 57 | void SigMinBtnClicked(); //< 点击最小化按钮 58 | void SigMaxBtnClicked(); //< 点击最大化按钮 59 | void SigDoubleClicked(); //< 双击标题栏 60 | 61 | void SigFullScreenBtnClicked(); ///< 点击全屏按钮 62 | 63 | void SigOpenFile(QString strFileName); //打开文件 64 | void SigShowMenu(); 65 | private: 66 | Ui::Title *ui; 67 | 68 | QString MovieName; 69 | 70 | QMenu Menu; 71 | QActionGroup ActionGroup; 72 | }; 73 | -------------------------------------------------------------------------------- /ctrlbar.h: -------------------------------------------------------------------------------- 1 | // 控制栏界面 2 | // Editor: Liwh 2024/12/20 3 | #pragma once 4 | #include 5 | #include "ui_ctrlbar.h" 6 | class CtrlBar : public QWidget,public Ui::CtrlBar 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | explicit CtrlBar(QWidget *parent = 0); 12 | void checkSpeedBtnTextChanged(); 13 | void checkClearBtnTextChanged(); 14 | void checkSettingBtnTextChanged(); 15 | void ChangeText(); 16 | ~CtrlBar(); 17 | bool Init(); 18 | 19 | public: 20 | void OnVideoTotalSeconds(double nSeconds); 21 | void OnVideoPlaySeconds(double nSeconds); 22 | void OnVideopVolume(double dPercent); 23 | void OnPauseStat(bool bPaused); 24 | void OnStopFinished(); 25 | void OnSpeed(float speed); 26 | 27 | void OnChangeVideo(QString s); 28 | private: 29 | void OnPlaySliderValueChanged(); 30 | void OnVolumeSliderValueChanged(); 31 | void OnRecordBtn(); 32 | 33 | private slots: 34 | void on_PlayOrPauseBtn_clicked(); 35 | void on_VolumeBtn_clicked(); 36 | void on_SettingBtn_clicked(); 37 | void on_speedBtn_changed(); 38 | void on_clearBtn_changed(); 39 | bool ConnectSignalSlots(); 40 | void On_WordBtn_clicked(); 41 | 42 | signals: 43 | void SigShowOrHidePlaylist(); 44 | void SigPlaySeek(double dPercent); 45 | void SigPlayVolume(double dPercent); 46 | void SigPlayOrPause(); 47 | void SigStop(); 48 | void SigForwardPlay(); 49 | void SigBackwardPlay(); 50 | void SigShowMenu(); 51 | void SigShowSetting(); 52 | void SigSpeed(double speed); 53 | 54 | void SigLoadSubtitle(QString word); 55 | void SigHideSubtitle(); 56 | 57 | private: 58 | Ui::CtrlBar *ui; 59 | 60 | double TotalPlaySeconds; 61 | double LastVolumePercent; 62 | }; 63 | -------------------------------------------------------------------------------- /globalhelper.h: -------------------------------------------------------------------------------- 1 | // PCH.H 公共头 2 | // Editor: Liwh 2024/12/21 3 | 4 | #pragma once 5 | #pragma execution_character_set("utf-8") 6 | 7 | enum ERROR_CODE 8 | { 9 | NoError = 0, 10 | ErrorFileInvalid 11 | }; 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | class GlobalHelper 19 | { 20 | public: 21 | GlobalHelper(); 22 | static void CreateSpeedMenu(QPushButton* speedButton); 23 | static void CreateClearMenu(QPushButton* clearButton); 24 | static void CreateSettingMenu(QPushButton* settingButton); 25 | 26 | static QString GetQssStr(QString strQssPath); 27 | static void SetIcon(QPushButton* btn, int iconSize, QChar icon); 28 | static void SavePlaylist(QStringList& playList); 29 | static void GetPlaylist(QStringList& playList); 30 | static void SavePlayVolume(double& nVolume); 31 | static void GetPlayVolume(double& nVolume); 32 | }; 33 | 34 | extern "C"{ 35 | #include "libavutil/avstring.h" 36 | #include "libavutil/channel_layout.h" 37 | #include "libavutil/eval.h" 38 | #include "libavutil/mathematics.h" 39 | #include "libavutil/pixdesc.h" 40 | #include "libavutil/imgutils.h" 41 | #include "libavutil/dict.h" 42 | #include "libavutil/fifo.h" 43 | #include "libavutil/parseutils.h" 44 | #include "libavutil/samplefmt.h" 45 | #include "libavutil/time.h" 46 | #include "libavutil/bprint.h" 47 | #include "libavformat/avformat.h" 48 | #include "libavcodec/avcodec.h" 49 | #include "libavfilter/avfilter.h" 50 | #include "libavdevice/avdevice.h" 51 | #include "libswscale/swscale.h" 52 | #include "libavutil/opt.h" 53 | #include "libavcodec/avfft.h" 54 | #include "libswresample/swresample.h" 55 | #include "SDL.h" 56 | #include "SDL_image.h" 57 | #include "SDL_ttf.h" 58 | } 59 | -------------------------------------------------------------------------------- /res/qss/playlist.css: -------------------------------------------------------------------------------- 1 | * { 2 | background-color: #1a1a20; 3 | color: white; 4 | } 5 | 6 | QPushButton { 7 | color: white; 8 | } 9 | 10 | QPushButton:hover { 11 | color: Cyan; 12 | } 13 | 14 | QPushButton:pressed { 15 | color: CadetBlue; 16 | } 17 | 18 | QLabel:hover { 19 | color: Cyan; 20 | background-color: rgba(0, 255, 255, 0.1); 21 | border: 1px solid Cyan; 22 | } 23 | 24 | /*****列表*******/ 25 | QListWidget { 26 | border: 1px solid Black; 27 | } 28 | 29 | QListWidget::item:hover { 30 | /*background: Cyan;*/ 31 | padding: 5px; 32 | margin: 1px; 33 | color: Cyan; 34 | border: 1px solid Cyan; 35 | } 36 | 37 | QListWidget::item:selected { 38 | padding: 5px; 39 | margin: 1px; 40 | color: Cyan; 41 | border: 1px solid Cyan; 42 | } 43 | 44 | 45 | 46 | QScrollBar:vertical { 47 | width: 10px; 48 | background: transparent; 49 | } 50 | 51 | QScrollBar::handle:vertical { 52 | min-height: 30px; 53 | background: #202129; 54 | margin-top: 0px; 55 | margin-bottom: 0px; 56 | } 57 | 58 | QScrollBar::handle:vertical:hover { 59 | background: rgb(80, 80, 80); 60 | } 61 | 62 | QScrollBar::sub-line:vertical { 63 | height: 0px; 64 | background: transparent; 65 | image: url(:/Black/arrowTop); 66 | subcontrol-position: top; 67 | } 68 | 69 | QScrollBar::add-line:vertical { 70 | height: 0px; 71 | background: transparent; 72 | image: url(:/Black/arrowBottom); 73 | subcontrol-position: bottom; 74 | } 75 | 76 | QScrollBar::sub-line:vertical:hover { 77 | background: rgb(68, 69, 73); 78 | } 79 | 80 | QScrollBar::add-line:vertical:hover { 81 | background: rgb(68, 69, 73); 82 | } 83 | 84 | QScrollBar::add-page:vertical, 85 | QScrollBar::sub-page:vertical { 86 | background: transparent; 87 | } -------------------------------------------------------------------------------- /show.h: -------------------------------------------------------------------------------- 1 | // 视频显示界面 2 | // Editor: Liwh 2024/12/22 3 | #pragma once 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "videoctl.h" 16 | #include "ui_show.h" 17 | 18 | class Show : public QWidget,public Ui::Show 19 | { 20 | Q_OBJECT 21 | 22 | public: 23 | Show(QWidget *parent); 24 | ~Show(); 25 | bool Init(); 26 | 27 | bool eventFilter(QObject* obj, QEvent* event); 28 | 29 | protected: 30 | void dropEvent(QDropEvent *event); 31 | 32 | void dragEnterEvent(QDragEnterEvent *event); 33 | void resizeEvent(QResizeEvent *event); 34 | void keyReleaseEvent(QKeyEvent *event); 35 | 36 | 37 | void mousePressEvent(QMouseEvent* event); 38 | 39 | void contextMenuEvent(QContextMenuEvent* event) override; 40 | 41 | public: 42 | void OnPlay(QString strFile); 43 | void OnStopFinished(); 44 | void OnFrameDimensionsChanged(int nFrameWidth, int nFrameHeight); 45 | 46 | private: 47 | 48 | void OnDisplayMsg(QString strMsg); 49 | 50 | 51 | void OnTimerShowCursorUpdate(); 52 | 53 | void OnActionsTriggered(QAction *action); 54 | private: 55 | 56 | bool ConnectSignalSlots(); 57 | 58 | 59 | void ChangeShow(); 60 | signals: 61 | void SigOpenFile(QString strFileName);///< 增加视频文件 62 | void SigPlay(QString strFile); ///<播放 63 | 64 | void SigFullScreen();//全屏播放 65 | void SigPlayOrPause(); 66 | void SigStop(); 67 | void SigShowMenu(); 68 | 69 | void SigSeekForward(); 70 | void SigSeekBack(); 71 | void SigAddVolume(); 72 | void SigSubVolume(); 73 | 74 | public slots: 75 | void OnResolutionChanged(bool success,QString outputFilePath,double pos); 76 | private: 77 | Ui::Show *ui; 78 | 79 | int LastFrameWidth; 80 | int LastFrameHeight; 81 | 82 | QTimer timerShowCursor; 83 | 84 | QMenu Menu; 85 | QActionGroup ActionGroup; 86 | }; 87 | -------------------------------------------------------------------------------- /playlist.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Playlist 4 | 5 | 6 | 7 | 0 8 | 0 9 | 115 10 | 254 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | Qt::FocusPolicy::NoFocus 21 | 22 | 23 | Qt::ScrollBarPolicy::ScrollBarAsNeeded 24 | 25 | 26 | Qt::ScrollBarPolicy::ScrollBarAlwaysOff 27 | 28 | 29 | 30 | testlist0 31 | 32 | 33 | 34 | 35 | testlist1 36 | 37 | 38 | 39 | 40 | testlist2 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 楷体 50 | 12 51 | true 52 | 53 | 54 | 55 | 播放列表 56 | 57 | 58 | Qt::AlignmentFlag::AlignCenter 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | MediaList 67 | QListWidget 68 |
medialist.h
69 |
70 |
71 | 72 | 73 |
74 | -------------------------------------------------------------------------------- /res/qss/ctrlbar.css: -------------------------------------------------------------------------------- 1 | /* 全局初始设为透明背景、白色文字 */ 2 | * { 3 | background-color: transparent; 4 | color: white; 5 | } 6 | 7 | /* 控制条背景部分 */ 8 | QWidget#PlaySliderBgWidget { 9 | border-bottom: 1px solid black; 10 | } 11 | 12 | /* 按钮悬停 & 按下时的视觉反馈 */ 13 | QPushButton { 14 | border: none; 15 | } 16 | 17 | QPushButton:hover { 18 | color: Cyan; 19 | background-color: rgba(0, 255, 255, 0.1); 20 | border: 1px solid Cyan; 21 | } 22 | 23 | QPushButton:pressed { 24 | color: CadetBlue; 25 | background-color: rgba(0, 255, 255, 0.2); 26 | border: 1px solid CadetBlue; 27 | } 28 | 29 | /********** 滑竿 (QSlider) **********/ 30 | /* 滑竿本身 */ 31 | QSlider::groove:horizontal { 32 | border: 1px solid #4A708B; 33 | background: #C0C0C0; 34 | height: 3px; 35 | border-radius: 2px; 36 | padding-left: -1px; 37 | padding-right: -1px; 38 | } 39 | 40 | /* 已滑过的部分 */ 41 | QSlider::sub-page:horizontal { 42 | background: qlineargradient(x1: 0, y1: 0.2, x2: 1, y2: 1, 43 | stop: 0 #5DCCFF, stop: 1 #1874CD); 44 | border: 1px solid #4A708B; 45 | border-radius: 2px; 46 | } 47 | 48 | /* 未滑过部分 */ 49 | QSlider::add-page:horizontal { 50 | background: #4c4c4c; 51 | border: 0px solid #777; 52 | border-radius: 2px; 53 | } 54 | 55 | /* 滑块 (handle) */ 56 | QSlider::handle:horizontal { 57 | background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, 58 | fx:0.5, fy:0.5, stop:0.6 #45ADED, 59 | stop:0.8 rgba(255, 255, 255, 255)); 60 | width: 8px; 61 | border-radius: 4px; 62 | margin-top: -3px; 63 | margin-bottom: -2px; 64 | } 65 | 66 | QSlider::handle:horizontal:hover { 67 | background: qradialgradient(spread:pad, cx:0.5, cy:0.5, radius:0.5, 68 | fx:0.5, fy:0.5, stop:0 #2A8BDA, 69 | stop:0.8 rgba(255, 255, 255, 255)); 70 | width: 8px; 71 | border-radius: 4px; 72 | margin-top: -3px; 73 | margin-bottom: -2px; 74 | } 75 | 76 | /********** 时间显示 **********/ 77 | QTimeEdit#VideoTotalTimeTimeEdit { 78 | color: #c4c6d2; 79 | } 80 | 81 | QLabel#TimeSplitLabel { 82 | color: #c4c6d2; 83 | } 84 | 85 | /********** 提示 (Tooltip) **********/ 86 | QToolTip { 87 | border: none; 88 | background-color: #2e2f37; 89 | color: white; 90 | border-radius: 4px; 91 | padding: 5px 8px; 92 | font-size: 12px; 93 | } -------------------------------------------------------------------------------- /res/qss/mainwid.css: -------------------------------------------------------------------------------- 1 | /* *{ 2 | background-color: #202129; 3 | color:white; 4 | } */ 5 | 6 | /********** 主窗口 **********/ 7 | QMainWindow#MainWid { 8 | border: 1px solid black; 9 | background-color: #202129; 10 | border-radius: 20px; 11 | } 12 | 13 | /********** 背景小部件 **********/ 14 | QWidget#BgWidget { 15 | border: 1px solid #444; 16 | border-radius: 20px; 17 | background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, 18 | stop:0 #2e2f37, stop:1 #202129); 19 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.3); 20 | } 21 | 22 | QWidget#ShowCtrlBarPlaylistBgWidget { 23 | border: 1px solid black; 24 | border-top: none; 25 | } 26 | 27 | QWidget#PlaylistWid>PlaylistContents { 28 | border: 1px solid black; 29 | } 30 | 31 | /********** 菜单 **********/ 32 | QMenu { 33 | border: 1px solid #555; 34 | background: #292b33; 35 | color: #ddd; 36 | border-radius: 8px; 37 | } 38 | 39 | QMenu::item { 40 | height: 26px; 41 | padding: 5px 20px; 42 | margin: 2px 10px; 43 | /* 增加内边距和边距 */ 44 | border-radius: 8px; 45 | } 46 | 47 | /* 可用状态下的菜单项文字颜色 */ 48 | QMenu::item:enabled { 49 | color: rgb(225, 225, 225); 50 | } 51 | 52 | /* 不可用状态下的菜单项文字颜色 */ 53 | QMenu::item: !enabled { 54 | color: rgb(155, 155, 155); 55 | } 56 | 57 | /* 鼠标悬停 (hover) 与选中 (selected) 状态 */ 58 | QMenu::item:enabled:hover, 59 | QMenu::item:selected { 60 | background: rgba(255, 255, 255, 0.15); 61 | color: #fff; 62 | border: 1px solid rgba(255, 255, 255, 0.3); 63 | } 64 | 65 | /* 分割线 */ 66 | QMenu::separator { 67 | height: 2px; 68 | background: rgba(255, 255, 255, 0.2); 69 | margin: 4px 10px; 70 | } 71 | 72 | /* 复选框、单选框的指示器大小 */ 73 | QMenu::indicator { 74 | width: 13px; 75 | height: 13px; 76 | } 77 | 78 | /* 菜单图标的左右内边距 */ 79 | QMenu::icon { 80 | padding-left: 2px; 81 | padding-right: 2px; 82 | } 83 | 84 | /********** 提示 (ToolTip) **********/ 85 | QToolTip { 86 | border: 1px solid #444; 87 | background: rgba(50, 50, 60, 0.9); 88 | color: #eee; 89 | border-radius: 6px; 90 | padding: 5px 8px; 91 | font-size: 12px; 92 | } 93 | 94 | /********** 状态栏 **********/ 95 | QStatusBar { 96 | background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, 97 | stop:0 #3a3b3f, stop:1 #2c2d30); 98 | border-top: 1px solid #555; 99 | color: #ccc; 100 | font-size: 12px; 101 | } 102 | 103 | QStatusBar::item { 104 | border: 1px solid #444; 105 | border-radius: 4px; 106 | padding: 3px 6px; 107 | background: #2e2f33; 108 | margin: 2px; 109 | } 110 | 111 | QLabel { 112 | background: transparent; 113 | color: white; 114 | } -------------------------------------------------------------------------------- /mainwid.h: -------------------------------------------------------------------------------- 1 | // 主界面 2 | // Editor: Liwh 2024/12/20 3 | #pragma once 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "playlist.h" 14 | #include "title.h" 15 | #include "ui_mainwid.h" 16 | 17 | class MainWid : public QMainWindow,public Ui::MainWid 18 | { 19 | Q_OBJECT 20 | 21 | public: 22 | explicit MainWid(QMainWindow *parent = 0); 23 | ~MainWid(); 24 | bool Init(); 25 | protected: 26 | void paintEvent(QPaintEvent *event); 27 | void enterEvent(QEvent *event); 28 | void leaveEvent(QEvent *event); 29 | void keyReleaseEvent(QKeyEvent *event); 30 | void mousePressEvent(QMouseEvent *event); 31 | void mouseReleaseEvent(QMouseEvent *event); 32 | void mouseMoveEvent(QMouseEvent *event); 33 | void contextMenuEvent(QContextMenuEvent* event); 34 | private: 35 | bool ConnectSignalSlots(); 36 | void OnCloseBtnClicked(); 37 | void OnMinBtnClicked(); 38 | void OnMaxBtnClicked(); 39 | void OnShowOrHidePlaylist(); 40 | 41 | void OnFullScreenPlay(); 42 | 43 | void OnCtrlBarAnimationTimeOut(); 44 | void OnFullscreenMouseDetectTimeOut(); 45 | 46 | void OnCtrlBarHideTimeOut(); 47 | void OnShowMenu(); 48 | void OnShowAbout(); 49 | void OpenFile(); 50 | 51 | void OnSaveBmp(); 52 | 53 | void OnShowSettingWid(); 54 | 55 | void OnSavePeriod(); 56 | 57 | void OnLoop(); 58 | void OnList(); 59 | void OnRandom(); 60 | 61 | void InitMenu(); 62 | void InitMenuActions(); 63 | 64 | void MenuJsonParser(QJsonObject& json_obj, QMenu* menu); 65 | QMenu* AddMenuFun(QString menu_title, QMenu* menu); 66 | void AddActionFun(QString action_title, QMenu* menu, void(MainWid::* slot_addr)()); 67 | 68 | signals: 69 | 70 | void SigShowMax(bool bIfMax); 71 | void SigSeekForward(); 72 | void SigSeekBack(); 73 | void SigAddVolume(); 74 | void SigSubVolume(); 75 | void SigPlayOrPause(); 76 | void SigOpenFile(QString strFilename); 77 | 78 | void SigClear(QString file); 79 | 80 | void SigVideoLoop(QString file); 81 | private: 82 | Ui::MainWid *ui; 83 | 84 | bool isPlaying; // 正在播放 85 | 86 | 87 | int ShadowWidth; // 阴影宽度 88 | 89 | bool FullScreenPlay; ///< 全屏播放标志 90 | 91 | QPropertyAnimation *CtrlbarAnimationShow; //全屏时控制面板浮动显示 92 | QPropertyAnimation *CtrlbarAnimationHide; //全屏时控制面板浮动显示 93 | QRect CtrlBarAnimationShowRect;//控制面板显示区域 94 | QRect CtrlBarAnimationHideRect;//控制面板隐藏区域 95 | 96 | QTimer CtrlBarAnimationTimer; 97 | QTimer FullscreenMouseDetectTimer;//全屏时鼠标位置监测时钟 98 | bool FullscreenCtrlBarShow; 99 | QTimer CtrlBarHideTimer; 100 | 101 | Playlist Playlist; 102 | Title Title; 103 | 104 | bool MoveDrag;//移动窗口标志 105 | QPoint DragPosition; 106 | 107 | typedef void (MainWid::* MenuAction)(); 108 | 109 | QMenu Menu; 110 | QAction ActFullscreen; 111 | 112 | QActionGroup* loopActionGroup; 113 | 114 | QMap Map_act; 115 | }; 116 | -------------------------------------------------------------------------------- /mainwid.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWid 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1100 10 | 600 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | res/icon.pngres/icon.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 0 27 | 28 | 29 | 0 30 | 31 | 32 | 0 33 | 34 | 35 | 0 36 | 37 | 38 | 0 39 | 40 | 41 | 42 | 43 | 44 | 0 45 | 60 46 | 47 | 48 | 49 | 50 | 16777215 51 | 60 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 100 61 | 100 62 | 63 | 64 | 65 | 66 | 67 | ShowWid 68 | CtrlBarWid 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 0 77 | 0 78 | 1100 79 | 18 80 | 81 | 82 | 83 | 84 | 85 | Qt::LayoutDirection::LeftToRight 86 | 87 | 88 | false 89 | 90 | 91 | 2 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | Qt::LayoutDirection::LeftToRight 103 | 104 | 105 | 4 106 | 107 | 108 | 109 | 110 | 111 | 112 | Show 113 | QWidget 114 |
show.h
115 | 1 116 |
117 | 118 | CtrlBar 119 | QWidget 120 |
ctrlbar.h
121 | 1 122 |
123 |
124 | 125 | 126 |
127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoPlayer 2 | 3 | 改编自 https://github.com/itisyang/playerdemo/tree/master 4 | 5 | 用Visual Studio 2022编写,在原先的基础上修复了一些隐藏的Bug,同时增加了字幕显示与插入、倍速播放(变速不变调)、视频质量调整、截图、录屏、播放方式调整、爬取B站视频等功能 6 | 7 | 编译环境:Visual Studio 2022、FFmpeg 4.3、SDL2 8 | 9 | 注意:本项目非纯从FFmpeg底层原理上实现部分功能,在一些没有必要的情况下直接使用了FFmpeg命令行实现对应功能,如有需要可自行实现。 10 | 11 | ## 2025年1月5日 一改: 12 | Bug修复: 13 | 1.切换视频后菜单难以切出 14 | 15 | 具体修改:因为貌似涉及到SDL的窗口绑定,将原先的QT的Label锁定,但在很多线程上还是用到了SDL,所以无法直接调用SDL_Exit()函数来进行初始化;所以退而求其次,新增一个快捷键M来快速打开对应的菜单 16 | 17 | 2.视频条切换只能1秒之间跳转 18 | 19 | 具体修改:将原先使用int型的变量替换为double,并修改对应的SliderMAX,使视频条能够平滑过渡移动 20 | 21 | 功能修改: 22 | 23 | 1.添加音乐播放功能并设置对应的播放界面(去除原先黑色底)、封面设置为唱片 (已完成) 24 | 25 | 2.修改原先的视频质量调整功能 (完成一半但受阻) 26 | 27 | # 一、主交互界面: 28 | 29 | 未运行时: 30 | 31 | ![image](https://github.com/user-attachments/assets/0d054bc4-d9e5-44a4-9393-1ac9dfc0d52b) 32 | 33 | 运行时界面: 34 | 35 | ![image](https://github.com/user-attachments/assets/ddf7b178-eb31-456a-849f-5c2e9b8b08a4) 36 | 37 | # 二、控制栏界面: 38 | 39 | 该界面包含了前向播放、播放/停止、向后播放按钮,用于控制视频的播放;同时还有用于显示视频播放的全部时长和当前时长,附加上倍速控制、画质控制、字幕控制、录制控制、列表控制、播放方式控制,构成整个控制栏界面。倍速控制支持多个倍速的调整功能(例如0.5x,0.75x,2.0x等),画质控制支持(360P,720P,1080P,1080P 60帧等),播放方式包括循环播放、列表播放、随机播放等。字幕控制会自动检测当前视频有无字幕,若内嵌字幕则播放字幕,否则可以支持在视频中嵌入软字幕(.srt,.ass等字幕文件)。 40 | 41 | ![image](https://github.com/user-attachments/assets/466980e1-8899-4fc1-b11f-49d9133b885a) 42 | 43 | # 三、快捷菜单栏: 44 | 45 | 快捷菜单栏给出了部分功能的快捷按钮提示,同时也附加了一些控制栏功能在一起,方便用户能够更快的进行界面或画面的控制,包括快捷打开文件、快捷退出等等操作。 46 | 47 | ![image](https://github.com/user-attachments/assets/aff3abd2-b669-45a9-b60d-9d5053a9d4f7) 48 | 49 | # 四、视频播放列表展示: 50 | 51 | 视频播放列表界面提供了播放视频的标题和具体封面(视频的第一个i帧),同时针对视频播放列表使用不同的菜单栏,帮助更快的进行添加文件、删除文件等等操作。 52 | 53 | ![image](https://github.com/user-attachments/assets/1e992b9c-8641-4783-8f98-1cb94e87dd17) 54 | 55 | # 五、标题界面展示: 56 | 57 | 标题界面主要用于展示当前模仿视频的名称和控制窗口的最小化、最大化、全屏、关闭等操作,同时为了增加功能性,在点击界面上的Bilibili按钮后输入播放的B站视频的URL就能够在线爬取B站的视频到本地并播放。 58 | 59 | ![image](https://github.com/user-attachments/assets/1ba81f45-60d8-4737-9b94-5677cc7f4d9c) 60 | 61 | 62 | ## 功能实现效果展示: 63 | # 基础功能: 64 | # 一、视频播放、暂停: 65 | 66 | ![image](https://github.com/user-attachments/assets/cb100bc9-cacb-4bf1-8b1c-acf22795732c) 67 | 68 | # 二、视频进度、音量调整: 69 | 70 | ![image](https://github.com/user-attachments/assets/c5fa8291-6e6a-4fe5-8947-a4873967cd69) 71 | 72 | # 三、支持添加并播放多种视频格式: 73 | 74 | ![image](https://github.com/user-attachments/assets/f2d7f12e-d79f-4864-9681-d6c49bcfd8c4) 75 | 76 | # 拓展功能: 77 | # 一、视频倍速播放控制: 78 | 79 | ![image](https://github.com/user-attachments/assets/873f02f6-0ff0-492c-acb4-a37d6271c485) 80 | 81 | # 二、视频截图功能: 82 | 83 | ![image](https://github.com/user-attachments/assets/4d2722d8-f180-459a-aafa-ffd29ffa975f) 84 | 85 | # 三、视频质量调整控制: 86 | 87 | ![image](https://github.com/user-attachments/assets/1fda9bb8-a981-452a-aa7f-07304bed8293) 88 | 89 | # 四、字幕加载与显示控制: 90 | 未打开时: 91 | 92 | ![image](https://github.com/user-attachments/assets/f87df601-3273-451f-9ceb-c9938716ade0) 93 | 94 | 打开时: 95 | 96 | ![image](https://github.com/user-attachments/assets/023edfab-9289-498b-8a94-76ed071ed1ef) 97 | 98 | 对于没有字幕的原视频: 先进行字幕文件的加载 99 | 100 | ![image](https://github.com/user-attachments/assets/28cd4d21-3a79-4cb5-9bd7-1fdf9433ba67) 101 | 102 | # 五、录制功能: 103 | 按下后: 左下角会有提示 104 | 105 | ![image](https://github.com/user-attachments/assets/8999db06-21ba-4a80-b7f1-27032d0ac9d3) 106 | 107 | 再次按下后即可保存下来: 108 | 109 | ![image](https://github.com/user-attachments/assets/da2e21ce-c25d-47d2-9876-1f3977d4b03b) 110 | 111 | # 六、播放方式功能: 112 | 针对于一个视频播放结束后接下来要做什么,目前实现了三种方式:循环播放、列表播放、随机播放。 113 | 114 | ![image](https://github.com/user-attachments/assets/40fd643c-6b81-42fb-a74f-612184d82aea) 115 | 116 | # 七、网络视频爬取功能: 117 | 目前主要只实现了对于Bilibili上的视频进行爬取,通过网络技术连接Bilibili的网络视频库爬取其中对应视频的最高清晰度视频到本地,同时也可以设置大会员Cookie来进行更高画质的爬取。 118 | 119 | ![image](https://github.com/user-attachments/assets/107ad124-7218-45a8-a1b0-2e35479b3a24) 120 | 121 | 下载后即可播放: 122 | 123 | ![image](https://github.com/user-attachments/assets/969a23ca-7fd3-4446-a800-9688af66f92d) 124 | -------------------------------------------------------------------------------- /QTD.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | {99349809-55BA-4b9d-BF79-8FDBB0286EB3} 18 | ui 19 | 20 | 21 | {639EADAA-A684-42e4-A9AD-28FC9BCB8F7C} 22 | ts 23 | 24 | 25 | 26 | 27 | Resource Files 28 | 29 | 30 | 31 | 32 | Source Files 33 | 34 | 35 | Source Files 36 | 37 | 38 | Source Files 39 | 40 | 41 | Source Files 42 | 43 | 44 | Source Files 45 | 46 | 47 | Source Files 48 | 49 | 50 | Source Files 51 | 52 | 53 | Source Files 54 | 55 | 56 | Source Files 57 | 58 | 59 | Source Files 60 | 61 | 62 | Source Files 63 | 64 | 65 | 66 | 67 | Header Files 68 | 69 | 70 | Header Files 71 | 72 | 73 | Header Files 74 | 75 | 76 | Header Files 77 | 78 | 79 | Header Files 80 | 81 | 82 | Header Files 83 | 84 | 85 | Header Files 86 | 87 | 88 | Header Files 89 | 90 | 91 | 92 | 93 | Form Files 94 | 95 | 96 | Form Files 97 | 98 | 99 | Form Files 100 | 101 | 102 | Form Files 103 | 104 | 105 | Form Files 106 | 107 | 108 | 109 | 110 | Header Files 111 | 112 | 113 | Header Files 114 | 115 | 116 | Header Files 117 | 118 | 119 | Header Files 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /title.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Title 4 | 5 | 6 | 7 | 0 8 | 0 9 | 826 10 | 50 11 | 12 | 13 | 14 | 15 | 16777215 16 | 50 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | movie_name 42 | 43 | 44 | 15 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 50 53 | 50 54 | 55 | 56 | 57 | 58 | 50 59 | 50 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 50 72 | 50 73 | 74 | 75 | 76 | 77 | 50 78 | 50 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 50 91 | 50 92 | 93 | 94 | 95 | 96 | 50 97 | 50 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 50 106 | 50 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 50 116 | 50 117 | 118 | 119 | 120 | 121 | 50 122 | 50 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 130 135 | 0 136 | 137 | 138 | 139 | 140 | 150 141 | 16777215 142 | 143 | 144 | 145 | 146 | Bahnschrift Light SemiCondensed 147 | 18 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | res/al2l.pngres/al2l.png 156 | 157 | 158 | 159 | 125 160 | 125 161 | 162 | 163 | 164 | false 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /sonic.h: -------------------------------------------------------------------------------- 1 | #ifdef __cplusplus 2 | extern "C" { 3 | #endif 4 | 5 | /* Uncomment this to use sin-wav based overlap add which in theory can improve 6 | sound quality slightly, at the expense of lots of floating point math. */ 7 | /* #define SONIC_USE_SIN */ 8 | 9 | /* This specifies the range of voice pitches we try to match. 10 | Note that if we go lower than 65, we could overflow in findPitchInRange */ 11 | #define SONIC_MIN_PITCH 65 12 | #define SONIC_MAX_PITCH 400 13 | 14 | /* These are used to down-sample some inputs to improve speed */ 15 | #define SONIC_AMDF_FREQ 4000 16 | 17 | struct sonicStreamStruct; 18 | typedef struct sonicStreamStruct *sonicStream; 19 | 20 | /* For all of the following functions, numChannels is multiplied by numSamples 21 | to determine the actual number of values read or returned. */ 22 | 23 | /* Create a sonic stream. Return NULL only if we are out of memory and cannot 24 | allocate the stream. Set numChannels to 1 for mono, and 2 for stereo. */ 25 | // 创建一个音频流,如果内存溢出不能创建流会返回NULL,numCHannels表示声道的个数,1为单声道,2为双声道 26 | sonicStream sonicCreateStream(int sampleRate, int numChannels); 27 | /* Destroy the sonic stream. */ 28 | // 销毁一个音频流 29 | void sonicDestroyStream(sonicStream stream); 30 | /* Use this to write floating point data to be speed up or down into the stream. 31 | Values must be between -1 and 1. Return 0 if memory realloc failed, otherwise 1 */ 32 | // 33 | int sonicWriteFloatToStream(sonicStream stream, float *samples, int numSamples); 34 | /* Use this to write 16-bit data to be speed up or down into the stream. 35 | Return 0 if memory realloc failed, otherwise 1 */ 36 | int sonicWriteShortToStream(sonicStream stream, short *samples, int numSamples); 37 | /* Use this to write 8-bit unsigned data to be speed up or down into the stream. 38 | Return 0 if memory realloc failed, otherwise 1 */ 39 | int sonicWriteUnsignedCharToStream(sonicStream stream, unsigned char *samples, int numSamples); 40 | /* Use this to read floating point data out of the stream. Sometimes no data 41 | will be available, and zero is returned, which is not an error condition. */ 42 | int sonicReadFloatFromStream(sonicStream stream, float *samples, int maxSamples); 43 | /* Use this to read 16-bit data out of the stream. Sometimes no data will 44 | be available, and zero is returned, which is not an error condition. */ 45 | int sonicReadShortFromStream(sonicStream stream, short *samples, int maxSamples); 46 | /* Use this to read 8-bit unsigned data out of the stream. Sometimes no data will 47 | be available, and zero is returned, which is not an error condition. */ 48 | int sonicReadUnsignedCharFromStream(sonicStream stream, unsigned char *samples, int maxSamples); 49 | /* Force the sonic stream to generate output using whatever data it currently 50 | has. No extra delay will be added to the output, but flushing in the middle of 51 | words could introduce distortion. */ 52 | // 立即强制刷新流 53 | int sonicFlushStream(sonicStream stream); 54 | /* Return the number of samples in the output buffer */ 55 | // 返回输出缓冲中的采样点数目 56 | int sonicSamplesAvailable(sonicStream stream); 57 | /* Get the speed of the stream. */ 58 | // 得到音频流的速度 59 | float sonicGetSpeed(sonicStream stream); 60 | /* Set the speed of the stream. */ 61 | // 设置音频流的速度 62 | void sonicSetSpeed(sonicStream stream, float speed); 63 | /* Get the pitch of the stream. */ 64 | float sonicGetPitch(sonicStream stream); 65 | /* Set the pitch of the stream. */ 66 | void sonicSetPitch(sonicStream stream, float pitch); 67 | /* Get the rate of the stream. */ 68 | float sonicGetRate(sonicStream stream); 69 | /* Set the rate of the stream. */ 70 | void sonicSetRate(sonicStream stream, float rate); 71 | /* Get the scaling factor of the stream. */ 72 | float sonicGetVolume(sonicStream stream); 73 | /* Set the scaling factor of the stream. */ 74 | void sonicSetVolume(sonicStream stream, float volume); 75 | /* Get the chord pitch setting. */ 76 | int sonicGetChordPitch(sonicStream stream); 77 | /* Set chord pitch mode on or off. Default is off. See the documentation 78 | page for a description of this feature. */ 79 | void sonicSetChordPitch(sonicStream stream, int useChordPitch); 80 | /* Get the quality setting. */ 81 | // 得到音频流的质量 82 | int sonicGetQuality(sonicStream stream); 83 | /* Set the "quality". Default 0 is virtually as good as 1, but very much faster. */ 84 | // 设置音频流的质量,默认的0的质量几乎和1的一样好,但是更快 85 | void sonicSetQuality(sonicStream stream, int quality); 86 | /* Get the sample rate of the stream. */ 87 | // 得到音频流的采样率 88 | int sonicGetSampleRate(sonicStream stream); 89 | /* Set the sample rate of the stream. This will drop any samples that have not been read. */ 90 | // 设置音频流的采样率 91 | void sonicSetSampleRate(sonicStream stream, int sampleRate); 92 | /* Get the number of channels. */ 93 | // 得到音频的声道数 94 | int sonicGetNumChannels(sonicStream stream); 95 | /* Set the number of channels. This will drop any samples that have not been read. */ 96 | // 设置音频流的声道数 97 | void sonicSetNumChannels(sonicStream stream, int numChannels); 98 | /* This is a non-stream oriented interface to just change the speed of a sound 99 | sample. It works in-place on the sample array, so there must be at least 100 | speed*numSamples available space in the array. Returns the new number of samples. */ 101 | // 这是一个非面向流的借口,只是改变声音采样的速率。它工作在采样数组内部, 102 | //所以在数组内至少要有speed*numSampes大小的空间。返回值是新的采样点的数目 103 | 104 | int sonicChangeFloatSpeed(float *samples, int numSamples, float speed, float pitch, 105 | float rate, float volume, int useChordPitch, int sampleRate, int numChannels); 106 | /* This is a non-stream oriented interface to just change the speed of a sound 107 | sample. It works in-place on the sample array, so there must be at least 108 | speed*numSamples available space in the array. Returns the new number of samples. */ 109 | int sonicChangeShortSpeed(short *samples, int numSamples, float speed, float pitch, 110 | float rate, float volume, int useChordPitch, int sampleRate, int numChannels); 111 | 112 | #ifdef __cplusplus 113 | } 114 | #endif 115 | -------------------------------------------------------------------------------- /globalhelper.cpp: -------------------------------------------------------------------------------- 1 | // PCH.H 公共头 2 | // Editor: Liwh 2024/12/21 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "globalhelper.h" 12 | #include "ctrlbar.h" 13 | 14 | const QString PLAYER_CONFIG_BASEDIR = QDir::tempPath(); 15 | const QString PLAYER_CONFIG = "player_config.ini"; 16 | 17 | GlobalHelper::GlobalHelper() 18 | { 19 | 20 | } 21 | void GlobalHelper::CreateSpeedMenu(QPushButton* speedButton) 22 | { 23 | // 创建菜单 24 | QMenu* menu = new QMenu(speedButton); 25 | menu->setStyleSheet("QMenu { background-color: #2C2C2C; color: white; border: 1px solid #5C5C5C; }" 26 | "QMenu::item { padding: 5px 20px; }" 27 | "QMenu::item:selected { background-color: #4C9AFF; }"); 28 | 29 | // 添加倍速选项 30 | QAction* slowSpeed = menu->addAction("0.5x"); 31 | QAction* noSlowSpeed = menu->addAction("0.75x"); 32 | QAction* normalSpeed = menu->addAction("1.0x"); 33 | QAction* nofastSpeed = menu->addAction("1.25x"); 34 | QAction* fastSpeed = menu->addAction("1.5x"); 35 | QAction* fasterSpeed = menu->addAction("2.0x"); 36 | 37 | QObject::connect(normalSpeed, &QAction::triggered, [=]() { 38 | speedButton->setText("倍速"); 39 | }); 40 | QObject::connect(noSlowSpeed, &QAction::triggered, [=]() { 41 | speedButton->setText("0.75x"); 42 | }); 43 | QObject::connect(nofastSpeed, &QAction::triggered, [=]() { 44 | speedButton->setText("1.25x"); 45 | }); 46 | 47 | QObject::connect(fastSpeed, &QAction::triggered, [=]() { 48 | speedButton->setText("1.5x"); 49 | }); 50 | 51 | QObject::connect(fasterSpeed, &QAction::triggered, [=]() { 52 | speedButton->setText("2.0x"); 53 | }); 54 | QObject::connect(slowSpeed, &QAction::triggered, [=]() { 55 | speedButton->setText("0.5x"); 56 | }); 57 | 58 | speedButton->setMenu(menu); 59 | 60 | } 61 | 62 | void GlobalHelper::CreateClearMenu(QPushButton* clearButton) 63 | { 64 | // 创建菜单 65 | QMenu* menu = new QMenu(clearButton); 66 | menu->setStyleSheet("QMenu { background-color: #2C2C2C; color: white; border: 1px solid #5C5C5C; }" 67 | "QMenu::item { padding: 5px 20px; }" 68 | "QMenu::item:selected { background-color: #4C9AFF; }"); 69 | 70 | // 添加倍速选项 71 | QAction* slowSpeed = menu->addAction("360P 流畅"); 72 | QAction* noSlowSpeed = menu->addAction("480P 清晰"); 73 | QAction* normalSpeed = menu->addAction("720P 高清"); 74 | QAction* nofastSpeed = menu->addAction("1080P 高清"); 75 | QAction* fastSpeed = menu->addAction("1080P 60帧"); 76 | QAction* fasterSpeed = menu->addAction("自动"); 77 | 78 | QObject::connect(normalSpeed, &QAction::triggered, [=]() { 79 | clearButton->setText("720P 高清"); 80 | }); 81 | QObject::connect(noSlowSpeed, &QAction::triggered, [=]() { 82 | clearButton->setText("480P 清晰"); 83 | }); 84 | QObject::connect(nofastSpeed, &QAction::triggered, [=]() { 85 | clearButton->setText("1080P 高清"); 86 | }); 87 | 88 | QObject::connect(fastSpeed, &QAction::triggered, [=]() { 89 | clearButton->setText("1080P 60帧"); 90 | }); 91 | 92 | QObject::connect(fasterSpeed, &QAction::triggered, [=]() { 93 | clearButton->setText("自动"); 94 | }); 95 | QObject::connect(slowSpeed, &QAction::triggered, [=]() { 96 | clearButton->setText("360P 流畅"); 97 | }); 98 | 99 | clearButton->setMenu(menu); 100 | 101 | } 102 | void GlobalHelper::CreateSettingMenu(QPushButton* settingButton) 103 | { 104 | QMenu* menu = new QMenu(settingButton); 105 | menu->setStyleSheet("QMenu { background-color: #2C2C2C; color: white; border: 1px solid #5C5C5C; }" 106 | "QMenu::item { padding: 5px 20px; }" 107 | "QMenu::item:selected { background-color: #4C9AFF; }"); 108 | 109 | QAction* slowSpeed = menu->addAction("循环播放"); 110 | QAction* noSlowSpeed = menu->addAction("列表播放"); 111 | QAction* normalSpeed = menu->addAction("随机播放"); 112 | 113 | QObject::connect(slowSpeed, &QAction::triggered, [=]() { 114 | GlobalHelper::SetIcon(settingButton, 15, QChar(0xf2f9)); 115 | }); 116 | QObject::connect(noSlowSpeed, &QAction::triggered, [=]() { 117 | GlobalHelper::SetIcon(settingButton, 15, QChar(0xf883)); 118 | }); 119 | QObject::connect(normalSpeed, &QAction::triggered, [=]() { 120 | GlobalHelper::SetIcon(settingButton, 15,QChar(0xf074)); 121 | }); 122 | 123 | settingButton->setMenu(menu); 124 | 125 | } 126 | QString GlobalHelper::GetQssStr(QString strQssPath) 127 | { 128 | QString strQss; 129 | QFile FileQss(strQssPath); 130 | if (FileQss.open(QIODevice::ReadOnly)) 131 | { 132 | strQss = FileQss.readAll(); 133 | FileQss.close(); 134 | } 135 | else 136 | { 137 | qDebug() << "读取样式表失败" << strQssPath; 138 | } 139 | return strQss; 140 | } 141 | 142 | void GlobalHelper::SetIcon(QPushButton* btn, int iconSize, QChar icon) 143 | { 144 | QFont font; 145 | font.setFamily("Font Awesome 6 Pro"); 146 | font.setPointSize(iconSize); 147 | 148 | btn->setFont(font); 149 | btn->setText(icon); 150 | } 151 | 152 | void GlobalHelper::SavePlaylist(QStringList& playList) 153 | { 154 | QString strPlayerConfigFileName = PLAYER_CONFIG_BASEDIR + QDir::separator() + PLAYER_CONFIG; 155 | QSettings settings(strPlayerConfigFileName, QSettings::IniFormat); 156 | settings.beginWriteArray("playlist"); 157 | for (int i = 0; i < playList.size(); ++i) 158 | { 159 | settings.setArrayIndex(i); 160 | settings.setValue("movie", playList.at(i)); 161 | } 162 | settings.endArray(); 163 | } 164 | 165 | void GlobalHelper::GetPlaylist(QStringList& playList) 166 | { 167 | QString strPlayerConfigFileName = PLAYER_CONFIG_BASEDIR + QDir::separator() + PLAYER_CONFIG; 168 | QSettings settings(strPlayerConfigFileName, QSettings::IniFormat); 169 | 170 | int size = settings.beginReadArray("playlist"); 171 | for (int i = 0; i < size; ++i) 172 | { 173 | settings.setArrayIndex(i); 174 | playList.append(settings.value("movie").toString()); 175 | } 176 | settings.endArray(); 177 | } 178 | 179 | void GlobalHelper::SavePlayVolume(double& nVolume) 180 | { 181 | QString strPlayerConfigFileName = PLAYER_CONFIG_BASEDIR + QDir::separator() + PLAYER_CONFIG; 182 | QSettings settings(strPlayerConfigFileName, QSettings::IniFormat); 183 | settings.setValue("volume/size", nVolume); 184 | } 185 | 186 | void GlobalHelper::GetPlayVolume(double& nVolume) 187 | { 188 | QString strPlayerConfigFileName = PLAYER_CONFIG_BASEDIR + QDir::separator() + PLAYER_CONFIG; 189 | QSettings settings(strPlayerConfigFileName, QSettings::IniFormat); 190 | QString str = settings.value("volume/size").toString(); 191 | nVolume = settings.value("volume/size", nVolume).toDouble(); 192 | } 193 | 194 | -------------------------------------------------------------------------------- /show.cpp: -------------------------------------------------------------------------------- 1 | // 视频显示界面 2 | // Editor: Liwh 2024/12/22 3 | 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | 25 | #include "show.h" 26 | #include "ui_show.h" 27 | #include "globalhelper.h" 28 | #include 29 | #include "videoctl.h" 30 | 31 | #pragma execution_character_set("utf-8") 32 | 33 | QMutex show_rect; 34 | 35 | double start = 0; 36 | 37 | Show::Show(QWidget *parent) : 38 | QWidget(parent), 39 | ui(new Ui::Show), 40 | ActionGroup(this) 41 | { 42 | ui->setupUi(this); 43 | 44 | setStyleSheet(GlobalHelper::GetQssStr("://res/qss/show.css")); 45 | setAcceptDrops(true); 46 | 47 | this->setAttribute(Qt::WA_OpaquePaintEvent); 48 | this->setMouseTracking(true); 49 | 50 | 51 | 52 | LastFrameWidth = 0; 53 | LastFrameHeight = 0; 54 | 55 | connect(VideoCtl::GetInstance(), &VideoCtl::ResolutionChanged,this,&Show::OnResolutionChanged); 56 | 57 | } 58 | 59 | Show::~Show() 60 | { 61 | delete ui; 62 | } 63 | 64 | bool Show::Init() 65 | { 66 | if (ConnectSignalSlots() == false) 67 | { 68 | return false; 69 | } 70 | 71 | ui->label->installEventFilter(this); 72 | ui->label_2->installEventFilter(this); 73 | return true; 74 | } 75 | bool Show::eventFilter(QObject* obj, QEvent* event) 76 | { 77 | if (obj == ui->label) 78 | { 79 | if (event->type() == QEvent::MouseButtonPress) 80 | { 81 | QMouseEvent* mouseEvent = static_cast(event); 82 | if (mouseEvent->button() == Qt::RightButton) 83 | { 84 | qDebug() << "Show::eventFilter: Right button clicked on label"; 85 | emit SigShowMenu(); 86 | return true; 87 | } 88 | } 89 | } 90 | else if (obj == ui->label_2) 91 | { 92 | if (event->type() == QEvent::MouseButtonPress) 93 | { 94 | QMouseEvent* mouseEvent = static_cast(event); 95 | if (mouseEvent->button() == Qt::RightButton) 96 | { 97 | qDebug() << "Show::eventFilter: Right button clicked on label"; 98 | emit SigShowMenu(); 99 | return true; 100 | } 101 | } 102 | } 103 | 104 | return QWidget::eventFilter(obj, event); 105 | } 106 | void Show::contextMenuEvent(QContextMenuEvent* event) 107 | { 108 | qDebug() << "Show::contextMenuEvent triggered"; 109 | emit SigShowMenu(); 110 | } 111 | void Show::OnFrameDimensionsChanged(int nFrameWidth, int nFrameHeight) 112 | { 113 | qDebug() << "Show::OnFrameDimensionsChanged" << nFrameWidth << nFrameHeight; 114 | LastFrameWidth = nFrameWidth; 115 | LastFrameHeight = nFrameHeight; 116 | 117 | ChangeShow(); 118 | } 119 | 120 | void Show::ChangeShow() 121 | { 122 | QMutexLocker locker(&show_rect); 123 | 124 | if (LastFrameWidth == 0 && LastFrameHeight == 0) 125 | { 126 | ui->label->setGeometry(0, 0, width(), height()); 127 | qDebug() << "0 0 " << width() << " " << height() << '\n'; 128 | return; 129 | } 130 | float videoAspectRatio = static_cast(LastFrameWidth) / static_cast(LastFrameHeight); 131 | int containerWidth = this->width(); 132 | int containerHeight = this->height(); 133 | float containerAspectRatio = static_cast(containerWidth) / static_cast(containerHeight); 134 | 135 | int displayWidth, displayHeight, offsetX, offsetY; 136 | 137 | if (videoAspectRatio > containerAspectRatio) 138 | { 139 | displayWidth = containerWidth; 140 | displayHeight = static_cast(containerWidth / videoAspectRatio); 141 | offsetX = 0; 142 | offsetY = (containerHeight - displayHeight) / 2; 143 | } 144 | else 145 | { 146 | displayHeight = containerHeight; 147 | displayWidth = static_cast(containerHeight * videoAspectRatio); 148 | offsetX = (containerWidth - displayWidth) / 2; 149 | offsetY = 0; 150 | } 151 | ui->label->setGeometry(offsetX, offsetY, displayWidth, displayHeight); 152 | } 153 | 154 | void Show::dragEnterEvent(QDragEnterEvent *event) 155 | { 156 | event->acceptProposedAction(); 157 | } 158 | 159 | void Show::resizeEvent(QResizeEvent *event) 160 | { 161 | Q_UNUSED(event); 162 | 163 | ChangeShow(); 164 | } 165 | 166 | void Show::keyReleaseEvent(QKeyEvent *event) 167 | { 168 | qDebug() << "Show::keyPressEvent:" << event->key(); 169 | switch (event->key()) 170 | { 171 | case Qt::Key_Return: 172 | SigFullScreen(); 173 | break; 174 | case Qt::Key_Left: 175 | emit SigSeekBack(); 176 | break; 177 | case Qt::Key_Right: 178 | qDebug() << "前进5s"; 179 | emit SigSeekForward(); 180 | break; 181 | case Qt::Key_Up: 182 | emit SigAddVolume(); 183 | break; 184 | case Qt::Key_Down: 185 | emit SigSubVolume(); 186 | break; 187 | case Qt::Key_Space: 188 | emit SigPlayOrPause(); 189 | break; 190 | 191 | default: 192 | QWidget::keyPressEvent(event); 193 | break; 194 | } 195 | } 196 | void Show::mousePressEvent(QMouseEvent* event) 197 | { 198 | if (event->button() == Qt::RightButton) 199 | { 200 | qDebug() << "我点击了右键" << '\n'; 201 | emit SigShowMenu(); 202 | event->accept(); 203 | } 204 | else 205 | { 206 | QWidget::mousePressEvent(event); 207 | } 208 | } 209 | void Show::OnDisplayMsg(QString strMsg) 210 | { 211 | qDebug() << "Show::OnDisplayMsg " << strMsg; 212 | } 213 | 214 | void Show::OnPlay(QString strFile) 215 | { 216 | qDebug() << "调用了OnPlay函数" << '\n'; 217 | LastFrameWidth = 0; 218 | LastFrameHeight = 0; 219 | ui->label->update(); 220 | ChangeShow(); 221 | 222 | // VideoCtl::GetInstance()->ReInit(); 223 | VideoCtl::GetInstance()->StartPlay(strFile, ui->label, start); 224 | start = 0.0; 225 | } 226 | void Show::OnStopFinished() 227 | { 228 | update(); 229 | } 230 | void Show::OnTimerShowCursorUpdate() 231 | { 232 | } 233 | 234 | void Show::OnActionsTriggered(QAction *action) 235 | { 236 | QString strAction = action->text(); 237 | if (strAction == "全屏") 238 | { 239 | emit SigFullScreen(); 240 | } 241 | else if (strAction == "停止") 242 | { 243 | emit SigStop(); 244 | } 245 | else if (strAction == "暂停" || strAction == "播放") 246 | { 247 | emit SigPlayOrPause(); 248 | } 249 | } 250 | 251 | bool Show::ConnectSignalSlots() 252 | { 253 | QList listRet; 254 | bool bRet; 255 | 256 | bRet = connect(this, &Show::SigPlay, this, &Show::OnPlay); 257 | listRet.append(bRet); 258 | 259 | timerShowCursor.setInterval(2000); 260 | bRet = connect(&timerShowCursor, &QTimer::timeout, this, &Show::OnTimerShowCursorUpdate); 261 | listRet.append(bRet); 262 | 263 | connect(&ActionGroup, &QActionGroup::triggered, this, &Show::OnActionsTriggered); 264 | 265 | for (bool bReturn : listRet) 266 | { 267 | if (bReturn == false) 268 | { 269 | return false; 270 | } 271 | } 272 | 273 | return true; 274 | } 275 | 276 | void Show::dropEvent(QDropEvent *event) 277 | { 278 | QList urls = event->mimeData()->urls(); 279 | if(urls.isEmpty()) 280 | { 281 | return; 282 | } 283 | 284 | for(QUrl url: urls) 285 | { 286 | QString strFileName = url.toLocalFile(); 287 | qDebug() << strFileName; 288 | emit SigOpenFile(strFileName); 289 | break; 290 | } 291 | } 292 | 293 | void Show::OnResolutionChanged(bool success,QString outputFilePath,double pos) 294 | { 295 | if (success) 296 | { 297 | qDebug() << "转换后的视频文件:" << outputFilePath; 298 | start = pos; 299 | OnPlay(outputFilePath); 300 | } 301 | else { 302 | QMessageBox::warning(this, "转换失败", ""); 303 | } 304 | } -------------------------------------------------------------------------------- /VideoCtl.h: -------------------------------------------------------------------------------- 1 | // 核心部分:控制播放视频 2 | // Editor: Liwh 2024/12/23 3 | 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "globalhelper.h" 14 | #include "data.h" 15 | #include "sonic.h" 16 | 17 | #include 18 | #include 19 | #include 20 | using namespace soundtouch; 21 | 22 | 23 | #define FFP_PROP_FLOAT_PLAYBACK_RATE 0003 // 设置播放速率 24 | #define FFP_PROP_FLOAT_PLAYBACK_VOLUME 10006 25 | 26 | 27 | class VideoCtl : public QObject 28 | { 29 | Q_OBJECT 30 | public: 31 | static VideoCtl* GetInstance(); 32 | ~VideoCtl(); 33 | bool ReInit(); 34 | bool StartPlay(QString strFileName, QLabel* label, double start); 35 | 36 | void update_sample_display(VideoState* is, short* samples, int samples_size); 37 | 38 | int AudioDecodeFromFrame(VideoState* is); // 解码并重采样音频帧 39 | void SetClockAt(Clock* c, double pts, int serial, double time); 40 | void ClockToSlave(Clock* c, Clock* slave); 41 | 42 | void ChangeResolution(const std::string& resolution); 43 | 44 | double GetCurrentPosition(); 45 | 46 | float ffp_get_property_float(int id, float default_value); 47 | void ffp_set_property_float(int id, float value); 48 | 49 | bool TakeScreenshot(QString& filePath); 50 | 51 | signals: 52 | void SigPlayMsg(QString strMsg); 53 | void SigFrameDimensionsChanged(int nFrameWidth, int nFrameHeight); //<视频宽高发生变化 54 | 55 | void SigVideoTotalSeconds(double nSeconds); 56 | void SigVideoPlaySeconds(double nSeconds); 57 | 58 | void SigVideoVolume(double dPercent); 59 | void SigPauseStat(bool bPaused); 60 | 61 | void ResolutionChanged(bool flag, QString Filename, double pos); 62 | 63 | void SigStop(); 64 | 65 | void SigStopFinished(); 66 | void SigSpeed(float speed); 67 | 68 | void SigStartPlay(QString strFileName); 69 | 70 | void SigClear(); 71 | 72 | void SigCaptureStarted(double startTime); 73 | void SigCaptureStopped(QString outputFile, bool success); 74 | 75 | void SigList(); 76 | void SigRandom(); 77 | 78 | public: 79 | void OnPlaySeek(double dPercent); 80 | void OnPlayVolume(double dPercent); 81 | void OnSeekForward(); 82 | void OnSeekBack(); 83 | void OnAddVolume(); 84 | void OnSubVolume(); 85 | void OnPause(); 86 | void OnStop(); 87 | 88 | int IsLooping(); 89 | 90 | void OnSpeed(double newSpeed); 91 | 92 | void OnSetWordPlay(bool flag); 93 | 94 | bool GetHasWord(); 95 | 96 | void LoadingSc(); 97 | 98 | void OnSetLoop(int flag); 99 | 100 | public: 101 | explicit VideoCtl(QObject* parent = nullptr); 102 | void ShowTemporarySubtitle(const QString& text, int duration_ms); 103 | void ClearSubtitle(); 104 | bool Init(); 105 | bool ConnectSignalSlots(); 106 | 107 | int GetVideoFrame(VideoState* is, AVFrame* frame); 108 | int AudioThread(void* arg); // 音频线程 109 | int VideoThread(void* arg); // 视频线程 110 | int SubTitleThread(void* arg); // 字幕解码线程 111 | 112 | int SynchronizeAudio(VideoState* is, int nb_samples); 113 | 114 | int AudioOpen(void* opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams* audio_hw_params); 115 | int StreamOpen(VideoState* is, int stream_index); 116 | int StreamPackets(AVStream* st, int stream_id, PacketQueue* queue); 117 | int RealTime(AVFormatContext* s); 118 | void ReadThread(VideoState* CurStream); 119 | void LoopThread(VideoState* CurStream); 120 | VideoState* StreamOpen(const char* filename); 121 | 122 | void ClearSubtitleCache(); 123 | 124 | void StreamCycleChannel(VideoState* is, int codec_type); 125 | void RefreshLoop(VideoState* is, SDL_Event* event); 126 | 127 | void VideoRefresh(void* opaque, double* remaining_time); 128 | int QueuePicture(VideoState* is, AVFrame* src_frame, double pts, double duration, int64_t pos, int serial); 129 | 130 | //更新音量 131 | void UpdateVolume(int sign, double step); 132 | 133 | void VideoDisplay(VideoState* is); 134 | int VideoOpen(VideoState* is); 135 | void DoExit(VideoState*& is); 136 | 137 | int ReallocTexture(SDL_Texture** texture, Uint32 new_format, int new_width, int new_height, SDL_BlendMode blendmode, int init_texture); 138 | void CalculateDisplayRect(SDL_Rect* rect, int scr_xleft, int scr_ytop, int scr_width, int scr_height, int pic_width, int pic_height, AVRational pic_sar); 139 | int UploadTexture(SDL_Texture* tex, AVFrame* frame, struct SwsContext** img_convert_ctx); 140 | void VideoImageDisplay(VideoState* is); 141 | void StreamComponentClose(VideoState* is, int stream_index); 142 | void StreamClose(VideoState* is); 143 | double GetClock(Clock* c); 144 | 145 | void SetClock(Clock* c, double pts, int serial); 146 | void SetClockSpeed(Clock* c, double speed); 147 | void InitClock(Clock* c, int* queue_serial); 148 | 149 | int GetMasterType(VideoState* is); 150 | double GetMasterClock(VideoState* is); 151 | void CheckExternalClockSpeed(VideoState* is); 152 | void StreamSeek(VideoState* is, int64_t pos, int64_t rel); 153 | void StreamTogglePause(VideoState* is); 154 | void TogglePause(VideoState* is); 155 | void ToNextFrame(VideoState* is); 156 | double TargetDelay(double delay, VideoState* is); 157 | double VpDuration(VideoState* is, Frame* vp, Frame* nextvp); 158 | void UpdateVideoPts(VideoState* is, double pts, int64_t pos, int serial); 159 | public: 160 | void ffp_set_playback_rate(float rate); 161 | float ffp_get_playback_rate(); 162 | 163 | int ffp_get_playback_rate_change(); 164 | void ffp_set_playback_rate_change(int change); 165 | 166 | int64_t get_target_frequency(); 167 | int get_target_channels(); 168 | int is_normal_playback_rate(); 169 | 170 | public slots: 171 | void onFfmpegFinished(int exitCode, QProcess::ExitStatus exitStatus); 172 | void onWordFinished(int exitCode, QProcess::ExitStatus exitStatus); 173 | void LoadSubtitle(const QString& subtitleFilePath); 174 | void HideSubtitle(); 175 | void OnCaptureFinished(int exitCode, QProcess::ExitStatus exitStatus); 176 | 177 | public: 178 | 179 | static VideoCtl* m_pInstance; //< 单例指针 180 | 181 | bool m_bInited; //< 初始化标志 182 | bool m_bPlayLoop; //刷新循环标志 183 | 184 | VideoState* m_CurStream; 185 | 186 | SDL_Window* window; 187 | SDL_Renderer* renderer; 188 | SDL_RendererInfo renderer_info = { 0 }; 189 | SDL_AudioDeviceID audio_dev; 190 | WId play_wid;//播放窗口 191 | 192 | 193 | int screen_width; 194 | int screen_height; 195 | int startup_volume; 196 | 197 | //播放刷新循环线程 198 | std::thread m_tPlayLoopThread; 199 | 200 | int m_nFrameW; 201 | int m_nFrameH; 202 | 203 | soundtouch::SoundTouch soundTouch; 204 | 205 | 206 | SDL_mutex* soundTouchMutex; 207 | 208 | std::map resmap; 209 | 210 | 211 | TTF_Font* subtitle_font = nullptr; 212 | std::string subtitle_font_path = "res//杨任东竹石体-Medium.ttf"; 213 | int subtitle_font_size = 50; 214 | 215 | SDL_Texture* subtitle_texture = nullptr; 216 | 217 | void RenderText(const std::string& text, int x, int y,int id); 218 | 219 | std::map subtitle_texture_cache; 220 | std::mutex subtitle_cache_mutex; 221 | 222 | QProcess* ffmpegProcess; 223 | QProcess* OnloadWord; 224 | QProcess* captureProcess; 225 | 226 | float speed; // 播放速率 227 | int ischange; // 播放速率改变 228 | 229 | bool wordPlay; 230 | 231 | bool HasWord; 232 | 233 | bool isRecording; 234 | double recordStartTime; 235 | double recordEndTime; 236 | QString captureOutputFile; 237 | 238 | 239 | QString currentSubtitle; 240 | QTimer* subtitleTimer; 241 | 242 | int isloop; 243 | public: 244 | // 变速相关 245 | sonicStreamStruct* audio_speed_convert; 246 | 247 | private: 248 | int m_targetWidth; 249 | int m_targetHeight; 250 | }; 251 | -------------------------------------------------------------------------------- /QTD.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | x64 7 | 8 | 9 | Release 10 | x64 11 | 12 | 13 | 14 | {96CD23A5-AFFF-4692-89D9-B54F983A2CC7} 15 | QtVS_v304 16 | 10.0 17 | 10.0 18 | $(MSBuildProjectDirectory)\QtMsBuild 19 | 20 | 21 | 22 | Application 23 | v143 24 | true 25 | MultiByte 26 | 27 | 28 | Application 29 | v143 30 | false 31 | true 32 | MultiByte 33 | 34 | 35 | 36 | 37 | 38 | 39 | 6.8.0_msvc2022_64 40 | core;gui;widgets 41 | debug 42 | 43 | 44 | 6.8.0_msvc2022_64 45 | core;gui;widgets 46 | release 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | F:\Digital Video Solve\Final-Work\lib\SDL2\include;F:\opencv\soundtouch\include;D:\lists\ffmpeg-n4.4.3-3-gb48951bd29-win64-lgpl-shared-4.4\include;F:\QT\6.8.0\msvc2022_64\include;$(IncludePath) 64 | F:\opencv\soundtouch\lib;F:\QT\6.8.0\msvc2022_64\lib;F:\Digital Video Solve\Final-Work\lib\SDL2\lib\x64;D:\lists\ffmpeg-n4.4.3-3-gb48951bd29-win64-lgpl-shared-4.4\lib;$(LibraryPath) 65 | 66 | 67 | F:\Digital Video Solve\Final-Work\lib\SDL2\include;F:\opencv\soundtouch\include;D:\lists\ffmpeg-n4.4.3-3-gb48951bd29-win64-lgpl-shared-4.4\include;F:\QT\6.8.0\msvc2022_64\include;$(IncludePath) 68 | F:\opencv\soundtouch\lib;F:\QT\6.8.0\msvc2022_64\lib;F:\Digital Video Solve\Final-Work\lib\SDL2\lib\x64;D:\lists\ffmpeg-n4.4.3-3-gb48951bd29-win64-lgpl-shared-4.4\lib;$(LibraryPath) 69 | 70 | 71 | 72 | Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Networkd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;Qt6Cored.lib;SoundTouchD_x64.lib;avcodec.lib;avdevice.lib;avfilter.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;SDL2.lib;SDL2_ttf.lib;SDL2_image.lib;%(AdditionalDependencies) 73 | 74 | 75 | 4996 76 | stdcpp20 77 | 78 | 79 | 80 | 81 | Qt6Multimediad.lib;Qt6MultimediaWidgetsd.lib;Qt6Networkd.lib;Qt6Svgd.lib;Qt6SvgWidgetsd.lib;Qt6Cored.lib;SoundTouchD_x64.lib;avcodec.lib;avdevice.lib;avfilter.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;SDL2.lib;SDL2_ttf.lib;SDL2_image.lib;%(AdditionalDependencies) 82 | 83 | 84 | 4996 85 | stdcpp20 86 | 87 | 88 | 89 | 90 | true 91 | Level3 92 | true 93 | true 94 | 95 | 96 | Console 97 | true 98 | 99 | 100 | 101 | 102 | true 103 | Level3 104 | true 105 | true 106 | true 107 | true 108 | 109 | 110 | Console 111 | false 112 | true 113 | true 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /playlist.cpp: -------------------------------------------------------------------------------- 1 | // 播放列表界面 2 | // Editor: Liwh 2024/12/22 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "playlist.h" 24 | #include "ui_playlist.h" 25 | #include "globalhelper.h" 26 | #include 27 | 28 | QString Playlist::GenerateThumbnail(const QString& filePath) 29 | { 30 | QFileInfo fileInfo(filePath); 31 | QString extension = fileInfo.suffix().toLower(); 32 | 33 | // 定义音频和视频扩展名 34 | const QStringList audioExtensions = { "mp3", "wav", "aac", "flac", "ogg", "m4a", "wma" }; 35 | const QStringList videoExtensions = { "mkv", "rmvb", "mp4", "avi", "flv", "wmv", "mov", "yuv", "3gp" }; 36 | 37 | // 如果是音频文件,返回默认封面图像 38 | if (audioExtensions.contains(extension)) 39 | { 40 | QString defaultImagePath = "://res//c.jpeg"; // 确保这个路径正确 41 | return defaultImagePath; 42 | } 43 | 44 | if (videoExtensions.contains(extension)) 45 | { 46 | QString thumbnailPath = QDir::tempPath() + "/" + fileInfo.baseName() + "_thumbnail.jpg"; 47 | 48 | if (QFile::exists(thumbnailPath)) 49 | { 50 | return thumbnailPath; 51 | } 52 | 53 | QString ffmpegCmd = QString("ffmpeg -i \"%1\" -vf \"select='eq(pict_type,I)'\" -frames:v 1 \"%2\"") 54 | .arg(filePath) 55 | .arg(thumbnailPath); 56 | 57 | QProcess process; 58 | process.start(ffmpegCmd); 59 | process.waitForFinished(); 60 | 61 | // 检查是否生成成功 62 | if (!QFile::exists(thumbnailPath)) 63 | { 64 | qDebug() << "Failed to generate thumbnail for" << filePath; 65 | return QString(); 66 | } 67 | 68 | return thumbnailPath; 69 | } 70 | return QString(); 71 | } 72 | 73 | Playlist::Playlist(QWidget *parent) : 74 | QWidget(parent), 75 | ui(new Ui::Playlist) 76 | { 77 | ui->setupUi(this); 78 | } 79 | 80 | Playlist::~Playlist() 81 | { 82 | QStringList strListPlayList; 83 | for (int i = 0; i < ui->List->count(); i++) 84 | { 85 | strListPlayList.append(ui->List->item(i)->toolTip()); 86 | } 87 | GlobalHelper::SavePlaylist(strListPlayList); 88 | delete ui; 89 | } 90 | 91 | bool Playlist::Init() 92 | { 93 | if (ui->List->Init() == false) 94 | { 95 | return false; 96 | } 97 | 98 | if (InitUi() == false) 99 | { 100 | return false; 101 | } 102 | 103 | if (ConnectSignalSlots() == false) 104 | { 105 | return false; 106 | } 107 | 108 | setAcceptDrops(true); 109 | 110 | return true; 111 | } 112 | 113 | bool Playlist::InitUi() 114 | { 115 | setStyleSheet(GlobalHelper::GetQssStr("://res/qss/playlist.css")); 116 | ui->List->clear(); 117 | 118 | QStringList strListPlaylist; 119 | GlobalHelper::GetPlaylist(strListPlaylist); 120 | 121 | for (QString strVideoFile : strListPlaylist) 122 | { 123 | QFileInfo fileInfo(strVideoFile); 124 | if (fileInfo.exists()) 125 | { 126 | QListWidgetItem *pItem = new QListWidgetItem(ui->List); 127 | pItem->setData(Qt::UserRole, QVariant(fileInfo.filePath())); // 用户数据 128 | pItem->setText(QString("%1").arg(fileInfo.fileName())); // 显示文本 129 | pItem->setToolTip(fileInfo.filePath()); 130 | QString thumbnailPath = GenerateThumbnail(fileInfo.filePath()); 131 | if (!thumbnailPath.isEmpty()) 132 | { 133 | QPixmap pixmap; 134 | if (thumbnailPath.startsWith("://")) // 资源文件路径 135 | { 136 | pixmap.load(thumbnailPath); 137 | } 138 | else 139 | { 140 | pixmap.load(thumbnailPath); 141 | } 142 | if (!pixmap.isNull()) 143 | { 144 | pItem->setIcon(QIcon(pixmap.scaled(100, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation))); // 设置缩略图 145 | } 146 | else 147 | { 148 | qDebug() << "Failed to load pixmap from" << thumbnailPath; 149 | } 150 | } 151 | ui->List->addItem(pItem); 152 | } 153 | } 154 | if (strListPlaylist.length() > 0) 155 | { 156 | ui->List->setCurrentRow(0); 157 | } 158 | 159 | ui->List->setIconSize(QSize(75, 75)); 160 | ui->List->setResizeMode(QListWidget::Adjust); 161 | ui->List->setStyleSheet("QListWidget { background: #222222; border: none; }" 162 | "QListWidget::item { border: none; }"); 163 | return true; 164 | } 165 | 166 | bool Playlist::ConnectSignalSlots() 167 | { 168 | QList listRet; 169 | bool bRet; 170 | 171 | bRet = connect(ui->List, &MediaList::SigAddFile, this, &Playlist::OnAddFile); 172 | listRet.append(bRet); 173 | 174 | for (bool bReturn : listRet) 175 | { 176 | if (bReturn == false) 177 | { 178 | return false; 179 | } 180 | } 181 | 182 | return true; 183 | } 184 | 185 | void Playlist::on_List_itemDoubleClicked(QListWidgetItem *item) 186 | { 187 | emit SigPlay(item->data(Qt::UserRole).toString()); 188 | CurrentPlayListIndex = ui->List->row(item); 189 | ui->List->setCurrentRow(CurrentPlayListIndex); 190 | } 191 | 192 | bool Playlist::GetPlaylistStatus() 193 | { 194 | if (this->isHidden()) 195 | { 196 | return false; 197 | } 198 | 199 | return true; 200 | } 201 | 202 | void Playlist::OnAddFile(QString strFileName) 203 | { 204 | bool bSupportMovie = strFileName.endsWith(".mkv", Qt::CaseInsensitive) || 205 | strFileName.endsWith(".rmvb", Qt::CaseInsensitive) || 206 | strFileName.endsWith(".mp4", Qt::CaseInsensitive) || 207 | strFileName.endsWith(".avi", Qt::CaseInsensitive) || 208 | strFileName.endsWith(".flv", Qt::CaseInsensitive) || 209 | strFileName.endsWith(".wmv", Qt::CaseInsensitive) || 210 | strFileName.endsWith(".mov", Qt::CaseInsensitive) || 211 | strFileName.endsWith(".yuv", Qt::CaseInsensitive) || 212 | strFileName.endsWith(".mp3", Qt::CaseInsensitive) || 213 | strFileName.endsWith(".wav", Qt::CaseInsensitive) || 214 | strFileName.endsWith(".ogg", Qt::CaseInsensitive) || 215 | strFileName.endsWith(".3gp", Qt::CaseInsensitive); 216 | if (!bSupportMovie) 217 | { 218 | return; 219 | } 220 | 221 | QFileInfo fileInfo(strFileName); 222 | QList listItem = ui->List->findItems(fileInfo.fileName(), Qt::MatchExactly); 223 | QListWidgetItem *pItem = nullptr; 224 | if (listItem.isEmpty()) 225 | { 226 | pItem = new QListWidgetItem(ui->List); 227 | pItem->setData(Qt::UserRole, QVariant(fileInfo.filePath())); // 用户数据 228 | pItem->setText(fileInfo.fileName()); // 显示文本 229 | pItem->setToolTip(fileInfo.filePath()); 230 | QString thumbnailPath = GenerateThumbnail(fileInfo.filePath()); 231 | if (!thumbnailPath.isEmpty()) 232 | { 233 | QPixmap pixmap(thumbnailPath); 234 | pItem->setIcon(QIcon(pixmap.scaled(100, 100, Qt::KeepAspectRatio))); // 设置缩略图 235 | } 236 | ui->List->addItem(pItem); 237 | } 238 | else 239 | { 240 | pItem = listItem.at(0); 241 | } 242 | } 243 | 244 | void Playlist::OnAddFileAndPlay(QString strFileName) 245 | { 246 | bool bSupportMovie = strFileName.endsWith(".mkv", Qt::CaseInsensitive) || 247 | strFileName.endsWith(".rmvb", Qt::CaseInsensitive) || 248 | strFileName.endsWith(".mp4", Qt::CaseInsensitive) || 249 | strFileName.endsWith(".avi", Qt::CaseInsensitive) || 250 | strFileName.endsWith(".flv", Qt::CaseInsensitive) || 251 | strFileName.endsWith(".wmv", Qt::CaseInsensitive) || 252 | strFileName.endsWith(".mov", Qt::CaseInsensitive) || 253 | strFileName.endsWith(".yuv", Qt::CaseInsensitive) || 254 | strFileName.endsWith(".mp3", Qt::CaseInsensitive) || 255 | strFileName.endsWith(".wav", Qt::CaseInsensitive) || 256 | strFileName.endsWith(".ogg", Qt::CaseInsensitive) || 257 | strFileName.endsWith(".3gp", Qt::CaseInsensitive); 258 | if (!bSupportMovie) 259 | { 260 | return; 261 | } 262 | 263 | QFileInfo fileInfo(strFileName); 264 | QList listItem = ui->List->findItems(fileInfo.fileName(), Qt::MatchExactly); 265 | QListWidgetItem *pItem = nullptr; 266 | if (listItem.isEmpty()) 267 | { 268 | pItem = new QListWidgetItem(ui->List); 269 | pItem->setData(Qt::UserRole, QVariant(fileInfo.filePath())); // 用户数据 270 | pItem->setText(fileInfo.fileName()); // 显示文本 271 | pItem->setToolTip(fileInfo.filePath()); 272 | QString thumbnailPath = GenerateThumbnail(fileInfo.filePath()); 273 | if (!thumbnailPath.isEmpty()) 274 | { 275 | QPixmap pixmap(thumbnailPath); 276 | pItem->setIcon(QIcon(pixmap.scaled(100, 100, Qt::KeepAspectRatio))); // 设置缩略图 277 | } 278 | ui->List->addItem(pItem); 279 | } 280 | else 281 | { 282 | pItem = listItem.at(0); 283 | } 284 | on_List_itemDoubleClicked(pItem); 285 | } 286 | 287 | void Playlist::OnBackwardPlay() 288 | { 289 | if (CurrentPlayListIndex == 0) 290 | { 291 | CurrentPlayListIndex = ui->List->count() - 1; 292 | on_List_itemDoubleClicked(ui->List->item(CurrentPlayListIndex)); 293 | ui->List->setCurrentRow(CurrentPlayListIndex); 294 | } 295 | else 296 | { 297 | CurrentPlayListIndex--; 298 | on_List_itemDoubleClicked(ui->List->item(CurrentPlayListIndex)); 299 | ui->List->setCurrentRow(CurrentPlayListIndex); 300 | } 301 | } 302 | 303 | void Playlist::OnForwardPlay() 304 | { 305 | if (CurrentPlayListIndex == ui->List->count() - 1) 306 | { 307 | CurrentPlayListIndex = 0; 308 | on_List_itemDoubleClicked(ui->List->item(CurrentPlayListIndex)); 309 | ui->List->setCurrentRow(CurrentPlayListIndex); 310 | } 311 | else 312 | { 313 | CurrentPlayListIndex++; 314 | on_List_itemDoubleClicked(ui->List->item(CurrentPlayListIndex)); 315 | ui->List->setCurrentRow(CurrentPlayListIndex); 316 | } 317 | } 318 | void Playlist::OnRandomPlay() 319 | { 320 | int listCount = ui->List->count(); 321 | if (listCount == 0) 322 | { 323 | return; 324 | } 325 | int newIndex; 326 | if (listCount == 1) 327 | { 328 | newIndex = 0; 329 | } 330 | else 331 | { 332 | do 333 | { 334 | newIndex = QRandomGenerator::global()->bounded(listCount); 335 | } while (newIndex == CurrentPlayListIndex); 336 | } 337 | CurrentPlayListIndex = newIndex; 338 | on_List_itemDoubleClicked(ui->List->item(CurrentPlayListIndex)); 339 | ui->List->setCurrentRow(CurrentPlayListIndex); 340 | } 341 | 342 | void Playlist::dropEvent(QDropEvent *event) 343 | { 344 | QList urls = event->mimeData()->urls(); 345 | if (urls.isEmpty()) 346 | { 347 | return; 348 | } 349 | 350 | for (QUrl url : urls) 351 | { 352 | QString strFileName = url.toLocalFile(); 353 | 354 | OnAddFile(strFileName); 355 | } 356 | } 357 | 358 | void Playlist::dragEnterEvent(QDragEnterEvent *event) 359 | { 360 | event->acceptProposedAction(); 361 | } 362 | -------------------------------------------------------------------------------- /ctrlbar.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | CtrlBar 4 | 5 | 6 | 7 | 0 8 | 0 9 | 594 10 | 60 11 | 12 | 13 | 14 | 15 | 16777215 16 | 60 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 0 37 | 38 | 39 | 40 | 41 | 0 42 | 43 | 44 | 45 | 46 | 47 | 30 48 | 30 49 | 50 | 51 | 52 | 53 | 30 54 | 30 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 70 67 | 16777215 68 | 69 | 70 | 71 | false 72 | 73 | 74 | false 75 | 76 | 77 | Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter 78 | 79 | 80 | true 81 | 82 | 83 | QAbstractSpinBox::ButtonSymbols::NoButtons 84 | 85 | 86 | true 87 | 88 | 89 | HH:mm:ss 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 30 98 | 30 99 | 100 | 101 | 102 | 103 | 100 104 | 30 105 | 106 | 107 | 108 | 109 | 楷体 110 | 12 111 | true 112 | 113 | 114 | 115 | 倍速 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 30 124 | 30 125 | 126 | 127 | 128 | 129 | 30 130 | 30 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 30 143 | 30 144 | 145 | 146 | 147 | 148 | 30 149 | 30 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 30 162 | 30 163 | 164 | 165 | 166 | 167 | 30 168 | 30 169 | 170 | 171 | 172 | 173 | 20 174 | 175 | 176 | 177 | 1 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 30 186 | 30 187 | 188 | 189 | 190 | 191 | 100 192 | 30 193 | 194 | 195 | 196 | 197 | 楷体 198 | 12 199 | true 200 | 201 | 202 | 203 | 自动 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 70 212 | 16777215 213 | 214 | 215 | 216 | false 217 | 218 | 219 | Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter 220 | 221 | 222 | true 223 | 224 | 225 | QAbstractSpinBox::ButtonSymbols::NoButtons 226 | 227 | 228 | HH:mm:ss 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 8 237 | 16777215 238 | 239 | 240 | 241 | / 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 30 250 | 30 251 | 252 | 253 | 254 | 255 | 30 256 | 30 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 30 269 | 30 270 | 271 | 272 | 273 | 274 | 30 275 | 30 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 30 288 | 30 289 | 290 | 291 | 292 | 293 | 30 294 | 30 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 16777215 309 | 25 310 | 311 | 312 | 313 | 314 | 0 315 | 316 | 317 | 3 318 | 319 | 320 | 0 321 | 322 | 323 | 0 324 | 325 | 326 | 0 327 | 328 | 329 | 330 | 331 | 65536 332 | 333 | 334 | Qt::Orientation::Horizontal 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 108 343 | 25 344 | 345 | 346 | 347 | 348 | 0 349 | 350 | 351 | 0 352 | 353 | 354 | 0 355 | 356 | 357 | 0 358 | 359 | 360 | 0 361 | 362 | 363 | 364 | 365 | 366 | 20 367 | 20 368 | 369 | 370 | 371 | 372 | 20 373 | 20 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 80 386 | 25 387 | 388 | 389 | 390 | 391 | 80 392 | 25 393 | 394 | 395 | 396 | Qt::Orientation::Horizontal 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | CustomSlider 411 | QSlider 412 |
CustomSlider.h
413 |
414 |
415 | 416 | 417 |
418 | -------------------------------------------------------------------------------- /ctrlbar.cpp: -------------------------------------------------------------------------------- 1 | // 控制栏界面 2 | // Editor: Liwh 2024/12/20 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include "ctrlbar.h" 13 | #include "ui_ctrlbar.h" 14 | #include "globalhelper.h" 15 | #include 16 | #include 17 | #include "mainwid.h" 18 | 19 | extern bool isRecording; 20 | 21 | const int PLAY_SLIDER_MAX = 3600000; // 例如,最大支持1小时的视频(以毫秒为单位) 22 | const int VOLUME_SLIDER_MAX = 1000; // 体积滑块最大值,代表100% 23 | 24 | CtrlBar::CtrlBar(QWidget *parent) : 25 | QWidget(parent), 26 | ui(new Ui::CtrlBar) 27 | { 28 | ui->setupUi(this); 29 | LastVolumePercent = 1.0; 30 | 31 | ui->PlaySlider->setMinimum(0); 32 | ui->PlaySlider->setMaximum(PLAY_SLIDER_MAX); 33 | ui->PlaySlider->setSingleStep(100); // 每次步进100毫秒 34 | ui->PlaySlider->setPageStep(1000); // 每次页面步进1秒 35 | 36 | ui->VolumeSlider->setMinimum(0); 37 | ui->VolumeSlider->setMaximum(VOLUME_SLIDER_MAX); 38 | ui->VolumeSlider->setSingleStep(50); // 每次步进5% 39 | ui->VolumeSlider->setPageStep(100); // 每次页面步进10% 40 | 41 | 42 | QTimer* textCheckTimer = new QTimer(this); 43 | connect(textCheckTimer, &QTimer::timeout, this, &CtrlBar::checkSpeedBtnTextChanged); 44 | connect(textCheckTimer, &QTimer::timeout, this, &CtrlBar::checkClearBtnTextChanged); 45 | connect(textCheckTimer, &QTimer::timeout, this, &CtrlBar::checkSettingBtnTextChanged); 46 | textCheckTimer->start(100); 47 | 48 | } 49 | void CtrlBar::checkSpeedBtnTextChanged() 50 | { 51 | static QString lastText = ui->speedBtn->text(); 52 | QString currentText = ui->speedBtn->text(); 53 | 54 | if (currentText != lastText) 55 | { 56 | on_speedBtn_changed(); 57 | lastText = currentText; 58 | } 59 | } 60 | void CtrlBar::checkClearBtnTextChanged() 61 | { 62 | static QString lastText = ui->clearBtn->text(); 63 | QString currentText = ui->clearBtn->text(); 64 | 65 | if (currentText != lastText) 66 | { 67 | on_clearBtn_changed(); 68 | lastText = currentText; 69 | } 70 | } 71 | void CtrlBar::checkSettingBtnTextChanged() 72 | { 73 | static QString lastText = ui->SettingBtn->text(); 74 | QString currentText = ui->SettingBtn->text(); 75 | 76 | if (currentText != lastText) 77 | { 78 | on_SettingBtn_clicked(); 79 | lastText = currentText; 80 | } 81 | } 82 | void CtrlBar::ChangeText() 83 | { 84 | ui->clearBtn->setText("自动"); 85 | GlobalHelper::SetIcon(ui->WordBtn, 15, QChar(0xe135)); 86 | VideoCtl::GetInstance()->OnSetWordPlay(false); 87 | } 88 | CtrlBar::~CtrlBar() 89 | { 90 | delete ui; 91 | } 92 | 93 | bool CtrlBar::Init() 94 | { 95 | setStyleSheet(GlobalHelper::GetQssStr("://res/qss/ctrlbar.css")); 96 | 97 | 98 | GlobalHelper::SetIcon(ui->PlayOrPauseBtn, 15, QChar(0xf04b)); 99 | GlobalHelper::SetIcon(ui->WordBtn, 15, QChar(0xe135)); 100 | GlobalHelper::SetIcon(ui->VolumeBtn, 12, QChar(0xf6a8)); 101 | GlobalHelper::SetIcon(ui->PlaylistCtrlBtn, 15, QChar(0xf0ae)); 102 | GlobalHelper::SetIcon(ui->ForwardBtn, 15, QChar(0xf051)); 103 | GlobalHelper::SetIcon(ui->BackwardBtn, 15, QChar(0xf048)); 104 | GlobalHelper::SetIcon(ui->RecordBtn, 15, QChar(0xf8d9)); 105 | GlobalHelper::SetIcon(ui->SettingBtn, 15, QChar(0xf2f9)); 106 | 107 | GlobalHelper::CreateSpeedMenu(ui->speedBtn); 108 | GlobalHelper::CreateClearMenu(ui->clearBtn); 109 | GlobalHelper::CreateSettingMenu(ui->SettingBtn); 110 | 111 | ui->PlaylistCtrlBtn->setToolTip("播放列表"); 112 | ui->SettingBtn->setToolTip("设置播放方式"); 113 | ui->VolumeBtn->setToolTip("静音"); 114 | ui->ForwardBtn->setToolTip("下一个"); 115 | ui->BackwardBtn->setToolTip("上一个"); 116 | ui->WordBtn->setToolTip("字幕"); 117 | ui->PlayOrPauseBtn->setToolTip("播放"); 118 | ui->speedBtn->setToolTip("倍速"); 119 | ui->clearBtn->setToolTip("清晰度"); 120 | ui->RecordBtn->setToolTip("录屏"); 121 | 122 | ConnectSignalSlots(); 123 | 124 | double dPercent = -1.0; 125 | GlobalHelper::GetPlayVolume(dPercent); 126 | if (dPercent != -1.0) 127 | { 128 | emit SigPlayVolume(dPercent); 129 | OnVideopVolume(dPercent); 130 | } 131 | 132 | return true; 133 | 134 | } 135 | 136 | bool CtrlBar::ConnectSignalSlots() 137 | { 138 | QList listRet; 139 | bool bRet; 140 | 141 | connect(ui->PlaylistCtrlBtn, &QPushButton::clicked, this, &CtrlBar::SigShowOrHidePlaylist); 142 | connect(ui->PlaySlider, &CustomSlider::SigCustomSliderValueChanged, this, &CtrlBar::OnPlaySliderValueChanged); 143 | connect(ui->VolumeSlider, &CustomSlider::SigCustomSliderValueChanged, this, &CtrlBar::OnVolumeSliderValueChanged); 144 | connect(ui->BackwardBtn, &QPushButton::clicked, this, &CtrlBar::SigBackwardPlay); 145 | connect(ui->ForwardBtn, &QPushButton::clicked, this, &CtrlBar::SigForwardPlay); 146 | connect(ui->WordBtn, &QPushButton::clicked, this, &CtrlBar::On_WordBtn_clicked); 147 | 148 | //connect(ui->speedBtn, &QPushButton::, this, &CtrlBar::on_speedBtn_changed); 149 | connect(VideoCtl::GetInstance(), &VideoCtl::SigClear, this, &CtrlBar::ChangeText); 150 | connect(ui->RecordBtn, &QPushButton::clicked, this, &CtrlBar::OnRecordBtn); 151 | 152 | return true; 153 | } 154 | 155 | void CtrlBar::OnVideoTotalSeconds(double nSeconds) 156 | { 157 | TotalPlaySeconds = nSeconds; 158 | 159 | // 计算小时、分钟、秒和毫秒 160 | int thh = static_cast(nSeconds) / 3600; 161 | int tmm = (static_cast(nSeconds) % 3600) / 60; 162 | int tss = static_cast(nSeconds) % 60; 163 | int ms = static_cast((nSeconds - static_cast(nSeconds)) * 1000); 164 | 165 | // 创建包含毫秒的 QTime 对象 166 | QTime TotalTime(thh, tmm, tss, ms); 167 | ui->VideoTotalTimeTimeEdit->setTime(TotalTime); 168 | 169 | // 设置滑块的最大值为总毫秒数,确保不会超过预设的最大值 170 | int totalMillis = static_cast(nSeconds * 1000); 171 | totalMillis = qMin(totalMillis, PLAY_SLIDER_MAX); 172 | ui->PlaySlider->setMaximum(totalMillis); 173 | } 174 | 175 | 176 | 177 | void CtrlBar::OnVideoPlaySeconds(double currentSeconds) 178 | { 179 | currentSeconds = std::min(currentSeconds, TotalPlaySeconds); 180 | 181 | // 计算小时、分钟、秒和毫秒 182 | int thh = static_cast(currentSeconds) / 3600; 183 | int tmm = (static_cast(currentSeconds) % 3600) / 60; 184 | int tss = static_cast(currentSeconds) % 60; 185 | int ms = static_cast((currentSeconds - static_cast(currentSeconds)) * 1000); 186 | 187 | // 创建包含毫秒的 QTime 对象 188 | QTime CurrentTime(thh, tmm, tss, ms); 189 | ui->VideoPlayTimeTimeEdit->setTime(CurrentTime); 190 | 191 | // 将当前播放时间转换为毫秒,并限制在滑块的范围内 192 | int currentMillis = static_cast(currentSeconds * 1000); 193 | currentMillis = qMin(currentMillis, ui->PlaySlider->maximum()); 194 | 195 | // 设置滑块值 196 | ui->PlaySlider->setValue(currentMillis); 197 | } 198 | 199 | void CtrlBar::OnVideopVolume(double dPercent) 200 | { 201 | dPercent = qBound(0.0, dPercent, 1.0); 202 | 203 | int volumeValue = static_cast(dPercent * VOLUME_SLIDER_MAX); 204 | ui->VolumeSlider->setValue(volumeValue); 205 | LastVolumePercent = dPercent; 206 | qDebug() << "LastVolumePercent:" << dPercent << '\n'; 207 | 208 | if (LastVolumePercent == 0) 209 | { 210 | GlobalHelper::SetIcon(ui->VolumeBtn, 12, QChar(0xf2e2)); // 静音图标 211 | ui->VolumeBtn->setToolTip("取消静音"); 212 | } 213 | else if (LastVolumePercent == 1) 214 | { 215 | GlobalHelper::SetIcon(ui->VolumeBtn, 12, QChar(0xf028)); // 最大音量图标 216 | ui->VolumeBtn->setToolTip("静音"); 217 | } 218 | else 219 | { 220 | GlobalHelper::SetIcon(ui->VolumeBtn, 12, QChar(0xf6a8)); // 中等音量图标 221 | ui->VolumeBtn->setToolTip("静音"); 222 | } 223 | 224 | GlobalHelper::SavePlayVolume(dPercent); 225 | } 226 | 227 | void CtrlBar::OnPauseStat(bool bPaused) 228 | { 229 | qDebug() << "CtrlBar::OnPauseStat" << bPaused; 230 | if (bPaused) 231 | { 232 | GlobalHelper::SetIcon(ui->PlayOrPauseBtn, 15, QChar(0xf04b)); 233 | ui->PlayOrPauseBtn->setToolTip("播放"); 234 | } 235 | else 236 | { 237 | GlobalHelper::SetIcon(ui->PlayOrPauseBtn, 15, QChar(0xf04c)); 238 | ui->PlayOrPauseBtn->setToolTip("暂停"); 239 | } 240 | } 241 | 242 | void CtrlBar::OnStopFinished() 243 | { 244 | ui->PlaySlider->setValue(0); 245 | QTime StopTime(0, 0, 0, 0); 246 | ui->VideoTotalTimeTimeEdit->setTime(StopTime); 247 | ui->VideoPlayTimeTimeEdit->setTime(StopTime); 248 | GlobalHelper::SetIcon(ui->PlayOrPauseBtn, 15, QChar(0xf04b)); // 播放图标 249 | ui->PlayOrPauseBtn->setToolTip("播放"); 250 | } 251 | 252 | void CtrlBar::OnSpeed(float speed) 253 | { 254 | ui->speedBtn->setText(QString("倍速:").arg(speed)); 255 | } 256 | void CtrlBar::OnChangeVideo(QString s) 257 | { 258 | if (s == "循环播放") 259 | { 260 | GlobalHelper::SetIcon(ui->SettingBtn, 15, QChar(0xf2f9)); 261 | } 262 | else if (s == "列表播放") 263 | { 264 | GlobalHelper::SetIcon(ui->SettingBtn, 15, QChar(0xf883)); 265 | } 266 | else 267 | { 268 | GlobalHelper::SetIcon(ui->SettingBtn, 15, QChar(0xf074)); 269 | } 270 | } 271 | void CtrlBar::OnPlaySliderValueChanged() 272 | { 273 | 274 | int currentMillis = ui->PlaySlider->value(); 275 | 276 | double targetSeconds = static_cast(currentMillis) / 1000.0; 277 | 278 | emit SigPlaySeek(targetSeconds/TotalPlaySeconds); 279 | qDebug() << "Seeking to:" << targetSeconds << "seconds"; 280 | } 281 | 282 | void CtrlBar::OnVolumeSliderValueChanged() 283 | { 284 | int volumeValue = ui->VolumeSlider->value(); 285 | 286 | double dPercent = static_cast(volumeValue) / VOLUME_SLIDER_MAX; 287 | 288 | emit SigPlayVolume(dPercent); 289 | 290 | OnVideopVolume(dPercent); 291 | } 292 | 293 | void CtrlBar::on_PlayOrPauseBtn_clicked() 294 | { 295 | emit SigPlayOrPause(); 296 | } 297 | 298 | void CtrlBar::on_VolumeBtn_clicked() 299 | { 300 | if (ui->VolumeBtn->text() == QChar(0xf6a8)|| ui->VolumeBtn->text() == QChar(0xf028)) 301 | { 302 | GlobalHelper::SetIcon(ui->VolumeBtn, 12, QChar(0xf2e2)); 303 | ui->VolumeSlider->setValue(0); 304 | emit SigPlayVolume(0); 305 | } 306 | else 307 | { 308 | GlobalHelper::SetIcon(ui->VolumeBtn, 12, QChar(0xf028)); 309 | ui->VolumeSlider->setValue(LastVolumePercent * MAX_SLIDER_VALUE); 310 | emit SigPlayVolume(LastVolumePercent); 311 | } 312 | 313 | } 314 | void CtrlBar::On_WordBtn_clicked() 315 | { 316 | if (ui->WordBtn->text() == QChar(0xe135)) 317 | { 318 | VideoCtl::GetInstance()->OnSetWordPlay(true); 319 | int flag = VideoCtl::GetInstance()->GetHasWord(); 320 | if (flag) 321 | { 322 | GlobalHelper::SetIcon(ui->WordBtn, 15, QChar(0xf20a)); 323 | } 324 | else 325 | { 326 | QString subtitleFilePath = QFileDialog::getOpenFileName(this, "选择字幕文件", "", "字幕文件 (*.srt *.ass *.sub *.ssa)"); 327 | if (!subtitleFilePath.isEmpty()) 328 | { 329 | emit SigLoadSubtitle(subtitleFilePath); 330 | //GlobalHelper::SetIcon(ui->WordBtn, 15, QChar(0xf20a)); 331 | } 332 | } 333 | } 334 | else 335 | { 336 | VideoCtl::GetInstance()->OnSetWordPlay(false); 337 | emit SigHideSubtitle(); 338 | GlobalHelper::SetIcon(ui->WordBtn, 15, QChar(0xe135)); 339 | } 340 | } 341 | void CtrlBar::OnRecordBtn() 342 | { 343 | if (!isRecording) 344 | { 345 | VideoCtl::GetInstance()->LoadingSc(); 346 | qDebug() << "正在录屏中" << '\n'; 347 | isRecording = true; 348 | } 349 | else 350 | { 351 | VideoCtl::GetInstance()->LoadingSc(); 352 | qDebug() << "录屏完成" << '\n'; 353 | isRecording = false; 354 | } 355 | } 356 | void CtrlBar::on_SettingBtn_clicked() 357 | { 358 | if (ui->SettingBtn->text() == QChar(0xf2f9)) 359 | { 360 | GlobalHelper::SetIcon(ui->SettingBtn, 15, QChar(0xf2f9)); 361 | VideoCtl::GetInstance()->OnSetLoop(1); 362 | } 363 | else if (ui->SettingBtn->text() == QChar(0xf883)) 364 | { 365 | GlobalHelper::SetIcon(ui->SettingBtn, 15, QChar(0xf883)); 366 | VideoCtl::GetInstance()->OnSetLoop(2); 367 | } 368 | else 369 | { 370 | VideoCtl::GetInstance()->OnSetLoop(3); 371 | GlobalHelper::SetIcon(ui->SettingBtn, 15, QChar(0xf074)); 372 | } 373 | } 374 | void CtrlBar::on_speedBtn_changed() 375 | { 376 | if (ui->speedBtn->text() == "倍速") 377 | { 378 | emit SigSpeed(1); 379 | } 380 | else if (ui->speedBtn->text() == "0.75x") 381 | { 382 | emit SigSpeed(0.75); 383 | } 384 | else if (ui->speedBtn->text() == "1.25x") 385 | { 386 | emit SigSpeed(1.25); 387 | } 388 | else if (ui->speedBtn->text() == "1.5x") 389 | { 390 | emit SigSpeed(1.5); 391 | } 392 | else if(ui->speedBtn->text()=="0.5x") 393 | { 394 | emit SigSpeed(0.5); 395 | } 396 | else emit SigSpeed(2); 397 | 398 | } 399 | void CtrlBar::on_clearBtn_changed() 400 | { 401 | if (ui->clearBtn->text() == "自动") 402 | { 403 | qDebug() << "选择了自动" << '\n'; 404 | VideoCtl::GetInstance()->ChangeResolution("自动"); 405 | } 406 | else if (ui->clearBtn->text() == "360P 流畅") 407 | { 408 | qDebug() << "选择了360P" << '\n'; 409 | VideoCtl::GetInstance()->ChangeResolution("360p"); 410 | } 411 | else if (ui->clearBtn->text() == "480P 清晰") 412 | { 413 | qDebug() << "选择了480P" << '\n'; 414 | VideoCtl::GetInstance()->ChangeResolution("480p"); 415 | } 416 | else if (ui->clearBtn->text() == "720P 高清") 417 | { 418 | qDebug() << "选择了720P" << '\n'; 419 | VideoCtl::GetInstance()->ChangeResolution("720p"); 420 | } 421 | else if (ui->clearBtn->text() == "1080P 高清") 422 | { 423 | qDebug() << "选择了1080P" << '\n'; 424 | VideoCtl::GetInstance()->ChangeResolution("1080p"); 425 | } 426 | else 427 | { 428 | qDebug() << "选择了1080P 60帧" << '\n'; 429 | VideoCtl::GetInstance()->ChangeResolution("1080p_60"); 430 | } 431 | } -------------------------------------------------------------------------------- /title.cpp: -------------------------------------------------------------------------------- 1 | // 标题界面:显示名称和视频名 2 | // Editor: Liwh 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | 36 | 37 | #include "title.h" 38 | #include "ui_title.h" 39 | #include "globalhelper.h" 40 | #include 41 | 42 | #pragma execution_character_set("utf-8") 43 | 44 | 45 | void parseAvailableQualities(const QJsonObject& data) 46 | { 47 | if (data.contains("accept_quality") && data.contains("accept_description")) 48 | { 49 | QJsonArray acceptQuality = data["accept_quality"].toArray(); 50 | QJsonArray acceptDescription = data["accept_description"].toArray(); 51 | 52 | qDebug() << "Available Qualities:"; 53 | for (int i = 0; i < acceptQuality.size(); ++i) { 54 | qDebug() << acceptQuality[i].toInt() << "-" << acceptDescription[i].toString(); 55 | } 56 | } 57 | } 58 | 59 | QString extractBV(const QString& url) 60 | { 61 | QRegularExpression bvRegex("BV[\\w\\d]{10}"); 62 | QRegularExpressionMatch match = bvRegex.match(url); 63 | if (match.hasMatch()) { 64 | return match.captured(0); 65 | } 66 | return QString(); 67 | } 68 | 69 | QString getVideoCID(const QString& bv) 70 | { 71 | QString apiUrl = QString("https://api.bilibili.com/x/web-interface/view?bvid=%1").arg(bv); 72 | QUrl url(apiUrl); 73 | 74 | QNetworkAccessManager manager; 75 | QNetworkRequest request(url); 76 | 77 | // 添加必要的头信息 78 | request.setRawHeader("Referer", "https://www.bilibili.com/"); 79 | request.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36"); 80 | request.setRawHeader("Cookie", "buvid4=A9ABD297-5E42-890F-CB6F-0B21BE41617071464-023021210-GRYOe0wAEu5lob3K%2BtPmgw%3D%3D; buvid_fp_plain=undefined; DedeUserID=405828899; DedeUserID__ckMd5=b1d4a52913d5ed67; is-2022-channel=1; enable_web_push=DISABLE; header_theme_version=CLOSE; buvid3=2DCAED86-E007-E95C-F23D-3764EF27E56493798infoc; b_nut=1707703593; _uuid=2106797D4-276B-2652-B51E-35F5ACD3416B29018infoc; hit-dyn-v2=1; FEED_LIVE_VERSION=V_WATCHLATER_PIP_WINDOW2; rpdid=|(m)~uJ|Jlm0J'u~u|~JlR)k; LIVE_BUVID=AUTO7017112857089501; b-user-id=22eeb5df-91d4-8850-1b5e-6ccfc35d49c1; buvid_fp=fac1143bfd37ac4898dbaaf242b17e91; CURRENT_QUALITY=116; fingerprint=9172e218046df6d5df4a9a6fc7962abd; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzUzODQ1NzUsImlhdCI6MTczNTEyNTMxNSwicGx0IjotMX0.ePRbQrlU91bUwfuspT4ZtZo36EvTehSwz4COrTs2EcI; bili_ticket_expires=1735384515; blackside_state=0; CURRENT_BLACKGAP=0; PVID=2; home_feed_column=4; bmg_af_switch=1; bmg_src_def_domain=i0.hdslb.com; browser_resolution=1245-714; bp_t_offset_405828899=1015303678631870464; b_lsid=7A457B10A_1940458A260; SESSDATA=0864b01a%2C1750792849%2Cc6feb%2Ac2CjDVf6T9W6I4xTwu_7RIAMlzv_09oi-bTtLJcAeuu2PgYfdnVYkfSdKLyw6hkirE4Z0SVjg3SG5mdmZOeC1uU2gxNFJxZi1oYUJfQ21TeUw4TmQ0RVRzRmJiRy0tOGpFeFhyQTV5N0ZIX3BTTFMzcDEweVQyYm1HcEJuVG04bENhVHVrT0xzVGd3IIEC; bili_jct=1162fc29620c29b16a63d1d4c0314a96; bsource=search_baidu; sid=8em2tvpc; CURRENT_FNVAL=4048"); 81 | 82 | 83 | QNetworkReply* reply = manager.get(request); 84 | QEventLoop loop; 85 | 86 | QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); 87 | loop.exec(); 88 | 89 | QByteArray response = reply->readAll(); 90 | 91 | // 保存 API 响应到文件 92 | QString filePath = "cid_api_response_debug.json"; 93 | QFile file(filePath); 94 | if (file.open(QIODevice::WriteOnly)) { 95 | file.write(response); 96 | file.close(); 97 | qDebug() << "Saved API Response to:" << filePath; 98 | } 99 | 100 | reply->deleteLater(); 101 | 102 | QJsonDocument jsonDoc = QJsonDocument::fromJson(response); 103 | if (!jsonDoc.isObject()) { 104 | qDebug() << "Failed to parse JSON. Response might be invalid!"; 105 | return QString(); 106 | } 107 | 108 | QJsonObject obj = jsonDoc.object(); 109 | if (obj["code"].toInt() == 0) { 110 | QJsonObject data = obj["data"].toObject(); 111 | qDebug() << "Data Object:" << data; 112 | 113 | if (data.contains("pages")) { 114 | QJsonArray pages = data["pages"].toArray(); 115 | qDebug() << "Pages Array:" << pages; 116 | 117 | if (!pages.isEmpty()) { 118 | QJsonObject firstPage = pages[0].toObject(); 119 | qDebug() << "First Page Object:" << firstPage; 120 | 121 | if (firstPage.contains("cid")) { 122 | QVariant cidVariant = firstPage["cid"].toVariant(); 123 | QString cidStr = cidVariant.toString(); 124 | qDebug() << "Extracted CID from QVariant as QString:" << cidStr; 125 | return cidStr; 126 | } 127 | } 128 | } 129 | } 130 | 131 | return QString(); 132 | } 133 | 134 | QString getVideoStreamURL(const QString& bv, const QString& cid) 135 | { 136 | QString apiUrl = QString("https://api.bilibili.com/x/player/playurl?cid=%1&bvid=%2&qn=%3") 137 | .arg(cid) 138 | .arg(bv) 139 | .arg(116); 140 | 141 | QUrl url(apiUrl); 142 | QNetworkAccessManager manager; 143 | QNetworkRequest request(url); 144 | 145 | // 添加头信息 146 | request.setRawHeader("Referer", "https://www.bilibili.com/video/"); 147 | request.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36"); 148 | request.setRawHeader("Cookie", "buvid4=A9ABD297-5E42-890F-CB6F-0B21BE41617071464-023021210-GRYOe0wAEu5lob3K%2BtPmgw%3D%3D; buvid_fp_plain=undefined; DedeUserID=405828899; DedeUserID__ckMd5=b1d4a52913d5ed67; is-2022-channel=1; enable_web_push=DISABLE; header_theme_version=CLOSE; buvid3=2DCAED86-E007-E95C-F23D-3764EF27E56493798infoc; b_nut=1707703593; _uuid=2106797D4-276B-2652-B51E-35F5ACD3416B29018infoc; hit-dyn-v2=1; FEED_LIVE_VERSION=V_WATCHLATER_PIP_WINDOW2; rpdid=|(m)~uJ|Jlm0J'u~u|~JlR)k; LIVE_BUVID=AUTO7017112857089501; b-user-id=22eeb5df-91d4-8850-1b5e-6ccfc35d49c1; buvid_fp=fac1143bfd37ac4898dbaaf242b17e91; CURRENT_QUALITY=116; fingerprint=9172e218046df6d5df4a9a6fc7962abd; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzUzODQ1NzUsImlhdCI6MTczNTEyNTMxNSwicGx0IjotMX0.ePRbQrlU91bUwfuspT4ZtZo36EvTehSwz4COrTs2EcI; bili_ticket_expires=1735384515; blackside_state=0; CURRENT_BLACKGAP=0; PVID=2; home_feed_column=4; bmg_af_switch=1; bmg_src_def_domain=i0.hdslb.com; browser_resolution=1245-714; bp_t_offset_405828899=1015303678631870464; b_lsid=7A457B10A_1940458A260; SESSDATA=0864b01a%2C1750792849%2Cc6feb%2Ac2CjDVf6T9W6I4xTwu_7RIAMlzv_09oi-bTtLJcAeuu2PgYfdnVYkfSdKLyw6hkirE4Z0SVjg3SG5mdmZOeC1uU2gxNFJxZi1oYUJfQ21TeUw4TmQ0RVRzRmJiRy0tOGpFeFhyQTV5N0ZIX3BTTFMzcDEweVQyYm1HcEJuVG04bENhVHVrT0xzVGd3IIEC; bili_jct=1162fc29620c29b16a63d1d4c0314a96; bsource=search_baidu; sid=8em2tvpc; CURRENT_FNVAL=4048"); 149 | 150 | QNetworkReply* reply = manager.get(request); 151 | QEventLoop loop; 152 | 153 | QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); 154 | loop.exec(); 155 | 156 | if (reply->error() != QNetworkReply::NoError) { 157 | qDebug() << "Network error:" << reply->errorString(); 158 | reply->deleteLater(); 159 | return QString(); 160 | } 161 | 162 | QByteArray response = reply->readAll(); 163 | qDebug() << "API Response:" << response; // 输出 JSON 数据 164 | 165 | reply->deleteLater(); 166 | 167 | QJsonDocument jsonDoc = QJsonDocument::fromJson(response); 168 | if (jsonDoc.isObject()) { 169 | QJsonObject obj = jsonDoc.object(); 170 | if (obj["code"].toInt() == 0) { 171 | QJsonObject data = obj["data"].toObject(); 172 | QJsonArray durlArray = data["durl"].toArray(); 173 | parseAvailableQualities(data); 174 | if (!durlArray.isEmpty()) { 175 | QJsonObject firstDurl = durlArray[0].toObject(); 176 | return firstDurl["url"].toString(); // 返回视频流 URL 177 | } 178 | } 179 | else { 180 | qDebug() << "API Error:" << obj["message"].toString(); 181 | } 182 | } 183 | 184 | return QString(); 185 | } 186 | 187 | QString downloadVideo(const QString& url, const QString& outputFile, const QString& bv) 188 | { 189 | QUrl qurl(url); 190 | QNetworkAccessManager manager; 191 | QNetworkRequest request(qurl); 192 | 193 | QString refererUrl = QString("https://www.bilibili.com/video/%1").arg(bv); 194 | request.setRawHeader("Referer", refererUrl.toUtf8()); 195 | request.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36"); 196 | request.setRawHeader("Cookie", "buvid4=A9ABD297-5E42-890F-CB6F-0B21BE41617071464-023021210-GRYOe0wAEu5lob3K%2BtPmgw%3D%3D; buvid_fp_plain=undefined; DedeUserID=405828899; DedeUserID__ckMd5=b1d4a52913d5ed67; is-2022-channel=1; enable_web_push=DISABLE; header_theme_version=CLOSE; buvid3=2DCAED86-E007-E95C-F23D-3764EF27E56493798infoc; b_nut=1707703593; _uuid=2106797D4-276B-2652-B51E-35F5ACD3416B29018infoc; hit-dyn-v2=1; FEED_LIVE_VERSION=V_WATCHLATER_PIP_WINDOW2; rpdid=|(m)~uJ|Jlm0J'u~u|~JlR)k; LIVE_BUVID=AUTO7017112857089501; b-user-id=22eeb5df-91d4-8850-1b5e-6ccfc35d49c1; buvid_fp=fac1143bfd37ac4898dbaaf242b17e91; CURRENT_QUALITY=116; fingerprint=9172e218046df6d5df4a9a6fc7962abd; bili_ticket=eyJhbGciOiJIUzI1NiIsImtpZCI6InMwMyIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzUzODQ1NzUsImlhdCI6MTczNTEyNTMxNSwicGx0IjotMX0.ePRbQrlU91bUwfuspT4ZtZo36EvTehSwz4COrTs2EcI; bili_ticket_expires=1735384515; blackside_state=0; CURRENT_BLACKGAP=0; PVID=2; home_feed_column=4; bmg_af_switch=1; bmg_src_def_domain=i0.hdslb.com; browser_resolution=1245-714; bp_t_offset_405828899=1015303678631870464; b_lsid=7A457B10A_1940458A260; SESSDATA=0864b01a%2C1750792849%2Cc6feb%2Ac2CjDVf6T9W6I4xTwu_7RIAMlzv_09oi-bTtLJcAeuu2PgYfdnVYkfSdKLyw6hkirE4Z0SVjg3SG5mdmZOeC1uU2gxNFJxZi1oYUJfQ21TeUw4TmQ0RVRzRmJiRy0tOGpFeFhyQTV5N0ZIX3BTTFMzcDEweVQyYm1HcEJuVG04bENhVHVrT0xzVGd3IIEC; bili_jct=1162fc29620c29b16a63d1d4c0314a96; bsource=search_baidu; sid=8em2tvpc; CURRENT_FNVAL=4048"); 197 | 198 | QNetworkReply* reply = manager.get(request); 199 | QEventLoop loop; 200 | 201 | QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); 202 | loop.exec(); 203 | 204 | if (reply->error() != QNetworkReply::NoError) { 205 | qDebug() << "下载失败:" << reply->errorString(); 206 | reply->deleteLater(); 207 | return QString(); 208 | } 209 | 210 | QFile file(outputFile); 211 | if (!file.open(QIODevice::WriteOnly)) { 212 | qDebug() << "无法保存文件:" << outputFile; 213 | reply->deleteLater(); 214 | return QString(); 215 | } 216 | 217 | file.write(reply->readAll()); 218 | file.close(); 219 | reply->deleteLater(); 220 | 221 | return outputFile; 222 | } 223 | 224 | QString downloadAndSaveBilibiliVideo(const QString& url) 225 | { 226 | QString bv = extractBV(url); 227 | if (bv.isEmpty()) { 228 | qDebug() << "无法提取 BV 号!"; 229 | return QString(); 230 | } 231 | qDebug() << "BV:" << bv << '\n'; 232 | QString cid = getVideoCID(bv); 233 | if (cid.isEmpty()) { 234 | qDebug() << "无法获取 CID!"; 235 | return QString(); 236 | } 237 | qDebug() << "CID:" << cid << '\n'; 238 | QString streamURL = getVideoStreamURL(bv, cid); 239 | if (streamURL.isEmpty()) { 240 | qDebug() << "无法获取视频流 URL!"; 241 | return QString(); 242 | } 243 | 244 | QString outputFile = bv + ".mp4"; 245 | return downloadVideo(streamURL, outputFile,bv); 246 | } 247 | void Title::OnMenuBtnClicked() 248 | { 249 | bool ok; 250 | 251 | // 创建输入框 252 | QInputDialog inputDialog(this); 253 | inputDialog.setWindowTitle("输入 Bilibili 视频 URL"); 254 | inputDialog.setLabelText("请输入 Bilibili 视频 URL:"); 255 | inputDialog.setFixedSize(400, 200); // 设置窗口大小 256 | 257 | // 设置样式 258 | inputDialog.setStyleSheet( 259 | "QInputDialog {" 260 | " background-color: #2C2C2C;" // 背景色 261 | " color: #FFFFFF;" // 字体颜色 262 | " border-radius: 10px;" // 圆角 263 | "}" 264 | "QLabel {" 265 | " font-size: 18px;" 266 | " color: #FFFFFF;" 267 | "}" 268 | "QLineEdit {" 269 | " font-size: 16px;" 270 | " color: #000000;" 271 | " background-color: #FFFFFF;" 272 | " border: 1px solid #4C9AFF;" 273 | " border-radius: 5px;" 274 | " padding: 5px;" 275 | "}" 276 | "QPushButton {" 277 | " font-size: 16px;" 278 | " color: #FFFFFF;" 279 | " background-color: #4C9AFF;" 280 | " border: none;" 281 | " border-radius: 5px;" 282 | " padding: 5px 15px;" 283 | "}" 284 | "QPushButton:hover {" 285 | " background-color: #3A8ED6;" 286 | "}" 287 | "QPushButton:pressed {" 288 | " background-color: #2E76B4;" 289 | "}" 290 | ); 291 | 292 | // 居中显示输入框 293 | QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); 294 | inputDialog.move(screenGeometry.center() - inputDialog.rect().center()); 295 | 296 | // 获取用户输入 297 | if (inputDialog.exec() != QDialog::Accepted) 298 | { 299 | return; // 用户取消或关闭输入框 300 | } 301 | QString url = inputDialog.textValue().trimmed(); // 获取用户输入并去掉首尾空格 302 | 303 | // 验证 URL 格式 304 | if (url.isEmpty() || !url.startsWith("https://www.bilibili.com/video/")) 305 | { 306 | QMessageBox::warning(this, "错误", "请输入有效的 Bilibili 视频 URL!"); 307 | return; 308 | } 309 | 310 | // 下载进度提示 311 | QProgressDialog progressDialog("正在下载视频,请稍候...", "取消", 0, 100, this); 312 | progressDialog.setWindowModality(Qt::WindowModal); 313 | progressDialog.setMinimumDuration(0); 314 | progressDialog.setValue(0); // 设置初始进度 315 | progressDialog.setCancelButton(nullptr); // 移除取消按钮 316 | progressDialog.setStyleSheet( 317 | "QProgressDialog {" 318 | " background-color: #2C2C2C;" 319 | " color: #FFFFFF;" 320 | " border-radius: 10px;" 321 | "}" 322 | "QLabel {" 323 | " font-size: 18px;" 324 | " color: #FFFFFF;" 325 | "}" 326 | "QProgressBar {" 327 | " border: 1px solid #4C9AFF;" 328 | " border-radius: 5px;" 329 | " background: #1A1A1A;" 330 | " text-align: center;" 331 | " color: #FFFFFF;" 332 | "}" 333 | "QProgressBar::chunk {" 334 | " background-color: #4C9AFF;" 335 | " width: 20px;" 336 | "}" 337 | ); 338 | progressDialog.show(); 339 | 340 | QTimer timer; 341 | int progressValue = 0; 342 | 343 | connect(&timer, &QTimer::timeout, [&]() 344 | { 345 | if (progressValue < 95) { 346 | progressValue += 1; // 缓慢增加进度值,最大值设置为 95 347 | progressDialog.setValue(progressValue); 348 | } 349 | }); 350 | 351 | timer.start(200); 352 | 353 | QString localFile = downloadAndSaveBilibiliVideo(url); 354 | 355 | if (localFile.isEmpty()) 356 | { 357 | progressDialog.close(); 358 | QMessageBox::warning(this, "错误", "无法下载视频!"); 359 | return; 360 | } 361 | 362 | progressDialog.setValue(100); 363 | progressDialog.close(); 364 | 365 | emit SigOpenFile(localFile); 366 | 367 | } 368 | 369 | Title::Title(QWidget *parent) : 370 | QWidget(parent), 371 | ui(new Ui::Title), 372 | ActionGroup(this), 373 | Menu(this) 374 | { 375 | ui->setupUi(this); 376 | 377 | connect(ui->CloseBtn, &QPushButton::clicked, this, &Title::SigCloseBtnClicked); 378 | connect(ui->MinBtn, &QPushButton::clicked, this, &Title::SigMinBtnClicked); 379 | connect(ui->MaxBtn, &QPushButton::clicked, this, &Title::SigMaxBtnClicked); 380 | connect(ui->FullScreenBtn, &QPushButton::clicked, this, &Title::SigFullScreenBtnClicked); 381 | connect(ui->MenuBtn, &QPushButton::clicked, this, &Title::OnMenuBtnClicked); 382 | 383 | Menu.addAction("最大化", this, &Title::SigMaxBtnClicked); 384 | Menu.addAction("最小化", this, &Title::SigMinBtnClicked); 385 | Menu.addAction("退出", this, &Title::SigCloseBtnClicked); 386 | 387 | QMenu* stMenu = Menu.addMenu("打开"); 388 | stMenu->addAction("打开文件", this, &Title::OpenFile); 389 | 390 | ui->MenuBtn->setToolTip("Bilibili视频爬取"); 391 | ui->MinBtn->setToolTip("最小化"); 392 | ui->MaxBtn->setToolTip("最大化"); 393 | ui->CloseBtn->setToolTip("关闭"); 394 | ui->FullScreenBtn->setToolTip("全屏"); 395 | } 396 | 397 | Title::~Title() 398 | { 399 | delete ui; 400 | } 401 | 402 | bool Title::Init() 403 | { 404 | if (InitUi() == false) 405 | { 406 | return false; 407 | } 408 | 409 | return true; 410 | } 411 | 412 | bool Title::InitUi() 413 | { 414 | ui->MovieNameLab->clear(); 415 | 416 | setAttribute(Qt::WA_TranslucentBackground); 417 | 418 | setStyleSheet(GlobalHelper::GetQssStr("://res/qss/title.css")); 419 | 420 | GlobalHelper::SetIcon(ui->MaxBtn, 15, QChar(0xf2d0)); 421 | GlobalHelper::SetIcon(ui->MinBtn, 15, QChar(0xf068)); 422 | GlobalHelper::SetIcon(ui->CloseBtn, 15, QChar(0xf00d)); 423 | GlobalHelper::SetIcon(ui->FullScreenBtn, 15, QChar(0xf065)); 424 | 425 | //loadSvgFromUrl("http://www.w3.org/2000/svg", ui->FullScreenBtn); 426 | 427 | 428 | return true; 429 | } 430 | 431 | void Title::OpenFile() 432 | { 433 | QString strFileName = QFileDialog::getOpenFileName(this, "打开文件", QDir::homePath(), 434 | "视频文件(*.mkv *.rmvb *.mp4 *.avi *.flv *.wmv *.3gp *.mp3)"); 435 | 436 | emit SigOpenFile(strFileName); 437 | } 438 | 439 | void Title::paintEvent(QPaintEvent *event) 440 | { 441 | Q_UNUSED(event); 442 | } 443 | 444 | void Title::mouseDoubleClickEvent(QMouseEvent *event) 445 | { 446 | if(event->button() == Qt::LeftButton) 447 | { 448 | emit SigDoubleClicked(); 449 | } 450 | } 451 | 452 | void Title::resizeEvent(QResizeEvent *event) 453 | { 454 | 455 | } 456 | 457 | void Title::ChangeMovieNameShow() 458 | { 459 | QFontMetrics font_metrics(ui->MovieNameLab->font()); 460 | QRect rect = font_metrics.boundingRect(MovieName); 461 | int font_width = rect.width(); 462 | int show_width = ui->MovieNameLab->width(); 463 | if (font_width > show_width) 464 | { 465 | QString str = font_metrics.elidedText(MovieName, Qt::ElideRight, ui->MovieNameLab->width()); 466 | ui->MovieNameLab->setText(str); 467 | } 468 | else 469 | { 470 | ui->MovieNameLab->setText(MovieName); 471 | } 472 | } 473 | 474 | void Title::OnChangeMaxBtnStyle(bool bIfMax) 475 | { 476 | if (bIfMax) 477 | { 478 | GlobalHelper::SetIcon(ui->MaxBtn, 9, QChar(0xf2d2)); 479 | ui->MaxBtn->setToolTip("还原"); 480 | } 481 | else 482 | { 483 | GlobalHelper::SetIcon(ui->MaxBtn, 9, QChar(0xf2d0)); 484 | ui->MaxBtn->setToolTip("最大化"); 485 | } 486 | } 487 | 488 | void Title::OnPlay(QString strMovieName) 489 | { 490 | qDebug() << "Title::OnPlay"; 491 | QFileInfo fileInfo(strMovieName); 492 | MovieName = fileInfo.fileName(); 493 | ui->MovieNameLab->setText(MovieName); 494 | } 495 | 496 | void Title::OnStopFinished() 497 | { 498 | qDebug() << "Title::OnStopFinished"; 499 | ui->MovieNameLab->clear(); 500 | } 501 | 502 | -------------------------------------------------------------------------------- /mainwid.cpp: -------------------------------------------------------------------------------- 1 | // 主界面 2 | // Editor: Liwh 2024/12/20 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "mainwid.h" 22 | #include "ui_mainwid.h" 23 | #include "globalhelper.h" 24 | #include "videoctl.h" 25 | #include 26 | #include 27 | 28 | bool isRecording; 29 | const int FULLSCREEN_MOUSE_DETECT_TIME = 500; 30 | 31 | MainWid::MainWid(QMainWindow *parent) : 32 | QMainWindow(parent), 33 | ui(new Ui::MainWid), 34 | ShadowWidth(0), 35 | Menu(this), 36 | Playlist(this), 37 | Title(this), 38 | MoveDrag(false), 39 | ActFullscreen(this) 40 | { 41 | isRecording = false; 42 | ui->setupUi(this); 43 | 44 | setWindowFlags(Qt::FramelessWindowHint| Qt::WindowMinimizeButtonHint); 45 | 46 | ShadowWidth = 10; 47 | 48 | 49 | 50 | QString qss = GlobalHelper::GetQssStr("://res/qss/mainwid.css"); 51 | setStyleSheet(qss); 52 | 53 | this->setMouseTracking(true); 54 | 55 | isPlaying = false; 56 | 57 | FullScreenPlay = false; 58 | 59 | CtrlBarAnimationTimer.setInterval(2000); 60 | FullscreenMouseDetectTimer.setInterval(FULLSCREEN_MOUSE_DETECT_TIME); 61 | 62 | 63 | loopActionGroup = new QActionGroup(this); 64 | loopActionGroup->setExclusive(true); 65 | } 66 | MainWid::~MainWid() 67 | { 68 | delete ui; 69 | } 70 | 71 | bool MainWid::Init() 72 | { 73 | QWidget *em = new QWidget(this); 74 | ui->PlaylistWid->setTitleBarWidget(em); 75 | ui->PlaylistWid->setFixedWidth(200); 76 | ui->PlaylistWid->setWidget(&Playlist); 77 | 78 | QWidget *emTitle = new QWidget(this); 79 | ui->TitleWid->setTitleBarWidget(emTitle); 80 | ui->TitleWid->setWidget(&Title); 81 | 82 | if (ConnectSignalSlots() == false) 83 | { 84 | return false; 85 | } 86 | 87 | if (ui->CtrlBarWid->Init() == false || Playlist.Init() == false ||ui->ShowWid->Init() == false || Title.Init() == false) 88 | { 89 | return false; 90 | } 91 | 92 | 93 | CtrlbarAnimationShow = new QPropertyAnimation(ui->CtrlBarWid, "geometry"); 94 | CtrlbarAnimationHide = new QPropertyAnimation(ui->CtrlBarWid, "geometry"); 95 | 96 | 97 | InitMenuActions(); 98 | 99 | 100 | InitMenu(); 101 | return true; 102 | } 103 | void MainWid::InitMenuActions() 104 | { 105 | Map_act["OpenFile"] = &MainWid::OpenFile; 106 | Map_act["OnCloseBtnClicked"] = &MainWid::OnCloseBtnClicked; 107 | Map_act["OnSaveBmp"] = &MainWid::OnSaveBmp; 108 | Map_act["OnFullScreenPlay"] = &MainWid::OnFullScreenPlay; 109 | Map_act["OnSavePeriod"] = &MainWid::OnSavePeriod; 110 | Map_act["OnLoop"] = &MainWid::OnLoop; 111 | Map_act["OnList"] = &MainWid::OnList; 112 | Map_act["OnRandom"] = &MainWid::OnRandom; 113 | } 114 | void MainWid::paintEvent(QPaintEvent *event) 115 | { 116 | Q_UNUSED(event); 117 | } 118 | 119 | 120 | void MainWid::enterEvent(QEvent *event) 121 | { 122 | Q_UNUSED(event); 123 | 124 | } 125 | void MainWid::leaveEvent(QEvent *event) 126 | { 127 | Q_UNUSED(event); 128 | 129 | } 130 | 131 | bool MainWid::ConnectSignalSlots() 132 | { 133 | connect(&Title, &Title::SigCloseBtnClicked, this, &MainWid::OnCloseBtnClicked); 134 | connect(&Title, &Title::SigMaxBtnClicked, this, &MainWid::OnMaxBtnClicked); 135 | connect(&Title, &Title::SigMinBtnClicked, this, &MainWid::OnMinBtnClicked); 136 | connect(&Title, &Title::SigDoubleClicked, this, &MainWid::OnMaxBtnClicked); 137 | connect(&Title, &Title::SigFullScreenBtnClicked, this, &MainWid::OnFullScreenPlay); 138 | connect(&Title, &Title::SigOpenFile, &Playlist, &Playlist::OnAddFileAndPlay); 139 | connect(&Title, &Title::SigShowMenu, this, &MainWid::OnShowMenu); 140 | 141 | 142 | connect(&Playlist, &Playlist::SigPlay, ui->ShowWid, &Show::SigPlay); 143 | 144 | connect(ui->ShowWid, &Show::SigOpenFile, &Playlist, &Playlist::OnAddFileAndPlay); 145 | connect(ui->ShowWid, &Show::SigFullScreen, this, &MainWid::OnFullScreenPlay); 146 | connect(ui->ShowWid, &Show::SigPlayOrPause, VideoCtl::GetInstance(), &VideoCtl::OnPause); 147 | connect(ui->ShowWid, &Show::SigStop, VideoCtl::GetInstance(), &VideoCtl::OnStop); 148 | connect(ui->ShowWid, &Show::SigShowMenu, this, &MainWid::OnShowMenu); 149 | connect(ui->ShowWid, &Show::SigSeekForward, VideoCtl::GetInstance(), &VideoCtl::OnSeekForward); 150 | connect(ui->ShowWid, &Show::SigSeekBack, VideoCtl::GetInstance(), &VideoCtl::OnSeekBack); 151 | connect(ui->ShowWid, &Show::SigAddVolume, VideoCtl::GetInstance(), &VideoCtl::OnAddVolume); 152 | connect(ui->ShowWid, &Show::SigSubVolume, VideoCtl::GetInstance(), &VideoCtl::OnSubVolume); 153 | 154 | 155 | connect(ui->CtrlBarWid, &CtrlBar::SigSpeed, VideoCtl::GetInstance(), &VideoCtl::OnSpeed); 156 | connect(ui->CtrlBarWid, &CtrlBar::SigShowOrHidePlaylist, this, &MainWid::OnShowOrHidePlaylist); 157 | connect(ui->CtrlBarWid, &CtrlBar::SigPlaySeek, VideoCtl::GetInstance(), &VideoCtl::OnPlaySeek); 158 | connect(ui->CtrlBarWid, &CtrlBar::SigPlayVolume, VideoCtl::GetInstance(), &VideoCtl::OnPlayVolume); 159 | connect(ui->CtrlBarWid, &CtrlBar::SigPlayOrPause, VideoCtl::GetInstance(), &VideoCtl::OnPause); 160 | connect(ui->CtrlBarWid, &CtrlBar::SigStop, VideoCtl::GetInstance(), &VideoCtl::OnStop); 161 | connect(ui->CtrlBarWid, &CtrlBar::SigBackwardPlay, &Playlist, &Playlist::OnBackwardPlay); 162 | connect(ui->CtrlBarWid, &CtrlBar::SigForwardPlay, &Playlist, &Playlist::OnForwardPlay); 163 | connect(ui->CtrlBarWid, &CtrlBar::SigShowMenu, this, &MainWid::OnShowMenu); 164 | connect(ui->CtrlBarWid, &CtrlBar::SigShowSetting, this, &MainWid::OnShowSettingWid); 165 | 166 | connect(this, &MainWid::SigShowMax, &Title, &Title::OnChangeMaxBtnStyle); 167 | connect(this, &MainWid::SigSeekForward, VideoCtl::GetInstance(), &VideoCtl::OnSeekForward); 168 | connect(this, &MainWid::SigSeekBack, VideoCtl::GetInstance(), &VideoCtl::OnSeekBack); 169 | connect(this, &MainWid::SigAddVolume, VideoCtl::GetInstance(), &VideoCtl::OnAddVolume); 170 | connect(this, &MainWid::SigSubVolume, VideoCtl::GetInstance(), &VideoCtl::OnSubVolume); 171 | connect(this, &MainWid::SigOpenFile, &Playlist, &Playlist::OnAddFileAndPlay); 172 | connect(this, &MainWid::SigVideoLoop, ui->CtrlBarWid, &CtrlBar::OnChangeVideo); 173 | 174 | connect(VideoCtl::GetInstance(), &VideoCtl::SigVideoTotalSeconds, ui->CtrlBarWid, &CtrlBar::OnVideoTotalSeconds); 175 | connect(VideoCtl::GetInstance(), &VideoCtl::SigVideoPlaySeconds, ui->CtrlBarWid, &CtrlBar::OnVideoPlaySeconds); 176 | connect(VideoCtl::GetInstance(), &VideoCtl::SigVideoVolume, ui->CtrlBarWid, &CtrlBar::OnVideopVolume); 177 | connect(VideoCtl::GetInstance(), &VideoCtl::SigPauseStat, ui->CtrlBarWid, &CtrlBar::OnPauseStat, Qt::QueuedConnection); 178 | connect(VideoCtl::GetInstance(), &VideoCtl::SigStopFinished, ui->ShowWid, &Show::OnStopFinished, Qt::QueuedConnection); 179 | connect(VideoCtl::GetInstance(), &VideoCtl::SigFrameDimensionsChanged, ui->ShowWid, &Show::OnFrameDimensionsChanged, Qt::QueuedConnection); 180 | connect(VideoCtl::GetInstance(), &VideoCtl::SigStopFinished, &Title, &Title::OnStopFinished, Qt::DirectConnection); 181 | connect(VideoCtl::GetInstance(), &VideoCtl::SigStartPlay, &Title, &Title::OnPlay, Qt::DirectConnection); 182 | connect(VideoCtl::GetInstance(), &VideoCtl::SigList, &Playlist, &Playlist::OnForwardPlay); 183 | connect(VideoCtl::GetInstance(), &VideoCtl::SigRandom, &Playlist, &Playlist::OnRandomPlay); 184 | 185 | connect(ui->CtrlBarWid, &CtrlBar::SigLoadSubtitle, VideoCtl::GetInstance(), &VideoCtl::LoadSubtitle); 186 | 187 | connect(&CtrlBarAnimationTimer, &QTimer::timeout, this, &MainWid::OnCtrlBarAnimationTimeOut); 188 | 189 | connect(&FullscreenMouseDetectTimer, &QTimer::timeout, this, &MainWid::OnFullscreenMouseDetectTimeOut); 190 | 191 | 192 | connect(&ActFullscreen, &QAction::triggered, this, &MainWid::OnFullScreenPlay); 193 | 194 | 195 | 196 | return true; 197 | } 198 | 199 | 200 | void MainWid::keyReleaseEvent(QKeyEvent *event) 201 | { 202 | qDebug() << "MainWid::keyPressEvent:" << event->key(); 203 | switch (event->key()) 204 | { 205 | case Qt::Key_F: 206 | OpenFile(); 207 | break; 208 | case Qt::Key_Return: 209 | OnFullScreenPlay(); 210 | break; 211 | case Qt::Key_Left: 212 | emit SigSeekBack(); 213 | break; 214 | case Qt::Key_Right: 215 | qDebug() << "前进5s"; 216 | emit SigSeekForward(); 217 | break; 218 | case Qt::Key_Up: 219 | emit SigAddVolume(); 220 | break; 221 | case Qt::Key_Down: 222 | emit SigSubVolume(); 223 | break; 224 | case Qt::Key_Space: 225 | emit SigPlayOrPause(); 226 | break; 227 | case Qt::Key_S: 228 | OnSaveBmp(); 229 | break; 230 | case Qt::Key_Escape: 231 | OnCloseBtnClicked(); 232 | break; 233 | case Qt::Key_A: 234 | OnSavePeriod(); 235 | break; 236 | case Qt::Key_M: 237 | OnShowMenu(); 238 | break; 239 | default: 240 | break; 241 | } 242 | } 243 | 244 | 245 | void MainWid::mousePressEvent(QMouseEvent *event) 246 | { 247 | if (event->buttons() & Qt::LeftButton) 248 | { 249 | if (ui->TitleWid->geometry().contains(event->pos())) 250 | { 251 | MoveDrag = true; 252 | DragPosition = event->globalPos() - this->pos(); 253 | } 254 | } 255 | 256 | QWidget::mousePressEvent(event); 257 | } 258 | 259 | void MainWid::mouseReleaseEvent(QMouseEvent *event) 260 | { 261 | MoveDrag = false; 262 | 263 | QWidget::mouseReleaseEvent(event); 264 | } 265 | 266 | void MainWid::mouseMoveEvent(QMouseEvent *event) 267 | { 268 | if (MoveDrag) 269 | { 270 | move(event->globalPos() - DragPosition); 271 | } 272 | 273 | QWidget::mouseMoveEvent(event); 274 | } 275 | 276 | void MainWid::contextMenuEvent(QContextMenuEvent* event) 277 | { 278 | Menu.popup(event->globalPos()); 279 | } 280 | 281 | void MainWid::OnFullScreenPlay() 282 | { 283 | if (FullScreenPlay == false) 284 | { 285 | FullScreenPlay = true; 286 | ActFullscreen.setChecked(true); 287 | 288 | ui->ShowWid->setWindowFlags(Qt::Window); 289 | QScreen *pStCurScreen = screen(); 290 | ui->ShowWid->windowHandle()->setScreen(pStCurScreen); 291 | 292 | ui->ShowWid->showFullScreen(); 293 | 294 | QRect stScreenRect = pStCurScreen->geometry(); 295 | int nCtrlBarHeight = ui->CtrlBarWid->height(); 296 | int nX = ui->ShowWid->x(); 297 | CtrlBarAnimationShowRect = QRect(nX, stScreenRect.height() - nCtrlBarHeight, stScreenRect.width(), nCtrlBarHeight); 298 | CtrlBarAnimationHideRect = QRect(nX, stScreenRect.height(), stScreenRect.width(), nCtrlBarHeight); 299 | 300 | CtrlbarAnimationShow->setStartValue(CtrlBarAnimationHideRect); 301 | CtrlbarAnimationShow->setEndValue(CtrlBarAnimationShowRect); 302 | CtrlbarAnimationShow->setDuration(1000); 303 | 304 | CtrlbarAnimationHide->setStartValue(CtrlBarAnimationShowRect); 305 | CtrlbarAnimationHide->setEndValue(CtrlBarAnimationHideRect); 306 | CtrlbarAnimationHide->setDuration(1000); 307 | 308 | ui->CtrlBarWid->setWindowFlags(Qt::FramelessWindowHint | Qt::Window); 309 | ui->CtrlBarWid->windowHandle()->setScreen(pStCurScreen); 310 | ui->CtrlBarWid->raise(); 311 | ui->CtrlBarWid->setWindowOpacity(0.5); 312 | ui->CtrlBarWid->showNormal(); 313 | ui->CtrlBarWid->windowHandle()->setScreen(pStCurScreen); 314 | 315 | CtrlbarAnimationShow->start(); 316 | FullscreenCtrlBarShow = true; 317 | FullscreenMouseDetectTimer.start(); 318 | 319 | this->setFocus(); 320 | } 321 | else 322 | { 323 | FullScreenPlay = false; 324 | ActFullscreen.setChecked(false); 325 | 326 | CtrlbarAnimationShow->stop(); 327 | CtrlbarAnimationHide->stop(); 328 | ui->CtrlBarWid->setWindowOpacity(1); 329 | ui->CtrlBarWid->setWindowFlags(Qt::SubWindow); 330 | 331 | ui->ShowWid->setWindowFlags(Qt::SubWindow); 332 | 333 | ui->CtrlBarWid->showNormal(); 334 | ui->ShowWid->showNormal(); 335 | 336 | FullscreenMouseDetectTimer.stop(); 337 | this->setFocus(); 338 | } 339 | } 340 | 341 | void MainWid::OnCtrlBarAnimationTimeOut() 342 | { 343 | QApplication::setOverrideCursor(Qt::BlankCursor); 344 | } 345 | 346 | void MainWid::OnFullscreenMouseDetectTimeOut() 347 | { 348 | if (FullScreenPlay) 349 | { 350 | if (CtrlBarAnimationShowRect.contains(cursor().pos())) 351 | { 352 | 353 | if (ui->CtrlBarWid->geometry().contains(cursor().pos())) 354 | { 355 | FullscreenCtrlBarShow = true; 356 | } 357 | else 358 | { 359 | ui->CtrlBarWid->raise(); 360 | 361 | CtrlbarAnimationShow->start(); 362 | CtrlbarAnimationHide->stop(); 363 | CtrlBarHideTimer.stop(); 364 | } 365 | } 366 | else 367 | { 368 | if (FullscreenCtrlBarShow) 369 | { 370 | FullscreenCtrlBarShow = false; 371 | CtrlBarHideTimer.singleShot(2000, this, &MainWid::OnCtrlBarHideTimeOut); 372 | } 373 | 374 | } 375 | 376 | } 377 | } 378 | 379 | void MainWid::OnCtrlBarHideTimeOut() 380 | { 381 | if (FullScreenPlay) 382 | { 383 | CtrlbarAnimationHide->start(); 384 | } 385 | } 386 | 387 | void MainWid::OnShowMenu() 388 | { 389 | qDebug() << "MainWid::OnShowMenu triggered at position:" << QCursor::pos(); 390 | QPoint globalPos = QCursor::pos(); 391 | QScreen* currentScreen = QGuiApplication::screenAt(globalPos); 392 | if (currentScreen) 393 | { 394 | QRect screenGeometry = currentScreen->geometry(); 395 | QPoint adjustedPos = globalPos; 396 | if (globalPos.x() + Menu.sizeHint().width() > screenGeometry.right()) 397 | adjustedPos.setX(screenGeometry.right() - Menu.sizeHint().width()); 398 | if (globalPos.y() + Menu.sizeHint().height() > screenGeometry.bottom()) 399 | adjustedPos.setY(screenGeometry.bottom() - Menu.sizeHint().height()); 400 | Menu.popup(adjustedPos); 401 | } 402 | else 403 | { 404 | Menu.popup(globalPos); 405 | } 406 | } 407 | 408 | void MainWid::OnShowAbout() 409 | { 410 | 411 | } 412 | 413 | void MainWid::OpenFile() 414 | { 415 | QString strFileName = QFileDialog::getOpenFileName(this, "打开文件", QDir::homePath(),"音视频文件(*.wav *.ogg *.mp3 *.mkv *.rmvb *.mp4 *.avi *.flv *.wmv *.3gp *.mov *.yuv)"); 416 | emit SigOpenFile(strFileName); 417 | } 418 | void MainWid::OnSaveBmp() 419 | { 420 | QString savePath; 421 | bool success = VideoCtl::GetInstance()->TakeScreenshot(savePath); 422 | if (success) 423 | { 424 | QMessageBox::information(this, "截图成功", QString("截图已保存到:%1").arg(savePath)); 425 | } 426 | else 427 | { 428 | QMessageBox::warning(this, "截图失败", "无法保存截图。"); 429 | } 430 | } 431 | void MainWid::OnShowSettingWid() 432 | { 433 | 434 | } 435 | void MainWid::InitMenu() 436 | { 437 | QString menu_json_file_name = ":/res/menu.json"; 438 | QByteArray ba_json; 439 | QFile json_file(menu_json_file_name); 440 | if (json_file.open(QIODevice::ReadOnly)) 441 | { 442 | ba_json = json_file.readAll(); 443 | json_file.close(); 444 | } 445 | 446 | QJsonDocument json_doc = QJsonDocument::fromJson(ba_json); 447 | 448 | if (json_doc.isObject()) 449 | { 450 | QJsonObject json_obj = json_doc.object(); 451 | MenuJsonParser(json_obj, &Menu); 452 | } 453 | Menu.setStyleSheet("QMenu { background-color: #2C2C2C; color: white; border: 1px solid #5C5C5C; }" 454 | "QMenu::item { padding: 5px 20px; }" 455 | "QMenu::item:selected { background-color: #4C9AFF; }"); 456 | } 457 | void MainWid::MenuJsonParser(QJsonObject& json_obj, QMenu* menu) 458 | { 459 | QJsonObject::iterator it = json_obj.begin(); 460 | QJsonObject::iterator end = json_obj.end(); 461 | while (it != end) 462 | { 463 | QString key = it.key(); 464 | auto value = it.value(); 465 | if (value.isObject()) 466 | { 467 | QMenu* sub_menu = menu->addMenu(key); 468 | QJsonObject obj = value.toObject(); 469 | MenuJsonParser(obj, sub_menu); 470 | } 471 | else 472 | { 473 | QString value_str = value.toString(); 474 | qDebug() << value_str << "\n"; 475 | QStringList value_info = value_str.split("/"); 476 | 477 | if (value_info.size() == 2) 478 | { 479 | QString fun_str = value_info[0]; 480 | QString hot_key = value_info[1]; 481 | if (!hot_key.isEmpty()) 482 | { 483 | key += "\t" + hot_key; 484 | } 485 | QAction* action = menu->addAction(key); 486 | 487 | if (!hot_key.isEmpty()) 488 | { 489 | action->setShortcut(QKeySequence(hot_key)); 490 | action->setShortcutContext(Qt::ApplicationShortcut); 491 | } 492 | 493 | if (Map_act.contains(fun_str)) 494 | { 495 | qDebug() << "设置了菜单" << fun_str << '\n'; 496 | MenuAction pFunc = Map_act.value(fun_str); 497 | connect(action, &QAction::triggered, this, pFunc); 498 | if (fun_str == "OnLoop" || fun_str == "OnList") 499 | { 500 | action->setCheckable(true); 501 | loopActionGroup->addAction(action); 502 | if (fun_str == "OnLoop") 503 | action->setChecked(VideoCtl::GetInstance()->IsLooping()); 504 | else if (fun_str == "OnList") 505 | action->setChecked(!VideoCtl::GetInstance()->IsLooping()); 506 | } 507 | } 508 | else 509 | { 510 | qWarning() << "MenuJsonParser: 未找到对应函数名:" << fun_str; 511 | } 512 | } 513 | else 514 | { 515 | QString fun_str = value_info[0]; 516 | QAction* action = menu->addAction(key); 517 | if (Map_act.contains(fun_str)) 518 | { 519 | qDebug() << "设置了菜单" << fun_str << '\n'; 520 | MenuAction pFunc = Map_act.value(fun_str); 521 | connect(action, &QAction::triggered, this, pFunc); 522 | } 523 | else 524 | { 525 | qWarning() << "MenuJsonParser: 未找到对应函数名:" << fun_str; 526 | } 527 | } 528 | } 529 | 530 | it++; 531 | } 532 | } 533 | 534 | QMenu* MainWid::AddMenuFun(QString menu_title, QMenu* menu) 535 | { 536 | QMenu* menu_t = new QMenu(this); 537 | menu_t->setTitle(menu_title); 538 | menu->addMenu(menu_t); 539 | return menu_t; 540 | } 541 | 542 | void MainWid::AddActionFun(QString action_title, QMenu* menu, void(MainWid::* slot_addr)()) 543 | { 544 | QAction* action = new QAction(this);; 545 | action->setText(action_title); 546 | menu->addAction(action); 547 | connect(action, &QAction::triggered, this, slot_addr); 548 | } 549 | 550 | void MainWid::OnCloseBtnClicked() 551 | { 552 | this->close(); 553 | } 554 | 555 | void MainWid::OnMinBtnClicked() 556 | { 557 | this->showMinimized(); 558 | } 559 | 560 | void MainWid::OnMaxBtnClicked() 561 | { 562 | if (isMaximized()) 563 | { 564 | showNormal(); 565 | emit SigShowMax(false); 566 | } 567 | else 568 | { 569 | showMaximized(); 570 | emit SigShowMax(true); 571 | } 572 | } 573 | 574 | void MainWid::OnShowOrHidePlaylist() 575 | { 576 | if (ui->PlaylistWid->isHidden()) 577 | { 578 | ui->PlaylistWid->show(); 579 | } 580 | else 581 | { 582 | ui->PlaylistWid->hide(); 583 | } 584 | this->repaint(); 585 | } 586 | void MainWid::OnSavePeriod() 587 | { 588 | if (!isRecording) 589 | { 590 | VideoCtl::GetInstance()->LoadingSc(); 591 | qDebug() << "正在录屏中" << '\n'; 592 | isRecording = true; 593 | } 594 | else 595 | { 596 | VideoCtl::GetInstance()->LoadingSc(); 597 | qDebug() << "录屏完成" << '\n'; 598 | isRecording = false; 599 | } 600 | } 601 | 602 | void MainWid::OnLoop() 603 | { 604 | VideoCtl::GetInstance()->OnSetLoop(1); 605 | emit SigVideoLoop("循环播放"); 606 | } 607 | void MainWid::OnList() 608 | { 609 | VideoCtl::GetInstance()->OnSetLoop(2); 610 | emit SigVideoLoop("列表播放"); 611 | } 612 | 613 | void MainWid::OnRandom() 614 | { 615 | VideoCtl::GetInstance()->OnSetLoop(3); 616 | emit SigVideoLoop("随机播放"); 617 | } 618 | -------------------------------------------------------------------------------- /data.h: -------------------------------------------------------------------------------- 1 | // 数据控制 2 | // Editor: Liwh 2024/12/21 3 | 4 | #pragma once 5 | 6 | #ifdef _windows_ 7 | #include 8 | #include 9 | #endif 10 | 11 | #ifdef linux 12 | #include 13 | #endif 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "globalhelper.h" 25 | 26 | #define MAX_QUEUE_SIZE (15 * 1024 * 1024) 27 | #define MIN_FRAMES 25 28 | #define EXTERNAL_CLOCK_MIN_FRAMES 2 29 | #define EXTERNAL_CLOCK_MAX_FRAMES 10 30 | 31 | /* Minimum SDL audio buffer size, in samples. */ 32 | #define SDL_AUDIO_MIN_BUFFER_SIZE 512 33 | /* Calculate actual buffer size keeping in mind not cause too frequent audio callbacks */ 34 | #define SDL_AUDIO_MAX_CALLBACKS_PER_SEC 30 35 | 36 | /* Step size for volume control in dB */ 37 | #define SDL_VOLUME_STEP (0.75) 38 | 39 | /* no AV sync correction is done if below the minimum AV sync threshold */ 40 | #define AV_SYNC_THRESHOLD_MIN 0.04 41 | /* AV sync correction is done if above the maximum AV sync threshold */ 42 | #define AV_SYNC_THRESHOLD_MAX 0.1 43 | /* If a frame duration is longer than this, it will not be duplicated to compensate AV sync */ 44 | #define AV_SYNC_FRAMEDUP_THRESHOLD 0.1 45 | /* no AV correction is done if too big error */ 46 | #define AV_NOSYNC_THRESHOLD 10.0 47 | 48 | /* maximum audio speed change to get correct sync */ 49 | #define SAMPLE_CORRECTION_PERCENT_MAX 10 50 | 51 | /* external clock speed adjustment constants for realtime sources based on buffer fullness */ 52 | #define EXTERNAL_CLOCK_SPEED_MIN 0.900 53 | #define EXTERNAL_CLOCK_SPEED_MAX 1.010 54 | #define EXTERNAL_CLOCK_SPEED_STEP 0.001 55 | 56 | /* we use about AUDIO_DIFF_AVG_NB A-V differences to make the average */ 57 | #define AUDIO_DIFF_AVG_NB 20 58 | 59 | /* polls for possible required screen refresh at least this often, should be less than 1/fps */ 60 | #define REFRESH_RATE 0.01 61 | 62 | /* NOTE: the size must be big enough to compensate the hardware audio buffersize size */ 63 | /* TODO: We assume that a decoded and resampled frame fits into this buffer */ 64 | #define SAMPLE_ARRAY_SIZE (8 * 65536) 65 | 66 | #define CURSOR_HIDE_DELAY 1000000 67 | 68 | #define USE_ONEPASS_SUBTITLE_RENDER 1 69 | 70 | 71 | 72 | //数据包列表 73 | typedef struct MyAVPacketList { 74 | AVPacket pkt; 75 | struct MyAVPacketList* next; 76 | int serial; 77 | } MyAVPacketList; 78 | 79 | //数据包队列 80 | typedef struct PacketQueue { 81 | MyAVPacketList* first_pkt, * last_pkt; 82 | int nb_packets; 83 | int size; 84 | int64_t duration; 85 | int abort_request; 86 | int serial; 87 | SDL_mutex* mutex; 88 | SDL_cond* cond; 89 | } PacketQueue; 90 | 91 | #define VIDEO_PICTURE_QUEUE_SIZE 3 92 | #define SUBPICTURE_QUEUE_SIZE 16 93 | #define SAMPLE_QUEUE_SIZE 9 94 | #define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE, FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE)) 95 | 96 | //音频参数 97 | typedef struct AudioParams { 98 | int freq; 99 | int channels; 100 | int64_t channel_layout; 101 | enum AVSampleFormat fmt; 102 | int frame_size; 103 | int bytes_per_sec; 104 | } AudioParams; 105 | 106 | //时钟 107 | typedef struct Clock { 108 | double pts; /* clock base */ 109 | double pts_drift; /* clock base minus time at which we updated the clock */ 110 | double last_updated; 111 | double speed; 112 | int serial; /* clock is based on a packet with this serial */ 113 | int paused; 114 | int* queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */ 115 | } Clock; 116 | 117 | /* Common struct for handling all types of decoded data and allocated render buffers. */ 118 | //解码后的帧 119 | typedef struct Frame { 120 | AVFrame* frame; 121 | AVSubtitle sub; 122 | int serial; 123 | double pts; /* presentation timestamp for the frame */ 124 | double duration; /* estimated duration of the frame */ 125 | int64_t pos; /* byte position of the frame in the input file */ 126 | int width; 127 | int height; 128 | int format; 129 | AVRational sar; 130 | int uploaded; 131 | int flip_v; 132 | 133 | std::string subtitle_text; 134 | 135 | } Frame; 136 | 137 | //帧队列 138 | typedef struct FrameQueue { 139 | Frame queue[FRAME_QUEUE_SIZE]; 140 | int rindex; 141 | int windex; 142 | int size; 143 | int max_size; 144 | int keep_last; 145 | int rindex_shown; 146 | SDL_mutex* mutex; 147 | SDL_cond* cond; 148 | PacketQueue* pktq; 149 | } FrameQueue; 150 | 151 | enum { 152 | AV_SYNC_AUDIO_MASTER, /* default choice */ 153 | AV_SYNC_VIDEO_MASTER, 154 | AV_SYNC_EXTERNAL_CLOCK, /* synchronize to an external clock */ 155 | }; 156 | 157 | //解码器,管理数据队列 158 | typedef struct Decoder { 159 | AVPacket pkt; 160 | AVPacket pkt_temp; 161 | PacketQueue* queue; 162 | AVCodecContext* avctx; 163 | int pkt_serial; 164 | int finished; 165 | int packet_pending; 166 | SDL_cond* empty_queue_cond; 167 | int64_t start_pts; 168 | AVRational start_pts_tb; 169 | int64_t next_pts; 170 | AVRational next_pts_tb; 171 | std::thread decode_thread; 172 | } Decoder; 173 | 174 | //视频状态,管理所有的视频信息及数据 175 | typedef struct VideoState { 176 | std::thread read_tid; //读取线程 177 | AVInputFormat* iformat; 178 | int abort_request; //停止读取标志 179 | int force_refresh; 180 | int paused; 181 | int last_paused; 182 | int queue_attachments_req; 183 | int seek_req; 184 | int seek_flags; 185 | int64_t seek_pos; 186 | int64_t seek_rel; 187 | int read_pause_return; 188 | AVFormatContext* ic; 189 | int realtime; 190 | 191 | Clock audclk; 192 | Clock vidclk; 193 | Clock extclk; 194 | 195 | FrameQueue pictq; 196 | FrameQueue subpq; 197 | FrameQueue sampq; 198 | 199 | Decoder auddec; 200 | Decoder viddec; 201 | Decoder subdec; 202 | 203 | int audio_stream; 204 | 205 | int av_sync_type; 206 | 207 | double audio_clock; 208 | int audio_clock_serial; 209 | double audio_diff_cum; /* used for AV difference average computation */ 210 | double audio_diff_avg_coef; 211 | double audio_diff_threshold; 212 | int audio_diff_avg_count; 213 | AVStream* audio_st; 214 | PacketQueue audioq; 215 | int audio_hw_buf_size; 216 | uint8_t* audio_buf; 217 | uint8_t* audio_buf1; 218 | unsigned int audio_buf_size; /* in bytes */ 219 | unsigned int audio_buf1_size; 220 | int audio_buf_index; /* in bytes */ 221 | int audio_write_buf_size; 222 | int audio_volume; 223 | 224 | struct AudioParams audio_src; 225 | 226 | struct AudioParams audio_tgt; 227 | struct SwrContext* swr_ctx; 228 | int frame_drops_early; 229 | int frame_drops_late; 230 | 231 | int16_t sample_array[SAMPLE_ARRAY_SIZE]; 232 | int sample_array_index; 233 | int last_i_start; 234 | RDFTContext* rdft; 235 | int rdft_bits; 236 | FFTSample* rdft_data; 237 | int xpos; 238 | double last_vis_time; 239 | 240 | SDL_Texture* sub_texture; 241 | SDL_Texture* vid_texture; 242 | 243 | int subtitle_stream; 244 | AVStream* subtitle_st; 245 | PacketQueue subtitleq; 246 | 247 | double frame_timer; 248 | double frame_last_returned_time; 249 | double frame_last_filter_delay; 250 | int video_stream; 251 | AVStream* video_st; 252 | PacketQueue videoq; 253 | double max_frame_duration; // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity 254 | struct SwsContext* img_convert_ctx; 255 | struct SwsContext* sub_convert_ctx; 256 | int eof; 257 | 258 | char* filename; 259 | int width, height, xleft, ytop; 260 | int step; 261 | 262 | int last_video_stream, last_audio_stream, last_subtitle_stream; 263 | 264 | SDL_cond* continue_read_thread; 265 | struct CurrentSubtitle 266 | { 267 | std::string text; 268 | double end_time; 269 | } current_subtitle; 270 | 271 | 272 | bool no_video; 273 | SDL_Texture* default_tex; 274 | 275 | struct SwsContext* img_convert_ctx_scaled; // 缩放上下文 276 | struct CudaScaleContext* cuda_scale_ctx; // CUDA 缩放上下文 277 | int vid_texture_width; // 视频纹理宽度 278 | int vid_texture_height; // 视频纹理高度 279 | 280 | } VideoState; 281 | 282 | 283 | 284 | 285 | static AVPacket flush_pkt; 286 | //数据包队列存放数据包(供队列内部使用) 287 | static int packet_queue_put_private(PacketQueue* q, AVPacket* pkt) 288 | { 289 | MyAVPacketList* pkt1; 290 | 291 | if (q->abort_request) 292 | return -1; 293 | 294 | pkt1 = (MyAVPacketList*)av_malloc(sizeof(MyAVPacketList)); 295 | if (!pkt1) 296 | return -1; 297 | pkt1->pkt = *pkt; 298 | pkt1->next = NULL; 299 | if (pkt == &flush_pkt) 300 | q->serial++; 301 | pkt1->serial = q->serial; 302 | 303 | if (!q->last_pkt) 304 | q->first_pkt = pkt1; 305 | else 306 | q->last_pkt->next = pkt1; 307 | q->last_pkt = pkt1; 308 | q->nb_packets++; 309 | q->size += pkt1->pkt.size + sizeof(*pkt1); 310 | q->duration += pkt1->pkt.duration; 311 | /* XXX: should duplicate packet data in DV case */ 312 | SDL_CondSignal(q->cond); 313 | return 0; 314 | } 315 | 316 | //数据包队列存放数据包 317 | static int packet_queue_put(PacketQueue* q, AVPacket* pkt) 318 | { 319 | int ret; 320 | 321 | SDL_LockMutex(q->mutex); 322 | ret = packet_queue_put_private(q, pkt); 323 | SDL_UnlockMutex(q->mutex); 324 | 325 | if (pkt != &flush_pkt && ret < 0) 326 | av_packet_unref(pkt); 327 | 328 | return ret; 329 | } 330 | 331 | //数据包队列存放空数据包 332 | static int packet_queue_put_nullpacket(PacketQueue* q, int stream_index) 333 | { 334 | AVPacket pkt1, * pkt = &pkt1; 335 | av_init_packet(pkt); 336 | pkt->data = NULL; 337 | pkt->size = 0; 338 | pkt->stream_index = stream_index; 339 | return packet_queue_put(q, pkt); 340 | } 341 | 342 | /* packet queue handling */ 343 | //数据包队列初始化 344 | static int packet_queue_init(PacketQueue* q) 345 | { 346 | memset(q, 0, sizeof(PacketQueue)); 347 | q->mutex = SDL_CreateMutex(); 348 | if (!q->mutex) { 349 | av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError()); 350 | return AVERROR(ENOMEM); 351 | } 352 | q->cond = SDL_CreateCond(); 353 | if (!q->cond) { 354 | av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError()); 355 | return AVERROR(ENOMEM); 356 | } 357 | q->abort_request = 1; 358 | return 0; 359 | } 360 | //数据包队列清空 361 | static void packet_queue_flush(PacketQueue* q) 362 | { 363 | MyAVPacketList* pkt, * pkt1; 364 | 365 | SDL_LockMutex(q->mutex); 366 | for (pkt = q->first_pkt; pkt; pkt = pkt1) { 367 | pkt1 = pkt->next; 368 | av_packet_unref(&pkt->pkt); 369 | av_freep(&pkt); 370 | } 371 | q->last_pkt = NULL; 372 | q->first_pkt = NULL; 373 | q->nb_packets = 0; 374 | q->size = 0; 375 | q->duration = 0; 376 | SDL_UnlockMutex(q->mutex); 377 | } 378 | //数据包队列销毁 379 | static void packet_queue_destroy(PacketQueue* q) 380 | { 381 | packet_queue_flush(q); 382 | SDL_DestroyMutex(q->mutex); 383 | SDL_DestroyCond(q->cond); 384 | } 385 | //数据包队列停用 386 | static void packet_queue_abort(PacketQueue* q) 387 | { 388 | SDL_LockMutex(q->mutex); 389 | 390 | q->abort_request = 1; 391 | 392 | SDL_CondSignal(q->cond); 393 | 394 | SDL_UnlockMutex(q->mutex); 395 | } 396 | //数据包队列开始使用 397 | static void packet_queue_start(PacketQueue* q) 398 | { 399 | //初始化清理包 400 | av_init_packet(&flush_pkt); 401 | flush_pkt.data = (uint8_t*)&flush_pkt; 402 | 403 | SDL_LockMutex(q->mutex); 404 | q->abort_request = 0; 405 | packet_queue_put_private(q, &flush_pkt); 406 | SDL_UnlockMutex(q->mutex); 407 | } 408 | 409 | /* return < 0 if aborted, 0 if no packet and > 0 if packet. */ 410 | //从数据包队列中获取数据包 411 | static int packet_queue_get(PacketQueue* q, AVPacket* pkt, int block, int* serial) 412 | { 413 | MyAVPacketList* pkt1; 414 | int ret; 415 | 416 | SDL_LockMutex(q->mutex); 417 | 418 | for (;;) { 419 | if (q->abort_request) { 420 | ret = -1; 421 | break; 422 | } 423 | 424 | pkt1 = q->first_pkt; 425 | if (pkt1) { 426 | q->first_pkt = pkt1->next; 427 | if (!q->first_pkt) 428 | q->last_pkt = NULL; 429 | q->nb_packets--; 430 | q->size -= pkt1->pkt.size + sizeof(*pkt1); 431 | q->duration -= pkt1->pkt.duration; 432 | *pkt = pkt1->pkt; 433 | if (serial) 434 | *serial = pkt1->serial; 435 | av_free(pkt1); 436 | ret = 1; 437 | break; 438 | } 439 | else if (!block) { 440 | ret = 0; 441 | break; 442 | } 443 | else { 444 | SDL_CondWait(q->cond, q->mutex); 445 | } 446 | } 447 | SDL_UnlockMutex(q->mutex); 448 | return ret; 449 | } 450 | 451 | 452 | 453 | 454 | 455 | 456 | //解码器初始化(绑定解码结构体、数据包队列、信号量,初始化pts) 457 | static void decoder_init(Decoder* d, AVCodecContext* avctx, PacketQueue* queue, SDL_cond* empty_queue_cond) { 458 | memset(d, 0, sizeof(Decoder)); 459 | d->avctx = avctx; 460 | d->queue = queue; 461 | d->empty_queue_cond = empty_queue_cond; 462 | d->start_pts = AV_NOPTS_VALUE; 463 | } 464 | 465 | 466 | static int decoder_reorder_pts = -1; 467 | #if 0 468 | //解码一帧数据 469 | static int decoder_decode_frame(Decoder* d, AVFrame* frame, AVSubtitle* sub) { 470 | int got_frame = 0; 471 | 472 | do { 473 | int ret = -1; 474 | 475 | if (d->queue->abort_request) 476 | return -1; 477 | 478 | if (!d->packet_pending || d->queue->serial != d->pkt_serial) { 479 | AVPacket pkt; 480 | do { 481 | if (d->queue->nb_packets == 0) 482 | SDL_CondSignal(d->empty_queue_cond); 483 | //从对应的队列中获取原始数据 484 | if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0) 485 | return -1; 486 | if (pkt.data == flush_pkt.data) { 487 | avcodec_flush_buffers(d->avctx); 488 | d->finished = 0; 489 | d->next_pts = d->start_pts; 490 | d->next_pts_tb = d->start_pts_tb; 491 | } 492 | } while (pkt.data == flush_pkt.data || d->queue->serial != d->pkt_serial); 493 | av_packet_unref(&d->pkt); 494 | d->pkt_temp = d->pkt = pkt; 495 | d->packet_pending = 1; 496 | } 497 | 498 | switch (d->avctx->codec_type) { 499 | case AVMEDIA_TYPE_VIDEO: 500 | //解码视频帧 501 | ret = avcodec_decode_video2(d->avctx, frame, &got_frame, &d->pkt_temp); 502 | if (got_frame) { 503 | if (decoder_reorder_pts == -1) { 504 | frame->pts = av_frame_get_best_effort_timestamp(frame); 505 | } 506 | else if (!decoder_reorder_pts) { 507 | frame->pts = frame->pkt_dts; 508 | } 509 | } 510 | break; 511 | case AVMEDIA_TYPE_AUDIO: 512 | //解码音频帧 513 | ret = avcodec_decode_audio4(d->avctx, frame, &got_frame, &d->pkt_temp); 514 | if (got_frame) { 515 | //AVRational tb = (AVRational) { 1, frame->sample_rate }; 516 | AVRational tb = { 1, frame->sample_rate }; 517 | if (frame->pts != AV_NOPTS_VALUE) 518 | frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), tb); 519 | else if (d->next_pts != AV_NOPTS_VALUE) 520 | frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb); 521 | if (frame->pts != AV_NOPTS_VALUE) { 522 | d->next_pts = frame->pts + frame->nb_samples; 523 | d->next_pts_tb = tb; 524 | } 525 | } 526 | break; 527 | case AVMEDIA_TYPE_SUBTITLE: 528 | //解码字幕帧 529 | ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &d->pkt_temp); 530 | break; 531 | } 532 | 533 | if (ret < 0) { 534 | d->packet_pending = 0; 535 | } 536 | else { 537 | d->pkt_temp.dts = 538 | d->pkt_temp.pts = AV_NOPTS_VALUE; 539 | if (d->pkt_temp.data) { 540 | if (d->avctx->codec_type != AVMEDIA_TYPE_AUDIO) 541 | ret = d->pkt_temp.size; 542 | d->pkt_temp.data += ret; 543 | d->pkt_temp.size -= ret; 544 | if (d->pkt_temp.size <= 0) 545 | d->packet_pending = 0; 546 | } 547 | else { 548 | if (!got_frame) { 549 | d->packet_pending = 0; 550 | d->finished = d->pkt_serial; 551 | } 552 | } 553 | } 554 | } while (!got_frame && !d->finished); 555 | 556 | return got_frame; 557 | } 558 | #endif 559 | #if 1 560 | static int decoder_decode_frame(Decoder* d, AVFrame* frame, AVSubtitle* sub) { 561 | int ret = AVERROR(EAGAIN); 562 | 563 | for (;;) { 564 | AVPacket pkt; 565 | // 1. 流连续情况下获取解码后的帧 566 | if (d->queue->serial == d->pkt_serial) { // 1.1 先判断是否是同一播放序列的数据 567 | do { 568 | if (d->queue->abort_request) 569 | return -1; // 是否请求退出 570 | // 1.2. 获取解码帧 571 | switch (d->avctx->codec_type) { 572 | case AVMEDIA_TYPE_VIDEO: 573 | ret = avcodec_receive_frame(d->avctx, frame); 574 | //printf("frame pts:%ld, dts:%ld\n", frame->pts, frame->pkt_dts); 575 | if (ret >= 0) { 576 | if (decoder_reorder_pts == -1) { 577 | frame->pts = frame->best_effort_timestamp; 578 | } 579 | else if (!decoder_reorder_pts) { 580 | frame->pts = frame->pkt_dts; 581 | } 582 | } 583 | break; 584 | case AVMEDIA_TYPE_AUDIO: 585 | ret = avcodec_receive_frame(d->avctx, frame); 586 | if (ret >= 0) { 587 | AVRational tb = { 1, frame->sample_rate }; // 588 | if (frame->pts != AV_NOPTS_VALUE) { 589 | // 如果frame->pts正常则先将其从pkt_timebase转成{1, frame->sample_rate} 590 | // pkt_timebase实质就是stream->time_base 591 | frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb); 592 | } 593 | else if (d->next_pts != AV_NOPTS_VALUE) { 594 | // 如果frame->pts不正常则使用上一帧更新的next_pts和next_pts_tb 595 | // 转成{1, frame->sample_rate} 596 | frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb); 597 | } 598 | if (frame->pts != AV_NOPTS_VALUE) { 599 | // 根据当前帧的pts和nb_samples预估下一帧的pts 600 | d->next_pts = frame->pts + frame->nb_samples; 601 | d->next_pts_tb = tb; // 设置timebase 602 | } 603 | } 604 | break; 605 | } 606 | 607 | // 1.3. 检查解码是否已经结束,解码结束返回0 608 | if (ret == AVERROR_EOF) { 609 | d->finished = d->pkt_serial; 610 | printf("avcodec_flush_buffers %s(%d)\n", __FUNCTION__, __LINE__); 611 | avcodec_flush_buffers(d->avctx); 612 | return 0; 613 | } 614 | // 1.4. 正常解码返回1 615 | if (ret >= 0) 616 | return 1; 617 | } while (ret != AVERROR(EAGAIN)); // 1.5 没帧可读时ret返回EAGIN,需要继续送packet 618 | } 619 | 620 | // 2 获取一个packet,如果播放序列不一致(数据不连续)则过滤掉“过时”的packet 621 | do { 622 | // 2.1 如果没有数据可读则唤醒read_thread, 实际是continue_read_thread SDL_cond 623 | if (d->queue->nb_packets == 0) // 没有数据可读 624 | SDL_CondSignal(d->empty_queue_cond);// 通知read_thread放入packet 625 | // 2.2 如果还有pending的packet则使用它 626 | if (d->packet_pending) { 627 | av_packet_move_ref(&pkt, &d->pkt); 628 | d->packet_pending = 0; 629 | } 630 | else { 631 | // 2.3 阻塞式读取packet 632 | if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0) 633 | return -1; 634 | } 635 | if (d->queue->serial != d->pkt_serial) { 636 | // darren自己的代码 637 | printf("%s(%d) discontinue:queue->serial:%d,pkt_serial:%d\n", 638 | __FUNCTION__, __LINE__, d->queue->serial, d->pkt_serial); 639 | av_packet_unref(&pkt); // fixed me? 释放要过滤的packet 640 | } 641 | } while (d->queue->serial != d->pkt_serial);// 如果不是同一播放序列(流不连续)则继续读取 642 | 643 | // 3 将packet送入解码器 644 | if (pkt.data == flush_pkt.data) {// 645 | // when seeking or when switching to a different stream 646 | avcodec_flush_buffers(d->avctx); //清空里面的缓存帧 647 | d->finished = 0; // 重置为0 648 | d->next_pts = d->start_pts; // 主要用在了audio 649 | d->next_pts_tb = d->start_pts_tb;// 主要用在了audio 650 | } 651 | else { 652 | if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) { 653 | int got_frame = 0; 654 | ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &pkt); 655 | if (ret < 0) { 656 | ret = AVERROR(EAGAIN); 657 | } 658 | else { 659 | if (got_frame && !pkt.data) { 660 | d->packet_pending = 1; 661 | av_packet_move_ref(&d->pkt, &pkt); 662 | } 663 | ret = got_frame ? 0 : (pkt.data ? AVERROR(EAGAIN) : AVERROR_EOF); 664 | } 665 | } 666 | else { 667 | if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) { 668 | av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n"); 669 | d->packet_pending = 1; 670 | av_packet_move_ref(&d->pkt, &pkt); 671 | } 672 | } 673 | av_packet_unref(&pkt); // 一定要自己去释放音视频数据 674 | } 675 | } 676 | } 677 | #endif 678 | //解码器销毁 679 | static void decoder_destroy(Decoder* d) { 680 | av_packet_unref(&d->pkt); 681 | avcodec_free_context(&d->avctx); 682 | } 683 | 684 | static void frame_queue_unref_item(Frame* vp) 685 | { 686 | av_frame_unref(vp->frame); 687 | avsubtitle_free(&vp->sub); 688 | } 689 | //帧队列初始化(绑定数据包队列,初始化最大值) 690 | static int frame_queue_init(FrameQueue* f, PacketQueue* pktq, int max_size, int keep_last) 691 | { 692 | int i; 693 | memset(f, 0, sizeof(FrameQueue)); 694 | if (!(f->mutex = SDL_CreateMutex())) { 695 | av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError()); 696 | return AVERROR(ENOMEM); 697 | } 698 | if (!(f->cond = SDL_CreateCond())) { 699 | av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError()); 700 | return AVERROR(ENOMEM); 701 | } 702 | f->pktq = pktq; 703 | f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE); 704 | f->keep_last = !!keep_last; 705 | //为队列中所有的缓存帧预先申请内存 706 | for (i = 0; i < f->max_size; i++) 707 | if (!(f->queue[i].frame = av_frame_alloc())) 708 | return AVERROR(ENOMEM); 709 | return 0; 710 | } 711 | //帧队列销毁 712 | static void frame_queue_destory(FrameQueue* f) 713 | { 714 | int i; 715 | for (i = 0; i < f->max_size; i++) { 716 | Frame* vp = &f->queue[i]; 717 | frame_queue_unref_item(vp); 718 | av_frame_free(&vp->frame); 719 | } 720 | SDL_DestroyMutex(f->mutex); 721 | SDL_DestroyCond(f->cond); 722 | } 723 | //帧队列信号 724 | static void frame_queue_signal(FrameQueue* f) 725 | { 726 | SDL_LockMutex(f->mutex); 727 | SDL_CondSignal(f->cond); 728 | SDL_UnlockMutex(f->mutex); 729 | } 730 | 731 | static Frame* frame_queue_peek(FrameQueue* f) 732 | { 733 | return &f->queue[(f->rindex + f->rindex_shown) % f->max_size]; 734 | } 735 | 736 | static Frame* frame_queue_peek_next(FrameQueue* f) 737 | { 738 | return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size]; 739 | } 740 | 741 | static Frame* frame_queue_peek_last(FrameQueue* f) 742 | { 743 | return &f->queue[f->rindex]; 744 | } 745 | 746 | static Frame* frame_queue_peek_writable(FrameQueue* f) 747 | { 748 | /* wait until we have space to put a new frame */ 749 | SDL_LockMutex(f->mutex); 750 | while (f->size >= f->max_size && 751 | !f->pktq->abort_request) { 752 | SDL_CondWait(f->cond, f->mutex); 753 | } 754 | SDL_UnlockMutex(f->mutex); 755 | 756 | if (f->pktq->abort_request) 757 | return NULL; 758 | 759 | return &f->queue[f->windex]; 760 | } 761 | 762 | static Frame* frame_queue_peek_readable(FrameQueue* f) 763 | { 764 | /* wait until we have a readable a new frame */ 765 | SDL_LockMutex(f->mutex); 766 | while (f->size - f->rindex_shown <= 0 && 767 | !f->pktq->abort_request) { 768 | SDL_CondWait(f->cond, f->mutex); 769 | } 770 | SDL_UnlockMutex(f->mutex); 771 | 772 | if (f->pktq->abort_request) 773 | return NULL; 774 | 775 | return &f->queue[(f->rindex + f->rindex_shown) % f->max_size]; 776 | } 777 | 778 | static void frame_queue_push(FrameQueue* f) 779 | { 780 | if (++f->windex == f->max_size) 781 | f->windex = 0; 782 | SDL_LockMutex(f->mutex); 783 | f->size++; 784 | SDL_CondSignal(f->cond); 785 | SDL_UnlockMutex(f->mutex); 786 | } 787 | 788 | static void frame_queue_next(FrameQueue* f) 789 | { 790 | if (f->keep_last && !f->rindex_shown) { 791 | f->rindex_shown = 1; 792 | return; 793 | } 794 | frame_queue_unref_item(&f->queue[f->rindex]); 795 | if (++f->rindex == f->max_size) 796 | f->rindex = 0; 797 | SDL_LockMutex(f->mutex); 798 | f->size--; 799 | SDL_CondSignal(f->cond); 800 | SDL_UnlockMutex(f->mutex); 801 | } 802 | 803 | static int frame_queue_nb_remaining(FrameQueue* f) 804 | { 805 | return f->size - f->rindex_shown; 806 | } 807 | 808 | static int64_t frame_queue_last_pos(FrameQueue* f) 809 | { 810 | Frame* fp = &f->queue[f->rindex]; 811 | if (f->rindex_shown && fp->serial == f->pktq->serial) 812 | return fp->pos; 813 | else 814 | return -1; 815 | } 816 | 817 | static void decoder_abort(Decoder* d, FrameQueue* fq) 818 | { 819 | packet_queue_abort(d->queue); 820 | frame_queue_signal(fq); 821 | d->decode_thread.join(); 822 | packet_queue_flush(d->queue); 823 | } --------------------------------------------------------------------------------