From d0074858bf258821e8aff51c03f7fd987c4944c3 Mon Sep 17 00:00:00 2001 From: "evan.liu" <583229071@qq.com> Date: Fri, 18 Sep 2026 20:41:13 +0800 Subject: [PATCH] =?UTF-8?q?CAIIC=20=E5=AE=9A=E5=88=B6=E7=89=88=20v1.0.0?= =?UTF-8?q?=EF=BC=9A=E8=8B=B1=E6=96=87=E9=BB=98=E8=AE=A4=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E3=80=812K/4K=20=E6=94=AF=E6=8C=81=E3=80=81=E9=9B=86=E6=88=90?= =?UTF-8?q?=E6=8A=A0=E5=9B=BE=E6=8F=92=E4=BB=B6=E3=80=81=E6=9B=B4=E6=8D=A2?= =?UTF-8?q?=E5=93=81=E7=89=8C=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 界面与品牌 - 默认 UI 语言固定为英文,不再跟随系统区域设置自动切换(frontend/OBSApp.cpp),用户仍可在设置中手动切换 - 程序图标更换为 CAIIC logo:exe 图标 obs-studio.ico 及界面/托盘/About 图标 obs.png - About 窗口精简为 logo + 名称 + 版本号,移除项目介绍、赞助名单、Authors/License 标签页 - 版本号定为 1.0.0(构建参数 OBS_VERSION_OVERRIDE=1.0.0) 分辨率支持 - 移除首次启动将画布强制限制为 1920x1080 的逻辑,默认使用显示器原生分辨率 - 设置-视频 与自动配置向导新增 3840x2160 / 2560x1440 预设 - 自动配置向导性能测试上限放宽至 4K60(软硬件编码数据率上限及起始分辨率钳制) 插件 - 集成 obs-shaderfilter 2.6.0(绿幕抠图/自定义着色器滤镜),并适配新版 libobs 的 gs_image_file_ex API - 补齐缺失的源码:obs-browser、obs-websocket、libdshowcapture(含 external/capture-device-support) 工程 - 新增 .gitignore,排除 deploy/ 发布目录与构建日志 --- .gitignore | 4 + caiic_logo.jpg | Bin 0 -> 165025 bytes .../deps/libdshowcapture/src/.gitattributes | 9 + .../deps/libdshowcapture/src/.gitignore | 47 + .../deps/libdshowcapture/src/.gitmodules | 3 + .../deps/libdshowcapture/src/CMakeLists.txt | 88 + obs-studio/deps/libdshowcapture/src/COPYING | 502 ++ obs-studio/deps/libdshowcapture/src/README | 11 + .../deps/libdshowcapture/src/dshowcapture.hpp | 306 + .../src/external/.clang-format | 3 + .../capture-device-support/.gitignore | 13 + .../capture-device-support/CMakeLists.txt | 64 + .../external/capture-device-support/LICENSE | 21 + .../Library/EGAVDevice.h | 72 + .../capture-device-support/Library/EGAVHID.h | 69 + .../Library/EGAVResult.cpp | 131 + .../Library/EGAVResult.h | 213 + .../Library/ElgatoUVCDevice.cpp | 265 + .../Library/ElgatoUVCDevice.h | 84 + .../Library/HDMIInfoFramesAPI.h | 746 ++ .../Library/mac/EGAVHIDImplementation.cpp | 284 + .../Library/mac/EGAVHIDImplementation.h | 76 + .../Library/win/EGAVHIDImplementation.cpp | 216 + .../Library/win/EGAVHIDImplementation.h | 65 + .../SampleCode/DriverInterface.cpp | 156 + .../SampleCode/DriverInterface.h | 72 + .../SampleCode/main.cpp | 91 + .../external/capture-device-support/readme.md | 63 + .../src/source/CoTaskMemPtr.hpp | 49 + .../libdshowcapture/src/source/ComPtr.hpp | 156 + .../src/source/avermedia-encode.h | 66 + .../src/source/capture-filter.cpp | 696 ++ .../src/source/capture-filter.hpp | 173 + .../src/source/device-vendor.cpp | 204 + .../libdshowcapture/src/source/device.cpp | 875 ++ .../libdshowcapture/src/source/device.hpp | 120 + .../libdshowcapture/src/source/dshow-base.cpp | 1031 +++ .../libdshowcapture/src/source/dshow-base.hpp | 80 + .../src/source/dshow-demux.cpp | 156 + .../src/source/dshow-demux.hpp | 38 + .../src/source/dshow-device-defs.hpp | 77 + .../src/source/dshow-encoded-device.cpp | 260 + .../libdshowcapture/src/source/dshow-enum.cpp | 573 ++ .../libdshowcapture/src/source/dshow-enum.hpp | 46 + .../src/source/dshow-formats.cpp | 335 + .../src/source/dshow-formats.hpp | 37 + .../src/source/dshow-media-type.cpp | 96 + .../src/source/dshow-media-type.hpp | 154 + .../src/source/dshowcapture.cpp | 313 + .../src/source/dshowencode.cpp | 110 + .../libdshowcapture/src/source/encoder.cpp | 411 + .../libdshowcapture/src/source/encoder.hpp | 86 + .../src/source/external/.clang-format | 3 + .../src/source/external/IVideoCaptureFilter.h | 238 + .../deps/libdshowcapture/src/source/log.cpp | 96 + .../deps/libdshowcapture/src/source/log.hpp | 37 + .../src/source/output-filter.cpp | 992 +++ .../src/source/output-filter.hpp | 262 + .../deps/libdshowcapture/src/tests/.gitignore | 0 .../src/vs/2013/dshowcapture.sln | 26 + .../vs/2013/dshowcapture/dshowcapture.vcxproj | 179 + .../dshowcapture/dshowcapture.vcxproj.filters | 102 + obs-studio/frontend/OBSApp.cpp | 29 +- .../frontend/cmake/windows/obs-studio.ico | Bin 93455 -> 65121 bytes obs-studio/frontend/dialogs/OBSAbout.cpp | 145 - obs-studio/frontend/dialogs/OBSAbout.hpp | 5 - obs-studio/frontend/forms/OBSAbout.ui | 268 +- obs-studio/frontend/forms/images/obs.png | Bin 6456 -> 40205 bytes .../frontend/settings/OBSBasicSettings.cpp | 2 + obs-studio/frontend/widgets/OBSBasic.cpp | 10 - .../frontend/wizards/AutoConfigTestPage.cpp | 16 +- .../frontend/wizards/AutoConfigVideoPage.cpp | 2 + obs-studio/plugins/CMakeLists.txt | 1 + obs-studio/plugins/obs-browser/.clang-format | 209 + .../plugins/obs-browser/.cmake-format.json | 46 + .../.github/actions/check-changes/action.yaml | 82 + .../actions/run-clang-format/action.yaml | 60 + .../.github/workflows/check-format.yaml | 15 + .../.github/workflows/pr-pull.yaml | 20 + .../obs-browser/.github/workflows/push.yaml | 20 + obs-studio/plugins/obs-browser/.gitignore | 85 + obs-studio/plugins/obs-browser/CMakeLists.txt | 62 + obs-studio/plugins/obs-browser/COPYING | 340 + obs-studio/plugins/obs-browser/README.md | 377 + .../plugins/obs-browser/browser-app.cpp | 498 ++ .../plugins/obs-browser/browser-app.hpp | 117 + .../plugins/obs-browser/browser-client.cpp | 666 ++ .../plugins/obs-browser/browser-client.hpp | 154 + .../plugins/obs-browser/browser-config.h.in | 21 + .../obs-browser/browser-dummy-client.cpp | 87 + .../obs-browser/browser-dummy-client.hpp | 64 + .../plugins/obs-browser/browser-scheme.cpp | 71 + .../plugins/obs-browser/browser-scheme.hpp | 31 + .../plugins/obs-browser/browser-version.h | 19 + .../build-aux/.functions/log_debug | 3 + .../build-aux/.functions/log_error | 3 + .../build-aux/.functions/log_group | 16 + .../obs-browser/build-aux/.functions/log_info | 7 + .../build-aux/.functions/log_output | 7 + .../build-aux/.functions/log_status | 7 + .../build-aux/.functions/log_warning | 7 + .../build-aux/.functions/set_loglevel | 17 + .../obs-browser/build-aux/.run-format.zsh | 276 + .../obs-browser/build-aux/run-clang-format | 1 + .../plugins/obs-browser/cef-headers.hpp | 62 + .../obs-browser/cmake/feature-panels.cmake | 28 + .../cmake/macos/entitlements-helper.gpu.plist | 10 + .../cmake/macos/entitlements-helper.plist | 12 + .../macos/entitlements-helper.plugin.plist | 10 + .../macos/entitlements-helper.renderer.plist | 10 + .../plugins/obs-browser/cmake/os-linux.cmake | 38 + .../plugins/obs-browser/cmake/os-macos.cmake | 71 + .../obs-browser/cmake/os-windows.cmake | 40 + .../cmake/windows/obs-browser-page.manifest | 21 + .../cmake/windows/obs-module-helper.rc.in | 24 + .../cmake/windows/obs-module.rc.in | 24 + .../plugins/obs-browser/data/error.html | 183 + .../plugins/obs-browser/data/locale/af-ZA.ini | 40 + .../plugins/obs-browser/data/locale/ar-SA.ini | 45 + .../plugins/obs-browser/data/locale/az-AZ.ini | 43 + .../plugins/obs-browser/data/locale/ba-RU.ini | 4 + .../plugins/obs-browser/data/locale/be-BY.ini | 42 + .../plugins/obs-browser/data/locale/bg-BG.ini | 38 + .../plugins/obs-browser/data/locale/bn-BD.ini | 30 + .../plugins/obs-browser/data/locale/ca-ES.ini | 41 + .../plugins/obs-browser/data/locale/cs-CZ.ini | 44 + .../plugins/obs-browser/data/locale/da-DK.ini | 41 + .../plugins/obs-browser/data/locale/de-DE.ini | 41 + .../plugins/obs-browser/data/locale/el-GR.ini | 44 + .../plugins/obs-browser/data/locale/en-GB.ini | 1 + .../plugins/obs-browser/data/locale/en-US.ini | 53 + .../plugins/obs-browser/data/locale/eo-UY.ini | 16 + .../plugins/obs-browser/data/locale/es-ES.ini | 41 + .../plugins/obs-browser/data/locale/et-EE.ini | 42 + .../plugins/obs-browser/data/locale/eu-ES.ini | 42 + .../plugins/obs-browser/data/locale/fa-IR.ini | 45 + .../plugins/obs-browser/data/locale/fi-FI.ini | 43 + .../obs-browser/data/locale/fil-PH.ini | 34 + .../plugins/obs-browser/data/locale/fr-FR.ini | 44 + .../plugins/obs-browser/data/locale/gd-GB.ini | 27 + .../plugins/obs-browser/data/locale/gl-ES.ini | 42 + .../plugins/obs-browser/data/locale/he-IL.ini | 44 + .../plugins/obs-browser/data/locale/hi-IN.ini | 42 + .../plugins/obs-browser/data/locale/hr-HR.ini | 43 + .../plugins/obs-browser/data/locale/hu-HU.ini | 43 + .../plugins/obs-browser/data/locale/hy-AM.ini | 45 + .../plugins/obs-browser/data/locale/id-ID.ini | 42 + .../plugins/obs-browser/data/locale/it-IT.ini | 42 + .../plugins/obs-browser/data/locale/ja-JP.ini | 41 + .../plugins/obs-browser/data/locale/ka-GE.ini | 44 + .../plugins/obs-browser/data/locale/kaa.ini | 33 + .../obs-browser/data/locale/kab-KAB.ini | 40 + .../obs-browser/data/locale/kmr-TR.ini | 42 + .../plugins/obs-browser/data/locale/ko-KR.ini | 42 + .../plugins/obs-browser/data/locale/lo-LA.ini | 42 + .../plugins/obs-browser/data/locale/lt-LT.ini | 31 + .../plugins/obs-browser/data/locale/mn-MN.ini | 8 + .../plugins/obs-browser/data/locale/ms-MY.ini | 42 + .../plugins/obs-browser/data/locale/nb-NO.ini | 33 + .../plugins/obs-browser/data/locale/nl-NL.ini | 39 + .../plugins/obs-browser/data/locale/nn-NO.ini | 7 + .../plugins/obs-browser/data/locale/oc-FR.ini | 1 + .../plugins/obs-browser/data/locale/pl-PL.ini | 44 + .../plugins/obs-browser/data/locale/pt-BR.ini | 42 + .../plugins/obs-browser/data/locale/pt-PT.ini | 42 + .../plugins/obs-browser/data/locale/ro-RO.ini | 41 + .../plugins/obs-browser/data/locale/ru-RU.ini | 44 + .../plugins/obs-browser/data/locale/si-LK.ini | 34 + .../plugins/obs-browser/data/locale/sk-SK.ini | 42 + .../plugins/obs-browser/data/locale/sl-SI.ini | 44 + .../plugins/obs-browser/data/locale/sq-AL.ini | 3 + .../plugins/obs-browser/data/locale/sr-CS.ini | 10 + .../plugins/obs-browser/data/locale/sr-SP.ini | 43 + .../plugins/obs-browser/data/locale/sv-SE.ini | 43 + .../obs-browser/data/locale/szl-PL.ini | 20 + .../plugins/obs-browser/data/locale/ta-IN.ini | 12 + .../plugins/obs-browser/data/locale/th-TH.ini | 43 + .../plugins/obs-browser/data/locale/tr-TR.ini | 44 + .../plugins/obs-browser/data/locale/tt-RU.ini | 9 + .../plugins/obs-browser/data/locale/ug-CN.ini | 45 + .../plugins/obs-browser/data/locale/uk-UA.ini | 43 + .../plugins/obs-browser/data/locale/ur-PK.ini | 39 + .../plugins/obs-browser/data/locale/vi-VN.ini | 42 + .../plugins/obs-browser/data/locale/zh-CN.ini | 44 + .../plugins/obs-browser/data/locale/zh-TW.ini | 45 + .../obs-browser/deps/base64/.clang-format | 3 + .../obs-browser/deps/base64/base64.cpp | 123 + .../obs-browser/deps/base64/base64.hpp | 14 + .../obs-browser/deps/ip-string-posix.cpp | 35 + .../obs-browser/deps/ip-string-windows.cpp | 37 + .../plugins/obs-browser/deps/ip-string.hpp | 23 + .../obs-browser/deps/signal-restore.cpp | 49 + .../obs-browser/deps/signal-restore.hpp | 23 + .../plugins/obs-browser/deps/wide-string.cpp | 55 + .../plugins/obs-browser/deps/wide-string.hpp | 23 + obs-studio/plugins/obs-browser/drm-format.cpp | 59 + obs-studio/plugins/obs-browser/drm-format.hpp | 21 + .../obs-browser/linux-keyboard-helpers.hpp | 749 ++ .../obs-browser/obs-browser-page.manifest | 27 + .../obs-browser-page-main.cpp | 134 + .../obs-browser/obs-browser-plugin.cpp | 914 +++ .../obs-browser/obs-browser-source.cpp | 679 ++ .../obs-browser/obs-browser-source.hpp | 135 + .../panel/browser-panel-client.cpp | 517 ++ .../panel/browser-panel-client.hpp | 116 + .../panel/browser-panel-internal.hpp | 68 + .../obs-browser/panel/browser-panel.cpp | 622 ++ .../obs-browser/panel/browser-panel.hpp | 122 + .../plugins/obs-shaderfilter/.clang-format | 2 + .../obs-shaderfilter/.github/FUNDING.yml | 3 + .../.github/actions/build-plugin/action.yml | 77 + .../.github/actions/package-plugin/action.yml | 99 + .../obs-shaderfilter/.github/scripts/.Aptfile | 9 + .../.github/scripts/.Brewfile | 6 + .../.github/scripts/.Wingetfile | 3 + .../.github/scripts/.build.zsh | 246 + .../.github/scripts/.package.zsh | 192 + .../.github/scripts/Build-Windows.ps1 | 101 + .../.github/scripts/Package-Windows.ps1 | 92 + .../.github/scripts/build-linux.sh | 13 + .../.github/scripts/build-linux.zsh | 1 + .../.github/scripts/build-macos.zsh | 1 + .../.github/scripts/check-changes.sh | 11 + .../.github/scripts/check-cmake.sh | 53 + .../.github/scripts/check-format.sh | 60 + .../.github/scripts/package-linux.sh | 13 + .../.github/scripts/package-linux.zsh | 1 + .../.github/scripts/package-macos.zsh | 1 + .../.github/scripts/utils.pwsh/Check-Git.ps1 | 25 + .../scripts/utils.pwsh/Ensure-Location.ps1 | 29 + .../scripts/utils.pwsh/Expand-ArchiveExt.ps1 | 70 + .../utils.pwsh/Install-BuildDependencies.ps1 | 60 + .../scripts/utils.pwsh/Invoke-External.ps1 | 40 + .../scripts/utils.pwsh/Invoke-GitCheckout.ps1 | 117 + .../.github/scripts/utils.pwsh/Logger.ps1 | 123 + .../.github/scripts/utils.pwsh/Setup-Host.ps1 | 103 + .../.github/scripts/utils.pwsh/Setup-Obs.ps1 | 84 + .../.github/scripts/utils.zsh/check_linux | 36 + .../.github/scripts/utils.zsh/check_macos | 20 + .../.github/scripts/utils.zsh/check_packages | 52 + .../.github/scripts/utils.zsh/log_debug | 3 + .../.github/scripts/utils.zsh/log_error | 3 + .../.github/scripts/utils.zsh/log_info | 7 + .../.github/scripts/utils.zsh/log_output | 7 + .../.github/scripts/utils.zsh/log_status | 7 + .../.github/scripts/utils.zsh/log_warning | 5 + .../.github/scripts/utils.zsh/mkcd | 1 + .../.github/scripts/utils.zsh/read_codesign | 7 + .../scripts/utils.zsh/read_codesign_installer | 7 + .../scripts/utils.zsh/read_codesign_pass | 33 + .../scripts/utils.zsh/read_codesign_user | 7 + .../.github/scripts/utils.zsh/set_loglevel | 17 + .../.github/scripts/utils.zsh/setup_ccache | 14 + .../.github/scripts/utils.zsh/setup_linux | 62 + .../.github/scripts/utils.zsh/setup_macos | 127 + .../.github/scripts/utils.zsh/setup_obs | 122 + .../.github/workflows/build.yml | 308 + .../plugins/obs-shaderfilter/.gitignore | 15 + .../plugins/obs-shaderfilter/CMakeLists.txt | 74 + .../obs-shaderfilter/CMakeSettings.json | 127 + obs-studio/plugins/obs-shaderfilter/LICENSE | 339 + obs-studio/plugins/obs-shaderfilter/README.md | 257 + .../plugins/obs-shaderfilter/buildspec.json | 83 + .../cmake/Bundle/macos/Plugin-Info.plist.in | 26 + .../cmake/Bundle/macos/entitlements.plist | 17 + .../Bundle/macos/installer-macos.pkgproj.in | 920 +++ .../cmake/ObsPluginHelpers.cmake | 699 ++ .../data/examples/3d-panel.shader | 256 + .../data/examples/3d_swap_transition.shader | 99 + .../obs-shaderfilter/data/examples/Add.shader | 8 + .../data/examples/BulgePinch.shader | 68 + .../data/examples/Chroma+UV-Distortion.shader | 60 + .../data/examples/Invert.shader | 5 + .../data/examples/Luminance.shader | 51 + .../data/examples/RGSSAA.shader | 145 + .../data/examples/Swirl.shader | 74 + .../obs-shaderfilter/data/examples/VCR.shader | 99 + .../obs-shaderfilter/data/examples/VHS.shader | 127 + .../data/examples/ZigZag.shader | 112 + .../examples/alpha-gaming-bent-camera.shader | 117 + .../data/examples/alpha_border.shader | 34 + .../data/examples/animated_path.effect | 89 + .../data/examples/animated_texture.effect | 160 + .../data/examples/ascii.shader | 100 + .../data/examples/aspect_ratio.shader | 92 + .../data/examples/audio.shader | 42 + .../data/examples/background_removal.effect | 113 + .../data/examples/blend_opacity.shader | 81 + .../data/examples/blink.shader | 14 + .../data/examples/bloom.shader | 66 + .../data/examples/border.shader | 13 + .../data/examples/box-blur.shader | 80 + .../data/examples/bt601tobt709.shader | 13 + .../data/examples/bt709tobt601.shader | 7 + .../data/examples/burn.shader | 159 + .../data/examples/cartoon.effect | 95 + .../data/examples/cell_shaded.shader | 65 + .../data/examples/chromatic-aberration.shader | 205 + .../data/examples/circle-mask-filter.shader | 76 + .../data/examples/circle.shader | 8 + .../data/examples/clock_analog.shader | 158 + .../data/examples/clock_digital_led.shader | 205 + .../data/examples/clock_digital_nixie.shader | 472 ++ .../data/examples/color-depth.shader | 31 + .../data/examples/color_grade_filter.shader | 69 + .../data/examples/corner-pin.shader | 179 + .../data/examples/corner_pin.shader | 79 + .../data/examples/crt-curvature.shader | 39 + .../data/examples/cube_rotating.shader | 73 + .../data/examples/curve.shader | 41 + .../data/examples/cut_rect_per_corner.shader | 176 + .../data/examples/cylinder.shader | 49 + .../data/examples/darken.shader | 13 + .../data/examples/dead-pixel-fixer.shader | 54 + .../data/examples/density_sat_hue.shader | 250 + .../data/examples/diffuse_transition.shader | 83 + .../data/examples/digital-rain.shader | 71 + .../data/examples/displacement_map.shader | 33 + .../examples/displacement_map_advanced.shader | 200 + .../displacement_map_advanced_invert.shader | 200 + .../examples/displacement_map_invert.shader | 33 + .../data/examples/divide_rotate.shader | 80 + .../data/examples/doodle.effect | 85 + .../data/examples/drawings.shader | 134 + .../data/examples/drop_shadow.shader | 55 + .../data/examples/drunk.shader | 151 + .../data/examples/dynamic-mask.shader | 211 + .../data/examples/edge_detection.shader | 91 + .../data/examples/embers.effect | 361 + .../data/examples/emboss.shader | 33 + .../data/examples/emboss_color.shader | 80 + .../data/examples/exeldro-bent-camera.shader | 117 + .../data/examples/fade-transition.shader | 20 + .../data/examples/fill_color_gradient.shader | 72 + .../data/examples/fill_color_linear.shader | 48 + .../examples/fill_color_radial_degrees.shader | 86 + .../fill_color_radial_percentage.shader | 89 + .../data/examples/filter_template.effect | 68 + .../data/examples/filter_template.shader | 64 + .../data/examples/fire-3.effect | 432 + .../data/examples/fire.shader | 245 + .../data/examples/fireworks.shader | 165 + .../data/examples/fireworks2.shader | 119 + .../data/examples/fisheye-xy.shader | 59 + .../data/examples/fisheye.shader | 40 + .../data/examples/flip.shader | 11 + .../data/examples/frosted_glass.shader | 68 + .../data/examples/gamma_correction.shader | 35 + .../examples/gaussian-blur-advanced.shader | 99 + .../data/examples/gaussian-blur-simple.shader | 92 + .../data/examples/gaussian-blur.effect | 144 + .../data/examples/gaussian-example.effect | 182 + .../data/examples/gaussian-simple.shader | 91 + .../data/examples/gb-camera.shader | 155 + .../data/examples/glass.shader | 86 + .../data/examples/glitch-periodic.shader | 93 + .../data/examples/glitch.shader | 72 + .../data/examples/glitch_analog.effect | 173 + .../data/examples/glitch_analog.shader | 144 + .../data/examples/glow.shader | 94 + .../data/examples/gradient.shader | 198 + .../data/examples/halftone.shader | 54 + .../data/examples/hard_blink.shader | 31 + .../data/examples/heat-wave-simple.shader | 62 + .../data/examples/hexagon.shader | 122 + .../data/examples/hsl_hsv_saturation.shader | 162 + .../data/examples/hue-rotaton.shader | 92 + .../data/examples/intensity-scope.shader | 51 + .../data/examples/invert-luma.shader | 112 + .../data/examples/luminance2.shader | 74 + .../data/examples/luminance_alpha.effect | 99 + .../data/examples/matrix.effect | 217 + .../data/examples/motion_blur.shader | 13 + .../data/examples/multiply.shader | 8 + .../data/examples/night_sky.shader | 288 + .../data/examples/noise.shader | 56 + .../data/examples/normal_map.shader | 96 + .../obs-shaderfilter/data/examples/obs.shader | 49 + .../data/examples/opacity.shader | 18 + .../data/examples/page-peel-transition.shader | 66 + .../data/examples/page-peel.shader | 75 + .../data/examples/perlin_noise.shader | 246 + .../data/examples/perspective.shader | 94 + .../data/examples/pie-chart.shader | 150 + .../examples/pixelation-transition.shader | 52 + .../data/examples/pixelation.effect | 101 + .../data/examples/pixelation.shader | 44 + .../data/examples/polar.shader | 74 + .../data/examples/pulse.effect | 74 + .../data/examples/quadrilateral_crop.shader | 73 + .../data/examples/rain-window.shader | 237 + .../data/examples/rainbow.shader | 139 + .../examples/rectangular_drop_shadow.shader | 45 + .../examples/remove_partial_pixels.shader | 27 + .../data/examples/repeat.effect | 68 + .../examples/repeat_grid_center_crop.effect | 51 + .../data/examples/repeat_texture.effect | 91 + .../data/examples/rgb_color_wheel.shader | 91 + .../data/examples/rgb_split.shader | 55 + .../data/examples/rgbvisibility.shader | 63 + .../data/examples/ripple.shader | 44 + .../data/examples/rotating-source.shader | 76 + .../data/examples/rotatoe.effect | 148 + .../data/examples/rounded_rect.shader | 34 + .../data/examples/rounded_rect2.shader | 162 + .../examples/rounded_rect_per_corner.shader | 176 + .../examples/rounded_rect_per_side.shader | 153 + .../data/examples/rounded_stroke.shader | 130 + .../examples/rounded_stroke_gradient.shader | 188 + .../data/examples/scan_line.shader | 95 + .../data/examples/seascape.shader | 272 + .../data/examples/seasick.shader | 40 + .../data/examples/selective_color.shader | 117 + .../data/examples/shake.effect | 137 + .../data/examples/shine.shader | 172 + .../data/examples/simple_gradient.shader | 115 + .../data/examples/simplex_noise.shader | 195 + .../data/examples/smart_denoise.shader | 76 + .../data/examples/specular-shine.shader | 99 + .../data/examples/spotlight.shader | 53 + .../data/examples/tetra.shader | 194 + .../data/examples/thermal.shader | 39 + .../data/examples/tv-crt-subpixel.shader | 70 + .../data/examples/twist.shader | 50 + .../data/examples/two-pass-drop-shadow.effect | 117 + .../data/examples/vignetting.shader | 51 + .../data/examples/voronoi-pixelation.shader | 89 + .../examples/walking-dead-pixel-fixer.shader | 196 + .../data/examples/zoom.shader | 30 + .../data/examples/zoom_blur.shader | 97 + .../data/examples/zoom_blur_transition.shader | 72 + .../data/internal/render_output.effect | 49 + .../obs-shaderfilter/data/locale/de-DE.ini | 20 + .../obs-shaderfilter/data/locale/en-US.ini | 20 + .../data/textures/Rainbow-a.png | Bin 0 -> 234764 bytes .../data/textures/Rainbow-a2.png | Bin 0 -> 220040 bytes .../data/textures/Rainbow-b.png | Bin 0 -> 309324 bytes .../data/textures/Rainbow-c.png | Bin 0 -> 312390 bytes .../data/textures/Rainbow.png | Bin 0 -> 282026 bytes .../data/textures/Rainbow2.png | Bin 0 -> 274882 bytes ...nearApertureGrille15Wide8And5d5Spacing.png | Bin 0 -> 198848 bytes ...reGrille15Wide8And5d5SpacingResizeTo64.png | Bin 0 -> 4173 bytes .../textures/TileableLinearShadowMask.png | Bin 0 -> 218631 bytes .../textures/TileableLinearShadowMaskEDP.png | Bin 0 -> 206668 bytes .../TileableLinearShadowMaskEDPResizeTo64.png | Bin 0 -> 5373 bytes .../TileableLinearShadowMaskResizeTo64.png | Bin 0 -> 6008 bytes ...de9And4d5Horizontal9d14VerticalSpacing.png | Bin 0 -> 204254 bytes ...orizontal9d14VerticalSpacingResizeTo64.png | Bin 0 -> 6916 bytes .../obs-shaderfilter/data/textures/blank.png | Bin 0 -> 2805 bytes .../data/textures/burngradient.png | Bin 0 -> 625 bytes .../data/textures/burngradient_small.png | Bin 0 -> 176 bytes .../obs-shaderfilter/data/textures/canvas.png | Bin 0 -> 89687 bytes .../obs-shaderfilter/data/textures/font.png | Bin 0 -> 1320842 bytes .../data/textures/giraffe.png | Bin 0 -> 7297 bytes .../obs-shaderfilter/data/textures/noise.png | Bin 0 -> 264082 bytes .../plugins/obs-shaderfilter/installer.iss.in | 119 + .../plugins/obs-shaderfilter/media/icon.ico | Bin 0 -> 101350 bytes .../plugins/obs-shaderfilter/media/logo.png | Bin 0 -> 1117 bytes .../obs-shaderfilter/obs-shaderfilter.c | 3655 +++++++++ .../obs-shaderfilter/obs-shaderfilter.h | 0 .../plugins/obs-shaderfilter/resource.rc.in | 32 + .../plugins/obs-shaderfilter/version.h.in | 6 + .../plugins/obs-websocket/.clang-format | 209 + .../plugins/obs-websocket/.cmake-format.json | 46 + .../plugins/obs-websocket/.editorconfig | 25 + .../plugins/obs-websocket/.github/FUNDING.yml | 1 + .../.github/ISSUE_TEMPLATE/bug_report.yaml | 127 + .../.github/ISSUE_TEMPLATE/config.yml | 5 + .../ISSUE_TEMPLATE/feature_request.yaml | 42 + .../.github/images/obsws_logo.png | Bin 0 -> 58525 bytes .../.github/pull_request_template.md | 34 + .../.github/workflows/crowdin_upload.yml | 22 + .../.github/workflows/generate_docs.yml | 27 + .../obs-websocket/.github/workflows/lint.yml | 24 + obs-studio/plugins/obs-websocket/.gitignore | 12 + obs-studio/plugins/obs-websocket/.gitmodules | 0 .../plugins/obs-websocket/.markdownlintignore | 2 + .../plugins/obs-websocket/.markdownlintrc | 3 + .../plugins/obs-websocket/CI/generate-docs.sh | 36 + .../plugins/obs-websocket/CMakeLists.txt | 185 + obs-studio/plugins/obs-websocket/LICENSE | 339 + obs-studio/plugins/obs-websocket/README.md | 93 + .../obs-websocket/cmake/macos/Info.plist.in | 28 + .../cmake/obs-websocket-api.cmake | 14 + .../cmake/obs-websocket-apiConfig.cmake.in | 8 + .../obs-websocket/data/locale/af-ZA.ini | 35 + .../obs-websocket/data/locale/ar-SA.ini | 41 + .../obs-websocket/data/locale/az-AZ.ini | 41 + .../obs-websocket/data/locale/be-BY.ini | 41 + .../obs-websocket/data/locale/ca-ES.ini | 41 + .../obs-websocket/data/locale/cs-CZ.ini | 41 + .../obs-websocket/data/locale/da-DK.ini | 41 + .../obs-websocket/data/locale/de-DE.ini | 41 + .../obs-websocket/data/locale/el-GR.ini | 41 + .../obs-websocket/data/locale/en-GB.ini | 1 + .../obs-websocket/data/locale/en-US.ini | 47 + .../obs-websocket/data/locale/es-ES.ini | 41 + .../obs-websocket/data/locale/et-EE.ini | 41 + .../obs-websocket/data/locale/eu-ES.ini | 40 + .../obs-websocket/data/locale/fa-IR.ini | 41 + .../obs-websocket/data/locale/fi-FI.ini | 41 + .../obs-websocket/data/locale/fil-PH.ini | 37 + .../obs-websocket/data/locale/fr-FR.ini | 41 + .../obs-websocket/data/locale/gl-ES.ini | 41 + .../obs-websocket/data/locale/he-IL.ini | 41 + .../obs-websocket/data/locale/hi-IN.ini | 41 + .../obs-websocket/data/locale/hr-HR.ini | 41 + .../obs-websocket/data/locale/hu-HU.ini | 41 + .../obs-websocket/data/locale/hy-AM.ini | 41 + .../obs-websocket/data/locale/id-ID.ini | 41 + .../obs-websocket/data/locale/it-IT.ini | 41 + .../obs-websocket/data/locale/ja-JP.ini | 41 + .../obs-websocket/data/locale/ka-GE.ini | 41 + .../plugins/obs-websocket/data/locale/kaa.ini | 25 + .../obs-websocket/data/locale/kab-KAB.ini | 36 + .../obs-websocket/data/locale/kmr-TR.ini | 41 + .../obs-websocket/data/locale/ko-KR.ini | 41 + .../obs-websocket/data/locale/lo-LA.ini | 41 + .../obs-websocket/data/locale/ms-MY.ini | 41 + .../obs-websocket/data/locale/nb-NO.ini | 25 + .../obs-websocket/data/locale/nl-NL.ini | 41 + .../obs-websocket/data/locale/nn-NO.ini | 2 + .../obs-websocket/data/locale/pl-PL.ini | 41 + .../obs-websocket/data/locale/pt-BR.ini | 41 + .../obs-websocket/data/locale/pt-PT.ini | 41 + .../obs-websocket/data/locale/ro-RO.ini | 41 + .../obs-websocket/data/locale/ru-RU.ini | 41 + .../obs-websocket/data/locale/si-LK.ini | 22 + .../obs-websocket/data/locale/sk-SK.ini | 41 + .../obs-websocket/data/locale/sl-SI.ini | 41 + .../obs-websocket/data/locale/sq-AL.ini | 11 + .../obs-websocket/data/locale/sr-SP.ini | 41 + .../obs-websocket/data/locale/sv-SE.ini | 41 + .../obs-websocket/data/locale/th-TH.ini | 41 + .../obs-websocket/data/locale/tl-PH.ini | 5 + .../obs-websocket/data/locale/tr-TR.ini | 41 + .../obs-websocket/data/locale/tt-RU.ini | 10 + .../obs-websocket/data/locale/ug-CN.ini | 41 + .../obs-websocket/data/locale/uk-UA.ini | 41 + .../obs-websocket/data/locale/vi-VN.ini | 37 + .../obs-websocket/data/locale/zh-CN.ini | 41 + .../obs-websocket/data/locale/zh-TW.ini | 41 + .../plugins/obs-websocket/docs/.gitignore | 1 + .../plugins/obs-websocket/docs/README.md | 140 + .../plugins/obs-websocket/docs/build_docs.sh | 9 + .../obs-websocket/docs/comments/.gitignore | 2 + .../obs-websocket/docs/comments/.npmrc | 1 + .../obs-websocket/docs/comments/comments.js | 24 + .../obs-websocket/docs/comments/config.json | 4 + .../obs-websocket/docs/comments/package.json | 15 + .../obs-websocket/docs/docs/generate_md.py | 313 + .../docs/docs/partials/enumsHeader.md | 5 + .../docs/docs/partials/eventsHeader.md | 3 + .../docs/docs/partials/introduction.md | 432 + .../docs/docs/partials/requestsHeader.md | 3 + .../docs/docs/process_comments.py | 208 + .../docs/generated/protocol.json | 7243 +++++++++++++++++ .../obs-websocket/docs/generated/protocol.md | 5798 +++++++++++++ .../plugins/obs-websocket/docs/versions.json | 5 + .../lib/example/simplest-plugin.c | 79 + .../obs-websocket/lib/obs-websocket-api.h | 259 + .../plugins/obs-websocket/src/Config.cpp | 227 + obs-studio/plugins/obs-websocket/src/Config.h | 47 + .../obs-websocket/src/WebSocketApi.cpp | 377 + .../plugins/obs-websocket/src/WebSocketApi.h | 89 + .../src/eventhandler/EventHandler.cpp | 671 ++ .../src/eventhandler/EventHandler.h | 201 + .../eventhandler/EventHandler_Canvases.cpp | 88 + .../src/eventhandler/EventHandler_Config.cpp | 145 + .../src/eventhandler/EventHandler_Filters.cpp | 227 + .../src/eventhandler/EventHandler_General.cpp | 36 + .../src/eventhandler/EventHandler_Inputs.cpp | 452 + .../eventhandler/EventHandler_MediaInputs.cpp | 207 + .../src/eventhandler/EventHandler_Outputs.cpp | 174 + .../eventhandler/EventHandler_SceneItems.cpp | 307 + .../src/eventhandler/EventHandler_Scenes.cpp | 184 + .../eventhandler/EventHandler_Transitions.cpp | 155 + .../src/eventhandler/EventHandler_Ui.cpp | 64 + .../eventhandler/types/EventSubscription.h | 224 + .../obs-websocket/src/forms/ConnectInfo.cpp | 135 + .../obs-websocket/src/forms/ConnectInfo.h | 47 + .../obs-websocket/src/forms/ConnectInfo.ui | 204 + .../src/forms/SettingsDialog.cpp | 318 + .../obs-websocket/src/forms/SettingsDialog.h | 55 + .../obs-websocket/src/forms/SettingsDialog.ui | 336 + .../obs-websocket/src/forms/images/help.svg | 1 + .../src/forms/images/help_light.svg | 1 + .../obs-websocket/src/forms/resources.qrc | 6 + .../obs-websocket/src/obs-websocket.cpp | 304 + .../plugins/obs-websocket/src/obs-websocket.h | 53 + .../obs-websocket/src/plugin-macros.h.in | 31 + .../requesthandler/RequestBatchHandler.cpp | 234 + .../src/requesthandler/RequestBatchHandler.h | 31 + .../src/requesthandler/RequestHandler.cpp | 239 + .../src/requesthandler/RequestHandler.h | 227 + .../RequestHandler_Canvases.cpp | 64 + .../requesthandler/RequestHandler_Config.cpp | 658 ++ .../requesthandler/RequestHandler_Filters.cpp | 377 + .../requesthandler/RequestHandler_General.cpp | 374 + .../requesthandler/RequestHandler_Inputs.cpp | 1114 +++ .../RequestHandler_MediaInputs.cpp | 206 + .../requesthandler/RequestHandler_Outputs.cpp | 487 ++ .../requesthandler/RequestHandler_Record.cpp | 248 + .../RequestHandler_SceneItems.cpp | 846 ++ .../requesthandler/RequestHandler_Scenes.cpp | 460 ++ .../requesthandler/RequestHandler_Sources.cpp | 357 + .../requesthandler/RequestHandler_Stream.cpp | 162 + .../RequestHandler_Transitions.cpp | 334 + .../src/requesthandler/RequestHandler_Ui.cpp | 309 + .../src/requesthandler/rpc/Request.cpp | 408 + .../src/requesthandler/rpc/Request.h | 86 + .../rpc/RequestBatchRequest.cpp | 28 + .../requesthandler/rpc/RequestBatchRequest.h | 30 + .../src/requesthandler/rpc/RequestResult.cpp | 38 + .../src/requesthandler/rpc/RequestResult.h | 34 + .../types/RequestBatchExecutionType.h | 84 + .../src/requesthandler/types/RequestStatus.h | 419 + .../obs-websocket/src/utils/Compat.cpp | 32 + .../plugins/obs-websocket/src/utils/Compat.h | 38 + .../obs-websocket/src/utils/Crypto.cpp | 85 + .../plugins/obs-websocket/src/utils/Crypto.h | 32 + .../plugins/obs-websocket/src/utils/Json.cpp | 224 + .../plugins/obs-websocket/src/utils/Json.h | 103 + .../plugins/obs-websocket/src/utils/Obs.cpp | 21 + .../plugins/obs-websocket/src/utils/Obs.h | 294 + .../src/utils/Obs_ActionHelper.cpp | 120 + .../src/utils/Obs_ArrayHelper.cpp | 388 + .../src/utils/Obs_NumberHelper.cpp | 85 + .../src/utils/Obs_ObjectHelper.cpp | 113 + .../src/utils/Obs_SearchHelper.cpp | 139 + .../src/utils/Obs_StringHelper.cpp | 118 + .../src/utils/Obs_VolumeMeter.cpp | 357 + .../obs-websocket/src/utils/Obs_VolumeMeter.h | 100 + .../src/utils/Obs_VolumeMeter_Helpers.h | 107 + .../obs-websocket/src/utils/Platform.cpp | 127 + .../obs-websocket/src/utils/Platform.h | 33 + .../plugins/obs-websocket/src/utils/Utils.h | 27 + .../src/websocketserver/WebSocketServer.cpp | 464 ++ .../src/websocketserver/WebSocketServer.h | 104 + .../WebSocketServer_Protocol.cpp | 411 + .../websocketserver/rpc/WebSocketSession.h | 109 + .../types/WebSocketCloseCode.h | 172 + .../websocketserver/types/WebSocketOpCode.h | 129 + 643 files changed, 83639 insertions(+), 408 deletions(-) create mode 100644 .gitignore create mode 100644 caiic_logo.jpg create mode 100644 obs-studio/deps/libdshowcapture/src/.gitattributes create mode 100644 obs-studio/deps/libdshowcapture/src/.gitignore create mode 100644 obs-studio/deps/libdshowcapture/src/.gitmodules create mode 100644 obs-studio/deps/libdshowcapture/src/CMakeLists.txt create mode 100644 obs-studio/deps/libdshowcapture/src/COPYING create mode 100644 obs-studio/deps/libdshowcapture/src/README create mode 100644 obs-studio/deps/libdshowcapture/src/dshowcapture.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/external/.clang-format create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/.gitignore create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/CMakeLists.txt create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/LICENSE create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVDevice.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVHID.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/HDMIInfoFramesAPI.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.h create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/main.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/external/capture-device-support/readme.md create mode 100644 obs-studio/deps/libdshowcapture/src/source/CoTaskMemPtr.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/ComPtr.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/avermedia-encode.h create mode 100644 obs-studio/deps/libdshowcapture/src/source/capture-filter.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/capture-filter.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/device-vendor.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/device.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/device.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-base.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-base.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-demux.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-demux.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-device-defs.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-encoded-device.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-enum.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-enum.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-formats.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-formats.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-media-type.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshow-media-type.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshowcapture.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/dshowencode.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/encoder.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/encoder.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/external/.clang-format create mode 100644 obs-studio/deps/libdshowcapture/src/source/external/IVideoCaptureFilter.h create mode 100644 obs-studio/deps/libdshowcapture/src/source/log.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/log.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/output-filter.cpp create mode 100644 obs-studio/deps/libdshowcapture/src/source/output-filter.hpp create mode 100644 obs-studio/deps/libdshowcapture/src/tests/.gitignore create mode 100644 obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture.sln create mode 100644 obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj create mode 100644 obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj.filters create mode 100644 obs-studio/plugins/obs-browser/.clang-format create mode 100644 obs-studio/plugins/obs-browser/.cmake-format.json create mode 100644 obs-studio/plugins/obs-browser/.github/actions/check-changes/action.yaml create mode 100644 obs-studio/plugins/obs-browser/.github/actions/run-clang-format/action.yaml create mode 100644 obs-studio/plugins/obs-browser/.github/workflows/check-format.yaml create mode 100644 obs-studio/plugins/obs-browser/.github/workflows/pr-pull.yaml create mode 100644 obs-studio/plugins/obs-browser/.github/workflows/push.yaml create mode 100644 obs-studio/plugins/obs-browser/.gitignore create mode 100644 obs-studio/plugins/obs-browser/CMakeLists.txt create mode 100644 obs-studio/plugins/obs-browser/COPYING create mode 100644 obs-studio/plugins/obs-browser/README.md create mode 100644 obs-studio/plugins/obs-browser/browser-app.cpp create mode 100644 obs-studio/plugins/obs-browser/browser-app.hpp create mode 100644 obs-studio/plugins/obs-browser/browser-client.cpp create mode 100644 obs-studio/plugins/obs-browser/browser-client.hpp create mode 100644 obs-studio/plugins/obs-browser/browser-config.h.in create mode 100644 obs-studio/plugins/obs-browser/browser-dummy-client.cpp create mode 100644 obs-studio/plugins/obs-browser/browser-dummy-client.hpp create mode 100644 obs-studio/plugins/obs-browser/browser-scheme.cpp create mode 100644 obs-studio/plugins/obs-browser/browser-scheme.hpp create mode 100644 obs-studio/plugins/obs-browser/browser-version.h create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/log_debug create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/log_error create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/log_group create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/log_info create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/log_output create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/log_status create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/log_warning create mode 100644 obs-studio/plugins/obs-browser/build-aux/.functions/set_loglevel create mode 100644 obs-studio/plugins/obs-browser/build-aux/.run-format.zsh create mode 100644 obs-studio/plugins/obs-browser/build-aux/run-clang-format create mode 100644 obs-studio/plugins/obs-browser/cef-headers.hpp create mode 100644 obs-studio/plugins/obs-browser/cmake/feature-panels.cmake create mode 100644 obs-studio/plugins/obs-browser/cmake/macos/entitlements-helper.gpu.plist create mode 100644 obs-studio/plugins/obs-browser/cmake/macos/entitlements-helper.plist create mode 100644 obs-studio/plugins/obs-browser/cmake/macos/entitlements-helper.plugin.plist create mode 100644 obs-studio/plugins/obs-browser/cmake/macos/entitlements-helper.renderer.plist create mode 100644 obs-studio/plugins/obs-browser/cmake/os-linux.cmake create mode 100644 obs-studio/plugins/obs-browser/cmake/os-macos.cmake create mode 100644 obs-studio/plugins/obs-browser/cmake/os-windows.cmake create mode 100644 obs-studio/plugins/obs-browser/cmake/windows/obs-browser-page.manifest create mode 100644 obs-studio/plugins/obs-browser/cmake/windows/obs-module-helper.rc.in create mode 100644 obs-studio/plugins/obs-browser/cmake/windows/obs-module.rc.in create mode 100644 obs-studio/plugins/obs-browser/data/error.html create mode 100644 obs-studio/plugins/obs-browser/data/locale/af-ZA.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ar-SA.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/az-AZ.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ba-RU.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/be-BY.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/bg-BG.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/bn-BD.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ca-ES.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/cs-CZ.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/da-DK.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/de-DE.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/el-GR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/en-GB.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/en-US.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/eo-UY.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/es-ES.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/et-EE.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/eu-ES.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/fa-IR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/fi-FI.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/fil-PH.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/fr-FR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/gd-GB.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/gl-ES.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/he-IL.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/hi-IN.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/hr-HR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/hu-HU.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/hy-AM.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/id-ID.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/it-IT.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ja-JP.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ka-GE.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/kaa.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/kab-KAB.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/kmr-TR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ko-KR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/lo-LA.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/lt-LT.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/mn-MN.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ms-MY.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/nb-NO.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/nl-NL.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/nn-NO.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/oc-FR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/pl-PL.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/pt-BR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/pt-PT.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ro-RO.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ru-RU.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/si-LK.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/sk-SK.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/sl-SI.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/sq-AL.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/sr-CS.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/sr-SP.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/sv-SE.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/szl-PL.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ta-IN.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/th-TH.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/tr-TR.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/tt-RU.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ug-CN.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/uk-UA.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/ur-PK.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/vi-VN.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/zh-CN.ini create mode 100644 obs-studio/plugins/obs-browser/data/locale/zh-TW.ini create mode 100644 obs-studio/plugins/obs-browser/deps/base64/.clang-format create mode 100644 obs-studio/plugins/obs-browser/deps/base64/base64.cpp create mode 100644 obs-studio/plugins/obs-browser/deps/base64/base64.hpp create mode 100644 obs-studio/plugins/obs-browser/deps/ip-string-posix.cpp create mode 100644 obs-studio/plugins/obs-browser/deps/ip-string-windows.cpp create mode 100644 obs-studio/plugins/obs-browser/deps/ip-string.hpp create mode 100644 obs-studio/plugins/obs-browser/deps/signal-restore.cpp create mode 100644 obs-studio/plugins/obs-browser/deps/signal-restore.hpp create mode 100644 obs-studio/plugins/obs-browser/deps/wide-string.cpp create mode 100644 obs-studio/plugins/obs-browser/deps/wide-string.hpp create mode 100644 obs-studio/plugins/obs-browser/drm-format.cpp create mode 100644 obs-studio/plugins/obs-browser/drm-format.hpp create mode 100644 obs-studio/plugins/obs-browser/linux-keyboard-helpers.hpp create mode 100644 obs-studio/plugins/obs-browser/obs-browser-page.manifest create mode 100644 obs-studio/plugins/obs-browser/obs-browser-page/obs-browser-page-main.cpp create mode 100644 obs-studio/plugins/obs-browser/obs-browser-plugin.cpp create mode 100644 obs-studio/plugins/obs-browser/obs-browser-source.cpp create mode 100644 obs-studio/plugins/obs-browser/obs-browser-source.hpp create mode 100644 obs-studio/plugins/obs-browser/panel/browser-panel-client.cpp create mode 100644 obs-studio/plugins/obs-browser/panel/browser-panel-client.hpp create mode 100644 obs-studio/plugins/obs-browser/panel/browser-panel-internal.hpp create mode 100644 obs-studio/plugins/obs-browser/panel/browser-panel.cpp create mode 100644 obs-studio/plugins/obs-browser/panel/browser-panel.hpp create mode 100644 obs-studio/plugins/obs-shaderfilter/.clang-format create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/FUNDING.yml create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/actions/build-plugin/action.yml create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/actions/package-plugin/action.yml create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/.Aptfile create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/.Brewfile create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/.Wingetfile create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/.build.zsh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/.package.zsh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/Build-Windows.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/Package-Windows.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/build-linux.sh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/build-linux.zsh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/build-macos.zsh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/check-changes.sh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/check-cmake.sh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/check-format.sh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/package-linux.sh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/package-linux.zsh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/package-macos.zsh create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Check-Git.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Ensure-Location.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Expand-ArchiveExt.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Install-BuildDependencies.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Invoke-External.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Invoke-GitCheckout.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Logger.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Setup-Host.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.pwsh/Setup-Obs.ps1 create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/check_linux create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/check_macos create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/check_packages create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/log_debug create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/log_error create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/log_info create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/log_output create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/log_status create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/log_warning create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/mkcd create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/read_codesign create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/read_codesign_installer create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/read_codesign_pass create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/read_codesign_user create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/set_loglevel create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/setup_ccache create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/setup_linux create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/setup_macos create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/scripts/utils.zsh/setup_obs create mode 100644 obs-studio/plugins/obs-shaderfilter/.github/workflows/build.yml create mode 100644 obs-studio/plugins/obs-shaderfilter/.gitignore create mode 100644 obs-studio/plugins/obs-shaderfilter/CMakeLists.txt create mode 100644 obs-studio/plugins/obs-shaderfilter/CMakeSettings.json create mode 100644 obs-studio/plugins/obs-shaderfilter/LICENSE create mode 100644 obs-studio/plugins/obs-shaderfilter/README.md create mode 100644 obs-studio/plugins/obs-shaderfilter/buildspec.json create mode 100644 obs-studio/plugins/obs-shaderfilter/cmake/Bundle/macos/Plugin-Info.plist.in create mode 100644 obs-studio/plugins/obs-shaderfilter/cmake/Bundle/macos/entitlements.plist create mode 100644 obs-studio/plugins/obs-shaderfilter/cmake/Bundle/macos/installer-macos.pkgproj.in create mode 100644 obs-studio/plugins/obs-shaderfilter/cmake/ObsPluginHelpers.cmake create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/3d-panel.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/3d_swap_transition.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/Add.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/BulgePinch.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/Chroma+UV-Distortion.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/Invert.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/Luminance.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/RGSSAA.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/Swirl.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/VCR.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/VHS.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/ZigZag.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/alpha-gaming-bent-camera.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/alpha_border.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/animated_path.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/animated_texture.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/ascii.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/aspect_ratio.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/audio.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/background_removal.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/blend_opacity.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/blink.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/bloom.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/border.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/box-blur.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/bt601tobt709.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/bt709tobt601.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/burn.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/cartoon.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/cell_shaded.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/chromatic-aberration.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/circle-mask-filter.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/circle.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/clock_analog.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/clock_digital_led.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/clock_digital_nixie.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/color-depth.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/color_grade_filter.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/corner-pin.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/corner_pin.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/crt-curvature.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/cube_rotating.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/curve.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/cut_rect_per_corner.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/cylinder.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/darken.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/dead-pixel-fixer.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/density_sat_hue.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/diffuse_transition.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/digital-rain.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/displacement_map.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/displacement_map_advanced.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/displacement_map_advanced_invert.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/displacement_map_invert.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/divide_rotate.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/doodle.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/drawings.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/drop_shadow.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/drunk.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/dynamic-mask.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/edge_detection.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/embers.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/emboss.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/emboss_color.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/exeldro-bent-camera.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fade-transition.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fill_color_gradient.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fill_color_linear.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fill_color_radial_degrees.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fill_color_radial_percentage.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/filter_template.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/filter_template.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fire-3.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fire.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fireworks.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fireworks2.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fisheye-xy.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/fisheye.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/flip.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/frosted_glass.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gamma_correction.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gaussian-blur-advanced.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gaussian-blur-simple.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gaussian-blur.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gaussian-example.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gaussian-simple.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gb-camera.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/glass.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/glitch-periodic.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/glitch.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/glitch_analog.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/glitch_analog.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/glow.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/gradient.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/halftone.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/hard_blink.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/heat-wave-simple.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/hexagon.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/hsl_hsv_saturation.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/hue-rotaton.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/intensity-scope.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/invert-luma.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/luminance2.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/luminance_alpha.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/matrix.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/motion_blur.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/multiply.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/night_sky.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/noise.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/normal_map.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/obs.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/opacity.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/page-peel-transition.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/page-peel.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/perlin_noise.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/perspective.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/pie-chart.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/pixelation-transition.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/pixelation.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/pixelation.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/polar.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/pulse.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/quadrilateral_crop.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rain-window.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rainbow.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rectangular_drop_shadow.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/remove_partial_pixels.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/repeat.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/repeat_grid_center_crop.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/repeat_texture.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rgb_color_wheel.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rgb_split.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rgbvisibility.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/ripple.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rotating-source.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rotatoe.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rounded_rect.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rounded_rect2.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rounded_rect_per_corner.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rounded_rect_per_side.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rounded_stroke.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/rounded_stroke_gradient.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/scan_line.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/seascape.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/seasick.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/selective_color.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/shake.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/shine.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/simple_gradient.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/simplex_noise.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/smart_denoise.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/specular-shine.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/spotlight.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/tetra.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/thermal.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/tv-crt-subpixel.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/twist.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/two-pass-drop-shadow.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/vignetting.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/voronoi-pixelation.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/walking-dead-pixel-fixer.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/zoom.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/zoom_blur.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/examples/zoom_blur_transition.shader create mode 100644 obs-studio/plugins/obs-shaderfilter/data/internal/render_output.effect create mode 100644 obs-studio/plugins/obs-shaderfilter/data/locale/de-DE.ini create mode 100644 obs-studio/plugins/obs-shaderfilter/data/locale/en-US.ini create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/Rainbow-a.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/Rainbow-a2.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/Rainbow-b.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/Rainbow-c.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/Rainbow.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/Rainbow2.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearApertureGrille15Wide8And5d5Spacing.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearApertureGrille15Wide8And5d5SpacingResizeTo64.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearShadowMask.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearShadowMaskEDP.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearShadowMaskEDPResizeTo64.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearShadowMaskResizeTo64.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearSlotMaskTall15Wide9And4d5Horizontal9d14VerticalSpacing.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/TileableLinearSlotMaskTall15Wide9And4d5Horizontal9d14VerticalSpacingResizeTo64.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/blank.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/burngradient.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/burngradient_small.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/canvas.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/font.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/giraffe.png create mode 100644 obs-studio/plugins/obs-shaderfilter/data/textures/noise.png create mode 100644 obs-studio/plugins/obs-shaderfilter/installer.iss.in create mode 100644 obs-studio/plugins/obs-shaderfilter/media/icon.ico create mode 100644 obs-studio/plugins/obs-shaderfilter/media/logo.png create mode 100644 obs-studio/plugins/obs-shaderfilter/obs-shaderfilter.c create mode 100644 obs-studio/plugins/obs-shaderfilter/obs-shaderfilter.h create mode 100644 obs-studio/plugins/obs-shaderfilter/resource.rc.in create mode 100644 obs-studio/plugins/obs-shaderfilter/version.h.in create mode 100644 obs-studio/plugins/obs-websocket/.clang-format create mode 100644 obs-studio/plugins/obs-websocket/.cmake-format.json create mode 100644 obs-studio/plugins/obs-websocket/.editorconfig create mode 100644 obs-studio/plugins/obs-websocket/.github/FUNDING.yml create mode 100644 obs-studio/plugins/obs-websocket/.github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 obs-studio/plugins/obs-websocket/.github/ISSUE_TEMPLATE/config.yml create mode 100644 obs-studio/plugins/obs-websocket/.github/ISSUE_TEMPLATE/feature_request.yaml create mode 100644 obs-studio/plugins/obs-websocket/.github/images/obsws_logo.png create mode 100644 obs-studio/plugins/obs-websocket/.github/pull_request_template.md create mode 100644 obs-studio/plugins/obs-websocket/.github/workflows/crowdin_upload.yml create mode 100644 obs-studio/plugins/obs-websocket/.github/workflows/generate_docs.yml create mode 100644 obs-studio/plugins/obs-websocket/.github/workflows/lint.yml create mode 100644 obs-studio/plugins/obs-websocket/.gitignore create mode 100644 obs-studio/plugins/obs-websocket/.gitmodules create mode 100644 obs-studio/plugins/obs-websocket/.markdownlintignore create mode 100644 obs-studio/plugins/obs-websocket/.markdownlintrc create mode 100644 obs-studio/plugins/obs-websocket/CI/generate-docs.sh create mode 100644 obs-studio/plugins/obs-websocket/CMakeLists.txt create mode 100644 obs-studio/plugins/obs-websocket/LICENSE create mode 100644 obs-studio/plugins/obs-websocket/README.md create mode 100644 obs-studio/plugins/obs-websocket/cmake/macos/Info.plist.in create mode 100644 obs-studio/plugins/obs-websocket/cmake/obs-websocket-api.cmake create mode 100644 obs-studio/plugins/obs-websocket/cmake/obs-websocket-apiConfig.cmake.in create mode 100644 obs-studio/plugins/obs-websocket/data/locale/af-ZA.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ar-SA.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/az-AZ.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/be-BY.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ca-ES.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/cs-CZ.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/da-DK.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/de-DE.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/el-GR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/en-GB.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/en-US.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/es-ES.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/et-EE.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/eu-ES.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/fa-IR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/fi-FI.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/fil-PH.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/fr-FR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/gl-ES.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/he-IL.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/hi-IN.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/hr-HR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/hu-HU.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/hy-AM.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/id-ID.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/it-IT.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ja-JP.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ka-GE.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/kaa.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/kab-KAB.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/kmr-TR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ko-KR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/lo-LA.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ms-MY.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/nb-NO.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/nl-NL.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/nn-NO.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/pl-PL.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/pt-BR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/pt-PT.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ro-RO.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ru-RU.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/si-LK.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/sk-SK.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/sl-SI.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/sq-AL.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/sr-SP.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/sv-SE.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/th-TH.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/tl-PH.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/tr-TR.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/tt-RU.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/ug-CN.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/uk-UA.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/vi-VN.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/zh-CN.ini create mode 100644 obs-studio/plugins/obs-websocket/data/locale/zh-TW.ini create mode 100644 obs-studio/plugins/obs-websocket/docs/.gitignore create mode 100644 obs-studio/plugins/obs-websocket/docs/README.md create mode 100644 obs-studio/plugins/obs-websocket/docs/build_docs.sh create mode 100644 obs-studio/plugins/obs-websocket/docs/comments/.gitignore create mode 100644 obs-studio/plugins/obs-websocket/docs/comments/.npmrc create mode 100644 obs-studio/plugins/obs-websocket/docs/comments/comments.js create mode 100644 obs-studio/plugins/obs-websocket/docs/comments/config.json create mode 100644 obs-studio/plugins/obs-websocket/docs/comments/package.json create mode 100644 obs-studio/plugins/obs-websocket/docs/docs/generate_md.py create mode 100644 obs-studio/plugins/obs-websocket/docs/docs/partials/enumsHeader.md create mode 100644 obs-studio/plugins/obs-websocket/docs/docs/partials/eventsHeader.md create mode 100644 obs-studio/plugins/obs-websocket/docs/docs/partials/introduction.md create mode 100644 obs-studio/plugins/obs-websocket/docs/docs/partials/requestsHeader.md create mode 100644 obs-studio/plugins/obs-websocket/docs/docs/process_comments.py create mode 100644 obs-studio/plugins/obs-websocket/docs/generated/protocol.json create mode 100644 obs-studio/plugins/obs-websocket/docs/generated/protocol.md create mode 100644 obs-studio/plugins/obs-websocket/docs/versions.json create mode 100644 obs-studio/plugins/obs-websocket/lib/example/simplest-plugin.c create mode 100644 obs-studio/plugins/obs-websocket/lib/obs-websocket-api.h create mode 100644 obs-studio/plugins/obs-websocket/src/Config.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/Config.h create mode 100644 obs-studio/plugins/obs-websocket/src/WebSocketApi.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/WebSocketApi.h create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler.h create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Canvases.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Config.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Filters.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_General.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Inputs.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_MediaInputs.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Outputs.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_SceneItems.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Scenes.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Transitions.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/EventHandler_Ui.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/eventhandler/types/EventSubscription.h create mode 100644 obs-studio/plugins/obs-websocket/src/forms/ConnectInfo.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/forms/ConnectInfo.h create mode 100644 obs-studio/plugins/obs-websocket/src/forms/ConnectInfo.ui create mode 100644 obs-studio/plugins/obs-websocket/src/forms/SettingsDialog.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/forms/SettingsDialog.h create mode 100644 obs-studio/plugins/obs-websocket/src/forms/SettingsDialog.ui create mode 100644 obs-studio/plugins/obs-websocket/src/forms/images/help.svg create mode 100644 obs-studio/plugins/obs-websocket/src/forms/images/help_light.svg create mode 100644 obs-studio/plugins/obs-websocket/src/forms/resources.qrc create mode 100644 obs-studio/plugins/obs-websocket/src/obs-websocket.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/obs-websocket.h create mode 100644 obs-studio/plugins/obs-websocket/src/plugin-macros.h.in create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestBatchHandler.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestBatchHandler.h create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler.h create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Canvases.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Config.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Filters.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_General.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Inputs.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_MediaInputs.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Outputs.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Record.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_SceneItems.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Scenes.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Sources.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Stream.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Transitions.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/RequestHandler_Ui.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/rpc/Request.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/rpc/Request.h create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/rpc/RequestBatchRequest.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/rpc/RequestBatchRequest.h create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/rpc/RequestResult.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/rpc/RequestResult.h create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/types/RequestBatchExecutionType.h create mode 100644 obs-studio/plugins/obs-websocket/src/requesthandler/types/RequestStatus.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Compat.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Compat.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Crypto.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Crypto.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Json.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Json.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_ActionHelper.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_ArrayHelper.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_NumberHelper.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_ObjectHelper.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_SearchHelper.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_StringHelper.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_VolumeMeter.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_VolumeMeter.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Obs_VolumeMeter_Helpers.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Platform.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Platform.h create mode 100644 obs-studio/plugins/obs-websocket/src/utils/Utils.h create mode 100644 obs-studio/plugins/obs-websocket/src/websocketserver/WebSocketServer.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/websocketserver/WebSocketServer.h create mode 100644 obs-studio/plugins/obs-websocket/src/websocketserver/WebSocketServer_Protocol.cpp create mode 100644 obs-studio/plugins/obs-websocket/src/websocketserver/rpc/WebSocketSession.h create mode 100644 obs-studio/plugins/obs-websocket/src/websocketserver/types/WebSocketCloseCode.h create mode 100644 obs-studio/plugins/obs-websocket/src/websocketserver/types/WebSocketOpCode.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a73b00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/deploy/ +/build_log.txt +/configure_log.txt +/icon_preview.png diff --git a/caiic_logo.jpg b/caiic_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f8af0847d66c20d6d321c719b35c73faab0164d9 GIT binary patch literal 165025 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�EZNV6O6D1Ew0BZ@qWou9|99uf5nZ zIq&Z4RtT9FXR}+~`IPAhShYwn%$%Ugj{!v6|x&y?;kUZ`a zOSBcPbib6=#9p_=D#dgLa&U&FVT8oDIBU20=)<*d|EM4PsX3czd->urJNur_y?m$7 zieK;Ejw6O8Yj*;x6;HRz0I`?qsV8>xuMO%%tAY@+`i2ie6uqQWs!@=ap9KXLjUS&b zreo+1D(_PIda^!bJrgf;d)7p;GulA!#@1&U-+-=#{W3c|CdkD)yPR`{glug zx--_IEz7|buR<1~rC1pKQ;9kL{-c5?GBr4%q$%BtrIXiKEy<o~c#r;#tK9}_rTwb*X1`+l2&`uV-1#>o()oWOhe zN9Q=Bj!_8s(=<;K?G7rwobcveHN%9ZhWffP<}(#)gaet~4u+ajzEDe1VISg{v= z&n++N)KyU-?V++;P9AeAmzl#mrgto?QaJ;u`@8S8uxXx&d~j;Z+oOjHf5^wL#&ru^ z)-Hx;x^!&arkWN8&kUlB1<&G!5(lkpOVfjQeXK3ir7(C@bYiHtIj!f1)sxy2QxhXf z`geqM8Z-vfwN*8rs;Xt4}3utZ-q55S#^uK0#@{wP#sJnkqhW4##$~vxFEPFl01K&G_ zr0{JIetqTNjeqg+?7wesU15EBXPxGk`1Eo${oi~4SZ^+0AN%&rzb}jaz4X~Co&I+7 z)i1^2NB?k@@O!8KohBY#{V`N+N&4E5F9mzwuP^bhn*X!5M*hXTYwO?p&HAzYVr}xB znf~wjKgylG8C$-6gY>2Czi%6dZLOV+JU%S^cSQ}6JNA1S&jlO}p^ zHw*iE`*&5VvzLRwrDIGDi!ZHWHBtZYb+7yN#r7Xh|BMCZasE!rEZ^fF?>{{6^mX0a z<7>4Ze?QB2_~^6W>~b0xgM=M#NG~x-Yi zyPHi(RvTRvjdK+a<~T8IrjHUWqpPAR2|Y4mfC{UkVY^na&)bsWreUpxvbArt$#ug! zz%Sl^44kzeO`T+IQz0X1P&nO4hK(a!3uWW`qeTkjjvo>#UdqQsP-a+0V}MW$jJCr` zNS)Lu4M*GI^bYXrqpcLWHqu7h;q*@7qwR3|c0)$n;q*@7mq*KR`j+8WMgyF_0e%wz Ds|haR literal 0 HcmV?d00001 diff --git a/obs-studio/deps/libdshowcapture/src/.gitattributes b/obs-studio/deps/libdshowcapture/src/.gitattributes new file mode 100644 index 0000000..4c165cd --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/.gitattributes @@ -0,0 +1,9 @@ +* text=auto + +*.sln text eol=crlf +*.vcproj text eol=crlf +*.vcxproj text eol=crlf +*.vcxproj text eol=crlf +*.vcxproj.filters text eol=crlf + +cmake/ALL_BUILD.vcxproj.user.in text eol=crlf diff --git a/obs-studio/deps/libdshowcapture/src/.gitignore b/obs-studio/deps/libdshowcapture/src/.gitignore new file mode 100644 index 0000000..be62c56 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/.gitignore @@ -0,0 +1,47 @@ +*.exe +*.dll + +/release/ +/debug/ +/builds/ +/other/ +Release_MD/ +Release/ +Debug/ +x64/ +ipch/ +GeneratedFiles/ + +.hg +tags +*.trace +*.vsp +*.psess +*.swp +*.dat +*.clbin +*.log +*.tlog +*.sdf +*.opensdf +*.xml +*.ipch +*.css +*.xslt +*.aps +*.suo +*.ncb +*.user +*.lo +*.ilk +*.la +*.o +*.obj +*.pdb +*.res +*.manifest +*.dep +*.zip +*.lnk +*.chm +*~ diff --git a/obs-studio/deps/libdshowcapture/src/.gitmodules b/obs-studio/deps/libdshowcapture/src/.gitmodules new file mode 100644 index 0000000..7ed50be --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/capture-device-support"] + path = external/capture-device-support + url = https://github.com/elgatosf/capture-device-support diff --git a/obs-studio/deps/libdshowcapture/src/CMakeLists.txt b/obs-studio/deps/libdshowcapture/src/CMakeLists.txt new file mode 100644 index 0000000..a60ab9b --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 3.28...3.30) + +project(libdshowcapture) + +if(NOT MSVC) + message(FATAL_ERROR "libdshowcapture only supports building with Visual Studio and MSVC.") +endif() + +option(BUILD_SHARED_LIBS "Build shared library" ON) + +# Set C and C++ language standards to C17 and C++17 +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED TRUE) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) + +# Disable C and C++ extensions +set(CMAKE_C_EXTENSIONS FALSE) +set(CMAKE_CXX_EXTENSIONS FALSE) + +# Set symbols to be hidden by default for C and C++ +set(CMAKE_C_VISIBILITY_PRESET hidden) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE) + +add_library(libdshowcapture) + +target_sources( + libdshowcapture + PRIVATE + external/capture-device-support/Library/EGAVResult.cpp + external/capture-device-support/Library/ElgatoUVCDevice.cpp + external/capture-device-support/Library/win/EGAVHIDImplementation.cpp + external/capture-device-support/SampleCode/DriverInterface.cpp + source/capture-filter.cpp + source/output-filter.cpp + source/dshowcapture.cpp + source/dshowencode.cpp + source/device.cpp + source/device-vendor.cpp + source/encoder.cpp + source/dshow-base.cpp + source/dshow-demux.cpp + source/dshow-enum.cpp + source/dshow-formats.cpp + source/dshow-media-type.cpp + source/dshow-encoded-device.cpp + source/log.cpp + dshowcapture.hpp + source/external/IVideoCaptureFilter.h + source/capture-filter.hpp + source/output-filter.hpp + source/device.hpp + source/encoder.hpp + source/dshow-base.hpp + source/dshow-demux.hpp + source/dshow-device-defs.hpp + source/dshow-enum.hpp + source/dshow-formats.hpp + source/dshow-media-type.hpp + source/log.hpp +) + +# Set /W4 +# Disable pointless constant condition warnings +target_compile_options(libdshowcapture PRIVATE /W4 /wd4127 /wd4201) + +if(CMAKE_VS_PLATFORM_NAME STREQUAL Win32) + target_link_options(libdshowcapture PRIVATE /SAFESEH:NO) +endif() + +target_include_directories(libdshowcapture PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/external/capture-device-support/Library) + +target_compile_definitions( + libdshowcapture + PRIVATE + UNICODE + _UNICODE + $<$:DEBUG> + $<$:_DEBUG> + $<$:DSHOWCAPTURE_EXPORTS> + _UP_WINDOWS=1 +) + +target_link_libraries( + libdshowcapture + PRIVATE setupapi strmiids ksuser winmm wmcodecdspuuid +) diff --git a/obs-studio/deps/libdshowcapture/src/COPYING b/obs-studio/deps/libdshowcapture/src/COPYING new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/COPYING @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/obs-studio/deps/libdshowcapture/src/README b/obs-studio/deps/libdshowcapture/src/README new file mode 100644 index 0000000..b5d41e5 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/README @@ -0,0 +1,11 @@ + +libdshowcapture + + This library was created as a means to simplify the process of using + DirectShow to capture video and/or audio devices, such as webcams, capture + devices (internal, USB 2.0, USB 3.0), microphones, auxiliary sound inputs, + etc. + + The biggest goal of this project is to eventually support as many devices as + possible, as well as add more interesting features later on for improving + performance. diff --git a/obs-studio/deps/libdshowcapture/src/dshowcapture.hpp b/obs-studio/deps/libdshowcapture/src/dshowcapture.hpp new file mode 100644 index 0000000..89ce5fe --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/dshowcapture.hpp @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include +#include +#include + +#ifdef DSHOWCAPTURE_EXPORTS +#define DSHOWCAPTURE_EXPORT __declspec(dllexport) +#else +#define DSHOWCAPTURE_EXPORT +#endif + +#define DSHOWCAPTURE_VERSION_MAJOR 0 +#define DSHOWCAPTURE_VERSION_MINOR 10 +#define DSHOWCAPTURE_VERSION_PATCH 0 + +#define MAKE_DSHOWCAPTURE_VERSION(major, minor, patch) \ + ((major << 24) | (minor << 16) | (patch)) + +#define DSHOWCAPTURE_VERSION \ + MAKE_DSHOWCAPTURE_VERSION(DSHOWCAPTURE_VERSION_MAJOR, \ + DSHOWCAPTURE_VERSION_MINOR, \ + DSHOWCAPTURE_VERSION_PATCH) + +#define DSHOW_MAX_PLANES 8 + +namespace DShow { +/* internal forward */ +struct HDevice; +struct HVideoEncoder; +struct VideoConfig; +struct AudioConfig; + +typedef std::function + VideoProc; + +typedef std::function + AudioProc; + +typedef std::function ReactivateProc; + +enum class InitGraph { + False, + True, +}; + +/** DirectShow configuration dialog type */ +enum class DialogType { + ConfigVideo, + ConfigAudio, + ConfigCrossbar, + ConfigCrossbar2, +}; + +enum class VideoFormat { + Any, + Unknown, + + /* raw formats */ + ARGB = 100, + XRGB, + RGB24, + + /* planar YUV formats */ + I420 = 200, + NV12, + YV12, + Y800, + P010, + + /* packed YUV formats */ + YVYU = 300, + YUY2, + UYVY, + HDYC, + + /* encoded formats */ + MJPEG = 400, + H264, + HEVC, +}; + +enum class AudioFormat { + Any, + Unknown, + + /* raw formats */ + Wave16bit = 100, + WaveFloat, + + /* encoded formats */ + AAC = 200, + AC3, + MPGA, /* MPEG 1 */ +}; + +enum class AudioMode { + Capture, + DirectSound, + WaveOut, +}; + +enum class Result { + Success, + InUse, + Error, +}; + +struct VideoInfo { + int minCX, minCY; + int maxCX, maxCY; + int granularityCX, granularityCY; + long long minInterval, maxInterval; + VideoFormat format; +}; + +struct AudioInfo { + int minChannels, maxChannels; + int channelsGranularity; + int minSampleRate, maxSampleRate; + int sampleRateGranularity; + AudioFormat format; +}; + +struct DeviceId { + std::wstring name; + std::wstring path; +}; + +struct VideoDevice : DeviceId { + bool audioAttached = false; + bool separateAudioFilter = false; + std::vector caps; +}; + +struct AudioDevice : DeviceId { + std::vector caps; +}; + +struct Config : DeviceId { + /** Use the device's desired default config */ + bool useDefaultConfig = true; +}; + +struct VideoConfig : Config { + VideoProc callback; + ReactivateProc reactivateCallback; + + /** Desired width/height of video. */ + int cx = 0, cy_abs = 0; + + /** Whether or not cy was negative. */ + bool cy_flip = false; + + /** Desired frame interval (in 100-nanosecond units) */ + long long frameInterval = 0; + + /** Internal video format. */ + VideoFormat internalFormat = VideoFormat::Any; + + /** Desired video format. */ + VideoFormat format = VideoFormat::Any; +}; + +struct AudioConfig : Config { + AudioProc callback; + + /** + * Use the audio attached to the video device + * + * (name/path member variables will be ignored) + */ + bool useVideoDevice = false; + + /** Use separate filter for audio */ + bool useSeparateAudioFilter = false; + + /** Desired sample rate */ + int sampleRate = 0; + + /** Desired channels */ + int channels = 0; + + /** Desired audio format */ + AudioFormat format = AudioFormat::Any; + + /** Audio playback mode */ + AudioMode mode = AudioMode::Capture; + + /** Desired buffer */ + int buffer = 0; +}; + +class DSHOWCAPTURE_EXPORT Device { + HDevice *context; + +public: + Device(InitGraph initialize = InitGraph::False); + ~Device(); + + bool Valid() const; + + bool ResetGraph(); + void ShutdownGraph(); + + bool SetVideoConfig(VideoConfig *config); + bool SetAudioConfig(AudioConfig *config); + + /** + * Connects all the configured filters together. + * + * Call SetVideoConfig and/or SetAudioConfig before using. + */ + bool ConnectFilters(); + + Result Start(); + void Stop(); + + bool GetVideoConfig(VideoConfig &config) const; + bool GetAudioConfig(AudioConfig &config) const; + bool GetVideoDeviceId(DeviceId &id) const; + bool GetAudioDeviceId(DeviceId &id) const; + + /** + * Opens a DirectShow dialog associated with this device + * + * @param type The dialog type + */ + void OpenDialog(void *hwnd, DialogType type) const; + + static bool EnumVideoDevices(std::vector &devices); + static bool EnumAudioDevices(std::vector &devices); +}; + +struct VideoEncoderConfig : DeviceId { + int fpsNumerator; + int fpsDenominator; + int bitrate; + int keyframeInterval; + int cx; + int cy; +}; + +struct EncoderPacket { + unsigned char *data; + size_t size; + long long pts; + long long dts; +}; + +class VideoEncoder { + HVideoEncoder *context; + +public: + VideoEncoder(); + ~VideoEncoder(); + + bool Valid() const; + bool Active() const; + + bool ResetGraph(); + + bool SetConfig(VideoEncoderConfig &config); + bool GetConfig(VideoEncoderConfig &config) const; + + bool Encode(unsigned char *data[DSHOW_MAX_PLANES], + size_t linesize[DSHOW_MAX_PLANES], long long timestampStart, + long long timestampEnd, EncoderPacket &packet, + bool &new_packet); + + static bool EnumEncoders(std::vector &encoders); +}; + +enum class LogType { + Error, + Warning, + Info, + Debug, +}; + +typedef void (*LogCallback)(LogType type, const wchar_t *msg, void *param); + +DSHOWCAPTURE_EXPORT void SetLogCallback(LogCallback callback, void *param); +}; diff --git a/obs-studio/deps/libdshowcapture/src/external/.clang-format b/obs-studio/deps/libdshowcapture/src/external/.clang-format new file mode 100644 index 0000000..6420a46 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/.clang-format @@ -0,0 +1,3 @@ +Language: Cpp +SortIncludes: false +DisableFormat: true diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/.gitignore b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/.gitignore new file mode 100644 index 0000000..bd30095 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/.gitignore @@ -0,0 +1,13 @@ +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +.vs/ +out/ diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/CMakeLists.txt b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/CMakeLists.txt new file mode 100644 index 0000000..a519525 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/CMakeLists.txt @@ -0,0 +1,64 @@ +# MIT License +# +# Copyright (c) 2022 Corsair Memory, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +# CMakeList.txt : CMake project for EGAVHIDSample, include source and define +# project specific logic here. +# +cmake_minimum_required (VERSION 3.8) + +project ("EGAVHIDSample") + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(FRAMEWORK_FOLDER "Library") + +if(WIN32) + set(PLATFORM_FOLDER "win") + set(PLATFORM_SOURCES + SampleCode/DriverInterface.cpp + ) + +elseif(APPLE) + set(PLATFORM_FOLDER "mac") + set(PLATFORM_SOURCES) +endif() + +# Add source to this project's executable. +add_executable (EGAVHIDSample + ${PLATFORM_SOURCES} + "${FRAMEWORK_FOLDER}/EGAVResult.cpp" + "${FRAMEWORK_FOLDER}/ElgatoUVCDevice.cpp" + "${FRAMEWORK_FOLDER}/${PLATFORM_FOLDER}/EGAVHIDImplementation.cpp" + "SampleCode/DriverInterface.cpp" + "SampleCode/main.cpp" +) + +target_include_directories(EGAVHIDSample PRIVATE ${FRAMEWORK_FOLDER}) +target_compile_definitions(EGAVHIDSample PUBLIC EGAV_API) + +if(WIN32) + target_compile_definitions(EGAVHIDSample PUBLIC _UP_WINDOWS=1) +elseif(APPLE) + target_compile_definitions(EGAVHIDSample PUBLIC _UP_MAC=1) +endif() \ No newline at end of file diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/LICENSE b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/LICENSE new file mode 100644 index 0000000..e44c08e --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVDevice.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVDevice.h new file mode 100644 index 0000000..4b7068a --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVDevice.h @@ -0,0 +1,72 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include + + +//============================================================================== +// # Class EGAVDeviceID +//============================================================================== +enum class EGAVBusType { Unknown, USB, PCI }; + +struct EGAVDeviceID +{ + EGAVDeviceID() {} + + //! @brief EGAVDeviceID constructor + //! @param inBusType USB or PCIe + //! @param inVendorID USB vendor ID or PCIe subsystem vendor ID + //! @param inProductID USB product ID or PCIe subsystem device ID + //! @param inLocationID Location ID (macOS only) + EGAVDeviceID(EGAVBusType inBusType, uint16_t inVendorID, uint16_t inProductID, uint32_t inLocationID = 0) : + busType(inBusType), vendorID(inVendorID), productID(inProductID), locationID(inLocationID) + { + } + + EGAVBusType busType = EGAVBusType::Unknown; + uint16_t vendorID = 0; //!< USB vendor ID or PCI subvendor ID + uint16_t productID = 0; //!< USB product ID or PCI subdevice ID + uint32_t locationID = 0; //!< USB location ID (macOS only) + + bool Equals(const EGAVDeviceID& inDeviceID, bool inIgnoreLocation) const + { + if (this->busType != inDeviceID.busType) return false; + if (this->vendorID != inDeviceID.vendorID) return false; + if (this->productID != inDeviceID.productID) return false; + if (this->locationID != inDeviceID.locationID && !inIgnoreLocation) return false; + return true; + } + + bool operator == (const EGAVDeviceID& inDeviceID) const { return Equals(inDeviceID, false); } + bool operator != (const EGAVDeviceID& inDeviceID) const { return !Equals(inDeviceID, false); } + + std::string toString(); +}; + + + diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVHID.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVHID.h new file mode 100644 index 0000000..db07545 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVHID.h @@ -0,0 +1,69 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file EGAVHID.h + +@brief HID specific types and constants. +**/ +//============================================================================== + +#pragma once + +#include + +#include "EGAVResult.h" +#include "EGAVDevice.h" // required for EGAVDeviceID + +const int kHidDefaultReportID = 0; //! Dummy report ID + +//============================================================================== +// # Interfaces (HID) +//============================================================================== + +//! @brief HID Interface for cross-platform support. (EVH-442) +class EGAVHIDInterface +{ +public: + virtual ~EGAVHIDInterface() { } + + virtual EGAVResult InitHIDInterface(const EGAVDeviceID& inDeviceID) = 0; + virtual EGAVResult DeinitHIDInterface() = 0; + + //! @brief Reads a HID response message from the OS. + //! @param outReceivedMessage will contain the resulting message. Its length will be adjusted automatically. + //! @param report ID The reportID is always 0 (kHidDefaultReportID) for Facecam (Penna) + //! @param inReadBufferSize size of buffer passed to ID read routine. If 0 mHIDCaps->InputReportByteLength is used. + virtual EGAVResult ReadHID(std::vector& outMessage, int inReportID, int inReadBufferSize = 0) = 0; + + //! @brief Writes specified message. + //! The implementation should construct a HID report to send to the hardware + //! @param message (NOT report!) + //! @param report ID The reportID is always 0 (kHidDefaultReportID) for Facecam (Penna). The Penna-specific message tag is in the first byte of the message. + virtual EGAVResult WriteHID(const std::vector& inMessage, int inReportID) = 0; +}; + +//! @brief Platform specific factory method +std::shared_ptr CreateEGAVHIDInterface(); diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.cpp b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.cpp new file mode 100644 index 0000000..21835ba --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.cpp @@ -0,0 +1,131 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file EGAVResult.cpp + +@brief Definition of error and success codes +**/ +//============================================================================== + +#include "EGAVResult.h" +#if _UP_WINDOWS +#include // for HRESULT +#endif + +//------------------------------------------------------------------------------ +// Constructor / Destructor +//------------------------------------------------------------------------------ + +EGAVResult::EGAVResult(EGAVResultCode inResultCode) +{ + mResultCode = inResultCode; +} + +EGAVResult::EGAVResult(EGAVResultCustomType inCustomResultType, int64_t inCustomResultCode) +{ + mResultCode = ErrCustom; + mCustomResultType = inCustomResultType; + mCustomResultCode = inCustomResultCode; +} + +#if _UP_WINDOWS +void EGAVResult::InitWithHresult(HRESULT hr) +{ + mResultCode = ErrCustom; + mCustomResultType = EGAVResultCustomType::Hresult; + mCustomResultCode = hr; +} + +void EGAVResult::InitWithWinError(LONG err) +{ + mResultCode = ErrCustom; + mCustomResultType = EGAVResultCustomType::WinError; + mCustomResultCode = err; +} +#endif + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +bool EGAVResult::Succeeded() const +{ + if (mResultCode == ErrCustom) + { + switch (mCustomResultType) + { +#if _UP_WINDOWS + case EGAVResultCustomType::Hresult: return SUCCEEDED(mCustomResultCode); + case EGAVResultCustomType::WinError: return (mCustomResultCode == ERROR_SUCCESS) ? true : false; +#elif _UP_MAC + case EGAVResultCustomType::Mac: return (mCustomResultCode == 0) ? true : false; +#endif + case EGAVResultCustomType::MainConcept: + case EGAVResultCustomType::Device: + return (mCustomResultCode == 0) ? true : false; + default: + return false; + } + } + else + { + return (int32_t)mResultCode > 0 ? true : false; + } +} + + +//------------------------------------------------------------------------------ +// Operators +//------------------------------------------------------------------------------ +void EGAVResult::operator=(const EGAVResultCode inResultCode) +{ + mCustomResultType = EGAVResultCustomType::None; + mResultCode = inResultCode; +} + +bool EGAVResult::operator==(const EGAVResult inResult) const +{ + if (mResultCode != inResult.mResultCode ) return false; + if (mCustomResultType != inResult.mCustomResultType) return false; + if (mCustomResultCode != inResult.mCustomResultCode) return false; + return true; +} + +bool EGAVResult::operator!=(const EGAVResult inResult) const +{ + return !(*this == inResult); +} + + +bool EGAVResult::operator==(const EGAVResultCode inResultCode) const +{ + return mResultCode == inResultCode; +} + +bool EGAVResult::operator!=(const EGAVResultCode inResultCode) const +{ + return mResultCode != inResultCode; +} diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.h new file mode 100644 index 0000000..5017976 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/EGAVResult.h @@ -0,0 +1,213 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file EGAVResult.h + +@brief Definition of error and success codes +**/ +//============================================================================== + +#pragma once + + +//------------------------------------------------------------------------------ +// Includes +//------------------------------------------------------------------------------ + +#include +#if _UP_WINDOWS + // FMB NOTE: For some strange reason must be before . + // This is only necessary because contents of are required in other code files. + // However, if is include somewhere it must be guaranteded that was never include before. + #define NOMINMAX + #include // for sockaddr_in + #include +#endif + +//#include "EGAVFeatures.h" // for EGAV_API + + +#define EVH_AVOID_UNNEEDED_COPY_CONSTRUCTORS (!_UP_WINDOWS) //!< 0 - explicit copy constructors: required for .NET frontends connected via SWIG e.g. 4KCU and EVH Test App + //!< 1 - to avoid "warning: definition of implicit copy constructor for different classes e.g. + //! 'EGAVAudioSampleType' is deprecated because it has a user-declared copy assignment operator [-Wdeprecated-copy]" + + + + + +//============================================================================== +// EGAVResultCustomType +//============================================================================== + +enum class EGAVResultCustomType +{ + None, + Hresult, //!< HRESULT (Windows) + WinError, //!< System error code (Windows) + MainConcept, //!< MainConcept error codes (BS_OK etc.) + Device, //!< Device Error Codes + Mac //!< Errors from macOS +}; + +enum class EGAVResultCustomTypeDevice { + None, + SpeedInsufficient, + ResultUnexpected, +}; + +//============================================================================== +// EGAVResult +//============================================================================== +typedef int EGAVResultCode; + +//! Error description +class /*EGAV_API*/ EGAVResult +{ +public: + + //------------------------------------------------------------------------------ + // Constants + //------------------------------------------------------------------------------ + + // EXTEND_EGAVResultCode: add new error codes here and in GetResultCodeString function in cpp file + static const EGAVResultCode ErrInvalidOperation = -300; //!< Execution of the operation would lead to an error/invalid state + + static const EGAVResultCode ErrUnknownUnit = -200; //!< Can't instantiate desired EGAV unit (source, sink, input, output, etc.) + + static const EGAVResultCode ErrDeviceInUse = -108; //!< Device is in use by another application (from EVHALResultCode) + static const EGAVResultCode ErrInvalidPath = -101; //!< File error: specifided path is not valid + static const EGAVResultCode ErrCouldNotOpenFile = -100; //!< File error: Could not open file + + static const EGAVResultCode ErrResultPending = -19; //!< hardware busy, try again later + static const EGAVResultCode ErrResourceNotAvail = -18; //!< Resource not available + static const EGAVResultCode ErrOutOfRange = -17; //!< Out of Range + static const EGAVResultCode ErrTimeOut = -16; //!< Operation timed out + static const EGAVResultCode ErrNotSupported = -15; //!< Operation not supported; used with firmware update + static const EGAVResultCode ErrConversionFailed = -14; //!< Conversion failed + static const EGAVResultCode ErrNotFound = -13; //!< Not found + static const EGAVResultCode ErrNoData = -12; //!< No data + static const EGAVResultCode ErrVideoScaler = -11; //!< Video scaler error + static const EGAVResultCode ErrEncoder = -10; //!< Encoder error + static const EGAVResultCode ErrInvalidFormat = -9; //!< Invalid format + static const EGAVResultCode ErrInvalidParameter = -8; //!< Invalid parameter + static const EGAVResultCode ErrInvalidState = -7; //!< Invalid state (e.g. when trying to process data while a unit is desinitialized) + static const EGAVResultCode ErrInsufficientMemory = -6; //!< Out of memory + static const EGAVResultCode ErrNotInitialized = -5; //!< Not initialized + static const EGAVResultCode ErrInvalidCast = -4; //!< Cast operation failed + static const EGAVResultCode ErrNotImplemented = -3; //!< Can't instantiate desired EGAV unit (source, sink, input, output, etc.) + static const EGAVResultCode ErrNullPointer = -2; //!< Null pointer + static const EGAVResultCode ErrUnknown = -1; //!< General failure + + static const EGAVResultCode ErrCustom = 0; //!< Custom error code: error code is in mCustomResultCode + + static const EGAVResultCode Ok = 1; //!< Success + static const EGAVResultCode OkNoDataChanged = 2; //!< Success: No data were changed (similar to HRESULT value S_FALSE ) + static const EGAVResultCode OkFileNotFound = 3; //!< Success: File was not found, but that is a valid state. + static const EGAVResultCode OkButIncomplete = 4; //!< Success: Operation didn't fail, but had some internal uncritical errors (similar to S_FALSE on Windows) + + //------------------------------------------------------------------------------ + // Construction + //------------------------------------------------------------------------------ + + //! Constructor + EGAVResult() {} + EGAVResult(EGAVResultCode inResultCode); + + EGAVResult(EGAVResultCustomType inCustomResultType, int64_t inCustomResultCode); + + //------------------------------------------------------------------------------ + // Initialization + //------------------------------------------------------------------------------ +#if _UP_WINDOWS + + //! Init with HRESULT (EGAVResultCustomType::Hresult) + void InitWithHresult(HRESULT hr); + //! Init with Windows error code (EGAVResultCustomType::WinError) + void InitWithWinError(LONG err); + +#endif + + //------------------------------------------------------------------------------ + // Helpers + //------------------------------------------------------------------------------ + + bool Succeeded() const; + bool Failed() const { return !Succeeded(); } + + EGAVResultCode GetResultCode() const { return mResultCode; } + EGAVResultCustomType GetCustomResultType() const { return mCustomResultType; } + int64_t GetCustomResultCode() const { return mCustomResultCode; } + + + void operator=(const EGAVResultCode inResultCode); + bool operator==(const EGAVResult inResult) const; + bool operator!=(const EGAVResult inResult) const; + bool operator==(const EGAVResultCode inResultCode) const; + bool operator!=(const EGAVResultCode inResultCode) const; + + //------------------------------------------------------------------------------ + // Members + //------------------------------------------------------------------------------ + + // Common + EGAVResultCode mResultCode = ErrCustom; + + // Custom error codes (platform error codes or error codes from other APIs) + EGAVResultCustomType mCustomResultType = EGAVResultCustomType::None; + int64_t mCustomResultCode = 0; + +private: + std::string mMessage; +}; + + + +//============================================================================== +// # Macros +//============================================================================== + +#define EGAVResult_CheckPointer(_p_) \ + { \ + if (!_p_) \ + return EGAVResult::ErrNullPointer; \ + } + +#define EGAVResult_CheckCondition(_cond_) \ + { \ + if (false == (_cond_)) \ + return EGAVResult::ErrUnknown; \ + } + + + +#ifndef EGAV_OVERRIDE_DEBUG_MACROS + inline void dummy() {} + + #define EPL_ASSERT_BREAK(...) dummy() + #define error_printf(...) dummy() + #define warning_printf(...) dummy() + #define info_printf(...) dummy() +#endif diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.cpp b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.cpp new file mode 100644 index 0000000..1da5ae7 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.cpp @@ -0,0 +1,265 @@ +/* +MIT License + +Copyright (c) 2022-23 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "ElgatoUVCDevice.h" + + +#define WORKAROUND_HD60_S_PLUS_PAYLOAD_SIZE 1 //!< Workaround HD60 S+ firmware issue: invalid payload length (seen with HDR and SPD info frames) + + +//============================================================================== +// # Elgato HID interface for UVC devices +//============================================================================== + + +enum class I2CAddress +{ + MCU = 0x55, +}; + +//! @brief I2C registers for MCU (I2C address 0x55). +enum class MCU_I2C_REGISTER +{ + GET_HDR_PACKET = 0x09, //!< HDR capable devices (HD60 S+, HD60 X) + XET_HDR_TONEMAPPING = 0x0A, //!< HDR capable devices (HD60 S+, HD60 X): Enable hardware tonemapping; param 0/1 (uint8_t) +}; + + + +//============================================================================== +// ## HID interface - I2C +//============================================================================== + +//! @brief HID report case for new device type. +enum class REPORT_CASE_NEW +{ + REPORT_IIC_WRITE = 6, + REPORT_IIC_READ = 7 +}; + +//! @brief HID report IDs for new device type. Can also be queried via HidP_GetValueCaps() +enum class HID_REPORT_ID_NEW +{ + I2C_READ = 5, + I2C_WRITE = 6 +}; + +//! @brief HDI report IDs for original device type. Can also be queried via HidP_GetValueCaps() +enum class HID_REPORT_ID +{ + I2C_READ_SET_ID = 9, + I2C_READ_GET_ID = 10, + I2C_WRITE_ID = 11 +}; + +const int I2C_BUFFER_HEADER_SIZE = 4; +const int MAX_COMM_READ_BUFFER_SIZE = 32; + + +//============================================================================== +// # Helpers +//============================================================================== + +std::vector GetElgatoUVCDeviceIDs() +{ + std::vector devices {deviceIDHD60SPlus, deviceIDHD60X, deviceIDHD60XRev2 }; + // EXTEND_DEVICES + return devices; +} + +//! @return true for new USB chipset +bool IsNewDeviceType(const EGAVDeviceID& inDeviceID) { return (inDeviceID != deviceIDHD60SPlus); } + + + +//============================================================================== +// # Class ElgatoUVCDevice +//============================================================================== + +ElgatoUVCDevice::ElgatoUVCDevice(std::shared_ptr hid, bool isNewDeviceType) : mHIDImpl(hid), mNewDeviceType(isNewDeviceType) {} + +EGAVResult ElgatoUVCDevice::ReadI2cData(uint8_t inI2CAddress, uint8_t inRegister, uint8_t* outData, uint8_t inLength) +{ + EGAVResult_CheckPointer(outData); + EGAVResult_CheckPointer(mHIDImpl); + + EPL_ASSERT_BREAK(inLength <= MAX_COMM_READ_BUFFER_SIZE); + + const std::lock_guard lock(mHIDMutex); + + EGAVResult res = EGAVResult::ErrUnknown; + + if (mNewDeviceType) + { + const uint8_t writeLen = 1 /* +1 for byte register address*/, readLen = inLength, reportLen = 4 + writeLen + sizeof(readLen); + std::vector outputMessage{ reportLen, (uint8_t)REPORT_CASE_NEW::REPORT_IIC_READ, inI2CAddress, writeLen, inRegister, readLen }; + EPL_ASSERT_BREAK(reportLen == outputMessage.size()); + res = mHIDImpl->WriteHID(outputMessage, (int)HID_REPORT_ID_NEW::I2C_WRITE); + if (res.Failed()) + error_printf("WriteHID() FAILED for I2C address 0x%02x, register 0x%02x", inI2CAddress, inRegister); + else + { + std::vector inputMessage; + const int inputReportLength = 0xFF | ((int)REPORT_CASE_NEW::REPORT_IIC_READ << 8); // report case is coded into report length + res = mHIDImpl->ReadHID(inputMessage, (int)HID_REPORT_ID_NEW::I2C_READ, inputReportLength); + if (res.Failed()) + error_printf("ReadHID() FAILED for I2C address 0x%02x, register 0x%02x", inI2CAddress, inRegister); + else + { + int dataLen = std::min((int)inLength, (int)(inputMessage.size() - 1)); + memcpy(outData, inputMessage.data() + 1, dataLen); + } + } + } + else + { + std::vector outputMessage{ inI2CAddress, inRegister, inLength }; + res = mHIDImpl->WriteHID(outputMessage, (int)HID_REPORT_ID::I2C_READ_SET_ID); + if (res.Failed()) + error_printf("WriteHID() FAILED for I2C address 0x%02x, register 0x%02x", inI2CAddress, inRegister); + else + { + std::vector inputMessage(I2C_BUFFER_HEADER_SIZE + MAX_COMM_READ_BUFFER_SIZE); + res = mHIDImpl->ReadHID(inputMessage, (int)HID_REPORT_ID::I2C_READ_GET_ID); + if (res.Failed()) + error_printf("ReadHID() FAILED for I2C address 0x%02x, register 0x%02x", inI2CAddress, inRegister); + else + memcpy(outData, inputMessage.data(), inLength); + } + } + + EPL_ASSERT_BREAK(res.Succeeded()); + return res; +} + + +EGAVResult ElgatoUVCDevice::WriteI2cData(uint8_t inI2CAddress, uint8_t inRegister, uint8_t* inData, uint8_t inLength) +{ + EGAVResult_CheckPointer(inData); + EGAVResult_CheckPointer(mHIDImpl); + + const std::lock_guard lock(mHIDMutex); + + EGAVResult res = EGAVResult::ErrUnknown; + + if (mNewDeviceType) + { + const uint8_t writeLen = 1 + inLength /* +1 for byte register address*/, reportLen = 4 + writeLen; + std::vector outputMessage{ reportLen, (uint8_t)REPORT_CASE_NEW::REPORT_IIC_WRITE, inI2CAddress, writeLen, inRegister }; + for (int i = 0; i < inLength; i++) + outputMessage.push_back(inData[i]); + EPL_ASSERT_BREAK(reportLen == outputMessage.size()); + + res = mHIDImpl->WriteHID(outputMessage, (int)HID_REPORT_ID_NEW::I2C_WRITE); + } + else + { + std::vector outputMessage{ inI2CAddress, inRegister, inLength }; + for (int i = 0; i < inLength; i++) + outputMessage.push_back(inData[i]); + res = mHIDImpl->WriteHID(outputMessage, (int)HID_REPORT_ID::I2C_WRITE_ID); + } + + if (res.Failed()) + error_printf("WriteHID() FAILED for I2C address 0x%02d, register 0x%02d", inI2CAddress, inRegister); + return res; +} + +void ElgatoUVCDevice::SetHDRTonemappingEnabled(bool inValue) +{ + const std::lock_guard lock(mHIDMutex); + + uint8_t buffer = inValue ? 1 : 0; + WriteI2cData((uint8_t)I2CAddress::MCU, (uint8_t)MCU_I2C_REGISTER::XET_HDR_TONEMAPPING, &buffer, sizeof(buffer)); +} + +EGAVResult ElgatoUVCDevice::GetHDMIHDRStatusPacket(HDMI_GENERIC_INFOFRAME& outFrame) +{ + const std::lock_guard lock(mHIDMutex); + + const size_t bufSize = mNewDeviceType ? 32 : 33; + uint8_t* buffer = new uint8_t[bufSize]; + EGAVResult res = ReadI2cData((uint8_t)I2CAddress::MCU, (uint8_t)MCU_I2C_REGISTER::GET_HDR_PACKET, buffer, (uint8_t)bufSize); + if (res.Succeeded()) + { + size_t size = (bufSize < sizeof(outFrame)) ? bufSize : sizeof(outFrame); + memcpy(&outFrame, mNewDeviceType ? buffer : buffer+1, size); + +#if WORKAROUND_HD60_S_PLUS_PAYLOAD_SIZE + // Workaround HD60 S+ firmware issue: invalid payload length (seen with HDR and SPD info frames) + // Also with HD60 X FW 22.03.24 (MCU: 22.03.16) + if (outFrame.header.bPayloadLength > HDMI_MAX_INFOFRAME_PAYLOAD) + { + if (HDMI_INFOFRAME_TYPE_DR == outFrame.header.bfType) + { + int diff = outFrame.header.bPayloadLength - sizeof(outFrame.plDR1); + outFrame.header.bPayloadLength = sizeof(outFrame.plDR1); + outFrame.bChecksum += diff; + } + } +#endif + } + delete [] buffer; + return res; +} + +EGAVResult ElgatoUVCDevice::IsVideoHDR(bool& outIsHDR) +{ + // Try to read HDR meta data + HDMI_GENERIC_INFOFRAME frame{}, emptyFrame{}; + memset(&emptyFrame, 0, sizeof(emptyFrame)); + EGAVResult res = GetHDMIHDRStatusPacket(frame); + if (res.Succeeded()) + { + bool isInfoFrameValid = HDMI_IsInfoFrameValid(&frame); + res = isInfoFrameValid ? EGAVResult::Ok : EGAVResult::ErrUnknown; + if (isInfoFrameValid) + { + // Check type in header and EOTF flag in payload + if (HDMI_INFOFRAME_TYPE_DR == frame.header.bfType && HDMI_DR_EOTF_SDRGAMMA != frame.plDR1.bfEOTF) + { + outIsHDR = true; + } + else if (HDMI_INFOFRAME_TYPE_DR == frame.header.bfType && HDMI_DR_EOTF_SDRGAMMA == frame.plDR1.bfEOTF) + { + outIsHDR = false; // we get here with HD60 X (22.03.24 (MCU: 22.03.16)) + } + else if (0 /*HDMI_INFOFRAME_TYPE_RESERVED*/ == frame.header.bfType && (0 == memcmp(&frame, &emptyFrame, sizeof(emptyFrame)))) + { + // all empty (seen with HD60 S+ when HDR is not active) + outIsHDR = false; + } + else if (HDMI_INFOFRAME_TYPE_DR != frame.header.bfType) + { + warning_printf("HDMI Metadata: Wrong header type: %d", frame.header.bfType); + res = EGAVResult::ErrNotFound; + } + } + else + warning_printf("HDMI Metadata: HDMI_IsInfoFrameValid() returned error (checksum)!"); + } + else + warning_printf("HDMI Metadata: GetHDMIHDRStatusPacket() failed!"); + return res; +} diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.h new file mode 100644 index 0000000..a91afbe --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/ElgatoUVCDevice.h @@ -0,0 +1,84 @@ +/* +MIT License + +Copyright (c) 2022-23 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include + +#include "EGAVResult.h" +#include "EGAVDevice.h" +#include "HDMIInfoFramesAPI.h" + +#ifdef _MSC_VER +#include "win/EGAVHIDImplementation.h" +#else +#include "mac/EGAVHIDImplementation.h" +#endif + +// Supported devices +inline const EGAVDeviceID deviceIDHD60SPlus (EGAVBusType::USB, 0x0FD9, 0x006A); //!< HD60 S+ +inline const EGAVDeviceID deviceIDHD60X (EGAVBusType::USB, 0x0FD9, 0x0082); //!< HD60 X +inline const EGAVDeviceID deviceIDHD60XRev2 (EGAVBusType::USB, 0x0FD9, 0x008A); //!< HD60 X Rev. 2 +// EXTEND_DEVICES + + +//! @return Device IDs of supported Elgato UVC devices +std::vector GetElgatoUVCDeviceIDs(); + + +//! @return true for new devices with new USB chipset +bool IsNewDeviceType(const EGAVDeviceID& inDeviceID); + + + +//============================================================================== +// # Class ElgatoUVCDevice +//============================================================================== + +class ElgatoUVCDevice +{ + +public: + ElgatoUVCDevice(std::shared_ptr hid, bool isNewDeviceType); + + //! @brief Works with HD60 S+, HD60 X or newer + void SetHDRTonemappingEnabled(bool inEnable); + + //! @brief Works with HD60 S+, HD60 X or newer + EGAVResult GetHDMIHDRStatusPacket(HDMI_GENERIC_INFOFRAME& outFrame); + + //! @brief Works with HD60 S+, HD60 X or newer + EGAVResult IsVideoHDR(bool& outIsHDR); + + +private: + EGAVResult WriteI2cData(uint8_t inI2CAddress, uint8_t inRegister, uint8_t* inData, uint8_t inLength); + EGAVResult ReadI2cData(uint8_t inI2CAddress, uint8_t inRegister, uint8_t* outData, uint8_t inLength); + + bool mNewDeviceType = false; //!< true: HD60 X and newer devices , false: HD60 S+ + + std::shared_ptr mHIDImpl; + std::recursive_mutex mHIDMutex; +}; diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/HDMIInfoFramesAPI.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/HDMIInfoFramesAPI.h new file mode 100644 index 0000000..21c6ec7 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/HDMIInfoFramesAPI.h @@ -0,0 +1,746 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include + +#pragma pack(push, 1) + + +//==================================================================================== +// # VIDEO IDENTIFICATION CODES (VIC) +//==================================================================================== + +// Video ID Code, see CEA-861-E chapter 4.1, table 4 +// Video ID Code, see CEA-861-G chapter 4.1, table 3 + +typedef struct _HDMI_VIC_DESCRIPTOR +{ + uint8_t bID; //!< Video ID Code + int iWidth; //!< width in pixels + int iHeight; //!< height in pixels + int iFieldRate; //!< field refresh rate in Hz + bool bInterlaced; //!< interlaced + short iAspectX; //!< picture aspect ratio H + short iAspectY; //!< picture aspect ratio V +} +HDMI_VIC_DESCRIPTOR; + +// a field refresh value of 24Hz means either 24.00Hz or 23.98Hz +// a field refresh value of 30Hz means either 30.00Hz or 29.97Hz +// a field refresh value of 48Hz means either 48.00Hz or 47.95Hz +// a field refresh value of 60Hz means either 60.00Hz or 59.94Hz +// a field refresh value of 120Hz means either 120.00Hz or 119.88Hz +// a field refresh value of 240Hz means either 240.00Hz or 239.76Hz + +#define HDMI_VIC_TABLE_SIZE 220 + +extern const HDMI_VIC_DESCRIPTOR g_HDMI_VIC_TABLE[HDMI_VIC_TABLE_SIZE]; + +//==================================================================================== +// # INFO FRAME TYPES +//==================================================================================== + + +// see CEA-861-E chapter 6, table 6 +// see CEA-861-G chapter 6, table 5 + +#define HDMI_INFOFRAME_TYPE_RESERVED 0x00 // reserved + +#define HDMI_INFOFRAME_TYPE_VS 0x01 // Vendor Specific +#define HDMI_INFOFRAME_TYPE_AVI 0x02 // Auxiliary Video Information +#define HDMI_INFOFRAME_TYPE_SPD 0x03 // Source Product Description +#define HDMI_INFOFRAME_TYPE_A 0x04 // Audio +#define HDMI_INFOFRAME_TYPE_MS 0x05 // MPEG Source +#define HDMI_INFOFRAME_TYPE_VBI 0x06 // NTSC VBI +#define HDMI_INFOFRAME_TYPE_DR 0x07 // Dynamic Range and Mastering + +#define HDMI_INFOFRAME_TYPE_MIN HDMI_INFOFRAME_TYPE_VS +#define HDMI_INFOFRAME_TYPE_MAX HDMI_INFOFRAME_TYPE_DR + + +//==================================================================================== +// # INFO FRAME HEADER +//==================================================================================== + +// see CEA-861-G Annex D.1 + +#define HDMI_MAX_INFOFRAME_SIZE 31 // 3 bytes header + 1 byte checksum + 27 bytes payload +#define HDMI_MAX_INFOFRAME_PAYLOAD 27 + + +// see CEA-861-E chapter 6 + +typedef struct _HDMI_INFOFRAMEHEADER +{ + uint8_t bfType : 7; // InfoFrame Type Code (see HDMI_INFOFRAME_TYPE_*) + uint8_t bfPacketType : 1; // the HDMI Packet Type is 0x80+InfoframeType for HDMI InfoFrame Packets + uint8_t bfVersion : 7; // InfoFrame Version Number, starting with 1 + uint8_t bfChangeBit : 1; // InfoFrame Change Bit, VS Infoframe only + uint8_t bPayloadLength; // Size of InfoFrame payload, not including Type, Version, Length +} +HDMI_INFOFRAMEHEADER; + + +//==================================================================================== +// # VENDOR SPECIFIC INFO FRAME +//==================================================================================== + +// see CEA-861-E chapter 6.1, table 7 +// see CEA-861-G chapter 6.1, table 6 +// type code is 0x01, version is 0x01, size is vendor specific + +typedef struct _HDMI_VS1_PAYLOAD +{ + uint8_t IEEERegistrationID[3]; // IEEE OUI + uint8_t bVendorSpecificPayload[HDMI_MAX_INFOFRAME_SIZE - 3 - 3 - 1]; // 24 bytes +} +HDMI_VS1_PAYLOAD; + + +//------------------------------------------------------------------------------------ + + +// see CEA-861-G chapter 6.1, table 7 +// type code is 0x01, version is 0x02, size is vendor specific +// VS infoframe version 2 uses bit 7 of the version number as the ChangeBit + +typedef struct _HDMI_VS2_PAYLOAD +{ + uint8_t IEEERegistrationID[3]; // IEEE OUI + uint8_t bVendorSpecificPayload[HDMI_MAX_INFOFRAME_SIZE - 3 - 3 - 1]; // 24 bytes +} +HDMI_VS2_PAYLOAD; + + +//==================================================================================== +// # AUXILIARY VIDEO INFORMATION INFOFRAME +//==================================================================================== + +// Scan Information, see CEA-861-E chapter 6.4, table 10 +#define HDMI_AVI_S_NODATA 0x00 // no data +#define HDMI_AVI_S_OVERSCAN 0x01 // composed for overscan +#define HDMI_AVI_S_UNDERSCAN 0x02 // composed for underscan + +// Bar Data Present, see CEA-861-E chapter 6.4, table 10 +#define HDMI_AVI_B_NODATA 0x00 // no data +#define HDMI_AVI_B_V 0x01 // vertical bar info present +#define HDMI_AVI_B_H 0x02 // horizontal bar info present +#define HDMI_AVI_B_VH 0x03 // vertical and horizontal bar info present + +// Active Format Information Present, see CEA-861-E chapter 6.4, table 10 +#define HDMI_AVI_A_NONE 0x00 // no information +#define HDMI_AVI_A_PRESENT 0x01 // information present + +// RGB or YCbCr, see CEA-861-E chapter 6.4, table 10 +// RGB or YCbCr, see CEA-861-G chapter 6.4, table 10 +#define HDMI_AVI_Y_RGB 0x00 // RGB +#define HDMI_AVI_Y_YCBCR422 0x01 // YCbCr 4:2:2 +#define HDMI_AVI_Y_YCBCR444 0x02 // YCbCr 4:4:4 +#define HDMI_AVI_Y_YCBCR420 0x03 // YCbCr 4:2:0 +#define HDMI_AVI_Y_IDO 0x07 // IDO-Defined + +// Active Portion Aspect Ration, see CEA-861-E chapter 6.4, table 11 +#define HDMI_AVI_R_SAME 0x08 // same as coded frame aspect ration +#define HDMI_AVI_R_4TO3 0x09 // 4:3 (center) +#define HDMI_AVI_R_16TO9 0x0A // 16:9 (center) +#define HDMI_AVI_R_14TO9 0x0B // 14:9 (center) + +// Coded Frame Aspect Ration, see CEA-861-E chapter 6.4, table 11 +#define HDMI_AVI_M_NODATA 0x00 // no data +#define HDMI_AVI_M_4TO3 0x01 // 4:3 +#define HDMI_AVI_M_16TO9 0x02 // 16:9 + +// Colorimetry, see CEA-861-E chapter 6.4, table 11 +#define HDMI_AVI_C_NODATA 0x00 // no data +#define HDMI_AVI_C_SMTPE170M 0x01 // SMTPE 170M +#define HDMI_AVI_C_ITUR709 0x02 // ITU-R 709 +#define HDMI_AVI_C_EXTENDED 0x03 // extended colorimetry information valid + +// Non-Uniform Picture Scaling, see CEA-861-E chapter 6.4, table 13 +#define HDMI_AVI_SC_NO 0x00 // no known scaling +#define HDMI_AVI_SC_H 0x01 // picture has been scaled horizontally +#define HDMI_AVI_SC_V 0x02 // picture has been scaled vertically +#define HDMI_AVI_SC_HV 0x03 // picture has been scaled horizontally and vertically + +// RGB Quantization Range, see CEA-861-E chapter 6.4, table 13 +#define HDMI_AVI_Q_DEFAULT 0x00 // default, depends on video format +#define HDMI_AVI_Q_LIMITED 0x01 // limited range +#define HDMI_AVI_Q_FULL 0x02 // full range + +// Extended Colorimetry, see CEA-861-E chapter 6.4, table 13 +// Extended Colorimetry, see CEA-861-G chapter 6.4, table 13 +#define HDMI_AVI_EC_XVYCC601 0x00 // xvYCC 601 +#define HDMI_AVI_EC_XVYCC709 0x01 // xvYCC 709 +#define HDMI_AVI_EC_SYCC601 0x02 // sYCC 601 +#define HDMI_AVI_EC_ADOBEYCC601 0x03 // Adobe YCC 601 +#define HDMI_AVI_EC_ADOBERGB 0x04 // Adobe RGB +#define HDMI_AVI_EC_BT2020C 0x05 // ITU BT2020 YcCbcCrc +#define HDMI_AVI_EC_BT2020 0x06 // ITU BT2020 RGB or YCbCr +#define HDMI_AVI_EC_EXTENDED 0x07 // extended information, see HDMI_AVI_ACE_* + +// IT Content, see CEA-861-E chapter 6.4, table 13 +#define HDMI_AVI_ITC_NODATA 0x00 // no data +#define HDMI_AVI_ITC_VALID 0x01 // IT Content, CN is valid + +// Pixel Repetition Factor, see CEA-861-E chapter 6.4, table 15 +#define HDMI_AVI_PR_NONE 0x00 // pixels are not repeated, i.e. only sent once in total +#define HDMI_AVI_PR_1 0x01 // pixels are repeated once, i.e. sent twice in total +#define HDMI_AVI_PR_2 0x02 // pixels are repeated twice, i.e. sent three times in total +#define HDMI_AVI_PR_3 0x03 // pixels are repeated three times, i.e. sent 4 times in total +#define HDMI_AVI_PR_4 0x04 // pixels are repeated 4 times, i.e. sent 5 times in total +#define HDMI_AVI_PR_5 0x05 // pixels are repeated 5 times, i.e. sent 6 times in total +#define HDMI_AVI_PR_6 0x06 // pixels are repeated 6 times, i.e. sent 7 times in total +#define HDMI_AVI_PR_7 0x07 // pixels are repeated 7 times, i.e. sent 8 times in total +#define HDMI_AVI_PR_8 0x08 // pixels are repeated 8 times, i.e. sent 9 times in total +#define HDMI_AVI_PR_9 0x09 // pixels are repeated 9 times, i.e. sent 10 times in total + +// IT Content Type, see CEA-861-E chapter 6.4, table 16 +#define HDMI_AVI_CN_GRAPHICS 0x00 // graphics +#define HDMI_AVI_CN_PHOTO 0x01 // photo +#define HDMI_AVI_CN_CINEMA 0x02 // cinema +#define HDMI_AVI_CN_GAME 0x03 // game + +// YCC Quantization Range, see CEA-861-E chapter 6.4, table 17 +#define HDMI_AVI_YQ_LIMITED 0x00 // limited range +#define HDMI_AVI_YQ_FULL 0x01 // full range + +// Additional Colorimetry Extension, see CEA-861-G chapter 6.4, table 25 +#define HDMI_AVI_ACE_DCIP3D65 0x00 // DCI-P3 R'G'B' (D65) +#define HDMI_AVI_ACE_DCIP3TH 0x01 // DCI-P3 R'G'B' (Theater) + + +//------------------------------------------------------------------------------------ + + +// see CEA-861-E chapter 6.3, table 8 +// type code is 0x02, version is 0x01, size is 13 +// ATTENTION: this AVI version is obsolete and should not be used! + +typedef struct _HDMI_AVI1_PAYLOAD +{ + // data byte 1, see CEA-861-E chapter 6.4, table 10 + uint8_t bfScanInformation : 2; // S0, S1 + uint8_t bfBarDataPresent : 2; // B0, B1 + uint8_t bfActiveFormatInformationPresent : 1; // A0 + uint8_t bfRGBorYCbCr : 2; // Y0, Y1 + uint8_t bfFutureUse1 : 1; // reserved, zero + + // data byte 2, see CEA-861-E chapter 6.4, table 11 + uint8_t bfActivePortionAspectRatio : 4; // R0, R1, R2, R3 + uint8_t bfCodedFrameAspectRatio : 2; // M0, M1 + uint8_t bfColorimetry : 2; // C0, C1 + + // data byte 3, see CEA-861-E chapter 6.4, table 13 + uint8_t bfNonUniformPictureScaling : 2; // SC0, SC1 + uint8_t bfFutureUse3 : 6; // reserved, zero + + uint8_t bfFutureUse4 = 0; // reserved, zero + uint8_t bfFutureUse5 = 0; // reserved, zero + + uint16_t wLineNumberOfEndOfTopBar; // ETB + uint16_t wLineNumberOfStartOfBottomBar; // SBB + uint16_t wPixelNumberOfEndOfLeftBar; // ELB + uint16_t wPixelNumberOfStartOfRightBar; // SRB +} +HDMI_AVI1_PAYLOAD; + + +//------------------------------------------------------------------------------------ + + +// see CEA-861-E chapter 6.4, table 9 +// see CEA-861-E chapter 6.4, table 8 +// type code is 0x02, version is 0x02, size is 13 + +typedef struct _HDMI_AVI2_PAYLOAD +{ + // data byte 1, see CEA-861-E chapter 6.4, table 10 + uint8_t bfScanInformation : 2; // S0, S1 + uint8_t bfBarDataPresent : 2; // B0, B1 + uint8_t bfActiveFormatInformationPresent : 1; // A0 + uint8_t bfRGBorYCbCr : 2; // Y0, Y1 + uint8_t bfFutureUse1 : 1; // reserved, zero + + // data byte 2, see CEA-861-E chapter 6.4, table 11 + uint8_t bfActivePortionAspectRatio : 4; // R0, R1, R2, R3 + uint8_t bfCodedFrameAspectRatio : 2; // M0, M1 + uint8_t bfColorimetry : 2; // C0, C1 + + // data byte 3, see CEA-861-E chapter 6.4, table 13 + uint8_t bfNonUniformPictureScaling : 2; // SC0, SC1 + uint8_t bfRGBQuantizationRange : 2; // Q0, Q1 + uint8_t bfExtendedColorimetry : 3; // EC0, EC1, EC2 + uint8_t bfITContent : 1; // ITC + + // data byte 4, see CEA-861-E chapter 4.1, table 4 + uint8_t bfVIC : 7; // VIC + uint8_t bfFutureUse4 : 1; // reserved, zero + + // data byte 5, see CEA-861-E chapter 6.4, tables 15, 16, 17 + uint8_t bfPixelRepetitionFactor : 4; // PR0, PR1, PR2, PR3 + uint8_t bfITContentType : 2; // CN0, CN1 + uint8_t bfYCCQuantizationRange : 2; // YQ0, YQ1 + + uint16_t wLineNumberOfEndOfTopBar; // ETB + uint16_t wLineNumberOfStartOfBottomBar; // SBB + uint16_t wPixelNumberOfEndOfLeftBar; // ELB + uint16_t wPixelNumberOfStartOfRightBar; // SRB +} +HDMI_AVI2_PAYLOAD; + + +//------------------------------------------------------------------------------------ + + +// see CEA-861-E chapter 6.4, table 8 +// type code is 0x02, version is 0x03, size is 13 + +typedef struct _HDMI_AVI3_PAYLOAD +{ + // data byte 1, see CEA-861-G chapter 6.4, table 10 + uint8_t bfScanInformation : 2; // S0, S1 + uint8_t bfBarDataPresent : 2; // B0, B1 + uint8_t bfActiveFormatInformationPresent : 1; // A0 + uint8_t bfRGBorYCbCr : 3; // Y0, Y1, Y2 + + // data byte 2, see CEA-861-E chapter 6.4, table 11 + uint8_t bfActivePortionAspectRatio : 4; // R0, R1, R2, R3 + uint8_t bfCodedFrameAspectRatio : 2; // M0, M1 + uint8_t bfColorimetry : 2; // C0, C1 + + // data byte 3, see CEA-861-E chapter 6.4, table 13 + uint8_t bfNonUniformPictureScaling : 2; // SC0, SC1 + uint8_t bfRGBQuantizationRange : 2; // Q0, Q1 + uint8_t bfExtendedColorimetry : 3; // EC0, EC1, EC2 + uint8_t bfITContent : 1; // ITC + + // data byte 4, see CEA-861-E chapter 4.1, table 4 + uint8_t bVIC; // VIC + + // data byte 5, see CEA-861-E chapter 6.4, tables 15, 16, 17 + uint8_t bfPixelRepetitionFactor : 4; // PR0, PR1, PR2, PR3 + uint8_t bfITContentType : 2; // CN0, CN1 + uint8_t bfYCCQuantizationRange : 2; // YQ0, YQ1 + + uint16_t wLineNumberOfEndOfTopBar; // ETB + uint16_t wLineNumberOfStartOfBottomBar; // SBB + uint16_t wPixelNumberOfEndOfLeftBar; // ELB + uint16_t wPixelNumberOfStartOfRightBar; // SRB +} +HDMI_AVI3_PAYLOAD; + + +//------------------------------------------------------------------------------------ + + +// see CEA-861-E chapter 6.4, table 9 +// type code is 0x02, version is 0x04, size is 14 + +typedef struct _HDMI_AVI4_PAYLOAD +{ + // data byte 1, see CEA-861-G chapter 6.4, table 10 + uint8_t bfScanInformation : 2; // S0, S1 + uint8_t bfBarDataPresent : 2; // B0, B1 + uint8_t bfActiveFormatInformationPresent : 1; // A0 + uint8_t bfRGBorYCbCr : 3; // Y0, Y1, Y2 + + // data byte 2, see CEA-861-E chapter 6.4, table 11 + uint8_t bfActivePortionAspectRatio : 4; // R0, R1, R2, R3 + uint8_t bfCodedFrameAspectRatio : 2; // M0, M1 + uint8_t bfColorimetry : 2; // C0, C1 + + // data byte 3, see CEA-861-E chapter 6.4, table 13 + uint8_t bfNonUniformPictureScaling : 2; // SC0, SC1 + uint8_t bfRGBQuantizationRange : 2; // Q0, Q1 + uint8_t bfExtendedColorimetry : 3; // EC0, EC1, EC2 + uint8_t bfITContent : 1; // ITC + + // data byte 4, see CEA-861-E chapter 4.1, table 4 + uint8_t bVIC; // VIC + + // data byte 5, see CEA-861-E chapter 6.4, tables 15, 16, 17 + uint8_t bfPixelRepetitionFactor : 4; // PR0, PR1, PR2, PR3 + uint8_t bfITContentType : 2; // CN0, CN1 + uint8_t bfYCCQuantizationRange : 2; // YQ0, YQ1 + + uint16_t wLineNumberOfEndOfTopBar; // ETB + uint16_t wLineNumberOfStartOfBottomBar; // SBB + uint16_t wPixelNumberOfEndOfLeftBar; // ELB + uint16_t wPixelNumberOfStartOfRightBar; // SRB + + // data byte 14, see CEA-861-G chapter 6.4, tables 15, 16, 17 + uint8_t bfReserved14 : 4; // reserved, zero + uint8_t bfAdditionalColorimetry : 4; // ACE +} +HDMI_AVI4_PAYLOAD; + + +//------------------------------------------------------------------------------------ + + +#define HDMI_ERROR -1 + +#define HDMI_UNKNOWN 0 + +#define HDMI_FORMAT_RGB 1 // RGB +#define HDMI_FORMAT_YCBCR420 2 // YCbCr 4:2:0 +#define HDMI_FORMAT_YCBCR422 3 // YCbCr 4:2:2 +#define HDMI_FORMAT_YCBCR444 4 // YCbCr 4:4:4 + +#define HDMI_COLOR_ADOBERGB 1 +#define HDMI_COLOR_BT2020 2 +#define HDMI_COLOR_DCIP3D65 3 +#define HDMI_COLOR_DCIP3TH 4 +#define HDMI_COLOR_SMPTE170M 5 +#define HDMI_COLOR_BT709 6 +#define HDMI_COLOR_XVYCC601 7 +#define HDMI_COLOR_XVYCC709 8 +#define HDMI_COLOR_SYCC601 9 +#define HDMI_COLOR_ADOBEYCC601 10 +#define HDMI_COLOR_BT2020C 11 + + +//==================================================================================== +// # SOURCE PRODUCT DESCRIPTION INFOFRAME (SPD) +//==================================================================================== + + +// Source Information, see CEA-861-E chapter 6.5, table 22 +#define HDMI_SPD_SI_UNKNOWN 0x00 +#define HDMI_SPD_SI_STB 0x01 +#define HDMI_SPD_SI_DVD 0x02 +#define HDMI_SPD_SI_DVHS 0x03 +#define HDMI_SPD_SI_DVR 0x04 +#define HDMI_SPD_SI_DVC 0x05 +#define HDMI_SPD_SI_DSC 0x06 +#define HDMI_SPD_SI_VCD 0x07 +#define HDMI_SPD_SI_GAME 0x08 +#define HDMI_SPD_SI_PC 0x09 +#define HDMI_SPD_SI_BD 0x0A +#define HDMI_SPD_SI_SACD 0x0B +#define HDMI_SPD_SI_HDDVD 0x0C +#define HDMI_SPD_SI_PMP 0x0D + +const char* HDMI_SPD_ToString(uint8_t inByte); + +//! @brief Maps abbreviated names to user-friendly names (e.g. MSFT --> Microsoft) +//! @param inManufacturer manufacturer string as contained in HDMI info packet +std::string HDMI_SPD_MapManufacturerString(const std::string& inManufacturer); + +//------------------------------------------------------------------------------------ + + +// see CEA-861-E chapter 6.5, table 21 +// type code is 0x03, version is 1, size is 25 + +typedef struct _HDMI_SPD1_PAYLOAD +{ + uint8_t bVendorName[8]; + uint8_t bProductDescription[16]; + uint8_t bSourceInformation; +} +HDMI_SPD1_PAYLOAD; + + +//==================================================================================== +// AUDIO INFOFRAME +//==================================================================================== + + +// Audio Channel Count, see CEA-861-E chapter 6.6.1, table 24 +#define HDMI_A_CC_STREAM 0x00 // see stream header + +// Audio Coding Type, see CEA-861-E chapter 6.6.1, table 24 +#define HDMI_A_CT_STREAM 0x00 // see stream header +#define HDMI_A_CT_PCM 0x01 // PCM +#define HDMI_A_CT_AC3 0x02 // AC-3 +#define HDMI_A_CT_MPEG1 0x03 // MPEG-1 +#define HDMI_A_CT_MP3 0x04 // MP3 +#define HDMI_A_CT_MPEG2 0x05 // MPEG-2 +#define HDMI_A_CT_AACLC 0x06 // AAC-LC +#define HDMI_A_CT_DTS 0x07 // DTS +#define HDMI_A_CT_ATRAC 0x08 // ATRAC +#define HDMI_A_CT_DSD 0x09 // DSD +#define HDMI_A_CT_EAC3 0x0A // E-AC-3 +#define HDMI_A_CT_DTSHD 0x0B // DTS-HD +#define HDMI_A_CT_MLP 0x0C // MLP +#define HDMI_A_CT_DST 0x0D // DST +#define HDMI_A_CT_WMAPRO 0x0E // WMA Pro +#define HDMI_A_CT_CXT 0x0F // see Audio Coding Extension Type (CXT) + +// Sample Size, see CEA-861-E chapter 6.6.1, table 25 +#define HDMI_A_SS_STREAM 0x00 // see stream header +#define HDMI_A_SS_16BIT 0x01 // 16bit +#define HDMI_A_SS_20BIT 0x02 // 20bit +#define HDMI_A_SS_24BIT 0x03 // 24bit + +// Sample Frequency, see CEA-861-E chapter 6.6.1, table 25 +#define HDMI_A_SF_STREAM 0x00 // see stream header +#define HDMI_A_SF_32000 0x01 // 32kHz +#define HDMI_A_SF_44100 0x02 // 44.1kHz +#define HDMI_A_SF_48000 0x03 // 48kHz +#define HDMI_A_SF_88200 0x04 // 88.2kHz +#define HDMI_A_SF_96000 0x05 // 96kHz +#define HDMI_A_SF_176400 0x06 // 176.4kHz +#define HDMI_A_SF_192000 0x07 // 192kHz + +// Audio Coding Extension Type, see CEA-861-E chapter 6.6.1, table 26 +#define HDMI_A_CXT_CT 0x00 // see Audio Coding Type (CT) +#define HDMI_A_CXT_HEAAC 0x01 // HE-AAC +#define HDMI_A_CXT_HEAAC2 0x02 // HE-AAC v2 +#define HDMI_A_CXT_MPEGSURROUND 0x03 // MPEG Surround + +// Down-mix Inhibit, see CEA-861-E chapter 6.6.2, table 30 +#define HDMI_A_DM_PERMITTED 0x00 // down mix permitted +#define HDMI_A_DM_PROHIBITED 0x01 // down mix prohibited + +// LFE Playback Level, see CEA-861-E chapter 6.6.2, table 31 +#define HDMI_A_LFEPBL_UNKNOWN 0x00 // unknown +#define HDMI_A_LFEPBL_0DB 0x01 // 0 dB +#define HDMI_A_LFEPBL_10DB 0x02 // +10 dB + + +//------------------------------------------------------------------------------------ + + +// see CEA-861-E chapter 6.6, table 23 +// type code is 0x04, version is 1, size is 10 + +typedef struct _HDMI_A1_PAYLOAD +{ + // data byte 1, see CEA-861-E chapter 6.6.1, table 24 + uint8_t bfChannelCount : 3; // CC0, CC1, CC2 (channel count - 1) + uint8_t bfReserved1 : 1; // reserved, zero + uint8_t bfAudioCodingType : 4; // CT0, CT1, CT2, CT3 + + // data byte 2, see CEA-861-E chapter 6.6.1, table 25 + uint8_t bfSampleSize : 2; // SS0, SS1 + uint8_t bfSampleFrequency : 3; // SF0, SF1, SF2 + uint8_t bfReserved2 : 3; // reserved, zero + + // data byte 3, see CEA-861-E chapter 6.6.1, table 26 + uint8_t bfAudioCodingExtensionType : 5; // CXT0, CXT1, CXT2, CXT3, CXT4 + uint8_t bfReserved3 : 3; // reserved, zero + + // data byte 4, see CEA-861-E chapter 6.6.2, table 28 + uint8_t bChannelAllocation; // CA (channel to speaker) + + // data byte 5, see CEA-861-E chapter 6.6.2, tables 29, 30, 31 + uint8_t bfLFEPlaybackLevel : 2; // LFEPBL0, LFEPBL1 + uint8_t bfReserved5 : 1; // reserved, zero + uint8_t bfLevelShiftValue : 4; // LSV0, LSV1 (dB) + uint8_t bfDownMixInhibitFlag : 1; // DM_INH + + uint8_t bReserved6 = 0; // reserved, or Speaker Mask, or Channel Index + uint8_t bReserved7 = 0; // reserved, or Speaker Mask, or Channel Index + uint8_t bReserved8 = 0; // reserved, or Speaker Mask, or Channel Index + uint8_t bReserved9 = 0; // reserved, or Speaker Mask, or Channel Index + uint8_t bReserved10 = 0; // reserved, zero +} +HDMI_A1_PAYLOAD; + + +//==================================================================================== +// MPEG SOURCE INFOFRAME +//==================================================================================== + + +// MPEG Frame, see CEA-861-E chapter 6.7, table 33 +#define HDMI_MS_MF_UNKNOWN 0x00 // unknown +#define HDMI_MS_MF_I 0x01 // I-Frame +#define HDMI_MS_MF_P 0x02 // P-Frame +#define HDMI_MS_MF_B 0x03 // B-Frame + +// Field Repeat, see CEA-861-E chapter 6.7, table 33 +#define HDMI_MS_FR_NEW 0x00 // new field +#define HDMI_MS_FR_REPEATED 0x01 // repeated field + + +//------------------------------------------------------------------------------------ + + +// see CEA-861-E chapter 6.7, table 32 +// type code is 0x05, version is 1, size is 10 +// ATTENTION: it is recommended not to use this info frame + +typedef struct _HDMI_MS1_PAYLOAD +{ + uint32_t dwMPEGBitRate; // MPEG bit rate in Hz + + // data byte 5, see CEA-861-E chapter 6.7, table 33 + uint8_t bfMPEGFrame : 2; // MF0, MF1 + uint8_t bfReserved5a : 2; // reserved, zero + uint8_t bfFieldRepeat : 1; // FR0 + uint8_t bfReserved5b : 3; // reserved, zero + + uint8_t bReserved6 = 0; // reserved + uint8_t bReserved7 = 0; // reserved + uint8_t bReserved8 = 0; // reserved + uint8_t bReserved9 = 0; // reserved + uint8_t bReserved10 = 0; // reserved +} +HDMI_MS1_PAYLOAD; + + +//==================================================================================== +// NTSC VBI INFOFRAME +//==================================================================================== + + +// see CEA-861-E chapter 6.8, table 34 +// type code is 0x06, version is 1, size depends + +typedef struct _HDMI_VBI1_PAYLOAD +{ + uint8_t bPESDataField[HDMI_MAX_INFOFRAME_SIZE - 3 - 1]; // PES data field, limited to max 27 bytes +} +HDMI_VBI1_PAYLOAD; + + +//==================================================================================== +// # DYNAMIC RANGE AND MASTERING INFOFRAME +//==================================================================================== + +/* +Example for valid DR info frame: + +87 (type) | Header +01 (version) | +1A (length) | +8D (checksum) +02 00 FA 00 AE 02 85 00 | Payload +29 00 A3 02 5C 01 40 01 | +51 01 DB 05 00 00 DB 05 | +1F 03 | +*/ + +//------------------------------------------------------------------------------------ + + +// EOTF, see CEA-861.3-A chapter 3.2, table 3 +#define HDMI_DR_EOTF_SDRGAMMA 0x00 // traditional gamma, SDR +#define HDMI_DR_EOTF_HDRGAMMA 0x01 // traditional gamma, HDR +#define HDMI_DR_EOTF_ST2084 0x02 // ST2084 PQ +#define HDMI_DR_EOTF_HLG 0x03 // BT2100 HLG + +// Metadata, see CEA-861.3-A chapter 3.2, table 4 +#define HDMI_DR_MD_STATIC 0x00 // static metadata type 1 + + +//------------------------------------------------------------------------------------ + + +// used for static metadata, see CEA-861.3-A chapter 3.2.1 +typedef struct _HDMI_XY +{ + uint16_t X; // encoded in units of 0.00002 + uint16_t Y; // encoded in units of 0.00002 +} +HDMI_XY; + + +//------------------------------------------------------------------------------------ + +//! HDR Meta Data +//! type code is 0x07, version is 1, size depends (30 for static metadata type 1) +//! @sa CEA-861.3-A chapter 3.2, table 2 + +typedef struct _HDMI_DR1_PAYLOAD +{ + // data byte 1, see CEA-861.3-A chapter 3.2, table 3 + uint8_t bfEOTF : 3; // EOTF + uint8_t bfReserved1 : 5; // reserved + + // data byte 2, see CEA-861.3-A chapter 3.2, table 4 + uint8_t bfMetadataID : 3; // static metadata descriptor ID + uint8_t bfReserved2 : 5; // reserved + + // data bytes 3-22, for static metadata type 1, see CEA-861.3-A chapter 3.2.1, table 5 + HDMI_XY xyDisplayPrimaries[3]; // chromaticity of red or green or blue (ST2086) + HDMI_XY xyWhitePoint; // white point (ST2086) + uint16_t wMaxDisplayLuminance; // maximum display mastering luminance (ST2086), nit + uint16_t wMinDisplayLuminance; // minimum display mastering luminance (ST2086), 0.0001 nit + uint16_t wMaxCLL; // maximum content light level, nit + uint16_t wMaxFALL; // maximum frame-average light level, nit +} +HDMI_DR1_PAYLOAD; + + +//==================================================================================== +// GENERIC INFOFRAME TYPE +//==================================================================================== + + +typedef struct _HDMI_GENERIC_INFOFRAME +{ + HDMI_INFOFRAMEHEADER header; // type, version, length + uint8_t bChecksum; // the sum of all bytes in the info frame must be zero + + union + { + // generic byte array, to address the InfoFrame by index + uint8_t bPayload[HDMI_MAX_INFOFRAME_SIZE - 3 - 1] = { 0 }; + + // specific infoframes + HDMI_VS1_PAYLOAD plVS1; + HDMI_VS2_PAYLOAD plVS2; + HDMI_AVI1_PAYLOAD plAVI1; + HDMI_AVI2_PAYLOAD plAVI2; + HDMI_AVI3_PAYLOAD plAVI3; + HDMI_AVI4_PAYLOAD plAVI4; + HDMI_SPD1_PAYLOAD plSPD1; + HDMI_A1_PAYLOAD plA1; + HDMI_MS1_PAYLOAD plMS1; + HDMI_VBI1_PAYLOAD plVBI1; + HDMI_DR1_PAYLOAD plDR1; + }; +} +HDMI_GENERIC_INFOFRAME; + + +#pragma pack(pop) + + +//==================================================================================== +// # FUNCTIONS +//==================================================================================== + +// Verifies the checksum of the HDMI Info Frame +inline bool HDMI_IsInfoFrameValid(const _HDMI_GENERIC_INFOFRAME* pInfoFrame) +{ + if (pInfoFrame == NULL) return false; + + unsigned char* data = (unsigned char*)pInfoFrame; + int size = sizeof(HDMI_INFOFRAMEHEADER) + 1 + pInfoFrame->header.bPayloadLength; + + unsigned char checksum = 0; + while (size-- != 0) checksum += *data++; + + return (checksum == 0); +} + + + diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.cpp b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.cpp new file mode 100644 index 0000000..c34a91c --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.cpp @@ -0,0 +1,284 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file EGAVHIDImplementation.cpp + +@brief macOS implementation of EGAVHIDInterface +**/ +//============================================================================== + +#include "EGAVHIDImplementation.h" +#include +#include +#include + + +//------------------------------------------------------------------------------ +// PREPROCESSOR SWITCHES +//------------------------------------------------------------------------------ + +std::shared_ptr CreateEGAVHIDInterface() +{ + return std::make_shared(); +} + +//============================================================================== +// # HID Device Enumeration +//============================================================================== + +static int LocationIDOfHIDDevice(IOHIDDeviceRef hidRef) +{ + int locationID = 0; + + // this block is just here to see if we can find the corresponding device + + CFTypeRef uniqueID = IOHIDDeviceGetProperty(hidRef, CFSTR(kIOHIDUniqueIDKey)); + if (CFGetTypeID(uniqueID) == CFNumberGetTypeID()) + { + uint64_t uniqueID64 = 0; + CFNumberGetValue((CFNumberRef)uniqueID, kCFNumberLongLongType, &uniqueID64); + //NSLog(@"uniqueID %lld or 0x%16llx", uniqueID64, uniqueID64); + + CFMutableDictionaryRef matchingDict = IORegistryEntryIDMatching(uniqueID64); + + // next call consumes a reference to matchingDict + io_service_t matchedService = IOServiceGetMatchingService(kIOMasterPortDefault, matchingDict); + if (matchedService != 0) + { + CFNumberRef loc = (CFNumberRef)IORegistryEntrySearchCFProperty(matchedService, kIOServicePlane, CFSTR("locationID"), kCFAllocatorDefault, kIORegistryIterateRecursively | kIORegistryIterateParents); + if (loc != NULL) + { + CFNumberGetValue(loc, kCFNumberIntType, &locationID); + CFRelease(loc); + } + } + } + + return locationID; +} + +static void HIDDeviceMatchingCallback(void* inContext, + IOReturn /*inResult*/, + void* /*inSender*/, + IOHIDDeviceRef inIOHIDDeviceRef) +{ + reinterpret_cast(inContext)->DeviceAdded(inIOHIDDeviceRef); +} + +static void HIDDeviceRemovalCallback(void* inContext, + IOReturn /*inResult*/, + void* /*inSender*/, + IOHIDDeviceRef inIOHIDDeviceRef) +{ + reinterpret_cast(inContext)->DeviceRemoved(inIOHIDDeviceRef); +} + + + + +//============================================================================== +// # Class EGAVHID +//============================================================================== + +EGAVHID::EGAVHID() +{ +} + +void EGAVHID::DeviceAdded(IOHIDDeviceRef deviceRef) +{ + if (mLocationID == 0) // we don't care about a specific locationID + { + mHIDDevice = deviceRef; + info_printf("## DeviceAdded()"); + } + else if (LocationIDOfHIDDevice(deviceRef) == mLocationID) + { + mHIDDevice = deviceRef; + info_printf("## DeviceAdded(): Location ID %d", mLocationID); + } +} + +void EGAVHID::DeviceRemoved(IOHIDDeviceRef deviceRef) +{ + if (mHIDDevice == deviceRef) + { + info_printf("## DeviceRemoved()"); + mHIDDevice = 0; + } +} + + +//============================================================================== +// ## HID interface +//============================================================================== + +EGAVResult EGAVHID::InitHIDInterface(const EGAVDeviceID& inDeviceID, EGAVUnitPtr /*inOwner*/, bool /* inIgnoreDevicePathCheck = false */) +{ + dbgFunctionI(); + EGAVResult res = EGAVResult::Ok; + + mLocationID = inDeviceID.locationID; + + if (!mWorkerCreated) // program dies if thread is assigned when it isn't already null + { + mWorkerCreated = true; + mWorker = std::thread([this, inDeviceID] + { + mRunLoop = CFRunLoopGetCurrent(); + + IOHIDManagerRef manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone); + + IOHIDManagerRegisterDeviceMatchingCallback(manager, HIDDeviceMatchingCallback, this); + IOHIDManagerRegisterDeviceRemovalCallback(manager, HIDDeviceRemovalCallback, this); + + CFMutableDictionaryRef matchingDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + CFNumberRef vendor = CFNumberCreate(kCFAllocatorDefault, kCFNumberShortType, &inDeviceID.vendorID); + CFNumberRef product = CFNumberCreate(kCFAllocatorDefault, kCFNumberShortType, &inDeviceID.productID); + + CFDictionaryAddValue(matchingDict, CFSTR(kIOHIDVendorIDKey), vendor); + CFDictionaryAddValue(matchingDict, CFSTR(kIOHIDProductIDKey), product); + + IOHIDManagerSetDeviceMatching(manager, matchingDict); + + IOHIDManagerScheduleWithRunLoop(manager, mRunLoop, kCFRunLoopDefaultMode); + + IOReturn ret = IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone); + EPL_ASSERT_BREAK(ret == kIOReturnSuccess); + + CFRunLoopRun(); + + IOHIDManagerClose(manager, kIOHIDOptionsTypeNone); + + CFRelease(manager); + }); + } + + // Wait for device found + int64_t startTimeMsec = EplTime_GetMonotonicMilliseconds(); + const int64_t kHidDiscoveryTimeoutMsec = 1500; + while (!mHIDDevice && EplTime_GetMonotonicMilliseconds() - startTimeMsec < kHidDiscoveryTimeoutMsec) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + if (mHIDDevice) + { + // Query input report size //! @todo do this only once + CFIndex reportSize = 0; + CFTypeRef number = IOHIDDeviceGetProperty(mHIDDevice, CFSTR(kIOHIDMaxInputReportSizeKey)); + CFNumberGetValue((CFNumberRef)number, kCFNumberCFIndexType, &reportSize); + mInputReportSize = (int)reportSize; + + number = IOHIDDeviceGetProperty(mHIDDevice, CFSTR(kIOHIDMaxOutputReportSizeKey)); + CFNumberGetValue((CFNumberRef)number, kCFNumberCFIndexType, &reportSize); + mOutputReportSize = (int)reportSize; + } + + return mHIDDevice ? EGAVResult::Ok : EGAVResult::ErrNotFound; +} + +EGAVResult EGAVHID::DeinitHIDInterface() +{ + dbgFunctionI(); + mHIDDevice = nullptr; + if (mRunLoop != nullptr) + { + CFRunLoopStop(mRunLoop); + mWorker.join(); + mRunLoop = nullptr; + } + mWorkerCreated = false; + return EGAVResult::Ok; +} + +EGAVResult EGAVHID::ReadHID(std::vector& outMessage, int inReportId, int inReadBufferSize /*= 0*/) +{ + EGAVResult_CheckPointer(mHIDDevice); + + EGAVResult res = EGAVResult::Ok; + + std::vector report(mInputReportSize); // from the hardware, no zero prepended + report[0] = inReportId; + + int usedSize = 0; + + { + CFIndex bufferSize = inReadBufferSize > 0 ? inReadBufferSize : report.size(); + IOReturn err = IOHIDDeviceGetReport(mHIDDevice, kIOHIDReportTypeInput, inReportId, + &report[0], &bufferSize); + if (err == noErr) + usedSize = (int)bufferSize; + else error_printf("IOHIDDeviceGetReport() failed with IOReturn %d (0x%08X)", err, err); + res = (err == noErr) ? EGAVResult::Ok : EGAVResult::ErrUnknown; + } + + if (res.Succeeded()) + { + outMessage.clear(); + + // Facecam: calling code expects the report ID (0) in front of the report (Facecam) + //! @todo check if this behavior is really necessary. Check on Windows also. + if (inReportId == kHidDefaultReportID) + outMessage.push_back(inReportId); + + for (int i = 0; i< usedSize; i++) + outMessage.push_back(report[i]); + } + return res; +} + +//! If device only has one report ID, it is zero (kHidDefaultReportID) +EGAVResult EGAVHID::WriteHID(const std::vector& inMessage, int inReportID) +{ + // dbgFunctionI(); + + EGAVResult_CheckPointer(mHIDDevice); + + std::vector report; + + // From Device Class Definition for Human Interface Devices (HID) Version 1.11 + // If a device has multiple report structures, all data transfers start with a 1-byte identifier prefix that indicates which report structure + // applies to the transfer. This allows the class driver to distinguish incoming pointer data from keyboard data by examining the transfer prefix. + if (inReportID != kHidDefaultReportID) + report.push_back(inReportID); + + for (auto m : inMessage) + report.push_back(m); + + report.resize(mInputReportSize, 0); // pad report with zeros, ensure it is always the right length + IOReturn err = IOHIDDeviceSetReport(mHIDDevice, kIOHIDReportTypeOutput, inReportID, + &report[0], report.size()); + + // 0xE00002D6 - kIOReturnTimeout + // 0xE00002EB - kIOReturnAborted + if (err != noErr) + error_printf("IOHIDDeviceSetReport() failed with IOReturn %d (0x%08X)", err, err); + + EGAVResult res = (err == noErr) ? EGAVResult::Ok : EGAVResult::ErrUnknown; + + return res; +} diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.h new file mode 100644 index 0000000..649859f --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/mac/EGAVHIDImplementation.h @@ -0,0 +1,76 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file EGAVHIDImplementation.h + +@brief macOS implementation of EGAVHIDInterface +**/ +//============================================================================== + +#pragma once + +#include +#include + +// macOS +#include + +#include "EGAVEngine/EGAVHID.h" + +class HIDTransport; + +class EGAVHID : public EGAVHIDInterface +{ +public: + EGAVHID(); + + void DeviceAdded(IOHIDDeviceRef deviceRef); + void DeviceRemoved(IOHIDDeviceRef deviceRef); + + //----------------------------------------------------------------------------- + // ## EGAVHIDInterface implementation + //----------------------------------------------------------------------------- + virtual EGAVResult InitHIDInterface(const EGAVDeviceID& inDeviceID, EGAVUnitPtr inOwner, bool inIgnoreDevicePathCheck = false) override; + virtual EGAVResult DeinitHIDInterface() override; + + //! @brief Reads a HID response message from the OS. + //! @param outMessage will contain the resulting message. Its length will be adjusted automatically. + virtual EGAVResult ReadHID(std::vector& outMessage, int inReportID, int inReadBufferSize = 0) override; + + //! @brief Writes a HID report (with ID 0) containing inMessage to the OS. + //! @param inMessage the report contents, not including the report ID. + virtual EGAVResult WriteHID(const std::vector& inMessage, int inReportID) override; + +private: + int mLocationID = 0; + int mInputReportSize = 0, mOutputReportSize = 0; + + std::atomic mHIDDevice = nullptr; + + std::atomic mRunLoop = nullptr; + std::thread mWorker; //!< background worker for HID device discovery + bool mWorkerCreated = false; +}; diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.cpp b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.cpp new file mode 100644 index 0000000..cdc5d17 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.cpp @@ -0,0 +1,216 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file EGAVHIDImplementation.cpp + +@brief Windows implementation of EGAVHIDInterface +**/ +//============================================================================== + +// https://docs.microsoft.com/en-us/windows-hardware/drivers/hid/introduction-to-hid-concepts + +#include "EGAVHIDImplementation.h" + +#include // for ostringstream + +// Windows headers; for HID interface +#include +#pragma comment(lib,"hid.lib") +#include +#include + +#pragma comment(lib, "setupapi.lib") + + +#ifndef SAFE_CLOSE_HANDLE + #define SAFE_CLOSE_HANDLE(_handle_) \ + { \ + if ((_handle_ != 0) && (_handle_ != INVALID_HANDLE_VALUE))\ + { \ + CloseHandle(_handle_); \ + _handle_= INVALID_HANDLE_VALUE; \ + } \ + } +#endif + +std::string GetHIDDevicePath(int inIndex) +{ + std::string devicePath; + + GUID guid; + HidD_GetHidGuid(&guid); + + HDEVINFO DeviceInfo = SetupDiGetClassDevs(&guid, NULL, NULL, (DIGCF_PRESENT | DIGCF_DEVICEINTERFACE)); + + SP_DEVICE_INTERFACE_DATA DeviceInterface; + DeviceInterface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + + if (!SetupDiEnumDeviceInterfaces(DeviceInfo, NULL, &guid, inIndex, &DeviceInterface)) + { + SetupDiDestroyDeviceInfoList(DeviceInfo); + return ""; + } + + unsigned long size = 0; + SetupDiGetDeviceInterfaceDetail(DeviceInfo, &DeviceInterface, NULL, 0, &size, 0); + + PSP_INTERFACE_DEVICE_DETAIL_DATA pDeviceDetail = (PSP_INTERFACE_DEVICE_DETAIL_DATA)malloc(size); + if (pDeviceDetail) + { + pDeviceDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA); + + if (SetupDiGetDeviceInterfaceDetail(DeviceInfo, &DeviceInterface, pDeviceDetail, size, &size, NULL)) + devicePath = CT2A(pDeviceDetail->DevicePath); + free(pDeviceDetail); + } + + SetupDiDestroyDeviceInfoList(DeviceInfo); + return devicePath; +} + + + +std::shared_ptr CreateEGAVHIDInterface() +{ + return std::make_shared(); +} + + +//============================================================================== +// ## Class EGAVHID +//============================================================================== + +EGAVHID::EGAVHID() + : mHIDCaps(std::make_unique<_HIDP_CAPS>()) +{ +} + +EGAVResult EGAVHID::InitHIDInterface(const EGAVDeviceID& inDeviceID) +{ + EGAVResult res = EGAVResult::ErrNotFound; + + + DWORD index = 0; + std::string path; + while ((path = GetHIDDevicePath(index++)) != "") + { + HANDLE hHidDevice = CreateFileA(path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); + if (hHidDevice == INVALID_HANDLE_VALUE) + continue; + + bool isCorrectHidDevice = false; + HIDD_ATTRIBUTES attr; + if (HidD_GetAttributes(hHidDevice, &attr)) + isCorrectHidDevice = (attr.VendorID == inDeviceID.vendorID && attr.ProductID == inDeviceID.productID); + + if (isCorrectHidDevice) + { + mHIDHandle = hHidDevice; + + { + PHIDP_PREPARSED_DATA p; + HidD_GetPreparsedData(mHIDHandle, &p); + HIDP_CAPS c; + HidP_GetCaps(p, &c); + + *mHIDCaps = c; + + HidD_FreePreparsedData(p); + } + + res = EGAVResult::Ok; + break; + } + CloseHandle(hHidDevice); + } + + return res; +} + +EGAVResult EGAVHID::DeinitHIDInterface() +{ + SAFE_CLOSE_HANDLE(mHIDHandle); + return EGAVResult::Ok; +} + +//! This reads the report from the hardware +//! See Facemcam (Penna) sample code in https://elgato.atlassian.net/browse/EVH-493 +EGAVResult EGAVHID::ReadHID(std::vector& outMessage, int inReportID, int inReadBufferSize/* = 0*/) +{ + if (outMessage.size() >= mHIDCaps->InputReportByteLength) + return EGAVResult::ErrInvalidParameter; + + std::vector inputReport(mHIDCaps->InputReportByteLength); + + inputReport[0] = (uint8_t)inReportID; + + if (inReadBufferSize > 0) + inputReport.resize(inReadBufferSize); // Required for Cam Link PD575 (EVH-1418) + + BOOL success = HidD_GetInputReport(mHIDHandle, &inputReport[0], (ULONG)inputReport.size()); + EGAVResult res = success ? EGAVResult::Ok : EGAVResult::ErrInvalidOperation; + + // 121- ERROR_SEM_TIMEOUT + // 31 - ERROR_GEN_FAILURE - for invalid report ID + if (FALSE == success) // 87 - ERROR_INVALID_PARAMETER - if (buffer size != caps.InputReportByteLength) + { + // error_printf("HidD_GetInputReport() for report ID %d FAILED with %d", inReportID, GetLastError()); + } + else + { + outMessage.assign(inputReport.begin(), inputReport.end()); + } + + return res; +} + +// this only prepends a zero byte (the report ID) to the message, pads it out to the size of an output +// report and sends it to the hardware + +EGAVResult EGAVHID::WriteHID(const std::vector& inMessage, int inReportID) +{ + if (!mHIDCaps) + return EGAVResult::ErrInvalidState; + + if (inMessage.size() > mHIDCaps->OutputReportByteLength-1) + return EGAVResult::ErrInvalidParameter; + + std::vector outputReport(mHIDCaps->OutputReportByteLength, 0); + + outputReport[0] = (uint8_t)inReportID; + memcpy(&outputReport[1], &inMessage[0], inMessage.size()); + + // If the top-level collection includes report IDs, the caller must set the first byte of the ReportBuffer parameter to a non-zero report ID. + BOOL success = HidD_SetOutputReport(mHIDHandle, &outputReport[0], (ULONG)outputReport.size()); + EGAVResult res = success ? EGAVResult::Ok : EGAVResult::ErrInvalidOperation; + // 1167 - ERROR_DEVICE_NOT_CONNECTED + if (FALSE == success) // 87 - ERROR_INVALID_PARAMETER - if (buffer size != c.OutputReportByteLength) + { + // error_printf("#### HID: HidD_SetOutputReport() FAILED with %d", GetLastError()); + } + + return res; +} \ No newline at end of file diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.h new file mode 100644 index 0000000..8eac577 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/Library/win/EGAVHIDImplementation.h @@ -0,0 +1,65 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file EGAVHIDImplementation.h + +@brief Windows implementation of EGAVHIDInterface +**/ +//============================================================================== + +#pragma once + +#include + +#include "EGAVHID.h" + +struct _HIDP_CAPS; + +class EGAVHID : public EGAVHIDInterface +{ +public: + EGAVHID(); + + //----------------------------------------------------------------------------- + // ## EGAVHIDInterface implementation + //----------------------------------------------------------------------------- + //! @param inIgnoreDevicePathCheck true for Cyclops because HID has a different DeviceID + virtual EGAVResult InitHIDInterface(const EGAVDeviceID& inDeviceID) override; + virtual EGAVResult DeinitHIDInterface() override; + + //! @brief Reads a HID response message from the OS. + //! @param outMessage will contain the resulting message. Its length will be adjusted automatically. + virtual EGAVResult ReadHID(std::vector& outMessage, int inReportID, int inReadBufferSize = 0) override; + + //! @brief Writes a HID report (with ID 0) containing inMessage to the OS. + //! @param inMessage the report contents, not including the report ID. + virtual EGAVResult WriteHID(const std::vector& inMessage, int inReportID) override; + + HANDLE GetHIDHandle() { return mHIDHandle; } +private: + HANDLE mHIDHandle = nullptr; + std::unique_ptr<_HIDP_CAPS> mHIDCaps; +}; \ No newline at end of file diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.cpp b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.cpp new file mode 100644 index 0000000..91b0b2c --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.cpp @@ -0,0 +1,156 @@ +/* +MIT License + +Copyright (c) 2022 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file DriverInterface.cpp + +@brief EGAVDeviceProperties class implementation + Support for 4K60 Pro MK.2 and 4K60 S+ +**/ +//============================================================================== + +#include "HDMIInfoFramesAPI.h" +#include "DriverInterface.h" + +#ifndef EGAV_OVERRIDE_DEBUG_MACROS + inline void dummy() {} + + #define warning_printf(...) dummy() + + #ifndef HR_CHKRET_POINTER + #define HR_CHKRET_POINTER(_ptr_) if (!(_ptr_)) return E_POINTER; + #endif +#endif + + +//! Property IDs for IKsPropertySet +enum class DriverProperty +{ + XET_ENCODER_VIDEO_FORMAT = 400, //!< 4K60 S+ encoder format. uint32_t parameter: 0 - H.264 and 1 - HEVC + GET_HDMI_HDR_PACKET_00_15 = 720, //!< HDMI HDR status packet - part 1 + GET_HDMI_HDR_PACKET_16_31 = 721, //!< HDMI HDR status packet - part 2 + XET_HDMI_HDR_TO_SDR = 722 //!< 4K60 Pro MK.2 Set HDR tonemapping. uint32_t parameter: 1 - on / 0 - off +}; + +static const int HDMI_PACKET_SIZE = 32; + + +EGAVDeviceProperties::EGAVDeviceProperties(IKsPropertySet* inKsPropertySet, DeviceType inDeviceType) + : mICustomPropertySet(inKsPropertySet), mDeviceType(inDeviceType) +{ + switch (inDeviceType) + { + case EGAVDeviceProperties::DeviceType::GC4K60ProMK2: + mCustomPropertySetGUID = { 0xD1E5209F, 0x68FD, 0x4529, 0xBE, 0xE0, 0x5E, 0x7A, 0x1F, 0x47, 0x92, 0x26 }; + break; + case EGAVDeviceProperties::DeviceType::GC4K60SPlus: + mCustomPropertySetGUID = { 0xD1E5209F, 0x68FD, 0x4529, 0xBE, 0xE0, 0x5E, 0x7A, 0x1F, 0x47, 0x92, 0x24 }; + break; + default: + break; + } +} + +HRESULT EGAVDeviceProperties::SetEncoderType(bool inHEVC) +{ + if (mDeviceType != DeviceType::GC4K60SPlus) + return E_FAIL; + + HR_CHKRET_POINTER(mICustomPropertySet); + + uint32_t param = inHEVC ? 1 : 0; + HRESULT hr = mICustomPropertySet->Set(mCustomPropertySetGUID, (DWORD)DriverProperty::XET_ENCODER_VIDEO_FORMAT, nullptr, 0, ¶m, sizeof(param)); + return hr; +} + +HRESULT EGAVDeviceProperties::SetHDRTonemapping(bool inEnable) +{ + if (mDeviceType != DeviceType::GC4K60ProMK2) + return E_FAIL; + + HR_CHKRET_POINTER(mICustomPropertySet); + + uint32_t param = inEnable ? 1 : 0; + HRESULT hr = mICustomPropertySet->Set(mCustomPropertySetGUID, (DWORD)DriverProperty::XET_HDMI_HDR_TO_SDR, nullptr, 0, ¶m, sizeof(param)); + return hr; +} + +HRESULT EGAVDeviceProperties::GetHDMIHDRStatusPacket(uint8_t *outBuffer, int inBufferSize) +{ + HR_CHKRET_POINTER(outBuffer); + + if (inBufferSize < HDMI_PACKET_SIZE) + return E_INVALIDARG; + + HR_CHKRET_POINTER(mICustomPropertySet); + + DWORD dwRet = 0; + HRESULT hr = mICustomPropertySet->Get(mCustomPropertySetGUID, (DWORD)DriverProperty::GET_HDMI_HDR_PACKET_00_15, nullptr, 0, &outBuffer[0], 16, &dwRet); + if (SUCCEEDED(hr)) + hr = mICustomPropertySet->Get(mCustomPropertySetGUID, (DWORD)DriverProperty::GET_HDMI_HDR_PACKET_16_31, nullptr, 0, &outBuffer[16], 16, &dwRet); + return hr ; +} + +HRESULT EGAVDeviceProperties::IsVideoHDR(bool& outIsHDR) +{ + outIsHDR = false; + + // Try to read HDR meta data + static const uint8_t emptyBuffer[HDMI_PACKET_SIZE] = { 0 }; + uint8_t buffer[HDMI_PACKET_SIZE] = { 0 }; + HRESULT hr = GetHDMIHDRStatusPacket(buffer, sizeof(buffer)); + if (SUCCEEDED(hr)) + { + HDMI_GENERIC_INFOFRAME* frame = (HDMI_GENERIC_INFOFRAME*)(&buffer[0]); + hr = (true == HDMI_IsInfoFrameValid(frame)) ? S_OK : E_FAIL; + if (SUCCEEDED(hr)) + { + // Check type in header and EOTF flag in payload + if (HDMI_INFOFRAME_TYPE_DR == frame->header.bfType && HDMI_DR_EOTF_SDRGAMMA != frame->plDR1.bfEOTF) + { + outIsHDR = true; + } + else if (HDMI_INFOFRAME_TYPE_DR == frame->header.bfType && HDMI_DR_EOTF_SDRGAMMA == frame->plDR1.bfEOTF) + { + outIsHDR = false; + } + else if (0 /*HDMI_INFOFRAME_TYPE_RESERVED */ == frame->header.bfType && (0 == memcmp(buffer, emptyBuffer, sizeof(buffer)))) + { + outIsHDR = false; + } + else if (HDMI_INFOFRAME_TYPE_DR != frame->header.bfType) + { + warning_printf("HDMI Metadata: Wrong header type: %d", frame->header.bfType); + hr = E_FAIL; + } + } + else + warning_printf("HDMI Metadata: HDMI_IsInfoFrameValid() returned error (checksum)!"); + } + else + warning_printf("HDMI Metadata: GetHDMIHDRStatusPacket() failed!"); + + return hr; +} diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.h b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.h new file mode 100644 index 0000000..4a24a97 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/DriverInterface.h @@ -0,0 +1,72 @@ +/* +MIT License + +Copyright (c) 2022-23 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +//============================================================================== +/** +@file DriverInterface.h + +@brief EGAVDeviceProperties class implementation. + Support for 4K60 Pro MK.2 and 4K60 S+ +**/ +//============================================================================== + + +#include +#include + +#include "HDMIInfoFramesAPI.h" + +//! @brief Device properties for Elgato's non-UVC devices +class EGAVDeviceProperties +{ +public: + enum class DeviceType + { + None = 0, + GC4K60ProMK2, //!< 4K60 Pro MK.2: PCI\VEN_12AB& DEV_0710& SUBSYS_000E1CFA + GC4K60SPlus //!< 4K60 S+: USB\VID_0FD9&PID_0068 or USB\VID_0FD9&PID_0075 + }; + + //! @brief + //! @param inKsPropertySet Interface for driver property set. + //! Can be queried from the DirectShow filter via IBaseFilter::QueryInterface() + //! @param inDeviceType + EGAVDeviceProperties(IKsPropertySet* inKsPropertySet, DeviceType inDeviceType); + + //! @brief 4K60 S+ only + //! @param inHEVC 1 - HEVC, 0 - H.264 + HRESULT SetEncoderType(bool inHEVC); + + //! @brief 4K60 Pro MK.2 only + //! @param inEnable 1 - enable tone mapping, 0 - disable HDR tonemapping + HRESULT SetHDRTonemapping(bool inEnable); + + HRESULT IsVideoHDR(bool& outIsHDR); + HRESULT GetHDMIHDRStatusPacket(uint8_t* outBuffer, int inBufferSize); + +private: + DeviceType mDeviceType = DeviceType::None; + GUID mCustomPropertySetGUID = GUID_NULL; + CComPtr mICustomPropertySet; +}; \ No newline at end of file diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/main.cpp b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/main.cpp new file mode 100644 index 0000000..0dbe61d --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/SampleCode/main.cpp @@ -0,0 +1,91 @@ +/* +MIT License + +Copyright (c) 2022-23 Corsair Memory, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include +#include + +#include "ElgatoUVCDevice.h" + + +//============================================================================== +// # Constants +//============================================================================== + +const EGAVDeviceID& selectedDeviceID = deviceIDHD60SPlus; // Select your device here + + +//============================================================================== +// # main() +//============================================================================== +int main() +{ + std::cout << "========================================" << std::endl; + std::cout << " Sample: HDR Tonemapping" << std::endl; + std::cout << "========================================" << std::endl; + std::cout << std::endl; + + std::shared_ptr hid = std::make_shared(); + EGAVResult res = hid->InitHIDInterface(selectedDeviceID); + if (res.Failed()) + { + std::cout << "InitHIDInterface() failed. Do you have the correct device connected?" << std::endl << std::endl; + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + } + else + { + ElgatoUVCDevice device(hid, IsNewDeviceType(selectedDeviceID)); + HDMI_GENERIC_INFOFRAME frame{}; + memset(&frame, 0, sizeof(frame)); + res = device.GetHDMIHDRStatusPacket(frame); + if (res.Succeeded()) + { + bool isHDR = false; + res = device.IsVideoHDR(isHDR); + std::cout << "Video is " << (isHDR ? "HDR" : "SDR") << std::endl; + if (res.Succeeded() && isHDR) + { + std::cout << "Disable HDR tonemapping" << std::endl; + device.SetHDRTonemappingEnabled(false); + +#if 1 // TEST: TOGGLE TONEMAPPINING + for (int i = 0; i < 2; i++) + { + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + std::cout << "Enable HDR tonemapping" << std::endl; + device.SetHDRTonemappingEnabled(true); + + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + std::cout << "Disable HDR tonemapping" << std::endl; + device.SetHDRTonemappingEnabled(false); + } +#endif + } + } + hid->DeinitHIDInterface(); + } + + return 0; +} diff --git a/obs-studio/deps/libdshowcapture/src/external/capture-device-support/readme.md b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/readme.md new file mode 100644 index 0000000..acd2cdc --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/external/capture-device-support/readme.md @@ -0,0 +1,63 @@ +HID API for Elgato UVC devices +============================== + +The folder `Library` contains cross-platform code to access device-specific features of selected Elgato +video products. + +The CMake project in this folder builds a small console app for testing. +Modify `selectedDeviceID` in `SampleCode/main.cpp` to select the correct device type. + +Supported platforms +------------------- +* Windows (10 or higher) +* macOS + +Supported devices +----------------- +* HD60 S+ +* HD60 X + +Supported features +----------------- +* Switch on-device HDR tonemapping on/off +* Read HDMI HDR status packet (for HDR detection) + +Limitations +----------- +The library was written for macOS and Windows. +However, the sample project was only built with Visual Studio 2019 and tested on Windows so far. + +-------------------------------------------------------------------------------- + +Driver API for Elgato devices +============================= + +For non-UVC devices device properties can be accessed via a custom driver property set (`IKsPropertySet`). + +Sample code is provided, see `SampleCode/DriverInterface.h/.cpp` + +Supported platforms +------------------- +* Windows (10 or higher) + +Supported devices +----------------- +* 4K60 Pro MK.2 +* 4K60 S+ + +Supported features +------------------ +* Switch on-device HDR tonemapping on/off (4K60 Pro MK.2 only) +* Set video compression (4K60 S+ only) +* Read HDMI HDR status packet (for HDR detection) + +#### 4K60 Pro MK.2 + +To receive HDR the format on DirectShow filter pin must be set to P010. + +#### 4K60 S+ + +The 4K60 S+ always produces compressed video output. +For HDR the encoder format must be set to HEVC via the driver interface. + + diff --git a/obs-studio/deps/libdshowcapture/src/source/CoTaskMemPtr.hpp b/obs-studio/deps/libdshowcapture/src/source/CoTaskMemPtr.hpp new file mode 100644 index 0000000..db0c148 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/CoTaskMemPtr.hpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 Lain Bailey + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +template class CoTaskMemPtr { + T *ptr; + + inline void Clear() + { + if (ptr) + CoTaskMemFree(ptr); + } + +public: + inline CoTaskMemPtr() : ptr(NULL) {} + inline CoTaskMemPtr(T *ptr_) : ptr(ptr_) {} + inline ~CoTaskMemPtr() { Clear(); } + + inline operator T *() const { return ptr; } + inline T *operator->() const { return ptr; } + + inline CoTaskMemPtr &operator=(T *val) + { + Clear(); + ptr = val; + return *this; + } + + inline T **operator&() + { + Clear(); + ptr = NULL; + return &ptr; + } +}; diff --git a/obs-studio/deps/libdshowcapture/src/source/ComPtr.hpp b/obs-studio/deps/libdshowcapture/src/source/ComPtr.hpp new file mode 100644 index 0000000..9bddc20 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/ComPtr.hpp @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023 Lain Bailey + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +/* Oh no I have my own com pointer class, the world is ending, how dare you + * write your own! */ + +template class ComPtr { + +protected: + T *ptr; + + inline void Kill() + { + if (ptr) + ptr->Release(); + } + + inline void Replace(T *p) + { + if (ptr != p) { + if (p) + p->AddRef(); + if (ptr) + ptr->Release(); + ptr = p; + } + } + +public: + inline ComPtr() : ptr(nullptr) {} + inline ComPtr(T *p) : ptr(p) + { + if (ptr) + ptr->AddRef(); + } + inline ComPtr(const ComPtr &c) : ptr(c.ptr) + { + if (ptr) + ptr->AddRef(); + } + inline ComPtr(ComPtr &&c) noexcept : ptr(c.ptr) { c.ptr = nullptr; } + inline ~ComPtr() { Kill(); } + + inline void Clear() + { + if (ptr) { + ptr->Release(); + ptr = nullptr; + } + } + + inline ComPtr &operator=(T *p) + { + Replace(p); + return *this; + } + + inline ComPtr &operator=(const ComPtr &c) + { + Replace(c.ptr); + return *this; + } + + inline ComPtr &operator=(ComPtr &&c) noexcept + { + if (&ptr != &c.ptr) { + Kill(); + ptr = c.ptr; + c.ptr = nullptr; + } + + return *this; + } + + inline T *Detach() + { + T *out = ptr; + ptr = nullptr; + return out; + } + + inline void CopyTo(T **out) + { + if (out) { + if (ptr) + ptr->AddRef(); + *out = ptr; + } + } + + inline ULONG Release() + { + ULONG ref; + + if (!ptr) + return 0; + ref = ptr->Release(); + ptr = nullptr; + return ref; + } + + inline T **Assign() + { + Clear(); + return &ptr; + } + inline void Set(T *p) + { + Kill(); + ptr = p; + } + + inline T *Get() const { return ptr; } + + inline T **operator&() { return Assign(); } + + inline operator T *() const { return ptr; } + inline T *operator->() const { return ptr; } + + inline bool operator==(T *p) const { return ptr == p; } + inline bool operator!=(T *p) const { return ptr != p; } + + inline bool operator!() const { return !ptr; } +}; + +template class ComQIPtr : public ComPtr { + +public: + inline ComQIPtr(IUnknown *unk) + { + this->ptr = nullptr; + unk->QueryInterface(__uuidof(T), (void **)&this->ptr); + } + + inline ComPtr &operator=(IUnknown *unk) + { + ComPtr::Clear(); + unk->QueryInterface(__uuidof(T), (void **)&this->ptr); + return *this; + } +}; diff --git a/obs-studio/deps/libdshowcapture/src/source/avermedia-encode.h b/obs-studio/deps/libdshowcapture/src/source/avermedia-encode.h new file mode 100644 index 0000000..6ef436f --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/avermedia-encode.h @@ -0,0 +1,66 @@ +/* + * aver_prophwencode.h -- This header filer is provide to 3rd party AP + * to use the HW encode function of AVerMedia. + * + * Copyright (C) 2014 AVerMedia TECHNOLOGIES, Inc. + * + * Authors: Morris Pan, AVerMedia TECHNOLOGIES, Inc. + * + * This content is released under the MIT License + * (http://opensource.org/licenses/MIT). + * + */ +#pragma once + +static const GUID AVER_HW_ENCODE_PROPERTY = {0x1bd55918, + 0xbaf5, + 0x4781, + {0x8d, 0x76, 0xe0, 0xa0, 0xa5, + 0xe1, 0xd2, 0xb8}}; + +enum { + // @brief PropertySet Enumeration + // param AVER_PARAMETERS + PROPERTY_HW_ENCODE_PARAMETER = 0 +}; + +enum { + // property to set/get the encode frame rate + // ulParam1 = Frames per second + AVER_PARAMETER_ENCODE_FRAME_RATE = 0, + + // property to set/get the encode bit rate + // ulParam1 = Bitrate (kb/s) + AVER_PARAMETER_ENCODE_BIT_RATE = 1, + + // property to get the output resolution + // ulParam1 = Resolution width + // ulParam2 = Resolution height + AVER_PARAMETER_CURRENT_RESOLUTION = 2, + + // property to set the output resolution + // ulParam1 = Resolution width + // ulParam2 = Resolution height + AVER_PARAMETER_ENCODE_RESOLUTION = 3, + + // property to set/get the encode GOP + // ulParam1 = GOP length + AVER_PARAMETER_ENCODE_GOP = 4, + + // property to insert an I frame to the encoded stream + AVER_PARAMETER_INSERT_I_FRAME = 6 +}; + +struct AVER_PARAMETERS { + // @brief Use the PROPERTY_PARAMETER Property to Get or Set the + // Device Parameter. + // + // param ulIndex Parameter Index (AVER_PARAMETER_*) + // param ulParam1 Parameter 1 (if any) + // param ulParam2 Parameter 2 (if any) + // param ulParam3 Parameter 3 (if any) + ULONG ulIndex; + ULONG ulParam1; + ULONG ulParam2; + ULONG ulParam3; +}; diff --git a/obs-studio/deps/libdshowcapture/src/source/capture-filter.cpp b/obs-studio/deps/libdshowcapture/src/source/capture-filter.cpp new file mode 100644 index 0000000..7d8884f --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/capture-filter.cpp @@ -0,0 +1,696 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "capture-filter.hpp" +#include "log.hpp" + +namespace DShow { + +#if 0 +#define PrintFunc(x) Debug(x) +#else +#define PrintFunc(x) +#endif + +#define FILTER_NAME L"Capture Filter" +#define VIDEO_PIN_NAME L"Video Capture" +#define AUDIO_PIN_NAME L"Audio Capture" + +CapturePin::CapturePin(CaptureFilter *filter_, const PinCaptureInfo &info) + : refCount(0), captureInfo(info), filter(filter_) +{ + connectedMediaType->majortype = info.expectedMajorType; +} + +CapturePin::~CapturePin() {} + +STDMETHODIMP CapturePin::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else if (riid == IID_IPin) { + AddRef(); + *ppv = (IPin *)this; + } else if (riid == IID_IMemInputPin) { + AddRef(); + *ppv = (IMemInputPin *)this; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; +} + +STDMETHODIMP_(ULONG) CapturePin::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CapturePin::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IPin methods +STDMETHODIMP CapturePin::Connect(IPin *pReceivePin, const AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::Connect"); + + if (filter->state == State_Running) + return VFW_E_NOT_STOPPED; + + if (connectedPin) + return VFW_E_ALREADY_CONNECTED; + + if (!pmt) + return S_OK; + + if (pmt->majortype != GUID_NULL && + pmt->majortype != captureInfo.expectedMajorType) + return S_FALSE; + + if (pmt->majortype == captureInfo.expectedMajorType && + !IsValidMediaType(pmt)) + return S_FALSE; + + DSHOW_UNUSED(pReceivePin); + return S_OK; +} + +STDMETHODIMP CapturePin::ReceiveConnection(IPin *pConnector, + const AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::ReceiveConnection"); + + if (filter->state != State_Stopped) + return VFW_E_NOT_STOPPED; + if (!pConnector || !pmt) + return E_POINTER; + if (connectedPin) + return VFW_E_ALREADY_CONNECTED; + + if (QueryAccept(pmt) != S_OK) + return VFW_E_TYPE_NOT_ACCEPTED; + + connectedPin = pConnector; + connectedMediaType = pmt; + + return S_OK; +} + +STDMETHODIMP CapturePin::Disconnect() +{ + PrintFunc(L"CapturePin::Disconnect"); + + if (!connectedPin) + return S_FALSE; + + connectedPin = nullptr; + return S_OK; +} + +STDMETHODIMP CapturePin::ConnectedTo(IPin **pPin) +{ + PrintFunc(L"CapturePin::ConnectedTo"); + + if (!connectedPin) + return VFW_E_NOT_CONNECTED; + + IPin *pin = connectedPin; + pin->AddRef(); + *pPin = pin; + return S_OK; +} + +STDMETHODIMP CapturePin::ConnectionMediaType(AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::ConnectionMediaType"); + + if (!connectedPin) + return VFW_E_NOT_CONNECTED; + + return CopyMediaType(pmt, connectedMediaType); +} + +STDMETHODIMP CapturePin::QueryPinInfo(PIN_INFO *pInfo) +{ + PrintFunc(L"CapturePin::QueryPinInfo"); + + pInfo->pFilter = filter; + if (filter) { + IBaseFilter *ptr = filter; + ptr->AddRef(); + } + + if (captureInfo.expectedMajorType == MEDIATYPE_Video) + memcpy(pInfo->achName, VIDEO_PIN_NAME, sizeof(VIDEO_PIN_NAME)); + else + memcpy(pInfo->achName, AUDIO_PIN_NAME, sizeof(AUDIO_PIN_NAME)); + + pInfo->dir = PINDIR_INPUT; + + return NOERROR; +} + +STDMETHODIMP CapturePin::QueryDirection(PIN_DIRECTION *pPinDir) +{ + *pPinDir = PINDIR_INPUT; + return NOERROR; +} + +#define CAPTURE_PIN_NAME L"Capture Pin" + +STDMETHODIMP CapturePin::QueryId(LPWSTR *lpId) +{ + wchar_t *str = (wchar_t *)CoTaskMemAlloc(sizeof(CAPTURE_PIN_NAME)); + memcpy(str, CAPTURE_PIN_NAME, sizeof(CAPTURE_PIN_NAME)); + *lpId = str; + return S_OK; +} + +STDMETHODIMP CapturePin::QueryAccept(const AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::QueryAccept"); + + if (pmt->majortype != captureInfo.expectedMajorType) + return S_FALSE; + if (!IsValidMediaType(pmt)) + return S_FALSE; + + if (connectedPin) + connectedMediaType = pmt; + + return S_OK; +} + +STDMETHODIMP CapturePin::EnumMediaTypes(IEnumMediaTypes **ppEnum) +{ + PrintFunc(L"CapturePin::EnumMediaTypes"); + + *ppEnum = new CaptureEnumMediaTypes(this); + if (!*ppEnum) + return E_OUTOFMEMORY; + + return NOERROR; +} + +STDMETHODIMP CapturePin::QueryInternalConnections(IPin **apPin, ULONG *nPin) +{ + PrintFunc(L"CapturePin::QueryInternalConnections"); + + DSHOW_UNUSED(apPin); + DSHOW_UNUSED(nPin); + return E_NOTIMPL; +} + +STDMETHODIMP CapturePin::EndOfStream() +{ + PrintFunc(L"CapturePin::EndOfStream"); + + return S_OK; +} + +STDMETHODIMP CapturePin::BeginFlush() +{ + PrintFunc(L"CapturePin::BeginFlush"); + + flushing = true; + return S_OK; +} + +STDMETHODIMP CapturePin::EndFlush() +{ + PrintFunc(L"CapturePin::EndFlush"); + + flushing = false; + return S_OK; +} + +STDMETHODIMP CapturePin::NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, + double dRate) +{ + PrintFunc(L"CapturePin::NewSegment"); + + DSHOW_UNUSED(tStart); + DSHOW_UNUSED(tStop); + DSHOW_UNUSED(dRate); + return S_OK; +} + +// IMemInputPin methods +STDMETHODIMP CapturePin::GetAllocator(IMemAllocator **ppAllocator) +{ + PrintFunc(L"CapturePin::GetAllocator"); + + DSHOW_UNUSED(ppAllocator); + return VFW_E_NO_ALLOCATOR; +} + +STDMETHODIMP CapturePin::NotifyAllocator(IMemAllocator *pAllocator, + BOOL bReadOnly) +{ + PrintFunc(L"CapturePin::NotifyAllocator"); + + DSHOW_UNUSED(pAllocator); + DSHOW_UNUSED(bReadOnly); + return S_OK; +} + +STDMETHODIMP CapturePin::GetAllocatorRequirements(ALLOCATOR_PROPERTIES *pProps) +{ + PrintFunc(L"CapturePin::GetAllocatorRequirements"); + + DSHOW_UNUSED(pProps); + return E_NOTIMPL; +} + +STDMETHODIMP CapturePin::Receive(IMediaSample *pSample) +{ + PrintFunc(L"CapturePin::Receive"); + + if (flushing) + return S_FALSE; + + if (pSample) + captureInfo.callback(pSample); + + return S_OK; +} + +STDMETHODIMP CapturePin::ReceiveMultiple(IMediaSample **pSamples, long nSamples, + long *nSamplesProcessed) +{ + PrintFunc(L"CapturePin::ReceiveMultiple"); + + if (flushing) + return S_FALSE; + + for (long i = 0; i < nSamples; i++) + if (pSamples[i]) + captureInfo.callback(pSamples[i]); + + *nSamplesProcessed = nSamples; + + return S_OK; +} + +STDMETHODIMP CapturePin::ReceiveCanBlock() +{ + return S_FALSE; +} + +bool CapturePin::IsValidMediaType(const AM_MEDIA_TYPE *pmt) const +{ + if (pmt->pbFormat) { + if (pmt->subtype != captureInfo.expectedSubType || + pmt->majortype != captureInfo.expectedMajorType) + return false; + + if (captureInfo.expectedMajorType == MEDIATYPE_Video) { + const BITMAPINFOHEADER *bih = GetBitmapInfoHeader(*pmt); + if (!bih || bih->biHeight == 0 || bih->biWidth == 0) + return false; + } + } + + return true; +} + +// ============================================================================ + +class MiscFlagsHandler : public IAMFilterMiscFlags { + volatile long refCount = 0; + +public: + inline MiscFlagsHandler() {} + virtual ~MiscFlagsHandler() {} + + STDMETHODIMP QueryInterface(REFIID riid, void **ppv) + { + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; + } + + STDMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&refCount); + } + + STDMETHODIMP_(ULONG) Release() + { + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return refCount; + } + + STDMETHODIMP_(ULONG) GetMiscFlags() + { + return AM_FILTER_MISC_FLAGS_IS_RENDERER; + } +}; + +CaptureFilter::CaptureFilter(const PinCaptureInfo &info) + : refCount(0), + state(State_Stopped), + pin(new CapturePin(this, info)), + misc(new MiscFlagsHandler) +{ +} + +CaptureFilter::~CaptureFilter() {} + +// IUnknown methods +STDMETHODIMP CaptureFilter::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else if (riid == IID_IPersist) { + AddRef(); + *ppv = (IPersist *)this; + } else if (riid == IID_IMediaFilter) { + AddRef(); + *ppv = (IMediaFilter *)this; + } else if (riid == IID_IBaseFilter) { + AddRef(); + *ppv = (IBaseFilter *)this; + } else if (riid == IID_IAMFilterMiscFlags) { + misc.CopyTo((IAMFilterMiscFlags **)ppv); + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; +} + +STDMETHODIMP_(ULONG) CaptureFilter::AddRef() +{ + return InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CaptureFilter::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return refCount; +} + +// IPersist method +STDMETHODIMP CaptureFilter::GetClassID(CLSID *pClsID) +{ + DSHOW_UNUSED(pClsID); + return E_NOTIMPL; +} + +// IMediaFilter methods +STDMETHODIMP CaptureFilter::GetState(DWORD dwMSecs, FILTER_STATE *State) +{ + PrintFunc(L"CaptureFilter::GetState"); + + *State = state; + + DSHOW_UNUSED(dwMSecs); + return S_OK; +} + +STDMETHODIMP CaptureFilter::SetSyncSource(IReferenceClock *pClock) +{ + DSHOW_UNUSED(pClock); + return S_OK; +} + +STDMETHODIMP CaptureFilter::GetSyncSource(IReferenceClock **pClock) +{ + *pClock = nullptr; + return NOERROR; +} + +STDMETHODIMP CaptureFilter::Stop() +{ + PrintFunc(L"CaptureFilter::Stop"); + + state = State_Stopped; + return S_OK; +} + +STDMETHODIMP CaptureFilter::Pause() +{ + PrintFunc(L"CaptureFilter::Pause"); + + state = State_Paused; + return S_OK; +} + +STDMETHODIMP CaptureFilter::Run(REFERENCE_TIME tStart) +{ + PrintFunc(L"CaptureFilter::Run"); + + state = State_Running; + + DSHOW_UNUSED(tStart); + return S_OK; +} + +// IBaseFilter methods +STDMETHODIMP CaptureFilter::EnumPins(IEnumPins **ppEnum) +{ + PrintFunc(L"CaptureFilter::EnumPins"); + + *ppEnum = new CaptureEnumPins(this, nullptr); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +STDMETHODIMP CaptureFilter::FindPin(LPCWSTR Id, IPin **ppPin) +{ + PrintFunc(L"CaptureFilter::FindPin"); + + if (Id == nullptr || ppPin == nullptr) + return E_POINTER; + + if (lstrcmpW(Id, CAPTURE_PIN_NAME) == 0) { + *ppPin = pin; + pin->AddRef(); + return S_OK; + } else { + *ppPin = nullptr; + return VFW_E_NOT_FOUND; + } +} + +STDMETHODIMP CaptureFilter::QueryFilterInfo(FILTER_INFO *pInfo) +{ + PrintFunc(L"CaptureFilter::QueryFilterInfo"); + + memcpy(pInfo->achName, FILTER_NAME, sizeof(FILTER_NAME)); + + pInfo->pGraph = graph; + if (graph) { + IFilterGraph *graph_ptr = graph; + graph_ptr->AddRef(); + } + return NOERROR; +} + +STDMETHODIMP CaptureFilter::JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName) +{ + DSHOW_UNUSED(pName); + + graph = pGraph; + return NOERROR; +} + +STDMETHODIMP CaptureFilter::QueryVendorInfo(LPWSTR *pVendorInfo) +{ + DSHOW_UNUSED(pVendorInfo); + return E_NOTIMPL; +} + +// ============================================================================ + +CaptureEnumPins::CaptureEnumPins(CaptureFilter *filter_, CaptureEnumPins *pEnum) + : filter(filter_) +{ + curPin = (pEnum != nullptr) ? pEnum->curPin : 0; +} + +CaptureEnumPins::~CaptureEnumPins() {} + +// IUnknown +STDMETHODIMP CaptureEnumPins::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown || riid == IID_IEnumPins) { + AddRef(); + *ppv = (IEnumPins *)this; + return NOERROR; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } +} + +STDMETHODIMP_(ULONG) CaptureEnumPins::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CaptureEnumPins::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IEnumPins +STDMETHODIMP CaptureEnumPins::Next(ULONG cPins, IPin **ppPins, ULONG *pcFetched) +{ + UINT nFetched = 0; + + if (curPin == 0 && cPins > 0) { + IPin *pPin = filter->GetPin(); + + *ppPins = pPin; + pPin->AddRef(); + + nFetched = 1; + curPin++; + } + + if (pcFetched) + *pcFetched = nFetched; + + return (nFetched == cPins) ? S_OK : S_FALSE; +} + +STDMETHODIMP CaptureEnumPins::Skip(ULONG cPins) +{ + return ((curPin += cPins) > 1) ? S_FALSE : S_OK; +} + +STDMETHODIMP CaptureEnumPins::Reset() +{ + curPin = 0; + return S_OK; +} + +STDMETHODIMP CaptureEnumPins::Clone(IEnumPins **ppEnum) +{ + *ppEnum = new CaptureEnumPins(filter, this); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +// ============================================================================ + +CaptureEnumMediaTypes::CaptureEnumMediaTypes(CapturePin *pin_) : pin(pin_) {} + +CaptureEnumMediaTypes::~CaptureEnumMediaTypes() {} + +STDMETHODIMP CaptureEnumMediaTypes::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown || riid == IID_IEnumMediaTypes) { + AddRef(); + *ppv = this; + return NOERROR; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } +} + +STDMETHODIMP_(ULONG) CaptureEnumMediaTypes::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CaptureEnumMediaTypes::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IEnumMediaTypes +STDMETHODIMP CaptureEnumMediaTypes::Next(ULONG cMediaTypes, + AM_MEDIA_TYPE **ppMediaTypes, + ULONG *pcFetched) +{ + PrintFunc(L"CaptureEnumMediaTypes::Next"); + + UINT nFetched = 0; + + if (curMT == 0 && cMediaTypes > 0) { + *ppMediaTypes = pin->connectedMediaType.Duplicate(); + nFetched = 1; + curMT++; + } + + if (pcFetched) + *pcFetched = nFetched; + + return (nFetched == cMediaTypes) ? S_OK : S_FALSE; +} + +STDMETHODIMP CaptureEnumMediaTypes::Skip(ULONG cMediaTypes) +{ + PrintFunc(L"CaptureEnumMediaTypes::Skip"); + + return ((curMT += cMediaTypes) > 1) ? S_FALSE : S_OK; +} + +STDMETHODIMP CaptureEnumMediaTypes::Reset() +{ + PrintFunc(L"CaptureEnumMediaTypes::Reset"); + + curMT = 0; + return S_OK; +} + +STDMETHODIMP CaptureEnumMediaTypes::Clone(IEnumMediaTypes **ppEnum) +{ + *ppEnum = new CaptureEnumMediaTypes(pin); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/capture-filter.hpp b/obs-studio/deps/libdshowcapture/src/source/capture-filter.hpp new file mode 100644 index 0000000..858a147 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/capture-filter.hpp @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "dshow-base.hpp" +#include "dshow-media-type.hpp" +#include "../dshowcapture.hpp" + +namespace DShow { + +class CaptureFilter; +class CaptureSource; + +typedef void (*CaptureCallback)(void *param, IMediaSample *sample); + +struct PinCaptureInfo { + std::function callback; + GUID expectedMajorType{}; + GUID expectedSubType{}; +}; + +class CapturePin : public IPin, public IMemInputPin { + friend class CaptureEnumMediaTypes; + + volatile long refCount; + + PinCaptureInfo captureInfo; + ComPtr connectedPin; + CaptureFilter *filter; + MediaType connectedMediaType; + volatile bool flushing = false; + + bool IsValidMediaType(const AM_MEDIA_TYPE *pmt) const; + +public: + CapturePin(CaptureFilter *filter, const PinCaptureInfo &info); + virtual ~CapturePin(); + + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IPin methods + STDMETHODIMP Connect(IPin *pReceivePin, const AM_MEDIA_TYPE *pmt); + STDMETHODIMP ReceiveConnection(IPin *connector, + const AM_MEDIA_TYPE *pmt); + STDMETHODIMP Disconnect(); + STDMETHODIMP ConnectedTo(IPin **pPin); + STDMETHODIMP ConnectionMediaType(AM_MEDIA_TYPE *pmt); + STDMETHODIMP QueryPinInfo(PIN_INFO *pInfo); + STDMETHODIMP QueryDirection(PIN_DIRECTION *pPinDir); + STDMETHODIMP QueryId(LPWSTR *lpId); + STDMETHODIMP QueryAccept(const AM_MEDIA_TYPE *pmt); + STDMETHODIMP EnumMediaTypes(IEnumMediaTypes **ppEnum); + STDMETHODIMP QueryInternalConnections(IPin **apPin, ULONG *nPin); + STDMETHODIMP EndOfStream(); + + STDMETHODIMP BeginFlush(); + STDMETHODIMP EndFlush(); + STDMETHODIMP NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, + double dRate); + + // IMemInputPin methods + STDMETHODIMP GetAllocator(IMemAllocator **ppAllocator); + STDMETHODIMP NotifyAllocator(IMemAllocator *pAllocator, BOOL bReadOnly); + STDMETHODIMP GetAllocatorRequirements(ALLOCATOR_PROPERTIES *pProps); + STDMETHODIMP Receive(IMediaSample *pSample); + STDMETHODIMP ReceiveMultiple(IMediaSample **pSamples, long nSamples, + long *nSamplesProcessed); + STDMETHODIMP ReceiveCanBlock(); +}; + +class CaptureFilter : public IBaseFilter { + friend class CapturePin; + + volatile long refCount; + FILTER_STATE state; + ComPtr graph; + ComPtr pin; + + ComPtr misc; + +public: + CaptureFilter(const PinCaptureInfo &info); + virtual ~CaptureFilter(); + + // IUnknown methods + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IPersist method + STDMETHODIMP GetClassID(CLSID *pClsID); + + // IMediaFilter methods + STDMETHODIMP GetState(DWORD dwMSecs, FILTER_STATE *State); + STDMETHODIMP SetSyncSource(IReferenceClock *pClock); + STDMETHODIMP GetSyncSource(IReferenceClock **pClock); + STDMETHODIMP Stop(); + STDMETHODIMP Pause(); + STDMETHODIMP Run(REFERENCE_TIME tStart); + + // IBaseFilter methods + STDMETHODIMP EnumPins(IEnumPins **ppEnum); + STDMETHODIMP FindPin(LPCWSTR Id, IPin **ppPin); + STDMETHODIMP QueryFilterInfo(FILTER_INFO *pInfo); + STDMETHODIMP JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName); + STDMETHODIMP QueryVendorInfo(LPWSTR *pVendorInfo); + + inline CapturePin *GetPin() const { return (CapturePin *)pin; } +}; + +class CaptureEnumPins : public IEnumPins { + volatile long refCount = 1; + ComPtr filter; + UINT curPin; + +public: + CaptureEnumPins(CaptureFilter *filter, CaptureEnumPins *pEnum); + virtual ~CaptureEnumPins(); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IEnumPins + STDMETHODIMP Next(ULONG cPins, IPin **ppPins, ULONG *pcFetched); + STDMETHODIMP Skip(ULONG cPins); + STDMETHODIMP Reset(); + STDMETHODIMP Clone(IEnumPins **ppEnum); +}; + +class CaptureEnumMediaTypes : public IEnumMediaTypes { + volatile long refCount = 1; + ComPtr pin; + UINT curMT = 0; + +public: + CaptureEnumMediaTypes(CapturePin *pin); + virtual ~CaptureEnumMediaTypes(); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IEnumMediaTypes + STDMETHODIMP Next(ULONG cMediaTypes, AM_MEDIA_TYPE **ppMediaTypes, + ULONG *pcFetched); + STDMETHODIMP Skip(ULONG cMediaTypes); + STDMETHODIMP Reset(); + STDMETHODIMP Clone(IEnumMediaTypes **ppEnum); +}; + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/device-vendor.cpp b/obs-studio/deps/libdshowcapture/src/source/device-vendor.cpp new file mode 100644 index 0000000..a842c4f --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/device-vendor.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include +#include "../external/capture-device-support/SampleCode/DriverInterface.h" +#include "device.hpp" +#include "log.hpp" + +#include + +#include + +namespace DShow { + +bool IsVendorVideoHDR(IKsPropertySet *propertySet) +{ + EGAVDeviceProperties properties( + propertySet, EGAVDeviceProperties::DeviceType::GC4K60SPlus); + bool isHDR = false; + return SUCCEEDED(properties.IsVideoHDR(isHDR)) ? isHDR : false; +} + +void SetVendorVideoFormat(IKsPropertySet *propertySet, bool hevcTrueAvcFalse) +{ + EGAVDeviceProperties properties( + propertySet, EGAVDeviceProperties::DeviceType::GC4K60SPlus); + const HRESULT hr = properties.SetEncoderType(hevcTrueAvcFalse); + if (SUCCEEDED(hr)) { + Info(L"Elgato GC4K60SPlus encoder type=%ls", + hevcTrueAvcFalse ? L"HEVC" : L"AVC"); + } +} + +static void SetTonemapperAvermedia(IKsPropertySet *propertySet, bool enable) +{ + typedef struct _KSPROPERTY_AVER_HW_HDR2SDR { + KSPROPERTY Property; + DWORD Enable; + } KSPROPERTY_AVER_HW_HDR2SDR, *PKSPROPERTY_AVER_HW_HDR2SDR; + + static constexpr GUID KSPROPSETID_AVER_HDR_PROPERTY = { + 0x8A80D56F, + 0xFAC5, + 0x4692, + { + 0xA4, + 0x16, + 0xCF, + 0x20, + 0xD4, + 0xA1, + 0x8F, + 0x47, + }, + }; + KSPROPERTY_AVER_HW_HDR2SDR data{}; + data.Enable = enable; + const HRESULT hr = propertySet->Set( + KSPROPSETID_AVER_HDR_PROPERTY, 2, &data.Enable, + sizeof(data) - sizeof(data.Property), &data, sizeof(data)); + if (SUCCEEDED(hr)) + Info(L"AVerMedia tonemapper enable=%lu", data.Enable); +} + +/* Special thanks to AVerMedia development team */ +static bool FindExtensionUnitNodeID(DWORD *pnNode, IBaseFilter *spCapture) +{ + bool bFindNode = false; + if (!spCapture) + return bFindNode; + + ComPtr spKsTopologyInfo = NULL; + HRESULT hr = spCapture->QueryInterface(IID_PPV_ARGS(&spKsTopologyInfo)); + if (spKsTopologyInfo == NULL || FAILED(hr)) + return bFindNode; + + DWORD nNodeNum = 0; + hr = spKsTopologyInfo->get_NumNodes(&nNodeNum); + if (FAILED(hr) || nNodeNum <= 0) + return bFindNode; + + GUID guidNodeType; + for (int i = 0; i < nNodeNum; i++) { + spKsTopologyInfo->get_NodeType(i, &guidNodeType); + if (IsEqualGUID(guidNodeType, KSNODETYPE_DEV_SPECIFIC)) { + *pnNode = i; + bFindNode = true; + } + } + + return bFindNode; +} + +static bool SetTonemapperAvermedia2(IBaseFilter *filter, bool enable) +{ + static constexpr GUID GUID_GC553 = { + 0xC835261B, + 0xFF1C, + 0x4C9A, + { + 0xB2, + 0xF7, + 0x93, + 0xC9, + 0x1F, + 0xCF, + 0xBE, + 0x77, + }, + }; + + static constexpr int nId = 11; + + ComPtr spKsControl; + HRESULT hr = filter->QueryInterface(IID_PPV_ARGS(&spKsControl)); + if (spKsControl == NULL || FAILED(hr)) + return false; + + KSP_NODE ExtensionProp{}; + if (!FindExtensionUnitNodeID(&ExtensionProp.NodeId, filter)) + return false; + + ExtensionProp.Property.Set = GUID_GC553; + ExtensionProp.Property.Id = nId; + ExtensionProp.Property.Flags = KSPROPERTY_TYPE_GET | + KSPROPERTY_TYPE_TOPOLOGY; + + char pData[20]; + + ULONG ulBytesReturned; + hr = spKsControl->KsProperty(&ExtensionProp.Property, + sizeof(ExtensionProp), pData, + sizeof(pData), &ulBytesReturned); + if (FAILED(hr) || (ulBytesReturned < 18)) + return false; + + pData[15] = 0x02; + pData[17] = enable; + + ExtensionProp.Property.Flags = KSPROPERTY_TYPE_SET | + KSPROPERTY_TYPE_TOPOLOGY; + hr = spKsControl->KsProperty(&ExtensionProp.Property, + sizeof(ExtensionProp), pData, + sizeof(pData), &ulBytesReturned); + const bool succeeded = SUCCEEDED(hr); + if (succeeded) + Info(L"AVerMedia GC553 tonemapper enable=%d", (int)enable); + + return succeeded; +} + +static void SetTonemapperElgato(IKsPropertySet *propertySet, bool enable) +{ + EGAVDeviceProperties properties( + propertySet, EGAVDeviceProperties::DeviceType::GC4K60ProMK2); + const HRESULT hr = properties.SetHDRTonemapping(enable); + if (SUCCEEDED(hr)) { + Info(L"Elgato GC4K60ProMK2 tonemapper enable=%d", (int)enable); + } else { + for (const EGAVDeviceID &deviceID : GetElgatoUVCDeviceIDs()) { + std::shared_ptr hid = + CreateEGAVHIDInterface(); + if (hid->InitHIDInterface(deviceID).Succeeded()) { + ElgatoUVCDevice device( + hid, IsNewDeviceType(deviceID)); + device.SetHDRTonemappingEnabled(enable); + Info(L"Elgato UVC device (PID = 0x%04X) tonemapper enable=%d", + deviceID.productID, (int)enable); + hid->DeinitHIDInterface(); + } + } + } +} + +void SetVendorTonemapperUsage(IBaseFilter *filter, bool enable) +{ + if (filter) { + ComPtr propertySet = + ComQIPtr(filter); + if (propertySet) { + SetTonemapperAvermedia(propertySet, enable); + SetTonemapperAvermedia2(filter, enable); + SetTonemapperElgato(propertySet, enable); + } + } +} + +} /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/device.cpp b/obs-studio/deps/libdshowcapture/src/source/device.cpp new file mode 100644 index 0000000..644548e --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/device.cpp @@ -0,0 +1,875 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "device.hpp" +#include "dshow-device-defs.hpp" +#include "dshow-media-type.hpp" +#include "dshow-formats.hpp" +#include "dshow-enum.hpp" +#include "log.hpp" + +#define ROCKET_WAIT_TIME_MS 5000 + +namespace DShow { + +/* device-vendor.cpp API */ +extern bool IsVendorVideoHDR(IKsPropertySet *propertySet); +extern void SetVendorVideoFormat(IKsPropertySet *propertySet, + bool hevcTrueAvcFalse); +extern void SetVendorTonemapperUsage(IBaseFilter *filter, bool enable); + +bool SetRocketEnabled(IBaseFilter *encoder, bool enable); + +HDevice::HDevice() : initialized(false), active(false) {} + +HDevice::~HDevice() +{ + if (active) + Stop(); + + DisconnectFilters(); + + /* + * the sleeps for the rocket are required. It seems that you cannot + * simply start/stop the stream right away after/before you enable or + * disable the rocket. If you start it too fast after enabling, it + * won't return any data. If you try to turn off the rocket too + * quickly after stopping, then it'll be perpetually stuck on, and then + * you'll have to unplug/replug the device to get it working again. + */ + if (!!rocketEncoder) { + Sleep(ROCKET_WAIT_TIME_MS); + SetRocketEnabled(rocketEncoder, false); + } +} + +bool HDevice::EnsureInitialized(const wchar_t *func) +{ + if (!initialized) { + Error(L"%s: context not initialized", func); + return false; + } + + return true; +} + +bool HDevice::EnsureActive(const wchar_t *func) +{ + if (!active) { + Error(L"%s: cannot be used while inactive", func); + return false; + } + + return true; +} + +bool HDevice::EnsureInactive(const wchar_t *func) +{ + if (active) { + Error(L"%s: cannot be used while active", func); + return false; + } + + return true; +} + +inline void HDevice::SendToCallback(bool video, unsigned char *data, + size_t size, long long startTime, + long long stopTime, long rotation) +{ + if (!size) + return; + + if (video) + videoConfig.callback(videoConfig, data, size, startTime, + stopTime, rotation); + else + audioConfig.callback(audioConfig, data, size, startTime, + stopTime); +} + +void HDevice::Receive(bool isVideo, IMediaSample *sample) +{ + BYTE *ptr; + MediaTypePtr mt; + long roll = 0; + bool encoded = isVideo ? ((int)videoConfig.format >= 400) + : ((int)audioConfig.format >= 200); + + if (!sample) + return; + + if (isVideo ? !videoConfig.callback : !audioConfig.callback) + return; + + if (reactivatePending) + return; + + /* auto-rotation for devices such as streamcam */ + if (isVideo && rotatableDevice) { + ComQIPtr cc(videoFilter); + if (cc) { + long ccf = 0; + cc->Get(CameraControl_Roll, &roll, &ccf); + } + } + + if (isVideo && videoConfig.reactivateCallback) { + ComQIPtr propertySet(videoFilter); + if (propertySet) { + const bool hdr = IsVendorVideoHDR(propertySet); + if (deviceHdrSignal != hdr) { + deviceHdrSignal = hdr; +#ifdef ENABLE_HEVC + SetVendorVideoFormat(propertySet, hdr); +#endif + videoConfig.reactivateCallback(); + reactivatePending = true; + return; + } + } + } + + if (sample->GetMediaType(&mt) == S_OK) { + if (isVideo) { + videoMediaType = mt; + ConvertVideoSettings(); + } else { + audioMediaType = mt; + ConvertAudioSettings(); + } + } + + int size = sample->GetActualDataLength(); + if (!size) + return; + + if (FAILED(sample->GetPointer(&ptr))) + return; + + long long startTime, stopTime; + bool hasTime = SUCCEEDED(sample->GetTime(&startTime, &stopTime)); + + if (encoded) { + EncodedData &data = isVideo ? encodedVideo : encodedAudio; + + /* packets that have time are the first packet in a group of + * segments */ + if (hasTime) { + SendToCallback(isVideo, data.bytes.data(), + data.bytes.size(), data.lastStartTime, + data.lastStopTime, roll); + + data.bytes.resize(0); + data.lastStartTime = startTime; + data.lastStopTime = stopTime; + } + + data.bytes.insert(data.bytes.end(), (unsigned char *)ptr, + (unsigned char *)ptr + size); + + } else if (hasTime) { + SendToCallback(isVideo, ptr, size, startTime, stopTime, roll); + } +} + +void HDevice::ConvertVideoSettings() +{ + VIDEOINFOHEADER *vih = (VIDEOINFOHEADER *)videoMediaType->pbFormat; + BITMAPINFOHEADER *bmih = GetBitmapInfoHeader(videoMediaType); + + if (bmih) { + Debug(L"Video media type changed"); + + videoConfig.cx = bmih->biWidth; + videoConfig.cy_abs = labs(bmih->biHeight); + videoConfig.cy_flip = bmih->biHeight < 0; + videoConfig.frameInterval = vih->AvgTimePerFrame; + + bool same = videoConfig.internalFormat == videoConfig.format; + GetMediaTypeVFormat(videoMediaType, videoConfig.internalFormat); + + if (same) + videoConfig.format = videoConfig.internalFormat; + } +} + +void HDevice::ConvertAudioSettings() +{ + WAVEFORMATEX *wfex = + reinterpret_cast(audioMediaType->pbFormat); + + Debug(L"Audio media type changed"); + + audioConfig.sampleRate = wfex->nSamplesPerSec; + audioConfig.channels = wfex->nChannels; + + if (wfex->wFormatTag == WAVE_FORMAT_RAW_AAC1) + audioConfig.format = AudioFormat::AAC; + else if (wfex->wFormatTag == WAVE_FORMAT_DVM) + audioConfig.format = AudioFormat::AC3; + else if (wfex->wFormatTag == WAVE_FORMAT_MPEG) + audioConfig.format = AudioFormat::MPGA; + else if (wfex->wBitsPerSample == 16) + audioConfig.format = AudioFormat::Wave16bit; + else if (wfex->wBitsPerSample == 32) + audioConfig.format = AudioFormat::WaveFloat; + else + audioConfig.format = AudioFormat::Unknown; +} + +#define HD_PVR1_NAME L"Hauppauge HD PVR Capture" + +bool HDevice::SetupExceptionVideoCapture(IBaseFilter *filter, + VideoConfig &config) +{ + ComPtr pin; + + if (GetPinByName(filter, PINDIR_OUTPUT, L"656", &pin)) + return SetupEncodedVideoCapture(filter, config, HD_PVR2); + + else if (GetPinByName(filter, PINDIR_OUTPUT, L"TS Out", &pin)) + return SetupEncodedVideoCapture(filter, config, Roxio); + + return false; +} + +static bool GetPinMediaType(IPin *pin, MediaType &mt) +{ + ComPtr mediaTypes; + + if (SUCCEEDED(pin->EnumMediaTypes(&mediaTypes))) { + MediaTypePtr curMT; + ULONG count = 0; + + while (mediaTypes->Next(1, &curMT, &count) == S_OK) { + if (curMT->formattype == FORMAT_VideoInfo) { + mt = curMT; + return true; + } + } + } + + return false; +} + +bool HDevice::SetupVideoCapture(IBaseFilter *filter, VideoConfig &config) +{ + ComPtr pin; + HRESULT hr; + bool success; + + if (config.name.find(L"C875") != std::string::npos || + config.name.find(L"Prif Streambox") != std::string::npos || + config.name.find(L"C835") != std::string::npos) + return SetupEncodedVideoCapture(filter, config, AV_LGP); + + else if (config.name.find(L"IT9910") != std::string::npos) + return SetupEncodedVideoCapture(filter, config, HD_PVR_Rocket); + + else if (config.name.find(HD_PVR1_NAME) != std::string::npos) + return SetupEncodedVideoCapture(filter, config, HD_PVR1); + + rotatableDevice = videoConfig.name.find(L"StreamCam") != + std::string::npos; + + success = GetFilterPin(filter, MEDIATYPE_Video, PIN_CATEGORY_CAPTURE, + PINDIR_OUTPUT, &pin); + if (!success) { + if (SetupExceptionVideoCapture(filter, config)) { + return true; + } else { + Error(L"Could not get video pin"); + return false; + } + } + + ComQIPtr pinConfig(pin); + if (pinConfig == NULL) { + Error(L"Could not get IAMStreamConfig for device"); + return false; + } + + if (config.useDefaultConfig) { + MediaTypePtr defaultMT; + + hr = pinConfig->GetFormat(&defaultMT); + if (hr == E_NOTIMPL) { + if (!GetPinMediaType(pin, videoMediaType)) { + Error(L"Couldn't get pin media type"); + return false; + } + + } else if (FAILED(hr)) { + ErrorHR(L"Could not get default format for video", hr); + return false; + + } else { + videoMediaType = defaultMT; + } + + ConvertVideoSettings(); + + config.format = config.internalFormat = VideoFormat::Any; + } + + if (!GetClosestVideoMediaType(filter, config, videoMediaType)) { + Error(L"Could not get closest video media type"); + return false; + } + + hr = pinConfig->SetFormat(videoMediaType); + if (FAILED(hr) && hr != E_NOTIMPL) { + ErrorHR(L"Could not set video format", hr); + return false; + } + + ConvertVideoSettings(); + + PinCaptureInfo info; + info.callback = [this](IMediaSample *s) { Receive(true, s); }; + info.expectedMajorType = videoMediaType->majortype; + + /* attempt to force intermediary filters for these types */ + if (videoConfig.format == VideoFormat::XRGB) + info.expectedSubType = MEDIASUBTYPE_RGB32; + else if (videoConfig.format == VideoFormat::ARGB) + info.expectedSubType = MEDIASUBTYPE_ARGB32; + else if (videoConfig.format == VideoFormat::RGB24) + info.expectedSubType = MEDIASUBTYPE_RGB24; + else if (videoConfig.format == VideoFormat::YVYU) + info.expectedSubType = MEDIASUBTYPE_YVYU; + else if (videoConfig.format == VideoFormat::YUY2) + info.expectedSubType = MEDIASUBTYPE_YUY2; + else if (videoConfig.format == VideoFormat::UYVY) + info.expectedSubType = MEDIASUBTYPE_UYVY; + else + info.expectedSubType = videoMediaType->subtype; + + videoCapture = new CaptureFilter(info); + videoFilter = filter; + + graph->AddFilter(videoCapture, L"Video Capture Filter"); + graph->AddFilter(videoFilter, L"Video Filter"); + return true; +} + +bool HDevice::SetVideoConfig(VideoConfig *config) +{ + ComPtr filter; + + if (!EnsureInitialized(L"SetVideoConfig") || + !EnsureInactive(L"SetVideoConfig")) + return false; + + videoMediaType = NULL; + graph->RemoveFilter(videoFilter); + graph->RemoveFilter(videoCapture); + videoFilter.Release(); + videoCapture.Release(); + + if (!config) + return true; + + if (config->name.empty() && config->path.empty()) { + Error(L"No video device name or path specified"); + return false; + } + + bool success = GetDeviceFilter(CLSID_VideoInputDeviceCategory, + config->name.c_str(), + config->path.c_str(), &filter); + if (!success) { + Error(L"Video device '%s': %s not found", config->name.c_str(), + config->path.c_str()); + return false; + } + + if (filter == NULL) { + Error(L"Could not get video filter"); + return false; + } + + deviceHdrSignal = false; + reactivatePending = false; + + ComPtr propertySet = ComQIPtr(filter); + if (propertySet) { + const bool hdr = IsVendorVideoHDR(propertySet); +#ifdef ENABLE_HEVC + SetVendorVideoFormat(propertySet, hdr); +#endif + deviceHdrSignal = hdr; + } + + videoConfig = *config; + + if (!SetupVideoCapture(filter, videoConfig)) + return false; + + *config = videoConfig; + return true; +} + +bool HDevice::SetupExceptionAudioCapture(IPin *pin) +{ + ComPtr enumMediaTypes; + ULONG count = 0; + HRESULT hr; + MediaTypePtr mt; + + hr = pin->EnumMediaTypes(&enumMediaTypes); + if (FAILED(hr)) { + WarningHR(L"SetupExceptionAudioCapture: pin->EnumMediaTypes " + L"failed", + hr); + return false; + } + + enumMediaTypes->Reset(); + + if (enumMediaTypes->Next(1, &mt, &count) == S_OK && + mt->formattype == FORMAT_WaveFormatEx) { + audioMediaType = mt; + return true; + } + + return false; +} + +static bool is24BitAudio(AM_MEDIA_TYPE *mt) +{ + if (mt->formattype == FORMAT_WaveFormatEx) { + WAVEFORMATEX *wfex = (WAVEFORMATEX *)mt->pbFormat; + return wfex->wBitsPerSample == 24; + } + + return false; +} + +bool HDevice::SetupAudioCapture(IBaseFilter *filter, AudioConfig &config) +{ + ComPtr pin; + MediaTypePtr defaultMT; + bool success; + HRESULT hr; + + success = GetFilterPin(filter, MEDIATYPE_Audio, PIN_CATEGORY_CAPTURE, + PINDIR_OUTPUT, &pin); + if (!success) { + Error(L"Could not get audio pin"); + return false; + } + + ComQIPtr pinConfig(pin); + + if (config.useDefaultConfig) { + MediaTypePtr defaultMT; + + if (pinConfig && SUCCEEDED(pinConfig->GetFormat(&defaultMT))) { + if (is24BitAudio(defaultMT)) { + WAVEFORMATEX *wfex = + (WAVEFORMATEX *)defaultMT->pbFormat; + config.sampleRate = wfex->nSamplesPerSec; + config.channels = wfex->nChannels; + config.format = AudioFormat::Wave16bit; + config.useDefaultConfig = false; + } else { + audioMediaType = defaultMT; + } + } else { + if (!SetupExceptionAudioCapture(pin)) { + Error(L"Could not get default format for " + L"audio pin"); + return false; + } + } + } + + if (!config.useDefaultConfig) { + if (!GetClosestAudioMediaType(filter, config, audioMediaType)) { + Error(L"Could not get closest audio media type"); + return false; + } + } + + if (!!pinConfig) { + hr = pinConfig->SetFormat(audioMediaType); + + if (FAILED(hr) && hr != E_NOTIMPL) { + Error(L"Could not set audio format"); + return false; + } + } + + ConvertAudioSettings(); + + PinCaptureInfo info; + info.callback = [this](IMediaSample *s) { Receive(false, s); }; + info.expectedMajorType = audioMediaType->majortype; + info.expectedSubType = audioMediaType->subtype; + + audioCapture = new CaptureFilter(info); + audioFilter = filter; + audioConfig = config; + + graph->AddFilter(audioCapture, L"Audio Capture Filter"); + if (!config.useVideoDevice) + graph->AddFilter(audioFilter, L"Audio Filter"); + return true; +} + +bool HDevice::SetupAudioOutput(IBaseFilter *filter, AudioConfig &config) +{ + ComPtr outputFilter; + const CLSID *clsID; + HRESULT hr; + + if (config.mode == AudioMode::WaveOut) { + clsID = &CLSID_AudioRender; + } else { + clsID = &CLSID_DSoundRender; + } + + hr = CoCreateInstance(*clsID, nullptr, CLSCTX_INPROC_SERVER, + IID_IBaseFilter, (void **)&outputFilter); + if (FAILED(hr)) { + ErrorHR(L"Failed to create audio sound output filter", hr); + return false; + } + + audioFilter = filter; + audioOutput = std::move(outputFilter); + + graph->AddFilter(audioOutput, L"Audio Output Filter"); + if (!config.useVideoDevice) + graph->AddFilter(audioFilter, L"Audio Filter"); + return true; +} + +bool HDevice::SetAudioConfig(AudioConfig *config) +{ + ComPtr filter; + + if (!EnsureInitialized(L"SetAudioConfig") || + !EnsureInactive(L"SetAudioConfig")) + return false; + + if (!audioConfig.useVideoDevice) + graph->RemoveFilter(audioFilter); + graph->RemoveFilter(audioCapture); + graph->RemoveFilter(audioOutput); + audioFilter.Release(); + audioCapture.Release(); + audioOutput.Release(); + audioMediaType = NULL; + + if (!config) + return true; + + if (!config->useVideoDevice && !config->useSeparateAudioFilter && + config->name.empty() && config->path.empty()) { + Error(L"No audio device name or path specified"); + return false; + } + + if (config->useVideoDevice) { + if (videoFilter == NULL) { + Error(L"Tried to use video device's built-in audio, " + L"but no video device is present"); + return false; + } + + filter = videoFilter; + } else if (config->useSeparateAudioFilter) { + bool success = + GetDeviceAudioFilter(videoConfig.path.c_str(), &filter); + if (!success) { + Error(L"Corresponding audio device for '%s' not found", + videoConfig.path.c_str()); + return false; + } + + } else { + bool success = GetDeviceFilter(CLSID_AudioInputDeviceCategory, + config->name.c_str(), + config->path.c_str(), &filter); + if (!success) { + Error(L"Audio device '%s': %s not found", + config->name.c_str(), config->path.c_str()); + return false; + } + } + + if (filter == NULL) + return false; + + audioConfig = *config; + + if (config->mode == AudioMode::Capture) { + if (!SetupAudioCapture(filter, audioConfig)) + return false; + + *config = audioConfig; + return true; + } + + return SetupAudioOutput(filter, audioConfig); +} + +bool HDevice::CreateGraph() +{ + if (initialized) { + Warning(L"Graph already created"); + return false; + } + + if (!CreateFilterGraph(&graph, &builder, &control)) + return false; + + initialized = true; + return true; +} + +bool HDevice::FindCrossbar(IBaseFilter *filter, IBaseFilter **crossbar) +{ + ComPtr pin; + REGPINMEDIUM medium; + HRESULT hr; + + hr = builder->FindInterface(NULL, NULL, filter, IID_IAMCrossbar, + (void **)crossbar); + if (SUCCEEDED(hr)) + return true; + + if (!GetPinByName(filter, PINDIR_INPUT, nullptr, &pin)) + return false; + if (!GetPinMedium(pin, medium)) + return false; + if (!GetFilterByMedium(AM_KSCATEGORY_CROSSBAR, medium, crossbar)) + return false; + + graph->AddFilter(*crossbar, L"Crossbar Filter"); + return true; +} + +bool HDevice::ConnectPins(const GUID &category, const GUID &type, + IBaseFilter *filter, IBaseFilter *capture) +{ + HRESULT hr; + ComPtr crossbar; + ComPtr filterPin; + ComPtr capturePin; + bool connectCrossbar = !encodedDevice && type == MEDIATYPE_Video; + + if (!EnsureInitialized(L"HDevice::ConnectPins") || + !EnsureInactive(L"HDevice::ConnectPins")) + return false; + + if (connectCrossbar && FindCrossbar(filter, &crossbar)) { + if (!DirectConnectFilters(graph, crossbar, filter)) { + Warning(L"HDevice::ConnectPins: Failed to connect " + L"crossbar"); + return false; + } + } + + if (!GetFilterPin(filter, type, category, PINDIR_OUTPUT, &filterPin)) { + Error(L"HDevice::ConnectPins: Failed to find pin"); + return false; + } + + if (!GetPinByName(capture, PINDIR_INPUT, nullptr, &capturePin)) { + Error(L"HDevice::ConnectPins: Failed to find capture pin"); + return false; + } + + hr = graph->ConnectDirect(filterPin, capturePin, nullptr); + if (FAILED(hr)) { + WarningHR(L"HDevice::ConnectPins: failed to connect pins", hr); + return false; + } + + return true; +} + +bool HDevice::RenderFilters(const GUID &category, const GUID &type, + IBaseFilter *filter, IBaseFilter *capture) +{ + HRESULT hr; + + if (!EnsureInitialized(L"HDevice::RenderFilters") || + !EnsureInactive(L"HDevice::RenderFilters")) + return false; + + hr = builder->RenderStream(&category, &type, filter, NULL, capture); + if (FAILED(hr)) { + WarningHR(L"HDevice::ConnectFilters: RenderStream failed", hr); + return false; + } + + return true; +} + +void HDevice::SetAudioBuffering(int bufferingMs) +{ + ComPtr pin; + bool success = GetFilterPin(audioFilter, MEDIATYPE_Audio, + PIN_CATEGORY_CAPTURE, PINDIR_OUTPUT, &pin); + if (!success) + return; + + ComQIPtr config(pin); + if (!config) + return; + + ComQIPtr neg(pin); + if (!neg) + return; + + MediaTypePtr mt; + if (FAILED(config->GetFormat(&mt))) + return; + + if (mt->formattype != FORMAT_WaveFormatEx) + return; + if (mt->cbFormat != sizeof(WAVEFORMATEX)) + return; + + WAVEFORMATEX *wfex = (WAVEFORMATEX *)mt->pbFormat; + + ALLOCATOR_PROPERTIES props; + props.cBuffers = -1; + props.cbBuffer = wfex->nAvgBytesPerSec * bufferingMs / 1000; + props.cbAlign = -1; + props.cbPrefix = -1; + HRESULT hr = neg->SuggestAllocatorProperties(&props); + if (FAILED(hr)) + WarningHR(L"Could not set allocator properties on audio " + L"capture pin", + hr); +} + +bool HDevice::ConnectFilters() +{ + bool success = true; + + if (!EnsureInitialized(L"ConnectFilters") || + !EnsureInactive(L"ConnectFilters")) + return false; + + if (videoCapture != NULL) { + /* use hardware tonemapper for narrow format (SDR), not wide (HDR) */ + const bool enable_tonemapper = videoConfig.format != + VideoFormat::P010; + SetVendorTonemapperUsage(videoFilter, enable_tonemapper); + + success = ConnectPins(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, + videoFilter, videoCapture); + if (!success) { + success = RenderFilters(PIN_CATEGORY_CAPTURE, + MEDIATYPE_Video, videoFilter, + videoCapture); + } + } + + if ((audioCapture || audioOutput) && success) { + IBaseFilter *filter = (audioCapture != nullptr) + ? audioCapture.Get() + : audioOutput.Get(); + + /* Stream engine has a bug where it will break if you try to + * set different audio buffering, so don't use audio buffering + * if using the stream engine's audio */ + bool streamEngine = audioConfig.useVideoDevice && + (videoConfig.name.find(L"Stream Engine") != + std::string::npos); + + if (!streamEngine && audioCapture != nullptr) + SetAudioBuffering(audioConfig.buffer ? audioConfig.buffer : 10); + + success = ConnectPins(PIN_CATEGORY_CAPTURE, MEDIATYPE_Audio, + audioFilter, filter); + if (!success) { + success = RenderFilters(PIN_CATEGORY_CAPTURE, + MEDIATYPE_Audio, audioFilter, + filter); + } + } + + if (success) + LogFilters(graph); + + return success; +} + +void HDevice::DisconnectFilters() +{ + ComPtr filterEnum; + HRESULT hr; + + if (!graph) + return; + + hr = graph->EnumFilters(&filterEnum); + if (FAILED(hr)) + return; + + ComPtr filter; + while (filterEnum->Next(1, &filter, nullptr) == S_OK) { + graph->RemoveFilter(filter); + filterEnum->Reset(); + } +} + +Result HDevice::Start() +{ + HRESULT hr; + + if (!EnsureInitialized(L"Start") || !EnsureInactive(L"Start")) + return Result::Error; + + if (!!rocketEncoder) + Sleep(ROCKET_WAIT_TIME_MS); + + hr = control->Run(); + + if (FAILED(hr)) { + if (hr == (HRESULT)0x8007001F) { + WarningHR(L"Run failed, device already in use", hr); + return Result::InUse; + } else { + WarningHR(L"Run failed", hr); + return Result::Error; + } + } + + active = true; + return Result::Success; +} + +void HDevice::Stop() +{ + if (active) { + control->Stop(); + active = false; + } +} + +} /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/device.hpp b/obs-studio/deps/libdshowcapture/src/source/device.hpp new file mode 100644 index 0000000..216b98a --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/device.hpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "../dshowcapture.hpp" +#include "capture-filter.hpp" + +#include +#include +using namespace std; + +namespace DShow { + +struct EncodedData { + long long lastStartTime = 0; + long long lastStopTime = 0; + vector bytes; +}; + +struct EncodedDevice { + VideoFormat videoFormat; + ULONG videoPacketID; + long width; + long height; + long long frameInterval; + + AudioFormat audioFormat; + ULONG audioPacketID; + DWORD samplesPerSec; +}; + +struct HDevice { + ComPtr graph; + ComPtr builder; + ComPtr control; + + ComPtr videoFilter; + ComPtr audioFilter; + ComPtr videoCapture; + ComPtr audioCapture; + ComPtr audioOutput; + ComPtr rocketEncoder; + MediaType videoMediaType; + MediaType audioMediaType; + VideoConfig videoConfig; + AudioConfig audioConfig; + + bool encodedDevice = false; + bool rotatableDevice = false; + bool deviceHdrSignal = false; + bool reactivatePending = false; + bool initialized; + bool active; + + EncodedData encodedVideo; + EncodedData encodedAudio; + + HDevice(); + ~HDevice(); + + void ConvertVideoSettings(); + void ConvertAudioSettings(); + + bool EnsureInitialized(const wchar_t *func); + bool EnsureActive(const wchar_t *func); + bool EnsureInactive(const wchar_t *func); + + inline void SendToCallback(bool video, unsigned char *data, size_t size, + long long startTime, long long stopTime, + long rotation); + + void Receive(bool video, IMediaSample *sample); + + bool SetupEncodedVideoCapture(IBaseFilter *filter, VideoConfig &config, + const EncodedDevice &info); + + bool SetupExceptionVideoCapture(IBaseFilter *filter, + VideoConfig &config); + + bool SetupExceptionAudioCapture(IPin *pin); + + bool SetupVideoCapture(IBaseFilter *filter, VideoConfig &config); + bool SetupAudioCapture(IBaseFilter *filter, AudioConfig &config); + + bool SetupAudioOutput(IBaseFilter *filter, AudioConfig &config); + + bool SetVideoConfig(VideoConfig *config); + bool SetAudioConfig(AudioConfig *config); + + bool CreateGraph(); + bool FindCrossbar(IBaseFilter *filter, IBaseFilter **crossbar); + bool ConnectPins(const GUID &category, const GUID &type, + IBaseFilter *filter, IBaseFilter *capture); + bool RenderFilters(const GUID &category, const GUID &type, + IBaseFilter *filter, IBaseFilter *capture); + void SetAudioBuffering(int bufferingMs); + bool ConnectFilters(); + void DisconnectFilters(); + Result Start(); + void Stop(); +}; + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-base.cpp b/obs-studio/deps/libdshowcapture/src/source/dshow-base.cpp new file mode 100644 index 0000000..0a223b7 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-base.cpp @@ -0,0 +1,1031 @@ +/* + * Copyright (C) 2014 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "dshow-base.hpp" +#include "dshow-enum.hpp" +#include "log.hpp" + +#include + +#include +#include + +#include // for DRV_QUERYDEVICEINTERFACE +#include // for SetupDixxx +#include // for CM_xxx +#include // for std::transform + +using namespace std; + +namespace DShow { + +bool CreateFilterGraph(IGraphBuilder **pgraph, ICaptureGraphBuilder2 **pbuilder, + IMediaControl **pcontrol) +{ + ComPtr graph; + ComPtr builder; + ComPtr control; + HRESULT hr; + + hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, + IID_IFilterGraph, (void **)&graph); + if (FAILED(hr)) { + ErrorHR(L"Failed to create IGraphBuilder", hr); + return false; + } + + hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, + CLSCTX_INPROC_SERVER, IID_ICaptureGraphBuilder2, + (void **)&builder); + if (FAILED(hr)) { + ErrorHR(L"Failed to create ICaptureGraphBuilder2", hr); + return false; + } + + hr = builder->SetFiltergraph(graph); + if (FAILED(hr)) { + ErrorHR(L"Failed to set filter graph", hr); + return false; + } + + hr = graph->QueryInterface(IID_IMediaControl, (void **)&control); + if (FAILED(hr)) { + ErrorHR(L"Failed to create IMediaControl", hr); + return false; + } + + *pgraph = graph.Detach(); + *pbuilder = builder.Detach(); + *pcontrol = control.Detach(); + return true; +} + +void LogFilters(IGraphBuilder *graph) +{ + ComPtr filterEnum; + ComPtr filter; + HRESULT hr; + + hr = graph->EnumFilters(&filterEnum); + if (FAILED(hr)) + return; + + Debug(L"Loaded filters:"); + + while (filterEnum->Next(1, &filter, NULL) == S_OK) { + FILTER_INFO filterInfo; + + hr = filter->QueryFilterInfo(&filterInfo); + if (SUCCEEDED(hr)) { + if (filterInfo.pGraph) + filterInfo.pGraph->Release(); + + Debug(L"\t%s", filterInfo.achName); + } + } +} + +struct DeviceFilterCallbackInfo { + ComPtr filter; + const wchar_t *name; + const wchar_t *path; +}; + +static bool GetDeviceCallback(DeviceFilterCallbackInfo &info, + IBaseFilter *filter, const wchar_t *name, + const wchar_t *path) +{ + if (info.name && *info.name && wcscmp(name, info.name) != 0) + return true; + + info.filter = filter; + + /* continue if path does not match */ + if (!path || !info.path || wcscmp(path, info.path) != 0) + return true; + + return false; +} + +bool GetDeviceFilter(const IID &type, const wchar_t *name, const wchar_t *path, + IBaseFilter **out) +{ + DeviceFilterCallbackInfo info; + info.name = name; + info.path = path; + + if (!EnumDevices(type, EnumDeviceCallback(GetDeviceCallback), &info)) + return false; + + if (info.filter != NULL) { + *out = info.filter.Detach(); + return true; + } + + return false; +} + +/* checks to see if a pin's config caps have a specific media type */ +static bool PinConfigHasMajorType(IPin *pin, const GUID &type) +{ + HRESULT hr; + ComPtr config; + int count, size; + + hr = pin->QueryInterface(IID_IAMStreamConfig, (void **)&config); + if (FAILED(hr)) + return false; + + hr = config->GetNumberOfCapabilities(&count, &size); + if (FAILED(hr)) + return false; + + vector caps; + caps.resize(size); + + for (int i = 0; i < count; i++) { + MediaTypePtr mt; + if (SUCCEEDED(config->GetStreamCaps(i, &mt, caps.data()))) + if (mt->majortype == type) + return true; + } + + return false; +} + +/* checks to see if a pin has a certain major media type */ +static bool PinHasMajorType(IPin *pin, const GUID &type) +{ + HRESULT hr; + MediaTypePtr mt; + ComPtr mediaEnum; + + /* first, check the config caps. */ + if (PinConfigHasMajorType(pin, type)) + return true; + + /* then let's check the media type for the pin */ + if (FAILED(pin->EnumMediaTypes(&mediaEnum))) + return false; + + ULONG curVal; + hr = mediaEnum->Next(1, &mt, &curVal); + if (hr != S_OK) + return false; + + return mt->majortype == type; +} + +static inline bool PinIsDirection(IPin *pin, PIN_DIRECTION dir) +{ + if (!pin) + return false; + + PIN_DIRECTION pinDir; + return SUCCEEDED(pin->QueryDirection(&pinDir)) && pinDir == dir; +} + +static HRESULT GetPinCategory(IPin *pin, GUID &category) +{ + if (!pin) + return E_POINTER; + + ComQIPtr propertySet(pin); + DWORD size; + + if (propertySet == NULL) + return E_NOINTERFACE; + + return propertySet->Get(AMPROPSETID_Pin, AMPROPERTY_PIN_CATEGORY, NULL, + 0, &category, sizeof(GUID), &size); +} + +static inline bool PinIsCategory(IPin *pin, const GUID &category) +{ + if (!pin) + return false; + + GUID pinCategory; + HRESULT hr = GetPinCategory(pin, pinCategory); + + /* if the pin has no category interface, chances are we created it */ + if (FAILED(hr)) + return (hr == E_NOINTERFACE); + + return category == pinCategory; +} + +static inline bool PinNameIs(IPin *pin, const wchar_t *name) +{ + if (!pin) + return false; + if (!name) + return true; + + PIN_INFO pinInfo; + + if (FAILED(pin->QueryPinInfo(&pinInfo))) + return false; + + if (pinInfo.pFilter) + pinInfo.pFilter->Release(); + + return wcscmp(name, pinInfo.achName) == 0; +} + +static inline bool PinMatches(IPin *pin, const GUID &type, const GUID &category, + PIN_DIRECTION &dir) +{ + if (!PinHasMajorType(pin, type)) + return false; + if (!PinIsDirection(pin, dir)) + return false; + if (!PinIsCategory(pin, category)) + return false; + + return true; +} + +bool GetFilterPin(IBaseFilter *filter, const GUID &type, const GUID &category, + PIN_DIRECTION dir, IPin **pin) +{ + ComPtr curPin; + ComPtr pinsEnum; + ULONG num; + + if (!filter) + return false; + if (FAILED(filter->EnumPins(&pinsEnum))) + return false; + + while (pinsEnum->Next(1, &curPin, &num) == S_OK) { + + if (PinMatches(curPin, type, category, dir)) { + *pin = curPin; + (*pin)->AddRef(); + return true; + } + } + + return false; +} + +bool GetPinByName(IBaseFilter *filter, PIN_DIRECTION dir, const wchar_t *name, + IPin **pin) +{ + ComPtr curPin; + ComPtr pinsEnum; + ULONG num; + + if (!filter) + return false; + if (FAILED(filter->EnumPins(&pinsEnum))) + return false; + + while (pinsEnum->Next(1, &curPin, &num) == S_OK) { + if (PinIsDirection(curPin, dir) && PinNameIs(curPin, name)) { + *pin = curPin.Detach(); + return true; + } + } + + return false; +} + +bool GetPinByMedium(IBaseFilter *filter, REGPINMEDIUM &medium, IPin **pin) +{ + ComPtr curPin; + ComPtr pinsEnum; + ULONG num; + + if (!filter) + return false; + if (FAILED(filter->EnumPins(&pinsEnum))) + return false; + + while (pinsEnum->Next(1, &curPin, &num) == S_OK) { + REGPINMEDIUM curMedium; + + if (GetPinMedium(curPin, curMedium) && + memcmp(&medium, &curMedium, sizeof(medium)) == 0) { + *pin = curPin.Detach(); + return true; + } + } + + return false; +} + +static bool GetFilterByMediumFromMoniker(IMoniker *moniker, + REGPINMEDIUM &medium, + IBaseFilter **filter) +{ + ComPtr curFilter; + HRESULT hr; + + hr = moniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, + (void **)&curFilter); + if (SUCCEEDED(hr)) { + ComPtr pin; + if (GetPinByMedium(curFilter, medium, &pin)) { + *filter = curFilter.Detach(); + return true; + } + } else { + WarningHR(L"GetFilterByMediumFromMoniker: BindToObject failed", + hr); + } + + return false; +} + +bool GetFilterByMedium(const CLSID &id, REGPINMEDIUM &medium, + IBaseFilter **filter) +{ + ComPtr deviceEnum; + ComPtr enumMoniker; + ComPtr moniker; + DWORD count = 0; + HRESULT hr; + + hr = CoCreateInstance(CLSID_SystemDeviceEnum, nullptr, + CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, + (void **)&deviceEnum); + if (FAILED(hr)) { + WarningHR(L"GetFilterByMedium: Failed to create device enum", + hr); + return false; + } + + hr = deviceEnum->CreateClassEnumerator(id, &enumMoniker, 0); + if (hr != S_OK) { + WarningHR(L"GetFilterByMedium: Failed to create enum moniker", + hr); + return false; + } + + enumMoniker->Reset(); + + while (enumMoniker->Next(1, &moniker, &count) == S_OK) { + if (GetFilterByMediumFromMoniker(moniker, medium, filter)) + return true; + } + + return false; +} + +bool GetPinMedium(IPin *pin, REGPINMEDIUM &medium) +{ + ComQIPtr ksPin(pin); + CoTaskMemPtr items; + + if (!ksPin) + return false; + + if (FAILED(ksPin->KsQueryMediums(&items))) + return false; + + REGPINMEDIUM *curMed = reinterpret_cast(items + 1); + for (ULONG i = 0; i < items->Count; i++, curMed++) { + if (!IsEqualGUID(curMed->clsMedium, GUID_NULL) && + !IsEqualGUID(curMed->clsMedium, KSMEDIUMSETID_Standard)) { + medium = *curMed; + return true; + } + } + + memset(&medium, 0, sizeof(medium)); + return false; +} + +static inline bool PinIsConnected(IPin *pin) +{ + ComPtr connectedPin; + return SUCCEEDED(pin->ConnectedTo(&connectedPin)); +} + +static bool DirectConnectOutputPin(IFilterGraph *graph, IPin *pin, + IBaseFilter *filterIn) +{ + ComPtr curPin; + ComPtr pinsEnum; + ULONG num; + + if (!graph || !filterIn || !pin) + return false; + if (FAILED(filterIn->EnumPins(&pinsEnum))) + return false; + + while (pinsEnum->Next(1, &curPin, &num) == S_OK) { + + if (PinIsDirection(curPin, PINDIR_INPUT) && + !PinIsConnected(curPin)) { + if (graph->ConnectDirect(pin, curPin, nullptr) == S_OK) + return true; + } + } + + return false; +} + +bool DirectConnectFilters(IFilterGraph *graph, IBaseFilter *filterOut, + IBaseFilter *filterIn) +{ + ComPtr curPin; + ComPtr pinsEnum; + ULONG num; + bool connected = false; + + if (!graph || !filterOut || !filterIn) + return false; + if (FAILED(filterOut->EnumPins(&pinsEnum))) + return false; + + while (pinsEnum->Next(1, &curPin, &num) == S_OK) { + + if (PinIsDirection(curPin, PINDIR_OUTPUT) && + !PinIsConnected(curPin)) { + if (DirectConnectOutputPin(graph, curPin, filterIn)) + connected = true; + } + } + + return connected; +} + +HRESULT MapPinToPacketID(IPin *pin, ULONG packetID) +{ + ComQIPtr pidMap(pin); + if (!pidMap) + return E_NOINTERFACE; + + return pidMap->MapPID(1, &packetID, MEDIA_ELEMENTARY_STREAM); +} + +wstring ConvertHRToEnglish(HRESULT hr) +{ + LPWSTR buffer = NULL; + wstring str; + + FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, hr, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + (LPTSTR)&buffer, 0, NULL); + + if (buffer) { + str = buffer; + LocalFree(buffer); + } + + return str; +} + +static HRESULT DevicePathToDeviceInstancePath(const wchar_t *devicePath, + wchar_t *devInstPath, int size) +{ + /* Sanity checks */ + if (!devicePath) + return E_POINTER; + if (!devInstPath) + return E_POINTER; + + /* Convert to uppercase STL string */ + wstring parseDevicePath = devicePath; + for (wchar_t &c : parseDevicePath) + c = (wchar_t)toupper(c); + + /* Find start position ('\\?\' or '\?\') */ + wstring startToken = L"\\\\?\\"; + size_t start = parseDevicePath.find(startToken, 0); + if (start == string::npos) { + startToken = L"\\??\\"; + start = parseDevicePath.find(startToken, 0); + if (start == string::npos) + return E_FAIL; + } + parseDevicePath = parseDevicePath.substr( + startToken.size(), parseDevicePath.size() - startToken.size()); + + /* Find end position (last occurrence of '#') */ + wchar_t endToken = '#'; + size_t end = + parseDevicePath.find_last_of(endToken, parseDevicePath.size()); + if (end == string::npos) + return E_FAIL; + parseDevicePath = parseDevicePath.substr(0, end); + + /* Replace '#' by '\' */ + std::replace(parseDevicePath.begin(), parseDevicePath.end(), L'#', + L'\\'); + + /* Set output parameter */ + StringCchCopyW(devInstPath, size, parseDevicePath.c_str()); + + return S_OK; +} + +static HRESULT GetParentDeviceInstancePath(const wchar_t *devInstPath, + wchar_t *parentDevInstPath, int size) +{ + /* Init return value */ + HRESULT hr = E_FAIL; + + /* Get device info */ + HDEVINFO hDevInfo = SetupDiCreateDeviceInfoList(nullptr, NULL); + if (NULL != hDevInfo) { + SP_DEVINFO_DATA did; + did.cbSize = sizeof(SP_DEVINFO_DATA); + BOOL success = SetupDiOpenDeviceInfo(hDevInfo, devInstPath, + NULL, 0, &did); + if (success) { + + /* Get parent device */ + DEVINST devParent; + CONFIGRET ret = + CM_Get_Parent(&devParent, did.DevInst, 0); + if (CR_SUCCESS == ret) { + + /* Get parent device instance path */ + ret = CM_Get_Device_ID( + devParent, parentDevInstPath, size, 0); + if (CR_SUCCESS == ret) + hr = S_OK; + } + + /* Cleanup */ + SetupDiDeleteDeviceInfo(hDevInfo, &did); + } + + /* Cleanup */ + SetupDiDestroyDeviceInfoList(hDevInfo); + } + + return hr; +} + +static bool IsSameInstPath(const wchar_t *audDevPath, + const wchar_t *vidDevInstPath) +{ + /* Get audio device instance path */ + wchar_t audDevInstPath[512]; + HRESULT hr = DevicePathToDeviceInstancePath(audDevPath, audDevInstPath, + _ARRAYSIZE(audDevInstPath)); + + /* Compare audio and video device instance path */ + if (FAILED(hr)) + return false; + + return wcscmp(audDevInstPath, vidDevInstPath) == 0; +} + +static HRESULT +GetAudioCaptureParentDeviceInstancePath(IMoniker *audioCapture, + wchar_t *parentDevInstPath, int size) +{ + /* Sanity checks */ + if (!audioCapture) + return E_POINTER; + + /* Bind to property bag */ + ComPtr propertyBag; + HRESULT hr = audioCapture->BindToStorage(0, 0, IID_IPropertyBag, + (void **)&propertyBag); + if (SUCCEEDED(hr)) { + + /* Init variant */ + VARIANT var; + VariantInit(&var); + + /* Get "WaveInId" */ + hr = propertyBag->Read(L"WaveInId", &var, nullptr); + if (SUCCEEDED(hr) && var.vt == VT_I4) { + + /* Get device path */ + wchar_t devicePath[512]; + MMRESULT res = waveInMessage((HWAVEIN)var.iVal, + DRV_QUERYDEVICEINTERFACE, + (DWORD_PTR)devicePath, + sizeof(devicePath)); + if (res == MMSYSERR_NOERROR) { + /* Get device instance path */ + wchar_t devInstPath[512]; + hr = DevicePathToDeviceInstancePath( + devicePath, devInstPath, + _ARRAYSIZE(devInstPath)); + + /* Get parent */ + if (SUCCEEDED(hr)) + hr = GetParentDeviceInstancePath( + devInstPath, parentDevInstPath, + size); + } + } + + /* Cleanup */ + VariantClear(&var); + } + + return hr; +} + +static bool IsMonikerSameParentInstPath(IMoniker *moniker, + const wchar_t *vidDevInstPath) +{ + /* Get video parent device instance path */ + wchar_t vidParentDevInstPath[512]; + HRESULT hr = GetParentDeviceInstancePath( + vidDevInstPath, vidParentDevInstPath, + _ARRAYSIZE(vidParentDevInstPath)); + + if (FAILED(hr)) + return false; + + /* Get audio parent device instance path */ + wchar_t audParentDevInstPath[512]; + hr = GetAudioCaptureParentDeviceInstancePath( + moniker, audParentDevInstPath, + _ARRAYSIZE(audParentDevInstPath)); + + if (FAILED(hr)) + return false; + + /* Compare audio and video parent device instance path */ + return wcscmp(audParentDevInstPath, vidParentDevInstPath) == 0; +} + +#define VEN_ID_SIZE 4 + +static inline bool MatchingStartToken(const wstring &path, + const wstring &start_token) +{ + return path.find(start_token) == 0 && + path.size() >= start_token.size() + VEN_ID_SIZE; +} + +static bool IsUncoupledDevice(const wchar_t *vidDevInstPath) +{ + /* Sanity checks */ + if (!vidDevInstPath) + return false; + + const wstring path = vidDevInstPath; + + /* USB */ + const wstring usbToken = L"USB\\VID_"; + const wstring usbVidIdWhitelist[] = { + L"0FD9", /* elgato */ + L"3842", /* evga */ + L"0B05", /* asus */ + L"07CA", /* avermedia */ + L"048D", /* digitnow/pengo */ + L"04B4", /* mokose */ + L"0557", /* aten */ + L"1164", /* startek/kapchr */ + L"1532", /* razer */ + L"1BCF", /* mypin/treaslin/mirabox */ + L"1E4E", /* pengo/cloneralliance */ + L"1E71", /* nzxt */ + L"2040", /* hauppauge */ + L"2935", /* magewell */ + L"298F", /* genki */ + L"2B77", /* epiphan */ + L"32ED", /* ezcap */ + L"534D", /* brand-less/pacoxi/ucec */ + L"EBA4", /* zasluke */ + }; + + if (MatchingStartToken(path, usbToken)) { + /* Get USB vendor ID */ + const wstring vid = path.substr(usbToken.size(), VEN_ID_SIZE); + for (const wstring &whitelistId : usbVidIdWhitelist) { + if (vid == whitelistId) { + return true; + } + } + } + + /* PCI */ + const wstring pciVenToken = L"PCI\\VEN_"; + const wstring pciSubsysToken = L"SUBSYS_"; + const wstring pciVenIdWhitelist[] = { + L"1CD7", /* magewell */ + L"8888", /* acasis */ + L"1461", /* avermedia */ + }; + const wstring pciSubsysIdWhitelist[] = { + L"1CFA", /* elgato */ + }; + + if (MatchingStartToken(path, pciVenToken)) { + const wstring vid = path.substr(usbToken.size(), VEN_ID_SIZE); + for (const wstring &whitelistId : pciVenIdWhitelist) { + if (vid == whitelistId) { + return true; + } + } + + size_t subsysPos = path.find(pciSubsysToken); + size_t subsysIdPos = + subsysPos + pciSubsysToken.size() + VEN_ID_SIZE; + size_t expectedSize = subsysIdPos + VEN_ID_SIZE; + + if (subsysPos != string::npos && path.size() >= expectedSize) { + /* Get PCI subsystem vendor ID */ + const wstring ssid = + path.substr(subsysIdPos, VEN_ID_SIZE); + for (const wstring &whitelistId : + pciSubsysIdWhitelist) { + if (ssid == whitelistId) { + return true; + } + } + } + } + + return false; +} + +static HRESULT ReadProperty(IMoniker *moniker, const wchar_t *property, + wchar_t *value, int size) +{ + /* Sanity checks */ + if (!moniker) + return E_POINTER; + if (!property) + return E_POINTER; + if (!value) + return E_POINTER; + + /* Increment reference count */ + moniker->AddRef(); + + /* Bind to property bag */ + ComPtr propertyBag; + HRESULT hr = moniker->BindToStorage(0, 0, IID_IPropertyBag, + (void **)&propertyBag); + if (SUCCEEDED(hr)) { + /* Initialize variant */ + VARIANT var; + VariantInit(&var); + + /* Read property */ + hr = propertyBag->Read(property, &var, nullptr); + if (SUCCEEDED(hr)) + StringCchCopyW(value, size, var.bstrVal); + + /* Cleanup */ + VariantClear(&var); + } + + /* Decrement reference count */ + moniker->Release(); + + return hr; +} + +static HRESULT GetFriendlyName(REFCLSID deviceClass, const wchar_t *devPath, + wchar_t *name, int nameSize) +{ + /* Sanity checks */ + if (!devPath) + return E_POINTER; + if (!name) + return E_POINTER; + + /* Create device enumerator */ + ComPtr createDevEnum; + HRESULT hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, + CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, + (void **)&createDevEnum); + + /* Enumerate filters */ + ComPtr enumMoniker; + if (SUCCEEDED(hr)) { + /* returns S_FALSE if no devices are installed */ + hr = createDevEnum->CreateClassEnumerator(deviceClass, + &enumMoniker, 0); + if (!enumMoniker) + hr = E_FAIL; + } + + /* Cycle through the enumeration */ + if (SUCCEEDED(hr)) { + ULONG fetched = 0; + ComPtr moniker; + + enumMoniker->Reset(); + + while (enumMoniker->Next(1, &moniker, &fetched) == S_OK) { + + /* Get device path from moniker */ + wchar_t monikerDevPath[512]; + hr = ReadProperty(moniker, L"DevicePath", + monikerDevPath, + _ARRAYSIZE(monikerDevPath)); + + /* Find desired filter */ + if (wcscmp(devPath, monikerDevPath) == 0) { + + /* Get friendly name */ + hr = ReadProperty(moniker, L"FriendlyName", + name, nameSize); + return hr; + } + } + } + + return E_FAIL; +} + +static bool MatchFriendlyNames(const wchar_t *vidName, const wchar_t *audName) +{ + /* Sanity checks */ + if (!vidName) + return false; + if (!audName) + return false; + + /* Convert strings to lower case */ + wstring strVidName = vidName; + for (wchar_t &c : strVidName) + c = (wchar_t)tolower(c); + wstring strAudName = audName; + for (wchar_t &c : strAudName) + c = (wchar_t)tolower(c); + + /* Remove 'video' from friendly name */ + size_t posVid; + const wstring searchVid[] = {L"(video) ", L"(video)", L"video ", + L"video", L"hdmi", L" / multiview"}; + for (int i = 0; i < _ARRAYSIZE(searchVid); i++) { + const wstring &search = searchVid[i]; + while ((posVid = strVidName.find(search)) != + std::string::npos) { + strVidName.replace(posVid, search.length(), L""); + } + } + + /* Remove 'audio' from friendly name */ + size_t posAud; + const wstring searchAud[] = {L"(audio) ", L"(audio)", L"audio ", + L"audio"}; + for (int i = 0; i < _ARRAYSIZE(searchAud); i++) { + const wstring &search = searchAud[i]; + while ((posAud = strAudName.find(search)) != + std::string::npos) { + strAudName.replace(posAud, search.length(), L""); + } + } + + return strVidName == strAudName; +} + +static bool GetDeviceAudioFilterInternal(REFCLSID deviceClass, + const wchar_t *vidDevPath, + IBaseFilter **audioCaptureFilter, + bool matchFilterName = false) +{ + /* Get video device instance path */ + wchar_t vidDevInstPath[512]; + HRESULT hr = DevicePathToDeviceInstancePath(vidDevPath, vidDevInstPath, + _ARRAYSIZE(vidDevInstPath)); + if (FAILED(hr)) + return false; + +#if 1 + /* Only enabled for certain whitelisted devices for now */ + if (!IsUncoupledDevice(vidDevInstPath)) + return false; +#endif + + /* Get friendly name */ + wchar_t vidName[512]; + if (matchFilterName) { + hr = GetFriendlyName(CLSID_VideoInputDeviceCategory, vidDevPath, + vidName, _ARRAYSIZE(vidName)); + if (FAILED(hr)) + return false; + } + + /* Create device enumerator */ + ComPtr createDevEnum; + if (SUCCEEDED(hr)) + hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, + CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, + (void **)&createDevEnum); + + /* Enumerate filters */ + ComPtr enumMoniker; + if (SUCCEEDED(hr)) { + /* returns S_FALSE if no devices are installed */ + hr = createDevEnum->CreateClassEnumerator(deviceClass, + &enumMoniker, 0); + if (!enumMoniker) + hr = E_FAIL; + } + + /* Cycle through the enumeration */ + if (SUCCEEDED(hr)) { + ULONG fetched = 0; + ComPtr moniker; + + enumMoniker->Reset(); + + while (enumMoniker->Next(1, &moniker, &fetched) == S_OK) { + bool samePath = false; + + /* Get device path */ + wchar_t audDevPath[512]; + hr = ReadProperty(moniker, L"DevicePath", audDevPath, + _ARRAYSIZE(audDevPath)); + if (SUCCEEDED(hr)) { + /* Skip if it is the video device */ + if (wcscmp(audDevPath, vidDevPath) == 0) + continue; + + samePath = IsSameInstPath(audDevPath, + vidDevInstPath); + } else { + samePath = IsMonikerSameParentInstPath( + moniker, vidDevInstPath); + } + + /* Get audio capture filter */ + if (samePath) { + /* Match video and audio filter names */ + bool isSameFilterName = false; + if (matchFilterName) { + wchar_t audName[512]; + hr = ReadProperty(moniker, + L"FriendlyName", + audName, + _ARRAYSIZE(audName)); + if (SUCCEEDED(hr)) { + isSameFilterName = + MatchFriendlyNames( + vidName, + audName); + } + } + + if (!matchFilterName || isSameFilterName) { + hr = moniker->BindToObject( + 0, 0, IID_IBaseFilter, + (void **)audioCaptureFilter); + if (SUCCEEDED(hr)) + return true; + } + } + } + } + + return false; +} + +bool GetDeviceAudioFilter(const wchar_t *vidDevPath, + IBaseFilter **audioCaptureFilter) +{ + /* Search in "Audio capture sources" and match filter name */ + bool success = GetDeviceAudioFilterInternal( + CLSID_AudioInputDeviceCategory, vidDevPath, audioCaptureFilter, + true); + + /* Search in "WDM Streaming Capture Devices" and match filter name */ + if (!success) + success = GetDeviceAudioFilterInternal(KSCATEGORY_CAPTURE, + vidDevPath, + audioCaptureFilter, + true); + + /* Search in "Audio capture sources" */ + if (!success) + success = GetDeviceAudioFilterInternal( + CLSID_AudioInputDeviceCategory, vidDevPath, + audioCaptureFilter); + + /* Search in "WDM Streaming Capture Devices" */ + if (!success) + success = GetDeviceAudioFilterInternal( + KSCATEGORY_CAPTURE, vidDevPath, audioCaptureFilter); + + return success; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-base.hpp b/obs-studio/deps/libdshowcapture/src/source/dshow-base.hpp new file mode 100644 index 0000000..696922b --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-base.hpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#define WIN32_MEAN_AND_LEAN +#define __STREAMS__ +#include +#include +#include +#include +#include +#include +#include + +#include "ComPtr.hpp" +#include "CoTaskMemPtr.hpp" + +#include +using namespace std; + +#define DSHOW_UNUSED(param) (void)param; + +namespace DShow { + +bool CreateFilterGraph(IGraphBuilder **graph, ICaptureGraphBuilder2 **builder, + IMediaControl **control); + +void LogFilters(IGraphBuilder *graph); + +bool GetDeviceFilter(const IID &type, const wchar_t *name, const wchar_t *path, + IBaseFilter **filter); + +bool GetFilterPin(IBaseFilter *filter, const GUID &type, const GUID &category, + PIN_DIRECTION dir, IPin **pin); + +bool GetPinByName(IBaseFilter *filter, PIN_DIRECTION dir, const wchar_t *name, + IPin **pin); + +bool GetPinByMedium(IBaseFilter *filter, REGPINMEDIUM &medium, IPin **pin); +bool GetFilterByMedium(const CLSID &id, REGPINMEDIUM &medium, + IBaseFilter **filter); + +bool GetPinMedium(IPin *pin, REGPINMEDIUM &medium); + +bool DirectConnectFilters(IFilterGraph *graph, IBaseFilter *filterOut, + IBaseFilter *filterIn); + +/** + * This maps a created demuxer pin to a packet ID for the mux stream. Note + * that this needs to be called after the device filters are connected to the + * demux filter. + */ +HRESULT MapPinToPacketID(IPin *pin, ULONG packetID); + +wstring ConvertHRToEnglish(HRESULT hr); + +/** + * Get audio filter for the same device as the given video device path + */ +bool GetDeviceAudioFilter(const wchar_t *videoDevicePath, + IBaseFilter **audioCaptureFilter); + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-demux.cpp b/obs-studio/deps/libdshowcapture/src/source/dshow-demux.cpp new file mode 100644 index 0000000..5c19eb6 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-demux.cpp @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "dshow-demux.hpp" +#include "dshow-formats.hpp" +#include "log.hpp" + +namespace DShow { + +static inline DWORD VideoFormatToFourCC(VideoFormat format) +{ + if (format == VideoFormat::H264) + return MAKEFOURCC('H', '2', '6', '4'); + + return 0; +} + +static inline const GUID &VideoFormatToSubType(VideoFormat format) +{ + if (format == VideoFormat::H264) + return MEDIASUBTYPE_H264; + + return GUID_NULL; +} + +bool CreateDemuxVideoPin(IBaseFilter *demuxFilter, MediaType &mt, long width, + long height, long long frameTime, VideoFormat format) +{ + ComQIPtr demuxer(demuxFilter); + if (!demuxer) { + Warning(L"CreateDemuxVideoPin: Failed to get " + L"IMpeg2Demultiplexer from filter"); + return false; + } + + ComPtr pin; + HRESULT hr; + + VIDEOINFOHEADER *vih = mt.AllocFormat(); + vih->bmiHeader.biSize = sizeof(vih->bmiHeader); + vih->bmiHeader.biWidth = width; + vih->bmiHeader.biHeight = height; + vih->bmiHeader.biCompression = VideoFormatToFourCC(format); + vih->rcSource.right = width; + vih->rcSource.bottom = height; + vih->AvgTimePerFrame = frameTime; + + if (!vih->bmiHeader.biCompression) { + Warning(L"CreateDemuxVideoPin: Invalid video format"); + return false; + } + + mt->majortype = MEDIATYPE_Video; + mt->subtype = VideoFormatToSubType(format); + mt->formattype = FORMAT_VideoInfo; + mt->bTemporalCompression = true; + + wchar_t *name = (wchar_t *)CoTaskMemAlloc(sizeof(DEMUX_VIDEO_PIN)); + memcpy(name, DEMUX_VIDEO_PIN, sizeof(DEMUX_VIDEO_PIN)); + + hr = demuxer->CreateOutputPin(mt, name, &pin); + if (FAILED(hr)) { + WarningHR(L"CreateDemuxVideoPin: Failed to create video pin " + L"on demuxer", + hr); + return false; + } + + return true; +} + +static inline WORD AudioFormatToFormatTag(AudioFormat format) +{ + if (format == AudioFormat::AAC) + return WAVE_FORMAT_RAW_AAC1; + else if (format == AudioFormat::AC3) + return WAVE_FORMAT_DVM; + else if (format == AudioFormat::MPGA) + return WAVE_FORMAT_MPEG; + + return 0; +} + +static inline const GUID &AudioFormatToSubType(AudioFormat format) +{ + if (format == AudioFormat::AAC) + return MEDIASUBTYPE_RAW_AAC1; + else if (format == AudioFormat::AC3) + return MEDIASUBTYPE_DVM; + else if (format == AudioFormat::MPGA) + return MEDIASUBTYPE_MPEG1AudioPayload; + + return GUID_NULL; +} + +bool CreateDemuxAudioPin(IBaseFilter *demuxFilter, MediaType &mt, + DWORD samplesPerSec, WORD bitsPerSample, WORD channels, + AudioFormat format) +{ + ComQIPtr demuxer(demuxFilter); + if (!demuxer) { + Warning(L"CreateDemuxAudioPin: Failed to get " + L"IMpeg2Demultiplexer from filter"); + return false; + } + + ComPtr pin; + HRESULT hr; + + WAVEFORMATEX *wfex = mt.AllocFormat(); + wfex->wFormatTag = AudioFormatToFormatTag(format); + wfex->nChannels = channels; + wfex->nSamplesPerSec = samplesPerSec; + wfex->wBitsPerSample = bitsPerSample; + + if (!wfex->wFormatTag) { + Warning(L"CreateDemuxAudioPin: Invalid audio format"); + return false; + } + + mt->majortype = MEDIATYPE_Audio; + mt->subtype = AudioFormatToSubType(format); + mt->formattype = FORMAT_WaveFormatEx; + mt->bTemporalCompression = true; + + wchar_t *name = (wchar_t *)CoTaskMemAlloc(sizeof(DEMUX_AUDIO_PIN)); + memcpy(name, DEMUX_AUDIO_PIN, sizeof(DEMUX_AUDIO_PIN)); + + hr = demuxer->CreateOutputPin(mt, name, &pin); + if (FAILED(hr)) { + WarningHR(L"CreateDemuxAudioPin: Failed to create audio pin " + L"on demuxer", + hr); + return false; + } + + return true; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-demux.hpp b/obs-studio/deps/libdshowcapture/src/source/dshow-demux.hpp new file mode 100644 index 0000000..741bdec --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-demux.hpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "../dshowcapture.hpp" +#include "dshow-base.hpp" +#include "dshow-media-type.hpp" + +namespace DShow { + +#define DEMUX_VIDEO_PIN L"Demuxer Video Pin" +#define DEMUX_AUDIO_PIN L"Demuxer Audio Pin" + +bool CreateDemuxVideoPin(IBaseFilter *demuxFilter, MediaType &mt, long width, + long height, long long frameTime, VideoFormat format); + +bool CreateDemuxAudioPin(IBaseFilter *demuxFilter, MediaType &mt, + DWORD samplesPerSec, WORD bitsPerSample, WORD channels, + AudioFormat format); + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-device-defs.hpp b/obs-studio/deps/libdshowcapture/src/source/dshow-device-defs.hpp new file mode 100644 index 0000000..04ac483 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-device-defs.hpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +namespace DShow { + +#define COMMON_ENCODED_CX 720 +#define COMMON_ENCODED_CY 480 +#define COMMON_ENCODED_INTERVAL (10010000000LL / 60000LL) +#define COMMON_ENCODED_VFORMAT VideoFormat::H264 +#define COMMON_ENCODED_SAMPLERATE 48000 + +static const EncodedDevice HD_PVR1 = {COMMON_ENCODED_VFORMAT, + 0x1011UL, + COMMON_ENCODED_CX, + COMMON_ENCODED_CY, + COMMON_ENCODED_INTERVAL, + AudioFormat::AC3, + 0x1100UL, + COMMON_ENCODED_SAMPLERATE}; + +static const EncodedDevice HD_PVR2 = {COMMON_ENCODED_VFORMAT, + 0x1011UL, + COMMON_ENCODED_CX, + COMMON_ENCODED_CY, + COMMON_ENCODED_INTERVAL, + AudioFormat::AAC, + 0x1100UL, + COMMON_ENCODED_SAMPLERATE}; + +static const EncodedDevice Roxio = { + COMMON_ENCODED_VFORMAT, + 0x1011UL, + COMMON_ENCODED_CX, + COMMON_ENCODED_CY, + COMMON_ENCODED_INTERVAL, + AudioFormat::AAC, + 0x010FUL, + COMMON_ENCODED_SAMPLERATE, +}; + +static const EncodedDevice HD_PVR_Rocket = {COMMON_ENCODED_VFORMAT, + 0x07D1UL, + COMMON_ENCODED_CX, + COMMON_ENCODED_CY, + COMMON_ENCODED_INTERVAL, + AudioFormat::AAC, + 0x07D2UL, + COMMON_ENCODED_SAMPLERATE}; + +static const EncodedDevice AV_LGP = {COMMON_ENCODED_VFORMAT, + 68, + COMMON_ENCODED_CX, + COMMON_ENCODED_CY, + COMMON_ENCODED_INTERVAL, + AudioFormat::AAC, + 69, + COMMON_ENCODED_SAMPLERATE}; + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-encoded-device.cpp b/obs-studio/deps/libdshowcapture/src/source/dshow-encoded-device.cpp new file mode 100644 index 0000000..0d4d352 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-encoded-device.cpp @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "dshow-base.hpp" +#include "dshow-media-type.hpp" +#include "dshow-formats.hpp" +#include "dshow-demux.hpp" +#include "capture-filter.hpp" +#include "device.hpp" +#include "log.hpp" + +namespace DShow { + +static inline bool CreateFilters(IBaseFilter *filter, IBaseFilter **crossbar, + IBaseFilter **encoder, IBaseFilter **demuxer) +{ + ComPtr inputPin; + ComPtr outputPin; + REGPINMEDIUM inMedium; + REGPINMEDIUM outMedium; + bool hasOutMedium; + HRESULT hr; + + if (!GetPinByName(filter, PINDIR_INPUT, nullptr, &inputPin)) { + Warning(L"Encoded Device: Failed to get input pin"); + return false; + } + + if (!GetPinByName(filter, PINDIR_OUTPUT, nullptr, &outputPin)) { + Warning(L"Encoded Device: Failed to get output pin"); + return false; + } + + if (!GetPinMedium(inputPin, inMedium)) { + Warning(L"Encoded Device: Failed to get input pin medium"); + return false; + } + + hasOutMedium = GetPinMedium(outputPin, outMedium); + + if (!GetFilterByMedium(AM_KSCATEGORY_CROSSBAR, inMedium, crossbar)) { + Warning(L"Encoded Device: Failed to get crossbar filter"); + return false; + } + + /* perfectly okay if there's no encoder filter, some don't have them */ + if (hasOutMedium) + GetFilterByMedium(KSCATEGORY_ENCODER, outMedium, encoder); + + hr = CoCreateInstance(CLSID_MPEG2Demultiplexer, nullptr, + CLSCTX_INPROC_SERVER, IID_IBaseFilter, + (void **)demuxer); + if (FAILED(hr)) { + WarningHR(L"Encoded Device: Failed to create demuxer", hr); + return false; + } + + return true; +} + +static inline bool ConnectEncodedFilters(IGraphBuilder *graph, + IBaseFilter *filter, + IBaseFilter *crossbar, + IBaseFilter *encoder, + IBaseFilter *demuxer) +{ + if (!DirectConnectFilters(graph, crossbar, filter)) { + Warning(L"Encoded Device: Failed to connect crossbar to " + L"device"); + return false; + } + + if (!!encoder) { + if (!DirectConnectFilters(graph, filter, encoder)) { + Warning(L"Encoded Device: Failed to connect device to " + L"encoder"); + return false; + } + + if (!DirectConnectFilters(graph, encoder, demuxer)) { + Warning(L"Encoded Device: Failed to connect encoder to " + L"demuxer"); + return false; + } + } else { + if (!DirectConnectFilters(graph, filter, demuxer)) { + Warning(L"Encoded Device: Failed to connect device to " + L"demuxer"); + return false; + } + } + + return true; +} + +static inline bool MapPacketIDs(IBaseFilter *demuxer, ULONG video, ULONG audio) +{ + ComPtr videoPin, audioPin; + HRESULT hr; + + if (!GetPinByName(demuxer, PINDIR_OUTPUT, DEMUX_VIDEO_PIN, &videoPin)) { + Warning(L"Encoded Device: Could not get video pin from " + L"demuxer"); + return false; + } + + if (!GetPinByName(demuxer, PINDIR_OUTPUT, DEMUX_AUDIO_PIN, &audioPin)) { + Warning(L"Encoded Device: Could not get audio pin from " + L"demuxer"); + return false; + } + + hr = MapPinToPacketID(videoPin, video); + if (FAILED(hr)) { + WarningHR(L"Encoded Device: Failed to map demuxer video pin " + L"packet ID", + hr); + return false; + } + + hr = MapPinToPacketID(audioPin, audio); + if (FAILED(hr)) { + WarningHR(L"Encoded Device: Failed to map demuxer audio pin " + L"packet ID", + hr); + return false; + } + + return true; +} + +/* + * rocket-specific workaround code. I have no idea what any of these numbers + * are except the GUID which was obvious. All I know is calling + * IKsPropertySet::Set turns on/off some sort of 'mode' on the device. + * I discovered this merely by chance while monitoring API usage in other + * programs because I could not figure out how the hell to get this thing + * to turn on. + */ +static const GUID RocketEncoderGUID = {0x99100000, + 0xa330, + 0x11e1, + {0xa3, 0x80, 0x99, 0x10, 0x68, 0x64, + 0x00, 0x00}}; + +struct RocketPropStruct { + DWORD dwSize; + DWORD unknown1; + DWORD unknown2; + DWORD unknown3; + DWORD code; + DWORD unknown4; + BOOL enabled; +}; + +struct RocketInstStruct { + DWORD code; + DWORD unknown1; +}; + +bool SetRocketEnabled(IBaseFilter *encoder, bool enable) +{ + static const ULONG rocketEnableId = 0x9910E001; + static const DWORD rocketEnableCode = 0x38384001; + RocketInstStruct rocketInstance = {}; + RocketPropStruct rocketProperty = {}; + + ComQIPtr propertySet(encoder); + if (!propertySet) + return false; + + rocketProperty.dwSize = sizeof(rocketProperty); + rocketProperty.code = rocketEnableCode; + rocketProperty.enabled = enable; + rocketInstance.code = rocketEnableCode; + + HRESULT hr = propertySet->Set(RocketEncoderGUID, rocketEnableId, + &rocketInstance, sizeof(rocketInstance), + &rocketProperty, sizeof(rocketProperty)); + + return SUCCEEDED(hr); +} + +bool HDevice::SetupEncodedVideoCapture(IBaseFilter *filter, VideoConfig &config, + const EncodedDevice &info) +{ + ComPtr crossbar; + ComPtr encoder; + ComPtr demuxer; + MediaType mtVideo; + MediaType mtAudio; + + if (!CreateFilters(filter, &crossbar, &encoder, &demuxer)) + return false; + + if (!CreateDemuxVideoPin(demuxer, mtVideo, info.width, info.height, + info.frameInterval, info.videoFormat)) + return false; + + if (!CreateDemuxAudioPin(demuxer, mtAudio, info.samplesPerSec, 16, 2, + info.audioFormat)) + return false; + + config.cx = info.width; + config.cy_abs = labs(info.height); + config.cy_flip = info.height < 0; + config.frameInterval = info.frameInterval; + config.format = info.videoFormat; + config.internalFormat = info.videoFormat; + + PinCaptureInfo pci; + pci.callback = [this](IMediaSample *s) { Receive(true, s); }; + pci.expectedMajorType = mtVideo->majortype; + pci.expectedSubType = mtVideo->subtype; + + videoCapture = new CaptureFilter(pci); + videoFilter = demuxer; + + if (!!encoder && config.name.find(L"IT9910") != std::string::npos) { + rocketEncoder = encoder; + + if (!SetRocketEnabled(rocketEncoder, true)) + return false; + } + + graph->AddFilter(crossbar, L"Crossbar"); + graph->AddFilter(filter, L"Device"); + graph->AddFilter(demuxer, L"Demuxer"); + graph->AddFilter(videoCapture, L"Capture Filter"); + + if (!!encoder) + graph->AddFilter(encoder, L"Encoder"); + + bool success = ConnectEncodedFilters(graph, filter, crossbar, encoder, + demuxer); + if (success) + success = MapPacketIDs(demuxer, info.videoPacketID, + info.audioPacketID); + + encodedDevice = success; + return success; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-enum.cpp b/obs-studio/deps/libdshowcapture/src/source/dshow-enum.cpp new file mode 100644 index 0000000..6a7548b --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-enum.cpp @@ -0,0 +1,573 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include +#include +#include "dshow-enum.hpp" +#include "dshow-formats.hpp" +#include "log.hpp" + +#undef DEFINE_GUID +#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + EXTERN_C const GUID DECLSPEC_SELECTANY name = { \ + l, w1, w2, {b1, b2, b3, b4, b5, b6, b7, b8}} + +#include "external/IVideoCaptureFilter.h" + +namespace DShow { +using namespace std; + +typedef bool (*EnumCapsCallback)(void *param, const AM_MEDIA_TYPE &mt, + const BYTE *data); + +static void EnumElgatoCaps(IPin *pin, EnumCapsCallback callback, void *param) +{ + ComPtr mediaTypes; + + if (SUCCEEDED(pin->EnumMediaTypes(&mediaTypes))) { + MediaTypePtr mt; + ULONG count = 0; + + while (mediaTypes->Next(1, &mt, &count) == S_OK) { + if (!callback(param, *mt, nullptr)) + break; + } + } +} + +static bool EnumPinCaps(IPin *pin, EnumCapsCallback callback, void *param) +{ + HRESULT hr; + ComQIPtr config(pin); + int count, size; + + if (config == NULL) + return false; + + hr = config->GetNumberOfCapabilities(&count, &size); + if (SUCCEEDED(hr)) { + vector caps; + caps.resize(size); + + for (int i = 0; i < count; i++) { + MediaTypePtr mt; + hr = config->GetStreamCaps(i, &mt, caps.data()); + if (SUCCEEDED(hr)) + if (!callback(param, *mt, caps.data())) + break; + } + } else if (hr == E_NOTIMPL) { + EnumElgatoCaps(pin, callback, param); + } else { + return false; + } + + return true; +} + +/* Note: DEVICE_VideoInfo is not to be confused with Device::VideoInfo */ +static bool Get_FORMAT_VideoInfo_Data(VideoInfo &info, const AM_MEDIA_TYPE &mt, + const BYTE *data) +{ + const VIDEO_STREAM_CONFIG_CAPS *vscc; + const VIDEOINFOHEADER *viHeader; + const BITMAPINFOHEADER *bmiHeader; + VideoFormat format; + + vscc = reinterpret_cast(data); + viHeader = reinterpret_cast(mt.pbFormat); + bmiHeader = &viHeader->bmiHeader; + + if (!GetMediaTypeVFormat(mt, format)) + return false; + + info.format = format; + + if (vscc) { + info.minInterval = vscc->MinFrameInterval; + info.maxInterval = vscc->MaxFrameInterval; + info.minCX = vscc->MinOutputSize.cx; + info.minCY = vscc->MinOutputSize.cy; + info.maxCX = vscc->MaxOutputSize.cx; + info.maxCY = vscc->MaxOutputSize.cy; + + if (!info.minCX || !info.minCY || !info.maxCX || !info.maxCY) { + info.minCX = info.maxCX = bmiHeader->biWidth; + info.minCY = info.maxCY = bmiHeader->biHeight; + } + + info.granularityCX = max(vscc->OutputGranularityX, 1); + info.granularityCY = max(vscc->OutputGranularityY, 1); + } else { + info.minInterval = info.maxInterval = 10010000000LL / 60000LL; + info.minCX = info.maxCX = bmiHeader->biWidth; + info.minCY = info.maxCY = bmiHeader->biHeight; + info.granularityCX = 1; + info.granularityCY = 1; + } + + return true; +} + +static bool Get_FORMAT_WaveFormatEx_Data(AudioInfo &info, + const AM_MEDIA_TYPE &mt, + const BYTE *data) +{ + const AUDIO_STREAM_CONFIG_CAPS *ascc; + const WAVEFORMATEX *wfex; + + ascc = reinterpret_cast(data); + wfex = reinterpret_cast(mt.pbFormat); + if (!wfex || !ascc) { + return false; + } + + switch (wfex->wBitsPerSample) { + case 16: + info.format = AudioFormat::Wave16bit; + break; + case 32: + info.format = AudioFormat::WaveFloat; + break; + } + + info.minChannels = ascc->MinimumChannels; + info.maxChannels = ascc->MaximumChannels; + info.channelsGranularity = ascc->ChannelsGranularity; + info.minSampleRate = ascc->MinimumSampleFrequency; + info.maxSampleRate = ascc->MaximumSampleFrequency; + info.sampleRateGranularity = ascc->SampleFrequencyGranularity; + return true; +} + +struct ClosestVideoData { + VideoConfig &config; + MediaType &mt; + long long bestVal; + bool found; + + ClosestVideoData &operator=(ClosestVideoData const &) = delete; + ClosestVideoData &operator=(ClosestVideoData &&) = delete; + + inline ClosestVideoData(VideoConfig &config, MediaType &mt) + : config(config), mt(mt), bestVal(0), found(false) + { + } +}; + +static inline void ClampToGranularity(LONG &val, int minVal, int granularity) +{ + val -= ((val - minVal) % granularity); +} + +static inline int GetFormatRating(VideoFormat format) +{ + if (format >= VideoFormat::I420 && format < VideoFormat::YVYU) + return 0; + else if (format >= VideoFormat::YVYU && format < VideoFormat::MJPEG) + return 5; + else if (format == VideoFormat::MJPEG) + return 10; + + return 15; +} + +static bool ClosestVideoMTCallback(ClosestVideoData &data, + const AM_MEDIA_TYPE &mt, const BYTE *capData) +{ + VideoInfo info; + + if (mt.formattype == FORMAT_VideoInfo) { + if (!Get_FORMAT_VideoInfo_Data(info, mt, capData)) + return true; + } else { + return true; + } + + MediaType copiedMT = mt; + VIDEOINFOHEADER *vih = (VIDEOINFOHEADER *)copiedMT->pbFormat; + BITMAPINFOHEADER *bmih = GetBitmapInfoHeader(copiedMT); + + if (data.config.internalFormat != VideoFormat::Any && + data.config.internalFormat != info.format) + return true; + + int xVal = 0; + int yVal = 0; + int formatVal = 0; + long long frameVal = 0; + + if (data.config.cx < info.minCX) + xVal = info.minCX - data.config.cx; + else if (data.config.cx > info.maxCX) + xVal = data.config.cx - info.maxCX; + + const int absMinCY = abs(info.minCY); + const int absMaxCY = abs(info.maxCY); + if (data.config.cy_abs < absMinCY) + yVal = absMinCY - data.config.cy_abs; + else if (data.config.cy_abs > absMaxCY) + yVal = data.config.cy_abs - absMaxCY; + + const long long frameInterval = data.config.frameInterval; + if (frameInterval < info.minInterval) + frameVal = info.minInterval - frameInterval; + else if (frameInterval > info.maxInterval) + frameVal = frameInterval - info.maxInterval; + + formatVal = GetFormatRating(info.format); + + long long totalVal = frameVal + yVal + xVal + formatVal; + + if (!data.found || data.bestVal > totalVal) { + if (xVal == 0) { + bmih->biWidth = data.config.cx; + ClampToGranularity(bmih->biWidth, info.minCX, + info.granularityCX); + } + + if (yVal == 0) { + LONG cy_abs_clamp = data.config.cy_abs; + ClampToGranularity(cy_abs_clamp, info.minCY, + info.granularityCY); + bmih->biHeight = data.config.cy_flip ? -cy_abs_clamp + : cy_abs_clamp; + } + + if (frameVal == 0) { + // Close enough. Fixes GV-USB2 29.97 FPS setting. + if (abs(vih->AvgTimePerFrame - frameInterval) > 1) + vih->AvgTimePerFrame = frameInterval; + } + + data.found = true; + data.bestVal = totalVal; + data.mt = copiedMT; + + if (totalVal == 0) + return false; + } + + return true; +} + +bool GetClosestVideoMediaType(IBaseFilter *filter, VideoConfig &config, + MediaType &mt) +{ + ComPtr pin; + ClosestVideoData data(config, mt); + bool success; + + success = GetFilterPin(filter, MEDIATYPE_Video, PIN_CATEGORY_CAPTURE, + PINDIR_OUTPUT, &pin); + if (!success || pin == NULL) { + Error(L"GetClosestVideoMediaType: Could not get pin"); + return false; + } + + success = EnumPinCaps(pin, EnumCapsCallback(ClosestVideoMTCallback), + &data); + if (!success) { + Error(L"GetClosestVideoMediaType: Could not enumerate caps"); + return false; + } + + return data.found; +} + +struct ClosestAudioData { + AudioConfig &config; + MediaType &mt; + int bestVal; + bool found; + + ClosestAudioData &operator=(ClosestAudioData const &) = delete; + ClosestAudioData &operator=(ClosestAudioData &&) = delete; + + inline ClosestAudioData(AudioConfig &config, MediaType &mt) + : config(config), mt(mt), bestVal(0), found(false) + { + } +}; + +static bool ClosestAudioMTCallback(ClosestAudioData &data, + const AM_MEDIA_TYPE &mt, const BYTE *capData) +{ + AudioInfo info = {}; + + if (mt.formattype == FORMAT_WaveFormatEx) { + if (!Get_FORMAT_WaveFormatEx_Data(info, mt, capData)) + return false; + } else { + return true; + } + + MediaType copiedMT = mt; + WAVEFORMATEX *wfex = (WAVEFORMATEX *)copiedMT->pbFormat; + + if (data.config.format != AudioFormat::Any && + data.config.format != info.format) + return true; + + int sampleRateVal = 0; + int channelsVal = 0; + + if (data.config.sampleRate < info.minSampleRate) + sampleRateVal = info.minSampleRate - data.config.sampleRate; + else if (data.config.sampleRate > info.maxSampleRate) + sampleRateVal = data.config.sampleRate - info.maxSampleRate; + else if (data.config.sampleRate == info.minSampleRate) + sampleRateVal = data.config.sampleRate; + else if (data.config.sampleRate == info.maxSampleRate) + sampleRateVal = data.config.sampleRate; + + if (data.config.channels < info.minChannels) + channelsVal = info.minChannels - data.config.channels; + else if (data.config.channels == info.minChannels) + channelsVal = info.minChannels; + else if (info.maxChannels > data.config.channels) + channelsVal = data.config.channels - info.maxChannels; + else if (data.config.channels == info.maxChannels) + channelsVal = data.config.channels; + + int totalVal = sampleRateVal + channelsVal; + + if (!data.found || data.bestVal > totalVal) { + if (channelsVal == 0) { + LONG channels = data.config.channels; + ClampToGranularity(channels, info.minChannels, + info.channelsGranularity); + wfex->nChannels = (WORD)channels; + + wfex->nBlockAlign = + wfex->wBitsPerSample * wfex->nChannels / 8; + } + + if (sampleRateVal == 0) { + wfex->nSamplesPerSec = data.config.sampleRate; + ClampToGranularity((LONG &)wfex->nSamplesPerSec, + info.minSampleRate, + info.sampleRateGranularity); + } + + wfex->nAvgBytesPerSec = + wfex->nSamplesPerSec * wfex->nBlockAlign; + + data.mt = copiedMT; + data.found = true; + data.bestVal = totalVal; + + if (totalVal == 0) + return false; + } + + return true; +} + +bool GetClosestAudioMediaType(IBaseFilter *filter, AudioConfig &config, + MediaType &mt) +{ + ComPtr pin; + ClosestAudioData data(config, mt); + bool success; + + success = GetFilterPin(filter, MEDIATYPE_Audio, PIN_CATEGORY_CAPTURE, + PINDIR_OUTPUT, &pin); + if (!success || pin == NULL) { + Error(L"GetClosestAudioMediaType: Could not get pin"); + return false; + } + + success = EnumPinCaps(pin, EnumCapsCallback(ClosestAudioMTCallback), + &data); + if (!success) { + Error(L"GetClosestAudioMediaType: Could not enumerate caps"); + return false; + } + + return data.found; +} + +static bool EnumVideoCap(vector &caps, const AM_MEDIA_TYPE &mt, + const BYTE *data) +{ + VideoInfo info; + + if (mt.formattype == FORMAT_VideoInfo) + if (Get_FORMAT_VideoInfo_Data(info, mt, data)) + caps.push_back(info); + + return true; +} + +bool EnumVideoCaps(IPin *pin, vector &caps) +{ + return EnumPinCaps(pin, EnumCapsCallback(EnumVideoCap), &caps); +} + +static bool EnumAudioCap(vector &caps, const AM_MEDIA_TYPE &mt, + const BYTE *data) +{ + AudioInfo info; + + if (mt.formattype == FORMAT_WaveFormatEx) { + if (Get_FORMAT_WaveFormatEx_Data(info, mt, data)) + caps.push_back(info); + } + + return true; +} + +bool EnumAudioCaps(IPin *pin, vector &caps) +{ + return EnumPinCaps(pin, EnumCapsCallback(EnumAudioCap), &caps); +} + +static bool decklinkVideoPresent = false; + +static bool EnumDevice(const GUID &type, IMoniker *deviceInfo, + EnumDeviceCallback callback, void *param) +{ + ComPtr propertyData; + ComPtr filter; + HRESULT hr; + + hr = deviceInfo->BindToStorage(0, 0, IID_IPropertyBag, + (void **)&propertyData); + if (FAILED(hr)) + return true; + + VARIANT deviceName, devicePath; + deviceName.vt = VT_BSTR; + devicePath.vt = VT_BSTR; + devicePath.bstrVal = NULL; + + hr = propertyData->Read(L"FriendlyName", &deviceName, NULL); + if (FAILED(hr)) + return true; + + /* workaround to a crash in decklink drivers; if no decklink device + * is plugged in to the system, it will still try to enumerate the + * decklink audio device, but will crash when trying to bind it to + * a filter due to a bug in the drivers */ + if (deviceName.bstrVal && type == CLSID_AudioInputDeviceCategory && + wcsstr(deviceName.bstrVal, L"Decklink") != nullptr && + !decklinkVideoPresent) { + return true; + } + + propertyData->Read(L"DevicePath", &devicePath, NULL); + + hr = deviceInfo->BindToObject(NULL, 0, IID_IBaseFilter, + (void **)&filter); + if (SUCCEEDED(hr)) { + if (!callback(param, filter, deviceName.bstrVal, + devicePath.bstrVal)) + return false; + } + + return true; +} + +static bool EnumExceptionVideoDevices(EnumDeviceCallback callback, void *param) +{ + ComPtr filter; + HRESULT hr; + + hr = CoCreateInstance(CLSID_ElgatoVideoCaptureFilter, nullptr, + CLSCTX_INPROC_SERVER, IID_IBaseFilter, + (void **)&filter); + if (SUCCEEDED(hr)) { + if (!callback(param, filter, L"Elgato Game Capture HD", + L"__elgato")) + return false; + } + + return true; +} + +static recursive_mutex enumMutex; + +static bool CheckForDLCallback(void *unused, IBaseFilter *filter, + const wchar_t *deviceName, + const wchar_t *devicePath) +{ + if (wcsstr(deviceName, L"Decklink") != nullptr) { + decklinkVideoPresent = true; + return false; + } + + DSHOW_UNUSED(unused); + DSHOW_UNUSED(filter); + DSHOW_UNUSED(devicePath); + return true; +} + +static void CheckForDecklinkVideo() +{ + decklinkVideoPresent = false; + EnumDevices(CLSID_VideoInputDeviceCategory, CheckForDLCallback, + nullptr); +} + +bool EnumDevices(const GUID &type, EnumDeviceCallback callback, void *param) +{ + lock_guard lock(enumMutex); + ComPtr deviceEnum; + ComPtr enumMoniker; + ComPtr deviceInfo; + HRESULT hr; + DWORD count = 0; + + if (type == CLSID_AudioInputDeviceCategory) { + CheckForDecklinkVideo(); + } + + hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, + CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, + (void **)&deviceEnum); + if (FAILED(hr)) { + WarningHR(L"EnumAudioDevices: Could not create " + L"ICreateDeviceEnum", + hr); + return false; + } + + hr = deviceEnum->CreateClassEnumerator(type, &enumMoniker, 0); + if (FAILED(hr)) { + WarningHR(L"EnumAudioDevices: CreateClassEnumerator failed", + hr); + return false; + } + + if (hr == S_OK) { + while (enumMoniker->Next(1, &deviceInfo, &count) == S_OK) { + if (!EnumDevice(type, deviceInfo, callback, param)) + return true; + } + } + + if (type == CLSID_VideoInputDeviceCategory) + if (!EnumExceptionVideoDevices(callback, param)) + return true; + + return true; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-enum.hpp b/obs-studio/deps/libdshowcapture/src/source/dshow-enum.hpp new file mode 100644 index 0000000..e8b02b4 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-enum.hpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "../dshowcapture.hpp" +#include "dshow-base.hpp" +#include "dshow-media-type.hpp" + +#include + +using namespace std; + +namespace DShow { + +bool GetClosestVideoMediaType(IBaseFilter *filter, VideoConfig &config, + MediaType &mt); +bool GetClosestAudioMediaType(IBaseFilter *filter, AudioConfig &config, + MediaType &mt); + +bool EnumVideoCaps(IPin *pin, vector &caps); +bool EnumAudioCaps(IPin *pin, vector &caps); + +typedef bool (*EnumDeviceCallback)(void *param, IBaseFilter *filter, + const wchar_t *deviceName, + const wchar_t *devicePath); + +bool EnumDevices(const GUID &type, EnumDeviceCallback callback, void *param); + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-formats.cpp b/obs-studio/deps/libdshowcapture/src/source/dshow-formats.cpp new file mode 100644 index 0000000..4baf381 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-formats.cpp @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "dshow-formats.hpp" +#include "dshow-media-type.hpp" + +#ifndef __MINGW32__ + +const GUID MEDIASUBTYPE_RAW_AAC1 = {0x000000FF, + 0x0000, + 0x0010, + {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, + 0x71}}; + +const GUID MEDIASUBTYPE_I420 = {0x30323449, + 0x0000, + 0x0010, + {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, + 0x71}}; + +const GUID MEDIASUBTYPE_DVM = {0x00002000, + 0x0000, + 0x0010, + {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, + 0x71}}; + +#endif + +const GUID MEDIASUBTYPE_Y800 = {0x30303859, + 0x0000, + 0x0010, + {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, + 0x71}}; + +#ifdef ENABLE_HEVC +const GUID MEDIASUBTYPE_HEVC = {0x43564548, + 0x0000, + 0x0010, + {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, + 0x71}}; +#endif + +namespace DShow { + +DWORD VFormatToFourCC(VideoFormat format) +{ + switch (format) { + /* raw formats */ + case VideoFormat::ARGB: + return MAKEFOURCC('A', 'R', 'G', 'B'); + case VideoFormat::XRGB: + return MAKEFOURCC('R', 'G', 'B', '4'); + + /* planar YUV formats */ + case VideoFormat::I420: + return MAKEFOURCC('I', '4', '2', '0'); + case VideoFormat::NV12: + return MAKEFOURCC('N', 'V', '1', '2'); + case VideoFormat::YV12: + return MAKEFOURCC('Y', 'V', '1', '2'); + case VideoFormat::Y800: + return MAKEFOURCC('Y', '8', '0', '0'); + case VideoFormat::P010: + return MAKEFOURCC('P', '0', '1', '0'); + + /* packed YUV formats */ + case VideoFormat::YVYU: + return MAKEFOURCC('Y', 'V', 'Y', 'U'); + case VideoFormat::YUY2: + return MAKEFOURCC('Y', 'U', 'Y', '2'); + case VideoFormat::UYVY: + return MAKEFOURCC('U', 'Y', 'V', 'Y'); + case VideoFormat::HDYC: + return MAKEFOURCC('H', 'D', 'Y', 'C'); + + /* encoded formats */ + case VideoFormat::MJPEG: + return MAKEFOURCC('M', 'J', 'P', 'G'); + case VideoFormat::H264: + return MAKEFOURCC('H', '2', '6', '4'); +#ifdef ENABLE_HEVC + case VideoFormat::HEVC: + return MAKEFOURCC('H', 'E', 'V', 'C'); +#endif + + default: + return 0; + } +} + +GUID VFormatToSubType(VideoFormat format) +{ + switch (format) { + /* raw formats */ + case VideoFormat::ARGB: + return MEDIASUBTYPE_ARGB32; + case VideoFormat::XRGB: + return MEDIASUBTYPE_RGB32; + + /* planar YUV formats */ + case VideoFormat::I420: + return MEDIASUBTYPE_I420; + case VideoFormat::NV12: + return MEDIASUBTYPE_NV12; + case VideoFormat::YV12: + return MEDIASUBTYPE_YV12; + case VideoFormat::Y800: + return MEDIASUBTYPE_Y800; + case VideoFormat::P010: + return MEDIASUBTYPE_P010; + + /* packed YUV formats */ + case VideoFormat::YVYU: + return MEDIASUBTYPE_YVYU; + case VideoFormat::YUY2: + return MEDIASUBTYPE_YUY2; + case VideoFormat::UYVY: + return MEDIASUBTYPE_UYVY; + + /* encoded formats */ + case VideoFormat::MJPEG: + return MEDIASUBTYPE_MJPG; + case VideoFormat::H264: + return MEDIASUBTYPE_H264; +#ifdef ENABLE_HEVC + case VideoFormat::HEVC: + return MEDIASUBTYPE_HEVC; +#endif + + default: + return GUID(); + } +} + +WORD VFormatBits(VideoFormat format) +{ + switch (format) { + /* raw formats */ + case VideoFormat::ARGB: + case VideoFormat::XRGB: + return 32; + + /* planar YUV formats */ + case VideoFormat::I420: + case VideoFormat::NV12: + case VideoFormat::YV12: + return 12; + case VideoFormat::Y800: + return 8; + + /* packed YUV formats */ + case VideoFormat::YVYU: + case VideoFormat::YUY2: + case VideoFormat::UYVY: + return 16; + + default: + return 0; + } +} + +WORD VFormatPlanes(VideoFormat format) +{ + switch (format) { + /* raw formats */ + case VideoFormat::ARGB: + case VideoFormat::XRGB: + return 1; + + /* planar YUV formats */ + case VideoFormat::I420: + return 3; + case VideoFormat::NV12: + case VideoFormat::YV12: + return 2; + case VideoFormat::Y800: + return 1; + + /* packed YUV formats */ + case VideoFormat::YVYU: + case VideoFormat::YUY2: + case VideoFormat::UYVY: + return 1; + + default: + return 0; + } +} + +static bool GetFourCCVFormat(DWORD fourCC, VideoFormat &format) +{ + switch (fourCC) { + /* raw formats */ + case MAKEFOURCC('R', 'G', 'B', '2'): + format = VideoFormat::XRGB; + break; + case MAKEFOURCC('R', 'G', 'B', '4'): + format = VideoFormat::XRGB; + break; + case MAKEFOURCC('A', 'R', 'G', 'B'): + format = VideoFormat::ARGB; + break; + + /* planar YUV formats */ + case MAKEFOURCC('I', '4', '2', '0'): + case MAKEFOURCC('I', 'Y', 'U', 'V'): + format = VideoFormat::I420; + break; + case MAKEFOURCC('Y', 'V', '1', '2'): + format = VideoFormat::YV12; + break; + case MAKEFOURCC('N', 'V', '1', '2'): + format = VideoFormat::NV12; + break; + case MAKEFOURCC('Y', '8', '0', '0'): + format = VideoFormat::Y800; + break; + case MAKEFOURCC('P', '0', '1', '0'): + format = VideoFormat::P010; + break; + + /* packed YUV formats */ + case MAKEFOURCC('Y', 'V', 'Y', 'U'): + format = VideoFormat::YVYU; + break; + case MAKEFOURCC('Y', 'U', 'Y', '2'): + format = VideoFormat::YUY2; + break; + case MAKEFOURCC('U', 'Y', 'V', 'Y'): + format = VideoFormat::UYVY; + break; + case MAKEFOURCC('H', 'D', 'Y', 'C'): + format = VideoFormat::HDYC; + break; + + /* compressed formats */ + case MAKEFOURCC('H', '2', '6', '4'): + format = VideoFormat::H264; + break; +#ifdef ENABLE_HEVC + case MAKEFOURCC('H', 'E', 'V', 'C'): + format = VideoFormat::HEVC; + break; +#endif + + /* compressed formats that can automatically create intermediary + * filters for decompression */ + case MAKEFOURCC('M', 'J', 'P', 'G'): + format = VideoFormat::MJPEG; + break; + + default: + return false; + } + + return true; +} + +bool GetMediaTypeVFormat(const AM_MEDIA_TYPE &mt, VideoFormat &format) +{ + if (mt.majortype != MEDIATYPE_Video) + return false; + + const BITMAPINFOHEADER *bmih = GetBitmapInfoHeader(mt); + + format = VideoFormat::Unknown; + + /* raw formats */ + if (mt.subtype == MEDIASUBTYPE_RGB24) + format = VideoFormat::XRGB; + else if (mt.subtype == MEDIASUBTYPE_RGB32) + format = VideoFormat::XRGB; + else if (mt.subtype == MEDIASUBTYPE_ARGB32) + format = VideoFormat::ARGB; + + /* planar YUV formats */ + else if (mt.subtype == MEDIASUBTYPE_I420) + format = VideoFormat::I420; + else if (mt.subtype == MEDIASUBTYPE_IYUV) + format = VideoFormat::I420; + else if (mt.subtype == MEDIASUBTYPE_YV12) + format = VideoFormat::YV12; + else if (mt.subtype == MEDIASUBTYPE_NV12) + format = VideoFormat::NV12; + else if (mt.subtype == MEDIASUBTYPE_Y800) + format = VideoFormat::Y800; + else if (mt.subtype == MEDIASUBTYPE_P010) + format = VideoFormat::P010; + + /* packed YUV formats */ + else if (mt.subtype == MEDIASUBTYPE_YVYU) + format = VideoFormat::YVYU; + else if (mt.subtype == MEDIASUBTYPE_YUY2) + format = VideoFormat::YUY2; + else if (mt.subtype == MEDIASUBTYPE_UYVY) + format = VideoFormat::UYVY; + + /* compressed formats */ + else if (mt.subtype == MEDIASUBTYPE_H264) + format = VideoFormat::H264; +#ifdef ENABLE_HEVC + else if (mt.subtype == MEDIASUBTYPE_HEVC) + format = VideoFormat::HEVC; +#endif + + /* compressed formats that can automatically create intermediary + * filters for decompression */ + else if (mt.subtype == MEDIASUBTYPE_MJPG) + format = VideoFormat::MJPEG; + + /* no valid types, check fourcc value instead */ + else + return bmih ? GetFourCCVFormat(bmih->biCompression, format) + : false; + + return true; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-formats.hpp b/obs-studio/deps/libdshowcapture/src/source/dshow-formats.hpp new file mode 100644 index 0000000..9de11af --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-formats.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "../dshowcapture.hpp" +#include "dshow-base.hpp" + +#include +#include + +namespace DShow { + +DWORD VFormatToFourCC(VideoFormat format); +WORD VFormatBits(VideoFormat format); +WORD VFormatPlanes(VideoFormat format); +GUID VFormatToSubType(VideoFormat format); + +bool GetMediaTypeVFormat(const AM_MEDIA_TYPE &mt, VideoFormat &format); + +}; /*namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-media-type.cpp b/obs-studio/deps/libdshowcapture/src/source/dshow-media-type.cpp new file mode 100644 index 0000000..ce32163 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-media-type.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "dshow-media-type.hpp" + +namespace DShow { + +HRESULT CopyMediaType(AM_MEDIA_TYPE *pmtTarget, const AM_MEDIA_TYPE *pmtSource) +{ + if (!pmtSource || !pmtTarget) + return S_FALSE; + + *pmtTarget = *pmtSource; + + if (pmtSource->cbFormat && pmtSource->pbFormat) { + pmtTarget->pbFormat = + (PBYTE)CoTaskMemAlloc(pmtSource->cbFormat); + + if (pmtTarget->pbFormat == nullptr) { + pmtTarget->cbFormat = 0; + return E_OUTOFMEMORY; + } else { + memcpy(pmtTarget->pbFormat, pmtSource->pbFormat, + pmtTarget->cbFormat); + } + } + + if (pmtTarget->pUnk != nullptr) + pmtTarget->pUnk->AddRef(); + + return S_OK; +} + +void FreeMediaType(AM_MEDIA_TYPE &mt) +{ + if (mt.cbFormat != 0) { + CoTaskMemFree((LPVOID)mt.pbFormat); + mt.cbFormat = 0; + mt.pbFormat = nullptr; + } + + if (mt.pUnk) { + mt.pUnk->Release(); + mt.pUnk = nullptr; + } +} + +BITMAPINFOHEADER *GetBitmapInfoHeader(AM_MEDIA_TYPE &mt) +{ + if (mt.formattype == FORMAT_VideoInfo) { + VIDEOINFOHEADER *vih; + vih = reinterpret_cast(mt.pbFormat); + return &vih->bmiHeader; + + } else if (mt.formattype == FORMAT_VideoInfo2) { + VIDEOINFOHEADER2 *vih; + vih = reinterpret_cast(mt.pbFormat); + return &vih->bmiHeader; + } + + return NULL; +} + +const BITMAPINFOHEADER *GetBitmapInfoHeader(const AM_MEDIA_TYPE &mt) +{ + if (mt.formattype == FORMAT_VideoInfo) { + const VIDEOINFOHEADER *vih; + vih = reinterpret_cast(mt.pbFormat); + return &vih->bmiHeader; + + } else if (mt.formattype == FORMAT_VideoInfo2) { + const VIDEOINFOHEADER2 *vih; + vih = reinterpret_cast(mt.pbFormat); + return &vih->bmiHeader; + } + + return NULL; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshow-media-type.hpp b/obs-studio/deps/libdshowcapture/src/source/dshow-media-type.hpp new file mode 100644 index 0000000..fc2a81b --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshow-media-type.hpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "dshow-base.hpp" + +namespace DShow { + +HRESULT CopyMediaType(AM_MEDIA_TYPE *pmtTarget, const AM_MEDIA_TYPE *pmtSource); +void FreeMediaType(AM_MEDIA_TYPE &mt); + +BITMAPINFOHEADER *GetBitmapInfoHeader(AM_MEDIA_TYPE &mt); +const BITMAPINFOHEADER *GetBitmapInfoHeader(const AM_MEDIA_TYPE &mt); + +class MediaTypePtr; + +class MediaType { + friend class MediaTypePtr; + + AM_MEDIA_TYPE type; + +public: + inline MediaType() { memset(&type, 0, sizeof(type)); } + + inline MediaType(const MediaType &mt) + { + CopyMediaType(&type, &mt.type); + } + + inline MediaType(const AM_MEDIA_TYPE &type_) + { + CopyMediaType(&type, &type_); + } + + inline ~MediaType() { FreeMediaType(type); } + + inline operator AM_MEDIA_TYPE *() { return &type; } + inline operator AM_MEDIA_TYPE &() { return type; } + inline operator const AM_MEDIA_TYPE *() const { return &type; } + inline operator const AM_MEDIA_TYPE &() const { return type; } + inline AM_MEDIA_TYPE *Ptr() { return &type; } + + inline AM_MEDIA_TYPE *operator->() { return &type; } + inline const AM_MEDIA_TYPE *operator->() const { return &type; } + + inline AM_MEDIA_TYPE *Duplicate() const + { + AM_MEDIA_TYPE *ptr = + (AM_MEDIA_TYPE *)CoTaskMemAlloc(sizeof(*ptr)); + if (ptr) { + memset(ptr, 0, sizeof(*ptr)); + CopyMediaType(ptr, &type); + } + return ptr; + } + + inline bool operator==(const AM_MEDIA_TYPE *pMT) const + { + return pMT == &type; + } + + inline void operator=(const MediaType &mt) + { + FreeMediaType(type); + CopyMediaType(&type, &mt.type); + } + + inline void operator=(const AM_MEDIA_TYPE *pMT) + { + FreeMediaType(type); + CopyMediaType(&type, pMT); + } + + inline void operator=(const AM_MEDIA_TYPE &type_) + { + FreeMediaType(type); + CopyMediaType(&type, &type_); + } + + template inline T *AllocFormat() + { + if (type.pbFormat) { + CoTaskMemFree(type.pbFormat); + type.pbFormat = nullptr; + type.cbFormat = 0; + } + + type.pbFormat = (PBYTE)CoTaskMemAlloc(sizeof(T)); + type.cbFormat = sizeof(T); + memset(type.pbFormat, 0, sizeof(T)); + + return (T *)type.pbFormat; + } +}; + +class MediaTypePtr { + friend class MediaType; + + AM_MEDIA_TYPE *ptr; + +public: + inline void Clear() + { + if (ptr) { + FreeMediaType(*ptr); + CoTaskMemFree(ptr); + ptr = nullptr; + } + } + + inline MediaTypePtr() : ptr(nullptr) {} + inline MediaTypePtr(AM_MEDIA_TYPE *ptr_) : ptr(ptr_) {} + inline ~MediaTypePtr() { Clear(); } + + inline AM_MEDIA_TYPE **operator&() + { + Clear(); + return &ptr; + } + + inline AM_MEDIA_TYPE *operator->() const { return ptr; } + + inline operator AM_MEDIA_TYPE *() const { return ptr; } + + inline void operator=(AM_MEDIA_TYPE *ptr_) + { + Clear(); + ptr = ptr_; + } + + inline bool operator==(const AM_MEDIA_TYPE *ptr_) const + { + return ptr == ptr_; + } +}; + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshowcapture.cpp b/obs-studio/deps/libdshowcapture/src/source/dshowcapture.cpp new file mode 100644 index 0000000..03711fa --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshowcapture.cpp @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "../dshowcapture.hpp" +#include "dshow-base.hpp" +#include "dshow-enum.hpp" +#include "device.hpp" +#include "dshow-device-defs.hpp" +#include "log.hpp" + +#include + +namespace DShow { + +Device::Device(InitGraph initialize) : context(new HDevice) +{ + if (initialize == InitGraph::True) + context->CreateGraph(); +} + +Device::~Device() +{ + delete context; +} + +bool Device::Valid() const +{ + return context->initialized; +} + +bool Device::ResetGraph() +{ + /* cheap and easy way to clear all the filters */ + delete context; + context = new HDevice; + + return context->CreateGraph(); +} + +void Device::ShutdownGraph() +{ + delete context; + context = new HDevice; +} + +bool Device::SetVideoConfig(VideoConfig *config) +{ + return context->SetVideoConfig(config); +} + +bool Device::SetAudioConfig(AudioConfig *config) +{ + return context->SetAudioConfig(config); +} + +bool Device::ConnectFilters() +{ + return context->ConnectFilters(); +} + +Result Device::Start() +{ + return context->Start(); +} + +void Device::Stop() +{ + context->Stop(); +} + +bool Device::GetVideoConfig(VideoConfig &config) const +{ + if (context->videoCapture == NULL) + return false; + + config = context->videoConfig; + return true; +} + +bool Device::GetAudioConfig(AudioConfig &config) const +{ + if (context->audioCapture == NULL) + return false; + + config = context->audioConfig; + return true; +} + +bool Device::GetVideoDeviceId(DeviceId &id) const +{ + if (context->videoCapture == NULL) + return false; + + id = context->videoConfig; + return true; +} + +bool Device::GetAudioDeviceId(DeviceId &id) const +{ + if (context->audioCapture == NULL) + return false; + + id = context->audioConfig; + return true; +} + +static void OpenPropertyPages(HWND hwnd, IUnknown *propertyObject) +{ + if (!propertyObject) + return; + + ComQIPtr pages(propertyObject); + CAUUID cauuid; + + if (pages != NULL) { + if (SUCCEEDED(pages->GetPages(&cauuid)) && cauuid.cElems) { + OleCreatePropertyFrame(hwnd, 0, 0, NULL, 1, + (LPUNKNOWN *)&propertyObject, + cauuid.cElems, cauuid.pElems, 0, + 0, NULL); + CoTaskMemFree(cauuid.pElems); + } + } +} + +void Device::OpenDialog(void *hwnd, DialogType type) const +{ + ComPtr ptr; + HRESULT hr; + + if (type == DialogType::ConfigVideo) { + ptr = context->videoFilter; + } else if (type == DialogType::ConfigCrossbar || + type == DialogType::ConfigCrossbar2) { + hr = context->builder->FindInterface(NULL, NULL, + context->videoFilter, + IID_IAMCrossbar, + (void **)&ptr); + if (FAILED(hr)) { + WarningHR(L"Failed to find crossbar", hr); + return; + } + + if (ptr != NULL && type == DialogType::ConfigCrossbar2) { + ComQIPtr xbar(ptr); + ComQIPtr filter(xbar); + + hr = context->builder->FindInterface( + &LOOK_UPSTREAM_ONLY, NULL, filter, + IID_IAMCrossbar, (void **)&ptr); + if (FAILED(hr)) { + WarningHR(L"Failed to find crossbar2", hr); + return; + } + } + } else if (type == DialogType::ConfigAudio) { + ptr = context->audioFilter; + } + + if (!ptr) { + Warning(L"Could not find filter to open dialog type: %d with", + (int)type); + return; + } + + OpenPropertyPages((HWND)hwnd, ptr); +} + +static void EnumEncodedVideo(std::vector &devices, + const wchar_t *deviceName, + const wchar_t *devicePath, + const EncodedDevice &info) +{ + VideoDevice device; + VideoInfo caps; + + device.name = deviceName; + device.path = devicePath; + device.audioAttached = true; + device.separateAudioFilter = false; + + caps.minCX = caps.maxCX = info.width; + caps.minCY = caps.maxCY = info.height; + caps.granularityCX = caps.granularityCY = 1; + caps.minInterval = caps.maxInterval = info.frameInterval; + caps.format = info.videoFormat; + + device.caps.push_back(caps); + devices.push_back(device); +} + +static void EnumExceptionVideoDevice(std::vector &devices, + IBaseFilter *filter, + const wchar_t *deviceName, + const wchar_t *devicePath) +{ + ComPtr pin; + + if (GetPinByName(filter, PINDIR_OUTPUT, L"656", &pin)) + EnumEncodedVideo(devices, deviceName, devicePath, HD_PVR2); + + else if (GetPinByName(filter, PINDIR_OUTPUT, L"TS Out", &pin)) + EnumEncodedVideo(devices, deviceName, devicePath, Roxio); +} + +static bool EnumVideoDevice(std::vector &devices, + IBaseFilter *filter, const wchar_t *deviceName, + const wchar_t *devicePath) +{ + ComPtr pin; + ComPtr audioPin; + ComPtr audioFilter; + VideoDevice info; + + if (wcsstr(deviceName, L"C875") != nullptr || + wcsstr(deviceName, L"Prif Streambox") != nullptr || + wcsstr(deviceName, L"C835") != nullptr) { + EnumEncodedVideo(devices, deviceName, devicePath, AV_LGP); + return true; + + } else if (wcsstr(deviceName, L"Hauppauge HD PVR Capture") != nullptr) { + EnumEncodedVideo(devices, deviceName, devicePath, HD_PVR1); + return true; + } + + bool success = GetFilterPin(filter, MEDIATYPE_Video, + PIN_CATEGORY_CAPTURE, PINDIR_OUTPUT, &pin); + + /* if this device has no standard capture pin, see if it's an + * encoded device, and get its information if so (all encoded devices + * are exception devices pretty much) */ + if (!success) { + EnumExceptionVideoDevice(devices, filter, deviceName, + devicePath); + return true; + } + + if (!EnumVideoCaps(pin, info.caps)) + return true; + + info.audioAttached = GetFilterPin(filter, MEDIATYPE_Audio, + PIN_CATEGORY_CAPTURE, PINDIR_OUTPUT, + &audioPin); + + // Fallback: Find a corresponding audio filter for the same device + if (!info.audioAttached) { + info.separateAudioFilter = + GetDeviceAudioFilter(devicePath, &audioFilter); + info.audioAttached = info.separateAudioFilter; + } + + info.name = deviceName; + if (devicePath) + info.path = devicePath; + + devices.push_back(info); + return true; +} + +bool Device::EnumVideoDevices(std::vector &devices) +{ + devices.clear(); + return EnumDevices(CLSID_VideoInputDeviceCategory, + EnumDeviceCallback(EnumVideoDevice), &devices); +} + +static bool EnumAudioDevice(vector &devices, IBaseFilter *filter, + const wchar_t *deviceName, + const wchar_t *devicePath) +{ + ComPtr pin; + AudioDevice info; + + bool success = GetFilterPin(filter, MEDIATYPE_Audio, + PIN_CATEGORY_CAPTURE, PINDIR_OUTPUT, &pin); + if (!success) + return true; + + if (!EnumAudioCaps(pin, info.caps)) + return true; + + info.name = deviceName; + if (devicePath) + info.path = devicePath; + + devices.push_back(info); + return true; +} + +bool Device::EnumAudioDevices(vector &devices) +{ + devices.clear(); + return EnumDevices(CLSID_AudioInputDeviceCategory, + EnumDeviceCallback(EnumAudioDevice), &devices); +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/dshowencode.cpp b/obs-studio/deps/libdshowcapture/src/source/dshowencode.cpp new file mode 100644 index 0000000..0e1667d --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/dshowencode.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "../dshowcapture.hpp" +#include "dshow-base.hpp" +#include "dshow-enum.hpp" +#include "encoder.hpp" +#include "log.hpp" + +namespace DShow { + +VideoEncoder::VideoEncoder() : context(new HVideoEncoder) {} + +VideoEncoder::~VideoEncoder() +{ + delete context; +} + +bool VideoEncoder::Valid() const +{ + return context->initialized; +} + +bool VideoEncoder::Active() const +{ + return context->active; +} + +bool VideoEncoder::ResetGraph() +{ + delete context; + context = new HVideoEncoder; + + return context->initialized; +} + +bool VideoEncoder::SetConfig(VideoEncoderConfig &config) +{ + if (context->active) { + delete context; + context = new HVideoEncoder; + } + + return context->SetConfig(config); +} + +bool VideoEncoder::GetConfig(VideoEncoderConfig &config) const +{ + if (context->encoder == nullptr) + return false; + + config = context->config; + return true; +} + +bool VideoEncoder::Encode(unsigned char *data[DSHOW_MAX_PLANES], + size_t linesize[DSHOW_MAX_PLANES], + long long timestampStart, long long timestampEnd, + EncoderPacket &packet, bool &new_packet) +{ + if (context->encoder == nullptr) + return false; + + return context->Encode(data, linesize, timestampStart, timestampEnd, + packet, new_packet); +} + +static bool EnumVideoEncoder(vector &encoders, IBaseFilter *encoder, + const wchar_t *deviceName, + const wchar_t *devicePath) +{ + DeviceId id; + bool validDevice = wcsstr(deviceName, L"C985") || + wcsstr(deviceName, L"C353"); + + if (!validDevice) + return true; + + id.name = deviceName; + id.path = devicePath; + encoders.push_back(id); + + (void)encoder; + return true; +} + +bool VideoEncoder::EnumEncoders(vector &encoders) +{ + encoders.clear(); + return EnumDevices(KSCATEGORY_ENCODER, + EnumDeviceCallback(EnumVideoEncoder), &encoders); +} + +}; diff --git a/obs-studio/deps/libdshowcapture/src/source/encoder.cpp b/obs-studio/deps/libdshowcapture/src/source/encoder.cpp new file mode 100644 index 0000000..8bf57f0 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/encoder.cpp @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "encoder.hpp" +#include "log.hpp" +#include "avermedia-encode.h" + +namespace DShow { + +HVideoEncoder::HVideoEncoder() +{ + initialized = CreateFilterGraph(&graph, &builder, &control); +} + +HVideoEncoder::~HVideoEncoder() +{ + ComPtr filterEnum; + IBaseFilter *filter; + HRESULT hr; + + if (!initialized) + return; + + if (active) + control->Stop(); + + /* seems like you have to manually release the entire graph otherwise + * the encoder device might not end up releasing properly */ + hr = graph->EnumFilters(&filterEnum); + if (hr == S_OK) { + while (filterEnum->Next(1, &filter, nullptr) == S_OK) { + graph->RemoveFilter(filter); + filterEnum->Reset(); + filter->Release(); + } + } +} + +bool HVideoEncoder::ConnectFilters() +{ + ComPtr deviceIn; + ComPtr deviceOut; + ComPtr encoderIn; + ComPtr encoderOut; + bool success; + HRESULT hr; + + success = GetPinByName(device, PINDIR_INPUT, L"YUV In", &deviceIn); + if (!success) { + Warning(L"Failed to get YUV In pin"); + return false; + } + + success = GetPinByName(device, PINDIR_OUTPUT, L"Virtual Video Out", + &deviceOut); + if (!success) { + Warning(L"Failed to get Virtual Video Out pin"); + return false; + } + + success = GetPinByName(encoder, PINDIR_INPUT, L"Virtual Video In", + &encoderIn); + if (!success) { + Warning(L"Failed to get encoder input pin"); + return false; + } + + success = GetPinByName(encoder, PINDIR_OUTPUT, nullptr, &encoderOut); + if (!success) { + Warning(L"Failed to get encoder output pin"); + return false; + } + + hr = graph->ConnectDirect(output->GetPin(), deviceIn, nullptr); + if (FAILED(hr)) { + WarningHR(L"Failed to connect output to device", hr); + return false; + } + + hr = graph->ConnectDirect(deviceOut, encoderIn, nullptr); + if (FAILED(hr)) { + WarningHR(L"Failed to connect device to encoder", hr); + return false; + } + + hr = graph->ConnectDirect(encoderOut, capture->GetPin(), nullptr); + if (FAILED(hr)) { + WarningHR(L"Failed to connect encoder to capture", hr); + return false; + } + + return true; +} + +static bool GetPinFirstMediaType(IPin *pin, AM_MEDIA_TYPE **mt) +{ + ComPtr mediaEnum; + HRESULT hr; + ULONG fetched; + + hr = pin->EnumMediaTypes(&mediaEnum); + if (FAILED(hr)) { + Warning(L"Failed to get pin media type enum"); + return false; + } + + if (mediaEnum->Next(1, mt, &fetched) != S_OK) { + Warning(L"Failed to get pin media type"); + return false; + } + + return true; +} + +bool HVideoEncoder::SetupCrossbar() +{ + ComPtr crossbar; + ComPtr pin; + REGPINMEDIUM medium; + + /* C353 has no crossbar */ + if (config.name.find(L"C353") != std::string::npos) + return true; + + if (!GetPinByName(device, PINDIR_INPUT, L"Analog Video In", &pin)) { + Warning(L"Failed to get Analog Video In pin"); + return false; + } + if (!GetPinMedium(pin, medium)) { + Warning(L"Failed to get Analog Video In pin medium"); + return false; + } + if (!GetFilterByMedium(AM_KSCATEGORY_CROSSBAR, medium, &crossbar)) { + Warning(L"Failed to get crossbar filter"); + return false; + } + + graph->AddFilter(crossbar, L"Crossbar Filter"); + + if (!DirectConnectFilters(graph, crossbar, device)) { + Warning(L"Failed to connect crossbar to device"); + return false; + } + + return true; +} + +bool HVideoEncoder::SetupEncoder(IBaseFilter *filter) +{ + ComPtr deviceFilter; + ComPtr inputPin; + ComPtr outputPin; + REGPINMEDIUM medium; + MediaTypePtr mtRaw; + MediaTypePtr mtEncoded; + + if (!GetPinByName(filter, PINDIR_INPUT, nullptr, &inputPin)) { + Warning(L"Could not get encoder input pin"); + return false; + } + if (!GetPinByName(filter, PINDIR_OUTPUT, nullptr, &outputPin)) { + Warning(L"Could not get encoder output pin"); + return false; + } + if (!GetPinMedium(inputPin, medium)) { + Warning(L"Could not get input pin medium"); + return false; + } + + inputPin.Release(); + + if (!GetFilterByMedium(CLSID_VideoInputDeviceCategory, medium, + &deviceFilter)) { + Warning(L"Could not get device filter from medium"); + return false; + } + if (!GetPinByName(deviceFilter, PINDIR_INPUT, L"YUV In", &inputPin)) { + Warning(L"Could not device YUV pin"); + return false; + } + if (!GetPinFirstMediaType(inputPin, &mtRaw)) { + Warning(L"Could not get YUV pin media type"); + return false; + } + if (!GetPinFirstMediaType(outputPin, &mtEncoded)) { + Warning(L"Could not get encoder output pin media type"); + return false; + } + + PinCaptureInfo captureInfo; + captureInfo.callback = [this](IMediaSample *s) { Receive(s); }; + captureInfo.expectedMajorType = mtEncoded->majortype; + captureInfo.expectedSubType = mtEncoded->subtype; + + long long frameTime; + frameTime = config.fpsDenominator; + frameTime *= 10000000; + frameTime /= config.fpsNumerator; + + encoder = filter; + device = std::move(deviceFilter); + capture = new CaptureFilter(captureInfo); + output = new OutputFilter(VideoFormat::YV12, config.cx, config.cy, + frameTime); + + graph->AddFilter(output, nullptr); + graph->AddFilter(device, L"Device Filter"); + graph->AddFilter(encoder, L"Encoder Filter"); + graph->AddFilter(capture, nullptr); + return true; +} + +static inline void Clamp(ULONG &val, ULONG minVal, ULONG maxVal) +{ + if (val < minVal) + val = minVal; + else if (val > maxVal) + val = maxVal; +} + +HRESULT SetAVMEncoderSetting(IKsPropertySet *propertySet, ULONG setting, + ULONG param1, ULONG param2) +{ + AVER_PARAMETERS params = {}; + params.ulIndex = setting; + params.ulParam1 = param1; + params.ulParam2 = param2; + + if (setting == AVER_PARAMETER_ENCODE_FRAME_RATE) { + Clamp(param1, 15, 60); + } else if (setting == AVER_PARAMETER_ENCODE_BIT_RATE) { + Clamp(param1, 1000, 60000); + } else if (setting == AVER_PARAMETER_CURRENT_RESOLUTION) { + Clamp(param1, 1, 30); + } + + return propertySet->Set(AVER_HW_ENCODE_PROPERTY, + PROPERTY_HW_ENCODE_PARAMETER, ¶ms, + sizeof(params), ¶ms, sizeof(params)); +} + +bool SetAvermediaEncoderConfig(IBaseFilter *encoder, VideoEncoderConfig &config) +{ + HRESULT hr; + + ComQIPtr propertySet(encoder); + if (!propertySet) { + Warning(L"Could not get IKsPropertySet for encoder"); + return false; + } + + double fps = + double(config.fpsNumerator) / double(config.fpsDenominator); + + hr = SetAVMEncoderSetting(propertySet, AVER_PARAMETER_ENCODE_FRAME_RATE, + ULONG(fps), 0); + if (FAILED(hr)) { + WarningHR(L"Failed to set Avermedia encoder FPS", hr); + return false; + } + + hr = SetAVMEncoderSetting(propertySet, AVER_PARAMETER_ENCODE_BIT_RATE, + ULONG(config.bitrate), 0); + if (FAILED(hr)) { + WarningHR(L"Failed to set Avermedia encoder bitrate", hr); + return false; + } + + hr = SetAVMEncoderSetting(propertySet, + AVER_PARAMETER_CURRENT_RESOLUTION, + ULONG(config.cx), ULONG(config.cy)); + if (FAILED(hr)) { + WarningHR(L"Failed to set Avermedia encoder current res", hr); + return false; + } + + hr = SetAVMEncoderSetting(propertySet, AVER_PARAMETER_ENCODE_RESOLUTION, + ULONG(config.cx), ULONG(config.cy)); + if (FAILED(hr)) { + WarningHR(L"Failed to set Avermedia encoder res", hr); + return false; + } + + hr = SetAVMEncoderSetting(propertySet, AVER_PARAMETER_ENCODE_GOP, + ULONG(config.keyframeInterval), 0); + if (FAILED(hr)) { + WarningHR(L"Failed to set Avermedia encoder GOP", hr); + return false; + } + + return true; +} + +bool HVideoEncoder::SetConfig(VideoEncoderConfig &config) +{ + ComPtr filter; + ComPtr crossbar; + + if (config.name.empty() && config.path.empty()) { + Warning(L"No video encoder name or path specified"); + return false; + } + + bool success = GetDeviceFilter(KSCATEGORY_ENCODER, config.name.c_str(), + config.path.c_str(), &filter); + if (!success) { + Warning(L"Video encoder '%s': %s not found", + config.name.c_str(), config.path.c_str()); + return false; + } + + if (!filter) { + Warning(L"Could not get encoder filter"); + return false; + } + + this->config = config; + + if (!SetupEncoder(filter)) { + Warning(L"Failed to set up encoder"); + return false; + } + if (!SetupCrossbar()) { + Warning(L"Failed to set up crossbar"); + return false; + } + if (!SetAvermediaEncoderConfig(device, config)) { + Warning(L"Failed to set Avermedia encoder settings"); + return false; + } + if (!ConnectFilters()) { + Warning(L"Failed to connect encoder filters"); + return false; + } + + LogFilters(graph); + + HRESULT hr = control->Run(); + if (FAILED(hr)) { + WarningHR(L"Run failed", hr); + return false; + } + + active = true; + return true; +} + +void HVideoEncoder::Receive(IMediaSample *s) +{ + BYTE *data; + size_t size; + + if (FAILED(s->GetPointer(&data))) + return; + + size = (size_t)s->GetActualDataLength(); + if (!size) + return; + + packetMutex.lock(); + packets.emplace_back(data, size); + packetMutex.unlock(); +} + +bool HVideoEncoder::Encode(unsigned char *data[DSHOW_MAX_PLANES], + size_t linesize[DSHOW_MAX_PLANES], + long long timestampStart, long long timestampEnd, + EncoderPacket &packet, bool &new_packet) +{ + new_packet = false; + + if (!active) + return false; + + output->Send(data, linesize, timestampStart, timestampEnd); + ptsVals.push_back(timestampStart); + + packetMutex.lock(); + if (packets.size() > 0) { + curPacket = move(packets.front()); + long long ptsOut = ptsVals[0]; + packets.pop_front(); + ptsVals.pop_front(); + + packet.data = curPacket.data.data(); + packet.size = curPacket.data.size(); + packet.pts = ptsOut; + packet.dts = ptsOut; + new_packet = true; + } + packetMutex.unlock(); + return true; +} + +}; diff --git a/obs-studio/deps/libdshowcapture/src/source/encoder.hpp b/obs-studio/deps/libdshowcapture/src/source/encoder.hpp new file mode 100644 index 0000000..b866382 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/encoder.hpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "../dshowcapture.hpp" +#include "output-filter.hpp" +#include "capture-filter.hpp" + +#include +#include +#include +#include +using namespace std; + +struct EncodedData { + vector data; + + inline EncodedData() = default; + + inline EncodedData(unsigned char *data_, size_t size) + { + data.resize(size); + memcpy(data.data(), data_, size); + } +}; + +namespace DShow { + +struct HVideoEncoder { + ComPtr graph; + ComPtr builder; + ComPtr control; + + ComPtr encoder; + ComPtr device; + ComPtr output; + ComPtr capture; + + VideoEncoderConfig config; + + mutex packetMutex; + deque packets; + EncodedData curPacket; + + deque ptsVals; + + bool initialized = false; + bool active = false; + + HVideoEncoder(); + ~HVideoEncoder(); + + bool SetupCrossbar(); + + void Receive(IMediaSample *s); + + bool ConnectFilters(); + + bool SetupEncoder(IBaseFilter *filter); + + bool SetConfig(VideoEncoderConfig &config); + + bool Encode(unsigned char *frame[DSHOW_MAX_PLANES], + size_t linesize[DSHOW_MAX_PLANES], long long timestampStart, + long long timestampEnd, EncoderPacket &packet, + bool &new_packet); +}; + +}; diff --git a/obs-studio/deps/libdshowcapture/src/source/external/.clang-format b/obs-studio/deps/libdshowcapture/src/source/external/.clang-format new file mode 100644 index 0000000..6420a46 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/external/.clang-format @@ -0,0 +1,3 @@ +Language: Cpp +SortIncludes: false +DisableFormat: true diff --git a/obs-studio/deps/libdshowcapture/src/source/external/IVideoCaptureFilter.h b/obs-studio/deps/libdshowcapture/src/source/external/IVideoCaptureFilter.h new file mode 100644 index 0000000..26d68eb --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/external/IVideoCaptureFilter.h @@ -0,0 +1,238 @@ +//============================================================================= +//! @file IVideoCaptureFilter.h +//! @bc ----------------------------------------------------------------------- +//! @ec @brief Interface declaration for Elgato Video Capture Filter +//! @author F.M.Birth, T.Schnitzler +//! @date 01-Oct-12 FMB - Creation +//! @date 08-Apr-13 TS - Added IVideoCaptureFilter2 +//! @date 14-Nov-13 TS - Added IVideoCaptureFilter3 +//! @date 21-Jul-14 TS - Added IVideoCaptureFilter4 +//! @date 12-Aug-14 TS - Added IVideoCaptureFilter5 +//! @date 28-Aug-14 FDj - MIT license added +//! @note Supports Elgato Game Capture HD +//! @bc ----------------------------------------------------------------------- +//! @ec @par Copyright +//! @n (c) 2012-14, Elgato Systems. All Rights Reserved. +//! @n +//! @n The MIT License (MIT) +//! @n +//! @n Permission is hereby granted, free of charge, to any person obtaining a copy +//! @n of this software and associated documentation files (the "Software"), to deal +//! @n in the Software without restriction, including without limitation the rights +//! @n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +//! @n copies of the Software, and to permit persons to whom the Software is +//! @n furnished to do so, subject to the following conditions: +//! @n +//! @n The above copyright notice and this permission notice shall be included in all +//! @n copies or substantial portions of the Software. +//! @n +//! @n THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +//! @n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +//! @n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +//! @n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +//! @n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +//! @n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +//! @n SOFTWARE. +//! @n +//============================================================================= + +#pragma once + +/*============================================================================= +// FILTER INTERFACE +=============================================================================*/ +#define VIDEO_CAPTURE_FILTER_NAME "Elgato Game Capture HD" +#define VIDEO_CAPTURE_FILTER_NAME_L L"Elgato Game Capture HD" + +// {39F50F4C-99E1-464a-B6F9-D605B4FB5918} +DEFINE_GUID(CLSID_ElgatoVideoCaptureFilter, +0x39f50f4c, 0x99e1, 0x464a, 0xb6, 0xf9, 0xd6, 0x5, 0xb4, 0xfb, 0x59, 0x18); + +// {39F50F4C-99E1-464a-B6F9-D605B4FB5919} +DEFINE_GUID(CLSID_ElgatoVideoCaptureFilterProperties, +0x39f50f4c, 0x99e1, 0x464a, 0xb6, 0xf9, 0xd6, 0x5, 0xb4, 0xfb, 0x59, 0x19); + +// {39F50F4C-99E1-464a-B6F9-D605B4FB5920} +DEFINE_GUID(IID_IElgatoVideoCaptureFilter, +0x39f50f4c, 0x99e1, 0x464a, 0xb6, 0xf9, 0xd6, 0x5, 0xb4, 0xfb, 0x59, 0x20); + +// {585B2914-252E-49bd-B730-7B4C40F4D4E5} +DEFINE_GUID(IID_IElgatoVideoCaptureFilter2, +0x585b2914, 0x252e, 0x49bd, 0xb7, 0x30, 0x7b, 0x4c, 0x40, 0xf4, 0xd4, 0xe5); + +// {CC415EB7-B1C7-428c-9E5E-D9747DB4BE76} +DEFINE_GUID(IID_IElgatoVideoCaptureFilter3, +0xcc415eb7, 0xb1c7, 0x428c, 0x9e, 0x5e, 0xd9, 0x74, 0x7d, 0xb4, 0xbe, 0x76); + +// {197992FF-ED65-47CB-8032-D287AB40B33F} +DEFINE_GUID(IID_IElgatoVideoCaptureFilter4, +0x197992ff, 0xed65, 0x47cb, 0x80, 0x32, 0xd2, 0x87, 0xab, 0x40, 0xb3, 0x3f); + +// {7E6E9E9E-4062-4364-99B1-15C2F662B502} +DEFINE_GUID(IID_IElgatoVideoCaptureFilter5, +0x7e6e9e9e, 0x4062, 0x4364, 0x99, 0xb1, 0x15, 0xc2, 0xf6, 0x62, 0xb5, 0x2); + +/*============================================================================= +// IElgatoVideoCaptureFilter +=============================================================================*/ + +//! Interface +DECLARE_INTERFACE_(IElgatoVideoCaptureFilter, IUnknown) +{ +}; + +/*============================================================================= +// IElgatoVideoCaptureFilter2 +=============================================================================*/ + +//! Video Capture device type +enum VIDEO_CAPTURE_FILTER_DEVICE_TYPE +{ + VIDEO_CAPTURE_FILTER_DEVICE_TYPE_INVALID = 0, //!< Invalid + VIDEO_CAPTURE_FILTER_DEVICE_TYPE_GAME_CAPTURE_HD = 2, //!< Game Capture HD (VID: 0x0fd9 PID: 0x0044, 0x004e, 0x0051) + VIDEO_CAPTURE_FILTER_DEVICE_TYPE_GAME_CAPTURE_HD60 = 8, //!< Game Capture HD60 (VID: 0x0fd9 PID: 0x005c) + NUM_VIDEO_CAPTURE_FILTER_DEVICE_TYPE +}; + +//! Input device +enum VIDEO_CAPTURE_FILTER_INPUT_DEVICE +{ + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_INVALID = 0, //!< Invalid + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_XBOX360 = 1, //!< Microsoft Xbox 360 + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_PLAYSTATION3 = 2, //!< Sony PlayStation 3 + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_IPAD = 3, //!< Apple iPad + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_IPOD_IPHONE = 4, //!< Apple iPod or iPhone + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_WII = 5, //!< Nintendo Wii + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_OTHER = 6, //!< Other + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_WII_U = 7, //!< Nintendo Wii U + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_XBOX_ONE = 8, //!< Microsoft Xbox One + VIDEO_CAPTURE_FILTER_INPUT_DEVICE_PLAYSTATION4 = 9, //!< Sony PlayStation 4 +}; + +//! Video inputs +enum VIDEO_CAPTURE_FILTER_VIDEO_INPUT +{ + VIDEO_CAPTURE_FILTER_VIDEO_INPUT_INVALID = 0, //!< Invalid + VIDEO_CAPTURE_FILTER_VIDEO_INPUT_COMPOSITE = 1, //!< Composite + VIDEO_CAPTURE_FILTER_VIDEO_INPUT_SVIDEO = 2, //!< S-Video + VIDEO_CAPTURE_FILTER_VIDEO_INPUT_COMPONENT = 3, //!< Component + VIDEO_CAPTURE_FILTER_VIDEO_INPUT_HDMI = 4, //!< HDMI +}; + +//! Video encoder profile +enum VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE +{ + VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE_INVALID = 0x00000000, //!< Invalid + VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE_240 = 0x00000001, //!< 320x240 + VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE_360 = 0x00000002, //!< 480x360 + VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE_480 = 0x00000004, //!< 640x480 + VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE_720 = 0x00000008, //!< 1280x720 + VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE_1080 = 0x00000010, //!< 1920x1080 +}; + +//! Color range +enum VIDEO_CAPTURE_FILTER_COLOR_RANGE +{ + VIDEO_CAPTURE_FILTER_COLOR_RANGE_INVALID = 0, //!< Invalid + VIDEO_CAPTURE_FILTER_COLOR_RANGE_FULL = 1, //!< 0-255 + VIDEO_CAPTURE_FILTER_COLOR_RANGE_LIMITED = 2, //!< 16-235 + VIDEO_CAPTURE_FILTER_COLOR_RANGE_SHOOT = 3, //!< +}; + +//! Settings +struct VIDEO_CAPTURE_FILTER_SETTINGS +{ + TCHAR deviceName[256]; //!< Device name (get only) + VIDEO_CAPTURE_FILTER_INPUT_DEVICE inputDevice; //!< Input device (e.g. Xbox360) + VIDEO_CAPTURE_FILTER_VIDEO_INPUT videoInput; //!< Video input (e.g. HDMI) + VIDEO_CAPTURE_FILTER_VID_ENC_PROFILE profile; //!< Video encoder profile (maximum resolution) + BOOL useAnalogAudioInput; //!< for HDMI with analog audio input + VIDEO_CAPTURE_FILTER_COLOR_RANGE hdmiColorRange; //!< HDMI color range + int brightness; //!< Brightness (0-10000) + int contrast; //!< Contrast (0-10000) + int saturation; //!< Saturation (0-10000) + int hue; //!< Hue (0-10000) + int analogAudioGain; //!< Analog audio gain (-60 - 12 dB) + int digitalAudioGain; //!< Digital audio gain (-60 - 12 dB) + BOOL preserveInputFormat; //!< Input Format will be preserved (e.g. do not convert interlaced to progressive) + BOOL stretchStandardDefinitionInput; //!< Stretch SD input to 16:9 +}; + +//! Interface +DECLARE_INTERFACE_(IElgatoVideoCaptureFilter2, IElgatoVideoCaptureFilter) +{ + // Get current settings + STDMETHOD(GetSettings)(THIS_ VIDEO_CAPTURE_FILTER_SETTINGS *pSettings) PURE; + + // Set settings + STDMETHOD(SetSettings)(THIS_ const VIDEO_CAPTURE_FILTER_SETTINGS *pcSettings) PURE; +}; + +/*============================================================================= +// IElgatoVideoCaptureFilter3 +=============================================================================*/ + +//! Interface +DECLARE_INTERFACE_(IElgatoVideoCaptureFilter3, IElgatoVideoCaptureFilter2) +{ + //! Get A/V delay in milli-seconds (approximate delay between input signal and DirectShow + //! filter output) + STDMETHOD(GetDelayMs)(THIS_ int* pnDelayMs) PURE; +}; + +/*============================================================================= +// IElgatoVideoCaptureFilter4 +=============================================================================*/ + +//! Messages +enum VIDEO_CAPTURE_FILTER_NOTIFICATION +{ + //! Description: Delay of the device has changed. Call GetDelayMs() to get the new delay. + VIDEO_CAPTURE_FILTER_NOTIFICATION_DEVICE_DELAY_CHANGED = 110, //!< Data: none + + //! Description: Output format has changed. Update your signal path accordingly. + VIDEO_CAPTURE_FILTER_NOTIFICATION_CAPTURE_OUTPUT_FORMAT_CHANGED = 305, //!< Data: none +}; + +//! Custom event that can be received by IMediaEvent::GetEvent. If SetNotificationCallback() was not set this method is used to send notifications. +//! lEventCode = VIDEO_CAPTURE_FILTER_EVENT +//! lParam1 = VIDEO_CAPTURE_FILTER_NOTIFICATION +//! lParam2 = reserved for future use (e.g. notifications with additional data) +#define VIDEO_CAPTURE_FILTER_EVENT EC_USER + 0x0FD9 + +//! Message callback +typedef void (CALLBACK* PFN_VIDEO_CAPTURE_FILTER_NOTIFICATION_CALLBACK)(VIDEO_CAPTURE_FILTER_NOTIFICATION nMessage, void* pData, int nSize, void* pContext); + +//! Interface +DECLARE_INTERFACE_(IElgatoVideoCaptureFilter4, IElgatoVideoCaptureFilter3) +{ + //! Check device is present + STDMETHOD(GetDevicePresent)(THIS_ BOOL* pfDevicePresent) PURE; + + //! Get current device type + STDMETHOD(GetDeviceType)(THIS_ VIDEO_CAPTURE_FILTER_DEVICE_TYPE* pnDeviceType) PURE; + + //! Set callback to receive notifications + STDMETHOD(SetNotificationCallback)(THIS_ PFN_VIDEO_CAPTURE_FILTER_NOTIFICATION_CALLBACK pCallback, void* pContext) PURE; +}; + +/*============================================================================= +// IElgatoVideoCaptureFilter5 +=============================================================================*/ + +//! Extended Settings +struct VIDEO_CAPTURE_FILTER_SETTINGS_EX +{ + VIDEO_CAPTURE_FILTER_SETTINGS Settings; + BOOL enableFullFrameRate; //!< Enable full frame rate (50/60 fps) + BYTE reserved[20 * 1024]; +}; + +DECLARE_INTERFACE_(IElgatoVideoCaptureFilter5, IElgatoVideoCaptureFilter4) +{ + //! Get current settings + STDMETHOD(GetSettingsEx)(THIS_ VIDEO_CAPTURE_FILTER_SETTINGS_EX *pSettings) PURE; + + //! Set settings + STDMETHOD(SetSettingsEx)(THIS_ const VIDEO_CAPTURE_FILTER_SETTINGS_EX *pcSettings) PURE; +}; diff --git a/obs-studio/deps/libdshowcapture/src/source/log.cpp b/obs-studio/deps/libdshowcapture/src/source/log.cpp new file mode 100644 index 0000000..1589a5a --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/log.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "dshow-base.hpp" +#include "log.hpp" +#include "../dshowcapture.hpp" + +namespace DShow { + +void *logParam = NULL; +static LogCallback logCallback = NULL; + +void SetLogCallback(LogCallback callback, void *param) +{ + logCallback = callback; + logParam = param; +} + +static void Log(LogType type, const wchar_t *format, va_list args) +{ + wchar_t str[4096]; + vswprintf_s(str, 4096, format, args); + + if (logCallback) + logCallback(type, str, logParam); +} + +void Error(const wchar_t *format, ...) +{ + va_list args; + va_start(args, format); + Log(LogType::Error, format, args); + va_end(args); +} + +void Warning(const wchar_t *format, ...) +{ + va_list args; + va_start(args, format); + Log(LogType::Warning, format, args); + va_end(args); +} + +void Info(const wchar_t *format, ...) +{ + va_list args; + va_start(args, format); + Log(LogType::Info, format, args); + va_end(args); +} + +void Debug(const wchar_t *format, ...) +{ + va_list args; + va_start(args, format); + Log(LogType::Debug, format, args); + va_end(args); +} + +void ErrorHR(const wchar_t *str, HRESULT hr) +{ + Error(L"%s (0x%08lX): %s", str, hr, ConvertHRToEnglish(hr).c_str()); +} + +void WarningHR(const wchar_t *str, HRESULT hr) +{ + Warning(L"%s (0x%08lX): %s", str, hr, ConvertHRToEnglish(hr).c_str()); +} + +void InfoHR(const wchar_t *str, HRESULT hr) +{ + Info(L"%s (0x%08lX): %s", str, hr, ConvertHRToEnglish(hr).c_str()); +} + +void DebugHR(const wchar_t *str, HRESULT hr) +{ + Debug(L"%s (0x%08lX): %s", str, hr, ConvertHRToEnglish(hr).c_str()); +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/log.hpp b/obs-studio/deps/libdshowcapture/src/source/log.hpp new file mode 100644 index 0000000..7240c09 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/log.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include "windows.h" + +namespace DShow { + +void Error(const wchar_t *format, ...); +void Warning(const wchar_t *format, ...); +void Info(const wchar_t *format, ...); +void Debug(const wchar_t *format, ...); + +void ErrorHR(const wchar_t *str, HRESULT hr); +void WarningHR(const wchar_t *str, HRESULT hr); +void InfoHR(const wchar_t *str, HRESULT hr); +void DebugHR(const wchar_t *str, HRESULT hr); + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/output-filter.cpp b/obs-studio/deps/libdshowcapture/src/source/output-filter.cpp new file mode 100644 index 0000000..fe91463 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/output-filter.cpp @@ -0,0 +1,992 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#include "output-filter.hpp" +#include "dshow-formats.hpp" +#include "log.hpp" + +#include + +namespace DShow { + +#if 0 +#define PrintFunc(x) Debug(x) +#else +#define PrintFunc(x) +#endif + +#define FILTER_NAME L"Output Filter" +#define VIDEO_PIN_NAME L"Video Output" +#define AUDIO_PIN_NAME L"Audio Output" + +OutputPin::OutputPin(OutputFilter *filter_) : refCount(0), filter(filter_) {} + +OutputPin::OutputPin(OutputFilter *filter_, VideoFormat format, int cx, int cy, + long long interval) + : OutputPin(filter_) +{ + curCX = cx; + curCY = cy; + curInterval = interval; + + AddVideoFormat(format, cx, cy, interval); + SetVideoFormat(format, cx, cy, interval); +} + +OutputPin::~OutputPin() {} + +STDMETHODIMP OutputPin::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else if (riid == IID_IPin) { + AddRef(); + *ppv = (IPin *)this; + } else if (riid == IID_IMemInputPin) { + AddRef(); + *ppv = (IMemInputPin *)this; + } else if (riid == IID_IAMStreamConfig) { + AddRef(); + *ppv = (IAMStreamConfig *)this; + return S_OK; + } else if (riid == IID_IKsPropertySet) { + AddRef(); + *ppv = (IKsPropertySet *)this; + return S_OK; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; +} + +STDMETHODIMP_(ULONG) OutputPin::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) OutputPin::Release() +{ + long newRefs = InterlockedDecrement(&refCount); + if (!newRefs) { + delete this; + return 0; + } + + return (ULONG)newRefs; +} + +// IPin methods +STDMETHODIMP OutputPin::Connect(IPin *pReceivePin, const AM_MEDIA_TYPE *pmt) +{ + HRESULT hr; + + PrintFunc(L"OutputPin::Connect"); + + if (filter->state == State_Running) + return VFW_E_NOT_STOPPED; + + if (connectedPin) + return VFW_E_ALREADY_CONNECTED; + + hr = pReceivePin->ReceiveConnection(this, mt); + if (FAILED(hr)) { +#if 0 /* debug code to test caps on fail */ + ComPtr enumMT; + pReceivePin->EnumMediaTypes(&enumMT); + + if (enumMT) { + MediaTypePtr mt; + ULONG count = 0; + + while (enumMT->Next(1, &mt, &count) == S_OK) { + int test = 0; + test = 1; + } + } +#endif + return E_FAIL; + } + + if (!AllocateBuffers(pReceivePin, true)) { + return E_FAIL; + } + + connectedPin = pReceivePin; + DSHOW_UNUSED(pmt); + return S_OK; +} + +STDMETHODIMP OutputPin::ReceiveConnection(IPin *pConnector, + const AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"OutputPin::ReceiveConnection"); + + DSHOW_UNUSED(pConnector); + DSHOW_UNUSED(pmt); + return S_OK; +} + +STDMETHODIMP OutputPin::Disconnect() +{ + PrintFunc(L"OutputPin::Disconnect"); + + if (!connectedPin) + return S_FALSE; + + if (!!allocator) { + allocator->Decommit(); + allocator.Clear(); + } + + connectedPin = nullptr; + return S_OK; +} + +STDMETHODIMP OutputPin::ConnectedTo(IPin **pPin) +{ + PrintFunc(L"OutputPin::ConnectedTo"); + + if (!connectedPin) { + *pPin = nullptr; + return VFW_E_NOT_CONNECTED; + } + + IPin *pin = connectedPin; + pin->AddRef(); + *pPin = pin; + return S_OK; +} + +STDMETHODIMP OutputPin::ConnectionMediaType(AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"OutputPin::ConnectionMediaType"); + + if (!connectedPin) + return VFW_E_NOT_CONNECTED; + + return CopyMediaType(pmt, mt); +} + +STDMETHODIMP OutputPin::QueryPinInfo(PIN_INFO *pInfo) +{ + PrintFunc(L"OutputPin::QueryPinInfo"); + + pInfo->pFilter = filter; + if (filter) { + IBaseFilter *ptr = filter; + ptr->AddRef(); + } + + if (mt->majortype == MEDIATYPE_Video) + memcpy(pInfo->achName, VIDEO_PIN_NAME, sizeof(VIDEO_PIN_NAME)); + else + memcpy(pInfo->achName, AUDIO_PIN_NAME, sizeof(AUDIO_PIN_NAME)); + + pInfo->dir = PINDIR_OUTPUT; + + return NOERROR; +} + +STDMETHODIMP OutputPin::QueryDirection(PIN_DIRECTION *pPinDir) +{ + *pPinDir = PINDIR_OUTPUT; + return NOERROR; +} + +#define OUTPUT_PIN_NAME L"Output Pin" + +STDMETHODIMP OutputPin::QueryId(LPWSTR *lpId) +{ + wchar_t *str = (wchar_t *)CoTaskMemAlloc(sizeof(OUTPUT_PIN_NAME)); + memcpy(str, OUTPUT_PIN_NAME, sizeof(OUTPUT_PIN_NAME)); + *lpId = str; + return S_OK; +} + +STDMETHODIMP OutputPin::QueryAccept(const AM_MEDIA_TYPE *) +{ + PrintFunc(L"OutputPin::QueryAccept"); + + return S_OK; +} + +STDMETHODIMP OutputPin::EnumMediaTypes(IEnumMediaTypes **ppEnum) +{ + PrintFunc(L"OutputPin::EnumMediaTypes"); + + *ppEnum = new OutputEnumMediaTypes(this); + if (!*ppEnum) + return E_OUTOFMEMORY; + + return NOERROR; +} + +STDMETHODIMP OutputPin::QueryInternalConnections(IPin **apPin, ULONG *nPin) +{ + PrintFunc(L"OutputPin::QueryInternalConnections"); + + DSHOW_UNUSED(apPin); + DSHOW_UNUSED(nPin); + return E_NOTIMPL; +} + +STDMETHODIMP OutputPin::EndOfStream() +{ + PrintFunc(L"OutputPin::EndOfStream"); + + return S_OK; +} + +STDMETHODIMP OutputPin::BeginFlush() +{ + PrintFunc(L"OutputPin::BeginFlush"); + + flushing = true; + return S_OK; +} + +STDMETHODIMP OutputPin::EndFlush() +{ + PrintFunc(L"OutputPin::EndFlush"); + + flushing = false; + return S_OK; +} + +STDMETHODIMP OutputPin::NewSegment(REFERENCE_TIME, REFERENCE_TIME, double) +{ + PrintFunc(L"OutputPin::NewSegment"); + + return S_OK; +} + +STDMETHODIMP OutputPin::GetFormat(AM_MEDIA_TYPE **ppmt) +{ + PrintFunc(L"OutputPin::GetFormat"); + + if (!ppmt) { + return E_POINTER; + } + + *ppmt = mt.Duplicate(); + return S_OK; +} + +STDMETHODIMP OutputPin::GetNumberOfCapabilities(int *piCount, int *piSize) +{ + PrintFunc(L"OutputPin::GetNumberOfCapabilities"); + + if (!piCount || !piSize) { + return E_POINTER; + } + + *piCount = (int)mtList.size(); + *piSize = sizeof(VIDEO_STREAM_CONFIG_CAPS); + return S_OK; +} + +STDMETHODIMP OutputPin::GetStreamCaps(int iIndex, AM_MEDIA_TYPE **ppmt, + BYTE *pSCC) +{ + PrintFunc(L"OutputPin::GetStreamCaps"); + + int count = (int)mtList.size(); + + if (!ppmt || !pSCC) { + return E_POINTER; + } + if (iIndex > (count - 1)) { + return S_FALSE; + } + if (iIndex < 0) { + return E_INVALIDARG; + } + + AM_MEDIA_TYPE *pmt = mtList[iIndex].Duplicate(); + VIDEOINFOHEADER *vih = reinterpret_cast(pmt->pbFormat); + + VIDEO_STREAM_CONFIG_CAPS caps = {}; + caps.guid = FORMAT_VideoInfo; + caps.MinFrameInterval = vih->AvgTimePerFrame; + caps.MaxFrameInterval = vih->AvgTimePerFrame; + caps.MinOutputSize.cx = vih->bmiHeader.biWidth; + caps.MinOutputSize.cy = vih->bmiHeader.biHeight; + caps.MaxOutputSize = caps.MinOutputSize; + caps.InputSize = caps.MinOutputSize; + caps.MinCroppingSize = caps.MinOutputSize; + caps.MaxCroppingSize = caps.MinOutputSize; + caps.CropGranularityX = vih->bmiHeader.biWidth; + caps.CropGranularityY = vih->bmiHeader.biHeight; + caps.MinBitsPerSecond = vih->dwBitRate; + caps.MaxBitsPerSecond = caps.MinBitsPerSecond; + + *ppmt = pmt; + + memcpy(pSCC, &caps, sizeof(caps)); + return S_OK; +} + +STDMETHODIMP OutputPin::SetFormat(AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"OutputPin::SetFormat"); + + if (pmt == nullptr) + return VFW_E_INVALIDMEDIATYPE; + + mt = pmt; + + GetMediaTypeVFormat(mt, curVFormat); + + VIDEOINFOHEADER *vih = reinterpret_cast(mt->pbFormat); + curCX = vih->bmiHeader.biWidth; + curCY = vih->bmiHeader.biHeight; + curInterval = vih->AvgTimePerFrame; + + return S_OK; +} + +STDMETHODIMP OutputPin::Set(REFGUID, DWORD, void *, DWORD, void *, DWORD) +{ + PrintFunc(L"OutputPin::Set"); + return E_NOTIMPL; +} + +STDMETHODIMP OutputPin::Get(REFGUID guidPropSet, DWORD dwPropID, void *, DWORD, + void *pPropData, DWORD cbPropData, + DWORD *pcbReturned) +{ + PrintFunc(L"OutputPin::Get"); + + if (guidPropSet != AMPROPSETID_Pin) + return E_PROP_SET_UNSUPPORTED; + if (dwPropID != AMPROPERTY_PIN_CATEGORY) + return E_PROP_ID_UNSUPPORTED; + if (pPropData == NULL && pcbReturned == NULL) + return E_POINTER; + + if (pcbReturned) + *pcbReturned = sizeof(GUID); + if (pPropData == NULL) + return S_OK; + if (cbPropData < sizeof(GUID)) + return E_UNEXPECTED; + + *(GUID *)pPropData = PIN_CATEGORY_CAPTURE; + return S_OK; +} + +STDMETHODIMP OutputPin::QuerySupported(REFGUID guidPropSet, DWORD dwPropID, + DWORD *pTypeSupport) +{ + PrintFunc(L"OutputPin::QuerySupported"); + + if (guidPropSet != AMPROPSETID_Pin) + return E_PROP_SET_UNSUPPORTED; + if (dwPropID != AMPROPERTY_PIN_CATEGORY) + return E_PROP_ID_UNSUPPORTED; + if (pTypeSupport) + *pTypeSupport = KSPROPERTY_SUPPORT_GET; + return S_OK; +} + +bool OutputPin::AllocateBuffers(IPin *target, bool connecting) +{ + HRESULT hr; + + ComQIPtr memInput(target); + if (!memInput) + return false; + + if (!!allocator) { + allocator->Decommit(); + } + + hr = memInput->GetAllocator(&allocator); + if (hr == VFW_E_NO_ALLOCATOR) + hr = CoCreateInstance(CLSID_MemoryAllocator, NULL, + CLSCTX_INPROC_SERVER, + __uuidof(IMemAllocator), + (void **)&allocator); + + if (FAILED(hr)) + return false; + + VIDEOINFOHEADER *vih = reinterpret_cast(mt->pbFormat); + + int cx = vih->bmiHeader.biWidth; + int cy = vih->bmiHeader.biHeight; + + WORD bits = VFormatBits(curVFormat); + bufSize = cx * cy * bits / 8; + + ALLOCATOR_PROPERTIES props; + + hr = memInput->GetAllocatorRequirements(&props); + if (hr == E_NOTIMPL) { + props.cBuffers = 4; + props.cbAlign = 32; + props.cbPrefix = 0; + + } else if (FAILED(hr)) { + return false; + } + + props.cbBuffer = (long)bufSize; + + ALLOCATOR_PROPERTIES actual; + hr = allocator->SetProperties(&props, &actual); + if (FAILED(hr)) + return false; + + if (!connecting && FAILED(allocator->Commit())) { + return false; + } + + memInput->NotifyAllocator(allocator, false); + return true; +} + +static MediaType CreateMediaType(VideoFormat format, int cx, int cy, + long long interval) +{ + MediaType mt; + + WORD bits = VFormatBits(format); + DWORD size = cx * cy * bits / 8; + uint64_t rate = + (uint64_t)size * 10000000ULL / (uint64_t)interval * 8ULL; + + VIDEOINFOHEADER *vih = mt.AllocFormat(); + vih->bmiHeader.biSize = sizeof(vih->bmiHeader); + vih->bmiHeader.biWidth = cx; + vih->bmiHeader.biHeight = cy; + vih->bmiHeader.biPlanes = VFormatPlanes(format); + vih->bmiHeader.biBitCount = bits; + vih->bmiHeader.biSizeImage = size; + vih->bmiHeader.biCompression = VFormatToFourCC(format); + vih->rcSource.right = cx; + vih->rcSource.bottom = cy; + vih->rcTarget = vih->rcSource; + vih->dwBitRate = (DWORD)rate; + vih->AvgTimePerFrame = interval; + + mt->majortype = MEDIATYPE_Video; + mt->subtype = VFormatToSubType(format); + mt->formattype = FORMAT_VideoInfo; + mt->bFixedSizeSamples = true; + mt->lSampleSize = size; + + return mt; +} + +void OutputPin::AddVideoFormat(VideoFormat format, int cx, int cy, + long long interval) +{ + MediaType newMT = CreateMediaType(format, cx, cy, interval); + mtList.push_back(newMT); +} + +bool OutputPin::SetVideoFormat(VideoFormat format, int cx, int cy, + long long interval) +{ + mt = CreateMediaType(format, cx, cy, interval); + + if (curCX != cx || curCY != cy || curInterval != interval || + curVFormat != format) { + curVFormat = format; + curCX = cx; + curCY = cy; + curInterval = interval; + + if (!!connectedPin) { + setSampleMediaType = true; + return ReallocateBuffers(); + } + } + + return true; +} + +bool OutputPin::LockSampleData(unsigned char **ptr) +{ + if (!connectedPin) + return false; + + ComQIPtr memInput(connectedPin); + HRESULT hr; + + if (!memInput || !allocator) + return false; + + hr = allocator->GetBuffer(&sample, nullptr, nullptr, 0); + if (FAILED(hr)) + return false; + + if (FAILED(sample->SetActualDataLength((long)bufSize))) + return false; + if (FAILED(sample->SetDiscontinuity(false))) + return false; + if (FAILED(sample->SetPreroll(false))) + return false; + if (FAILED(sample->SetSyncPoint(true))) + return false; + if (FAILED(sample->GetPointer(ptr))) + return false; + + if (setSampleMediaType) { + sample->SetMediaType(mt); + setSampleMediaType = false; + } + + return true; +} + +void OutputPin::Send(unsigned char *data[DSHOW_MAX_PLANES], + size_t linesize[DSHOW_MAX_PLANES], + long long timestampStart, long long timestampEnd) +{ + BYTE *ptr; + if (!LockSampleData(&ptr)) + return; + + size_t total = 0; + for (size_t i = 0; i < DSHOW_MAX_PLANES; i++) { + if (!linesize[i]) + break; + + memcpy(ptr + total, data[i], linesize[i]); + total += linesize[i]; + } + + UnlockSampleData(timestampStart, timestampEnd); +} + +void OutputPin::UnlockSampleData(long long timestampStart, + long long timestampEnd) +{ + if (!connectedPin) + return; + + ComQIPtr memInput(connectedPin); + REFERENCE_TIME startTime = timestampStart; + REFERENCE_TIME endTime = timestampEnd; + + sample->SetMediaTime(&startTime, &endTime); + sample->SetTime(&startTime, &endTime); + + memInput->Receive(sample); + + sample.Clear(); +} + +void OutputPin::Stop() +{ + if (!!connectedPin) { + connectedPin->BeginFlush(); + connectedPin->EndFlush(); + } +} + +// ============================================================================ + +class SourceMiscFlags : public IAMFilterMiscFlags { + volatile long refCount = 0; + +public: + inline SourceMiscFlags() {} + virtual ~SourceMiscFlags() {} + + STDMETHODIMP QueryInterface(REFIID riid, void **ppv) + { + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; + } + + STDMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&refCount); + } + + STDMETHODIMP_(ULONG) Release() + { + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return refCount; + } + + STDMETHODIMP_(ULONG) GetMiscFlags() + { + return AM_FILTER_MISC_FLAGS_IS_SOURCE; + } +}; + +OutputFilter::OutputFilter() + : refCount(0), + state(State_Stopped), + graph(nullptr), + pin(new OutputPin(this)), + misc(new SourceMiscFlags) +{ +} + +OutputFilter::OutputFilter(VideoFormat format, int cx, int cy, + long long interval) + : refCount(0), + state(State_Stopped), + graph(nullptr), + pin(new OutputPin(this, format, cx, cy, interval)), + misc(new SourceMiscFlags) +{ +} + +OutputFilter::~OutputFilter() {} + +// IUnknown methods +STDMETHODIMP OutputFilter::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else if (riid == IID_IPersist) { + AddRef(); + *ppv = (IPersist *)this; + } else if (riid == IID_IMediaFilter) { + AddRef(); + *ppv = (IMediaFilter *)this; + } else if (riid == IID_IBaseFilter) { + AddRef(); + *ppv = (IBaseFilter *)this; + } else if (riid == IID_IAMFilterMiscFlags) { + misc.CopyTo((IAMFilterMiscFlags **)ppv); + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; +} + +STDMETHODIMP_(ULONG) OutputFilter::AddRef() +{ + return InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) OutputFilter::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return refCount; +} + +// IPersist method +STDMETHODIMP OutputFilter::GetClassID(CLSID *pClsID) +{ + DSHOW_UNUSED(pClsID); + return E_NOTIMPL; +} + +// IMediaFilter methods +STDMETHODIMP OutputFilter::GetState(DWORD dwMSecs, FILTER_STATE *State) +{ + PrintFunc(L"OutputFilter::GetState"); + + *State = state; + + DSHOW_UNUSED(dwMSecs); + return S_OK; +} + +STDMETHODIMP OutputFilter::SetSyncSource(IReferenceClock *pClock) +{ + clock = pClock; + return S_OK; +} + +STDMETHODIMP OutputFilter::GetSyncSource(IReferenceClock **pClock) +{ + *pClock = clock.Get(); + if (*pClock) { + (*pClock)->AddRef(); + } + return NOERROR; +} + +STDMETHODIMP OutputFilter::Stop() +{ + PrintFunc(L"OutputFilter::Stop"); + + if (state != State_Stopped) { + pin->Stop(); + } + + state = State_Stopped; + return S_OK; +} + +STDMETHODIMP OutputFilter::Pause() +{ + PrintFunc(L"OutputFilter::Pause"); + + OutputPin *pin = GetPin(); + if (!!pin->allocator && state == State_Stopped) { + pin->allocator->Commit(); + } + + state = State_Paused; + return S_OK; +} + +STDMETHODIMP OutputFilter::Run(REFERENCE_TIME tStart) +{ + PrintFunc(L"OutputFilter::Run"); + + state = State_Running; + + DSHOW_UNUSED(tStart); + return S_OK; +} + +// IBaseFilter methods +STDMETHODIMP OutputFilter::EnumPins(IEnumPins **ppEnum) +{ + PrintFunc(L"OutputFilter::EnumPins"); + + *ppEnum = new OutputEnumPins(this, nullptr); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +STDMETHODIMP OutputFilter::FindPin(LPCWSTR Id, IPin **ppPin) +{ + PrintFunc(L"OutputFilter::FindPin"); + + if (Id == nullptr || ppPin == nullptr) + return E_POINTER; + + if (lstrcmpW(Id, OUTPUT_PIN_NAME) == 0) { + *ppPin = pin; + pin->AddRef(); + return S_OK; + } else { + *ppPin = nullptr; + return VFW_E_NOT_FOUND; + } +} + +STDMETHODIMP OutputFilter::QueryFilterInfo(FILTER_INFO *pInfo) +{ + PrintFunc(L"OutputFilter::QueryFilterInfo"); + + StringCbCopyW(pInfo->achName, sizeof(pInfo->achName), FilterName()); + + pInfo->pGraph = graph; + if (graph) { + IFilterGraph *graph_ptr = graph; + graph_ptr->AddRef(); + } + return NOERROR; +} + +STDMETHODIMP OutputFilter::JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName) +{ + DSHOW_UNUSED(pName); + + graph = pGraph; + return NOERROR; +} + +STDMETHODIMP OutputFilter::QueryVendorInfo(LPWSTR *pVendorInfo) +{ + DSHOW_UNUSED(pVendorInfo); + return E_NOTIMPL; +} + +const wchar_t *OutputFilter::FilterName() const +{ + return FILTER_NAME; +} + +// ============================================================================ + +OutputEnumPins::OutputEnumPins(OutputFilter *filter_, OutputEnumPins *pEnum) + : filter(filter_) +{ + curPin = (pEnum != nullptr) ? pEnum->curPin : 0; +} + +OutputEnumPins::~OutputEnumPins() {} + +// IUnknown +STDMETHODIMP OutputEnumPins::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown || riid == IID_IEnumPins) { + AddRef(); + *ppv = (IEnumPins *)this; + return NOERROR; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } +} + +STDMETHODIMP_(ULONG) OutputEnumPins::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) OutputEnumPins::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IEnumPins +STDMETHODIMP OutputEnumPins::Next(ULONG cPins, IPin **ppPins, ULONG *pcFetched) +{ + UINT nFetched = 0; + + if (curPin == 0 && cPins > 0) { + IPin *pPin = filter->GetPin(); + + *ppPins = pPin; + pPin->AddRef(); + + nFetched = 1; + curPin++; + } + + if (pcFetched) + *pcFetched = nFetched; + + return (nFetched == cPins) ? S_OK : S_FALSE; +} + +STDMETHODIMP OutputEnumPins::Skip(ULONG cPins) +{ + return ((curPin += cPins) > 1) ? S_FALSE : S_OK; +} + +STDMETHODIMP OutputEnumPins::Reset() +{ + curPin = 0; + return S_OK; +} + +STDMETHODIMP OutputEnumPins::Clone(IEnumPins **ppEnum) +{ + *ppEnum = new OutputEnumPins(filter, this); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +// ============================================================================ + +OutputEnumMediaTypes::OutputEnumMediaTypes(OutputPin *pin_) : pin(pin_) {} + +OutputEnumMediaTypes::~OutputEnumMediaTypes() {} + +STDMETHODIMP OutputEnumMediaTypes::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown || riid == IID_IEnumMediaTypes) { + AddRef(); + *ppv = this; + return NOERROR; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } +} + +STDMETHODIMP_(ULONG) OutputEnumMediaTypes::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) OutputEnumMediaTypes::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IEnumMediaTypes +STDMETHODIMP OutputEnumMediaTypes::Next(ULONG cMediaTypes, + AM_MEDIA_TYPE **ppMediaTypes, + ULONG *pcFetched) +{ + PrintFunc(L"OutputEnumMediaTypes::Next"); + + UINT total = (UINT)pin->mtList.size(); + UINT nFetched = 0; + + for (ULONG i = 0; i < cMediaTypes && curMT < total; i++) { + *(ppMediaTypes++) = pin->mtList[curMT++].Duplicate(); + nFetched++; + } + + if (pcFetched) + *pcFetched = nFetched; + + return (nFetched == cMediaTypes) ? S_OK : S_FALSE; +} + +STDMETHODIMP OutputEnumMediaTypes::Skip(ULONG cMediaTypes) +{ + PrintFunc(L"OutputEnumMediaTypes::Skip"); + + UINT total = (UINT)pin->mtList.size(); + return ((curMT += cMediaTypes) > total) ? S_FALSE : S_OK; +} + +STDMETHODIMP OutputEnumMediaTypes::Reset() +{ + PrintFunc(L"OutputEnumMediaTypes::Reset"); + + curMT = 0; + return S_OK; +} + +STDMETHODIMP OutputEnumMediaTypes::Clone(IEnumMediaTypes **ppEnum) +{ + *ppEnum = new OutputEnumMediaTypes(pin); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/source/output-filter.hpp b/obs-studio/deps/libdshowcapture/src/source/output-filter.hpp new file mode 100644 index 0000000..d6c6226 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/source/output-filter.hpp @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2023 Lain Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once + +#include "dshow-base.hpp" +#include "dshow-media-type.hpp" +#include "../dshowcapture.hpp" + +namespace DShow { + +class OutputFilter; + +class OutputPin : public IPin, public IAMStreamConfig, public IKsPropertySet { + friend class OutputEnumMediaTypes; + friend class OutputFilter; + + volatile long refCount; + + std::vector mtList; + + MediaType mt; + VideoFormat curVFormat; + long long curInterval = 0; + int curCX = 0; + int curCY = 0; + bool setSampleMediaType = false; + + ComPtr connectedPin; + OutputFilter *filter; + volatile bool flushing = false; + ComPtr allocator; + ComPtr sample; + size_t bufSize; + + bool IsValidMediaType(const AM_MEDIA_TYPE *pmt) const; + + bool AllocateBuffers(IPin *target, bool connecting = false); + +public: + OutputPin(OutputFilter *filter); + OutputPin(OutputFilter *filter, VideoFormat format, int cx, int cy, + long long interval); + virtual ~OutputPin(); + + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IPin methods + STDMETHODIMP Connect(IPin *pReceivePin, const AM_MEDIA_TYPE *pmt); + STDMETHODIMP ReceiveConnection(IPin *connector, + const AM_MEDIA_TYPE *pmt); + STDMETHODIMP Disconnect(); + STDMETHODIMP ConnectedTo(IPin **pPin); + STDMETHODIMP ConnectionMediaType(AM_MEDIA_TYPE *pmt); + STDMETHODIMP QueryPinInfo(PIN_INFO *pInfo); + STDMETHODIMP QueryDirection(PIN_DIRECTION *pPinDir); + STDMETHODIMP QueryId(LPWSTR *lpId); + STDMETHODIMP QueryAccept(const AM_MEDIA_TYPE *pmt); + STDMETHODIMP EnumMediaTypes(IEnumMediaTypes **ppEnum); + STDMETHODIMP QueryInternalConnections(IPin **apPin, ULONG *nPin); + STDMETHODIMP EndOfStream(); + + STDMETHODIMP BeginFlush(); + STDMETHODIMP EndFlush(); + STDMETHODIMP NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, + double dRate); + + // IAMStreamConfig methods + STDMETHODIMP GetFormat(AM_MEDIA_TYPE **ppmt) override; + STDMETHODIMP GetNumberOfCapabilities(int *piCount, + int *piSize) override; + STDMETHODIMP GetStreamCaps(int iIndex, AM_MEDIA_TYPE **ppmt, + BYTE *pSCC) override; + STDMETHODIMP SetFormat(AM_MEDIA_TYPE *pmt) override; + + // IKsPropertySet methods + STDMETHODIMP Set(REFGUID guidPropSet, DWORD dwID, void *pInstanceData, + DWORD cbInstanceData, void *pPropData, + DWORD cbPropData) override; + + STDMETHODIMP Get(REFGUID guidPropSet, DWORD dwPropID, + void *pInstanceData, DWORD cbInstanceData, + void *pPropData, DWORD cbPropData, + DWORD *pcbReturned) override; + + STDMETHODIMP QuerySupported(REFGUID guidPropSet, DWORD dwPropID, + DWORD *pTypeSupport) override; + + // Other methods + inline bool ReallocateBuffers() + { + return !!connectedPin ? AllocateBuffers(connectedPin) : false; + } + + inline VideoFormat GetVideoFormat() const { return curVFormat; } + inline int GetCX() const { return curCX; } + inline int GetCY() const { return curCY; } + inline long long GetInterval() const { return curInterval; } + + void AddVideoFormat(VideoFormat format, int cx, int cy, + long long interval); + bool SetVideoFormat(VideoFormat format, int cx, int cy, + long long interval); + + void Send(unsigned char *data[DSHOW_MAX_PLANES], + size_t linesize[DSHOW_MAX_PLANES], long long timestampStart, + long long timestampEnd); + + bool LockSampleData(unsigned char **ptr); + void UnlockSampleData(long long timestampStart, long long timestampEnd); + + void Stop(); +}; + +class OutputFilter : public IBaseFilter { + friend class OutputPin; + + volatile long refCount; + FILTER_STATE state; + IFilterGraph *graph; + ComPtr pin; + + ComPtr misc; + +protected: + ComPtr clock; + +public: + OutputFilter(); + OutputFilter(VideoFormat format, int cx, int cy, long long interval); + virtual ~OutputFilter(); + + // IUnknown methods + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IPersist method + STDMETHODIMP GetClassID(CLSID *pClsID); + + // IMediaFilter methods + STDMETHODIMP GetState(DWORD dwMSecs, FILTER_STATE *State); + STDMETHODIMP SetSyncSource(IReferenceClock *pClock); + STDMETHODIMP GetSyncSource(IReferenceClock **pClock); + STDMETHODIMP Stop(); + STDMETHODIMP Pause(); + STDMETHODIMP Run(REFERENCE_TIME tStart); + + // IBaseFilter methods + STDMETHODIMP EnumPins(IEnumPins **ppEnum); + STDMETHODIMP FindPin(LPCWSTR Id, IPin **ppPin); + STDMETHODIMP QueryFilterInfo(FILTER_INFO *pInfo); + STDMETHODIMP JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName); + STDMETHODIMP QueryVendorInfo(LPWSTR *pVendorInfo); + + virtual const wchar_t *FilterName() const; + + inline OutputPin *GetPin() const { return (OutputPin *)pin; } + + inline bool ReallocateBuffers() { return pin->ReallocateBuffers(); } + + inline VideoFormat GetVideoFormat() const + { + return pin->GetVideoFormat(); + } + + inline int GetCX() const { return pin->GetCX(); } + inline int GetCY() const { return pin->GetCY(); } + inline long long GetInterval() const { return pin->GetInterval(); } + + inline void AddVideoFormat(VideoFormat format, int cx, int cy, + long long interval) + { + pin->AddVideoFormat(format, cx, cy, interval); + } + inline bool SetVideoFormat(VideoFormat format, int cx, int cy, + long long interval) + { + return pin->SetVideoFormat(format, cx, cy, interval); + } + + inline void Send(unsigned char *data[DSHOW_MAX_PLANES], + size_t linesize[DSHOW_MAX_PLANES], + long long timestampStart, long long timestampEnd) + { + pin->Send(data, linesize, timestampStart, timestampEnd); + } + + inline bool LockSampleData(unsigned char **ptr) + { + return pin->LockSampleData(ptr); + } + + inline void UnlockSampleData(long long timestampStart, + long long timestampEnd) + { + pin->UnlockSampleData(timestampStart, timestampEnd); + } +}; + +class OutputEnumPins : public IEnumPins { + volatile long refCount = 1; + ComPtr filter; + UINT curPin; + +public: + OutputEnumPins(OutputFilter *filter, OutputEnumPins *pEnum); + virtual ~OutputEnumPins(); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IEnumPins + STDMETHODIMP Next(ULONG cPins, IPin **ppPins, ULONG *pcFetched); + STDMETHODIMP Skip(ULONG cPins); + STDMETHODIMP Reset(); + STDMETHODIMP Clone(IEnumPins **ppEnum); +}; + +class OutputEnumMediaTypes : public IEnumMediaTypes { + volatile long refCount = 1; + ComPtr pin; + UINT curMT = 0; + +public: + OutputEnumMediaTypes(OutputPin *pin); + virtual ~OutputEnumMediaTypes(); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IEnumMediaTypes + STDMETHODIMP Next(ULONG cMediaTypes, AM_MEDIA_TYPE **ppMediaTypes, + ULONG *pcFetched); + STDMETHODIMP Skip(ULONG cMediaTypes); + STDMETHODIMP Reset(); + STDMETHODIMP Clone(IEnumMediaTypes **ppEnum); +}; + +}; /* namespace DShow */ diff --git a/obs-studio/deps/libdshowcapture/src/tests/.gitignore b/obs-studio/deps/libdshowcapture/src/tests/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture.sln b/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture.sln new file mode 100644 index 0000000..b127de3 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "dshowcapture", "dshowcapture\dshowcapture.vcxproj", "{FFF52519-38BB-4155-851D-4209EC67ACA7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Debug|Win32.ActiveCfg = Debug|Win32 + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Debug|Win32.Build.0 = Debug|Win32 + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Debug|x64.ActiveCfg = Debug|x64 + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Debug|x64.Build.0 = Debug|x64 + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Release|Win32.ActiveCfg = Release|Win32 + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Release|Win32.Build.0 = Release|Win32 + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Release|x64.ActiveCfg = Release|x64 + {FFF52519-38BB-4155-851D-4209EC67ACA7}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj b/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj new file mode 100644 index 0000000..7917122 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj @@ -0,0 +1,179 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + {FFF52519-38BB-4155-851D-4209EC67ACA7} + Win32Proj + dshowcapture + + + + DynamicLibrary + true + Unicode + v120 + + + DynamicLibrary + true + Unicode + v120 + + + DynamicLibrary + false + true + Unicode + v120 + + + DynamicLibrary + false + true + Unicode + v120 + + + + + + + + + + + + + + + + + + + true + + + true + + + false + + + false + + + + + + Level4 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;DSHOWCAPTURE_EXPORTS;%(PreprocessorDefinitions) + + + Windows + true + strmiids.lib;%(AdditionalDependencies) + + + + + + + Level4 + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;DSHOWCAPTURE_EXPORTS;%(PreprocessorDefinitions) + + + Windows + true + strmiids.lib;%(AdditionalDependencies) + + + + + Level4 + + + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;DSHOWCAPTURE_EXPORTS;%(PreprocessorDefinitions) + + + Windows + true + true + true + strmiids.lib;%(AdditionalDependencies) + + + + + Level4 + + + MaxSpeed + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;DSHOWCAPTURE_EXPORTS;%(PreprocessorDefinitions) + + + Windows + true + true + true + strmiids.lib;%(AdditionalDependencies) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj.filters b/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj.filters new file mode 100644 index 0000000..cc3c525 --- /dev/null +++ b/obs-studio/deps/libdshowcapture/src/vs/2013/dshowcapture/dshowcapture.vcxproj.filters @@ -0,0 +1,102 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/obs-studio/frontend/OBSApp.cpp b/obs-studio/frontend/OBSApp.cpp index 2b95502..43f5c5e 100644 --- a/obs-studio/frontend/OBSApp.cpp +++ b/obs-studio/frontend/OBSApp.cpp @@ -748,34 +748,7 @@ bool OBSApp::InitLocale() } if (!userLocale && defaultLang) { - for (auto &locale_ : GetPreferredLocales()) { - if (locale_ == lang) { - return true; - } - - stringstream file; - file << "locale/" << locale_ << ".ini"; - - string path; - if (!GetDataFilePath(file.str().c_str(), path)) { - continue; - } - - if (!text_lookup_add(textLookup, path.c_str())) { - continue; - } - - blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); - locale = locale_; - - // set application default locale to the new chosen one - if (!locale.empty()) { - QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); - } - - return true; - } - + // Default to English UI; ignore system preferred locales. return true; } diff --git a/obs-studio/frontend/cmake/windows/obs-studio.ico b/obs-studio/frontend/cmake/windows/obs-studio.ico index 7127372917d0ae308412302998833d60e420fd4f..bbb75dc26018a76f6ccaf19fb301c22c00b4d2e9 100644 GIT binary patch literal 65121 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

|y? z_<{r|F|ZZ4g74M>X}#aIIVkUfMF!YnC!6T61zR4I#_shH0-B9|m?g?#z5@!c-*?8m zg$v>m%1`*FvA@=+62Rg^%;eF>8uY63%cp1yywBupz3${44KBy-!wt;(PDHqtc`YbQzaG7$u=AwD#1pz)j!|M0 z5~hyHtNs>ADb$-yB%4aJ`2ypK1z6eGzFxu*aZT46&1D3YLEb8)sD?3hVuy7zGrLi= zBxB2V>ukbKc&4Ti)92+kdbm{@HR*tM;~|(_zo$3SSKy&D5#7LFPNlybs>zZfPs78W zZ<+y*Ne^QNU!4!#_p?gx@15*}9hCM5`KMR4WmOu?Ds^2~*$sAKv_Tt@>|M?#974!DuW^;J^bsq4MKzi!ev{EE^H%@t4_c_T@ z&Z8IL*6IEj-TdP&-U~%Ris$nDT#mp37pS}iqyG#O8)XP?Fk>IMT* zNFzPqbNlO^_GMf~IQZlJ>9+UjYCvBH>w&`4r^e$GdDp`q8a?RvQ9~x+g&jB

haWFa95ql*R zNxAL-c%z&@@^gzc;3tUm;`=;bjtFesDyL43{ftol&u2)1PJ6u1=UEvzTKC2O`Dpl+ zHf6l~&fVE1U$I1J)#~cyT6xi?nSHzbD=ZSTzo8J9r)~Kz{y0%tYHtMK`>eYSq;bF4 zh&P#E5{gtuQ#kmvTW>YqAHDxK(s6v>d9<|q+DoU`pe2gcw&M-(J7v9`AA6TQtF{EO zw|y5900CDVFD-`eT(PmSlYL&FMRd|V=;P5Z!PnzB){SEKF5J3Ip@sP@1oXWHrsr>V zlTId=LYJVm&yr-;apw2D8ROA&uD{w~N-Hmgn}1w1Z)IZ98bqytV+vBbYV~+yGxt2G zr^=sYm6es%yPYqrHhMK3{Cj`C9?~mbQ*#RtPL2kDyYlD@InCnl{3F$@!?acsi&A@HTdkjr4{_D@1Hnsu6jf}`qq;jZmZ_~R|^_bfht(q#{UY+nAI&c;?08=fq z=!f%R_Z<)$k%T+XgE_rMD|(8#hsb6nKaSOQpq1Em{gbm5gh^vk5u|?%>hVGSJwK

4g2sgK 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 guW!&SHm_gcdb72fcsL1?V4#H<@c;k- literal 93455 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

C#Vyy0Xt+}qRZF9tgd$_hM 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 -#include - #include -#include - #include "moc_OBSAbout.cpp" -using namespace json11; - -extern bool steam; - OBSAbout::OBSAbout(QWidget *parent) : QDialog(parent), ui(new Ui::OBSAbout) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); @@ -31,142 +22,6 @@ OBSAbout::OBSAbout(QWidget *parent) : QDialog(parent), ui(new Ui::OBSAbout) ui->version->setText(ver + bitness); - ui->contribute->setText(QTStr("About.Contribute")); - - if (steam) { - delete ui->donate; - } else { - ui->donate->setText("  " + QTStr("About.Donate") + - ""); - ui->donate->setTextInteractionFlags(Qt::TextBrowserInteraction); - ui->donate->setOpenExternalLinks(true); - } - - ui->getInvolved->setText("  " + - QTStr("About.GetInvolved") + ""); - ui->getInvolved->setTextInteractionFlags(Qt::TextBrowserInteraction); - ui->getInvolved->setOpenExternalLinks(true); - - ui->about->setText("" + QTStr("About") + ""); - ui->authors->setText("" + QTStr("About.Authors") + ""); - ui->license->setText("" + QTStr("About.License") + ""); - ui->name->setProperty("class", "text-heading"); ui->version->setProperty("class", "text-large"); - ui->about->setProperty("class", "bg-base"); - ui->authors->setProperty("class", "bg-base"); - ui->license->setProperty("class", "bg-base"); - ui->info->setProperty("class", ""); - - connect(ui->about, &ClickableLabel::clicked, this, &OBSAbout::ShowAbout); - connect(ui->authors, &ClickableLabel::clicked, this, &OBSAbout::ShowAuthors); - connect(ui->license, &ClickableLabel::clicked, this, &OBSAbout::ShowLicense); - - QPointer about(this); - - OBSBasic *main = OBSBasic::Get(); - if (main->patronJson.empty() && !main->patronJsonThread) { - RemoteTextThread *thread = - new RemoteTextThread("https://obsproject.com/patreon/about-box.json", "application/json"); - QObject::connect(thread, &RemoteTextThread::Result, main, &OBSBasic::UpdatePatronJson); - QObject::connect(thread, &RemoteTextThread::Result, this, &OBSAbout::ShowAbout); - main->patronJsonThread.reset(thread); - thread->start(); - } else { - ShowAbout(); - } -} - -void OBSAbout::ShowAbout() -{ - OBSBasic *main = OBSBasic::Get(); - - if (main->patronJson.empty()) { - return; - } - - std::string error; - Json json = Json::parse(main->patronJson, error); - const Json::array &patrons = json.array_items(); - QString text; - - text += "

Top Patreon contributors:

"; - text += "

"; - bool first = true; - bool top = true; - - for (const Json &patron : patrons) { - std::string name = patron["name"].string_value(); - std::string link = patron["link"].string_value(); - int amount = patron["amount"].int_value(); - - if (top && amount < 5000) { - text += "

"; - top = false; - } else if (!first) { - text += "
"; - } - - if (!link.empty()) { - text += ""; - } - text += QT_UTF8(name.c_str()).toHtmlEscaped(); - if (!link.empty()) { - text += ""; - } - - if (first) { - first = false; - } - } - - ui->textBrowser->setHtml(text); -} - -void OBSAbout::ShowAuthors() -{ - std::string path; - QString error = QTStr("About.Error").arg("https://github.com/obsproject/obs-studio/blob/master/AUTHORS"); - -#ifdef __APPLE__ - if (!GetDataFilePath("AUTHORS", path)) { -#else - if (!GetDataFilePath("authors/AUTHORS", path)) { -#endif - ui->textBrowser->setPlainText(error); - return; - } - - ui->textBrowser->setPlainText(QString::fromStdString(path)); - - BPtr text = os_quick_read_utf8_file(path.c_str()); - - if (!text || !*text) { - ui->textBrowser->setPlainText(error); - return; - } - - ui->textBrowser->setPlainText(QT_UTF8(text)); -} - -void OBSAbout::ShowLicense() -{ - std::string path; - QString error = QTStr("About.Error").arg("https://github.com/obsproject/obs-studio/blob/master/COPYING"); - - if (!GetDataFilePath("license/gplv2.txt", path)) { - ui->textBrowser->setPlainText(error); - return; - } - - BPtr text = os_quick_read_utf8_file(path.c_str()); - - if (!text || !*text) { - ui->textBrowser->setPlainText(error); - return; - } - - ui->textBrowser->setPlainText(QT_UTF8(text)); } diff --git a/obs-studio/frontend/dialogs/OBSAbout.hpp b/obs-studio/frontend/dialogs/OBSAbout.hpp index e3ec87c..ce22ee5 100644 --- a/obs-studio/frontend/dialogs/OBSAbout.hpp +++ b/obs-studio/frontend/dialogs/OBSAbout.hpp @@ -11,9 +11,4 @@ public: explicit OBSAbout(QWidget *parent = 0); std::unique_ptr ui; - -private slots: - void ShowAbout(); - void ShowAuthors(); - void ShowLicense(); }; diff --git a/obs-studio/frontend/forms/OBSAbout.ui b/obs-studio/frontend/forms/OBSAbout.ui index ee271ba..9cbb4c0 100644 --- a/obs-studio/frontend/forms/OBSAbout.ui +++ b/obs-studio/frontend/forms/OBSAbout.ui @@ -6,246 +6,90 @@ 0 0 - 840 - 519 + 520 + 300 About - + + + 30 + - 0 + 30 - 0 + 30 - 0 + 30 - 0 + 30 - + + + + 200 + 200 + + + + + 200 + 200 + + + + + + + :res/images/obs.png + + + true + + + + + - 30 - - - 30 - - - 10 - - - 6 - - - 6 + 0 - - - - 256 - 256 - - - - - 256 - 256 - - + - - - - :res/images/obs.png - - - true + OBS Studio - - - 0 + + + Version - - - - OBS Studio - - - - - - - Version - - - - - - - Qt::Orientation::Vertical - - - QSizePolicy::Policy::Fixed - - - - 20 - 20 - - - - - - - - About.Info - - - true - - - - - - - Qt::Orientation::Vertical - - - QSizePolicy::Policy::Fixed - - - - 20 - 20 - - - - - - - - Contribute - - - true - - - - - - - Donate - - - - - - - Get Involved - - - - - - - Qt::Orientation::Vertical - - - QSizePolicy::Policy::Fixed - - - - 20 - 20 - - - - - - - - true - - - - + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + - - - - - 0 - 60 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - About - - - Qt::AlignmentFlag::AlignCenter - - - - - - - Authors - - - Qt::AlignmentFlag::AlignCenter - - - - - - - License - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - ClickableLabel - QLabel -
components/ClickableLabel.hpp
-
-
diff --git a/obs-studio/frontend/forms/images/obs.png b/obs-studio/frontend/forms/images/obs.png index dbae0949fe9d8812f50ac606248f758fbf64356c..def164bb7e4b9d3ac6badcb9ed9fe348dc25e25c 100644 GIT binary patch literal 40205 zcmb??^LHHX6Yg$oV`DdV8rwEDjcwZ-+iu#}jh&{k(Z*k+e00P{{lfG-YB>?bkPEJxx)BD?LC;T__ zdo3D&!>XlrKVR1cYiYuG*MvVqEn)TXjqJU3b?YpAOIfQHA{iFY&%IbEOORfm3>#X5 zi)KrMc?p4Bs+o$4%6`qw*1f>}n~}$FYwGcoPd01K3ac6hoAW=suG{ya+ILbESpI)J zjMFnLTn2`6DV=V=lC}#PJ-$)}oj&ZVy^rkTV2(GWqz0gT{LJ{cW`Z3|QmGAMkH+w| zkSlu%HAINae$dq@teh{lLPCI@=O^^^04U@i*GxVfHDsH&ZrlRA3rvI0%=gEBbjviSo?@^V=B(z%lTvO0Fi;cxB!nIO99X zIYPsQ6v?9i?6g0Y%HC|(tU>el$ojcfUD^SofP{I;J1cBOuC%VF3^o4=*l`a+jtR!OMP>9 zGm;90i7gEv9LbZv5gEz(}2UoA4Q-X?ph2+U-vwYGX$BEvrIdGPr zF#jm`RWGVVAP+N>-`H2c^P;fr1XZDyk3t5oO#5UCak*5x4>*{fqTeB|6}9~c?w^R- zzs10jJ6+^L6+Sk?ZGMWC{#so5@|@eVsq)JHG;Edh1WRpGl~@lxjP&-zdz`8?TZl-~|x zyIf~l*-=6Q&FHv~q4u?hQBy7O;+YpD1-?n`4py(wi-QPL>%3{`UU6?!P!=*1ddAY% z5i64YiguC5f}=)gx}W)m(&@Dd*P$;)JhzPYDdIyRo<)U-FxHkNzje}Liek# zCOT<^!Nnji?@Wxjq$sp8Pz>9xU*aI}{k$Nkw^9&`Hyl-zH19iB-`h`yzY`M_T-}d0 z4;R!bUh44myFE&#ipvxsI%{DiBDWN3ftPcd8a_2t1^}sNiw%l5?!+fMB3Lu~hBm6_ zoFl$#p9RVXf!Sc64UE~elKpN%A(7XPtmQV$-}yilb%aAe4HF#sS{W>4Px zbQ>L`uvFXrtAeV9@nR^LD4TV{=67)7`2dvVLr+y~*{NIr2_6vp2EAq|#On>O@P}c% zFL?3!cc?W>A(wPLSnTQId)AUKnR-sOgc$(Ew$8w7WVcw}5ugm5Hq8zXeI^a?2x(_|8FS`JiJH`GYXPct1mN+}7XWzTOhBYuk)96ap!tcUFT_ znLm6#ER^E9Tgq8~=l}Lsmzu@Rb8Wj2@AKxbJ!Y``uAaqH!r`x-gHp$yefc%k=(jY$OB>7|&2Mnxr>A{Gh>Jt=a+oC5RcW zxxKFOfPb<$;@fOue%G;!SxysPZ{D?w6br3}E3|Wpl(ek_QRJNvn%^=W4zmtUR198( zn=DHQlEh-SL4+$pBdu_Lbr1E`!OfflOhLqvI{Lv>Bd;8$BqCuo#&2@*&{pjzZ4d=z z5G44Sqgcl*g>vtbU|Ni_nqsNtEneDqnka~L`Z1Hj{yL2vKrs*=!{7JP-D-y=NAr^|dw>#81>+p38bKsZTT_}!5wTH}D0-+&2P|fTeZo}+ z+;Pa-nl68?9GMP&l;A>=N;7#3ml0NJIr5p&%r31MSo zksoH<36wusOV}#=7b4cdD^wi?x;%dB!<|`#eJxO}sB?uWmO3Ynm$P$W1nbIA=y%_J zZo#_KBuFqr3FXtzu{t5-isPO;;w68f%-4Xz`w6=o_g;B*t`wMcZ+jS{Aam9~di)R~ zR>K$`qo2eIPvcQ3n$C!t{;1nFir#F9c-5Uy%QgPEg?e3RJo7AYdNC>8_5s9lIK3`H z-?7OpHzjjl^b_bmYz{75V|`xTIIw|7I}!6KO=$WTN^MVZQc-vx(b}s(k~+>!edctp+_D#5^e_KSR16y6rkM3Fi-=ao*!G@??gh zZYr1AtD@nZ4pl|hlWOMXeZ!cznUhG|^IU5b+iKXQNt8I0BsU=@;}q93bG(*dpt6z( z8#%Ph)W`K@epF5|{%oKp#pATk$`PpCwF9>i78AT?nI@DGZmBBcFp1~BoDeoMSM@LA zy59SN>G8XgKRcd73fgD5Y?7=B_=o-=Ur^rD8Q#R7rYQ{;Od)D%$h_~k8zcmN+0DeS zg%A=0-*M7=OTsIRiXZ@lxdhAhBUle2^dV?(9>P0Mwn#i&M--LD=wu?W&$sHpMR};j@TJy!D#>V*{D@o%WoAHaGD;~~TZCC0Sa1ge&RvMOBmh^5Lh ziUr>Kck@NIz`gOJHR}6uC%)NyNaY2rHIW)Y=mW50vJ0nU;amJz3rUoXw>o*9xUqR8 zAG!~cbEWJWoM5o4;)>o6pq;JwkWDqObih2}4O;DIM4w?!#oAm!pyt~tyw&B~yBo5j z&jp~e26{XO6O)?S+h3==Jq&8aW#D)j7M~@(yERN)2XXKWsuEht9bIdhQ(x>ogarjf zYg#~G%A+nWHyDXQ0`~zL7Q8BaQkW>N15B@s^~)peXMLH!zq}b~k|dCq`4lftkAjC} z0BU=Q&r>jII1RuO6Y|eX^xj|z4dQMBW#FWdkY9S?zNc_ z(58hRn3?F>n&_ICSkv&qNd?Bsb+eMrP}SNoUmAg4EFnfWs`qx*t}Y`e* z>`w#(3pfB+GV~@(^rD_5UO6@$`U0;<>Ch=F>uPec;9J5j$Y0Ai^nfkk%KMIqrUES2 zHppfUrm!jgvDH)x9w|67?a5)O3%zwGTKpLoeSYnOC}KfJS9QfdAiD;zRYxo)4n~+i z;A6lXbK&7}Fi-LaN7aRMdKn0Pd;i!bCEB;Pr-d3HF3L!IV34ySBEIRtKQyj1A{Y?W zN^{!xnXpf_Y;x;a{~@-`i`W0Sa+p1csZ}=IiA!d@#B-M6OS7Lg?^EVQ+Z9MNmG*3= zA&Ht_&k_Tk<@_JlIu96w(E^a52T~`mO*b-Ed1C#F8v%aV|ynE%F@u-`OF`u%oCwa(I+)Fn2l9}nF)3Ea#_%R|owr;GB|$2?T5ywbMMf@f+!}z$QxSCSUa{VL z)f<0zTzwQ@G`zofPXV7nY_skSIF3!qdm;OS?i1sH;#{nP zF^FBD&ot!jl^9j!w}}&Z8r#mYp)Lm_7E&*4=JUdAsb0KCGo86@+5*Mm5nKQR&{3Gi z^{P}*!;Na5B<}5aKS|zTDv6mI4yql}5a?T&FnWzz{}Sx1h@!M&_E&n>eZj;Cr+ef^ zQ)<4bcXI3vw|i1!&P@Bwt30*Zjndnvh2VJ}CKCuPXHksuQ5jPDpl-m#Di8ifU#ioR zLCn(gLO(JKBNAN>ug{);Z*%=jItdc!-{&n$ZLWJ!U5DuzWF-`qK3nxEv8bZle` z_?n08mG@G)^8T{Ai%LI_O|vZCYEiVNQ6m1{XGASloJIs0g>9f^FnQ05a@kunrwv&&WzxJR9&yUbyS6$MA&UJA0m( zNcg%COqr@=N|J07O!wTZ3-=Y(w}&ST(;q}$ozkM2 zShC;@7}_RR^Efc_z`KACt;CcCITnLB7xB*kp;TNpV&cLOwmk+Qj7dy3+o0q(Qx2s8 z3`MG5E+52MoX>PbZ2K@fol6^T%)F%V(FkM<+(q_;Nj@+{X0xb*H#g;mV7Rs&9eCqs zngN@*Hg)lm87FWgO0*mct=7j8_DM>(BmR!x$D_x?)a(dqbonb>Hry3-k7BDv zjv%_ZV?yU7?mvEK&J2o&B-~>?6s^4d{d$om#OsM@imWy+J{31*U7BS*1VYA&E9L&A zj=XIXGtJeznI;X)iV7Yb5Kmf?1@XxyC!JRyu_lM_Jbz7jNMLs8P1+tG0(h4oG1bxy zQJ)-5-{rg0E3afv?*64Yhx*2LNE|dznYZ&W$kG<70Li}e)J*bm81}4lvc-EYC3J4f zT~>UT8%X+6XF#M@H7*k7x9+)j9rT@(>pPThkSD5o?<4l%{sK0y6msMWObwNw2o|in zEFrsXfSD7oFFifI>Kq$dt+2B?oU%)lZqFvGgLB; zmFA1NcQPNY&p}Y7?0-jTzC@}E1@*9Nn5OO6vm?aQAG5Tgvz+u*eDiFSzmyQ=-GeI0 zJHYWWT~4gWa};|ds@FScT=U%mM~6#;jA$7Pali!X2WHW(MeXPYxpa}a$x8FI#VnwB z#sncbUmAK!o}GxgoCljrk9~_x3XAZUm?@(Sxd38Id}u232_LwBrYpwAieDVumkqhP za!V-28z0JX*wRUqOkc8?0kG>C z%8+4GW2mM+P-8Vlyk(p(v-l583N&}qB$05=q&q2uo+yc^I=4yQntyeo0$466v@xp3 z^d^U6$Twz0J!&;Cv_>Cj3)w}G4wOjAVF9B>dO))2jYpHes@a7{ZAs1m@n(wsM;JQC z^}iZ;FCe=6LQDjPeZ##qegz_7e45xTFbU(TqajYzT|g(M{tOibp^L%9WttO2u8~nu z_J=T^4za~d+rnCNQ_fj^L9m1lyG2k-r5gb2#h$Q?U{OfJ(BF~d8ku9mel9k_uHLoV zz^jbFl{15IH>j_<~D!sSTYjL0(Kx#RBpC6*I3`3q4)h>K(74hW4 zGz*!zwjxq3d|^UY`DZt9gC-3KLyU~VhW9L_4_0-TJE*xZfoW-Z`vN(()d{|aQMbWa z5oM@buOp?bf=Ae=408i+`O?{lE|}!kn~0M24l(fok+k$%+_2msn@XfXO+_fg}VzSsba#1F@(! zO!7+DsH0>;2zko(Gz`Q5?lVhG6*+fK=&&@K;ib#kSLEM)SQb)a^u5Zc|0T*4dz3a3 zM00Qu^O;9p0Kq4;b7}pY@I?*#B3Ol*&=9L}-wSF_%v7*Op94|FxYp{;LfD-=r}Eq% z_8|$Rh?C7m-UFK)Hcuwr0V0>N#DIVWJZgqIYP}6-EXDey8)G#_@fDPtd&{u$>69Qh zI)r3cMtK@36B2Q3oczoiF?CGmq=~qqrb8s&^fOPOW-B6*M3M~D|CmfZDQuwJ6V=%H z7Ph>#RBgc6kIG4R{y3_B26xTu3HzzF3XP4 zx2uAYCkBB9Z zabXmS-pG|k0_W>!3K-F!i%EjvkN7rF#Xbc}`l;Uv#0|h7k4&G<=i`aa-Q6z~tXuJ) zI&n3dNudI&{8xI>lM^_u-Hsm|8Jc>&9tJ2p%Y#lB7*AXFduW?8&k(Q!(L}T)&oJ z9PkTvw`NeGK}Ynleb0dexjLE~MT#_pAon4puT)SfKR?V#!$6IsVY5V5zIuPG-MhjS zI$(T`xiQl;at=RRE)bh?$R)&N6Qc8$_1Dn=7)6UBkSK@Y3`p7kjfy5tTY!|Mnfyw= zm5CD#p_WZ{*5;jZ6zwY{cMI4gGe4O8C6e(EHu1}s)DY1a=ht>*4~-LPXp9+t(HJ`&G!wORVSBsq3k`VYyyNz!D!fvYAr8vM z$0|fwuO3=h(7QtgFe#t_Pw>x;*P}R^t<^ICbjJA$9y!P!s=O@ojtmtL8}w^b6k|EI zUXkXC_HfAi*EcTxu5zNxYTA@&O2>6~Xq1w60;m~EOEhR8}*{)*lG9%Q6LJ z5YuS5zL|yZJz*Y+5Ys4AYG!n~Y8ujYOJ>m?8JM>ue`+jW=LI~hKWMzsinwnbjG4wR zc^q#D8+42}79=VfOA}h}bIF3Ryn{3vU%edlL*8;j9Peoijf^`I9A9=nF8)qOgRG|I zEzMvn4XX1`|2AmYw$5A-pmwK+MWBum? z6H;1W+%5DWcd_#~5cBv)eMlL&b<}))I`T5Z&-|bBZRQ)B7D7_L0%oy-@0U?7k)^z) zhzxr}hU2@jp6nD7A!HTEZQS2t&^yM%R~`CK>rK39E`HEgt|C79F{9l5Jvj8(&><-{ zj9F4?W1OK&D{@Vd%FO#qtXMzMX*1p2 zfWhi5)00p_(IQYp^}EJ3qx+l#Dj3tp|#yKD?IYs@4zz(fK_z+i4edkB9CqJ1cZ~8@F!AF^G<_=|b6p zefVHUw1DD$RE9ZVXrf(8ReEqaKlc)wEHNIoMlcleDBbf5F|Fl%UP1TokX&6T`I~%l zL<1O?klr&&;fD+*u<%lN6toye)qq(p4@~>FpC4t4hQ2#QTFqVaXdLf;3!l}9o>|n_ zxKp)&UpN#ZJ zM0LREM|73|{9jySeX(!B5H#3!$kN>fqZ2rh$Dm0wE$=+fg|Jyy#f`6o<|ELd3&z?4 zli-k#Sc6h;5y%3JYbVYGxLYqJXZo$8NdID#qj%h#tP2ny*nsBp(H7XN!b4To$C4Cq{*3S%3eMK>&0J|=O^3lM1qo%S8)cMk|f zI9zk3x^(A(8p(xsYP%`PH<#n_B3XawF9(-NBlCcpM{#38)tl*&sC(W7DlYLv3*}8l zz{?O3_m)VPWpx9PsB~K}Q&4EP&C03&d2r zvi!EyhuiruMZ~i)zt55)o~N3WonUl!nHYOvSNkR7^JM)U)_+}<%c1#i4dPCio$Hf6 zO?Bif>!yp{vZ0H|;rs0kMQrT>E#rSbu{XboG`i2`nu)2lz41-U$fzu_P6x2d1VB^U z6Hp^kjt{ApK<WKhvm6K&xS*S;RPDK&LL__h6j_g?j_yG6Cv*4HSD>d7w~KLQ zc>gUPsrLyxlR*nHXEXm<$3CgP>l&OrxC?Zg$=v&|01Hk&aNl=^TfxPuoYg@r6>5ae z^-bz~1|0cEmxeauSbZ2&ZPhOFoJ`+cg985)EBu82*fwInN4VlbyXO8|{$#b22>$Jm z``2Z<)n-aOvf9z~@6MCU-vyI2J1iU1Yh-CWolxS)hVrgRmkT{3vK*<;|ST zfrVDx^<{{F?(|~yMLErg#j12?{e^%|e(CZ=nj(`^=3~Y`-bfg(Z}|4#9A34dP6YY& z@mvs!m-D|NWWM{}%U|W-|M%89F;D+Jnd5#Wu(uDLc8Q$J+qV9ISt=}oFDp!qBe zAKpgXf1hm?ZVO)L>UK7rAszf;bqIZHwAlX6TIdFNQgnT3g@>8Ip@{F}^#rL64#-t^ZT^vQWQJ9-ff@x$#@2F171M5XX=>*b|4kMolq)X2@USEHun z=jL3-$mv-v@Qq&I*5#>jHin``P&gg|aqMbu@A}F*qaOu6v`r&m^!$51iI#jNRi=MK z-F~*#=HK<>YEb0<*QEes!j9piB?0bOcrri2fKX(%&l>$2daWk6W4IvVgTAfsSMc<5 z_#pBa__Jp3)`yrZrQ-wl`*^g4oDk*j+c9zTnLuXd(~n6$pd0&4y`>(p3nghDbx`sB z7DtjIqRf!pQiBsAy2$4U_?CE+>o=D;B^ZPdO}ajVNo`YAN9;S|;`%4IF7XgBp8t)e zJeW^^uoSnm&D}$UyXFgI@Kq|UU0#+K+BBNDA$eOrA@(CWikZs7qOV>pzIr-7?Lz`Q zAsF|Lr2XF$&xWfLNqUMphSh3i=ra7h^$9Em8n_kv0(1O;TW@oOO+IoVNHSVzIzf^GMk#bN>B+kP2Y-{SdlaK+zG^04S#+mH%>hGhYRzep$GgkppWD+-H; zbLY;?qWM7IfW;8zdpmeBdq@lRO}@2L`CPOo?h->&y8vXchfTT&vqwdv%4i|d$oJLv z*JTLJzk2ere3D~fiGF>57>~9&*1dLQHy`Z}=d&p)HFc9G58CPP=y__r_i!He&(HhF z0=5EAh_kUQJikBok@L96h|d-YpfN3G{1WldoUV94JGb)P(5(vy_;orBa?Ym3%{;F- z9~>-fkWjpX9a!OJ7#n|{rHdc|nqU;-MmRI$pLdz^1-|~?FQ`2^euJ3R!CL(u)xXWl z{J_)%hE|Z4e-SClJ+t=1=(?3z%XHuSQ{VQ(k6K|Q#+J>$mYjJu$m)j#KlO5%<&|7j zBHmuUcl6mH5D!OeFN=<5=6UvJumGImK-K_aPEtja@xI1R<mXVu}yPiR(w2-<5M%?Do3IBL)aR(B6-&}M-$08ZsZD zAvWpx{^UvG+Cq5LxPEIRps)=?$oVBDs~3lXB`FqFj@ut&CixqIX1$l}*D>X<-7AZz z*(jP(PiEu5VNpk@z^I*Au|J?`5~C+hIclACFK@i}%i6O(l0*_wr)0)o;?n_tmvXS4 zhqb@cH+W0{RijB5*pGp(jf2YqUGS|Ng{tnGctX4%eZZ(hf$=zt=%m%YXs2)hHx+~p z1rrvrkWFkU2JE#R8&rvm9#60`t>c~b;GFlBT&**bVKUAD*D zk)ihHg=7KNp1KNg3V>taJ#^h6g+EvsY5G^HVk3WquBUNRjs$WFsD6^EkCfGp8ZK`Y zpVPyH!b2lLP9Q3<;1O#~1CtVm)tSEWNcTeZNk^@}4!oA*2>sf7ks>)BP?S&7gQT*2 zfJsWS-%-mrV)cDj<9>VnY&kTykXGwHtD5r*gf{|rT0tYb?IK4F0$flI~Ce{oOmRme_sk~n8@_Oy&Zy}l|(aWHGL;9~_ zkQzv)RS$a}cXAvQ`2hE7NywjLjlYPqss1Z;B1Etx1ws4qVn8q0#?xVb$aZ_3PZ6dw z2|eJ+BI1o@Op44v3LSS+dcIz@g$d_GG6}*(!3C%u5oh*fBl?t8;8OR4^VT$&=zbtSY7fSnUB&WuIYW{zlGMLfTl3$$fg?&IlFC`x< ziUz;0Q=`7n4Su#6t%skySc!vSti=!JM`Y4|{=Ldk+~3sW5q_^Q6~uH^uXLby;Sb++ zE_Yyb2QkDy@N}ptlo~8efhKiGuqfDtx{VqmD>|rS9(@h|LMw|&CrfAfWs54^%=~#O zB0a^4Dfk?4?~v0=7I0?rGOc1%aUdS8>bfiP_rEB?!Xg-#uJr}I^+{NB9nmedc&pj+ z;~F6dU=fBD3ssq6U(${Up#EqMml-TFRhP&{vc^mz{GF!=zr^A$gx|Gac<_&|1MjCk zazITe3#0U)i)ucQ{d@FCgK8^w8UaDwwrCT%Ahit>7+A{0rHELT3T8t2tnNZdRKHE> zA^uPkKj|MtW>{~7Z|HZ)5N(g3@Mm}Dzi{7?Lro!{@&)FyZ--Bh4*eo8+u z$BT~H>*Z3+2l=ke8AsTklGzF+^9Jp{WzdNp0XbgGsCK(;c&XFik!s>oj`O#^e6*gt z|1?^Fd0-esHUt^vjbVE5Ug{>f_KOC-+StGD@T3UxxJ@`wb;S}bSz`5*iwAXj$tSg# zLraLr^OMF$`eyU($VS?Mmi!oCN$$zv;r(mN_8RYU@qhaB-8MD6L?M_LCG+sLeQ+@) zuL_=x(0FMw9}>}5cN^8ku%H~_w^Q2q{%?64F1QIG^9`s0n-aygMF&PistGOJEi@G< zk1;V0GcFqUm^yAh@M1!FBK1Wk{fR`%K!aYQjv1K!00qyDC>g`T7%fYEQMXxNVqJ1@ zr@!*NBj$gX;DDuj(YWvG`jK$UAgaM4Zui21d>o-EG9z&eO(ZgeXR#rT$QTsqT#$Ee zl%N#aiO`cqB@x$i!P-pm2*jcKkzF(E?leNtwCQ?YO2zBczE+I~?HDZeG<9SeUOEA^ zF}$f=@PP-jJEtif@5jDp)8mLhK7`7X@S&rH0$Uo;LL(thtW7!Gvv{@^3T?*ubpI{- z;eVT9`3KFyD(wV0*>7ccDw{;;r@<;xa#&&Nql-5DQZMYk7V+|NR+qdo{{bSs;xhE` zun=FB+F1Wpi`?MiaHU-$N8pM2=VlN@F_WvS3bf&h8E~ht1$V7X$09dNU?9TQ$ z#K=+tQex6*QIzf*xVT?VoZCiPO&)WLDKmB{Oisl<5k2bK`|rPDPH=zi<0&85fkQ~& zhE^{BC>LPlrRNK*jYv|mv+SaIZb)d>WwCXxyygC)!t@lz$o5XK0kDsq)YO{KXoWS> zaz)~wETtidX71!8!fPOlr6@DxYF??>fHy8FA*HD%doJUm>|$%VDj+)&llqt)RtcSl zR7zqgM*zd^g|J|kTLUz4mDQd#;f;Pm z?T2My@*i`4rM%|H9+b0n?mvhIr>6=ztm%ciiiE5lYtm|ai#}GkfdxRGapA^}l7Ex$ z?2sgaKxVWUU=+&F#5EDheWMs(acBV72(vYfs~r^iN_X}bYf)6FM7efxoM9O%iIWgB zQJI}j`9NbN@ga)KH+y(CxMcJ{-_|3}sk5qhTqaY^)T=RSLR2mWH&1 z67S0fj1xv_c{t)8B{C->+jl@b4nmJCB!WULUK)sJE4$MLku1mv7yXwQ!#~#vZBA?% zn|uv#FZD^*8o6Pf%Jdt8d`cocN!Yny9d@&2=>^>2h&v5D!gsb&7XU4|iH;Wgy*v{0 zY=u7pN{&Cwav9?Ez=DD;>65V$6~}k&1VwU zoGi^PSu`UAN=IQbc%z*K`4T^)Ejr)L7`nOMh{T}tPOnd7|FR&1GCFN`O62-H4*Ea<*}y7*+jTIsj-&T**gQ$t~X7uOFGnJ$HGfOB8B% z{Xzh_W2mxv%#j=*=v=@I62k35IrZ>JF%kA~Mw;~yHw{k14sv~DroS9rntq~HF zTA0Pakn~RpeG;V9EOt{07%pgQNsKshM%!FKL%_eZYB+-yIjJH%YxryV zbo+UQ+ONZybm_&fMOEQpjYjv$?e2 zI)408W8Gz%djTJg{h9Xp{(Yb16Y-y}w3}=_XBAZT!JlZyO%C?T)*UpNlcW|I9?rQ& zlwO}E>53o>Y?)!b( z0X5++8@V~mIpmw_hA25_ue&uQngk(nVM8jO^>NHt6sM5PnnGYc)=x3QWyMdqsdW|9 zj_|*nPCg(@qS_;?oPb2GIXG6wY^vA_xCuS`d9_EtJBAoadgi=w%_ZlWAlY}~)Iig5e zKZV%vB5zo$R%?mBE#KTPuPKDDEmH76E}ysnJ4e_NnGSU=(FRjd2 z5-`HKzX(sc2lFilvljK*f*;;c_9a@-w_$$MiG{|j%TFO?Y1g&nhx@xCl$sQCqQEds zUn@*<=y}T1HG~G3RdHllZm_5C(+E8~*mQ@^7Yw-?92OTAvP$$aH}%;JARaD8PRCfP zUEvdN-=-IyX5;5Xx6x zfl5*d4UR^fl2V#n*7&ka6NytlGz9s<&9i16E%tMqu%RY}%-)>?;pcJ5PlfrOGLrf0 zT2xumoqQp_*EW*Xw3sd?NaEgat~f}?s`M-}UN+Tq-E8#DG}p3MCQ4z`YQ;iQJ2xDF zy}rc?UJ}3G85)+fM(-KBqh&1`+k{Nz_fGRF0BSdKDaqk;K@_1IDtb;@repo@D1md?q3sW;}c~)BpVc z)Ev)h(Gy5r{;~itl(N9EjB?te&-bx0gu`0n;Ze!549%UamVa(76lf!pJI(>+U9p`p zh3mC}R_9|?>8l}DI&tjNlRv6|Kc;PI#y}|(L9?wd?%{I%xo`8Dhk*q_o&6jfA3+_UbfxWYOMf;-!o z;P<`j=(_2OF@5z2$qx5E+M%T+|{DTk5n6C^BuRtlwqC=K~!8k3z_|iz3WZ;ZJ|HQj;V*j z50_zeF~7r`{-*-6QZXhXoT>0w4}l}#U$l=5*weki@u0pj$eJR;$2@NIi5iVtrrymI ze&~2xT=nlhx^bHwXuMKk11#*1RzNtJg^P+KkvDHOkHtCYnCX_lTVqwzIJKoBlY9Uh z6MxV>>V>hQY6hEi3RR*@Sbz>`!`68lqZhYgz_byi7Bmv@pY`^9&k@Q4Zxywj@Lq8J)wdm`8-GGa0MPNgMm?c}n*_wrX9>XkFdaeK- zogdodX3r+vkmXUzkarg&rjpLIv{!Rffi0p$Cyl#HORxXBt^F+>Nh`$B3-jl=lLWIj z%_plfsi|ME28Y`R&AB}jb7ml~uE@ig&j;r|und9Sk`||S!#iTvaJ6o@IEHU<#$6^M zML56xUrVtRC)1TWJ{Q7yDmo0OZtK-~SPEDYYJkB8VW`mi#vb>!RR_8hqS+H&M-Nqh z{TZFyZjkIkvr=ORn!MwBRhh=8jmBPbyYTNxn)lr-99oI7D4IBs^GVe?L3fir)8)u1 z<0@GqT$EYo{#jdCjE`37rGJ!l5X^n&m@)-`A*=MeClq zRFnVU$j&KSSaypfix-x2qDqTOEzX;$QHlPG!{EWnFQKM@a#*M-Has~s`E>2VLFF`a zx$otTrV9lag+x?f%twV^HSsvW>T@XJGqy^eRzy!YU;b6m7=3|bgdb~{?lOlK7 z#oR!u_rsI~FRwsIyEoeSPo0fst9R;!yIP4>9fv$_ipeCgEEN9nc&#sxe|#MA$)lgJ z7<(ra=@?tJnaYi4D+6%?oZgn=Oq+lFys}%+Ws@qzpDVQGNnc)#iE&DppJvm@m?4 zaT^LU2nT9Ltc6R2e_j%KB)Bmlf)j#|e=wOVYc;jnW@j>bxW2IOCOIwOL`^~W*gA%$ zt$<6S&#p+bd}Y5hMjDDrK}bvx3VW758_xavy}a4>fE9h4R=bk`AH^uFa&bQJv+t+F zJS%e$Vx5&enwQ{DvK%j^jZFu$a`>L!Xg*ydF*s6}D)UZyKX83r-1ww!nB5=UFeSz( zRTgXOTmoo{YHPB+*rsU`mGe(7aQpie;FUcE2FJ~p$-S5PWrZahl#f69`T9jXOF|K9 z8{*w&^|>pe)8L=*)f9-j|W zi%Vy5@vksPXkwrl=i6!UDleDRIT{WlX=Q)~iti3H?F&0x8^iC*bpF+tO$8j>q5(5S z^{{G;$WRcxr?T0{sK)>F?bW+~P~l>MQCqf&ylDPPzt+GAyyQku8D7Hc{LmQHuQ#=e zzfs1_6_QhLb?%6Q{?1=H7)}CqxF*Rhb{M6tM#%D6WP&d|*A_*t^YdhNTy|s3&0PwS zo&aj0Z zfi+-tg@!WgnZrp0bali!8zAxK3T=iPuAHJ&lz4J)^wnJ%41z@fMk?6PNR#EF>1)Nh zx@YUb5qr;>@QI&0rxg9uiXwqYxO^wBsTYo#uGNKVD7A8X6`S&(v(d3h1*Ci--Pl!<3{a zUC|rv6yV;bT3-oS{N4>Z?=)d%G3XFQl8fU`IJtj|B3ltvsvxq9=omOaeD5MsD42{U zkyJ!1m&Ka?!J)1;sCOO2PT+OB{!%2#U`bR*^YTtS{MklSdz`JPFNR&+Rh3}L1x!2j z+(cJ4#P?%lcvhGn_Npn+uod}Z z1;&V}haUWsDf}0lzC+5A^>iu^Xjd}CKF$i7w;D!DS<0E0A;x)sFVx$9kvH(%f#`DK z$07oL2#%3W+=S28=LYxQ=XxtHv6Y_$T3htrIG#?`h)LhOd6QmH@a{WXND#qRnlMwr zk?Hm$4Qh$DZ4Q&CZEo-1tp}CpV|=s`oxrcs`t1kuT44z$gjZv#6!%JVm&rMKJv!%W z;nSC;Fu!-9TK{1n*KSv2RQ24MOVq?oBFPB<9{_VejKA4yIM2@0&#=(s+A%?L^z#K_ z(7E}sQ9Q7F3O8P~6hHf_v$3dGLqBQB{Yav`eoE4z$~oZ0VuaY3myR1@!LD-tXH^;6Uc~7 zo?8*JaG&o-<$rwF1U~e)cjMNF_Mxk*1h2r32bqt3IOcnID?Xg=0^FV=3SBOW1y7;_ zZb;%xgb=JJ9CuC-Hc~`E05=HHEP80oF`WveF5)*+*wrFt>JfhH6Axo-s)nC_?FLke z1vy_D320hI3iYMK@5A2F5Pt9t6lHyzk0EakQ`4WqVY`Y2y;mDTmy@$A>4X}9Ed1pM zruPufZ`+t^P2%5neFOK6JOIaX;aU!gb_uUt{W@H=_^KmV;)Ii)AQuaGX!~LOr%yhF zd!9Nd|MtMpEtYlFQaAIc1k80kxWo=H4`$eBn-y7X^SHb~Xdw9NT)V~am6dLK;Q$rC zgt1zHzy01*cx>Mce&fdVxODYkM=!$=Za$AqH#)1I;_%pEY~QvOdw1+d+3!aqW}Z&G zzUc_W9ug}soB*5zWPVSbVZ~mHq|(YGN|2G1EdTHf9XO}?l!!wKKRsTGX6rEPgiJLU zK5HE$F+cfX*ph4K)NNc|W)uwWfQ3PU_h!2al&erYzC-|8GmyFlJwJzpgvc~BEJ(cA zb#+-{h1j^N&ya%5qwwTfIRd)V(JC~=rxJInkI!g` z-!H#sySoU#y9B@6MX6dqwWokm$&=O}N0W&dkyJEEFiY>>^wF3LG3z&Fk!sVe$t0xm zGCI;N30n#N_{&=mgb9A_Ef<>TrDHBd2v&V88GJ8BMyHTOccSEw9?eCcbqM2AAIAZB zSk!arNyDm~Fayw;`!dwYa{igtG`_O!t9b0tqwpP9z5w4V`SY+~ zt#M@B0*r>J{5uXz4br zm6Ynd+p|z~0AK0}DJH`R)2#&4#Sn*zE@r)^;vzT<#i`9&C@G{HTWB^c{P|a(!fQ1rUd*Z*#ejqXFq+69ZQ$||7S9>t-lkD;r$7+uAsMn~q2oU$X0 zzB~*-XAfSKpZs%C4PW2>HT+=gVR%kK;hAu7-O_7t)sidQ6=B||>#%u|1K)UX4}Sj( zk7CEZDRdXz`3f|PQc0{~zdlv-TyC?W;`=NHd2q@eydEEJw-2xA!S&PtIAp!#I4k?w zF%3Y7Xf{Qo7Q<=)gSLZh6Ak>%Cm+NgzUv~q?t;Yvqb2XX`K~pT>-enG)6*Ck8NuU^ zKQ0Bx1q&DAm9N@_;RVAOJ2ZlUU^il?1dr`3=>jnaF-l};ygCO~BzHH;r<7ZsZ2X}i zu-NXCaNqpSD!CX4rR7d&5>wwy84~U*xrBBb4Ar;<;zCExHlrnAo+KnvP;nWO0kEwn z5r962N>0oIY0JeHn%rM1{=#moho#VCeupuF7+Alo2XDH56Y7&Axc`CsVg10xsue4+ zbje~24J|O6jwp(1%!o~R_e8CQKlseUxcA9JLQ5{iv}~|RsLypuGI3n97}bN%1*jDt z)oKy_RS!c&8;e{I{hlRTkGKzkuRrBE81oa1a)o-y6_Qgzs3j|tkJT_>D2_{~7@Ml& z&%U+==dKvW1#1W9SGOA#BgJcPc_o^C??Y|sf1&C`Ld5E^YXGLMpG0x^*WkIuw!<=7 zbKLdvxEg>12M%E2!i8^w& zirTwv^8x(cmmZZiADL;xRv^eTD~ymNp)Su5)F6wB%`J;(KJP|EO>IlY1nRR* z2S>2{x~V%h#U*6`mg&F@&MMsdOd(uZ|0Rn+SlF}_K1|m|u0Ev-*Ww?e0J8B)9f8l4 zV>i@XboG3k6!w%?kl`*P0_OQj{Gs8$L|RnzY}iK(f(OBP^olb&6=~YU~mB4y*=|@)x-s#`rdYY<@>u) zE_z5f!IUyjhnzEMFPg|lHX*qlT-TEb&@Z?s6daU&2Ys%M1-^p?+>Rt$k#+Wpdk^}e z6kWoZz~Y}CHAz%rLY;|lY{p3K5V7T;x9nj1fm!_Nzy1LK`xmalP*162|6x|#i!j`K zCAQC81uwcCo=c+{!LduIhIeD%#5$~6@YbUsjmN(zczh#(#f#g&wxPEA?clzVdvNc8 zyG=#dmcH_m-sO1JidVt2itT8C8capzHN5xG9RA0bpTrY;rZM2#XhnprCUB^*aVR0E zKug#XV>B$exhjQtLgs{p0^D*DE~9`h50!2o-CaJaEC?B_JEPo27@|JwW5#Q$2jP-T z)X}m$RMQGZr)v1aFFuSVzi<`SF62TjPPWx?XmkX7_wE(N9~cpLzKZ(pp#UDA-sGh zdVksfr=h>a3tj3STKJQibYe?%!=j*s#3ctV!(!5R(^xQ-F!L3%e1!7=h>)a_bXbT@ zU_>OtJq&?`FDxau7ZQg3aU@V*8gSWPDA9oS>gn~xdlPGrG751JTM-Jy0;YD{i~sWb z-^KYGFT^F6U4o4p&&Ap6&cfu>BzA7!j)xz92&Hlf{e8Vyxnddm28U596!6IIN&Lx| z9+6=s4kk(4QRvJ}OE5Jx4nvy4i-LA6F$R)FQqQqaqUU!~R2)lo9k{kea}_&9kxz>) z1Vo{hV&PA0wu)#q0=UftZY@I62w|}uV{to@gU{W%71wTDf}eT)+3iB0v~rnpvCL=z zef@97O4&)CqljU4NNvKL0@tC(Mojs8i3A2&+zvQ>-Ns=#rO8zjz-v! zqMuwJu7#@?Ux}q%%!SmON{(qNt1Ptq`L{OXzU_z5U-A$(1K6@LmxC$H`vV<)HK;bG z3z7p$G0a8ck+#}}$5623YX!JkM76t!{)&q}dIb)O7zk}GwlV41@O)cBLYd1+L&R}} zBy`c$Rl%0g8vgIsp1=p+v=)a)cVo|<5qZDf-d?=!b+1ELR~HT)I)rb1>sz?%t~;@H z+jdk+C0u*M^|@tH30`6@Ea0 z3Myd8b%lOgmw;-xPou@3Y;Tg2U+BAi=E0sA0I%o@m%pwGGvPJgBV?|xRKmQMH3@+k zgxnuMht6m#hAnAV$6^n2-bDFjd8xKB#Drzs|Bfxqmf6I@B`Zq!tyjMVyS8n{efQpv zyY9XdLqkKj^wLXl<&{_AqKhw)+s@rPv3J)V+;iW1azN;Mm-&$O0}Ih}2pNIf-T@qKpC&XV=5Abq%_qIj>SptrUTow{OR_n-*f-k{-4`TB0m(a8LKekbV`^#&YgVtpyWaT@Tz1*z=<4pq*ytE` z?b(Cgav85UYY_L0Ti8D~D+Nh@1qE}B+G}9Cf?mw_?kdc795x513)gv~9ik2;P0j@&h`m=JF%J!pT z#Yepu$Qm@ybrBj!NR(QrRvbLG^Dw?~|30i+(%aqyrHIl@qiytcU5nAVe?!rpRd{*I zt;;F=@Bxg~9>;Lk`6p~L;P}=67B2=tk77^FfXid!Kfu$ITj6p;tz6(Ss$L0KF1{K? zDS~9(=`5E6=9&@y_LglJn+;I19bG>y7nWkobwZAc%gauC{fr~AVIzL({qM)7jTbC;L!_E(;VRuE$U!CkTq`Kqg~#5LDkjs5%g;r{y{z(Wr|jE{Zn zqv-AH!=_D}aMcx8V*UE{*mBDxQtL1xvm+5nB)&S0ETl|_*a>3vquGeolGq^%IO&+8 z6~vfp#h3~rOn9!;0Scza#I6MupJzI)#frYvPhwe% zRTTQczE`eAf43I(&_O=Juczn^A@wgrGrSc=AzI*7>!Dzcp+0vzqRJ-0bFf%` z5C>1=6yU&o9>6euta(_l{b3yH#S0>xc09cpm=Aj_{up!|!)t!QqB#7!Erp+|W1+{g(Q5%EGoG8! zdHNP2+wkI7Q_R&o1dSM8xq#MO6S3{VcM}|%i}A-F{|36IAH>#eJJHiq!L`?3jms{( z469eJMl%YqbJs3;@BaQiEMB}A#bQy>aAqp(CpIj?T|1|+f3gYJ3lUf;91`}i2$*Os znP((o#6-LTb9|d#)$9dem0BVJjR-7uHOda$vWv6`SfvyWm7hj{@b^Lr%TEOV;1YRg zR)@7hB_NTImMN9TqL-el)Ct*GkPDM|3*j6TzECm`u~3<@HR)iMv?@9A2pbWNfbAkc zgtuI^3KyZzx3=iPVUp$8wtowwhC zu4QNAj=8fH48`{}tx5qM3CR$xlq;=Z@)bzkgvXV5h=K&IS`4=wqtQq))37kX#ioJ? zCDKxB7zI-_=GmS}Q%uGw>Rh5C#ZOJI#KF)-anQMqJ`L9^l@D)MVTMx6W6cJH-GCR--64vQJ$-hlB)*fmT z1X4DCpp|T*HfvFYi&ppHo!75!M|b6NNp38d*6VeQj*d$D#H#(ev)AFA^VVZ@bPQX! zJb}OY?hYKDi=|1T^&xs?7Hth4feK23l@6mIT@1sF6bC@qIPC(U7P=%2Gd*(o z@?_eNhJepy!AwIm*@$b{x9CwDsfF&uLYL>FQZ8Vs*ixh7yI=`}9@w=)GztT_^tG4c zm6xr-fw3{{JT!)4v4XSLuSc;sKkzbdZR4CFcSN2$~F1Og9qD)*}S< z2qF0onNkFVJ_C{Y9<(MC3hepOtz!rxGE`|NMn{+Db9&z!K;kS%80|noT7t?0$q2L_ zJb^|uiL%!ZVB|E%0W4+}0N63T0|)8{gtAM%Kbhilu)KQ}x(hnmYl27h(ni;(8ZGRZ zYUl|jt#!6NM=PuYe5+)1dhlJ z`D3u?x*;`3p;!q5cv>mrAa#NI2%~%pW2_ zLh@YM>~<7VRQwb@1y;B%6u1T~1*U8c!liJT_jt_p%{o&;WQAcUYZhdaic~*wv6}14 zHEfJ+_5~V}|S^)(H1vBj72Xf4;TC}1VGtCr-J+47FMpyPI z#%KgFYRwdL^$@euP1I&u2o38|LIXe>Nh3oX0)a@veE>|%HnDa81kPPCB>xtB-rjh` z$g2q3U4kfbw)WrXQsdo}S!>xu7A%KkbO5DES3x`w=gF zG#9+U-)#%`@0^fbb}r~eTV|Zp?4Xk~Ye+STjTfb$`_r9yI_s_*a&=3M%)ScdJ_vwj zkYY9hrs4!O$CbT!b;TVjCV&N`Ec}%Sf*l6aZ)cH4=%W>1&x`J<11&6FeJ=W{{`{D3 z*3!xNKso?Xl=jN8b12KE>viFSb2BJ#LLIh(<{X!H~jLtp+U+>P$fV& z1#edNWw^)ye{|GW=Lc+t$?`j+A3dfBj(9w-{tAeKjM?93hsG#V*5)$=D;bfdz!c0{ zvW%Ww9khbKh@T)i$HwJ-_;2q#UkqkOhLMHC#}WZPC8UnVs`%uK@&UjIvk6f(kj-P3V?wiB-g> zoRo+6&SExc3A>H1wl<+<)+mv2KDNyawX|8AJ&&ZyxMBeY{D**i@wzab{&{P?^#0{lD5P6c|8C8MbLTi zn`*fkbp3@3mSNX;4d2;*7-f&<$bsz>Z(|AOK}bYm6A74IeiH4-VX-jB3zN}egpo69SqsHEk z@O)}0q=82E8qdb>Ty++v8VMdeFoBZiARt;#A`vq>O89Ly882Q~BO?PC){8$%;3Oez zdb!9(>{@6x`G^=XA5V;;a*)#)5MXVh~9>!}P(Rt0l!TTn?S$dP69Dp_Vh0gKw$_TX)HcTJ0mjLl z&j>RyIjIm{nj(;-!Bj$JoR=0~HY{Ui&}Ddo(Lt073zXp5O<31p{Lg+aVJbuvmQqZ+ z3A!zyYTMFkoa1y@kYI)>Ln}qAsiVR&F(@H2FBBUt$t1!S_D)U%SlU*0$qVQ4Ovje? zehOXk$>SaF_VAx?ScMONH^mc&ro|`_kHh!k#huus+%}bW3HvhxL4H8(&9i+Ux2UK* zPl}@FF?0J*y_L#RQYETn=ZsMi8Gd2``$&PS9wKgpNT~RY79tvf7UwOD$1~?W!`(~m zgz<{wAQW&xg#Y*L=i^;hE!Vc9nUzpdQ}d-i>nX`hnLegv7AJ`PV3;oKN0yBID#@ne zBb3Tb9SN1#qJv<%$XOA^hbsXqQ%6sMXwpt*3PWR&F)H)3kqsz{5CtI;o|9IHs1?dN z<@Ll?s9-YM{0JH8ibjfawc0!nWXEUC44v|6 zaH1C5LCS>7i%h#Ba0vonraU++Ig_x?haQme~@g)~{ zQ~CWP8h{!-Ukgby00Lr#qjpp!pV8DAWYpJoL}@r|^q=2;A%5}IXBplQ)+(~1=kpkH zF@&MI0!F}z2w9L7fDADm8Iz8o+ZId+4I@cjvO*vS#5mXlSe$6&xnLc@Y*MmCXElAZ zrO0Z?R?T;!7nflm%BDOe>Vp^r_Dy93n$ZH^U%nhI)-pQHI=)#fI4@?jJ&DkYY!ro# zBmZ*zfktN8zhZPFdHWbh!XRG3Q>ZD z(pm{x%oF2S_TSCTMQGGR@dz>|%b8Z?hn7f(2*rX^(AkD;-@q|<4IC-$3$AVbI60?a zr*-`W{MT2m#{a&{!tJ}pQ6`*DIva!sTf$wJJvUnt%6SFu_LfqGc+JQl;}QvTvlv~9 zgVGsE+ZS;d(%W(SXCxHg;{6%=)&s-;$G#VH9jCb&PTsP8Yl;W*c6$ZOYD0w!fTaFCvNLWZHxWSEPwI7X47!jBb09UH9OG7S* zn3a0nk-%#vX2|a-=t~TWYwA#9NX#BHI(eouZJKyBJp8_e9K;ZwO2lI3G?eJZLU*;K zMk?bvG2fF$K&ETJVIqQ-7#U0bLSM@lfHD)7)7$_w!-j;$td0^ZZkQG1f+%_>{HJu( zZZfaQkdq zZ5y$+z_2@P0(CFN90zmQM<*`^FLolqS~2RR6Q9wg7t9Gc7eH^E#4VYglU$fR-FEr$ zS$3HpCF&V@p(>z8#&l%@QX_#Cy=DCV4Qug{`+a=*sWFi1#9}X-#W1;$B(v2vlRzkV zOfgb+x_A%!^={ne4bZuKRV^XWkTyTPI`d@$W~lro^rzQvMzA^kN9eB<6ittm??q*i zi9%qu8ROh#-T3csJ0Gt&XQAOTFvBp}`DmAApP3UMc~ZH$>{MXHWcyl>GbFT$MGS}8sUV9(Oc*ZIa5MsmTv9SX zLe2(Hq&J}$x=J2~q#I<~zU>7x*)juOs-Co%g-Eq?ABIFEE^4g< zm@S@#)7k(8Nei>BX(iM&Wh(&z8?HN*Y}tiod_s)j5UJ*%Ad#6FSGEZW9;0!nA z;^zN|b1jhkgF`?Hk<~F-=6r;18tAOJN9N1S4|UwrHH$g|J4Q{29ZA#8k=H~x;#zY7 z!nqLfOiR3?dPK4i=@61kmKog;Qld8Reb$Y5HZk5*I1r2%Fy7fQpK-8Kzz45gh4l-1 z@bQQC;?P`Ej#rq3Sv(>>z(hY1Z1dve@KD^6b3-U}hKN8VK#1W^%gcKy;d@KwRV1J{7mX0q@<$TQV+7&Rgp~3f-4Tn zbZFGOm?0e8F@CV*G;=qI1-Jg*5$vt;HA4_W6fgaJJeRI!EWe1^50f8hNx9D zKr}P%otZ}&!EPS+aiHUF0OkNxU#=kp%Y?B8RHtf8T;^5ZFv zTp*`1Sr#Fx1&Ejr&o&X&L+L>9e&(4;ME*bzC6^QBl)Jm-p_*eFvtbrb4D67EL8ukrpaB5)43zkp;tcD?)7MFS44Hro!5D zmCs^0{v8&jnB9mG)Yp@zFuD?Nui-aIJ?IUW4OH+SUbPnQxO6Fe?jm4*t;iQ;?`?m^ ztfZH9E1p}lfI80ydCBTQ+_`;17LRb-k?H=K8jyKE=J^>7xj`Jr=M~7 zhYHRbkwD6gx0mpq^#cP7%{UNv%^BKusjCPpvuVEyv$EEpmM-eWP)~8bFxZZNbkvv< zgtM?w7e~RtBb~}Nv5_01><>as$mwhV;+D?*cl3G1bH_0P#;67uA>*eCBsaWh30Cw} z(c8^wD@!WYLg?VKZKIfUiO84|h?n<;{F8kgww`RE%JSv@l#3gW(~geVGiPM&|4@nlwYEi4*TDC<05AfD&e7wg&Y z%3j9cDAx5?@W-!Phi^SSfq!~pKXy!T@k9XM*8){2;2kq-ARGxgZ@@O45`kt0!1Tze z^s+!SnWqL9#bcOEj5;R)4LAz63~74R6W`wMAh#GlFE$2AP31|bdieEq&vYls6i19+?1k|aqEdQZsOh?Z}eP7wv zg`z(pc{lZ&HbZ*4EC3oWF4WHk0?fRh13{_R$&*iXAQ6a4oS6|#W*3k2vKbS~#befA zv-dqmx-sP$q=8^RS&Rc=gaQL4If%BO81fSOd){ho+o-qcbo(!m-YHX7b851=eG*^1 zas2{Zy}B1SKRu3HwvFTIiCIk4V~LV{CtK&G-cVb2Sr6Q_y3EqfZ1JYU zrCe~^Q6oED4S-9J9fh*?N(<&%0|?UuldVZah~QgfbXB$>HPL#JQZ8QHg?}v+FeT?Y z(BT%&%d?`L4frs;C%u)7{nGOn1dPF6FPU=K`VwfckVO(YtVWp3F>S7WF*>yPTIdq) zJ6U+yYV+I+AqQ!&LhVZ_2fh~1XvkJCu44Ic<>;usv+DJuOV%aHz(r=ZA*&_z`X1i1 zaS?7@JA^y-PvU`nQ+Rx&hMiM2G=i2G1F21z+*FJQ3;r2c&I}rHwV3euNN~hKAP&P2 z!_VhLSlCs-H4A%j`RW0@;;bQT7_PPryx=ch;$*d*AV_g^{{G8W_F?bjtQO=FTRJ$> zf|D+QNzP4YM(TMq0K^LLI*^pl5Q`c>m*aAKPywZ`f-prn9*ekSnj$|d;hj8B?8_&x z8V+Knk7k9Mw4zyXbRDg1uzr*R=dbO|_c4FGALS!ANel1`~9cJSm+wx&@JYVa9ewIQ+w-DbkR^B4ADzX$jex)9dyZPUy+sO~H>`0Kz!Lf*_Yd6v+z2XdxTQQT}>)al+qA#MYbU~v z{Khpy_^~xZ*f}|e&4(L!a&!t?4%e}JvWcmBfM%#Gz;wEh20>eb!tTSpgjRwmN#MpV zy2=(-c6nIe-;0e)y5+WhsEVGF4mdIYm&pnl?Z!(k(uFg_Q=;nzIrxfo1NhQo``eF6 z_&rJr$28E8gtYf1Djb_FKS}@C4dRfK8Kd?{`BwJeRtv)M;FfJz1?8JyJ%Don?5C!o zg}6z=kywohqaw}|NGYFbre$fRRZm9sTQi4mXUM`|gE(ngCvqJRl!tECX zh?RNbysSDsZx{~~~HUNF)J_+#~6e>8~xFpp~DfFd6m1dV6SY^N|B=E}#hPo{*uGlC$z;pxzbsKhlzV^a%Ep0zW zf@VV%2d{`x;XGfZ=;D`OzX3fJUvv3(%Q}nM^!jf_b<}HjqT6Qz?8=@5_9~@S$nHQK zqEuN2-|5Bimg7!5UU4cYIh=Shm~k0JA@L&fpcZyc?m#PUwDIyvgRO^_^~6{7R`8P> zhvg&}NWmmaJPhdv_r8(rQ-(q;-2hYN;tvOIj|Z>YL7~S+q055bZNsZt@X7$`2e}^1 zrBUGPv4YNbBgB`>AzLi?G5m{RapiT$-$SiEnDI?cDAF4Y^;hxcYtKSgnc=bY&-5c? z8untme>?O)5+tSd1yv4Bo3e3Kv;Le>fw@nixJuf=ZVJ6U2aGsr$dTi6!IfD94qU0n zGS6WFQE&~)J*Ysn`CSbLN`&JhX4_InLp3X^ym{kdT)uJ$q05nSM^K)Ap(HHFvc$vC zz3JeXc{!Q=7m`55gA^U{{0#Y>l7(`W#>Ex7`4YDXgZzaFnjHCM$0uU6*hCx#LXJY9 z5hHpQe%X4ghrs)TZw~Gah*gJ zIPf4&YXiXf|Ak$P3}Aveo90+CnQLqx`)2lHq|Wt6I$PgPnN8VSVU@s58y4ZL;i~kq z{i62#eV2J$B5R(7AApvg5kQ$21qm>1;l-==yI44AVZ~sA<^3s!`z(~ZEf9&Ec$@;{ zPzhOd%uKxy2AJ(PQ7Eua0$o`3jjTZT)R01)(AUt$n=V<3tIruS3a6FmcJVLk?YvOw zh$fCOBIb3q7GTf=^Z#i-KHCVaAgkwNhO(`~W`^SEXvXmDXVKY9kk~wnm?f(1b?h0o zG8ItJdoNjr<$Yzz>5F7~E?Mx4j$9jLJ(hwn7p)lN9#MUaE(PN2b0&fcoJB$E(Q9#p zeL;+E%@q4;F(#+$XwJ2y5F9ij!G1)IhB6I_v(};`M<-r?;UfI%8#gMnINSCsDS$br z=D#dL&dh!f{r0RhovfpXVVE|wtc9>#EdU;8MzG^=065OLs5+#r7B1|NLWqJ;=(VR? zlXz-;s{n5l3^xylq-4Yr%X>Wh)CEgqgOBGpLTx3qTsFZ$Obb}iW!PuJEv-<&^HX${ zfx#{ZYs)UymK_XLE%a7czfm6=fz;xh?28bkcJzooPz!*y zOR9L^YuBUbd5SLd z8>Ce$!$I@ZW=R}LI>+h9@tF{NrUGmkYv9R)HS9e!i>b)~(NqKBOatL$9nnk!7TY2* z*M+d4?10FCi`Nd~zrJ-7hPw*dWoZX#hN#3~Dk!s!orvn+ftO9e3Iv$xmtp}(bzbl{ zp;TCMLQdy#H2@DDcu=aFE2_%`_b>SD%r|8&(gGeCdl+L4PCpqx)eil#{8kn9wW|m4 z=JgANPq5$@U`u;Pr>;DQTzJCGs=ESYw39pNVkvet(ipQG??s9*O=aT?!*_DRvE{p(_L^nZ-A1Wo;RCN-iObe4kcw^g#piPTBuf?P&42vt zh4{J4)}W_ck!!&t-FcC_4;&OqE(&EEKKCMI$ZuoBGoq8I86c#0ueC5YRl}jfHH?kc zF?nzf&Cv$p!*gg&)X_TJKr%6fv{pmPu7gcXii60uv0-^X{^%X&W5c3uv;DXu?Gvnp z*}e=ynj=gc{xk;Mmh2{wnGcC3m0ZnbT`VG-^Lg;?k{GPTIE@*B7J=1JU_ogShD(dE zV|FVFcF`=MR5TXTfCF;}@aWhhSh)N(CT=Bg#e8E~3S}{G1%CeGl^CsuxOMv>6n#Tu zE!NeM78bDrM0O=j8?jHOS73sb+bF>yKap`#j`B{{Le%C0neUd?o^ZVJ;?UM$BnvJ= zMQGUxLP2>c!G|Oa{+@T@W&s43XK@voqq-mA!1301XTBxCFr zF%1n^g>@*EH_1~KI)C8w?gH@Sclq5|+q(+ernee$9EDJ3WSE}i;NE?A;`{~YVYp;g ziOA5P;e9g;Dp-vkD7*OWtJk0wg?MPsq%Ke}TmeGO)G}WtNBoH<3uDxR7>!vj2yr!k zPa*_lydj>Sq!+Uh;<-8^-q@~WjKOXL!9l+6>eFnL^k9zk-rsroQvBwd&y$f)wfxVd zxXR`D$h?!aWmm+{Ua(MBeSh}h{n&bVMm9}*uBqE_ktgSQr7dEJH@Hl+LAbCFJ7N{y z&~XVm3=-xit(J6zoO%FTn25A^7|UJ+53kuUjNf_9*;u!rs{05H*@@P#3~N&x$>?CB zwiRyUD?&TZEke>#mZzwx@W^_^>C(#O=q?Ov7mOF+@m&pg{0)GvyavwhKN}B?{s7a# zly2-X01oCq#8FL!)3{~#EqMFcZ#PRF=Q#p0)|sqTxTxymKVP>Ve|q1aoO8mjQHesNvq_qp#EOBg_t6Yx5HI+?I3YbY3ki2#MFxK=G@u?q-;J*D6 zn5olK$8v3O$>r1G;r}wni~C{x|5wA#l3|IV819V>(ZS62~nB-f~pB zZs>q|xu^GXP#(J@)i_{r_Yy4cSs~0e1(I2q_L8(}*ceP*{9yD4Sl+z?S1!I%H+{AF z@D0tE$(g8a1grek)hjVvF5#b_Jb?Oahyp`6*?CO9J=F z<%h`pM4>L^%y#fyu&4(s`^xy%_C9=T+hIISRb(m{Rb_lI52?*qb~1JuTFw;z`s5*e@rnJ| z!-5U>w$qvmOQuqTv(P6Lkpdg%*er4V4D)p6odZ8C`jOyKX5(d0h^8wMVn-JEE?zl+ zpL^vR{P@Moq;H;qmb87Tor|NV;Bq-WLPIWe^KlJfQb)lVkm!!l7@^6Ok(m&;>>0zo z`=;^KSPi4IHQ`7U%Ca~SlKGG+L9EPAOydY|R&q|Zt0UR3X6R`lbBRsFbn<$%Ei zDdkHB(N-x=^~5Kzd3>|>ONH!Hiye_oWp@$9A(nM7$9p!s z8$;yE5UDalqWsNl_)tisP+y9z6ZDs7y(cBHBNYAlZC`_JV#URQ=qAWRyV ztUZY+oWW4S=EP7Gh2!F2ne} z0W1s(NEl(-#D?Atc=NiS#87#_Y@bylAoJKAkYdG7=>m#I9OIFZX?$br1RmKxgM(8w z)LJYC>BN|h`mzUXbGAn|%m}BRu;uC(LhkvYE*}fJN_hRo#dy_*1wur@G@r&uCgKU( zM1twY6g;nhid#8~|1X#0D`pXg44IiNAnDxcz;_#)+k)xw zkD(h|P~s*m1(7Heo={|7^K#8PsK+(5O7Fz71wU(e5UrPy0Z3C$Gt6ZmUk%+H+`Qvv zd~fe}b^ck9UXHAw7OF&JS)(MxS%d3v)0!X0qVl47&ug~z2;Q5Kff+_o?6rQJnP!Ns z<25{Tur3B+@A!Se-)^G3Ed89@OWJk8EW_g zgu-6A1z9_ak`^ZHE3tUducGXBAFG0Nx*LG~WngjJ0Az1Rmf*j8jGTgZKC!~#tY<-f()FjPLFJ4jNGtS1-N>10_Z%G)TH4 zD>ncqM$`kF89K|t$kbg3CjMMHF;4aE80>wOcz|g5@kaML26S8$( zoz|u>pZ`{6`cGVyVXB!iYcSg1_KbrAqM?^T)*a3xOllm3kfOnkxPAn=f zR&*mhj2Ztt0qE*8UNRC;QRVh2w{U3YUIY_=0xz9J!#)qC-XBA`xCzz5P`jii*G>mH zrJKsBcAHMqZm(WVM^PL4X zf^$6$)mWJ&HMZ196Sz(R8~e}24NGspl5XvdN)ga-scHC~g=OZ8YU^^xaIyJ94P&)= zqM2zoTy0wEHOxdD-%JARoZOCY@BTIp)(+xl&-*#7>0M(Cj2gBhCiQYT&XUa|YcVeC zzw^5qnYshb$&X@za|jc$kH}sRuY3Xg0$aCd%XaGdmr2Y_qXNC!48!6iOj;@T=zy1b zXG}Ntp&4vPTz?p?<^!nOQz)@UWKu*~Rx5=sz4~;gfMGL^TR4<1!_d%gpucjq9D~*n zjne{Nt~G#;`;BH2d~xTVyYS7O|Art5;L2>b&h{(4whn$sXh#12G{kUqF)m(k5zZUh zBwYgSzGrKhI=!#DhIyvJdHk4K_Riz%fR`PW$8Vk5jz>oy!UKoyhhr7+mUTaYixylo zZ)9>gC8UGh0Ate+V|w!ASm=1z*?)&iS`Wl|iBxpvCMaCH&R z88`>$4Q#~X%Hnz8#QeJ$&&L$NRt`(PKY8$+2QM2cnKm5;i@bJ(Rf0r*~NtDB9*zg zfT(yKhKGI{CAY_*1he+>e7E7uFaY|;Hh3mVV%)LsR@}DtcGSp~Zo3NVFud@w6~pHK zCixyUzarR&``miGUaan4jWs>11w}Ym9D>Icuh07+$-vS=m_`_FjA8fGc04uyB(0r! zJXBxY$Cae4A^S2FQQ5PvgR+N6)xe={gzU?ZWymr~mLbLxV;f_} zGBak(Jo9@!|2}^{_mBJ6x#xB7Irn_e=bU@LpZD>s@h>)^irv#nrnbQG;((=Jk@%%u zuYWuyorJ|3EVvMq8 zqxqukycwGuIj%WY6&mu~eH0y8$x%IW$tLTOrr_(vBX1dz2zKq7j|{Eid5K<=ql$zS z7==x*n5-~5`4|%W3Xrawh}|X0as1&(<<4#x-7Ql|;Pq~CV$kZy$$Cht1>ZmV!Y#;_ z&-bWJGo8A6h2@2{QyQrHAwKOaS6!0I-6x55b2)@-TDYZlmx8Ag6}&2!0=;Y*OvLO$ z>IWcS<8Y-6CAg`4**5TsrEIl_0jR48WjKD;_r`rX4P+!?}y z6Zg9-<|MdO*w%t%z1b{&m`FoGJQmct@6W=7$^{2MId=PYpgnQRR^?7(3a7}`HW_X^ z)%W64vr4@T-&xi1E0Ys##}m7K;o)F@XUW!0ey{OX>h1IVMH^{s>7yg(|Lq~-gB!=z=8<`qrU$FRd-=xjq2k(d9sZLo74Ko24{LbL}&HSUW z6MebrECCFL!OQEXVd55b#J`VakI~8MeWK+4s*v0Kh+G5nKazI zIl6J}f%eC2_fXhGphqYFyKvpVzo!K@J1#M`&cDO@z>)eQzk0Cu-Y^{2b3et&3(iS8 zgREIM_Iu0e{MzAIb@gC9zW6p|*wy|Q`77U!$RnDp=dTth&X^^s2s2pExC?TxR+ZQK zigT{lF)+(ccsldt*=uBms7wGf`I+*s7ME9!Ri9ciDy}2LLY~NR_e3yhI<9GNMWpf(LOgAZ#`5BO)CDY?CR1MW`8e+~ee1H=H|O`%M?7w9 z{L22wPyXDNj8V8;rp6juZ^L~DZ>7|DC$UeM{!{WEJnxN^B3Js(H7n3pWcc$3Ro6uGOQ)5mU|?xt0|qmY@T|)sPAIP+}PgKob6xk^X3Wa;hWkeQ(z80 zdlmca?`w12NRXv@A!qAC=Z&}L0(p*RZiq-5i2qG?@)X`xHWC+!XRM1~6;iCLI9SnQ zT_&++I`Cfp%XEXEuRpYNrB#Fat8O25ZaZ{C@TKZdzr(JjE@k8qx3g4>`|KgdZZR?$ zH*>Bcrz^II?}E1;!z;FKWy);GOOoR?uZkDj@0WA$hnYJ}!&quM?bmkW*>7|{PT(75 z(TOa^uHO>8A)DLaO4UWFWLfH7)H?UgyP%ebJK}!1ibZDDp(2aE=-Z9_-TMe_#$V?n zy?N}Stk7p!JnDU%l+^!-^vAbbr>nsgrbUduT4!&*jAVev^s5^nZ@yo(Pb_@w9(gHY z{i;l@%+QaY3TCc`X!wHCm3#6zDmU`;i$ge0LeBbgCs>Z_JwnXxBN!nYZiF_G!@|c5 z^A;@`n0aGLx97GMpvsGb4Otw>c=RblfhF!dr-zAXE@t*EcjSd**Ipc*tGL8trS zJYJdWAlJGeT4^n9Sp)PvK|%Ye`RRnlhC=In*0NXCr03`K$AanvPwL zi>i4Xc5{>%t|3-qkM>Ap2uJ%NR`g)O7&f5f+PIbh+CeaAv^;LjC}4=@dkmgI19q_0 z$@nr?U(W}*J{@7#lWQ;OquU&Vf;1B7#?bBmg3i;) zH$fxoY_JRM>p^Ifry4kS(!O{h4C3^;^dbP^ zd@K}Gt4mEX4_j(PNRoxMmh9C<#@}L9XpWHB^me+nu)2vY z0qa5Q#&M{7$&yl14Z-NJfd)C3KoNeQ(q@k7r$>>7yro7-29VBRG)FoFGK#)h^Si>( z{F~QEmZybQMoePm_e3^6HrBSJvsD0`ZfW)U>-FQ~D{rL4;4{w`6mO1#}4(;RC^QYhdFvwLrV-0ZP+Qp=Qg+|@S z#woZoctl3c%ujD2cV<3H+t-=E_LJm?Q9l$Y?|^scxofn3Fp;qJyV87$o(g1|FY8=H zh0t)uh!bK|HQ)+79>>0@#Xenyt{r4~uWrQ-8`=tAW6w9breQ-)-?3 zksRiRYM7lJQ?(b`Qr%lsYL#Fb0i17SodX@p5^(BXlZ-sK&&{f1mj|YzXk;nku(9r| zQ}Xe9J5Gs9vX`Ibm@g^)@YI!mrqbY#a^3yg%-o8KlbZRe#fpYT*WG1-vs7!{i95=X z;{b^D&-(WWc7|i)cN8wSUP`>#bKG3NY~ddz&XTmsdDH=CnG|f^U1;U(sjgSkjU#H5 zAuc3C2q3vFI=@eS#t(4XWw~9Kz|6UN2SB;fLmKz@__wT25(Z{t+YwP5J4c$Tzy?7G zutzYOfEmK?Izu`CxizZl7hU#gW)FU@q?x_0;S+%3n?9;TcZ%asI z$miWw`y$-T)1(xh-iC61wbmJY20GKyaCHz7LvTh>B*S+9W_V0a>!jFRGXFn zHgqsQ*p5Ev*qB}+_1pk8IOtt&U5-M;ZYf|U;bfETS|7UicFYt60p3$0HoK8k($mv_ z!Xl{^H;lTuhEs{Ju)};$8ubr?Rh?xM%c1f@H+#xFcTS%F4D`V0VCZ9C{+(C*b}V5r zExdcjWv;prx}G21Szs;c@Hx-&J`7}@l1 zo=m{@gFYawL&3UPsb^01!eI$pg_<2-48J?cB5O<@i)ch#XkCnwR(+<9X1Zzc^vj7O zuPe*GF>oWrGEujui5yr94_%V7zAvy^uZ!oROfZ9atP3h^gVww76kb*_)zWaeysr+a zqjKFMezVZG3eWmlM4v5Cm0RdGX|h(@H;>}GqdDJC{&IojfJhM8_A?Q05|{Sgj9aDP zv40yLbgAe1d$+t{y2BVjBcmX#ur-&qe_7_xgV4=N8m1W43!@z54@Bz%+y{%ru;s~O z1B4gtOm%)~w;QggDk;^7URd}6r1Ya!PJyR!wl#|w+jUZ;aia{P^-p5nsQ@Rq$1F@{ z9Z^wD!_p}JF+>7YiKXYQ*`Tq2k*~5-8~*1gj@~B1*k}}aa%$?JwtoE67BspCf`~gV z`{p+*cT;a8DC^%k=-jT14USM)xOMnbS}xnew*6n_bSmGMy7rAPhgvAA9l1f~2X1K9 z7A?z`5XL1h{SouGeN7M>TDvyP;sVB@!ID_yL)nkHL07xChSWaxvz~u|YMOeAv~roX z#I~Kz08WaG!A4@Eol8d?k(j5ISC~dQV>iR0N4RDu*;u&NQrN&6kg~$5vqSbWoRFs; z@c&+dLccp4u1D7Gd>nk0PhCm|tBC>ten2StO_@f97UC+xFW_!p$U;l?ly_4ueI)+} zoV2|yZv?qxMI*y#ZUF=xg6W(ULet)FS$WRw4&Glw3@$iB9etN?_5O8g~1SU7Y@-^>@Tqmyb!&3a} zx~Y+jZ!=oGpkQ=eDe)`n#A0lG{JkxH()U~dh!c#j2B*4F2HmpYc#+$sBlBP2Su#b- zk)WoHS{|aQrPWAzul&hsb0A!_b&ZEc42*!jihX*-C)c@aLs>+fmfip!I2|`=)y%-R z%Y0HabXD*C*2(uWyN7HKmg+fO#J<5RJQJf^niQibJj4b-bi!UwgKPve2@$iQR8U&_ z1U3Y*^NDzJo!mt!0h|`Yh9*Qit#9#iohzqcfTz_oiJxURm3e8T%Uq-)_!;cdhTzKp0&+yw_uX$Dp)1jHhz3~-g*ncZ+7h&G_BUkB;zETjOOh!| z%*j-l?)7<3B$}EJCi#qwjrHYOfVAV7!TTsGnG3E4;fkO(v9(hI=<$;^hs}?AiUk8Y z!!SmlH_MlNJp#U2vFS-7TG^)y&wByO5W&PYEGm>g47-$O&}ZBvxSW+H-@C2iH_!Eh zp06(r=Fxk{q)jDyM(j2iTVAX;5kIb!kG&|znO=kHKWFb2N>b{$4%}{lvXo&$=!mT@ zy^-@aVsXT)%FglsFl^oVaxNLdFCq*(+m1d&ReWGKs@7LOX>Pnv@C1;B+c=nCKD!&3 zS{AWmRnh6}9GfFu-@%>l7WL+(uYk9eg{6657Gj(Y`R2F)N#-IA2XD4{$6YhU9fx<_ zneahJ4zCmG1ZLNBo9N{}u4hYKj(eWv5X)W4fm*rEXA#BC1CN}Y&6_aGgiKxsN$ zGp;XzeFGTk+g5lnCubmj5fNAGI!=THZLaxBrgO*8V@md#y|@anCAuPs(dJEww2o_B z20c(#u=`J}6X}E$*ThTV6Bkujr-V^iS47J%Ra3L)uYS54!)_Z4)_L>l*J@*Isp6B6D_mjdXA# z{36q|BV{Y~`O04O-aEP1$E#KxUdO%hj9En&UP>{&KF@3|m!Cl#YZn31pCL^n()iob z*aM%(c$?Na%?}JcShOxd@Sdf`2V0qVYYG#;t4zPH<68s6h1{I7Q)F3xuO6q+_@!wx zE-Rg8UexJC_>sU{Zm4fzi2%@}u2)jjX<|9}2OT^iZy<2Yx|k2@-F}XI@gNe%)cfJO zl7j7~OK12hGerB`e3=4^^{tb$&=pkCxHoyO0Ck|j)|@ZzmT|PB_65#sX|U{!I+3iZ zu3ixk53E@K8;p2x{<`9)0c8=R57WZF zylwaxiM63MzqlnIoeVcHxO3X5IZWIEqB?`>Vf-rGQN>^Yy1E{uTEF zK5FUxyH?E%08;`<3nQO8;BUgrxo4!GEiVtJT()mo(?FUG=6C*E>yB8DpBM&^>%;Sg(^wOm(wg>n!M-pTqo$O z75*zoruL96n_8V6nZLB&I~a0{KT1x5H)BmSUx*?1a3xvn2LT3Fi?L#Ul7jP@qwRBw zjfc2Oh-32Huf!vt7FZa1PRdK#BAfTy5nwSGr3Usoo*K_n7qBz)70aN>WyQ6!fFh~N zb!}#OHqtiEuT35oL~YJj=&l<)I4c9Jq_B+j5?uRU5yovp?2#VS`|-w+U)?%OP;gapPIJ8AwFwBH zTOT8+|32NM^ujQRy=1$+(Um2Dj$jHc(80tY;zn|-Y(@3Y=mo(L# zt!%mU1fZNdjO)OMng7}R%fZn+;k%{UPHCCmKz$!;dhAZLPBaFP&J0OOE-wg)Qs_#Q z_eB>@vc%WjEO^R7ZAG4nuV0Rqum}yn>JUa!j_9>_c<$X8UazEcBfIn_75|>AkC?6r zpyzt6IisjzsZ8%Tqlw%0FiF?lMQof^wB&Nq&&lRQxLEW(gV-RdBFoiULOt+A5fspu zggIM&%S_e$J!0pNsMr*ZQbuE>VPL0MQ<(sD!Yj~D?9ef0!7Gt;*EH{UK*TnJwkjSp zoG7kfK`KR&o`d2@xpYgQiCmhx8T2eYKC{?M9LJ10qh1FUN|am$%^F<#zF*E6dtY+z z_MO1mish!o*+?+DN|bM3m(Y%A*73M}y=|Go8n9KF&$<|1X_K3DOboJ8zM<^o|mi4dayWGO)wKQqZ_Ca;X`_OP0YxrU)nk#zy zc2$|>5NK&LS1UQLk+4b8(9yX*07w*L4Rt74eeNIaw{Rt-XRm4TlX1+LqtDmDqgj`< zs1Mgmm;y^!Y72tK?vvFMQOM(u9t|n~DYeO|#pfm#B01)|R2&a2k*;mP$qH;4R}{u4 zW+%}KO5CMrg?u!9@!rLtp(+2kjy4IQ%JRvIWI?+gSN>oh*3U0~5ye9#LTGA|bMxDJ zoMN$~e6JdrdmfMSM9`;_1mNiuFckRBis7w;lrkOq`fou&ODozy7ru2Xl6~256|aQoIIayP zCW9#wOJpeF!W}F7{8c2{+h^YycCWXo38tIk@sMntufe6 z;`}%&6Xp^}TE`H|f5+i@$}-2*DdA0mEA0Le>~{5WD;nf!`t3~D*ge87bsOmG>L^Kb z4GE+6hkRcDg5UhOcQ^yXOT%7QL!*QMG$N3tFZFrN)!5}8av-MJekHU%T4(71N*MOp z+uP3aHFvusr9{$r0mInE9M!lDPC2)(y<$~8z3>N3XT#FPoS9D>b$+&oql{W@wZaG> zhSR-eI6J;v$9ZS)coOKcb(t9`0TFdocxh1nYiJiSHufmUOovw*KW)~xS?s#pl>&%Z zj&sMC_g#{QR<+R=&aB(mLbiTHZ25uCRwx4X-IVQjMk(Ed@WAH6{&FmV#T5R#ax~-b z6)#b+$1jhDL?9`fCasbRBObtVT5D}tpWh+p)_-d|QPD6q&uK*0iSh<2d9_ ziA_Kg#E_f0R(GhU9&C;>t}b@Ldlrj)#Z z6W!DiSGFJ{)5}{PsdhEFai<%_QvleO#pfLd40~@~!k=Kw_hqfm*sZsO$M1C(3{JM~ z7c;3C6xAYX^Vg@Yna$0 zZMQcQf3V8O%pkyo#c)kfQ!APMWqcJzyZV!Z literal 6456 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 S7k#<_0000+ diff --git a/obs-studio/frontend/settings/OBSBasicSettings.cpp b/obs-studio/frontend/settings/OBSBasicSettings.cpp index 43a82d4..babcfcd 100644 --- a/obs-studio/frontend/settings/OBSBasicSettings.cpp +++ b/obs-studio/frontend/settings/OBSBasicSettings.cpp @@ -1654,6 +1654,8 @@ void OBSBasicSettings::LoadResolutionLists() addRes(as_width, as_height); } + addRes(3840, 2160); + addRes(2560, 1440); addRes(1920, 1080); addRes(1280, 720); diff --git a/obs-studio/frontend/widgets/OBSBasic.cpp b/obs-studio/frontend/widgets/OBSBasic.cpp index 97e89f4..107a5b6 100644 --- a/obs-studio/frontend/widgets/OBSBasic.cpp +++ b/obs-studio/frontend/widgets/OBSBasic.cpp @@ -622,16 +622,6 @@ bool OBSBasic::InitBasicConfigDefaults() cx *= devicePixelRatioF(); cy *= devicePixelRatioF(); - bool oldResolutionDefaults = config_get_bool(App()->GetUserConfig(), "General", "Pre19Defaults"); - - /* use 1920x1080 for new default base res if main monitor is above - * 1920x1080, but don't apply for people from older builds -- only to - * new users */ - if (!oldResolutionDefaults && (cx * cy) > (1920 * 1080)) { - cx = 1920; - cy = 1080; - } - bool changed = false; /* ----------------------------------------------------- */ diff --git a/obs-studio/frontend/wizards/AutoConfigTestPage.cpp b/obs-studio/frontend/wizards/AutoConfigTestPage.cpp index ef4affc..1a1044e 100644 --- a/obs-studio/frontend/wizards/AutoConfigTestPage.cpp +++ b/obs-studio/frontend/wizards/AutoConfigTestPage.cpp @@ -467,14 +467,14 @@ struct Result { static void CalcBaseRes(int &baseCX, int &baseCY) { - const int maxBaseArea = 1920 * 1200; - const int clipResArea = 1920 * 1080; + const int maxBaseArea = 3840 * 2160; + const int clipResArea = 3840 * 2160; /* if base resolution unusually high, recalculate to a more reasonable - * value to start the downscaling at, based upon 1920x1080's area. + * value to start the downscaling at, based upon 3840x2160's area. * * for 16:9 resolutions this will always change the starting value to - * 1920x1080 */ + * 3840x2160 */ if ((baseCX * baseCY) > maxBaseArea) { long double xyAspect = (long double)baseCX / (long double)baseCY; baseCY = (int)sqrt((long double)clipResArea / xyAspect); @@ -559,15 +559,15 @@ bool AutoConfigTestPage::TestSoftwareEncoding() int maxDataRate; if (lcores > 8 || pcores > 4) { /* superb */ - maxDataRate = 1920 * 1200 * 60 + 1000; + maxDataRate = 3840 * 2160 * 60 + 1000; } else if (lcores > 4 && pcores == 4) { /* great */ - maxDataRate = 1920 * 1080 * 60 + 1000; + maxDataRate = 2560 * 1440 * 60 + 1000; } else if (pcores == 4) { /* okay */ - maxDataRate = 1920 * 1080 * 30 + 1000; + maxDataRate = 1920 * 1080 * 60 + 1000; } else { /* toaster */ @@ -779,7 +779,7 @@ void AutoConfigTestPage::FindIdealHardwareResolution() int pcores = os_get_physical_cores(); int maxDataRate; if (pcores >= 4) { - maxDataRate = 1920 * 1200 * 60 + 1000; + maxDataRate = 3840 * 2160 * 60 + 1000; } else { maxDataRate = 1280 * 720 * 30 + 1000; } diff --git a/obs-studio/frontend/wizards/AutoConfigVideoPage.cpp b/obs-studio/frontend/wizards/AutoConfigVideoPage.cpp index 549238f..886eea2 100644 --- a/obs-studio/frontend/wizards/AutoConfigVideoPage.cpp +++ b/obs-studio/frontend/wizards/AutoConfigVideoPage.cpp @@ -76,6 +76,8 @@ AutoConfigVideoPage::AutoConfigVideoPage(QWidget *parent) : QWizardPage(parent), ui->canvasRes->addItem(str, encRes); }; + addRes(3840, 2160); + addRes(2560, 1440); addRes(1920, 1080); addRes(1280, 720); diff --git a/obs-studio/plugins/CMakeLists.txt b/obs-studio/plugins/CMakeLists.txt index d9717dd..764f683 100644 --- a/obs-studio/plugins/CMakeLists.txt +++ b/obs-studio/plugins/CMakeLists.txt @@ -84,6 +84,7 @@ add_obs_plugin( PLATFORMS WINDOWS LINUX ARCHITECTURES x64 x86_64 ) +add_obs_plugin(obs-shaderfilter) add_obs_plugin(obs-text PLATFORMS WINDOWS) add_obs_plugin(obs-transitions) add_obs_plugin( diff --git a/obs-studio/plugins/obs-browser/.clang-format b/obs-studio/plugins/obs-browser/.clang-format new file mode 100644 index 0000000..73c5b0f --- /dev/null +++ b/obs-studio/plugins/obs-browser/.clang-format @@ -0,0 +1,209 @@ +# please use clang-format version 16 or later + +Standard: c++17 +AccessModifierOffset: -8 +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLambdasOnASingleLine: Inline +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: false +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakStringLiterals: false # apparently unpredictable +ColumnLimit: 120 +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +FixNamespaceComments: true +ForEachMacros: + - 'json_object_foreach' + - 'json_object_foreach_safe' + - 'json_array_foreach' + - 'HASH_ITER' +IncludeBlocks: Preserve +IndentCaseLabels: false +IndentPPDirectives: None +IndentWidth: 8 +IndentWrappedFunctionNames: false +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 8 +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true + +PenaltyBreakAssignment: 10 +PenaltyBreakBeforeFirstCallParameter: 30 +PenaltyBreakComment: 10 +PenaltyBreakFirstLessLess: 0 +PenaltyBreakString: 10 +PenaltyExcessCharacter: 100 +PenaltyReturnTypeOnItsOwnLine: 60 + +PointerAlignment: Right +ReflowComments: false +SkipMacroDefinitionBody: true +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +StatementMacros: + - 'Q_OBJECT' +TabWidth: 8 +TypenameMacros: + - 'DARRAY' +UseTab: ForContinuationAndIndentation +--- +Language: ObjC +AccessModifierOffset: 2 +AlignArrayOfStructures: Right +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignConsecutiveMacros: + Enabled: true + AcrossEmptyLines: false + AcrossComments: true +AllowShortBlocksOnASingleLine: Never +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: None +AttributeMacros: ['__unused', '__autoreleasing', '_Nonnull', '__bridge'] +BitFieldColonSpacing: Both +#BreakBeforeBraces: Webkit +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Never + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +BreakAfterAttributes: Never +BreakArrays: false +BreakBeforeConceptDeclarations: Allowed +BreakBeforeInlineASMColon: OnlyMultiline +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterComma +ColumnLimit: 120 +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: Indent +IndentGotoLabels: false +IndentRequiresClause: true +IndentWidth: 4 +IndentWrappedFunctionNames: true +InsertBraces: false +InsertNewlineAtEOF: true +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +NamespaceIndentation: All +ObjCBinPackProtocolList: Auto +ObjCBlockIndentWidth: 4 +ObjCBreakBeforeNestedBlockParam: false +ObjCSpaceAfterProperty: true +ObjCSpaceBeforeProtocolList: true +PPIndentWidth: -1 +PackConstructorInitializers: NextLine +QualifierAlignment: Leave +ReferenceAlignment: Right +RemoveSemicolon: false +RequiresClausePosition: WithPreceding +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Leave +ShortNamespaceLines: 1 +SortIncludes: false +#SortUsingDeclarations: LexicographicNumeric +SortUsingDeclarations: true +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAroundPointerQualifiers: Default +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: true +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInConditionalStatement: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +Standard: c++17 +TabWidth: 4 +UseTab: Never diff --git a/obs-studio/plugins/obs-browser/.cmake-format.json b/obs-studio/plugins/obs-browser/.cmake-format.json new file mode 100644 index 0000000..20610be --- /dev/null +++ b/obs-studio/plugins/obs-browser/.cmake-format.json @@ -0,0 +1,46 @@ +{ + "format": { + "line_width": 120, + "tab_size": 2, + "enable_sort": true, + "autosort": true + }, + "additional_commands": { + "find_qt": { + "flags": [], + "kwargs": { + "COMPONENTS": "+", + "COMPONENTS_WIN": "+", + "COMPONENTS_MACOS": "+", + "COMPONENTS_LINUX": "+" + } + }, + "set_target_properties_obs": { + "pargs": 1, + "flags": [], + "kwargs": { + "PROPERTIES": { + "kwargs": { + "PREFIX": 1, + "OUTPUT_NAME": 1, + "FOLDER": 1, + "VERSION": 1, + "SOVERSION": 1, + "FRAMEWORK": 1, + "BUNDLE": 1, + "AUTOMOC": 1, + "AUTOUIC": 1, + "AUTORCC": 1, + "AUTOUIC_SEARCH_PATHS": 1, + "BUILD_RPATH": 1, + "INSTALL_RPATH": 1, + "XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC": 1, + "XCODE_ATTRIBUTE_CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION": 1, + "XCODE_ATTRIBUTE_GCC_WARN_SHADOW":1 , + "LIBRARY_OUTPUT_DIRECTORY": 1 + } + } + } + } + } +} diff --git a/obs-studio/plugins/obs-browser/.github/actions/check-changes/action.yaml b/obs-studio/plugins/obs-browser/.github/actions/check-changes/action.yaml new file mode 100644 index 0000000..e5690b8 --- /dev/null +++ b/obs-studio/plugins/obs-browser/.github/actions/check-changes/action.yaml @@ -0,0 +1,82 @@ +name: Check For Changed Files +description: Checks for changed files compared to specific git reference and glob expression +inputs: + baseRef: + description: Git reference to check against + required: false + ref: + description: Git reference to check with + required: false + default: HEAD + checkGlob: + description: Glob expression to limit check to specific files + required: false + useFallback: + description: Use fallback compare against prior commit + required: false + default: 'true' + diffFilter: + description: git diff-filter string to use + required: false + default: '' +outputs: + hasChangedFiles: + value: ${{ steps.checks.outputs.hasChangedFiles }} + description: True if specified files were changed in comparison to specified git reference + changedFiles: + value: ${{ steps.checks.outputs.changedFiles }} + description: List of changed files +runs: + using: composite + steps: + - name: Check For Changed Files ✅ + shell: bash + id: checks + env: + GIT_BASE_REF: ${{ inputs.baseRef }} + GIT_REF: ${{ inputs.ref }} + GITHUB_EVENT_FORCED: ${{ github.event.forced }} + GITHUB_REF_BEFORE: ${{ github.event.before }} + USE_FALLBACK: ${{ inputs.useFallback }} + DIFF_FILTER: ${{ inputs.diffFilter }} + run: | + : Check for Changed Files ✅ + if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi + shopt -s extglob + shopt -s dotglob + + # 4b825dc642cb6eb9a060e54bf8d69288fbee4904 is a "hidden" sha1 hash of + # the "empty tree", retrived via 'git hash-object -t tree /dev/null', + # and used here as a last-resort fallback to always provide a valid + # git ref. + + if [[ "${GIT_BASE_REF}" ]]; then + if ! git cat-file -e "${GIT_BASE_REF}" &> /dev/null; then + echo "::warning::Provided base reference ${GIT_BASE_REF} is invalid" + if [[ "${USE_FALLBACK}" == 'true' ]]; then + GIT_BASE_REF='HEAD~1' + fi + fi + else + if ! git cat-file -e ${GITHUB_REF_BEFORE} &> /dev/null; then + GITHUB_REF_BEFORE='4b825dc642cb6eb9a060e54bf8d69288fbee4904' + fi + + GIT_BASE_REF='HEAD~1' + case "${GITHUB_EVENT_NAME}" in + pull_request) GIT_BASE_REF="origin/${GITHUB_BASE_REF}" ;; + push) if [[ "${GITHUB_EVENT_FORCED}" != 'true' ]]; then GIT_BASE_REF="${GITHUB_REF_BEFORE}"; fi ;; + *) ;; + esac + fi + + changes=($(git diff --name-only --diff-filter="${DIFF_FILTER}" ${GIT_BASE_REF} ${GIT_REF} -- ${{ inputs.checkGlob }})) + + if (( ${#changes[@]} )); then + file_string="${changes[*]}" + echo "hasChangedFiles=true" >> $GITHUB_OUTPUT + echo "changedFiles=[\"${file_string// /\",\"}\"]" >> $GITHUB_OUTPUT + else + echo "hasChangedFiles=false" >> $GITHUB_OUTPUT + echo "changedFiles=[]" >> GITHUB_OUTPUT + fi diff --git a/obs-studio/plugins/obs-browser/.github/actions/run-clang-format/action.yaml b/obs-studio/plugins/obs-browser/.github/actions/run-clang-format/action.yaml new file mode 100644 index 0000000..669fe8e --- /dev/null +++ b/obs-studio/plugins/obs-browser/.github/actions/run-clang-format/action.yaml @@ -0,0 +1,60 @@ +name: Run clang-format +description: Runs clang-format and checks for any changes introduced by it +inputs: + failCondition: + description: Controls whether failed checks also fail the workflow run + required: false + default: never + workingDirectory: + description: Working directory for checks + required: false + default: ${{ github.workspace }} +runs: + using: composite + steps: + - name: Check Runner Operating System 🏃‍♂️ + if: runner.os == 'Windows' + shell: bash + run: | + : Check Runner Operating System 🏃‍♂️ + echo "::notice::run-clang-format action requires a macOS-based or Linux-based runner." + exit 2 + + - name: Check for Changed Files ✅ + uses: ./.github/actions/check-changes + id: checks + with: + checkGlob: "'*.c' '*.h' '*.cpp' '*.hpp' '*.m' '*.mm'" + diffFilter: 'ACM' + + - name: Install Dependencies 🛍️ + if: runner.os == 'Linux' && fromJSON(steps.checks.outputs.hasChangedFiles) + shell: bash + run: | + : Install Dependencies 🛍️ + echo ::group::Install Dependencies + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH + echo "/home/linuxbrew/.linuxbrew/opt/clang-format@19/bin" >> $GITHUB_PATH + brew install --quiet zsh + echo ::endgroup:: + + - name: Run clang-format 🐉 + if: fromJSON(steps.checks.outputs.hasChangedFiles) + id: result + shell: zsh --no-rcs --errexit --pipefail {0} + working-directory: ${{ inputs.workingDirectory }} + env: + CHANGED_FILES: ${{ steps.checks.outputs.changedFiles }} + run: | + : Run clang-format 🐉 + if (( ${+RUNNER_DEBUG} )) setopt XTRACE + + print ::group::Install clang-format-19 + brew install --quiet obsproject/tools/clang-format@19 + print ::endgroup:: + + print ::group::Run clang-format-19 + local -a changes=(${(s:,:)CHANGED_FILES//[\[\]\'\"]/}) + ./build-aux/run-clang-format --fail-${{ inputs.failCondition }} --check ${changes} + print ::endgroup:: diff --git a/obs-studio/plugins/obs-browser/.github/workflows/check-format.yaml b/obs-studio/plugins/obs-browser/.github/workflows/check-format.yaml new file mode 100644 index 0000000..0b9a367 --- /dev/null +++ b/obs-studio/plugins/obs-browser/.github/workflows/check-format.yaml @@ -0,0 +1,15 @@ +name: Check Code Formatting 🛠️ +on: + workflow_call: +jobs: + clang-format: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: clang-format Check 🐉 + id: clang-format + uses: ./.github/actions/run-clang-format + with: + failCondition: error diff --git a/obs-studio/plugins/obs-browser/.github/workflows/pr-pull.yaml b/obs-studio/plugins/obs-browser/.github/workflows/pr-pull.yaml new file mode 100644 index 0000000..b2ffa34 --- /dev/null +++ b/obs-studio/plugins/obs-browser/.github/workflows/pr-pull.yaml @@ -0,0 +1,20 @@ +name: Pull +run-name: ${{ github.event.pull_request.title }} pull request run 🚀 +on: + workflow_dispatch: + pull_request: + paths-ignore: + - '**.md' + branches: [master] + types: [ opened, synchronize, reopened ] +permissions: + contents: read +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true +jobs: + check-format: + name: Format 🔍 + uses: ./.github/workflows/check-format.yaml + permissions: + contents: read diff --git a/obs-studio/plugins/obs-browser/.github/workflows/push.yaml b/obs-studio/plugins/obs-browser/.github/workflows/push.yaml new file mode 100644 index 0000000..c6f8521 --- /dev/null +++ b/obs-studio/plugins/obs-browser/.github/workflows/push.yaml @@ -0,0 +1,20 @@ +name: Push +run-name: ${{ github.ref_name }} push run 🚀 +on: + push: + paths-ignore: + - '**.md' + branches: + - master + - 'release/**' + tags: + - '*' +permissions: + contents: write +jobs: + check-format: + name: Format 🔍 + if: github.ref_name == 'master' + uses: ./.github/workflows/check-format.yaml + permissions: + contents: read diff --git a/obs-studio/plugins/obs-browser/.gitignore b/obs-studio/plugins/obs-browser/.gitignore new file mode 100644 index 0000000..6d5e884 --- /dev/null +++ b/obs-studio/plugins/obs-browser/.gitignore @@ -0,0 +1,85 @@ +#binaries +*.exe +*.dll +*.dylib +*.so + +#cmake +/cmbuild/ +/build/ +/build32/ +/build64/ +/release/ +/release32/ +/release64/ +/debug/ +/debug32/ +/debug64/ +/builds/ +*.o.d +*.ninja +.ninja* +.dirstamp + +#xcode +*.xcodeproj/ + +#other stuff (windows stuff, qt moc stuff, etc) +Release_MD/ +Release/ +Debug/ +x64/ +ipch/ +GeneratedFiles/ +.moc/ + +/other/ + +#make stuff +configure +depcomp +install-sh +Makefile.in +Makefile + +#random useless file stuff +*.dmg +*.app +.DS_Store +.directory +.hg +.depend +tags +*.trace +*.vsp +*.psess +*.swp +*.dat +*.clbin +*.log +*.tlog +*.sdf +*.opensdf +*.xml +*.ipch +*.css +*.xslt +*.aps +*.suo +*.ncb +*.user +*.lo +*.ilk +*.la +*.o +*.obj +*.pdb +*.res +*.dep +*.zip +*.lnk +*.chm +*~ +.DS_Store +*/.DS_Store +*/**/.DS_Store diff --git a/obs-studio/plugins/obs-browser/CMakeLists.txt b/obs-studio/plugins/obs-browser/CMakeLists.txt new file mode 100644 index 0000000..ba56c5a --- /dev/null +++ b/obs-studio/plugins/obs-browser/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.16...3.25) + +option(ENABLE_BROWSER "Enable browser source plugin (required Chromium Embedded Framework)" OFF) + +if(NOT ENABLE_BROWSER) + target_disable(obs-browser) + target_disable_feature(obs-browser "Browser sources are not enabled by default (set CEF_ROOT_DIR and ENABLE_BROWSER)") + + return() +endif() + +find_package(CEF 95 REQUIRED) +find_package(nlohmann_json 3.11 REQUIRED) + +add_library(obs-browser MODULE) +add_library(OBS::browser ALIAS obs-browser) + +option(ENABLE_BROWSER_PANELS "Enable Qt web browser panel support" ON) +mark_as_advanced(ENABLE_BROWSER_PANELS) + +target_sources( + obs-browser + PRIVATE # cmake-format: sortable + browser-app.cpp + browser-app.hpp + browser-client.cpp + browser-client.hpp + browser-dummy-client.cpp + browser-dummy-client.hpp + browser-scheme.cpp + browser-scheme.hpp + browser-version.h + cef-headers.hpp + deps/base64/base64.cpp + deps/base64/base64.hpp + deps/ip-string.hpp + deps/signal-restore.cpp + deps/signal-restore.hpp + deps/wide-string.cpp + deps/wide-string.hpp + obs-browser-plugin.cpp + obs-browser-source.cpp + obs-browser-source.hpp) + +target_include_directories(obs-browser PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/deps") + +target_compile_features(obs-browser PRIVATE cxx_std_20) +target_link_libraries(obs-browser PRIVATE OBS::libobs OBS::frontend-api OBS::websocket-api nlohmann_json::nlohmann_json) + +if(OS_WINDOWS) + include(cmake/os-windows.cmake) +elseif(OS_MACOS) + include(cmake/os-macos.cmake) +elseif(OS_LINUX) + include(cmake/os-linux.cmake) +endif() + +if(ENABLE_BROWSER_PANELS) + include(cmake/feature-panels.cmake) +endif() + +set_target_properties_obs(obs-browser PROPERTIES FOLDER plugins/obs-browser PREFIX "") diff --git a/obs-studio/plugins/obs-browser/COPYING b/obs-studio/plugins/obs-browser/COPYING new file mode 100644 index 0000000..d60c31a --- /dev/null +++ b/obs-studio/plugins/obs-browser/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/obs-studio/plugins/obs-browser/README.md b/obs-studio/plugins/obs-browser/README.md new file mode 100644 index 0000000..3e17328 --- /dev/null +++ b/obs-studio/plugins/obs-browser/README.md @@ -0,0 +1,377 @@ +# obs-browser + +obs-browser introduces a cross-platform Browser Source, powered by CEF ([Chromium Embedded Framework](https://bitbucket.org/chromiumembedded/cef/src/master/README.md)), to OBS Studio. A Browser Source allows the user to integrate web-based overlays into their scenes, with complete access to modern web APIs. + +Additionally, obs-browser enables Service Integration (linking third party services) and Browser Docks (webpages loaded into the interface itself) on all supported platforms, except for Wayland (Linux). + +**This plugin is included by default** on official packages on Windows, macOS, the Ubuntu PPA and the official [Flatpak](https://flathub.org/apps/details/com.obsproject.Studio) (most Linux distributions). + +## JS Bindings + +obs-browser provides a global object that allows access to some OBS-specific functionality from JavaScript. This can be used to create an overlay that adapts dynamically to changes in OBS. + +### TypeScript Type Definitions + +If you're using TypeScript, type definitions for the obs-browser bindings are available through npm and yarn. + +```sh +# npm +npm install --save-dev @types/obs-studio + +# yarn +yarn add --dev @types/obs-studio +``` + +### Get Browser Plugin Version + +```js +/** + * @returns {string} OBS Browser plugin version + */ +window.obsstudio.pluginVersion +// => 2.17.0 +``` + +### Register for event callbacks + +```js +/** + * @callback EventListener + * @param {CustomEvent} event + */ + +/** + * @param {string} type + * @param {EventListener} listener + */ +window.addEventListener('obsSceneChanged', function(event) { + var t = document.createTextNode(event.detail.name) + document.body.appendChild(t) +}) +``` + +#### Available events + +Descriptions for these events can be [found here](https://obsproject.com/docs/reference-frontend-api.html?highlight=paused#c.obs_frontend_event). + +* obsSceneChanged +* obsSceneListChanged +* obsTransitionChanged +* obsTransitionListChanged +* obsSourceVisibleChanged +* obsSourceActiveChanged +* obsStreamingStarting +* obsStreamingStarted +* obsStreamingStopping +* obsStreamingStopped +* obsRecordingStarting +* obsRecordingStarted +* obsRecordingPaused +* obsRecordingUnpaused +* obsRecordingStopping +* obsRecordingStopped +* obsReplaybufferStarting +* obsReplaybufferStarted +* obsReplaybufferSaved +* obsReplaybufferStopping +* obsReplaybufferStopped +* obsVirtualcamStarted +* obsVirtualcamStopped +* obsExit +* [Any custom event emitted via obs-websocket vendor requests] + + +### Control OBS +#### Get webpage control permissions +Permissions required: NONE +```js +/** + * @typedef {number} Level - The level of permissions. 0 for NONE, 1 for READ_OBS (OBS data), 2 for READ_USER (User data), 3 for BASIC, 4 for ADVANCED and 5 for ALL + */ + +/** + * @callback LevelCallback + * @param {Level} level + */ + +/** + * @param {LevelCallback} cb - The callback that receives the current control level. + */ +window.obsstudio.getControlLevel(function (level) { + console.log(level) +}) +``` + +#### Get OBS output status +Permissions required: READ_OBS +```js +/** + * @typedef {Object} Status + * @property {boolean} recording - not affected by pause state + * @property {boolean} recordingPaused + * @property {boolean} streaming + * @property {boolean} replaybuffer + * @property {boolean} virtualcam + */ + +/** + * @callback StatusCallback + * @param {Status} status + */ + +/** + * @param {StatusCallback} cb - The callback that receives the current output status of OBS. + */ +window.obsstudio.getStatus(function (status) { + console.log(status) +}) +``` + +#### Get the current scene +Permissions required: READ_USER +```js +/** + * @typedef {Object} Scene + * @property {string} name - name of the scene + * @property {number} width - width of the scene + * @property {number} height - height of the scene + */ + +/** + * @callback SceneCallback + * @param {Scene} scene + */ + +/** + * @param {SceneCallback} cb - The callback that receives the current scene in OBS. + */ +window.obsstudio.getCurrentScene(function(scene) { + console.log(scene) +}) +``` + +#### Get scenes +Permissions required: READ_USER +```js +/** + * @callback ScenesCallback + * @param {string[]} scenes + */ + +/** + * @param {ScenesCallback} cb - The callback that receives the scenes. + */ +window.obsstudio.getScenes(function (scenes) { + console.log(scenes) +}) +``` + +#### Get transitions +Permissions required: READ_USER +```js +/** + * @callback TransitionsCallback + * @param {string[]} transitions + */ + +/** + * @param {TransitionsCallback} cb - The callback that receives the transitions. + */ +window.obsstudio.getTransitions(function (transitions) { + console.log(transitions) +}) +``` + +#### Get current transition +Permissions required: READ_USER +```js +/** + * @callback TransitionCallback + * @param {string} transition + */ + +/** + * @param {TransitionCallback} cb - The callback that receives the transition currently set. + */ +window.obsstudio.getCurrentTransition(function (transition) { + console.log(transition) +}) +``` + +#### Save the Replay Buffer +Permissions required: BASIC +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.saveReplayBuffer() +``` + +#### Start the Replay Buffer +Permissions required: ADVANCED +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.startReplayBuffer() +``` + +#### Stop the Replay Buffer +Permissions required: ADVANCED +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.stopReplayBuffer() +``` + +#### Change scene +Permissions required: ADVANCED +```js +/** + * @param {string} name - Name of the scene + */ +window.obsstudio.setCurrentScene(name) +``` + +#### Set the current transition +Permissions required: ADVANCED +```js +/** + * @param {string} name - Name of the transition + */ +window.obsstudio.setCurrentTransition(name) +``` + +#### Start streaming +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.startStreaming() +``` + +#### Stop streaming +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.stopStreaming() +``` + +#### Start recording +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.startRecording() +``` + +#### Stop recording +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.stopRecording() +``` + +#### Pause recording +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.pauseRecording() +``` + +#### Unpause recording +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.unpauseRecording() +``` + +#### Start the Virtual Camera +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.startVirtualcam() +``` + +#### Stop the Virtual Camera +Permissions required: ALL +```js +/** + * Does not accept any parameters and does not return anything + */ +window.obsstudio.stopVirtualcam() +``` + + +### Register for visibility callbacks + +**This method is legacy. Register an event listener instead.** + +```js +/** + * onVisibilityChange gets callbacks when the visibility of the browser source changes in OBS + * + * @deprecated + * @see obsSourceVisibleChanged + * @param {boolean} visibility - True -> visible, False -> hidden + */ +window.obsstudio.onVisibilityChange = function(visibility) { + +}; +``` + +### Register for active/inactive callbacks + +**This method is legacy. Register an event listener instead.** + +```js +/** + * onActiveChange gets callbacks when the active/inactive state of the browser source changes in OBS + * + * @deprecated + * @see obsSourceActiveChanged + * @param {bool} True -> active, False -> inactive + */ +window.obsstudio.onActiveChange = function(active) { + +}; +``` + +### obs-websocket Vendor +obs-browser includes integration with obs-websocket's Vendor requests. The vendor name to use is `obs-browser`, and available requests are: + +- `emit_event` - Takes `event_name` and ?`event_data` parameters. Emits a custom event to all browser sources. To subscribe to events, see [here](#register-for-event-callbacks) + - See [#340](https://github.com/obsproject/obs-browser/pull/340) for example usage. + +There are no available vendor events at this time. + +## Building + +OBS Browser cannot be built standalone. It is built as part of OBS Studio. + +By following the instructions, this will enable Browser Source & Custom Browser Docks on all three platforms. Both `BUILD_BROWSER` and `CEF_ROOT_DIR` are required. + +### On Windows + +Follow the [build instructions](https://obsproject.com/wiki/Install-Instructions#windows-build-directions) and be sure to download the **CEF Wrapper** and set `CEF_ROOT_DIR` in CMake to point to the extracted wrapper. + +### On macOS + +Use the [macOS Full Build Script](https://obsproject.com/wiki/Install-Instructions#macos-build-directions). This will automatically download & enable OBS Browser. + +### On Linux + +Follow the [build instructions](https://obsproject.com/wiki/Install-Instructions#linux-build-directions) and choose the "If building with browser source" option. This includes steps to download/extract the CEF Wrapper, and set the required CMake variables. diff --git a/obs-studio/plugins/obs-browser/browser-app.cpp b/obs-studio/plugins/obs-browser/browser-app.cpp new file mode 100644 index 0000000..f24350e --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-app.cpp @@ -0,0 +1,498 @@ +/****************************************************************************** + Copyright (C) 2014 by John R. Bradley + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#include "browser-app.hpp" +#include "browser-version.h" +#include + +#ifdef _WIN32 +#include +#endif + +#ifdef ENABLE_BROWSER_QT_LOOP +#include +#include +#include +#include +#endif + +#ifndef UNUSED_PARAMETER +#define UNUSED_PARAMETER(x) \ + { \ + (void)x; \ + } +#endif + +#ifdef _WIN32 +constexpr bool kIsPlatformWindows = true; +#else +constexpr bool kIsPlatformWindows = false; +#endif + +CefRefPtr BrowserApp::GetRenderProcessHandler() +{ + return this; +} + +CefRefPtr BrowserApp::GetBrowserProcessHandler() +{ + return this; +} + +CefRefPtr BrowserApp::GetDefaultClient() +{ + return GetDummy(); +} + +void BrowserApp::OnRegisterCustomSchemes(CefRawPtr registrar) +{ + registrar->AddCustomScheme("http", CEF_SCHEME_OPTION_STANDARD | CEF_SCHEME_OPTION_CORS_ENABLED); +} + +void BrowserApp::OnContextInitialized() +{ + // Without a default client, CefBrowser is unmanaged, allowing full-blown Chromium windows outside of our control. + // We don't actually want those, so define a dummy client which will automatically close any such windows. + dummy = new BrowserDummyClient(); + + CefRefPtr requestContext = CefRequestContext::GetGlobalContext(); + CefString errorMessage; + CefRefPtr optionValue = CefValue::Create(); + + constexpr std::array kBrowserFeaturesToDisable{ + "autofill.credit_card_enabled", + "autofill.enabled", + "autofill.iban_enabled", + "autofill.payment_card_benefits", + "autofill.payment_cvc_storage", + "autofill.profile_enabled", + "autologin.enabled", + "browser_labs_enabled", + "credentials_enable_autosignin", + "credentials_enable_service", + "payments.can_make_payment_enabled", + "printing.enabled", + "search.suggest_enabled", + "shopping_list_enabled", + "side_panel.google_search_side_panel_enabled", + "side_search.enabled", + "signin.allowed", + "signin.allowed_on_next_startup", + "translate", + "url_keyed_anonymized_data_collection.enabled"}; + + constexpr std::array kBrowserFeaturesToEnable{"extensions.block_external_extensions", + "extensions.disabled"}; + + optionValue->SetBool(false); + for (std::string_view feature : kBrowserFeaturesToDisable) { + requestContext->SetPreference(feature.data(), optionValue.get(), errorMessage); + } + + optionValue->SetBool(true); + for (std::string_view feature : kBrowserFeaturesToEnable) { + requestContext->SetPreference(feature.data(), optionValue.get(), errorMessage); + } +} + +void BrowserApp::OnBeforeChildProcessLaunch(CefRefPtr command_line) +{ +#ifdef _WIN32 + std::string pid = std::to_string(GetCurrentProcessId()); + command_line->AppendSwitchWithValue("parent_pid", pid); +#else + (void)command_line; +#endif +} + +void BrowserApp::OnBeforeCommandLineProcessing(const CefString &, CefRefPtr command_line) +{ + if (!shared_texture_available) { + bool enableGPU = command_line->HasSwitch("enable-gpu"); + CefString type = command_line->GetSwitchValue("type"); + + if (!enableGPU && type.empty()) { + command_line->AppendSwitch("disable-gpu-compositing"); + } + } + + // See https://github.com/chromiumembedded/cef/issues/3966 for 'StorageNotificationService' requirement. + constexpr std::string_view kDefaultDisabledFeatures{ + "CalculateNativeWinOcclusion,GlobalShortcutsPortal,HardwareMediaKeyHandling,LiveCaption," + "LocalNetworkAccessChecksWebSockets,MediaRouter,StorageNotificationService,WebBluetooth," + "EnableWindowsGamingInputDataFetcher"}; + + std::string disableFeatures{}; + + if constexpr (kIsPlatformWindows) { + disableFeatures.append(kDefaultDisabledFeatures); + } else { + constexpr std::string_view kWindowsFeature{",EnableWindowsGamingInputDataFetcher"}; + constexpr size_t kNonWindowsLength = kDefaultDisabledFeatures.size() - kWindowsFeature.size(); + disableFeatures.append(kDefaultDisabledFeatures.substr(0, kNonWindowsLength)); + } + + if (command_line->HasSwitch("disable-features")) { + // Don't override existing, as this can break OSR + disableFeatures.append(","); + disableFeatures.append(command_line->GetSwitchValue("disable-features")); + } + + if (command_line->HasSwitch("disable-blink-features")) { + std::string disableBlinkFeatures = command_line->GetSwitchValue("disable-blink-features"); + disableBlinkFeatures += ",DocumentPictureInPictureAPI"; + command_line->AppendSwitchWithValue("disable-blink-features", disableBlinkFeatures); + } else { + command_line->AppendSwitchWithValue("disable-blink-features", "DocumentPictureInPictureAPI"); + } + + command_line->AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required"); + command_line->AppendSwitch("disable-extensions"); + command_line->AppendSwitchWithValue("disable-features", disableFeatures); + command_line->AppendSwitch("hide-crash-restore-bubble"); +#ifdef __APPLE__ + command_line->AppendSwitch("use-mock-keychain"); +#elif !defined(_WIN32) + command_line->AppendSwitchWithValue("ozone-platform", wayland ? "wayland" : "x11"); +#endif +} + +std::vector exposedFunctions = {"getControlLevel", "getCurrentScene", "getStatus", + "startRecording", "stopRecording", "startStreaming", + "stopStreaming", "pauseRecording", "unpauseRecording", + "startReplayBuffer", "stopReplayBuffer", "saveReplayBuffer", + "startVirtualcam", "stopVirtualcam", "getScenes", + "setCurrentScene", "getTransitions", "getCurrentTransition", + "setCurrentTransition"}; + +void BrowserApp::OnContextCreated(CefRefPtr browser, CefRefPtr, CefRefPtr context) +{ + CefRefPtr globalObj = context->GetGlobal(); + + CefRefPtr obsStudioObj = CefV8Value::CreateObject(nullptr, nullptr); + globalObj->SetValue("obsstudio", obsStudioObj, V8_PROPERTY_ATTRIBUTE_NONE); + + CefRefPtr pluginVersion = CefV8Value::CreateString(OBS_BROWSER_VERSION_STRING); + obsStudioObj->SetValue("pluginVersion", pluginVersion, V8_PROPERTY_ATTRIBUTE_NONE); + + for (std::string name : exposedFunctions) { + CefRefPtr func = CefV8Value::CreateFunction(name, this); + obsStudioObj->SetValue(name, func, V8_PROPERTY_ATTRIBUTE_NONE); + } + + UNUSED_PARAMETER(browser); +} + +void BrowserApp::ExecuteJSFunction(CefRefPtr browser, const char *functionName, CefV8ValueList arguments) +{ + std::vector names; + browser->GetFrameNames(names); + for (auto &name : names) { + CefRefPtr frame = +#if CHROME_VERSION_BUILD >= 6261 + browser->GetFrameByName(name); +#else + browser->GetFrame(name); +#endif + CefRefPtr context = frame->GetV8Context(); + + context->Enter(); + + CefRefPtr globalObj = context->GetGlobal(); + CefRefPtr obsStudioObj = globalObj->GetValue("obsstudio"); + CefRefPtr jsFunction = obsStudioObj->GetValue(functionName); + + if (jsFunction && jsFunction->IsFunction()) + jsFunction->ExecuteFunction(nullptr, arguments); + + context->Exit(); + } +} + +CefRefPtr CefValueToCefV8Value(CefRefPtr value) +{ + CefRefPtr result; + switch (value->GetType()) { + case VTYPE_INVALID: + result = CefV8Value::CreateNull(); + break; + case VTYPE_NULL: + result = CefV8Value::CreateNull(); + break; + case VTYPE_BOOL: + result = CefV8Value::CreateBool(value->GetBool()); + break; + case VTYPE_INT: + result = CefV8Value::CreateInt(value->GetInt()); + break; + case VTYPE_DOUBLE: + result = CefV8Value::CreateDouble(value->GetDouble()); + break; + case VTYPE_STRING: + result = CefV8Value::CreateString(value->GetString()); + break; + case VTYPE_BINARY: + result = CefV8Value::CreateNull(); + break; + case VTYPE_DICTIONARY: { + result = CefV8Value::CreateObject(nullptr, nullptr); + CefRefPtr dict = value->GetDictionary(); + CefDictionaryValue::KeyList keys; + dict->GetKeys(keys); + for (unsigned int i = 0; i < keys.size(); i++) { + CefString key = keys[i]; + result->SetValue(key, CefValueToCefV8Value(dict->GetValue(key)), V8_PROPERTY_ATTRIBUTE_NONE); + } + } break; + case VTYPE_LIST: { + CefRefPtr list = value->GetList(); + size_t size = list->GetSize(); + result = CefV8Value::CreateArray((int)size); + for (size_t i = 0; i < size; i++) { + result->SetValue((int)i, CefValueToCefV8Value(list->GetValue(i))); + } + } break; +#if !defined(_WIN32) && CHROME_VERSION_BUILD >= 6943 + case VTYPE_NUM_VALUES: + result = CefV8Value::CreateNull(); + break; +#endif + } + return result; +} + +bool BrowserApp::OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, + CefProcessId source_process, CefRefPtr message) +{ + UNUSED_PARAMETER(frame); + DCHECK(source_process == PID_BROWSER); + + CefRefPtr args = message->GetArgumentList(); + + if (message->GetName() == "Visibility") { + CefV8ValueList arguments; + arguments.push_back(CefV8Value::CreateBool(args->GetBool(0))); + + ExecuteJSFunction(browser, "onVisibilityChange", arguments); + + } else if (message->GetName() == "Active") { + CefV8ValueList arguments; + arguments.push_back(CefV8Value::CreateBool(args->GetBool(0))); + + ExecuteJSFunction(browser, "onActiveChange", arguments); + + } else if (message->GetName() == "DispatchJSEvent") { + nlohmann::json payloadJson = nlohmann::json::parse(args->GetString(1).ToString(), nullptr, false); + + nlohmann::json wrapperJson; + if (args->GetSize() > 1) + wrapperJson["detail"] = payloadJson; + std::string wrapperJsonString = wrapperJson.dump(); + std::string script; + + script += "new CustomEvent('"; + script += args->GetString(0).ToString(); + script += "', "; + script += wrapperJsonString; + script += ");"; + + std::vector names; + browser->GetFrameNames(names); + for (auto &name : names) { + CefRefPtr frame = +#if CHROME_VERSION_BUILD >= 6261 + browser->GetFrameByName(name); +#else + browser->GetFrame(name); +#endif + CefRefPtr context = frame->GetV8Context(); + + context->Enter(); + + CefRefPtr globalObj = context->GetGlobal(); + + CefRefPtr returnValue; + CefRefPtr exception; + + /* Create the CustomEvent object + * We have to use eval to invoke the new operator */ + context->Eval(script, browser->GetMainFrame()->GetURL(), 0, returnValue, exception); + + CefV8ValueList arguments; + arguments.push_back(returnValue); + + CefRefPtr dispatchEvent = globalObj->GetValue("dispatchEvent"); + dispatchEvent->ExecuteFunction(nullptr, arguments); + + context->Exit(); + } + + } else if (message->GetName() == "executeCallback") { + CefRefPtr context = browser->GetMainFrame()->GetV8Context(); + + context->Enter(); + + CefRefPtr arguments = message->GetArgumentList(); + int callbackID = arguments->GetInt(0); + CefString jsonString = arguments->GetString(1); + + CefRefPtr json = CefParseJSON(arguments->GetString(1).ToString(), {}); + + CefRefPtr callback = callbackMap[callbackID]; + CefV8ValueList args; + + CefRefPtr retval = CefValueToCefV8Value(json); + + args.push_back(retval); + + if (callback) + callback->ExecuteFunction(nullptr, args); + + context->Exit(); + + callbackMap.erase(callbackID); + + } else { + return false; + } + + return true; +} + +bool IsValidFunction(std::string function) +{ + std::vector::iterator iterator; + iterator = std::find(exposedFunctions.begin(), exposedFunctions.end(), function); + return iterator != exposedFunctions.end(); +} + +bool BrowserApp::Execute(const CefString &name, CefRefPtr, const CefV8ValueList &arguments, + CefRefPtr &, CefString &) +{ + if (IsValidFunction(name.ToString())) { + if (arguments.size() >= 1 && arguments[0]->IsFunction()) { + callbackId++; + callbackMap[callbackId] = arguments[0]; + } + + CefRefPtr msg = CefProcessMessage::Create(name); + CefRefPtr args = msg->GetArgumentList(); + args->SetInt(0, callbackId); + + /* Pass on arguments */ + for (u_long l = 0; l < arguments.size(); l++) { + u_long pos; + if (arguments[0]->IsFunction()) + pos = l; + else + pos = l + 1; + + if (arguments[l]->IsString()) + args->SetString(pos, arguments[l]->GetStringValue()); + else if (arguments[l]->IsInt()) + args->SetInt(pos, arguments[l]->GetIntValue()); + else if (arguments[l]->IsBool()) + args->SetBool(pos, arguments[l]->GetBoolValue()); + else if (arguments[l]->IsDouble()) + args->SetDouble(pos, arguments[l]->GetDoubleValue()); + } + + CefRefPtr browser = CefV8Context::GetCurrentContext()->GetBrowser(); + SendBrowserProcessMessage(browser, PID_BROWSER, msg); + + } else { + /* Function does not exist. */ + return false; + } + + return true; +} + +#ifdef ENABLE_BROWSER_QT_LOOP +Q_DECLARE_METATYPE(MessageTask); +MessageObject messageObject; + +void QueueBrowserTask(CefRefPtr browser, BrowserFunc func) +{ + std::lock_guard lock(messageObject.browserTaskMutex); + messageObject.browserTasks.emplace_back(browser, func); + + QMetaObject::invokeMethod(&messageObject, "ExecuteNextBrowserTask", Qt::QueuedConnection); +} + +bool MessageObject::ExecuteNextBrowserTask() +{ + Task nextTask; + { + std::lock_guard lock(browserTaskMutex); + if (!browserTasks.size()) + return false; + + nextTask = browserTasks[0]; + browserTasks.pop_front(); + } + + nextTask.func(nextTask.browser); + return true; +} + +void MessageObject::ExecuteTask(MessageTask task) +{ + task(); +} + +void MessageObject::DoCefMessageLoop(int ms) +{ + if (ms) + QTimer::singleShot((int)ms + 2, []() { CefDoMessageLoopWork(); }); + else + CefDoMessageLoopWork(); +} + +void MessageObject::Process() +{ + CefDoMessageLoopWork(); +} + +void ProcessCef() +{ + QMetaObject::invokeMethod(&messageObject, "DoCefMessageLoop", Qt::QueuedConnection, Q_ARG(int, (int)0)); +} + +#define MAX_DELAY (1000 / 30) + +#if CHROME_VERSION_BUILD < 5938 +void BrowserApp::OnScheduleMessagePumpWork(int64 delay_ms) +#else +void BrowserApp::OnScheduleMessagePumpWork(int64_t delay_ms) +#endif +{ + if (delay_ms < 0) + delay_ms = 0; + else if (delay_ms > MAX_DELAY) + delay_ms = MAX_DELAY; + + if (!frameTimer.isActive()) { + QObject::connect(&frameTimer, &QTimer::timeout, &messageObject, &MessageObject::Process); + frameTimer.setSingleShot(false); + frameTimer.start(33); + } + + QMetaObject::invokeMethod(&messageObject, "DoCefMessageLoop", Qt::QueuedConnection, Q_ARG(int, (int)delay_ms)); +} +#endif diff --git a/obs-studio/plugins/obs-browser/browser-app.hpp b/obs-studio/plugins/obs-browser/browser-app.hpp new file mode 100644 index 0000000..0694d0c --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-app.hpp @@ -0,0 +1,117 @@ +/****************************************************************************** + Copyright (C) 2014 by John R. Bradley + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#pragma once + +#include +#include +#include +#include "cef-headers.hpp" +#include "browser-dummy-client.hpp" + +typedef std::function)> BrowserFunc; + +#ifdef ENABLE_BROWSER_QT_LOOP +#include +#include +#include +#include + +typedef std::function MessageTask; + +class MessageObject : public QObject { + Q_OBJECT + + friend void QueueBrowserTask(CefRefPtr browser, BrowserFunc func); + + struct Task { + CefRefPtr browser; + BrowserFunc func; + + inline Task() {} + inline Task(CefRefPtr browser_, BrowserFunc func_) : browser(browser_), func(func_) {} + }; + + std::mutex browserTaskMutex; + std::deque browserTasks; + +public slots: + bool ExecuteNextBrowserTask(); + void ExecuteTask(MessageTask task); + void DoCefMessageLoop(int ms); + void Process(); +}; + +extern void QueueBrowserTask(CefRefPtr browser, BrowserFunc func); +#endif + +class BrowserApp : public CefApp, public CefRenderProcessHandler, public CefBrowserProcessHandler, public CefV8Handler { + + void ExecuteJSFunction(CefRefPtr browser, const char *functionName, CefV8ValueList arguments); + + typedef std::map> CallbackMap; + + bool shared_texture_available; + CallbackMap callbackMap; + int callbackId; +#if !defined(__APPLE__) && !defined(_WIN32) + bool wayland; +#endif + +public: +#if defined(__APPLE__) || defined(_WIN32) + inline BrowserApp(bool shared_texture_available_ = false) : shared_texture_available(shared_texture_available_) +#else + inline BrowserApp(bool shared_texture_available_ = false, bool wayland_ = false) + : shared_texture_available(shared_texture_available_), + wayland(wayland_) +#endif + { + } + + virtual CefRefPtr GetRenderProcessHandler() override; + virtual CefRefPtr GetBrowserProcessHandler() override; + virtual CefRefPtr GetDefaultClient() override; + virtual void OnContextInitialized() override; + virtual void OnBeforeChildProcessLaunch(CefRefPtr command_line) override; + virtual void OnRegisterCustomSchemes(CefRawPtr registrar) override; + virtual void OnBeforeCommandLineProcessing(const CefString &process_type, + CefRefPtr command_line) override; + virtual void OnContextCreated(CefRefPtr browser, CefRefPtr frame, + CefRefPtr context) override; + virtual bool OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) override; + virtual bool Execute(const CefString &name, CefRefPtr object, const CefV8ValueList &arguments, + CefRefPtr &retval, CefString &exception) override; + +#ifdef ENABLE_BROWSER_QT_LOOP +#if CHROME_VERSION_BUILD < 5938 + virtual void OnScheduleMessagePumpWork(int64 delay_ms) override; +#else + virtual void OnScheduleMessagePumpWork(int64_t delay_ms) override; +#endif + QTimer frameTimer; +#endif + + CefRefPtr dummy = nullptr; + + BrowserDummyClient *GetDummy() const { return dummy.get(); }; + + IMPLEMENT_REFCOUNTING(BrowserApp); +}; diff --git a/obs-studio/plugins/obs-browser/browser-client.cpp b/obs-studio/plugins/obs-browser/browser-client.cpp new file mode 100644 index 0000000..1083793 --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-client.cpp @@ -0,0 +1,666 @@ +/****************************************************************************** + Copyright (C) 2014 by John R. Bradley + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#include "browser-client.hpp" +#include "obs-browser-source.hpp" +#include "base64/base64.hpp" +#include +#include +#include +#include +#include +#include +#include +#if defined(__APPLE__) && CHROME_VERSION_BUILD > 4430 +#include +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) +#include + +#include "drm-format.hpp" +#endif + +inline bool BrowserClient::valid() const +{ + return !!bs && !bs->destroying; +} + +CefRefPtr BrowserClient::GetLoadHandler() +{ + return this; +} + +CefRefPtr BrowserClient::GetRenderHandler() +{ + return this; +} + +CefRefPtr BrowserClient::GetDisplayHandler() +{ + return this; +} + +CefRefPtr BrowserClient::GetLifeSpanHandler() +{ + return this; +} + +CefRefPtr BrowserClient::GetContextMenuHandler() +{ + return this; +} + +CefRefPtr BrowserClient::GetAudioHandler() +{ + return reroute_audio ? this : nullptr; +} + +CefRefPtr BrowserClient::GetRequestHandler() +{ + return this; +} + +CefRefPtr BrowserClient::GetResourceRequestHandler(CefRefPtr, + CefRefPtr, + CefRefPtr request, bool, bool, + const CefString &, bool &) +{ + if (request->GetHeaderByName("origin") == "null") { + return this; + } + + return nullptr; +} + +void BrowserClient::OnRenderProcessTerminated(CefRefPtr, TerminationStatus +#if CHROME_VERSION_BUILD >= 6367 + , + int, const CefString &error_string +#endif +) +{ + if (!valid()) + return; + +#if CHROME_VERSION_BUILD >= 6367 + std::string str_text = error_string; +#else + std::string str_text = ""; +#endif + + const char *sourceName = ""; + + if (bs && bs->source) + sourceName = obs_source_get_name(bs->source); + + blog(LOG_ERROR, "[obs-browser: '%s'] Webpage has crashed unexpectedly! Reason: '%s'", sourceName, + str_text.c_str()); +} + +CefResourceRequestHandler::ReturnValue BrowserClient::OnBeforeResourceLoad(CefRefPtr, CefRefPtr, + CefRefPtr, + CefRefPtr) +{ + return RV_CONTINUE; +} + +bool BrowserClient::OnBeforePopup(CefRefPtr, CefRefPtr, +#if CHROME_VERSION_BUILD >= 6834 + int, +#endif + const CefString &, const CefString &, cef_window_open_disposition_t, bool, + const CefPopupFeatures &, CefWindowInfo &, CefRefPtr &, + CefBrowserSettings &, CefRefPtr &, bool *) +{ + /* block popups */ + return true; +} + +void BrowserClient::OnBeforeContextMenu(CefRefPtr, CefRefPtr, CefRefPtr, + CefRefPtr model) +{ + /* remove all context menu contributions */ + model->Clear(); +} + +bool BrowserClient::OnProcessMessageReceived(CefRefPtr browser, CefRefPtr, CefProcessId, + CefRefPtr message) +{ + const std::string &name = message->GetName(); + CefRefPtr input_args = message->GetArgumentList(); + nlohmann::json json; + + if (!valid()) { + return false; + } + + // Fall-through switch, so that higher levels also have lower-level rights + switch (webpage_control_level) { + case ControlLevel::All: + if (name == "startRecording") { + obs_frontend_recording_start(); + } else if (name == "stopRecording") { + obs_frontend_recording_stop(); + } else if (name == "startStreaming") { + obs_frontend_streaming_start(); + } else if (name == "stopStreaming") { + obs_frontend_streaming_stop(); + } else if (name == "pauseRecording") { + obs_frontend_recording_pause(true); + } else if (name == "unpauseRecording") { + obs_frontend_recording_pause(false); + } else if (name == "startVirtualcam") { + obs_frontend_start_virtualcam(); + } else if (name == "stopVirtualcam") { + obs_frontend_stop_virtualcam(); + } + [[fallthrough]]; + case ControlLevel::Advanced: + if (name == "startReplayBuffer") { + obs_frontend_replay_buffer_start(); + } else if (name == "stopReplayBuffer") { + obs_frontend_replay_buffer_stop(); + } else if (name == "setCurrentScene") { + const std::string scene_name = input_args->GetString(1).ToString(); + OBSSourceAutoRelease source = obs_get_source_by_name(scene_name.c_str()); + if (!source) { + blog(LOG_WARNING, + "Browser source '%s' tried to switch to scene '%s' which doesn't exist", + obs_source_get_name(bs->source), scene_name.c_str()); + } else if (!obs_source_is_scene(source)) { + blog(LOG_WARNING, "Browser source '%s' tried to switch to '%s' which isn't a scene", + obs_source_get_name(bs->source), scene_name.c_str()); + } else { + obs_frontend_set_current_scene(source); + } + } else if (name == "setCurrentTransition") { + const std::string transition_name = input_args->GetString(1).ToString(); + obs_frontend_source_list transitions = {}; + obs_frontend_get_transitions(&transitions); + + OBSSourceAutoRelease transition; + for (size_t i = 0; i < transitions.sources.num; i++) { + obs_source_t *source = transitions.sources.array[i]; + if (obs_source_get_name(source) == transition_name) { + transition = obs_source_get_ref(source); + break; + } + } + + obs_frontend_source_list_free(&transitions); + + if (transition) + obs_frontend_set_current_transition(transition); + else + blog(LOG_WARNING, + "Browser source '%s' tried to change the current transition to '%s' which doesn't exist", + obs_source_get_name(bs->source), transition_name.c_str()); + } + [[fallthrough]]; + case ControlLevel::Basic: + if (name == "saveReplayBuffer") { + obs_frontend_replay_buffer_save(); + } + [[fallthrough]]; + case ControlLevel::ReadUser: + if (name == "getScenes") { + struct obs_frontend_source_list list = {}; + obs_frontend_get_scenes(&list); + std::vector scenes_vector; + for (size_t i = 0; i < list.sources.num; i++) { + obs_source_t *source = list.sources.array[i]; + scenes_vector.push_back(obs_source_get_name(source)); + } + json = scenes_vector; + obs_frontend_source_list_free(&list); + } else if (name == "getCurrentScene") { + OBSSourceAutoRelease current_scene = obs_frontend_get_current_scene(); + + if (!current_scene) + return false; + + const char *name = obs_source_get_name(current_scene); + if (!name) + return false; + + json = {{"name", name}, + {"width", obs_source_get_width(current_scene)}, + {"height", obs_source_get_height(current_scene)}}; + } else if (name == "getTransitions") { + struct obs_frontend_source_list list = {}; + obs_frontend_get_transitions(&list); + std::vector transitions_vector; + for (size_t i = 0; i < list.sources.num; i++) { + obs_source_t *source = list.sources.array[i]; + transitions_vector.push_back(obs_source_get_name(source)); + } + json = transitions_vector; + obs_frontend_source_list_free(&list); + } else if (name == "getCurrentTransition") { + OBSSourceAutoRelease source = obs_frontend_get_current_transition(); + json = obs_source_get_name(source); + } + [[fallthrough]]; + case ControlLevel::ReadObs: + if (name == "getStatus") { + json = {{"recording", obs_frontend_recording_active()}, + {"streaming", obs_frontend_streaming_active()}, + {"recordingPaused", obs_frontend_recording_paused()}, + {"replaybuffer", obs_frontend_replay_buffer_active()}, + {"virtualcam", obs_frontend_virtualcam_active()}}; + } + [[fallthrough]]; + case ControlLevel::None: + if (name == "getControlLevel") { + json = (int)webpage_control_level; + } + } + + CefRefPtr msg = CefProcessMessage::Create("executeCallback"); + + CefRefPtr execute_args = msg->GetArgumentList(); + execute_args->SetInt(0, input_args->GetInt(0)); + execute_args->SetString(1, json.dump()); + + SendBrowserProcessMessage(browser, PID_RENDERER, msg); + + return true; +} + +void BrowserClient::GetViewRect(CefRefPtr, CefRect &rect) +{ + if (!valid()) { + rect.Set(0, 0, 16, 16); + return; + } + + rect.Set(0, 0, bs->width < 1 ? 1 : bs->width, bs->height < 1 ? 1 : bs->height); +} + +bool BrowserClient::OnTooltip(CefRefPtr, CefString &text) +{ + std::string str_text = text; + QMetaObject::invokeMethod(QCoreApplication::instance()->thread(), + [str_text]() { QToolTip::showText(QCursor::pos(), str_text.c_str()); }); + return true; +} + +void BrowserClient::OnPaint(CefRefPtr, PaintElementType type, const RectList &, const void *buffer, + int width, int height) +{ + if (type != PET_VIEW) { + // TODO Overlay texture on top of bs->texture + return; + } + +#ifdef ENABLE_BROWSER_SHARED_TEXTURE + if (sharing_available) { + return; + } +#endif + + if (!valid()) { + return; + } + + if (bs->width != width || bs->height != height) { + obs_enter_graphics(); + bs->DestroyTextures(); + obs_leave_graphics(); + } + + if (!bs->texture && width && height) { + obs_enter_graphics(); + bs->texture = gs_texture_create(width, height, GS_BGRA, 1, (const uint8_t **)&buffer, GS_DYNAMIC); + bs->width = width; + bs->height = height; + obs_leave_graphics(); + } else { + obs_enter_graphics(); + gs_texture_set_image(bs->texture, (const uint8_t *)buffer, width * 4, false); + obs_leave_graphics(); + } +} + +#ifdef ENABLE_BROWSER_SHARED_TEXTURE +void BrowserClient::UpdateExtraTexture() +{ + if (bs->texture) { + const uint32_t cx = gs_texture_get_width(bs->texture); + const uint32_t cy = gs_texture_get_height(bs->texture); + const gs_color_format format = gs_texture_get_color_format(bs->texture); + const gs_color_format linear_format = gs_generalize_format(format); + + if (linear_format != format) { + if (!bs->extra_texture || bs->last_format != linear_format || bs->last_cx != cx || + bs->last_cy != cy) { + if (bs->extra_texture) { + gs_texture_destroy(bs->extra_texture); + bs->extra_texture = nullptr; + } + bs->extra_texture = gs_texture_create(cx, cy, linear_format, 1, nullptr, 0); + bs->last_cx = cx; + bs->last_cy = cy; + bs->last_format = linear_format; + } + } else if (bs->extra_texture) { + gs_texture_destroy(bs->extra_texture); + bs->extra_texture = nullptr; + bs->last_cx = 0; + bs->last_cy = 0; + bs->last_format = GS_UNKNOWN; + } + } +} + +void BrowserClient::OnAcceleratedPaint(CefRefPtr, PaintElementType type, const RectList &, +#if CHROME_VERSION_BUILD >= 6367 + const CefAcceleratedPaintInfo &info) +#else + void *shared_handle) +#endif +{ + if (type != PET_VIEW) { + // TODO Overlay texture on top of bs->texture + return; + } + + if (!valid()) { + return; + } + +#if !defined(_WIN32) && !defined(__APPLE__) + if (info.plane_count == 0) + return; + + struct obs_cef_video_format format = obs_cef_format_from_cef_type(info.format); + uint64_t modifier = info.modifier; + + if (format.gs_format == GS_UNKNOWN) + return; + + uint32_t *strides = (uint32_t *)alloca(info.plane_count * sizeof(uint32_t)); + uint32_t *offsets = (uint32_t *)alloca(info.plane_count * sizeof(uint32_t)); + uint64_t *modifiers = (uint64_t *)alloca(info.plane_count * sizeof(uint64_t)); + int *fds = (int *)alloca(info.plane_count * sizeof(int)); + + /* NOTE: This a workaround under X11 where the modifier is always invalid where it can mean "no modifier" in + * Chromium's code. */ + if (obs_get_nix_platform() == OBS_NIX_PLATFORM_X11_EGL && modifier == DRM_FORMAT_MOD_INVALID) + modifier = DRM_FORMAT_MOD_LINEAR; + + for (size_t i = 0; i < kAcceleratedPaintMaxPlanes; i++) { + auto *plane = &info.planes[i]; + + strides[i] = plane->stride; + offsets[i] = plane->offset; + fds[i] = plane->fd; + + modifiers[i] = modifier; + } +#endif + +#if !defined(_WIN32) && CHROME_VERSION_BUILD < 6367 + if (shared_handle == bs->last_handle) + return; +#endif + + obs_enter_graphics(); + + if (bs->texture) { +#ifdef _WIN32 + //gs_texture_release_sync(bs->texture, 0); +#endif + gs_texture_destroy(bs->texture); + bs->texture = nullptr; + } + +#if defined(__APPLE__) && CHROME_VERSION_BUILD > 6367 + bs->texture = gs_texture_create_from_iosurface((IOSurfaceRef)(uintptr_t)info.shared_texture_io_surface); +#elif defined(__APPLE__) && CHROME_VERSION_BUILD > 4183 + bs->texture = gs_texture_create_from_iosurface((IOSurfaceRef)(uintptr_t)shared_handle); +#elif defined(_WIN32) && CHROME_VERSION_BUILD > 4183 + bs->texture = +#if CHROME_VERSION_BUILD >= 6367 + gs_texture_open_nt_shared((uint32_t)(uintptr_t)info.shared_texture_handle); +#else + gs_texture_open_nt_shared((uint32_t)(uintptr_t)shared_handle); +#endif + //if (bs->texture) + // gs_texture_acquire_sync(bs->texture, 1, INFINITE); + +#elif defined(_WIN32) + bs->texture = gs_texture_open_shared((uint32_t)(uintptr_t)shared_handle); +#else + bs->texture = gs_texture_create_from_dmabuf(info.extra.coded_size.width, info.extra.coded_size.height, + format.drm_format, format.gs_format, info.plane_count, fds, strides, + offsets, modifier != DRM_FORMAT_MOD_INVALID ? modifiers : NULL); +#endif + UpdateExtraTexture(); + obs_leave_graphics(); + +#if defined(__APPLE__) && CHROME_VERSION_BUILD >= 6367 + bs->last_handle = info.shared_texture_io_surface; +#elif defined(_WIN32) && CHROME_VERSION_BUILD >= 6367 + bs->last_handle = info.shared_texture_handle; +#elif defined(__APPLE__) || defined(_WIN32) + bs->last_handle = shared_handle; +#endif +} + +#ifdef CEF_ON_ACCELERATED_PAINT2 +void BrowserClient::OnAcceleratedPaint2(CefRefPtr, PaintElementType type, const RectList &, + void *shared_handle, bool new_texture) +{ + if (type != PET_VIEW) { + // TODO Overlay texture on top of bs->texture + return; + } + + if (!valid()) { + return; + } + + if (!new_texture) { + return; + } + + obs_enter_graphics(); + + if (bs->texture) { + gs_texture_destroy(bs->texture); + bs->texture = nullptr; + } + +#if defined(__APPLE__) && CHROME_VERSION_BUILD > 4183 + bs->texture = gs_texture_create_from_iosurface((IOSurfaceRef)(uintptr_t)shared_handle); +#elif defined(_WIN32) && CHROME_VERSION_BUILD > 4183 + bs->texture = gs_texture_open_nt_shared((uint32_t)(uintptr_t)shared_handle); + +#else + bs->texture = gs_texture_open_shared((uint32_t)(uintptr_t)shared_handle); +#endif + UpdateExtraTexture(); + obs_leave_graphics(); +} +#endif +#endif + +static speaker_layout GetSpeakerLayout(CefAudioHandler::ChannelLayout cefLayout) +{ + switch (cefLayout) { + case CEF_CHANNEL_LAYOUT_MONO: + return SPEAKERS_MONO; /**< Channels: MONO */ + case CEF_CHANNEL_LAYOUT_STEREO: + return SPEAKERS_STEREO; /**< Channels: FL, FR */ + case CEF_CHANNEL_LAYOUT_2POINT1: + case CEF_CHANNEL_LAYOUT_2_1: + case CEF_CHANNEL_LAYOUT_SURROUND: + return SPEAKERS_2POINT1; /**< Channels: FL, FR, LFE */ + case CEF_CHANNEL_LAYOUT_2_2: + case CEF_CHANNEL_LAYOUT_QUAD: + case CEF_CHANNEL_LAYOUT_4_0: + return SPEAKERS_4POINT0; /**< Channels: FL, FR, FC, RC */ + case CEF_CHANNEL_LAYOUT_4_1: + case CEF_CHANNEL_LAYOUT_5_0: + case CEF_CHANNEL_LAYOUT_5_0_BACK: + return SPEAKERS_4POINT1; /**< Channels: FL, FR, FC, LFE, RC */ + case CEF_CHANNEL_LAYOUT_5_1: + case CEF_CHANNEL_LAYOUT_5_1_BACK: + return SPEAKERS_5POINT1; /**< Channels: FL, FR, FC, LFE, RL, RR */ + case CEF_CHANNEL_LAYOUT_7_1: + case CEF_CHANNEL_LAYOUT_7_1_WIDE_BACK: + case CEF_CHANNEL_LAYOUT_7_1_WIDE: + return SPEAKERS_7POINT1; /**< Channels: FL, FR, FC, LFE, RL, RR, SL, SR */ + default: + return SPEAKERS_UNKNOWN; + } +} + +void BrowserClient::OnAudioStreamStarted(CefRefPtr browser, const CefAudioParameters ¶ms_, + int channels_) +{ + UNUSED_PARAMETER(browser); + channels = channels_; + channel_layout = (ChannelLayout)params_.channel_layout; + sample_rate = params_.sample_rate; + frames_per_buffer = params_.frames_per_buffer; +} + +void BrowserClient::OnAudioStreamPacket(CefRefPtr browser, const float **data, int frames, int64_t pts) +{ + UNUSED_PARAMETER(browser); + if (!valid()) { + return; + } + struct obs_source_audio audio = {}; + const uint8_t **pcm = (const uint8_t **)data; + speaker_layout speakers = GetSpeakerLayout(channel_layout); + int speaker_count = get_audio_channels(speakers); + for (int i = 0; i < speaker_count; i++) + audio.data[i] = pcm[i]; + audio.samples_per_sec = sample_rate; + audio.frames = frames; + audio.format = AUDIO_FORMAT_FLOAT_PLANAR; + audio.speakers = speakers; + audio.timestamp = (uint64_t)pts * 1000000LLU; + obs_source_output_audio(bs->source, &audio); +} + +void BrowserClient::OnAudioStreamStopped(CefRefPtr browser) +{ + UNUSED_PARAMETER(browser); +} + +void BrowserClient::OnAudioStreamError(CefRefPtr browser, const CefString &message) +{ + UNUSED_PARAMETER(browser); + UNUSED_PARAMETER(message); +} + +static CefAudioHandler::ChannelLayout Convert2CEFSpeakerLayout(int channels) +{ + switch (channels) { + case 1: + return CEF_CHANNEL_LAYOUT_MONO; + case 2: + return CEF_CHANNEL_LAYOUT_STEREO; + case 3: + return CEF_CHANNEL_LAYOUT_2_1; + case 4: + return CEF_CHANNEL_LAYOUT_4_0; + case 5: + return CEF_CHANNEL_LAYOUT_4_1; + case 6: + return CEF_CHANNEL_LAYOUT_5_1; + case 8: + return CEF_CHANNEL_LAYOUT_7_1; + default: + return CEF_CHANNEL_LAYOUT_UNSUPPORTED; + } +} + +bool BrowserClient::GetAudioParameters(CefRefPtr browser, CefAudioParameters ¶ms) +{ + UNUSED_PARAMETER(browser); + int channels = (int)audio_output_get_channels(obs_get_audio()); + params.channel_layout = Convert2CEFSpeakerLayout(channels); + params.sample_rate = (int)audio_output_get_sample_rate(obs_get_audio()); + params.frames_per_buffer = kFramesPerBuffer; + return true; +} + +void BrowserClient::OnLoadEnd(CefRefPtr, CefRefPtr frame, int) +{ + if (!valid()) { + return; + } + + if (frame->IsMain() && bs->css.length()) { + std::string uriEncodedCSS = CefURIEncode(bs->css, false).ToString(); + + std::string script; + script += "const obsCSS = document.createElement('style');"; + script += "obsCSS.appendChild(document.createTextNode(" + "decodeURIComponent(\"" + + uriEncodedCSS + "\")));"; + script += "document.querySelector('head').appendChild(obsCSS);"; + + frame->ExecuteJavaScript(script, "", 0); + } +} + +void BrowserClient::OnLoadError(CefRefPtr, [[maybe_unused]] CefRefPtr frame, + CefLoadHandler::ErrorCode, const CefString &, const CefString &) +{ +#if CHROME_VERSION_BUILD > 6533 + // CEF doesn't currently provide a way to properly disable/override the default Chrome error page. + // https://github.com/obsproject/obs-studio/issues/13499 + // FIXME: https://github.com/chromiumembedded/cef/issues/3852 + frame->LoadURL("about:blank"); +#endif +} + +bool BrowserClient::OnConsoleMessage(CefRefPtr, cef_log_severity_t level, const CefString &message, + const CefString &source, int line) +{ + int errorLevel = LOG_INFO; + const char *code = "Info"; + switch (level) { + case LOGSEVERITY_ERROR: + errorLevel = LOG_WARNING; + code = "Error"; + break; + case LOGSEVERITY_FATAL: + errorLevel = LOG_ERROR; + code = "Fatal"; + break; + default: + return false; + } + + const char *sourceName = ""; + + if (bs && bs->source) + sourceName = obs_source_get_name(bs->source); + + blog(errorLevel, "[obs-browser: '%s'] %s: %s (%s:%d)", sourceName, code, message.ToString().c_str(), + source.ToString().c_str(), line); + return false; +} diff --git a/obs-studio/plugins/obs-browser/browser-client.hpp b/obs-studio/plugins/obs-browser/browser-client.hpp new file mode 100644 index 0000000..069c3ff --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-client.hpp @@ -0,0 +1,154 @@ +/****************************************************************************** + Copyright (C) 2014 by John R. Bradley + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#pragma once + +#include +#include +#include "cef-headers.hpp" +#include "obs-browser-source.hpp" + +struct BrowserSource; + +class BrowserClient : public CefClient, + public CefDisplayHandler, + public CefLifeSpanHandler, + public CefRequestHandler, + public CefResourceRequestHandler, + public CefContextMenuHandler, + public CefRenderHandler, + public CefAudioHandler, + public CefLoadHandler { + + bool sharing_available = false; + bool reroute_audio = true; + ControlLevel webpage_control_level = DEFAULT_CONTROL_LEVEL; + + inline bool valid() const; + + void UpdateExtraTexture(); + +public: + BrowserSource *bs; + CefRect popupRect; + CefRect originalPopupRect; + + int sample_rate; + int channels; + ChannelLayout channel_layout; + int frames_per_buffer; + + inline BrowserClient(BrowserSource *bs_, bool sharing_avail, bool reroute_audio_, + ControlLevel webpage_control_level_) + : sharing_available(sharing_avail), + reroute_audio(reroute_audio_), + webpage_control_level(webpage_control_level_), + bs(bs_) + { + } + + /* CefClient */ + virtual CefRefPtr GetLoadHandler() override; + virtual CefRefPtr GetRenderHandler() override; + virtual CefRefPtr GetDisplayHandler() override; + virtual CefRefPtr GetLifeSpanHandler() override; + virtual CefRefPtr GetRequestHandler() override; + virtual CefRefPtr GetContextMenuHandler() override; + virtual CefRefPtr GetAudioHandler() override; + + virtual bool OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) override; + + /* CefDisplayHandler */ + virtual bool OnConsoleMessage(CefRefPtr browser, cef_log_severity_t level, const CefString &message, + const CefString &source, int line) override; + virtual bool OnTooltip(CefRefPtr browser, CefString &text) override; + + /* CefLifeSpanHandler */ + virtual bool OnBeforePopup(CefRefPtr browser, CefRefPtr frame, +#if CHROME_VERSION_BUILD >= 6834 + int popup_id, +#endif + const CefString &target_url, const CefString &target_frame_name, + cef_window_open_disposition_t target_disposition, bool user_gesture, + const CefPopupFeatures &popupFeatures, CefWindowInfo &windowInfo, + CefRefPtr &client, CefBrowserSettings &settings, + CefRefPtr &extra_info, bool *no_javascript_access) override; + + /* CefRequestHandler */ + virtual CefRefPtr + GetResourceRequestHandler(CefRefPtr browser, CefRefPtr frame, + CefRefPtr request, bool is_navigation, bool is_download, + const CefString &request_initiator, bool &disable_default_handling) override; + virtual void OnRenderProcessTerminated(CefRefPtr browser, TerminationStatus status +#if CHROME_VERSION_BUILD >= 6367 + , + int error_code, const CefString &error_string +#endif + ) override; + + /* CefResourceRequestHandler */ + virtual CefResourceRequestHandler::ReturnValue OnBeforeResourceLoad(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr request, + CefRefPtr callback) override; + + /* CefContextMenuHandler */ + virtual void OnBeforeContextMenu(CefRefPtr browser, CefRefPtr frame, + CefRefPtr params, + CefRefPtr model) override; + + /* CefRenderHandler */ + virtual void GetViewRect(CefRefPtr browser, CefRect &rect) override; + virtual void OnPaint(CefRefPtr browser, PaintElementType type, const RectList &dirtyRects, + const void *buffer, int width, int height) override; +#ifdef ENABLE_BROWSER_SHARED_TEXTURE + virtual void OnAcceleratedPaint(CefRefPtr browser, PaintElementType type, + const RectList &dirtyRects, +#if CHROME_VERSION_BUILD >= 6367 + const CefAcceleratedPaintInfo &info) override; +#else + void *shared_handle) override; +#endif +#ifdef CEF_ON_ACCELERATED_PAINT2 + virtual void OnAcceleratedPaint2(CefRefPtr browser, PaintElementType type, + const RectList &dirtyRects, void *shared_handle, bool new_texture) override; +#endif +#endif + + virtual void OnAudioStreamPacket(CefRefPtr browser, const float **data, int frames, + int64_t pts) override; + + virtual void OnAudioStreamStopped(CefRefPtr browser) override; + + virtual void OnAudioStreamStarted(CefRefPtr browser, const CefAudioParameters ¶ms, + int channels) override; + virtual void OnAudioStreamError(CefRefPtr browser, const CefString &message) override; + const int kFramesPerBuffer = 1024; + virtual bool GetAudioParameters(CefRefPtr browser, CefAudioParameters ¶ms) override; + + /* CefLoadHandler */ + virtual void OnLoadEnd(CefRefPtr browser, CefRefPtr frame, int httpStatusCode) override; + + virtual void OnLoadError(CefRefPtr browser, CefRefPtr frame, + CefLoadHandler::ErrorCode errorCode, const CefString &errorText, + const CefString &failedUrl) override; + + IMPLEMENT_REFCOUNTING(BrowserClient); +}; diff --git a/obs-studio/plugins/obs-browser/browser-config.h.in b/obs-studio/plugins/obs-browser/browser-config.h.in new file mode 100644 index 0000000..9427ec4 --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-config.h.in @@ -0,0 +1,21 @@ +#pragma once + +#ifndef ON +#define ON true +#endif + +#ifndef OFF +#define OFF false +#endif + +#ifndef TRUE +#define TRUE true +#endif + +#ifndef FALSE +#define FALSE false +#endif + +#ifdef __APPLE__ +#define CEF_LIBRARY "@CEF_LIBRARY@" +#endif diff --git a/obs-studio/plugins/obs-browser/browser-dummy-client.cpp b/obs-studio/plugins/obs-browser/browser-dummy-client.cpp new file mode 100644 index 0000000..e891ca7 --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-dummy-client.cpp @@ -0,0 +1,87 @@ +/****************************************************************************** + Copyright (C) 2026 by Matt Gajownik + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "browser-dummy-client.hpp" + +CefRefPtr BrowserDummyClient::GetCommandHandler() +{ + return this; +} + +CefRefPtr BrowserDummyClient::GetRequestHandler() +{ + return this; +} + +CefRefPtr BrowserDummyClient::GetLifeSpanHandler() +{ + return this; +} + +bool BrowserDummyClient::OnChromeCommand(CefRefPtr, int, cef_window_open_disposition_t) +{ + return true; +} + +bool BrowserDummyClient::IsChromeAppMenuItemVisible(CefRefPtr, int) +{ + return false; +} + +bool BrowserDummyClient::IsChromeToolbarButtonVisible(cef_chrome_toolbar_button_type_t) +{ + return false; +} + +bool BrowserDummyClient::IsChromePageActionIconVisible(cef_chrome_page_action_icon_type_t) +{ + return false; +} + +bool BrowserDummyClient::IsChromeAppMenuItemEnabled(CefRefPtr, int) +{ + return false; +} + +bool BrowserDummyClient::OnBeforePopup(CefRefPtr, CefRefPtr, +#if CHROME_VERSION_BUILD >= 6834 + int, +#endif + const CefString &, const CefString &, cef_window_open_disposition_t, bool, + const CefPopupFeatures &, CefWindowInfo &, CefRefPtr &, + CefBrowserSettings &, CefRefPtr &, bool *) +{ + return true; +} + +void BrowserDummyClient::OnAfterCreated(CefRefPtr browser) +{ + if (browser && browser->GetHost()) { + browser->GetHost()->CloseBrowser(false); + } +} + +bool BrowserDummyClient::OnOpenURLFromTab(CefRefPtr, CefRefPtr, const CefString &, + CefRequestHandler::WindowOpenDisposition, bool) +{ + return true; +} + +bool BrowserDummyClient::OnBeforeBrowse(CefRefPtr, CefRefPtr, CefRefPtr, bool, bool) +{ + return true; +} diff --git a/obs-studio/plugins/obs-browser/browser-dummy-client.hpp b/obs-studio/plugins/obs-browser/browser-dummy-client.hpp new file mode 100644 index 0000000..12cf1a5 --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-dummy-client.hpp @@ -0,0 +1,64 @@ +/****************************************************************************** + Copyright (C) 2026 by Matt Gajownik + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "cef-headers.hpp" + +class BrowserDummyClient : public CefClient, + public CefCommandHandler, + public CefRequestHandler, + public CefLifeSpanHandler { +public: + virtual CefRefPtr GetCommandHandler() override; + virtual CefRefPtr GetRequestHandler() override; + virtual CefRefPtr GetLifeSpanHandler() override; + + /* CefCommandHandler */ + virtual bool OnChromeCommand(CefRefPtr browser, int command_id, + cef_window_open_disposition_t disposition) override; + virtual bool IsChromeAppMenuItemVisible(CefRefPtr browser, int command_id) override; + virtual bool IsChromeToolbarButtonVisible(cef_chrome_toolbar_button_type_t button_type) override; + + virtual bool IsChromePageActionIconVisible(cef_chrome_page_action_icon_type_t icon_type) override; + + virtual bool IsChromeAppMenuItemEnabled(CefRefPtr browser, int command_id) override; + + /* CefLifeSpanHandler */ + virtual bool OnBeforePopup(CefRefPtr browser, CefRefPtr frame, +#if CHROME_VERSION_BUILD >= 6834 + int, +#endif + const CefString &target_url, const CefString &target_frame_name, + cef_window_open_disposition_t target_disposition, bool user_gesture, + const CefPopupFeatures &popupFeatures, CefWindowInfo &windowInfo, + CefRefPtr &client, CefBrowserSettings &settings, + CefRefPtr &extra_info, bool *no_javascript_access) override; + + virtual void OnAfterCreated(CefRefPtr browser) override; + + /* CefRequestHandler */ + virtual bool OnOpenURLFromTab(CefRefPtr browser, CefRefPtr frame, + const CefString &target_url, + CefRequestHandler::WindowOpenDisposition target_disposition, + bool user_gesture) override; + + virtual bool OnBeforeBrowse(CefRefPtr browser, CefRefPtr frame, + CefRefPtr request, bool user_gesture, bool is_redirect) override; + + IMPLEMENT_REFCOUNTING(BrowserDummyClient); +}; diff --git a/obs-studio/plugins/obs-browser/browser-scheme.cpp b/obs-studio/plugins/obs-browser/browser-scheme.cpp new file mode 100644 index 0000000..4b3df23 --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-scheme.cpp @@ -0,0 +1,71 @@ +/****************************************************************************** + Copyright (C) 2014 by John R. Bradley + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#include "browser-scheme.hpp" +#include "ip-string.hpp" +#include "wide-string.hpp" +#include + +CefRefPtr BrowserSchemeHandlerFactory::Create(CefRefPtr browser, CefRefPtr, + const CefString &, CefRefPtr request) +{ + if (!browser || !request) + return nullptr; + + CefURLParts parts; + CefParseURL(request->GetURL(), parts); + + std::string path = CefString(&parts.path); + + path = CefURIDecode(path, true, cef_uri_unescape_rule_t::UU_SPACES); + path = CefURIDecode(path, true, cef_uri_unescape_rule_t::UU_URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS); + + std::string fileExtension = path.substr(path.find_last_of(".") + 1); + + for (char &ch : fileExtension) + ch = (char)tolower(ch); + if (fileExtension.compare("woff2") == 0) + fileExtension = "woff"; + + std::string filePath = path.substr(1); + + std::string checkString = path.substr(path.find_first_not_of("/")); + checkString = checkString.substr(0, checkString.find_first_of("/")); + + // An IP address should never be a valid path for CreateForFile normally, but in some cases an OS + // can resolve one as such. As an extra safeguard, we prevent any IP addresses in the path. + if (checkForIpv4String(checkString)) { + return nullptr; + } + + if (checkForIpv6String(checkString)) { + return nullptr; + } + + CefRefPtr stream = CefStreamReader::CreateForFile(filePath); + if (!stream) { + return nullptr; + } + + CefString mimeType = CefGetMimeType(fileExtension); + if (mimeType.empty()) { + mimeType = "application/octet-stream"; + } + + return new CefStreamResourceHandler(mimeType, stream); +} diff --git a/obs-studio/plugins/obs-browser/browser-scheme.hpp b/obs-studio/plugins/obs-browser/browser-scheme.hpp new file mode 100644 index 0000000..323e947 --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-scheme.hpp @@ -0,0 +1,31 @@ +/****************************************************************************** + Copyright (C) 2014 by John R. Bradley + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + ******************************************************************************/ + +#pragma once + +#include "cef-headers.hpp" +#include +#include + +class BrowserSchemeHandlerFactory : public CefSchemeHandlerFactory { +public: + virtual CefRefPtr Create(CefRefPtr browser, CefRefPtr, + const CefString &, CefRefPtr request) override; + + IMPLEMENT_REFCOUNTING(BrowserSchemeHandlerFactory); +}; diff --git a/obs-studio/plugins/obs-browser/browser-version.h b/obs-studio/plugins/obs-browser/browser-version.h new file mode 100644 index 0000000..90f4d77 --- /dev/null +++ b/obs-studio/plugins/obs-browser/browser-version.h @@ -0,0 +1,19 @@ +#pragma once + +#define OBS_BROWSER_VERSION_MAJOR 2 +#define OBS_BROWSER_VERSION_MINOR 27 +#define OBS_BROWSER_VERSION_PATCH 0 + +#ifndef MAKE_SEMANTIC_VERSION +#define MAKE_SEMANTIC_VERSION(major, minor, patch) ((major << 24) | (minor << 16) | patch) +#endif + +#define OBS_BROWSER_VERSION_INT \ + MAKE_SEMANTIC_VERSION(OBS_BROWSER_VERSION_MAJOR, OBS_BROWSER_VERSION_MINOR, OBS_BROWSER_VERSION_PATCH) + +#define OBS_BROWSER_MACRO_STR_(x) #x +#define OBS_BROWSER_MACRO_STR(x) OBS_BROWSER_MACRO_STR_(x) + +#define OBS_BROWSER_VERSION_STRING \ + OBS_BROWSER_MACRO_STR(OBS_BROWSER_VERSION_MAJOR) \ + "." OBS_BROWSER_MACRO_STR(OBS_BROWSER_VERSION_MINOR) "." OBS_BROWSER_MACRO_STR(OBS_BROWSER_VERSION_PATCH) diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/log_debug b/obs-studio/plugins/obs-browser/build-aux/.functions/log_debug new file mode 100644 index 0000000..6a477a1 --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/log_debug @@ -0,0 +1,3 @@ +if (( ! ${+_loglevel} )) typeset -g _loglevel=1 + +if (( _loglevel > 2 )) print -PR -e -- "${CI:+::debug::}%F{220}DEBUG: ${@}%f" diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/log_error b/obs-studio/plugins/obs-browser/build-aux/.functions/log_error new file mode 100644 index 0000000..f1c7b43 --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/log_error @@ -0,0 +1,3 @@ +local icon=' ✖︎ ' + +print -u2 -PR "${CI:+::error::}%F{1} ${icon} %f ${@}" diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/log_group b/obs-studio/plugins/obs-browser/build-aux/.functions/log_group new file mode 100644 index 0000000..7b6aca9 --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/log_group @@ -0,0 +1,16 @@ +autoload -Uz log_info + +if (( ! ${+_log_group} )) typeset -g _log_group=0 + +if (( ${+CI} )) { + if (( _log_group )) { + print "::endgroup::" + typeset -g _log_group=0 + } + if (( # )) { + print "::group::${@}" + typeset -g _log_group=1 + } +} else { + if (( # )) log_info ${@} +} diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/log_info b/obs-studio/plugins/obs-browser/build-aux/.functions/log_info new file mode 100644 index 0000000..d437c29 --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/log_info @@ -0,0 +1,7 @@ +if (( ! ${+_loglevel} )) typeset -g _loglevel=1 + +if (( _loglevel > 0 )) { + local icon=' =>' + + print -PR "%F{4} ${(r:5:)icon}%f %B${@}%b" +} diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/log_output b/obs-studio/plugins/obs-browser/build-aux/.functions/log_output new file mode 100644 index 0000000..4d9b52f --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/log_output @@ -0,0 +1,7 @@ +if (( ! ${+_loglevel} )) typeset -g _loglevel=1 + +if (( _loglevel > 0 )) { + local icon='' + + print -PR " ${(r:5:)icon} ${@}" +} diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/log_status b/obs-studio/plugins/obs-browser/build-aux/.functions/log_status new file mode 100644 index 0000000..950e681 --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/log_status @@ -0,0 +1,7 @@ +if (( ! ${+_loglevel} )) typeset -g _loglevel=1 + +if (( _loglevel > 0 )) { + local icon=' >' + + print -PR "%F{2} ${(r:5:)icon}%f ${@}" +} diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/log_warning b/obs-studio/plugins/obs-browser/build-aux/.functions/log_warning new file mode 100644 index 0000000..ceff982 --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/log_warning @@ -0,0 +1,7 @@ +if (( ! ${+_loglevel} )) typeset -g _loglevel=1 + +if (( _loglevel > 0 )) { + local icon=' =>' + + print -PR "${CI:+::warning::}%F{3} ${(r:5:)icon} ${@}%f" +} diff --git a/obs-studio/plugins/obs-browser/build-aux/.functions/set_loglevel b/obs-studio/plugins/obs-browser/build-aux/.functions/set_loglevel new file mode 100644 index 0000000..e32f4bb --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.functions/set_loglevel @@ -0,0 +1,17 @@ +autoload -Uz log_debug log_error + +local -r _usage="Usage: %B${0}%b + +Set log level, following levels are supported: 0 (quiet), 1 (normal), 2 (verbose), 3 (debug)" + +if (( ! # )); then + log_error 'Called without arguments.' + log_output ${_usage} + return 2 +elif (( ${1} >= 4 )); then + log_error 'Called with loglevel > 3.' + log_output ${_usage} +fi + +typeset -g -i -r _loglevel=${1} +log_debug "Log level set to '${1}'" diff --git a/obs-studio/plugins/obs-browser/build-aux/.run-format.zsh b/obs-studio/plugins/obs-browser/build-aux/.run-format.zsh new file mode 100644 index 0000000..ab74d28 --- /dev/null +++ b/obs-studio/plugins/obs-browser/build-aux/.run-format.zsh @@ -0,0 +1,276 @@ +#!/usr/bin/env zsh + +builtin emulate -L zsh +setopt EXTENDED_GLOB +setopt PUSHD_SILENT +setopt ERR_EXIT +setopt ERR_RETURN +setopt NO_UNSET +setopt PIPE_FAIL +setopt NO_AUTO_PUSHD +setopt NO_PUSHD_IGNORE_DUPS +setopt FUNCTION_ARGZERO + +## Enable for script debugging +# setopt WARN_CREATE_GLOBAL +# setopt WARN_NESTED_VAR +# setopt XTRACE + +autoload -Uz is-at-least && if ! is-at-least 5.2; then + print -u2 -PR "%F{1}${funcstack[1]##*/}:%f Running on Zsh version %B${ZSH_VERSION}%b, but Zsh %B5.2%b is the minimum supported version. Upgrade zsh to fix this issue." + exit 1 +fi + +invoke_formatter() { + if (( # < 1 )) { + log_error "Usage invoke_formatter [formatter_name]" + exit 2 + } + + local formatter="${1}" + shift + local -a source_files=(${@}) + + case ${formatter} { + clang) + if (( ${+commands[clang-format-19]} )) { + local formatter=clang-format-19 + } elif (( ${+commands[clang-format]} )) { + local formatter=clang-format + } else { + log_error "No viable clang-format version found (required 19.1.1)" + exit 2 + } + + local -a formatter_version=($(${formatter} --version)) + + if ! is-at-least 19.1.1 ${formatter_version[-1]}; then + log_error "clang-format is not version 19.1.1 or above (found ${formatter_version[-1]}." + exit 2 + fi + + if ! is-at-least ${formatter_version[-1]} 19.1.1; then + log_error "clang-format is more recent than version 19.1.1 (found ${formatter_version[-1]})." + exit 2 + fi + + if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins|deps|shared|test)/**/*.(c|cpp|h|hpp|m|mm)(.N)) + + source_files=(${source_files:#*/(obs-websocket/deps|decklink/*/decklink-sdk|mac-syphon/syphon-framework|libdshowcapture)/*}) + + local -a format_args=(-style=file -fallback-style=none) + if (( _loglevel > 2 )) format_args+=(--verbose) + + check_files() { + local -i num_failures=0 + local -a source_files=($@) + local file + local -a format_args=(-style=file -fallback-style=none) + if (( _loglevel > 2 )) format_args+=(--verbose) + + local -a command=(${formatter} ${format_args}) + + for file (${source_files}) { + if ! ${command} "${file}" | diff -q "${file}" - &> /dev/null; then + log_error "${file} requires formatting changes." + if (( fail_on_error == 2 )) return 2; + num_failures=$(( num_failures + 1 )) + fi + } + if (( num_failures && fail_on_error == 1 )) return 2 + } + + format_files() { + local -a source_files=($@) + + if (( ${#source_files} )) { + local -a format_args=(-style=file -fallback-style=none -i) + if (( _loglevel > 2 )) format_args+=(--verbose) + + "${formatter}" ${format_args} ${source_files} + } + } + ;; + gersemi) + local formatter=gersemi + if (( ${+commands[gersemi]} )) { + local gersemi_version=($(gersemi --version)) + + if ! is-at-least 0.21.0 ${gersemi_version[2]}; then + log_error "gersemi is not version 0.21.0 or above (found ${gersemi_version[2]}." + exit 2 + fi + } + + if (( ! #source_files )) source_files=(CMakeLists.txt (libobs|libobs-*|frontend|plugins|deps|shared|cmake|test)/**/(CMakeLists.txt|*.cmake)(.N)) + + source_files=(${source_files:#*/(jansson|decklink/*/decklink-sdk|obs-websocket|obs-browser|libdshowcapture)/*}) + source_files=(${source_files:#(cmake/Modules/*|*/legacy.cmake)}) + + check_files() { + local -i num_failures=0 + local -a source_files=($@) + local file + local -a command=(${formatter} -c --no-cache ${source_files}) + + if (( ${#source_files} )) { + while read -r line; do + local -a line_tokens=(${(z)line}) + if (( #line_tokens )) { + file=${line_tokens[1]//*${project_root}\//} + + log_error "${file} requires formatting changes." + } else { + log_error "${line}" + } + + if (( fail_on_error == 2 )) return 2 + num_failures=$(( num_failures + 1 )) + done < <(${command} 2>&1) + + if (( num_failures && fail_on_error == 1 )) return 2 + } + } + + format_files() { + local -a source_files=($@) + + if (( ${#source_files} )) { + "${formatter}" -i ${source_files} + } + } + ;; + swift) + local formatter=swift-format + if (( ${+commands[swift-format]} )) { + local swift_format_version=$(swift-format --version) + + if ! is-at-least 508.0.0 ${swift_format_version}; then + log_error "swift-format is not version 508.0.0 or above (found ${swift_format_version})." + exit 2 + fi + } else { + log_error "No viable swift-format version found (required 508.0.0)" + exit 2 + } + + if (( ! #source_files )) source_files=((libobs|libobs-*|frontend|plugins)/**/*.swift(.N)) + + check_files() { + local -i num_failures=0 + local -a source_files=($@) + local file + local -a format_args=() + + local -a command=(${formatter} ${format_args}) + + for file (${source_files}) { + if ! "${command}" "${file}" | diff -q "${file}" - &> /dev/null; then + log_error "${file} requires formatting changes." + if (( fail_on_error == 2 )) return 2; + num_failures=$(( num_failures + 1 )) + fi + } + if (( num_failures && fail_on_error == 1 )) return 2 + } + + format_files() { + local -a source_files=($@) + + if (( ${#source_files} )) { + local -a format_args=(-i) + + "${formatter}" ${format_args} ${source_files} + } + } + ;; + *) log_error "Invalid formatter specified: ${1}. Valid options are clang-format, gersemi, and swift-format."; exit 2 ;; + } + + local file + local -i num_failures=0 + if (( check_only )) { + if (( ${+functions[check_files]} )) { + check_files ${source_files} + } else { + log_error "No format check function defined for formatter '${formatter}'" + exit 2 + } + } else { + if (( ${+functions[format_files]} )) { + format_files ${source_files} + } else { + log_error "No format function defined for formatter '${formatter}'" + exit 2 + } + } +} + +run_format() { + if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} + if (( ! ${+FORMATTER_NAME} )) typeset -g FORMATTER_NAME=${${(s:-:)ZSH_ARGZERO:t:r}[2]} + local project_root=${SCRIPT_HOME:A:h} + + typeset -g host_os=${${(L)$(uname -s)}//darwin/macos} + local -i fail_on_error=0 + local -i check_only=0 + local -i verbosity=1 + local -r _version='1.0.0' + + fpath=("${SCRIPT_HOME}/.functions" ${fpath}) + autoload -Uz set_loglevel log_info log_error log_output log_status log_warning + + local -r _usage=" +Usage: %B${functrace[1]%:*}%b