From daef481ac0ad34083b0bbcd1f91440d9d075e384 Mon Sep 17 00:00:00 2001 From: levlam Date: Tue, 12 Jan 2021 17:05:25 +0300 Subject: [PATCH] Add group video calls support. --- CMakeLists.txt | 2 + td/generate/scheme/td_api.tl | 43 ++- td/telegram/GroupCallManager.cpp | 115 +------ td/telegram/GroupCallManager.h | 6 +- td/telegram/GroupCallParticipant.cpp | 20 +- td/telegram/GroupCallParticipant.h | 3 + td/telegram/GroupCallVideoPayload.cpp | 417 ++++++++++++++++++++++++++ td/telegram/GroupCallVideoPayload.h | 67 +++++ td/telegram/Td.cpp | 7 +- td/telegram/cli.cpp | 24 +- 10 files changed, 570 insertions(+), 134 deletions(-) create mode 100644 td/telegram/GroupCallVideoPayload.cpp create mode 100644 td/telegram/GroupCallVideoPayload.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 93f9a442f..7c783f0cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/td/generate/scheme/td_api.tl b/td/generate/scheme/td_api.tl index 07e44f605..9f71a9687 100644 --- a/td/generate/scheme/td_api.tl +++ b/td/generate/scheme/td_api.tl @@ -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 = 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 parameters:vector = 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 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 extensions:vector source_groups:vector = 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 = 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 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 diff --git a/td/telegram/GroupCallManager.cpp b/td/telegram/GroupCallManager.cpp index e1b7f9cba..e5c91bdce 100644 --- a/td/telegram/GroupCallManager.cpp +++ b/td/telegram/GroupCallManager.cpp @@ -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 &&payload, int32 audio_source, - bool is_muted, const string &invite_hash, + td_api::object_ptr &&video_payload, bool is_muted, + const string &invite_hash, Promise> &&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(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 &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(); @@ -2492,75 +2454,6 @@ void GroupCallManager::process_join_group_call_response(InputGroupCallId input_g })); } -Result> 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(); - } - - 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> 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(hash, setup, fingerprint_value)); - } - - vector> 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( - port, protocol, network, generation, id, component, foundation, priority, ip, type, tcp_type, rel_addr, - rel_port)); - } - - auto payload = td_api::make_object(ufrag, pwd, std::move(fingerprints_object)); - return td_api::make_object(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()) { diff --git a/td/telegram/GroupCallManager.h b/td/telegram/GroupCallManager.h index 7d5694b2b..19c4b9596 100644 --- a/td/telegram/GroupCallManager.h +++ b/td/telegram/GroupCallManager.h @@ -65,7 +65,8 @@ class GroupCallManager : public Actor { void start_scheduled_group_call(GroupCallId group_call_id, Promise &&promise); void join_group_call(GroupCallId group_call_id, DialogId as_dialog_id, - td_api::object_ptr &&payload, int32 audio_source, bool is_muted, + td_api::object_ptr &&payload, int32 audio_source, + td_api::object_ptr &&video_payload, bool is_muted, const string &invite_hash, Promise> &&promise); void set_group_call_title(GroupCallId group_call_id, string title, Promise &&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> 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, diff --git a/td/telegram/GroupCallParticipant.cpp b/td/telegram/GroupCallParticipant.cpp index fd4a5ec16..df07e74a4 100644 --- a/td/telegram/GroupCallParticipant.cpp +++ b/td/telegram/GroupCallParticipant.cpp @@ -55,6 +55,15 @@ GroupCallParticipant::GroupCallParticipant(const tl_object_ptrjust_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 &participant) { @@ -247,16 +256,17 @@ td_api::object_ptr GroupCallParticipant::get_group } return td_api::make_object( - 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 && diff --git a/td/telegram/GroupCallParticipant.h b/td/telegram/GroupCallParticipant.h index cbd5d65c3..8900ab1e5 100644 --- a/td/telegram/GroupCallParticipant.h +++ b/td/telegram/GroupCallParticipant.h @@ -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; diff --git a/td/telegram/GroupCallVideoPayload.cpp b/td/telegram/GroupCallVideoPayload.cpp new file mode 100644 index 000000000..77473e915 --- /dev/null +++ b/td/telegram/GroupCallVideoPayload.cpp @@ -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 get_group_call_video_payload_feedback_type_object( + const GroupCallVideoPayloadFeedbackType &feedback_type) { + return td_api::make_object(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 get_group_call_video_payload_parameter_object( + const GroupCallVideoPayloadParameter ¶meter) { + return td_api::make_object(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 get_group_call_video_payload_type_object( + const GroupCallVideoPayloadType &payload_type) { + return td_api::make_object( + 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 get_group_call_video_extension_object( + const GroupCallVideoExtension &extension) { + return td_api::make_object(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 get_group_call_video_source_group_object( + const GroupCallVideoSourceGroup &source_group) { + return td_api::make_object(vector(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 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( + 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 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 ¶meter_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 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 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(source_str)); + result.sources.push_back(source_id); + } + + return result; +} + +Result 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 encode_join_group_call_payload(td_api::object_ptr &&payload, + int32 audio_source, + td_api::object_ptr &&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 ¶meter : 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(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 &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 &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 &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 ¶meter : *parameters) { + o(parameter->name_, parameter->value_); + } + })); + } + }); + })); + o("rtp-hdrexts", json_array(video_payload->extensions_, + [](const td_api::object_ptr &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 &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> 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(); + } + + 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> 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(hash, setup, fingerprint_value)); + } + + vector> 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( + 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(server_sources_array[0].get_number())); + } + + auto payload = td_api::make_object(ufrag, pwd, std::move(fingerprints_object)); + return td_api::make_object(std::move(payload), std::move(candidates_object), + server_video_bandwidth_probing_source); +} + +} // namespace td diff --git a/td/telegram/GroupCallVideoPayload.h b/td/telegram/GroupCallVideoPayload.h new file mode 100644 index 000000000..cf1016912 --- /dev/null +++ b/td/telegram/GroupCallVideoPayload.h @@ -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 feedback_types; + vector parameters; +}; + +struct GroupCallVideoExtension { + int32 id; + string name; +}; + +struct GroupCallVideoSourceGroup { + vector sources; + string semantics; +}; + +struct GroupCallVideoPayload { + vector payload_types; + vector extensions; + vector source_groups; +}; + +bool operator==(const GroupCallVideoPayload &lhs, const GroupCallVideoPayload &rhs); + +bool operator!=(const GroupCallVideoPayload &lhs, const GroupCallVideoPayload &rhs); + +td_api::object_ptr get_group_call_video_payload_object( + const GroupCallVideoPayload &payload); + +Result get_group_call_video_payload(string json, string &endpoint); + +Result encode_join_group_call_payload(td_api::object_ptr &&payload, + int32 audio_source, + td_api::object_ptr &&video_payload); + +Result> get_group_call_join_response_object(string json); + +} // namespace td diff --git a/td/telegram/Td.cpp b/td/telegram/Td.cpp index fa88924dc..a306d53ab 100644 --- a/td/telegram/Td.cpp +++ b/td/telegram/Td.cpp @@ -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) { diff --git a/td/telegram/cli.cpp b/td/telegram/cli.cpp index fa0f548d6..a04859f90 100644 --- a/td/telegram/cli.cpp +++ b/td/telegram/cli.cpp @@ -2691,7 +2691,7 @@ class CliClient final : public Actor { } else if (op == "tgcesn" || op == "tgcesne") { send_request(td_api::make_object(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> fingerprints; fingerprints.push_back(td_api::make_object("hash", "setup", "fingerprint")); fingerprints.push_back(td_api::make_object("h2", "s2", "fingerprint2")); + + vector> feedback_types; + feedback_types.push_back( + td_api::make_object("transport-cc", "subtype1")); + feedback_types.push_back(td_api::make_object("type2", "subtype2")); + + vector> parameters; + parameters.push_back(td_api::make_object("minptime", "10")); + parameters.push_back(td_api::make_object("useinbandfec", "1")); + + auto video_payload = td_api::make_object(); + video_payload->payload_types_.push_back(td_api::make_object( + 12345, "opus", 48000, 2, std::move(feedback_types), std::move(parameters))); + video_payload->extensions_.push_back( + td_api::make_object(1, "urn:ietf:params:rtp-hdrext:ssrc-audio-level")); + video_payload->source_groups_.push_back( + td_api::make_object(vector{1, 2}, "SIM")); + video_payload->source_groups_.push_back( + td_api::make_object(vector{3, 4}, "FID")); + send_request(td_api::make_object( as_group_call_id(group_call_id), as_message_sender(participant_id), td_api::make_object("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;