diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index 0880ad7a99..e5ab61e77c 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -78,10 +78,28 @@ if(WIN32 AND QT_DEPLOY_FILES) if(QT_CANDIDATE_DIR AND EXISTS "${QT_CANDIDATE_DIR}") message(STATUS "Using preferred Qt directory for Qt${QT_MAJOR} ${PREFERRED_QT_VER}: ${QT_CANDIDATE_DIR}") list(APPEND CMAKE_PREFIX_PATH "${QT_CANDIDATE_DIR}") - find_package(Qt${QT_MAJOR} ${PREFERRED_QT_VER} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets REQUIRED) + find_package(Qt${QT_MAJOR} ${PREFERRED_QT_VER} REQUIRED COMPONENTS + Widgets + SerialPort + Multimedia + MultimediaWidgets + OpenGLWidgets + Qml + Quick + QuickWidgets + ) else() # Find all subdirectories in the Qt base directory - find_package(Qt${QT_MAJOR} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets REQUIRED) + find_package(Qt${QT_MAJOR} REQUIRED COMPONENTS + Widgets + SerialPort + Multimedia + MultimediaWidgets + OpenGLWidgets + Qml + Quick + QuickWidgets + ) file(GLOB QT_SUBDIRS LIST_DIRECTORIES true "${QT_BASE_DIR}/${QT_MAJOR}*") # Filter and sort the directories to find the latest version @@ -109,7 +127,16 @@ if(WIN32 AND QT_DEPLOY_FILES) REQUIRED ) else() - find_package(Qt${QT_MAJOR} COMPONENTS Widgets SerialPort Multimedia MultimediaWidgets REQUIRED) + find_package(Qt${QT_MAJOR} REQUIRED COMPONENTS + Widgets + SerialPort + Multimedia + MultimediaWidgets + OpenGLWidgets + Qml + Quick + QuickWidgets + ) endif() # disable deprecated Qt APIs @@ -188,7 +215,16 @@ endif() # Function to apply common properties to both library and executable targets function(apply_common_target_properties target_name) set_target_properties(${target_name} PROPERTIES LINKER_LANGUAGE CXX) - target_link_libraries(${target_name} PRIVATE Qt${QT_MAJOR}::Widgets Qt${QT_MAJOR}::SerialPort Qt${QT_MAJOR}::Multimedia Qt${QT_MAJOR}::MultimediaWidgets) + target_link_libraries(${target_name} PRIVATE + Qt${QT_MAJOR}::Widgets + Qt${QT_MAJOR}::SerialPort + Qt${QT_MAJOR}::Multimedia + Qt${QT_MAJOR}::MultimediaWidgets + Qt${QT_MAJOR}::OpenGLWidgets + Qt${QT_MAJOR}::Qml + Qt${QT_MAJOR}::Quick + Qt${QT_MAJOR}::QuickWidgets + ) target_link_libraries(${target_name} PRIVATE Threads::Threads) #add defines @@ -652,8 +688,22 @@ if (WIN32) endif() else() add_custom_target(run_windeployqt COMMENT "Skipping windeployqt (CI environment)") + endif() + # Copy QML library dependencies. + get_target_property(_qt_widgets_location Qt6::Widgets LOCATION) + get_filename_component(_qt_bin_dir "${_qt_widgets_location}" DIRECTORY) + set(_qt_qml_source_dir "${_qt_bin_dir}/../qml/QtMultimedia") + set(_local_qml_dest_dir "${WIN_DEPLOY_DIR}/qml/QtMultimedia") + add_custom_command( + TARGET SerialPrograms POST_BUILD + COMMAND ${CMAKE_COMMAND} -E echo "[Deploy-Local] Auto-copying QML Multimedia modules from ${_qt_qml_source_dir}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${_local_qml_dest_dir}" + COMMAND ${CMAKE_COMMAND} -E copy_directory "${_qt_qml_source_dir}" "${_local_qml_dest_dir}" + COMMENT "Syncing local dev environment QML bindings..." + ) + # Copy Discord SDK DLL add_custom_command( TARGET SerialPrograms POST_BUILD @@ -725,6 +775,8 @@ if (WIN32) ) endif() # end WIN32 +set_target_properties(SerialPrograms PROPERTIES QT_QML_IMPORT_DEPENDENCIES "QtMultimedia") + if (QT_MAJOR GREATER_EQUAL 6) qt_finalize_target(SerialPrograms) endif() diff --git a/SerialPrograms/Source/CommonFramework/Main.cpp b/SerialPrograms/Source/CommonFramework/Main.cpp index b4e94eb794..7ed9d7aaa3 100644 --- a/SerialPrograms/Source/CommonFramework/Main.cpp +++ b/SerialPrograms/Source/CommonFramework/Main.cpp @@ -88,6 +88,8 @@ FileLogger& global_file_logger(){ int run_program(int argc, char *argv[]){ +// qputenv("QT_RHI_BACKEND", "opengl"); + #if defined(__APPLE__) Filesystem::set_startup_profile(argc, argv); QApplication application(argc, argv); @@ -95,6 +97,12 @@ int run_program(int argc, char *argv[]){ QApplication application(argc, argv); #endif + { + QString bin_dir = QCoreApplication::applicationDirPath(); + QString qml_import_path = QDir(bin_dir).filePath("qml"); + qputenv("QML_IMPORT_PATH", qml_import_path.toLocal8Bit()); + } + GlobalOutputRedirector redirect_stdout(std::cout, "stdout", Color()); GlobalOutputRedirector redirect_stderr(std::cerr, "stderr", COLOR_RED); diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraImplementations.cpp b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraImplementations.cpp index 446a722af3..6b2a789566 100644 --- a/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraImplementations.cpp +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraImplementations.cpp @@ -19,6 +19,7 @@ #include "CameraWidgetQt6.h" #if QT_VERSION_MINOR >= 5 #include "CameraWidgetQt6.5.h" +#include "CameraWidgetQt6QML.h" #endif #endif @@ -62,6 +63,12 @@ struct CameraBackends{ std::make_unique() ); #endif +#if QT_VERSION_MAJOR == 6 + m_backends.emplace_back( + "qt6-QQuickWidget", "Qt6: QML QQuickWidget", + std::make_unique() + ); +#endif size_t items = 0; for (const auto& item : m_backends){ diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.h b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.h index f657983056..fd03e50cd5 100644 --- a/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.h +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.h @@ -11,7 +11,6 @@ #include #include #include -#include "Common/Cpp/Concurrency/Mutex.h" #include "CommonFramework/Tools/StatAccumulator.h" #include "CommonFramework/VideoPipeline/VideoSource.h" #include "CommonFramework/VideoPipeline/CameraInfo.h" @@ -101,8 +100,6 @@ class CameraVideoSource : public VideoSource{ VideoFormat m_format; FramesPerSecond m_fps; - Mutex m_snapshot_lock; - std::unique_ptr m_camera; std::unique_ptr m_video_sink; std::unique_ptr m_capture; diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.cpp b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.cpp new file mode 100644 index 0000000000..8beae3f0c3 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.cpp @@ -0,0 +1,263 @@ +/* Camera Widget (Qt6 QML) + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include "Common/Qt/Redispatch.h" +#include "VideoFrameQt.h" +#include "QFormatAggregator.h" +#include "MediaServicesQt6.h" +#include "CameraWidgetQt6QML.h" + +//#include +//using std::cout; +//using std::endl; + +namespace PokemonAutomation{ +namespace CameraQt6QML{ + + + + + + +std::vector CameraBackend::get_all_cameras() const{ + const auto cameras = GlobalMediaServices::instance().get_all_cameras(); + std::vector ret; + for (const auto& info : cameras){ + ret.emplace_back(info.id().toStdString()); + } + return ret; +} +std::string CameraBackend::get_camera_name(const CameraInfo& info) const{ + const auto cameras = GlobalMediaServices::instance().get_all_cameras(); + for (const auto& camera : cameras){ + if (camera.id().toStdString() == info.device_name()){ + return camera.description().toStdString(); + } + } + std::cout << "Error: no such camera for CameraInfo: " << info.device_name() << std::endl; + return ""; +} +std::unique_ptr CameraBackend::make_video_source( + Logger& logger, + const CameraInfo& info, + Resolution resolution, + VideoFormat format, + FramesPerSecond fps +) const{ + return std::make_unique(logger, info, resolution, format, fps); +} + + + + + + +CameraVideoSource::~CameraVideoSource(){ + if (!m_capture){ + return; + } + try{ + m_logger.log("Stopping Camera..."); + }catch (...){} + +// cout << "Stopping Camera..." << endl; + + run_on_main_thread_and_wait([&]{ + m_metaobject.reset(); +// m_camera->stop(); + m_capture.reset(); + m_video_sink.reset(); + m_camera.reset(); + }); +} +CameraVideoSource::CameraVideoSource( + Logger& logger, + const CameraInfo& info, + Resolution desired_resolution, + VideoFormat desired_format, + FramesPerSecond desired_fps +) + : VideoSource(logger, true) + , m_logger(logger) + , m_last_frame(logger) + , m_snapshot_manager(logger, m_last_frame) +{ + if (!info){ + return; + } + m_logger.log("Starting Camera: Backend = CameraQt6QVideoSink"); + + run_on_main_thread_and_wait([&]{ + init(info, desired_resolution, desired_format, desired_fps); + }); +} +void CameraVideoSource::init( + const CameraInfo& info, + Resolution desired_resolution, + VideoFormat desired_format, + FramesPerSecond desired_fps +){ + m_metaobject.reset(new QObject()); + + auto cameras = QMediaDevices::videoInputs(); + const QCameraDevice* device = nullptr; + for (const auto& camera : cameras){ + if (camera.id().toStdString() == info.device_name()){ + device = &camera; + break; + } + } + if (device == nullptr){ + m_logger.log("Camera not found: " + info.device_name(), COLOR_RED); + return; + } + + QCameraFormat format = build_format_set( + m_logger, + m_formats, + *device, + desired_resolution, + desired_format, + desired_fps + ); + if (format.isNull()){ + return; + } + + get_format(format, m_resolution, m_format, m_fps); + m_logger.log( + "Resolution: " + m_resolution.to_string() + + ", Format: " + VideoFormat_database().find(m_format)->display + + ", FPS: " + std::to_string(m_fps) + ); + + m_camera.reset(new QCameraThread(m_logger, *device, format)); + m_video_sink.reset(new QVideoSink()); + m_capture.reset(new QMediaCaptureSession()); + m_capture->setCamera(&m_camera->camera()); + m_capture->setVideoSink(m_video_sink.get()); + +#if 0 + connect(m_camera.get(), &QCamera::errorOccurred, this, [&](){ + if (m_camera->error() != QCamera::NoError){ + m_logger.log("QCamera error: " + m_camera->errorString().toStdString()); + } + }); +#endif + m_metaobject->connect( + m_video_sink.get(), &QVideoSink::videoFrameChanged, + &m_camera->camera(), [&](const QVideoFrame& frame){ + // This runs on the QCamera's thread. So it is off the critical path. + + WallClock now = current_time(); + if (!m_last_frame.push_frame(frame, now)){ + return; + } + report_source_frame(std::make_shared(now, frame)); + } + ); +} + + +QWidget* CameraVideoSource::make_display_QtWidget(QWidget* parent){ + CameraVideoDisplay* display = new CameraVideoDisplay(parent); + + QObject* root = display->rootObject(); + if (root == nullptr){ + return display; + } + + QObject* vo = root->findChild("videoOutput"); + if (vo == nullptr){ + return display; + } + + QVideoSink* qml_sink = vo->property("videoSink").value(); + if (qml_sink == nullptr){ + return display; + } + + m_metaobject->disconnect(); + m_metaobject->connect( + m_video_sink.get(), &QVideoSink::videoFrameChanged, + qml_sink, [this, qml_sink](const QVideoFrame& frame){ + WallClock now = current_time(); + if (!m_last_frame.push_frame(frame, now)){ + return; + } + + report_source_frame(std::make_shared(now, frame)); + + QMetaObject::invokeMethod(qml_sink, [qml_sink, frame]() { + qml_sink->setVideoFrame(frame); + }, Qt::QueuedConnection); + + report_rendered_frame(now); + } + ); + + return display; +} + + + + + +CameraVideoDisplay::~CameraVideoDisplay(){ +} +CameraVideoDisplay::CameraVideoDisplay(QWidget* parent) + : QQuickWidget(parent) + , m_sanitizer("CameraVideoDisplay") +{ + this->setMinimumSize(80, 45); + this->setResizeMode(QQuickWidget::SizeRootObjectToView); + this->setFocusPolicy(Qt::NoFocus); + + QString qml = QStringLiteral( + "import QtQuick\n" + "import QtMultimedia\n" + "Rectangle {\n" + " color: \"black\"\n" + " VideoOutput {\n" + " id: videoOutput\n" + " anchors.fill: parent\n" + " objectName: \"videoOutput\"\n" + " fillMode: VideoOutput.PreserveAspectFit\n" + " }\n" + "}\n" + ); + + QTemporaryFile file; + if (file.open()){ + file.write(qml.toUtf8()); + file.close(); + this->setSource(QUrl::fromLocalFile(file.fileName())); + } + +} + + + + + + + + + + + + + + + + +} +} diff --git a/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.h b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.h new file mode 100644 index 0000000000..59aa26ed23 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.h @@ -0,0 +1,145 @@ +/* Camera Widget (Qt6 OpenGL) + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_VideoPipeline_Qt6OpenGL_H +#define PokemonAutomation_VideoPipeline_Qt6OpenGL_H + +#include +#include +#include +#include +#include +#include +#include "CommonFramework/Tools/StatAccumulator.h" +#include "CommonFramework/VideoPipeline/VideoSource.h" +#include "CommonFramework/VideoPipeline/CameraInfo.h" +#include "QCameraThread.h" +#include "QVideoFrameCache.h" +#include "SnapshotManager.h" +#include "CameraImplementations.h" + +class QCamera; +class QVideoSink; + +namespace PokemonAutomation{ +namespace CameraQt6QML{ + + +class CameraBackend : public PokemonAutomation::CameraBackend{ +public: + // Get all cameras' info. + // Note: to avoid freezing the UI while launching the application, the camera backend when + // constructed, use a separate thread to query and load camera info. If you call + // `get_all_cameras()` immidiately after the backend is constructed, it may not give you + // all cameras' info. + virtual std::vector get_all_cameras() const override; + virtual std::string get_camera_name(const CameraInfo& info) const override; + + virtual std::unique_ptr make_video_source( + Logger& logger, + const CameraInfo& info, + Resolution resolution, + VideoFormat format, + FramesPerSecond fps + ) const override; +}; + + + +class CameraVideoSource : public VideoSource{ +public: + virtual ~CameraVideoSource(); + CameraVideoSource( + Logger& logger, + const CameraInfo& info, + Resolution desired_resolution, + VideoFormat desired_format, + FramesPerSecond desired_fps + ); + + virtual Resolution current_resolution() const override{ + return m_resolution; + } + virtual VideoFormat current_format() const override{ + return m_format; + } + virtual FramesPerSecond current_fps() const override{ + return m_fps; + } + virtual const VideoFormatSet& supported_formats() const override{ + return m_formats; + } + + virtual VideoSnapshot snapshot_latest_blocking() override{ + return m_snapshot_manager.snapshot_latest_blocking(); + } + virtual VideoSnapshot snapshot_recent_nonblocking(WallClock min_time) override{ + return m_snapshot_manager.snapshot_recent_nonblocking(min_time); + } + + virtual QWidget* make_display_QtWidget(QWidget* parent) override; + +private: + void init( + const CameraInfo& info, + Resolution desired_resolution, + VideoFormat desired_format, + FramesPerSecond desired_fps + ); +// void set_video_output(QGraphicsVideoItem& item); + + +private: + friend class CameraVideoDisplay; + + std::unique_ptr m_metaobject; + + Logger& m_logger; + Resolution m_resolution; + VideoFormat m_format; + FramesPerSecond m_fps; + + std::unique_ptr m_camera; + std::unique_ptr m_video_sink; + std::unique_ptr m_capture; + + VideoFormatSet m_formats; + + +private: + QVideoFrameCache m_last_frame; + SnapshotManager m_snapshot_manager; +}; + + + +class CameraVideoDisplay : public QQuickWidget{ +public: + virtual ~CameraVideoDisplay(); + CameraVideoDisplay(QWidget* parent); + +private: + LifetimeSanitizer m_sanitizer; +}; + + + + + + + + + + + + + + + + +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 8ea4a3c331..34a4e01c30 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -574,6 +574,8 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.5.h Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.cpp Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6.h + Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.cpp + Source/CommonFramework/VideoPipeline/Backends/CameraWidgetQt6QML.h Source/CommonFramework/VideoPipeline/Backends/MediaServicesQt6.cpp Source/CommonFramework/VideoPipeline/Backends/MediaServicesQt6.h Source/CommonFramework/VideoPipeline/Backends/QCameraThread.h