Add group video calls support.

This commit is contained in:
levlam 2021-01-12 17:05:25 +03:00
parent 4bb76a7b6f
commit daef481ac0
10 changed files with 570 additions and 134 deletions

View File

@ -323,6 +323,7 @@ set(TDLIB_SOURCE
td/telegram/GroupCallManager.cpp
td/telegram/GroupCallParticipant.cpp
td/telegram/GroupCallParticipantOrder.cpp
td/telegram/GroupCallVideoPayload.cpp
td/telegram/HashtagHints.cpp
td/telegram/InlineQueriesManager.cpp
td/telegram/InputDialogId.cpp
@ -503,6 +504,7 @@ set(TDLIB_SOURCE
td/telegram/GroupCallManager.h
td/telegram/GroupCallParticipant.h
td/telegram/GroupCallParticipantOrder.h
td/telegram/GroupCallVideoPayload.h
td/telegram/HashtagHints.h
td/telegram/InlineQueriesManager.h
td/telegram/InputDialogId.h

View File

@ -2174,6 +2174,25 @@ groupCallPayloadFingerprint hash:string setup:string fingerprint:string = GroupC
//@description Describes a payload for interaction with tgcalls @ufrag Value of the field ufrag @pwd Value of the field pwd @fingerprints The list of fingerprints
groupCallPayload ufrag:string pwd:string fingerprints:vector<groupCallPayloadFingerprint> = GroupCallPayload;
//@description Describes a video payload feedback type for interaction with tgcalls @type Value of the field type @subtype Value of the field subtype
groupCallVideoPayloadFeedbackType type:string subtype:string = GroupCallVideoPayloadFeedbackType;
//@description Describes a video payload parameter for interaction with tgcalls @name Parameter's name @value Parameter's value
groupCallVideoPayloadParameter name:string value:string = GroupCallVideoPayloadParameter;
//@description Describes a video payload type for interaction with tgcalls @id Video payload identifier @name Name of the video payload @clock_rate Video clock rate
//@channel_count Number of channels @feedback_types Video payload feedback types @parameters Video payload parameters
groupCallVideoPayloadType id:int32 name:string clock_rate:int32 channel_count:int32 feedback_types:vector<groupCallVideoPayloadFeedbackType> parameters:vector<groupCallVideoPayloadParameter> = GroupCallVideoPayloadType;
//@description Describes an RTP header extension for interaction with tgcalls @id Extension identifier @name Name of the extension
groupCallVideoExtension id:int32 name:string = GroupCallVideoExtension;
//@description Describes a group of video synchronization sources @sources The list of synchronization sources @semantics The semantics of sources, one of "SIM" or "FID"
groupCallVideoSourceGroup sources:vector<int32> semantics:string = GroupCallVideoSourceGroup;
//@description Describes a video payload for interaction with tgcalls @payload_types List of payload types @extensions List of RTP header extensions @source_groups List of video source groups
groupCallVideoPayload payload_types:vector<groupCallVideoPayloadType> extensions:vector<groupCallVideoExtension> source_groups:vector<groupCallVideoSourceGroup> = GroupCallVideoPayload;
//@description Describes a join response candidate for interaction with tgcalls @port Value of the field port @protocol Value of the field protocol @network Value of the field network
//@generation Value of the field generation @id Value of the field id @component Value of the field component @foundation Value of the field foundation @priority Value of the field priority
//@ip Value of the field ip @type Value of the field type @tcp_type Value of the field tcp_type @rel_addr Value of the field rel_addr @rel_port Value of the field rel_port
@ -2182,8 +2201,11 @@ groupCallJoinResponseCandidate port:string protocol:string network:string genera
//@class GroupCallJoinResponse @description Describes a group call join response
//@description Contains data needed to join the group call with WebRTC @payload Group call payload to pass to tgcalls @candidates Join response candidates to pass to tgcalls
groupCallJoinResponseWebrtc payload:groupCallPayload candidates:vector<groupCallJoinResponseCandidate> = GroupCallJoinResponse;
//@description Contains data needed to join the group call with WebRTC
//@payload Group call payload to pass to tgcalls
//@candidates Join response candidates to pass to tgcalls
//@server_video_bandwidth_probing_source Join response serverVideoBandwidthProbingSource to pass to tgcalls
groupCallJoinResponseWebrtc payload:groupCallPayload candidates:vector<groupCallJoinResponseCandidate> server_video_bandwidth_probing_source:int32 = GroupCallJoinResponse;
//@description Describes that group call needs to be joined as a stream
groupCallJoinResponseStream = GroupCallJoinResponse;
@ -2191,7 +2213,9 @@ groupCallJoinResponseStream = GroupCallJoinResponse;
//@description Represents a group call participant
//@participant_id Identifier of the group call participant
//@source User's synchronization source
//@audio_source User's audio synchronization source
//@endpoint_id User's endpoint identifier
//@video_payload Description of user's video payload; may be null
//@bio The participant user's bio or the participant chat's description
//@is_current_user True, if the participant is the current user
//@is_speaking True, if the participant is speaking as set by setGroupCallParticipantIsSpeaking
@ -2205,7 +2229,7 @@ groupCallJoinResponseStream = GroupCallJoinResponse;
//@can_unmute_self True, if the participant is muted for all users, but can unmute themself
//@volume_level Participant's volume level; 1-20000 in hundreds of percents
//@order User's order in the group call participant list. Orders must be compared lexicographically. The bigger is order, the higher is user in the list. If order is empty, the user must be removed from the participant list
groupCallParticipant participant_id:MessageSender source:int32 bio:string is_current_user:Bool is_speaking:Bool is_hand_raised:Bool can_be_muted_for_all_users:Bool can_be_unmuted_for_all_users:Bool can_be_muted_for_current_user:Bool can_be_unmuted_for_current_user:Bool is_muted_for_all_users:Bool is_muted_for_current_user:Bool can_unmute_self:Bool volume_level:int32 order:string = GroupCallParticipant;
groupCallParticipant participant_id:MessageSender audio_source:int32 endpoint_id:string video_payload:groupCallVideoPayload bio:string is_current_user:Bool is_speaking:Bool is_hand_raised:Bool can_be_muted_for_all_users:Bool can_be_unmuted_for_all_users:Bool can_be_muted_for_current_user:Bool can_be_unmuted_for_current_user:Bool is_muted_for_all_users:Bool is_muted_for_current_user:Bool can_unmute_self:Bool volume_level:int32 order:string = GroupCallParticipant;
//@class CallProblem @description Describes the exact type of a problem with a call
@ -4633,10 +4657,11 @@ toggleGroupCallEnabledStartNotification group_call_id:int32 enabled_start_notifi
//@group_call_id Group call identifier
//@participant_id Identifier of a group call participant, which will be used to join the call; voice chats only
//@payload Group join payload; received from tgcalls
//@source Caller synchronization source identifier; received from tgcalls
//@audio_source Caller audio synchronization source identifier; received from tgcalls
//@video_payload Group join video payload, received from tgcalls. Use null if video is not supported
//@is_muted True, if the user's microphone is muted
//@invite_hash If non-empty, invite hash to be used to join the group call without being muted by administrators
joinGroupCall group_call_id:int32 participant_id:MessageSender payload:groupCallPayload source:int32 is_muted:Bool invite_hash:string = GroupCallJoinResponse;
joinGroupCall group_call_id:int32 participant_id:MessageSender payload:groupCallPayload audio_source:int32 video_payload:groupCallVideoPayload is_muted:Bool invite_hash:string = GroupCallJoinResponse;
//@description Sets group call title. Requires groupCall.can_be_managed group call flag @group_call_id Group call identifier @title New group call title; 1-64 characters
setGroupCallTitle group_call_id:int32 title:string = Ok;
@ -4663,9 +4688,9 @@ startGroupCallRecording group_call_id:int32 title:string = Ok;
//@description Ends recording of an active group call. Requires groupCall.can_be_managed group call flag @group_call_id Group call identifier
endGroupCallRecording group_call_id:int32 = Ok;
//@description Informs TDLib that a participant of an active group call speaking state has changed @group_call_id Group call identifier
//@source Group call participant's synchronization source identifier, or 0 for the current user @is_speaking True, if the user is speaking
setGroupCallParticipantIsSpeaking group_call_id:int32 source:int32 is_speaking:Bool = Ok;
//@description Informs TDLib that speaking state of a participant of an active group has changed @group_call_id Group call identifier
//@audio_source Group call participant's synchronization audio source identifier, or 0 for the current user @is_speaking True, if the user is speaking
setGroupCallParticipantIsSpeaking group_call_id:int32 audio_source:int32 is_speaking:Bool = Ok;
//@description Toggles whether a participant of an active group call is muted, unmuted, or allowed to unmute themself
//@group_call_id Group call identifier @participant_id Participant identifier @is_muted Pass true if the user must be muted and false otherwise

View File

@ -20,7 +20,6 @@
#include "td/utils/algorithm.h"
#include "td/utils/buffer.h"
#include "td/utils/JsonBuilder.h"
#include "td/utils/logging.h"
#include "td/utils/misc.h"
#include "td/utils/Random.h"
@ -2275,7 +2274,8 @@ void GroupCallManager::start_scheduled_group_call(GroupCallId group_call_id, Pro
void GroupCallManager::join_group_call(GroupCallId group_call_id, DialogId as_dialog_id,
td_api::object_ptr<td_api::groupCallPayload> &&payload, int32 audio_source,
bool is_muted, const string &invite_hash,
td_api::object_ptr<td_api::groupCallVideoPayload> &&video_payload, bool is_muted,
const string &invite_hash,
Promise<td_api::object_ptr<td_api::GroupCallJoinResponse>> &&promise) {
TRY_RESULT_PROMISE(promise, input_group_call_id, get_input_group_call_id(group_call_id));
@ -2320,52 +2320,14 @@ void GroupCallManager::join_group_call(GroupCallId group_call_id, DialogId as_di
}
}
if (audio_source == 0) {
return promise.set_error(Status::Error(400, "Audio source must be non-zero"));
}
if (payload == nullptr) {
return promise.set_error(Status::Error(400, "Payload must be non-empty"));
}
if (!clean_input_string(payload->ufrag_)) {
return promise.set_error(Status::Error(400, "Payload ufrag must be encoded in UTF-8"));
}
if (!clean_input_string(payload->pwd_)) {
return promise.set_error(Status::Error(400, "Payload pwd must be encoded in UTF-8"));
}
for (auto &fingerprint : payload->fingerprints_) {
if (fingerprint == nullptr) {
return promise.set_error(Status::Error(400, "Payload fingerprint must be non-empty"));
}
if (!clean_input_string(fingerprint->hash_)) {
return promise.set_error(Status::Error(400, "Fingerprint hash must be encoded in UTF-8"));
}
if (!clean_input_string(fingerprint->setup_)) {
return promise.set_error(Status::Error(400, "Fingerprint setup must be encoded in UTF-8"));
}
if (!clean_input_string(fingerprint->fingerprint_)) {
return promise.set_error(Status::Error(400, "Fingerprint must be encoded in UTF-8"));
}
}
TRY_RESULT_PROMISE(promise, json_payload,
encode_join_group_call_payload(std::move(payload), audio_source, std::move(video_payload)));
if (group_call->is_being_left) {
group_call->is_being_left = false;
need_update |= group_call->is_joined;
}
auto json_payload = json_encode<string>(json_object([&payload, audio_source](auto &o) {
o("ufrag", payload->ufrag_);
o("pwd", payload->pwd_);
o("fingerprints", json_array(payload->fingerprints_,
[](const td_api::object_ptr<td_api::groupCallPayloadFingerprint> &fingerprint) {
return json_object([&fingerprint](auto &o) {
o("hash", fingerprint->hash_);
o("setup", fingerprint->setup_);
o("fingerprint", fingerprint->fingerprint_);
});
}));
o("ssrc", audio_source);
}));
auto generation = ++join_group_request_generation_;
auto &request = pending_join_requests_[input_group_call_id];
request = make_unique<PendingJoinRequest>();
@ -2492,75 +2454,6 @@ void GroupCallManager::process_join_group_call_response(InputGroupCallId input_g
}));
}
Result<td_api::object_ptr<td_api::GroupCallJoinResponse>> GroupCallManager::get_group_call_join_response_object(
string json_response) {
auto r_value = json_decode(json_response);
if (r_value.is_error()) {
return Status::Error("Can't parse JSON object");
}
auto value = r_value.move_as_ok();
if (value.type() != JsonValue::Type::Object) {
return Status::Error("Expected an Object");
}
auto &value_object = value.get_object();
auto r_stream = get_json_object_bool_field(value_object, "stream");
if (r_stream.is_ok() && r_stream.ok() == true) {
return td_api::make_object<td_api::groupCallJoinResponseStream>();
}
TRY_RESULT(transport, get_json_object_field(value_object, "transport", JsonValue::Type::Object, false));
CHECK(transport.type() == JsonValue::Type::Object);
auto &transport_object = transport.get_object();
TRY_RESULT(candidates, get_json_object_field(transport_object, "candidates", JsonValue::Type::Array, false));
TRY_RESULT(fingerprints, get_json_object_field(transport_object, "fingerprints", JsonValue::Type::Array, false));
TRY_RESULT(ufrag, get_json_object_string_field(transport_object, "ufrag", false));
TRY_RESULT(pwd, get_json_object_string_field(transport_object, "pwd", false));
// skip "xmlns", "rtcp-mux"
vector<td_api::object_ptr<td_api::groupCallPayloadFingerprint>> fingerprints_object;
for (auto &fingerprint : fingerprints.get_array()) {
if (fingerprint.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as fingerprint");
}
auto &fingerprint_object = fingerprint.get_object();
TRY_RESULT(hash, get_json_object_string_field(fingerprint_object, "hash", false));
TRY_RESULT(setup, get_json_object_string_field(fingerprint_object, "setup", false));
TRY_RESULT(fingerprint_value, get_json_object_string_field(fingerprint_object, "fingerprint", false));
fingerprints_object.push_back(
td_api::make_object<td_api::groupCallPayloadFingerprint>(hash, setup, fingerprint_value));
}
vector<td_api::object_ptr<td_api::groupCallJoinResponseCandidate>> candidates_object;
for (auto &candidate : candidates.get_array()) {
if (candidate.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as candidate");
}
auto &candidate_object = candidate.get_object();
TRY_RESULT(port, get_json_object_string_field(candidate_object, "port", false));
TRY_RESULT(protocol, get_json_object_string_field(candidate_object, "protocol", false));
TRY_RESULT(network, get_json_object_string_field(candidate_object, "network", false));
TRY_RESULT(generation, get_json_object_string_field(candidate_object, "generation", false));
TRY_RESULT(id, get_json_object_string_field(candidate_object, "id", false));
TRY_RESULT(component, get_json_object_string_field(candidate_object, "component", false));
TRY_RESULT(foundation, get_json_object_string_field(candidate_object, "foundation", false));
TRY_RESULT(priority, get_json_object_string_field(candidate_object, "priority", false));
TRY_RESULT(ip, get_json_object_string_field(candidate_object, "ip", false));
TRY_RESULT(type, get_json_object_string_field(candidate_object, "type", false));
TRY_RESULT(tcp_type, get_json_object_string_field(candidate_object, "tcptype"));
TRY_RESULT(rel_addr, get_json_object_string_field(candidate_object, "rel-addr"));
TRY_RESULT(rel_port, get_json_object_string_field(candidate_object, "rel-port"));
candidates_object.push_back(td_api::make_object<td_api::groupCallJoinResponseCandidate>(
port, protocol, network, generation, id, component, foundation, priority, ip, type, tcp_type, rel_addr,
rel_port));
}
auto payload = td_api::make_object<td_api::groupCallPayload>(ufrag, pwd, std::move(fingerprints_object));
return td_api::make_object<td_api::groupCallJoinResponseWebrtc>(std::move(payload), std::move(candidates_object));
}
bool GroupCallManager::on_join_group_call_response(InputGroupCallId input_group_call_id, string json_response) {
auto it = pending_join_requests_.find(input_group_call_id);
if (it == pending_join_requests_.end()) {

View File

@ -65,7 +65,8 @@ class GroupCallManager : public Actor {
void start_scheduled_group_call(GroupCallId group_call_id, Promise<Unit> &&promise);
void join_group_call(GroupCallId group_call_id, DialogId as_dialog_id,
td_api::object_ptr<td_api::groupCallPayload> &&payload, int32 audio_source, bool is_muted,
td_api::object_ptr<td_api::groupCallPayload> &&payload, int32 audio_source,
td_api::object_ptr<td_api::groupCallVideoPayload> &&video_payload, bool is_muted,
const string &invite_hash, Promise<td_api::object_ptr<td_api::GroupCallJoinResponse>> &&promise);
void set_group_call_title(GroupCallId group_call_id, string title, Promise<Unit> &&promise);
@ -288,9 +289,6 @@ class GroupCallManager : public Actor {
DialogId set_group_call_participant_is_speaking_by_source(InputGroupCallId input_group_call_id, int32 audio_source,
bool is_speaking, int32 date);
static Result<td_api::object_ptr<td_api::GroupCallJoinResponse>> get_group_call_join_response_object(
string json_response);
bool try_clear_group_call_participants(InputGroupCallId input_group_call_id);
bool set_group_call_participant_count(GroupCall *group_call, int32 count, const char *source,

View File

@ -55,6 +55,15 @@ GroupCallParticipant::GroupCallParticipant(const tl_object_ptr<telegram_api::gro
is_just_joined = participant->just_joined_;
is_min = participant->min_;
version = call_version;
if (participant->params_ != nullptr) {
auto r_video_payload = get_group_call_video_payload(participant->params_->data_, endpoint);
if (r_video_payload.is_error()) {
LOG(ERROR) << "Failed to parse GroupCallParticipant params: " << r_video_payload.error();
} else {
video_payload = r_video_payload.move_as_ok();
}
}
}
bool GroupCallParticipant::is_versioned_update(const tl_object_ptr<telegram_api::groupCallParticipant> &participant) {
@ -247,16 +256,17 @@ td_api::object_ptr<td_api::groupCallParticipant> GroupCallParticipant::get_group
}
return td_api::make_object<td_api::groupCallParticipant>(
td->messages_manager_->get_message_sender_object(dialog_id), audio_source, about, is_self, is_speaking,
get_is_hand_raised(), can_be_muted_for_all_users, can_be_unmuted_for_all_users, can_be_muted_only_for_self,
td->messages_manager_->get_message_sender_object(dialog_id), audio_source, endpoint,
get_group_call_video_payload_object(video_payload), about, is_self, is_speaking, get_is_hand_raised(),
can_be_muted_for_all_users, can_be_unmuted_for_all_users, can_be_muted_only_for_self,
can_be_unmuted_only_for_self, get_is_muted_for_all_users(), get_is_muted_locally(), get_is_muted_by_themselves(),
get_volume_level(), order.get_group_call_participant_order_object());
}
bool operator==(const GroupCallParticipant &lhs, const GroupCallParticipant &rhs) {
return lhs.dialog_id == rhs.dialog_id && lhs.audio_source == rhs.audio_source && lhs.about == rhs.about &&
lhs.is_self == rhs.is_self && lhs.is_speaking == rhs.is_speaking &&
lhs.get_is_hand_raised() == rhs.get_is_hand_raised() &&
return lhs.dialog_id == rhs.dialog_id && lhs.audio_source == rhs.audio_source && lhs.endpoint == rhs.endpoint &&
lhs.video_payload == rhs.video_payload && lhs.about == rhs.about && lhs.is_self == rhs.is_self &&
lhs.is_speaking == rhs.is_speaking && lhs.get_is_hand_raised() == rhs.get_is_hand_raised() &&
lhs.can_be_muted_for_all_users == rhs.can_be_muted_for_all_users &&
lhs.can_be_unmuted_for_all_users == rhs.can_be_unmuted_for_all_users &&
lhs.can_be_muted_only_for_self == rhs.can_be_muted_only_for_self &&

View File

@ -8,6 +8,7 @@
#include "td/telegram/DialogId.h"
#include "td/telegram/GroupCallParticipantOrder.h"
#include "td/telegram/GroupCallVideoPayload.h"
#include "td/telegram/td_api.h"
#include "td/telegram/telegram_api.h"
@ -21,6 +22,8 @@ class Td;
struct GroupCallParticipant {
DialogId dialog_id;
string about;
GroupCallVideoPayload video_payload;
string endpoint;
int32 audio_source = 0;
int32 joined_date = 0;
int32 active_date = 0;

View File

@ -0,0 +1,417 @@
//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/telegram/GroupCallVideoPayload.h"
#include "td/telegram/misc.h"
#include "td/utils/algorithm.h"
#include "td/utils/JsonBuilder.h"
#include "td/utils/misc.h"
namespace td {
static bool operator==(const GroupCallVideoPayloadFeedbackType &lhs, const GroupCallVideoPayloadFeedbackType &rhs) {
return lhs.type == rhs.type && lhs.subtype == rhs.subtype;
}
static td_api::object_ptr<td_api::groupCallVideoPayloadFeedbackType> get_group_call_video_payload_feedback_type_object(
const GroupCallVideoPayloadFeedbackType &feedback_type) {
return td_api::make_object<td_api::groupCallVideoPayloadFeedbackType>(feedback_type.type, feedback_type.subtype);
}
static bool operator==(const GroupCallVideoPayloadParameter &lhs, const GroupCallVideoPayloadParameter &rhs) {
return lhs.name == rhs.name && lhs.value == rhs.value;
}
static td_api::object_ptr<td_api::groupCallVideoPayloadParameter> get_group_call_video_payload_parameter_object(
const GroupCallVideoPayloadParameter &parameter) {
return td_api::make_object<td_api::groupCallVideoPayloadParameter>(parameter.name, parameter.value);
}
static bool operator==(const GroupCallVideoPayloadType &lhs, const GroupCallVideoPayloadType &rhs) {
return lhs.id == rhs.id && lhs.name == rhs.name && lhs.clock_rate == rhs.clock_rate &&
lhs.channel_count == rhs.channel_count && lhs.feedback_types == rhs.feedback_types &&
lhs.parameters == rhs.parameters;
}
static td_api::object_ptr<td_api::groupCallVideoPayloadType> get_group_call_video_payload_type_object(
const GroupCallVideoPayloadType &payload_type) {
return td_api::make_object<td_api::groupCallVideoPayloadType>(
payload_type.id, payload_type.name, payload_type.clock_rate, payload_type.channel_count,
transform(payload_type.feedback_types, get_group_call_video_payload_feedback_type_object),
transform(payload_type.parameters, get_group_call_video_payload_parameter_object));
}
static bool operator==(const GroupCallVideoExtension &lhs, const GroupCallVideoExtension &rhs) {
return lhs.id == rhs.id && lhs.name == rhs.name;
}
static td_api::object_ptr<td_api::groupCallVideoExtension> get_group_call_video_extension_object(
const GroupCallVideoExtension &extension) {
return td_api::make_object<td_api::groupCallVideoExtension>(extension.id, extension.name);
}
static bool operator==(const GroupCallVideoSourceGroup &lhs, const GroupCallVideoSourceGroup &rhs) {
return lhs.sources == rhs.sources && lhs.semantics == rhs.semantics;
}
static td_api::object_ptr<td_api::groupCallVideoSourceGroup> get_group_call_video_source_group_object(
const GroupCallVideoSourceGroup &source_group) {
return td_api::make_object<td_api::groupCallVideoSourceGroup>(vector<int32>(source_group.sources),
source_group.semantics);
}
bool operator==(const GroupCallVideoPayload &lhs, const GroupCallVideoPayload &rhs) {
return lhs.payload_types == rhs.payload_types && lhs.extensions == rhs.extensions &&
lhs.source_groups == rhs.source_groups;
}
bool operator!=(const GroupCallVideoPayload &lhs, const GroupCallVideoPayload &rhs) {
return !(lhs == rhs);
}
td_api::object_ptr<td_api::groupCallVideoPayload> get_group_call_video_payload_object(
const GroupCallVideoPayload &payload) {
if (payload.payload_types.empty() && payload.extensions.empty() && payload.source_groups.empty()) {
return nullptr;
}
return td_api::make_object<td_api::groupCallVideoPayload>(
transform(payload.payload_types, get_group_call_video_payload_type_object),
transform(payload.extensions, get_group_call_video_extension_object),
transform(payload.source_groups, get_group_call_video_source_group_object));
}
static Result<GroupCallVideoPayloadType> get_group_call_video_payload_type(JsonValue &&value) {
if (value.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as payload type");
}
GroupCallVideoPayloadType result;
auto &value_object = value.get_object();
TRY_RESULT_ASSIGN(result.id, get_json_object_int_field(value_object, "id", false));
TRY_RESULT_ASSIGN(result.name, get_json_object_string_field(value_object, "name", false));
TRY_RESULT_ASSIGN(result.clock_rate, get_json_object_int_field(value_object, "clockrate", false));
TRY_RESULT_ASSIGN(result.channel_count, get_json_object_int_field(value_object, "channels"));
TRY_RESULT(feedback_types, get_json_object_field(value_object, "rtcp-fbs", JsonValue::Type::Array));
TRY_RESULT(parameters, get_json_object_field(value_object, "parameters", JsonValue::Type::Object));
if (feedback_types.type() != JsonValue::Type::Null) {
CHECK(feedback_types.type() == JsonValue::Type::Array);
for (auto &feedback_type_value : feedback_types.get_array()) {
if (feedback_type_value.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as feedback type");
}
auto &feedback_type_object = feedback_type_value.get_object();
GroupCallVideoPayloadFeedbackType feedback_type;
TRY_RESULT_ASSIGN(feedback_type.type, get_json_object_string_field(feedback_type_object, "type", false));
TRY_RESULT_ASSIGN(feedback_type.subtype, get_json_object_string_field(feedback_type_object, "subtype"));
result.feedback_types.push_back(std::move(feedback_type));
}
}
if (parameters.type() != JsonValue::Type::Null) {
CHECK(parameters.type() == JsonValue::Type::Object);
for (auto &parameter_value : parameters.get_object()) {
GroupCallVideoPayloadParameter parameter;
parameter.name = parameter_value.first.str();
if (parameter_value.second.type() == JsonValue::Type::String) {
parameter.value = parameter_value.second.get_string().str();
} else if (parameter_value.second.type() == JsonValue::Type::Number) {
parameter.value = parameter_value.second.get_number().str();
} else {
return Status::Error("Receive unexpected parameter type");
}
result.parameters.push_back(std::move(parameter));
}
}
return result;
}
static Result<GroupCallVideoExtension> get_group_call_video_extension(JsonValue &&value) {
if (value.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as RTP header extension");
}
GroupCallVideoExtension result;
auto &value_object = value.get_object();
TRY_RESULT_ASSIGN(result.id, get_json_object_int_field(value_object, "id", false));
TRY_RESULT_ASSIGN(result.name, get_json_object_string_field(value_object, "uri", false));
return result;
}
static Result<GroupCallVideoSourceGroup> get_group_call_video_source_group(JsonValue &&value) {
if (value.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as synchronization source group");
}
GroupCallVideoSourceGroup result;
auto &value_object = value.get_object();
TRY_RESULT(sources, get_json_object_field(value_object, "sources", JsonValue::Type::Array, false));
TRY_RESULT_ASSIGN(result.semantics, get_json_object_string_field(value_object, "semantics", false));
for (auto &source : sources.get_array()) {
Slice source_str;
if (source.type() == JsonValue::Type::String) {
source_str = source.get_string();
} else if (source.type() == JsonValue::Type::Number) {
source_str = source.get_number();
}
TRY_RESULT(source_id, to_integer_safe<int32>(source_str));
result.sources.push_back(source_id);
}
return result;
}
Result<GroupCallVideoPayload> get_group_call_video_payload(string json, string &endpoint) {
auto r_value = json_decode(json);
if (r_value.is_error()) {
return Status::Error("Can't parse JSON object");
}
auto value = r_value.move_as_ok();
if (value.type() != JsonValue::Type::Object) {
return Status::Error("Expected an Object");
}
auto &value_object = value.get_object();
TRY_RESULT_ASSIGN(endpoint, get_json_object_string_field(value_object, "endpoint", false));
TRY_RESULT(payload_types, get_json_object_field(value_object, "payload-types", JsonValue::Type::Array, false));
TRY_RESULT(extensions, get_json_object_field(value_object, "rtp-hdrexts", JsonValue::Type::Array, false));
TRY_RESULT(source_groups, get_json_object_field(value_object, "ssrc-groups", JsonValue::Type::Array, false));
GroupCallVideoPayload result;
for (auto &payload_type_object : payload_types.get_array()) {
TRY_RESULT(payload_type, get_group_call_video_payload_type(std::move(payload_type_object)));
result.payload_types.push_back(std::move(payload_type));
}
for (auto &extension_object : extensions.get_array()) {
TRY_RESULT(extension, get_group_call_video_extension(std::move(extension_object)));
result.extensions.push_back(std::move(extension));
}
for (auto &source_group_object : source_groups.get_array()) {
TRY_RESULT(source_group, get_group_call_video_source_group(std::move(source_group_object)));
result.source_groups.push_back(std::move(source_group));
}
return result;
}
Result<string> encode_join_group_call_payload(td_api::object_ptr<td_api::groupCallPayload> &&payload,
int32 audio_source,
td_api::object_ptr<td_api::groupCallVideoPayload> &&video_payload) {
if (payload == nullptr) {
return Status::Error(400, "Payload must be non-empty");
}
if (!clean_input_string(payload->ufrag_)) {
return Status::Error(400, "Payload ufrag must be encoded in UTF-8");
}
if (!clean_input_string(payload->pwd_)) {
return Status::Error(400, "Payload pwd must be encoded in UTF-8");
}
for (auto &fingerprint : payload->fingerprints_) {
if (fingerprint == nullptr) {
return Status::Error(400, "Payload fingerprint must be non-empty");
}
if (!clean_input_string(fingerprint->hash_)) {
return Status::Error(400, "Fingerprint hash must be encoded in UTF-8");
}
if (!clean_input_string(fingerprint->setup_)) {
return Status::Error(400, "Fingerprint setup must be encoded in UTF-8");
}
if (!clean_input_string(fingerprint->fingerprint_)) {
return Status::Error(400, "Fingerprint must be encoded in UTF-8");
}
}
if (audio_source == 0) {
return Status::Error(400, "Audio synchronization source must be non-zero");
}
if (video_payload != nullptr) {
for (auto &payload_type : video_payload->payload_types_) {
if (!clean_input_string(payload_type->name_)) {
return Status::Error(400, "Video payload type name must be encoded in UTF-8");
}
for (auto &feedback_type : payload_type->feedback_types_) {
if (!clean_input_string(feedback_type->type_)) {
return Status::Error(400, "Video feedback type must be encoded in UTF-8");
}
if (!clean_input_string(feedback_type->subtype_)) {
return Status::Error(400, "Video feedback subtype must be encoded in UTF-8");
}
}
for (auto &parameter : payload_type->parameters_) {
if (!clean_input_string(parameter->name_)) {
return Status::Error(400, "Video parameter name must be encoded in UTF-8");
}
if (!clean_input_string(parameter->value_)) {
return Status::Error(400, "Video parameter value must be encoded in UTF-8");
}
}
}
for (auto &extension : video_payload->extensions_) {
if (!clean_input_string(extension->name_)) {
return Status::Error(400, "RTP header extension name must be encoded in UTF-8");
}
}
for (auto &source_group : video_payload->source_groups_) {
if (!clean_input_string(source_group->semantics_)) {
return Status::Error(400, "Video source group semantics must be encoded in UTF-8");
}
}
}
return json_encode<string>(json_object([&payload, audio_source, &video_payload](auto &o) {
o("ufrag", payload->ufrag_);
o("pwd", payload->pwd_);
o("fingerprints", json_array(payload->fingerprints_,
[](const td_api::object_ptr<td_api::groupCallPayloadFingerprint> &fingerprint) {
return json_object([&fingerprint](auto &o) {
o("hash", fingerprint->hash_);
o("setup", fingerprint->setup_);
o("fingerprint", fingerprint->fingerprint_);
});
}));
o("ssrc", audio_source);
if (video_payload != nullptr) {
o("payload-types",
json_array(video_payload->payload_types_,
[](const td_api::object_ptr<td_api::groupCallVideoPayloadType> &payload_type) {
return json_object([&payload_type](auto &o) {
o("id", payload_type->id_);
o("name", payload_type->name_);
o("clockrate", payload_type->clock_rate_);
o("channels", payload_type->channel_count_);
if (!payload_type->feedback_types_.empty()) {
o("rtcp-fbs",
json_array(
payload_type->feedback_types_,
[](const td_api::object_ptr<td_api::groupCallVideoPayloadFeedbackType> &feedback_type) {
return json_object([&feedback_type](auto &o) {
o("type", feedback_type->type_);
if (!feedback_type->subtype_.empty()) {
o("subtype", feedback_type->subtype_);
}
});
}));
}
if (!payload_type->parameters_.empty()) {
o("parameters", json_object([parameters = &payload_type->parameters_](auto &o) {
for (auto &parameter : *parameters) {
o(parameter->name_, parameter->value_);
}
}));
}
});
}));
o("rtp-hdrexts", json_array(video_payload->extensions_,
[](const td_api::object_ptr<td_api::groupCallVideoExtension> &extension) {
return json_object([&extension](auto &o) {
o("id", extension->id_);
o("uri", extension->name_);
});
}));
o("ssrc-groups", json_array(video_payload->source_groups_,
[](const td_api::object_ptr<td_api::groupCallVideoSourceGroup> &source_group) {
return json_object([&source_group](auto &o) {
o("sources",
json_array(source_group->sources_, [](int32 source) { return source; }));
o("semantics", source_group->semantics_);
});
}));
}
}));
}
Result<td_api::object_ptr<td_api::GroupCallJoinResponse>> get_group_call_join_response_object(string json) {
auto r_value = json_decode(json);
if (r_value.is_error()) {
return Status::Error("Can't parse JSON object");
}
auto value = r_value.move_as_ok();
if (value.type() != JsonValue::Type::Object) {
return Status::Error("Expected an Object");
}
auto &value_object = value.get_object();
auto r_stream = get_json_object_bool_field(value_object, "stream");
if (r_stream.is_ok() && r_stream.ok() == true) {
return td_api::make_object<td_api::groupCallJoinResponseStream>();
}
TRY_RESULT(transport, get_json_object_field(value_object, "transport", JsonValue::Type::Object, false));
CHECK(transport.type() == JsonValue::Type::Object);
auto &transport_object = transport.get_object();
TRY_RESULT(candidates, get_json_object_field(transport_object, "candidates", JsonValue::Type::Array, false));
TRY_RESULT(fingerprints, get_json_object_field(transport_object, "fingerprints", JsonValue::Type::Array, false));
TRY_RESULT(ufrag, get_json_object_string_field(transport_object, "ufrag", false));
TRY_RESULT(pwd, get_json_object_string_field(transport_object, "pwd", false));
// skip "xmlns", "rtcp-mux"
vector<td_api::object_ptr<td_api::groupCallPayloadFingerprint>> fingerprints_object;
for (auto &fingerprint : fingerprints.get_array()) {
if (fingerprint.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as fingerprint");
}
auto &fingerprint_object = fingerprint.get_object();
TRY_RESULT(hash, get_json_object_string_field(fingerprint_object, "hash", false));
TRY_RESULT(setup, get_json_object_string_field(fingerprint_object, "setup", false));
TRY_RESULT(fingerprint_value, get_json_object_string_field(fingerprint_object, "fingerprint", false));
fingerprints_object.push_back(
td_api::make_object<td_api::groupCallPayloadFingerprint>(hash, setup, fingerprint_value));
}
vector<td_api::object_ptr<td_api::groupCallJoinResponseCandidate>> candidates_object;
for (auto &candidate : candidates.get_array()) {
if (candidate.type() != JsonValue::Type::Object) {
return Status::Error("Expected JSON object as candidate");
}
auto &candidate_object = candidate.get_object();
TRY_RESULT(port, get_json_object_string_field(candidate_object, "port", false));
TRY_RESULT(protocol, get_json_object_string_field(candidate_object, "protocol", false));
TRY_RESULT(network, get_json_object_string_field(candidate_object, "network", false));
TRY_RESULT(generation, get_json_object_string_field(candidate_object, "generation", false));
TRY_RESULT(id, get_json_object_string_field(candidate_object, "id", false));
TRY_RESULT(component, get_json_object_string_field(candidate_object, "component", false));
TRY_RESULT(foundation, get_json_object_string_field(candidate_object, "foundation", false));
TRY_RESULT(priority, get_json_object_string_field(candidate_object, "priority", false));
TRY_RESULT(ip, get_json_object_string_field(candidate_object, "ip", false));
TRY_RESULT(type, get_json_object_string_field(candidate_object, "type", false));
TRY_RESULT(tcp_type, get_json_object_string_field(candidate_object, "tcptype"));
TRY_RESULT(rel_addr, get_json_object_string_field(candidate_object, "rel-addr"));
TRY_RESULT(rel_port, get_json_object_string_field(candidate_object, "rel-port"));
candidates_object.push_back(td_api::make_object<td_api::groupCallJoinResponseCandidate>(
port, protocol, network, generation, id, component, foundation, priority, ip, type, tcp_type, rel_addr,
rel_port));
}
TRY_RESULT(video, get_json_object_field(value_object, "video", JsonValue::Type::Object, true));
int32 server_video_bandwidth_probing_source = 0;
if (video.type() == JsonValue::Type::Object) {
auto &video_object = video.get_object();
TRY_RESULT(server_sources, get_json_object_field(video_object, "server_sources", JsonValue::Type::Array, false));
auto &server_sources_array = server_sources.get_array();
if (server_sources_array.empty()) {
return Status::Error("Expected at least one server source");
}
if (server_sources_array[0].type() != JsonValue::Type::Number) {
return Status::Error("Expected Number as server source");
}
TRY_RESULT_ASSIGN(server_video_bandwidth_probing_source,
to_integer_safe<int32>(server_sources_array[0].get_number()));
}
auto payload = td_api::make_object<td_api::groupCallPayload>(ufrag, pwd, std::move(fingerprints_object));
return td_api::make_object<td_api::groupCallJoinResponseWebrtc>(std::move(payload), std::move(candidates_object),
server_video_bandwidth_probing_source);
}
} // namespace td

View File

@ -0,0 +1,67 @@
//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2021
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#pragma once
#include "td/telegram/td_api.h"
#include "td/telegram/telegram_api.h"
#include "td/utils/common.h"
#include "td/utils/Status.h"
namespace td {
struct GroupCallVideoPayloadFeedbackType {
string type;
string subtype;
};
struct GroupCallVideoPayloadParameter {
string name;
string value;
};
struct GroupCallVideoPayloadType {
int32 id;
string name;
int32 clock_rate;
int32 channel_count;
vector<GroupCallVideoPayloadFeedbackType> feedback_types;
vector<GroupCallVideoPayloadParameter> parameters;
};
struct GroupCallVideoExtension {
int32 id;
string name;
};
struct GroupCallVideoSourceGroup {
vector<int32> sources;
string semantics;
};
struct GroupCallVideoPayload {
vector<GroupCallVideoPayloadType> payload_types;
vector<GroupCallVideoExtension> extensions;
vector<GroupCallVideoSourceGroup> source_groups;
};
bool operator==(const GroupCallVideoPayload &lhs, const GroupCallVideoPayload &rhs);
bool operator!=(const GroupCallVideoPayload &lhs, const GroupCallVideoPayload &rhs);
td_api::object_ptr<td_api::groupCallVideoPayload> get_group_call_video_payload_object(
const GroupCallVideoPayload &payload);
Result<GroupCallVideoPayload> get_group_call_video_payload(string json, string &endpoint);
Result<string> encode_join_group_call_payload(td_api::object_ptr<td_api::groupCallPayload> &&payload,
int32 audio_source,
td_api::object_ptr<td_api::groupCallVideoPayload> &&video_payload);
Result<td_api::object_ptr<td_api::GroupCallJoinResponse>> get_group_call_join_response_object(string json);
} // namespace td

View File

@ -6023,7 +6023,8 @@ void Td::on_request(uint64 id, td_api::joinGroupCall &request) {
CREATE_REQUEST_PROMISE();
group_call_manager_->join_group_call(
GroupCallId(request.group_call_id_), group_call_manager_->get_group_call_participant_id(request.participant_id_),
std::move(request.payload_), request.source_, request.is_muted_, request.invite_hash_, std::move(promise));
std::move(request.payload_), request.audio_source_, std::move(request.video_payload_), request.is_muted_,
request.invite_hash_, std::move(promise));
}
void Td::on_request(uint64 id, td_api::setGroupCallTitle &request) {
@ -6086,8 +6087,8 @@ void Td::on_request(uint64 id, const td_api::endGroupCallRecording &request) {
void Td::on_request(uint64 id, const td_api::setGroupCallParticipantIsSpeaking &request) {
CHECK_IS_USER();
CREATE_OK_REQUEST_PROMISE();
group_call_manager_->set_group_call_participant_is_speaking(GroupCallId(request.group_call_id_), request.source_,
request.is_speaking_, std::move(promise));
group_call_manager_->set_group_call_participant_is_speaking(
GroupCallId(request.group_call_id_), request.audio_source_, request.is_speaking_, std::move(promise));
}
void Td::on_request(uint64 id, const td_api::toggleGroupCallParticipantIsMuted &request) {

View File

@ -2691,7 +2691,7 @@ class CliClient final : public Actor {
} else if (op == "tgcesn" || op == "tgcesne") {
send_request(td_api::make_object<td_api::toggleGroupCallEnabledStartNotification>(as_group_call_id(args),
op == "tgcesne"));
} else if (op == "jgc") {
} else if (op == "jgc" || op == "jgcv") {
string group_call_id;
string participant_id;
string invite_hash;
@ -2699,10 +2699,30 @@ class CliClient final : public Actor {
vector<td_api::object_ptr<td_api::groupCallPayloadFingerprint>> fingerprints;
fingerprints.push_back(td_api::make_object<td_api::groupCallPayloadFingerprint>("hash", "setup", "fingerprint"));
fingerprints.push_back(td_api::make_object<td_api::groupCallPayloadFingerprint>("h2", "s2", "fingerprint2"));
vector<td_api::object_ptr<td_api::groupCallVideoPayloadFeedbackType>> feedback_types;
feedback_types.push_back(
td_api::make_object<td_api::groupCallVideoPayloadFeedbackType>("transport-cc", "subtype1"));
feedback_types.push_back(td_api::make_object<td_api::groupCallVideoPayloadFeedbackType>("type2", "subtype2"));
vector<td_api::object_ptr<td_api::groupCallVideoPayloadParameter>> parameters;
parameters.push_back(td_api::make_object<td_api::groupCallVideoPayloadParameter>("minptime", "10"));
parameters.push_back(td_api::make_object<td_api::groupCallVideoPayloadParameter>("useinbandfec", "1"));
auto video_payload = td_api::make_object<td_api::groupCallVideoPayload>();
video_payload->payload_types_.push_back(td_api::make_object<td_api::groupCallVideoPayloadType>(
12345, "opus", 48000, 2, std::move(feedback_types), std::move(parameters)));
video_payload->extensions_.push_back(
td_api::make_object<td_api::groupCallVideoExtension>(1, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"));
video_payload->source_groups_.push_back(
td_api::make_object<td_api::groupCallVideoSourceGroup>(vector<int32>{1, 2}, "SIM"));
video_payload->source_groups_.push_back(
td_api::make_object<td_api::groupCallVideoSourceGroup>(vector<int32>{3, 4}, "FID"));
send_request(td_api::make_object<td_api::joinGroupCall>(
as_group_call_id(group_call_id), as_message_sender(participant_id),
td_api::make_object<td_api::groupCallPayload>("ufrag", "pwd", std::move(fingerprints)), group_call_source_,
true, invite_hash));
op == "jgc" ? nullptr : std::move(video_payload), true, invite_hash));
} else if (op == "sgct") {
string chat_id;
string title;