diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index b2201c1f9..1f58a45f8 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1015,3 +1015,6 @@ groupCallStartsInTop: 10px; groupCallStartsWhenTop: 160px; groupCallCountdownFont: font(64px semibold); groupCallCountdownTop: 52px; + +desktopCaptureSourceSize: size(160px, 120px); +desktopCaptureSourceSkip: 12px; diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp index e6a14abf3..d568351b0 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -31,7 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "webrtc/webrtc_media_devices.h" #include "webrtc/webrtc_create_adm.h" -#include #include #include #include @@ -406,18 +405,12 @@ void GroupCall::switchToCamera() { _videoCapture->switchToDevice(_videoDeviceId.toStdString()); } -void GroupCall::switchToScreenSharing() { - if (isScreenSharing()) { +void GroupCall::switchToScreenSharing(const QString &uniqueId) { + if (_videoDeviceId == uniqueId) { return; } - auto manager = tgcalls::DesktopCaptureSourceManager( - tgcalls::DesktopCaptureType::Screen); - const auto sources = manager.sources(); - if (!sources.empty()) { - const auto key = sources.front().deviceIdKey(); - _videoDeviceId = QString::fromStdString(key); - _videoCapture->switchToDevice(_videoDeviceId.toStdString()); - } + _videoDeviceId = uniqueId; + _videoCapture->switchToDevice(_videoDeviceId.toStdString()); } void GroupCall::setScheduledDate(TimeId date) { diff --git a/Telegram/SourceFiles/calls/calls_group_call.h b/Telegram/SourceFiles/calls/calls_group_call.h index e88f7c20d..0fa69f142 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.h +++ b/Telegram/SourceFiles/calls/calls_group_call.h @@ -212,7 +212,7 @@ public: void setCurrentVideoDevice(const QString &deviceId); bool isScreenSharing() const; void switchToCamera(); - void switchToScreenSharing(); + void switchToScreenSharing(const QString &uniqueId); //void setAudioVolume(bool input, float level); void setAudioDuckingEnabled(bool enabled); diff --git a/Telegram/SourceFiles/calls/calls_group_panel.cpp b/Telegram/SourceFiles/calls/calls_group_panel.cpp index 7767c2029..bb589861e 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_group_panel.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/calls_group_members.h" #include "calls/calls_group_settings.h" #include "calls/calls_group_menu.h" +#include "calls/group/ui/desktop_capture_choose_source.h" #include "ui/platform/ui_platform_window_title.h" #include "ui/platform/ui_platform_utility.h" #include "ui/controls/call_mute_button.h" @@ -473,6 +474,18 @@ void Panel::subscribeToPeerChanges() { }, _peerLifetime); } +QWidget *Panel::chooseSourceParent() { + return _window.get(); +} + +rpl::lifetime &Panel::chooseSourceInstanceLifetime() { + return _window->lifetime(); +} + +void Panel::chooseSourceAccepted(const QString &deviceId) { + _call->switchToScreenSharing(deviceId); +} + void Panel::initWindow() { _window->setAttribute(Qt::WA_OpaquePaintEvent); _window->setAttribute(Qt::WA_NoSystemBackground); @@ -676,7 +689,7 @@ void Panel::refreshLeftButton() { if (_call->isScreenSharing()) { _call->switchToCamera(); } else { - _call->switchToScreenSharing(); + Ui::DesktopCapture::ChooseSource(this); } //_layerBg->showBox(Box(SettingsBox, _call)); }); diff --git a/Telegram/SourceFiles/calls/calls_group_panel.h b/Telegram/SourceFiles/calls/calls_group_panel.h index 71d68042c..e788e4df5 100644 --- a/Telegram/SourceFiles/calls/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/calls_group_panel.h @@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/object_ptr.h" #include "calls/calls_group_call.h" #include "calls/calls_choose_join_as.h" +#include "calls/group/ui/desktop_capture_choose_source.h" #include "ui/effects/animations.h" #include "ui/rp_widget.h" @@ -53,7 +54,7 @@ namespace Calls::Group { class Members; -class Panel final { +class Panel final : private Ui::DesktopCapture::ChooseSourceDelegate { public: Panel(not_null call); ~Panel(); @@ -108,6 +109,10 @@ private: void migrate(not_null channel); void subscribeToPeerChanges(); + QWidget *chooseSourceParent() override; + rpl::lifetime &chooseSourceInstanceLifetime() override; + void chooseSourceAccepted(const QString &deviceId) override; + const not_null _call; not_null _peer; diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp new file mode 100644 index 000000000..73d4a2319 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.cpp @@ -0,0 +1,395 @@ +/* +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 "calls/group/ui/desktop_capture_choose_source.h" + +#include "ui/widgets/window.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/buttons.h" +#include "base/platform/base_platform_info.h" +#include "webrtc/webrtc_video_track.h" +#include "styles/style_calls.h" + +#include +#include +#include + +namespace Calls::Group::Ui::DesktopCapture { +namespace { + +constexpr auto kColumns = 3; +constexpr auto kRows = 2; + +struct Preview { + explicit Preview(tgcalls::DesktopCaptureSource source); + + tgcalls::DesktopCaptureSourceHelper helper; + Webrtc::VideoTrack track; + rpl::lifetime lifetime; +}; + +class Source final { +public: + Source( + not_null parent, + tgcalls::DesktopCaptureSource source, + const QString &title); + + void setGeometry(QRect geometry); + void clearHelper(); + + [[nodiscard]] bool ready() const; + [[nodiscard]] rpl::producer<> clicks() const; + [[nodiscard]] rpl::lifetime &lifetime(); + +private: + void paint(); + void setupPreview(); + + AbstractButton _widget; + FlatLabel _label; + tgcalls::DesktopCaptureSource _source; + std::unique_ptr _preview; + QImage _frame; + +}; + +class ChooseSourceProcess final { +public: + static void Start(not_null delegate); + + explicit ChooseSourceProcess(not_null delegate); + + void activate(); + +private: + void setupPanel(); + void setupSources(); + void setupGeometryWithParent(not_null parent); + void fillSources(); + void setupSourcesGeometry(); + void destroy(); + + static base::flat_map< + not_null, + std::unique_ptr> &Map(); + + const not_null _delegate; + const std::unique_ptr<::Ui::Window> _window; + const std::unique_ptr _scroll; + const not_null _inner; + + std::vector> _sources; + +}; + +[[nodiscard]] tgcalls::DesktopCaptureSourceData SourceData() { + const auto factor = style::DevicePixelRatio(); + const auto size = st::desktopCaptureSourceSize * factor; + return { + .aspectSize = { size.width(), size.height() }, + .fps = 1, + .captureMouse = false, + }; +} + +Preview::Preview(tgcalls::DesktopCaptureSource source) +: helper(source, SourceData()) +, track(Webrtc::VideoState::Active) { + helper.setOutput(track.sink()); + helper.start(); +} + +Source::Source( + not_null parent, + tgcalls::DesktopCaptureSource source, + const QString &title) +: _widget(parent) +, _label(&_widget, title) +, _source(source) { + _widget.paintRequest( + ) | rpl::start_with_next([=] { + paint(); + }, _widget.lifetime()); + + _widget.sizeValue( + ) | rpl::start_with_next([=](QSize size) { + _label.resizeToNaturalWidth(size.width()); + _label.move( + (size.width() - _label.width()) / 2, + size.height() - _label.height()); + }, _label.lifetime()); +} + +rpl::producer<> Source::clicks() const { + return _widget.clicks() | rpl::to_empty; +} + +void Source::setGeometry(QRect geometry) { + _widget.setGeometry(geometry); +} + +void Source::clearHelper() { + _preview = nullptr; +} + +void Source::paint() { + auto p = QPainter(&_widget); + + if (_frame.isNull() && !_preview) { + setupPreview(); + } + const auto size = _preview ? _preview->track.frameSize() : QSize(); + const auto factor = style::DevicePixelRatio(); + const auto rect = _widget.rect(); + const auto inner = QRect( + rect.x(), + rect.y(), + rect.width(), + rect.height() - _label.height()); + if (!size.isEmpty()) { + const auto scaled = size.scaled(inner.size(), Qt::KeepAspectRatio); + const auto request = Webrtc::FrameRequest{ + .resize = scaled * factor, + .outer = scaled * factor, + }; + _frame = _preview->track.frame(request); + _preview->track.markFrameShown(); + } + if (!_frame.isNull()) { + clearHelper(); + const auto size = _frame.size() / factor; + const auto x = inner.x() + (inner.width() - size.width()) / 2; + const auto y = inner.y() + (inner.height() - size.height()) / 2; + auto hq = PainterHighQualityEnabler(p); + p.drawImage(QRect(x, y, size.width(), size.height()), _frame); + } +} + +void Source::setupPreview() { + _preview = std::make_unique(_source); + _preview->track.renderNextFrame( + ) | rpl::start_with_next([=] { + if (_preview->track.frameSize().isEmpty()) { + _preview->track.markFrameShown(); + } else { + _widget.update(); + } + }, _preview->lifetime); +} + +rpl::lifetime &Source::lifetime() { + return _widget.lifetime(); +} + +ChooseSourceProcess::ChooseSourceProcess( + not_null delegate) +: _delegate(delegate) +, _window(std::make_unique<::Ui::Window>()) +, _scroll(std::make_unique(_window->body())) +, _inner(_scroll->setOwnedWidget(object_ptr(_scroll.get()))) { + setupPanel(); + setupSources(); + activate(); +} + +void ChooseSourceProcess::Start(not_null delegate) { + auto &map = Map(); + auto i = map.find(delegate); + if (i == end(map)) { + i = map.emplace(delegate, nullptr).first; + delegate->chooseSourceInstanceLifetime().add([=] { + Map().erase(delegate); + }); + } + if (!i->second) { + i->second = std::make_unique(delegate); + } else { + i->second->activate(); + } +} + +void ChooseSourceProcess::activate() { + if (_window->windowState() & Qt::WindowMinimized) { + _window->showNormal(); + } else { + _window->show(); + } + _window->activateWindow(); +} + +[[nodiscard]] base::flat_map< + not_null, + std::unique_ptr> &ChooseSourceProcess::Map() { + static auto result = base::flat_map< + not_null, + std::unique_ptr>(); + return result; +} + +void ChooseSourceProcess::setupPanel() { + const auto width = kColumns * st::desktopCaptureSourceSize.width() + + (kColumns + 1) * st::desktopCaptureSourceSkip; + const auto height = kRows * st::desktopCaptureSourceSize.height() + + (kRows + 1) * st::desktopCaptureSourceSkip + + (st::desktopCaptureSourceSize.height() / 2); + _window->setFixedSize({ width, height }); + _window->setWindowFlags(Qt::WindowStaysOnTopHint); + + _window->body()->sizeValue( + ) | rpl::start_with_next([=](QSize size) { + _scroll->setGeometry({ QPoint(), size }); + }, _scroll->lifetime()); + + _scroll->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto rows = int(std::ceil(_sources.size() / float(kColumns))); + const auto height = rows * st::desktopCaptureSourceSize.height() + + (rows + 1) * st::desktopCaptureSourceSkip; + _inner->resize(width, height); + }, _inner->lifetime()); + + if (const auto parent = _delegate->chooseSourceParent()) { + setupGeometryWithParent(parent); + } + + _window->events( + ) | rpl::filter([=](not_null e) { + return e->type() == QEvent::Close; + }) | rpl::start_with_next([=] { + destroy(); + }, _window->lifetime()); +} + +void ChooseSourceProcess::setupSources() { + fillSources(); + setupSourcesGeometry(); +} + +void ChooseSourceProcess::fillSources() { + using Type = tgcalls::DesktopCaptureType; + auto screensManager = tgcalls::DesktopCaptureSourceManager(Type::Screen); + auto windowsManager = tgcalls::DesktopCaptureSourceManager(Type::Window); + + auto screenIndex = 0; + auto windowIndex = 0; + const auto append = [&](const tgcalls::DesktopCaptureSource &source) { + const auto title = !source.title().empty() + ? QString::fromStdString(source.title()) + : source.isWindow() + ? "Window " + QString::number(++windowIndex) + : "Screen " + QString::number(++screenIndex); + _sources.push_back(std::make_unique(_inner, source, title)); + _sources.back()->clicks( + ) | rpl::start_with_next([=, id = source.deviceIdKey()]{ + _delegate->chooseSourceAccepted(QString::fromStdString(id)); + }, _sources.back()->lifetime()); + }; + for (const auto &source : screensManager.sources()) { + append(source); + } + for (const auto &source : windowsManager.sources()) { + append(source); + } +} + +void ChooseSourceProcess::setupSourcesGeometry() { + if (_sources.empty()) { + //LOG(()); + destroy(); + return; + } + _inner->widthValue( + ) | rpl::start_with_next([=](int width) { + const auto rows = int(std::ceil(_sources.size() / float(kColumns))); + const auto skip = st::desktopCaptureSourceSkip; + const auto single = (width - (kColumns + 1) * skip) / kColumns; + const auto height = st::desktopCaptureSourceSize.height(); + auto top = skip; + auto index = 0; + for (auto row = 0; row != rows; ++row) { + auto left = skip; + for (auto column = 0; column != kColumns; ++column) { + _sources[index]->setGeometry({ left, top, single, height }); + if (++index == _sources.size()) { + break; + } + left += single + skip; + } + if (index >= _sources.size()) { + break; + } + top += height + skip; + } + }, _inner->lifetime()); + + rpl::combine( + _scroll->scrollTopValue(), + _scroll->heightValue() + ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) { + const auto rows = int(std::ceil(_sources.size() / float(kColumns))); + const auto skip = st::desktopCaptureSourceSkip; + const auto height = st::desktopCaptureSourceSize.height(); + auto top = skip; + auto index = 0; + for (auto row = 0; row != rows; ++row) { + const auto hidden = (top + height <= scrollTop) + || (top >= scrollTop + scrollHeight); + if (hidden) { + for (auto column = 0; column != kColumns; ++column) { + _sources[index]->clearHelper(); + if (++index == _sources.size()) { + break; + } + } + } else { + index += kColumns; + } + if (index >= _sources.size()) { + break; + } + top += height + skip; + } + }, _inner->lifetime()); +} + +void ChooseSourceProcess::setupGeometryWithParent( + not_null parent) { + if (const auto handle = parent->windowHandle()) { + if (::Platform::IsLinux()) { + _window->windowHandle()->setTransientParent( + parent->windowHandle()); + _window->setWindowModality(Qt::WindowModal); + } + const auto parentScreen = handle->screen(); + const auto myScreen = _window->windowHandle()->screen(); + if (parentScreen && myScreen != parentScreen) { + _window->windowHandle()->setScreen(parentScreen); + } + } + _window->move( + parent->x() + (parent->width() - _window->width()) / 2, + parent->y() + (parent->height() - _window->height()) / 2); +} + +void ChooseSourceProcess::destroy() { + auto &map = Map(); + if (const auto i = map.find(_delegate); i != end(map)) { + if (i->second.get() == this) { + base::take(i->second); + } + } +} + +} // namespace + +void ChooseSource(not_null delegate) { + ChooseSourceProcess::Start(delegate); +} + +} // namespace Calls::Group::Ui::DesktopCapture diff --git a/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h new file mode 100644 index 000000000..22b871fa8 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/ui/desktop_capture_choose_source.h @@ -0,0 +1,28 @@ +/* +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 +*/ +#pragma once + +namespace Ui { +} // namespace Ui + +namespace Calls::Group::Ui { +using namespace ::Ui; +} // namespace Calls::Group::Ui + +namespace Calls::Group::Ui::DesktopCapture { + +class ChooseSourceDelegate { +public: + virtual QWidget *chooseSourceParent() = 0; + virtual rpl::lifetime &chooseSourceInstanceLifetime() = 0; + virtual void chooseSourceAccepted(const QString &deviceId) = 0; +}; + +void ChooseSource(not_null delegate); + +} // namespace Calls::Group::Ui::DesktopCapture diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 75d522651..28d27d2df 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -47,6 +47,9 @@ nice_target_sources(td_ui ${src_loc} PRIVATE ${style_files} + calls/group/ui/desktop_capture_choose_source.cpp + calls/group/ui/desktop_capture_choose_source.h + core/file_location.cpp core/file_location.h core/mime_type.cpp @@ -163,7 +166,9 @@ PUBLIC desktop-app::lib_ui desktop-app::lib_lottie PRIVATE + tdesktop::lib_tgcalls desktop-app::lib_ffmpeg desktop-app::lib_webview + desktop-app::lib_webrtc desktop-app::lib_stripe )