From c672ae60758611ebc3c54ee645a611fb42760165 Mon Sep 17 00:00:00 2001 From: Paulchen Panther Date: Fri, 18 Dec 2020 17:38:21 +0100 Subject: [PATCH] - New Media Foundation grabber - JsonAPI available grabber fix - commented json config removed --- .github/workflows/pull-request.yml | 5 + CMakeLists.txt | 19 +- HyperionConfig.h.in | 31 +- assets/webconfig/i18n/en.json | 16 +- assets/webconfig/js/content_grabber.js | 45 +- cmake/FindTurboJPEG.cmake | 34 +- config/hyperion.config.json.commented | 535 ----------- config/hyperion.config.json.default | 9 +- include/grabber/MFGrabber.h | 138 +++ include/grabber/MFThread.h | 138 +++ include/grabber/MFWrapper.h | 46 + include/grabber/V4L2Grabber.h | 10 +- include/grabber/V4L2Wrapper.h | 2 +- include/hyperion/Grabber.h | 7 + include/hyperion/GrabberWrapper.h | 18 +- include/utils/PixelFormat.h | 52 +- libsrc/api/JsonAPI.cpp | 21 +- libsrc/grabber/CMakeLists.txt | 14 +- libsrc/grabber/mediafoundation/CMakeLists.txt | 16 + libsrc/grabber/mediafoundation/MFGrabber.cpp | 868 ++++++++++++++++++ .../mediafoundation/MFSourceReaderCB.h | 134 +++ libsrc/grabber/mediafoundation/MFThread.cpp | 175 ++++ libsrc/grabber/mediafoundation/MFWrapper.cpp | 161 ++++ libsrc/grabber/v4l2/V4L2Grabber.cpp | 12 +- libsrc/grabber/v4l2/V4L2Wrapper.cpp | 2 +- libsrc/hyperion/GrabberWrapper.cpp | 30 +- .../hyperion/schema/schema-grabberV4L2.json | 110 ++- src/hyperiond/CMakeLists.txt | 30 +- src/hyperiond/hyperiond.cpp | 78 +- src/hyperiond/hyperiond.h | 7 + 30 files changed, 2097 insertions(+), 666 deletions(-) delete mode 100644 config/hyperion.config.json.commented create mode 100644 include/grabber/MFGrabber.h create mode 100644 include/grabber/MFThread.h create mode 100644 include/grabber/MFWrapper.h create mode 100644 libsrc/grabber/mediafoundation/CMakeLists.txt create mode 100644 libsrc/grabber/mediafoundation/MFGrabber.cpp create mode 100644 libsrc/grabber/mediafoundation/MFSourceReaderCB.h create mode 100644 libsrc/grabber/mediafoundation/MFThread.cpp create mode 100644 libsrc/grabber/mediafoundation/MFWrapper.cpp diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a5c462e9..465ec4ed 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -170,6 +170,11 @@ jobs: run: | choco install --no-progress python nsis openssl directx-sdk -y + - name: Install libjpeg-turbo + run: | + Invoke-WebRequest https://netcologne.dl.sourceforge.net/project/libjpeg-turbo/2.0.6/libjpeg-turbo-2.0.6-vc64.exe -OutFile libjpeg-turbo.exe + .\libjpeg-turbo /S + - name: Set up x64 build architecture environment shell: cmd run: call "${{env.VCINSTALLDIR}}\Auxiliary\Build\vcvars64.bat" diff --git a/CMakeLists.txt b/CMakeLists.txt index 62200bb2..aeb29d1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ SET ( DEFAULT_USE_SYSTEM_PROTO_LIBS OFF ) SET ( DEFAULT_USE_SYSTEM_MBEDTLS_LIBS OFF ) SET ( DEFAULT_TESTS OFF ) SET ( DEFAULT_EXPERIMENTAL OFF ) +SET ( DEFAULT_MF OFF ) IF ( ${CMAKE_SYSTEM} MATCHES "Linux" ) SET ( DEFAULT_V4L2 ON ) @@ -60,6 +61,7 @@ IF ( ${CMAKE_SYSTEM} MATCHES "Linux" ) SET ( DEFAULT_CEC ON ) ELSEIF ( WIN32 ) SET ( DEFAULT_DX ON ) + SET ( DEFAULT_MF ON ) ELSE() SET ( DEFAULT_V4L2 OFF ) SET ( DEFAULT_FB OFF ) @@ -159,7 +161,7 @@ else() endif() message(STATUS "ENABLE_FB = ${ENABLE_FB}") -option(ENABLE_OSX "Enable the osx grabber" ${DEFAULT_OSX} ) +option(ENABLE_OSX "Enable the OSX grabber" ${DEFAULT_OSX} ) message(STATUS "ENABLE_OSX = ${ENABLE_OSX}") option(ENABLE_SPIDEV "Enable the SPIDEV device" ${DEFAULT_SPIDEV} ) @@ -171,6 +173,9 @@ message(STATUS "ENABLE_TINKERFORGE = ${ENABLE_TINKERFORGE}") option(ENABLE_V4L2 "Enable the V4L2 grabber" ${DEFAULT_V4L2}) message(STATUS "ENABLE_V4L2 = ${ENABLE_V4L2}") +option(ENABLE_MF "Enable the Media Foundation grabber" ${DEFAULT_MF}) +message(STATUS "ENABLE_MF = ${ENABLE_MF}") + option(ENABLE_WS281XPWM "Enable the WS281x-PWM device" ${DEFAULT_WS281XPWM} ) message(STATUS "ENABLE_WS281XPWM = ${ENABLE_WS281XPWM}") @@ -189,7 +194,7 @@ message(STATUS "ENABLE_X11 = ${ENABLE_X11}") option(ENABLE_XCB "Enable the XCB grabber" ${DEFAULT_XCB}) message(STATUS "ENABLE_XCB = ${ENABLE_XCB}") -option(ENABLE_QT "Enable the qt grabber" ${DEFAULT_QT}) +option(ENABLE_QT "Enable the Qt grabber" ${DEFAULT_QT}) message(STATUS "ENABLE_QT = ${ENABLE_QT}") option(ENABLE_DX "Enable the DirectX grabber" ${DEFAULT_DX}) @@ -303,7 +308,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") # The Qt5_DIR should point to Qt5Config.cmake -> C:/Qt/5.xx/msvc2017_64/lib/cmake/Qt5 # The CMAKE_PREFIX_PATH should point to the install directory -> C:/Qt/5.xx/msvc2017_64 # - # Alternatively, use Qt5_BASE_DIR environment variable to point to Qt version to be used + # Alternatively, use Qt5_BASE_DIR environment variable to point to Qt version to be used # In MSVC19 add into CMakeSettings.json # # "environments": [ @@ -333,7 +338,7 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") message(STATUS "Add ${qt_module_path} to CMAKE_MODULE_PATH") SET(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${qt_module_path}") - + #message(STATUS "CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}") #message(STATUS "CMAKE_MODULE_PATH: ${CMAKE_MODULE_PATH}") @@ -394,14 +399,14 @@ find_package(Threads REQUIRED) add_definitions(${QT_DEFINITIONS}) # Add JPEG library -if (ENABLE_V4L2) +if (ENABLE_V4L2 OR ENABLE_MF) # Turbo JPEG find_package(TurboJPEG) if (TURBOJPEG_FOUND) add_definitions(-DHAVE_TURBO_JPEG) message( STATUS "Using Turbo JPEG library: ${TurboJPEG_LIBRARY}") include_directories(${TurboJPEG_INCLUDE_DIRS}) - else() + elseif(ENABLE_V4L2) # System JPEG find_package(JPEG) if (JPEG_FOUND) @@ -411,7 +416,7 @@ if (ENABLE_V4L2) else() message( STATUS "JPEG library not found, MJPEG camera format won't work in V4L2 grabber.") endif() - endif (TURBOJPEG_FOUND) + endif () if (TURBOJPEG_FOUND OR JPEG_FOUND) add_definitions(-DHAVE_JPEG_DECODER) diff --git a/HyperionConfig.h.in b/HyperionConfig.h.in index 00537e71..4933ad00 100644 --- a/HyperionConfig.h.in +++ b/HyperionConfig.h.in @@ -1,48 +1,51 @@ // Generated config file -// Define to enable the dispmanx grabber +// Define to enable the DispmanX grabber #cmakedefine ENABLE_DISPMANX -// Define to enable the v4l2 grabber +// Define to enable the V4L2 grabber #cmakedefine ENABLE_V4L2 -// Define to enable the framebuffer grabber +// Define to enable the Media Foundation grabber +#cmakedefine ENABLE_MF + +// Define to enable the Framebuffer grabber #cmakedefine ENABLE_FB -// Define to enable the amlogic grabber +// Define to enable the AMLogic grabber #cmakedefine ENABLE_AMLOGIC -// Define to enable the osx grabber +// Define to enable the OSX grabber #cmakedefine ENABLE_OSX -// Define to enable the x11 grabber +// Define to enable the X11 grabber #cmakedefine ENABLE_X11 -// Define to enable the xcb grabber +// Define to enable the XCB grabber #cmakedefine ENABLE_XCB -// Define to enable the qt grabber +// Define to enable the Qt grabber #cmakedefine ENABLE_QT // Define to enable the DirectX grabber #cmakedefine ENABLE_DX -// Define to enable the spi-device +// Define to enable the SPI-Device #cmakedefine ENABLE_SPIDEV -// Define to enable the ws281x-pwm-via-dma-device using jgarff's library +// Define to enable the WS281x-PWM-via-DMA-device using jgarff's library #cmakedefine ENABLE_WS281XPWM -// Define to enable the tinkerforge device +// Define to enable the Tinkerforge device #cmakedefine ENABLE_TINKERFORGE -// Define to enable avahi +// Define to enable AVAHI #cmakedefine ENABLE_AVAHI -// Define to enable cec +// Define to enable CEC #cmakedefine ENABLE_CEC -// Define to enable the usb / hid devices +// Define to enable the USB / HID devices #cmakedefine ENABLE_USB_HID // Define to enable profiler for development purpose diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 5e46251e..10575d45 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -424,6 +424,20 @@ "edt_conf_v4l2_sizeDecimation_title": "Size decimation", "edt_conf_v4l2_standard_expl": "Select the video standard for your region. 'Automatic' keeps the value chosen by the v4l2 interface.", "edt_conf_v4l2_standard_title": "Video standard", + "edt_conf_v4l2_fpsSoftwareDecimation_title" : "Software frame skipping", + "edt_conf_v4l2_fpsSoftwareDecimation_expl" : "To save resources every n'th frame will be processed only. For ex. if grabber is set to 30FPS with this option set to 5 the final result will be around 6FPS (1 - disabled)", + "edt_conf_v4l2_encoding_title" : "Encoding format", + "edt_conf_v4l2_encoding_expl" : "Force video encoding for multiformat capable grabbers", + "edt_conf_v4l2_hardware_brightness_title" : "Hardware brightness control", + "edt_conf_v4l2_hardware_brightness_expl" : "Set hardware brightness if device supports it, check logs (0=disabled)", + "edt_conf_v4l2_hardware_contrast_title" : "Hardware contrast control", + "edt_conf_v4l2_hardware_contrast_expl" : "Set hardware contrast if device supports it, check logs (0=disabled)", + "edt_conf_v4l2_noSignalCounterThreshold_title" : "Signal Counter Threshold", + "edt_conf_v4l2_noSignalCounterThreshold_expl" : "Count of frames (check that with grabber's current FPS mode) after which the no signal is triggered", + "edt_conf_v4l2_hardware_saturation_title" : "Hardware saturation control", + "edt_conf_v4l2_hardware_saturation_expl" : "Set hardware saturation if device supports it, check logs (0=disabled)", + "edt_conf_v4l2_hardware_hue_title" : "Hardware hue control", + "edt_conf_v4l2_hardware_hue_expl" : "Set hardware hue if device supports it, check logs (0=disabled)", "edt_conf_webc_crtPath_expl": "Path to the certification file (format should be PEM)", "edt_conf_webc_crtPath_title": "Certificate path", "edt_conf_webc_docroot_expl": "Local webinterface root path (just for webui developer)", @@ -935,4 +949,4 @@ "wiz_yeelight_intro1": "This wizards configures Hyperion for the Yeelight system. Features are the Yeelighs' auto detection, setting each light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", "wiz_yeelight_title": "Yeelight Wizard", "wiz_yeelight_unsupported": "Unsupported" -} \ No newline at end of file +} diff --git a/assets/webconfig/js/content_grabber.js b/assets/webconfig/js/content_grabber.js index 246f32e9..b570379b 100644 --- a/assets/webconfig/js/content_grabber.js +++ b/assets/webconfig/js/content_grabber.js @@ -22,18 +22,25 @@ $(document).ready(function () { "propertyOrder": 3, "required": true }, + "encoding_format": + { + "type": "string", + "title": "edt_conf_v4l2_encoding_title", + "propertyOrder": 5, + "required": true + }, "resolutions": { "type": "string", "title": "edt_conf_v4l2_resolution_title", - "propertyOrder": 6, + "propertyOrder": 8, "required": true }, "framerates": { "type": "string", "title": "edt_conf_v4l2_framerate_title", - "propertyOrder": 9, + "propertyOrder": 11, "required": true } }; @@ -53,7 +60,7 @@ $(document).ready(function () { ? enumTitelVals.push(v4l2_properties[i]['name']) : enumTitelVals.push(v4l2_properties[i]['device']); } - } else if (key == 'resolutions' || key == 'framerates') { + } else if (key == 'resolutions' || key == 'framerates' || key == 'encoding_format') { for (var i = 0; i < v4l2_properties.length; i++) { if (v4l2_properties[i]['device'] == device) { enumVals = enumTitelVals = v4l2_properties[i][key]; @@ -105,7 +112,7 @@ $(document).ready(function () { var val = ed.getValue(); if (key == 'available_devices') { - var V4L2properties = ['device_inputs', 'resolutions', 'framerates']; + var V4L2properties = ['device_inputs', 'resolutions', 'framerates', 'encoding_format']; if (val == 'custom') { var grabberV4L2 = ed.parent; V4L2properties.forEach(function (item) { @@ -134,7 +141,7 @@ $(document).ready(function () { (toggleOption('device', false), toggleOption('input', false), toggleOption('width', false), toggleOption('height', false), - toggleOption('fps', false)); + toggleOption('fps', false), toggleOption('encoding', false)); } else { var grabberV4L2 = ed.parent; V4L2properties.forEach(function (item) { @@ -169,6 +176,11 @@ $(document).ready(function () { val != 'custom' ? toggleOption('input', false) : toggleOption('input', true); + + if (key == 'encoding_format') + val != 'custom' + ? toggleOption('encoding', false) + : toggleOption('encoding', true); }); }); }; @@ -239,8 +251,16 @@ $(document).ready(function () { if (window.serverInfo.grabbers.active) { - var activegrabber = window.serverInfo.grabbers.active.toLowerCase(); - $("#" + selector + " option[value='" + activegrabber + "']").attr('selected', 'selected'); + var activegrabbers = window.serverInfo.grabbers.active.map(v => v.toLowerCase()); + options = $("#" + selector + " option"); + + for (var i = 0; i < options.length; i++) { + var type = options[i].value.toLowerCase(); + if (activegrabbers.indexOf(type) > -1) { + $("#" + selector + " option[value='" + type + "']").attr('selected', 'selected'); + break; + } + } } var selectedType = $("#root_framegrabber_type").val(); @@ -272,7 +292,7 @@ $(document).ready(function () { conf_editor_v4l2.getEditor('root.grabberV4L2.available_devices').setValue('auto'); if (window.serverConfig.grabberV4L2.available_devices == 'auto') { - ['device_inputs', 'standard', 'resolutions', 'framerates'].forEach(function (item) { + ['device_inputs', 'standard', 'resolutions', 'framerates', 'encoding_format'].forEach(function (item) { conf_editor_v4l2.getEditor('root.grabberV4L2.' + item).setValue('auto'); conf_editor_v4l2.getEditor('root.grabberV4L2.' + item).disable(); }); @@ -286,6 +306,9 @@ $(document).ready(function () { if (window.serverConfig.grabberV4L2.framerates == 'custom' && window.serverConfig.grabberV4L2.device != 'auto') toggleOption('fps', true); + + if (window.serverConfig.grabberV4L2.encoding_format == 'custom' && window.serverConfig.grabberV4L2.device != 'auto') + toggleOption('encoding', true); }); $('#btn_submit_v4l2').off().on('click', function () { @@ -303,6 +326,12 @@ $(document).ready(function () { if (v4l2Options.grabberV4L2.device_inputs == 'auto') v4l2Options.grabberV4L2.input = -1; + if (v4l2Options.grabberV4L2.encoding_format != 'custom' && v4l2Options.grabberV4L2.encoding_format != 'auto' && v4l2Options.grabberV4L2.available_devices != 'auto') + v4l2Options.grabberV4L2.encoding = v4l2Options.grabberV4L2.encoding_format; + + if (v4l2Options.grabberV4L2.encoding_format == 'auto' || v4l2Options.grabberV4L2.encoding_format == 'NO_CHANGE') + v4l2Options.grabberV4L2.encoding = 'NO_CHANGE'; + if (v4l2Options.grabberV4L2.resolutions != 'custom' && v4l2Options.grabberV4L2.resolutions != 'auto' && v4l2Options.grabberV4L2.available_devices != 'auto') (v4l2Options.grabberV4L2.width = parseInt(v4l2Options.grabberV4L2.resolutions.split('x')[0]), v4l2Options.grabberV4L2.height = parseInt(v4l2Options.grabberV4L2.resolutions.split('x')[1])); diff --git a/cmake/FindTurboJPEG.cmake b/cmake/FindTurboJPEG.cmake index d6a1c9af..37900e8a 100644 --- a/cmake/FindTurboJPEG.cmake +++ b/cmake/FindTurboJPEG.cmake @@ -3,15 +3,31 @@ # TurboJPEG_INCLUDE_DIRS # TurboJPEG_LIBRARY -find_path(TurboJPEG_INCLUDE_DIRS - NAMES turbojpeg.h - PATH_SUFFIXES include -) +if (ENABLE_MF) + find_path(TurboJPEG_INCLUDE_DIRS + NAMES turbojpeg.h + PATHS + "C:/libjpeg-turbo64" + PATH_SUFFIXES include + ) -find_library(TurboJPEG_LIBRARY - NAMES turbojpeg turbojpeg-static - PATH_SUFFIXES bin lib -) + find_library(TurboJPEG_LIBRARY + NAMES turbojpeg turbojpeg-static + PATHS + "C:/libjpeg-turbo64" + PATH_SUFFIXES bin lib + ) +else() + find_path(TurboJPEG_INCLUDE_DIRS + NAMES turbojpeg.h + PATH_SUFFIXES include + ) + + find_library(TurboJPEG_LIBRARY + NAMES turbojpeg turbojpeg-static + PATH_SUFFIXES bin lib + ) +endif() if(TurboJPEG_INCLUDE_DIRS AND TurboJPEG_LIBRARY) include(CheckCSourceCompiles) @@ -26,7 +42,7 @@ if(TurboJPEG_INCLUDE_DIRS AND TurboJPEG_LIBRARY) endif() include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(TurboJpeg +find_package_handle_standard_args(TurboJPEG FOUND_VAR TURBOJPEG_FOUND REQUIRED_VARS TurboJPEG_LIBRARY TurboJPEG_INCLUDE_DIRS TURBOJPEG_WORKS TurboJPEG_INCLUDE_DIRS TurboJPEG_LIBRARY diff --git a/config/hyperion.config.json.commented b/config/hyperion.config.json.commented deleted file mode 100644 index 35ab38af..00000000 --- a/config/hyperion.config.json.commented +++ /dev/null @@ -1,535 +0,0 @@ -// This is a example config (hyperion.config.json) with comments, in any case you need to create your own one with HyperCon! -// location of all configs: /etc/hyperion -// Webpage: https://www.hyperion-project.org - - -{ - /// general Settings - /// * 'name' : The user friendly name of the hyperion instance (used for network things) - /// * 'versionBranch' : Which branch should be used for hyperion version - /// * 'showOptHelp' : Show option expanations at the webui. Highly recommended for beginners. - "general" : - { - "name" : "MyHyperionConfig", - "watchedVersionBranch" : "Stable", - "showOptHelp" : true - }, - /// set log level: silent warn verbose debug - "logger" : - { - "level" : "warn" - }, - - /// Device configuration contains the following fields: - /// * 'name' : The user friendly name of the device (only used for display purposes) - /// * 'type' : The type of the device - /// * [device type specific configuration] - /// * 'colorOrder' : The order of the color bytes ('rgb', 'rbg', 'bgr', etc.). - /// * 'rewriteTime': in ms. Data is resend to leds, if no new data is available in thistime. 0 means no refresh - "device" : - { - "type" : "file", - "hardwareLedCount" : 1, - "output" : "/dev/null", - "rate" : 1000000, - "colorOrder" : "rgb", - "rewriteTime": 5000 - }, - - /// Color manipulation configuration used to tune the output colors to specific surroundings. - /// The configuration contains a list of color-transforms. Each transform contains the - /// following fields: - /// * 'imageToLedMappingType' : multicolor_mean - every led has it's own calculatedmean color - /// unicolor_mean - every led has same color, color is the mean of whole image - /// * 'channelAdjustment' - /// * 'id' : The unique identifier of the channel adjustments (eg 'device_1') - /// * 'leds' : The indices (or index ranges) of the leds to which this channel adjustment applies - /// (eg '0-5, 9, 11, 12-17'). The indices are zero based. - /// * 'white'/'red'/'green'/'blue'/'cyan'/'magenta'/'yellow' : Array of RGB to adjust the output color - /// * 'gammaRed'/'gammaGreen'/'gammaBlue' : Gamma value for each channel - /// * 'id' : The unique identifier of the channel adjustments (eg 'device_1') - /// * 'id' : The unique identifier of the channel adjustments (eg 'device_1') - /// * 'backlightThreshold' : Minimum brightness (backlight) - /// * 'backlightColored' : backlight with color, instead of white - /// * 'brightness' : overall brightness - /// * 'brightnessCompensation' : 100 means brightness differences are compensated (white is as bright as red, is as bright as yellow. - /// 0 means white is 3x brighter than red, yellow is 2x brighter than red - "color" : - { - "imageToLedMappingType" : "multicolor_mean", - "channelAdjustment" : - [ - { - "id" : "default", - "leds" : "*", - "white" : [255,255,255], - "red" : [255,0,0], - "green" : [0,255,0], - "blue" : [0,0,255], - "cyan" : [0,255,255], - "magenta" : [255,0,255], - "yellow" : [255,255,0], - "gammaRed" : 1.5, - "gammaGreen" : 1.5, - "gammaBlue" : 1.5, - "backlightThreshold" : 0, - "backlightColored" : false, - "brightness" : 100, - "brightnessCompensation" : 80 - } - ] - }, - - /// smoothing - /// * 'smoothing' : Smoothing of the colors in the time-domain with the following tuning - /// parameters: - /// - 'enable' Enable or disable the smoothing (true/false) - /// - 'type' The type of smoothing algorithm ('linear' or 'none') - /// - 'time_ms' The time constant for smoothing algorithm in milliseconds - /// - 'updateFrequency' The update frequency of the leds in Hz - /// - 'updateDelay' The delay of the output to leds (in periods of smoothing) - /// - 'continuousOutput' Flag for enabling continuous output to Leds regardless of new input or not - "smoothing" : - { - "enable" : true, - "type" : "linear", - "time_ms" : 200, - "updateFrequency" : 25.0000, - "updateDelay" : 0, - "continuousOutput" : true - }, - - /// Configuration for the embedded V4L2 grabber - /// * device : V4L2 Device to use [default="auto"] (Auto detection) - /// * width : The width of the grabbed frames (pixels) [default=0] - /// * height : The height of the grabbed frames (pixels) [default=0] - /// * standard : Video standard (PAL/NTSC/SECAM/NO_CHANGE) [default="NO_CHANGE"] - /// * sizeDecimation : Size decimation factor [default=8] - /// * cropLeft : Cropping from the left [default=0] - /// * cropRight : Cropping from the right [default=0] - /// * cropTop : Cropping from the top [default=0] - /// * cropBottom : Cropping from the bottom [default=0] - /// * signalDetection : enable/disable signal detection [default=false] - /// * cecDetection : enable/disable cec detection [default=false] - /// * redSignalThreshold : Signal threshold for the red channel between 0 and 100 [default=5] - /// * greenSignalThreshold : Signal threshold for the green channel between 0 and 100 [default=5] - /// * blueSignalThreshold : Signal threshold for the blue channel between 0 and 100 [default=5] - /// * sDHOffsetMin : area for signal detection - horizontal minimum offset value. Values between 0.0 and 1.0 - /// * sDVOffsetMin : area for signal detection - vertical minimum offset value. Values between 0.0 and 1.0 - /// * sDHOffsetMax : area for signal detection - horizontal maximum offset value. Values between 0.0 and 1.0 - /// * sDVOffsetMax : area for signal detection - vertical maximum offset value. Values between 0.0 and 1.0 - "grabberV4L2" : - { - "device" : "auto", - "width" : 0, - "height" : 0, - "standard" : "NO_CHANGE", - "sizeDecimation" : 8, - "priority" : 240, - "cropLeft" : 0, - "cropRight" : 0, - "cropTop" : 0, - "cropBottom" : 0, - "redSignalThreshold" : 5, - "greenSignalThreshold" : 5, - "blueSignalThreshold" : 5, - "signalDetection" : false, - "cecDetection" : false, - "sDVOffsetMin" : 0.25, - "sDHOffsetMin" : 0.25, - "sDVOffsetMax" : 0.75, - "sDHOffsetMax" : 0.75 - }, - - /// The configuration for the frame-grabber, contains the following items: - /// * type : type of grabber. (auto|osx|dispmanx|amlogic|x11|xcb|framebuffer|qt) [auto] - /// * width : The width of the grabbed frames [pixels] - /// * height : The height of the grabbed frames [pixels] - /// * frequency_Hz : The frequency of the frame grab [Hz] - /// * ATTENTION : Power-of-Two resolution is not supported and leads to unexpected behaviour! - "framegrabber" : - { - // for all type of grabbers - "type" : "framebuffer", - "frequency_Hz" : 10, - "cropLeft" : 0, - "cropRight" : 0, - "cropTop" : 0, - "cropBottom" : 0, - - // valid for grabber: osx|dispmanx|amlogic|framebuffer - "width" : 96, - "height" : 96, - - // valid for x11|xcb|qt - "pixelDecimation" : 8, - - // valid for qt - "display" 0 - }, - - /// The black border configuration, contains the following items: - /// * enable : true if the detector should be activated - /// * threshold : Value below which a pixel is regarded as black (value between 0 and 100 [%]) - /// * unknownFrameCnt : Number of frames without any detection before the border is set to 0 (default 600) - /// * borderFrameCnt : Number of frames before a consistent detected border gets set (default 50) - /// * maxInconsistentCnt : Number of inconsistent frames that are ignored before a new border gets a chance to proof consistency - /// * blurRemoveCnt : Number of pixels that get removed from the detected border to cut away blur (default 1) - /// * mode : Border detection mode (values=default,classic,osd,letterbox) - "blackborderdetector" : - { - "enable" : true, - "threshold" : 5, - "unknownFrameCnt" : 600, - "borderFrameCnt" : 50, - "maxInconsistentCnt" : 10, - "blurRemoveCnt" : 1, - "mode" : "default" - }, - - /// foregroundEffect sets a "booteffect" or "bootcolor" during startup for a given period in ms (duration_ms) - /// * enable : if true, foreground effect is enabled - /// * type : choose between "color" or "effect" - /// * color : if type is color, a color is used (RGB) (example: [0,0,255]) - /// * effect : if type is effect a effect is used (example: "Rainbow swirl fast") - /// * duration_ms : The duration of the selected effect or color (0=endless) - /// HINT: "foregroundEffect" starts always with priority 0, so it blocks all remotes and grabbers if the duration_ms is endless (0) - "foregroundEffect" : - { - "enable" : true, - "type" : "effect", - "color" : [0,0,255], - "effect" : "Rainbow swirl fast", - "duration_ms" : 3000 - }, - - /// backgroundEffect sets a background effect or color. It is used when all capture devices are stopped (manual via remote). Could be also selected via priorities selection. - /// * enable : if true, background effect is enabled - /// * type : choose between "color" or "effect" - /// * color : if type is color, a color is used (RGB) (example: [255,134,0]) - /// * effect : if type is effect a effect is used (example: "Rainbow swirl fast") - "backgroundEffect" : - { - "enable" : true, - "type" : "effect", - "color" : [255,138,0], - "effect" : "Warm mood blobs" - }, - - /// The configuration of the Json/Proto forwarder. Forward messages to multiple instances of Hyperion on same and/or other hosts - /// 'proto' is mostly used for video streams and 'json' for effects - /// * enable : Enable or disable the forwarder (true/false) - /// * proto : Proto server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19401"] or more instances to forward ["127.0.0.1:19401","192.168.0.24:19403"] - /// * json : Json server adress and port of your target. Syntax:[IP:PORT] -> ["127.0.0.1:19446"] or more instances to forward ["127.0.0.1:19446","192.168.0.24:19448"] - /// HINT:If you redirect to "127.0.0.1" (localhost) you could start a second hyperion with another device/led config! - /// Be sure your client(s) is/are listening on the configured ports. The second Hyperion (if used) also needs to be configured! (WebUI -> Settings Level (Expert) -> Configuration -> Network Services -> Forwarder) - "forwarder" : - { - "enable" : false, - "flat" : ["127.0.0.1:19401"], - "json" : ["127.0.0.1:19446"] - }, - - /// The configuration of the Json server which enables the json remote interface - /// * port : Port at which the json server is started - "jsonServer" : - { - "port" : 19444 - }, - - /// The configuration of the Flatbuffer server which enables the Flatbuffer remote interface - /// * port : Port at which the flatbuffer server is started - "flatbufServer" : - { - "enable" : true, - "port" : 19400, - "timeout" : 5 - }, - - /// The configuration of the Protobuffer server which enables the Protobuffer remote interface - /// * port : Port at which the protobuffer server is started - "protoServer" : - { - "enable" : true, - "port" : 19445, - "timeout" : 5 - }, - - /// The configuration of the boblight server which enables the boblight remote interface - /// * enable : Enable or disable the boblight server (true/false) - /// * port : Port at which the boblight server is started - /// * priority : Priority of the boblight server (Default=128) HINT: lower value result in HIGHER priority! - "boblightServer" : - { - "enable" : false, - "port" : 19333, - "priority" : 128 - }, - - /// Configuration of the Hyperion webserver - /// * document_root : path to hyperion webapp files (webconfig developer only) - /// * port : the port where hyperion webapp is accasible - /// * sslPort : the secure (HTTPS) port of the hyperion webapp - /// * crtPath : the path to a certificate file to allow HTTPS connections. Should be in PEM format - /// * keyPath : the path to a private key file to allow HTTPS connections. Should be in PEM format and RSA encrypted - /// * keyPassPhrase : optional: If the key file requires a password add it here - "webConfig" : - { - "document_root" : "/path/to/files", - "port" : 8090, - "sslPort" : 8092, - "crtPath" : "/path/to/mycert.crt", - "keyPath" : "/path/to/mykey.key", - "keyPassPhrase" : "" - }, - - /// The configuration of the effect engine, contains the following items: - /// * paths : An array with absolute location(s) of directories with effects, - /// $ROOT is a keyword which will be replaced with the current rootPath that can be specified on startup from the commandline (defaults to your home directory) - /// * disable : An array with effect names that shouldn't be loaded - "effects" : - { - "paths" : - [ - "$ROOT/custom-effects", - "/usr/share/hyperion/effects" - ], - "disable" : - [ - "Rainbow swirl", - "X-Mas" - ] - }, - - "instCapture" : - { - "systemEnable" : true, - "systemPriority" : 250, - "v4lEnable" : false, - "v4lPriority" : 240 - }, - - /// The configuration of the network security restrictions, contains the following items: - /// * internetAccessAPI : When true allows connection from internet to the API. When false it blocks all outside connections - /// * restirctedInternetAccessAPI : webui voodoo only - ignore it - /// * ipWhitelist : Whitelist ip addresses from the internet to allow access to the API - /// * apiAuth : When true the API requires authentication through tokens to use the API. Read also "localApiAuth" - /// * localApiAuth : When false connections from the local network don't require an API authentification. - /// * localAdminApiAuth : When false connections from the local network don't require an authentification for administration access. - "network" : - { - "internetAccessAPI" : false, - "restirctedInternetAccessAPI" : false, - "ipWhitelist" : [], - "apiAuth" : true, - "localApiAuth" : false, - "localAdminAuth": true - }, - - /// Recreate and save led layouts made with web config. These values are just helpers for ui, not for Hyperion. - "ledConfig" : - { - "classic": - { - "top" : 8, - "bottom" : 8, - "left" : 5, - "right" : 5, - "glength" : 0, - "gpos" : 0, - "position" : 0, - "reverse" : false, - "hdepth" : 8, - "vdepth" : 5, - "overlap" : 0, - "edgegap" : 0, - "ptlh" : 0, - "ptlv" : 0, - "ptrh" : 100, - "ptrv" : 0, - "pblh" : 0, - "pblv" : 100, - "pbrh" : 100, - "pbrv" : 100 - - }, - "matrix": - { - "ledshoriz": 10, - "ledsvert" : 10, - "cabling" : "snake", - "start" : "top-left" - } - }, - - /// The configuration for each individual led. This contains the specification of the area - /// averaged of an input image for each led to determine its color. Each item in the list - /// contains the following fields: - /// * hmin: The fractional part of the image along the horizontal used for the averaging (minimum) - /// * hmax: The fractional part of the image along the horizontal used for the averaging (maximum) - /// * vmin: The fractional part of the image along the vertical used for the averaging (minimum) - /// * vmax: The fractional part of the image along the vertical used for the averaging (maximum) - /// * colorOrder: Usually the global colorOrder is set at the device section, you can overwrite it here per led - - "leds": - [ - { - "hmax": 0.125, - "hmin": 0, - "vmax": 0.08, - "vmin": 0 - - }, - { - "hmax": 0.25, - "hmin": 0.125, - "vmax": 0.08, - "vmin": 0 - - }, - { - "hmax": 0.375, - "hmin": 0.25, - "vmax": 0.08, - "vmin": 0 - }, - { - "hmax": 0.5, - "hmin": 0.375, - "vmax": 0.08, - "vmin": 0 - }, - { - "hmax": 0.625, - "hmin": 0.5, - "vmax": 0.08, - "vmin": 0 - }, - { - "hmax": 0.75, - "hmin": 0.625, - "vmax": 0.08, - "vmin": 0 - }, - { - "hmax": 0.875, - "hmin": 0.75, - "vmax": 0.08, - "vmin": 0 - }, - { - "hmax": 1, - "hmin": 0.875, - "vmax": 0.08, - "vmin": 0 - }, - { - "hmax": 1, - "hmin": 0.95, - "vmax": 0.2, - "vmin": 0 - }, - { - "hmax": 1, - "hmin": 0.95, - "vmax": 0.4, - "vmin": 0.2 - }, - { - "hmax": 1, - "hmin": 0.95, - "vmax": 0.6, - "vmin": 0.4 - }, - { - "hmax": 1, - "hmin": 0.95, - "vmax": 0.8, - "vmin": 0.6 - }, - { - "hmax": 1, - "hmin": 0.95, - "vmax": 1, - "vmin": 0.8 - }, - { - "hmax": 1, - "hmin": 0.875, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.875, - "hmin": 0.75, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.75, - "hmin": 0.625, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.625, - "hmin": 0.5, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.5, - "hmin": 0.375, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.375, - "hmin": 0.25, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.25, - "hmin": 0.125, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.125, - "hmin": 0, - "vmax": 1, - "vmin": 0.92 - }, - { - "hmax": 0.05, - "hmin": 0, - "vmax": 1, - "vmin": 0.8 - }, - { - "hmax": 0.05, - "hmin": 0, - "vmax": 0.8, - "vmin": 0.6 - }, - { - "hmax": 0.05, - "hmin": 0, - "vmax": 0.6, - "vmin": 0.4 - }, - { - "hmax": 0.05, - "hmin": 0, - "vmax": 0.4, - "vmin": 0.2 - }, - { - "hmax": 0.05, - "hmin": 0, - "vmax": 0.2, - "vmin": 0 - } - ] -} diff --git a/config/hyperion.config.json.default b/config/hyperion.config.json.default index ac5ef2ba..6cc7e6b9 100644 --- a/config/hyperion.config.json.default +++ b/config/hyperion.config.json.default @@ -64,10 +64,12 @@ { "device" : "auto", "input" : 0, + "encoding" : "NO_CHANGE", "width" : 0, "height" : 0, "fps" : 15, "standard" : "NO_CHANGE", + "fpsSoftwareDecimation" : 1, "sizeDecimation" : 8, "cropLeft" : 0, "cropRight" : 0, @@ -77,11 +79,16 @@ "greenSignalThreshold" : 5, "blueSignalThreshold" : 5, "signalDetection" : false, + "noSignalCounterThreshold" : 200, "cecDetection" : false, "sDVOffsetMin" : 0.25, "sDHOffsetMin" : 0.25, "sDVOffsetMax" : 0.75, - "sDHOffsetMax" : 0.75 + "sDHOffsetMax" : 0.75, + "hardware_brightness" : 0, + "hardware_contrast" : 0, + "hardware_saturation" : 0, + "hardware_hue" : 0 }, "framegrabber" : diff --git a/include/grabber/MFGrabber.h b/include/grabber/MFGrabber.h new file mode 100644 index 00000000..d8008407 --- /dev/null +++ b/include/grabber/MFGrabber.h @@ -0,0 +1,138 @@ +#pragma once + +// COM includes +#include + +// Qt includes +#include +#include +#include +#include + +// utils includes +#include +#include +#include + +// decoder thread includes +#include + +// TurboJPEG decoder +#ifdef HAVE_TURBO_JPEG + #include +#endif + +/// Forward class declaration +class SourceReaderCB; +/// Forward struct declaration +struct IMFSourceReader; + +/// +/// Media Foundation capture class +/// + +class MFGrabber : public Grabber +{ + Q_OBJECT + friend class SourceReaderCB; + +public: + struct DevicePropertiesItem + { + int x, y,fps,fps_a,fps_b; + PixelFormat pf; + GUID guid; + }; + + struct DeviceProperties + { + QString name = QString(); + QMultiMap inputs = QMultiMap(); + QStringList displayResolutions = QStringList(); + QStringList framerates = QStringList(); + QList valid = QList(); + }; + + MFGrabber(const QString & device, const unsigned width, const unsigned height, const unsigned fps, const unsigned input, int pixelDecimation); + ~MFGrabber() override; + + void receive_image(const void *frameImageBuffer, int size, QString message); + QRectF getSignalDetectionOffset() const { return QRectF(_x_frac_min, _y_frac_min, _x_frac_max, _y_frac_max); } + bool getSignalDetectionEnabled() const { return _signalDetectionEnabled; } + bool getCecDetectionEnabled() const { return _cecDetectionEnabled; } + QStringList getV4L2devices() const override; + QString getV4L2deviceName(const QString& devicePath) const override { return devicePath; } + QMultiMap getV4L2deviceInputs(const QString& devicePath) const override { return _deviceProperties.value(devicePath).inputs; } + QStringList getResolutions(const QString& devicePath) const override { return _deviceProperties.value(devicePath).displayResolutions; } + QStringList getFramerates(const QString& devicePath) const override { return _deviceProperties.value(devicePath).framerates; } + QStringList getV4L2EncodingFormats(const QString& devicePath) const override; + void setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold, int noSignalCounterThreshold) override; + void setSignalDetectionOffset( double verticalMin, double horizontalMin, double verticalMax, double horizontalMax) override; + void setSignalDetectionEnable(bool enable) override; + void setPixelDecimation(int pixelDecimation) override; + void setCecDetectionEnable(bool enable) override; + void setDeviceVideoStandard(QString device, VideoStandard videoStandard) override; + bool setInput(int input) override; + bool setWidthHeight(int width, int height) override; + bool setFramerate(int fps) override; + void setFpsSoftwareDecimation(int decimation); + void setEncoding(QString enc); + void setBrightnessContrastSaturationHue(int brightness, int contrast, int saturation, int hue); + +public slots: + bool start(); + void stop(); + void newThreadFrame(unsigned int _workerIndex, const Image& image,unsigned int sourceCount); + +signals: + void newFrame(const Image & image); + +private: + struct buffer + { + void *start; + size_t length; + }; + + bool init(); + void uninit(); + bool init_device(QString device, DevicePropertiesItem props); + void uninit_device(); + void enumVideoCaptureDevices(); + void start_capturing(); + bool process_image(const void *frameImageBuffer, int size); + void checkSignalDetectionEnabled(Image image); + + QString _deviceName; + QMap _deviceProperties; + std::vector _buffers; + HRESULT _hr; + SourceReaderCB* _sourceReaderCB; + PixelFormat _pixelFormat; + int _pixelDecimation, + _lineLength, + _frameByteSize, + _noSignalCounterThreshold, + _noSignalCounter, + _fpsSoftwareDecimation, + _brightness, + _contrast, + _saturation, + _hue; + volatile unsigned int _currentFrame; + ColorRgb _noSignalThresholdColor; + bool _signalDetectionEnabled, + _cecDetectionEnabled, + _noSignalDetected, + _initialized; + double _x_frac_min, + _y_frac_min, + _x_frac_max, + _y_frac_max; + MFThreadManager _threadManager; + IMFSourceReader* _sourceReader; + +#ifdef HAVE_TURBO_JPEG + int _subsamp; +#endif +}; diff --git a/include/grabber/MFThread.h b/include/grabber/MFThread.h new file mode 100644 index 00000000..1faaefb0 --- /dev/null +++ b/include/grabber/MFThread.h @@ -0,0 +1,138 @@ +#pragma once + +// Qt includes +#include +#include + +// util includes +#include +#include + +// TurboJPEG decoder +#ifdef HAVE_TURBO_JPEG + #include + #include + #include +#endif + +// Forward class declaration +class MFThreadManager; + +/// Encoder thread for USB devices +class MFThread : public QThread +{ + Q_OBJECT + friend class MFThreadManager; + +public: + MFThread(); + ~MFThread(); + + void setup( + unsigned int threadIndex, PixelFormat pixelFormat, uint8_t* sharedData, + int size, int width, int height, int lineLength, + int subsamp, unsigned cropLeft, unsigned cropTop, unsigned cropBottom, unsigned cropRight, + VideoMode videoMode, int currentFrame, int pixelDecimation); + void run(); + + bool isBusy(); + void noBusy(); + +signals: + void newFrame(unsigned int threadIndex, const Image& data, unsigned int sourceCount); + +private: + void processImageMjpeg(); + +#ifdef HAVE_TURBO_JPEG + tjhandle _decompress; + int _scalingFactorsCount = 0; + tjscalingfactor* _scalingFactors = nullptr; +#endif + + static volatile bool _isActive; + volatile bool _isBusy; + QSemaphore _semaphore; + unsigned int _workerIndex; + PixelFormat _pixelFormat; + uint8_t* _localData; + int _localDataSize; + int _size; + int _width; + int _height; + int _lineLength; + int _subsamp; + unsigned _cropLeft; + unsigned _cropTop; + unsigned _cropBottom; + unsigned _cropRight; + int _currentFrame; + int _pixelDecimation; + ImageResampler _imageResampler; +}; + +class MFThreadManager : public QObject +{ + Q_OBJECT + +public: + MFThreadManager() : _threads(nullptr) + { + int select = QThread::idealThreadCount(); + + if (select >= 2 && select <= 3) + select = 2; + else if (select > 3 && select <= 5) + select = 3; + else if (select > 5) + select = 4; + + _maxThreads = qMax(select, 1); + } + + ~MFThreadManager() + { + if (_threads != nullptr) + { + for(unsigned i=0; i < _maxThreads; i++) + if (_threads[i] != nullptr) + { + _threads[i]->deleteLater(); + _threads[i] = nullptr; + } + delete[] _threads; + _threads = nullptr; + } + } + + void initThreads() + { + if (_maxThreads >= 1) + { + _threads = new MFThread*[_maxThreads]; + for (unsigned i=0; i < _maxThreads; i++) + _threads[i] = new MFThread(); + } + } + + void start() { MFThread::_isActive = true; } + bool isActive() { return MFThread::_isActive; } + + void stop() + { + MFThread::_isActive = false; + + if (_threads != nullptr) + { + for(unsigned i=0; i < _maxThreads; i++) + if (_threads[i] != nullptr) + { + _threads[i]->quit(); + _threads[i]->wait(); + } + } + } + + unsigned int _maxThreads; + MFThread** _threads; +}; diff --git a/include/grabber/MFWrapper.h b/include/grabber/MFWrapper.h new file mode 100644 index 00000000..e726a666 --- /dev/null +++ b/include/grabber/MFWrapper.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +class MFWrapper : public GrabberWrapper +{ + Q_OBJECT + +public: + MFWrapper(const QString & device, const unsigned grabWidth, const unsigned grabHeight, const unsigned fps, const unsigned input, int pixelDecimation); + ~MFWrapper() override; + + bool getSignalDetectionEnable() const; + bool getCecDetectionEnable() const; + +public slots: + bool start() override; + void stop() override; + + void setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold, int noSignalCounterThreshold); + void setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom) override; + void setSignalDetectionOffset(double verticalMin, double horizontalMin, double verticalMax, double horizontalMax); + void setSignalDetectionEnable(bool enable); + void setCecDetectionEnable(bool enable); + void setDeviceVideoStandard(const QString& device, VideoStandard videoStandard); + void handleSettingsUpdate(settings::type type, const QJsonDocument& config) override; + + /// + /// @brief set software decimation (v4l2) + /// + void setFpsSoftwareDecimation(int decimation); + + void setEncoding(QString enc); + + void setBrightnessContrastSaturationHue(int brightness, int contrast, int saturation, int hue); + +private slots: + void newFrame(const Image & image); + + void action() override; + +private: + /// The Media Foundation grabber + MFGrabber _grabber; +}; diff --git a/include/grabber/V4L2Grabber.h b/include/grabber/V4L2Grabber.h index 894317d3..7dcc367e 100644 --- a/include/grabber/V4L2Grabber.h +++ b/include/grabber/V4L2Grabber.h @@ -51,15 +51,7 @@ public: QStringList framerates = QStringList(); }; - V4L2Grabber(const QString & device, - const unsigned width, - const unsigned height, - const unsigned fps, - const unsigned input, - VideoStandard videoStandard, - PixelFormat pixelFormat, - int pixelDecimation - ); + V4L2Grabber(const QString & device, const unsigned width, const unsigned height, const unsigned fps, const unsigned input, VideoStandard videoStandard, PixelFormat pixelFormat, int pixelDecimation); ~V4L2Grabber() override; QRectF getSignalDetectionOffset() const diff --git a/include/grabber/V4L2Wrapper.h b/include/grabber/V4L2Wrapper.h index 78474fd6..b6d6dd7a 100644 --- a/include/grabber/V4L2Wrapper.h +++ b/include/grabber/V4L2Wrapper.h @@ -15,7 +15,7 @@ public: const unsigned input, VideoStandard videoStandard, PixelFormat pixelFormat, - int pixelDecimation ); + int pixelDecimation); ~V4L2Wrapper() override; bool getSignalDetectionEnable() const; diff --git a/include/hyperion/Grabber.h b/include/hyperion/Grabber.h index d198842e..3cb02746 100644 --- a/include/hyperion/Grabber.h +++ b/include/hyperion/Grabber.h @@ -135,6 +135,13 @@ public: /// virtual QMultiMap getV4L2deviceInputs(const QString& /*devicePath*/) const { return QMultiMap(); } + /// + /// @brief Get a list of supported hardware encoding formats + /// @param devicePath The device path + /// @return List of hardware encoding formats on success else empty List + /// + virtual QStringList getV4L2EncodingFormats(const QString& /*devicePath*/) const { return QStringList(); } + /// /// @brief Get a list of supported device resolutions /// @param devicePath The device path diff --git a/include/hyperion/GrabberWrapper.h b/include/hyperion/GrabberWrapper.h index 00a50f6e..2ec44ddb 100644 --- a/include/hyperion/GrabberWrapper.h +++ b/include/hyperion/GrabberWrapper.h @@ -18,9 +18,9 @@ class Grabber; class GlobalSignals; class QTimer; -/// List of Hyperion instances that requested screen capt -static QList GRABBER_SYS_CLIENTS; -static QList GRABBER_V4L_CLIENTS; +/// Map of Hyperion instances with grabber name that requested screen capture +static QMap GRABBER_SYS_CLIENTS; +static QMap GRABBER_V4L_CLIENTS; /// /// This class will be inherted by FramebufferWrapper and others which contains the real capture interface @@ -76,6 +76,13 @@ public: /// virtual QMultiMap getV4L2deviceInputs(const QString& devicePath) const; + /// + /// @brief Get a list of supported hardware encoding formats + /// @param devicePath The device path + /// @return List of hardware encoding formats on success else empty List + /// + virtual QStringList getV4L2EncodingFormats(const QString& devicePath) const; + /// /// @brief Get a list of supported device resolutions /// @param devicePath The device path @@ -92,9 +99,10 @@ public: /// /// @brief Get active grabber name - /// @return Active grabber name + /// @param hyperionInd The instance index + /// @return Active grabbers /// - virtual QString getActive() const; + virtual QStringList getActive(int inst) const; static QStringList availableGrabbers(); diff --git a/include/utils/PixelFormat.h b/include/utils/PixelFormat.h index a60a20f4..c9b911c2 100644 --- a/include/utils/PixelFormat.h +++ b/include/utils/PixelFormat.h @@ -23,32 +23,32 @@ inline PixelFormat parsePixelFormat(const QString& pixelFormat) // convert to lower case QString format = pixelFormat.toLower(); - if (format.compare("yuyv") ) + if (format.compare("yuyv") == 0) { return PixelFormat::YUYV; } - else if (format.compare("uyvy") ) + else if (format.compare("uyvy") == 0) { return PixelFormat::UYVY; } - else if (format.compare("bgr16") ) + else if (format.compare("bgr16") == 0) { return PixelFormat::BGR16; } - else if (format.compare("bgr24") ) + else if (format.compare("bgr24") == 0) { return PixelFormat::BGR24; } - else if (format.compare("rgb32") ) + else if (format.compare("rgb32") == 0) { return PixelFormat::RGB32; } - else if (format.compare("bgr32") ) + else if (format.compare("bgr32") == 0) { return PixelFormat::BGR32; } #ifdef HAVE_JPEG_DECODER - else if (format.compare("mjpeg") ) + else if (format.compare("mjpeg") == 0) { return PixelFormat::MJPEG; } @@ -57,3 +57,41 @@ inline PixelFormat parsePixelFormat(const QString& pixelFormat) // return the default NO_CHANGE return PixelFormat::NO_CHANGE; } + +inline QString pixelFormatToString(const PixelFormat& pixelFormat) +{ + + if ( pixelFormat == PixelFormat::YUYV) + { + return "yuyv"; + } + else if (pixelFormat == PixelFormat::UYVY) + { + return "uyvy"; + } + else if (pixelFormat == PixelFormat::BGR16) + { + return "bgr16"; + } + else if (pixelFormat == PixelFormat::BGR24) + { + return "bgr24"; + } + else if (pixelFormat == PixelFormat::RGB32) + { + return "rgb32"; + } + else if (pixelFormat == PixelFormat::BGR32) + { + return "bgr32"; + } +#ifdef HAVE_JPEG_DECODER + else if (pixelFormat == PixelFormat::MJPEG) + { + return "mjpeg"; + } +#endif + + // return the default NO_CHANGE + return "NO_CHANGE"; +} diff --git a/libsrc/api/JsonAPI.cpp b/libsrc/api/JsonAPI.cpp index 49abfd5b..086fd6f9 100644 --- a/libsrc/api/JsonAPI.cpp +++ b/libsrc/api/JsonAPI.cpp @@ -467,11 +467,18 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString QJsonObject grabbers; QJsonArray availableGrabbers; -#if defined(ENABLE_DISPMANX) || defined(ENABLE_V4L2) || defined(ENABLE_FB) || defined(ENABLE_AMLOGIC) || defined(ENABLE_OSX) || defined(ENABLE_X11) || defined(ENABLE_XCB) || defined(ENABLE_QT) +#if defined(ENABLE_DISPMANX) || defined(ENABLE_V4L2) || defined(ENABLE_MF) || defined(ENABLE_FB) || defined(ENABLE_AMLOGIC) || defined(ENABLE_OSX) || defined(ENABLE_X11) || defined(ENABLE_XCB) || defined(ENABLE_QT) if ( GrabberWrapper::getInstance() != nullptr ) { - grabbers["active"] = GrabberWrapper::getInstance()->getActive(); + QStringList activeGrabbers = GrabberWrapper::getInstance()->getActive(_hyperion->getInstanceIndex()); + QJsonArray activeGrabberNames; + for (auto grabberName : activeGrabbers) + { + activeGrabberNames.append(grabberName); + } + + grabbers["active"] = activeGrabberNames; } // get available grabbers @@ -482,7 +489,7 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString #endif -#if defined(ENABLE_V4L2) +#if defined(ENABLE_V4L2) || defined(ENABLE_MF) QJsonArray availableV4L2devices; for (const auto& devicePath : GrabberWrapper::getInstance()->getV4L2devices()) @@ -502,6 +509,14 @@ void JsonAPI::handleServerInfoCommand(const QJsonObject &message, const QString } device.insert("inputs", availableInputs); + QJsonArray availableEncodingFormats; + QStringList encodingFormats = GrabberWrapper::getInstance()->getV4L2EncodingFormats(devicePath); + for (auto encodingFormat : encodingFormats) + { + availableEncodingFormats.append(encodingFormat); + } + device.insert("encoding_format", availableEncodingFormats); + QJsonArray availableResolutions; QStringList resolutions = GrabberWrapper::getInstance()->getResolutions(devicePath); for (auto resolution : resolutions) diff --git a/libsrc/grabber/CMakeLists.txt b/libsrc/grabber/CMakeLists.txt index 302aece4..6e5f42c1 100644 --- a/libsrc/grabber/CMakeLists.txt +++ b/libsrc/grabber/CMakeLists.txt @@ -12,24 +12,28 @@ endif (ENABLE_FB) if (ENABLE_OSX) add_subdirectory(osx) -endif() +endif(ENABLE_OSX) if (ENABLE_V4L2) add_subdirectory(v4l2) endif (ENABLE_V4L2) +if (ENABLE_MF) + add_subdirectory(mediafoundation) +endif (ENABLE_MF) + if (ENABLE_X11) add_subdirectory(x11) -endif() +endif(ENABLE_X11) if (ENABLE_XCB) add_subdirectory(xcb) -endif() +endif(ENABLE_XCB) if (ENABLE_QT) add_subdirectory(qt) -endif() +endif(ENABLE_QT) if (ENABLE_DX) add_subdirectory(directx) -endif() +endif(ENABLE_DX) diff --git a/libsrc/grabber/mediafoundation/CMakeLists.txt b/libsrc/grabber/mediafoundation/CMakeLists.txt new file mode 100644 index 00000000..0a019e72 --- /dev/null +++ b/libsrc/grabber/mediafoundation/CMakeLists.txt @@ -0,0 +1,16 @@ +# Define the current source locations +SET(CURRENT_HEADER_DIR ${CMAKE_SOURCE_DIR}/include/grabber) +SET(CURRENT_SOURCE_DIR ${CMAKE_SOURCE_DIR}/libsrc/grabber/mediafoundation) + +FILE ( GLOB MF_SOURCES "${CURRENT_HEADER_DIR}/MF*.h" "${CURRENT_SOURCE_DIR}/*.h" "${CURRENT_SOURCE_DIR}/*.cpp" ) + +add_library(mf-grabber ${MF_SOURCES} ) + +target_link_libraries(mf-grabber + hyperion + ${QT_LIBRARIES} +) + +if(TURBOJPEG_FOUND) + target_link_libraries(mf-grabber ${TurboJPEG_LIBRARY}) +endif(TURBOJPEG_FOUND) diff --git a/libsrc/grabber/mediafoundation/MFGrabber.cpp b/libsrc/grabber/mediafoundation/MFGrabber.cpp new file mode 100644 index 00000000..62633330 --- /dev/null +++ b/libsrc/grabber/mediafoundation/MFGrabber.cpp @@ -0,0 +1,868 @@ +#include "MFSourceReaderCB.h" +#include "grabber/MFGrabber.h" + +static PixelFormat GetPixelFormatForGuid(const GUID guid) +{ + if (IsEqualGUID(guid, MFVideoFormat_RGB32)) return PixelFormat::RGB32; + if (IsEqualGUID(guid, MFVideoFormat_YUY2)) return PixelFormat::YUYV; + if (IsEqualGUID(guid, MFVideoFormat_UYVY)) return PixelFormat::UYVY; + if (IsEqualGUID(guid, MFVideoFormat_MJPG)) return PixelFormat::MJPEG; + return PixelFormat::NO_CHANGE; +}; + +MFGrabber::MFGrabber(const QString & device, unsigned width, unsigned height, unsigned fps, unsigned input, int pixelDecimation) + : Grabber("V4L2:"+device) + , _deviceName(device) + , _buffers() + , _hr(S_FALSE) + , _sourceReader(nullptr) + , _pixelDecimation(pixelDecimation) + , _lineLength(-1) + , _frameByteSize(-1) + , _noSignalCounterThreshold(40) + , _noSignalCounter(0) + , _fpsSoftwareDecimation(1) + , _brightness(0) + , _contrast(0) + , _saturation(0) + , _hue(0) + , _currentFrame(0) + , _noSignalThresholdColor(ColorRgb{0,0,0}) + , _signalDetectionEnabled(true) + , _cecDetectionEnabled(true) + , _noSignalDetected(false) + , _initialized(false) + , _x_frac_min(0.25) + , _y_frac_min(0.25) + , _x_frac_max(0.75) + , _y_frac_max(0.75) +{ + setInput(input); + setWidthHeight(width, height); + setFramerate(fps); + // setDeviceVideoStandard(device, videoStandard); + + CoInitializeEx(0, COINIT_MULTITHREADED); + _hr = MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET); + if (FAILED(_hr)) + CoUninitialize(); + else + _sourceReaderCB = new SourceReaderCB(this); +} + +MFGrabber::~MFGrabber() +{ + uninit(); + + SAFE_RELEASE(_sourceReader); + SAFE_RELEASE(_sourceReaderCB); + + if (SUCCEEDED(_hr) && SUCCEEDED(MFShutdown())) + CoUninitialize(); +} + +bool MFGrabber::init() +{ + if (!_initialized && SUCCEEDED(_hr)) + { + QString foundDevice = ""; + int foundIndex = -1, bestGuess = -1, bestGuessMinX = INT_MAX, bestGuessMinFPS = INT_MAX; + bool autoDiscovery = (QString::compare(_deviceName, "auto", Qt::CaseInsensitive) == 0 ); + + // enumerate the video capture devices on the user's system + enumVideoCaptureDevices(); + + if (!autoDiscovery && !_deviceProperties.contains(_deviceName)) + { + Debug(_log, "Device '%s' is not available. Changing to auto.", QSTRING_CSTR(_deviceName)); + autoDiscovery = true; + } + + if (autoDiscovery) + { + Debug(_log, "Forcing auto discovery device"); + if (_deviceProperties.count()>0) + { + foundDevice = _deviceProperties.firstKey(); + _deviceName = foundDevice; + Debug(_log, "Auto discovery set to %s", QSTRING_CSTR(_deviceName)); + } + } + else + foundDevice = _deviceName; + + if (foundDevice.isNull() || foundDevice.isEmpty() || !_deviceProperties.contains(foundDevice)) + { + Error(_log, "Could not find any capture device"); + return false; + } + + MFGrabber::DeviceProperties dev = _deviceProperties[foundDevice]; + + Debug(_log, "Searching for %s %d x %d @ %d fps (%s)", QSTRING_CSTR(foundDevice), _width, _height,_fps, QSTRING_CSTR(pixelFormatToString(_pixelFormat))); + + for( int i = 0; i < dev.valid.count() && foundIndex < 0; ++i ) + { + bool strict = false; + const auto& val = dev.valid[i]; + + if (bestGuess == -1 || (val.x <= bestGuessMinX && val.x >= 640 && val.fps <= bestGuessMinFPS && val.fps >= 10)) + { + bestGuess = i; + bestGuessMinFPS = val.fps; + bestGuessMinX = val.x; + } + + if(_width && _height) + { + strict = true; + if (val.x != _width || val.y != _height) + continue; + } + + if(_fps && _fps!=15) + { + strict = true; + if (val.fps != _fps) + continue; + } + + if(_pixelFormat != PixelFormat::NO_CHANGE) + { + strict = true; + if (val.pf != _pixelFormat) + continue; + } + + if (strict) + foundIndex = i; + } + + if (foundIndex < 0 && bestGuess >= 0) + { + if (!autoDiscovery) + Warning(_log, "Selected resolution not found in supported modes. Forcing best resolution"); + else + Debug(_log, "Forcing best resolution"); + + foundIndex = bestGuess; + } + + if (foundIndex>=0) + { + if (init_device(foundDevice, dev.valid[foundIndex])) + _initialized = true; + } + else + Error(_log, "Could not find any capture device settings"); + } + + return _initialized; +} + +void MFGrabber::uninit() +{ + // stop if the grabber was not stopped + if (_initialized) + { + Debug(_log,"uninit grabber: %s", QSTRING_CSTR(_deviceName)); + stop(); + } +} + +bool MFGrabber::init_device(QString deviceName, DevicePropertiesItem props) +{ + bool setStreamParamOK = false; + PixelFormat pixelformat = GetPixelFormatForGuid(props.guid); + QString error, guid = _deviceProperties[deviceName].name; + HRESULT hr,hr1,hr2; + + Debug(_log, "Init %s, %d x %d @ %d fps (%s) => %s", QSTRING_CSTR(deviceName), props.x, props.y, props.fps, QSTRING_CSTR(pixelFormatToString(pixelformat)), QSTRING_CSTR(guid)); + + IMFMediaSource* device = nullptr; + IMFAttributes* attr; + hr = MFCreateAttributes(&attr, 2); + if (SUCCEEDED(hr)) + { + hr = attr->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + if (SUCCEEDED(hr)) + { + int size = guid.length() + 1024; + wchar_t *name = new wchar_t[size]; + memset(name, 0, size); + guid.toWCharArray(name); + + if (SUCCEEDED(attr->SetString(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, (LPCWSTR)name)) && _sourceReaderCB) + { + hr = MFCreateDeviceSource(attr, &device); + if (FAILED(hr)) + { + SAFE_RELEASE(device);; + error = QString("MFCreateDeviceSource %1").arg(hr); + } + } + else + error = QString("IMFAttributes_SetString_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK %1").arg(hr); + + delete[] name; + } + SAFE_RELEASE(attr); + } + else + { + SAFE_RELEASE(attr); + error = QString("MFCreateAttributes_MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE %1").arg(hr); + } + + if (device) + { + Debug(_log, "Device opened"); + if (_brightness != 0 || _contrast != 0 || _saturation != 0 || _hue != 0) + { + IAMVideoProcAmp *pProcAmp = NULL; + if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&pProcAmp)))) + { + long lMin, lMax, lStep, lDefault, lCaps, Val; + if (_brightness != 0) + { + if (SUCCEEDED(pProcAmp->GetRange(VideoProcAmp_Brightness, &lMin, &lMax, &lStep, &lDefault, &lCaps))) + { + Debug(_log, "Brightness: min=%i, max=%i, default=%i", lMin, lMax, lDefault); + + if (SUCCEEDED(pProcAmp->Get(VideoProcAmp_Brightness, &Val, &lCaps))) + Debug(_log, "Current brightness set to: %i",Val); + + if (SUCCEEDED(pProcAmp->Set(VideoProcAmp_Brightness, _brightness, VideoProcAmp_Flags_Manual))) + Debug(_log, "Brightness set to: %i",_brightness); + else + Error(_log, "Could not set brightness"); + } + else + Error(_log, "Brightness is not supported by the grabber"); + } + + if (_contrast != 0) + { + if (SUCCEEDED(pProcAmp->GetRange(VideoProcAmp_Contrast, &lMin, &lMax, &lStep, &lDefault, &lCaps))) + { + Debug(_log, "Contrast: min=%i, max=%i, default=%i", lMin, lMax, lDefault); + + if (SUCCEEDED(pProcAmp->Get(VideoProcAmp_Contrast, &Val, &lCaps))) + Debug(_log, "Current contrast set to: %i",Val); + + if (SUCCEEDED(pProcAmp->Set(VideoProcAmp_Contrast, _contrast, VideoProcAmp_Flags_Manual))) + Debug(_log, "Contrast set to: %i",_contrast); + else + Error(_log, "Could not set contrast"); + } + else + Error(_log, "Contrast is not supported by the grabber"); + } + + if (_saturation != 0) + { + if (SUCCEEDED(pProcAmp->GetRange(VideoProcAmp_Saturation, &lMin, &lMax, &lStep, &lDefault, &lCaps))) + { + Debug(_log, "Saturation: min=%i, max=%i, default=%i", lMin, lMax, lDefault); + + if (SUCCEEDED(pProcAmp->Get(VideoProcAmp_Saturation, &Val, &lCaps))) + Debug(_log, "Current saturation set to: %i",Val); + + if (SUCCEEDED(pProcAmp->Set(VideoProcAmp_Saturation, _saturation, VideoProcAmp_Flags_Manual))) + Debug(_log, "Saturation set to: %i",_saturation); + else + Error(_log, "Could not set saturation"); + } + else + Error(_log, "Saturation is not supported by the grabber"); + } + + if (_hue != 0) + { + hr = pProcAmp->GetRange(VideoProcAmp_Hue, &lMin, &lMax, &lStep, &lDefault, &lCaps); + + if (SUCCEEDED(hr)) + { + Debug(_log, "Hue: min=%i, max=%i, default=%i", lMin, lMax, lDefault); + + hr = pProcAmp->Get(VideoProcAmp_Hue, &Val, &lCaps); + if (SUCCEEDED(hr)) + Debug(_log, "Current hue set to: %i",Val); + + hr = pProcAmp->Set(VideoProcAmp_Hue, _hue, VideoProcAmp_Flags_Manual); + if (SUCCEEDED(hr)) + Debug(_log, "Hue set to: %i",_hue); + else + Error(_log, "Could not set hue"); + } + else + Error(_log, "Hue is not supported by the grabber"); + } + + pProcAmp->Release(); + } + } + + IMFAttributes* pAttributes; + hr1 = MFCreateAttributes(&pAttributes, 1); + + if (SUCCEEDED(hr1)) + hr2 = pAttributes->SetUnknown(MF_SOURCE_READER_ASYNC_CALLBACK, (IMFSourceReaderCallback *)_sourceReaderCB); + + if (SUCCEEDED(hr1) && SUCCEEDED(hr2)) + hr = MFCreateSourceReaderFromMediaSource(device, pAttributes, &_sourceReader); + else + hr = E_INVALIDARG; + + if (SUCCEEDED(hr1)) + pAttributes->Release(); + + device->Release(); + + if (SUCCEEDED(hr)) + { + IMFMediaType* type; + + hr = MFCreateMediaType(&type); + if (SUCCEEDED(hr)) + { + hr = type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + if (SUCCEEDED(hr)) + { + hr = type->SetGUID(MF_MT_SUBTYPE, props.guid); + if (SUCCEEDED(hr)) + { + hr = MFSetAttributeSize(type, MF_MT_FRAME_SIZE, props.x, props.y); + if (SUCCEEDED(hr)) + { + hr = MFSetAttributeSize(type, MF_MT_FRAME_RATE, props.fps_a, props.fps_b); + if (SUCCEEDED(hr)) + { + MFSetAttributeRatio(type, MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + + hr = _sourceReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, type); + if (SUCCEEDED(hr)) + { + setStreamParamOK = true; + } + else + error = QString("SetCurrentMediaType %1").arg(hr); + } + else + error = QString("MFSetAttributeSize_MF_MT_FRAME_RATE %1").arg(hr); + } + else + error = QString("SMFSetAttributeSize_MF_MT_FRAME_SIZE %1").arg(hr); + } + else + error = QString("SetGUID_MF_MT_SUBTYPE %1").arg(hr); + } + else + error = QString("SetGUID_MF_MT_MAJOR_TYPE %1").arg(hr); + + type->Release(); + } + else + error = QString("IMFAttributes_SetString %1").arg(hr); + + if (!setStreamParamOK) + Error(_log, "Could not stream set params (%s)", QSTRING_CSTR(error)); + } + else + Error(_log, "MFCreateSourceReaderFromMediaSource (%i)", hr); + } + else + Error(_log, "Could not open device (%s)", QSTRING_CSTR(error)); + + if (!setStreamParamOK) + { + SAFE_RELEASE(_sourceReader); + } + else + { + _pixelFormat = props.pf; + _width = props.x; + _height = props.y; + + switch (_pixelFormat) + { + case PixelFormat::UYVY: + case PixelFormat::YUYV: + { + _frameByteSize = props.x * props.y * 2; + _lineLength = props.x * 2; + } + break; + + case PixelFormat::RGB32: + { + _frameByteSize = props.x * props.y * 4; + _lineLength = props.x * 3; + } + break; + + case PixelFormat::MJPEG: + { + _lineLength = props.x * 3; + } + break; + } + } + + return setStreamParamOK; +} + +void MFGrabber::uninit_device() +{ + SAFE_RELEASE(_sourceReader); +} + +void MFGrabber::enumVideoCaptureDevices() +{ + if (FAILED(_hr)) + { + Error(_log, "enumVideoCaptureDevices(): Media Foundation not initialized"); + return; + } + + _deviceProperties.clear(); + + IMFAttributes* attr; + if(SUCCEEDED(MFCreateAttributes(&attr, 1))) + { + if(SUCCEEDED(attr->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID))) + { + UINT32 count; + IMFActivate** devices; + if(SUCCEEDED(MFEnumDeviceSources(attr, &devices, &count))) + { + Debug(_log, "Detected devices: %u", count); + for (UINT32 i = 0; i < count; i++) + { + UINT32 length; + LPWSTR name; + LPWSTR symlink; + + if(SUCCEEDED(devices[i]->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &name, &length))) + { + if(SUCCEEDED(devices[i]->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, &symlink, &length))) + { + QString dev = QString::fromUtf16((const ushort*)name); + MFGrabber::DeviceProperties properties; + properties.name = QString::fromUtf16((const ushort*)symlink); + + Info(_log, "Found capture device: %s", QSTRING_CSTR(dev)); + IMFMediaSource *pSource = nullptr; + if(SUCCEEDED(devices[i]->ActivateObject(IID_PPV_ARGS(&pSource)))) + { + IMFMediaType *pType = nullptr; + IMFSourceReader* reader; + if(SUCCEEDED(MFCreateSourceReaderFromMediaSource(pSource, NULL, &reader))) + { + for (DWORD i = 0; ; i++) + { + if (FAILED(reader->GetNativeMediaType((DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, i, &pType))) + break; + + GUID format; + UINT64 frame_size; + UINT64 frame_rate; + + if( SUCCEEDED(pType->GetGUID(MF_MT_SUBTYPE, &format)) && + SUCCEEDED(pType->GetUINT64(MF_MT_FRAME_SIZE, &frame_size)) && + SUCCEEDED(pType->GetUINT64(MF_MT_FRAME_RATE, &frame_rate)) && + frame_rate > 0) + { + PixelFormat pixelformat = GetPixelFormatForGuid(format); + DWORD w = frame_size >> 32; + DWORD h = (DWORD) frame_size; + DWORD fr1 = frame_rate >> 32; + DWORD fr2 = (DWORD) frame_rate; + if (pixelformat != PixelFormat::NO_CHANGE) + { + int framerate = fr1/fr2; + QString sFrame = QString::number(framerate).rightJustified(2,' '); + QString displayResolutions = QString::number(w).rightJustified(4,' ') +"x"+ QString::number(h).rightJustified(4,' '); + + if (!properties.displayResolutions.contains(displayResolutions)) + properties.displayResolutions << displayResolutions; + + if (!properties.framerates.contains(sFrame)) + properties.framerates << sFrame; + + DevicePropertiesItem di; + di.x = w; + di.y = h; + di.fps = framerate; + di.fps_a = fr1; + di.fps_b = fr2; + di.pf = pixelformat; + di.guid = format; + properties.valid.append(di); + } + } + + pType->Release(); + } + reader->Release(); + } + pSource->Release(); + } + properties.displayResolutions.sort(); + properties.framerates.sort(); + _deviceProperties.insert(dev, properties); + } + CoTaskMemFree(symlink); + } + CoTaskMemFree(name); + devices[i]->Release(); + } + + CoTaskMemFree(devices); + } + attr->Release(); + } + } +} + +void MFGrabber::start_capturing() +{ + if (_sourceReader) + { + HRESULT hr = _sourceReader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, + 0, NULL, NULL, NULL, NULL); + if (!SUCCEEDED(hr)) + Error(_log, "ReadSample (%i)", hr); + } +} + +bool MFGrabber::process_image(const void *frameImageBuffer, int size) +{ + bool frameSend = false; + + unsigned int processFrameIndex = _currentFrame++; + + // frame skipping + if ( (processFrameIndex % _fpsSoftwareDecimation != 0) && (_fpsSoftwareDecimation > 1)) + return frameSend; + + // CEC detection + if (_cecDetectionEnabled) + return frameSend; + + // We do want a new frame... + if (size < _frameByteSize && _pixelFormat != PixelFormat::MJPEG) + Error(_log, "Frame too small: %d != %d", size, _frameByteSize); + else + { + if (_threadManager.isActive()) + { + if (_threadManager._threads == nullptr) + { + _threadManager.initThreads(); + Debug(_log, "Max thread count = %d", _threadManager._maxThreads); + + for (unsigned int i=0; i < _threadManager._maxThreads && _threadManager._threads != nullptr; i++) + { + MFThread* _thread=_threadManager._threads[i]; + connect(_thread, SIGNAL(newFrame(unsigned int, const Image &,unsigned int)), this , SLOT(newThreadFrame(unsigned int, const Image &, unsigned int))); + } + } + + for (unsigned int i=0;_threadManager.isActive() && i < _threadManager._maxThreads && _threadManager._threads != nullptr; i++) + { + if ((_threadManager._threads[i]->isFinished() || !_threadManager._threads[i]->isRunning())) + // aquire lock + if ( _threadManager._threads[i]->isBusy() == false) + { + MFThread* _thread = _threadManager._threads[i]; + _thread->setup(i, _pixelFormat, (uint8_t *)frameImageBuffer, size, _width, _height, _lineLength, _subsamp, _cropLeft, _cropTop, _cropBottom, _cropRight, _videoMode, processFrameIndex, _pixelDecimation); + + if (_threadManager._maxThreads > 1) + _threadManager._threads[i]->start(); + + frameSend = true; + break; + } + } + } + } + + return frameSend; +} + +void MFGrabber::setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold, int noSignalCounterThreshold) +{ + _noSignalThresholdColor.red = uint8_t(255*redSignalThreshold); + _noSignalThresholdColor.green = uint8_t(255*greenSignalThreshold); + _noSignalThresholdColor.blue = uint8_t(255*blueSignalThreshold); + _noSignalCounterThreshold = qMax(1, noSignalCounterThreshold); + + Info(_log, "Signal threshold set to: {%d, %d, %d} and frames: %d", _noSignalThresholdColor.red, _noSignalThresholdColor.green, _noSignalThresholdColor.blue, _noSignalCounterThreshold ); +} + +void MFGrabber::setSignalDetectionOffset(double horizontalMin, double verticalMin, double horizontalMax, double verticalMax) +{ + // rainbow 16 stripes 0.47 0.2 0.49 0.8 + // unicolor: 0.25 0.25 0.75 0.75 + + _x_frac_min = horizontalMin; + _y_frac_min = verticalMin; + _x_frac_max = horizontalMax; + _y_frac_max = verticalMax; + + Info(_log, "Signal detection area set to: %f,%f x %f,%f", _x_frac_min, _y_frac_min, _x_frac_max, _y_frac_max ); +} + +bool MFGrabber::start() +{ + try + { + _threadManager.start(); + Info(_log, "Decoding threads: %d",_threadManager._maxThreads ); + + if (init()) + { + start_capturing(); + Info(_log, "Started"); + return true; + } + } + catch(std::exception& e) + { + Error(_log, "Start failed (%s)", e.what()); + } + + return false; +} + +void MFGrabber::stop() +{ + if (_initialized) + { + _threadManager.stop(); + uninit_device(); + _deviceProperties.clear(); + _initialized = false; + Info(_log, "Stopped"); + } +} + +void MFGrabber::receive_image(const void *frameImageBuffer, int size, QString message) +{ + if (frameImageBuffer == NULL || size ==0) + Error(_log, "Received empty image frame: %s", QSTRING_CSTR(message)); + else + { + if (!message.isEmpty()) + Debug(_log, "Received image frame: %s", QSTRING_CSTR(message)); + process_image(frameImageBuffer, size); + } + + start_capturing(); +} + +void MFGrabber::newThreadFrame(unsigned int threadIndex, const Image& image, unsigned int sourceCount) +{ + checkSignalDetectionEnabled(image); + + // get next frame + if (threadIndex >_threadManager._maxThreads) + Error(_log, "Frame index %d out of range", sourceCount); + + if (threadIndex <= _threadManager._maxThreads) + _threadManager._threads[threadIndex]->noBusy(); +} + +void MFGrabber::checkSignalDetectionEnabled(Image image) +{ + if (_signalDetectionEnabled) + { + // check signal (only in center of the resulting image, because some grabbers have noise values along the borders) + bool noSignal = true; + + // top left + unsigned xOffset = image.width() * _x_frac_min; + unsigned yOffset = image.height() * _y_frac_min; + + // bottom right + unsigned xMax = image.width() * _x_frac_max; + unsigned yMax = image.height() * _y_frac_max; + + for (unsigned x = xOffset; noSignal && x < xMax; ++x) + for (unsigned y = yOffset; noSignal && y < yMax; ++y) + noSignal &= (ColorRgb&)image(x, y) <= _noSignalThresholdColor; + + if (noSignal) + ++_noSignalCounter; + else + { + if (_noSignalCounter >= _noSignalCounterThreshold) + { + _noSignalDetected = true; + Info(_log, "Signal detected"); + } + + _noSignalCounter = 0; + } + + if ( _noSignalCounter < _noSignalCounterThreshold) + { + emit newFrame(image); + } + else if (_noSignalCounter == _noSignalCounterThreshold) + { + _noSignalDetected = false; + Info(_log, "Signal lost"); + } + } + else + emit newFrame(image); +} + +QStringList MFGrabber::getV4L2devices() const +{ + QStringList result = QStringList(); + for (auto it = _deviceProperties.begin(); it != _deviceProperties.end(); ++it) + result << it.key(); + + return result; +} + +QStringList MFGrabber::getV4L2EncodingFormats(const QString& devicePath) const +{ + QStringList result = QStringList(); + + for(int i = 0; i < _deviceProperties[devicePath].valid.count(); ++i ) + if (!result.contains(pixelFormatToString(_deviceProperties[devicePath].valid[i].pf), Qt::CaseInsensitive)) + result << pixelFormatToString(_deviceProperties[devicePath].valid[i].pf).toLower(); + + return result; +} + +void MFGrabber::setSignalDetectionEnable(bool enable) +{ + if (_signalDetectionEnabled != enable) + { + _signalDetectionEnabled = enable; + Info(_log, "Signal detection is now %s", enable ? "enabled" : "disabled"); + } +} + +void MFGrabber::setCecDetectionEnable(bool enable) +{ + if (_cecDetectionEnabled != enable) + { + _cecDetectionEnabled = enable; + Info(_log, QString("CEC detection is now %1").arg(enable ? "enabled" : "disabled").toLocal8Bit()); + } +} + +void MFGrabber::setPixelDecimation(int pixelDecimation) +{ + if (_pixelDecimation != pixelDecimation) + _pixelDecimation = pixelDecimation; +} + +void MFGrabber::setDeviceVideoStandard(QString device, VideoStandard videoStandard) +{ + if (_deviceName != device) + { + _deviceName = device; + if (_initialized && !device.isEmpty()) + { + Debug(_log,"Restarting Media Foundation grabber"); + uninit(); + start(); + } + } +} + +bool MFGrabber::setInput(int input) +{ + if(Grabber::setInput(input)) + { + bool started = _initialized; + uninit(); + if(started) + start(); + + return true; + } + + return false; +} + +bool MFGrabber::setWidthHeight(int width, int height) +{ + if(Grabber::setWidthHeight(width,height)) + { + Debug(_log,"Set width:height to: %i:&i", width, height); + if (_initialized) + { + Debug(_log,"Restarting Media Foundation grabber"); + uninit(); + start(); + } + return true; + } + return false; +} + +bool MFGrabber::setFramerate(int fps) +{ + if(Grabber::setFramerate(fps)) + { + Debug(_log,"Set fps to: %i", fps); + if (_initialized) + { + Debug(_log,"Restarting Media Foundation grabber"); + uninit(); + start(); + } + return true; + } + return false; +} + +void MFGrabber::setFpsSoftwareDecimation(int decimation) +{ + _fpsSoftwareDecimation = decimation; + if (decimation > 1) + Debug(_log,"Every %ith image per second are processed", decimation); +} + +void MFGrabber::setEncoding(QString enc) +{ + if (_pixelFormat != parsePixelFormat(enc)) + { + Debug(_log,"Set encoding to: %s", QSTRING_CSTR(enc)); + _pixelFormat = parsePixelFormat(enc); + if (_initialized) + { + Debug(_log,"Restarting Media Foundation Grabber"); + uninit(); + start(); + } + } +} + +void MFGrabber::setBrightnessContrastSaturationHue(int brightness, int contrast, int saturation, int hue) +{ + if (_brightness != brightness || _contrast != contrast || _saturation != saturation || _hue != hue) + { + _brightness = brightness; + _contrast = contrast; + _saturation = saturation; + _hue = hue; + + Debug(_log,"Set brightness to %i, contrast to %i, saturation to %i, hue to %i", _brightness, _contrast, _saturation, _hue); + + if (_initialized) + { + Debug(_log,"Restarting Media Foundation Grabber"); + uninit(); + start(); + } + } +} diff --git a/libsrc/grabber/mediafoundation/MFSourceReaderCB.h b/libsrc/grabber/mediafoundation/MFSourceReaderCB.h new file mode 100644 index 00000000..5ff330b6 --- /dev/null +++ b/libsrc/grabber/mediafoundation/MFSourceReaderCB.h @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#pragma comment (lib, "ole32.lib") +#pragma comment (lib, "mf.lib") +#pragma comment (lib, "mfplat.lib") +#pragma comment (lib, "mfuuid.lib") +#pragma comment (lib, "mfreadwrite.lib") + +#include + +#define SAFE_RELEASE(x) if(x) { x->Release(); x = nullptr; } + +class SourceReaderCB : public IMFSourceReaderCallback +{ +public: + SourceReaderCB(MFGrabber* grabber) + : _nRefCount(1) + , _grabber(grabber) + , _bEOS(FALSE) + , _hrStatus(S_OK) + { + InitializeCriticalSection(&_critsec); + } + + // IUnknown methods + STDMETHODIMP QueryInterface(REFIID iid, void** ppv) + { + static const QITAB qit[] = + { + QITABENT(SourceReaderCB, IMFSourceReaderCallback), + { 0 }, + }; + return QISearch(this, qit, iid, ppv); + } + + STDMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&_nRefCount); + } + + STDMETHODIMP_(ULONG) Release() + { + ULONG uCount = InterlockedDecrement(&_nRefCount); + if (uCount == 0) + { + delete this; + } + return uCount; + } + + // IMFSourceReaderCallback methods + STDMETHODIMP OnReadSample(HRESULT hrStatus, DWORD dwStreamIndex, + DWORD dwStreamFlags, LONGLONG llTimestamp, IMFSample *pSample) + { + EnterCriticalSection(&_critsec); + + if (SUCCEEDED(hrStatus)) + { + QString error = ""; + bool frameSend = false; + + if (pSample) + { + IMFMediaBuffer* buffer; + + hrStatus = pSample->ConvertToContiguousBuffer(&buffer); + if (SUCCEEDED(hrStatus)) + { + + BYTE* data = nullptr; + DWORD maxLength = 0, currentLength = 0; + + hrStatus = buffer->Lock(&data, &maxLength, ¤tLength); + if(SUCCEEDED(hrStatus)) + { + frameSend = true; + _grabber->receive_image(data,currentLength,error); + + buffer->Unlock(); + } + else + error = QString("buffer->Lock failed => %1").arg(hrStatus); + + SAFE_RELEASE(buffer); + } + else + error = QString("pSample->ConvertToContiguousBuffer failed => %1").arg(hrStatus); + + } + else + error = "pSample is NULL"; + + if (!frameSend) + _grabber->receive_image(NULL,0,error); + } + else + { + // Streaming error. + NotifyError(hrStatus); + } + + if (MF_SOURCE_READERF_ENDOFSTREAM & dwStreamFlags) + { + // Reached the end of the stream. + _bEOS = TRUE; + } + _hrStatus = hrStatus; + + LeaveCriticalSection(&_critsec); + return S_OK; + } + + STDMETHODIMP OnEvent(DWORD, IMFMediaEvent *) { return S_OK; } + STDMETHODIMP OnFlush(DWORD) { return S_OK; } + +private: + virtual ~SourceReaderCB() {} + void NotifyError(HRESULT hr) { Error(_grabber->_log, "Source Reader error"); } + +private: + long _nRefCount; + CRITICAL_SECTION _critsec; + MFGrabber* _grabber; + BOOL _bEOS; + HRESULT _hrStatus; +}; diff --git a/libsrc/grabber/mediafoundation/MFThread.cpp b/libsrc/grabber/mediafoundation/MFThread.cpp new file mode 100644 index 00000000..dba7da6b --- /dev/null +++ b/libsrc/grabber/mediafoundation/MFThread.cpp @@ -0,0 +1,175 @@ +#include "grabber/MFThread.h" +#include + +volatile bool MFThread::_isActive = false; + +MFThread::MFThread(): + _localData(nullptr), + _localDataSize(0), + _decompress(nullptr), + _isBusy(false), + _semaphore(1), + _imageResampler() +{ +} + +MFThread::~MFThread() +{ + if (_decompress == nullptr) + tjDestroy(_decompress); + + if (_localData != NULL) + { + free(_localData); + _localData = NULL; + _localDataSize = 0; + } +} + +void MFThread::setup( + unsigned int threadIndex, PixelFormat pixelFormat, uint8_t* sharedData, + int size, int width, int height, int lineLength, + int subsamp, unsigned cropLeft, unsigned cropTop, unsigned cropBottom, unsigned cropRight, + VideoMode videoMode, int currentFrame, int pixelDecimation) +{ + _workerIndex = threadIndex; + _lineLength = lineLength; + _pixelFormat = pixelFormat; + _size = size; + _width = width; + _height = height; + _subsamp = subsamp; + _cropLeft = cropLeft; + _cropTop = cropTop; + _cropBottom = cropBottom; + _cropRight = cropRight; + _currentFrame = currentFrame; + _pixelDecimation = pixelDecimation; + + _imageResampler.setVideoMode(videoMode); + _imageResampler.setCropping(cropLeft, cropRight, cropTop, cropBottom); + _imageResampler.setHorizontalPixelDecimation(_pixelDecimation); + _imageResampler.setVerticalPixelDecimation(_pixelDecimation); + + if (size > _localDataSize) + { + if (_localData != NULL) + { + free(_localData); + _localData = NULL; + _localDataSize = 0; + } + _localData = (uint8_t *) malloc(size+1); + _localDataSize = size; + } + memcpy(_localData, sharedData, size); +} + +void MFThread::run() +{ + if (_isActive && _width > 0 && _height > 0) + { + if (_pixelFormat == PixelFormat::MJPEG) + { + processImageMjpeg(); + } + else + { + Image image = Image(); + _imageResampler.processImage(_localData, _width, _height, _lineLength, _pixelFormat, image); + emit newFrame(_workerIndex, image, _currentFrame); + } + } +} + +bool MFThread::isBusy() +{ + bool temp; + _semaphore.acquire(); + if (_isBusy) + temp = true; + else + { + temp = false; + _isBusy = true; + } + _semaphore.release(); + return temp; +} + +void MFThread::noBusy() +{ + _semaphore.acquire(); + _isBusy = false; + _semaphore.release(); +} + +void MFThread::processImageMjpeg() +{ + if (_decompress == nullptr) + { + _decompress = tjInitDecompress(); + _scalingFactors = tjGetScalingFactors (&_scalingFactorsCount); + } + + if (tjDecompressHeader2(_decompress, _localData, _size, &_width, &_height, &_subsamp) != 0) + { + if (tjGetErrorCode(_decompress) == TJERR_FATAL) + return; + } + + int scaledWidth = _width, scaledHeight = _height; + if(_scalingFactors != nullptr && _pixelDecimation > 1) + { + for (int i = 0; i < _scalingFactorsCount ; i++) + { + const int tempWidth = TJSCALED(_width, _scalingFactors[i]); + const int tempHeight = TJSCALED(_height, _scalingFactors[i]); + if (tempWidth <= _width/_pixelDecimation && tempHeight <= _height/_pixelDecimation) + { + scaledWidth = tempWidth; + scaledHeight = tempHeight; + break; + } + } + + if (scaledWidth == _width && scaledHeight == _height) + { + scaledWidth = TJSCALED(_width, _scalingFactors[_scalingFactorsCount-1]); + scaledHeight = TJSCALED(_height, _scalingFactors[_scalingFactorsCount-1]); + } + } + + Image srcImage(scaledWidth, scaledHeight); + + if (tjDecompress2(_decompress, _localData , _size, (unsigned char*)srcImage.memptr(), scaledWidth, 0, scaledHeight, TJPF_RGB, TJFLAG_FASTDCT | TJFLAG_FASTUPSAMPLE) != 0) + { + if (tjGetErrorCode(_decompress) == TJERR_FATAL) + return; + } + + // got image, process it + if ( !(_cropLeft > 0 || _cropTop > 0 || _cropBottom > 0 || _cropRight > 0)) + emit newFrame(_workerIndex, srcImage, _currentFrame); + else + { + // calculate the output size + int outputWidth = (_width - _cropLeft - _cropRight); + int outputHeight = (_height - _cropTop - _cropBottom); + + if (outputWidth <= 0 || outputHeight <= 0) + return; + + Image destImage(outputWidth, outputHeight); + + for (unsigned int y = 0; y < destImage.height(); y++) + { + unsigned char* source = (unsigned char*)srcImage.memptr() + (y + _cropTop)*srcImage.width()*3 + _cropLeft*3; + unsigned char* dest = (unsigned char*)destImage.memptr() + y*destImage.width()*3; + memcpy(dest, source, destImage.width()*3); + } + + // emit + emit newFrame(_workerIndex, destImage, _currentFrame); + } +} diff --git a/libsrc/grabber/mediafoundation/MFWrapper.cpp b/libsrc/grabber/mediafoundation/MFWrapper.cpp new file mode 100644 index 00000000..34cba000 --- /dev/null +++ b/libsrc/grabber/mediafoundation/MFWrapper.cpp @@ -0,0 +1,161 @@ +#include + +#include + +// qt +#include + +MFWrapper::MFWrapper(const QString &device, unsigned grabWidth, unsigned grabHeight, unsigned fps, unsigned input, int pixelDecimation ) + : GrabberWrapper("V4L2:"+device, &_grabber, grabWidth, grabHeight, 10) + , _grabber(device, grabWidth, grabHeight, fps, input, pixelDecimation) +{ + _ggrabber = &_grabber; + + // register the image type + qRegisterMetaType>("Image"); + + // Handle the image in the captured thread using a direct connection + connect(&_grabber, &MFGrabber::newFrame, this, &MFWrapper::newFrame, Qt::DirectConnection); +} + +MFWrapper::~MFWrapper() +{ + stop(); +} + +bool MFWrapper::start() +{ + return ( _grabber.start() && GrabberWrapper::start()); +} + +void MFWrapper::stop() +{ + _grabber.stop(); + GrabberWrapper::stop(); +} + +void MFWrapper::setSignalThreshold(double redSignalThreshold, double greenSignalThreshold, double blueSignalThreshold, int noSignalCounterThreshold) +{ + _grabber.setSignalThreshold( redSignalThreshold, greenSignalThreshold, blueSignalThreshold, noSignalCounterThreshold); +} + +void MFWrapper::setCropping(unsigned cropLeft, unsigned cropRight, unsigned cropTop, unsigned cropBottom) +{ + _grabber.setCropping(cropLeft, cropRight, cropTop, cropBottom); +} + +void MFWrapper::setSignalDetectionOffset(double verticalMin, double horizontalMin, double verticalMax, double horizontalMax) +{ + _grabber.setSignalDetectionOffset(verticalMin, horizontalMin, verticalMax, horizontalMax); +} + +void MFWrapper::newFrame(const Image &image) +{ + emit systemImage(_grabberName, image); +} + +void MFWrapper::action() +{ + // dummy +} + +void MFWrapper::setSignalDetectionEnable(bool enable) +{ + _grabber.setSignalDetectionEnable(enable); +} + +bool MFWrapper::getSignalDetectionEnable() const +{ + return _grabber.getSignalDetectionEnabled(); +} + +void MFWrapper::setCecDetectionEnable(bool enable) +{ + _grabber.setCecDetectionEnable(enable); +} + +bool MFWrapper::getCecDetectionEnable() const +{ + return _grabber.getCecDetectionEnabled(); +} + +void MFWrapper::setDeviceVideoStandard(const QString& device, VideoStandard videoStandard) +{ + _grabber.setDeviceVideoStandard(device, VideoStandard::NO_CHANGE); +} + +void MFWrapper::setFpsSoftwareDecimation(int decimation) +{ + _grabber.setFpsSoftwareDecimation(decimation); +} + +void MFWrapper::setEncoding(QString enc) +{ + _grabber.setEncoding(enc); +} + +void MFWrapper::setBrightnessContrastSaturationHue(int brightness, int contrast, int saturation, int hue) +{ + _grabber.setBrightnessContrastSaturationHue(brightness, contrast, saturation, hue); +} + +void MFWrapper::handleSettingsUpdate(settings::type type, const QJsonDocument& config) +{ + if(type == settings::V4L2 && _grabberName.startsWith("V4L2")) + { + // extract settings + const QJsonObject& obj = config.object(); + + // device name, video standard + _grabber.setDeviceVideoStandard( + obj["device"].toString("auto"), + parseVideoStandard(obj["standard"].toString("no-change"))); + + // device input + _grabber.setInput(obj["input"].toInt(-1)); + + // device resolution + _grabber.setWidthHeight(obj["width"].toInt(0), obj["height"].toInt(0)); + + // device framerate + _grabber.setFramerate(obj["fps"].toInt(15)); + + // image size decimation + _grabber.setPixelDecimation(obj["sizeDecimation"].toInt(8)); + + // image cropping + _grabber.setCropping( + obj["cropLeft"].toInt(0), + obj["cropRight"].toInt(0), + obj["cropTop"].toInt(0), + obj["cropBottom"].toInt(0)); + + // Brightness, Contrast, Saturation, Hue + _grabber.setBrightnessContrastSaturationHue(obj["hardware_brightness"].toInt(0), + obj["hardware_contrast"].toInt(0), + obj["hardware_saturation"].toInt(0), + obj["hardware_hue"].toInt(0)); + + // CEC Standby + _grabber.setCecDetectionEnable(obj["cecDetection"].toBool(true)); + + // software frame skipping + _grabber.setFpsSoftwareDecimation(obj["fpsSoftwareDecimation"].toInt(1)); + + // Signal detection + _grabber.setSignalDetectionOffset( + obj["sDHOffsetMin"].toDouble(0.25), + obj["sDVOffsetMin"].toDouble(0.25), + obj["sDHOffsetMax"].toDouble(0.75), + obj["sDVOffsetMax"].toDouble(0.75)); + _grabber.setSignalThreshold( + obj["redSignalThreshold"].toDouble(0.0)/100.0, + obj["greenSignalThreshold"].toDouble(0.0)/100.0, + obj["blueSignalThreshold"].toDouble(0.0)/100.0, + obj["noSignalCounterThreshold"].toInt(50) ); + _grabber.setSignalDetectionEnable(obj["signalDetection"].toBool(true)); + + // Hardware encoding format + _grabber.setEncoding(obj["encoding"].toString("NO_CHANGE")); + } +} diff --git a/libsrc/grabber/v4l2/V4L2Grabber.cpp b/libsrc/grabber/v4l2/V4L2Grabber.cpp index fce3f348..8c6e0338 100644 --- a/libsrc/grabber/v4l2/V4L2Grabber.cpp +++ b/libsrc/grabber/v4l2/V4L2Grabber.cpp @@ -30,15 +30,7 @@ #define V4L2_CAP_META_CAPTURE 0x00800000 // Specified in kernel header v4.16. Required for backward compatibility. #endif -V4L2Grabber::V4L2Grabber(const QString & device - , unsigned width - , unsigned height - , unsigned fps - , unsigned input - , VideoStandard videoStandard - , PixelFormat pixelFormat - , int pixelDecimation - ) +V4L2Grabber::V4L2Grabber(const QString & device, unsigned width, unsigned height, unsigned fps, unsigned input, VideoStandard videoStandard, PixelFormat pixelFormat, int pixelDecimation) : Grabber("V4L2:"+device) , _deviceName() , _videoStandard(videoStandard) @@ -46,7 +38,7 @@ V4L2Grabber::V4L2Grabber(const QString & device , _fileDescriptor(-1) , _buffers() , _pixelFormat(pixelFormat) - , _pixelDecimation(-1) + , _pixelDecimation(pixelDecimation) , _lineLength(-1) , _frameByteSize(-1) , _noSignalCounterThreshold(40) diff --git a/libsrc/grabber/v4l2/V4L2Wrapper.cpp b/libsrc/grabber/v4l2/V4L2Wrapper.cpp index 98207df0..8322053c 100644 --- a/libsrc/grabber/v4l2/V4L2Wrapper.cpp +++ b/libsrc/grabber/v4l2/V4L2Wrapper.cpp @@ -12,7 +12,7 @@ V4L2Wrapper::V4L2Wrapper(const QString &device, unsigned input, VideoStandard videoStandard, PixelFormat pixelFormat, - int pixelDecimation ) + int pixelDecimation) : GrabberWrapper("V4L2:"+device, &_grabber, grabWidth, grabHeight, 10) , _grabber(device, grabWidth, diff --git a/libsrc/hyperion/GrabberWrapper.cpp b/libsrc/hyperion/GrabberWrapper.cpp index 51f04fe3..f7123cf6 100644 --- a/libsrc/hyperion/GrabberWrapper.cpp +++ b/libsrc/hyperion/GrabberWrapper.cpp @@ -65,9 +65,17 @@ bool GrabberWrapper::isActive() const return _timer->isActive(); } -QString GrabberWrapper::getActive() const +QStringList GrabberWrapper::getActive(int inst) const { - return _grabberName; + QStringList result = QStringList(); + + if(GRABBER_V4L_CLIENTS.contains(inst)) + result << GRABBER_V4L_CLIENTS.value(inst); + + if(GRABBER_SYS_CLIENTS.contains(inst)) + result << GRABBER_SYS_CLIENTS.value(inst); + + return result; } QStringList GrabberWrapper::availableGrabbers() @@ -78,7 +86,7 @@ QStringList GrabberWrapper::availableGrabbers() grabbers << "dispmanx"; #endif - #ifdef ENABLE_V4L2 + #if defined(ENABLE_V4L2) || defined(ENABLE_MF) grabbers << "v4l2"; #endif @@ -178,9 +186,9 @@ void GrabberWrapper::handleSourceRequest(hyperion::Components component, int hyp if(component == hyperion::Components::COMP_GRABBER && !_grabberName.startsWith("V4L")) { if(listen && !GRABBER_SYS_CLIENTS.contains(hyperionInd)) - GRABBER_SYS_CLIENTS.append(hyperionInd); + GRABBER_SYS_CLIENTS.insert(hyperionInd, _grabberName); else if (!listen) - GRABBER_SYS_CLIENTS.removeOne(hyperionInd); + GRABBER_SYS_CLIENTS.remove(hyperionInd); if(GRABBER_SYS_CLIENTS.empty()) stop(); @@ -190,9 +198,9 @@ void GrabberWrapper::handleSourceRequest(hyperion::Components component, int hyp else if(component == hyperion::Components::COMP_V4L && _grabberName.startsWith("V4L")) { if(listen && !GRABBER_V4L_CLIENTS.contains(hyperionInd)) - GRABBER_V4L_CLIENTS.append(hyperionInd); + GRABBER_V4L_CLIENTS.insert(hyperionInd, _grabberName); else if (!listen) - GRABBER_V4L_CLIENTS.removeOne(hyperionInd); + GRABBER_V4L_CLIENTS.remove(hyperionInd); if(GRABBER_V4L_CLIENTS.empty()) stop(); @@ -234,6 +242,14 @@ QMultiMap GrabberWrapper::getV4L2deviceInputs(const QString& devic return QMultiMap(); } +QStringList GrabberWrapper::getV4L2EncodingFormats(const QString& devicePath) const +{ + if(_grabberName.startsWith("V4L")) + return _ggrabber->getV4L2EncodingFormats(devicePath); + + return QStringList(); +} + QStringList GrabberWrapper::getResolutions(const QString& devicePath) const { if(_grabberName.startsWith("V4L")) diff --git a/libsrc/hyperion/schema/schema-grabberV4L2.json b/libsrc/hyperion/schema/schema-grabberV4L2.json index 4816fd8b..32641139 100644 --- a/libsrc/hyperion/schema/schema-grabberV4L2.json +++ b/libsrc/hyperion/schema/schema-grabberV4L2.json @@ -28,6 +28,18 @@ "propertyOrder" : 4, "comment" : "The 'device_inputs' settings are dynamically inserted into the WebUI under PropertyOrder '3'." }, + "encoding" : + { + "type" : "string", + "title" : "edt_conf_enum_custom", + "default" : "auto", + "options" : { + "hidden":true + }, + "required" : true, + "propertyOrder" : 6, + "comment" : "The 'device_encodings' settings are dynamically inserted into the WebUI under PropertyOrder '5'." + }, "standard" : { "type" : "string", @@ -38,7 +50,7 @@ "enum_titles" : ["edt_conf_enum_NO_CHANGE", "edt_conf_enum_PAL", "edt_conf_enum_NTSC", "edt_conf_enum_SECAM"] }, "required" : true, - "propertyOrder" : 5 + "propertyOrder" : 7 }, "width" : { @@ -51,8 +63,8 @@ "hidden":true }, "required" : true, - "propertyOrder" : 7, - "comment" : "The 'resolutions' settings are dynamically inserted into the WebUI under PropertyOrder '6'." + "propertyOrder" : 9, + "comment" : "The 'resolutions' settings are dynamically inserted into the WebUI under PropertyOrder '8'." }, "height" : { @@ -65,7 +77,8 @@ "hidden":true }, "required" : true, - "propertyOrder" : 8 + "propertyOrder" : 10, + "comment" : "The 'resolutions' settings are dynamically inserted into the WebUI under PropertyOrder '6'." }, "fps" : { @@ -78,8 +91,18 @@ "hidden":true }, "required" : true, - "propertyOrder" : 10, - "comment" : "The 'framerates' setting is dynamically inserted into the WebUI under PropertyOrder '9'." + "propertyOrder" : 12, + "comment" : "The 'framerates' setting is dynamically inserted into the WebUI under PropertyOrder '11'." + }, + "fpsSoftwareDecimation" : + { + "type" : "integer", + "title" : "edt_conf_v4l2_fpsSoftwareDecimation_title", + "minimum" : 1, + "maximum" : 60, + "default" : 1, + "required" : true, + "propertyOrder" : 13 }, "sizeDecimation" : { @@ -89,7 +112,7 @@ "maximum" : 30, "default" : 6, "required" : true, - "propertyOrder" : 11 + "propertyOrder" : 14 }, "cropLeft" : { @@ -99,7 +122,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 12 + "propertyOrder" : 15 }, "cropRight" : { @@ -109,7 +132,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 13 + "propertyOrder" : 16 }, "cropTop" : { @@ -119,7 +142,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 14 + "propertyOrder" : 17 }, "cropBottom" : { @@ -129,7 +152,7 @@ "default" : 0, "append" : "edt_append_pixel", "required" : true, - "propertyOrder" : 15 + "propertyOrder" : 18 }, "cecDetection" : { @@ -137,7 +160,7 @@ "title" : "edt_conf_v4l2_cecDetection_title", "default" : false, "required" : true, - "propertyOrder" : 16 + "propertyOrder" : 19 }, "signalDetection" : { @@ -145,7 +168,7 @@ "title" : "edt_conf_v4l2_signalDetection_title", "default" : false, "required" : true, - "propertyOrder" : 17 + "propertyOrder" : 20 }, "redSignalThreshold" : { @@ -161,7 +184,7 @@ } }, "required" : true, - "propertyOrder" : 18 + "propertyOrder" : 21 }, "greenSignalThreshold" : { @@ -177,7 +200,7 @@ } }, "required" : true, - "propertyOrder" : 19 + "propertyOrder" : 22 }, "blueSignalThreshold" : { @@ -193,7 +216,22 @@ } }, "required" : true, - "propertyOrder" : 20 + "propertyOrder" : 23 + }, + "noSignalCounterThreshold" : + { + "type" : "integer", + "title" : "edt_conf_v4l2_noSignalCounterThreshold_title", + "minimum" : 1, + "maximum" : 1000, + "default" : 200, + "options": { + "dependencies": { + "signalDetection": true + } + }, + "required" : true, + "propertyOrder" : 24 }, "sDVOffsetMin" : { @@ -209,7 +247,7 @@ } }, "required" : true, - "propertyOrder" : 21 + "propertyOrder" : 25 }, "sDVOffsetMax" : { @@ -225,7 +263,7 @@ } }, "required" : true, - "propertyOrder" : 22 + "propertyOrder" : 26 }, "sDHOffsetMin" : { @@ -241,7 +279,7 @@ } }, "required" : true, - "propertyOrder" : 23 + "propertyOrder" : 27 }, "sDHOffsetMax" : { @@ -257,7 +295,39 @@ } }, "required" : true, - "propertyOrder" : 24 + "propertyOrder" : 28 + }, + "hardware_brightness" : + { + "type" : "integer", + "title" : "edt_conf_v4l2_hardware_brightness_title", + "default" : 0, + "required" : true, + "propertyOrder" : 29 + }, + "hardware_contrast" : + { + "type" : "integer", + "title" : "edt_conf_v4l2_hardware_contrast_title", + "default" : 0, + "required" : true, + "propertyOrder" : 30 + }, + "hardware_saturation" : + { + "type" : "integer", + "title" : "edt_conf_v4l2_hardware_saturation_title", + "default" : 0, + "required" : true, + "propertyOrder" : 31 + }, + "hardware_hue" : + { + "type" : "integer", + "title" : "edt_conf_v4l2_hardware_hue_title", + "default" : 0, + "required" : true, + "propertyOrder" : 32 } }, "additionalProperties" : true diff --git a/src/hyperiond/CMakeLists.txt b/src/hyperiond/CMakeLists.txt index 2ec4cf75..3d22efe4 100644 --- a/src/hyperiond/CMakeLists.txt +++ b/src/hyperiond/CMakeLists.txt @@ -12,7 +12,7 @@ find_package(Qt5Widgets REQUIRED) if (WIN32) include(${CMAKE_SOURCE_DIR}/cmake/win/win_rc.cmake) generate_win_rc_file(hyperiond) -endif() +endif(WIN32) add_executable(hyperiond console.h @@ -27,7 +27,7 @@ add_executable(hyperiond # promote hyperiond as GUI app if (WIN32) target_link_options(hyperiond PUBLIC /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup) -endif() +endif(WIN32) target_link_libraries(hyperiond commandline @@ -52,14 +52,14 @@ endif() if (ENABLE_AVAHI) target_link_libraries(hyperiond bonjour) -endif () +endif (ENABLE_AVAHI) if (ENABLE_AMLOGIC) target_link_libraries(hyperiond Qt5::Core pcre16 dl z ) -endif() +endif(ENABLE_AMLOGIC) if (ENABLE_DISPMANX) IF ( "${PLATFORM}" MATCHES rpi) @@ -70,47 +70,51 @@ if (ENABLE_DISPMANX) SET(BCM_LIBRARIES "") ENDIF() target_link_libraries(hyperiond dispmanx-grabber) -endif () +endif (ENABLE_DISPMANX) if (ENABLE_FB) target_link_libraries(hyperiond framebuffer-grabber) -endif () +endif (ENABLE_FB) if (ENABLE_OSX) target_link_libraries(hyperiond osx-grabber) -endif () +endif (ENABLE_OSX) if (ENABLE_V4L2) target_link_libraries(hyperiond v4l2-grabber) endif () +if (ENABLE_MF) + target_link_libraries(hyperiond mf-grabber) +endif (ENABLE_MF) + if (ENABLE_AMLOGIC) target_link_libraries(hyperiond amlogic-grabber) -endif () +endif (ENABLE_AMLOGIC) if (ENABLE_X11) if(APPLE) include_directories("/opt/X11/include") endif(APPLE) target_link_libraries(hyperiond x11-grabber) -endif () +endif (ENABLE_X11) if (ENABLE_XCB) target_link_libraries(hyperiond xcb-grabber) -endif () +endif (ENABLE_XCB) if (ENABLE_QT) target_link_libraries(hyperiond qt-grabber) -endif () +endif (ENABLE_QT) if (ENABLE_DX) include_directories(${DIRECTX9_INCLUDE_DIRS}) target_link_libraries(hyperiond directx-grabber) -endif () +endif (ENABLE_DX) if (ENABLE_CEC) target_link_libraries(hyperiond cechandler) -endif () +endif (ENABLE_CEC) if(NOT WIN32) install ( TARGETS hyperiond DESTINATION "share/hyperion/bin" COMPONENT "Hyperion" ) diff --git a/src/hyperiond/hyperiond.cpp b/src/hyperiond/hyperiond.cpp index 1c12289d..78d9b414 100644 --- a/src/hyperiond/hyperiond.cpp +++ b/src/hyperiond/hyperiond.cpp @@ -74,6 +74,7 @@ HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool lo , _sslWebserver(nullptr) , _jsonServer(nullptr) , _v4l2Grabber(nullptr) + , _mfGrabber(nullptr) , _dispmanx(nullptr) , _x11Grabber(nullptr) , _xcbGrabber(nullptr) @@ -145,7 +146,7 @@ HyperionDaemon::HyperionDaemon(const QString& rootPath, QObject* parent, bool lo // init system capture (framegrabber) handleSettingsUpdate(settings::SYSTEMCAPTURE, getSetting(settings::SYSTEMCAPTURE)); - // init v4l2 capture + // init v4l2 && media foundation capture handleSettingsUpdate(settings::V4L2, getSetting(settings::V4L2)); // ---- network services ----- @@ -253,15 +254,16 @@ void HyperionDaemon::freeObjects() delete _qtGrabber; delete _dxGrabber; delete _v4l2Grabber; + delete _mfGrabber; _v4l2Grabber = nullptr; - - _amlGrabber = nullptr; - _dispmanx = nullptr; - _fbGrabber = nullptr; - _osxGrabber = nullptr; - _qtGrabber = nullptr; - _dxGrabber = nullptr; + _mfGrabber = nullptr; + _amlGrabber = nullptr; + _dispmanx = nullptr; + _fbGrabber = nullptr; + _osxGrabber = nullptr; + _qtGrabber = nullptr; + _dxGrabber = nullptr; } void HyperionDaemon::startNetworkServices() @@ -579,7 +581,7 @@ void HyperionDaemon::handleSettingsUpdate(settings::type settingsType, const QJs else if (settingsType == settings::V4L2) { -#if defined(ENABLE_CEC) || defined(ENABLE_V4L2) +#if defined(ENABLE_CEC) || defined(ENABLE_V4L2) || defined(ENABLE_MF) const QJsonObject& grabberConfig = config.object(); #endif @@ -594,6 +596,62 @@ void HyperionDaemon::handleSettingsUpdate(settings::type settingsType, const QJs } #endif +#if defined(ENABLE_MF) + if (_mfGrabber == nullptr) + { + _mfGrabber = new MFWrapper( + grabberConfig["device"].toString("auto"), + grabberConfig["width"].toInt(0), + grabberConfig["height"].toInt(0), + grabberConfig["fps"].toInt(15), + grabberConfig["input"].toInt(-1), + grabberConfig["sizeDecimation"].toInt(8)); + + // Image cropping + _mfGrabber->setCropping( + grabberConfig["cropLeft"].toInt(0), + grabberConfig["cropRight"].toInt(0), + grabberConfig["cropTop"].toInt(0), + grabberConfig["cropBottom"].toInt(0)); + + // Software frame decimation + _mfGrabber->setFpsSoftwareDecimation(grabberConfig["fpsSoftwareDecimation"].toInt(1)); + + // Hardware encoding format + _mfGrabber->setEncoding(grabberConfig["encoding"].toString("NONE")); + + // Signal detection + _mfGrabber->setSignalDetectionOffset( + grabberConfig["sDHOffsetMin"].toDouble(0.25), + grabberConfig["sDVOffsetMin"].toDouble(0.25), + grabberConfig["sDHOffsetMax"].toDouble(0.75), + grabberConfig["sDVOffsetMax"].toDouble(0.75)); + _mfGrabber->setSignalThreshold( + grabberConfig["redSignalThreshold"].toDouble(0.0) / 100.0, + grabberConfig["greenSignalThreshold"].toDouble(0.0) / 100.0, + grabberConfig["blueSignalThreshold"].toDouble(0.0) / 100.0, + grabberConfig["noSignalCounterThreshold"].toInt(50) ); + _mfGrabber->setSignalDetectionEnable(grabberConfig["signalDetection"].toBool(true)); + + // CEC Standby + _mfGrabber->setCecDetectionEnable(grabberConfig["cecDetection"].toBool(true)); + + // Brightness, Contrast, Saturation, Hue + _mfGrabber->setBrightnessContrastSaturationHue(grabberConfig["hardware_brightness"].toInt(0), + grabberConfig["hardware_contrast"].toInt(0), + grabberConfig["hardware_saturation"].toInt(0), + grabberConfig["hardware_hue"].toInt(0)); + + Debug(_log, "Media Foundation grabber created"); + + // connect to HyperionDaemon signal + connect(this, &HyperionDaemon::videoMode, _mfGrabber, &MFWrapper::setVideoMode); + connect(this, &HyperionDaemon::settingsChanged, _mfGrabber, &MFWrapper::handleSettingsUpdate); + } +#elif !defined(ENABLE_V4L2) + Warning(_log, "The Media Foundation grabber can not be instantiated, because it has been left out from the build"); +#endif + if (_v4l2Grabber != nullptr) { return; @@ -632,7 +690,7 @@ void HyperionDaemon::handleSettingsUpdate(settings::type settingsType, const QJs // connect to HyperionDaemon signal connect(this, &HyperionDaemon::videoMode, _v4l2Grabber, &V4L2Wrapper::setVideoMode); connect(this, &HyperionDaemon::settingsChanged, _v4l2Grabber, &V4L2Wrapper::handleSettingsUpdate); -#else +#elif !defined(ENABLE_MF) Debug(_log, "The v4l2 grabber is not supported on this platform"); #endif } diff --git a/src/hyperiond/hyperiond.h b/src/hyperiond/hyperiond.h index 7c690a94..6f0d790f 100644 --- a/src/hyperiond/hyperiond.h +++ b/src/hyperiond/hyperiond.h @@ -16,6 +16,12 @@ typedef QObject V4L2Wrapper; #endif +#ifdef ENABLE_MF + #include +#else + typedef QObject MFWrapper; +#endif + #ifdef ENABLE_FB #include #else @@ -171,6 +177,7 @@ private: WebServer* _sslWebserver; JsonServer* _jsonServer; V4L2Wrapper* _v4l2Grabber; + MFWrapper* _mfGrabber; DispmanxWrapper* _dispmanx; X11Wrapper* _x11Grabber; XcbWrapper* _xcbGrabber;