From 9a812090a22912fc6bbd8092bfe88e9acbc567ae Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 28 May 2021 13:23:24 +0400 Subject: [PATCH] Add some error tooltips in group calls. --- Telegram/Resources/langs/lang.strings | 14 +- .../calls/group/calls_group_call.cpp | 120 +++++++++++------- .../calls/group/calls_group_call.h | 13 ++ .../calls/group/calls_group_common.h | 9 ++ .../calls/group/calls_group_panel.cpp | 67 ++++++++-- .../calls/group/calls_group_panel.h | 3 + 6 files changed, 164 insertions(+), 62 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 68b3ffd18..ebcc01b40 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2004,8 +2004,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_screen_share_stop" = "Stop Sharing"; "lng_group_call_screen_title" = "Screen {index}"; "lng_group_call_unmute_small" = "Unmute"; -"lng_group_call_you_are_live_small" = "Mute"; -"lng_group_call_force_muted_small" = "Muted"; "lng_group_call_more" = "More"; "lng_group_call_unmute" = "Unmute"; "lng_group_call_unmute_sub" = "or hold spacebar to talk"; @@ -2014,7 +2012,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_force_muted_sub" = "You are in Listen Only mode"; "lng_group_call_raise_hand_tip" = "Click if you want to speak"; "lng_group_call_raised_hand" = "You asked to speak"; -"lng_group_call_raised_hand_small" = "Raised hand"; "lng_group_call_raised_hand_sub" = "We let the speakers know"; "lng_group_call_connecting" = "Connecting..."; "lng_group_call_leave" = "Leave"; @@ -2027,6 +2024,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_create_sure" = "Do you really want to start a voice chat in this group?"; "lng_group_call_create_sure_channel" = "Are you sure you want to start a voice chat in this channel as your personal account?"; "lng_group_call_join_sure_personal" = "Are you sure you want to join this voice chat as your personal account?"; +"lng_group_call_muted_no_camera" = "You can't turn on video while you're muted by admin."; +"lng_group_call_muted_no_screen" = "You can't share your screen while you're muted by admin."; +"lng_group_call_chat_no_camera" = "You can't turn on video in this chat."; +"lng_group_call_chat_no_screen" = "You can't share your screen in this chat."; +"lng_group_call_failed_screen" = "An error occured. Screencast has stopped."; +"lng_group_call_tooltip_screen" = "Share screen"; +"lng_group_call_tooltip_camera" = "Your camera is off. Click here to enable camera."; +"lng_group_call_tooltip_microphone" = "You are on mute. Click here to speak."; +"lng_group_call_tooltip_camera_off" = "Disable camera"; +"lng_group_call_tooltip_force_muted" = "Muted by admin. Click if you want to speak."; +"lng_group_call_tooltip_raised_hand" = "You asked to speak. We let the speakers know."; "lng_group_call_also_end" = "End voice chat"; "lng_group_call_settings_title" = "Settings"; "lng_group_call_invite" = "Invite Member"; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index 2a669251f..95d460623 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -457,8 +457,7 @@ GroupCall::~GroupCall() { } bool GroupCall::isSharingScreen() const { - return _screenOutgoing - && (_screenOutgoing->state() == Webrtc::VideoState::Active); + return _isSharingScreen.current(); } rpl::producer GroupCall::isSharingScreenValue() const { @@ -470,8 +469,7 @@ const std::string &GroupCall::screenSharingEndpoint() const { } bool GroupCall::isSharingCamera() const { - return _cameraOutgoing - && (_cameraOutgoing->state() == Webrtc::VideoState::Active); + return _isSharingCamera.current(); } rpl::producer GroupCall::isSharingCameraValue() const { @@ -507,12 +505,9 @@ void GroupCall::toggleVideo(bool active) { return; } ensureOutgoingVideo(); - const auto state = active + _cameraOutgoing->setState(active ? Webrtc::VideoState::Active - : Webrtc::VideoState::Inactive; - if (_cameraOutgoing->state() != state) { - _cameraOutgoing->setState(state); - } + : Webrtc::VideoState::Inactive); } void GroupCall::toggleScreenSharing(std::optional uniqueId) { @@ -529,9 +524,7 @@ void GroupCall::toggleScreenSharing(std::optional uniqueId) { } const auto changed = (_screenDeviceId != *uniqueId); _screenDeviceId = *uniqueId; - if (_screenOutgoing->state() != Webrtc::VideoState::Active) { - _screenOutgoing->setState(Webrtc::VideoState::Active); - } + _screenOutgoing->setState(Webrtc::VideoState::Active); if (changed) { _screenCapture->switchToDevice(uniqueId->toStdString()); } @@ -1110,6 +1103,9 @@ void GroupCall::rejoinPresentation() { LOG(("Call Error: " "Could not screen join, error: %1").arg(type)); _screenOutgoing->setState(Webrtc::VideoState::Inactive); + _errors.fire_copy(mutedByAdmin() + ? Error::MutedNoScreen + : Error::ScreenFailed); } }).send(); }); @@ -1669,6 +1665,48 @@ void GroupCall::setupMediaDevices() { }, _lifetime); } +bool GroupCall::emitShareCameraError() { + const auto emitError = [=](Error error) { + emitShareCameraError(error); + return true; + }; + if (const auto real = lookupReal(); real && !real->canStartVideo()) { + return emitError(Error::DisabledNoCamera); + } else if (mutedByAdmin()) { + return emitError(Error::MutedNoCamera); + } else if (Webrtc::GetVideoInputList().empty()) { + return emitError(Error::NoCamera); + } + return false; +} + +void GroupCall::emitShareCameraError(Error error) { + if (_cameraOutgoing) { + _cameraOutgoing->setState(Webrtc::VideoState::Inactive); + } + _errors.fire_copy(error); +} + +bool GroupCall::emitShareScreenError() { + const auto emitError = [=](Error error) { + emitShareScreenError(error); + return true; + }; + if (const auto real = lookupReal(); real && !real->canStartVideo()) { + return emitError(Error::DisabledNoScreen); + } else if (mutedByAdmin()) { + return emitError(Error::MutedNoScreen); + } + return false; +} + +void GroupCall::emitShareScreenError(Error error) { + if (_screenOutgoing) { + _screenOutgoing->setState(Webrtc::VideoState::Inactive); + } + _errors.fire_copy(error); +} + void GroupCall::ensureOutgoingVideo() { Expects(_id != 0); @@ -1682,33 +1720,21 @@ void GroupCall::ensureOutgoingVideo() { Webrtc::VideoState::Inactive, _requireARGB32); - using namespace rpl::mappers; - _isSharingCamera = _cameraOutgoing->stateValue( - ) | rpl::map(_1 == Webrtc::VideoState::Active); - _isSharingScreen = _screenOutgoing->stateValue( - ) | rpl::map(_1 == Webrtc::VideoState::Active); - - //static const auto hasDevices = [] { - // return !Webrtc::GetVideoInputList().empty(); - //}; _cameraOutgoing->stateValue( ) | rpl::start_with_next([=](Webrtc::VideoState state) { - //if (state != Webrtc::VideoState::Inactive && !hasDevices()) { - //_errors.fire({ ErrorType::NoCamera }); // #TODO calls - //_videoOutgoing->setState(Webrtc::VideoState::Inactive); - //} else if (state != Webrtc::VideoState::Inactive - // && _instance - // && !_instance->supportsVideo()) { - // _errors.fire({ ErrorType::NotVideoCall }); - // _videoOutgoing->setState(Webrtc::VideoState::Inactive); - /*} else */if (state != Webrtc::VideoState::Inactive) { + const auto active = (state != Webrtc::VideoState::Inactive); + if (active) { // Paused not supported right now. Assert(state == Webrtc::VideoState::Active); - if (!_cameraCapture) { + if (emitShareCameraError()) { + return; + } else if (!_cameraCapture) { _cameraCapture = _delegate->groupCallGetVideoCapture( _cameraInputId); if (!_cameraCapture) { + return emitShareCameraError(Error::NoCamera); _cameraOutgoing->setState(Webrtc::VideoState::Inactive); + _errors.fire_copy(Error::NoCamera); return; } } else { @@ -1721,31 +1747,33 @@ void GroupCall::ensureOutgoingVideo() { } else if (_cameraCapture) { _cameraCapture->setState(tgcalls::VideoState::Inactive); } - markEndpointActive({ _joinAs, _cameraEndpoint }, isSharingCamera()); + _isSharingCamera = active; + markEndpointActive({ _joinAs, _cameraEndpoint }, active); sendSelfUpdate(SendUpdateType::VideoMuted); applyMeInCallLocally(); }, _lifetime); _screenOutgoing->stateValue( ) | rpl::start_with_next([=](Webrtc::VideoState state) { - if (state != Webrtc::VideoState::Inactive) { + const auto active = (state != Webrtc::VideoState::Inactive); + if (active) { // Paused not supported right now. Assert(state == Webrtc::VideoState::Active); - if (!_screenCapture) { - _screenCapture = std::shared_ptr( - tgcalls::VideoCaptureInterface::Create( - tgcalls::StaticThreads::getThreads(), - _screenDeviceId.toStdString())); + if (emitShareScreenError()) { + return; + } else if (!_screenCapture) { + _screenCapture = std::shared_ptr< + tgcalls::VideoCaptureInterface + >(tgcalls::VideoCaptureInterface::Create( + tgcalls::StaticThreads::getThreads(), + _screenDeviceId.toStdString())); if (!_screenCapture) { - _screenOutgoing->setState(Webrtc::VideoState::Inactive); - return; + return emitShareScreenError(Error::ScreenFailed); } const auto weak = base::make_weak(this); _screenCapture->setOnFatalError([=] { crl::on_main(weak, [=] { - _screenOutgoing->setState( - Webrtc::VideoState::Inactive); - // #TODO calls show error toast, receive here device. + emitShareScreenError(Error::ScreenFailed); }); }); } else { @@ -1758,7 +1786,8 @@ void GroupCall::ensureOutgoingVideo() { } else if (_screenCapture) { _screenCapture->setState(tgcalls::VideoState::Inactive); } - markEndpointActive({ _joinAs, _screenEndpoint }, isSharingScreen()); + _isSharingScreen = active; + markEndpointActive({ _joinAs, _screenEndpoint }, active); _screenJoinState.nextActionPending = true; checkNextJoinAction(); }, _lifetime); @@ -2457,8 +2486,7 @@ void GroupCall::sendSelfUpdate(SendUpdateType type) { MTP_bool(muted() != MuteState::Active), MTP_int(100000), // volume MTP_bool(muted() == MuteState::RaisedHand), - MTP_bool(!_cameraOutgoing - || _cameraOutgoing->state() != Webrtc::VideoState::Active) + MTP_bool(!isSharingCamera()) )).done([=](const MTPUpdates &result) { _updateMuteRequestId = 0; _peer->session().api().applyUpdates(result); diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index 91cdf02a6..83a19b471 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -48,6 +48,7 @@ struct ParticipantState; struct JoinInfo; struct RejoinEvent; enum class VideoQuality; +enum class Error; } // namespace Group enum class MuteState { @@ -225,6 +226,13 @@ public: void startScheduledNow(); void toggleScheduleStartSubscribed(bool subscribed); + bool emitShareScreenError(); + bool emitShareCameraError(); + + [[nodiscard]] rpl::producer errors() const { + return _errors.events(); + } + void addVideoOutput( const std::string &endpoint, not_null track); @@ -367,6 +375,7 @@ public: private: using GlobalShortcutValue = base::GlobalShortcutValue; + using Error = Group::Error; struct SinkPointer; static constexpr uint32 kDisabledSsrc = uint32(-1); @@ -421,6 +430,9 @@ private: bool tryCreateScreencast(); void destroyScreencast(); + void emitShareCameraError(Error error); + void emitShareScreenError(Error error); + void setState(State state); void finish(FinishType type); void maybeSendMutedUpdate(MuteState previous); @@ -490,6 +502,7 @@ private: rpl::event_stream> _realChanges; rpl::variable _state = State::Creating; base::flat_set _unresolvedSsrcs; + rpl::event_stream _errors; bool _recordingStoppedByMe = false; bool _requestedVideoChannelsUpdateScheduled = false; diff --git a/Telegram/SourceFiles/calls/group/calls_group_common.h b/Telegram/SourceFiles/calls/group/calls_group_common.h index cfe6eb9bc..d4c0c0f35 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_common.h +++ b/Telegram/SourceFiles/calls/group/calls_group_common.h @@ -59,4 +59,13 @@ enum class VideoQuality { Full, }; +enum class Error { + NoCamera, + ScreenFailed, + MutedNoCamera, + MutedNoScreen, + DisabledNoCamera, + DisabledNoScreen, +}; + } // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp index 0ff128c0d..74c74de4e 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.cpp @@ -66,6 +66,7 @@ constexpr auto kRecordingOpacity = 0.6; constexpr auto kStartNoConfirmation = TimeId(10); constexpr auto kControlsBackgroundOpacity = 0.8; constexpr auto kOverrideActiveColorBgAlpha = 172; +constexpr auto kErrorDuration = 2 * crl::time(1000); class InviteController final : public ParticipantsBoxController { public: @@ -437,9 +438,7 @@ Panel::Panel(not_null call) initControls(); initLayout(); showAndActivate(); - setupJoinAsChangedToasts(); - setupTitleChangedToasts(); - setupAllowedToSpeakToasts(); + setupToasts(); } Panel::~Panel() { @@ -866,18 +865,12 @@ void Panel::setupRealMuteButtonState(not_null real) { : state == GroupCall::InstanceState::Disconnected ? tr::lng_group_call_connecting(tr::now) : mute == MuteState::ForceMuted - ? (wide - ? tr::lng_group_call_force_muted_small(tr::now) - : tr::lng_group_call_force_muted(tr::now)) + ? tr::lng_group_call_force_muted(tr::now) : mute == MuteState::RaisedHand - ? (wide - ? tr::lng_group_call_raised_hand_small(tr::now) - : tr::lng_group_call_raised_hand(tr::now)) + ? tr::lng_group_call_raised_hand(tr::now) : mute == MuteState::Muted ? tr::lng_group_call_unmute(tr::now) - : (wide - ? tr::lng_group_call_you_are_live_small(tr::now) - : tr::lng_group_call_you_are_live(tr::now))), + : tr::lng_group_call_you_are_live(tr::now)), .subtext = ((scheduleDate || wide) ? QString() : state == GroupCall::InstanceState::Disconnected @@ -1206,6 +1199,14 @@ void Panel::toggleWideControls(bool shown) { }); } +void Panel::setupToasts() { + setupJoinAsChangedToasts(); + setupTitleChangedToasts(); + setupRequestedToSpeakToasts(); + setupAllowedToSpeakToasts(); + setupErrorToasts(); +} + void Panel::setupJoinAsChangedToasts() { _call->rejoinEvents( ) | rpl::filter([](RejoinEvent event) { @@ -1270,6 +1271,46 @@ void Panel::setupAllowedToSpeakToasts() { }, widget()->lifetime()); } +void Panel::setupRequestedToSpeakToasts() { + _call->mutedValue( + ) | rpl::combine_previous( + ) | rpl::start_with_next([=](MuteState was, MuteState now) { + if (was == MuteState::ForceMuted && now == MuteState::RaisedHand) { + Ui::ShowMultilineToast({ + .parentOverride = widget(), + .text = tr::lng_group_call_tooltip_raised_hand(tr::now), + }); + } + }, widget()->lifetime()); +} + +void Panel::setupErrorToasts() { + _call->errors( + ) | rpl::start_with_next([=](Error error) { + const auto key = [&] { + switch (error) { + case Error::NoCamera: return tr::lng_call_error_no_camera; + case Error::ScreenFailed: + return tr::lng_group_call_failed_screen; + case Error::MutedNoCamera: + return tr::lng_group_call_muted_no_camera; + case Error::MutedNoScreen: + return tr::lng_group_call_muted_no_screen; + case Error::DisabledNoCamera: + return tr::lng_group_call_chat_no_camera; + case Error::DisabledNoScreen: + return tr::lng_group_call_chat_no_screen; + } + Unexpected("Error in Calls::Group::Panel::setupErrorToasts."); + }(); + Ui::ShowMultilineToast({ + .parentOverride = widget(), + .text = { key(tr::now) }, + .duration = kErrorDuration, + }); + }, widget()->lifetime()); +} + void Panel::subscribeToChanges(not_null real) { const auto validateRecordingMark = [=](bool recording) { if (!recording && _recordingMark) { @@ -1397,7 +1438,7 @@ void Panel::refreshTopButton() { } void Panel::chooseShareScreenSource() { - if (!_call->mutedByAdmin()) { + if (!_call->emitShareScreenError()) { Ui::DesktopCapture::ChooseSource(this); } } diff --git a/Telegram/SourceFiles/calls/group/calls_group_panel.h b/Telegram/SourceFiles/calls/group/calls_group_panel.h index 334c722a4..500286122 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_panel.h +++ b/Telegram/SourceFiles/calls/group/calls_group_panel.h @@ -87,9 +87,12 @@ private: void setupScheduledLabels(rpl::producer date); void setupMembers(); void setupVideo(); + void setupToasts(); void setupJoinAsChangedToasts(); void setupTitleChangedToasts(); + void setupRequestedToSpeakToasts(); void setupAllowedToSpeakToasts(); + void setupErrorToasts(); void setupRealMuteButtonState(not_null real); bool handleClose();