/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "settings/sections/settings_calls.h" #include "api/api_authorizations.h" #include "apiwrap.h" #include "base/timer.h" #include "calls/calls_call.h" #include "calls/calls_instance.h" #include "calls/calls_video_bubble.h" #include "core/application.h" #include "core/core_settings.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "platform/platform_specific.h" #include "settings/sections/settings_main.h" #include "settings/settings_builder.h" #include "settings/settings_common_session.h" #include "tgcalls/VideoCaptureInterface.h" #include "ui/boxes/confirm_box.h" #include "ui/boxes/single_choice_box.h" #include "ui/effects/animations.h" #include "ui/vertical_list.h" #include "ui/widgets/buttons.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/continuous_sliders.h" #include "ui/widgets/labels.h" #include "ui/widgets/level_meter.h" #include "ui/wrap/slide_wrap.h" #include "ui/wrap/vertical_layout.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_create_adm.h" #include "webrtc/webrtc_environment.h" #include "webrtc/webrtc_video_track.h" #include "window/window_session_controller.h" #include "styles/style_layers.h" #include "styles/style_menu_icons.h" #include "styles/style_settings.h" namespace Settings { Webrtc::VideoTrack *AddCameraSubsection( std::shared_ptr show, not_null content, bool saveToSettings); namespace { using namespace Webrtc; using namespace Builder; [[nodiscard]] rpl::producer DeviceNameValue( DeviceType type, rpl::producer id) { return std::move(id) | rpl::map([type](const QString &id) { return Core::App().mediaDevices().devicesValue( type ) | rpl::map([id](const std::vector &list) { const auto i = ranges::find(list, id, &DeviceInfo::id); return (i != end(list) && !i->inactive) ? i->name : tr::lng_settings_call_device_default(tr::now); }); }) | rpl::flatten_latest(); } void InitPlaybackButton( not_null controller, not_null container, rpl::producer text, rpl::producer resolvedId, Fn set) { AddButtonWithLabel( container, tr::lng_settings_call_output_device(), PlaybackDeviceNameValue(rpl::duplicate(resolvedId)), st::settingsButtonNoIcon )->addClickHandler([=] { controller->show(ChoosePlaybackDeviceBox( rpl::duplicate(resolvedId), [=](const QString &id) { set(id); Core::App().saveSettingsDelayed(); })); }); } void InitCaptureButton( not_null controller, not_null container, rpl::producer text, rpl::producer resolvedId, Fn set, rpl::variable *testingMicrophone) { AddButtonWithLabel( container, tr::lng_settings_call_input_device(), CaptureDeviceNameValue(rpl::duplicate(resolvedId)), st::settingsButtonNoIcon )->addClickHandler([=] { controller->show(ChooseCaptureDeviceBox( rpl::duplicate(resolvedId), [=](const QString &id) { set(id); Core::App().saveSettingsDelayed(); })); }); struct LevelState { std::unique_ptr deviceId; std::unique_ptr tester; base::Timer timer; Ui::Animations::Simple animation; float level = 0.; }; const auto level = container->add( object_ptr( container, st::defaultLevelMeter), st::settingsLevelMeterPadding); const auto state = level->lifetime().make_state(); level->resize(QSize(0, st::defaultLevelMeter.height)); state->timer.setCallback([=] { const auto was = state->level; state->level = state->tester->getAndResetLevel(); state->animation.start([=] { level->setValue(state->animation.value(state->level)); }, was, state->level, kMicTestAnimationDuration); }); testingMicrophone->value() | rpl::on_next([=](bool testing) { if (testing) { state->deviceId = std::make_unique( &Core::App().mediaDevices(), Webrtc::DeviceType::Capture, rpl::duplicate(resolvedId)); state->tester = std::make_unique( state->deviceId->value()); state->timer.callEach(kMicTestUpdateInterval); } else { state->timer.cancel(); state->animation.stop(); state->tester = nullptr; state->deviceId = nullptr; } }, level->lifetime()); } void BuildOutputSection(SectionBuilder &builder) { const auto controller = builder.controller(); if (!controller) { return; } const auto settings = &Core::App().settings(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"calls/output"_q, .title = tr::lng_settings_call_section_output(), .keywords = { u"speakers"_q, u"output"_q, u"audio"_q }, }); builder.add([controller, settings](const WidgetContext &ctx) { InitPlaybackButton( controller, ctx.container, tr::lng_settings_call_output_device(), rpl::deferred([=] { return DeviceIdOrDefault(settings->playbackDeviceIdValue()); }), [=](const QString &id) { settings->setPlaybackDeviceId(id); }); return SectionBuilder::WidgetToAdd{}; }, [] { return SearchEntry{ .id = u"calls/output/device"_q, .title = tr::lng_settings_call_output_device(tr::now), .keywords = { u"speakers"_q, u"output"_q, u"playback"_q }, }; }); } void BuildInputSection( SectionBuilder &builder, rpl::variable *testingMicrophone) { const auto controller = builder.controller(); if (!controller) { return; } const auto settings = &Core::App().settings(); builder.addSkip(); builder.addDivider(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"calls/input"_q, .title = tr::lng_settings_call_section_input(), .keywords = { u"microphone"_q, u"input"_q, u"audio"_q }, }); builder.add([controller, settings, testingMicrophone](const WidgetContext &ctx) { InitCaptureButton( controller, ctx.container, tr::lng_settings_call_input_device(), rpl::deferred([=] { return DeviceIdOrDefault(settings->captureDeviceIdValue()); }), [=](const QString &id) { settings->setCaptureDeviceId(id); }, testingMicrophone); return SectionBuilder::WidgetToAdd{}; }, [] { return SearchEntry{ .id = u"calls/input/device"_q, .title = tr::lng_settings_call_input_device(tr::now), .keywords = { u"microphone"_q, u"input"_q, u"capture"_q }, }; }); } void BuildCallDevicesSection(SectionBuilder &builder) { const auto controller = builder.controller(); if (!controller) { return; } const auto settings = &Core::App().settings(); builder.addSkip(); builder.addDivider(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"calls/devices"_q, .title = tr::lng_settings_devices_calls(), .keywords = { u"calls"_q, u"devices"_q, u"same"_q }, }); const auto orDefault = [](const QString &value) { return value.isEmpty() ? kDefaultDeviceId : value; }; const auto same = builder.addButton({ .id = u"calls/same-devices"_q, .title = tr::lng_settings_devices_calls_same(), .st = &st::settingsButtonNoIcon, .toggled = rpl::combine( settings->callPlaybackDeviceIdValue(), settings->callCaptureDeviceIdValue() ) | rpl::map([](const QString &playback, const QString &capture) { return playback.isEmpty() && capture.isEmpty(); }), .keywords = { u"same"_q, u"separate"_q, u"devices"_q }, }); if (same) { same->toggledValue() | rpl::filter([=](bool toggled) { const auto empty = settings->callPlaybackDeviceId().isEmpty() && settings->callCaptureDeviceId().isEmpty(); return (empty != toggled); }) | rpl::on_next([=](bool toggled) { if (toggled) { settings->setCallPlaybackDeviceId(QString()); settings->setCallCaptureDeviceId(QString()); } else { settings->setCallPlaybackDeviceId( orDefault(settings->playbackDeviceId())); settings->setCallCaptureDeviceId( orDefault(settings->captureDeviceId())); } Core::App().saveSettingsDelayed(); }, same->lifetime()); } builder.add([controller, settings, same](const WidgetContext &ctx) { const auto container = ctx.container.get(); const auto different = container->add( object_ptr>( container, object_ptr(container))); const auto calls = different->entity(); InitPlaybackButton( controller, calls, tr::lng_group_call_speakers(), rpl::deferred([=] { return DeviceIdValueWithFallback( settings->callPlaybackDeviceIdValue(), settings->playbackDeviceIdValue()); }), [=](const QString &id) { settings->setCallPlaybackDeviceId(id); }); struct LevelStateStub { std::unique_ptr deviceId; std::unique_ptr tester; base::Timer timer; Ui::Animations::Simple animation; float level = 0.; }; const auto captureId = rpl::deferred([=] { return DeviceIdValueWithFallback( settings->callCaptureDeviceIdValue(), settings->captureDeviceIdValue()); }); AddButtonWithLabel( calls, tr::lng_group_call_microphone(), CaptureDeviceNameValue(rpl::duplicate(captureId)), st::settingsButtonNoIcon )->addClickHandler([=] { controller->show(ChooseCaptureDeviceBox( rpl::duplicate(captureId), [=](const QString &id) { settings->setCallCaptureDeviceId(id); Core::App().saveSettingsDelayed(); })); }); if (same) { different->toggleOn( same->toggledValue() | rpl::map(!rpl::mappers::_1)); } return SectionBuilder::WidgetToAdd{}; }, [] { return SearchEntry{ .id = u"calls/call-speakers"_q, .title = tr::lng_group_call_speakers(tr::now), .keywords = { u"speakers"_q, u"calls"_q }, }; }); builder.add(nullptr, [] { return SearchEntry{ .id = u"calls/call-microphone"_q, .title = tr::lng_group_call_microphone(tr::now), .keywords = { u"microphone"_q, u"calls"_q }, }; }); } void BuildCameraSection(SectionBuilder &builder) { const auto controller = builder.controller(); if (!controller) { return; } if (Core::App().mediaDevices().defaultId( Webrtc::DeviceType::Camera).isEmpty()) { return; } builder.addSkip(); builder.addDivider(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"calls/camera"_q, .title = tr::lng_settings_call_camera(), .keywords = { u"camera"_q, u"video"_q, u"webcam"_q }, }); builder.add([controller](const WidgetContext &ctx) { AddCameraSubsection(controller->uiShow(), ctx.container, true); return SectionBuilder::WidgetToAdd{}; }, [] { return SearchEntry{ .id = u"calls/camera/device"_q, .title = tr::lng_settings_call_input_device(tr::now), .keywords = { u"camera"_q, u"video"_q, u"webcam"_q }, }; }); } void BuildOtherSection(SectionBuilder &builder) { const auto controller = builder.controller(); const auto session = builder.session(); builder.addSkip(); builder.addDivider(); builder.addSkip(); builder.addSubsectionTitle({ .id = u"calls/other"_q, .title = tr::lng_settings_call_section_other(), .keywords = { u"calls"_q, u"accept"_q, u"system"_q }, }); const auto api = &session->api(); const auto authorizations = &api->authorizations(); authorizations->reload(); const auto acceptCalls = builder.addButton({ .id = u"calls/accept"_q, .title = tr::lng_settings_call_accept_calls(), .st = &st::settingsButtonNoIcon, .toggled = authorizations->callsDisabledHereValue() | rpl::map(!rpl::mappers::_1), .keywords = { u"accept"_q, u"receive"_q, u"incoming"_q }, .highlight = { .rippleShape = true }, }); if (acceptCalls) { acceptCalls->toggledChanges( ) | rpl::filter([=](bool value) { return (value == authorizations->callsDisabledHere()); }) | rpl::on_next([=](bool value) { authorizations->toggleCallsDisabledHere(!value); }, acceptCalls->lifetime()); } builder.addButton({ .id = u"calls/system-prefs"_q, .title = tr::lng_settings_call_open_system_prefs(), .st = &st::settingsButtonNoIcon, .onClick = [controller] { using namespace ::Platform; const auto opened = OpenSystemSettings(SystemSettingsType::Audio); if (!opened) { controller->show( Ui::MakeInformBox(tr::lng_linux_no_audio_prefs())); } }, .keywords = { u"system"_q, u"preferences"_q, u"audio"_q }, .highlight = { .rippleShape = true }, }); builder.addSkip(); } void BuildCallsSectionContent(SectionBuilder &builder) { auto *testingMicrophone = builder.container() ? builder.container()->lifetime().make_state>() : nullptr; BuildOutputSection(builder); BuildInputSection(builder, testingMicrophone); BuildCallDevicesSection(builder); BuildCameraSection(builder); BuildOtherSection(builder); } void ChooseMediaDeviceBox( not_null box, rpl::producer title, rpl::producer> devicesValue, rpl::producer currentId, Fn chosen, const style::Checkbox *st, const style::Radio *radioSt) { box->setTitle(std::move(title)); box->addButton(tr::lng_box_ok(), [=] { box->closeBox(); }); const auto layout = box->verticalLayout(); const auto skip = st::boxOptionListPadding.top() + st::defaultBoxCheckbox.margin.top(); layout->add(object_ptr(layout, skip)); if (!st) { st = &st::defaultBoxCheckbox; } if (!radioSt) { radioSt = &st::defaultRadio; } struct State { std::vector list; base::flat_map ids; rpl::variable currentId; QString currentName; bool ignoreValueChange = false; }; const auto state = box->lifetime().make_state(); state->currentId = std::move(currentId); const auto choose = [=](const QString &id) { const auto weak = base::make_weak(box); chosen(id); if (weak) { box->closeBox(); } }; const auto group = std::make_shared(); const auto fake = std::make_shared(0); const auto buttons = layout->add(object_ptr(layout)); const auto other = layout->add(object_ptr(layout)); const auto margins = QMargins( st::boxPadding.left() + st::boxOptionListPadding.left(), 0, st::boxPadding.right(), st::boxOptionListSkip); const auto def = buttons->add( object_ptr( buttons, group, 0, tr::lng_settings_call_device_default(tr::now), *st, *radioSt), margins); def->clicks( ) | rpl::filter([=] { return !group->value(); }) | rpl::on_next([=] { choose(kDefaultDeviceId); }, def->lifetime()); const auto showUnavailable = [=](QString text) { AddSkip(other); AddSubsectionTitle(other, tr::lng_settings_devices_inactive()); const auto &radio = *radioSt; const auto button = other->add( object_ptr(other, fake, 0, text, *st, radio), margins); button->show(); button->setDisabled(true); button->finishAnimating(); button->setAttribute(Qt::WA_TransparentForMouseEvents); while (other->count() > 3) { delete other->widgetAt(0); } if (const auto width = box->width()) { other->resizeToWidth(width); } }; const auto hideUnavailable = [=] { while (other->count() > 0) { delete other->widgetAt(0); } }; const auto selectCurrent = [=](QString current) { state->ignoreValueChange = true; const auto guard = gsl::finally([&] { state->ignoreValueChange = false; }); if (current.isEmpty() || current == kDefaultDeviceId) { group->setValue(0); hideUnavailable(); } else { auto found = false; for (const auto &[index, id] : state->ids) { if (id == current) { group->setValue(index); found = true; break; } } if (found) { hideUnavailable(); } else { group->setValue(0); const auto i = ranges::find( state->list, current, &DeviceInfo::id); if (i != end(state->list)) { showUnavailable(i->name); } else { hideUnavailable(); } } } }; std::move( devicesValue ) | rpl::on_next([=](std::vector &&list) { auto count = buttons->count(); auto index = 1; state->ids.clear(); state->list = std::move(list); state->ignoreValueChange = true; const auto guard = gsl::finally([&] { state->ignoreValueChange = false; }); const auto current = state->currentId.current(); for (const auto &info : state->list) { const auto id = info.id; if (info.inactive) { continue; } else if (current == id) { group->setValue(index); } const auto button = buttons->insert( index, object_ptr( buttons, group, index, info.name, *st, *radioSt), margins); button->show(); button->finishAnimating(); button->clicks( ) | rpl::filter([=] { return (group->current() == index); }) | rpl::on_next([=] { choose(id); }, button->lifetime()); state->ids.emplace(index, id); if (index < count) { delete buttons->widgetAt(index + 1); } ++index; } while (index < count) { delete buttons->widgetAt(index); --count; } if (const auto width = box->width()) { buttons->resizeToWidth(width); } selectCurrent(current); }, box->lifetime()); state->currentId.changes( ) | rpl::on_next(selectCurrent, box->lifetime()); def->finishAnimating(); group->setChangedCallback([=](int value) { if (state->ignoreValueChange) { return; } const auto i = state->ids.find(value); choose((i != end(state->ids)) ? i->second : kDefaultDeviceId); }); } class Calls : public Section { public: Calls(QWidget *parent, not_null controller); ~Calls(); [[nodiscard]] rpl::producer title() override; void sectionSaveChanges(FnMut done) override; private: void setupContent(); void requestPermissionAndStartTestingMicrophone(); rpl::variable _testingMicrophone; }; Calls::Calls( QWidget *parent, not_null controller) : Section(parent, controller) { controller->session().api().authorizations().reload(); setupContent(); requestPermissionAndStartTestingMicrophone(); } Calls::~Calls() = default; rpl::producer Calls::title() { return tr::lng_settings_section_devices(); } void Calls::sectionSaveChanges(FnMut done) { _testingMicrophone = false; done(); } void Calls::setupContent() { const auto content = Ui::CreateChild(this); const SectionBuildMethod buildMethod = []( not_null container, not_null controller, Fn showOther, rpl::producer<> showFinished) { const auto isPaused = Window::PausedIn( controller, Window::GifPauseReason::Layer); auto builder = SectionBuilder(WidgetContext{ .container = container, .controller = controller, .showOther = std::move(showOther), .isPaused = isPaused, }); BuildCallsSectionContent(builder); }; build(content, buildMethod); Ui::ResizeFitChild(this, content); } void Calls::requestPermissionAndStartTestingMicrophone() { using PermissionType = ::Platform::PermissionType; using PermissionStatus = ::Platform::PermissionStatus; const auto status = GetPermissionStatus( PermissionType::Microphone); if (status == PermissionStatus::Granted) { _testingMicrophone = true; } else if (status == PermissionStatus::CanRequest) { const auto startTestingChecked = crl::guard(this, [=]( PermissionStatus status) { if (status == PermissionStatus::Granted) { crl::on_main(crl::guard(this, [=] { _testingMicrophone = true; })); } }); RequestPermission( PermissionType::Microphone, startTestingChecked); } else { const auto showSystemSettings = [controller = controller()] { OpenSystemSettingsForPermission( PermissionType::Microphone); controller->hideLayer(); }; controller()->show(Ui::MakeConfirmBox({ .text = tr::lng_no_mic_permission(), .confirmed = showSystemSettings, .confirmText = tr::lng_menu_settings(), })); } } const auto kMeta = BuildHelper({ .id = Calls::Id(), .parentId = MainId(), .title = &tr::lng_settings_section_devices, .icon = &st::menuIconUnmute, }, [](SectionBuilder &builder) { BuildCallsSectionContent(builder); }); } // namespace Type CallsId() { return Calls::Id(); } Webrtc::VideoTrack *AddCameraSubsection( std::shared_ptr show, not_null content, bool saveToSettings) { auto &lifetime = content->lifetime(); const auto hasCall = (Core::App().calls().currentCall() != nullptr); auto capturerOwner = lifetime.make_state< std::shared_ptr >(); const auto track = lifetime.make_state( (hasCall ? VideoState::Inactive : VideoState::Active)); const auto deviceId = lifetime.make_state>( Core::App().settings().cameraDeviceId()); auto resolvedId = rpl::deferred([=] { return DeviceIdOrDefault(deviceId->value()); }); AddButtonWithLabel( content, tr::lng_settings_call_input_device(), CameraDeviceNameValue(rpl::duplicate(resolvedId)), st::settingsButtonNoIcon )->addClickHandler([=] { show->show(ChooseCameraDeviceBox( rpl::duplicate(resolvedId), [=](const QString &id) { *deviceId = id; if (saveToSettings) { Core::App().settings().setCameraDeviceId(id); Core::App().saveSettingsDelayed(); } if (*capturerOwner) { (*capturerOwner)->switchToDevice( id.toStdString(), false); } })); }); const auto bubbleWrap = content->add(object_ptr(content)); const auto bubble = lifetime.make_state<::Calls::VideoBubble>( bubbleWrap, track); const auto padding = st::settingsButtonNoIcon.padding.left(); const auto top = st::boxRoundShadow.extend.top(); const auto bottom = st::boxRoundShadow.extend.bottom(); auto frameSize = track->renderNextFrame( ) | rpl::map([=] { return track->frameSize(); }) | rpl::filter([=](QSize size) { return !size.isEmpty() && !Core::App().calls().currentCall() && !Core::App().calls().currentGroupCall(); }); auto bubbleWidth = bubbleWrap->widthValue( ) | rpl::filter([=](int width) { return width > 2 * padding + 1; }); rpl::combine( std::move(bubbleWidth), std::move(frameSize) ) | rpl::on_next([=](int width, QSize frame) { const auto useWidth = (width - 2 * padding); const auto useHeight = std::min( ((useWidth * frame.height()) / frame.width()), (useWidth * 480) / 640); bubbleWrap->resize(width, top + useHeight + bottom); bubble->updateGeometry( ::Calls::VideoBubble::DragMode::None, QRect(padding, top, useWidth, useHeight)); bubbleWrap->update(); }, bubbleWrap->lifetime()); using namespace rpl::mappers; const auto checkCapturer = [=] { if (*capturerOwner || Core::App().calls().currentCall() || Core::App().calls().currentGroupCall()) { return; } *capturerOwner = Core::App().calls().getVideoCapture( Core::App().settings().cameraDeviceId(), false); (*capturerOwner)->setPreferredAspectRatio(0.); track->setState(VideoState::Active); (*capturerOwner)->setState(tgcalls::VideoState::Active); (*capturerOwner)->setOutput(track->sink()); }; rpl::combine( Core::App().calls().currentCallValue(), Core::App().calls().currentGroupCallValue(), _1 || _2 ) | rpl::on_next([=](bool has) { if (has) { track->setState(VideoState::Inactive); bubbleWrap->resize(bubbleWrap->width(), 0); *capturerOwner = nullptr; } else { crl::on_main(content, checkCapturer); } }, lifetime); return track; } rpl::producer PlaybackDeviceNameValue(rpl::producer id) { return DeviceNameValue(DeviceType::Playback, std::move(id)); } rpl::producer CaptureDeviceNameValue(rpl::producer id) { return DeviceNameValue(DeviceType::Capture, std::move(id)); } rpl::producer CameraDeviceNameValue( rpl::producer id) { return DeviceNameValue(DeviceType::Camera, std::move(id)); } object_ptr ChoosePlaybackDeviceBox( rpl::producer currentId, Fn chosen, const style::Checkbox *st, const style::Radio *radioSt) { return Box( ChooseMediaDeviceBox, tr::lng_settings_call_output_device(), Core::App().mediaDevices().devicesValue(DeviceType::Playback), std::move(currentId), std::move(chosen), st, radioSt); } object_ptr ChooseCaptureDeviceBox( rpl::producer currentId, Fn chosen, const style::Checkbox *st, const style::Radio *radioSt) { return Box( ChooseMediaDeviceBox, tr::lng_settings_call_input_device(), Core::App().mediaDevices().devicesValue(DeviceType::Capture), std::move(currentId), std::move(chosen), st, radioSt); } object_ptr ChooseCameraDeviceBox( rpl::producer currentId, Fn chosen, const style::Checkbox *st, const style::Radio *radioSt) { return Box( ChooseMediaDeviceBox, tr::lng_settings_call_camera(), Core::App().mediaDevices().devicesValue(DeviceType::Camera), std::move(currentId), std::move(chosen), st, radioSt); } namespace Builder { SectionBuildMethod CallsSection = kMeta.build; } // namespace Builder } // namespace Settings