/* 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/settings_calls.h" #include "ui/wrap/vertical_layout.h" #include "ui/wrap/slide_wrap.h" #include "ui/widgets/labels.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/level_meter.h" #include "ui/widgets/buttons.h" #include "ui/boxes/single_choice_box.h" #include "ui/boxes/confirm_box.h" #include "ui/vertical_list.h" #include "platform/platform_specific.h" #include "main/main_session.h" #include "lang/lang_keys.h" #include "styles/style_settings.h" #include "ui/widgets/continuous_sliders.h" #include "window/window_session_controller.h" #include "core/application.h" #include "core/core_settings.h" #include "calls/calls_call.h" #include "calls/calls_instance.h" #include "calls/calls_video_bubble.h" #include "apiwrap.h" #include "api/api_authorizations.h" #include "webrtc/webrtc_media_devices.h" #include "webrtc/webrtc_video_track.h" #include "webrtc/webrtc_audio_input_tester.h" #include "webrtc/webrtc_create_adm.h" // Webrtc::Backend. #include "tgcalls/VideoCaptureInterface.h" #include "styles/style_layers.h" namespace Settings { namespace { using namespace Webrtc; } // namespace Calls::Calls( QWidget *parent, not_null controller) : Section(parent) , _controller(controller) { // Request valid value of calls disabled flag. controller->session().api().authorizations().reload(); setupContent(); requestPermissionAndStartTestingMicrophone(); } Calls::~Calls() = default; rpl::producer Calls::title() { return tr::lng_settings_section_call_settings(); } Webrtc::VideoTrack *Calls::AddCameraSubsection( std::shared_ptr show, not_null content, bool saveToSettings) { auto &lifetime = content->lifetime(); const auto hasCall = (Core::App().calls().currentCall() != nullptr); const auto cameraNameStream = lifetime.make_state< rpl::event_stream >(); auto capturerOwner = lifetime.make_state< std::shared_ptr >(); const auto track = lifetime.make_state( (hasCall ? VideoState::Inactive : VideoState::Active)); const auto currentCameraName = [&] { const auto cameras = GetVideoInputList(); const auto i = ranges::find( cameras, Core::App().settings().callVideoInputDeviceId(), &VideoInput::id); return (i != end(cameras)) ? i->name : tr::lng_settings_call_device_default(tr::now); }(); AddButtonWithLabel( content, tr::lng_settings_call_input_device(), rpl::single( currentCameraName ) | rpl::then( cameraNameStream->events() ), st::settingsButtonNoIcon )->addClickHandler([=] { const auto &devices = GetVideoInputList(); const auto options = ranges::views::concat( ranges::views::single( tr::lng_settings_call_device_default(tr::now)), devices | ranges::views::transform(&VideoInput::name) ) | ranges::to_vector; const auto i = ranges::find( devices, Core::App().settings().callVideoInputDeviceId(), &VideoInput::id); const auto currentOption = (i != end(devices)) ? int(i - begin(devices) + 1) : 0; const auto save = crl::guard(content, [=](int option) { cameraNameStream->fire_copy(options[option]); const auto deviceId = option ? devices[option - 1].id : "default"; if (saveToSettings) { Core::App().settings().setCallVideoInputDeviceId(deviceId); Core::App().saveSettingsDelayed(); } if (const auto call = Core::App().calls().currentCall()) { call->setCurrentCameraDevice(deviceId); } if (*capturerOwner) { (*capturerOwner)->switchToDevice( deviceId.toStdString(), false); } }); show->showBox(Box([=](not_null box) { SingleChoiceBox(box, { .title = tr::lng_settings_call_camera(), .options = options, .initialSelection = currentOption, .callback = save, }); })); }); 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::start_with_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().callVideoInputDeviceId(), 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::start_with_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; } void Calls::sectionSaveChanges(FnMut done) { if (_micTester) { _micTester.reset(); } done(); } void Calls::setupContent() { const auto content = Ui::CreateChild(this); if (!GetVideoInputList().empty()) { Ui::AddSkip(content); Ui::AddSubsectionTitle(content, tr::lng_settings_call_camera()); AddCameraSubsection(_controller->uiShow(), content, true); Ui::AddSkip(content); Ui::AddDivider(content); } Ui::AddSkip(content); Ui::AddSubsectionTitle(content, tr::lng_settings_call_section_output()); AddButtonWithLabel( content, tr::lng_settings_call_output_device(), rpl::single( CurrentAudioOutputName() ) | rpl::then( _outputNameStream.events() ), st::settingsButtonNoIcon )->addClickHandler([=] { _controller->show(ChooseAudioOutputBox(crl::guard(this, [=]( const QString &id, const QString &name) { _outputNameStream.fire_copy(name); }))); }); Ui::AddSkip(content); Ui::AddDivider(content); Ui::AddSkip(content); Ui::AddSubsectionTitle(content, tr::lng_settings_call_section_input()); AddButtonWithLabel( content, tr::lng_settings_call_input_device(), rpl::single( CurrentAudioInputName() ) | rpl::then( _inputNameStream.events() ), st::settingsButtonNoIcon )->addClickHandler([=] { _controller->show(ChooseAudioInputBox(crl::guard(this, [=]( const QString &id, const QString &name) { _inputNameStream.fire_copy(name); if (_micTester) { _micTester->setDeviceId(id); } }))); }); _micTestLevel = content->add( object_ptr( content, st::defaultLevelMeter), st::settingsLevelMeterPadding); _micTestLevel->resize(QSize(0, st::defaultLevelMeter.height)); _levelUpdateTimer.setCallback([=] { const auto was = _micLevel; _micLevel = _micTester->getAndResetLevel(); _micLevelAnimation.start([=] { _micTestLevel->setValue(_micLevelAnimation.value(_micLevel)); }, was, _micLevel, kMicTestAnimationDuration); }); Ui::AddSkip(content); Ui::AddDivider(content); Ui::AddSkip(content); Ui::AddSubsectionTitle(content, tr::lng_settings_call_section_other()); const auto api = &_controller->session().api(); content->add(object_ptr( content, tr::lng_settings_call_accept_calls(), st::settingsButtonNoIcon ))->toggleOn( api->authorizations().callsDisabledHereValue( ) | rpl::map(!rpl::mappers::_1) )->toggledChanges( ) | rpl::filter([=](bool value) { return (value == api->authorizations().callsDisabledHere()); }) | start_with_next([=](bool value) { api->authorizations().toggleCallsDisabledHere(!value); }, content->lifetime()); content->add(object_ptr( content, tr::lng_settings_call_open_system_prefs(), st::settingsButtonNoIcon ))->addClickHandler([=] { const auto opened = Platform::OpenSystemSettings( Platform::SystemSettingsType::Audio); if (!opened) { _controller->show( Ui::MakeInformBox(tr::lng_linux_no_audio_prefs())); } }); Ui::AddSkip(content); Ui::ResizeFitChild(this, content); } void Calls::requestPermissionAndStartTestingMicrophone() { const auto status = Platform::GetPermissionStatus( Platform::PermissionType::Microphone); if (status == Platform::PermissionStatus::Granted) { startTestingMicrophone(); } else if (status == Platform::PermissionStatus::CanRequest) { const auto startTestingChecked = crl::guard(this, [=]( Platform::PermissionStatus status) { if (status == Platform::PermissionStatus::Granted) { crl::on_main(crl::guard(this, [=] { startTestingMicrophone(); })); } }); Platform::RequestPermission( Platform::PermissionType::Microphone, startTestingChecked); } else { const auto showSystemSettings = [controller = _controller] { Platform::OpenSystemSettingsForPermission( Platform::PermissionType::Microphone); controller->hideLayer(); }; _controller->show(Ui::MakeConfirmBox({ .text = tr::lng_no_mic_permission(), .confirmed = showSystemSettings, .confirmText = tr::lng_menu_settings(), })); } } void Calls::startTestingMicrophone() { _levelUpdateTimer.callEach(kMicTestUpdateInterval); _micTester = std::make_unique( Core::App().settings().callAudioBackend(), Core::App().settings().callInputDeviceId()); } QString CurrentAudioOutputName() { const auto &settings = Core::App().settings(); const auto list = GetAudioOutputList(settings.callAudioBackend()); const auto i = ranges::find( list, settings.callOutputDeviceId(), &AudioOutput::id); return (i != end(list)) ? i->name : tr::lng_settings_call_device_default(tr::now); } QString CurrentAudioInputName() { const auto &settings = Core::App().settings(); const auto list = GetAudioInputList(settings.callAudioBackend()); const auto i = ranges::find( list, settings.callInputDeviceId(), &AudioInput::id); return (i != end(list)) ? i->name : tr::lng_settings_call_device_default(tr::now); } object_ptr ChooseAudioOutputBox( Fn chosen, const style::Checkbox *st, const style::Radio *radioSt) { const auto &settings = Core::App().settings(); const auto list = GetAudioOutputList(settings.callAudioBackend()); const auto options = ranges::views::concat( ranges::views::single(tr::lng_settings_call_device_default(tr::now)), list | ranges::views::transform(&AudioOutput::name) ) | ranges::to_vector; const auto i = ranges::find( list, settings.callOutputDeviceId(), &AudioOutput::id); const auto currentOption = (i != end(list)) ? int(i - begin(list) + 1) : 0; const auto save = [=](int option) { const auto deviceId = option ? list[option - 1].id : "default"; Core::App().calls().setCurrentAudioDevice(false, deviceId); chosen(deviceId, options[option]); }; return Box([=](not_null box) { SingleChoiceBox(box, { .title = tr::lng_settings_call_output_device(), .options = options, .initialSelection = currentOption, .callback = save, .st = st, .radioSt = radioSt, }); }); } object_ptr ChooseAudioInputBox( Fn chosen, const style::Checkbox *st, const style::Radio *radioSt) { const auto &settings = Core::App().settings(); const auto list = GetAudioInputList(settings.callAudioBackend()); const auto options = ranges::views::concat( ranges::views::single(tr::lng_settings_call_device_default(tr::now)), list | ranges::views::transform(&AudioInput::name) ) | ranges::to_vector; const auto i = ranges::find( list, Core::App().settings().callInputDeviceId(), &AudioInput::id); const auto currentOption = (i != end(list)) ? int(i - begin(list) + 1) : 0; const auto save = [=](int option) { const auto deviceId = option ? list[option - 1].id : "default"; Core::App().calls().setCurrentAudioDevice(true, deviceId); chosen(deviceId, options[option]); }; return Box([=](not_null box) { SingleChoiceBox(box, { .title = tr::lng_settings_call_input_device(), .options = options, .initialSelection = currentOption, .callback = save, .st = st, .radioSt = radioSt, }); }); } // //object_ptr ChooseAudioBackendBox( // const style::Checkbox *st, // const style::Radio *radioSt) { // const auto &settings = Core::App().settings(); // const auto list = GetAudioInputList(settings.callAudioBackend()); // const auto options = std::vector{ // "OpenAL", // "Webrtc ADM", //#ifdef Q_OS_WIN // "Webrtc ADM2", //#endif // Q_OS_WIN // }; // const auto currentOption = static_cast(settings.callAudioBackend()); // const auto save = [=](int option) { // Core::App().settings().setCallAudioBackend( // static_cast(option)); // Core::App().saveSettings(); // Core::Restart(); // }; // return Box([=](not_null box) { // SingleChoiceBox(box, { // .title = rpl::single("Calls audio backend"), // .options = options, // .initialSelection = currentOption, // .callback = save, // .st = st, // .radioSt = radioSt, // }); // }); //} } // namespace Settings