From 4e23832d527d4fbcd6323e0ebddf6e59cdd6a44d Mon Sep 17 00:00:00 2001 From: Andrey Nekrasov Date: Tue, 29 Jun 2021 13:06:12 +0300 Subject: [PATCH] Add new VideoConference module for muting mic/cam (#11798) * add new VideoConference module for muting mic/cam Co-authored-by: PrzemyslawTusinski <61138537+PrzemyslawTusinski@users.noreply.github.com> Co-authored-by: Niels Laute --- .github/actions/spell-check/expect.txt | 150 ++- .pipelines/build-tools.cmd | 2 + .pipelines/pipeline.user.windows.yml | 10 +- .pipelines/restore-tools.cmd | 1 + .pipelines/restore.cmd | 9 + Cpp.Build.props | 2 +- PowerToys.sln | 258 ++++- doc/devdocs/readme.md | 7 +- .../PowerToysSetup/PowerToysSetup.wixproj | 3 +- installer/PowerToysSetup/Product.wxs | 48 +- .../CustomAction.cpp | 160 +++ .../CustomAction.def | 6 +- .../PowerToysSetupCustomActions.vcxproj | 4 +- .../PowerToysSetupCustomActions/stdafx.h | 1 + .../SettingsAPI}/FileWatcher.cpp | 0 .../SettingsAPI}/FileWatcher.h | 9 +- src/common/SettingsAPI/SetttingsAPI.vcxproj | 2 + src/common/SettingsAPI/settings_helpers.h | 1 + src/common/Telemetry/ProjectTelemetry.h | 4 +- src/common/interop/PowerToysInterop.vcxproj | 17 +- .../interop/PowerToysInterop.vcxproj.filters | 16 +- src/common/interop/interop.cpp | 31 + src/common/interop/packages.config | 4 + .../fancyzones/FancyZonesLib/FancyZones.cpp | 2 +- .../FancyZonesLib/FancyZonesLib.vcxproj | 2 - .../FancyZonesLib.vcxproj.filters | 6 - .../Icons/Off-NotInUse Dark.png | Bin 0 -> 3959 bytes .../Icons/Off-NotInUse Dark.svg | 19 + .../Icons/Off-NotInUse Light.png | Bin 0 -> 3900 bytes .../Icons/Off-NotInUse Light.svg | 19 + .../Icons/Off-Off Dark.png | Bin 0 -> 3822 bytes .../Icons/Off-Off Dark.svg | 19 + .../Icons/Off-Off Light.png | Bin 0 -> 3846 bytes .../Icons/Off-Off Light.svg | 19 + .../Icons/Off-On Dark.png | Bin 0 -> 3496 bytes .../Icons/Off-On Dark.svg | 19 + .../Icons/Off-On Light.png | Bin 0 -> 3539 bytes .../Icons/Off-On Light.svg | 19 + .../Icons/On-NotInUse Dark.png | Bin 0 -> 3690 bytes .../Icons/On-NotInUse Dark.svg | 19 + .../Icons/On-NotInUse Light.png | Bin 0 -> 3630 bytes .../Icons/On-NotInUse Light.svg | 19 + .../Icons/On-Off Dark.png | Bin 0 -> 3630 bytes .../Icons/On-Off Dark.svg | 19 + .../Icons/On-Off Light.png | Bin 0 -> 3609 bytes .../Icons/On-Off Light.svg | 19 + .../Icons/On-On Dark.png | Bin 0 -> 3277 bytes .../Icons/On-On Dark.svg | 19 + .../Icons/On-On Light.png | Bin 0 -> 3298 bytes .../Icons/On-On Light.svg | 19 + .../VideoConferenceModule/README.md | 14 + .../VideoConferenceModule/Toolbar.cpp | 335 +++++++ .../VideoConferenceModule/Toolbar.h | 61 ++ .../Video Conference.filters | 55 ++ .../Video Conference.vcxproj | 183 ++++ .../Video Conference.vcxproj.filters | 100 ++ .../VideoConferenceModule.cpp | 568 +++++++++++ .../VideoConferenceModule.h | 86 ++ .../VideoConferenceModule/black.bmp | Bin 0 -> 822 bytes .../VideoConferenceModule/dllmain.cpp | 35 + .../VideoConferenceModule/framework.h | 5 + .../VideoConferenceModule/packages.config | 5 + .../VideoConferenceModule/pch.cpp | 5 + .../VideoConferenceModule/pch.h | 24 + .../VideoConferenceModule/trace.cpp | 57 ++ .../VideoConferenceModule/trace.h | 12 + .../DirectShowUtils.cpp | 118 +++ .../DirectShowUtils.h | 88 ++ .../ImageLoading.cpp | 425 ++++++++ .../VideoCaptureDevice.cpp | 634 ++++++++++++ .../VideoCaptureDevice.h | 62 ++ .../VideoCaptureProxyFilter.cpp | 919 ++++++++++++++++++ .../VideoCaptureProxyFilter.h | 120 +++ .../VideoConferenceProxyFilter.vcxproj | 124 +++ .../VideoConferenceProxyFilterx86.sln | 41 + .../build_vcm_x86.cmd | 3 + .../VideoConferenceProxyFilter/dllmain.cpp | 242 +++++ .../VideoConferenceProxyFilter/module.def | 7 + .../packages.config | 4 + .../CameraStateUpdateChannels.cpp | 15 + .../CameraStateUpdateChannels.h | 23 + .../VideoConferenceShared/Logging.cpp | 203 ++++ .../VideoConferenceShared/Logging.h | 62 ++ .../MicrophoneDevice.cpp | 152 +++ .../VideoConferenceShared/MicrophoneDevice.h | 65 ++ .../SerializedSharedMemory.cpp | 188 ++++ .../SerializedSharedMemory.h | 54 + .../VideoCaptureDeviceList.cpp | 100 ++ .../VideoCaptureDeviceList.h | 33 + .../VideoConferenceShared.vcxproj | 140 +++ .../VideoConferenceShared/naming.cpp | 19 + .../VideoConferenceShared/naming.h | 5 + .../VideoConferenceShared/packages.config | 5 + .../VideoConferenceShared/username.cpp | 20 + .../VideoConferenceShared/username.h | 9 + src/modules/videoconference/make_cab.ddf | 20 + src/runner/main.cpp | 10 +- src/runner/runner.vcxproj | 2 +- .../EnabledModules.cs | 16 + ...osoft.PowerToys.Settings.UI.Library.csproj | 7 + .../SndVideoConferenceSettings.cs | 28 + .../StringProperty.cs | 11 + .../VideoConferenceConfigProperties.cs | 86 ++ .../VideoConferenceSettings.cs | 33 + .../VideoConferenceSettingsIPCMessage.cs | 29 + .../ViewModels/VideoConferenceViewModel.cs | 432 ++++++++ .../Assets/Modules/VideoConference.png | Bin 0 -> 69380 bytes .../Microsoft.PowerToys.Settings.UI.csproj | 8 + .../Strings/en-us/Resources.resw | 98 +- .../ViewModels/ShellViewModel.cs | 9 + .../Views/ShellPage.xaml | 8 + .../Views/VideoConference.xaml | 210 ++++ .../Views/VideoConference.xaml.cs | 24 + tools/BugReportTool/BugReportTool/Main.cpp | 40 +- tools/WebcamReportTool/main.cpp | 14 +- tools/build/video_conference_make_cab.ps1 | 13 + 116 files changed, 7425 insertions(+), 81 deletions(-) rename src/{modules/fancyzones/FancyZonesLib => common/SettingsAPI}/FileWatcher.cpp (100%) rename src/{modules/fancyzones/FancyZonesLib => common/SettingsAPI}/FileWatcher.h (74%) create mode 100644 src/common/interop/packages.config create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Dark.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Dark.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Light.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Light.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Dark.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Dark.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Light.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Light.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-On Dark.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-On Dark.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-On Light.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/Off-On Light.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Dark.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Dark.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Light.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Light.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-Off Dark.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-Off Dark.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-Off Light.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-Off Light.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-On Dark.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-On Dark.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-On Light.png create mode 100644 src/modules/videoconference/VideoConferenceModule/Icons/On-On Light.svg create mode 100644 src/modules/videoconference/VideoConferenceModule/README.md create mode 100644 src/modules/videoconference/VideoConferenceModule/Toolbar.cpp create mode 100644 src/modules/videoconference/VideoConferenceModule/Toolbar.h create mode 100644 src/modules/videoconference/VideoConferenceModule/Video Conference.filters create mode 100644 src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj create mode 100644 src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj.filters create mode 100644 src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.cpp create mode 100644 src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.h create mode 100644 src/modules/videoconference/VideoConferenceModule/black.bmp create mode 100644 src/modules/videoconference/VideoConferenceModule/dllmain.cpp create mode 100644 src/modules/videoconference/VideoConferenceModule/framework.h create mode 100644 src/modules/videoconference/VideoConferenceModule/packages.config create mode 100644 src/modules/videoconference/VideoConferenceModule/pch.cpp create mode 100644 src/modules/videoconference/VideoConferenceModule/pch.h create mode 100644 src/modules/videoconference/VideoConferenceModule/trace.cpp create mode 100644 src/modules/videoconference/VideoConferenceModule/trace.h create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.cpp create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.h create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/ImageLoading.cpp create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.cpp create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.h create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.cpp create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.h create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilterx86.sln create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/build_vcm_x86.cmd create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/dllmain.cpp create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/module.def create mode 100644 src/modules/videoconference/VideoConferenceProxyFilter/packages.config create mode 100644 src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.cpp create mode 100644 src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.h create mode 100644 src/modules/videoconference/VideoConferenceShared/Logging.cpp create mode 100644 src/modules/videoconference/VideoConferenceShared/Logging.h create mode 100644 src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.cpp create mode 100644 src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.h create mode 100644 src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.cpp create mode 100644 src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.h create mode 100644 src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.cpp create mode 100644 src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.h create mode 100644 src/modules/videoconference/VideoConferenceShared/VideoConferenceShared.vcxproj create mode 100644 src/modules/videoconference/VideoConferenceShared/naming.cpp create mode 100644 src/modules/videoconference/VideoConferenceShared/naming.h create mode 100644 src/modules/videoconference/VideoConferenceShared/packages.config create mode 100644 src/modules/videoconference/VideoConferenceShared/username.cpp create mode 100644 src/modules/videoconference/VideoConferenceShared/username.h create mode 100644 src/modules/videoconference/make_cab.ddf create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndVideoConferenceSettings.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceConfigProperties.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettings.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettingsIPCMessage.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/VideoConferenceViewModel.cs create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/VideoConference.png create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/VideoConference.xaml create mode 100644 src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/VideoConference.xaml.cs create mode 100644 tools/build/video_conference_make_cab.ps1 diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 65754c0b17..c33d8eb07b 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -46,6 +46,7 @@ Aissue akamaihd ALarger alekhyareddy +alignas ALIGNLEFT ALLAPPS Alloc @@ -142,9 +143,11 @@ autoplay Autorun AUTOSIZECOLUMNS autoupdate -awakeversion AValid +avialable +awakeversion AWAYMODE +AYUV azurecr azurewebsites backend @@ -185,6 +188,7 @@ bms BNumber Bokm BOKMAL +boolalpha Bools bootstrapper Bopomofo @@ -213,6 +217,7 @@ BValue bytearray callbackptr callhistory +Camer Cangjie cangjieime CANRENAME @@ -285,6 +290,7 @@ CMock CMONITORS cmyk cnt +coc coclass codebase codecvt @@ -293,9 +299,10 @@ codereview Codespaces COINIT colorconv +colorfilter colorhistory colorhistorylimit -colorfilter +COLORKEY colorpicker COLORREF colorscheme @@ -429,6 +436,7 @@ dcomp DComposition ddd ddee +ddf Deact declspec decltype @@ -472,11 +480,14 @@ devenum deviceencryption devicemanagenent DEVMON +devpkey +DEVSOURCE DFactory DHCP Dialpad diffing difftime +DIIRFLAG dimm directaccess dirname @@ -489,6 +500,7 @@ Displayandhidethedesktop DISPLAYCHANGE displayname divyan +djsoref DLACTIVEXCTLS DLCONTROL dlg @@ -501,9 +513,10 @@ dllexport dllhost dllmain DNLEN -docsmsft Dns +docsmsft doctype +dogancelik domainlexicon DONTVALIDATEPATH dotnet @@ -531,7 +544,11 @@ dupenv dutil DVASPECT DVASPECTINFO +DVH +DVHD DVR +DVSD +DVSL DVTARGETDEVICE DWindow DWINRT @@ -550,6 +567,8 @@ dword dworigin dwrite dxgi +dxgiformat +dxguid dynamiclock EABF EAC @@ -602,6 +621,7 @@ efa efgh EFile egistry +elif elseif emailandaccounts Emoji @@ -610,6 +630,7 @@ ENABLEDPOPUP endforeach endif endl +endpointvolume endregion Enque ENTERSIZEMOVE @@ -662,7 +683,7 @@ exlist EXPCMDFLAGS EXPCMDSTATE explr -Expr +expr exsb EXSEL exstyle @@ -679,6 +700,7 @@ FANCYZONESDRAWLAYOUTTEST FANCYZONESEDITOR Farbraum FARPROC +fdw feimage ffcd FFDDDDDD @@ -690,13 +712,16 @@ FILEFLAGS FILEFLAGSMASK FILEOP FILEOS +filepath FILESUBTYPE FILESYSPATH filesystem FILETIME FILETYPE FILEVERSION +Filtergraph Filterkeyboard +Filterx finalizer findfast findmydevice @@ -705,6 +730,7 @@ FIXEDFILEINFO FLASHZONES FLASHZONESONQUICKSWITCH Fle +flt fluentui flyout fmtlib @@ -726,6 +752,7 @@ FTYPE FULLNAME fullscreen func +Functiondiscoverykeys fwlink fwrite fxcop @@ -736,6 +763,7 @@ Gamebar gamedvr gamemode GBs +GCLP gcnew gdi gdiplus @@ -778,8 +806,10 @@ Hashset hbitmap hbmp hbr +HBRBACKGROUND HBRUSH hcblack +HCERTSTORE hcwhite hdc HDF @@ -791,6 +821,8 @@ hdrop HDS HEB helptext +HEVC +hfile HGLOBAL hhk HHmmss @@ -865,11 +897,13 @@ IApp IApplication IAppx IAsync +IAudio IAuto IBackground IBase IBeam IBind +ICapture icase iccex ICEBLUE @@ -922,9 +956,11 @@ IFancy ifdef IFeatures IFile +IFilter ifndef IFolder ifstream +IGraph iid IImage Iindex @@ -944,8 +980,11 @@ imagingdevices IMain IMarkdown ime +IMedia +IMem imeutil img +iminstall IMoniker IMonitor IMouse @@ -1002,6 +1041,7 @@ IObject iobjectwithsitesetsite IOle iolewindowcontextsensitivehelp +iomanip iostream IPackage IPath @@ -1023,6 +1063,7 @@ IProperty IPublic IQuery IRead +IReference IReflect IRegistered IRegistration @@ -1055,6 +1096,7 @@ ith IThrottled IThumbnail ITrigger +itsme IUI IUnknown IUri @@ -1065,9 +1107,11 @@ IVector IView IVirtual IWeb -IXml +IWIC IWindows +IXml ixx +IYUV IZone IZoom JArray @@ -1116,17 +1160,17 @@ Keytool keyup KILLFOCUS Knownfolders +KSPROPERTY Kybd LAlt Lambson lamotile langword -langword Lastdevice LASTEXITCODE -Laute launchfaceenrollment launchfingerprintenrollment +Laute laute laviusmotileng LAYOUTRTL @@ -1140,9 +1184,11 @@ Lclean LCONTROL LCtrl Ldone +ldx LEFTSCROLLBAR lego len +LEQ LError Lessthan LEVELID @@ -1166,6 +1212,7 @@ LINQTo Linux listbox listview +lld llkhf Llvm lmcons @@ -1246,6 +1293,7 @@ MAINICON Mainwindow majortype makeappx +makecab MAKEINTRESOURCE MAKEINTRESOURCEW MAKELPARAM @@ -1264,6 +1312,7 @@ MATCHMODE MAXIMIZEBOX MAXSHORTCUTSIZE maxversiontested +MBs MBUTTON MBUTTONDBLCLK MBUTTONDOWN @@ -1273,7 +1322,7 @@ MDICHILD MDL mdpreviewhandler MEDIASUBTYPE -MEDIATYPE +mediatype Melman memcpy memset @@ -1285,8 +1334,17 @@ messageboxes METACHARSET metadata metafile +mfapi mfc mfcribbon +mfidl +mfobjects +mfplat +mfreadwrite +Mfsensorgroup +mftransform +mfuuid +mic microsoft Midl mii @@ -1299,10 +1357,12 @@ miniz minlevel MINMAXINFO Miracast +mirophone MJPG mkdir Mlcfg MLogo +mmdeviceapi MMI Mmsys mobilehotspot @@ -1369,6 +1429,7 @@ mutex mutexes muxc mvvm +myfile MYICON NAMECHANGE nameof @@ -1412,6 +1473,7 @@ netsh netstandard Neue newcolor +newdev newitem newpath newrow @@ -1421,6 +1483,7 @@ niels nielslaute NIF nightlight +nitroin NLD nlog NLSTEXT @@ -1438,6 +1501,7 @@ nodoc noexcept NOFRAMES NOINHERITLAYOUT +NOINTERFACE NOLINKINFO NOMINMAX NOMOVE @@ -1521,6 +1585,7 @@ OPTIMIZEFORINVOKE optin optionalfeatures OPTIONSGROUP +ORAW ORPHANEDDIALOGTITLE oss ostr @@ -1532,6 +1597,7 @@ otheroptions otherusers OUTOFCONTEXT OUTOFMEMORY +outpin Outptr outputtype outro @@ -1553,6 +1619,7 @@ PARENTRELATIVEPARSING parray PARTIALCONFIRMATIONDIALOGTITLE pathcch +PAUDIO pbc Pbgra pcb @@ -1593,6 +1660,7 @@ Pipelinhttps pipename pitem PKBDLLHOOKSTRUCT +PKEY placeholders plib PLK @@ -1611,6 +1679,7 @@ popd popup POPUPWINDOW posix +Postion powerappscds powercfg powerlauncher @@ -1626,6 +1695,7 @@ powertoyswiki Powrprof ppenum ppidl +ppmt pprm pproc ppsi @@ -1643,11 +1713,13 @@ Prefixer Preinstalled preload PREMULTIPLIED +preperty prevhost previewer PREVIEWGROUP PREVIEWHANDLERFRAMEINFO previewpane +previouscamera PREVIOUSVERSIONSINSTALLED prevpane prgms @@ -1664,6 +1736,7 @@ PROGRAMFILES progressbar Proj projectname +PROPBAG propkey propvarutil prpui @@ -1676,6 +1749,7 @@ psfgao Psr psrm psrree +pstr pstream pstrm psz @@ -1693,6 +1767,7 @@ PVOID pwa pwcs PWSTR +pwsz pwtd qianlifeng qit @@ -1738,20 +1813,27 @@ rectp rects recyclebin redirectedfrom +reencode +reencoded refactor refactoring REFCLSID refcount +REFGUID REFIID REGCLS regedit regex +REGFILTER +REGFILTERPINS regionformatting regionlanguage REGISTERCLASSFAILED Registery registrypath regkey +REGPINTYPES +regsvr reimplementing reloadable Remapper @@ -1803,13 +1885,14 @@ RKey RMENU RNumber roadmap +robocopy Roboto roslyn royvou Rpc RRF -rshift RSHIFT +rshift Rsp rst Rstrtmgr @@ -1859,6 +1942,7 @@ SEARCHFOR SEARCHREPLACEGROUP searchterm Secur +seekg Segoe Sekan SENDCHANGE @@ -1904,8 +1988,8 @@ Shl shldisp shlobj shlwapi +shmem shobjidl -shortsplit SHORTCUTATLEAST shortcutcontrol Shortcutguide @@ -1917,6 +2001,7 @@ SHORTCUTSTARTWITHMODIFIER Shortcuttool shortdate SHORTPATH +shortsplit showcolorname SHOWDEFAULT SHOWELEVATIONPROMPT @@ -1945,6 +2030,7 @@ SIZENESW SIZENS SIZENWSE sizeof +sizeread SIZEWE sketchapp SKIPOWNPROCESS @@ -1974,6 +2060,7 @@ spesi splitwstring sppd sppre +sprintf spsi spsia spsrif @@ -2031,6 +2118,7 @@ storagesense stoul stoull strcmp +streampos strftime Stringified Stringify @@ -2057,8 +2145,8 @@ surfacehub sut SVE svg -SVGIO SVGIn +SVGIO svgpreviewhandler SWC SWFO @@ -2068,6 +2156,7 @@ swprintf SWRESTORE SYMED SYMOPT +SYNCMFT SYNCPAINT sys SYSCHAR @@ -2083,9 +2172,9 @@ syslog SYSMENU systemd SYSTEMTIME -Tadele sz tabletmode +Tadele tadele Tahoma talenthrcore @@ -2111,6 +2200,7 @@ tchar tcscpy TCustom TDevice +tellg Templated templatenamespace Temporarilypeekatthedesktop @@ -2132,8 +2222,8 @@ THISCOMPONENT thre tif TILEDWINDOW -timediff timedate +timediff Timeline TIMERID timeunion @@ -2143,6 +2233,7 @@ TLayout tlb tlbimp tmp +TMPVAR TNP todo toggleswitch @@ -2161,12 +2252,15 @@ towlower towupper tracelogging traies +transcoded +transparrent TRAYMOUSEMESSAGE TRK trl trueplay truetype trunc +tspan TStr tsx TYMED @@ -2192,8 +2286,10 @@ UIPI UIs UITo ULARGE +ulazy ULLONG ulong +ULONGLONG umd unchecks uncomment @@ -2222,6 +2318,7 @@ unknwn UNLEN unlicense Unmap +unmute UNORM unregister unregistering @@ -2256,16 +2353,20 @@ uuidof uwp UWPUI uxtheme +UYVY validmodulename vcamp vccorlib +vcdl VCINSTALLDIR +vcm vcomp vcredist VCRT vcruntime vcvars vcxproj +vdi VDId vec VERBSONLY @@ -2275,10 +2376,14 @@ VERSIONINFO Versioning VFT vid +VIDCAP +videoconference +videoconferencevirtualdriver VIDEOINFOHEADER videoplayback viewbox viewmodel +vih virtualization visiblecolorformats Visibletrue @@ -2298,6 +2403,7 @@ VSCBD vscode VSCROLL vse +vsix vsonline vstemplate VSTHRD @@ -2320,6 +2426,9 @@ wcscpy wcslen wcsncmp wcsnicmp +WDK +wdksetup +wdkvsix wdp wdupenv We'd @@ -2346,6 +2455,8 @@ wikipedia wil wildcards winapi +wincodec +Wincodecsdk wincolor windef windevbuildagents @@ -2401,6 +2512,7 @@ WMKEYUP wmp WMSYSKEYDOWN WMSYSKEYUP +WMV wnd WNDCLASS WNDCLASSEX @@ -2431,9 +2543,11 @@ wstringstream wsz wtoi WTS +wtsapi WTSAT wu wubi +WVC Wwan www wxs @@ -2448,6 +2562,7 @@ XBUTTON XBUTTONDBLCLK XBUTTONDOWN XBUTTONUP +xcopy XDiff XDocument XElement @@ -2479,7 +2594,10 @@ yourinfo YourUserName YStr YUY +yuyoyuppe YUYV +YVU +YVYU ZEROINIT ZIndex zipfolder @@ -2491,9 +2609,3 @@ ZONESETCHANGE Zoneszonabletester Zoomusingmagnifier zzz -coc -djsoref -dogancelik -itsme -nitroin -ulazy diff --git a/.pipelines/build-tools.cmd b/.pipelines/build-tools.cmd index 6ae68dbed1..aa5f782932 100644 --- a/.pipelines/build-tools.cmd +++ b/.pipelines/build-tools.cmd @@ -7,3 +7,5 @@ set SolutionDir=%cd% popd SET IsPipeline=1 call msbuild ../tools/BugReportTool/BugReportTool.sln /p:Configuration=Release /p:Platform=x64 /p:CIBuild=true || exit /b 1 +call msbuild ../tools/WebcamReportTool/WebcamReportTool.sln /p:Configuration=Release /p:Platform=x64 /p:CIBuild=true || exit /b 1 + diff --git a/.pipelines/pipeline.user.windows.yml b/.pipelines/pipeline.user.windows.yml index 5898c1db0b..29581b3763 100644 --- a/.pipelines/pipeline.user.windows.yml +++ b/.pipelines/pipeline.user.windows.yml @@ -61,6 +61,11 @@ build: - 'x64/**/*.pdb' exclude: - 'x64/Release/obj/**/*.pdb' + # TODO(yuyoyuppe): uncomment when VCM should be enabled + #- from: 'x86/Release' + # to: 'Build_Output' + # include: + # - 'modules\VideoConference\VideoConferenceProxyFilter_x86.dll' - from: 'x64/Release' to: 'Build_Output' include: @@ -159,6 +164,9 @@ build: - 'modules\PowerRename\PowerRenameExt.dll' - 'modules\ShortcutGuide\ShortcutGuide\PowerToys.ShortcutGuide.exe' - 'modules\ShortcutGuide\ShortcutGuideModuleInterface\ShortcutGuideModuleInterface.dll' + # TODO(yuyoyuppe): uncomment when VCM should be enabled + #- 'modules\VideoConference\VideoConferenceModule.dll' + #- 'modules\VideoConference\VideoConferenceProxyFilter_x64.dll' - 'Settings\ManagedTelemetry.dll' - 'Settings\Microsoft.PowerToys.Settings.UI.exe' - 'Settings\Microsoft.PowerToys.Settings.UI.Lib.dll' @@ -177,6 +185,7 @@ build: to: 'Build_Output' include: - 'BugReportTool\BugReportTool.exe' + - 'WebcamReportTool\WebcamReportTool.exe' signing_options: sign_inline: true # This does signing a soon as this command completes - !!buildcommand @@ -237,4 +246,3 @@ static_analysis_options: files_to_scan: - exclude: - '**/*.lcl' - diff --git a/.pipelines/restore-tools.cmd b/.pipelines/restore-tools.cmd index 852d59942d..0f374f9b59 100644 --- a/.pipelines/restore-tools.cmd +++ b/.pipelines/restore-tools.cmd @@ -1,3 +1,4 @@ cd /D "%~dp0" nuget restore ../tools/BugReportTool/BugReportTool.sln || exit /b 1 +nuget restore ../tools/WebcamReportTool/WebcamReportTool.sln || exit /b 1 diff --git a/.pipelines/restore.cmd b/.pipelines/restore.cmd index fd630aea59..22eb393aaa 100644 --- a/.pipelines/restore.cmd +++ b/.pipelines/restore.cmd @@ -1,3 +1,12 @@ cd /D "%~dp0" nuget restore ../PowerToys.sln || exit /b 1 + +powershell.exe -Command "Invoke-WebRequest -OutFile %tmp%\wdksetup.exe https://go.microsoft.com/fwlink/p/?linkid=2085767" +%tmp%\wdksetup.exe /q + +copy "C:\Program Files (x86)\Windows Kits\10\Vsix\VS2019\WDK.vsix" %tmp%\wdkvsix.zip +powershell Expand-Archive %tmp%\wdkvsix.zip -DestinationPath %tmp%\wdkvsix -Force + +robocopy /e %tmp%\wdkvsix\$MSBuild\Microsoft\VC\v160 "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Microsoft\VC\v160" || IF %ERRORLEVEL% LEQ 7 EXIT 0 +robocopy /e %tmp%\wdkvsix\$VCTargets "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\VC\VCTargets" || IF %ERRORLEVEL% LEQ 7 EXIT 0 diff --git a/Cpp.Build.props b/Cpp.Build.props index ed8209a0ac..c6d115cd13 100644 --- a/Cpp.Build.props +++ b/Cpp.Build.props @@ -75,7 +75,7 @@ - v142 + v142 $(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\ Unicode Spectre diff --git a/PowerToys.sln b/PowerToys.sln index f4a534638c..4d0c415207 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -333,7 +333,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter.UnitTest", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj", "{3E424AD2-19E5-4AE6-B833-F53963EB5FC1}" EndProject -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shortcutguide", "shortcutguide", "{106CBECA-0701-4FC3-838C-9DF816A19AE2}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ShortcutGuideModuleInterface", "src\modules\ShortcutGuide\ShortcutGuideModuleInterface\ShortcutGuideModuleInterface.vcxproj", "{2D604C07-51FC-46BB-9EB7-75AECC7F5E81}" @@ -342,376 +341,597 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ShortcutGuide", "src\module EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZonesModuleInterface", "src\modules\fancyzones\FancyZonesModuleInterface\FancyZonesModuleInterface.vcxproj", "{48804216-2A0E-4168-A6D8-9CD068D14227}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZones", "src\modules\fancyzones\FancyZones\FancyZones.vcxproj", "{390AE700-B55F-4202-91EA-A822EB75B9BD}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZones", "src\modules\fancyzones\FancyZones\FancyZones.vcxproj", "{FF1D7936-842A-4BBB-8BEA-E9FE796DE700}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.Update", "src\Update\PowerToys.Update.vcxproj", "{44CE9AE1-4390-42C5-BACC-0FD6B40AA203}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.WindowsSettings", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VideoConferenceShared", "src\modules\videoconference\VideoConferenceShared\VideoConferenceShared.vcxproj", "{459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VideoConferenceModule", "src\modules\videoconference\VideoConferenceModule\Video Conference.vcxproj", "{5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VideoConferenceProxyFilter", "src\modules\videoconference\VideoConferenceProxyFilter\VideoConferenceProxyFilter.vcxproj", "{AC2857B4-103D-4D6D-9740-926EBF785042}" + ProjectSection(ProjectDependencies) = postProject + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A} = {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A} + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VideoConference", "VideoConference", "{470FBAF9-E1F8-4F3E-8786-198A1C81C8A8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Debug|x64.ActiveCfg = Debug|x64 {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Debug|x64.Build.0 = Debug|x64 + {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Debug|x86.ActiveCfg = Debug|x64 {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Release|x64.ActiveCfg = Release|x64 {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Release|x64.Build.0 = Release|x64 + {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Release|x86.ActiveCfg = Release|x64 {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Debug|x64.ActiveCfg = Debug|x64 {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Debug|x64.Build.0 = Debug|x64 + {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Debug|x86.ActiveCfg = Debug|x64 {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Release|x64.ActiveCfg = Release|x64 {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Release|x64.Build.0 = Release|x64 + {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Release|x86.ActiveCfg = Release|x64 {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Debug|x64.ActiveCfg = Debug|x64 {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Debug|x64.Build.0 = Debug|x64 + {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Debug|x86.ActiveCfg = Debug|x64 {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Release|x64.ActiveCfg = Release|x64 {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Release|x64.Build.0 = Release|x64 + {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Release|x86.ActiveCfg = Release|x64 {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Debug|x64.ActiveCfg = Debug|x64 {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Debug|x64.Build.0 = Debug|x64 + {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Debug|x86.ActiveCfg = Debug|x64 {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.ActiveCfg = Release|x64 {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.Build.0 = Release|x64 + {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x86.ActiveCfg = Release|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.ActiveCfg = Debug|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.Build.0 = Debug|x64 + {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x86.ActiveCfg = Debug|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.ActiveCfg = Release|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.Build.0 = Release|x64 + {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x86.ActiveCfg = Release|x64 {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x64.ActiveCfg = Debug|x64 {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x64.Build.0 = Debug|x64 + {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x86.ActiveCfg = Debug|x64 {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|x64.ActiveCfg = Release|x64 {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|x64.Build.0 = Release|x64 + {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|x86.ActiveCfg = Release|x64 {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|x64.ActiveCfg = Debug|x64 {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|x64.Build.0 = Debug|x64 + {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|x86.ActiveCfg = Debug|x64 {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|x64.ActiveCfg = Release|x64 {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|x64.Build.0 = Release|x64 + {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|x86.ActiveCfg = Release|x64 {0E072714-D127-460B-AFAD-B4C40B412798}.Debug|x64.ActiveCfg = Debug|x64 {0E072714-D127-460B-AFAD-B4C40B412798}.Debug|x64.Build.0 = Debug|x64 + {0E072714-D127-460B-AFAD-B4C40B412798}.Debug|x86.ActiveCfg = Debug|x64 {0E072714-D127-460B-AFAD-B4C40B412798}.Release|x64.ActiveCfg = Release|x64 {0E072714-D127-460B-AFAD-B4C40B412798}.Release|x64.Build.0 = Release|x64 + {0E072714-D127-460B-AFAD-B4C40B412798}.Release|x86.ActiveCfg = Release|x64 {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|x64.ActiveCfg = Debug|x64 {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|x64.Build.0 = Debug|x64 + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|x86.ActiveCfg = Debug|x64 {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|x64.ActiveCfg = Release|x64 {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|x64.Build.0 = Release|x64 + {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|x86.ActiveCfg = Release|x64 {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|x64.ActiveCfg = Debug|x64 {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|x64.Build.0 = Debug|x64 + {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|x86.ActiveCfg = Debug|x64 {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|x64.ActiveCfg = Release|x64 {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|x64.Build.0 = Release|x64 + {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|x86.ActiveCfg = Release|x64 {64A80062-4D8B-4229-8A38-DFA1D7497749}.Debug|x64.ActiveCfg = Debug|x64 {64A80062-4D8B-4229-8A38-DFA1D7497749}.Debug|x64.Build.0 = Debug|x64 + {64A80062-4D8B-4229-8A38-DFA1D7497749}.Debug|x86.ActiveCfg = Debug|x64 {64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.ActiveCfg = Release|x64 {64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.Build.0 = Release|x64 + {64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x86.ActiveCfg = Release|x64 {0485F45C-EA7A-4BB5-804B-3E8D14699387}.Debug|x64.ActiveCfg = Debug|x64 + {0485F45C-EA7A-4BB5-804B-3E8D14699387}.Debug|x64.Build.0 = Debug|x64 + {0485F45C-EA7A-4BB5-804B-3E8D14699387}.Debug|x86.ActiveCfg = Debug|x64 {0485F45C-EA7A-4BB5-804B-3E8D14699387}.Release|x64.ActiveCfg = Release|x64 + {0485F45C-EA7A-4BB5-804B-3E8D14699387}.Release|x64.Build.0 = Release|x64 + {0485F45C-EA7A-4BB5-804B-3E8D14699387}.Release|x86.ActiveCfg = Release|x64 {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.ActiveCfg = Debug|x64 {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.Build.0 = Debug|x64 + {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x86.ActiveCfg = Debug|x64 {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Release|x64.ActiveCfg = Release|x64 {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Release|x64.Build.0 = Release|x64 + {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Release|x86.ActiveCfg = Release|x64 {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Debug|x64.ActiveCfg = Debug|x64 {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Debug|x64.Build.0 = Debug|x64 + {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Debug|x86.ActiveCfg = Debug|x64 {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Release|x64.ActiveCfg = Release|x64 {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Release|x64.Build.0 = Release|x64 + {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Release|x86.ActiveCfg = Release|x64 {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Debug|x64.ActiveCfg = Debug|x64 {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Debug|x64.Build.0 = Debug|x64 + {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Debug|x86.ActiveCfg = Debug|x64 {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Release|x64.ActiveCfg = Release|x64 {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Release|x64.Build.0 = Release|x64 + {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Release|x86.ActiveCfg = Release|x64 {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Debug|x64.ActiveCfg = Debug|x64 {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Debug|x64.Build.0 = Debug|x64 + {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Debug|x86.ActiveCfg = Debug|x64 {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Release|x64.ActiveCfg = Release|x64 {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Release|x64.Build.0 = Release|x64 + {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Release|x86.ActiveCfg = Release|x64 {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Debug|x64.ActiveCfg = Debug|x64 {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Debug|x64.Build.0 = Debug|x64 + {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Debug|x86.ActiveCfg = Debug|x64 {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Release|x64.ActiveCfg = Release|x64 {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Release|x64.Build.0 = Release|x64 + {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Release|x86.ActiveCfg = Release|x64 {17DA04DF-E393-4397-9CF0-84DABE11032E}.Debug|x64.ActiveCfg = Debug|x64 {17DA04DF-E393-4397-9CF0-84DABE11032E}.Debug|x64.Build.0 = Debug|x64 + {17DA04DF-E393-4397-9CF0-84DABE11032E}.Debug|x86.ActiveCfg = Debug|x64 {17DA04DF-E393-4397-9CF0-84DABE11032E}.Release|x64.ActiveCfg = Release|x64 {17DA04DF-E393-4397-9CF0-84DABE11032E}.Release|x64.Build.0 = Release|x64 + {17DA04DF-E393-4397-9CF0-84DABE11032E}.Release|x86.ActiveCfg = Release|x64 {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Debug|x64.ActiveCfg = Debug|x64 {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Debug|x64.Build.0 = Debug|x64 + {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Debug|x86.ActiveCfg = Debug|x64 {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Release|x64.ActiveCfg = Release|x64 {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Release|x64.Build.0 = Release|x64 + {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Release|x86.ActiveCfg = Release|x64 {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|x64.ActiveCfg = Debug|x64 {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|x64.Build.0 = Debug|x64 + {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|x86.ActiveCfg = Debug|x64 {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Release|x64.ActiveCfg = Release|x64 {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Release|x64.Build.0 = Release|x64 + {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Release|x86.ActiveCfg = Release|x64 {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Debug|x64.ActiveCfg = Debug|x64 {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Debug|x64.Build.0 = Debug|x64 + {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Debug|x86.ActiveCfg = Debug|x64 {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x64.ActiveCfg = Release|x64 {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x64.Build.0 = Release|x64 + {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x86.ActiveCfg = Release|x64 {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|x64 {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|x64 + {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|x64 {FF742965-9A80-41A5-B042-D6C7D3A21708}.Release|x64.ActiveCfg = Release|x64 {FF742965-9A80-41A5-B042-D6C7D3A21708}.Release|x64.Build.0 = Release|x64 + {FF742965-9A80-41A5-B042-D6C7D3A21708}.Release|x86.ActiveCfg = Release|x64 {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Debug|x64.ActiveCfg = Debug|x64 {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Debug|x64.Build.0 = Debug|x64 + {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Debug|x86.ActiveCfg = Debug|x64 {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Release|x64.ActiveCfg = Release|x64 {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Release|x64.Build.0 = Release|x64 + {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Release|x86.ActiveCfg = Release|x64 {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Debug|x64.ActiveCfg = Debug|x64 {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Debug|x64.Build.0 = Debug|x64 + {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Debug|x86.ActiveCfg = Debug|x64 {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Release|x64.ActiveCfg = Release|x64 {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Release|x64.Build.0 = Release|x64 + {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Release|x86.ActiveCfg = Release|x64 {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Debug|x64.ActiveCfg = Debug|x64 {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Debug|x64.Build.0 = Debug|x64 + {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Debug|x86.ActiveCfg = Debug|x64 {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Release|x64.ActiveCfg = Release|x64 {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Release|x64.Build.0 = Release|x64 + {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Release|x86.ActiveCfg = Release|x64 {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Debug|x64.ActiveCfg = Debug|x64 {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Debug|x64.Build.0 = Debug|x64 + {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Debug|x86.ActiveCfg = Debug|x64 {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Release|x64.ActiveCfg = Release|x64 {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Release|x64.Build.0 = Release|x64 + {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Release|x86.ActiveCfg = Release|x64 {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|x64.ActiveCfg = Debug|x64 {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|x64.Build.0 = Debug|x64 + {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|x86.ActiveCfg = Debug|x64 {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Release|x64.ActiveCfg = Release|x64 {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Release|x64.Build.0 = Release|x64 + {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Release|x86.ActiveCfg = Release|x64 {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Debug|x64.ActiveCfg = Debug|x64 {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Debug|x64.Build.0 = Debug|x64 + {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Debug|x86.ActiveCfg = Debug|x64 {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Release|x64.ActiveCfg = Release|x64 {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Release|x64.Build.0 = Release|x64 + {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Release|x86.ActiveCfg = Release|x64 {E364F67B-BB12-4E91-B639-355866EBCD8B}.Debug|x64.ActiveCfg = Debug|x64 {E364F67B-BB12-4E91-B639-355866EBCD8B}.Debug|x64.Build.0 = Debug|x64 + {E364F67B-BB12-4E91-B639-355866EBCD8B}.Debug|x86.ActiveCfg = Debug|x64 {E364F67B-BB12-4E91-B639-355866EBCD8B}.Release|x64.ActiveCfg = Release|x64 {E364F67B-BB12-4E91-B639-355866EBCD8B}.Release|x64.Build.0 = Release|x64 + {E364F67B-BB12-4E91-B639-355866EBCD8B}.Release|x86.ActiveCfg = Release|x64 {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Debug|x64.ActiveCfg = Debug|x64 {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Debug|x64.Build.0 = Debug|x64 + {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Debug|x86.ActiveCfg = Debug|x64 {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Release|x64.ActiveCfg = Release|x64 {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Release|x64.Build.0 = Release|x64 + {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Release|x86.ActiveCfg = Release|x64 {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Debug|x64.ActiveCfg = Debug|x64 {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Debug|x64.Build.0 = Debug|x64 + {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Debug|x86.ActiveCfg = Debug|x64 {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Release|x64.ActiveCfg = Release|x64 {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Release|x64.Build.0 = Release|x64 + {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Release|x86.ActiveCfg = Release|x64 {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Debug|x64.ActiveCfg = Debug|x64 {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Debug|x64.Build.0 = Debug|x64 + {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Debug|x86.ActiveCfg = Debug|x64 {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Release|x64.ActiveCfg = Release|x64 {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Release|x64.Build.0 = Release|x64 + {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Release|x86.ActiveCfg = Release|x64 {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Debug|x64.ActiveCfg = Debug|x64 {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Debug|x64.Build.0 = Debug|x64 + {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Debug|x86.ActiveCfg = Debug|x64 {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Release|x64.ActiveCfg = Release|x64 {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Release|x64.Build.0 = Release|x64 + {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Release|x86.ActiveCfg = Release|x64 {DA425894-6E13-404F-8DCB-78584EC0557A}.Debug|x64.ActiveCfg = Debug|x64 {DA425894-6E13-404F-8DCB-78584EC0557A}.Debug|x64.Build.0 = Debug|x64 + {DA425894-6E13-404F-8DCB-78584EC0557A}.Debug|x86.ActiveCfg = Debug|x64 {DA425894-6E13-404F-8DCB-78584EC0557A}.Release|x64.ActiveCfg = Release|x64 {DA425894-6E13-404F-8DCB-78584EC0557A}.Release|x64.Build.0 = Release|x64 + {DA425894-6E13-404F-8DCB-78584EC0557A}.Release|x86.ActiveCfg = Release|x64 {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Debug|x64.ActiveCfg = Debug|x64 {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Debug|x64.Build.0 = Debug|x64 + {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Debug|x86.ActiveCfg = Debug|x64 {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Release|x64.ActiveCfg = Release|x64 {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Release|x64.Build.0 = Release|x64 + {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Release|x86.ActiveCfg = Release|x64 {748417CA-F17E-487F-9411-CAFB6D3F4877}.Debug|x64.ActiveCfg = Debug|x64 {748417CA-F17E-487F-9411-CAFB6D3F4877}.Debug|x64.Build.0 = Debug|x64 + {748417CA-F17E-487F-9411-CAFB6D3F4877}.Debug|x86.ActiveCfg = Debug|x64 {748417CA-F17E-487F-9411-CAFB6D3F4877}.Release|x64.ActiveCfg = Release|x64 {748417CA-F17E-487F-9411-CAFB6D3F4877}.Release|x64.Build.0 = Release|x64 + {748417CA-F17E-487F-9411-CAFB6D3F4877}.Release|x86.ActiveCfg = Release|x64 {217DF501-135C-4E38-BFC8-99D4821032EA}.Debug|x64.ActiveCfg = Debug|x64 {217DF501-135C-4E38-BFC8-99D4821032EA}.Debug|x64.Build.0 = Debug|x64 + {217DF501-135C-4E38-BFC8-99D4821032EA}.Debug|x86.ActiveCfg = Debug|x64 {217DF501-135C-4E38-BFC8-99D4821032EA}.Release|x64.ActiveCfg = Release|x64 {217DF501-135C-4E38-BFC8-99D4821032EA}.Release|x64.Build.0 = Release|x64 + {217DF501-135C-4E38-BFC8-99D4821032EA}.Release|x86.ActiveCfg = Release|x64 {47310AB4-9034-4BD1-8D8B-E88AD21A171B}.Debug|x64.ActiveCfg = Debug|x64 {47310AB4-9034-4BD1-8D8B-E88AD21A171B}.Debug|x64.Build.0 = Debug|x64 + {47310AB4-9034-4BD1-8D8B-E88AD21A171B}.Debug|x86.ActiveCfg = Debug|x64 {47310AB4-9034-4BD1-8D8B-E88AD21A171B}.Release|x64.ActiveCfg = Release|x64 {47310AB4-9034-4BD1-8D8B-E88AD21A171B}.Release|x64.Build.0 = Release|x64 + {47310AB4-9034-4BD1-8D8B-E88AD21A171B}.Release|x86.ActiveCfg = Release|x64 {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Debug|x64.ActiveCfg = Debug|x64 {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Debug|x64.Build.0 = Debug|x64 + {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Debug|x86.ActiveCfg = Debug|Win32 + {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Debug|x86.Build.0 = Debug|Win32 + {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Debug|x86.Deploy.0 = Debug|Win32 {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Release|x64.ActiveCfg = Release|x64 {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Release|x64.Build.0 = Release|x64 + {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Release|x86.ActiveCfg = Release|Win32 + {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Release|x86.Build.0 = Release|Win32 + {A7D5099E-F0FD-4BF3-8522-5A682759F915}.Release|x86.Deploy.0 = Release|Win32 {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Debug|x64.ActiveCfg = Debug|x64 {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Debug|x64.Build.0 = Debug|x64 + {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Debug|x86.ActiveCfg = Debug|x64 {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Release|x64.ActiveCfg = Release|x64 {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Release|x64.Build.0 = Release|x64 + {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Release|x86.ActiveCfg = Release|x64 {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|x64.ActiveCfg = Debug|x64 {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|x64.Build.0 = Debug|x64 + {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|x86.ActiveCfg = Debug|x64 {F055103B-F80B-4D0C-BF48-057C55620033}.Release|x64.ActiveCfg = Release|x64 {F055103B-F80B-4D0C-BF48-057C55620033}.Release|x64.Build.0 = Release|x64 + {F055103B-F80B-4D0C-BF48-057C55620033}.Release|x86.ActiveCfg = Release|x64 {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.ActiveCfg = Debug|x64 {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.Build.0 = Debug|x64 + {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x86.ActiveCfg = Debug|x64 {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.ActiveCfg = Release|x64 {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.Build.0 = Release|x64 + {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x86.ActiveCfg = Release|x64 {08C8C05F-0362-41BC-818C-724572DF8B06}.Debug|x64.ActiveCfg = Debug|x64 {08C8C05F-0362-41BC-818C-724572DF8B06}.Debug|x64.Build.0 = Debug|x64 + {08C8C05F-0362-41BC-818C-724572DF8B06}.Debug|x86.ActiveCfg = Debug|x64 {08C8C05F-0362-41BC-818C-724572DF8B06}.Release|x64.ActiveCfg = Release|x64 {08C8C05F-0362-41BC-818C-724572DF8B06}.Release|x64.Build.0 = Release|x64 + {08C8C05F-0362-41BC-818C-724572DF8B06}.Release|x86.ActiveCfg = Release|x64 {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Debug|x64.ActiveCfg = Debug|x64 {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Debug|x64.Build.0 = Debug|x64 + {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Debug|x86.ActiveCfg = Debug|x64 {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Release|x64.ActiveCfg = Release|x64 {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Release|x64.Build.0 = Release|x64 + {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Release|x86.ActiveCfg = Release|x64 {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|x64.ActiveCfg = Debug|x64 {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|x64.Build.0 = Debug|x64 + {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|x86.ActiveCfg = Debug|x64 {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.ActiveCfg = Release|x64 {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.Build.0 = Release|x64 + {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x86.ActiveCfg = Release|x64 {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.ActiveCfg = Debug|x64 {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.Build.0 = Debug|x64 + {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x86.ActiveCfg = Debug|x64 {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.ActiveCfg = Release|x64 {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.Build.0 = Release|x64 + {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x86.ActiveCfg = Release|x64 {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Debug|x64.ActiveCfg = Debug|x64 {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Debug|x64.Build.0 = Debug|x64 + {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Debug|x86.ActiveCfg = Debug|x64 {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Release|x64.ActiveCfg = Release|x64 {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Release|x64.Build.0 = Release|x64 + {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Release|x86.ActiveCfg = Release|x64 {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Debug|x64.ActiveCfg = Debug|x64 {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Debug|x64.Build.0 = Debug|x64 + {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Debug|x86.ActiveCfg = Debug|x64 {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Release|x64.ActiveCfg = Release|x64 {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Release|x64.Build.0 = Release|x64 + {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Release|x86.ActiveCfg = Release|x64 {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Debug|x64.ActiveCfg = Debug|x64 {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Debug|x64.Build.0 = Debug|x64 + {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Debug|x86.ActiveCfg = Debug|x64 {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Release|x64.ActiveCfg = Release|x64 {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Release|x64.Build.0 = Release|x64 + {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Release|x86.ActiveCfg = Release|x64 {BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|x64.ActiveCfg = Debug|x64 {BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|x64.Build.0 = Debug|x64 + {BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|x86.ActiveCfg = Debug|x64 {BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|x64.ActiveCfg = Release|x64 {BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|x64.Build.0 = Release|x64 + {BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|x86.ActiveCfg = Release|x64 {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|x64.ActiveCfg = Debug|x64 {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|x64.Build.0 = Debug|x64 + {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|x86.ActiveCfg = Debug|x64 {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|x64.ActiveCfg = Release|x64 {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|x64.Build.0 = Release|x64 + {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|x86.ActiveCfg = Release|x64 {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|x64.ActiveCfg = Debug|x64 {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|x64.Build.0 = Debug|x64 + {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|x86.ActiveCfg = Debug|x64 {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|x64.ActiveCfg = Release|x64 {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|x64.Build.0 = Release|x64 + {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|x86.ActiveCfg = Release|x64 {0F85E674-34AE-443D-954C-8321EB8B93B1}.Debug|x64.ActiveCfg = Debug|x64 {0F85E674-34AE-443D-954C-8321EB8B93B1}.Debug|x64.Build.0 = Debug|x64 + {0F85E674-34AE-443D-954C-8321EB8B93B1}.Debug|x86.ActiveCfg = Debug|x64 {0F85E674-34AE-443D-954C-8321EB8B93B1}.Release|x64.ActiveCfg = Release|x64 {0F85E674-34AE-443D-954C-8321EB8B93B1}.Release|x64.Build.0 = Release|x64 + {0F85E674-34AE-443D-954C-8321EB8B93B1}.Release|x86.ActiveCfg = Release|x64 {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Debug|x64.ActiveCfg = Debug|x64 {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Debug|x64.Build.0 = Debug|x64 + {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Debug|x86.ActiveCfg = Debug|x64 {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Release|x64.ActiveCfg = Release|x64 {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Release|x64.Build.0 = Release|x64 + {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Release|x86.ActiveCfg = Release|x64 {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|x64.ActiveCfg = Debug|x64 {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|x64.Build.0 = Debug|x64 + {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|x86.ActiveCfg = Debug|x64 {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.ActiveCfg = Release|x64 {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.Build.0 = Release|x64 + {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x86.ActiveCfg = Release|x64 {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|x64.ActiveCfg = Debug|x64 {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|x64.Build.0 = Debug|x64 + {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|x86.ActiveCfg = Debug|x64 {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|x64.ActiveCfg = Release|x64 {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|x64.Build.0 = Release|x64 + {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|x86.ActiveCfg = Release|x64 {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.ActiveCfg = Debug|x64 {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.Build.0 = Debug|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x86.ActiveCfg = Debug|x64 {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.ActiveCfg = Release|x64 {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.Build.0 = Release|x64 + {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x86.ActiveCfg = Release|x64 {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Debug|x64.ActiveCfg = Debug|x64 {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Debug|x64.Build.0 = Debug|x64 + {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Debug|x86.ActiveCfg = Debug|x64 {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Release|x64.ActiveCfg = Release|x64 {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Release|x64.Build.0 = Release|x64 + {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Release|x86.ActiveCfg = Release|x64 {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Debug|x64.ActiveCfg = Debug|x64 {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Debug|x64.Build.0 = Debug|x64 + {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Debug|x86.ActiveCfg = Debug|x64 {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Release|x64.ActiveCfg = Release|x64 {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Release|x64.Build.0 = Release|x64 + {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Release|x86.ActiveCfg = Release|x64 {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Debug|x64.ActiveCfg = Debug|x64 {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Debug|x64.Build.0 = Debug|x64 + {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Debug|x86.ActiveCfg = Debug|x64 {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Release|x64.ActiveCfg = Release|x64 {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Release|x64.Build.0 = Release|x64 + {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Release|x86.ActiveCfg = Release|x64 {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.ActiveCfg = Debug|x64 {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.Build.0 = Debug|x64 + {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x86.ActiveCfg = Debug|x64 {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.ActiveCfg = Release|x64 {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.Build.0 = Release|x64 + {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x86.ActiveCfg = Release|x64 {6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|x64.ActiveCfg = Debug|x64 {6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|x64.Build.0 = Debug|x64 + {6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|x86.ActiveCfg = Debug|x64 {6955446D-23F7-4023-9BB3-8657F904AF99}.Release|x64.ActiveCfg = Release|x64 {6955446D-23F7-4023-9BB3-8657F904AF99}.Release|x64.Build.0 = Release|x64 + {6955446D-23F7-4023-9BB3-8657F904AF99}.Release|x86.ActiveCfg = Release|x64 {58736667-1027-4AD7-BFDF-7A3A6474103A}.Debug|x64.ActiveCfg = Debug|x64 {58736667-1027-4AD7-BFDF-7A3A6474103A}.Debug|x64.Build.0 = Debug|x64 + {58736667-1027-4AD7-BFDF-7A3A6474103A}.Debug|x86.ActiveCfg = Debug|x64 {58736667-1027-4AD7-BFDF-7A3A6474103A}.Release|x64.ActiveCfg = Release|x64 {58736667-1027-4AD7-BFDF-7A3A6474103A}.Release|x64.Build.0 = Release|x64 + {58736667-1027-4AD7-BFDF-7A3A6474103A}.Release|x86.ActiveCfg = Release|x64 {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Debug|x64.ActiveCfg = Debug|x64 {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Debug|x64.Build.0 = Debug|x64 + {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Debug|x86.ActiveCfg = Debug|x64 {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Release|x64.ActiveCfg = Release|x64 {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Release|x64.Build.0 = Release|x64 + {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Release|x86.ActiveCfg = Release|x64 {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Debug|x64.ActiveCfg = Debug|x64 {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Debug|x64.Build.0 = Debug|x64 + {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Debug|x86.ActiveCfg = Debug|x64 {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Release|x64.ActiveCfg = Release|x64 {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Release|x64.Build.0 = Release|x64 + {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Release|x86.ActiveCfg = Release|x64 {0B593A6C-4143-4337-860E-DB5710FB87DB}.Debug|x64.ActiveCfg = Debug|x64 {0B593A6C-4143-4337-860E-DB5710FB87DB}.Debug|x64.Build.0 = Debug|x64 + {0B593A6C-4143-4337-860E-DB5710FB87DB}.Debug|x86.ActiveCfg = Debug|x64 {0B593A6C-4143-4337-860E-DB5710FB87DB}.Release|x64.ActiveCfg = Release|x64 {0B593A6C-4143-4337-860E-DB5710FB87DB}.Release|x64.Build.0 = Release|x64 + {0B593A6C-4143-4337-860E-DB5710FB87DB}.Release|x86.ActiveCfg = Release|x64 {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.ActiveCfg = Debug|x64 {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.Build.0 = Debug|x64 + {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x86.ActiveCfg = Debug|x64 {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.ActiveCfg = Release|x64 {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.Build.0 = Release|x64 + {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x86.ActiveCfg = Release|x64 {7319089E-46D6-4400-BC65-E39BDF1416EE}.Debug|x64.ActiveCfg = Debug|x64 {7319089E-46D6-4400-BC65-E39BDF1416EE}.Debug|x64.Build.0 = Debug|x64 + {7319089E-46D6-4400-BC65-E39BDF1416EE}.Debug|x86.ActiveCfg = Debug|x64 {7319089E-46D6-4400-BC65-E39BDF1416EE}.Release|x64.ActiveCfg = Release|x64 {7319089E-46D6-4400-BC65-E39BDF1416EE}.Release|x64.Build.0 = Release|x64 + {7319089E-46D6-4400-BC65-E39BDF1416EE}.Release|x86.ActiveCfg = Release|x64 {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Debug|x64.ActiveCfg = Debug|x64 {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Debug|x64.Build.0 = Debug|x64 + {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Debug|x86.ActiveCfg = Debug|x64 {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Release|x64.ActiveCfg = Release|x64 {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Release|x64.Build.0 = Release|x64 + {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Release|x86.ActiveCfg = Release|x64 {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Debug|x64.ActiveCfg = Debug|x64 {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Debug|x64.Build.0 = Debug|x64 + {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Debug|x86.ActiveCfg = Debug|x64 {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Release|x64.ActiveCfg = Release|x64 {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Release|x64.Build.0 = Release|x64 + {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Release|x86.ActiveCfg = Release|x64 {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Debug|x64.ActiveCfg = Debug|x64 {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Debug|x64.Build.0 = Debug|x64 + {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Debug|x86.ActiveCfg = Debug|x64 {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Release|x64.ActiveCfg = Release|x64 {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Release|x64.Build.0 = Release|x64 + {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Release|x86.ActiveCfg = Release|x64 {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Debug|x64.ActiveCfg = Debug|x64 {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Debug|x64.Build.0 = Debug|x64 + {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Debug|x86.ActiveCfg = Debug|x64 {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Release|x64.ActiveCfg = Release|x64 {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Release|x64.Build.0 = Release|x64 + {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Release|x86.ActiveCfg = Release|x64 {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Debug|x64.ActiveCfg = Debug|x64 {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Debug|x64.Build.0 = Debug|x64 + {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Debug|x86.ActiveCfg = Debug|x64 {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Release|x64.ActiveCfg = Release|x64 {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Release|x64.Build.0 = Release|x64 + {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Release|x86.ActiveCfg = Release|x64 {6ED2F4FC-E122-4CEE-90F1-97E4CCC8BC7A}.Debug|x64.ActiveCfg = Debug|x64 {6ED2F4FC-E122-4CEE-90F1-97E4CCC8BC7A}.Debug|x64.Build.0 = Debug|x64 + {6ED2F4FC-E122-4CEE-90F1-97E4CCC8BC7A}.Debug|x86.ActiveCfg = Debug|x64 {6ED2F4FC-E122-4CEE-90F1-97E4CCC8BC7A}.Release|x64.ActiveCfg = Release|x64 {6ED2F4FC-E122-4CEE-90F1-97E4CCC8BC7A}.Release|x64.Build.0 = Release|x64 + {6ED2F4FC-E122-4CEE-90F1-97E4CCC8BC7A}.Release|x86.ActiveCfg = Release|x64 {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Debug|x64.ActiveCfg = Debug|x64 {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Debug|x64.Build.0 = Debug|x64 + {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Debug|x86.ActiveCfg = Debug|x64 {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Release|x64.ActiveCfg = Release|x64 {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Release|x64.Build.0 = Release|x64 + {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Release|x86.ActiveCfg = Release|x64 {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Debug|x64.ActiveCfg = Debug|x64 {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Debug|x64.Build.0 = Debug|x64 + {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Debug|x86.ActiveCfg = Debug|x64 {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Release|x64.ActiveCfg = Release|x64 {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Release|x64.Build.0 = Release|x64 + {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Release|x86.ActiveCfg = Release|x64 {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Debug|x64.ActiveCfg = Debug|x64 {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Debug|x64.Build.0 = Debug|x64 + {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Debug|x86.ActiveCfg = Debug|x64 {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Release|x64.ActiveCfg = Release|x64 {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Release|x64.Build.0 = Release|x64 + {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Release|x86.ActiveCfg = Release|x64 {8DF78B53-200E-451F-9328-01EB907193AE}.Debug|x64.ActiveCfg = Debug|x64 {8DF78B53-200E-451F-9328-01EB907193AE}.Debug|x64.Build.0 = Debug|x64 + {8DF78B53-200E-451F-9328-01EB907193AE}.Debug|x86.ActiveCfg = Debug|x64 {8DF78B53-200E-451F-9328-01EB907193AE}.Release|x64.ActiveCfg = Release|x64 {8DF78B53-200E-451F-9328-01EB907193AE}.Release|x64.Build.0 = Release|x64 + {8DF78B53-200E-451F-9328-01EB907193AE}.Release|x86.ActiveCfg = Release|x64 {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Debug|x64.ActiveCfg = Debug|x64 {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Debug|x64.Build.0 = Debug|x64 + {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Debug|x86.ActiveCfg = Debug|x64 {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Release|x64.ActiveCfg = Release|x64 {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Release|x64.Build.0 = Release|x64 + {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Release|x86.ActiveCfg = Release|x64 {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x64.ActiveCfg = Debug|x64 {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x64.Build.0 = Debug|x64 + {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x86.ActiveCfg = Debug|x64 {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.ActiveCfg = Release|x64 {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.Build.0 = Release|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.ActiveCfg = Debug|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.Build.0 = Debug|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.ActiveCfg = Release|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.Build.0 = Release|x64 + {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x86.ActiveCfg = Release|x64 {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|x64.ActiveCfg = Debug|x64 {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|x64.Build.0 = Debug|x64 + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|x86.ActiveCfg = Debug|x64 {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.ActiveCfg = Release|x64 {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.Build.0 = Release|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.ActiveCfg = Debug|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.Build.0 = Debug|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.ActiveCfg = Release|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.Build.0 = Release|x64 + {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x86.ActiveCfg = Release|x64 {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.ActiveCfg = Debug|x64 {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.Build.0 = Debug|x64 + {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x86.ActiveCfg = Debug|x64 {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.ActiveCfg = Release|x64 {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.Build.0 = Release|x64 + {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x86.ActiveCfg = Release|x64 + {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.ActiveCfg = Debug|x64 + {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.Build.0 = Debug|x64 + {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x86.ActiveCfg = Debug|x64 + {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.ActiveCfg = Release|x64 + {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.Build.0 = Release|x64 + {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x86.ActiveCfg = Release|x64 + {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.ActiveCfg = Debug|x64 + {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.Build.0 = Debug|x64 + {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x86.ActiveCfg = Debug|x64 + {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.ActiveCfg = Release|x64 + {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.Build.0 = Release|x64 + {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x86.ActiveCfg = Release|x64 {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.ActiveCfg = Debug|x64 {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.Build.0 = Debug|x64 + {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x86.ActiveCfg = Debug|x64 {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|x64.ActiveCfg = Release|x64 {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|x64.Build.0 = Release|x64 + {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|x86.ActiveCfg = Release|x64 {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Debug|x64.ActiveCfg = Debug|x64 {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Debug|x64.Build.0 = Debug|x64 + {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Debug|x86.ActiveCfg = Debug|x64 {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Release|x64.ActiveCfg = Release|x64 {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Release|x64.Build.0 = Release|x64 + {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Release|x86.ActiveCfg = Release|x64 {48804216-2A0E-4168-A6D8-9CD068D14227}.Debug|x64.ActiveCfg = Debug|x64 {48804216-2A0E-4168-A6D8-9CD068D14227}.Debug|x64.Build.0 = Debug|x64 + {48804216-2A0E-4168-A6D8-9CD068D14227}.Debug|x86.ActiveCfg = Debug|x64 {48804216-2A0E-4168-A6D8-9CD068D14227}.Release|x64.ActiveCfg = Release|x64 {48804216-2A0E-4168-A6D8-9CD068D14227}.Release|x64.Build.0 = Release|x64 - {390AE700-B55F-4202-91EA-A822EB75B9BD}.Debug|x64.ActiveCfg = Debug|x64 - {390AE700-B55F-4202-91EA-A822EB75B9BD}.Debug|x64.Build.0 = Debug|x64 - {390AE700-B55F-4202-91EA-A822EB75B9BD}.Release|x64.ActiveCfg = Release|x64 - {390AE700-B55F-4202-91EA-A822EB75B9BD}.Release|x64.Build.0 = Release|x64 + {48804216-2A0E-4168-A6D8-9CD068D14227}.Release|x86.ActiveCfg = Release|x64 + {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Debug|x64.ActiveCfg = Debug|x64 + {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Debug|x64.Build.0 = Debug|x64 + {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Debug|x86.ActiveCfg = Debug|x64 + {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|x64.ActiveCfg = Release|x64 + {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|x64.Build.0 = Release|x64 + {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|x86.ActiveCfg = Release|x64 {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|x64.ActiveCfg = Debug|x64 {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|x64.Build.0 = Debug|x64 + {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|x86.ActiveCfg = Debug|x64 {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|x64.ActiveCfg = Release|x64 {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|x64.Build.0 = Release|x64 + {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|x86.ActiveCfg = Release|x64 {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x64.ActiveCfg = Debug|x64 {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x64.Build.0 = Debug|x64 + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x86.ActiveCfg = Debug|x64 {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.ActiveCfg = Release|x64 {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.Build.0 = Release|x64 + {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.ActiveCfg = Release|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|x64.ActiveCfg = Debug|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|x64.Build.0 = Debug|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|x86.ActiveCfg = Debug|Win32 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|x86.Build.0 = Debug|Win32 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|x64.ActiveCfg = Release|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|x64.Build.0 = Release|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|x86.ActiveCfg = Release|Win32 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|x86.Build.0 = Release|Win32 + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB}.Debug|x64.ActiveCfg = Debug|x64 + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB}.Debug|x64.Build.0 = Debug|x64 + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB}.Debug|x86.ActiveCfg = Debug|x64 + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB}.Release|x64.ActiveCfg = Release|x64 + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB}.Release|x64.Build.0 = Release|x64 + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB}.Release|x86.ActiveCfg = Release|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|x64.ActiveCfg = Debug|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|x64.Build.0 = Debug|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|x86.ActiveCfg = Debug|Win32 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|x86.Build.0 = Debug|Win32 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|x64.ActiveCfg = Release|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|x64.Build.0 = Release|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|x86.ActiveCfg = Release|Win32 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -809,16 +1029,20 @@ Global {23D2070D-E4AD-4ADD-85A7-083D9C76AD49} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {62173D9A-6724-4C00-A1C8-FB646480A9EC} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {127F38E0-40AA-4594-B955-5616BF206882} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4} = {4AFC9975-2456-4C70-94A4-84073C1CED93} {5E7360A8-D048-4ED3-8F09-0BFD64C5529A} = {127F38E0-40AA-4594-B955-5616BF206882} - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1} = {4AFC9975-2456-4C70-94A4-84073C1CED93} {D940E07F-532C-4FF3-883F-790DA014F19A} = {127F38E0-40AA-4594-B955-5616BF206882} + {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4} = {4AFC9975-2456-4C70-94A4-84073C1CED93} + {3E424AD2-19E5-4AE6-B833-F53963EB5FC1} = {4AFC9975-2456-4C70-94A4-84073C1CED93} {106CBECA-0701-4FC3-838C-9DF816A19AE2} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {2D604C07-51FC-46BB-9EB7-75AECC7F5E81} = {106CBECA-0701-4FC3-838C-9DF816A19AE2} {2EDB3EB4-FA92-4BFF-B2D8-566584837231} = {106CBECA-0701-4FC3-838C-9DF816A19AE2} {48804216-2A0E-4168-A6D8-9CD068D14227} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {390AE700-B55F-4202-91EA-A822EB75B9BD} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} + {FF1D7936-842A-4BBB-8BEA-E9FE796DE700} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {5043CECE-E6A7-4867-9CBE-02D27D83747A} = {4AFC9975-2456-4C70-94A4-84073C1CED93} + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A} = {470FBAF9-E1F8-4F3E-8786-198A1C81C8A8} + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB} = {470FBAF9-E1F8-4F3E-8786-198A1C81C8A8} + {AC2857B4-103D-4D6D-9740-926EBF785042} = {470FBAF9-E1F8-4F3E-8786-198A1C81C8A8} + {470FBAF9-E1F8-4F3E-8786-198A1C81C8A8} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/doc/devdocs/readme.md b/doc/devdocs/readme.md index 74419d2ae4..cbcc07d5ad 100644 --- a/doc/devdocs/readme.md +++ b/doc/devdocs/readme.md @@ -66,7 +66,12 @@ Various tools used by PowerToys. Includes the Visual Studio 2019 project templat 2. Visual Studio Community/Professional/Enterprise 2019 3. Once you've cloned and started the `PowerToys.sln`, in the solution explorer, if you see a dialog that says `install extra components`, click `install` -### Compile source code +**Optional step:**
+4. to build the Video Conference module, install the [WDK version 1903](https://docs.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads) ([direct download link](https://go.microsoft.com/fwlink/?linkid=2085767))
+ During the installation, make sure that, when prompted, the `Install Windows Driver Kit Visual Studio extension` option is checked. + + +### Compiling Source Code - Open `PowerToys.sln` in Visual Studio, in the `Solutions Configuration` drop-down menu select `Release` or `Debug`, from the `Build` menu choose `Build Solution`. - The PowerToys binaries will be in your repo under `x64\Release\`. diff --git a/installer/PowerToysSetup/PowerToysSetup.wixproj b/installer/PowerToysSetup/PowerToysSetup.wixproj index f7816d7a41..cab87ecbf9 100644 --- a/installer/PowerToysSetup/PowerToysSetup.wixproj +++ b/installer/PowerToysSetup/PowerToysSetup.wixproj @@ -3,7 +3,7 @@ - Version=$(Version); + Version=$(Version) Release @@ -74,7 +74,6 @@ IF NOT DEFINED IsPipeline ( - call "$([MSBuild]::GetVsInstallRoot())\Common7\Tools\VsDevCmd.bat" -arch=amd64 -host_arch=amd64 -winsdk=10.0.18362.0 SET PTRoot=..\..\..\.. call "..\..\publish.cmd" diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 3b7fde2275..1d85abba70 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -8,9 +8,11 @@ + + @@ -218,6 +220,10 @@ + + @@ -442,6 +448,9 @@ + + + @@ -637,6 +646,37 @@ + + + @@ -751,7 +791,7 @@ - + @@ -791,7 +831,7 @@ - + @@ -882,6 +922,9 @@ + + @@ -911,6 +954,7 @@ + diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index bf0e61835c..bb52d9c707 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -18,6 +18,7 @@ TRACELOGGING_DEFINE_PROVIDER( const DWORD USERNAME_DOMAIN_LEN = DNLEN + UNLEN + 2; // Domain Name + '\' + User Name + '\0' const DWORD USERNAME_LEN = UNLEN + 1; // User Name + '\0' +static const wchar_t* POWERTOYS_EXE_COMPONENT = L"{A2C66D91-3485-4D00-B04D-91844E6B345B}"; static const wchar_t* POWERTOYS_UPGRADE_CODE = L"{42B84BF7-5FBF-473B-9C8B-049DC16F7708}"; // Creates a Scheduled Task to run at logon for the current user. @@ -596,6 +597,165 @@ UINT __stdcall DetectPrevInstallPathCA(MSIHANDLE hInstall) return WcaFinalize(er); } +UINT __stdcall CertifyVirtualCameraDriverCA(MSIHANDLE hInstall) +{ +#ifdef CIBuild // On pipeline we are using microsoft certification + WcaInitialize(hInstall, "CertifyVirtualCameraDriverCA"); + return WcaFinalize(ERROR_SUCCESS); +#else + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + LPWSTR certificatePath = nullptr; + HCERTSTORE hCertStore = nullptr; + HANDLE hfile = nullptr; + DWORD size = INVALID_FILE_SIZE; + char * pFileContent = nullptr; + + hr = WcaInitialize(hInstall, "CertifyVirtualCameraDriverCA"); + ExitOnFailure(hr, "Failed to initialize", hr); + + hr = WcaGetProperty(L"CustomActionData", &certificatePath); + ExitOnFailure(hr, "Failed to get install preperty", hr); + + hCertStore = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_LOCAL_MACHINE, L"AuthRoot"); + if (!hCertStore) + { + hr = GetLastError(); + ExitOnFailure(hr, "Cannot put principal run level: %x", hr); + } + + hfile = CreateFile(certificatePath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hfile == INVALID_HANDLE_VALUE) + { + hr = GetLastError(); + ExitOnFailure(hr, "Certificate file open failed", hr); + } + + size = GetFileSize(hfile, nullptr); + if (size == INVALID_FILE_SIZE) + { + hr = GetLastError(); + ExitOnFailure(hr, "Certificate file size not valid", hr); + } + + pFileContent = (char*)malloc(size); + + DWORD sizeread; + if (!ReadFile(hfile, pFileContent, size, &sizeread, nullptr)) + { + hr = GetLastError(); + ExitOnFailure(hr, "Certificate file read failed", hr); + } + + if (!CertAddEncodedCertificateToStore(hCertStore, + X509_ASN_ENCODING, + (const BYTE*)pFileContent, + size, + CERT_STORE_ADD_ALWAYS, + nullptr)) + { + hr = GetLastError(); + ExitOnFailure(hr, "Adding certificate failed", hr); + } + + free(pFileContent); + +LExit: + ReleaseStr(certificatePath); + if (hCertStore) + { + CertCloseStore(hCertStore, 0); + } + if (hfile) + { + CloseHandle(hfile); + } + + if (!SUCCEEDED(hr)) + { + PMSIHANDLE hRecord = MsiCreateRecord(0); + MsiRecordSetString(hRecord, 0, TEXT("Failed to add certificate to store")); + MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_WARNING + MB_OK), hRecord); + } + + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +#endif +} + + +UINT __stdcall InstallVirtualCameraDriverCA(MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + LPWSTR driverPath = nullptr; + + hr = WcaInitialize(hInstall, "InstallVirtualCameraDriverCA"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &driverPath); + ExitOnFailure(hr, "Failed to get install preperty"); + + BOOL requiresReboot; + DiInstallDriverW(GetConsoleWindow(), driverPath, DIIRFLAG_FORCE_INF, &requiresReboot); + + hr = GetLastError(); + ExitOnFailure(hr, "Failed to install driver"); + +LExit: + + if (!SUCCEEDED(hr)) + { + PMSIHANDLE hRecord = MsiCreateRecord(0); + MsiRecordSetString(hRecord, 0, TEXT("Failed to install virtual camera driver")); + MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_WARNING + MB_OK), hRecord); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall UninstallVirtualCameraDriverCA(MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + LPWSTR driverPath = nullptr; + + hr = WcaInitialize(hInstall, "UninstallVirtualCameraDriverCA"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &driverPath); + ExitOnFailure(hr, "Failed to get uninstall preperty"); + + BOOL requiresReboot; + DiUninstallDriverW(GetConsoleWindow(), driverPath, 0, &requiresReboot); + + switch (GetLastError()) + { + case ERROR_ACCESS_DENIED: + case ERROR_FILE_NOT_FOUND: + case ERROR_INVALID_FLAGS: + case ERROR_IN_WOW64: + { + hr = GetLastError(); + ExitOnFailure(hr, "Failed to uninstall driver"); + break; + } + } + +LExit: + + if (!SUCCEEDED(hr)) + { + PMSIHANDLE hRecord = MsiCreateRecord(0); + MsiRecordSetString(hRecord, 0, TEXT("Filed to iminstall virtual camera driver")); + MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_WARNING + MB_OK), hRecord); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) { diff --git a/installer/PowerToysSetupCustomActions/CustomAction.def b/installer/PowerToysSetupCustomActions/CustomAction.def index 3e8a2ab72e..06383fb65f 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.def +++ b/installer/PowerToysSetupCustomActions/CustomAction.def @@ -12,4 +12,8 @@ EXPORTS TelemetryLogUninstallFailCA TelemetryLogRepairCancelCA TelemetryLogRepairFailCA - TerminateProcessesCA \ No newline at end of file + TerminateProcessesCA + TerminateProcessesCA + CertifyVirtualCameraDriverCA + InstallVirtualCameraDriverCA + UninstallVirtualCameraDriverCA diff --git a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj index 226a800022..6ad2af8a1c 100644 --- a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj +++ b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj @@ -56,7 +56,7 @@ $(WIX)sdk\$(WixPlatformToolset)\lib\x64;$(SolutionDir)\packages\WiX.3.11.2\tools\sdk\vs2017\lib\x64;..\..\$(PlatformShortName)\$(Configuration)\;%(AdditionalLibraryDirectories) - msi.lib;wcautil.lib;Psapi.lib;Pathcch.lib;comsupp.lib;taskschd.lib;Secur32.lib;msi.lib;dutil.lib;wcautil.lib;Version.lib;ApplicationUpdate.lib;Notifications.lib;Shlwapi.lib;%(AdditionalDependencies) + Newdev.lib;Crypt32.lib;msi.lib;wcautil.lib;Psapi.lib;Pathcch.lib;comsupp.lib;taskschd.lib;Secur32.lib;msi.lib;dutil.lib;wcautil.lib;Version.lib;ApplicationUpdate.lib;Notifications.lib;Shlwapi.lib;%(AdditionalDependencies) @@ -124,4 +124,4 @@ - \ No newline at end of file + diff --git a/installer/PowerToysSetupCustomActions/stdafx.h b/installer/PowerToysSetupCustomActions/stdafx.h index b53439bd31..61a3134864 100644 --- a/installer/PowerToysSetupCustomActions/stdafx.h +++ b/installer/PowerToysSetupCustomActions/stdafx.h @@ -4,6 +4,7 @@ #define DPSAPI_VERSION 1 // Windows Header Files: #include +#include #include #include #include diff --git a/src/modules/fancyzones/FancyZonesLib/FileWatcher.cpp b/src/common/SettingsAPI/FileWatcher.cpp similarity index 100% rename from src/modules/fancyzones/FancyZonesLib/FileWatcher.cpp rename to src/common/SettingsAPI/FileWatcher.cpp diff --git a/src/modules/fancyzones/FancyZonesLib/FileWatcher.h b/src/common/SettingsAPI/FileWatcher.h similarity index 74% rename from src/modules/fancyzones/FancyZonesLib/FileWatcher.h rename to src/common/SettingsAPI/FileWatcher.h index fd339578b9..206c1cd747 100644 --- a/src/modules/fancyzones/FancyZonesLib/FileWatcher.h +++ b/src/common/SettingsAPI/FileWatcher.h @@ -1,6 +1,13 @@ #pragma once -#include "pch.h" +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include + +#include +#include +#include +#include class FileWatcher { diff --git a/src/common/SettingsAPI/SetttingsAPI.vcxproj b/src/common/SettingsAPI/SetttingsAPI.vcxproj index 7a4a77284c..813c904374 100644 --- a/src/common/SettingsAPI/SetttingsAPI.vcxproj +++ b/src/common/SettingsAPI/SetttingsAPI.vcxproj @@ -28,10 +28,12 @@ + + Create diff --git a/src/common/SettingsAPI/settings_helpers.h b/src/common/SettingsAPI/settings_helpers.h index cd8f39432b..c5551a7f6b 100644 --- a/src/common/SettingsAPI/settings_helpers.h +++ b/src/common/SettingsAPI/settings_helpers.h @@ -8,6 +8,7 @@ namespace PTSettingsHelper { constexpr inline const wchar_t* log_settings_filename = L"log_settings.json"; + std::wstring get_powertoys_general_save_file_location(); std::wstring get_module_save_file_location(std::wstring_view powertoy_key); std::wstring get_module_save_folder_location(std::wstring_view powertoy_name); std::wstring get_root_save_folder_location(); diff --git a/src/common/Telemetry/ProjectTelemetry.h b/src/common/Telemetry/ProjectTelemetry.h index 1289d69201..fdf29dd694 100644 --- a/src/common/Telemetry/ProjectTelemetry.h +++ b/src/common/Telemetry/ProjectTelemetry.h @@ -1,6 +1,6 @@ #pragma once -#include -#include +#include "TraceLoggingProvider.h" +#include "TraceLoggingDefines.h" TRACELOGGING_DECLARE_PROVIDER(g_hProvider); diff --git a/src/common/interop/PowerToysInterop.vcxproj b/src/common/interop/PowerToysInterop.vcxproj index e395437c28..fb2d207fb9 100644 --- a/src/common/interop/PowerToysInterop.vcxproj +++ b/src/common/interop/PowerToysInterop.vcxproj @@ -47,6 +47,7 @@ + PowerToysInterop;%(PreprocessorDefinitions) NotUsing $(SolutionDir)src\common\interop;../../;../;%(AdditionalIncludeDirectories) false @@ -54,7 +55,7 @@ stdcpp17 - WindowsApp.lib;%(AdditionalDependencies) + Mf.lib;WindowsApp.lib;%(AdditionalDependencies) @@ -83,6 +84,8 @@ + + @@ -90,6 +93,12 @@ + + false + + + false + @@ -109,6 +118,12 @@ + + + + + + diff --git a/src/common/interop/PowerToysInterop.vcxproj.filters b/src/common/interop/PowerToysInterop.vcxproj.filters index 1a88f7682e..ddd7e81885 100644 --- a/src/common/interop/PowerToysInterop.vcxproj.filters +++ b/src/common/interop/PowerToysInterop.vcxproj.filters @@ -27,7 +27,13 @@ Header Files - + + Header Files + + + Header Files + + Header Files @@ -50,10 +56,16 @@ Source Files + + Source Files + + + Source Files + Resource Files - \ No newline at end of file + diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp index 117af72122..09529e9d1c 100644 --- a/src/common/interop/interop.cpp +++ b/src/common/interop/interop.cpp @@ -13,12 +13,17 @@ // Therefore the simplest way is to compile these functions as native using the pragmas below. #pragma managed(push, off) #include "../utils/os-detect.h" +// TODO: move to a separate library in common +#include "../../modules/videoconference/VideoConferenceShared/MicrophoneDevice.h" +#include "../../modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.h" #pragma managed(pop) #include + using namespace System; using namespace System::Runtime::InteropServices; +using System::Collections::Generic::List; // https://docs.microsoft.com/en-us/cpp/dotnet/how-to-wrap-native-class-for-use-by-csharp?view=vs-2019 namespace interop @@ -122,6 +127,32 @@ public static String ^ GetProductVersion() { return gcnew String(get_product_version().c_str()); } + + static List ^ GetAllActiveMicrophoneDeviceNames() { + auto names = gcnew List(); + for (const auto& device : MicrophoneDevice::getAllActive()) + { + names->Add(gcnew String(device.name().data())); + } + return names; + } + + static List ^ + GetAllVideoCaptureDeviceNames() { + auto names = gcnew List(); + VideoCaptureDeviceList vcdl; + vcdl.EnumerateDevices(); + + for (UINT32 i = 0; i < vcdl.Count(); ++i) + { + auto name = gcnew String(vcdl.GetDeviceName(i).data()); + if (name != L"PowerToys VideoConference Mute") + { + names->Add(name); + } + } + return names; + } }; public diff --git a/src/common/interop/packages.config b/src/common/interop/packages.config new file mode 100644 index 0000000000..3e9434647c --- /dev/null +++ b/src/common/interop/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp index 45b8dc0ad3..1574c7f247 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp @@ -7,13 +7,13 @@ #include #include #include +#include #include "FancyZones.h" #include "FancyZonesLib/Settings.h" #include "FancyZonesLib/ZoneWindow.h" #include "FancyZonesLib/FancyZonesData.h" #include "FancyZonesLib/ZoneSet.h" -#include "FancyZonesLib/FileWatcher.h" #include "FancyZonesLib/WindowMoveHandler.h" #include "FancyZonesLib/FancyZonesWinHookEventIDs.h" #include "FancyZonesLib/util.h" diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj index 0ac31d0179..3100570499 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj @@ -41,7 +41,6 @@ - @@ -67,7 +66,6 @@ - diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters index b3c04c2c80..3bcfab7d1d 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesLib.vcxproj.filters @@ -78,9 +78,6 @@ Header Files - - Header Files - Header Files @@ -140,9 +137,6 @@ Source Files - - Source Files - Source Files diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Dark.png b/src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Dark.png new file mode 100644 index 0000000000000000000000000000000000000000..4070bca9e332deba4eb4f79e7576d69b24ef160f GIT binary patch literal 3959 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 + + + + + + + + + + + + + Camera not in use + + + Microphone off + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Light.png b/src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Light.png new file mode 100644 index 0000000000000000000000000000000000000000..6962f0115a75e97b2831c4bd4bb995c61b424a1a GIT binary patch literal 3900 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 b1$+E8M;g+4ZR;>#@`V^1ni>@8-3|T^u?L|+ literal 0 HcmV?d00001 diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Light.svg b/src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Light.svg new file mode 100644 index 0000000000..a30e909ff1 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Icons/Off-NotInUse Light.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + Camera not in use + + + Microphone off + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Dark.png b/src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b0ce502a4d828ac47732470a6dfb85cae6ac9849 GIT binary patch literal 3822 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 + + + + + + + + + + + + + Camera off + + + Microphone off + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Light.png b/src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Light.png new file mode 100644 index 0000000000000000000000000000000000000000..500386523f588bfb8c806d43815e273a87435f90 GIT binary patch literal 3846 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 literal 0 HcmV?d00001 diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Light.svg b/src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Light.svg new file mode 100644 index 0000000000..25eecdd72f --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Icons/Off-Off Light.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + Camera off + + + Microphone off + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Dark.png b/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ba27ff46d4187b6905fe58a25a2e180dc9bb9487 GIT binary patch literal 3496 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 literal 0 HcmV?d00001 diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Dark.svg b/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Dark.svg new file mode 100644 index 0000000000..ea31e3d78d --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Dark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + Camera on + + Microphone off + + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Light.png b/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Light.png new file mode 100644 index 0000000000000000000000000000000000000000..abc8c972a280b9e5995a9f66ac39e3b127f760e1 GIT binary patch literal 3539 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 MRn%0dkTVVbAHNFeLI3~& literal 0 HcmV?d00001 diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Light.svg b/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Light.svg new file mode 100644 index 0000000000..e3c0352bcc --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Icons/Off-On Light.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + Camera on + + Microphone off + + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Dark.png b/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Dark.png new file mode 100644 index 0000000000000000000000000000000000000000..928c8f1e5016aa9206a8d66c7672c7fc5502d788 GIT binary patch literal 3690 zcmai1XH-*Lw+%%=q{F2*rCvHnmtGAe6p7M|2uKSAgwTS3G?8*a-~$X*x`3ey7!(kZ zCQVS<4HBA!VpK{%q`mF0_v5`Wz8@z!u*~FWv!QaK8>rNpM2ut6ei)t|NxhHtYhc_^3YpjParyjG5InWxiaXi(T<71;W*R?d9bZ18D}McbsvyY>A0d)McU!7j0;bz~T~y#CTD09d?EbhSwv&h6L2c z$FhXt3zvKU?+(T92CqW5Z_3EZ;$u!b4@f%^b4_=RW7loiXqWFu_3vGE^=ifLktX)m za%`q%W~u|iwTp{%BC{`YO70^ilLiSN%Z%|Mq;WTIr}3BvbU4a= zTfpahs<8>#`uz9S_O@2^@BN2se@jRt*-ER5`d*x@wkbU`7(72Bl|q>KRA@U0V^x;< zD;Bdpswy!j5fAi45RE)ec~EQX{%y)YXt#M*uiM*AbnxoHKd56a)3VxHU$$ViAmL2| z1~w)-tg;84_ju7{yS!6NB67CDw^2Yq;BOh3R7@$NQA$iK;n5>HZEfwBz2q2>5X{1YQ6c|QLhr+Q{|s4cXg03~ zO0cc1?ep|BCBn+HQ+mdmXPyU7_l!UigK}sWE3A{MKW#E3_dTz}5MOp$SXiV+7Rill zpgW(_-EN%q=pcB|y$jN`^6>B|ognh)-cFVq8EVm4q8w(iCYDAY@HKWF6mTeZ1=nL|1|Atp>=G{QSYQ z(_>|4Q?_aLE;B!)#~Ygc!*6PB%}k+CMvJtrHtb(vzGq{o zq^9;)2fdN74IQu(rN_b$zVRfMh&y zx9<@G7~<`HL4!Jw&3sKCFh* z)b7bWzSemwCnr8KnLN~D1wPQMA}dHUU2Y^oM_3%{{%x-LsgX9v$Hw;)dfJ=K$XziLV%IGyTQ&bE)~1Vou0w zY}?LM&iUnI-%r&$qgN*Yl5O0)+Ad`mE^$Meq7G51si|`vfg+GXjllIW4Wj4v3=Wbm zpplV}z@%hku;j_R41mkA)r%Q|>L~m=l3yXsczpPy=Gn7{09AWO$Du)y*wGYDDNwg} z?~D-96gXfX4Z6J(kkO!Vb`S7T*=v;7&(CkJJNnv4zTzL;!UG&MDKoCg(OqNk<;D*;bHnumBRAr^WN z9!P~Kl%uXLyPzO-+Hd-UiiSr1{{H?`vC`N+8eKVn;|Y*h18>{S6htElv?6PM>p5d` z-#0=>CcOS8Rg>2kKjdxN!*y`Ij8jzA`C;iTsaH8+|1ompOaWac0H^Fmuqoxe4_}yFHLN-a4Tt5OOqGq!sdOB~9@qIxQ`&*%?<^RYeta@XaY; zd~2%8QnPqmcto1LNH=c~v}z61Xqwm1Y`T)V1m9>FBM^_kYweEKI#cE?hQt<68<5ACkPa z4zJ+Px{MCztg{te~el(c#V>{LJl^&~qxe#|QkOKR9e zkA=L@X1B>aT0E|<>6{6Tnmq?j=Zi#o{Q9}fB%)U|HDxajEpKasM@fI0o8yJooCn;O zFSX;kkkI=PXSyZNpvzP`F{nX&(3c@}?H8CIFuDhs`e#f1JaEG+c(@%i|uE?azOA!K4_Z$kg@cQ?>6 z&=0S|$ow_1;uSDNq+!^&c8-7anBTq?6Qc)&+F8+f!?S}hJTg*ITblxu&<}|m9vdSo z`z&Nmkorh_%9wp0+_$n;1T;Ib-yMt-+&x5*+3bS6JZ?GeOY-va5nTvpXJ>ahNEad# z(OA>nU1A{0cFP&js3BwP?dj=5F^5mb+@=v^plx!L`&SpdtSwr*n5Bm}F*yq!a75V7dbU!PBcS9L%}oxToFyw(M6SDwrZae5_&~rPboY=nE{iEU{ato zKtPg#{Se3*uP1>#t*xyog>E^JFB^g$%2&KoT~-zc&5;B~S8m1}{gr~*!E{-b=&vwo3S}X0!xOzsEhlv~*&= z)f*^Ry3n<})Ko?=t$^VqYNM?>3uYkAmC7%?MZs{HMn0#%nzfmGJk6KTYhZ3ZXk7Id z-upF-U_g>KYn(mY)}9~w&t#XcgoFe;OY-aK&|(-E!)Us19>$gO2NJuB!->)PD=RC) zvpPaL5l?_@9a_?@PA@Jl)^K-rAdJk-&0Q}+J#hF=5ulK+<&(L@ADK}r`s}|R=w;{R z_@Yn)t2;`l1x?hK&Z;q1{q!JB!H`!oBA*PGgI?J%d&kl~j!LjktgAZ}eu$ANENM_{ zL3=&xr&L_}$hTPxlj2TG0+Qb3)=SN0x(q0p5PbR6_v?pT*Dj2j-?;6s!+q~eFcsz6 zRhZd1IY9v`SS;4l+xrFraW)rw&FJx+g}9G?ZNVqM?t3~(%F3nz1B~w#1(vh@Lx(XF ze|AQ}e*Wy>s=yB LVI~bmZt?#Enfd?W literal 0 HcmV?d00001 diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Dark.svg b/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Dark.svg new file mode 100644 index 0000000000..f2b667444e --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Dark.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + Camera not in use + + Microphone on + + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Light.png b/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Light.png new file mode 100644 index 0000000000000000000000000000000000000000..6a4662711b75fcb939a1e0f303404ade269da4bf GIT binary patch literal 3630 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 KR4bLO!u}8CCHUI_ literal 0 HcmV?d00001 diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Light.svg b/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Light.svg new file mode 100644 index 0000000000..9ce95b772a --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Icons/On-NotInUse Light.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + Camera not in use + + Microphone on + + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-Off Dark.png b/src/modules/videoconference/VideoConferenceModule/Icons/On-Off Dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d8fcd23cdce47f7859d7d63a3bdfad7fb3c1197c GIT binary patch literal 3630 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 + + + + + + + + + + + + + Camera off + + Microphone on + + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-Off Light.png b/src/modules/videoconference/VideoConferenceModule/Icons/On-Off Light.png new file mode 100644 index 0000000000000000000000000000000000000000..271809ed7a648e772c7f5b19fe2c7b7ce7957724 GIT binary patch literal 3609 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 E08 + + + + + + + + + + + + + Camera off + + Microphone on + + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-On Dark.png b/src/modules/videoconference/VideoConferenceModule/Icons/On-On Dark.png new file mode 100644 index 0000000000000000000000000000000000000000..52f461188342d5ce766514fcc68b673b8a8bf798 GIT binary patch literal 3277 zcmai1c{o&W|DFoj6Ph&Hw<&u__H5b77vYPDLDpgHG}0@PuT*$7*%Me<%`N6T!7jq>8mIo%U;W1CQ`yKFD_nwF0{8dg3acyn z%8v$L9UrfaYiMvWUU;pQ|GB7Td5gTf!=Lwn-f-iN3Tnk3NL+`?jU7;EO-6mD@~f9KAv;NXq)XScZ+W=m~$7WCBk@2|G6OjT0P9yHBnqsSF2H(F6%e;uAs3otEA)#js{CfkaemJ+E_3#H8m|MFJD~f zNpIm!$Kl}6WUT#;gsQZ>{Hom%m@35O;l3w3)0u16u4zpX^7Dl>!hgN_@F9%{Zu!Oz zpLq?z1h>rj@z(>rKO+Pl5gjL|%jxjjOJ7TEhHHQP@UC!ak!m3j2)O$C32KNbHcdo7 z!OGq~anv`X_8h$5&@Fsn8xseNVbHDl zYKAFjeWemQ{c0S+_I}d8#8zr6`bhuKq15*62o(TEt49AAEFk?-r6xVhXWpe#`Z}Jl zVGT;Gt~^anMu2Z&ID7Vtf@d$U_TJ2yZ!x=fA$AqcJ@5Xms*l=~92gjoajJdx`0P1r zJG-te>d0h0O6)!B@hVm$xzw3 zjm16)0|UeRkY&pgXbcNy%j>?qZD`0$6*EdpP1Uur;fZd^>%+(+6!^RY0z9W{1IW!$ z=|hD^^E8^pojVtjl9C|ix#}4&ZXWX$4t1~1eljsPFDfhRr9ODsw}{>vGCFzM*r<+1 zqZRnr|Bb=$K%s_cv}SpEdC916UhHwy(m*z`#&0|tmmd8W0x>T(PHBzNaH>OkQirk5 z&5@NxhuO+L+(;zy7p19gWaIsN1wf+F(a~p@loGIJnBe2zQQSN{m#+KrI+njWFJ>6D zzv1~MSA%#tu=!(ms#wp!fP}O(+oLCJ7ZoGu&i!6WtWV;0zdd+STrBxq=;oKY?+?u) zBUM+%$`PNRo);Gvzo(eV2VRCkp?>c#=Uc9Kdnj64TQhTTV2g{HTwPu52rdKE5gZFA zr{vYENnoP0bo%qQwwr8FnHjKjS_jBHi6iavP@qWke(>+#zw1%+Om1#&zjvks1-xuI ztL~G$hKo%4bJWsnYgPW;8_W5ZWcqnQLC*Gzu4r8p>Kf1mL|8)NNl}rwwT;cGwcTB~ ziV74z+S?qp=}_gxk$h2}iGEkA^YiC>Rg)79A@G8N0)B1u>u1l{YmpRFH#ZT*%xB;N zD&wFN9uR~y+SQyX=Ztu z2)G4^+Pu*#?cw3U1%t7Yo!}hc^D8S;7ZyC0NckjO{=^|_lrw#Qv@4GsI1Q(%&~ zw{PeERkd~Vc<=Gkr_Ar{q=C>scXfU68h}9(I4&ESo3jTuEktp=g*x&!1vKO7)pzq+o2Qe&6Jv!V))J^|g>CQMbG~`dN zAG(?chUj`BMC#8}s&p)8U}RheYo!!0r9AUZHB&oA-K#1Sm~{T$-kyt->nf=+tm<=L zUvhrz@uhlEDDjBVS&%DK=i++}y;HpirnfrFwrhQ3xOgv9-0Gq0wY>_u1Ik zPQ9O|@vDbE?Ma85glg*Q5H*esp;T_-3!$^^9*QEEzDo-CAR;0nS9n3iU)l?v#+AZiPOd;M^=P04@aJ#jBQ zPk@qjzSl$1C#4V{CbM^6cELn7Xa@2<_K#L)XD7Mk0j9Ut6i{Bh96#hOSWlnb`}DHM z1B?$~#ws>Zq{(D1u8J~fW1eT$&)&y|%P5((4W z`g@~~vl4GBg^T<{X%$=7n47jn0 z%eVdRm=0`9(JO%Hl-os=0FGXjmPUNZQ5&uW#qr?*6;3^>_8BSC(|2-N2a!SGJv7qBGLcxJ}K>a*+&bIjX1*YJIVJrd~jRY#oyF z^Fxe%_%8kKb3v`%gYA_EWJ6C+iHq_s3sod-{lX#R=@>NH>I(1P;bB$GVR*&erY=08 zb&9q_%2E6D`Wiwt=0I6OLLx^y1_XvI&TSBB)HF3UC#vqBi#^)LRBImxK1a*t zw{&zMjEs!7_VzY5Hu}I<5~Ekf32-H)^vSCGg;rNeDk{Ru#>r%J7Z*XrO!n-7t`DAl zg0T7C4C%>gA0D8=$fzh3oveoITm7P}qLTLNRoC&+VQE>J9$`E^H+Nw6Q)2E<+P9YM z_I91;{AzJM9(wpucLhE{*hrP+jT^hEE5%rBYHqHDyZanMYopqKQW5a%imdD+z|6bG zQA$e6*wl0ma2)t0$OfM?r0p9i;XXcBu~;lG6dJtPdjTj9Z&&^^C{8JRAV)1E0Z4Xe zVxoI{Wn3d*6@cpA5zoTQ%X?25w!XC`uBNtMU>{Urd6EtvL>xcfy{)~^(BeY+If}2G zrVX<*(k*I6eXnO`X6C4e{R5JcX3A@!q^2efK|#TDXmtBW;7TC6;QI2atChdx-z1H@ z#-=1Do&|nOnW}RkECY-!G5B);y&@xp#;iLUblV6V=|Cgba@Uxs1dV7Hmb$nbS^%K@TN^1qg%mPfD{GcuyfqcL&q@8p526Aw0tP5Cfp5E(@D)WBkC#_#We$@x|w7E=*n39Bg zFzW8f|3r!U2L{H@ + + + + + + + + + + + + + Camera on + + Microphone on + + + diff --git a/src/modules/videoconference/VideoConferenceModule/Icons/On-On Light.png b/src/modules/videoconference/VideoConferenceModule/Icons/On-On Light.png new file mode 100644 index 0000000000000000000000000000000000000000..3fa2959951b7d1c9dfeb8d24c386a31a124d93fb GIT binary patch literal 3298 zcmZ`+c{r3`8y*sxk~P_vu}iXyean`SWo)G=ONKFxrI67e63U*fn(QI@W#7VBvL_*A z8NyhmEDa4JTfWox$9H|#^R66opsBaYQHo+SQQ z+hk(haTWe~T-*(w*7v+3|l+2{FwEv&fJVYQpy0r&(EJItOqQ=sgcvu%aO5tjVQl) z^Cm&fH=Bgl(a{6a*c$Hva+J1>b@Ht``Zts8@xB?(cQ= zq%}1)3l;;<>FVlcYWPRj)W8R_v`qZ{H!JEeaK4zBn7MwnS#ed>z|WeYJuz`{ec`Dn z=hp3C_Gq-!`|t&e^6?Q8>EHd^qqkKq7jiV*NVF`TG5QqmDk}mY3g$2vd&``j=dv_1_?j;IpMcWiF&UeL$OG@7I^+h^5 z^0&0KpxoURJwEYpUvP?l_fGAs{0;JY>rwaMU=pwnv2t=6XuP-jb7SLGWo0txA%Fjl z-qz(wxHJ>IuJf_4kD%a6Z((8a*NFEdJT@+lREs@ZS63H+!)<=g3A3@YLj_g^S*dGi zOb1RI$8lE%QXRI}rnQ!076K18`YtR5Xg)jdsOowD?p;L;1`~wG+c-L!IXMaG>+7RX zC@XvWo?7g9eZVpYaMe6VH%mC|Ai4F=Ufj5ISXM!SI3#fGtErEVd|6o;&nZSI6w0F& zC?Z4H}oojtn=QrT~LSOP%e4h~mTR+iY_ZXk)w_To?$#Zl1hK9#- zLbrL9(E|k~CGkkz-N?HO-FZWv>S2GhnOKE7XJ^B%t@l4#%tl5gCntN6s-pJx@F5`~ z((>{ilQo$7#>UeQN949gc2}-gTU$RYEiENISCdSQ`R5;5h*L0C1tunDiaJMMZ(^PEAei=uY6@-CHexIAc}_PG6|uhGjqi{4uMrq^9P}&Q7DTvYUMe zyPJvxb8&I;r&(G3E2D28&LkVUx{BhPvoRyD0i}c~V1~uVO0FAPTeCYkISFX^MSl5W z2T}oD=N!DVR+yLfFg6yt=%88xw%Vz#QJGoBVdUQJ&>7KRmBly(B|8Ec*)C4t_YdKh)2#BS!T8O_a}Hy)g@KVdrC1KAtq;+4=EfY-A(@2s>n=B~RVk(A(Uq3FiU%uL>( z^gS9I^@OWs?kr_gD-+BC>Z(JZ_M&W3=stbJ!}$_OA6rta}U*| zkuaSo1JTm&^YiU-7h0s#e<44I;_>*3D!!nZ6>RGT+Rk|*DdwY%xP%04d&+lVZcg!s zDm^{@MEzYchf;1WEg~J|B)6Q1tZZyr8khWy`iqv9iFRX0>S=wHV|&8*{d+54iaicz zk8T&O*RL)wiz+Flq@S~|a_wLQrBDNW4G-GFA9@Q*`8C`SkUCN+nL=|xkK>z#vMkYc zH}Z#GNTepevXi}De@B!?qp1n9&};wtV!KG8xGL~KmR45ytV|?R6#*B6ido>kzq9^) zU;@&UD42RW<=`L$27~!DP6N)E`}VCnSd9cQ1-S9nEf}lbfx?+HXAFJx)YV^1PC6JE7(99ML_}O1^|>?j*UFVIw^b@DE6rcR zV*mj3r%tl6N`X3?oh<};aCC?(DJ`XKZ3*~!JwqYH45Xx_K%_LY645!K+`_`b)zvp} zI5mI?3J7)3{rd@?x!^nmbns~n{@-V6WEM42wON6ZU5}oHH{cgTJkoynuQ!w${IToY^8y_G zTW + + + + + + + + + + + + + Camera on + + Microphone on + + + diff --git a/src/modules/videoconference/VideoConferenceModule/README.md b/src/modules/videoconference/VideoConferenceModule/README.md new file mode 100644 index 0000000000..d53920685a --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/README.md @@ -0,0 +1,14 @@ +# Video Conference Mute + +# Introduction +The Video Conference Mute module allows muting microphone and/or web camera video stream during video calls or other activity. + +# Usage +If you'd like to mute your web camera, please select "PowerToys VideoConference Mute" device in your web camera-using app, then restart it. + +During a video call, you can use default shortcuts to mute microphone, web camera or both. You'll see a toolbar indicating corresponding mute statuses. + +# Options +You can tweak the toolbar position on the screen as well as set web camera overlay image during muting. + +# Backlog diff --git a/src/modules/videoconference/VideoConferenceModule/Toolbar.cpp b/src/modules/videoconference/VideoConferenceModule/Toolbar.cpp new file mode 100644 index 0000000000..5082298020 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Toolbar.cpp @@ -0,0 +1,335 @@ +#include "pch.h" +#include "Toolbar.h" + +#include + +#include + +#include "Logging.h" +#include "VideoConferenceModule.h" + +Toolbar* toolbar = nullptr; + +const int REFRESH_RATE = 100; +const int OVERLAY_SHOW_TIME = 500; +const int BORDER_OFFSET = 12; + +Toolbar::Toolbar() +{ + toolbar = this; + darkImages.camOnMicOn = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/On-On Dark.png"); + darkImages.camOffMicOn = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/On-Off Dark.png"); + darkImages.camOnMicOff = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/Off-On Dark.png"); + darkImages.camOffMicOff = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/Off-Off Dark.png"); + darkImages.camUnusedMicOn = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/On-NotInUse Dark.png"); + darkImages.camUnusedMicOff = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/Off-NotInUse Dark.png"); + + lightImages.camOnMicOn = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/On-On Light.png"); + lightImages.camOffMicOn = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/On-Off Light.png"); + lightImages.camOnMicOff = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/Off-On Light.png"); + lightImages.camOffMicOff = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/Off-Off Light.png"); + lightImages.camUnusedMicOn = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/On-NotInUse Light.png"); + lightImages.camUnusedMicOff = Gdiplus::Image::FromFile(L"modules/VideoConference/Icons/Off-NotInUse Light.png"); +} + +void Toolbar::scheduleModuleSettingsUpdate() +{ + moduleSettingsUpdateScheduled = true; +} + +void Toolbar::scheduleGeneralSettingsUpdate() +{ + generalSettingsUpdateScheduled = true; +} + +LRESULT Toolbar::WindowProcessMessages(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) +{ + switch (msg) + { + case WM_DESTROY: + return 0; + case WM_LBUTTONDOWN: + { + int x = GET_X_LPARAM(lparam); + int y = GET_Y_LPARAM(lparam); + + if (x < 322 / 2) + { + VideoConferenceModule::reverseMicrophoneMute(); + } + else + { + VideoConferenceModule::reverseVirtualCameraMuteState(); + } + + return DefWindowProcW(hwnd, msg, wparam, lparam); + } + case WM_CREATE: + case WM_PAINT: + { + PAINTSTRUCT ps; + HDC hdc; + + hdc = BeginPaint(hwnd, &ps); + + Gdiplus::Graphics graphic(hdc); + + ToolbarImages* themeImages = &toolbar->darkImages; + + if (toolbar->theme == L"light" || (toolbar->theme == L"system" && !WindowsColors::is_dark_mode())) + { + themeImages = &toolbar->lightImages; + } + else + { + themeImages = &toolbar->darkImages; + } + Gdiplus::Image* toolbarImage = nullptr; + if (!toolbar->cameraInUse) + { + if (toolbar->microphoneMuted) + { + toolbarImage = themeImages->camUnusedMicOff; + } + else + { + toolbarImage = themeImages->camUnusedMicOn; + } + } + else if (toolbar->microphoneMuted) + { + if (toolbar->cameraMuted) + { + toolbarImage = themeImages->camOffMicOff; + } + else + { + toolbarImage = themeImages->camOnMicOff; + } + } + else + { + if (toolbar->cameraMuted) + { + toolbarImage = themeImages->camOffMicOn; + } + else + { + toolbarImage = themeImages->camOnMicOn; + } + } + graphic.DrawImage(toolbarImage, 0, 0, toolbarImage->GetWidth(), toolbarImage->GetHeight()); + + EndPaint(hwnd, &ps); + break; + } + case WM_TIMER: + { + if (toolbar->generalSettingsUpdateScheduled) + { + instance->onGeneralSettingsChanged(); + toolbar->generalSettingsUpdateScheduled = false; + } + if (toolbar->moduleSettingsUpdateScheduled) + { + instance->onModuleSettingsChanged(); + toolbar->moduleSettingsUpdateScheduled = false; + } + + toolbar->cameraInUse = VideoConferenceModule::getVirtualCameraInUse(); + + InvalidateRect(hwnd, NULL, NULL); + + using namespace std::chrono; + const auto nowMillis = duration_cast(system_clock::now().time_since_epoch()).count(); + const bool showOverlayTimeout = nowMillis - toolbar->lastTimeCamOrMicMuteStateChanged > OVERLAY_SHOW_TIME; + + static bool previousShow = false; + bool show = false; + + if (toolbar->cameraInUse) + { + show = toolbar->HideToolbarWhenUnmuted ? toolbar->microphoneMuted || toolbar->cameraMuted : true; + } + else if (toolbar->previouscameraInUse) + { + VideoConferenceModule::unmuteAll(); + } + else + { + show = toolbar->microphoneMuted; + } + show = show || !showOverlayTimeout; + if (show) + { + ShowWindow(hwnd, SW_SHOW); + } + else + { + ShowWindow(hwnd, SW_HIDE); + } + if (previousShow != show) + { + previousShow = show; + LOG(show ? "Toolbar visibility changed to shown" : "Toolbar visibility changed to hidden"); + } + + KillTimer(hwnd, toolbar->nTimerId); + toolbar->previouscameraInUse = toolbar->cameraInUse; + break; + } + default: + return DefWindowProcW(hwnd, msg, wparam, lparam); + } + + toolbar->nTimerId = SetTimer(hwnd, 101, REFRESH_RATE, nullptr); + + return DefWindowProcW(hwnd, msg, wparam, lparam); +} + +void Toolbar::show(std::wstring position, std::wstring monitorString) +{ + for (auto& hwnd : hwnds) + { + PostMessageW(hwnd, WM_CLOSE, 0, 0); + } + hwnds.clear(); + + int overlayWidth = darkImages.camOffMicOff->GetWidth(); + int overlayHeight = darkImages.camOffMicOff->GetHeight(); + + // Register the window class + LPCWSTR CLASS_NAME = L"MuteNotificationWindowClass"; + WNDCLASS wc{}; + wc.hInstance = GetModuleHandleW(nullptr); + wc.lpszClassName = CLASS_NAME; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = (HBRUSH)COLOR_WINDOW; + wc.lpfnWndProc = WindowProcessMessages; + RegisterClassW(&wc); + + // Create the window + DWORD dwExtStyle = 0; + DWORD dwStyle = WS_POPUPWINDOW; + + std::vector monitorInfos; + + if (monitorString == L"All monitors") + { + monitorInfos = MonitorInfo::GetMonitors(false); + } + else //"Main monitor" or non-present + { + monitorInfos.push_back(MonitorInfo::GetPrimaryMonitor()); + } + + for (auto& monitorInfo : monitorInfos) + { + int positionX = 0; + int positionY = 0; + + if (position == L"Top left corner") + { + positionX = monitorInfo.left() + BORDER_OFFSET; + positionY = monitorInfo.top() + BORDER_OFFSET; + } + else if (position == L"Top center") + { + positionX = monitorInfo.middle().x - overlayWidth / 2; + positionY = monitorInfo.top() + BORDER_OFFSET; + } + else if (position == L"Bottom left corner") + { + positionX = monitorInfo.left() + BORDER_OFFSET; + positionY = monitorInfo.bottom() - overlayHeight - BORDER_OFFSET; + } + else if (position == L"Bottom center") + { + positionX = monitorInfo.middle().x - overlayWidth / 2; + positionY = monitorInfo.bottom() - overlayHeight - BORDER_OFFSET; + } + else if (position == L"Bottom right corner") + { + positionX = monitorInfo.right() - overlayWidth - BORDER_OFFSET; + positionY = monitorInfo.bottom() - overlayHeight - BORDER_OFFSET; + } + else //"Top right corner" or non-present + { + positionX = monitorInfo.right() - overlayWidth - BORDER_OFFSET; + positionY = monitorInfo.top() + BORDER_OFFSET; + } + + HWND hwnd; + hwnd = CreateWindowExW( + WS_EX_TOOLWINDOW | WS_EX_LAYERED, + CLASS_NAME, + CLASS_NAME, + WS_POPUP, + positionX, + positionY, + overlayWidth, + overlayHeight, + nullptr, + nullptr, + GetModuleHandleW(nullptr), + nullptr); + + auto transparrentColorKey = RGB(0, 0, 255); + HBRUSH brush = CreateSolidBrush(transparrentColorKey); + SetClassLongPtr(hwnd, GCLP_HBRBACKGROUND, (LONG_PTR)brush); + + SetLayeredWindowAttributes(hwnd, transparrentColorKey, 0, LWA_COLORKEY); + + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + + hwnds.push_back(hwnd); + } +} + +void Toolbar::hide() +{ + for (auto& hwnd : hwnds) + { + PostMessage(hwnd, WM_CLOSE, 0, 0); + } + hwnds.clear(); +} + +bool Toolbar::getCameraMute() +{ + return cameraMuted; +} + +void Toolbar::setCameraMute(bool mute) +{ + if (mute != cameraMuted) + { + lastTimeCamOrMicMuteStateChanged = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } + cameraMuted = mute; +} + +bool Toolbar::getMicrophoneMute() +{ + return microphoneMuted; +} + +void Toolbar::setMicrophoneMute(bool mute) +{ + if (mute != microphoneMuted) + { + lastTimeCamOrMicMuteStateChanged = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + } + + microphoneMuted = mute; +} + +void Toolbar::setHideToolbarWhenUnmuted(bool hide) +{ + HideToolbarWhenUnmuted = hide; +} + +void Toolbar::setTheme(std::wstring theme) +{ + Toolbar::theme = theme; +} diff --git a/src/modules/videoconference/VideoConferenceModule/Toolbar.h b/src/modules/videoconference/VideoConferenceModule/Toolbar.h new file mode 100644 index 0000000000..7bdc86f105 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Toolbar.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include + +struct ToolbarImages +{ + Gdiplus::Image* camOnMicOn = nullptr; + Gdiplus::Image* camOffMicOn = nullptr; + Gdiplus::Image* camOnMicOff = nullptr; + Gdiplus::Image* camOffMicOff = nullptr; + Gdiplus::Image* camUnusedMicOn = nullptr; + Gdiplus::Image* camUnusedMicOff = nullptr; +}; + +class Toolbar +{ +public: + Toolbar(); + + void scheduleModuleSettingsUpdate(); + void scheduleGeneralSettingsUpdate(); + + void show(std::wstring position, std::wstring monitorString); + void hide(); + + bool getCameraMute(); + void setCameraMute(bool mute); + bool getMicrophoneMute(); + void setMicrophoneMute(bool mute); + + void setTheme(std::wstring theme); + void setHideToolbarWhenUnmuted(bool hide); + +private: + static LRESULT CALLBACK WindowProcessMessages(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + + // Window callback can't be non-static so this members can't as well + std::vector hwnds; + + ToolbarImages darkImages; + ToolbarImages lightImages; + + bool cameraMuted = false; + bool cameraInUse = false; + bool previouscameraInUse = false; + bool microphoneMuted = false; + + std::wstring theme = L"system"; + + bool HideToolbarWhenUnmuted = true; + + uint64_t lastTimeCamOrMicMuteStateChanged; + + std::atomic_bool moduleSettingsUpdateScheduled = false; + std::atomic_bool generalSettingsUpdateScheduled = false; + UINT_PTR nTimerId; +}; diff --git a/src/modules/videoconference/VideoConferenceModule/Video Conference.filters b/src/modules/videoconference/VideoConferenceModule/Video Conference.filters new file mode 100644 index 0000000000..e19316f1bd --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Video Conference.filters @@ -0,0 +1,55 @@ + + + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + {2c7c97f7-0d87-4230-a4b2-baf2cfc35d58} + + + {aa4b6713-589d-42ef-804d-3a045833f83f} + + + + + + + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj b/src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj new file mode 100644 index 0000000000..425313bce1 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj @@ -0,0 +1,183 @@ + + + + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {5ABA70DE-3A3F-41F6-A1F5-D1F74F54F9BB} + Win32Proj + overlaywindow + 10.0.18362.0 + VideoConferenceModule + + + + DynamicLibrary + true + v142 + Unicode + Spectre + + + DynamicLibrary + false + v142 + true + Unicode + Spectre + + + + + + + + + + + + + + + + false + $(SolutionDir)$(Platform)\$(Configuration)\modules\VideoConference\ + $(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\ + + + true + $(SolutionDir)$(Platform)\$(Configuration)\modules\VideoConference\ + $(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\ + + + + Use + Level3 + MaxSpeed + true + true + true + NDEBUG;OVERLAYWINDOW_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + pch.h + MultiThreaded + ..\..\..\;..\..\;..\VideoConferenceShared\;%(AdditionalIncludeDirectories) + stdcpplatest + + + Windows + true + true + true + $(OutDir)$(TargetName)$(TargetExt) + mfplat.lib;mf.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;gdiplus.lib;dwmapi.lib;uxtheme.lib;shcore.lib;Wtsapi32.lib;%(AdditionalDependencies) + + + xcopy /y /I "$(ProjectDir)Icons\*" "$(OutDir)Icons" +xcopy /y /I "$(ProjectDir)black.bmp*" "$(OutDir)" + + + + + Use + Level3 + Disabled + true + _DEBUG;OVERLAYWINDOW_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + pch.h + MultiThreadedDebug + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\..\;..\..\;..\VideoConferenceShared\;%(AdditionalIncludeDirectories) + stdcpplatest + + + Windows + true + $(OutDir)$(TargetName)$(TargetExt) + mfplat.lib;mf.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;gdiplus.lib;dwmapi.lib;uxtheme.lib;shcore.lib;Wtsapi32.lib;dxguid.lib;%(AdditionalDependencies) + + + xcopy /y /I "$(ProjectDir)Icons\*" "$(OutDir)Icons" +xcopy /y /I "$(ProjectDir)black.bmp*" "$(OutDir)" + + + + + + + + + + + + + + + Create + Create + + + + + + {459e0768-7ebd-4c41-bba1-6db3b3815e0a} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + {caba8dfb-823b-4bf2-93ac-3f31984150d9} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {98537082-0fdb-40de-abd8-0dc5a4269bab} + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj.filters b/src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj.filters new file mode 100644 index 0000000000..014fce2e73 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/Video Conference.vcxproj.filters @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + Icons + + + + + {735361e2-82fa-4034-b9c9-cd6aa099eaa5} + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.cpp b/src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.cpp new file mode 100644 index 0000000000..b310315648 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.cpp @@ -0,0 +1,568 @@ +#include "pch.h" + +#include "VideoConferenceModule.h" + +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include "logging.h" +#include "trace.h" + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +VideoConferenceModule* instance = nullptr; + +VideoConferenceSettings VideoConferenceModule::settings; +Toolbar VideoConferenceModule::toolbar; + +HHOOK VideoConferenceModule::hook_handle; + +IAudioEndpointVolume* endpointVolume = NULL; + +bool VideoConferenceModule::isKeyPressed(unsigned int keyCode) +{ + return (GetKeyState(keyCode) & 0x8000); +} + +namespace fs = std::filesystem; + +bool VideoConferenceModule::isHotkeyPressed(DWORD code, PowerToysSettings::HotkeyObject& hotkey) +{ + return code == hotkey.get_code() && + isKeyPressed(VK_SHIFT) == hotkey.shift_pressed() && + isKeyPressed(VK_CONTROL) == hotkey.ctrl_pressed() && + isKeyPressed(VK_LWIN) == hotkey.win_pressed() && + (isKeyPressed(VK_LMENU)) == hotkey.alt_pressed(); +} + +void VideoConferenceModule::reverseMicrophoneMute() +{ + bool muted = false; + for (auto& controlledMic : instance->_controlledMicrophones) + { + const bool was_muted = controlledMic.muted(); + controlledMic.toggle_muted(); + muted = muted || !was_muted; + } + if (muted) + { + Trace::MicrophoneMuted(); + } + toolbar.setMicrophoneMute(muted); +} + +bool VideoConferenceModule::getMicrophoneMuteState() +{ + return instance->_microphoneTrackedInUI ? instance->_microphoneTrackedInUI->muted() : false; +} + +void VideoConferenceModule::reverseVirtualCameraMuteState() +{ + bool muted = false; + if (!instance->_settingsUpdateChannel.has_value()) + { + return; + } + + instance->_settingsUpdateChannel->access([&muted](auto settingsMemory) { + auto settings = reinterpret_cast(settingsMemory._data); + settings->useOverlayImage = !settings->useOverlayImage; + muted = settings->useOverlayImage; + }); + + if (muted) + { + Trace::CameraMuted(); + } + toolbar.setCameraMute(muted); +} + +bool VideoConferenceModule::getVirtualCameraMuteState() +{ + bool disabled = false; + if (!instance->_settingsUpdateChannel.has_value()) + { + return disabled; + } + instance->_settingsUpdateChannel->access([&disabled](auto settingsMemory) { + auto settings = reinterpret_cast(settingsMemory._data); + disabled = settings->useOverlayImage; + }); + return disabled; +} + +bool VideoConferenceModule::getVirtualCameraInUse() +{ + if (!instance->_settingsUpdateChannel.has_value()) + { + return false; + } + bool inUse = false; + instance->_settingsUpdateChannel->access([&inUse](auto settingsMemory) { + auto settings = reinterpret_cast(settingsMemory._data); + inUse = settings->cameraInUse; + }); + return inUse; +} + +LRESULT CALLBACK VideoConferenceModule::LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) +{ + if (nCode == HC_ACTION) + { + switch (wParam) + { + case WM_KEYDOWN: + KBDLLHOOKSTRUCT* kbd = reinterpret_cast(lParam); + + if (isHotkeyPressed(kbd->vkCode, settings.cameraAndMicrophoneMuteHotkey)) + { + const bool cameraInUse = getVirtualCameraInUse(); + const bool microphoneIsMuted = getMicrophoneMuteState(); + const bool cameraIsMuted = cameraInUse && getVirtualCameraMuteState(); + if (cameraInUse) + { + // we're likely on a video call, so we must mute the unmuted cam/mic or reverse the mute state + // of everything, if cam and mic mute states are the same + if (microphoneIsMuted == cameraIsMuted) + { + reverseMicrophoneMute(); + reverseVirtualCameraMuteState(); + } + else if (cameraIsMuted) + { + reverseMicrophoneMute(); + } + else if (microphoneIsMuted) + { + reverseVirtualCameraMuteState(); + } + } + else + { + // if the camera is not in use, we just mute/unmute the mic + reverseMicrophoneMute(); + } + return 1; + } + else if (isHotkeyPressed(kbd->vkCode, settings.microphoneMuteHotkey)) + { + reverseMicrophoneMute(); + return 1; + } + else if (isHotkeyPressed(kbd->vkCode, settings.cameraMuteHotkey)) + { + reverseVirtualCameraMuteState(); + return 1; + } + } + } + + return CallNextHookEx(hook_handle, nCode, wParam, lParam); +} + +void VideoConferenceModule::onGeneralSettingsChanged() +{ + auto settings = PTSettingsHelper::load_general_settings(); + bool enabled = false; + try + { + if (json::has(settings, L"enabled")) + { + for (const auto& mod : settings.GetNamedObject(L"enabled")) + { + const auto value = mod.Value(); + if (value.ValueType() != json::JsonValueType::Boolean) + { + continue; + } + if (mod.Key() == get_key()) + { + enabled = value.GetBoolean(); + break; + } + } + } + } + catch (...) + { + LOG("Couldn't get enabled state"); + } + if (enabled) + { + enable(); + } + else + { + disable(); + } +} + +void VideoConferenceModule::onModuleSettingsChanged() +{ + try + { + PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(get_key()); + //Trace::SettingsChanged(pressTime.value, overlayOpacity.value, theme.value); + + if (_enabled) + { + if (const auto val = values.get_json(L"mute_camera_and_microphone_hotkey")) + { + settings.cameraAndMicrophoneMuteHotkey = PowerToysSettings::HotkeyObject::from_json(*val); + } + if (const auto val = values.get_json(L"mute_microphone_hotkey")) + { + settings.microphoneMuteHotkey = PowerToysSettings::HotkeyObject::from_json(*val); + } + if (const auto val = values.get_json(L"mute_camera_hotkey")) + { + settings.cameraMuteHotkey = PowerToysSettings::HotkeyObject::from_json(*val); + } + if (const auto val = values.get_string_value(L"toolbar_position")) + { + settings.toolbarPositionString = val.value(); + } + if (const auto val = values.get_string_value(L"toolbar_monitor")) + { + settings.toolbarMonitorString = val.value(); + } + if (const auto val = values.get_string_value(L"selected_camera"); val && val != settings.selectedCamera) + { + settings.selectedCamera = val.value(); + sendSourceCameraNameUpdate(); + } + if (const auto val = values.get_string_value(L"camera_overlay_image_path"); val && val != settings.imageOverlayPath) + { + settings.imageOverlayPath = val.value(); + sendOverlayImageUpdate(); + } + if (const auto val = values.get_bool_value(L"hide_toolbar_when_unmuted")) + { + toolbar.setHideToolbarWhenUnmuted(val.value()); + } + + const auto selectedMic = values.get_string_value(L"selected_mic"); + if (selectedMic && selectedMic != settings.selectedMicrophone) + { + settings.selectedMicrophone = *selectedMic; + updateControlledMicrophones(settings.selectedMicrophone); + } + + toolbar.show(settings.toolbarPositionString, settings.toolbarMonitorString); + } + } + catch (...) + { + LOG("onModuleSettingsChanged encountered an exception"); + } +} + +VideoConferenceModule::VideoConferenceModule() : + _generalSettingsWatcher{ PTSettingsHelper::get_powertoys_general_save_file_location(), [this] { + toolbar.scheduleGeneralSettingsUpdate(); + } }, + _moduleSettingsWatcher{ PTSettingsHelper::get_module_save_file_location(get_key()), [this] { toolbar.scheduleModuleSettingsUpdate(); } } +{ + init_settings(); + _settingsUpdateChannel = + SerializedSharedMemory::create(CameraSettingsUpdateChannel::endpoint(), sizeof(CameraSettingsUpdateChannel), false); + if (_settingsUpdateChannel) + { + _settingsUpdateChannel->access([](auto memory) { + auto updatesChannel = new (memory._data) CameraSettingsUpdateChannel{}; + }); + } + sendSourceCameraNameUpdate(); + sendOverlayImageUpdate(); +} + +inline VideoConferenceModule::~VideoConferenceModule() +{ + instance->unmuteAll(); + toolbar.hide(); +} + +const wchar_t* VideoConferenceModule::get_name() +{ + return L"Video Conference"; +} + +const wchar_t* VideoConferenceModule::get_key() +{ + return L"Video Conference"; +} + +bool VideoConferenceModule::get_config(wchar_t* buffer, int* buffer_size) +{ + return true; +} + +void VideoConferenceModule::set_config(const wchar_t* config) +{ + try + { + PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + values.save_to_settings_file(); + } + catch (...) + { + LOG("VideoConferenceModule::set_config: exception during saving new settings values"); + } +} + +void VideoConferenceModule::init_settings() +{ + try + { + PowerToysSettings::PowerToyValues powerToysSettings = PowerToysSettings::PowerToyValues::load_from_settings_file(L"Video Conference"); + + if (const auto val = powerToysSettings.get_json(L"mute_camera_and_microphone_hotkey")) + { + settings.cameraAndMicrophoneMuteHotkey = PowerToysSettings::HotkeyObject::from_json(*val); + } + if (const auto val = powerToysSettings.get_json(L"mute_microphone_hotkey")) + { + settings.microphoneMuteHotkey = PowerToysSettings::HotkeyObject::from_json(*val); + } + if (const auto val = powerToysSettings.get_json(L"mute_camera_hotkey")) + { + settings.cameraMuteHotkey = PowerToysSettings::HotkeyObject::from_json(*val); + } + if (const auto val = powerToysSettings.get_string_value(L"toolbar_position")) + { + settings.toolbarPositionString = val.value(); + } + if (const auto val = powerToysSettings.get_string_value(L"toolbar_monitor")) + { + settings.toolbarMonitorString = val.value(); + } + if (const auto val = powerToysSettings.get_string_value(L"selected_camera")) + { + settings.selectedCamera = val.value(); + } + if (const auto val = powerToysSettings.get_string_value(L"camera_overlay_image_path")) + { + settings.imageOverlayPath = val.value(); + } + if (const auto val = powerToysSettings.get_bool_value(L"hide_toolbar_when_unmuted")) + { + toolbar.setHideToolbarWhenUnmuted(val.value()); + } + if (const auto val = powerToysSettings.get_string_value(L"selected_mic"); val && *val != settings.selectedMicrophone) + { + settings.selectedMicrophone = *val; + updateControlledMicrophones(settings.selectedMicrophone); + } + } + catch (std::exception&) + { + // Error while loading from the settings file. Just let default values stay as they are. + } + + try + { + auto loaded = PTSettingsHelper::load_general_settings(); + std::wstring settings_theme{ static_cast(loaded.GetNamedString(L"theme", L"system")) }; + if (settings_theme != L"dark" && settings_theme != L"light") + { + settings_theme = L"system"; + } + toolbar.setTheme(settings_theme); + } + catch (...) + { + } +} + +void VideoConferenceModule::updateControlledMicrophones(const std::wstring_view new_mic) +{ + for (auto& controlledMic : _controlledMicrophones) + { + controlledMic.set_muted(false); + } + _controlledMicrophones.clear(); + _microphoneTrackedInUI = nullptr; + auto allMics = MicrophoneDevice::getAllActive(); + if (new_mic == L"[All]") + { + _controlledMicrophones = std::move(allMics); + if (auto defaultMic = MicrophoneDevice::getDefault()) + { + for (auto& controlledMic : _controlledMicrophones) + { + if (controlledMic.id() == defaultMic->id()) + { + _microphoneTrackedInUI = &controlledMic; + break; + } + } + } + } + else + { + for (auto& controlledMic : allMics) + { + if (controlledMic.name() == new_mic) + { + _controlledMicrophones.emplace_back(std::move(controlledMic)); + _microphoneTrackedInUI = &_controlledMicrophones[0]; + break; + } + } + } + if (_microphoneTrackedInUI) + { + _microphoneTrackedInUI->set_mute_changed_callback([&](const bool muted) { + toolbar.setMicrophoneMute(muted); + }); + toolbar.setMicrophoneMute(_microphoneTrackedInUI->muted()); + } +} + +void toggleProxyCamRegistration(const bool enable) +{ + if (!is_process_elevated()) + { + return; + } + + auto vcmRoot = fs::path{ get_module_folderpath() } / "modules"; + vcmRoot /= "VideoConference"; + + std::array proxyFilters = { vcmRoot / "VideoConferenceProxyFilter_x64.dll", vcmRoot / "VideoConferenceProxyFilter_x86.dll" }; + for (const auto filter : proxyFilters) + { + std::wstring params{ L"/s " }; + if (!enable) + { + params += L"/u "; + } + params += '"'; + params += filter; + params += '"'; + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC }; + sei.lpFile = L"regsvr32"; + sei.lpParameters = params.c_str(); + sei.nShow = SW_SHOWNORMAL; + ShellExecuteExW(&sei); + } +} + +void VideoConferenceModule::enable() +{ + if (!_enabled) + { + toggleProxyCamRegistration(true); + toolbar.setMicrophoneMute(getMicrophoneMuteState()); + toolbar.setCameraMute(getVirtualCameraMuteState()); + + toolbar.show(settings.toolbarPositionString, settings.toolbarMonitorString); + + _enabled = true; + +#if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED) + if (IsDebuggerPresent()) + { + return; + } +#endif + hook_handle = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), NULL); + } +} + +void VideoConferenceModule::unmuteAll() +{ + if (getVirtualCameraMuteState()) + { + reverseVirtualCameraMuteState(); + } + + if (getMicrophoneMuteState()) + { + reverseMicrophoneMute(); + } +} + +void VideoConferenceModule::disable() +{ + if (_enabled) + { + toggleProxyCamRegistration(false); + if (hook_handle) + { + bool success = UnhookWindowsHookEx(hook_handle); + if (success) + { + hook_handle = nullptr; + } + } + + instance->unmuteAll(); + toolbar.hide(); + + _enabled = false; + } +} + +bool VideoConferenceModule::is_enabled() +{ + return _enabled; +} + +void VideoConferenceModule::destroy() +{ + delete this; + instance = nullptr; +} + +void VideoConferenceModule::sendSourceCameraNameUpdate() +{ + if (!_settingsUpdateChannel.has_value() || settings.selectedCamera.empty()) + { + return; + } + _settingsUpdateChannel->access([](auto memory) { + auto updatesChannel = reinterpret_cast(memory._data); + updatesChannel->sourceCameraName.emplace(); + std::copy(begin(settings.selectedCamera), end(settings.selectedCamera), begin(*updatesChannel->sourceCameraName)); + }); +} + +void VideoConferenceModule::sendOverlayImageUpdate() +{ + if (!_settingsUpdateChannel.has_value()) + { + return; + } + _imageOverlayChannel.reset(); + + wchar_t powertoysDirectory[MAX_PATH + 1]; + + DWORD length = GetModuleFileNameW(nullptr, powertoysDirectory, MAX_PATH); + PathRemoveFileSpecW(powertoysDirectory); + + std::wstring blankImagePath(powertoysDirectory); + blankImagePath += L"\\modules\\VideoConference\\black.bmp"; + + _imageOverlayChannel = SerializedSharedMemory::create_readonly(CameraOverlayImageChannel::endpoint(), + settings.imageOverlayPath != L"" ? settings.imageOverlayPath : blankImagePath); + + const auto imageSize = static_cast(_imageOverlayChannel->size()); + _settingsUpdateChannel->access([imageSize](auto memory) { + auto updatesChannel = reinterpret_cast(memory._data); + updatesChannel->overlayImageSize.emplace(imageSize); + updatesChannel->newOverlayImagePosted = true; + }); +} diff --git a/src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.h b/src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.h new file mode 100644 index 0000000000..2a37546362 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.h @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include +#include + +#include + +#include +#include + +#include "Toolbar.h" + +#include + +extern class VideoConferenceModule* instance; + +struct VideoConferenceSettings +{ + PowerToysSettings::HotkeyObject cameraAndMicrophoneMuteHotkey = PowerToysSettings::HotkeyObject::from_settings(true, false, false, false, 78); + PowerToysSettings::HotkeyObject microphoneMuteHotkey = PowerToysSettings::HotkeyObject::from_settings(true, false, false, true, 65); + PowerToysSettings::HotkeyObject cameraMuteHotkey = PowerToysSettings::HotkeyObject::from_settings(true, false, false, true, 79); + + std::wstring toolbarPositionString; + std::wstring toolbarMonitorString; + + std::wstring selectedCamera; + std::wstring imageOverlayPath; + std::wstring selectedMicrophone; +}; + +class VideoConferenceModule : public PowertoyModuleIface +{ +public: + VideoConferenceModule(); + ~VideoConferenceModule(); + virtual const wchar_t* get_name() override; + + virtual bool get_config(wchar_t* buffer, int* buffer_size) override; + + virtual void set_config(const wchar_t* config) override; + + virtual void enable() override; + virtual void disable() override; + virtual bool is_enabled() override; + virtual void destroy() override; + + virtual const wchar_t * get_key() override; + + void sendSourceCameraNameUpdate(); + void sendOverlayImageUpdate(); + + static void unmuteAll(); + static void reverseMicrophoneMute(); + static bool getMicrophoneMuteState(); + static void reverseVirtualCameraMuteState(); + static bool getVirtualCameraMuteState(); + static bool getVirtualCameraInUse(); + + void onGeneralSettingsChanged(); + void onModuleSettingsChanged(); +private: + + void init_settings(); + void updateControlledMicrophones(const std::wstring_view new_mic); + // all callback methods and used by callback have to be static + static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam); + static bool isKeyPressed(unsigned int keyCode); + static bool isHotkeyPressed(DWORD code, PowerToysSettings::HotkeyObject& hotkey); + + static HHOOK hook_handle; + bool _enabled = false; + + std::vector _controlledMicrophones; + MicrophoneDevice* _microphoneTrackedInUI = nullptr; + + std::optional _imageOverlayChannel; + std::optional _settingsUpdateChannel; + + FileWatcher _generalSettingsWatcher; + FileWatcher _moduleSettingsWatcher; + + static VideoConferenceSettings settings; + static Toolbar toolbar; +}; diff --git a/src/modules/videoconference/VideoConferenceModule/black.bmp b/src/modules/videoconference/VideoConferenceModule/black.bmp new file mode 100644 index 0000000000000000000000000000000000000000..18d40779cedd9cf94146441fb2f6aeb19c617d9d GIT binary patch literal 822 lcmZ?rHDhJ~12Z700mK4O%*Y@C7H5FULpY=4Xb6mk003!i0b>9F literal 0 HcmV?d00001 diff --git a/src/modules/videoconference/VideoConferenceModule/dllmain.cpp b/src/modules/videoconference/VideoConferenceModule/dllmain.cpp new file mode 100644 index 0000000000..c3d012cd6c --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/dllmain.cpp @@ -0,0 +1,35 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" +#include +#include "trace.h" +#include "VideoConferenceModule.h" + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; +} + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + if (!instance) + { + instance = new VideoConferenceModule(); + return instance; + } + else + { + return nullptr; + } +} diff --git a/src/modules/videoconference/VideoConferenceModule/framework.h b/src/modules/videoconference/VideoConferenceModule/framework.h new file mode 100644 index 0000000000..54b83e94fd --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/framework.h @@ -0,0 +1,5 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files +#include diff --git a/src/modules/videoconference/VideoConferenceModule/packages.config b/src/modules/videoconference/VideoConferenceModule/packages.config new file mode 100644 index 0000000000..20da4fefa3 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceModule/pch.cpp b/src/modules/videoconference/VideoConferenceModule/pch.cpp new file mode 100644 index 0000000000..64b7eef6d6 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/src/modules/videoconference/VideoConferenceModule/pch.h b/src/modules/videoconference/VideoConferenceModule/pch.h new file mode 100644 index 0000000000..8e268511bc --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/pch.h @@ -0,0 +1,24 @@ +#pragma once +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include diff --git a/src/modules/videoconference/VideoConferenceModule/trace.cpp b/src/modules/videoconference/VideoConferenceModule/trace.cpp new file mode 100644 index 0000000000..377d49b18a --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/trace.cpp @@ -0,0 +1,57 @@ +#include "pch.h" + +#include "trace.h" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + "Microsoft.PowerToys", + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::RegisterProvider() noexcept +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() noexcept +{ + TraceLoggingUnregister(g_hProvider); +} + +void Trace::SettingsChanged(const struct VideoConferenceSettings& settings) noexcept +{ + bool CustomOverlayImage = (settings.imageOverlayPath.length() > 0); + + TraceLoggingWrite( + g_hProvider, + "VideoConference_SettingsChanged", + TraceLoggingWideString(settings.toolbarPositionString.c_str(), "ToolbarPosition"), + TraceLoggingWideString(settings.toolbarMonitorString.c_str(), "ToolbarMonitorSelection"), + TraceLoggingBool(CustomOverlayImage, "CustomImageOverlayUsed"), + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::MicrophoneMuted() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "VideoConference_MicrophoneMuted", + TraceLoggingBoolean(true, "MicrophoneMuted"), + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::CameraMuted() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "VideoConference_CameraMuted", + TraceLoggingBoolean(true, "CameraMuted"), + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} diff --git a/src/modules/videoconference/VideoConferenceModule/trace.h b/src/modules/videoconference/VideoConferenceModule/trace.h new file mode 100644 index 0000000000..c9283256df --- /dev/null +++ b/src/modules/videoconference/VideoConferenceModule/trace.h @@ -0,0 +1,12 @@ +#pragma once +#include "VideoConferenceModule.h" + +class Trace +{ +public: + static void RegisterProvider() noexcept; + static void UnregisterProvider() noexcept; + static void SettingsChanged(const struct VideoConferenceSettings &settings) noexcept; + static void MicrophoneMuted() noexcept; + static void CameraMuted() noexcept; +}; diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.cpp b/src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.cpp new file mode 100644 index 0000000000..00359670f3 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.cpp @@ -0,0 +1,118 @@ +#include "DirectShowUtils.h" + +#include + +unique_media_type_ptr CopyMediaType(const AM_MEDIA_TYPE* source) +{ + unique_media_type_ptr target{ static_cast(CoTaskMemAlloc(sizeof(AM_MEDIA_TYPE))) }; + *target = *source; + if (source->cbFormat) + { + target->pbFormat = static_cast(CoTaskMemAlloc(source->cbFormat)); + std::copy(source->pbFormat, source->pbFormat + source->cbFormat, target->pbFormat); + } + + if (target->pUnk) + { + target->pUnk->AddRef(); + } + + return target; +} + +wil::com_ptr_nothrow GetPinAllocator(wil::com_ptr_nothrow& inputPin) +{ + if (!inputPin) + { + return nullptr; + } + wil::com_ptr_nothrow allocator; + if (auto memInput = inputPin.try_query(); memInput) + { + memInput->GetAllocator(&allocator); + return allocator; + } + + return nullptr; +} + +unique_media_type_ptr CopyMediaType(const unique_media_type_ptr& source) +{ + return CopyMediaType(source.get()); +} + +void MyFreeMediaType(AM_MEDIA_TYPE& mt) +{ + if (mt.cbFormat != 0) + { + CoTaskMemFree(mt.pbFormat); + mt.cbFormat = 0; + mt.pbFormat = nullptr; + } + + if (mt.pUnk != nullptr) + { + mt.pUnk->Release(); + mt.pUnk = nullptr; + } +} + +void MyDeleteMediaType(AM_MEDIA_TYPE* pmt) +{ + if (!pmt) + { + return; + } + + MyFreeMediaType(*pmt); + CoTaskMemFree(const_cast(pmt)); +} + +HRESULT MediaTypeEnumerator::Next(ULONG cObjects, AM_MEDIA_TYPE** outObjects, ULONG* pcFetched) +{ + if (!outObjects) + { + return E_POINTER; + } + + ULONG fetched = 0; + ULONG toFetch = cObjects; + while (toFetch-- && _pos < _objects.size()) + { + auto copy = CopyMediaType(_objects[_pos++]); + outObjects[fetched++] = copy.release(); + } + + if (pcFetched) + { + *pcFetched = fetched; + } + + return fetched == cObjects ? S_OK : S_FALSE; +} + +HRESULT MediaTypeEnumerator::Skip(ULONG cObjects) +{ + _pos += cObjects; + return _pos < _objects.size() ? S_OK : S_FALSE; +} + +HRESULT MediaTypeEnumerator::Reset() +{ + _pos = 0; + return S_OK; +} + +HRESULT MediaTypeEnumerator::Clone(IEnumMediaTypes** ppEnum) +{ + auto cloned = winrt::make_self(); + cloned->_objects.resize(_objects.size()); + for (size_t i = 0; i < _objects.size(); ++i) + { + cloned->_objects[i] = CopyMediaType(_objects[i]); + } + + cloned->_pos = _pos; + cloned.as().copy_to(ppEnum); + return S_OK; +} \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.h b/src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.h new file mode 100644 index 0000000000..4c5534d3e6 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/DirectShowUtils.h @@ -0,0 +1,88 @@ +#pragma once +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include + +#include + +#include "Logging.h" + +void MyDeleteMediaType(AM_MEDIA_TYPE* pmt); + +using unique_media_type_ptr = + wistd::unique_ptr>; + +unique_media_type_ptr CopyMediaType(const unique_media_type_ptr& source); +unique_media_type_ptr CopyMediaType(const AM_MEDIA_TYPE* source); + +template +struct ObjectEnumerator : public winrt::implements, EnumeratorInterface> +{ + std::vector> _objects; + ULONG _pos = 0; + + HRESULT STDMETHODCALLTYPE Next(ULONG cObjects, ObjectInterface** outObjects, ULONG* pcFetched) override + { + if (!outObjects) + { + return E_POINTER; + } + + ULONG fetched = 0; + ULONG toFetch = cObjects; + while (toFetch-- && _pos < _objects.size()) + { + _objects[_pos++].copy_to(&outObjects[fetched++]); + } + + if (pcFetched) + { + *pcFetched = fetched; + } + + return fetched == cObjects ? S_OK : S_FALSE; + } + + HRESULT STDMETHODCALLTYPE Skip(ULONG cObjects) override + { + _pos += cObjects; + return _pos < _objects.size() ? S_OK : S_FALSE; + } + + HRESULT STDMETHODCALLTYPE Reset() override + { + _pos = 0; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Clone(EnumeratorInterface** ppEnum) override + { + auto cloned = winrt::make_self(); + cloned->_objects = _objects; + cloned->_pos = _pos; + cloned.as().copy_to(ppEnum); + return S_OK; + } + + virtual ~ObjectEnumerator() = default; +}; + +struct MediaTypeEnumerator : public winrt::implements +{ + std::vector _objects; + ULONG _pos = 0; + + HRESULT STDMETHODCALLTYPE Next(ULONG cObjects, AM_MEDIA_TYPE** outObjects, ULONG* pcFetched) override; + HRESULT STDMETHODCALLTYPE Skip(ULONG cObjects) override; + HRESULT STDMETHODCALLTYPE Reset() override; + HRESULT STDMETHODCALLTYPE Clone(IEnumMediaTypes** ppEnum) override; + + virtual ~MediaTypeEnumerator() = default; +}; + +wil::com_ptr_nothrow GetPinAllocator(wil::com_ptr_nothrow& inputPin); diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/ImageLoading.cpp b/src/modules/videoconference/VideoConferenceProxyFilter/ImageLoading.cpp new file mode 100644 index 0000000000..656fd5ea7f --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/ImageLoading.cpp @@ -0,0 +1,425 @@ +#include + +#include +#include +#include + +#pragma warning(push) +#pragma warning(disable : 4005) +#include +#pragma warning(pop) + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "Logging.h" + +IWICImagingFactory* _GetWIC() noexcept +{ + static IWICImagingFactory* s_Factory = nullptr; + + if (s_Factory) + { + return s_Factory; + } + + OK_OR_BAIL(CoCreateInstance( + CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, __uuidof(IWICImagingFactory), (LPVOID*)&s_Factory)); + + return s_Factory; +} + +bool ReencodeJPGImage(BYTE* imageBuf, const DWORD imageSize, DWORD& reencodedSize) +{ + auto pWIC = _GetWIC(); + wil::com_ptr_nothrow imageStream = SHCreateMemStream(imageBuf, imageSize); + if (!imageStream) + { + return false; + } + + // Decode jpg into bitmap + wil::com_ptr_nothrow bitmapDecoder; + OK_OR_BAIL(pWIC->CreateDecoderFromStream(imageStream.get(), nullptr, WICDecodeMetadataCacheOnLoad, &bitmapDecoder)); + wil::com_ptr_nothrow decodedFrame; + OK_OR_BAIL(bitmapDecoder->GetFrame(0, &decodedFrame)); + wil::com_ptr_nothrow bitmap; + bitmap.attach(decodedFrame.detach()); + UINT width = 0, height = 0; + OK_OR_BAIL(bitmap->GetSize(&width, &height)); + + // Initialize jpg encoder + wil::com_ptr_nothrow encoder; + OK_OR_BAIL(pWIC->CreateEncoder(GUID_ContainerFormatJpeg, nullptr, &encoder)); + + wil::com_ptr_nothrow outputStream; + OK_OR_BAIL(CreateStreamOnHGlobal(nullptr, true, &outputStream)); + OK_OR_BAIL(encoder->Initialize(outputStream.get(), WICBitmapEncoderNoCache)); + wil::com_ptr_nothrow encodedFrame; + wil::com_ptr_nothrow encoderOptions; + OK_OR_BAIL(encoder->CreateNewFrame(&encodedFrame, &encoderOptions)); + + ULONG nProperties = 0; + OK_OR_BAIL(encoderOptions->CountProperties(&nProperties)); + for (ULONG propIdx = 0; propIdx < nProperties; ++propIdx) + { + PROPBAG2 propBag{}; + ULONG _; + OK_OR_BAIL(encoderOptions->GetPropertyInfo(propIdx, 1, &propBag, &_)); + if (propBag.pstrName == std::wstring_view{ L"ImageQuality" }) + { + wil::unique_variant variant; + variant.vt = VT_R4; + variant.fltVal = 0.1f; + OK_OR_BAIL(encoderOptions->Write(1, &propBag, &variant)); + LOG("Successfully set jpg compression quality"); + // skip the rest of the properties + propIdx = nProperties; + } + CoTaskMemFree(propBag.pstrName); + } + + OK_OR_BAIL(encodedFrame->Initialize(encoderOptions.get())); + WICPixelFormatGUID intermediateFormat = GUID_WICPixelFormat24bppRGB; + + OK_OR_BAIL(encodedFrame->SetPixelFormat(&intermediateFormat)); + OK_OR_BAIL(encodedFrame->SetSize(width, height)); + + // Commit the image encoding + OK_OR_BAIL(encodedFrame->WriteSource(bitmap.get(), nullptr)); + OK_OR_BAIL(encodedFrame->Commit()); + OK_OR_BAIL(encoder->Commit()); + + STATSTG intermediateStreamStat{}; + OK_OR_BAIL(outputStream->Stat(&intermediateStreamStat, STATFLAG_NONAME)); + const ULONGLONG jpgStreamSize = intermediateStreamStat.cbSize.QuadPart; + HGLOBAL streamMemoryHandle{}; + OK_OR_BAIL(GetHGlobalFromStream(outputStream.get(), &streamMemoryHandle)); + + auto jpgStreamMemory = static_cast(GlobalLock(streamMemoryHandle)); + std::copy(jpgStreamMemory, jpgStreamMemory + jpgStreamSize, imageBuf); + auto unlockJpgStreamMemory = wil::scope_exit([jpgStreamMemory] { GlobalUnlock(jpgStreamMemory); }); + reencodedSize = (DWORD)jpgStreamSize; + return true; +} + +wil::com_ptr_nothrow LoadAsRGB24BitmapWithSize(IWICImagingFactory* pWIC, + wil::com_ptr_nothrow image, + const UINT targetWidth, + const UINT targetHeight) +{ + wil::com_ptr_nothrow bitmap; + // Initialize image bitmap decoder from filename and get the image frame + wil::com_ptr_nothrow bitmapDecoder; + OK_OR_BAIL(pWIC->CreateDecoderFromStream(image.get(), nullptr, WICDecodeMetadataCacheOnLoad, &bitmapDecoder)); + + wil::com_ptr_nothrow decodedFrame; + OK_OR_BAIL(bitmapDecoder->GetFrame(0, &decodedFrame)); + + UINT imageWidth = 0, imageHeight = 0; + OK_OR_BAIL(decodedFrame->GetSize(&imageWidth, &imageHeight)); + + // Scale the image if required + if (targetWidth != imageWidth || targetHeight != imageHeight) + { + wil::com_ptr_nothrow scaler; + OK_OR_BAIL(pWIC->CreateBitmapScaler(&scaler)); + OK_OR_BAIL( + scaler->Initialize(decodedFrame.get(), targetWidth, targetHeight, WICBitmapInterpolationModeHighQualityCubic)); + bitmap.attach(scaler.detach()); + } + else + { + bitmap.attach(decodedFrame.detach()); + } + WICPixelFormatGUID pixelFormat{}; + OK_OR_BAIL(bitmap->GetPixelFormat(&pixelFormat)); + + const auto targetPixelFormat = GUID_WICPixelFormat24bppBGR; + if (pixelFormat != targetPixelFormat) + { + wil::com_ptr_nothrow convertedBitmap; + if (SUCCEEDED(WICConvertBitmapSource(targetPixelFormat, bitmap.get(), &convertedBitmap))) + { + return convertedBitmap; + } + } + + return bitmap; +} + +wil::com_ptr_nothrow EncodeBitmapToContainer(IWICImagingFactory* pWIC, + wil::com_ptr_nothrow bitmap, + const GUID& containerGUID, + const UINT width, + const UINT height, + const float quality) +{ + wil::com_ptr_nothrow encoder; + pWIC->CreateEncoder(containerGUID, nullptr, &encoder); + + if (!encoder) + { + return nullptr; + } + + // Prepare the encoder output memory stream and encoding params + wil::com_ptr_nothrow encodedBitmap; + OK_OR_BAIL(CreateStreamOnHGlobal(nullptr, true, &encodedBitmap)); + OK_OR_BAIL(encoder->Initialize(encodedBitmap.get(), WICBitmapEncoderNoCache)); + wil::com_ptr_nothrow encodedFrame; + + wil::com_ptr_nothrow encoderOptions; + OK_OR_BAIL(encoder->CreateNewFrame(&encodedFrame, &encoderOptions)); + + ULONG nProperties = 0; + OK_OR_BAIL(encoderOptions->CountProperties(&nProperties)); + for (ULONG propIdx = 0; propIdx < nProperties; ++propIdx) + { + PROPBAG2 propBag{}; + ULONG _; + OK_OR_BAIL(encoderOptions->GetPropertyInfo(propIdx, 1, &propBag, &_)); + if (propBag.pstrName == std::wstring_view{ L"ImageQuality" }) + { + wil::unique_variant variant; + variant.vt = VT_R4; + variant.fltVal = quality; + OK_OR_BAIL(encoderOptions->Write(1, &propBag, &variant)); + LOG("Successfully set jpg compression quality"); + // skip the rest of the properties + propIdx = nProperties; + } + CoTaskMemFree(propBag.pstrName); + } + + OK_OR_BAIL(encodedFrame->Initialize(encoderOptions.get())); + + WICPixelFormatGUID intermediateFormat = GUID_WICPixelFormat24bppRGB; + OK_OR_BAIL(encodedFrame->SetPixelFormat(&intermediateFormat)); + OK_OR_BAIL(encodedFrame->SetSize(width, height)); + + // Commit the image encoding + OK_OR_BAIL(encodedFrame->WriteSource(bitmap.get(), nullptr)); + OK_OR_BAIL(encodedFrame->Commit()); + OK_OR_BAIL(encoder->Commit()); + return encodedBitmap; +} + +IMFSample* ConvertIMFVideoSample(const MFT_REGISTER_TYPE_INFO& inputType, + IMFMediaType* outputMediaType, + const wil::com_ptr_nothrow& inputSample, + const UINT width, + const UINT height) +{ + IMFActivate** ppVDActivate = nullptr; + UINT32 count = 0; + + MFT_REGISTER_TYPE_INFO outputType = { MFMediaType_Video, {} }; + outputMediaType->GetGUID(MF_MT_SUBTYPE, &outputType.guidSubtype); + + const std::array transformerCategories = { + MFT_CATEGORY_VIDEO_PROCESSOR, MFT_CATEGORY_VIDEO_DECODER, MFT_CATEGORY_VIDEO_ENCODER + }; + + for (const auto& transformerCategory : transformerCategories) + { + OK_OR_BAIL(MFTEnumEx(transformerCategory, MFT_ENUM_FLAG_SYNCMFT, &inputType, &outputType, &ppVDActivate, &count)); + if (count != 0) + { + break; + } + } + + wil::com_ptr_nothrow videoTransformer; + + bool videoDecoderActivated = false; + for (UINT32 i = 0; i < count; ++i) + { + if (!videoDecoderActivated && !FAILED(ppVDActivate[i]->ActivateObject(IID_PPV_ARGS(&videoTransformer)))) + { + videoDecoderActivated = true; + } + ppVDActivate[i]->Release(); + } + + if (count) + { + CoTaskMemFree(ppVDActivate); + } + + if (!videoDecoderActivated) + { + LOG("No converter avialable for the selected format"); + return nullptr; + } + + auto shutdownVideoDecoder = wil::scope_exit([&videoTransformer] { MFShutdownObject(videoTransformer.get()); }); + // Set input/output types for the decoder + wil::com_ptr_nothrow intermediateFrameMediaType; + OK_OR_BAIL(MFCreateMediaType(&intermediateFrameMediaType)); + intermediateFrameMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + intermediateFrameMediaType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB24); + intermediateFrameMediaType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + intermediateFrameMediaType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE); + OK_OR_BAIL(MFSetAttributeSize(intermediateFrameMediaType.get(), MF_MT_FRAME_SIZE, width, height)); + OK_OR_BAIL(MFSetAttributeRatio(intermediateFrameMediaType.get(), MF_MT_PIXEL_ASPECT_RATIO, width, height)); + OK_OR_BAIL(videoTransformer->SetInputType(0, intermediateFrameMediaType.get(), 0)); + OK_OR_BAIL(videoTransformer->SetOutputType(0, outputMediaType, 0)); + + // Process the input sample + OK_OR_BAIL(videoTransformer->ProcessInput(0, inputSample.get(), 0)); + + // Check whether we need to allocate output sample and buffer ourselves + MFT_OUTPUT_STREAM_INFO outputStreamInfo{}; + OK_OR_BAIL(videoTransformer->GetOutputStreamInfo(0, &outputStreamInfo)); + const bool onlyProvidesSamples = outputStreamInfo.dwFlags & MFT_OUTPUT_STREAM_PROVIDES_SAMPLES; + const bool canProvideSamples = outputStreamInfo.dwFlags & MFT_OUTPUT_STREAM_CAN_PROVIDE_SAMPLES; + const bool mustAllocateSample = + (!onlyProvidesSamples && !canProvideSamples) || + (!onlyProvidesSamples && (outputStreamInfo.dwFlags & MFT_PROCESS_OUTPUT_DISCARD_WHEN_NO_BUFFER)); + + MFT_OUTPUT_DATA_BUFFER outputSamples{}; + IMFSample* outputSample = nullptr; + + // If so, do the allocation + if (mustAllocateSample) + { + OK_OR_BAIL(MFCreateSample(&outputSample)); + OK_OR_BAIL(outputSample->SetSampleDuration(333333)); + OK_OR_BAIL(outputSample->SetSampleTime(1)); + OK_OR_BAIL(outputSample->SetUINT32(MF_MT_VIDEO_ROTATION, MFVideoRotationFormat::MFVideoRotationFormat_0)); + IMFMediaBuffer* outputMediaBuffer = nullptr; + OK_OR_BAIL( + MFCreateAlignedMemoryBuffer(outputStreamInfo.cbSize, outputStreamInfo.cbAlignment - 1, &outputMediaBuffer)); + OK_OR_BAIL(outputMediaBuffer->SetCurrentLength(outputStreamInfo.cbSize)); + OK_OR_BAIL(outputSample->AddBuffer(outputMediaBuffer)); + outputSamples.pSample = outputSample; + } + + // Finally, produce the output sample + DWORD processStatus = 0; + if (failed(videoTransformer->ProcessOutput(0, 1, &outputSamples, &processStatus))) + { + LOG("Failed to convert image frame"); + } + if (outputSamples.pEvents) + { + outputSamples.pEvents->Release(); + } + + return outputSamples.pSample; +} + +wil::com_ptr_nothrow LoadImageAsSample(wil::com_ptr_nothrow imageStream, + IMFMediaType* sampleMediaType, + const float quality) noexcept +{ + UINT targetWidth = 0; + UINT targetHeight = 0; + OK_OR_BAIL(MFGetAttributeSize(sampleMediaType, MF_MT_FRAME_SIZE, &targetWidth, &targetHeight)); + MFT_REGISTER_TYPE_INFO outputType = { MFMediaType_Video, {} }; + OK_OR_BAIL(sampleMediaType->GetGUID(MF_MT_SUBTYPE, &outputType.guidSubtype)); + + IWICImagingFactory* pWIC = _GetWIC(); + if (!pWIC) + { + LOG("Failed to create IWICImagingFactory"); + return nullptr; + } + + if (!imageStream) + { + return nullptr; + } + + const auto srcImageBitmap = LoadAsRGB24BitmapWithSize(pWIC, imageStream, targetWidth, targetHeight); + if (!srcImageBitmap) + { + return nullptr; + } + + // First, let's create a sample containing RGB24 bitmap + IMFSample* outputSample = nullptr; + OK_OR_BAIL(MFCreateSample(&outputSample)); + OK_OR_BAIL(outputSample->SetUINT32(MF_MT_VIDEO_ROTATION, MFVideoRotationFormat::MFVideoRotationFormat_0)); + OK_OR_BAIL(outputSample->SetSampleDuration(333333)); + OK_OR_BAIL(outputSample->SetSampleTime(1)); + IMFMediaBuffer* outputMediaBuffer = nullptr; + const DWORD nPixelBytes = targetWidth * targetHeight * 3; + OK_OR_BAIL(MFCreateAlignedMemoryBuffer(nPixelBytes, MF_64_BYTE_ALIGNMENT, &outputMediaBuffer)); + + const UINT stride = 3 * targetWidth; + + DWORD max_length = 0, current_length = 0; + BYTE* sampleBufferMemory = nullptr; + OK_OR_BAIL(outputMediaBuffer->Lock(&sampleBufferMemory, &max_length, ¤t_length)); + OK_OR_BAIL(srcImageBitmap->CopyPixels(nullptr, stride, nPixelBytes, sampleBufferMemory)); + OK_OR_BAIL(outputMediaBuffer->Unlock()); + + OK_OR_BAIL(outputMediaBuffer->SetCurrentLength(nPixelBytes)); + OK_OR_BAIL(outputSample->AddBuffer(outputMediaBuffer)); + + if (outputType.guidSubtype == MFVideoFormat_RGB24) + { + return outputSample; + } + + // Special case for mjpg, since we need to use jpg container for it instead of supplying raw pixels + if (outputType.guidSubtype == MFVideoFormat_MJPG) + { + // Use an intermediate jpg container sample which will be transcoded to the target format + wil::com_ptr_nothrow jpgStream = + EncodeBitmapToContainer(pWIC, srcImageBitmap, GUID_ContainerFormatJpeg, targetWidth, targetHeight, quality); + + // Obtain stream size and lock its memory pointer + STATSTG intermediateStreamStat{}; + OK_OR_BAIL(jpgStream->Stat(&intermediateStreamStat, STATFLAG_NONAME)); + const ULONGLONG jpgStreamSize = intermediateStreamStat.cbSize.QuadPart; + HGLOBAL streamMemoryHandle{}; + OK_OR_BAIL(GetHGlobalFromStream(jpgStream.get(), &streamMemoryHandle)); + + auto jpgStreamMemory = static_cast(GlobalLock(streamMemoryHandle)); + auto unlockJpgStreamMemory = wil::scope_exit([jpgStreamMemory] { GlobalUnlock(jpgStreamMemory); }); + + // Create a sample from the input image buffer + wil::com_ptr_nothrow jpgSample; + OK_OR_BAIL(MFCreateSample(&jpgSample)); + OK_OR_BAIL(jpgSample->SetUINT32(MF_MT_VIDEO_ROTATION, MFVideoRotationFormat::MFVideoRotationFormat_0)); + IMFMediaBuffer* inputMediaBuffer = nullptr; + OK_OR_BAIL(MFCreateAlignedMemoryBuffer(static_cast(jpgStreamSize), MF_64_BYTE_ALIGNMENT, &inputMediaBuffer)); + BYTE* inputBuf = nullptr; + OK_OR_BAIL(inputMediaBuffer->Lock(&inputBuf, &max_length, ¤t_length)); + if (max_length < jpgStreamSize) + { + return nullptr; + } + + std::copy(jpgStreamMemory, jpgStreamMemory + jpgStreamSize, inputBuf); + unlockJpgStreamMemory.reset(); + OK_OR_BAIL(inputMediaBuffer->Unlock()); + OK_OR_BAIL(inputMediaBuffer->SetCurrentLength(static_cast(jpgStreamSize))); + OK_OR_BAIL(jpgSample->AddBuffer(inputMediaBuffer)); + + return jpgSample; + } + + // Now we are ready to convert it to the requested media type + MFT_REGISTER_TYPE_INFO intermediateType = { MFMediaType_Video, MFVideoFormat_RGB24 }; + + // But if no conversion is needed, just return the input sample + + return ConvertIMFVideoSample(intermediateType, sampleMediaType, outputSample, targetWidth, targetHeight); +} \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.cpp b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.cpp new file mode 100644 index 0000000000..ec40ea66c4 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.cpp @@ -0,0 +1,634 @@ +#include "Logging.h" +#include "VideoCaptureDevice.h" + +#include +#include + +struct VideoCaptureReceiverFilter : winrt::implements +{ + FILTER_STATE _state = State_Stopped; + IFilterGraph* _graph = nullptr; + wil::com_ptr_nothrow _videoReceiverPin; + + ULONG STDMETHODCALLTYPE GetMiscFlags() override { return AM_FILTER_MISC_FLAGS_IS_RENDERER; } + + HRESULT STDMETHODCALLTYPE GetClassID(CLSID*) override { return E_NOTIMPL; } + + HRESULT STDMETHODCALLTYPE Stop() override + { + _state = State_Stopped; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Pause() override + { + _state = State_Paused; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Run(REFERENCE_TIME) override + { + _state = State_Running; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetState(DWORD, FILTER_STATE* outState) override + { + *outState = _state; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetSyncSource(IReferenceClock** outRefClock) override + { + *outRefClock = nullptr; + return NOERROR; + } + + HRESULT STDMETHODCALLTYPE SetSyncSource(IReferenceClock*) override { return S_OK; } + + HRESULT STDMETHODCALLTYPE EnumPins(IEnumPins** ppEnum) override + { + auto enumerator = winrt::make_self>(); + enumerator->_objects.emplace_back(_videoReceiverPin); + *ppEnum = enumerator.detach(); + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE FindPin(LPCWSTR, IPin**) override { return E_NOTIMPL; } + + HRESULT STDMETHODCALLTYPE JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR) override + { + _graph = pGraph; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryFilterInfo(FILTER_INFO* pInfo) override + { + std::copy(std::begin(NAME), std::end(NAME), pInfo->achName); + if (_graph) + { + pInfo->pGraph = _graph; + _graph->AddRef(); + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryVendorInfo(LPWSTR* pVendorInfo) override + { + auto info = static_cast(CoTaskMemAlloc(sizeof(VENDOR))); + std::copy(std::begin(VENDOR), std::end(VENDOR), info); + *pVendorInfo = info; + return S_OK; + } + + virtual ~VideoCaptureReceiverFilter() = default; + + constexpr static inline wchar_t NAME[] = L"PowerToysVCMCaptureFilter"; + constexpr static inline wchar_t VENDOR[] = L"Microsoft Corporation"; +}; + +struct VideoCaptureReceiverPin : winrt::implements +{ + VideoCaptureReceiverFilter* _owningFilter = nullptr; + unique_media_type_ptr _expectedMediaType; + wil::com_ptr_nothrow _captureInputPin; + unique_media_type_ptr _inputCaptureMediaType; + std::atomic_bool _flushing = false; + VideoCaptureDevice::callback_t _frameCallback; + + wil::com_ptr_nothrow _allocator; + + VideoCaptureReceiverPin(unique_media_type_ptr mediaType, VideoCaptureReceiverFilter* filter) : + _expectedMediaType{ std::move(mediaType) }, _owningFilter{ filter } + { + } + + HRESULT STDMETHODCALLTYPE Connect(IPin*, const AM_MEDIA_TYPE* pmt) override + { + if (_owningFilter->_state == State_Running) + { + return VFW_E_NOT_STOPPED; + } + + if (_captureInputPin) + { + return VFW_E_ALREADY_CONNECTED; + } + + if (!pmt || pmt->majortype == GUID_NULL) + { + return S_OK; + } + + if (pmt->majortype != _expectedMediaType->majortype || pmt->subtype != _expectedMediaType->subtype) + { + return S_FALSE; + } + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ReceiveConnection(IPin* pConnector, const AM_MEDIA_TYPE* pmt) override + { + if (!pConnector || !pmt) + { + return E_POINTER; + } + + if (_captureInputPin) + { + return VFW_E_ALREADY_CONNECTED; + } + + if (_owningFilter->_state != State_Stopped) + { + return VFW_E_NOT_STOPPED; + } + + if (QueryAccept(pmt) != S_OK) + { + return VFW_E_TYPE_NOT_ACCEPTED; + } + + _captureInputPin = pConnector; + _inputCaptureMediaType = CopyMediaType(pmt); + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Disconnect() override + { + _allocator.reset(); + _captureInputPin.reset(); + _inputCaptureMediaType.reset(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ConnectedTo(IPin** pPin) override + { + if (!_captureInputPin) + { + return VFW_E_NOT_CONNECTED; + } + + _captureInputPin.copy_to(pPin); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ConnectionMediaType(AM_MEDIA_TYPE* pmt) override + { + if (!pmt) + { + return E_POINTER; + } + + if (!_inputCaptureMediaType) + { + return VFW_E_NOT_CONNECTED; + } + + *pmt = *CopyMediaType(_inputCaptureMediaType).release(); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryPinInfo(PIN_INFO* pInfo) override + { + if (!pInfo) + { + return E_POINTER; + } + + pInfo->pFilter = _owningFilter; + if (_owningFilter) + { + _owningFilter->AddRef(); + } + + pInfo->dir = PINDIR_INPUT; + std::copy(std::begin(NAME), std::end(NAME), pInfo->achName); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryDirection(PIN_DIRECTION* pPinDir) override + { + if (!pPinDir) + { + return E_POINTER; + } + + *pPinDir = PINDIR_INPUT; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryId(LPWSTR* lpId) override + { + if (!lpId) + { + return E_POINTER; + } + + *lpId = static_cast(CoTaskMemAlloc(sizeof(NAME))); + + std::copy(std::begin(NAME), std::end(NAME), *lpId); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryAccept(const AM_MEDIA_TYPE* pmt) override + { + if (!pmt) + { + return E_POINTER; + } + + if (pmt->majortype != _expectedMediaType->majortype || pmt->subtype != _expectedMediaType->subtype) + { + return S_FALSE; + } + + if (_captureInputPin) + { + _inputCaptureMediaType.reset(const_cast(pmt)); + } + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE EnumMediaTypes(IEnumMediaTypes** ppEnum) override + { + if (!ppEnum) + { + return E_POINTER; + } + + auto enumerator = winrt::make_self(); + enumerator->_objects.emplace_back(CopyMediaType(_expectedMediaType)); + *ppEnum = enumerator.detach(); + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE QueryInternalConnections(IPin**, ULONG*) override { return E_NOTIMPL; } + + HRESULT STDMETHODCALLTYPE EndOfStream() override { return S_OK; } + + HRESULT STDMETHODCALLTYPE BeginFlush() override + { + _flushing = true; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE EndFlush() override + { + _flushing = false; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE NewSegment(REFERENCE_TIME, REFERENCE_TIME, double) override { return S_OK; } + + HRESULT STDMETHODCALLTYPE GetAllocator(IMemAllocator** allocator) override + { + VERBOSE_LOG; + if (!_allocator) + { + return VFW_E_NO_ALLOCATOR; + } + + _allocator.copy_to(allocator); + return S_OK; + } + + HRESULT STDMETHODCALLTYPE NotifyAllocator(IMemAllocator* allocator, BOOL readOnly) override + { + VERBOSE_LOG; + LOG(readOnly ? "Allocator READONLY: true" : "Allocator READONLY: false"); + _allocator = allocator; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE GetAllocatorRequirements(ALLOCATOR_PROPERTIES*) override { return E_NOTIMPL; } + + HRESULT STDMETHODCALLTYPE Receive(IMediaSample* pSample) override + { + if (_flushing) + { + return S_FALSE; + } + + if (!pSample) + { + return E_POINTER; + } + + if (pSample && _frameCallback) + { + _frameCallback(pSample); + } + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ReceiveMultiple(IMediaSample** pSamples, long nSamples, long* nSamplesProcessed) override + { + if (!pSamples && nSamples) + { + return E_POINTER; + } + + if (_flushing) + { + return S_FALSE; + } + + for (long i = 0; i < nSamples; i++) + { + Receive(pSamples[i]); + } + + *nSamplesProcessed = nSamples; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE ReceiveCanBlock() override { return S_FALSE; } + + virtual ~VideoCaptureReceiverPin() = default; + + constexpr static inline wchar_t NAME[] = L"PowerToysVCMCapturePin"; +}; + +constexpr long MINIMAL_FPS_ALLOWED = 29; + +const char* GetMediaSubTypeString(const GUID& guid) +{ + if (guid == MEDIASUBTYPE_RGB24) + { + return "MEDIASUBTYPE_RGB24"; + } + + if (guid == MEDIASUBTYPE_YUY2) + { + return "MEDIASUBTYPE_YUY2"; + } + + if (guid == MEDIASUBTYPE_MJPG) + { + return "MEDIASUBTYPE_MJPG"; + } + + if (guid == MEDIASUBTYPE_NV12) + { + return "MEDIASUBTYPE_NV12"; + } + + return "MEDIASUBTYPE_UNKNOWN"; +} + +std::optional SelectBestMediaType(wil::com_ptr_nothrow& pin) +{ + VERBOSE_LOG; + wil::com_ptr_nothrow mediaTypeEnum; + if (pin->EnumMediaTypes(&mediaTypeEnum); !mediaTypeEnum) + { + return std::nullopt; + } + + ULONG _ = 0; + VideoStreamFormat bestFormat; + unique_media_type_ptr mt; + while (mediaTypeEnum->Next(1, wil::out_param(mt), &_) == S_OK) + { + if (mt->majortype != MEDIATYPE_Video) + { + continue; + } + + auto format = reinterpret_cast(mt->pbFormat); + if (!format || !format->AvgTimePerFrame) + { + LOG("VideoInfoHeader not found"); + continue; + } + + const auto formatAvgFPS = 10000000LL / format->AvgTimePerFrame; + if (format->AvgTimePerFrame > bestFormat.avgFrameTime || formatAvgFPS < MINIMAL_FPS_ALLOWED) + { + continue; + } + + if (format->bmiHeader.biWidth < bestFormat.width || format->bmiHeader.biHeight < bestFormat.height) + { + continue; + } + + if (mt->subtype != MEDIASUBTYPE_YUY2 && mt->subtype != MEDIASUBTYPE_MJPG && mt->subtype != MEDIASUBTYPE_RGB24) + { + OLECHAR* guidString; + StringFromCLSID(mt->subtype, &guidString); + LOG("Skipping mediatype due to unsupported subtype: "); + LOG(guidString); + ::CoTaskMemFree(guidString); + continue; + } + + bestFormat.avgFrameTime = format->AvgTimePerFrame; + bestFormat.width = format->bmiHeader.biWidth; + bestFormat.height = format->bmiHeader.biHeight; + bestFormat.mediaType = std::move(mt); + } + + if (!bestFormat.mediaType) + { + LOG(L"Couldn't select a suitable media format"); + return std::nullopt; + } + + char selectedFormat[512]{}; + sprintf_s(selectedFormat, "Selected media format: %s %ldx%ld %lld fps", GetMediaSubTypeString(bestFormat.mediaType->subtype), bestFormat.width, bestFormat.height, 10000000LL / bestFormat.avgFrameTime); + LOG(selectedFormat); + + return std::move(bestFormat); +} + +std::vector VideoCaptureDevice::ListAll() +{ + std::vector devices; + auto enumeratorFactory = wil::CoCreateInstanceNoThrow(CLSID_SystemDeviceEnum); + if (!enumeratorFactory) + { + LOG("Couldn't create devenum factory"); + return devices; + } + + wil::com_ptr_nothrow enumMoniker; + enumeratorFactory->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &enumMoniker, CDEF_DEVMON_PNP_DEVICE); + if (!enumMoniker) + { + LOG("Couldn't create class enumerator"); + return devices; + } + + ULONG _ = 0; + wil::com_ptr_nothrow moniker; + while (enumMoniker->Next(1, &moniker, &_) == S_OK) + { + LOG("Inspecting moniker"); + VideoCaptureDeviceInfo deviceInfo; + + wil::com_ptr_nothrow propertyData; + moniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, reinterpret_cast(&propertyData)); + if (!propertyData) + { + LOG("BindToStorage failed"); + continue; + } + + wil::unique_variant propVal; + propVal.vt = VT_BSTR; + + if (FAILED(propertyData->Read(L"FriendlyName", &propVal, nullptr))) + { + LOG("Couldn't obtain FriendlyName property"); + continue; + } + + deviceInfo.friendlyName = { propVal.bstrVal, SysStringLen(propVal.bstrVal) }; + LOG(deviceInfo.friendlyName); + + propVal.reset(); + propVal.vt = VT_BSTR; + + if (FAILED(propertyData->Read(L"DevicePath", &propVal, nullptr))) + { + LOG("Couldn't obtain DevicePath property"); + continue; + } + deviceInfo.devicePath = { propVal.bstrVal, SysStringLen(propVal.bstrVal) }; + + wil::com_ptr_nothrow filter; + moniker->BindToObject(nullptr, nullptr, IID_IBaseFilter, reinterpret_cast(&filter)); + if (!filter) + { + LOG("Couldn't BindToObject"); + continue; + } + + wil::com_ptr_nothrow pinsEnum; + if (FAILED(filter->EnumPins(&pinsEnum))) + { + LOG("BindToObject EnumPins"); + continue; + } + + wil::com_ptr_nothrow pin; + while (pinsEnum->Next(1, &pin, &_) == S_OK) + { + LOG("Inspecting pin"); + // Skip pins which do not belong to capture category + GUID category{}; + DWORD __; + if (auto props = pin.try_copy(); + !props || + FAILED(props->Get(AMPROPSETID_Pin, AMPROPERTY_PIN_CATEGORY, nullptr, 0, &category, sizeof(GUID), &__)) || + category != PIN_CATEGORY_CAPTURE) + { + continue; + } + + // Skip non-output pins + if (PIN_DIRECTION direction = {}; FAILED(pin->QueryDirection(&direction)) || direction != PINDIR_OUTPUT) + { + continue; + } + + LOG("Found a pin of suitable category and direction, selecting format"); + auto bestFormat = SelectBestMediaType(pin); + if (!bestFormat) + { + continue; + } + + deviceInfo.captureOutputPin = std::move(pin); + deviceInfo.bestFormat = std::move(bestFormat.value()); + deviceInfo.captureOutputFilter = std::move(filter); + devices.emplace_back(std::move(deviceInfo)); + } + } + + return devices; +} + +std::optional VideoCaptureDevice::Create(VideoCaptureDeviceInfo&& vdi, callback_t callback) +{ + VERBOSE_LOG; + VideoCaptureDevice result; + + result._graph = wil::CoCreateInstanceNoThrow(CLSID_FilterGraph); + result._builder = wil::CoCreateInstanceNoThrow(CLSID_CaptureGraphBuilder2); + if (!result._graph || !result._builder) + { + return std::nullopt; + } + + if (FAILED(result._builder->SetFiltergraph(result._graph.get()))) + { + return std::nullopt; + } + + result._control = result._graph.try_query(); + if (!result._control) + { + return std::nullopt; + } + + auto pinConfig = vdi.captureOutputPin.try_query(); + if (!pinConfig) + { + return std::nullopt; + } + + if (FAILED(pinConfig->SetFormat(vdi.bestFormat.mediaType.get()))) + { + return std::nullopt; + } + + auto captureInputFilter = winrt::make_self(); + auto receiverPin = winrt::make_self(std::move(vdi.bestFormat.mediaType), captureInputFilter.get()); + receiverPin->_frameCallback = std::move(callback); + captureInputFilter->_videoReceiverPin.attach(receiverPin.get()); + auto detachReceiverPin = wil::scope_exit([&receiverPin]() { receiverPin.detach(); }); + + if (FAILED(result._graph->AddFilter(captureInputFilter.get(), nullptr))) + { + return std::nullopt; + } + + if (FAILED(result._graph->AddFilter(vdi.captureOutputFilter.get(), nullptr))) + { + return std::nullopt; + } + + if (FAILED(result._graph->ConnectDirect(vdi.captureOutputPin.get(), captureInputFilter->_videoReceiverPin.get(), nullptr))) + { + return std::nullopt; + } + + result._allocator = receiverPin->_allocator; + return std::make_optional(std::move(result)); +} + +bool VideoCaptureDevice::StartCapture() +{ + VERBOSE_LOG; + return SUCCEEDED(_control->Run()); +} + +bool VideoCaptureDevice::StopCapture() +{ + VERBOSE_LOG; + return SUCCEEDED(_control->Stop()); +} + +VideoCaptureDevice::~VideoCaptureDevice() +{ + StopCapture(); +} diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.h b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.h new file mode 100644 index 0000000000..3af63691a6 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureDevice.h @@ -0,0 +1,62 @@ +#pragma once +#include +#define WIN32_LEAN_AND_MEAN + +#include +#include + +#include + +#include +#include +#include +#include + +#include "DirectShowUtils.h" + +struct VideoStreamFormat +{ + long width = 0; + long height = 0; + REFERENCE_TIME avgFrameTime = std::numeric_limits::max(); + unique_media_type_ptr mediaType; + + VideoStreamFormat() = default; + + VideoStreamFormat(const VideoStreamFormat&) = delete; + VideoStreamFormat& operator=(const VideoStreamFormat&) = delete; + + VideoStreamFormat(VideoStreamFormat&&) = default; + VideoStreamFormat& operator=(VideoStreamFormat&&) = default; +}; + +struct VideoCaptureDeviceInfo +{ + std::wstring friendlyName; + std::wstring devicePath; + wil::com_ptr_nothrow captureOutputPin; + wil::com_ptr_nothrow captureOutputFilter; + VideoStreamFormat bestFormat; +}; + +class VideoCaptureDevice final +{ +public: + wil::com_ptr_nothrow _allocator; + + using callback_t = std::function; + + static std::vector ListAll(); + static std::optional Create(VideoCaptureDeviceInfo&& vdi, callback_t callback); + + bool StartCapture(); + bool StopCapture(); + + ~VideoCaptureDevice(); + +private: + wil::com_ptr_nothrow _graph; + wil::com_ptr_nothrow _builder; + wil::com_ptr_nothrow _control; + callback_t _callback; +}; diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.cpp b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.cpp new file mode 100644 index 0000000000..bfd2c9e088 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.cpp @@ -0,0 +1,919 @@ +#include "VideoCaptureProxyFilter.h" + +#include "VideoCaptureDevice.h" +#include +#include +#include +#include + +constexpr static inline wchar_t FILTER_NAME[] = L"PowerToysVCMProxyFilter"; +constexpr static inline wchar_t PIN_NAME[] = L"PowerToysVCMProxyPIN"; +constexpr static inline wchar_t VENDOR[] = L"Microsoft Corporation"; + +namespace +{ + constexpr float initialJpgQuality = 0.5f; + constexpr std::array overlayColor = { 0, 0, 0 }; + // clang-format off + unsigned char bmpPixelData[58] = { + 0x42, 0x4D, 0x3A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0xC4, 0x0E, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, overlayColor[0], overlayColor[1], overlayColor[2], 0x00 + }; + // clang-format on +} + +wil::com_ptr_nothrow VideoCaptureProxyPin::FindAllocator() +{ + auto allocator = GetPinAllocator(_connectedInputPin); + if (!allocator && _owningFilter->_captureDevice) + { + allocator = _owningFilter->_captureDevice->_allocator; + } + + return allocator; +} + +wil::com_ptr_nothrow LoadImageAsSample(wil::com_ptr_nothrow imageStream, + IMFMediaType* sampleMediaType, + const float quality) noexcept; +bool ReencodeJPGImage(BYTE* imageBuf, const DWORD imageSize, DWORD& reencodedSize); + +HRESULT VideoCaptureProxyPin::Connect(IPin* pReceivePin, const AM_MEDIA_TYPE*) +{ + if (!pReceivePin) + { + LOG("VideoCaptureProxyPin::Connect FAILED pReceivePin"); + return E_POINTER; + } + + if (_owningFilter->_state == State_Running) + { + LOG("VideoCaptureProxyPin::Connect FAILED _owningFilter->_state"); + return VFW_E_NOT_STOPPED; + } + + if (_connectedInputPin) + { + LOG("VideoCaptureProxyPin::Connect FAILED _connectedInputPin"); + return VFW_E_ALREADY_CONNECTED; + } + + if (FAILED(pReceivePin->ReceiveConnection(this, _mediaFormat.get()))) + { + LOG("VideoCaptureProxyPin::Connect FAILED pReceivePin->ReceiveConnection"); + return E_POINTER; + } + + _connectedInputPin = pReceivePin; + + auto memInput = _connectedInputPin.try_query(); + if (!memInput) + { + LOG("VideoCaptureProxyPin::Connect FAILED _connectedInputPin.try_query"); + return VFW_E_NO_TRANSPORT; + } + + auto allocator = FindAllocator(); + if (allocator == nullptr) + { + LOG("VideoCaptureProxyPin::Connect FAILED FindAllocator"); + return VFW_E_NO_TRANSPORT; + } + + if (FAILED(memInput->NotifyAllocator(allocator.get(), false))) + { + LOG("VideoCaptureProxyPin::Connect FAILED memInput->NotifyAllocator"); + return VFW_E_NO_TRANSPORT; + } + + return S_OK; +} + +HRESULT VideoCaptureProxyPin::ReceiveConnection(IPin*, const AM_MEDIA_TYPE*) +{ + return S_OK; +} + +HRESULT VideoCaptureProxyPin::Disconnect(void) +{ + if (!_connectedInputPin) + { + LOG("VideoCaptureProxyPin::Disconnect FAILED _connectedInputPin"); + return S_FALSE; + } + + _connectedInputPin.reset(); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::ConnectedTo(IPin** pPin) +{ + if (!_connectedInputPin) + { + *pPin = nullptr; + return VFW_E_NOT_CONNECTED; + } + + _connectedInputPin.try_copy_to(pPin); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::ConnectionMediaType(AM_MEDIA_TYPE* pmt) +{ + if (!_connectedInputPin) + { + LOG("VideoCaptureProxyPin::ConnectionMediaType FAILED _connectedInputPin"); + return VFW_E_NOT_CONNECTED; + } + + *pmt = *CopyMediaType(_mediaFormat).release(); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::QueryPinInfo(PIN_INFO* pInfo) +{ + if (!pInfo) + { + LOG("VideoCaptureProxyPin::QueryPinInfo FAILED pInfo"); + return E_POINTER; + } + + pInfo->pFilter = _owningFilter; + if (_owningFilter) + { + _owningFilter->AddRef(); + } + + if (_mediaFormat->majortype == MEDIATYPE_Video) + { + std::copy(std::begin(PIN_NAME), std::end(PIN_NAME), pInfo->achName); + } + + pInfo->dir = PINDIR_OUTPUT; + return S_OK; +} + +HRESULT VideoCaptureProxyPin::QueryDirection(PIN_DIRECTION* pPinDir) +{ + if (!pPinDir) + { + LOG("VideoCaptureProxyPin::QueryDirection FAILED pPinDir"); + return E_POINTER; + } + + *pPinDir = PINDIR_OUTPUT; + return S_OK; +} + +HRESULT VideoCaptureProxyPin::QueryId(LPWSTR* Id) +{ + if (!Id) + { + LOG("VideoCaptureProxyPin::QueryId FAILED Id"); + return E_POINTER; + } + + *Id = static_cast(CoTaskMemAlloc(sizeof(PIN_NAME))); + std::copy(std::begin(PIN_NAME), std::end(PIN_NAME), *Id); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::QueryAccept(const AM_MEDIA_TYPE*) +{ + return S_OK; +} + +HRESULT VideoCaptureProxyPin::EnumMediaTypes(IEnumMediaTypes** ppEnum) +{ + if (!ppEnum) + { + LOG("VideoCaptureProxyPin::EnumMediaTypes FAILED ppEnum"); + return E_POINTER; + } + + auto enumerator = winrt::make_self(); + enumerator->_objects.emplace_back(CopyMediaType(_mediaFormat)); + *ppEnum = enumerator.detach(); + + return S_OK; +} + +HRESULT VideoCaptureProxyPin::QueryInternalConnections(IPin**, ULONG*) +{ + return E_NOTIMPL; +} + +HRESULT VideoCaptureProxyPin::EndOfStream(void) +{ + return S_OK; +} + +HRESULT VideoCaptureProxyPin::BeginFlush(void) +{ + _flushing = true; + return S_OK; +} + +HRESULT VideoCaptureProxyPin::EndFlush(void) +{ + _flushing = false; + return S_OK; +} + +HRESULT VideoCaptureProxyPin::NewSegment(REFERENCE_TIME, REFERENCE_TIME, double) +{ + return S_OK; +} + +HRESULT VideoCaptureProxyPin::SetFormat(AM_MEDIA_TYPE* pmt) +{ + if (pmt == nullptr) + { + return S_OK; + } + + _mediaFormat = CopyMediaType(pmt); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::GetFormat(AM_MEDIA_TYPE** ppmt) +{ + if (!ppmt) + { + LOG("VideoCaptureProxyPin::GetFormat FAILED ppmt"); + return E_POINTER; + } + + *ppmt = CopyMediaType(_mediaFormat).release(); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::GetNumberOfCapabilities(int* piCount, int* piSize) +{ + if (!piCount || !piSize) + { + LOG("VideoCaptureProxyPin::GetNumberOfCapabilities FAILED piCount || piSize"); + return E_POINTER; + } + + *piCount = 1; + *piSize = sizeof(VIDEO_STREAM_CONFIG_CAPS); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::GetStreamCaps(int iIndex, AM_MEDIA_TYPE** ppmt, BYTE* pSCC) +{ + if (!ppmt || !pSCC) + { + LOG("VideoCaptureProxyPin::GetStreamCaps FAILED ppmt || pSCC"); + return E_POINTER; + } + + if (iIndex != 0) + { + LOG("VideoCaptureProxyPin::GetStreamCaps FAILED iIndex"); + return S_FALSE; + } + + VIDEOINFOHEADER* vih = reinterpret_cast(_mediaFormat->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 = CopyMediaType(_mediaFormat).release(); + + const auto caps_begin = reinterpret_cast(&caps); + std::copy(caps_begin, caps_begin + sizeof(caps), pSCC); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::Set(REFGUID, DWORD, LPVOID, DWORD, LPVOID, DWORD) +{ + return E_NOTIMPL; +} + +HRESULT VideoCaptureProxyPin::Get( + REFGUID guidPropSet, + DWORD dwPropID, + LPVOID, + DWORD, + LPVOID pPropData, + DWORD cbPropData, + DWORD* pcbReturned) +{ + if (guidPropSet != AMPROPSETID_Pin) + { + LOG("VideoCaptureProxyPin::Get FAILED guidPropSet"); + return E_PROP_SET_UNSUPPORTED; + } + + if (dwPropID != AMPROPERTY_PIN_CATEGORY) + { + LOG("VideoCaptureProxyPin::Get FAILED dwPropID"); + return E_PROP_ID_UNSUPPORTED; + } + + if (!pPropData) + { + LOG("VideoCaptureProxyPin::Get FAILED pPropData || pcbReturned"); + return E_POINTER; + } + + if (pcbReturned) + { + *pcbReturned = sizeof(GUID); + } + + if (cbPropData < sizeof(GUID)) + { + LOG("VideoCaptureProxyPin::Get FAILED cbPropData"); + return E_UNEXPECTED; + } + + *(GUID*)pPropData = PIN_CATEGORY_CAPTURE; + + LOG("VideoCaptureProxyPin::Get SUCCESS"); + return S_OK; +} + +HRESULT VideoCaptureProxyPin::QuerySupported(REFGUID guidPropSet, DWORD dwPropID, DWORD* pTypeSupport) +{ + if (guidPropSet != AMPROPSETID_Pin) + { + LOG("VideoCaptureProxyPin::QuerySupported FAILED guidPropSet"); + return E_PROP_SET_UNSUPPORTED; + } + + if (dwPropID != AMPROPERTY_PIN_CATEGORY) + { + LOG("VideoCaptureProxyPin::QuerySupported FAILED dwPropID"); + return E_PROP_ID_UNSUPPORTED; + } + + if (pTypeSupport) + { + *pTypeSupport = KSPROPERTY_SUPPORT_GET; + } + + return S_OK; +} + +long GetImageSize(wil::com_ptr_nothrow& image) +{ + if (!image) + { + return 0; + } + + DWORD imageSize = 0; + wil::com_ptr_nothrow imageBuf; + + OK_OR_BAIL(image->GetBufferByIndex(0, &imageBuf)); + OK_OR_BAIL(imageBuf->GetCurrentLength(&imageSize)); + return imageSize; +} + +void ReencodeFrame(IMediaSample* frame) +{ + BYTE* frameData = nullptr; + frame->GetPointer(&frameData); + if (!frameData) + { + LOG("VideoCaptureProxyPin::ReencodeFrame FAILED frameData"); + return; + } + const DWORD frameSize = frame->GetSize(); + DWORD reencodedSize = 0; + if (!ReencodeJPGImage(frameData, frameSize, reencodedSize)) + { + LOG("VideoCaptureProxyPin::ReencodeJPGImage FAILED"); + return; + } + frame->SetActualDataLength(reencodedSize); +} + +bool OverwriteFrame(IMediaSample* frame, wil::com_ptr_nothrow& image) +{ + if (!image) + { + return false; + } + + BYTE* frameData = nullptr; + frame->GetPointer(&frameData); + if (!frameData) + { + LOG("VideoCaptureProxyPin::OverwriteFrame FAILED frameData"); + return false; + } + + wil::com_ptr_nothrow imageBuf; + const DWORD frameSize = frame->GetSize(); + + image->GetBufferByIndex(0, &imageBuf); + if (!imageBuf) + { + LOG("VideoCaptureProxyPin::OverwriteFrame FAILED imageBuf"); + return false; + } + + BYTE* imageData = nullptr; + DWORD _ = 0, imageSize = 0; + imageBuf->Lock(&imageData, &_, &imageSize); + if (!imageData) + { + LOG("VideoCaptureProxyPin::OverwriteFrame FAILED imageData"); + return false; + } + + if (imageSize > frameSize && failed(frame->SetActualDataLength(imageSize))) + { + char buf[512]{}; + sprintf_s(buf, "VideoCaptureProxyPin::OverwriteFrame FAILED overlay image size %lu is larger than frame size %lu", imageSize, frameSize); + LOG(buf); + imageBuf->Unlock(); + return false; + } + + std::copy(imageData, imageData + imageSize, frameData); + imageBuf->Unlock(); + frame->SetActualDataLength(imageSize); + + return true; +} + +//#define DEBUG_FRAME_DATA +//#define DEBUG_OVERWRITE_FRAME +//#define DEBUG_REENCODE_JPG_DATA + +#if defined(DEBUG_OVERWRITE_FRAME) +void DebugOverwriteFrame(IMediaSample* frame, std::string_view filepath) +{ + std::ifstream file{ filepath.data(), std::ios::binary }; + std::streampos fileSize = 0; + fileSize = file.tellg(); + file.seekg(0, std::ios::end); + fileSize = file.tellg() - fileSize; + + BYTE* frameData = nullptr; + if (!frame) + { + LOG("null frame provided"); + return; + } + frame->GetPointer(&frameData); + const DWORD frameSize = frame->GetSize(); + + if (fileSize > frameSize || !frameData) + { + LOG("frame can't be filled with data"); + return; + } + file.read((char*)frameData, fileSize); + frame->SetActualDataLength((long)fileSize); + LOG("DebugOverwriteFrame success"); +} + +#endif + +#if defined(DEBUG_FRAME_DATA) +#include + +namespace fs = std::filesystem; + +void DumpSample(IMediaSample* frame, const std::string_view filename) +{ + BYTE* data = nullptr; + frame->GetPointer(&data); + if (!data) + { + LOG("Couldn't get sample pointer"); + return; + } + const long nBytes = frame->GetActualDataLength(); + std::ofstream file{ fs::temp_directory_path() / filename, std::ios::binary }; + file.write((const char*)data, nBytes); +} +#endif + +VideoCaptureProxyFilter::VideoCaptureProxyFilter() : + _worker_thread{ + std::thread{ + [this]() { + using namespace std::chrono_literals; + const auto uninitializedSleepInterval = 15ms; + std::vector lowerJpgQualityModes = { 0.1f, 0.25f }; + while (!_shutdown_request) + { + std::unique_lock lock{ _worker_mutex }; + _worker_cv.wait(lock, [this] { return _pending_frame != nullptr || _shutdown_request; }); + + if (!_outPin || !_outPin->_connectedInputPin) + { + lock.unlock(); + std::this_thread::sleep_for(uninitializedSleepInterval); + continue; + } + + auto input = _outPin->_connectedInputPin.try_query(); + if (!input) + { + continue; + } + + IMediaSample* sample = _pending_frame; + if (!sample) + { + continue; + } +#if defined(DEBUG_FRAME_DATA) + static bool realFrameSaved = false; + if (!realFrameSaved) + { + DumpSample(sample, "PowerToysVCMRealFrame.binary"); + realFrameSaved = true; + } +#endif + auto newSettings = SyncCurrentSettings(); + if (newSettings.webcamDisabled) + { +#if !defined(DEBUG_OVERWRITE_FRAME) + bool overwritten = OverwriteFrame(_pending_frame, _overlayImage ? _overlayImage : _blankImage); + while (!overwritten && _overlayImage) + { + _overlayImage.reset(); + newSettings = SyncCurrentSettings(); + if (!lowerJpgQualityModes.empty() && newSettings.overlayImage) + { + const float quality = lowerJpgQualityModes.back(); + lowerJpgQualityModes.pop_back(); + char buf[512]{}; + sprintf_s(buf, "Reload overlay image with quality %f", quality); + LOG(buf); + _overlayImage = LoadImageAsSample(newSettings.overlayImage, _targetMediaType.get(), quality); + overwritten = OverwriteFrame(_pending_frame, _overlayImage); + } + else + { + LOG("Couldn't overwrite frame with image with all available quality modes."); + } + } +#if defined(DEBUG_FRAME_DATA) + static bool overlayFrameSaved = false; + if (!overlayFrameSaved && _overlayImage && overwritten) + { + DumpSample(sample, "PowerToysVCMOverlayImageFrame.binary"); + overlayFrameSaved = true; + } +#endif + if (!overwritten && !_overlayImage) + { + OverwriteFrame(_pending_frame, _blankImage); + } +#else + DebugOverwriteFrame(_pending_frame, "R:\\frame.data"); +#endif + } +#if defined(DEBUG_REENCODE_JPG_DATA) + else + { + GUID subtype{}; + _targetMediaType->GetGUID(MF_MT_SUBTYPE, &subtype); + if (subtype == MFVideoFormat_MJPG) + { + ReencodeFrame(_pending_frame); + } + } +#endif + + _pending_frame = nullptr; + input->Receive(sample); + sample->Release(); + } + } } + } +{ +} + +HRESULT VideoCaptureProxyFilter::Stop(void) +{ + if (_state != State_Stopped && _captureDevice) + { + _captureDevice->StopCapture(); + } + + _state = State_Stopped; + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::Pause(void) +{ + if (_state == State_Stopped) + { + std::unique_lock lock{ _worker_mutex }; + + if (!_outPin) + { + LOG("VideoCaptureProxyPin::Pause FAILED _outPin"); + return VFW_E_NO_TRANSPORT; + } + + auto allocator = _outPin->FindAllocator(); + if (!allocator) + { + LOG("VideoCaptureProxyPin::Pause FAILED allocator"); + return VFW_E_NO_TRANSPORT; + } + + allocator->Commit(); + } + + _state = State_Paused; + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::Run(REFERENCE_TIME) +{ + _state = State_Running; + if (_captureDevice) + { + _captureDevice->StartCapture(); + } + + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::GetState(DWORD, FILTER_STATE* State) +{ + *State = _state; + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::SetSyncSource(IReferenceClock* pClock) +{ + _clock = pClock; + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::GetSyncSource(IReferenceClock** pClock) +{ + if (!pClock) + { + return E_POINTER; + } + _clock.try_copy_to(pClock); + return S_OK; +} + +GUID MapDShowSubtypeToMFT(const GUID& dshowSubtype) +{ + if (dshowSubtype == MEDIASUBTYPE_YUY2) + { + return MFVideoFormat_YUY2; + } + else if (dshowSubtype == MEDIASUBTYPE_MJPG) + { + return MFVideoFormat_MJPG; + } + else if (dshowSubtype == MEDIASUBTYPE_RGB24) + { + return MFVideoFormat_RGB24; + } + else + { + LOG("MapDShowSubtypeToMFT: Unsupported media type format provided!"); + return MFVideoFormat_MJPG; + } +} + +HRESULT VideoCaptureProxyFilter::EnumPins(IEnumPins** ppEnum) +{ + if (!ppEnum) + { + LOG("VideoCaptureProxyFilter::EnumPins null arg provided"); + return E_POINTER; + } + + std::unique_lock lock{ _worker_mutex }; + + // We cannot initialize capture device and outpin during VideoCaptureProxyFilter ctor + // since that results in a deadlock -> initializing now. + if (!_outPin) + { + LOG("VideoCaptureProxyFilter::EnumPins started pin initialization"); + const auto newSettings = SyncCurrentSettings(); + std::vector webcams; + webcams = VideoCaptureDevice::ListAll(); + if (webcams.empty()) + { + LOG("VideoCaptureProxyFilter::EnumPins no physical webcams found"); + return E_FAIL; + } + + std::optional selectedCamIdx; + for (size_t i = 0; i < size(webcams); ++i) + { + if (newSettings.newCameraName == webcams[i].friendlyName) + { + selectedCamIdx = i; + LOG("VideoCaptureProxyFilter::EnumPins webcam selected using settings"); + break; + } + } + + if (!selectedCamIdx) + { + for (size_t i = 0; i < size(webcams); ++i) + { + if (newSettings.newCameraName != CAMERA_NAME) + { + LOG("VideoCaptureProxyFilter::EnumPins webcam selected using first fit"); + selectedCamIdx = i; + break; + } + } + } + + if (!selectedCamIdx) + { + LOG("VideoCaptureProxyFilter::EnumPins FAILED webcam couldn't be selected"); + return E_FAIL; + } + + auto& webcam = webcams[*selectedCamIdx]; + auto pin = winrt::make_self(); + pin->_mediaFormat = CopyMediaType(webcam.bestFormat.mediaType); + pin->_owningFilter = this; + _outPin.attach(pin.detach()); + + auto frameCallback = [this](IMediaSample* sample) { + std::unique_lock lock{ _worker_mutex }; + sample->AddRef(); + _pending_frame = sample; + _worker_cv.notify_one(); + }; + + _targetMediaType.reset(); + MFCreateMediaType(&_targetMediaType); + _targetMediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video); + _targetMediaType->SetGUID(MF_MT_SUBTYPE, MapDShowSubtypeToMFT(webcam.bestFormat.mediaType->subtype)); + _targetMediaType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive); + _targetMediaType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE); + MFSetAttributeSize( + _targetMediaType.get(), MF_MT_FRAME_SIZE, webcam.bestFormat.width, webcam.bestFormat.height); + MFSetAttributeRatio(_targetMediaType.get(), MF_MT_PIXEL_ASPECT_RATIO, 1, 1); + + _captureDevice = VideoCaptureDevice::Create(std::move(webcam), std::move(frameCallback)); + if (_captureDevice) + { + if (!_blankImage) + { + wil::com_ptr_nothrow blackBMPImage = SHCreateMemStream(bmpPixelData, sizeof(bmpPixelData)); + _blankImage = LoadImageAsSample(blackBMPImage, _targetMediaType.get(), initialJpgQuality); + } + + _overlayImage = LoadImageAsSample(newSettings.overlayImage, _targetMediaType.get(), initialJpgQuality); + LOG("VideoCaptureProxyFilter::EnumPins capture device created successfully"); + } + else + { + LOG("VideoCaptureProxyFilter::EnumPins FAILED couldn't create capture device"); + } + } + + auto enumerator = winrt::make_self>(); + enumerator->_objects.emplace_back(_outPin); + *ppEnum = enumerator.detach(); + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::FindPin(LPCWSTR, IPin**) +{ + return E_NOTIMPL; +} + +HRESULT VideoCaptureProxyFilter::QueryFilterInfo(FILTER_INFO* pInfo) +{ + if (!pInfo) + { + LOG("VideoCaptureProxyPin::QueryFilterInfo FAILED pInfo"); + return E_POINTER; + } + + VERBOSE_LOG; + std::copy(std::begin(FILTER_NAME), std::end(FILTER_NAME), pInfo->achName); + + pInfo->pGraph = _graph; + if (_graph) + { + _graph->AddRef(); + } + + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR) +{ + _graph = pGraph; + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::QueryVendorInfo(LPWSTR* pVendorInfo) +{ + auto info = static_cast(CoTaskMemAlloc(sizeof(VENDOR))); + std::copy(std::begin(VENDOR), std::end(VENDOR), info); + *pVendorInfo = info; + return S_OK; +} + +HRESULT VideoCaptureProxyFilter::GetClassID(CLSID*) +{ + return E_NOTIMPL; +} + +ULONG VideoCaptureProxyFilter::GetMiscFlags(void) +{ + return AM_FILTER_MISC_FLAGS_IS_SOURCE; +} + +VideoCaptureProxyFilter::~VideoCaptureProxyFilter() +{ + VERBOSE_LOG; + _shutdown_request = true; + + _worker_cv.notify_one(); + _worker_thread.join(); +} + +VideoCaptureProxyFilter::SyncedSettings VideoCaptureProxyFilter::SyncCurrentSettings() +{ + SyncedSettings result; + if (!_settingsUpdateChannel.has_value()) + { + _settingsUpdateChannel = SerializedSharedMemory::open(CameraSettingsUpdateChannel::endpoint(), sizeof(CameraSettingsUpdateChannel), false); + } + + if (!_settingsUpdateChannel) + { + return result; + } + + _settingsUpdateChannel->access([this, &result](auto settingsMemory) { + auto settings = reinterpret_cast(settingsMemory._data); + bool cameraNameUpdated = false; + result.webcamDisabled = settings->useOverlayImage; + + settings->cameraInUse = true; + + if (settings->sourceCameraName.has_value()) + { + std::wstring_view newCameraNameView{ settings->sourceCameraName->data() }; + if (!_currentSourceCameraName.has_value() || *_currentSourceCameraName != newCameraNameView) + { + cameraNameUpdated = true; + result.newCameraName = newCameraNameView; + } + } + + if (!settings->overlayImageSize.has_value()) + { + return; + } + + if (settings->newOverlayImagePosted || !_overlayImage) + { + auto imageChannel = + SerializedSharedMemory::open(CameraOverlayImageChannel::endpoint(), *settings->overlayImageSize, true); + if (!imageChannel) + { + return; + } + + imageChannel->access([this, settings, &result](auto imageMemory) { + result.overlayImage = SHCreateMemStream(imageMemory._data, static_cast(imageMemory._size)); + if (!result.overlayImage) + { + return; + } + + settings->newOverlayImagePosted = false; + }); + } + }); + return result; +} diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.h b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.h new file mode 100644 index 0000000000..58f49f032a --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/VideoCaptureProxyFilter.h @@ -0,0 +1,120 @@ +#pragma once + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include +#include +#include + +#include +#include + +#include "VideoCaptureDevice.h" + +#include +#include + +struct VideoCaptureProxyPin; +struct IMFSample; +struct IMFMediaType; + +inline const wchar_t CAMERA_NAME[] = L"PowerToys VideoConference Mute"; + +struct VideoCaptureProxyFilter : winrt::implements +{ + // BLOCK START: member accessed concurrently + wil::com_ptr_nothrow _outPin; + IMediaSample* _pending_frame = nullptr; + std::atomic_bool _shutdown_request = false; + std::optional _settingsUpdateChannel; + std::optional _currentSourceCameraName; + wil::com_ptr_nothrow _blankImage; + wil::com_ptr_nothrow _overlayImage; + wil::com_ptr_nothrow _targetMediaType; + // BLOCK END: member accessed concurrently + + std::mutex _worker_mutex; + std::condition_variable _worker_cv; + + FILTER_STATE _state = State_Stopped; + wil::com_ptr_nothrow _clock; + IFilterGraph* _graph = nullptr; + std::optional _captureDevice; + + std::thread _worker_thread; + + VideoCaptureProxyFilter(); + ~VideoCaptureProxyFilter(); + + struct SyncedSettings + { + bool webcamDisabled = false; + std::wstring newCameraName; + wil::com_ptr_nothrow overlayImage; + }; + + SyncedSettings SyncCurrentSettings(); + + HRESULT STDMETHODCALLTYPE Stop(void) override; + HRESULT STDMETHODCALLTYPE Pause(void) override; + HRESULT STDMETHODCALLTYPE Run(REFERENCE_TIME tStart) override; + HRESULT STDMETHODCALLTYPE GetState(DWORD dwMilliSecsTimeout, FILTER_STATE* State) override; + HRESULT STDMETHODCALLTYPE SetSyncSource(IReferenceClock* pClock) override; + HRESULT STDMETHODCALLTYPE GetSyncSource(IReferenceClock** pClock) override; + HRESULT STDMETHODCALLTYPE EnumPins(IEnumPins** ppEnum) override; + HRESULT STDMETHODCALLTYPE FindPin(LPCWSTR Id, IPin** ppPin) override; + HRESULT STDMETHODCALLTYPE QueryFilterInfo(FILTER_INFO* pInfo) override; + HRESULT STDMETHODCALLTYPE JoinFilterGraph(IFilterGraph* pGraph, LPCWSTR pName) override; + HRESULT STDMETHODCALLTYPE QueryVendorInfo(LPWSTR* pVendorInfo) override; + + HRESULT STDMETHODCALLTYPE GetClassID(CLSID* pClassID) override; + ULONG STDMETHODCALLTYPE GetMiscFlags(void) override; +}; +struct VideoCaptureProxyPin : winrt::implements +{ + VideoCaptureProxyFilter* _owningFilter = nullptr; + wil::com_ptr_nothrow _connectedInputPin; + unique_media_type_ptr _mediaFormat; + std::atomic_bool _flushing = false; + + HRESULT STDMETHODCALLTYPE Connect(IPin* pReceivePin, const AM_MEDIA_TYPE* pmt) override; + HRESULT STDMETHODCALLTYPE ReceiveConnection(IPin* pConnector, const AM_MEDIA_TYPE* pmt) override; + HRESULT STDMETHODCALLTYPE Disconnect(void) override; + HRESULT STDMETHODCALLTYPE ConnectedTo(IPin** pPin) override; + HRESULT STDMETHODCALLTYPE ConnectionMediaType(AM_MEDIA_TYPE* pmt) override; + HRESULT STDMETHODCALLTYPE QueryPinInfo(PIN_INFO* pInfo) override; + HRESULT STDMETHODCALLTYPE QueryDirection(PIN_DIRECTION* pPinDir) override; + HRESULT STDMETHODCALLTYPE QueryId(LPWSTR* Id) override; + HRESULT STDMETHODCALLTYPE QueryAccept(const AM_MEDIA_TYPE* pmt) override; + HRESULT STDMETHODCALLTYPE EnumMediaTypes(IEnumMediaTypes** ppEnum) override; + HRESULT STDMETHODCALLTYPE QueryInternalConnections(IPin** apPin, ULONG* nPin) override; + HRESULT STDMETHODCALLTYPE EndOfStream(void) override; + HRESULT STDMETHODCALLTYPE BeginFlush(void) override; + HRESULT STDMETHODCALLTYPE EndFlush(void) override; + HRESULT STDMETHODCALLTYPE NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, double dRate) override; + + HRESULT STDMETHODCALLTYPE SetFormat(AM_MEDIA_TYPE* pmt) override; + HRESULT STDMETHODCALLTYPE GetFormat(AM_MEDIA_TYPE** ppmt) override; + HRESULT STDMETHODCALLTYPE GetNumberOfCapabilities(int* piCount, int* piSize) override; + HRESULT STDMETHODCALLTYPE GetStreamCaps(int iIndex, AM_MEDIA_TYPE** ppmt, BYTE* pSCC) override; + HRESULT STDMETHODCALLTYPE Set(REFGUID guidPropSet, + DWORD dwPropID, + LPVOID pInstanceData, + DWORD cbInstanceData, + LPVOID pPropData, + DWORD cbPropData) override; + HRESULT STDMETHODCALLTYPE Get(REFGUID guidPropSet, + DWORD dwPropID, + LPVOID pInstanceData, + DWORD cbInstanceData, + LPVOID pPropData, + DWORD cbPropData, + DWORD* pcbReturned) override; + HRESULT STDMETHODCALLTYPE QuerySupported(REFGUID guidPropSet, DWORD dwPropID, DWORD* pTypeSupport) override; + + wil::com_ptr_nothrow FindAllocator(); +}; diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj b/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj new file mode 100644 index 0000000000..4bbb2c2282 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilter.vcxproj @@ -0,0 +1,124 @@ + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + 16.0 + {AC2857B4-103D-4D6D-9740-926EBF785042} + Win32Proj + VideoConferenceProxyFilter + VideoConferenceProxyFilter + true + 10.0.18362.0 + + + + DynamicLibrary + + + + $(SolutionDir)\src\;$(ProjectDir)..\..\..\ + Level4 + + + + + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + + + Console + true + + + true + + + + + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + $(SolutionDir)\src\; + + + Console + + + true + + + call "$(ProjectDir)build_vcm_x86.cmd" + + + + $(SolutionDir)$(Platform)\$(Configuration)\modules\VideoConference\ + VideoConferenceProxyFilter_x64 + + + ..\..\..\..\x86\$(Configuration)\modules\VideoConference\ + VideoConferenceProxyFilter_x86 + + + + + + + + + + ..\VideoConferenceShared\;%(AdditionalIncludeDirectories) + _LIB;NOMINMAX;%(PreprocessorDefinitions) + NotUsing + MultiThreaded + MultiThreadedDebug + + + $(OutDir)VideoConferenceShared.lib;mfplat.lib;Mfsensorgroup.lib;OneCoreUAP.lib;Mf.lib;Shlwapi.lib;Strmiids.lib;%(AdditionalDependencies); + module.def + + + + + + + + + + + + + + + + + + + + {459e0768-7ebd-4c41-bba1-6db3b3815e0a} + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilterx86.sln b/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilterx86.sln new file mode 100644 index 0000000000..e61fcf0bd6 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/VideoConferenceProxyFilterx86.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30907.101 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VideoConferenceProxyFilter", "VideoConferenceProxyFilter.vcxproj", "{AC2857B4-103D-4D6D-9740-926EBF785042}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VideoConferenceShared", "..\VideoConferenceShared\VideoConferenceShared.vcxproj", "{459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}" +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 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|Win32.ActiveCfg = Debug|Win32 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|Win32.Build.0 = Debug|Win32 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|x64.ActiveCfg = Debug|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Debug|x64.Build.0 = Debug|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|Win32.ActiveCfg = Release|Win32 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|Win32.Build.0 = Release|Win32 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|x64.ActiveCfg = Release|x64 + {AC2857B4-103D-4D6D-9740-926EBF785042}.Release|x64.Build.0 = Release|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|Win32.ActiveCfg = Debug|Win32 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|Win32.Build.0 = Debug|Win32 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|x64.ActiveCfg = Debug|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Debug|x64.Build.0 = Debug|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|Win32.ActiveCfg = Release|Win32 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|Win32.Build.0 = Release|Win32 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|x64.ActiveCfg = Release|x64 + {459E0768-7EBD-4C41-BBA1-6DB3B3815E0A}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0E41348C-22CB-45A4-8A16-8D7BEA070BB2} + EndGlobalSection +EndGlobal diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/build_vcm_x86.cmd b/src/modules/videoconference/VideoConferenceProxyFilter/build_vcm_x86.cmd new file mode 100644 index 0000000000..9b85078623 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/build_vcm_x86.cmd @@ -0,0 +1,3 @@ +msbuild VideoConferenceProxyFilterx86.sln -p:Configuration="Release" -p:Platform="Win32" + +exit 0 \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/dllmain.cpp b/src/modules/videoconference/VideoConferenceProxyFilter/dllmain.cpp new file mode 100644 index 0000000000..61133572c8 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/dllmain.cpp @@ -0,0 +1,242 @@ +#include "VideoCaptureProxyFilter.h" + +#include + +#include +#include + +namespace +{ +#if defined(_WIN64) + class __declspec(uuid("{31AD75E9-8C3A-49C8-B9ED-5880D6B4A764}")) GUID_DECL_POWERTOYS_VCM; +#elif defined(_WIN32) + class __declspec(uuid("{31AD75E9-8C3A-49C8-B9ED-5880D6B4A732}")) GUID_DECL_POWERTOYS_VCM; +#endif + const GUID CLSID_POWERTOYS_VCM = __uuidof(GUID_DECL_POWERTOYS_VCM); + + const REGPINTYPES MEDIA_TYPES = { &MEDIATYPE_Video, &MEDIASUBTYPE_MJPG }; + + const wchar_t FILTER_NAME[] = L"Output"; + const REGFILTERPINS PINS_REGISTRATION = { + (wchar_t*)FILTER_NAME, + false, + true, + false, + false, + &CLSID_NULL, + nullptr, + 1, + &MEDIA_TYPES + }; + + HINSTANCE DLLInstance{}; +} + +struct __declspec(uuid("9DCAF869-9C13-4BDF-BD0D-3592C5579DD6")) VideoCaptureProxyFilterFactory : winrt::implements +{ + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown*, REFIID riid, void** ppvObject) noexcept override + { + try + { + return winrt::make()->QueryInterface(riid, ppvObject); + } + catch (...) + { + return winrt::to_hresult(); + } + } + + HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) noexcept override + { + if (fLock) + { + ++winrt::get_module_lock(); + } + else + { + --winrt::get_module_lock(); + } + + return S_OK; + } +}; + +HRESULT STDMETHODCALLTYPE DllCanUnloadNow() +{ + if (winrt::get_module_lock()) + { + return S_FALSE; + } + + winrt::clear_factory_cache(); + return S_OK; +} + +HRESULT STDMETHODCALLTYPE DllGetClassObject(GUID const& clsid, GUID const& iid, void** result) +{ + if (!result) + { + return E_POINTER; + } + + if (iid != IID_IClassFactory && iid != IID_IUnknown) + { + return E_NOINTERFACE; + } + + if (clsid != CLSID_POWERTOYS_VCM) + { + return E_INVALIDARG; + } + + try + { + *result = nullptr; + + auto factory = winrt::make(); + factory->AddRef(); + *result = static_cast(factory.get()); + return S_OK; + } + catch (...) + { + return winrt::to_hresult(); + } +} + +std::wstring RegistryPath() +{ + std::wstring registryPath; + registryPath.resize(CHARS_IN_GUID, L'\0'); + + StringFromGUID2(CLSID_POWERTOYS_VCM, registryPath.data(), CHARS_IN_GUID); + registryPath.resize(registryPath.size() - 1); + registryPath = L"CLSID\\" + registryPath; + return registryPath; +} + +bool RegisterServer() +{ + std::wstring dllPath; + dllPath.resize(MAX_PATH, L'\0'); + if (auto length = GetModuleFileNameW(DLLInstance, dllPath.data(), MAX_PATH); length != 0) + { + dllPath.resize(length); + } + else + { + return false; + } + + wil::unique_hkey key; + wil::unique_hkey subkey; + const auto registryPath = RegistryPath(); + if (RegCreateKeyW(HKEY_CLASSES_ROOT, registryPath.c_str(), &key)) + { + return false; + } + + if (RegSetValueW(key.get(), nullptr, REG_SZ, CAMERA_NAME, sizeof(CAMERA_NAME))) + { + return false; + } + + if (RegCreateKeyW(key.get(), L"InprocServer32", &subkey)) + { + return false; + } + + if (RegSetValueW(subkey.get(), nullptr, REG_SZ, dllPath.c_str(), static_cast((dllPath.length() + 1) * sizeof(wchar_t)))) + { + return false; + } + const wchar_t THREADING_MODEL[] = L"Both"; + RegSetValueExW(subkey.get(), L"ThreadingModel", 0, REG_SZ, (const BYTE*)THREADING_MODEL, sizeof(THREADING_MODEL)); + + return true; +} + +bool UnregisterServer() +{ + const auto registryPath = RegistryPath(); + return !RegDeleteTreeW(HKEY_CLASSES_ROOT, registryPath.c_str()); +} + +bool RegisterFilter() +{ + auto filterMapper = wil::CoCreateInstanceNoThrow(CLSID_FilterMapper2); + if (!filterMapper) + { + return false; + } + + REGFILTER2 regFilter{ .dwVersion = 1, .dwMerit = MERIT_DO_NOT_USE, .cPins = 1, .rgPins = &PINS_REGISTRATION }; + + wil::com_ptr_nothrow moniker; + + return SUCCEEDED(filterMapper->RegisterFilter( + CLSID_POWERTOYS_VCM, CAMERA_NAME, &moniker, &CLSID_VideoInputDeviceCategory, nullptr, ®Filter)); +} + +bool UnregisterFilter() +{ + auto filterMapper = wil::CoCreateInstanceNoThrow(CLSID_FilterMapper2); + if (!filterMapper) + { + return false; + } + + return SUCCEEDED(filterMapper->UnregisterFilter(&CLSID_VideoInputDeviceCategory, nullptr, CLSID_POWERTOYS_VCM)); +} + +HRESULT STDMETHODCALLTYPE DllRegisterServer() +{ + if (!RegisterServer()) + { + UnregisterServer(); + return E_FAIL; + } + + auto COMContext = wil::CoInitializeEx(COINIT_APARTMENTTHREADED); + + if (!RegisterFilter()) + { + UnregisterFilter(); + UnregisterServer(); + return E_FAIL; + } + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE DllUnregisterServer() +{ + auto COMContext = wil::CoInitializeEx(COINIT_APARTMENTTHREADED); + + UnregisterFilter(); + UnregisterServer(); + + return S_OK; +} + +HRESULT STDMETHODCALLTYPE DllInstall(BOOL install, LPCWSTR) +{ + if (install) + { + return DllRegisterServer(); + } + else + { + return DllUnregisterServer(); + } +} + +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID) +{ + if (fdwReason == DLL_PROCESS_ATTACH) + { + DLLInstance = hinstDLL; + } + + return true; +} diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/module.def b/src/modules/videoconference/VideoConferenceProxyFilter/module.def new file mode 100644 index 0000000000..c4fcbcc661 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/module.def @@ -0,0 +1,7 @@ +EXPORTS + DllMain PRIVATE + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE + DllInstall PRIVATE diff --git a/src/modules/videoconference/VideoConferenceProxyFilter/packages.config b/src/modules/videoconference/VideoConferenceProxyFilter/packages.config new file mode 100644 index 0000000000..bac888e145 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceProxyFilter/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.cpp b/src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.cpp new file mode 100644 index 0000000000..534428531b --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.cpp @@ -0,0 +1,15 @@ +#include "CameraStateUpdateChannels.h" + +#include "naming.h" + +std::wstring_view CameraOverlayImageChannel::endpoint() +{ + static const std::wstring endpoint = ObtainStableGlobalNameForKernelObject(L"PowerToysVideoConferenceCameraOverlayImageChannelSharedMemory", true); + return endpoint; +} + +std::wstring_view CameraSettingsUpdateChannel::endpoint() +{ + static const std::wstring endpoint = ObtainStableGlobalNameForKernelObject(L"PowerToysVideoConferenceSettingsChannelSharedMemory", true); + return endpoint; +} diff --git a/src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.h b/src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.h new file mode 100644 index 0000000000..02ec542f7a --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/CameraStateUpdateChannels.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +struct alignas(16) CameraSettingsUpdateChannel +{ + bool useOverlayImage = false; + bool cameraInUse = false; + + std::optional overlayImageSize; + std::optional> sourceCameraName; + + bool newOverlayImagePosted = false; + + static std::wstring_view endpoint(); +}; + +namespace CameraOverlayImageChannel +{ + std::wstring_view endpoint(); +} diff --git a/src/modules/videoconference/VideoConferenceShared/Logging.cpp b/src/modules/videoconference/VideoConferenceShared/Logging.cpp new file mode 100644 index 0000000000..382233f974 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/Logging.cpp @@ -0,0 +1,203 @@ +#include "Logging.h" + +#include +#include +#include +#include +#include +#include + +#include + +#pragma warning(disable : 4127) + +static std::mutex logMutex; +constexpr inline size_t maxLogSizeMegabytes = 10; +constexpr inline bool alwaysLogVerbose = true; + +void LogToFile(std::wstring what, const bool verbose) +{ + std::error_code _; + const auto tempPath = std::filesystem::temp_directory_path(_); + if (verbose) + { + const bool verboseIndicatorFilePresent = std::filesystem::exists(tempPath / L"PowerToysVideoConferenceVerbose.flag", _); + if (!alwaysLogVerbose && !verboseIndicatorFilePresent) + { + return; + } + } + time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + std::tm tm; + localtime_s(&tm, &now); + char prefix[64]; + const auto pid = GetCurrentProcessId(); + const auto iter = prefix + sprintf_s(prefix, "[%ld]", pid); + std::strftime(iter, sizeof(prefix) - (prefix - iter), "[%d.%m %H:%M:%S] ", &tm); + + std::lock_guard lock{ logMutex }; + std::wstring logFilePath = tempPath; +#if defined(_WIN64) + logFilePath += L"\\PowerToysVideoConference_x64.log"; +#elif defined(_WIN32) + logFilePath += L"\\PowerToysVideoConference_x86.log"; +#endif + size_t logSizeMBs = 0; + try + { + logSizeMBs = static_cast(std::filesystem::file_size(logFilePath) >> 20); + } + catch (...) + { + } + if (logSizeMBs > maxLogSizeMegabytes) + { + std::error_code __; + // Truncate the log file to zero + std::filesystem::resize_file(logFilePath, 0, __); + } + std::wofstream myfile; + myfile.open(logFilePath, std::fstream::app); + + static const auto newLaunch = [&] { + myfile << prefix << "\n\n<<>"; + return 0; + }(); + + myfile << prefix << what << "\n"; + myfile.close(); +} + +void LogToFile(std::string what, const bool verbose) +{ + std::wstring native{ begin(what), end(what) }; + LogToFile(std::move(native), verbose); +} + +std::string toMediaTypeString(GUID subtype) +{ + if (subtype == MFVideoFormat_YUY2) + return "MFVideoFormat_YUY2"; + else if (subtype == MFVideoFormat_RGB32) + return "MFVideoFormat_RGB32"; + else if (subtype == MFVideoFormat_RGB24) + return "MFVideoFormat_RGB24"; + else if (subtype == MFVideoFormat_ARGB32) + return "MFVideoFormat_ARGB32"; + else if (subtype == MFVideoFormat_RGB555) + return "MFVideoFormat_RGB555"; + else if (subtype == MFVideoFormat_RGB565) + return "MFVideoFormat_RGB565"; + else if (subtype == MFVideoFormat_RGB8) + return "MFVideoFormat_RGB8"; + else if (subtype == MFVideoFormat_L8) + return "MFVideoFormat_L8"; + else if (subtype == MFVideoFormat_L16) + return "MFVideoFormat_L16"; + else if (subtype == MFVideoFormat_D16) + return "MFVideoFormat_D16"; + else if (subtype == MFVideoFormat_AYUV) + return "MFVideoFormat_AYUV"; + else if (subtype == MFVideoFormat_YUY2) + return "MFVideoFormat_YUY2"; + else if (subtype == MFVideoFormat_YVYU) + return "MFVideoFormat_YVYU"; + else if (subtype == MFVideoFormat_YVU9) + return "MFVideoFormat_YVU9"; + else if (subtype == MFVideoFormat_UYVY) + return "MFVideoFormat_UYVY"; + else if (subtype == MFVideoFormat_NV11) + return "MFVideoFormat_NV11"; + else if (subtype == MFVideoFormat_NV12) + return "MFVideoFormat_NV12"; + else if (subtype == MFVideoFormat_YV12) + return "MFVideoFormat_YV12"; + else if (subtype == MFVideoFormat_I420) + return "MFVideoFormat_I420"; + else if (subtype == MFVideoFormat_IYUV) + return "MFVideoFormat_IYUV"; + else if (subtype == MFVideoFormat_Y210) + return "MFVideoFormat_Y210"; + else if (subtype == MFVideoFormat_Y216) + return "MFVideoFormat_Y216"; + else if (subtype == MFVideoFormat_Y410) + return "MFVideoFormat_Y410"; + else if (subtype == MFVideoFormat_Y416) + return "MFVideoFormat_Y416"; + else if (subtype == MFVideoFormat_Y41P) + return "MFVideoFormat_Y41P"; + else if (subtype == MFVideoFormat_Y41T) + return "MFVideoFormat_Y41T"; + else if (subtype == MFVideoFormat_Y42T) + return "MFVideoFormat_Y42T"; + else if (subtype == MFVideoFormat_P210) + return "MFVideoFormat_P210"; + else if (subtype == MFVideoFormat_P216) + return "MFVideoFormat_P216"; + else if (subtype == MFVideoFormat_P010) + return "MFVideoFormat_P010"; + else if (subtype == MFVideoFormat_P016) + return "MFVideoFormat_P016"; + else if (subtype == MFVideoFormat_v210) + return "MFVideoFormat_v210"; + else if (subtype == MFVideoFormat_v216) + return "MFVideoFormat_v216"; + else if (subtype == MFVideoFormat_v410) + return "MFVideoFormat_v410"; + else if (subtype == MFVideoFormat_MP43) + return "MFVideoFormat_MP43"; + else if (subtype == MFVideoFormat_MP4S) + return "MFVideoFormat_MP4S"; + else if (subtype == MFVideoFormat_M4S2) + return "MFVideoFormat_M4S2"; + else if (subtype == MFVideoFormat_MP4V) + return "MFVideoFormat_MP4V"; + else if (subtype == MFVideoFormat_WMV1) + return "MFVideoFormat_WMV1"; + else if (subtype == MFVideoFormat_WMV2) + return "MFVideoFormat_WMV2"; + else if (subtype == MFVideoFormat_WMV3) + return "MFVideoFormat_WMV3"; + else if (subtype == MFVideoFormat_WVC1) + return "MFVideoFormat_WVC1"; + else if (subtype == MFVideoFormat_MSS1) + return "MFVideoFormat_MSS1"; + else if (subtype == MFVideoFormat_MSS2) + return "MFVideoFormat_MSS2"; + else if (subtype == MFVideoFormat_MPG1) + return "MFVideoFormat_MPG1"; + else if (subtype == MFVideoFormat_DVSL) + return "MFVideoFormat_DVSL"; + else if (subtype == MFVideoFormat_DVSD) + return "MFVideoFormat_DVSD"; + else if (subtype == MFVideoFormat_DVHD) + return "MFVideoFormat_DVHD"; + else if (subtype == MFVideoFormat_DV25) + return "MFVideoFormat_DV25"; + else if (subtype == MFVideoFormat_DV50) + return "MFVideoFormat_DV50"; + else if (subtype == MFVideoFormat_DVH1) + return "MFVideoFormat_DVH1"; + else if (subtype == MFVideoFormat_DVC) + return "MFVideoFormat_DVC"; + else if (subtype == MFVideoFormat_H264) + return "MFVideoFormat_H264"; + else if (subtype == MFVideoFormat_H265) + return "MFVideoFormat_H265"; + else if (subtype == MFVideoFormat_MJPG) + return "MFVideoFormat_MJPG"; + else if (subtype == MFVideoFormat_420O) + return "MFVideoFormat_420O"; + else if (subtype == MFVideoFormat_HEVC) + return "MFVideoFormat_HEVC"; + else if (subtype == MFVideoFormat_HEVC_ES) + return "MFVideoFormat_HEVC_ES"; + else if (subtype == MFVideoFormat_VP80) + return "MFVideoFormat_VP80"; + else if (subtype == MFVideoFormat_VP90) + return "MFVideoFormat_VP90"; + else if (subtype == MFVideoFormat_ORAW) + return "MFVideoFormat_ORAW"; + else + return "Other VideoFormat"; +} \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceShared/Logging.h b/src/modules/videoconference/VideoConferenceShared/Logging.h new file mode 100644 index 0000000000..633656f695 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/Logging.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +#include +#include + +void LogToFile(std::string what, const bool verbose = false); +void LogToFile(std::wstring what, const bool verbose = false); +std::string toMediaTypeString(GUID subtype); + +#define RETURN_IF_FAILED_WITH_LOGGING(val) \ + hr = (val); \ + if (FAILED(hr)) \ + { \ + LogToFile(std::string(__FUNCTION__ "() ") + #val + ": " + std::system_category().message(hr)); \ + return hr; \ + } + +#define RETURN_NULLPTR_IF_FAILED_WITH_LOGGING(val) \ + hr = val; \ + if (FAILED(hr)) \ + { \ + LogToFile(std::string(__FUNCTION__ "() ") + #val + ": " + std::system_category().message(hr)); \ + return nullptr; \ + } + +#define VERBOSE_LOG \ + std::string functionNameTMPVAR = __FUNCTION__; \ + LogToFile(std::string(functionNameTMPVAR + " enter"), true); \ + auto verboseLogOnScopeEnd = wil::scope_exit([&] { \ + LogToFile(std::string(functionNameTMPVAR + " exit"), true); \ + }); + +#if defined(PowerToysInterop) +#undef LOG +#define LOG(...) +#else +#define LOG(str) LogToFile(str, false); +#endif + +inline bool failed(HRESULT hr) +{ + return hr != S_OK; +} + +inline bool failed(bool val) +{ + return val == false; +} + +template +inline bool failed(wil::com_ptr_nothrow& ptr) +{ + return ptr == nullptr; +} + +#define OK_OR_BAIL(expr) \ + if (failed(expr)) \ + return {}; diff --git a/src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.cpp b/src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.cpp new file mode 100644 index 0000000000..ad277ce041 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.cpp @@ -0,0 +1,152 @@ +#include "MicrophoneDevice.h" + +#include "Logging.h" + +#include + +MicrophoneDevice::MicrophoneDevice(wil::com_ptr_nothrow device, wil::com_ptr_nothrow endpoint) : + _device{ std::move(device) }, + _endpoint{ std::move(endpoint) } +{ + if (!_device || !_endpoint) + { + throw std::logic_error("MicrophoneDevice was initialized with null objects"); + } + _device->GetId(&_id); + wil::com_ptr_nothrow props; + _device->OpenPropertyStore( + STGM_READ, &props); + if (props) + { + props->GetValue(PKEY_Device_FriendlyName, &_friendly_name); + } + else + { + LOG("MicrophoneDevice::MicrophoneDevice couldn't open property store"); + } +} + +MicrophoneDevice::~MicrophoneDevice() +{ + if (_notifier) + { + _endpoint->UnregisterControlChangeNotify(_notifier.get()); + } +} + +bool MicrophoneDevice::active() const noexcept +{ + DWORD state = 0; + _device->GetState(&state); + return state == DEVICE_STATE_ACTIVE; +} + +void MicrophoneDevice::set_muted(const bool muted) noexcept +{ + _endpoint->SetMute(muted, nullptr); +} + +bool MicrophoneDevice::muted() const noexcept +{ + BOOL muted = FALSE; + _endpoint->GetMute(&muted); + return muted; +} + +void MicrophoneDevice::toggle_muted() noexcept +{ + set_muted(!muted()); +} + +std::wstring_view MicrophoneDevice::id() const noexcept +{ + return _id ? _id.get() : FALLBACK_ID; +} + +std::wstring_view MicrophoneDevice::name() const noexcept +{ + return _friendly_name.pwszVal ? _friendly_name.pwszVal : FALLBACK_NAME; +} + +void MicrophoneDevice::set_mute_changed_callback(mute_changed_cb_t callback) noexcept +{ + _mute_changed_callback = std::move(callback); + _notifier = winrt::make(this); + + _endpoint->RegisterControlChangeNotify(_notifier.get()); +} + +std::optional MicrophoneDevice::getDefault() +{ + auto deviceEnumerator = wil::CoCreateInstanceNoThrow(); + if (!deviceEnumerator) + { + LOG("MicrophoneDevice::getDefault MMDeviceEnumerator returned null"); + return std::nullopt; + } + wil::com_ptr_nothrow captureDevice; + deviceEnumerator->GetDefaultAudioEndpoint(eCapture, eCommunications, &captureDevice); + if (!captureDevice) + { + LOG("MicrophoneDevice::getDefault captureDevice is null"); + return std::nullopt; + } + wil::com_ptr_nothrow microphoneEndpoint; + captureDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr, reinterpret_cast(µphoneEndpoint)); + if (!microphoneEndpoint) + { + LOG("MicrophoneDevice::getDefault captureDevice is null"); + return std::nullopt; + } + return std::make_optional(std::move(captureDevice), std::move(microphoneEndpoint)); +} + +std::vector MicrophoneDevice::getAllActive() +{ + std::vector microphoneDevices; + auto deviceEnumerator = wil::CoCreateInstanceNoThrow(); + if (!deviceEnumerator) + { + LOG("MicrophoneDevice::getAllActive MMDeviceEnumerator returned null"); + return microphoneDevices; + } + + wil::com_ptr_nothrow captureDevices; + deviceEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &captureDevices); + if (!captureDevices) + { + LOG("MicrophoneDevice::getAllActive EnumAudioEndpoints returned null"); + return microphoneDevices; + } + UINT nDevices = 0; + captureDevices->GetCount(&nDevices); + microphoneDevices.reserve(nDevices); + for (UINT i = 0; i < nDevices; ++i) + { + wil::com_ptr_nothrow device; + captureDevices->Item(i, &device); + if (!device) + { + continue; + } + wil::com_ptr_nothrow microphoneEndpoint; + device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr, reinterpret_cast(µphoneEndpoint)); + if (!microphoneEndpoint) + { + continue; + } + microphoneDevices.emplace_back(std::move(device), std::move(microphoneEndpoint)); + } + return microphoneDevices; +} + +MicrophoneDevice::VolumeNotifier::VolumeNotifier(MicrophoneDevice* subscribedDevice) : + _subscribedDevice{ subscribedDevice } +{ +} + +HRESULT __stdcall MicrophoneDevice::VolumeNotifier::OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA data) +{ + _subscribedDevice->_mute_changed_callback(data->bMuted); + return S_OK; +} diff --git a/src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.h b/src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.h new file mode 100644 index 0000000000..8bcd096a68 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/MicrophoneDevice.h @@ -0,0 +1,65 @@ +#pragma once +#define NOMINMAX + +#include +#include + +#include +#include + +#include +#include + + +#include + +#include +#include +#include + +#include +#include + +class MicrophoneDevice +{ +public: + using mute_changed_cb_t = std::function; + +private: + friend struct VolumeNotifier; + + struct VolumeNotifier : winrt::implements + { + MicrophoneDevice* _subscribedDevice = nullptr; + VolumeNotifier(MicrophoneDevice* subscribedDevice); + + virtual HRESULT __stdcall OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA data) override; + }; + + wil::unique_cotaskmem_string _id; + wil::unique_prop_variant _friendly_name; + mute_changed_cb_t _mute_changed_callback; + winrt::com_ptr _notifier; + wil::com_ptr_nothrow _endpoint; + wil::com_ptr_nothrow _device; + + constexpr static inline std::wstring_view FALLBACK_NAME = L"Unknown device"; + constexpr static inline std::wstring_view FALLBACK_ID = L"UNKNOWN_ID"; + +public: + MicrophoneDevice(MicrophoneDevice&&) noexcept = default; + MicrophoneDevice(wil::com_ptr_nothrow device, wil::com_ptr_nothrow endpoint); + ~MicrophoneDevice(); + + bool active() const noexcept; + void set_muted(const bool muted) noexcept; + bool muted() const noexcept; + void toggle_muted() noexcept; + + std::wstring_view id() const noexcept; + std::wstring_view name() const noexcept; + void set_mute_changed_callback(mute_changed_cb_t callback) noexcept; + + static std::optional getDefault(); + static std::vector getAllActive(); +}; diff --git a/src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.cpp b/src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.cpp new file mode 100644 index 0000000000..541db26057 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.cpp @@ -0,0 +1,188 @@ +#include "SerializedSharedMemory.h" + +inline char* SerializedSharedMemory::lock_flag_addr() noexcept +{ + return reinterpret_cast(_memory._data + _memory._size); +} + +inline void SerializedSharedMemory::lock() noexcept +{ + if (_read_only) + { + return; + } + while (LOCKED == _InterlockedCompareExchange8(lock_flag_addr(), LOCKED, !LOCKED)) + { + while (*lock_flag_addr() == LOCKED) + { + _mm_pause(); + } + } +} + +inline void SerializedSharedMemory::unlock() noexcept +{ + if (_read_only) + { + return; + } + _InterlockedExchange8(lock_flag_addr(), !LOCKED); +} + +SerializedSharedMemory::SerializedSharedMemory(std::array handles, + memory_t memory, + const bool readonly) noexcept + : + _handles{ std::move(handles) }, _memory{ std::move(memory) }, _read_only(readonly) +{ +} + +SerializedSharedMemory::~SerializedSharedMemory() noexcept +{ + if (_memory._data) + { + UnmapViewOfFile(_memory._data); + } +} + +SerializedSharedMemory::SerializedSharedMemory(SerializedSharedMemory&& rhs) noexcept +{ + *this = std::move(rhs); +} + +SerializedSharedMemory& SerializedSharedMemory::operator=(SerializedSharedMemory&& rhs) noexcept +{ + _handles = {}; + _handles.swap(rhs._handles); + _memory = std::move(rhs._memory); + rhs._memory = {}; + _read_only = rhs._read_only; + rhs._read_only = true; + + return *this; +} + +std::optional SerializedSharedMemory::create(const std::wstring_view object_name, + const size_t size, + const bool read_only, + SECURITY_ATTRIBUTES* maybe_attributes) noexcept +{ + SECURITY_DESCRIPTOR sd; + SECURITY_ATTRIBUTES sa = { sizeof SECURITY_ATTRIBUTES }; + if (!maybe_attributes) + { + sa.lpSecurityDescriptor = &sd; + sa.bInheritHandle = false; + if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION) || + !SetSecurityDescriptorDacl(&sd, true, nullptr, false)) + { + return std::nullopt; + } + } + + // We need an extra byte for locking if it's not readonly + const ULARGE_INTEGER UISize{ .QuadPart = size + !read_only }; + + wil::unique_handle hMapFile{ CreateFileMappingW(INVALID_HANDLE_VALUE, + maybe_attributes ? maybe_attributes : &sa, + read_only ? PAGE_READONLY : PAGE_READWRITE, + UISize.HighPart, + UISize.LowPart, + object_name.data()) }; + if (!hMapFile) + { + return std::nullopt; + } + auto shmem = static_cast( + MapViewOfFile(hMapFile.get(), read_only ? FILE_MAP_READ : FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, static_cast(UISize.QuadPart))); + if (!shmem) + { + return std::nullopt; + } + std::array handles = { std::move(hMapFile), {} }; + return SerializedSharedMemory{ std::move(handles), memory_t{ shmem, size }, read_only }; +} + +std::optional SerializedSharedMemory::open(const std::wstring_view object_name, + const size_t size, + const bool read_only) noexcept +{ + wil::unique_handle hMapFile{ OpenFileMappingW(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, object_name.data()) }; + if (!hMapFile) + { + return std::nullopt; + } + + auto shmem = static_cast( + MapViewOfFile(hMapFile.get(), read_only ? FILE_MAP_READ : FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, size + !read_only)); + + if (!shmem) + { + return std::nullopt; + } + std::array handles = { std::move(hMapFile), {} }; + return SerializedSharedMemory{ std::move(handles), memory_t{ shmem, size }, read_only }; +} + +std::optional SerializedSharedMemory::create_readonly( + const std::wstring_view object_name, + const std::wstring_view file_path, + SECURITY_ATTRIBUTES* maybe_attributes) noexcept +{ + SECURITY_DESCRIPTOR sd; + SECURITY_ATTRIBUTES sa = { sizeof SECURITY_ATTRIBUTES }; + if (!maybe_attributes) + { + sa.lpSecurityDescriptor = &sd; + sa.bInheritHandle = false; + if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION) || + !SetSecurityDescriptorDacl(&sd, true, nullptr, false)) + { + return std::nullopt; + } + } + wil::unique_handle hFile{ CreateFileW(file_path.data(), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + maybe_attributes ? maybe_attributes : &sa, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + nullptr) }; + + if (!hFile) + { + return std::nullopt; + } + + LARGE_INTEGER fileSize; + if (!GetFileSizeEx(hFile.get(), &fileSize)) + { + return std::nullopt; + } + wil::unique_handle hMapFile{ CreateFileMappingW(hFile.get(), + maybe_attributes ? maybe_attributes : &sa, + PAGE_READONLY, + fileSize.HighPart, + fileSize.LowPart, + object_name.data()) }; + if (!hMapFile) + { + return std::nullopt; + } + + auto shmem = static_cast(MapViewOfFile(nullptr, FILE_MAP_READ, 0, 0, static_cast(fileSize.QuadPart))); + if (shmem) + { + return std::nullopt; + } + std::array handles = { std::move(hMapFile), std::move(hFile) }; + + return SerializedSharedMemory{ std::move(handles), memory_t{ shmem, static_cast(fileSize.QuadPart) }, true }; +} + +void SerializedSharedMemory::access(std::function access_routine) noexcept +{ + lock(); + access_routine(_memory); + unlock(); +} \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.h b/src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.h new file mode 100644 index 0000000000..15f1052746 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/SerializedSharedMemory.h @@ -0,0 +1,54 @@ +#pragma once +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include +#include +#include +#include +#include +#include + +// Wrapper class allowing sharing readonly/writable memory with a serialized access via atomic locking. +// Note that it doesn't protect against a 3rd party concurrently modifying physical file contents. +class SerializedSharedMemory +{ +public: + struct memory_t + { + uint8_t * _data = nullptr; + size_t _size = 0; + }; + + static std::optional create(const std::wstring_view object_name, + const size_t size, + const bool read_only, + SECURITY_ATTRIBUTES* maybe_attributes = nullptr) noexcept; + static std::optional create_readonly( + const std::wstring_view object_name, + const std::wstring_view file_path, + SECURITY_ATTRIBUTES* maybe_attributes = nullptr) noexcept; + static std::optional open(const std::wstring_view object_name, + const size_t size, + const bool read_only) noexcept; + + void access(std::function access_routine) noexcept; + inline size_t size() const noexcept { return _memory._size; } + + ~SerializedSharedMemory() noexcept; + SerializedSharedMemory(SerializedSharedMemory&&) noexcept; + SerializedSharedMemory& operator=(SerializedSharedMemory&&) noexcept; + +private: + std::array _handles; + memory_t _memory; + bool _read_only = true; + constexpr static inline int64_t LOCKED = 1; + + char* lock_flag_addr() noexcept; + void lock() noexcept; + void unlock() noexcept; + + SerializedSharedMemory(std::array handles, memory_t memory, const bool readonly) noexcept; +}; \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.cpp b/src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.cpp new file mode 100644 index 0000000000..3245524aad --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.cpp @@ -0,0 +1,100 @@ +#include "VideoCaptureDeviceList.h" +#include "Logging.h" +#include +#include + +#include +#include + +void VideoCaptureDeviceList::Clear() +{ + for (UINT32 i = 0; i < m_numberDevices; i++) + { + CoTaskMemFree(m_deviceFriendlyNames[i]); + if (m_ppDevices[i]) + { + m_ppDevices[i]->Release(); + } + } + CoTaskMemFree(m_ppDevices); + m_ppDevices = nullptr; + if (m_deviceFriendlyNames) + { + delete[] m_deviceFriendlyNames; + } + + m_deviceFriendlyNames = nullptr; + m_numberDevices = 0; +} + +HRESULT VideoCaptureDeviceList::EnumerateDevices() +{ + HRESULT hr = S_OK; + wil::com_ptr pAttributes; + Clear(); + + // Initialize an attribute store. We will use this to + // specify the enumeration parameters. + + hr = MFCreateAttributes(&pAttributes, 1); + + // Ask for source type = video capture devices + if (SUCCEEDED(hr)) + { + hr = pAttributes->SetGUID( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + } + else + { + LOG("VideoCaptureDeviceList::EnumerateDevices(): Couldn't MFCreateAttributes"); + } + // Enumerate devices. + if (SUCCEEDED(hr)) + { + hr = MFEnumDeviceSources(pAttributes.get(), &m_ppDevices, &m_numberDevices); + } + else + { + LOG("VideoCaptureDeviceList::EnumerateDevices(): Couldn't SetGUID"); + } + + + if (FAILED(hr)) + { + LOG("VideoCaptureDeviceList::EnumerateDevices(): MFEnumDeviceSources failed"); + return hr; + } + + m_deviceFriendlyNames = new (std::nothrow) wchar_t*[m_numberDevices]; + for (UINT32 i = 0; i < m_numberDevices; i++) + { + UINT32 nameLength = 0; + m_ppDevices[i]->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &m_deviceFriendlyNames[i], &nameLength); + } + + return hr; +} + +HRESULT VideoCaptureDeviceList::GetDevice(UINT32 index, IMFActivate** ppActivate) +{ + if (index >= Count()) + { + return E_INVALIDARG; + } + + *ppActivate = m_ppDevices[index]; + (*ppActivate)->AddRef(); + + return S_OK; +} + +std::wstring_view VideoCaptureDeviceList::GetDeviceName(UINT32 index) +{ + if (index >= Count()) + { + return {}; + } + + return m_deviceFriendlyNames[index]; +} diff --git a/src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.h b/src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.h new file mode 100644 index 0000000000..eb0bf4afd4 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/VideoCaptureDeviceList.h @@ -0,0 +1,33 @@ +#pragma once + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include + +class VideoCaptureDeviceList +{ + UINT32 m_numberDevices; + // TODO: use wil + IMFActivate** m_ppDevices = nullptr; + wchar_t** m_deviceFriendlyNames = nullptr; + +public: + VideoCaptureDeviceList() : + m_ppDevices(NULL), m_numberDevices(0) + { + } + ~VideoCaptureDeviceList() + { + Clear(); + } + + UINT32 Count() const { return m_numberDevices; } + + void Clear(); + HRESULT EnumerateDevices(); + HRESULT GetDevice(UINT32 index, IMFActivate** ppActivate); + std::wstring_view GetDeviceName(UINT32 index); +}; diff --git a/src/modules/videoconference/VideoConferenceShared/VideoConferenceShared.vcxproj b/src/modules/videoconference/VideoConferenceShared/VideoConferenceShared.vcxproj new file mode 100644 index 0000000000..1b628c7e89 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/VideoConferenceShared.vcxproj @@ -0,0 +1,140 @@ + + + + + + Debug + Win32 + + + Debug + x64 + + + Release + Win32 + + + Release + x64 + + + + + mfplat.lib;Mfsensorgroup.lib;OneCoreUAP.lib;Mf.lib;Shlwapi.lib;Strmiids.lib;%(AdditionalDependencies); + + + + 16.0 + Win32Proj + {459e0768-7ebd-4c41-bba1-6db3b3815e0a} + VideoConferenceShared + true + 10.0.18362.0 + + + + StaticLibrary + true + v142 + Unicode + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\modules\VideoConference\ + $(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\ + + + ..\..\..\..\x86\$(Configuration)\modules\VideoConference\ + ..\..\..\..\x86\$(Configuration)\obj\$(ProjectName)\ + + + true + + + + NotUsing + + + + + Level4 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + MultiThreadedDebug + true + $(SolutionDir)\src\; + + + Console + true + + + true + + + + + Level4 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + MultiThreaded + true + $(SolutionDir)\src\; + ProgramDatabase + + + Console + true + true + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceShared/naming.cpp b/src/modules/videoconference/VideoConferenceShared/naming.cpp new file mode 100644 index 0000000000..b93088cd00 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/naming.cpp @@ -0,0 +1,19 @@ +#include "naming.h" + +#include "username.h" + +std::wstring ObtainStableGlobalNameForKernelObject(const std::wstring_view name, const bool restricted) +{ + static const std::optional username = ObtainActiveUserName(); + std::wstring result = L"Global\\"; + if (restricted) + { + result += L"Restricted\\"; + } + if (username) + { + result += *username; + } + result += name; + return result; +} diff --git a/src/modules/videoconference/VideoConferenceShared/naming.h b/src/modules/videoconference/VideoConferenceShared/naming.h new file mode 100644 index 0000000000..8d127b0623 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/naming.h @@ -0,0 +1,5 @@ +#pragma once +#include +#include + +std::wstring ObtainStableGlobalNameForKernelObject(const std::wstring_view name, const bool restricted); \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceShared/packages.config b/src/modules/videoconference/VideoConferenceShared/packages.config new file mode 100644 index 0000000000..eea1154c92 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/videoconference/VideoConferenceShared/username.cpp b/src/modules/videoconference/VideoConferenceShared/username.cpp new file mode 100644 index 0000000000..d8b99e8dd5 --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/username.cpp @@ -0,0 +1,20 @@ +#include "username.h" + +#include +#include + +std::optional ObtainActiveUserName() +{ + const DWORD sessionId = WTSGetActiveConsoleSessionId(); + WCHAR* pUserName; + DWORD _ = 0; + + if (!WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, sessionId, WTSUserName, &pUserName, &_)) + { + return std::nullopt; + } + WTSGetActiveConsoleSessionId(); + std::wstring result{ pUserName }; + WTSFreeMemory(pUserName); + return result; +} diff --git a/src/modules/videoconference/VideoConferenceShared/username.h b/src/modules/videoconference/VideoConferenceShared/username.h new file mode 100644 index 0000000000..dcbefec38b --- /dev/null +++ b/src/modules/videoconference/VideoConferenceShared/username.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + +std::optional ObtainActiveUserName(); + +std::wstring ObtainStableGlobalNameForKernelObject(const std::wstring_view name, const bool restricted); \ No newline at end of file diff --git a/src/modules/videoconference/make_cab.ddf b/src/modules/videoconference/make_cab.ddf new file mode 100644 index 0000000000..0c75024114 --- /dev/null +++ b/src/modules/videoconference/make_cab.ddf @@ -0,0 +1,20 @@ +; Disable default limits +.option EXPLICIT +.set CabinetFileCountThreshold=0 +.set FolderFileCountThreshold=0 +.set FolderSizeThreshold=0 +.set MaxCabinetSize=0 +.set MaxDiskFileCount=0 +.set MaxDiskSize=0 + +.set GenerateInf=ON +.set Compress=OFF +.set Cabinet=ON +.set CabinetNameTemplate=driver.cab +.set DestinationDir=cab_output +.set DiskDirectoryTemplate=driver + +VideoConferenceCustomMediaSource.dll +videoconferencevirtualdriver.cat +VideoConferenceVirtualDriver.dll +VideoConferenceVirtualDriver.inf \ No newline at end of file diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 2f1b07f29b..a1e9a4c5fd 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -43,6 +44,7 @@ #include #include #include +#include namespace { @@ -130,7 +132,7 @@ int runner(bool isProcessElevated, bool openSettings, bool openOobe) chdir_current_executable(); // Load Powertoys DLLs - const std::array knownModules = { + std::vector knownModules = { L"modules/FancyZones/FancyZonesModuleInterface.dll", L"modules/FileExplorerPreview/powerpreview.dll", L"modules/ImageResizer/ImageResizerExt.dll", @@ -140,6 +142,8 @@ int runner(bool isProcessElevated, bool openSettings, bool openOobe) L"modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.dll", L"modules/ColorPicker/ColorPicker.dll", L"modules/Awake/AwakeModuleInterface.dll", + // TODO(yuyoyuppe): uncomment when VCM should be enabled + //L"modules/VideoConference/VideoConferenceModule.dll" }; for (const auto& moduleSubdir : knownModules) @@ -264,6 +268,10 @@ toast_notification_handler_result toast_notification_handler(const std::wstring_ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { + Gdiplus::GdiplusStartupInput gpStartupInput; + ULONG_PTR gpToken; + GdiplusStartup(&gpToken, &gpStartupInput, NULL); + winrt::init_apartment(); const wchar_t* securityDescriptor = L"O:BA" // Owner: Builtin (local) administrator diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index 7c12539128..4311f3beab 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -38,7 +38,7 @@ AsInvoker $(OutDir)$(TargetName)$(TargetExt) - Shcore.lib;Msi.lib;WindowsApp.lib;taskschd.lib;Rstrtmgr.lib;Shlwapi.lib;%(AdditionalDependencies) + Shcore.lib;gdiplus.lib;Msi.lib;WindowsApp.lib;taskschd.lib;Rstrtmgr.lib;Shlwapi.lib;%(AdditionalDependencies) false diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs index 05039c6392..227ebb9488 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/EnabledModules.cs @@ -80,6 +80,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool videoConference = true; + + [JsonPropertyName("Video Conference")] + public bool VideoConference + { + get => this.videoConference; + set + { + if (this.videoConference != value) + { + LogTelemetryEvent(value); + this.videoConference = value; + } + } + } + private bool powerRename = true; public bool PowerRename diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj index e039aa704e..2b4e5447b9 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/Microsoft.PowerToys.Settings.UI.Library.csproj @@ -58,4 +58,11 @@ + + + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Windows.Forms.dll + + + diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndVideoConferenceSettings.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndVideoConferenceSettings.cs new file mode 100644 index 0000000000..46e7a420a0 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/SndVideoConferenceSettings.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class SndVideoConferenceSettings + { + [JsonPropertyName("Video Conference")] + public VideoConferenceSettings VideoConference { get; set; } + + public SndVideoConferenceSettings(VideoConferenceSettings settings) + { + VideoConference = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/StringProperty.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/StringProperty.cs index c138424afb..4e632f79b6 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/StringProperty.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/StringProperty.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Text.Json; using System.Text.Json.Serialization; @@ -29,5 +30,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library { return JsonSerializer.Serialize(this); } + + public static StringProperty ToStringProperty(string v) + { + return new StringProperty(v); + } + + public static implicit operator StringProperty(string v) + { + return new StringProperty(v); + } } } diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceConfigProperties.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceConfigProperties.cs new file mode 100644 index 0000000000..d72e49aa0b --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceConfigProperties.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class VideoConferenceConfigProperties + { + public VideoConferenceConfigProperties() + { + this.MuteCameraAndMicrophoneHotkey = new KeyboardKeysProperty( + new HotkeySettings() + { + Win = true, + Ctrl = false, + Alt = false, + Shift = false, + Key = "N", + Code = 78, + }); + + this.MuteMicrophoneHotkey = new KeyboardKeysProperty( + new HotkeySettings() + { + Win = true, + Ctrl = false, + Alt = false, + Shift = true, + Key = "A", + Code = 65, + }); + + this.MuteCameraHotkey = new KeyboardKeysProperty( + new HotkeySettings() + { + Win = true, + Ctrl = false, + Alt = false, + Shift = true, + Key = "O", + Code = 79, + }); + + this.HideToolbarWhenUnmuted = new BoolProperty(true); + } + + [JsonPropertyName("mute_camera_and_microphone_hotkey")] + public KeyboardKeysProperty MuteCameraAndMicrophoneHotkey { get; set; } + + [JsonPropertyName("mute_microphone_hotkey")] + public KeyboardKeysProperty MuteMicrophoneHotkey { get; set; } + + [JsonPropertyName("mute_camera_hotkey")] + public KeyboardKeysProperty MuteCameraHotkey { get; set; } + + [JsonPropertyName("selected_camera")] + public StringProperty SelectedCamera { get; set; } = string.Empty; + + [JsonPropertyName("selected_mic")] + public StringProperty SelectedMicrophone { get; set; } = string.Empty; + + [JsonPropertyName("toolbar_position")] + public StringProperty ToolbarPosition { get; set; } = "Top right corner"; + + [JsonPropertyName("toolbar_monitor")] + public StringProperty ToolbarMonitor { get; set; } = "Main monitor"; + + [JsonPropertyName("camera_overlay_image_path")] + public StringProperty CameraOverlayImagePath { get; set; } = string.Empty; + + [JsonPropertyName("theme")] + public StringProperty Theme { get; set; } + + [JsonPropertyName("hide_toolbar_when_unmuted")] + public BoolProperty HideToolbarWhenUnmuted { get; set; } + + // converts the current to a json string. + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettings.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettings.cs new file mode 100644 index 0000000000..b206dc7fb7 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettings.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class VideoConferenceSettings : BasePTModuleSettings, ISettingsConfig + { + public VideoConferenceSettings() + { + Version = "1"; + Name = "Video Conference"; + Properties = new VideoConferenceConfigProperties(); + } + + [JsonPropertyName("properties")] + public VideoConferenceConfigProperties Properties { get; set; } + + public string GetModuleName() + { + return Name; + } + + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettingsIPCMessage.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettingsIPCMessage.cs new file mode 100644 index 0000000000..1fdc7f489e --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/VideoConferenceSettingsIPCMessage.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class VideoConferenceSettingsIPCMessage + { + [JsonPropertyName("powertoys")] + public SndVideoConferenceSettings Powertoys { get; set; } + + public VideoConferenceSettingsIPCMessage() + { + } + + public VideoConferenceSettingsIPCMessage(SndVideoConferenceSettings settings) + { + this.Powertoys = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/VideoConferenceViewModel.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/VideoConferenceViewModel.cs new file mode 100644 index 0000000000..03494cc2f8 --- /dev/null +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/VideoConferenceViewModel.cs @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; + +namespace Microsoft.PowerToys.Settings.UI.ViewModels +{ + public class VideoConferenceViewModel : Observable + { + private readonly ISettingsUtils _settingsUtils; + + private VideoConferenceSettings Settings { get; set; } + + private GeneralSettings GeneralSettingsConfig { get; set; } + + private const string ModuleName = "Video Conference"; + + private Func SendConfigMSG { get; } + + private string _settingsConfigFileFolder = string.Empty; + + public VideoConferenceViewModel(ISettingsUtils settingsUtils, ISettingsRepository settingsRepository, Func ipcMSGCallBackFunc, string configFileSubfolder = "") + { + if (settingsRepository == null) + { + throw new ArgumentNullException(nameof(settingsRepository)); + } + + GeneralSettingsConfig = settingsRepository.SettingsConfig; + + _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); + + SendConfigMSG = ipcMSGCallBackFunc; + + _settingsConfigFileFolder = configFileSubfolder; + + try + { + Settings = _settingsUtils.GetSettings(GetSettingsSubPath()); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + Settings = new VideoConferenceSettings(); + _settingsUtils.SaveSettings(Settings.ToJsonString(), GetSettingsSubPath()); + } + + CameraNames = interop.CommonManaged.GetAllVideoCaptureDeviceNames(); + MicrophoneNames = interop.CommonManaged.GetAllActiveMicrophoneDeviceNames(); + MicrophoneNames.Insert(0, "[All]"); + + var shouldSaveSettings = false; + + if (string.IsNullOrEmpty(Settings.Properties.SelectedCamera.Value) && CameraNames.Count != 0) + { + _selectedCameraIndex = 0; + Settings.Properties.SelectedCamera.Value = CameraNames[0]; + shouldSaveSettings = true; + } + else + { + _selectedCameraIndex = CameraNames.FindIndex(name => name == Settings.Properties.SelectedCamera.Value); + } + + if (string.IsNullOrEmpty(Settings.Properties.SelectedMicrophone.Value)) + { + _selectedMicrophoneIndex = 0; + Settings.Properties.SelectedMicrophone.Value = MicrophoneNames[0]; + shouldSaveSettings = true; + } + else + { + _selectedMicrophoneIndex = MicrophoneNames.FindIndex(name => name == Settings.Properties.SelectedMicrophone.Value); + } + + _isEnabled = GeneralSettingsConfig.Enabled.VideoConference; + _cameraAndMicrophoneMuteHotkey = Settings.Properties.MuteCameraAndMicrophoneHotkey.Value; + _mirophoneMuteHotkey = Settings.Properties.MuteMicrophoneHotkey.Value; + _cameraMuteHotkey = Settings.Properties.MuteCameraHotkey.Value; + CameraImageOverlayPath = Settings.Properties.CameraOverlayImagePath.Value; + SelectOverlayImage = new ButtonClickCommand(SelectOverlayImageAction); + ClearOverlayImage = new ButtonClickCommand(ClearOverlayImageAction); + + _hideToolbarWhenUnmuted = Settings.Properties.HideToolbarWhenUnmuted.Value; + + switch (Settings.Properties.ToolbarPosition.Value) + { + case "Top left corner": + _toolbarPositionIndex = 0; + break; + case "Top center": + _toolbarPositionIndex = 1; + break; + case "Top right corner": + _toolbarPositionIndex = 2; + break; + case "Bottom left corner": + _toolbarPositionIndex = 3; + break; + case "Bottom center": + _toolbarPositionIndex = 4; + break; + case "Bottom right corner": + _toolbarPositionIndex = 5; + break; + } + + switch (Settings.Properties.ToolbarMonitor.Value) + { + case "Main monitor": + _toolbarMonitorIndex = 0; + break; + + case "All monitors": + _toolbarMonitorIndex = 1; + break; + } + + if (shouldSaveSettings) + { + _settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName); + } + } + + private bool _isEnabled; + private int _toolbarPositionIndex; + private int _toolbarMonitorIndex; + private HotkeySettings _cameraAndMicrophoneMuteHotkey; + private HotkeySettings _mirophoneMuteHotkey; + private HotkeySettings _cameraMuteHotkey; + private int _selectedCameraIndex = -1; + private int _selectedMicrophoneIndex; + private bool _hideToolbarWhenUnmuted; + + public List CameraNames { get; } + + public List MicrophoneNames { get; } + + public string CameraImageOverlayPath { get; set; } + + public ButtonClickCommand SelectOverlayImage { get; set; } + + public ButtonClickCommand ClearOverlayImage { get; set; } + + private void ClearOverlayImageAction() + { + CameraImageOverlayPath = string.Empty; + Settings.Properties.CameraOverlayImagePath = string.Empty; + RaisePropertyChanged(nameof(CameraImageOverlayPath)); + } + + private void SelectOverlayImageAction() + { + try + { + string pickedImage = null; + using (OpenFileDialog openFileDialog = new OpenFileDialog()) + { + openFileDialog.Filter = "Image Files (*.jpeg;*.jpg;*.png)|*.jpeg;*.jpg;*.png"; + openFileDialog.RestoreDirectory = true; + if (openFileDialog.ShowDialog() == DialogResult.OK) + { + pickedImage = openFileDialog.FileName; + } + } + + if (pickedImage != null) + { + CameraImageOverlayPath = pickedImage; + Settings.Properties.CameraOverlayImagePath = pickedImage; + RaisePropertyChanged(nameof(CameraImageOverlayPath)); + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch +#pragma warning restore CA1031 // Do not catch general exception types + { + } + } + + public int SelectedCameraIndex + { + get + { + return _selectedCameraIndex; + } + + set + { + if (_selectedCameraIndex != value) + { + _selectedCameraIndex = value; + if (_selectedCameraIndex >= 0 && _selectedCameraIndex < CameraNames.Count) + { + Settings.Properties.SelectedCamera.Value = CameraNames[_selectedCameraIndex]; + RaisePropertyChanged(); + } + } + } + } + + public int SelectedMicrophoneIndex + { + get + { + return _selectedMicrophoneIndex; + } + + set + { + if (_selectedMicrophoneIndex != value) + { + _selectedMicrophoneIndex = value; + if (_selectedMicrophoneIndex >= 0 && _selectedMicrophoneIndex < MicrophoneNames.Count) + { + Settings.Properties.SelectedMicrophone.Value = MicrophoneNames[_selectedMicrophoneIndex]; + RaisePropertyChanged(); + } + } + } + } + + public bool IsEnabled + { + get + { + return _isEnabled; + } + + set + { + if (value != _isEnabled) + { + _isEnabled = value; + GeneralSettingsConfig.Enabled.VideoConference = value; + OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig); + + SendConfigMSG(snd.ToString()); + OnPropertyChanged(nameof(IsEnabled)); + } + } + } + + public bool IsElevated + { + get + { + return GeneralSettingsConfig.IsElevated; + } + } + + public HotkeySettings CameraAndMicrophoneMuteHotkey + { + get + { + return _cameraAndMicrophoneMuteHotkey; + } + + set + { + if (value != _cameraAndMicrophoneMuteHotkey) + { + _cameraAndMicrophoneMuteHotkey = value; + Settings.Properties.MuteCameraAndMicrophoneHotkey.Value = value; + RaisePropertyChanged(nameof(CameraAndMicrophoneMuteHotkey)); + } + } + } + + public HotkeySettings MicrophoneMuteHotkey + { + get + { + return _mirophoneMuteHotkey; + } + + set + { + if (value != _mirophoneMuteHotkey) + { + _mirophoneMuteHotkey = value; + Settings.Properties.MuteMicrophoneHotkey.Value = value; + RaisePropertyChanged(nameof(MicrophoneMuteHotkey)); + } + } + } + + public HotkeySettings CameraMuteHotkey + { + get + { + return _cameraMuteHotkey; + } + + set + { + if (value != _cameraMuteHotkey) + { + _cameraMuteHotkey = value; + Settings.Properties.MuteCameraHotkey.Value = value; + RaisePropertyChanged(nameof(CameraMuteHotkey)); + } + } + } + + public int ToolbarPostionIndex + { + get + { + return _toolbarPositionIndex; + } + + set + { + if (_toolbarPositionIndex != value) + { + _toolbarPositionIndex = value; + switch (_toolbarPositionIndex) + { + case 0: + Settings.Properties.ToolbarPosition.Value = "Top left corner"; + break; + + case 1: + Settings.Properties.ToolbarPosition.Value = "Top center"; + break; + + case 2: + Settings.Properties.ToolbarPosition.Value = "Top right corner"; + break; + + case 3: + Settings.Properties.ToolbarPosition.Value = "Bottom left corner"; + break; + + case 4: + Settings.Properties.ToolbarPosition.Value = "Bottom center"; + break; + + case 5: + Settings.Properties.ToolbarPosition.Value = "Bottom right corner"; + break; + } + + RaisePropertyChanged(nameof(ToolbarPostionIndex)); + } + } + } + + public int ToolbarMonitorIndex + { + get + { + return _toolbarMonitorIndex; + } + + set + { + if (_toolbarMonitorIndex != value) + { + _toolbarMonitorIndex = value; + switch (_toolbarMonitorIndex) + { + case 0: + Settings.Properties.ToolbarMonitor.Value = "Main monitor"; + break; + + case 1: + Settings.Properties.ToolbarMonitor.Value = "All monitors"; + break; + } + + RaisePropertyChanged(nameof(ToolbarMonitorIndex)); + } + } + } + + public bool HideToolbarWhenUnmuted + { + get + { + return _hideToolbarWhenUnmuted; + } + + set + { + if (value != _hideToolbarWhenUnmuted) + { + _hideToolbarWhenUnmuted = value; + Settings.Properties.HideToolbarWhenUnmuted.Value = value; + RaisePropertyChanged(nameof(HideToolbarWhenUnmuted)); + } + } + } + + public string GetSettingsSubPath() + { + return _settingsConfigFileFolder + "\\" + ModuleName; + } + +#pragma warning disable CA1030 // Use events where appropriate + public void RaisePropertyChanged([CallerMemberName] string propertyName = null) +#pragma warning restore CA1030 // Use events where appropriate + { + OnPropertyChanged(propertyName); + SndVideoConferenceSettings outsettings = new SndVideoConferenceSettings(Settings); + SndModuleSettings ipcMessage = new SndModuleSettings(outsettings); + + SendConfigMSG(ipcMessage.ToJsonString()); + } + } + + [ComImport] + [Guid("3E68D4BD-7135-4D10-8018-9FB6D9F33FA1")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IInitializeWithWindow + { + void Initialize(IntPtr hwnd); + } +} diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/VideoConference.png b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Assets/Modules/VideoConference.png new file mode 100644 index 0000000000000000000000000000000000000000..538653baedb7f2ff9585c7aab7ca280b7f4e1f51 GIT binary patch literal 69380 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

w^1D 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 ShortcutGuidePage.xaml + + VideoConference.xaml + @@ -240,6 +243,7 @@ + @@ -428,6 +432,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw index a021a1bd6a..f107d8066a 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw @@ -117,6 +117,98 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Video Conference Mute + Navigation view item name for Video Conference + + + Enable Video Conference + + + Video Conference Mute is a quick and easy way to do an global "mute" of both your microphone and webcam. +Disabling this module or closing PowerToys will unmute the microphone and camera. + + + Mute camera and microphone + + + Mute microphone + + + Mute camera + + + Selected camera + + + Selected microphone + + + Camera overlay image + + + Toolbar position + + + Top center + + + Top left corner + + + Top right corner + + + Bottom left corner + + + Bottom center + + + Bottom right corner + + + Show toolbar on + + + Main monitor + + + Monitor under cursor + + + Active window monitor + + + All monitors + + + Hide toolbar when both camera and microphone are unmuted + + + About Video Conference + + + Camera + + + Microphone + + + Toolbar + + + Shortcuts + + + Camera overlay image preview + + + Browse + + + Clear + General Navigation view item name for General @@ -948,6 +1040,10 @@ https://aka.ms/PowerToysOverview_ShortcutGuide URL. Do not loc + + https://aka.ms/PowerToysOverview_VideoConference + URL. Do not loc + Win + Up/Down/Left/Right to move windows based on relative position @@ -1304,4 +1400,4 @@ From there, simply click on a Markdown file or SVG icon in the File Explorer and Download and install - \ No newline at end of file + diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/ViewModels/ShellViewModel.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI/ViewModels/ShellViewModel.cs index 2750930b7d..9ded11ab04 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/ViewModels/ShellViewModel.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/ViewModels/ShellViewModel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; @@ -36,6 +37,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels set { Set(ref isBackEnabled, value); } } + public bool IsVideoConferenceBuild + { + get + { + return this != null && File.Exists("modules/VideoConference/VideoConferenceModule.dll"); + } + } + public WinUI.NavigationViewItem Selected { get { return selected; } diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml index 0c8ab004ae..e79d2e8d91 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShellPage.xaml @@ -89,6 +89,14 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +