From 8fc77e913a503704c156f89bbebc204f87e2b299 Mon Sep 17 00:00:00 2001 From: paterkleomenis Date: Mon, 23 Feb 2026 03:57:20 +0200 Subject: [PATCH] Calls: screenshare audio fixes, volume control and fullscreen mode Fix system audio stopping when microphone is muted during 1-to-1 screen sharing. Previously setMuteMicrophone() muted the entire outgoing audio channel, killing both mic and system audio. Now when screen audio is active, mic is muted at the ADM mixing level while the channel stays unmuted so system audio continues flowing. Add right-click context menu on the incoming video in 1-to-1 calls with a volume slider and mute toggle for controlling the remote user's audio volume locally. Implements setOutputVolume via the WebRTC receive channel (same mechanism as group calls) so the slider actually affects playback. Always wrap the ADM with MixingAudioDeviceModule so playback volume control is available regardless of loopback capture support. Add separate controls for participant voice and screen-share audio in group call member menus, and expose a dedicated screen-share mute action when screen audio is available. Add screencast-only fullscreen mode for group call screenshare. When a screen endpoint is focused, fullscreen switches to a layout that hides side panels. Controls auto-hide on inactivity and reappear on mouse movement. Also disable speaking outline rendering in fullscreen to avoid full-frame green highlighting. --- Telegram/SourceFiles/calls/calls.style | 1 + Telegram/SourceFiles/calls/calls_call.cpp | 80 +++++++- Telegram/SourceFiles/calls/calls_call.h | 17 +- Telegram/SourceFiles/calls/calls_panel.cpp | 86 ++++++++- Telegram/SourceFiles/calls/calls_panel.h | 2 + .../calls/group/calls_group_call.cpp | 95 ++++++++- .../calls/group/calls_group_call.h | 20 ++ .../calls/group/calls_group_members.cpp | 136 ++++++++++++- .../calls/group/calls_group_members.h | 4 + .../calls/group/calls_group_panel.cpp | 180 +++++++++++++++--- .../calls/group/calls_group_panel.h | 3 + .../calls/group/calls_group_viewport.cpp | 27 +++ .../calls/group/calls_group_viewport.h | 4 + .../group/calls_group_viewport_opengl.cpp | 2 +- .../group/calls_group_viewport_raster.cpp | 2 +- Telegram/ThirdParty/tgcalls | 2 +- Telegram/lib_webrtc | 2 +- 17 files changed, 617 insertions(+), 46 deletions(-) diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 15ecfd33c090d..2c5c32e738b68 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1008,6 +1008,7 @@ groupCallScreenShareSmall: CallButton(groupCallSettingsSmall) { rippleAreaPosition: point(8px, 12px); } } + groupCallMenuToggleSmall: CallButton(groupCallSettingsSmall) { button: IconButton(groupCallSettingsInner) { icon: icon {{ "calls/calls_more", groupCallIconFg }}; diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index 91b46b066554f..44b5a5b414a4c 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -520,7 +520,12 @@ rpl::producer Call::captureMuteDeviceId() { void Call::setMuted(bool mute) { _muted = mute; - if (_instance) { + if (_screenWithAudio && _screenAudioControl) { + _screenAudioControl->setMicrophoneMuted(mute); + if (_instance) { + _instance->setMuteMicrophone(false); + } + } else if (_instance) { _instance->setMuteMicrophone(mute); } } @@ -1080,6 +1085,13 @@ void Call::createAndStartController(const MTPDphoneCall &call) { }); }; + _screenAudioControl = std::make_shared(); + auto admCreator = Webrtc::AudioDeviceModuleCreator( + saveSetDeviceIdCallback); + auto audioDeviceModuleCreator = Webrtc::MixingAudioDeviceModuleCreator( + std::move(admCreator), + _screenAudioControl); + tgcalls::Descriptor descriptor = { .version = versionString, .config = tgcalls::Config{ @@ -1139,8 +1151,7 @@ void Call::createAndStartController(const MTPDphoneCall &call) { sendSignalingData(bytes); }); }, - .createAudioDeviceModule = Webrtc::AudioDeviceModuleCreator( - saveSetDeviceIdCallback), + .createAudioDeviceModule = std::move(audioDeviceModuleCreator), }; if (Logs::DebugEnabled()) { const auto callLogFolder = cWorkingDir() + u"DebugLogs"_q; @@ -1381,6 +1392,48 @@ void Call::setState(State state) { // } //} +void Call::setPlaybackVolume(int volume) { + _playbackVolume = std::clamp(volume, 0, 20000); + const auto level = _playbackMuted.current() + ? 0.f + : (_playbackVolume.current() / 10000.f); + if (_instance) { + _instance->setOutputVolume(level); + } + if (_screenAudioControl) { + _screenAudioControl->setPlaybackVolume(level); + } +} + +int Call::playbackVolume() const { + return _playbackVolume.current(); +} + +void Call::setPlaybackMuted(bool muted) { + _playbackMuted = muted; + const auto level = muted + ? 0.f + : (_playbackVolume.current() / 10000.f); + if (_instance) { + _instance->setOutputVolume(level); + } + if (_screenAudioControl) { + _screenAudioControl->setPlaybackVolume(level); + } +} + +bool Call::playbackMuted() const { + return _playbackMuted.current(); +} + +rpl::producer Call::playbackVolumeValue() const { + return _playbackVolume.value(); +} + +rpl::producer Call::playbackMutedValue() const { + return _playbackMuted.value(); +} + void Call::setAudioDuckingEnabled(bool enabled) { if (_instance) { _instance->setAudioOutputDuckingEnabled(enabled); @@ -1399,6 +1452,10 @@ bool Call::isSharingScreen() const { return _videoCaptureIsScreencast && isSharingVideo(); } +bool Call::screenSharingWithAudio() const { + return isSharingScreen() && _screenWithAudio; +} + QString Call::cameraSharingDeviceId() const { return isSharingCamera() ? _videoCaptureDeviceId : QString(); } @@ -1445,6 +1502,13 @@ void Call::toggleScreenSharing( } _videoCaptureDeviceId = QString(); _videoCaptureIsScreencast = false; + if (_screenWithAudio && _screenAudioControl) { + _screenAudioControl->setMicrophoneMuted(false); + _screenAudioControl->setLoopbackEnabled(false); + if (_muted.current() && _instance) { + _instance->setMuteMicrophone(true); + } + } _screenWithAudio = false; if (_systemAudioCapture) { _systemAudioCapture->stop(); @@ -1459,6 +1523,15 @@ void Call::toggleScreenSharing( _videoCaptureIsScreencast = true; _videoCaptureDeviceId = *uniqueId; _screenWithAudio = withAudio; + if (_screenAudioControl) { + _screenAudioControl->setLoopbackEnabled(withAudio); + if (withAudio && _muted.current()) { + _screenAudioControl->setMicrophoneMuted(true); + if (_instance) { + _instance->setMuteMicrophone(false); + } + } + } if (_videoCapture) { _videoCapture->switchToDevice(uniqueId->toStdString(), true); if (_instance) { @@ -1647,6 +1720,7 @@ void Call::handleControllerError(const QString &error) { } void Call::destroyController() { + _screenAudioControl = nullptr; _instanceLifetime.destroy(); Core::App().mediaDevices().setCaptureMuteTracker(this, false); if (_systemAudioCapture) { diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index d40d0794c40e6..abba5cbb3849f 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -37,10 +37,10 @@ namespace Webrtc { enum class VideoState; class VideoTrack; struct DeviceResolvedId; +class MixingAudioControl; } // namespace Webrtc namespace Calls { - struct StartConferenceInfo; struct DhConfig { @@ -243,6 +243,13 @@ class Call final //void setAudioVolume(bool input, float level); void setAudioDuckingEnabled(bool enabled); + void setPlaybackVolume(int volume); + [[nodiscard]] int playbackVolume() const; + void setPlaybackMuted(bool muted); + [[nodiscard]] bool playbackMuted() const; + [[nodiscard]] rpl::producer playbackVolumeValue() const; + [[nodiscard]] rpl::producer playbackMutedValue() const; + [[nodiscard]] QString videoDeviceId() const { return _videoCaptureDeviceId; } @@ -250,15 +257,14 @@ class Call final [[nodiscard]] bool isSharingVideo() const; [[nodiscard]] bool isSharingCamera() const; [[nodiscard]] bool isSharingScreen() const; + [[nodiscard]] bool screenSharingWithAudio() const; [[nodiscard]] QString cameraSharingDeviceId() const; [[nodiscard]] QString screenSharingDeviceId() const; void toggleCameraSharing(bool enabled); void toggleScreenSharing( std::optional uniqueId, bool withAudio = false); - [[nodiscard]] bool screenSharingWithAudio() const { - return _screenWithAudio; - } + [[nodiscard]] auto peekVideoCapture() const -> std::shared_ptr; @@ -369,11 +375,14 @@ class Call final std::vector> _conferenceParticipants; std::unique_ptr _instance; + std::shared_ptr _screenAudioControl; std::shared_ptr _videoCapture; QString _videoCaptureDeviceId; bool _videoCaptureIsScreencast = false; bool _screenWithAudio = false; std::unique_ptr _systemAudioCapture; + rpl::variable _playbackVolume = 10000; + rpl::variable _playbackMuted = false; const std::unique_ptr _videoIncoming; const std::unique_ptr _videoOutgoing; diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index 082cedb1b6c0a..892303386c0e4 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -7,7 +7,12 @@ For license and copyright information please follow this link: */ #include "calls/calls_panel.h" +#include "ui/widgets/checkbox.h" +#include "webrtc/webrtc_create_adm.h" + #include "boxes/peers/replace_boost_box.h" // CreateUserpicsWithMoreBadge +#include "calls/group/calls_volume_item.h" +#include "calls/group/calls_group_common.h" #include "calls/calls_panel_background.h" #include "data/data_photo.h" #include "data/data_session.h" @@ -16,7 +21,6 @@ For license and copyright information please follow this link: #include "data/data_photo_media.h" #include "data/data_cloud_file.h" #include "data/data_changes.h" -#include "calls/group/calls_group_common.h" #include "calls/group/calls_group_invite_controller.h" #include "calls/ui/calls_device_menu.h" #include "calls/calls_emoji_fingerprint.h" @@ -382,6 +386,21 @@ void Panel::initWidget() { ) | rpl::skip(1) | rpl::on_next([=] { updateControlsGeometry(); }, lifetime()); + + base::install_event_filter(widget(), widget(), [=]( + not_null e) { + if (e->type() == QEvent::ContextMenu) { + const auto event = static_cast(e.get()); + const auto pos = event->pos(); + if (_incoming + && _incoming->widget()->isVisible() + && incomingFrameGeometry().contains(pos)) { + showIncomingVolumeMenu(event->globalPos()); + return base::EventFilterResult::Cancel; + } + } + return base::EventFilterResult::Continue; + }); } void Panel::initControls() { @@ -1056,6 +1075,71 @@ void Panel::initMediaDeviceToggles() { _audioDeviceToggle->setAccessibleName(tr::lng_settings_call_section_output(tr::now)); } +void Panel::showIncomingVolumeMenu(QPoint globalPos) { + if (!_call || _incomingVolumeMenu) { + return; + } + + _incomingVolumeMenu = base::make_unique_q( + widget(), + st::groupCallPopupMenuWithVolume); + + const auto menu = _incomingVolumeMenu.get(); + const auto call = _call; + const auto peer = _user; + const auto startVolume = call->playbackVolume(); + const auto startMuted = call->playbackMuted(); + + auto stateProducer = rpl::combine( + call->playbackVolumeValue(), + call->playbackMutedValue() + ) | rpl::map([peer](int volume, bool muted) { + return Group::ParticipantState{ + .peer = peer, + .volume = volume, + .mutedByMe = muted, + }; + }); + + auto volumeItem = base::make_unique_q( + menu->menu(), + st::groupCallPopupVolumeMenu, + st::groupCallMenuVolumeSlider, + std::move(stateProducer), + startVolume, + Group::kMaxVolume, + startMuted, + st::groupCallMenuVolumePadding); + + volumeItem->toggleMuteRequests( + ) | rpl::on_next([=](bool muted) { + call->setPlaybackMuted(muted); + if (muted) { + crl::on_main(menu, [=] { + menu->hideMenu(); + }); + } + }, volumeItem->lifetime()); + + volumeItem->toggleMuteLocallyRequests( + ) | rpl::on_next([=](bool muted) { + call->setPlaybackMuted(muted); + }, volumeItem->lifetime()); + + volumeItem->changeVolumeRequests( + ) | rpl::on_next([=](int volume) { + call->setPlaybackVolume(volume); + }, volumeItem->lifetime()); + + volumeItem->changeVolumeLocallyRequests( + ) | rpl::on_next([=](int volume) { + call->setPlaybackVolume(volume); + }, volumeItem->lifetime()); + + menu->addAction(std::move(volumeItem)); + _incomingVolumeMenu->popup(globalPos); +} + void Panel::showDevicesMenu( not_null button, std::vector types) { diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index aaed848f7fd2a..df39c55d86ecc 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -158,6 +158,7 @@ class Panel final void showDevicesMenu( not_null button, std::vector types); + void showIncomingVolumeMenu(QPoint globalPos); [[nodiscard]] QRect incomingFrameGeometry() const; [[nodiscard]] QRect outgoingFrameGeometry() const; @@ -212,6 +213,7 @@ class Panel final bool _mouseInside = false; base::unique_qptr _devicesMenu; + base::unique_qptr _incomingVolumeMenu; base::Timer _updateDurationTimer; base::Timer _updateOuterRippleTimer; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index f002f3cd220a4..d540c62b6d809 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -3570,6 +3570,24 @@ float64 GroupCall::singleSourceVolumeValue() const { return _singleSourceVolume / float64(Group::kDefaultVolume); } +float64 GroupCall::effectiveParticipantVolume( + const Data::GroupCallParticipant &participant) const { + return participant.mutedByMe + ? 0. + : (participant.volume / float64(Group::kDefaultVolume)); +} + +float64 GroupCall::effectiveParticipantScreenVolume( + const Data::GroupCallParticipant &participant) const { + const auto i = _screenVolumesByPeer.find(participant.peer); + if (i != end(_screenVolumesByPeer)) { + return i->second.muted + ? 0. + : (i->second.volume / float64(Group::kDefaultVolume)); + } + return effectiveParticipantVolume(participant); +} + void GroupCall::updateInstanceVolumes() { const auto real = lookupReal(); if (!real) { @@ -3606,14 +3624,13 @@ void GroupCall::updateInstanceVolume( && (volumeChanged || (was && (GetAdditionalAudioSsrc(was->videoParams) != additionalSsrc))); - const auto localVolume = now.mutedByMe - ? 0. - : (now.volume / float64(Group::kDefaultVolume)); + const auto localVolume = effectiveParticipantVolume(now); + const auto localScreenVolume = effectiveParticipantScreenVolume(now); if (set) { _instance->setVolume(now.ssrc, localVolume); } if (additionalSet) { - _instance->setVolume(additionalSsrc, localVolume); + _instance->setVolume(additionalSsrc, localScreenVolume); } } @@ -3996,6 +4013,42 @@ void GroupCall::changeVolume(const Group::VolumeRequest &data) { } } +void GroupCall::toggleScreenMute(const Group::MuteRequest &data) { + if (_rtmp || videoStream() || data.peer->isSelf()) { + return; + } + auto &state = _screenVolumesByPeer[data.peer]; + state.muted = data.mute; + _otherParticipantScreenStateValue.fire(participantScreenState(data.peer)); + applyScreenVolume(data.peer); +} + +void GroupCall::changeScreenVolume(const Group::VolumeRequest &data) { + if (_rtmp || videoStream() || data.peer->isSelf()) { + return; + } + auto &state = _screenVolumesByPeer[data.peer]; + state.volume = std::clamp(data.volume, 1, Group::kMaxVolume); + state.muted = false; + _otherParticipantScreenStateValue.fire(participantScreenState(data.peer)); + applyScreenVolume(data.peer); +} + +void GroupCall::applyScreenVolume(not_null participantPeer) { + if (!_instance) { + return; + } + const auto participant = LookupParticipant(this, participantPeer); + if (!participant) { + return; + } + const auto ssrc = GetAdditionalAudioSsrc(participant->videoParams); + if (!ssrc) { + return; + } + _instance->setVolume(ssrc, effectiveParticipantScreenVolume(*participant)); +} + void GroupCall::editParticipant( not_null participantPeer, bool mute, @@ -4222,6 +4275,40 @@ auto GroupCall::otherParticipantStateValue() const return _otherParticipantStateValue.events(); } +auto GroupCall::otherParticipantScreenStateValue() const +-> rpl::producer { + return _otherParticipantScreenStateValue.events(); +} + +bool GroupCall::hasScreenShareAudio( + not_null participantPeer) { + const auto participant = LookupParticipant(this, participantPeer); + return participant && GetAdditionalAudioSsrc(participant->videoParams); +} + +Group::ParticipantState GroupCall::participantScreenState( + not_null participantPeer) { + const auto participant = LookupParticipant(this, participantPeer); + const auto i = _screenVolumesByPeer.find(participantPeer); + const auto fallbackVolume = participant + ? participant->volume + : Group::kDefaultVolume; + const auto fallbackMuted = participant ? participant->mutedByMe : false; + return Group::ParticipantState{ + .peer = participantPeer, + .volume = std::clamp( + (i != end(_screenVolumesByPeer)) + ? i->second.volume + : fallbackVolume, + 1, + Group::kMaxVolume), + .mutedByMe = (i != end(_screenVolumesByPeer)) + ? i->second.muted + : fallbackMuted, + .locallyOnly = true, + }; +} + MTPInputGroupCall GroupCall::inputCall() const { Expects(_id != 0); diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index c2820cd8b7d0e..c6d97364b2975 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -10,6 +10,7 @@ For license and copyright information please follow this link: #include "base/weak_ptr.h" #include "base/timer.h" #include "base/bytes.h" +#include "calls/group/calls_group_common.h" #include "mtproto/sender.h" #include "mtproto/mtproto_auth_key.h" #include "webrtc/webrtc_device_common.h" @@ -338,6 +339,12 @@ class GroupCall final [[nodiscard]] auto otherParticipantStateValue() const -> rpl::producer; + [[nodiscard]] auto otherParticipantScreenStateValue() const + -> rpl::producer; + [[nodiscard]] bool hasScreenShareAudio( + not_null participantPeer); + [[nodiscard]] Group::ParticipantState participantScreenState( + not_null participantPeer); enum State { Creating, @@ -454,6 +461,8 @@ class GroupCall final void toggleMute(const Group::MuteRequest &data); void changeVolume(const Group::VolumeRequest &data); + void toggleScreenMute(const Group::MuteRequest &data); + void changeScreenVolume(const Group::VolumeRequest &data); void inviteUsers( const std::vector &requests, @@ -594,6 +603,11 @@ class GroupCall final void updateInstanceVolume( const std::optional &was, const Data::GroupCallParticipant &now); + [[nodiscard]] float64 effectiveParticipantVolume( + const Data::GroupCallParticipant &participant) const; + [[nodiscard]] float64 effectiveParticipantScreenVolume( + const Data::GroupCallParticipant &participant) const; + void applyScreenVolume(not_null participantPeer); void applyMeInCallLocally(); void startRejoin(); void rejoin(); @@ -730,7 +744,13 @@ class GroupCall final bool _acceptFields = false; rpl::event_stream _otherParticipantStateValue; + rpl::event_stream _otherParticipantScreenStateValue; std::vector _queuedSelfUpdates; + struct ScreenVolumeState { + int volume = Group::kDefaultVolume; + bool muted = false; + }; + base::flat_map, ScreenVolumeState> _screenVolumesByPeer; CallId _id = 0; CallId _accessHash = 0; diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index c3e4328e90063..d0d3dc807759a 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -80,6 +80,8 @@ class Members::Controller final } [[nodiscard]] rpl::producer toggleMuteRequests() const; [[nodiscard]] rpl::producer changeVolumeRequests() const; + [[nodiscard]] rpl::producer toggleScreenMuteRequests() const; + [[nodiscard]] rpl::producer changeScreenVolumeRequests() const; [[nodiscard]] auto kickParticipantRequests() const -> rpl::producer>; @@ -188,6 +190,8 @@ class Members::Controller final rpl::event_stream _toggleMuteRequests; rpl::event_stream _changeVolumeRequests; + rpl::event_stream _toggleScreenMuteRequests; + rpl::event_stream _changeScreenVolumeRequests; rpl::event_stream> _kickParticipantRequests; rpl::variable _fullCount = 1; @@ -1047,6 +1051,16 @@ auto Members::Controller::changeVolumeRequests() const return _changeVolumeRequests.events(); } +auto Members::Controller::toggleScreenMuteRequests() const +-> rpl::producer { + return _toggleScreenMuteRequests.events(); +} + +auto Members::Controller::changeScreenVolumeRequests() const +-> rpl::producer { + return _changeScreenVolumeRequests.events(); +} + bool Members::Controller::rowIsMe(not_null participantPeer) { return isMe(participantPeer); } @@ -1581,11 +1595,40 @@ void Members::Controller::addMuteActionsToContextMenu( .locallyOnly = local, }); }); + const auto toggleScreenMute = crl::guard(this, [=](bool mute) { + _toggleScreenMuteRequests.fire(Group::MuteRequest{ + .peer = participantPeer, + .mute = mute, + .locallyOnly = true, + }); + }); + const auto changeScreenVolume = crl::guard(this, [=](int volume) { + _changeScreenVolumeRequests.fire(Group::VolumeRequest{ + .peer = participantPeer, + .volume = std::clamp(volume, 1, Group::kMaxVolume), + .locallyOnly = true, + }); + }); + const auto addSectionLabel = [=](const QString &text) { + const auto action = menu->addAction(text, [] {}); + action->setEnabled(false); + }; + const auto screenMuteActionText = [=](bool muted) { + return tr::lng_group_call_screen_share_audio(tr::now) + + u": "_q + + (muted + ? tr::lng_call_unmute_audio(tr::now) + : tr::lng_call_mute_audio(tr::now)); + }; const auto muteState = row->state(); const auto muted = (muteState == Row::State::Muted) || (muteState == Row::State::RaisedHand); const auto mutedByMe = row->mutedByMe(); + const auto addScreenVolumeItem = !_call->rtmp() + && !_call->videoStream() + && !isMe(participantPeer) + && _call->hasScreenShareAudio(participantPeer); auto mutesFromVolume = rpl::never() | rpl::type_erased; @@ -1643,16 +1686,70 @@ void Members::Controller::addMuteActionsToContextMenu( if (menu->actions().size() > 1) { // First - cover. menu->addSeparator(); } + if (addScreenVolumeItem) { + addSectionLabel(tr::lng_group_call_microphone(tr::now)); + } menu->addAction(std::move(volumeItem)); - if (!_call->rtmp() - && !_call->videoStream() - && !isMe(participantPeer)) { + if (!addScreenVolumeItem && !isMe(participantPeer)) { menu->addSeparator(); } }; + if (addScreenVolumeItem) { + auto otherParticipantScreenStateValue = rpl::merge( + _call->otherParticipantScreenStateValue( + ) | rpl::filter([=](const Group::ParticipantState &data) { + return data.peer == participantPeer; + }), + _call->otherParticipantStateValue( + ) | rpl::filter([=](const Group::ParticipantState &data) { + return data.peer == participantPeer; + }) | rpl::map([=](const Group::ParticipantState &) { + return _call->participantScreenState(participantPeer); + })); + + const auto state = _call->participantScreenState(participantPeer); + auto volumeItem = base::make_unique_q( + menu->menu(), + st::groupCallPopupVolumeMenu, + st::groupCallMenuVolumeSlider, + std::move(otherParticipantScreenStateValue), + state.volume.value_or(Group::kDefaultVolume), + Group::kMaxVolume, + state.mutedByMe, + st::groupCallMenuVolumePadding); + + volumeItem->toggleMuteRequests( + ) | rpl::on_next([=](bool muted) { + toggleScreenMute(muted); + }, volumeItem->lifetime()); + + volumeItem->toggleMuteLocallyRequests( + ) | rpl::on_next([=](bool muted) { + toggleScreenMute(muted); + }, volumeItem->lifetime()); + + volumeItem->changeVolumeRequests( + ) | rpl::on_next([=](int volume) { + changeScreenVolume(volume); + }, volumeItem->lifetime()); + + volumeItem->changeVolumeLocallyRequests( + ) | rpl::on_next([=](int volume) { + changeScreenVolume(volume); + }, volumeItem->lifetime()); + + if (menu->actions().size() > 1) { + menu->addSeparator(); + } + addSectionLabel(tr::lng_group_call_screen_share_audio(tr::now)); + + menu->addAction(std::move(volumeItem)); + menu->addSeparator(); + } + const auto muteAction = [&]() -> QAction* { if (muteState == Row::State::Invited || muteState == Row::State::Calling @@ -1696,6 +1793,29 @@ void Members::Controller::addMuteActionsToContextMenu( muteAction->setText(muteUnmuteString(muted, mutedByMe)); }, menu->lifetime()); } + if (addScreenVolumeItem) { + const auto initial = _call->participantScreenState(participantPeer); + const auto screenMuteAction = menu->addAction( + screenMuteActionText(initial.mutedByMe), + [=] { + const auto state = _call->participantScreenState(participantPeer); + toggleScreenMute(!state.mutedByMe); + }); + rpl::merge( + _call->otherParticipantScreenStateValue( + ) | rpl::filter([=](const Group::ParticipantState &data) { + return data.peer == participantPeer; + }), + _call->otherParticipantStateValue( + ) | rpl::filter([=](const Group::ParticipantState &data) { + return data.peer == participantPeer; + }) | rpl::map([=](const Group::ParticipantState &) { + return _call->participantScreenState(participantPeer); + }) + ) | rpl::on_next([=](const Group::ParticipantState &state) { + screenMuteAction->setText(screenMuteActionText(state.mutedByMe)); + }, menu->lifetime()); + } } std::unique_ptr Members::Controller::createRowForMe() { @@ -1782,6 +1902,16 @@ auto Members::changeVolumeRequests() const return _listController->changeVolumeRequests(); } +auto Members::toggleScreenMuteRequests() const +-> rpl::producer { + return _listController->toggleScreenMuteRequests(); +} + +auto Members::changeScreenVolumeRequests() const +-> rpl::producer { + return _listController->changeScreenVolumeRequests(); +} + auto Members::kickParticipantRequests() const -> rpl::producer> { return _listController->kickParticipantRequests(); diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.h b/Telegram/SourceFiles/calls/group/calls_group_members.h index 739cccc990300..656c48672d1e4 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members.h @@ -55,6 +55,10 @@ class Members final -> rpl::producer; [[nodiscard]] auto changeVolumeRequests() const -> rpl::producer; + [[nodiscard]] auto toggleScreenMuteRequests() const + -> rpl::producer; + [[nodiscard]] auto changeScreenVolumeRequests() const + -> rpl::producer; [[nodiscard]] auto kickParticipantRequests() const -> rpl::producer>; [[nodiscard]] rpl::producer<> addMembersRequests() const { diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index fc6112da01b68..59da5c42c4539 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -254,7 +254,11 @@ Panel::Panel(not_null call, ConferencePanelMigration info) result.setAlphaF(kControlsBackgroundOpacity); return result; }) -, _hideControlsTimer([=] { toggleWideControls(false); }) { +, _hideControlsTimer([=] { + if (_rtmpFull || fullscreenForScreencastOnly()) { + toggleWideControls(false); + } +}) { _viewport->widget()->hide(); if (!_viewport->requireARGB32()) { _call->setNotRequireARGB32(); @@ -387,28 +391,48 @@ void Panel::initWindow() { subscribeToPeerChanges(); } + const auto wasFullscreen = std::make_shared( + (window()->windowState() & Qt::WindowFullScreen) != 0); const auto updateFullScreen = [=] { const auto state = window()->windowState(); - const auto full = (state & Qt::WindowFullScreen) - || (state & Qt::WindowMaximized); + const auto fullscreen = ((state & Qt::WindowFullScreen) != 0); + const auto maximized = ((state & Qt::WindowMaximized) != 0); + const auto full = (fullscreen || maximized); + if (!fullscreen) { + _screencastOnlyInFullscreen = false; + } else if (!*wasFullscreen && fullscreenForScreencast()) { + _screencastOnlyInFullscreen = true; + } + *wasFullscreen = fullscreen; _rtmpFull = _call->rtmp() && full; _fullScreenOrMaximized = full; + if (_subtitle && fullscreenForScreencastOnly()) { + _subtitle.destroy(); + } + updateControlsGeometry(); + if (fullscreenForScreencastOnly()) { + _hideControlsTimer.callOnce(kHideControlsTimeout); + toggleWideControls(true); + } else if (!_rtmpFull) { + _hideControlsTimer.cancel(); + toggleWideControls(_viewport->widget()->underMouse()); + } }; base::install_event_filter(window().get(), [=](not_null e) { const auto type = e->type(); if (type == QEvent::Close && handleClose()) { e->ignore(); return base::EventFilterResult::Cancel; - } else if (_call->rtmp() - && (type == QEvent::KeyPress || type == QEvent::KeyRelease)) { + } else if (type == QEvent::KeyPress || type == QEvent::KeyRelease) { const auto key = static_cast(e.get())->key(); - if (key == Qt::Key_Space) { + if (_call->rtmp() && key == Qt::Key_Space) { _call->pushToTalk( e->type() == QEvent::KeyPress, kSpacePushToTalkDelay); - } else if (key == Qt::Key_Escape - && _fullScreenOrMaximized.current()) { - toggleFullScreen(); + } else if ((type == QEvent::KeyPress) + && (key == Qt::Key_Escape) + && window()->isFullScreen()) { + toggleFullScreen(false); } } else if (type == QEvent::WindowStateChange) { updateFullScreen(); @@ -423,6 +447,12 @@ void Panel::initWindow() { if (!guard) { return (Flag::None | Flag(0)); } + if (fullscreenForScreencast() + && _viewport->widget()->geometry().contains(widgetPoint)) { + return window()->isFullScreen() + ? (Flag::None | Flag(0)) + : (Flag::FullScreen | Flag(0)); + } const auto titleRect = QRect( 0, 0, @@ -440,7 +470,11 @@ void Panel::initWindow() { } const auto shown = _window->topShownLayer(); return (!shown || !shown->geometry().contains(widgetPoint)) - ? (Flag::Move | Flag::Menu | Flag::Maximize) + ? (Flag::Move + | Flag::Menu + | (fullscreenForScreencast() + ? Flag::FullScreen + : Flag::Maximize)) : Flag::None; }); @@ -450,7 +484,8 @@ void Panel::initWindow() { }, lifetime()); _window->maximizeRequests() | rpl::on_next([=](bool maximized) { - if (_call->rtmp()) { + if (_call->rtmp() || fullscreenForScreencast()) { + _screencastOnlyInFullscreen = false; toggleFullScreen(maximized); } else { window()->setWindowState(maximized @@ -681,6 +716,9 @@ void Panel::toggleFullScreen() { } void Panel::toggleFullScreen(bool fullscreen) { + if (!fullscreen) { + _screencastOnlyInFullscreen = false; + } if (fullscreen) { window()->showFullScreen(); } else { @@ -1027,7 +1065,28 @@ void Panel::setupMembers() { ) | rpl::filter([=] { return !_rtmpFull; }) | rpl::on_next([=](bool inside) { - toggleWideControls(inside); + if (fullscreenForScreencastOnly()) { + if (inside) { + _hideControlsTimer.callOnce(kHideControlsTimeout); + toggleWideControls(true); + } else { + _hideControlsTimer.cancel(); + toggleWideControls(false); + } + } else { + _hideControlsTimer.cancel(); + toggleWideControls(inside); + } + }, _viewport->lifetime()); + + _viewport->rp()->events( + ) | rpl::filter([=](not_null e) { + return (e->type() == QEvent::MouseMove); + }) | rpl::on_next([=] { + if (fullscreenForScreencastOnly()) { + _hideControlsTimer.callOnce(kHideControlsTimeout); + toggleWideControls(true); + } }, _viewport->lifetime()); _members->show(); @@ -1051,6 +1110,16 @@ void Panel::setupMembers() { _call->changeVolume(request); }, _callLifetime); + _members->toggleScreenMuteRequests( + ) | rpl::on_next([=](MuteRequest request) { + _call->toggleScreenMute(request); + }, _callLifetime); + + _members->changeScreenVolumeRequests( + ) | rpl::on_next([=](VolumeRequest request) { + _call->changeScreenVolume(request); + }, _callLifetime); + _members->kickParticipantRequests( ) | rpl::on_next([=](not_null participantPeer) { kickParticipant(participantPeer); @@ -1080,6 +1149,7 @@ void Panel::setupMembers() { enlargeVideo(); } _viewport->showLarge(large); + updateControlsGeometry(); }, _callLifetime); } @@ -1261,6 +1331,34 @@ void Panel::setupVideo(not_null viewport) { } }, viewport->lifetime()); + if (viewport == _viewport.get()) { + viewport->doubleClicks( + ) | rpl::on_next([=](const VideoEndpoint &endpoint) { + if (endpoint.type != VideoEndpointType::Screen) { + return; + } + if (_call->videoEndpointLarge() != endpoint) { + if (_call->videoEndpointPinned()) { + _call->pinVideoEndpoint(endpoint); + } else { + _call->showVideoEndpointLarge(endpoint); + } + } + if (window()->isFullScreen()) { + _screencastOnlyInFullscreen = !_screencastOnlyInFullscreen; + if (fullscreenForScreencastOnly()) { + _hideControlsTimer.callOnce(kHideControlsTimeout); + } else { + _hideControlsTimer.cancel(); + } + updateControlsGeometry(); + } else { + _screencastOnlyInFullscreen = true; + toggleFullScreen(true); + } + }, viewport->lifetime()); + } + viewport->qualityRequests( ) | rpl::on_next([=](const VideoQualityRequest &request) { _call->requestVideoQuality(request.endpoint, request.quality); @@ -1284,7 +1382,8 @@ void Panel::updateWideControlsVisibility() { if (_wideControlsShown == shown) { return; } - _viewport->setCursorShown(!_rtmpFull || shown); + _viewport->setCursorShown( + (!_rtmpFull && !fullscreenForScreencastOnly()) || shown); _wideControlsShown = shown; _wideControlsAnimation.start( [=] { updateButtonsGeometry(); }, @@ -1889,9 +1988,9 @@ void Panel::updateButtonsStyles() { (wide ? &st::groupCallMessageActiveSmall : &st::groupCallMessageActive)); - _message->setText(wide - ? rpl::single(QString()) - : tr::lng_group_call_message()); + _message->setText(wide + ? rpl::single(QString()) + : tr::lng_group_call_message()); } if (_settings) { _settings->setText(wide @@ -2144,6 +2243,9 @@ void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) { } }); toggleWideControls(true); + if (_rtmpFull || fullscreenForScreencastOnly()) { + _hideControlsTimer.cancel(); + } } else if (type == QEvent::Leave) { *over = false; crl::on_main(widget, [=] { @@ -2152,6 +2254,9 @@ void Panel::trackControl(Ui::RpWidget *widget, rpl::lifetime &lifetime) { } }); toggleWideControls(false); + if (_rtmpFull || fullscreenForScreencastOnly()) { + _hideControlsTimer.callOnce(kHideControlsTimeout); + } } }, lifetime); } @@ -2468,13 +2573,16 @@ void Panel::updateButtonsGeometry() { + (_settings->width() + skip) + _hangup->width(); const auto membersSkip = st::groupCallNarrowSkip; - const auto membersWidth = rtmp + const auto screencastOnly = fullscreenForScreencastOnly(); + const auto membersWidth = (rtmp || screencastOnly) ? membersSkip : (st::groupCallNarrowMembersWidth + 2 * membersSkip); - auto left = membersSkip + (widget()->width() - - membersWidth - - membersSkip - - fullWidth) / 2; + auto left = screencastOnly + ? (widget()->width() - fullWidth) / 2 + : membersSkip + (widget()->width() + - membersWidth + - membersSkip + - fullWidth) / 2; const auto forMessagesLeft = left - st::groupCallControlsBackMargin.left(); @@ -2685,21 +2793,28 @@ void Panel::updateMembersGeometry() { if (!_members) { return; } - _members->setVisible(!_call->rtmp()); + _members->setVisible(!_call->rtmp() && !fullscreenForScreencastOnly()); const auto desiredHeight = _members->desiredHeight(); if (mode() == PanelMode::Wide) { - const auto skip = _rtmpFull ? 0 : st::groupCallNarrowSkip; - const auto membersWidth = st::groupCallNarrowMembersWidth; - const auto top = _rtmpFull ? 0 : st::groupCallWideVideoTop; + const auto screencastOnly = fullscreenForScreencastOnly(); + const auto skip = (_rtmpFull || screencastOnly) + ? 0 + : st::groupCallNarrowSkip; + const auto membersWidth = screencastOnly + ? 0 + : st::groupCallNarrowMembersWidth; + const auto top = (_rtmpFull || screencastOnly) + ? 0 + : st::groupCallWideVideoTop; _members->setGeometry( widget()->width() - skip - membersWidth, top, membersWidth, std::min(desiredHeight, widget()->height() - top - skip)); - const auto viewportSkip = _call->rtmp() + const auto viewportSkip = (_call->rtmp() || screencastOnly) ? 0 : (skip + membersWidth); - _viewport->setGeometry(_rtmpFull, { + _viewport->setGeometry(_rtmpFull || screencastOnly, { skip, top, widget()->width() - viewportSkip - 2 * skip, @@ -2729,6 +2844,17 @@ void Panel::updateMembersGeometry() { } } +bool Panel::fullscreenForScreencast() const { + const auto &endpoint = _call->videoEndpointLarge(); + return endpoint && (endpoint.type == VideoEndpointType::Screen); +} + +bool Panel::fullscreenForScreencastOnly() const { + return _screencastOnlyInFullscreen + && window()->isFullScreen() + && fullscreenForScreencast(); +} + rpl::producer Panel::titleText() { if (_call->conference()) { return tr::lng_confcall_join_title(); diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 6f2fa17533d7f..1e83a2e1911c0 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -153,6 +153,8 @@ class Panel final void updateControlsGeometry(); void updateButtonsGeometry(); void updateTooltipGeometry(); + [[nodiscard]] bool fullscreenForScreencast() const; + [[nodiscard]] bool fullscreenForScreencastOnly() const; void updateButtonsStyles(); void updateMembersGeometry(); void refreshControlsBackground(); @@ -210,6 +212,7 @@ class Panel final std::shared_ptr _window; rpl::variable _mode; rpl::variable _fullScreenOrMaximized = false; + bool _screencastOnlyInFullscreen = false; bool _unpinnedMaximized = false; bool _rtmpFull = false; diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp index 1db9eaf97afe1..2a332681f1924 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.cpp @@ -120,6 +120,10 @@ void Viewport::setup() { handleMousePress( static_cast(e.get())->pos(), static_cast(e.get())->button()); + } else if (type == QEvent::MouseButtonDblClick) { + handleMouseDoubleClick( + static_cast(e.get())->pos(), + static_cast(e.get())->button()); } else if (type == QEvent::MouseButtonRelease) { handleMouseRelease( static_cast(e.get())->pos(), @@ -202,10 +206,29 @@ void Viewport::handleMousePress(QPoint position, Qt::MouseButton button) { setPressed(_selected); } +void Viewport::handleMouseDoubleClick( + QPoint position, + Qt::MouseButton button) { + handleMouseMove(position); + if (button != Qt::LeftButton || videoStream()) { + return; + } + const auto tile = _selected.tile; + if (!tile) { + return; + } + _skipNextMouseRelease = true; + _doubleClicks.fire_copy(tile->endpoint()); +} + void Viewport::handleMouseRelease(QPoint position, Qt::MouseButton button) { handleMouseMove(position); const auto pressed = _pressed; setPressed({}); + if (_skipNextMouseRelease && button == Qt::LeftButton) { + _skipNextMouseRelease = false; + return; + } if (const auto tile = pressed.tile) { if (pressed == _selected) { if (videoStream()) { @@ -919,6 +942,10 @@ rpl::producer Viewport::clicks() const { return _clicks.events(); } +rpl::producer Viewport::doubleClicks() const { + return _doubleClicks.events(); +} + rpl::producer Viewport::qualityRequests() const { return _qualityRequests.events(); } diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport.h b/Telegram/SourceFiles/calls/group/calls_group_viewport.h index 7024c164be723..12e687a1bed32 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport.h +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport.h @@ -97,6 +97,7 @@ class Viewport final { [[nodiscard]] rpl::producer fullHeightValue() const; [[nodiscard]] rpl::producer pinToggled() const; [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] rpl::producer doubleClicks() const; [[nodiscard]] rpl::producer qualityRequests() const; [[nodiscard]] rpl::producer mouseInsideValue() const; @@ -179,6 +180,7 @@ class Viewport final { void setPressed(Selection value); void handleMousePress(QPoint position, Qt::MouseButton button); + void handleMouseDoubleClick(QPoint position, Qt::MouseButton button); void handleMouseRelease(QPoint position, Qt::MouseButton button); void handleMouseMove(QPoint position); void updateSelected(QPoint position); @@ -201,6 +203,7 @@ class Viewport final { int _scrollTop = 0; QImage _shadow; rpl::event_stream _clicks; + rpl::event_stream _doubleClicks; rpl::event_stream _pinToggles; rpl::event_stream _qualityRequests; float64 _controlsShownRatio = 1.; @@ -211,6 +214,7 @@ class Viewport final { Layout _finishTilesLayout; Selection _selected; Selection _pressed; + bool _skipNextMouseRelease = false; rpl::variable _mouseInside = false; Ui::RpWidgetWrap * const _borrowed = nullptr; diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp index 94ce5cded80ef..1133d65b03763 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_opengl.cpp @@ -1450,7 +1450,7 @@ void Viewport::RendererGL::validateNoiseTexture( void Viewport::RendererGL::validateOutlineAnimation( not_null tile, TileData &data) { - const auto outline = tile->row()->speaking(); + const auto outline = !_owner->_fullscreen && tile->row()->speaking(); if (data.outline == outline) { return; } diff --git a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp index 9f006fa06442c..fbef9987b46d1 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_viewport_raster.cpp @@ -186,7 +186,7 @@ void Viewport::RendererSW::paintTileOutline( int width, int height, not_null tile) { - if (!tile->row()->speaking()) { + if (_owner->_fullscreen || !tile->row()->speaking()) { return; } const auto outline = st::groupCallOutline; diff --git a/Telegram/ThirdParty/tgcalls b/Telegram/ThirdParty/tgcalls index 24876ebca7da1..cd5e5eedb42e0 160000 --- a/Telegram/ThirdParty/tgcalls +++ b/Telegram/ThirdParty/tgcalls @@ -1 +1 @@ -Subproject commit 24876ebca7da10f92dc972225734337f9e793054 +Subproject commit cd5e5eedb42e046132d5af863da38cad84234ee4 diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc index 01e157532633b..1088f5eb76440 160000 --- a/Telegram/lib_webrtc +++ b/Telegram/lib_webrtc @@ -1 +1 @@ -Subproject commit 01e157532633b5a56398f0d974c199a5769fcc39 +Subproject commit 1088f5eb764408e779942f409b72de0983336ff6