Added ability to start livestream with RTMP.

This commit is contained in:
23rd 2022-02-26 15:47:33 +03:00
parent 97dbb98862
commit 8909b654d3
9 changed files with 423 additions and 6 deletions

View file

@ -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

View file

@ -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}";

View file

@ -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);

View file

@ -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<void()> Instance::Delegate::groupCallAddAsyncWaiter() {
Instance::Instance()
: _delegate(std::make_unique<Delegate>(this))
, _cachedDhConfig(std::make_unique<DhConfig>())
, _chooseJoinAs(std::make_unique<Group::ChooseJoinAsProcess>()) {
, _chooseJoinAs(std::make_unique<Group::ChooseJoinAsProcess>())
, _startWithRtmp(std::make_unique<Group::StartRtmpProcess>()) {
}
Instance::~Instance() {
@ -202,6 +204,18 @@ void Instance::startOrJoinGroupCall(
not_null<PeerData*> peer,
const StartGroupCallArgs &args) {
using JoinConfirm = StartGroupCallArgs::JoinConfirm;
if (args.rtmpNeeded) {
_startWithRtmp->start(peer, [=](object_ptr<Ui::BoxContent> 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()

View file

@ -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<QString, std::unique_ptr<Media::Audio::Track>> _tracks;
const std::unique_ptr<Group::ChooseJoinAsProcess> _chooseJoinAs;
const std::unique_ptr<Group::StartRtmpProcess> _startWithRtmp;
base::flat_set<std::unique_ptr<crl::semaphore>> _asyncWaiters;

View file

@ -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<not_null<Data::GroupCall*>> 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<int32>()),
MTPstring(), // title

View file

@ -236,7 +236,7 @@ public:
[[nodiscard]] Data::GroupCall *lookupReal() const;
[[nodiscard]] rpl::producer<not_null<Data::GroupCall*>> real() const;
void start(TimeId scheduleDate);
void start(TimeId scheduleDate, bool rtmp);
void hangup();
void discard();
void rejoinAs(Group::JoinInfo info);

View file

@ -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 <QGuiApplication>
#include <QStyle>
namespace Calls::Group {
namespace {
void StartWithBox(
not_null<Ui::GenericBox*> box,
Fn<void()> done,
Fn<void()> revoke,
Fn<void(object_ptr<Ui::BoxContent>)> showBox,
Fn<void(QString)> showToast,
rpl::producer<StartRtmpProcess::Data> &&data) {
struct State {
rpl::variable<bool> hidden = true;
rpl::variable<QString> key;
rpl::variable<QString> url;
base::unique_qptr<Ui::PopupMenu> 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>();
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<QString> &&text) {
const auto &padding = st::groupCallRtmpCopyButton.padding;
auto wrap = object_ptr<Ui::RpWidget>(box);
auto button = Ui::CreateChild<Ui::RoundButton>(
wrap.data(),
rpl::duplicate(text),
st::groupCallRtmpCopyButton);
button->setTextTransform(Ui::RoundButton::TextTransform::NoTransform);
button->setClickedCallback(key
? Fn<void()>([=] {
QGuiApplication::clipboard()->setText(state->key.current());
showToast(tr::lng_group_call_rtmp_key_copied(tr::now));
})
: Fn<void()>([=] {
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<QString> &&text,
const style::FlatLabel &st) {
const auto label = box->addRow(
object_ptr<Ui::FlatLabel>(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<Ui::IconButton>(
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<Ui::ConfirmBox>(
tr::lng_group_call_rtmp_key_warn(tr::now),
tr::lng_from_request_understand(tr::now),
tr::lng_close(tr::now),
[=](Fn<void()> &&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<Ui::PopupMenu>(
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<PeerData*> peer,
Fn<void(object_ptr<Ui::BoxContent>)> showBox,
Fn<void(QString)> showToast,
Fn<void(JoinInfo)> 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>(
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<Ui::ConfirmBox>(
tr::lng_group_call_rtmp_revoke_sure(tr::now),
tr::lng_group_invite_context_revoke(tr::now),
crl::guard(guard, [=](Fn<void()> &&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

View file

@ -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<PeerData*> peer,
Fn<void(object_ptr<Ui::BoxContent>)> showBox,
Fn<void(QString)> showToast,
Fn<void(JoinInfo)> done);
private:
void requestUrl(bool revoke);
void processUrl(Data data);
void createBox();
void finish(JoinInfo info);
struct RtmpRequest {
not_null<PeerData*> peer;
rpl::variable<Data> data;
Fn<void(object_ptr<Ui::BoxContent>)> showBox;
Fn<void(QString)> showToast;
Fn<void(JoinInfo)> done;
base::has_weak_ptr guard;
QPointer<Ui::BoxContent> box;
rpl::lifetime lifetime;
mtpRequestId id = 0;
};
std::unique_ptr<RtmpRequest> _request;
};
} // namespace Calls::Group