From 18918517851b863a37b293e3c63ff6c8b888c02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=AE=E7=94=9F=E8=8B=A5=E6=A2=A6?= <1070753498@qq.com> Date: Wed, 5 Jun 2024 17:30:18 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=BC=95=E5=85=A5mpv=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=99=A8=E6=94=AF=E6=8C=81]=EF=BC=9A=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E4=BA=86mpv=E6=92=AD=E6=94=BE=E5=99=A8=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了mpv相关的CMakeLists.txt配置,使得mpv可以作为可选模块编译进项目中。 - 更新了install-dependencies/action.yml,增加了mpv的安装步骤,以便在macOS上通过Homebrew安装mpv。 - 对cmake.yml和qmake.yml进行了修改,加入了对mpv编译选项的配置。 - 修改了CMakeLists.txt,引入了BUILD_MPV选项,并根据此选项配置了项目。 - 为examples目录添加了mpvplayer子目录,并创建了相关的CMakeLists.txt、mainwindow.cc/.hpp等文件,实现了基于mpv的播放器界面。 - 更新了player和transcoder的CMakeLists.txt,以支持mpv选项。 - 为mpvplayer添加了控制widget、日志窗口、字幕延迟对话框等组件。 - 在src目录下添加了mpv子目录,实现了mpv播放器的核心功能,包括与mpv的交互、渲染以及事件处理。 - 更新了utils.cpp和utils.h,增加了设置OpenGL表面格式版本的功能。 - 根据是否启用mpv选项,调整了src.pro,以包含或排除mpv子目录。 --- .../actions/install-dependencies/action.yml | 5 +- .github/workflows/cmake.yml | 1 + .github/workflows/qmake.yml | 2 +- CMakeLists.txt | 2 + examples/CMakeLists.txt | 8 +- examples/{player => common}/controlwidget.cc | 16 + examples/{player => common}/controlwidget.hpp | 10 +- .../{player => common}/openwebmediadialog.cc | 0 .../{player => common}/openwebmediadialog.hpp | 0 examples/{player => common}/playlistmodel.cpp | 0 examples/{player => common}/playlistmodel.h | 0 examples/{player => common}/playlistview.cc | 0 examples/{player => common}/playlistview.hpp | 0 .../{player => common}/qmediaplaylist.cpp | 0 examples/{player => common}/qmediaplaylist.h | 0 .../{player => common}/qmediaplaylist_p.h | 0 .../qplaylistfileparser.cpp | 0 .../qplaylistfileparser_p.h | 0 examples/{player => common}/slider.cpp | 0 examples/{player => common}/slider.h | 0 examples/{player => common}/titlewidget.cc | 0 examples/{player => common}/titlewidget.hpp | 0 examples/examples.pro | 8 +- .../{player => ffmpegplayer}/CMakeLists.txt | 46 +- .../colorspacedialog.cc | 3 +- .../colorspacedialog.hpp | 0 examples/ffmpegplayer/ffmpegplayer.pro | 43 + examples/{player => ffmpegplayer}/main.cpp | 5 +- .../{player => ffmpegplayer}/mainwindow.cpp | 15 +- .../{player => ffmpegplayer}/mainwindow.h | 0 .../CMakeLists.txt | 10 +- .../audioencodermodel.cc | 0 .../audioencodermodel.hpp | 0 .../audioencodertableview.cc | 0 .../audioencodertableview.hpp | 0 .../audioencoderwidget.cc | 0 .../audioencoderwidget.hpp | 0 .../commonwidgets.cc | 0 .../commonwidgets.hpp | 0 .../ffmpegtranscoder.pro} | 2 +- .../{transcoder => ffmpegtranscoder}/main.cc | 2 +- .../mainwindow.cc | 0 .../mainwindow.hpp | 0 .../outputwidget.cc | 0 .../outputwidget.hpp | 0 .../previewwidget.cc | 0 .../previewwidget.hpp | 0 .../sourcewidget.cc | 0 .../sourcewidget.hpp | 0 .../stautuswidget.cc | 0 .../stautuswidget.hpp | 0 .../styleditemdelegate.cc | 0 .../styleditemdelegate.hpp | 0 .../subtitleencodermodel.cc | 0 .../subtitleencodermodel.hpp | 0 .../subtitleencodertableview.cc | 0 .../subtitleencodertableview.hpp | 0 .../subtitleencoderwidget.cc | 0 .../subtitleencoderwidget.hpp | 0 .../videoencoderwidget.cc | 0 .../videoencoderwidget.hpp | 0 examples/mpvplayer/CMakeLists.txt | 51 ++ examples/mpvplayer/main.cc | 88 ++ examples/mpvplayer/mainwindow.cc | 758 ++++++++++++++++++ examples/mpvplayer/mainwindow.hpp | 42 + examples/mpvplayer/mpvlogwindow.cc | 37 + examples/mpvplayer/mpvlogwindow.hpp | 25 + examples/mpvplayer/mpvplayer.pro | 52 ++ examples/mpvplayer/subtitledelaydialog.cc | 57 ++ examples/mpvplayer/subtitledelaydialog.hpp | 29 + examples/player/player.pro | 43 - src/CMakeLists.txt | 4 + src/ffmpeg/videorender/videorendercreate.cc | 8 - src/ffmpeg/videorender/videorendercreate.hpp | 2 - src/mpv/CMakeLists.txt | 32 + src/mpv/mediainfo.cc | 70 ++ src/mpv/mediainfo.hpp | 58 ++ src/mpv/mpv.pri | 18 + src/mpv/mpv.pro | 23 + src/mpv/mpv_global.h | 12 + src/mpv/mpvopenglwidget.cc | 155 ++++ src/mpv/mpvopenglwidget.hpp | 39 + src/mpv/mpvplayer.cc | 483 +++++++++++ src/mpv/mpvplayer.hpp | 114 +++ src/mpv/mpvwidget.cc | 17 + src/mpv/mpvwidget.hpp | 20 + src/mpv/previewwidget.cc | 67 ++ src/mpv/previewwidget.hpp | 28 + src/mpv/qthelper.hpp | 332 ++++++++ src/src.pro | 4 + src/utils/utils.cpp | 8 + src/utils/utils.h | 1 + 92 files changed, 2752 insertions(+), 103 deletions(-) rename examples/{player => common}/controlwidget.cc (95%) rename examples/{player => common}/controlwidget.hpp (88%) rename examples/{player => common}/openwebmediadialog.cc (100%) rename examples/{player => common}/openwebmediadialog.hpp (100%) rename examples/{player => common}/playlistmodel.cpp (100%) rename examples/{player => common}/playlistmodel.h (100%) rename examples/{player => common}/playlistview.cc (100%) rename examples/{player => common}/playlistview.hpp (100%) rename examples/{player => common}/qmediaplaylist.cpp (100%) rename examples/{player => common}/qmediaplaylist.h (100%) rename examples/{player => common}/qmediaplaylist_p.h (100%) rename examples/{player => common}/qplaylistfileparser.cpp (100%) rename examples/{player => common}/qplaylistfileparser_p.h (100%) rename examples/{player => common}/slider.cpp (100%) rename examples/{player => common}/slider.h (100%) rename examples/{player => common}/titlewidget.cc (100%) rename examples/{player => common}/titlewidget.hpp (100%) rename examples/{player => ffmpegplayer}/CMakeLists.txt (52%) rename examples/{player => ffmpegplayer}/colorspacedialog.cc (99%) rename examples/{player => ffmpegplayer}/colorspacedialog.hpp (100%) create mode 100644 examples/ffmpegplayer/ffmpegplayer.pro rename examples/{player => ffmpegplayer}/main.cpp (94%) rename examples/{player => ffmpegplayer}/mainwindow.cpp (98%) rename examples/{player => ffmpegplayer}/mainwindow.h (100%) rename examples/{transcoder => ffmpegtranscoder}/CMakeLists.txt (86%) rename examples/{transcoder => ffmpegtranscoder}/audioencodermodel.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/audioencodermodel.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/audioencodertableview.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/audioencodertableview.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/audioencoderwidget.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/audioencoderwidget.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/commonwidgets.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/commonwidgets.hpp (100%) rename examples/{transcoder/transcoder.pro => ffmpegtranscoder/ffmpegtranscoder.pro} (97%) rename examples/{transcoder => ffmpegtranscoder}/main.cc (98%) rename examples/{transcoder => ffmpegtranscoder}/mainwindow.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/mainwindow.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/outputwidget.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/outputwidget.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/previewwidget.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/previewwidget.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/sourcewidget.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/sourcewidget.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/stautuswidget.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/stautuswidget.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/styleditemdelegate.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/styleditemdelegate.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/subtitleencodermodel.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/subtitleencodermodel.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/subtitleencodertableview.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/subtitleencodertableview.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/subtitleencoderwidget.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/subtitleencoderwidget.hpp (100%) rename examples/{transcoder => ffmpegtranscoder}/videoencoderwidget.cc (100%) rename examples/{transcoder => ffmpegtranscoder}/videoencoderwidget.hpp (100%) create mode 100644 examples/mpvplayer/CMakeLists.txt create mode 100644 examples/mpvplayer/main.cc create mode 100644 examples/mpvplayer/mainwindow.cc create mode 100644 examples/mpvplayer/mainwindow.hpp create mode 100644 examples/mpvplayer/mpvlogwindow.cc create mode 100644 examples/mpvplayer/mpvlogwindow.hpp create mode 100644 examples/mpvplayer/mpvplayer.pro create mode 100644 examples/mpvplayer/subtitledelaydialog.cc create mode 100644 examples/mpvplayer/subtitledelaydialog.hpp delete mode 100644 examples/player/player.pro create mode 100644 src/mpv/CMakeLists.txt create mode 100644 src/mpv/mediainfo.cc create mode 100644 src/mpv/mediainfo.hpp create mode 100644 src/mpv/mpv.pri create mode 100644 src/mpv/mpv.pro create mode 100644 src/mpv/mpv_global.h create mode 100644 src/mpv/mpvopenglwidget.cc create mode 100644 src/mpv/mpvopenglwidget.hpp create mode 100644 src/mpv/mpvplayer.cc create mode 100644 src/mpv/mpvplayer.hpp create mode 100644 src/mpv/mpvwidget.cc create mode 100644 src/mpv/mpvwidget.hpp create mode 100644 src/mpv/previewwidget.cc create mode 100644 src/mpv/previewwidget.hpp create mode 100644 src/mpv/qthelper.hpp diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index d3086ad..32a7e5e 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -80,7 +80,7 @@ runs: if: startsWith(runner.os, 'macOS') shell: bash run: | - brew install ninja nasm python-setuptools + brew install ninja nasm python-setuptools mpv ninja --version cmake --version clang --version @@ -97,7 +97,8 @@ runs: run: | sudo apt-get update sudo apt-get install ninja-build nasm autopoint gperf libgl1-mesa-dev \ - libxinerama-dev libxcursor-dev xorg-dev libglu1-mesa-dev + libxinerama-dev libxcursor-dev xorg-dev libglu1-mesa-dev \ + mpv libmpv-dev ninja --version cmake --version gcc --version diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 4c7d375..4add60a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -53,6 +53,7 @@ jobs: -S . ` -B ./build ` -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ` + -DBUILD_MPV=OFF ` -G "${{ matrix.generators }}" cmake --build ./build --config ${{ matrix.build_type }} - name: Configure and build on ubuntu or macos diff --git a/.github/workflows/qmake.yml b/.github/workflows/qmake.yml index ab2479e..ccdbd2d 100644 --- a/.github/workflows/qmake.yml +++ b/.github/workflows/qmake.yml @@ -54,7 +54,7 @@ jobs: shell: pwsh run: | ..\scripts\windows\setVsDev.ps1 -VersionRange "[16.0,17.0)" -Arch "x64" - & qmake ./../. + & qmake "CONFIG-=BUILD_MPV" ./../. & jom working-directory: build - name: ubuntu-build diff --git a/CMakeLists.txt b/CMakeLists.txt index b443bd7..c78ad62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,8 @@ project( HOMEPAGE_URL "https://github.com/RealChuan/Qt-Ffmpeg" LANGUAGES CXX) +option(BUILD_MPV "build mpv" ON) + include(cmake/common.cmake) find_package(Qt6 REQUIRED COMPONENTS Widgets Network Core5Compat Concurrent diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e4114e3..b4e78f8 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,6 @@ -add_subdirectory(player) -add_subdirectory(transcoder) \ No newline at end of file +add_subdirectory(ffmpegplayer) +add_subdirectory(ffmpegtranscoder) + +if(BUILD_MPV) + add_subdirectory(mpvplayer) +endif() diff --git a/examples/player/controlwidget.cc b/examples/common/controlwidget.cc similarity index 95% rename from examples/player/controlwidget.cc rename to examples/common/controlwidget.cc index 56e419c..0553dc4 100644 --- a/examples/player/controlwidget.cc +++ b/examples/common/controlwidget.cc @@ -182,6 +182,11 @@ void ControlWidget::setPlayButtonChecked(bool checked) d_ptr->setPlayButtonIcon(); } +void ControlWidget::setVolumeMax(int max) +{ + d_ptr->volumeSlider->setMaximum(max); +} + void ControlWidget::setVolume(int value) { if (value < d_ptr->volumeSlider->minimum()) { @@ -208,6 +213,16 @@ void ControlWidget::setDuration(int value) setPosition(0); } +#ifdef MPV_ON +void ControlWidget::setChapters(const Mpv::Chapters &chapters) +{ + QVector nodes; + for (const auto &chapter : std::as_const(chapters)) { + nodes.append(chapter.milliseconds / 1000); + } + d_ptr->slider->setNodes(nodes); +} +#else void ControlWidget::setChapters(const Ffmpeg::Chapters &chapters) { QVector nodes; @@ -216,6 +231,7 @@ void ControlWidget::setChapters(const Ffmpeg::Chapters &chapters) } d_ptr->slider->setNodes(nodes); } +#endif void ControlWidget::setPosition(int value) { diff --git a/examples/player/controlwidget.hpp b/examples/common/controlwidget.hpp similarity index 88% rename from examples/player/controlwidget.hpp rename to examples/common/controlwidget.hpp index 24a1c0b..07d61b1 100644 --- a/examples/player/controlwidget.hpp +++ b/examples/common/controlwidget.hpp @@ -1,7 +1,11 @@ #ifndef CONTROLWIDGET_HPP #define CONTROLWIDGET_HPP +#ifdef MPV_ON +#include +#else #include +#endif #include @@ -17,8 +21,11 @@ class ControlWidget : public QWidget void setDuration(int value); [[nodiscard]] auto duration() const -> int; +#ifdef MPV_ON + void setChapters(const Mpv::Chapters &chapters); +#else void setChapters(const Ffmpeg::Chapters &chapters); - +#endif [[nodiscard]] auto sliderGlobalPos() const -> QPoint; void setSourceFPS(float fps); @@ -27,6 +34,7 @@ class ControlWidget : public QWidget void clickPlayButton(); void setPlayButtonChecked(bool checked); + void setVolumeMax(int max); void setVolume(int value); [[nodiscard]] auto volume() const -> int; diff --git a/examples/player/openwebmediadialog.cc b/examples/common/openwebmediadialog.cc similarity index 100% rename from examples/player/openwebmediadialog.cc rename to examples/common/openwebmediadialog.cc diff --git a/examples/player/openwebmediadialog.hpp b/examples/common/openwebmediadialog.hpp similarity index 100% rename from examples/player/openwebmediadialog.hpp rename to examples/common/openwebmediadialog.hpp diff --git a/examples/player/playlistmodel.cpp b/examples/common/playlistmodel.cpp similarity index 100% rename from examples/player/playlistmodel.cpp rename to examples/common/playlistmodel.cpp diff --git a/examples/player/playlistmodel.h b/examples/common/playlistmodel.h similarity index 100% rename from examples/player/playlistmodel.h rename to examples/common/playlistmodel.h diff --git a/examples/player/playlistview.cc b/examples/common/playlistview.cc similarity index 100% rename from examples/player/playlistview.cc rename to examples/common/playlistview.cc diff --git a/examples/player/playlistview.hpp b/examples/common/playlistview.hpp similarity index 100% rename from examples/player/playlistview.hpp rename to examples/common/playlistview.hpp diff --git a/examples/player/qmediaplaylist.cpp b/examples/common/qmediaplaylist.cpp similarity index 100% rename from examples/player/qmediaplaylist.cpp rename to examples/common/qmediaplaylist.cpp diff --git a/examples/player/qmediaplaylist.h b/examples/common/qmediaplaylist.h similarity index 100% rename from examples/player/qmediaplaylist.h rename to examples/common/qmediaplaylist.h diff --git a/examples/player/qmediaplaylist_p.h b/examples/common/qmediaplaylist_p.h similarity index 100% rename from examples/player/qmediaplaylist_p.h rename to examples/common/qmediaplaylist_p.h diff --git a/examples/player/qplaylistfileparser.cpp b/examples/common/qplaylistfileparser.cpp similarity index 100% rename from examples/player/qplaylistfileparser.cpp rename to examples/common/qplaylistfileparser.cpp diff --git a/examples/player/qplaylistfileparser_p.h b/examples/common/qplaylistfileparser_p.h similarity index 100% rename from examples/player/qplaylistfileparser_p.h rename to examples/common/qplaylistfileparser_p.h diff --git a/examples/player/slider.cpp b/examples/common/slider.cpp similarity index 100% rename from examples/player/slider.cpp rename to examples/common/slider.cpp diff --git a/examples/player/slider.h b/examples/common/slider.h similarity index 100% rename from examples/player/slider.h rename to examples/common/slider.h diff --git a/examples/player/titlewidget.cc b/examples/common/titlewidget.cc similarity index 100% rename from examples/player/titlewidget.cc rename to examples/common/titlewidget.cc diff --git a/examples/player/titlewidget.hpp b/examples/common/titlewidget.hpp similarity index 100% rename from examples/player/titlewidget.hpp rename to examples/common/titlewidget.hpp diff --git a/examples/examples.pro b/examples/examples.pro index 4ce264a..5f7b6c1 100644 --- a/examples/examples.pro +++ b/examples/examples.pro @@ -2,5 +2,9 @@ TEMPLATE = subdirs CONFIG += ordered SUBDIRS += \ - player \ - transcoder + ffmpegplayer \ + ffmpegtranscoder + +contains(CONFIG, BUILD_MPV) { + SUBDIRS += mpvplayer +} diff --git a/examples/player/CMakeLists.txt b/examples/ffmpegplayer/CMakeLists.txt similarity index 52% rename from examples/player/CMakeLists.txt rename to examples/ffmpegplayer/CMakeLists.txt index 223e9b9..f28b119 100644 --- a/examples/player/CMakeLists.txt +++ b/examples/ffmpegplayer/CMakeLists.txt @@ -1,30 +1,30 @@ set(PROJECT_SOURCES + ../common/controlwidget.cc + ../common/controlwidget.hpp + ../common/openwebmediadialog.cc + ../common/openwebmediadialog.hpp + ../common/playlistmodel.cpp + ../common/playlistmodel.h + ../common/playlistview.cc + ../common/playlistview.hpp + ../common/qmediaplaylist_p.h + ../common/qmediaplaylist.cpp + ../common/qmediaplaylist.h + ../common/qplaylistfileparser_p.h + ../common/qplaylistfileparser.cpp + ../common/slider.cpp + ../common/slider.h + ../common/titlewidget.cc + ../common/titlewidget.hpp colorspacedialog.cc colorspacedialog.hpp - controlwidget.cc - controlwidget.hpp main.cpp mainwindow.cpp - mainwindow.h - openwebmediadialog.cc - openwebmediadialog.hpp - playlistmodel.cpp - playlistmodel.h - playlistview.cc - playlistview.hpp - qmediaplaylist_p.h - qmediaplaylist.cpp - qmediaplaylist.h - qplaylistfileparser_p.h - qplaylistfileparser.cpp - slider.cpp - slider.h - titlewidget.cc - titlewidget.hpp) + mainwindow.h) -qt_add_executable(Player MANUAL_FINALIZATION ${PROJECT_SOURCES}) +qt_add_executable(FfmpegPlayer MANUAL_FINALIZATION ${PROJECT_SOURCES}) target_link_libraries( - Player + FfmpegPlayer PRIVATE ffmpeg thirdparty dump @@ -32,10 +32,10 @@ target_link_libraries( Qt6::Widgets Qt6::Multimedia Qt6::OpenGLWidgets) -target_link_libraries(Player PRIVATE PkgConfig::ffmpeg) +target_link_libraries(FfmpegPlayer PRIVATE PkgConfig::ffmpeg) if(CMAKE_HOST_APPLE) target_link_libraries( - Player + FfmpegPlayer PRIVATE ${Foundation_LIBRARY} ${CoreAudio_LIBRARY} ${AVFoundation_LIBRARY} @@ -52,4 +52,4 @@ if(CMAKE_HOST_APPLE) ${CoreVideo_LIBRARY} ${CoreServices_LIBRARY}) endif() -qt_finalize_executable(Player) +qt_finalize_executable(FfmpegPlayer) diff --git a/examples/player/colorspacedialog.cc b/examples/ffmpegplayer/colorspacedialog.cc similarity index 99% rename from examples/player/colorspacedialog.cc rename to examples/ffmpegplayer/colorspacedialog.cc index 2004437..7840e92 100644 --- a/examples/player/colorspacedialog.cc +++ b/examples/ffmpegplayer/colorspacedialog.cc @@ -1,5 +1,6 @@ #include "colorspacedialog.hpp" -#include "slider.h" + +#include #include diff --git a/examples/player/colorspacedialog.hpp b/examples/ffmpegplayer/colorspacedialog.hpp similarity index 100% rename from examples/player/colorspacedialog.hpp rename to examples/ffmpegplayer/colorspacedialog.hpp diff --git a/examples/ffmpegplayer/ffmpegplayer.pro b/examples/ffmpegplayer/ffmpegplayer.pro new file mode 100644 index 0000000..31b4dda --- /dev/null +++ b/examples/ffmpegplayer/ffmpegplayer.pro @@ -0,0 +1,43 @@ +include(../../common.pri) + +QT += core gui widgets network multimedia openglwidgets core5compat + +TEMPLATE = app + +TARGET = FfmpegPlayer + +LIBS += \ + -l$$replaceLibName(ffmpeg) \ + -l$$replaceLibName(thirdparty) \ + -l$$replaceLibName(dump) \ + -l$$replaceLibName(utils) + +include(../../src/3rdparty/3rdparty.pri) + +SOURCES += \ + ../common/controlwidget.cc \ + ../common/openwebmediadialog.cc \ + ../common/playlistmodel.cpp \ + ../common/playlistview.cc \ + ../common/qmediaplaylist.cpp \ + ../common/qplaylistfileparser.cpp \ + ../common/slider.cpp \ + ../common/titlewidget.cc \ + colorspacedialog.cc \ + main.cpp \ + mainwindow.cpp + +HEADERS += \ + ../common/controlwidget.hpp \ + ../common/openwebmediadialog.hpp \ + ../common/playlistmodel.h \ + ../common/playlistview.hpp \ + ../common/qmediaplaylist.h \ + ../common/qmediaplaylist_p.h \ + ../common/qplaylistfileparser_p.h \ + ../common/slider.h \ + ../common/titlewidget.hpp \ + colorspacedialog.hpp \ + mainwindow.h + +DESTDIR = $$APP_OUTPUT_PATH diff --git a/examples/player/main.cpp b/examples/ffmpegplayer/main.cpp similarity index 94% rename from examples/player/main.cpp rename to examples/ffmpegplayer/main.cpp index 3671b6d..a1f7b52 100644 --- a/examples/player/main.cpp +++ b/examples/ffmpegplayer/main.cpp @@ -3,7 +3,6 @@ #include <3rdparty/qtsingleapplication/qtsingleapplication.h> #include #include -#include #include #include @@ -12,7 +11,7 @@ #include #include -#define AppName "Player" +#define AppName "FfmpegPlayer" void setAppInfo() { @@ -36,7 +35,7 @@ auto main(int argc, char *argv[]) -> int #endif Utils::setHighDpiEnvironmentVariable(); - Ffmpeg::VideoRenderCreate::setSurfaceFormatVersion(3, 3); + Utils::setSurfaceFormatVersion(3, 3); SharedTools::QtSingleApplication::setAttribute(Qt::AA_ShareOpenGLContexts); SharedTools::QtSingleApplication app(AppName, argc, argv); diff --git a/examples/player/mainwindow.cpp b/examples/ffmpegplayer/mainwindow.cpp similarity index 98% rename from examples/player/mainwindow.cpp rename to examples/ffmpegplayer/mainwindow.cpp index e312dff..c81d64a 100644 --- a/examples/player/mainwindow.cpp +++ b/examples/ffmpegplayer/mainwindow.cpp @@ -1,12 +1,12 @@ #include "mainwindow.h" #include "colorspacedialog.hpp" -#include "controlwidget.hpp" -#include "openwebmediadialog.hpp" -#include "playlistmodel.h" -#include "playlistview.hpp" -#include "qmediaplaylist.h" -#include "titlewidget.hpp" +#include +#include +#include +#include +#include +#include #include #include #include @@ -299,8 +299,7 @@ void MainWindow::onOpenLocalMedia() { const auto path = QStandardPaths::standardLocations(QStandardPaths::MoviesLocation) .value(0, QDir::homePath()); - const auto filter = tr("Media (*.mp4 *.flv *.ts *.avi *.rmvb *.mkv *.wmv *.mp3 *.wav *.flac " - "*.ape *.m4a *.aac *.ogg *.ac3 *.mpg)"); + const auto filter = tr("Media (*)"); const auto urls = QFileDialog::getOpenFileUrls(this, tr("Open Media"), QUrl::fromUserInput(path), diff --git a/examples/player/mainwindow.h b/examples/ffmpegplayer/mainwindow.h similarity index 100% rename from examples/player/mainwindow.h rename to examples/ffmpegplayer/mainwindow.h diff --git a/examples/transcoder/CMakeLists.txt b/examples/ffmpegtranscoder/CMakeLists.txt similarity index 86% rename from examples/transcoder/CMakeLists.txt rename to examples/ffmpegtranscoder/CMakeLists.txt index 2e2b869..7524415 100644 --- a/examples/transcoder/CMakeLists.txt +++ b/examples/ffmpegtranscoder/CMakeLists.txt @@ -29,9 +29,9 @@ set(PROJECT_SOURCES videoencoderwidget.cc videoencoderwidget.hpp) -qt_add_executable(Transcoder MANUAL_FINALIZATION ${PROJECT_SOURCES}) +qt_add_executable(FfmpegTranscoder MANUAL_FINALIZATION ${PROJECT_SOURCES}) target_link_libraries( - Transcoder + FfmpegTranscoder PRIVATE ffmpeg thirdparty dump @@ -39,11 +39,11 @@ target_link_libraries( Qt6::Widgets Qt6::Multimedia Qt6::OpenGLWidgets) -target_link_libraries(Transcoder PRIVATE PkgConfig::ffmpeg) +target_link_libraries(FfmpegTranscoder PRIVATE PkgConfig::ffmpeg) if(CMAKE_HOST_APPLE) target_link_libraries( - Transcoder + FfmpegTranscoder PRIVATE ${Foundation_LIBRARY} ${CoreAudio_LIBRARY} ${AVFoundation_LIBRARY} @@ -61,4 +61,4 @@ if(CMAKE_HOST_APPLE) ${CoreServices_LIBRARY}) endif() -qt_finalize_executable(Transcoder) +qt_finalize_executable(FfmpegTranscoder) diff --git a/examples/transcoder/audioencodermodel.cc b/examples/ffmpegtranscoder/audioencodermodel.cc similarity index 100% rename from examples/transcoder/audioencodermodel.cc rename to examples/ffmpegtranscoder/audioencodermodel.cc diff --git a/examples/transcoder/audioencodermodel.hpp b/examples/ffmpegtranscoder/audioencodermodel.hpp similarity index 100% rename from examples/transcoder/audioencodermodel.hpp rename to examples/ffmpegtranscoder/audioencodermodel.hpp diff --git a/examples/transcoder/audioencodertableview.cc b/examples/ffmpegtranscoder/audioencodertableview.cc similarity index 100% rename from examples/transcoder/audioencodertableview.cc rename to examples/ffmpegtranscoder/audioencodertableview.cc diff --git a/examples/transcoder/audioencodertableview.hpp b/examples/ffmpegtranscoder/audioencodertableview.hpp similarity index 100% rename from examples/transcoder/audioencodertableview.hpp rename to examples/ffmpegtranscoder/audioencodertableview.hpp diff --git a/examples/transcoder/audioencoderwidget.cc b/examples/ffmpegtranscoder/audioencoderwidget.cc similarity index 100% rename from examples/transcoder/audioencoderwidget.cc rename to examples/ffmpegtranscoder/audioencoderwidget.cc diff --git a/examples/transcoder/audioencoderwidget.hpp b/examples/ffmpegtranscoder/audioencoderwidget.hpp similarity index 100% rename from examples/transcoder/audioencoderwidget.hpp rename to examples/ffmpegtranscoder/audioencoderwidget.hpp diff --git a/examples/transcoder/commonwidgets.cc b/examples/ffmpegtranscoder/commonwidgets.cc similarity index 100% rename from examples/transcoder/commonwidgets.cc rename to examples/ffmpegtranscoder/commonwidgets.cc diff --git a/examples/transcoder/commonwidgets.hpp b/examples/ffmpegtranscoder/commonwidgets.hpp similarity index 100% rename from examples/transcoder/commonwidgets.hpp rename to examples/ffmpegtranscoder/commonwidgets.hpp diff --git a/examples/transcoder/transcoder.pro b/examples/ffmpegtranscoder/ffmpegtranscoder.pro similarity index 97% rename from examples/transcoder/transcoder.pro rename to examples/ffmpegtranscoder/ffmpegtranscoder.pro index a5666a5..2f192a4 100644 --- a/examples/transcoder/transcoder.pro +++ b/examples/ffmpegtranscoder/ffmpegtranscoder.pro @@ -4,7 +4,7 @@ QT += core gui widgets network multimedia openglwidgets core5compat TEMPLATE = app -TARGET = Transcoder +TARGET = FfmpegTranscoder LIBS += \ -l$$replaceLibName(ffmpeg) \ diff --git a/examples/transcoder/main.cc b/examples/ffmpegtranscoder/main.cc similarity index 98% rename from examples/transcoder/main.cc rename to examples/ffmpegtranscoder/main.cc index d380ad6..119b6a6 100644 --- a/examples/transcoder/main.cc +++ b/examples/ffmpegtranscoder/main.cc @@ -11,7 +11,7 @@ #include #include -#define AppName "Transcoder" +#define AppName "FfmpegTranscoderr" void setAppInfo() { diff --git a/examples/transcoder/mainwindow.cc b/examples/ffmpegtranscoder/mainwindow.cc similarity index 100% rename from examples/transcoder/mainwindow.cc rename to examples/ffmpegtranscoder/mainwindow.cc diff --git a/examples/transcoder/mainwindow.hpp b/examples/ffmpegtranscoder/mainwindow.hpp similarity index 100% rename from examples/transcoder/mainwindow.hpp rename to examples/ffmpegtranscoder/mainwindow.hpp diff --git a/examples/transcoder/outputwidget.cc b/examples/ffmpegtranscoder/outputwidget.cc similarity index 100% rename from examples/transcoder/outputwidget.cc rename to examples/ffmpegtranscoder/outputwidget.cc diff --git a/examples/transcoder/outputwidget.hpp b/examples/ffmpegtranscoder/outputwidget.hpp similarity index 100% rename from examples/transcoder/outputwidget.hpp rename to examples/ffmpegtranscoder/outputwidget.hpp diff --git a/examples/transcoder/previewwidget.cc b/examples/ffmpegtranscoder/previewwidget.cc similarity index 100% rename from examples/transcoder/previewwidget.cc rename to examples/ffmpegtranscoder/previewwidget.cc diff --git a/examples/transcoder/previewwidget.hpp b/examples/ffmpegtranscoder/previewwidget.hpp similarity index 100% rename from examples/transcoder/previewwidget.hpp rename to examples/ffmpegtranscoder/previewwidget.hpp diff --git a/examples/transcoder/sourcewidget.cc b/examples/ffmpegtranscoder/sourcewidget.cc similarity index 100% rename from examples/transcoder/sourcewidget.cc rename to examples/ffmpegtranscoder/sourcewidget.cc diff --git a/examples/transcoder/sourcewidget.hpp b/examples/ffmpegtranscoder/sourcewidget.hpp similarity index 100% rename from examples/transcoder/sourcewidget.hpp rename to examples/ffmpegtranscoder/sourcewidget.hpp diff --git a/examples/transcoder/stautuswidget.cc b/examples/ffmpegtranscoder/stautuswidget.cc similarity index 100% rename from examples/transcoder/stautuswidget.cc rename to examples/ffmpegtranscoder/stautuswidget.cc diff --git a/examples/transcoder/stautuswidget.hpp b/examples/ffmpegtranscoder/stautuswidget.hpp similarity index 100% rename from examples/transcoder/stautuswidget.hpp rename to examples/ffmpegtranscoder/stautuswidget.hpp diff --git a/examples/transcoder/styleditemdelegate.cc b/examples/ffmpegtranscoder/styleditemdelegate.cc similarity index 100% rename from examples/transcoder/styleditemdelegate.cc rename to examples/ffmpegtranscoder/styleditemdelegate.cc diff --git a/examples/transcoder/styleditemdelegate.hpp b/examples/ffmpegtranscoder/styleditemdelegate.hpp similarity index 100% rename from examples/transcoder/styleditemdelegate.hpp rename to examples/ffmpegtranscoder/styleditemdelegate.hpp diff --git a/examples/transcoder/subtitleencodermodel.cc b/examples/ffmpegtranscoder/subtitleencodermodel.cc similarity index 100% rename from examples/transcoder/subtitleencodermodel.cc rename to examples/ffmpegtranscoder/subtitleencodermodel.cc diff --git a/examples/transcoder/subtitleencodermodel.hpp b/examples/ffmpegtranscoder/subtitleencodermodel.hpp similarity index 100% rename from examples/transcoder/subtitleencodermodel.hpp rename to examples/ffmpegtranscoder/subtitleencodermodel.hpp diff --git a/examples/transcoder/subtitleencodertableview.cc b/examples/ffmpegtranscoder/subtitleencodertableview.cc similarity index 100% rename from examples/transcoder/subtitleencodertableview.cc rename to examples/ffmpegtranscoder/subtitleencodertableview.cc diff --git a/examples/transcoder/subtitleencodertableview.hpp b/examples/ffmpegtranscoder/subtitleencodertableview.hpp similarity index 100% rename from examples/transcoder/subtitleencodertableview.hpp rename to examples/ffmpegtranscoder/subtitleencodertableview.hpp diff --git a/examples/transcoder/subtitleencoderwidget.cc b/examples/ffmpegtranscoder/subtitleencoderwidget.cc similarity index 100% rename from examples/transcoder/subtitleencoderwidget.cc rename to examples/ffmpegtranscoder/subtitleencoderwidget.cc diff --git a/examples/transcoder/subtitleencoderwidget.hpp b/examples/ffmpegtranscoder/subtitleencoderwidget.hpp similarity index 100% rename from examples/transcoder/subtitleencoderwidget.hpp rename to examples/ffmpegtranscoder/subtitleencoderwidget.hpp diff --git a/examples/transcoder/videoencoderwidget.cc b/examples/ffmpegtranscoder/videoencoderwidget.cc similarity index 100% rename from examples/transcoder/videoencoderwidget.cc rename to examples/ffmpegtranscoder/videoencoderwidget.cc diff --git a/examples/transcoder/videoencoderwidget.hpp b/examples/ffmpegtranscoder/videoencoderwidget.hpp similarity index 100% rename from examples/transcoder/videoencoderwidget.hpp rename to examples/ffmpegtranscoder/videoencoderwidget.hpp diff --git a/examples/mpvplayer/CMakeLists.txt b/examples/mpvplayer/CMakeLists.txt new file mode 100644 index 0000000..1c0ee4b --- /dev/null +++ b/examples/mpvplayer/CMakeLists.txt @@ -0,0 +1,51 @@ +set(PROJECT_SOURCES + ../common/controlwidget.cc + ../common/controlwidget.hpp + ../common/openwebmediadialog.cc + ../common/openwebmediadialog.hpp + ../common/playlistmodel.cpp + ../common/playlistmodel.h + ../common/playlistview.cc + ../common/playlistview.hpp + ../common/qmediaplaylist_p.h + ../common/qmediaplaylist.cpp + ../common/qmediaplaylist.h + ../common/qplaylistfileparser_p.h + ../common/qplaylistfileparser.cpp + ../common/slider.cpp + ../common/slider.h + ../common/titlewidget.cc + ../common/titlewidget.hpp + main.cc + mainwindow.cc + mainwindow.hpp + mpvlogwindow.cc + mpvlogwindow.hpp + subtitledelaydialog.cc + subtitledelaydialog.hpp) + +qt_add_executable(MpvPlayer MANUAL_FINALIZATION ${PROJECT_SOURCES}) +target_compile_definitions(MpvPlayer PRIVATE "MPV_ON") +target_link_libraries( + MpvPlayer + PRIVATE custommpv + thirdparty + dump + utils + Qt6::Widgets + Qt6::Multimedia + Qt6::Network + Qt6::OpenGLWidgets) +if(CMAKE_HOST_WIN32) + target_link_libraries(MpvPlayer PRIVATE C:\\3rd\\x64\\mpv\\libmpv.dll.a) + file(COPY C:\\3rd\\x64\\mpv\\libmpv-2.dll + DESTINATION ${EXECUTABLE_OUTPUT_PATH}/) +elseif(CMAKE_HOST_APPLE) + target_link_directories(MpvPlayer PRIVATE /usr/lib) + target_link_directories(MpvPlayer PRIVATE /usr/local/lib) + target_link_libraries(MpvPlayer PRIVATE mpv) +elseif(CMAKE_HOST_UNIX) + target_link_libraries(MpvPlayer PRIVATE mpv) +endif() + +qt_finalize_executable(MpvPlayer) diff --git a/examples/mpvplayer/main.cc b/examples/mpvplayer/main.cc new file mode 100644 index 0000000..5f18ddd --- /dev/null +++ b/examples/mpvplayer/main.cc @@ -0,0 +1,88 @@ +#include "mainwindow.hpp" + +#include <3rdparty/qtsingleapplication/qtsingleapplication.h> +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define AppName "MpvPlayer" + +void setAppInfo() +{ + qApp->setApplicationVersion(AppInfo::version.toString()); + qApp->setApplicationDisplayName(AppName); + qApp->setApplicationName(AppName); + qApp->setDesktopFileName(AppName); + qApp->setOrganizationDomain(AppInfo::organizationDomain); + qApp->setOrganizationName(AppInfo::organzationName); + qApp->setWindowIcon(qApp->style()->standardIcon(QStyle::SP_MediaPlay)); +} + +int main(int argc, char *argv[]) +{ +#if defined(Q_OS_WIN) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + if (!qEnvironmentVariableIsSet("QT_OPENGL")) { + QCoreApplication::setAttribute(Qt::AA_UseOpenGLES); + } +#else + qputenv("QSG_RHI_BACKEND", "opengl"); +#endif + Utils::setHighDpiEnvironmentVariable(); + + Utils::setSurfaceFormatVersion(3, 3); + + SharedTools::QtSingleApplication::setAttribute(Qt::AA_ShareOpenGLContexts); + SharedTools::QtSingleApplication app(AppName, argc, argv); + if (app.isRunning()) { + qWarning() << "This is already running"; + if (app.sendMessage("raise_window_noop", 5000)) { + return EXIT_SUCCESS; + } + } +#ifdef Q_OS_WIN + if (!qFuzzyCompare(app.devicePixelRatio(), 1.0) + && QApplication::style()->objectName().startsWith(QLatin1String("windows"), + Qt::CaseInsensitive)) { + QApplication::setStyle(QLatin1String("fusion")); + } +#endif +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + app.setAttribute(Qt::AA_UseHighDpiPixmaps); + app.setAttribute(Qt::AA_DisableWindowContextHelpButton); +#endif + setAppInfo(); + Dump::BreakPad::instance()->setDumpPath(Utils::crashPath()); + QDir::setCurrent(app.applicationDirPath()); + + // 异步日志 + auto *log = Utils::LogAsync::instance(); + log->setLogPath(Utils::logPath()); + log->setAutoDelFile(true); + log->setAutoDelFileDays(7); + log->setOrientation(Utils::LogAsync::Orientation::StdAndFile); + log->setLogLevel(QtDebugMsg); + log->startWork(); + + // Make sure we honor the system's proxy settings + QNetworkProxyFactory::setUseSystemConfiguration(true); + + // Qt sets the locale in the QApplication constructor, but libmpv requires + // the LC_NUMERIC category to be set to "C", so change it back. + std::setlocale(LC_NUMERIC, "C"); + + MainWindow w; + app.setActivationWindow(&w); + w.show(); + + auto ret = app.exec(); + log->stop(); + return ret; +} diff --git a/examples/mpvplayer/mainwindow.cc b/examples/mpvplayer/mainwindow.cc new file mode 100644 index 0000000..05131da --- /dev/null +++ b/examples/mpvplayer/mainwindow.cc @@ -0,0 +1,758 @@ +#include "mainwindow.hpp" +#include "mpvlogwindow.hpp" +#include "subtitledelaydialog.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static auto isPlaylist(const QUrl &url) -> bool // Check for ".m3u" playlists. +{ + if (!url.isLocalFile()) { + return false; + } + const QFileInfo fileInfo(url.toLocalFile()); + return fileInfo.exists() + && (fileInfo.suffix().compare(QLatin1String("m3u"), Qt::CaseInsensitive) == 0); +} + +class MainWindow::MainWindowPrivate +{ +public: + explicit MainWindowPrivate(MainWindow *q) + : q_ptr(q) + { + mpvPlayer = new Mpv::MpvPlayer(q_ptr); +#ifdef Q_OS_WIN + mpvWidget = new Mpv::MpvWidget(q_ptr); + mpvPlayer->initMpv(mpvWidget); +#else + mpvWidget = new Mpv::MpvOpenglWidget(mpvPlayer, q_ptr); + mpvPlayer->initMpv(nullptr); +#endif + mpvWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + mpvWidget->setAcceptDrops(true); + mpvPlayer->setPrintToStd(true); + + logWindow = new Mpv::MpvLogWindow(q_ptr); + logWindow->setMinimumSize(500, 325); + logWindow->show(); + logWindow->move(qApp->primaryScreen()->availableGeometry().topLeft()); + + controlWidget = new ControlWidget(q_ptr); + controlWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + titleWidget = new TitleWidget(q_ptr); + titleWidget->setMinimumHeight(80); + + playlistModel = new PlaylistModel(q_ptr); + playlistView = new PlayListView(q_ptr); + playlistView->setModel(playlistModel); + playlistView->setCurrentIndex( + playlistModel->index(playlistModel->playlist()->currentIndex(), 0)); + //playlistView->setMaximumWidth(250); + + menu = new QMenu(q_ptr); + gpuAction = new QAction(QCoreApplication::translate("MainWindowPrivate", "H/W"), q_ptr); + gpuAction->setCheckable(true); + videoMenu = new QMenu(QCoreApplication::translate("MainWindowPrivate", "Video"), q_ptr); + audioMenu = new QMenu(QCoreApplication::translate("MainWindowPrivate", "Audio"), q_ptr); + subMenu = new QMenu(QCoreApplication::translate("MainWindowPrivate", "Subtitles"), q_ptr); + subDelayAction = new QAction(QCoreApplication::translate("MainWindowPrivate", "Delay"), + q_ptr); + + loadSubTitlesAction = new QAction(QCoreApplication::translate("MainWindowPrivate", + "Load Subtitles"), + q_ptr); + videoTracksGroup = new QActionGroup(q_ptr); + videoTracksGroup->setExclusive(true); + audioTracksGroup = new QActionGroup(q_ptr); + audioTracksGroup->setExclusive(true); + subTracksGroup = new QActionGroup(q_ptr); + subTracksGroup->setExclusive(true); + + playListMenu = new QMenu(q_ptr); + + initShortcut(); + } + + void resetTrackMenu() + { + auto actions = audioTracksGroup->actions(); + for (auto *action : actions) { + audioTracksGroup->removeAction(action); + delete action; + } + actions = videoTracksGroup->actions(); + for (auto *action : actions) { + videoTracksGroup->removeAction(action); + delete action; + } + actions = subTracksGroup->actions(); + for (auto *action : actions) { + subTracksGroup->removeAction(action); + delete action; + } + + if (!audioTracksMenuPtr.isNull()) { + delete audioTracksMenuPtr.data(); + } + if (!videoTracksMenuPtr.isNull()) { + delete videoTracksMenuPtr.data(); + } + if (!subTracksMenuPtr.isNull()) { + delete subTracksMenuPtr.data(); + } + audioTracksMenuPtr = new QMenu(QCoreApplication::translate("MainWindowPrivate", + "Select audio track"), + q_ptr); + videoTracksMenuPtr = new QMenu(QCoreApplication::translate("MainWindowPrivate", + "Select video track"), + q_ptr); + subTracksMenuPtr = new QMenu(QCoreApplication::translate("MainWindowPrivate", + "Select subtitle track"), + q_ptr); + subTracksMenuPtr->addAction(loadSubTitlesAction); + subTracksMenuPtr->addSeparator(); + + videoMenu->addMenu(videoTracksMenuPtr.data()); + audioMenu->addMenu(audioTracksMenuPtr.data()); + subMenu->addMenu(subTracksMenuPtr.data()); + subMenu->addAction(subDelayAction); + } + + void initShortcut() + { + new QShortcut(QKeySequence::MoveToNextChar, q_ptr, q_ptr, [this] { + mpvPlayer->seekRelative(5); + titleWidget->setText( + QCoreApplication::translate("MainWindowPrivate", "Fast forward: 5 seconds")); + titleWidget->setAutoHide(3000); + setTitleWidgetGeometry(true); + }); + new QShortcut(QKeySequence::MoveToPreviousChar, q_ptr, q_ptr, [this] { + mpvPlayer->seekRelative(-5); + titleWidget->setText( + QCoreApplication::translate("MainWindowPrivate", "Fast return: 5 seconds")); + titleWidget->setAutoHide(3000); + setTitleWidgetGeometry(true); + }); + new QShortcut(QKeySequence::MoveToPreviousLine, q_ptr, q_ptr, [this] { + controlWidget->setVolume(controlWidget->volume() + 10); + }); + new QShortcut(QKeySequence::MoveToNextLine, q_ptr, q_ptr, [this] { + controlWidget->setVolume(controlWidget->volume() - 10); + }); + new QShortcut(Qt::Key_Space, q_ptr, q_ptr, [this] { pause(); }); + } + + void setupUI() const + { +#ifndef Q_OS_WIN + auto controlLayout = new QHBoxLayout; + controlLayout->addStretch(); + controlLayout->addWidget(controlWidget); + controlLayout->addStretch(); + auto layout = new QVBoxLayout(mpvWidget); + layout->addWidget(titleWidget); + layout->addStretch(); + layout->addLayout(controlLayout); +#endif + + auto *splitter = new QSplitter(q_ptr); + splitter->setHandleWidth(0); + splitter->addWidget(mpvWidget); + splitter->addWidget(playlistView); + splitter->setSizes({200, 1}); + + q_ptr->setCentralWidget(splitter); + } + + void setControlWidgetGeometry(bool show = true) + { +#ifdef Q_OS_WIN + controlWidget->setFixedSize({QWIDGETSIZE_MAX, QWIDGETSIZE_MAX}); + controlWidget->setMinimumWidth(mpvWidget->width() / 2); + controlWidget->adjustSize(); + if (controlWidget->width() * 2 < mpvWidget->width()) { + controlWidget->setMinimumWidth(mpvWidget->width() / 2); + } + auto margain = 10; + auto geometry = mpvWidget->geometry(); + auto p1 = QPoint(geometry.x() + (geometry.width() - controlWidget->width()) / 2.0, + geometry.bottomLeft().y() - controlWidget->height() - margain); + globalControlWidgetGeometry = {q_ptr->mapToGlobal(p1), controlWidget->size()}; + controlWidget->setFixedSize(globalControlWidgetGeometry.size()); + controlWidget->setGeometry(globalControlWidgetGeometry); + controlWidget->setVisible(show); +#else + controlWidget->setVisible(show); +#endif + } + + void setTitleWidgetGeometry(bool show = true) + { +#ifdef Q_OS_WIN + auto margain = 10; + auto geometry = mpvWidget->geometry(); + auto p1 = QPoint(geometry.x() + margain, geometry.y() + margain); + auto p2 = QPoint(geometry.topRight().x() - margain, geometry.y() + margain + 80); + globalTitlelWidgetGeometry = {q_ptr->mapToGlobal(p1), q_ptr->mapToGlobal(p2)}; + titleWidget->setFixedSize(globalTitlelWidgetGeometry.size()); + titleWidget->setGeometry(globalTitlelWidgetGeometry); + titleWidget->setVisible(show); +#else + titleWidget->setVisible(show); +#endif + } + + void pause() const { mpvPlayer->pauseAsync(); } + + MainWindow *q_ptr; + + Mpv::MpvPlayer *mpvPlayer; + QWidget *mpvWidget; + QScopedPointer previewWidgetPtr; + Mpv::MpvLogWindow *logWindow; + + ControlWidget *controlWidget; + TitleWidget *titleWidget; +#ifdef Q_OS_WIN + QRect globalTitlelWidgetGeometry; + QRect globalControlWidgetGeometry; +#endif + + PlayListView *playlistView; + PlaylistModel *playlistModel; + + QMenu *menu; + QAction *gpuAction; + + QMenu *videoMenu; + QPointer videoTracksMenuPtr; + QActionGroup *videoTracksGroup; + + QMenu *audioMenu; + QPointer audioTracksMenuPtr; + QActionGroup *audioTracksGroup; + + QMenu *subMenu; + QAction *subDelayAction; + QPointer subTracksMenuPtr; + QActionGroup *subTracksGroup; + QAction *loadSubTitlesAction; + + QMenu *playListMenu; +}; + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , d_ptr(new MainWindowPrivate(this)) +{ + d_ptr->setupUI(); + buildConnect(); + initMenu(); + initPlayListMenu(); + + setAttribute(Qt::WA_Hover); + d_ptr->mpvWidget->installEventFilter(this); + d_ptr->playlistView->installEventFilter(this); + installEventFilter(this); + + resize(1000, 618); +} + +MainWindow::~MainWindow() +{ + delete d_ptr->mpvWidget; +} + +void MainWindow::onOpenLocalMedia() +{ + const auto path = QStandardPaths::standardLocations(QStandardPaths::MoviesLocation) + .value(0, QDir::homePath()); + const auto filter = tr("Media (*)"); + const auto urls = QFileDialog::getOpenFileUrls(this, + tr("Open Media"), + QUrl::fromUserInput(path), + filter); + if (urls.isEmpty()) { + return; + } + addToPlaylist(urls); +} + +void MainWindow::onOpenWebMedia() +{ + OpenWebMediaDialog dialog(this); + dialog.setMinimumSize(size() / 2.0); + if (dialog.exec() != QDialog::Accepted) { + return; + } + addToPlaylist({QUrl(dialog.url())}); +} + +void MainWindow::onLoadSubtitleFiles() +{ + auto path = d_ptr->mpvPlayer->filepath(); + if (path.isEmpty()) { + return; + } + if (QFile::exists(path)) { + path = QFileInfo(path).absolutePath(); + } else { + path = QStandardPaths::standardLocations(QStandardPaths::MoviesLocation) + .value(0, QDir::homePath()); + } + const auto filePaths = QFileDialog::getOpenFileNames(this, + tr("Open File"), + path, + tr("Subtitle(*)")); + if (filePaths.isEmpty()) { + return; + } + d_ptr->mpvPlayer->addSub(filePaths); +} + +void MainWindow::onShowSubtitleDelayDialog() +{ + SubtitleDelayDialog dialog(this); + dialog.setDelay(d_ptr->mpvPlayer->subtitleDelay()); + connect(&dialog, &SubtitleDelayDialog::delayChanged, d_ptr->mpvPlayer, [&] { + d_ptr->mpvPlayer->setSubtitleDelay(dialog.delay()); + }); + dialog.exec(); +} + +void MainWindow::onFileLoaded() +{ + setWindowTitle(d_ptr->mpvPlayer->filename()); + d_ptr->controlWidget->setDuration(d_ptr->mpvPlayer->duration()); +} + +void MainWindow::onTrackChanged() +{ + d_ptr->resetTrackMenu(); + + auto videoTracks = d_ptr->mpvPlayer->videoTracks(); + for (const auto &item : std::as_const(videoTracks)) { + auto *action = new QAction(item.text(), this); + action->setData(QVariant::fromValue(item)); + action->setCheckable(true); + d_ptr->videoTracksMenuPtr->addAction(action); + d_ptr->videoTracksGroup->addAction(action); + if (item.selected) { + action->setChecked(true); + } + } + + auto audioTracks = d_ptr->mpvPlayer->audioTracks(); + for (const auto &item : std::as_const(audioTracks)) { + auto *action = new QAction(item.text(), this); + action->setData(QVariant::fromValue(item)); + action->setCheckable(true); + d_ptr->audioTracksMenuPtr->addAction(action); + d_ptr->audioTracksGroup->addAction(action); + if (item.selected) { + action->setChecked(true); + } + } + + auto subTracks = d_ptr->mpvPlayer->subTracks(); + for (const auto &item : std::as_const(subTracks)) { + auto *action = new QAction(item.text(), this); + action->setData(QVariant::fromValue(item)); + action->setCheckable(true); + d_ptr->subTracksMenuPtr->addAction(action); + d_ptr->subTracksGroup->addAction(action); + if (item.selected) { + action->setChecked(true); + } + } +} + +void MainWindow::onChapterChanged() +{ + d_ptr->controlWidget->setChapters(d_ptr->mpvPlayer->chapters()); +} + +void MainWindow::onRenderChanged(QAction *action) +{ + auto value = static_cast(action->data().toInt()); + d_ptr->mpvPlayer->initMpv(d_ptr->mpvWidget); + d_ptr->mpvPlayer->setGpuApi(value); + d_ptr->mpvPlayer->setPrintToStd(true); + d_ptr->mpvPlayer->setUseGpu(d_ptr->gpuAction->isChecked()); + auto index = d_ptr->playlistModel->playlist()->currentIndex(); + if (index > -1) { + playlistPositionChanged(d_ptr->playlistModel->playlist()->currentIndex()); + } +} + +void MainWindow::onPreview(int pos, int value) +{ + auto url = d_ptr->mpvPlayer->filepath(); + if (url.isEmpty()) { + return; + } + if (d_ptr->previewWidgetPtr.isNull()) { + d_ptr->previewWidgetPtr.reset(new Mpv::PreviewWidget); + d_ptr->previewWidgetPtr->setWindowFlags(d_ptr->previewWidgetPtr->windowFlags() | Qt::Tool + | Qt::FramelessWindowHint + | Qt::WindowStaysOnTopHint); + } + d_ptr->previewWidgetPtr->startPreview(url, value); + int w = 320; + int h = 200; + d_ptr->previewWidgetPtr->setFixedSize(w, h); + auto gpos = d_ptr->controlWidget->sliderGlobalPos() + QPoint(pos, 0); + d_ptr->previewWidgetPtr->move(gpos - QPoint(w / 2, h + 15)); + d_ptr->previewWidgetPtr->show(); +} + +void MainWindow::onPreviewFinish() +{ + if (!d_ptr->previewWidgetPtr.isNull()) { + d_ptr->previewWidgetPtr->hide(); + d_ptr->previewWidgetPtr->clearAllTask(); + } +} + +void MainWindow::playlistPositionChanged(int currentItem) +{ + if (currentItem < 0) { + return; + } + d_ptr->playlistView->setCurrentIndex(d_ptr->playlistModel->index(currentItem, 0)); + auto url = d_ptr->playlistModel->playlist()->currentMedia(); + d_ptr->mpvPlayer->openMedia(url.isLocalFile() ? url.toLocalFile() : url.toString()); + if (d_ptr->mpvPlayer->isPaused()) { + d_ptr->mpvPlayer->pauseAsync(); + } else { + d_ptr->controlWidget->setPlayButtonChecked(true); + } +} + +auto MainWindow::eventFilter(QObject *watched, QEvent *event) -> bool +{ + if (watched == d_ptr->mpvWidget) { + switch (event->type()) { + case QEvent::DragEnter: { + auto *e = static_cast(event); + e->acceptProposedAction(); + } break; + case QEvent::DragMove: { + auto *e = static_cast(event); + e->acceptProposedAction(); + } break; + case QEvent::Drop: { + auto *e = static_cast(event); + QList urls = e->mimeData()->urls(); + if (!urls.isEmpty()) { + addToPlaylist(urls); + } + } break; + case QEvent::ContextMenu: { + auto *e = static_cast(event); + d_ptr->menu->exec(e->globalPos()); + } break; +#ifdef Q_OS_WIN + case QEvent::Resize: + QMetaObject::invokeMethod( + this, + [=] { + d_ptr->setControlWidgetGeometry(d_ptr->controlWidget->isVisible()); + d_ptr->setTitleWidgetGeometry(d_ptr->titleWidget->isVisible()); + }, + Qt::QueuedConnection); + break; +#endif + // case QEvent::MouseButtonPress: { + // auto e = static_cast(event); + // if (e->button() & Qt::LeftButton) { + // d_ptr->mpvPlayer->pause(); + // } + // } break; + case QEvent::MouseButtonDblClick: + if (isFullScreen()) { + showNormal(); + } else { + d_ptr->playlistView->hide(); + showFullScreen(); + } + break; + default: break; + } + } else if (watched == d_ptr->playlistView) { + switch (event->type()) { + case QEvent::ContextMenu: { + auto *e = static_cast(event); + d_ptr->playListMenu->exec(e->globalPos()); + } break; + default: break; + } + } else if (watched == this) { + switch (event->type()) { +#ifdef Q_OS_WIN + case QEvent::Show: + case QEvent::Move: + QMetaObject::invokeMethod( + this, + [=] { + d_ptr->setControlWidgetGeometry(d_ptr->controlWidget->isVisible()); + d_ptr->setTitleWidgetGeometry(d_ptr->titleWidget->isVisible()); + }, + Qt::QueuedConnection); + break; + case QEvent::Hide: d_ptr->controlWidget->hide(); break; + case QEvent::HoverMove: + if (d_ptr->globalControlWidgetGeometry.isValid()) { + auto *e = static_cast(event); + bool contain = d_ptr->globalControlWidgetGeometry.contains( + e->globalPosition().toPoint()); + bool isVisible = d_ptr->controlWidget->isVisible(); + if (contain && !isVisible) { + d_ptr->setControlWidgetGeometry(true); + } else if (!contain && isVisible) { + d_ptr->controlWidget->hide(); + } + } + if (d_ptr->globalTitlelWidgetGeometry.isValid() && isFullScreen()) { + auto *e = static_cast(event); + bool contain = d_ptr->globalTitlelWidgetGeometry.contains( + e->globalPosition().toPoint()); + bool isVisible = d_ptr->titleWidget->isVisible(); + if (contain && !isVisible) { + d_ptr->titleWidget->setText(windowTitle()); + d_ptr->setTitleWidgetGeometry(true); + } else if (!contain && isVisible) { + d_ptr->titleWidget->hide(); + } + } + break; +#else + case QEvent::HoverMove: { + d_ptr->controlWidget->show(); + d_ptr->controlWidget->hide(); + auto controlWidgetGeometry = d_ptr->controlWidget->geometry(); + auto e = static_cast(event); + bool contain = controlWidgetGeometry.contains(e->position().toPoint()); + d_ptr->setControlWidgetGeometry(contain); + if (isFullScreen()) { + d_ptr->titleWidget->show(); + d_ptr->titleWidget->hide(); + auto titleWidgetGeometry = d_ptr->titleWidget->geometry(); + contain = titleWidgetGeometry.contains(e->position().toPoint()); + d_ptr->setTitleWidgetGeometry(contain); + } + break; + } +#endif + default: break; + } + } + return QMainWindow::eventFilter(watched, event); +} + +void MainWindow::keyPressEvent(QKeyEvent *ev) +{ + QMainWindow::keyPressEvent(ev); + + qDebug() << ev->key(); + switch (ev->key()) { + case Qt::Key_Escape: + if (isFullScreen()) { + showNormal(); + } else { + showMinimized(); + } + break; + case Qt::Key_Q: qApp->quit(); break; + default: break; + } +} + +void MainWindow::buildConnect() +{ + connect(d_ptr->mpvPlayer, &Mpv::MpvPlayer::fileLoaded, this, &MainWindow::onFileLoaded); + connect(d_ptr->mpvPlayer, + &Mpv::MpvPlayer::fileFinished, + d_ptr->playlistModel->playlist(), + &QMediaPlaylist::next); + connect(d_ptr->mpvPlayer, &Mpv::MpvPlayer::chapterChanged, this, &MainWindow::onChapterChanged); + connect(d_ptr->mpvPlayer, &Mpv::MpvPlayer::trackChanged, this, &MainWindow::onTrackChanged); + connect(d_ptr->mpvPlayer, + &Mpv::MpvPlayer::positionChanged, + d_ptr->controlWidget, + &ControlWidget::setPosition); + connect(d_ptr->mpvPlayer, + &Mpv::MpvPlayer::mpvLogMessage, + d_ptr->logWindow, + &Mpv::MpvLogWindow::onAppendLog); + connect(d_ptr->mpvPlayer, &Mpv::MpvPlayer::pauseStateChanged, this, [this](bool pause) { + d_ptr->controlWidget->setPlayButtonChecked(!pause); + }); + connect(d_ptr->mpvPlayer, + &Mpv::MpvPlayer::cacheSpeedChanged, + d_ptr->controlWidget, + &ControlWidget::setCacheSpeed); + + connect(d_ptr->controlWidget, + &ControlWidget::previous, + d_ptr->playlistModel->playlist(), + &QMediaPlaylist::previous); + connect(d_ptr->controlWidget, + &ControlWidget::next, + d_ptr->playlistModel->playlist(), + &QMediaPlaylist::next); + connect(d_ptr->controlWidget, &ControlWidget::seek, d_ptr->mpvPlayer, [this](int value) { + d_ptr->mpvPlayer->seek(value); + d_ptr->titleWidget->setText( + tr("Fast forward to: %1") + .arg(QTime::fromMSecsSinceStartOfDay(value * 1000).toString("hh:mm:ss"))); + d_ptr->titleWidget->setAutoHide(3000); + d_ptr->setTitleWidgetGeometry(true); + }); + connect(d_ptr->controlWidget, &ControlWidget::hoverPosition, this, &MainWindow::onPreview); + connect(d_ptr->controlWidget, &ControlWidget::leavePosition, this, &MainWindow::onPreviewFinish); + connect(d_ptr->controlWidget, &ControlWidget::volumeChanged, d_ptr->mpvPlayer, [this](int value) { + d_ptr->mpvPlayer->setVolume(value); + d_ptr->titleWidget->setText(tr("Volume: %1%").arg(value)); + d_ptr->titleWidget->setAutoHide(3000); + d_ptr->setTitleWidgetGeometry(true); + }); + auto max = d_ptr->mpvPlayer->volumeMax(); + d_ptr->controlWidget->setVolumeMax(max); + d_ptr->controlWidget->setVolume(max / 2); + connect(d_ptr->controlWidget, + &ControlWidget::speedChanged, + d_ptr->mpvPlayer, + [this](double value) { + d_ptr->mpvPlayer->setSpeed(value); + d_ptr->titleWidget->setText(tr("Speed: %1").arg(value)); + d_ptr->titleWidget->setAutoHide(3000); + d_ptr->setTitleWidgetGeometry(true); + }); + connect(d_ptr->controlWidget, + &ControlWidget::modelChanged, + d_ptr->playlistModel->playlist(), + [this](int model) { + d_ptr->playlistModel->playlist()->setPlaybackMode( + static_cast(model)); + }); + connect(d_ptr->controlWidget, &ControlWidget::showList, d_ptr->playlistView, [this] { + d_ptr->playlistView->setVisible(!d_ptr->playlistView->isVisible()); + }); + + connect(d_ptr->playlistModel->playlist(), + &QMediaPlaylist::currentIndexChanged, + this, + &MainWindow::playlistPositionChanged); + connect(d_ptr->playlistView, &QAbstractItemView::activated, this, &MainWindow::jump); +} + +void MainWindow::initMenu() +{ + d_ptr->menu->addAction(tr("Open Local Media"), this, &MainWindow::onOpenLocalMedia); + d_ptr->menu->addAction(tr("Open Web Media"), this, &MainWindow::onOpenWebMedia); + + d_ptr->menu->addAction(d_ptr->gpuAction); + connect(d_ptr->gpuAction, &QAction::toggled, d_ptr->mpvPlayer, [this](bool checked) { + d_ptr->mpvPlayer->setUseGpu(checked); + }); + d_ptr->gpuAction->setChecked(true); +#ifdef Q_OS_WIN + auto *menu = new QMenu(tr("Render"), this); + auto *actionGroup = new QActionGroup(this); + actionGroup->setExclusive(true); + auto metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < metaEnum.keyCount(); i++) { + auto value = metaEnum.value(i); + auto *action = new QAction(metaEnum.valueToKey(value), this); + action->setData(value); + action->setCheckable(true); + actionGroup->addAction(action); + menu->addAction(action); + action->setChecked(i == 0); + } + connect(actionGroup, &QActionGroup::triggered, this, &MainWindow::onRenderChanged); + d_ptr->menu->addMenu(menu); +#endif + d_ptr->menu->addMenu(d_ptr->videoMenu); + d_ptr->menu->addMenu(d_ptr->audioMenu); + d_ptr->menu->addMenu(d_ptr->subMenu); + + connect(d_ptr->videoTracksGroup, &QActionGroup::triggered, this, [this](QAction *action) { + auto data = action->data().value(); + d_ptr->mpvPlayer->setVideoTrack(data.id); + }); + connect(d_ptr->audioTracksGroup, &QActionGroup::triggered, this, [this](QAction *action) { + auto data = action->data().value(); + d_ptr->mpvPlayer->setAudioTrack(data.id); + }); + connect(d_ptr->subTracksGroup, &QActionGroup::triggered, this, [this](QAction *action) { + auto data = action->data().value(); + d_ptr->mpvPlayer->setSubTrack(data.id); + }); + connect(d_ptr->loadSubTitlesAction, &QAction::triggered, this, &MainWindow::onLoadSubtitleFiles); + connect(d_ptr->subDelayAction, + &QAction::triggered, + this, + &MainWindow::onShowSubtitleDelayDialog); +#ifdef Q_OS_MACOS + d_ptr->menu->setTitle(tr("Menu")); + menuBar()->addMenu(d_ptr->menu); +#endif +} + +void MainWindow::initPlayListMenu() +{ + d_ptr->playListMenu->addAction(tr("Open Local Media"), this, &MainWindow::onOpenLocalMedia); + d_ptr->playListMenu->addAction(tr("Open Web Media"), this, &MainWindow::onOpenWebMedia); + d_ptr->playListMenu + ->addAction(tr("Remove the currently selected item"), d_ptr->playlistView, [this] { + auto indexs = d_ptr->playlistView->selectedAllIndexs(); + std::sort(indexs.begin(), indexs.end(), [&](QModelIndex left, QModelIndex right) { + return left.row() > right.row(); + }); + for (const auto &index : std::as_const(indexs)) { + d_ptr->playlistModel->playlist()->removeMedia(index.row()); + } + }); + d_ptr->playListMenu->addAction(tr("Clear"), d_ptr->playlistView, [this] { + d_ptr->playlistModel->playlist()->clear(); + }); +} + +void MainWindow::addToPlaylist(const QList &urls) +{ + auto *playlist = d_ptr->playlistModel->playlist(); + const int previousMediaCount = playlist->mediaCount(); + for (const auto &url : std::as_const(urls)) { + if (isPlaylist(url)) { + playlist->load(url); + } else { + playlist->addMedia(url); + } + } + if (playlist->mediaCount() > previousMediaCount) { + auto index = d_ptr->playlistModel->index(previousMediaCount, 0); + d_ptr->playlistView->setCurrentIndex(index); + jump(index); + } +} + +void MainWindow::jump(const QModelIndex &index) +{ + if (index.isValid()) { + d_ptr->playlistModel->playlist()->setCurrentIndex(index.row()); + } +} diff --git a/examples/mpvplayer/mainwindow.hpp b/examples/mpvplayer/mainwindow.hpp new file mode 100644 index 0000000..a11b7bb --- /dev/null +++ b/examples/mpvplayer/mainwindow.hpp @@ -0,0 +1,42 @@ +#ifndef MAINWINDOW_HPP +#define MAINWINDOW_HPP + +#include + +class MainWindow : public QMainWindow +{ + Q_OBJECT +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private slots: + void onOpenLocalMedia(); + void onOpenWebMedia(); + void onLoadSubtitleFiles(); + void onShowSubtitleDelayDialog(); + void onFileLoaded(); + void onTrackChanged(); + void onChapterChanged(); + void onRenderChanged(QAction *action); + + void onPreview(int pos, int value); + void onPreviewFinish(); + + void playlistPositionChanged(int); + void jump(const QModelIndex &index); + +protected: + auto eventFilter(QObject *watched, QEvent *event) -> bool override; + void keyPressEvent(QKeyEvent *ev) override; + +private: + void buildConnect(); + void initMenu(); + void initPlayListMenu(); + void addToPlaylist(const QList &urls); + + class MainWindowPrivate; + QScopedPointer d_ptr; +}; +#endif // MAINWINDOW_HPP diff --git a/examples/mpvplayer/mpvlogwindow.cc b/examples/mpvplayer/mpvlogwindow.cc new file mode 100644 index 0000000..9ea175b --- /dev/null +++ b/examples/mpvplayer/mpvlogwindow.cc @@ -0,0 +1,37 @@ +#include "mpvlogwindow.hpp" + +#include + +namespace Mpv { + +class MpvLogWindow::MpvLogWindowPrivate +{ +public: + MpvLogWindowPrivate(MpvLogWindow *parent) + : owner(parent) + { + textEdit = new QTextEdit(owner); + textEdit->setReadOnly(true); + } + + MpvLogWindow *owner; + + QTextEdit *textEdit; +}; + +MpvLogWindow::MpvLogWindow(QWidget *parent) + : QMainWindow{parent} + , d_ptr(new MpvLogWindowPrivate(this)) +{ + setWindowTitle("mpv log window"); + setCentralWidget(d_ptr->textEdit); +} + +MpvLogWindow::~MpvLogWindow() {} + +void MpvLogWindow::onAppendLog(const QString &log) +{ + d_ptr->textEdit->append(log); +} + +} // namespace Mpv diff --git a/examples/mpvplayer/mpvlogwindow.hpp b/examples/mpvplayer/mpvlogwindow.hpp new file mode 100644 index 0000000..1523551 --- /dev/null +++ b/examples/mpvplayer/mpvlogwindow.hpp @@ -0,0 +1,25 @@ +#ifndef MPVLOGWINDOW_HPP +#define MPVLOGWINDOW_HPP + +#include + +namespace Mpv { + +class MpvLogWindow : public QMainWindow +{ + Q_OBJECT +public: + explicit MpvLogWindow(QWidget *parent = nullptr); + ~MpvLogWindow() override; + +public slots: + void onAppendLog(const QString &log); + +private: + class MpvLogWindowPrivate; + QScopedPointer d_ptr; +}; + +} // namespace Mpv + +#endif // MPVLOGWINDOW_HPP diff --git a/examples/mpvplayer/mpvplayer.pro b/examples/mpvplayer/mpvplayer.pro new file mode 100644 index 0000000..d3a009a --- /dev/null +++ b/examples/mpvplayer/mpvplayer.pro @@ -0,0 +1,52 @@ +include(../../common.pri) + +QT += core gui widgets network multimedia openglwidgets core5compat + +TEMPLATE = app + +TARGET = MpvPlayer + +DEFINES += MPV_ON + +LIBS += \ + -l$$replaceLibName(custommpv) \ + -l$$replaceLibName(thirdparty) \ + -l$$replaceLibName(dump) \ + -l$$replaceLibName(utils) + +include(../../src/3rdparty/3rdparty.pri) +include(../../src/mpv/mpv.pri) + +SOURCES += \ + ../common/controlwidget.cc \ + ../common/openwebmediadialog.cc \ + ../common/playlistmodel.cpp \ + ../common/playlistview.cc \ + ../common/qmediaplaylist.cpp \ + ../common/qplaylistfileparser.cpp \ + ../common/slider.cpp \ + ../common/titlewidget.cc \ + main.cc \ + mainwindow.cc \ + mpvlogwindow.cc \ + subtitledelaydialog.cc + +HEADERS += \ + ../common/controlwidget.hpp \ + ../common/openwebmediadialog.hpp \ + ../common/playlistmodel.h \ + ../common/playlistview.hpp \ + ../common/qmediaplaylist.h \ + ../common/qmediaplaylist_p.h \ + ../common/qplaylistfileparser_p.h \ + ../common/slider.h \ + ../common/titlewidget.hpp \ + mainwindow.hpp \ + mpvlogwindow.hpp \ + subtitledelaydialog.hpp + +DESTDIR = $$APP_OUTPUT_PATH + +win32{ + QMAKE_POST_LINK += $$QMAKE_COPY_FILE C:\\3rd\\x64\\mpv\\libmpv-2.dll $$replace(APP_OUTPUT_PATH, /, \\)\\libmpv-2.dll +} diff --git a/examples/mpvplayer/subtitledelaydialog.cc b/examples/mpvplayer/subtitledelaydialog.cc new file mode 100644 index 0000000..d4335d6 --- /dev/null +++ b/examples/mpvplayer/subtitledelaydialog.cc @@ -0,0 +1,57 @@ +#include "subtitledelaydialog.hpp" + +#include + +class SubtitleDelayDialog::SubtitleDelayDialogPrivate +{ +public: + explicit SubtitleDelayDialogPrivate(SubtitleDelayDialog *q) + : q_ptr(q) + { + delaySpinBox = new QDoubleSpinBox(q_ptr); + delaySpinBox->setRange(INT_MIN, INT_MAX); + delaySpinBox->setSingleStep(0.5); + } + + SubtitleDelayDialog *q_ptr; + + QDoubleSpinBox *delaySpinBox; +}; + +SubtitleDelayDialog::SubtitleDelayDialog(QWidget *parent) + : QDialog(parent) + , d_ptr(new SubtitleDelayDialogPrivate(this)) +{ + setupUI(); + setMinimumSize(350, 100); + resize(350, 100); +} + +SubtitleDelayDialog::~SubtitleDelayDialog() {} + +void SubtitleDelayDialog::setDelay(double delay) +{ + d_ptr->delaySpinBox->setValue(delay); +} + +auto SubtitleDelayDialog::delay() const -> double +{ + return d_ptr->delaySpinBox->value(); +} + +void SubtitleDelayDialog::onDelayChanged(double delay) +{ + emit delayChanged(d_ptr->delaySpinBox->value()); +} + +void SubtitleDelayDialog::setupUI() +{ + auto *button = new QToolButton(this); + button->setText(tr("Apply")); + connect(button, &QToolButton::clicked, this, &SubtitleDelayDialog::onDelayChanged); + + auto *layout = new QHBoxLayout(this); + layout->addWidget(new QLabel(tr("Subtitle delay(seconds):"), this)); + layout->addWidget(d_ptr->delaySpinBox); + layout->addWidget(button); +} diff --git a/examples/mpvplayer/subtitledelaydialog.hpp b/examples/mpvplayer/subtitledelaydialog.hpp new file mode 100644 index 0000000..8199877 --- /dev/null +++ b/examples/mpvplayer/subtitledelaydialog.hpp @@ -0,0 +1,29 @@ +#ifndef SUBTITLEDELAYDIALOG_HPP +#define SUBTITLEDELAYDIALOG_HPP + +#include + +class SubtitleDelayDialog : public QDialog +{ + Q_OBJECT +public: + explicit SubtitleDelayDialog(QWidget *parent = nullptr); + ~SubtitleDelayDialog() override; + + void setDelay(double delay); + auto delay() const -> double; + +signals: + void delayChanged(double delay); + +private slots: + void onDelayChanged(double delay); + +private: + void setupUI(); + + class SubtitleDelayDialogPrivate; + QScopedPointer d_ptr; +}; + +#endif // SUBTITLEDELAYDIALOG_HPP diff --git a/examples/player/player.pro b/examples/player/player.pro deleted file mode 100644 index 2ba417b..0000000 --- a/examples/player/player.pro +++ /dev/null @@ -1,43 +0,0 @@ -include(../../common.pri) - -QT += core gui widgets network multimedia openglwidgets core5compat - -TEMPLATE = app - -TARGET = Player - -LIBS += \ - -l$$replaceLibName(ffmpeg) \ - -l$$replaceLibName(thirdparty) \ - -l$$replaceLibName(dump) \ - -l$$replaceLibName(utils) - -include(../../src/3rdparty/3rdparty.pri) - -SOURCES += \ - colorspacedialog.cc \ - controlwidget.cc \ - main.cpp \ - mainwindow.cpp \ - openwebmediadialog.cc \ - playlistmodel.cpp \ - playlistview.cc \ - qmediaplaylist.cpp \ - qplaylistfileparser.cpp \ - slider.cpp \ - titlewidget.cc - -HEADERS += \ - colorspacedialog.hpp \ - controlwidget.hpp \ - mainwindow.h \ - openwebmediadialog.hpp \ - playlistmodel.h \ - playlistview.hpp \ - qmediaplaylist.h \ - qmediaplaylist_p.h \ - qplaylistfileparser_p.h \ - slider.h \ - titlewidget.hpp - -DESTDIR = $$APP_OUTPUT_PATH diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 467ab46..2d81447 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,3 +2,7 @@ add_subdirectory(utils) add_subdirectory(dump) add_subdirectory(3rdparty) add_subdirectory(ffmpeg) + +if(BUILD_MPV) + add_subdirectory(mpv) +endif() diff --git a/src/ffmpeg/videorender/videorendercreate.cc b/src/ffmpeg/videorender/videorendercreate.cc index 6858dad..0f7203e 100644 --- a/src/ffmpeg/videorender/videorendercreate.cc +++ b/src/ffmpeg/videorender/videorendercreate.cc @@ -17,14 +17,6 @@ auto create(RenderType type) -> VideoRender * return render; } -void setSurfaceFormatVersion(int major, int minor) -{ - auto surfaceFormat = QSurfaceFormat::defaultFormat(); - surfaceFormat.setVersion(major, minor); - surfaceFormat.setProfile(QSurfaceFormat::CoreProfile); - QSurfaceFormat::setDefaultFormat(surfaceFormat); -} - } // namespace VideoRenderCreate } // namespace Ffmpeg diff --git a/src/ffmpeg/videorender/videorendercreate.hpp b/src/ffmpeg/videorender/videorendercreate.hpp index 520965d..e5d352a 100644 --- a/src/ffmpeg/videorender/videorendercreate.hpp +++ b/src/ffmpeg/videorender/videorendercreate.hpp @@ -11,8 +11,6 @@ enum RenderType { Widget = 1, Opengl }; FFMPEG_EXPORT auto create(RenderType type) -> VideoRender *; -FFMPEG_EXPORT void setSurfaceFormatVersion(int major, int minor); - } // namespace VideoRenderCreate } // namespace Ffmpeg diff --git a/src/mpv/CMakeLists.txt b/src/mpv/CMakeLists.txt new file mode 100644 index 0000000..b81c139 --- /dev/null +++ b/src/mpv/CMakeLists.txt @@ -0,0 +1,32 @@ +set(PROJECT_SOURCES + mediainfo.cc + mediainfo.hpp + mpv_global.h + mpvopenglwidget.cc + mpvopenglwidget.hpp + mpvplayer.cc + mpvplayer.hpp + mpvwidget.cc + mpvwidget.hpp + previewwidget.cc + previewwidget.hpp + qthelper.hpp) + +add_custom_library(custommpv ${PROJECT_SOURCES} ${SOURCES}) +target_link_libraries(custommpv PRIVATE Qt6::Widgets Qt6::OpenGLWidgets) + +if(CMAKE_HOST_WIN32) + target_include_directories(custommpv PRIVATE "C:\\3rd\\x64\\mpv\\include") + target_link_libraries(custommpv PRIVATE C:\\3rd\\x64\\mpv\\libmpv.dll.a) +elseif(CMAKE_HOST_APPLE) + target_include_directories(custommpv PRIVATE "/usr/local/include") + target_link_directories(custommpv PRIVATE /usr/lib) + target_link_directories(custommpv PRIVATE /usr/local/lib) + target_link_libraries(custommpv PRIVATE mpv) +elseif(CMAKE_HOST_LINUX) + target_link_libraries(custommpv PRIVATE mpv) +endif() + +if(CMAKE_HOST_WIN32) + target_compile_definitions(custommpv PRIVATE "MPV_LIBRARY") +endif() diff --git a/src/mpv/mediainfo.cc b/src/mpv/mediainfo.cc new file mode 100644 index 0000000..2d4dab0 --- /dev/null +++ b/src/mpv/mediainfo.cc @@ -0,0 +1,70 @@ +#include "mediainfo.hpp" + +namespace Mpv { + +TraskInfo::TraskInfo(const QJsonObject &obj) +{ + albumart = obj.value("albumart").toBool(); + codec = obj.value("codec").toString(); + isDefault = obj.value("default").toBool(); + dependent = obj.value("dependent").toBool(); + external = obj.value("external").toBool(); + ff_index = obj.value("ff-index").toInt(); + forced = obj.value("forced").toBool(); + hearing_impaired = obj.value("hearing-impaired").toBool(); + id = obj.value("id").toInt(); + image = obj.value("image").toBool(); + lang = obj.value("lang").toString(); + selected = obj.value("selected").toBool(); + src_id = obj.value("src-id").toInt(); + title = obj.value("title").toString(); + type = obj.value("type").toString(); + visual_impaired = obj.value("visual-impaired").toBool(); + + size = QSize(obj.value("demux-w").toInt(), obj.value("demux-h").toInt()); + fps = obj.value("demux-fps").toDouble(); + + channelCount = obj.value("demux-channel-count").toInt(); + samplerate = obj.value("demux-samplerate").toInt(); +} + +auto TraskInfo::text() const -> QString +{ + auto text = title; + if (text.isEmpty()) { + text = lang; + } else if (!lang.isEmpty()) { + text += "-" + lang; + } + + if (text.isEmpty()) { + text = codec; + } else if (!codec.isEmpty()) { + text += "-" + codec; + } + if (text.isEmpty()) { + return type; + } + + if (isDefault) { + text += "-Default"; + } + if (type == "video") { + text += QString("-%1fps-[%2x%3]") + .arg(QString::number(fps, 'f', 2), + QString::number(size.width()), + QString::number(size.height())); + } else if (type == "audio") { + text += QString("-%1-%2hz").arg(QString::number(channelCount), QString::number(samplerate)); + } + + return text; +} + +Chapter::Chapter(const QJsonObject &obj) +{ + title = obj.value("title").toString(); + milliseconds = obj.value("time").toDouble() * 1000; +} + +} // namespace Mpv diff --git a/src/mpv/mediainfo.hpp b/src/mpv/mediainfo.hpp new file mode 100644 index 0000000..921694d --- /dev/null +++ b/src/mpv/mediainfo.hpp @@ -0,0 +1,58 @@ +#ifndef MEDIAINFO_HPP +#define MEDIAINFO_HPP + +#include "mpv_global.h" + +#include + +namespace Mpv { + +struct MPV_LIB_EXPORT TraskInfo +{ + TraskInfo() = default; + explicit TraskInfo(const QJsonObject &obj); + + [[nodiscard]] auto text() const -> QString; + + bool albumart = false; + QString codec; + bool isDefault = false; + bool dependent = false; + bool external = false; + int ff_index = 0; + bool forced = false; + bool hearing_impaired = false; + int id = 0; + bool image = false; + QString lang; + bool selected = false; + int src_id = 0; + QString title; + QString type; + bool visual_impaired = false; + + QSize size = QSize(0, 0); + double fps = 0.0; + + int channelCount = 0; + int samplerate = 0; +}; + +using TraskInfos = QVector; + +struct MPV_LIB_EXPORT Chapter +{ + Chapter() = default; + explicit Chapter(const QJsonObject &obj); + + QString title; + qint64 milliseconds; +}; + +using Chapters = QVector; + +} // namespace Mpv + +Q_DECLARE_METATYPE(Mpv::TraskInfo) + +#endif // MEDIAINFO_HPP diff --git a/src/mpv/mpv.pri b/src/mpv/mpv.pri new file mode 100644 index 0000000..6e9defd --- /dev/null +++ b/src/mpv/mpv.pri @@ -0,0 +1,18 @@ +QT_CONFIG -= no-pkg-config +CONFIG += link_pkgconfig debug +#PKGCONFIG += mpv + +win32 { + INCLUDEPATH += C:/3rd/x64/mpv/include + LIBS += C:/3rd/x64/mpv/libmpv.dll.a +} + +macx { + INCLUDEPATH += /usr/local/include + LIBS += -L/usr/lib -L/usr/local/lib + LIBS += -lmpv +} + +unix:!macx{ + LIBS += -lmpv +} \ No newline at end of file diff --git a/src/mpv/mpv.pro b/src/mpv/mpv.pro new file mode 100644 index 0000000..8648f52 --- /dev/null +++ b/src/mpv/mpv.pro @@ -0,0 +1,23 @@ +include(../slib.pri) +include(mpv.pri) + +QT += core gui network widgets openglwidgets + +DEFINES += MPV_LIBRARY +TARGET = $$replaceLibName(custommpv) + +SOURCES += \ + mediainfo.cc \ + mpvopenglwidget.cc \ + mpvplayer.cc \ + mpvwidget.cc \ + previewwidget.cc + +HEADERS += \ + mediainfo.hpp \ + mpv_global.h \ + mpvopenglwidget.hpp \ + mpvplayer.hpp \ + mpvwidget.hpp \ + previewwidget.hpp \ + qthelper.hpp diff --git a/src/mpv/mpv_global.h b/src/mpv/mpv_global.h new file mode 100644 index 0000000..56d3029 --- /dev/null +++ b/src/mpv/mpv_global.h @@ -0,0 +1,12 @@ +#ifndef MPV_GLOBAL_H +#define MPV_GLOBAL_H + +#include + +#if defined(MPV_LIBRARY) +#define MPV_LIB_EXPORT Q_DECL_EXPORT +#else +#define MPV_LIB_EXPORT Q_DECL_IMPORT +#endif + +#endif // MPV_GLOBAL_H diff --git a/src/mpv/mpvopenglwidget.cc b/src/mpv/mpvopenglwidget.cc new file mode 100644 index 0000000..7de7625 --- /dev/null +++ b/src/mpv/mpvopenglwidget.cc @@ -0,0 +1,155 @@ +#include "mpvopenglwidget.hpp" +#include "mpvplayer.hpp" + +#include + +#include +#include + +namespace Mpv { + +static auto get_proc_address(void *ctx, const char *name) -> void * +{ + Q_UNUSED(ctx); + auto *glctx = QOpenGLContext::currentContext(); + if (glctx == nullptr) { + return nullptr; + } + return reinterpret_cast(glctx->getProcAddress(QByteArray(name))); +} + +class MpvOpenglWidget::MpvOpenglWidgetPrivate +{ +public: + MpvOpenglWidgetPrivate(MpvOpenglWidget *parent) + : owner(parent) + { + resizeTimer = new QTimer(owner); + resizeTimer->setSingleShot(true); + } + + void clean() + { + if (mpv_gl) { + mpv_render_context_free(mpv_gl); + } + } + + MpvOpenglWidget *owner; + MpvPlayer *mpvPlayer = nullptr; + mpv_render_context *mpv_gl; + + bool resizePauseState = false; + bool isResizing = false; + QTimer *resizeTimer; +}; + +MpvOpenglWidget::MpvOpenglWidget(MpvPlayer *mpvPlayer, QWidget *parent) + : QOpenGLWidget{parent} + , d_ptr(new MpvOpenglWidgetPrivate(this)) +{ + d_ptr->mpvPlayer = mpvPlayer; + + buildConnect(); +} + +MpvOpenglWidget::~MpvOpenglWidget() +{ + makeCurrent(); + d_ptr->clean(); + d_ptr->mpvPlayer->quit(); + doneCurrent(); +} + +void MpvOpenglWidget::initializeGL() +{ + mpv_opengl_init_params gl_init_params{get_proc_address, nullptr}; + mpv_render_param params[]{{MPV_RENDER_PARAM_API_TYPE, + const_cast(MPV_RENDER_API_TYPE_OPENGL)}, + {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params}, + {MPV_RENDER_PARAM_INVALID, nullptr}}; + + if (mpv_render_context_create(&d_ptr->mpv_gl, d_ptr->mpvPlayer->mpv_handler(), params) < 0) + throw std::runtime_error("failed to initialize mpv GL context"); + mpv_render_context_set_update_callback(d_ptr->mpv_gl, + MpvOpenglWidget::on_update, + reinterpret_cast(this)); +} + +void MpvOpenglWidget::paintGL() +{ + auto devicePixelRatio = this->devicePixelRatio(); + mpv_opengl_fbo mpfbo{static_cast(defaultFramebufferObject()), + int(width() * devicePixelRatio), + int(height() * devicePixelRatio), + 0}; + int flip_y{1}; + + mpv_render_param params[] = {{MPV_RENDER_PARAM_OPENGL_FBO, &mpfbo}, + {MPV_RENDER_PARAM_FLIP_Y, &flip_y}, + {MPV_RENDER_PARAM_INVALID, nullptr}}; + // See render_gl.h on what OpenGL environment mpv expects, and + // other API details. + mpv_render_context_render(d_ptr->mpv_gl, params); +} + +void MpvOpenglWidget::maybeUpdate() +{ + // If the Qt window is not visible, Qt's update() will just skip rendering. + // This confuses mpv's render API, and may lead to small occasional + // freezes due to video rendering timing out. + // Handle this by manually redrawing. + // Note: Qt doesn't seem to provide a way to query whether update() will + // be skipped, and the following code still fails when e.g. switching + // to a different workspace with a reparenting window manager. + if (window()->isMinimized()) { + makeCurrent(); + paintGL(); + context()->swapBuffers(context()->surface()); + doneCurrent(); + } else { + update(); + } +} + +void MpvOpenglWidget::onBeforeResize() +{ + if (d_ptr->isResizing) { // MacOS 全屏切换时会触发两次 resize + return; + } + d_ptr->resizePauseState = d_ptr->mpvPlayer->isPaused(); + if (d_ptr->resizePauseState) { + return; + } + d_ptr->mpvPlayer->pauseSync(true); + d_ptr->isResizing = true; +} + +void MpvOpenglWidget::onAfterResize() +{ + d_ptr->isResizing = false; + if (d_ptr->resizePauseState) { + return; + } + d_ptr->mpvPlayer->pauseAsync(); +} + +void MpvOpenglWidget::on_update(void *ctx) +{ + QMetaObject::invokeMethod((MpvOpenglWidget *) ctx, "maybeUpdate"); +} + +void MpvOpenglWidget::buildConnect() +{ + connect(this, &QOpenGLWidget::aboutToResize, this, &MpvOpenglWidget::onBeforeResize); + connect( + this, + &QOpenGLWidget::resized, + d_ptr->resizeTimer, + [this] { d_ptr->resizeTimer->start(500); }, + Qt::QueuedConnection); + + connect(d_ptr->resizeTimer, &QTimer::timeout, this, &MpvOpenglWidget::onAfterResize); +} + +} // namespace Mpv diff --git a/src/mpv/mpvopenglwidget.hpp b/src/mpv/mpvopenglwidget.hpp new file mode 100644 index 0000000..2231e69 --- /dev/null +++ b/src/mpv/mpvopenglwidget.hpp @@ -0,0 +1,39 @@ +#ifndef MPVOPENGLWIDGET_HPP +#define MPVOPENGLWIDGET_HPP + +#include "mpv_global.h" + +#include + +namespace Mpv { + +class MpvPlayer; + +class MPV_LIB_EXPORT MpvOpenglWidget Q_DECL_FINAL : public QOpenGLWidget +{ + Q_OBJECT +public: + explicit MpvOpenglWidget(Mpv::MpvPlayer *mpvPlayer, QWidget *parent = nullptr); + ~MpvOpenglWidget() override; + +protected: + void initializeGL() Q_DECL_OVERRIDE; + void paintGL() Q_DECL_OVERRIDE; + +private Q_SLOTS: + void maybeUpdate(); + void onBeforeResize(); + void onAfterResize(); + +private: + static void on_update(void *ctx); + + void buildConnect(); + + class MpvOpenglWidgetPrivate; + QScopedPointer d_ptr; +}; + +} // namespace Mpv + +#endif // MPVOPENGLWIDGET_HPP diff --git a/src/mpv/mpvplayer.cc b/src/mpv/mpvplayer.cc new file mode 100644 index 0000000..4227a58 --- /dev/null +++ b/src/mpv/mpvplayer.cc @@ -0,0 +1,483 @@ +#include "mpvplayer.hpp" +#include "qthelper.hpp" + +#include +#include +#include + +#include + +#include +#include + +namespace Mpv { + +static void wakeup(void *ctx) +{ + // This callback is invoked from any mpv thread (but possibly also + // recursively from a thread that is calling the mpv API). Just notify + // the Qt GUI thread to wake up (so that it can process events with + // mpv_wait_event()), and return as quickly as possible. + auto *w = static_cast(ctx); + QMetaObject::invokeMethod(w, "onMpvEvents", Qt::QueuedConnection); +} + +class MpvPlayer::MpvPlayerPrivate +{ +public: + explicit MpvPlayerPrivate(MpvPlayer *parent) + : owner(parent) + {} + + ~MpvPlayerPrivate() { destroy(); } + + void init() + { + destroy(); + mpv = mpv_create(); + if (mpv == nullptr) { + throw std::runtime_error("can't create mpv instance"); + } + } + + void destroy() + { + if (mpv != nullptr) { + mpv_terminate_destroy(mpv); + mpv = nullptr; + } + } + + void handle_mpv_event(mpv_event *event) + { + switch (event->event_id) { + case MPV_EVENT_PROPERTY_CHANGE: { + auto *prop = static_cast(event->data); + if (strcmp(prop->name, "time-pos") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + position = *static_cast(prop->data); + emit owner->positionChanged(position); + } else if (prop->format == MPV_FORMAT_NONE) { + // The property is unavailable, which probably means playback + // was stopped. + position = 0; + emit owner->positionChanged(position); + } + } else if (strcmp(prop->name, "duration") == 0) { + if (prop->format == MPV_FORMAT_DOUBLE) { + emit owner->durationChanged(*static_cast(prop->data)); + } + } else if (strcmp(prop->name, "chapter-list") == 0) { + // Dump the properties as JSON for demo purposes. + if (prop->format == MPV_FORMAT_NODE) { + auto v = mpv::qt::node_to_variant(static_cast(prop->data)); + // Abuse JSON support for easily printing the mpv_node contents. + auto d = QJsonDocument::fromVariant(v); + emit owner->mpvLogMessage("Change property " + QString(prop->name) + ":\n"); + emit owner->mpvLogMessage(d.toJson()); + + chapters.clear(); + auto array = d.array(); + for (auto iter = array.cbegin(); iter != array.cend(); iter++) { + auto obj = (*iter).toObject(); + chapters.append(Chapter(obj)); + } + emit owner->chapterChanged(); + } + } else if (strcmp(prop->name, "track-list") == 0) { + // Dump the properties as JSON for demo purposes. + if (prop->format == MPV_FORMAT_NODE) { + auto v = mpv::qt::node_to_variant(static_cast(prop->data)); + // Abuse JSON support for easily printing the mpv_node contents. + auto d = QJsonDocument::fromVariant(v); + emit owner->mpvLogMessage("Change property " + QString(prop->name) + ":\n"); + emit owner->mpvLogMessage(d.toJson()); + + videoTracks.clear(); + audioTracks.clear(); + subTracks.clear(); + auto array = d.array(); + for (auto iter = array.cbegin(); iter != array.cend(); iter++) { + auto obj = (*iter).toObject(); + auto traskInfo = TraskInfo(obj); + if (obj.value("type") == "audio") { + audioTracks.append(traskInfo); + } else if (obj.value("type") == "video") { + videoTracks.append(traskInfo); + } else if (obj.value("type") == "sub") { + subTracks.append(traskInfo); + } + } + emit owner->trackChanged(); + } + } else if (strcmp(prop->name, "cache-speed") == 0) { + if (prop->format == MPV_FORMAT_INT64) { + cache_speed = *static_cast(prop->data); + emit owner->cacheSpeedChanged(cache_speed); + } + } + break; + } + case MPV_EVENT_VIDEO_RECONFIG: { + // Retrieve the new video size. + int64_t w, h; + if (mpv_get_property(mpv, "dwidth", MPV_FORMAT_INT64, &w) >= 0 + && mpv_get_property(mpv, "dheight", MPV_FORMAT_INT64, &h) >= 0 && w > 0 && h > 0) { + // Note that the MPV_EVENT_VIDEO_RECONFIG event doesn't necessarily + // imply a resize, and you should check yourself if the video + // dimensions really changed. + // mpv itself will scale/letter box the video to the container size + // if the video doesn't fit. + std::stringstream ss; + ss << "Reconfig: " << w << " " << h << "\n"; + emit owner->mpvLogMessage(QString::fromStdString(ss.str())); + } + break; + } + case MPV_EVENT_LOG_MESSAGE: { + auto *msg = static_cast(event->data); + std::stringstream ss; + ss << "[" << msg->prefix << "] " << msg->level << ": " << msg->text; + emit owner->mpvLogMessage(QString::fromStdString(ss.str())); + break; + } + case MPV_EVENT_SHUTDOWN: destroy(); break; + case MPV_EVENT_FILE_LOADED: emit owner->fileLoaded(); break; + case MPV_EVENT_END_FILE: { + auto *prop = static_cast(event->data); + if (prop->reason == MPV_END_FILE_REASON_EOF) { + emit owner->fileFinished(); + } + } break; + default: + break; + // Ignore uninteresting or unknown events. + } + } + + MpvPlayer *owner; + + struct mpv_handle *mpv = nullptr; + TraskInfos videoTracks; + TraskInfos audioTracks; + TraskInfos subTracks; + Chapters chapters; + double position = 0; + int64_t cache_speed = 0; +}; + +MpvPlayer::MpvPlayer(QObject *parent) + : QObject{parent} + , d_ptr(new MpvPlayerPrivate(this)) +{ + qInfo() << mpv_client_api_version(); +} + +MpvPlayer::~MpvPlayer() = default; + +void MpvPlayer::openMedia(const QString &filePath) +{ + if (d_ptr->mpv == nullptr) { + return; + } + const QByteArray c_filename = filePath.toUtf8(); + const char *args[] = {"loadfile", c_filename.data(), NULL}; + mpv_command_async(d_ptr->mpv, 0, args); +} + +void MpvPlayer::play() +{ + mpv::qt::set_property_async(d_ptr->mpv, "pause", false); +} + +void MpvPlayer::stop() +{ + mpv::qt::set_property_async(d_ptr->mpv, "pause", true); + mpv::qt::set_property_async(d_ptr->mpv, "time-pos", 0); +} + +auto MpvPlayer::filename() const -> QString +{ + return mpv::qt::get_property(d_ptr->mpv, "filename").toString(); +} + +auto MpvPlayer::filepath() const -> QString +{ + return mpv::qt::get_property(d_ptr->mpv, "path").toString(); +} + +auto MpvPlayer::filesize() const -> double +{ + return mpv::qt::get_property(d_ptr->mpv, "file-size").toDouble(); +} + +auto MpvPlayer::duration() const -> double +{ + return mpv::qt::get_property(d_ptr->mpv, "duration").toDouble(); +} + +auto MpvPlayer::position() const -> double +{ + // time-pos + return mpv::qt::get_property(d_ptr->mpv, "playback-time").toDouble(); +} + +auto MpvPlayer::chapters() const -> Chapters +{ + return d_ptr->chapters; +} + +auto MpvPlayer::videoTracks() const -> TraskInfos +{ + return d_ptr->videoTracks; +} + +auto MpvPlayer::audioTracks() const -> TraskInfos +{ + return d_ptr->audioTracks; +} + +auto MpvPlayer::subTracks() const -> TraskInfos +{ + return d_ptr->subTracks; +} + +void MpvPlayer::setVideoTrack(int vid) +{ + qInfo() << "vid: " << vid; + mpv::qt::set_property_async(d_ptr->mpv, "vid", vid); +} + +void MpvPlayer::blockVideoTrack() +{ + mpv::qt::set_property_async(d_ptr->mpv, "vid", "no"); +} + +void MpvPlayer::setAudioTrack(int aid) +{ + qInfo() << "aid: " << aid; + mpv::qt::set_property_async(d_ptr->mpv, "aid", aid); +} + +void MpvPlayer::blockAudioTrack() +{ + mpv::qt::set_property_async(d_ptr->mpv, "aid", "no"); +} + +void MpvPlayer::setSubTrack(int sid) +{ + qInfo() << "sid: " << sid; + mpv::qt::set_property_async(d_ptr->mpv, "sid", sid); +} + +void MpvPlayer::blockSubTrack() +{ + mpv::qt::set_property_async(d_ptr->mpv, "sid", "no"); +} + +void MpvPlayer::addAudio(const QStringList &paths) +{ + for (const auto &path : std::as_const(paths)) { + mpv::qt::command_async(d_ptr->mpv, QVariantList() << "audio-add" << path); + } +} + +void MpvPlayer::addSub(const QStringList &paths) +{ + for (const auto &path : std::as_const(paths)) { + mpv::qt::command_async(d_ptr->mpv, QVariantList() << "sub-add" << path); + } +} + +void MpvPlayer::setPrintToStd(bool print) +{ + mpv_set_option_string(d_ptr->mpv, "terminal", print ? "yes" : "no"); +} + +void MpvPlayer::setCache(bool cache) +{ + mpv::qt::set_property_async(d_ptr->mpv, "cahce", cache ? "auto" : "no"); +} + +void MpvPlayer::setCacheSeconds(int seconds) +{ + mpv::qt::set_property_async(d_ptr->mpv, "cache-secs", seconds); +} + +auto MpvPlayer::cacheSpeed() const -> double +{ + return mpv::qt::get_property(d_ptr->mpv, "cache-speed").toDouble(); +} + +void MpvPlayer::setCacheSpeed(double speed) +{ + mpv::qt::set_property_async(d_ptr->mpv, "cache-speed", speed); +} + +void MpvPlayer::setUseGpu(bool use) +{ + mpv::qt::set_property_async(d_ptr->mpv, "hwdec", use ? "auto-safe" : "no"); + //mpv::qt::set_property_async(d_ptr->mpv, "d3d11va-zero-copy", "yes"); +} + +void MpvPlayer::setGpuApi(GpuApiType type) +{ + QString typeStr; + switch (type) { + case Opengl: typeStr = "opengl"; break; + case Vulkan: typeStr = "vulkan"; break; +#ifdef Q_OS_WIN + case D3d11: typeStr = "d3d11"; break; +#endif + default: typeStr = "auto"; break; + } + mpv::qt::set_property(d_ptr->mpv, "gpu-api", typeStr); + qInfo() << "GpuApi: " << typeStr; +} + +void MpvPlayer::setVolume(int value) +{ + qInfo() << "volume: " << value; + mpv::qt::set_property_async(d_ptr->mpv, "volume", value); +} + +auto MpvPlayer::volume() const -> int +{ + return mpv::qt::get_property(d_ptr->mpv, "volume").toInt(); +} + +void MpvPlayer::seek(qint64 percent) +{ + qInfo() << "seek: " << percent; + mpv::qt::command_async(d_ptr->mpv, QVariantList() << "seek" << percent << "absolute"); +} + +void MpvPlayer::seekRelative(qint64 seconds) +{ + qInfo() << "seekRelative: " << seconds; + mpv::qt::command_async(d_ptr->mpv, QVariantList() << "seek" << seconds << "relative"); +} + +void MpvPlayer::setSpeed(double speed) +{ + qInfo() << "speed: " << speed; + mpv::qt::set_property_async(d_ptr->mpv, "speed", speed); +} + +auto MpvPlayer::speed() const -> double +{ + return mpv::qt::get_property(d_ptr->mpv, "speed").toDouble(); +} + +void MpvPlayer::setSubtitleDelay(double delay) +{ + qInfo() << "sub-delay: " << delay; + mpv::qt::set_property_async(d_ptr->mpv, "sub-delay", delay); +} + +auto MpvPlayer::subtitleDelay() const -> double +{ + return mpv::qt::get_property(d_ptr->mpv, "sub-delay").toDouble(); +} + +void MpvPlayer::pauseAsync() +{ + auto state = !isPaused(); + qInfo() << "pause: " << state; + mpv::qt::set_property_async(d_ptr->mpv, "pause", state); + emit pauseStateChanged(state); +} + +void MpvPlayer::pauseSync(bool state) +{ + qInfo() << "pause: " << state; + mpv::qt::set_property(d_ptr->mpv, "pause", state); + emit pauseStateChanged(state); +} + +auto MpvPlayer::isPaused() -> bool +{ + return mpv::qt::get_property(d_ptr->mpv, "pause").toBool(); +} + +auto MpvPlayer::volumeMax() const -> int +{ + return mpv::qt::get_property(d_ptr->mpv, "volume-max").toInt(); +} + +void MpvPlayer::abortAllAsyncCommands() +{ + mpv::qt::command_abort_async(d_ptr->mpv); +} + +void MpvPlayer::quit() +{ + mpv::qt::set_property(d_ptr->mpv, "quit", true); +} + +auto MpvPlayer::mpv_handler() -> mpv_handle * +{ + return d_ptr->mpv; +} + +void MpvPlayer::onMpvEvents() +{ + // Process all events, until the event queue is empty. + while (d_ptr->mpv != nullptr) { + auto *event = mpv_wait_event(d_ptr->mpv, 0); + if (event->event_id == MPV_EVENT_NONE) { + break; + } + d_ptr->handle_mpv_event(event); + } +} + +void MpvPlayer::initMpv(QWidget *widget) +{ + d_ptr->init(); + if (widget != nullptr) { + auto raw_wid = widget->winId(); +#ifdef _WIN32 + // Truncate to 32-bit, as all Windows handles are. This also ensures + // it doesn't go negative. + int64_t wid = static_cast(raw_wid); +#else + int64_t wid = raw_wid; +#endif + mpv_set_property(d_ptr->mpv, "wid", MPV_FORMAT_INT64, &wid); + } + // Enable default bindings, because we're lazy. Normally, a player using + // mpv as backend would implement its own key bindings. + mpv_set_property_string(d_ptr->mpv, "input-default-bindings", "yes"); + + // Enable keyboard input on the X11 window. For the messy details, see + // --input-vo-keyboard on the manpage. + mpv_set_property_string(d_ptr->mpv, "input-vo-keyboard", "yes"); + + // Let us receive property change events with MPV_EVENT_PROPERTY_CHANGE if + // this property changes. + mpv_observe_property(d_ptr->mpv, 0, "time-pos", MPV_FORMAT_DOUBLE); + mpv_observe_property(d_ptr->mpv, 0, "cache-speed", MPV_FORMAT_INT64); + + mpv_observe_property(d_ptr->mpv, 0, "track-list", MPV_FORMAT_NODE); + mpv_observe_property(d_ptr->mpv, 0, "chapter-list", MPV_FORMAT_NODE); + + // Request log messages with level "info" or higher. + // They are received as MPV_EVENT_LOG_MESSAGE. + mpv_request_log_messages(d_ptr->mpv, "info"); + + mpv::qt::set_property(d_ptr->mpv, "volume-max", 200); + + // From this point on, the wakeup function will be called. The callback + // can come from any thread, so we use the QueuedConnection mechanism to + // relay the wakeup in a thread-safe way. + mpv_set_wakeup_callback(d_ptr->mpv, wakeup, this); + + mpv_set_option_string(d_ptr->mpv, "msg-level", "all=v"); + + if (mpv_initialize(d_ptr->mpv) < 0) { + throw std::runtime_error("mpv failed to initialize"); + } +} + +} // namespace Mpv diff --git a/src/mpv/mpvplayer.hpp b/src/mpv/mpvplayer.hpp new file mode 100644 index 0000000..d9cca1f --- /dev/null +++ b/src/mpv/mpvplayer.hpp @@ -0,0 +1,114 @@ +#ifndef MPVPLAYER_HPP +#define MPVPLAYER_HPP + +#include "mediainfo.hpp" +#include "mpv_global.h" + +#include + +struct mpv_handle; + +namespace Mpv { + +class MPV_LIB_EXPORT MpvPlayer : public QObject +{ + Q_OBJECT +public: + enum GpuApiType { + Auto, + Opengl, + Vulkan, +#ifdef Q_OS_WIN + D3d11 +#endif + }; + Q_ENUM(GpuApiType) + + explicit MpvPlayer(QObject *parent = nullptr); + ~MpvPlayer() override; + + void initMpv(QWidget *widget); + + void openMedia(const QString &filePath); + + void play(); + void stop(); + + [[nodiscard]] auto filename() const -> QString; + [[nodiscard]] auto filepath() const -> QString; + [[nodiscard]] auto filesize() const -> double; + + [[nodiscard]] auto duration() const -> double; // seconds + [[nodiscard]] auto position() const -> double; // seconds + + [[nodiscard]] auto chapters() const -> Chapters; + + [[nodiscard]] auto videoTracks() const -> TraskInfos; + [[nodiscard]] auto audioTracks() const -> TraskInfos; + [[nodiscard]] auto subTracks() const -> TraskInfos; + + void setVideoTrack(int vid); + void blockVideoTrack(); + void setAudioTrack(int aid); + void blockAudioTrack(); + void setSubTrack(int sid); + void blockSubTrack(); + + void addAudio(const QStringList &paths); + void addSub(const QStringList &paths); + + void setPrintToStd(bool print); + + void setCache(bool cache); + void setCacheSeconds(int seconds); + [[nodiscard]] auto cacheSpeed() const -> double; // seconds + void setCacheSpeed(double speed); // bytes / seconds + + void setUseGpu(bool use); + void setGpuApi(GpuApiType type); + + void setVolume(int value); + [[nodiscard]] auto volume() const -> int; + [[nodiscard]] auto volumeMax() const -> int; + + void seek(qint64 seconds); + void seekRelative(qint64 seconds); + + void setSpeed(double speed); + [[nodiscard]] auto speed() const -> double; + + void setSubtitleDelay(double delay); // seconds + auto subtitleDelay() const -> double; + + void pauseAsync(); + void pauseSync(bool state); + auto isPaused() -> bool; + + void abortAllAsyncCommands(); + + void quit(); + + auto mpv_handler() -> mpv_handle *; + +signals: + void fileLoaded(); + void fileFinished(); + void chapterChanged(); + void trackChanged(); + void durationChanged(double duration); // ms + void positionChanged(double position); // ms + void mpvLogMessage(const QString &log); + void pauseStateChanged(bool state); + void cacheSpeedChanged(int64_t); + +private slots: + void onMpvEvents(); + +private: + class MpvPlayerPrivate; + QScopedPointer d_ptr; +}; + +} // namespace Mpv + +#endif // MPVPLAYER_HPP diff --git a/src/mpv/mpvwidget.cc b/src/mpv/mpvwidget.cc new file mode 100644 index 0000000..74700a9 --- /dev/null +++ b/src/mpv/mpvwidget.cc @@ -0,0 +1,17 @@ +#include "mpvwidget.hpp" + +namespace Mpv { + +MpvWidget::MpvWidget(QWidget *parent) + : QWidget{parent} +{ + setAttribute(Qt::WA_DontCreateNativeAncestors); + setAttribute(Qt::WA_NativeWindow); + + setAttribute(Qt::WA_StyledBackground); + setStyleSheet("QWidget{background:black;}"); +} + +MpvWidget::~MpvWidget() = default; + +} // namespace Mpv diff --git a/src/mpv/mpvwidget.hpp b/src/mpv/mpvwidget.hpp new file mode 100644 index 0000000..ac78d74 --- /dev/null +++ b/src/mpv/mpvwidget.hpp @@ -0,0 +1,20 @@ +#ifndef MPVWIDGET_HPP +#define MPVWIDGET_HPP + +#include "mpv_global.h" + +#include + +namespace Mpv { + +class MPV_LIB_EXPORT MpvWidget : public QWidget +{ + Q_OBJECT +public: + explicit MpvWidget(QWidget *parent = nullptr); + ~MpvWidget() override; +}; + +} // namespace Mpv + +#endif // MPVWIDGET_HPP diff --git a/src/mpv/previewwidget.cc b/src/mpv/previewwidget.cc new file mode 100644 index 0000000..9eaf77a --- /dev/null +++ b/src/mpv/previewwidget.cc @@ -0,0 +1,67 @@ +#include "previewwidget.hpp" +#include "mpvopenglwidget.hpp" +#include "mpvplayer.hpp" +#include "mpvwidget.hpp" + +#include + +namespace Mpv { + +class PreviewWidget::PreviewWidgetPrivate +{ +public: + PreviewWidgetPrivate(PreviewWidget *parent) + : owner(parent) + { + mpvPlayer = new Mpv::MpvPlayer(owner); +#ifdef Q_OS_WIN + mpvWidget = new Mpv::MpvWidget(owner); + mpvPlayer->initMpv(mpvWidget); +#else + mpvWidget = new Mpv::MpvOpenglWidget(mpvPlayer, owner); + mpvPlayer->initMpv(nullptr); +#endif + mpvPlayer->setUseGpu(true); + mpvPlayer->setCache(false); + mpvPlayer->pauseAsync(); + } + + PreviewWidget *owner; + + Mpv::MpvPlayer *mpvPlayer; + QWidget *mpvWidget; +}; + +PreviewWidget::PreviewWidget(QWidget *parent) + : QWidget(parent) + , d_ptr(new PreviewWidgetPrivate(this)) +{ + setupUI(); +} + +PreviewWidget::~PreviewWidget() {} + +void PreviewWidget::startPreview(const QString &filepath, int timestamp) +{ + if (filepath != d_ptr->mpvPlayer->filepath()) { + d_ptr->mpvPlayer->openMedia(filepath); + d_ptr->mpvPlayer->blockAudioTrack(); + d_ptr->mpvPlayer->blockSubTrack(); + } + d_ptr->mpvPlayer->seek(timestamp); +} + +void PreviewWidget::clearAllTask() +{ + d_ptr->mpvPlayer->abortAllAsyncCommands(); +} + +void PreviewWidget::setupUI() +{ + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(QMargins()); + layout->setSpacing(0); + layout->addWidget(d_ptr->mpvWidget); +} + +} // namespace Mpv diff --git a/src/mpv/previewwidget.hpp b/src/mpv/previewwidget.hpp new file mode 100644 index 0000000..3d56907 --- /dev/null +++ b/src/mpv/previewwidget.hpp @@ -0,0 +1,28 @@ +#ifndef PREVIEWWIDGET_HPP +#define PREVIEWWIDGET_HPP + +#include "mpv_global.h" + +#include + +namespace Mpv { + +class MPV_LIB_EXPORT PreviewWidget : public QWidget +{ +public: + explicit PreviewWidget(QWidget *parent = nullptr); + ~PreviewWidget() override; + + void startPreview(const QString &filepath, int timestamp); + void clearAllTask(); + +private: + void setupUI(); + + class PreviewWidgetPrivate; + QScopedPointer d_ptr; +}; + +} // namespace Mpv + +#endif // PREVIEWWIDGET_HPP diff --git a/src/mpv/qthelper.hpp b/src/mpv/qthelper.hpp new file mode 100644 index 0000000..7f9557b --- /dev/null +++ b/src/mpv/qthelper.hpp @@ -0,0 +1,332 @@ +#ifndef LIBMPV_QTHELPER_H_ +#define LIBMPV_QTHELPER_H_ + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace mpv { +namespace qt { + +// Wrapper around mpv_handle. Does refcounting under the hood. +class Handle +{ + struct container + { + explicit container(mpv_handle *h) + : mpv(h) + {} + ~container() { mpv_terminate_destroy(mpv); } + mpv_handle *mpv; + }; + QSharedPointer sptr; + +public: + // Construct a new Handle from a raw mpv_handle with refcount 1. If the + // last Handle goes out of scope, the mpv_handle will be destroyed with + // mpv_terminate_destroy(). + // Never destroy the mpv_handle manually when using this wrapper. You + // will create dangling pointers. Just let the wrapper take care of + // destroying the mpv_handle. + // Never create multiple wrappers from the same raw mpv_handle; copy the + // wrapper instead (that's what it's for). + static auto FromRawHandle(mpv_handle *handle) -> Handle + { + Handle h; + h.sptr = QSharedPointer(new container(handle)); + return h; + } + + // Return the raw handle; for use with the libmpv C API. + explicit operator mpv_handle *() const { return sptr ? (*sptr).mpv : 0; } +}; + +static inline auto node_to_variant(const mpv_node *node) -> QVariant +{ + switch (node->format) { + case MPV_FORMAT_STRING: return QVariant(QString::fromUtf8(node->u.string)); + case MPV_FORMAT_FLAG: return QVariant(static_cast(node->u.flag)); + case MPV_FORMAT_INT64: return QVariant(static_cast(node->u.int64)); + case MPV_FORMAT_DOUBLE: return QVariant(node->u.double_); + case MPV_FORMAT_NODE_ARRAY: { + mpv_node_list *list = node->u.list; + QVariantList qlist; + for (int n = 0; n < list->num; n++) { + qlist.append(node_to_variant(&list->values[n])); + } + return QVariant(qlist); + } + case MPV_FORMAT_NODE_MAP: { + mpv_node_list *list = node->u.list; + QVariantMap qmap; + for (int n = 0; n < list->num; n++) { + qmap.insert(QString::fromUtf8(list->keys[n]), node_to_variant(&list->values[n])); + } + return QVariant(qmap); + } + default: // MPV_FORMAT_NONE, unknown values (e.g. future extensions) + return QVariant(); + } +} + +struct node_builder +{ + explicit node_builder(const QVariant &v) { set(&node_, v); } + ~node_builder() { free_node(&node_); } + auto node() -> mpv_node * { return &node_; } + +private: + Q_DISABLE_COPY(node_builder) + mpv_node node_; + auto create_list(mpv_node *dst, bool is_map, int num) -> mpv_node_list * + { + dst->format = is_map ? MPV_FORMAT_NODE_MAP : MPV_FORMAT_NODE_ARRAY; + auto *list = new mpv_node_list(); + dst->u.list = list; + if (list == nullptr) { + goto err; + } + list->values = new mpv_node[num](); + if (list->values == nullptr) { + goto err; + } + if (is_map) { + list->keys = new char *[num](); + if (list->keys == nullptr) { + goto err; + } + } + return list; + err: + free_node(dst); + return nullptr; + } + auto dup_qstring(const QString &s) -> char * + { + QByteArray b = s.toUtf8(); + char *r = new char[b.size() + 1]; + if (r != nullptr) + std::memcpy(r, b.data(), b.size() + 1); + return r; + } + auto test_type(const QVariant &v, QMetaType::Type t) -> bool + { + // The Qt docs say: "Although this function is declared as returning + // "QVariant::Type(obsolete), the return value should be interpreted + // as QMetaType::Type." + // So a cast really seems to be needed to avoid warnings (urgh). + return static_cast(v.type()) == static_cast(t); + } + void set(mpv_node *dst, const QVariant &src) + { + if (test_type(src, QMetaType::QString)) { + dst->format = MPV_FORMAT_STRING; + dst->u.string = dup_qstring(src.toString()); + if (dst->u.string == nullptr) { + goto fail; + } + } else if (test_type(src, QMetaType::Bool)) { + dst->format = MPV_FORMAT_FLAG; + dst->u.flag = src.toBool() ? 1 : 0; + } else if (test_type(src, QMetaType::Int) || test_type(src, QMetaType::LongLong) + || test_type(src, QMetaType::UInt) || test_type(src, QMetaType::ULongLong)) { + dst->format = MPV_FORMAT_INT64; + dst->u.int64 = src.toLongLong(); + } else if (test_type(src, QMetaType::Double)) { + dst->format = MPV_FORMAT_DOUBLE; + dst->u.double_ = src.toDouble(); + } else if (src.canConvert()) { + QVariantList qlist = src.toList(); + mpv_node_list *list = create_list(dst, false, qlist.size()); + if (list == nullptr) { + goto fail; + } + list->num = qlist.size(); + for (int n = 0; n < qlist.size(); n++) { + set(&list->values[n], qlist[n]); + } + } else if (src.canConvert()) { + QVariantMap qmap = src.toMap(); + mpv_node_list *list = create_list(dst, true, qmap.size()); + if (list == nullptr) { + goto fail; + } + list->num = qmap.size(); + for (int n = 0; n < qmap.size(); n++) { + list->keys[n] = dup_qstring(qmap.keys()[n]); + if (list->keys[n] == nullptr) { + free_node(dst); + goto fail; + } + set(&list->values[n], qmap.values()[n]); + } + } else { + goto fail; + } + return; + fail: + dst->format = MPV_FORMAT_NONE; + } + void free_node(mpv_node *dst) + { + switch (dst->format) { + case MPV_FORMAT_STRING: delete[] dst->u.string; break; + case MPV_FORMAT_NODE_ARRAY: + case MPV_FORMAT_NODE_MAP: { + mpv_node_list *list = dst->u.list; + if (list != nullptr) { + for (int n = 0; n < list->num; n++) { + if (list->keys != nullptr) { + delete[] list->keys[n]; + } + if (list->values != nullptr) { + free_node(&list->values[n]); + } + } + delete[] list->keys; + delete[] list->values; + } + delete list; + break; + } + default:; + } + dst->format = MPV_FORMAT_NONE; + } +}; + +/** + * RAII wrapper that calls mpv_free_node_contents() on the pointer. + */ +struct node_autofree +{ + mpv_node *ptr; + explicit node_autofree(mpv_node *a_ptr) + : ptr(a_ptr) + {} + ~node_autofree() { mpv_free_node_contents(ptr); } +}; + +/** + * This is used to return error codes wrapped in QVariant for functions which + * return QVariant. + * + * You can use get_error() or is_error() to extract the error status from a + * QVariant value. + */ +struct ErrorReturn +{ + /** + * enum mpv_error value (or a value outside of it if ABI was extended) + */ + int error; + + ErrorReturn() + : error(0) + {} + explicit ErrorReturn(int err) + : error(err) + {} +}; + +/** + * Return the mpv error code packed into a QVariant, or 0 (success) if it's not + * an error value. + * + * @return error code (<0) or success (>=0) + */ +static inline auto get_error(const QVariant &v) -> int +{ + if (!v.canConvert()) { + return 0; + } + return v.value().error; +} + +/** + * Return whether the QVariant carries a mpv error code. + */ +static inline auto is_error(const QVariant &v) -> bool +{ + return get_error(v) < 0; +} + +/** + * Return the given property as mpv_node converted to QVariant, or QVariant() + * on error. + * + * @param name the property name + * @return the property value, or an ErrorReturn with the error code + */ +static inline auto get_property(mpv_handle *ctx, const QString &name) -> QVariant +{ + mpv_node node; + int err = mpv_get_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, &node); + if (err < 0) + return QVariant::fromValue(ErrorReturn(err)); + node_autofree f(&node); + return node_to_variant(&node); +} + +/** + * Set the given property as mpv_node converted from the QVariant argument. + * + * @return mpv error code (<0 on error, >= 0 on success) + */ +static inline auto set_property(mpv_handle *ctx, const QString &name, const QVariant &v) -> int +{ + node_builder node(v); + return mpv_set_property(ctx, name.toUtf8().data(), MPV_FORMAT_NODE, node.node()); +} + +static inline auto set_property_async(mpv_handle *ctx, const QString &name, const QVariant &v) + -> int +{ + node_builder node(v); + return mpv_set_property_async(ctx, 0, name.toUtf8().data(), MPV_FORMAT_NODE, node.node()); +} + +/** + * mpv_command_node() equivalent. + * + * @param args command arguments, with args[0] being the command name as string + * @return the property value, or an ErrorReturn with the error code + */ +static inline auto command(mpv_handle *ctx, const QVariant &args) -> QVariant +{ + node_builder node(args); + mpv_node res; + int err = mpv_command_node(ctx, node.node(), &res); + if (err < 0) + return QVariant::fromValue(ErrorReturn(err)); + node_autofree f(&res); + return node_to_variant(&res); +} + +static inline auto command_async(mpv_handle *ctx, const QVariant &args) -> QVariant +{ + node_builder node(args); + int err = mpv_command_node_async(ctx, 0, node.node()); + if (err < 0) + return QVariant::fromValue(ErrorReturn(err)); + return true; +} + +static inline void command_abort_async(mpv_handle *ctx) +{ + mpv_abort_async_command(ctx, 0); +} + +} // namespace qt +} // namespace mpv + +Q_DECLARE_METATYPE(mpv::qt::ErrorReturn) + +#endif diff --git a/src/src.pro b/src/src.pro index 9673fc5..d78c8d3 100644 --- a/src/src.pro +++ b/src/src.pro @@ -6,3 +6,7 @@ SUBDIRS += \ dump \ 3rdparty \ ffmpeg + +contains(CONFIG, BUILD_MPV) { + SUBDIRS += mpv +} diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 9adc077..d6c3489 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -10,6 +10,14 @@ #include #endif +void Utils::setSurfaceFormatVersion(int major, int minor) +{ + auto surfaceFormat = QSurfaceFormat::defaultFormat(); + surfaceFormat.setVersion(major, minor); + surfaceFormat.setProfile(QSurfaceFormat::CoreProfile); + QSurfaceFormat::setDefaultFormat(surfaceFormat); +} + QByteArray Utils::readAllFile(const QString &filePath) { QFile file(filePath); diff --git a/src/utils/utils.h b/src/utils/utils.h index 3951ad2..1f606ca 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -12,6 +12,7 @@ class QMenu; namespace Utils { +UTILS_EXPORT void setSurfaceFormatVersion(int major, int minor); UTILS_EXPORT auto readAllFile(const QString &filePath) -> QByteArray; UTILS_EXPORT auto rangeMap(float value, float min, float max, float newMin, float newMax) -> float;