600 lines
16 KiB
C++
600 lines
16 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "data/data_group_call.h"
|
|
|
|
#include "base/unixtime.h"
|
|
#include "data/data_channel.h"
|
|
#include "data/data_chat.h"
|
|
#include "data/data_changes.h"
|
|
#include "data/data_session.h"
|
|
#include "main/main_session.h"
|
|
#include "calls/calls_instance.h"
|
|
#include "calls/calls_group_call.h"
|
|
#include "calls/calls_group_common.h"
|
|
#include "core/application.h"
|
|
#include "apiwrap.h"
|
|
|
|
namespace Data {
|
|
namespace {
|
|
|
|
constexpr auto kRequestPerPage = 30;
|
|
constexpr auto kSpeakingAfterActive = crl::time(6000);
|
|
constexpr auto kActiveAfterJoined = crl::time(1000);
|
|
|
|
} // namespace
|
|
|
|
GroupCall::GroupCall(
|
|
not_null<PeerData*> peer,
|
|
uint64 id,
|
|
uint64 accessHash)
|
|
: _id(id)
|
|
, _accessHash(accessHash)
|
|
, _peer(peer)
|
|
, _speakingByActiveFinishTimer([=] { checkFinishSpeakingByActive(); }) {
|
|
}
|
|
|
|
GroupCall::~GroupCall() {
|
|
api().request(_unknownUsersRequestId).cancel();
|
|
api().request(_participantsRequestId).cancel();
|
|
api().request(_reloadRequestId).cancel();
|
|
}
|
|
|
|
uint64 GroupCall::id() const {
|
|
return _id;
|
|
}
|
|
|
|
not_null<PeerData*> GroupCall::peer() const {
|
|
return _peer;
|
|
}
|
|
|
|
MTPInputGroupCall GroupCall::input() const {
|
|
return MTP_inputGroupCall(MTP_long(_id), MTP_long(_accessHash));
|
|
}
|
|
|
|
void GroupCall::setPeer(not_null<PeerData*> peer) {
|
|
Expects(peer->migrateFrom() == _peer);
|
|
Expects(_peer->migrateTo() == peer);
|
|
|
|
_peer = peer;
|
|
}
|
|
|
|
auto GroupCall::participants() const
|
|
-> const std::vector<Participant> & {
|
|
return _participants;
|
|
}
|
|
|
|
void GroupCall::requestParticipants() {
|
|
if (_participantsRequestId || _reloadRequestId) {
|
|
return;
|
|
} else if (_participants.size() >= _fullCount.current() && _allReceived) {
|
|
return;
|
|
} else if (_allReceived) {
|
|
reload();
|
|
return;
|
|
}
|
|
_participantsRequestId = api().request(MTPphone_GetGroupParticipants(
|
|
input(),
|
|
MTP_vector<MTPint>(), // ids
|
|
MTP_vector<MTPint>(), // ssrcs
|
|
MTP_string(_nextOffset),
|
|
MTP_int(kRequestPerPage)
|
|
)).done([=](const MTPphone_GroupParticipants &result) {
|
|
result.match([&](const MTPDphone_groupParticipants &data) {
|
|
_nextOffset = qs(data.vnext_offset());
|
|
_peer->owner().processUsers(data.vusers());
|
|
applyParticipantsSlice(
|
|
data.vparticipants().v,
|
|
ApplySliceSource::SliceLoaded);
|
|
_fullCount = data.vcount().v;
|
|
if (!_allReceived
|
|
&& (data.vparticipants().v.size() < kRequestPerPage)) {
|
|
_allReceived = true;
|
|
}
|
|
if (_allReceived) {
|
|
_fullCount = _participants.size();
|
|
}
|
|
});
|
|
_participantsSliceAdded.fire({});
|
|
_participantsRequestId = 0;
|
|
changePeerEmptyCallFlag();
|
|
}).fail([=](const RPCError &error) {
|
|
_fullCount = _participants.size();
|
|
_allReceived = true;
|
|
_participantsRequestId = 0;
|
|
changePeerEmptyCallFlag();
|
|
}).send();
|
|
}
|
|
|
|
void GroupCall::changePeerEmptyCallFlag() {
|
|
const auto chat = _peer->asChat();
|
|
const auto channel = _peer->asChannel();
|
|
constexpr auto chatFlag = MTPDchat::Flag::f_call_not_empty;
|
|
constexpr auto channelFlag = MTPDchannel::Flag::f_call_not_empty;
|
|
if (_peer->groupCall() != this) {
|
|
return;
|
|
} else if (fullCount() > 0) {
|
|
if (chat && !(chat->flags() & chatFlag)) {
|
|
chat->addFlags(chatFlag);
|
|
chat->session().changes().peerUpdated(
|
|
chat,
|
|
Data::PeerUpdate::Flag::GroupCall);
|
|
} else if (channel && !(channel->flags() & channelFlag)) {
|
|
channel->addFlags(channelFlag);
|
|
channel->session().changes().peerUpdated(
|
|
channel,
|
|
Data::PeerUpdate::Flag::GroupCall);
|
|
}
|
|
} else if (chat && (chat->flags() & chatFlag)) {
|
|
chat->removeFlags(chatFlag);
|
|
chat->session().changes().peerUpdated(
|
|
chat,
|
|
Data::PeerUpdate::Flag::GroupCall);
|
|
} else if (channel && (channel->flags() & channelFlag)) {
|
|
channel->removeFlags(channelFlag);
|
|
channel->session().changes().peerUpdated(
|
|
channel,
|
|
Data::PeerUpdate::Flag::GroupCall);
|
|
}
|
|
}
|
|
|
|
int GroupCall::fullCount() const {
|
|
return _fullCount.current();
|
|
}
|
|
|
|
rpl::producer<int> GroupCall::fullCountValue() const {
|
|
return _fullCount.value();
|
|
}
|
|
|
|
bool GroupCall::participantsLoaded() const {
|
|
return _allReceived;
|
|
}
|
|
|
|
UserData *GroupCall::userBySsrc(uint32 ssrc) const {
|
|
const auto i = _userBySsrc.find(ssrc);
|
|
return (i != end(_userBySsrc)) ? i->second.get() : nullptr;
|
|
}
|
|
|
|
rpl::producer<> GroupCall::participantsSliceAdded() {
|
|
return _participantsSliceAdded.events();
|
|
}
|
|
|
|
auto GroupCall::participantUpdated() const
|
|
-> rpl::producer<ParticipantUpdate> {
|
|
return _participantUpdates.events();
|
|
}
|
|
|
|
void GroupCall::applyUpdate(const MTPGroupCall &update) {
|
|
applyCall(update, false);
|
|
}
|
|
|
|
void GroupCall::applyCall(const MTPGroupCall &call, bool force) {
|
|
call.match([&](const MTPDgroupCall &data) {
|
|
const auto changed = (_version != data.vversion().v)
|
|
|| (_fullCount.current() != data.vparticipants_count().v)
|
|
|| (_joinMuted != data.is_join_muted())
|
|
|| (_canChangeJoinMuted != data.is_can_change_join_muted());
|
|
if (!force && !changed) {
|
|
return;
|
|
} else if (!force && _version > data.vversion().v) {
|
|
reload();
|
|
return;
|
|
}
|
|
_joinMuted = data.is_join_muted();
|
|
_canChangeJoinMuted = data.is_can_change_join_muted();
|
|
_version = data.vversion().v;
|
|
_fullCount = data.vparticipants_count().v;
|
|
changePeerEmptyCallFlag();
|
|
}, [&](const MTPDgroupCallDiscarded &data) {
|
|
const auto id = _id;
|
|
const auto peer = _peer;
|
|
crl::on_main(&peer->session(), [=] {
|
|
if (peer->groupCall() && peer->groupCall()->id() == id) {
|
|
if (const auto chat = peer->asChat()) {
|
|
chat->clearGroupCall();
|
|
} else if (const auto channel = peer->asChannel()) {
|
|
channel->clearGroupCall();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
void GroupCall::reload() {
|
|
if (_reloadRequestId) {
|
|
return;
|
|
} else if (_participantsRequestId) {
|
|
api().request(_participantsRequestId).cancel();
|
|
_participantsRequestId = 0;
|
|
}
|
|
_reloadRequestId = api().request(
|
|
MTPphone_GetGroupCall(input())
|
|
).done([=](const MTPphone_GroupCall &result) {
|
|
result.match([&](const MTPDphone_groupCall &data) {
|
|
_peer->owner().processUsers(data.vusers());
|
|
_participants.clear();
|
|
_speakingByActiveFinishes.clear();
|
|
_userBySsrc.clear();
|
|
applyParticipantsSlice(
|
|
data.vparticipants().v,
|
|
ApplySliceSource::SliceLoaded);
|
|
applyCall(data.vcall(), true);
|
|
_allReceived = (_fullCount.current() == _participants.size());
|
|
_participantsSliceAdded.fire({});
|
|
});
|
|
_reloadRequestId = 0;
|
|
}).fail([=](const RPCError &error) {
|
|
_reloadRequestId = 0;
|
|
}).send();
|
|
}
|
|
|
|
void GroupCall::applyParticipantsSlice(
|
|
const QVector<MTPGroupCallParticipant> &list,
|
|
ApplySliceSource sliceSource) {
|
|
const auto amInCall = inCall();
|
|
const auto now = base::unixtime::now();
|
|
const auto speakingAfterActive = TimeId(kSpeakingAfterActive / 1000);
|
|
|
|
auto changedCount = _fullCount.current();
|
|
for (const auto &participant : list) {
|
|
participant.match([&](const MTPDgroupCallParticipant &data) {
|
|
const auto userId = data.vuser_id().v;
|
|
const auto user = _peer->owner().user(userId);
|
|
const auto i = ranges::find(
|
|
_participants,
|
|
user,
|
|
&Participant::user);
|
|
if (data.is_left()) {
|
|
if (i != end(_participants)) {
|
|
auto update = ParticipantUpdate{
|
|
.was = *i,
|
|
};
|
|
_userBySsrc.erase(i->ssrc);
|
|
_speakingByActiveFinishes.remove(user);
|
|
_participants.erase(i);
|
|
if (sliceSource != ApplySliceSource::SliceLoaded) {
|
|
_participantUpdates.fire(std::move(update));
|
|
}
|
|
}
|
|
if (changedCount > _participants.size()) {
|
|
--changedCount;
|
|
}
|
|
return;
|
|
}
|
|
const auto was = (i != end(_participants))
|
|
? std::make_optional(*i)
|
|
: std::nullopt;
|
|
const auto canSelfUnmute = !data.is_muted()
|
|
|| data.is_can_self_unmute();
|
|
const auto lastActive = data.vactive_date().value_or(
|
|
was ? was->lastActive : 0);
|
|
const auto speaking = canSelfUnmute
|
|
&& ((was ? was->speaking : false)
|
|
|| (!amInCall
|
|
&& (lastActive + speakingAfterActive > now)));
|
|
const auto volume = (was && data.is_min())
|
|
? was->volume
|
|
: data.vvolume().value_or(Calls::Group::kDefaultVolume);
|
|
const auto mutedByMe = (was && data.is_min())
|
|
? was->mutedByMe
|
|
: data.is_muted_by_you();
|
|
const auto onlyMinLoaded = data.is_min()
|
|
&& (!was || was->onlyMinLoaded);
|
|
const auto value = Participant{
|
|
.user = user,
|
|
.date = data.vdate().v,
|
|
.lastActive = lastActive,
|
|
.ssrc = uint32(data.vsource().v),
|
|
.volume = volume,
|
|
.speaking = canSelfUnmute && (was ? was->speaking : false),
|
|
.muted = data.is_muted(),
|
|
.mutedByMe = mutedByMe,
|
|
.canSelfUnmute = canSelfUnmute,
|
|
.onlyMinLoaded = onlyMinLoaded,
|
|
};
|
|
if (i == end(_participants)) {
|
|
_userBySsrc.emplace(value.ssrc, user);
|
|
_participants.push_back(value);
|
|
_peer->owner().unregisterInvitedToCallUser(_id, user);
|
|
} else {
|
|
if (i->ssrc != value.ssrc) {
|
|
_userBySsrc.erase(i->ssrc);
|
|
_userBySsrc.emplace(value.ssrc, user);
|
|
}
|
|
*i = value;
|
|
}
|
|
if (data.is_just_joined()) {
|
|
++changedCount;
|
|
}
|
|
if (sliceSource != ApplySliceSource::SliceLoaded) {
|
|
_participantUpdates.fire({
|
|
.was = was,
|
|
.now = value,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
if (sliceSource == ApplySliceSource::UpdateReceived) {
|
|
_fullCount = changedCount;
|
|
changePeerEmptyCallFlag();
|
|
}
|
|
}
|
|
|
|
void GroupCall::applyLastSpoke(
|
|
uint32 ssrc,
|
|
LastSpokeTimes when,
|
|
crl::time now) {
|
|
const auto i = _userBySsrc.find(ssrc);
|
|
if (i == end(_userBySsrc)) {
|
|
_unknownSpokenSsrcs[ssrc] = when;
|
|
requestUnknownParticipants();
|
|
return;
|
|
}
|
|
const auto j = ranges::find(_participants, i->second, &Participant::user);
|
|
Assert(j != end(_participants));
|
|
|
|
_speakingByActiveFinishes.remove(j->user);
|
|
const auto sounding = (when.anything + kSoundStatusKeptFor >= now)
|
|
&& j->canSelfUnmute;
|
|
const auto speaking = sounding
|
|
&& (when.voice + kSoundStatusKeptFor >= now);
|
|
if (j->sounding != sounding || j->speaking != speaking) {
|
|
const auto was = *j;
|
|
j->sounding = sounding;
|
|
j->speaking = speaking;
|
|
_participantUpdates.fire({
|
|
.was = was,
|
|
.now = *j,
|
|
});
|
|
}
|
|
}
|
|
|
|
void GroupCall::applyActiveUpdate(
|
|
UserId userId,
|
|
LastSpokeTimes when,
|
|
UserData *userLoaded) {
|
|
if (inCall()) {
|
|
return;
|
|
}
|
|
const auto i = userLoaded
|
|
? ranges::find(
|
|
_participants,
|
|
not_null{ userLoaded },
|
|
&Participant::user)
|
|
: _participants.end();
|
|
const auto notFound = (i == end(_participants));
|
|
const auto loadByUserId = notFound || i->onlyMinLoaded;
|
|
if (loadByUserId) {
|
|
_unknownSpokenUids[userId] = when;
|
|
requestUnknownParticipants();
|
|
}
|
|
if (notFound || !i->canSelfUnmute) {
|
|
return;
|
|
}
|
|
const auto was = std::make_optional(*i);
|
|
const auto now = crl::now();
|
|
const auto elapsed = TimeId((now - when.anything) / crl::time(1000));
|
|
const auto lastActive = base::unixtime::now() - elapsed;
|
|
const auto finishes = when.anything + kSpeakingAfterActive;
|
|
if (lastActive <= i->lastActive || finishes <= now) {
|
|
return;
|
|
}
|
|
_speakingByActiveFinishes[i->user] = finishes;
|
|
if (!_speakingByActiveFinishTimer.isActive()) {
|
|
_speakingByActiveFinishTimer.callOnce(finishes - now);
|
|
}
|
|
|
|
i->lastActive = lastActive;
|
|
i->speaking = true;
|
|
i->canSelfUnmute = true;
|
|
if (!was->speaking || !was->canSelfUnmute) {
|
|
_participantUpdates.fire({
|
|
.was = was,
|
|
.now = *i,
|
|
});
|
|
}
|
|
}
|
|
|
|
void GroupCall::checkFinishSpeakingByActive() {
|
|
const auto now = crl::now();
|
|
auto nearest = 0;
|
|
auto stop = std::vector<not_null<UserData*>>();
|
|
for (auto i = begin(_speakingByActiveFinishes); i != end(_speakingByActiveFinishes);) {
|
|
const auto when = i->second;
|
|
if (now >= when) {
|
|
stop.push_back(i->first);
|
|
i = _speakingByActiveFinishes.erase(i);
|
|
} else {
|
|
if (!nearest || nearest > when) {
|
|
nearest = when;
|
|
}
|
|
++i;
|
|
}
|
|
}
|
|
for (const auto user : stop) {
|
|
const auto i = ranges::find(_participants, user, &Participant::user);
|
|
if (i->speaking) {
|
|
const auto was = *i;
|
|
i->speaking = false;
|
|
_participantUpdates.fire({
|
|
.was = was,
|
|
.now = *i,
|
|
});
|
|
}
|
|
}
|
|
if (nearest) {
|
|
_speakingByActiveFinishTimer.callOnce(nearest - now);
|
|
}
|
|
}
|
|
|
|
void GroupCall::requestUnknownParticipants() {
|
|
if (_unknownUsersRequestId
|
|
|| (_unknownSpokenSsrcs.empty() && _unknownSpokenUids.empty())) {
|
|
return;
|
|
}
|
|
const auto ssrcs = [&] {
|
|
if (_unknownSpokenSsrcs.size() < kRequestPerPage) {
|
|
return base::take(_unknownSpokenSsrcs);
|
|
}
|
|
auto result = base::flat_map<uint32, LastSpokeTimes>();
|
|
result.reserve(kRequestPerPage);
|
|
while (result.size() < kRequestPerPage) {
|
|
const auto [ssrc, when] = _unknownSpokenSsrcs.back();
|
|
result.emplace(ssrc, when);
|
|
_unknownSpokenSsrcs.erase(_unknownSpokenSsrcs.end() - 1);
|
|
}
|
|
return result;
|
|
}();
|
|
const auto uids = [&] {
|
|
if (_unknownSpokenUids.size() + ssrcs.size() < kRequestPerPage) {
|
|
return base::take(_unknownSpokenUids);
|
|
}
|
|
auto result = base::flat_map<UserId, LastSpokeTimes>();
|
|
const auto available = (kRequestPerPage - int(ssrcs.size()));
|
|
if (available > 0) {
|
|
result.reserve(available);
|
|
while (result.size() < available) {
|
|
const auto [userId, when] = _unknownSpokenUids.back();
|
|
result.emplace(userId, when);
|
|
_unknownSpokenUids.erase(_unknownSpokenUids.end() - 1);
|
|
}
|
|
}
|
|
return result;
|
|
}();
|
|
auto ssrcInputs = QVector<MTPint>();
|
|
ssrcInputs.reserve(ssrcs.size());
|
|
for (const auto [ssrc, when] : ssrcs) {
|
|
ssrcInputs.push_back(MTP_int(ssrc));
|
|
}
|
|
auto uidInputs = QVector<MTPint>();
|
|
uidInputs.reserve(uids.size());
|
|
for (const auto [userId, when] : uids) {
|
|
uidInputs.push_back(MTP_int(userId));
|
|
}
|
|
_unknownUsersRequestId = api().request(MTPphone_GetGroupParticipants(
|
|
input(),
|
|
MTP_vector<MTPint>(uidInputs),
|
|
MTP_vector<MTPint>(ssrcInputs),
|
|
MTP_string(QString()),
|
|
MTP_int(kRequestPerPage)
|
|
)).done([=](const MTPphone_GroupParticipants &result) {
|
|
result.match([&](const MTPDphone_groupParticipants &data) {
|
|
_peer->owner().processUsers(data.vusers());
|
|
applyParticipantsSlice(
|
|
data.vparticipants().v,
|
|
ApplySliceSource::UnknownLoaded);
|
|
});
|
|
_unknownUsersRequestId = 0;
|
|
const auto now = crl::now();
|
|
for (const auto [ssrc, when] : ssrcs) {
|
|
applyLastSpoke(ssrc, when, now);
|
|
_unknownSpokenSsrcs.remove(ssrc);
|
|
}
|
|
for (const auto [userId, when] : uids) {
|
|
if (const auto user = _peer->owner().userLoaded(userId)) {
|
|
const auto isParticipant = ranges::contains(
|
|
_participants,
|
|
not_null{ user },
|
|
&Participant::user);
|
|
if (isParticipant) {
|
|
applyActiveUpdate(userId, when, user);
|
|
}
|
|
}
|
|
_unknownSpokenUids.remove(userId);
|
|
}
|
|
requestUnknownParticipants();
|
|
}).fail([=](const RPCError &error) {
|
|
_unknownUsersRequestId = 0;
|
|
for (const auto [ssrc, when] : ssrcs) {
|
|
_unknownSpokenSsrcs.remove(ssrc);
|
|
}
|
|
for (const auto [userId, when] : uids) {
|
|
_unknownSpokenUids.remove(userId);
|
|
}
|
|
requestUnknownParticipants();
|
|
}).send();
|
|
}
|
|
|
|
void GroupCall::setInCall() {
|
|
_unknownSpokenUids.clear();
|
|
if (_speakingByActiveFinishes.empty()) {
|
|
return;
|
|
}
|
|
auto restartTimer = true;
|
|
const auto latest = crl::now() + kActiveAfterJoined;
|
|
for (auto &[user, when] : _speakingByActiveFinishes) {
|
|
if (when > latest) {
|
|
when = latest;
|
|
} else {
|
|
restartTimer = false;
|
|
}
|
|
}
|
|
if (restartTimer) {
|
|
_speakingByActiveFinishTimer.callOnce(kActiveAfterJoined);
|
|
}
|
|
}
|
|
|
|
bool GroupCall::inCall() const {
|
|
const auto current = Core::App().calls().currentGroupCall();
|
|
return (current != nullptr)
|
|
&& (current->id() == _id)
|
|
&& (current->state() == Calls::GroupCall::State::Joined);
|
|
}
|
|
|
|
void GroupCall::applyUpdate(const MTPDupdateGroupCallParticipants &update) {
|
|
const auto version = update.vversion().v;
|
|
const auto applyUpdate = [&] {
|
|
if (version < _version) {
|
|
return false;
|
|
}
|
|
auto versionShouldIncrement = false;
|
|
for (const auto &participant : update.vparticipants().v) {
|
|
const auto versioned = participant.match([&](
|
|
const MTPDgroupCallParticipant &data) {
|
|
return data.is_versioned();
|
|
});
|
|
if (versioned) {
|
|
versionShouldIncrement = true;
|
|
break;
|
|
}
|
|
}
|
|
return versionShouldIncrement
|
|
? (version == _version + 1)
|
|
: (version == _version);
|
|
}();
|
|
if (!applyUpdate) {
|
|
return;
|
|
}
|
|
_version = version;
|
|
applyUpdateChecked(update);
|
|
}
|
|
|
|
void GroupCall::applyUpdateChecked(
|
|
const MTPDupdateGroupCallParticipants &update) {
|
|
applyParticipantsSlice(
|
|
update.vparticipants().v,
|
|
ApplySliceSource::UpdateReceived);
|
|
}
|
|
|
|
void GroupCall::setJoinMutedLocally(bool muted) {
|
|
_joinMuted = muted;
|
|
}
|
|
|
|
bool GroupCall::joinMuted() const {
|
|
return _joinMuted;
|
|
}
|
|
|
|
bool GroupCall::canChangeJoinMuted() const {
|
|
return _canChangeJoinMuted;
|
|
}
|
|
|
|
ApiWrap &GroupCall::api() const {
|
|
return _peer->session().api();
|
|
}
|
|
|
|
} // namespace Data
|