From b15623d4359f9889ca7c2d4c34ba6a282d3a4bf1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 23 Apr 2021 18:47:03 +0400 Subject: [PATCH] Allow pinning video to top of members list. --- Telegram/Resources/langs/lang.strings | 7 + .../calls/group/calls_group_call.cpp | 120 +++++++++-- .../calls/group/calls_group_call.h | 17 ++ .../calls/group/calls_group_members.cpp | 188 ++++++++++++++---- .../calls/group/calls_group_members.h | 12 +- Telegram/lib_webrtc | 2 +- 6 files changed, 292 insertions(+), 54 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d8d262f69c..6184a552f1 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1998,6 +1998,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_raised_hand_status" = "wants to speak"; "lng_group_call_settings" = "Settings"; "lng_group_call_share_button" = "Share"; +"lng_group_call_screen_share" = "Share"; +"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"; "lng_group_call_you_are_live" = "You are Live"; @@ -2059,6 +2064,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_context_remove_hand" = "Cancel request to speak"; "lng_group_call_context_mute_for_me" = "Mute for me"; "lng_group_call_context_unmute_for_me" = "Unmute for me"; +"lng_group_call_context_pin_video" = "Pin video"; +"lng_group_call_context_unpin_video" = "Unpin video"; "lng_group_call_context_remove" = "Remove"; "lng_group_call_remove_channel" = "Remove {channel} from the voice chat?"; "lng_group_call_duration_days#one" = "{count} day"; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index cc0b305b36..1910027c38 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -111,6 +111,14 @@ private: }; +struct GroupCall::LargeTrack { + LargeTrack() : track(Webrtc::VideoState::Active) { + } + + Webrtc::VideoTrack track; + std::shared_ptr sink; +}; + [[nodiscard]] bool IsGroupCallAdmin( not_null peer, not_null participantPeer) { @@ -427,6 +435,53 @@ void GroupCall::subscribeToReal(not_null real) { ) | rpl::start_with_next([=](TimeId date) { setScheduledDate(date); }, _lifetime); + + using Update = Data::GroupCall::ParticipantUpdate; + real->participantUpdated( + ) | rpl::start_with_next([=](const Update &data) { + const auto nowSpeaking = data.now && data.now->speaking; + const auto nowSounding = data.now && data.now->sounding; + const auto wasSpeaking = data.was && data.was->speaking; + const auto wasSounding = data.was && data.was->sounding; + if (nowSpeaking == wasSpeaking && nowSounding == wasSounding) { + return; + } else if (_videoStreamPinned) { + return; + } + const auto videoLargeSsrc = _videoStreamLarge.current(); + const auto &participants = real->participants(); + if ((wasSpeaking || wasSounding) + && (data.was->ssrc == videoLargeSsrc)) { + auto bestWithVideoSsrc = uint32(0); + for (const auto &participant : participants) { + if (!participant.sounding + || !participant.ssrc + || !_videoStreamSsrcs.contains(participant.ssrc)) { + continue; + } + if (participant.speaking) { + bestWithVideoSsrc = participant.ssrc; + break; + } else if (!bestWithVideoSsrc) { + bestWithVideoSsrc = participant.ssrc; + } + } + if (bestWithVideoSsrc) { + _videoStreamLarge = bestWithVideoSsrc; + } + } else if ((nowSpeaking || nowSounding) + && (data.now->ssrc != videoLargeSsrc)) { + const auto i = ranges::find( + participants, + videoLargeSsrc, + &Data::GroupCallParticipant::ssrc); + const auto speaking = (i != end(participants)) && i->speaking; + const auto sounding = (i != end(participants)) && i->sounding; + if ((nowSpeaking && !speaking) || (nowSounding && !sounding)) { + _videoStreamLarge = data.now->ssrc; + } + } + }, _lifetime); } void GroupCall::checkGlobalShortcutAvailability() { @@ -1531,6 +1586,22 @@ void GroupCall::ensureControllerCreated() { LOG(("Call Info: Creating group instance")); _instance = std::make_unique( std::move(descriptor)); + _videoStreamLarge.changes( + ) | rpl::start_with_next([=](uint32 ssrc) { + _instance->setFullSizeVideoSsrc(ssrc); + if (!ssrc) { + _videoLargeTrack = nullptr; + _videoLargeTrackWrap = nullptr; + return; + } + if (!_videoLargeTrackWrap) { + _videoLargeTrackWrap = std::make_unique(); + _videoLargeTrack = &_videoLargeTrackWrap->track; + } + _videoLargeTrackWrap->sink = Webrtc::CreateProxySink( + _videoLargeTrackWrap->track.sink()); + _instance->addIncomingVideoOutput(ssrc, _videoLargeTrackWrap->sink); + }, _lifetime); updateInstanceMuteState(); updateInstanceVolumes(); @@ -1641,15 +1712,14 @@ void GroupCall::requestParticipantsInformation( } void GroupCall::setVideoStreams(const std::vector &ssrcs) { + const auto real = lookupReal(); const auto large = _videoStreamLarge.current(); auto newLarge = large; if (large && !ranges::contains(ssrcs, large)) { newLarge = 0; _videoStreamPinned = 0; } - auto lastSpokeVoice = crl::time(0); auto lastSpokeVoiceSsrc = uint32(0); - auto lastSpokeAnything = crl::time(0); auto lastSpokeAnythingSsrc = uint32(0); auto removed = _videoStreamSsrcs; for (const auto ssrc : ssrcs) { @@ -1660,30 +1730,41 @@ void GroupCall::setVideoStreams(const std::vector &ssrcs) { _videoStreamSsrcs.emplace(ssrc); _streamsVideoUpdated.fire({ ssrc, true }); } - if (!newLarge) { - const auto j = _lastSpoke.find(ssrc); - if (j != end(_lastSpoke)) { - if (!lastSpokeVoiceSsrc - || lastSpokeVoice < j->second.voice) { + if (!newLarge && real) { + const auto &participants = real->participants(); + const auto i = ranges::find( + participants, + ssrc, + &Data::GroupCallParticipant::ssrc); + if (i != end(participants)) { + if (!lastSpokeVoiceSsrc && i->speaking) { lastSpokeVoiceSsrc = ssrc; - lastSpokeVoice = j->second.voice; } - if (!lastSpokeAnythingSsrc - || lastSpokeAnything < j->second.anything) { + if (!lastSpokeAnythingSsrc && i->sounding) { lastSpokeAnythingSsrc = ssrc; - lastSpokeAnything = j->second.anything; } } } } - if (!newLarge) { + if (!newLarge && real) { + const auto find = [&] { + const auto &participants = real->participants(); + for (const auto ssrc : ssrcs) { + const auto i = ranges::find( + participants, + ssrc, + &Data::GroupCallParticipant::ssrc); + if (i != end(participants)) { + return ssrc; + } + } + return std::uint32_t(0); + }; _videoStreamLarge = lastSpokeVoiceSsrc ? lastSpokeVoiceSsrc : lastSpokeAnythingSsrc ? lastSpokeAnythingSsrc - : ssrcs.empty() - ? 0 - : ssrcs.front(); + : find(); } for (const auto ssrc : removed) { _streamsVideoUpdated.fire({ ssrc, false }); @@ -1932,6 +2013,15 @@ void GroupCall::sendSelfUpdate(SendUpdateType type) { }).send(); } +void GroupCall::pinVideoStream(uint32 ssrc) { + if (!ssrc || _videoStreamSsrcs.contains(ssrc)) { + _videoStreamPinned = ssrc; + if (ssrc) { + _videoStreamLarge = ssrc; + } + } +} + void GroupCall::setCurrentAudioDevice(bool input, const QString &deviceId) { if (input) { _mediaDevices->switchToAudioInput(deviceId); diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index 7c20314f0a..a6773aa61c 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -210,12 +210,26 @@ public: -> rpl::producer { return _streamsVideoUpdated.events(); } + [[nodiscard]] bool streamsVideo(uint32 ssrc) const { + return _videoStreamSsrcs.contains(ssrc); + } + [[nodiscard]] uint32 videoStreamPinned() const { + return _videoStreamPinned; + } + void pinVideoStream(uint32 ssrc); [[nodiscard]] uint32 videoStreamLarge() const { return _videoStreamLarge.current(); } [[nodiscard]] rpl::producer videoStreamLargeValue() const { return _videoStreamLarge.value(); } + [[nodiscard]] Webrtc::VideoTrack *videoLargeTrack() const { + return _videoLargeTrack.current(); + } + [[nodiscard]] auto videoLargeTrackValue() const + -> rpl::producer { + return _videoLargeTrack.value(); + } [[nodiscard]] rpl::producer rejoinEvents() const { return _rejoinEvents.events(); } @@ -256,6 +270,7 @@ public: private: using GlobalShortcutValue = base::GlobalShortcutValue; + struct LargeTrack; struct LoadingPart { std::shared_ptr task; @@ -385,6 +400,8 @@ private: base::flat_set _videoStreamSsrcs; rpl::variable _videoStreamLarge = 0; uint32 _videoStreamPinned = 0; + std::unique_ptr _videoLargeTrackWrap; + rpl::variable _videoLargeTrack; base::flat_map _lastSpoke; rpl::event_stream _rejoinEvents; rpl::event_stream<> _allowedToSpeakNotifications; diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.cpp b/Telegram/SourceFiles/calls/group/calls_group_members.cpp index 8c78574761..c556bec5e5 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_members.cpp @@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "window/window_controller.h" // Controller::sessionController. #include "window/window_session_controller.h" +#include "media/view/media_view_pip.h" #include "webrtc/webrtc_video_track.h" #include "styles/style_calls.h" @@ -1147,7 +1148,6 @@ void MembersController::setupListChangeViewers() { } }, _lifetime); - _call->videoStreamLargeValue( ) | rpl::filter([=](uint32 largeSsrc) { return (_largeSsrc != largeSsrc); @@ -1844,6 +1844,17 @@ base::unique_qptr MembersController::createRowContextMenu( _kickParticipantRequests.fire_copy(participantPeer); }); + const auto ssrc = real->ssrc(); + if (ssrc != 0 && _call->streamsVideo(ssrc)) { + const auto pinned = (_call->videoStreamPinned() == ssrc); + const auto phrase = pinned + ? tr::lng_group_call_context_unpin_video(tr::now) + : tr::lng_group_call_context_pin_video(tr::now); + result->addAction(phrase, [=] { + _call->pinVideoStream(pinned ? 0 : ssrc); + }); + } + if (real->ssrc() != 0 && (!isMe(participantPeer) || _peer->canManageGroupCall())) { addMuteActionsToContextMenu(result, participantPeer, admin, real); @@ -2056,9 +2067,13 @@ Members::Members( : RpWidget(parent) , _call(call) , _scroll(this) -, _listController(std::make_unique(call, parent)) { +, _listController(std::make_unique(call, parent)) +, _layout(_scroll->setOwnedWidget( + object_ptr(_scroll.data()))) +, _pinnedVideo(_layout->add(object_ptr(_layout.get()))) { setupAddMember(call); setupList(); + setupPinnedVideo(); setContent(_list); setupFakeRoundCorners(); _listController->setDelegate(static_cast(this)); @@ -2083,15 +2098,17 @@ auto Members::kickParticipantRequests() const } int Members::desiredHeight() const { - const auto top = _addMember ? _addMember->height() : 0; - auto count = [&] { + const auto addMember = _addMemberButton.current(); + const auto top = _pinnedVideo->height() + + (addMember ? addMember->height() : 0); + const auto count = [&] { if (const auto real = _call->lookupReal()) { return real->fullCount(); } return 0; }(); const auto use = std::max(count, _list->fullRowsCount()); - const auto single = (_mode == PanelMode::Wide) + const auto single = (_mode.current() == PanelMode::Wide) ? (st::groupCallNarrowSize.height() + st::groupCallNarrowRowSkip) : st::groupCallMembersList.item.height; return top @@ -2137,34 +2154,33 @@ void Members::setupAddMember(not_null call) { _canAddMembers.value( ) | rpl::start_with_next([=](bool can) { if (!can) { + delete _addMemberButton.current(); _addMemberButton = nullptr; - _addMember.destroy(); updateControlsGeometry(); return; + } else if (_addMemberButton.current()) { + return; } - _addMember = Settings::CreateButton( + auto addMember = Settings::CreateButton( this, tr::lng_group_call_invite(), st::groupCallAddMember, &st::groupCallAddMemberIcon, st::groupCallAddMemberIconLeft); - _addMember->show(); - - _addMember->addClickHandler([=] { // TODO throttle(ripple duration) + addMember->show(); + addMember->addClickHandler([=] { // TODO throttle(ripple duration) _addMemberRequests.fire({}); }); - _addMemberButton = _addMember.data(); - - resizeToList(); + _addMemberButton = _layout->insert(1, std::move(addMember)); }, lifetime()); } void Members::setMode(PanelMode mode) { - if (_mode == mode) { + if (_mode.current() == mode) { return; } _mode = mode; - _list->setMode((_mode == PanelMode::Wide) + _list->setMode((mode == PanelMode::Wide) ? PeerListContent::Mode::Custom : PeerListContent::Mode::Default); } @@ -2176,25 +2192,139 @@ rpl::producer Members::fullCountValue() const { void Members::setupList() { _listController->setStyleOverrides(&st::groupCallMembersList); - _list = _scroll->setOwnedWidget(object_ptr( + _list = _layout->add(object_ptr( this, _listController.get())); - _list->heightValue( + _layout->heightValue( ) | rpl::start_with_next([=] { resizeToList(); - }, _list->lifetime()); + }, _layout->lifetime()); rpl::combine( _scroll->scrollTopValue(), _scroll->heightValue() ) | rpl::start_with_next([=](int scrollTop, int scrollHeight) { - _list->setVisibleTopBottom(scrollTop, scrollTop + scrollHeight); + _layout->setVisibleTopBottom(scrollTop, scrollTop + scrollHeight); }, _scroll->lifetime()); updateControlsGeometry(); } +void Members::setupPinnedVideo() { + using namespace rpl::mappers; + + // New video was pinned or mode changed. + rpl::merge( + _mode.changes() | rpl::filter( + _1 == PanelMode::Default + ) | rpl::to_empty, + _call->videoStreamLargeValue() | rpl::filter([=](uint32 ssrc) { + return ssrc == _call->videoStreamPinned(); + }) | rpl::to_empty + ) | rpl::start_with_next([=] { + _scroll->scrollToY(0); + }, _scroll->lifetime()); + + rpl::combine( + _mode.value(), + _call->videoLargeTrackValue() + ) | rpl::map([](PanelMode mode, Webrtc::VideoTrack *track) { + return (mode == PanelMode::Default) ? track : nullptr; + }) | rpl::distinct_until_changed( + ) | rpl::start_with_next([=](Webrtc::VideoTrack *track) { + _pinnedTrackLifetime.destroy(); + if (!track) { + _pinnedVideo->resize(_pinnedVideo->width(), 0); + return; + } + const auto frameSize = _pinnedTrackLifetime.make_state(); + const auto applyFrameSize = [=](QSize size) { + const auto width = _pinnedVideo->width(); + if (size.isEmpty() || !width) { + return; + } + const auto heightMin = (width * 9) / 16; + const auto heightMax = (width * 3) / 4; + const auto scaled = size.scaled( + QSize(width, heightMax), + Qt::KeepAspectRatio); + _pinnedVideo->resize( + width, + std::max(scaled.height(), heightMin)); + }; + track->renderNextFrame( + ) | rpl::start_with_next([=] { + const auto size = track->frameSize(); + if (size.isEmpty()) { + track->markFrameShown(); + } else { + if (*frameSize != size) { + *frameSize = size; + applyFrameSize(size); + } + _pinnedVideo->update(); + } + }, _pinnedTrackLifetime); + + _layout->widthValue( + ) | rpl::start_with_next([=] { + applyFrameSize(track->frameSize()); + }, _pinnedTrackLifetime); + + _pinnedVideo->paintRequest( + ) | rpl::start_with_next([=] { + const auto [image, rotation] + = track->frameOriginalWithRotation(); + if (image.isNull()) { + return; + } + auto p = QPainter(_pinnedVideo); + auto hq = PainterHighQualityEnabler(p); + using namespace Media::View; + const auto size = _pinnedVideo->size(); + const auto scaled = FlipSizeByRotation( + image.size(), + rotation + ).scaled(size, Qt::KeepAspectRatio); + const auto left = (size.width() - scaled.width()) / 2; + const auto top = (size.height() - scaled.height()) / 2; + const auto target = QRect(QPoint(left, top), scaled); + if (UsePainterRotation(rotation)) { + if (rotation) { + p.save(); + p.rotate(rotation); + } + p.drawImage(RotatedRect(target, rotation), image); + if (rotation) { + p.restore(); + } + } else if (rotation) { + p.drawImage(target, RotateFrameImage(image, rotation)); + } else { + p.drawImage(target, image); + } + if (left > 0) { + p.fillRect(0, 0, left, size.height(), Qt::black); + } + if (const auto right = left + scaled.width() + ; right < size.width()) { + const auto fill = size.width() - right; + p.fillRect(right, 0, fill, size.height(), Qt::black); + } + if (top > 0) { + p.fillRect(0, 0, size.width(), top, Qt::black); + } + if (const auto bottom = top + scaled.height() + ; bottom < size.height()) { + const auto fill = size.height() - bottom; + p.fillRect(0, bottom, size.width(), fill, Qt::black); + } + track->markFrameShown(); + }, _pinnedTrackLifetime); + }, lifetime()); +} + void Members::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } @@ -2203,11 +2333,8 @@ void Members::resizeToList() { if (!_list) { return; } - const auto listHeight = _list->height(); - const auto newHeight = (listHeight > 0) - ? ((_addMember ? _addMember->height() : 0) - + listHeight - + st::lineWidth) + const auto newHeight = (_list->height() > 0) + ? (_layout->height() + st::lineWidth) : 0; if (height() == newHeight) { updateControlsGeometry(); @@ -2217,17 +2344,8 @@ void Members::resizeToList() { } void Members::updateControlsGeometry() { - if (!_list) { - return; - } - auto topSkip = 0; - if (_addMember) { - _addMember->resizeToWidth(width()); - _addMember->move(0, 0); - topSkip = _addMember->height(); - } - _scroll->setGeometry(0, topSkip, width(), height() - topSkip); - _list->resizeToWidth(width()); + _scroll->setGeometry(rect()); + _layout->resizeToWidth(width()); } void Members::setupFakeRoundCorners() { diff --git a/Telegram/SourceFiles/calls/group/calls_group_members.h b/Telegram/SourceFiles/calls/group/calls_group_members.h index 6a4d5ad2cd..3383536ccc 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_members.h +++ b/Telegram/SourceFiles/calls/group/calls_group_members.h @@ -10,7 +10,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/peer_list_box.h" namespace Ui { +class RpWidget; class ScrollArea; +class VerticalLayout; class SettingsButton; } // namespace Ui @@ -74,21 +76,25 @@ private: void setupAddMember(not_null call); void resizeToList(); void setupList(); + void setupPinnedVideo(); void setupFakeRoundCorners(); void updateControlsGeometry(); const not_null _call; - PanelMode _mode = PanelMode(); + rpl::variable _mode = PanelMode(); object_ptr _scroll; std::unique_ptr _listController; - object_ptr _addMember = { nullptr }; + not_null _layout; + const not_null _pinnedVideo; rpl::variable _addMemberButton = nullptr; - ListWidget *_list = { nullptr }; + ListWidget *_list = nullptr; rpl::event_stream<> _addMemberRequests; rpl::variable _canAddMembers; + rpl::lifetime _pinnedTrackLifetime; + }; } // namespace Calls diff --git a/Telegram/lib_webrtc b/Telegram/lib_webrtc index 5270a1dbbd..86ca2dd27e 160000 --- a/Telegram/lib_webrtc +++ b/Telegram/lib_webrtc @@ -1 +1 @@ -Subproject commit 5270a1dbbdbee643e187e175f798595b4bc49996 +Subproject commit 86ca2dd27e52fa929423b61cd7861a0bc9483e28