From 058199aa0d558ab49222cc7234dcaaae33a563be Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 29 Nov 2020 12:20:33 +0300 Subject: [PATCH] Track speaking state in participants list. --- Telegram/SourceFiles/boxes/peer_list_box.h | 2 +- .../SourceFiles/calls/calls_group_call.cpp | 88 +++++- Telegram/SourceFiles/calls/calls_group_call.h | 5 + .../SourceFiles/calls/calls_group_members.cpp | 286 ++++++++++-------- Telegram/SourceFiles/data/data_group_call.cpp | 45 ++- Telegram/SourceFiles/data/data_group_call.h | 7 +- 6 files changed, 285 insertions(+), 148 deletions(-) diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 189c379f81..fa263606fe 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -760,7 +760,7 @@ public: _content->reorderRows([&]( auto &&begin, auto &&end) { - std::sort(begin, end, [&](auto &&a, auto &&b) { + std::stable_sort(begin, end, [&](auto &&a, auto &&b) { return compare(*a, *b); }); }); diff --git a/Telegram/SourceFiles/calls/calls_group_call.cpp b/Telegram/SourceFiles/calls/calls_group_call.cpp index 0952f814b4..03ce9378cb 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/calls_group_call.cpp @@ -34,6 +34,8 @@ namespace Calls { namespace { constexpr auto kMaxInvitePerSlice = 10; +constexpr auto kCheckLastSpokeInterval = 3 * crl::time(1000); +constexpr auto kSpeakLevelThreshold = 0.2; } // namespace @@ -43,7 +45,8 @@ GroupCall::GroupCall( const MTPInputGroupCall &inputCall) : _delegate(delegate) , _channel(channel) -, _api(&_channel->session().mtp()) { +, _api(&_channel->session().mtp()) +, _lastSpokeCheckTimer([=] { checkLastSpoke(); }) { const auto id = inputCall.c_inputGroupCall().vid().v; if (id) { if (const auto call = _channel->call(); call && call->id() == id) { @@ -119,9 +122,11 @@ void GroupCall::join(const MTPInputGroupCall &inputCall) { using Update = Data::GroupCall::ParticipantUpdate; _channel->call()->participantUpdated( ) | rpl::filter([=](const Update &update) { - return (_instance != nullptr) && update.removed; + return (_instance != nullptr) && !update.now; }) | rpl::start_with_next([=](const Update &update) { - _instance->removeSsrcs({ update.participant.source }); + Expects(update.was.has_value()); + + _instance->removeSsrcs({ update.was->source }); }, _lifetime); } @@ -404,7 +409,9 @@ void GroupCall::createAndStartController() { crl::on_main(weak, [=] { setInstanceConnected(connected); }); }, .audioLevelsUpdated = [=](const AudioLevels &data) { - crl::on_main(weak, [=] { audioLevelsUpdated(data); }); + if (!data.empty()) { + crl::on_main(weak, [=] { audioLevelsUpdated(data); }); + } }, .myAudioLevelUpdated = [=](float level) { if (*myLevel != level) { // Don't send many 0 while we're muted. @@ -446,22 +453,77 @@ void GroupCall::createAndStartController() { //raw->setAudioOutputDuckingEnabled(settings.callAudioDuckingEnabled()); } -void GroupCall::myLevelUpdated(float level) { - _levelUpdates.fire(LevelUpdate{ - .source = _mySsrc, - .value = level, - .self = true - }); -} +void GroupCall::handleLevelsUpdated( + gsl::span> data) { + Expects(!data.empty()); -void GroupCall::audioLevelsUpdated( - const std::vector> &data) { + auto check = false; + auto checkNow = false; + const auto now = crl::now(); for (const auto &[source, level] : data) { _levelUpdates.fire(LevelUpdate{ .source = source, .value = level, .self = (source == _mySsrc) }); + if (level <= kSpeakLevelThreshold) { + continue; + } + + check = true; + const auto i = _lastSpoke.find(source); + if (i == _lastSpoke.end()) { + _lastSpoke.emplace(source, now); + checkNow = true; + } else { + if (i->second + kCheckLastSpokeInterval / 3 <= now) { + checkNow = true; + } + i->second = now; + } + } + if (checkNow) { + checkLastSpoke(); + } else if (check && !_lastSpokeCheckTimer.isActive()) { + _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 2); + } +} + +void GroupCall::myLevelUpdated(float level) { + const auto pair = std::pair{ _mySsrc, level }; + handleLevelsUpdated({ &pair, &pair + 1 }); +} + +void GroupCall::audioLevelsUpdated( + const std::vector> &data) { + handleLevelsUpdated(gsl::make_span(data)); +} + +void GroupCall::checkLastSpoke() { + const auto real = _channel->call(); + if (!real || real->id() != _id) { + return; + } + + auto hasRecent = false; + const auto now = crl::now(); + auto list = base::take(_lastSpoke); + for (auto i = list.begin(); i != list.end();) { + const auto [source, when] = *i; + if (when + kCheckLastSpokeInterval >= now) { + hasRecent = true; + ++i; + } else { + i = list.erase(i); + } + real->applyLastSpoke(source, when, now); + } + _lastSpoke = std::move(list); + + if (!hasRecent) { + _lastSpokeCheckTimer.cancel(); + } else if (!_lastSpokeCheckTimer.isActive()) { + _lastSpokeCheckTimer.callEach(kCheckLastSpokeInterval / 3); } } diff --git a/Telegram/SourceFiles/calls/calls_group_call.h b/Telegram/SourceFiles/calls/calls_group_call.h index 73a66149f1..ec089d1c9a 100644 --- a/Telegram/SourceFiles/calls/calls_group_call.h +++ b/Telegram/SourceFiles/calls/calls_group_call.h @@ -124,7 +124,10 @@ private: void myLevelUpdated(float level); void audioLevelsUpdated( const std::vector> &data); + void handleLevelsUpdated( + gsl::span> data); void setInstanceConnected(bool connected); + void checkLastSpoke(); [[nodiscard]] MTPInputGroupCall inputCall() const; @@ -145,6 +148,8 @@ private: std::unique_ptr _instance; rpl::event_stream _levelUpdates; + base::flat_map _lastSpoke; + base::Timer _lastSpokeCheckTimer; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/calls/calls_group_members.cpp b/Telegram/SourceFiles/calls/calls_group_members.cpp index 1a401d155c..d48596eeb8 100644 --- a/Telegram/SourceFiles/calls/calls_group_members.cpp +++ b/Telegram/SourceFiles/calls/calls_group_members.cpp @@ -30,7 +30,6 @@ namespace Calls { namespace { constexpr auto kLevelThreshold = 0.2; -constexpr auto kLevelActiveTimeout = crl::time(1000); struct UpdateLevelResult { bool levelChanged = false; @@ -49,10 +48,13 @@ public: }; void updateState(const Data::GroupCall::Participant *participant); - UpdateLevelResult updateLevel(float level); + //UpdateLevelResult updateLevel(float level); [[nodiscard]] State state() const { return _state; } + [[nodiscard]] bool speaking() const { + return _speaking; + } void addActionRipple(QPoint point, Fn updateCallback) override; void stopLastActionRipple() override; @@ -83,8 +85,7 @@ public: private: void refreshStatus() override; - void refreshStatus(crl::time now); - void resetSpeakingState(); + void setSpeaking(bool speaking); [[nodiscard]] static State ComputeState( not_null channel, @@ -95,8 +96,8 @@ private: State _state = State::Inactive; not_null _channel; not_null _st; - float _level = 0.; - crl::time _markInactiveAt = 0; + bool _speaking = false; + //float _level = 0.; std::unique_ptr _actionRipple; @@ -130,16 +131,18 @@ private: const Data::GroupCall::Participant &participant) const; void prepareRows(not_null real); - void repaintByTimer(); + //void repaintByTimer(); void setupListChangeViewers(not_null call); void subscribeToChanges(not_null real); void updateRow( - const Data::GroupCall::Participant &participant); + const std::optional &was, + const Data::GroupCall::Participant &now); void updateRow( not_null row, const Data::GroupCall::Participant *participant) const; - void updateRowLevel(not_null user, float level); + void checkSpeakingRowPosition(not_null row); + //void updateRowLevel(not_null user, float level); Row *findRow(not_null user) const; [[nodiscard]] Data::GroupCall *resolvedRealCall() const; @@ -155,8 +158,8 @@ private: rpl::variable _fullCount = 1; Ui::BoxPointer _addBox; - base::flat_map, crl::time> _repaintByTimer; - base::Timer _repaintTimer; + //base::flat_map, crl::time> _repaintByTimer; + //base::Timer _repaintTimer; rpl::lifetime _lifetime; @@ -178,54 +181,61 @@ void Row::updateState(const Data::GroupCall::Participant *participant) { setCustomStatus(QString()); } _state = State::Inactive; - resetSpeakingState(); + setSpeaking(false); } else if (!participant->muted) { _state = State::Active; + setSpeaking(participant->speaking); } else if (participant->canSelfUnmute) { _state = State::Inactive; - resetSpeakingState(); + setSpeaking(false); } else { _state = State::Muted; - resetSpeakingState(); + setSpeaking(false); } _st = ComputeIconStyle(_state); } -void Row::resetSpeakingState() { - _markInactiveAt = 0; - updateLevel(0.); +void Row::setSpeaking(bool speaking) { + if (_speaking == speaking) { + return; + } + _speaking = speaking; + refreshStatus(); + //if (!_speaking) { + // updateLevel(0.); + //} } -UpdateLevelResult Row::updateLevel(float level) { - if (_level == level) { - return UpdateLevelResult{ .nextUpdateTime = _markInactiveAt }; - } - const auto now = crl::now(); - const auto stillActive = (now < _markInactiveAt); - const auto wasActive = (_level >= kLevelThreshold) && stillActive; - const auto nowActive = (level >= kLevelThreshold); - if (nowActive) { - _markInactiveAt = now + kLevelActiveTimeout; - if (_state != State::Active) { - _state = State::Active; - _st = ComputeIconStyle(_state); - } - } - _level = level; - const auto changed = wasActive != (nowActive || stillActive); - if (!changed) { - return UpdateLevelResult{ - .levelChanged = true, - .nextUpdateTime = _markInactiveAt, - }; - } - refreshStatus(now); - return UpdateLevelResult{ - .levelChanged = true, - .stateChanged = true, - .nextUpdateTime = _markInactiveAt, - }; -} +//UpdateLevelResult Row::updateLevel(float level) { +// if (_level == level) { +// return UpdateLevelResult{ .nextUpdateTime = _markInactiveAt }; +// } +// const auto now = crl::now(); +// const auto stillActive = (now < _markInactiveAt); +// const auto wasActive = (_level >= kLevelThreshold) && stillActive; +// const auto nowActive = (level >= kLevelThreshold); +// if (nowActive) { +// _markInactiveAt = now + kLevelActiveTimeout; +// if (_state != State::Active) { +// _state = State::Active; +// _st = ComputeIconStyle(_state); +// } +// } +// _level = level; +// const auto changed = wasActive != (nowActive || stillActive); +// if (!changed) { +// return UpdateLevelResult{ +// .levelChanged = true, +// .nextUpdateTime = _markInactiveAt, +// }; +// } +// refreshStatus(now); +// return UpdateLevelResult{ +// .levelChanged = true, +// .stateChanged = true, +// .nextUpdateTime = _markInactiveAt, +// }; +//} void Row::paintAction( Painter &p, @@ -251,16 +261,11 @@ void Row::paintAction( } void Row::refreshStatus() { - refreshStatus(crl::now()); -} - -void Row::refreshStatus(crl::time now) { - const auto active = (now < _markInactiveAt); setCustomStatus( - (active + (_speaking ? tr::lng_group_call_active(tr::now) : tr::lng_group_call_inactive(tr::now)), - active); + _speaking); } Row::State Row::ComputeState( @@ -315,8 +320,8 @@ void Row::stopLastActionRipple() { MembersController::MembersController(not_null call) : _call(call) -, _channel(call->channel()) -, _repaintTimer([=] { repaintByTimer(); }) { +, _channel(call->channel()) { +//, _repaintTimer([=] { repaintByTimer(); }) { setupListChangeViewers(call); } @@ -345,21 +350,21 @@ void MembersController::setupListChangeViewers(not_null call) { } }, _lifetime); - call->levelUpdates( - ) | rpl::start_with_next([=](const LevelUpdate &update) { - const auto findUserBySource = [&](uint32 source) -> UserData* { - if (const auto real = resolvedRealCall()) { - return real->userBySource(source); - } - return nullptr; - }; - const auto user = update.self - ? _channel->session().user().get() - : findUserBySource(update.source); - if (user) { - updateRowLevel(user, update.value); - } - }, _lifetime); + //call->levelUpdates( + //) | rpl::start_with_next([=](const LevelUpdate &update) { + // const auto findUserBySource = [&](uint32 source) -> UserData* { + // if (const auto real = resolvedRealCall()) { + // return real->userBySource(source); + // } + // return nullptr; + // }; + // const auto user = update.self + // ? _channel->session().user().get() + // : findUserBySource(update.source); + // if (user) { + // updateRowLevel(user, update.value); + // } + //}, _lifetime); } void MembersController::subscribeToChanges(not_null real) { @@ -379,8 +384,10 @@ void MembersController::subscribeToChanges(not_null real) { using Update = Data::GroupCall::ParticipantUpdate; real->participantUpdated( ) | rpl::start_with_next([=](const Update &update) { - const auto user = update.participant.user; - if (update.removed) { + Expects(update.was.has_value() || update.now.has_value()); + + const auto user = update.was ? update.was->user : update.now->user; + if (!update.now) { if (const auto row = findRow(user)) { if (user->isSelf()) { row->updateState(nullptr); @@ -391,21 +398,60 @@ void MembersController::subscribeToChanges(not_null real) { } } } else { - updateRow(update.participant); + updateRow(update.was, *update.now); } }, _lifetime); } void MembersController::updateRow( - const Data::GroupCall::Participant &participant) { - if (const auto row = findRow(participant.user)) { - updateRow(row, &participant); - } else if (auto row = createRow(participant)) { - delegate()->peerListAppendRow(std::move(row)); + const std::optional &was, + const Data::GroupCall::Participant &now) { + if (const auto row = findRow(now.user)) { + if (now.speaking && (!was || !was->speaking)) { + checkSpeakingRowPosition(row); + } + updateRow(row, &now); + } else if (auto row = createRow(now)) { + if (now.speaking) { + delegate()->peerListPrependRow(std::move(row)); + } else { + delegate()->peerListAppendRow(std::move(row)); + } delegate()->peerListRefreshRows(); } } +void MembersController::checkSpeakingRowPosition(not_null row) { + // Check if there are non-speaking rows above this one. + const auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count; ++i) { + const auto above = delegate()->peerListRowAt(i); + if (above == row) { + // All rows above are speaking. + return; + } else if (!static_cast(above.get())->speaking()) { + break; + } + } + // Someone started speaking and has a non-speaking row above him. Sort. + const auto proj = [&](const PeerListRow &other) { + if (&other == row.get()) { + // Bring this new one to the top. + return 0; + } else if (static_cast(other).speaking()) { + // Bring all the speaking ones below him. + return 1; + } else { + return 2; + } + }; + delegate()->peerListSortRows([&]( + const PeerListRow &a, + const PeerListRow &b) { + return proj(a) < proj(b); + }); +} + void MembersController::updateRow( not_null row, const Data::GroupCall::Participant *participant) const { @@ -413,47 +459,47 @@ void MembersController::updateRow( delegate()->peerListUpdateRow(row); } -void MembersController::updateRowLevel( - not_null user, - float level) { - if (const auto row = findRow(user)) { - const auto result = row->updateLevel(level); - if (result.stateChanged) { - // #TODO calls reorder. - } - if (result.stateChanged) { - delegate()->peerListUpdateRow(row); - } - if (result.nextUpdateTime) { - _repaintByTimer[user] = result.nextUpdateTime; - if (!_repaintTimer.isActive()) { - _repaintTimer.callOnce(kLevelActiveTimeout); - } - } else if (_repaintByTimer.remove(user) && _repaintByTimer.empty()) { - _repaintTimer.cancel(); - } - } -} +//void MembersController::updateRowLevel( +// not_null user, +// float level) { +// if (const auto row = findRow(user)) { +// const auto result = row->updateLevel(level); +// if (result.stateChanged) { +// // #TODO calls reorder. +// } +// if (result.stateChanged) { +// delegate()->peerListUpdateRow(row); +// } +// if (result.nextUpdateTime) { +// _repaintByTimer[user] = result.nextUpdateTime; +// if (!_repaintTimer.isActive()) { +// _repaintTimer.callOnce(kLevelActiveTimeout); +// } +// } else if (_repaintByTimer.remove(user) && _repaintByTimer.empty()) { +// _repaintTimer.cancel(); +// } +// } +//} -void MembersController::repaintByTimer() { - const auto now = crl::now(); - auto next = crl::time(0); - for (auto i = begin(_repaintByTimer); i != end(_repaintByTimer);) { - if (i->second > now) { - if (!next || next > i->second) { - next = i->second; - } - } else if (const auto row = findRow(i->first)) { - delegate()->peerListUpdateRow(row); - i = _repaintByTimer.erase(i); - continue; - } - ++i; - } - if (next) { - _repaintTimer.callOnce(next - now); - } -} +//void MembersController::repaintByTimer() { +// const auto now = crl::now(); +// auto next = crl::time(0); +// for (auto i = begin(_repaintByTimer); i != end(_repaintByTimer);) { +// if (i->second > now) { +// if (!next || next > i->second) { +// next = i->second; +// } +// } else if (const auto row = findRow(i->first)) { +// delegate()->peerListUpdateRow(row); +// i = _repaintByTimer.erase(i); +// continue; +// } +// ++i; +// } +// if (next) { +// _repaintTimer.callOnce(next - now); +// } +//} Row *MembersController::findRow(not_null user) const { return static_cast(delegate()->peerListFindRow(user->id)); @@ -681,7 +727,7 @@ void GroupMembers::setupButtons(not_null call) { using namespace rpl::mappers; _addMember->showOn(Data::CanWriteValue( - call->channel() + call->channel().get() )); _addMember->addClickHandler([=] { // TODO throttle(ripple duration) _addMemberRequests.fire({}); diff --git a/Telegram/SourceFiles/data/data_group_call.cpp b/Telegram/SourceFiles/data/data_group_call.cpp index 75ca74ee0b..82347da027 100644 --- a/Telegram/SourceFiles/data/data_group_call.cpp +++ b/Telegram/SourceFiles/data/data_group_call.cpp @@ -17,6 +17,7 @@ namespace Data { namespace { constexpr auto kRequestPerPage = 30; +constexpr auto kSpeakStatusKeptFor = crl::time(1000); } // namespace @@ -186,8 +187,7 @@ void GroupCall::applyParticipantsSlice( if (data.is_left()) { if (i != end(_participants)) { auto update = ParticipantUpdate{ - .participant = *i, - .removed = true, + .was = *i, }; _userBySource.erase(i->source); _participants.erase(i); @@ -200,10 +200,15 @@ void GroupCall::applyParticipantsSlice( } return; } + const auto was = (i != end(_participants)) + ? std::make_optional(*i) + : std::nullopt; const auto value = Participant{ .user = user, .date = data.vdate().v, + .lastActive = was ? was->lastActive : 0, .source = uint32(data.vsource().v), + .speaking = !data.is_muted() && (was ? was->speaking : false), .muted = data.is_muted(), .canSelfUnmute = !data.is_muted() || data.is_can_self_unmute(), }; @@ -220,17 +225,11 @@ void GroupCall::applyParticipantsSlice( *i = value; } _participantUpdates.fire({ - .participant = value, + .was = was, + .now = value, }); }); } - ranges::sort(_participants, std::greater<>(), [](const Participant &p) { - return p.lastSpoke - ? p.lastSpoke - : p.lastActive - ? p.lastActive - : p.date; - }); _fullCount = fullCount; } @@ -248,15 +247,39 @@ void GroupCall::applyParticipantsMutes( user, &Participant::user); if (i != end(_participants)) { + const auto was = *i; i->muted = data.is_muted(); i->canSelfUnmute = !i->muted || data.is_can_self_unmute(); + if (i->muted) { + i->speaking = false; + } _participantUpdates.fire({ - .participant = *i, + .was = was, + .now = *i, }); } }); } +} +void GroupCall::applyLastSpoke(uint32 source, crl::time when, crl::time now) { + const auto i = _userBySource.find(source); + if (i == end(_userBySource)) { + // #TODO calls load participant by source from server. + return; + } + const auto j = ranges::find(_participants, i->second, &Participant::user); + Assert(j != end(_participants)); + + const auto speaking = (when + kSpeakStatusKeptFor >= now) && !j->muted; + if (j->speaking != speaking) { + const auto was = *j; + j->speaking = speaking; + _participantUpdates.fire({ + .was = was, + .now = *j, + }); + } } void GroupCall::applyUpdate(const MTPDupdateGroupCallParticipants &update) { diff --git a/Telegram/SourceFiles/data/data_group_call.h b/Telegram/SourceFiles/data/data_group_call.h index 695513b60a..1b5e04e768 100644 --- a/Telegram/SourceFiles/data/data_group_call.h +++ b/Telegram/SourceFiles/data/data_group_call.h @@ -25,14 +25,14 @@ public: not_null user; TimeId date = 0; TimeId lastActive = 0; - TimeId lastSpoke = 0; uint32 source = 0; + bool speaking = false; bool muted = false; bool canSelfUnmute = false; }; struct ParticipantUpdate { - Participant participant; - bool removed = false; + std::optional was; + std::optional now; }; [[nodiscard]] auto participants() const @@ -48,6 +48,7 @@ public: void applyUpdate(const MTPDupdateGroupCallParticipants &update); void applyUpdateChecked( const MTPDupdateGroupCallParticipants &update); + void applyLastSpoke(uint32 source, crl::time when, crl::time now); [[nodiscard]] int fullCount() const; [[nodiscard]] rpl::producer fullCountValue() const;