PowerToys/src/modules/videoconference/VideoConferenceModule/VideoConferenceModule.cpp
Andrey Nekrasov 4e23832d52
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 <niels.laute@live.nl>
2021-06-29 13:06:12 +03:00

569 lines
17 KiB
C++

#include "pch.h"
#include "VideoConferenceModule.h"
#include <WinUser.h>
#include <gdiplus.h>
#include <shellapi.h>
#include <filesystem>
#include <common/debug_control.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/elevation.h>
#include <common/utils/process_path.h>
#include <CameraStateUpdateChannels.h>
#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<CameraSettingsUpdateChannel*>(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<CameraSettingsUpdateChannel*>(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<CameraSettingsUpdateChannel*>(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<KBDLLHOOKSTRUCT*>(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<std::wstring_view>(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<fs::path, 2> 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<CameraSettingsUpdateChannel*>(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<uint32_t>(_imageOverlayChannel->size());
_settingsUpdateChannel->access([imageSize](auto memory) {
auto updatesChannel = reinterpret_cast<CameraSettingsUpdateChannel*>(memory._data);
updatesChannel->overlayImageSize.emplace(imageSize);
updatesChannel->newOverlayImagePosted = true;
});
}