From 8909b654d3437bd9c45d823bb9702acee863a879 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Sat, 26 Feb 2022 15:47:33 +0300 Subject: [PATCH] Added ability to start livestream with RTMP. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 12 + Telegram/SourceFiles/calls/calls.style | 30 ++ Telegram/SourceFiles/calls/calls_instance.cpp | 16 +- Telegram/SourceFiles/calls/calls_instance.h | 5 +- .../calls/group/calls_group_call.cpp | 7 +- .../calls/group/calls_group_call.h | 2 +- .../calls/group/calls_group_rtmp.cpp | 295 ++++++++++++++++++ .../calls/group/calls_group_rtmp.h | 60 ++++ 9 files changed, 423 insertions(+), 6 deletions(-) create mode 100644 Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp create mode 100644 Telegram/SourceFiles/calls/group/calls_group_rtmp.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 8ba40c259..c9d14d104 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -280,6 +280,8 @@ PRIVATE calls/group/calls_group_menu.h calls/group/calls_group_panel.cpp calls/group/calls_group_panel.h + calls/group/calls_group_rtmp.cpp + calls/group/calls_group_rtmp.h calls/group/calls_group_settings.cpp calls/group/calls_group_settings.h calls/group/calls_group_toasts.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index a2fe88506..06fe5d795 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2381,6 +2381,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_group_call_join_as_changed" = "Members of this voice chat will now see you as {name}"; "lng_group_call_join_as_changed_channel" = "Members of this live stream will now see you as {name}"; +"lng_group_call_rtmp_title" = "Stream with other apps"; +"lng_group_call_rtmp_url_subtitle" = "Server URL"; +"lng_group_call_rtmp_url_copy" = "Copy Server URL"; +"lng_group_call_rtmp_url_copied" = "Server URL copied to clipboard."; +"lng_group_call_rtmp_key_subtitle" = "Stream Key"; +"lng_group_call_rtmp_key_copy" = "Copy Stream Key"; +"lng_group_call_rtmp_key_copied" = "Stream Key copied to clipboard."; +"lng_group_call_rtmp_key_warn" = "Never share your Stream Key with anyone or show it on stream!"; +"lng_group_call_rtmp_info" = "To stream video with another app, enter these Server URL and Stream Key in your streaming app.\n\nOnce you start broadcasting in your streaming app, tap Start Streaming below"; +"lng_group_call_rtmp_start" = "Start Streaming"; +"lng_group_call_rtmp_revoke_sure" = "Are you sure you want to revoke your Server URL and Stream Key?"; + "lng_no_mic_permission" = "Telegram needs access to your microphone so that you can make calls and record voice messages."; "lng_player_message_today" = "Today at {time}"; diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index b57e8c817..ff66fad93 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -1349,3 +1349,33 @@ groupCallRecordingSubLabelMargins: margins(8px, 22px, 8px, 22px); groupCallRecordingAudioSkip: 23px; groupCallRecordingSelectWidth: 2px; groupCallRecordingInfoHeight: 204px; + +groupCallRtmpCopyButton: RoundButton(defaultActiveButton) { + height: 32px; + width: -26px; + + textTop: 7px; + padding: margins(0px, 12px, 0px, 15px); +} +groupCallRtmpShowButton: IconButton(defaultIconButton) { + width: 32px; + height: 32px; + + icon: icon {{ "menu/show_in_chat", menuIconFg }}; + iconOver: icon {{ "menu/show_in_chat", menuIconFgOver }}; + iconPosition: point(4px, 4px); + + rippleAreaPosition: point(0px, 0px); + rippleAreaSize: 32px; + ripple: RippleAnimation(defaultRippleAnimation) { + color: windowBgOver; + } +} +groupCallRtmpUrlSkip: 1px; +groupCallRtmpKeySubsectionTitleSkip: 8px; +groupCallRtmpSubsectionTitleAddPadding: margins(0px, -1px, 0px, -4px); +groupCallRtmpShowButtonPosition: point(21px, -5px); +groupCallRtmpKeyLabel: FlatLabel(boxLabel) { + minWidth: 230px; +} +groupCallRtmpTopBarMenuPosition: point(-2px, -15px); diff --git a/Telegram/SourceFiles/calls/calls_instance.cpp b/Telegram/SourceFiles/calls/calls_instance.cpp index c044c9d0d..229aec683 100644 --- a/Telegram/SourceFiles/calls/calls_instance.cpp +++ b/Telegram/SourceFiles/calls/calls_instance.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "calls/group/calls_group_common.h" #include "calls/group/calls_choose_join_as.h" #include "calls/group/calls_group_call.h" +#include "calls/group/calls_group_rtmp.h" #include "mtproto/mtproto_dh_utils.h" #include "core/application.h" #include "main/main_session.h" @@ -170,7 +171,8 @@ FnMut Instance::Delegate::groupCallAddAsyncWaiter() { Instance::Instance() : _delegate(std::make_unique(this)) , _cachedDhConfig(std::make_unique()) -, _chooseJoinAs(std::make_unique()) { +, _chooseJoinAs(std::make_unique()) +, _startWithRtmp(std::make_unique()) { } Instance::~Instance() { @@ -202,6 +204,18 @@ void Instance::startOrJoinGroupCall( not_null peer, const StartGroupCallArgs &args) { using JoinConfirm = StartGroupCallArgs::JoinConfirm; + if (args.rtmpNeeded) { + _startWithRtmp->start(peer, [=](object_ptr box) { + Ui::show(std::move(box), Ui::LayerOption::KeepOther); + }, [=](QString text) { + Ui::Toast::Show(text); + }, [=](Group::JoinInfo info) { + createGroupCall( + std::move(info), + MTP_inputGroupCall(MTPlong(), MTPlong())); + }); + return; + } const auto context = (args.confirm == JoinConfirm::Always) ? Group::ChooseJoinAsProcess::Context::JoinWithConfirm : peer->groupCall() diff --git a/Telegram/SourceFiles/calls/calls_instance.h b/Telegram/SourceFiles/calls/calls_instance.h index bbfaf5bc3..7bfe56970 100644 --- a/Telegram/SourceFiles/calls/calls_instance.h +++ b/Telegram/SourceFiles/calls/calls_instance.h @@ -29,6 +29,7 @@ namespace Calls::Group { struct JoinInfo; class Panel; class ChooseJoinAsProcess; +class StartRtmpProcess; } // namespace Calls::Group namespace tgcalls { @@ -52,7 +53,8 @@ struct StartGroupCallArgs { QString joinHash; JoinConfirm confirm = JoinConfirm::IfNowInAnother; bool scheduleNeeded = false; - bool rtmp = false; + bool rtmpNeeded = false; + bool useRtmp = false; }; class Instance final : public base::has_weak_ptr { @@ -150,6 +152,7 @@ private: base::flat_map> _tracks; const std::unique_ptr _chooseJoinAs; + const std::unique_ptr _startWithRtmp; base::flat_set> _asyncWaiters; diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.cpp b/Telegram/SourceFiles/calls/group/calls_group_call.cpp index ba50719bb..07f542cc5 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.cpp +++ b/Telegram/SourceFiles/calls/group/calls_group_call.cpp @@ -639,7 +639,7 @@ GroupCall::GroupCall( if (_id) { join(inputCall); } else { - start(info.scheduleDate); + start(info.scheduleDate, info.rtmp); } if (_scheduleDate) { saveDefaultJoinAs(joinAs()); @@ -1013,10 +1013,11 @@ rpl::producer> GroupCall::real() const { return _realChanges.events(); } -void GroupCall::start(TimeId scheduleDate) { +void GroupCall::start(TimeId scheduleDate, bool rtmp) { using Flag = MTPphone_CreateGroupCall::Flag; _createRequestId = _api.request(MTPphone_CreateGroupCall( - MTP_flags(scheduleDate ? Flag::f_schedule_date : Flag(0)), + MTP_flags((scheduleDate ? Flag::f_schedule_date : Flag(0)) + | (rtmp ? Flag::f_rtmp_stream : Flag(0))), _peer->input, MTP_int(base::RandomValue()), MTPstring(), // title diff --git a/Telegram/SourceFiles/calls/group/calls_group_call.h b/Telegram/SourceFiles/calls/group/calls_group_call.h index be358ea0e..be11ac4ba 100644 --- a/Telegram/SourceFiles/calls/group/calls_group_call.h +++ b/Telegram/SourceFiles/calls/group/calls_group_call.h @@ -236,7 +236,7 @@ public: [[nodiscard]] Data::GroupCall *lookupReal() const; [[nodiscard]] rpl::producer> real() const; - void start(TimeId scheduleDate); + void start(TimeId scheduleDate, bool rtmp); void hangup(); void discard(); void rejoinAs(Group::JoinInfo info); diff --git a/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp new file mode 100644 index 000000000..88e607a46 --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_rtmp.cpp @@ -0,0 +1,295 @@ +/* +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 "calls/group/calls_group_rtmp.h" + +#include "apiwrap.h" +#include "calls/group/calls_group_common.h" +#include "data/data_peer.h" +#include "lang/lang_keys.h" +#include "main/main_account.h" +#include "main/main_session.h" +#include "settings/settings_common.h" // AddDivider. +#include "ui/boxes/confirm_box.h" +#include "ui/layers/generic_box.h" +#include "ui/text/text_utilities.h" +#include "ui/widgets/buttons.h" +#include "ui/widgets/popup_menu.h" +#include "styles/style_boxes.h" +#include "styles/style_calls.h" +#include "styles/style_info.h" +#include "styles/style_layers.h" +#include "styles/style_menu_icons.h" + +#include +#include + +namespace Calls::Group { +namespace { + +void StartWithBox( + not_null box, + Fn done, + Fn revoke, + Fn)> showBox, + Fn showToast, + rpl::producer &&data) { + struct State { + rpl::variable hidden = true; + rpl::variable key; + rpl::variable url; + base::unique_qptr menu; + bool warned = false; + }; + + const auto &rowPadding = st::boxRowPadding; + + const auto passChar = QChar(box->style()->styleHint( + QStyle::SH_LineEdit_PasswordCharacter)); + const auto state = box->lifetime().make_state(); + state->key = rpl::duplicate( + data + ) | rpl::map([=](const auto &d) { return d.key; }); + state->url = std::move( + data + ) | rpl::map([=](const auto &d) { return d.url; }); + + const auto addButton = [&]( + bool key, + rpl::producer &&text) { + const auto &padding = st::groupCallRtmpCopyButton.padding; + + auto wrap = object_ptr(box); + auto button = Ui::CreateChild( + wrap.data(), + rpl::duplicate(text), + st::groupCallRtmpCopyButton); + button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform); + button->setClickedCallback(key + ? Fn([=] { + QGuiApplication::clipboard()->setText(state->key.current()); + showToast(tr::lng_group_call_rtmp_key_copied(tr::now)); + }) + : Fn([=] { + QGuiApplication::clipboard()->setText(state->url.current()); + showToast(tr::lng_group_call_rtmp_url_copied(tr::now)); + })); + const auto weak = box->addRow(std::move(wrap), rowPadding); + button->heightValue( + ) | rpl::start_with_next([=](int height) { + weak->resize(weak->width(), height); + }, box->lifetime()); + return weak; + }; + + const auto addLabel = [&]( + rpl::producer &&text, + const style::FlatLabel &st) { + const auto label = box->addRow( + object_ptr(box, std::move(text), st), + st::boxRowPadding); + label->setSelectable(true); + label->setBreakEverywhere(true); + return label; + }; + + box->setTitle(tr::lng_group_call_rtmp_title()); + + // Server URL. + Settings::AddSubsectionTitle( + box->verticalLayout(), + tr::lng_group_call_rtmp_url_subtitle(), + st::groupCallRtmpSubsectionTitleAddPadding); + auto urlLabelContent = state->url.value(); + addLabel(std::move(urlLabelContent), st::boxLabel); + box->addSkip(st::groupCallRtmpUrlSkip); + addButton(false, tr::lng_group_call_rtmp_url_copy()); + // + + Settings::AddDivider(box->verticalLayout()); + + // Stream Key. + box->addSkip(st::groupCallRtmpKeySubsectionTitleSkip); + Settings::AddSubsectionTitle( + box->verticalLayout(), + tr::lng_group_call_rtmp_key_subtitle(), + st::groupCallRtmpSubsectionTitleAddPadding); + + auto keyLabelContent = rpl::combine( + state->hidden.value(), + state->key.value() + ) | rpl::map([passChar](bool hidden, const QString &key) { + return hidden + ? QString().fill(passChar, int(key.size())) + : key; + }); + const auto streamKeyLabel = addLabel( + std::move(keyLabelContent), + st::boxLabel); + const auto streamKeyButton = Ui::CreateChild( + box.get(), + st::groupCallRtmpShowButton); + + streamKeyLabel->topValue( + ) | rpl::start_with_next([=, right = rowPadding.right()](int top) { + streamKeyButton->moveToRight( + st::groupCallRtmpShowButtonPosition.x(), + top + st::groupCallRtmpShowButtonPosition.y()); + streamKeyButton->raise(); + }, box->lifetime()); + streamKeyButton->addClickHandler([=] { + if (!state->warned && state->hidden.current()) { + showBox(Box( + tr::lng_group_call_rtmp_key_warn(tr::now), + tr::lng_from_request_understand(tr::now), + tr::lng_close(tr::now), + [=](Fn &&close) { + state->warned = true; + state->hidden = !state->hidden.current(); + close(); + })); + } else { + state->hidden = !state->hidden.current(); + } + }); + + addButton(true, tr::lng_group_call_rtmp_key_copy()); + // + + Settings::AddDividerText( + box->verticalLayout(), + tr::lng_group_call_rtmp_info()); + + box->addButton(tr::lng_group_call_rtmp_start(), done); + box->addButton(tr::lng_cancel(), [=] { box->closeBox(); }); + box->setWidth(st::infoDesiredWidth); + { + const auto top = box->addTopButton(st::infoTopBarMenu); + top->setClickedCallback([=] { + state->menu = base::make_unique_q( + top, + st::popupMenuWithIcons); + state->menu->addAction( + tr::lng_group_invite_context_revoke(tr::now), + revoke, + &st::menuIconRemove); + state->menu->moveToRight( + st::groupCallRtmpTopBarMenuPosition.x(), + st::groupCallRtmpTopBarMenuPosition.y()); + state->menu->setForcedOrigin( + Ui::PanelAnimation::Origin::TopRight); + state->menu->popup(QCursor::pos()); + return true; + }); + } + +} + +} // namespace + +StartRtmpProcess::~StartRtmpProcess() { + if (_request) { + _request->peer->session().api().request(_request->id).cancel(); + } +} + +void StartRtmpProcess::start( + not_null peer, + Fn)> showBox, + Fn showToast, + Fn done) { + Expects(done != nullptr); + + const auto session = &peer->session(); + if (_request) { + if (_request->peer == peer) { + _request->showBox = std::move(showBox); + _request->showToast = std::move(showToast); + _request->done = std::move(done); + return; + } + session->api().request(_request->id).cancel(); + _request = nullptr; + } + _request = std::make_unique( + RtmpRequest{ + .peer = peer, + .showBox = std::move(showBox), + .showToast = std::move(showToast), + .done = std::move(done) }); + session->account().sessionChanges( + ) | rpl::start_with_next([=] { + _request = nullptr; + }, _request->lifetime); + + requestUrl(false); +} + +void StartRtmpProcess::requestUrl(bool revoke) { + const auto session = &_request->peer->session(); + _request->id = session->api().request(MTPphone_GetGroupCallStreamRtmpUrl( + _request->peer->input, + MTP_bool(revoke) + )).done([=](const MTPphone_GroupCallStreamRtmpUrl &result) { + auto data = result.match([&]( + const MTPDphone_groupCallStreamRtmpUrl &data) { + return Data{ .url = qs(data.vurl()), .key = qs(data.vkey()) }; + }); + processUrl(std::move(data)); + }).fail([=] { + _request->showToast(Lang::Hard::ServerError()); + }).send(); +} + +void StartRtmpProcess::processUrl(Data data) { + if (!_request->box) { + createBox(); + } + _request->data = std::move(data); +} + +void StartRtmpProcess::finish(JoinInfo info) { + const auto done = std::move(_request->done); + const auto box = _request->box; + _request = nullptr; + done(std::move(info)); + if (const auto strong = box.data()) { + strong->closeBox(); + } +} + +void StartRtmpProcess::createBox() { + auto done = [=] { + const auto peer = _request->peer; + finish({ .peer = peer, .joinAs = peer, .rtmp = true }); + }; + auto revoke = [=] { + const auto guard = base::make_weak(&_request->guard); + _request->showBox(Box( + tr::lng_group_call_rtmp_revoke_sure(tr::now), + tr::lng_group_invite_context_revoke(tr::now), + crl::guard(guard, [=](Fn &&close) { + requestUrl(true); + close(); + }))); + }; + auto object = Box( + StartWithBox, + std::move(done), + std::move(revoke), + _request->showBox, + _request->showToast, + _request->data.value()); + object->boxClosing( + ) | rpl::start_with_next([=] { + _request = nullptr; + }, _request->lifetime); + _request->box = Ui::MakeWeak(object.data()); + _request->showBox(std::move(object)); +} + +} // namespace Calls::Group diff --git a/Telegram/SourceFiles/calls/group/calls_group_rtmp.h b/Telegram/SourceFiles/calls/group/calls_group_rtmp.h new file mode 100644 index 000000000..fabc5136a --- /dev/null +++ b/Telegram/SourceFiles/calls/group/calls_group_rtmp.h @@ -0,0 +1,60 @@ +/* +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 +*/ +#pragma once + +#include "base/weak_ptr.h" +#include "base/object_ptr.h" + +class PeerData; + +namespace Ui { +class BoxContent; +} // namespace Ui + +namespace Calls::Group { + +struct JoinInfo; + +class StartRtmpProcess final { +public: + StartRtmpProcess() = default; + ~StartRtmpProcess(); + + struct Data { + QString url; + QString key; + }; + + void start( + not_null peer, + Fn)> showBox, + Fn showToast, + Fn done); + +private: + void requestUrl(bool revoke); + void processUrl(Data data); + void createBox(); + void finish(JoinInfo info); + + struct RtmpRequest { + not_null peer; + rpl::variable data; + Fn)> showBox; + Fn showToast; + Fn done; + base::has_weak_ptr guard; + QPointer box; + rpl::lifetime lifetime; + mtpRequestId id = 0; + }; + std::unique_ptr _request; + +}; + +} // namespace Calls::Group