clang-format
This commit is contained in:
parent
f14d141cb5
commit
e3e7595bab
@ -33,8 +33,7 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
|
|||||||
|
|
||||||
layout_ = new QHBoxLayout(this);
|
layout_ = new QHBoxLayout(this);
|
||||||
layout_->setSpacing(widgetMargin);
|
layout_->setSpacing(widgetMargin);
|
||||||
layout_->setContentsMargins(
|
layout_->setContentsMargins(2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin);
|
||||||
2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin);
|
|
||||||
|
|
||||||
QFont labelFont;
|
QFont labelFont;
|
||||||
labelFont.setPointSizeF(labelFont.pointSizeF() * 1.1);
|
labelFont.setPointSizeF(labelFont.pointSizeF() * 1.1);
|
||||||
@ -56,9 +55,9 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
|
|||||||
setMuteIcon(false);
|
setMuteIcon(false);
|
||||||
muteBtn_->setFixedSize(buttonSize_, buttonSize_);
|
muteBtn_->setFixedSize(buttonSize_, buttonSize_);
|
||||||
muteBtn_->setCornerRadius(buttonSize_ / 2);
|
muteBtn_->setCornerRadius(buttonSize_ / 2);
|
||||||
connect(muteBtn_, &FlatButton::clicked, this, [this](){
|
connect(muteBtn_, &FlatButton::clicked, this, [this]() {
|
||||||
if (WebRTCSession::instance().toggleMuteAudioSrc(muted_))
|
if (WebRTCSession::instance().toggleMuteAudioSrc(muted_))
|
||||||
setMuteIcon(muted_);
|
setMuteIcon(muted_);
|
||||||
});
|
});
|
||||||
|
|
||||||
layout_->addWidget(avatar_, 0, Qt::AlignLeft);
|
layout_->addWidget(avatar_, 0, Qt::AlignLeft);
|
||||||
@ -70,21 +69,21 @@ ActiveCallBar::ActiveCallBar(QWidget *parent)
|
|||||||
layout_->addSpacing(18);
|
layout_->addSpacing(18);
|
||||||
|
|
||||||
timer_ = new QTimer(this);
|
timer_ = new QTimer(this);
|
||||||
connect(timer_, &QTimer::timeout, this,
|
connect(timer_, &QTimer::timeout, this, [this]() {
|
||||||
[this](){
|
auto seconds = QDateTime::currentSecsSinceEpoch() - callStartTime_;
|
||||||
auto seconds = QDateTime::currentSecsSinceEpoch() - callStartTime_;
|
int s = seconds % 60;
|
||||||
int s = seconds % 60;
|
int m = (seconds / 60) % 60;
|
||||||
int m = (seconds / 60) % 60;
|
int h = seconds / 3600;
|
||||||
int h = seconds / 3600;
|
char buf[12];
|
||||||
char buf[12];
|
if (h)
|
||||||
if (h)
|
snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d", h, m, s);
|
||||||
snprintf(buf, sizeof(buf), "%.2d:%.2d:%.2d", h, m, s);
|
else
|
||||||
else
|
snprintf(buf, sizeof(buf), "%.2d:%.2d", m, s);
|
||||||
snprintf(buf, sizeof(buf), "%.2d:%.2d", m, s);
|
durationLabel_->setText(buf);
|
||||||
durationLabel_->setText(buf);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(&WebRTCSession::instance(), &WebRTCSession::stateChanged, this, &ActiveCallBar::update);
|
connect(
|
||||||
|
&WebRTCSession::instance(), &WebRTCSession::stateChanged, this, &ActiveCallBar::update);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -103,61 +102,59 @@ ActiveCallBar::setMuteIcon(bool muted)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ActiveCallBar::setCallParty(
|
ActiveCallBar::setCallParty(const QString &userid,
|
||||||
const QString &userid,
|
const QString &displayName,
|
||||||
const QString &displayName,
|
const QString &roomName,
|
||||||
const QString &roomName,
|
const QString &avatarUrl)
|
||||||
const QString &avatarUrl)
|
|
||||||
{
|
{
|
||||||
callPartyLabel_->setText(" " +
|
callPartyLabel_->setText(" " + (displayName.isEmpty() ? userid : displayName) + " ");
|
||||||
(displayName.isEmpty() ? userid : displayName) + " ");
|
|
||||||
|
|
||||||
if (!avatarUrl.isEmpty())
|
if (!avatarUrl.isEmpty())
|
||||||
avatar_->setImage(avatarUrl);
|
avatar_->setImage(avatarUrl);
|
||||||
else
|
else
|
||||||
avatar_->setLetter(utils::firstChar(roomName));
|
avatar_->setLetter(utils::firstChar(roomName));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
ActiveCallBar::update(WebRTCSession::State state)
|
ActiveCallBar::update(WebRTCSession::State state)
|
||||||
{
|
{
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case WebRTCSession::State::INITIATING:
|
case WebRTCSession::State::INITIATING:
|
||||||
show();
|
show();
|
||||||
stateLabel_->setText("Initiating call...");
|
stateLabel_->setText("Initiating call...");
|
||||||
break;
|
break;
|
||||||
case WebRTCSession::State::INITIATED:
|
case WebRTCSession::State::INITIATED:
|
||||||
show();
|
show();
|
||||||
stateLabel_->setText("Call initiated...");
|
stateLabel_->setText("Call initiated...");
|
||||||
break;
|
break;
|
||||||
case WebRTCSession::State::OFFERSENT:
|
case WebRTCSession::State::OFFERSENT:
|
||||||
show();
|
show();
|
||||||
stateLabel_->setText("Calling...");
|
stateLabel_->setText("Calling...");
|
||||||
break;
|
break;
|
||||||
case WebRTCSession::State::CONNECTING:
|
case WebRTCSession::State::CONNECTING:
|
||||||
show();
|
show();
|
||||||
stateLabel_->setText("Connecting...");
|
stateLabel_->setText("Connecting...");
|
||||||
break;
|
break;
|
||||||
case WebRTCSession::State::CONNECTED:
|
case WebRTCSession::State::CONNECTED:
|
||||||
show();
|
show();
|
||||||
callStartTime_ = QDateTime::currentSecsSinceEpoch();
|
callStartTime_ = QDateTime::currentSecsSinceEpoch();
|
||||||
timer_->start(1000);
|
timer_->start(1000);
|
||||||
stateLabel_->setPixmap(QIcon(":/icons/icons/ui/place-call.png").
|
stateLabel_->setPixmap(
|
||||||
pixmap(QSize(buttonSize_, buttonSize_)));
|
QIcon(":/icons/icons/ui/place-call.png").pixmap(QSize(buttonSize_, buttonSize_)));
|
||||||
durationLabel_->setText("00:00");
|
durationLabel_->setText("00:00");
|
||||||
durationLabel_->show();
|
durationLabel_->show();
|
||||||
break;
|
break;
|
||||||
case WebRTCSession::State::ICEFAILED:
|
case WebRTCSession::State::ICEFAILED:
|
||||||
case WebRTCSession::State::DISCONNECTED:
|
case WebRTCSession::State::DISCONNECTED:
|
||||||
hide();
|
hide();
|
||||||
timer_->stop();
|
timer_->stop();
|
||||||
callPartyLabel_->setText(QString());
|
callPartyLabel_->setText(QString());
|
||||||
stateLabel_->setText(QString());
|
stateLabel_->setText(QString());
|
||||||
durationLabel_->setText(QString());
|
durationLabel_->setText(QString());
|
||||||
durationLabel_->hide();
|
durationLabel_->hide();
|
||||||
setMuteIcon(false);
|
setMuteIcon(false);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,11 +19,10 @@ public:
|
|||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void update(WebRTCSession::State);
|
void update(WebRTCSession::State);
|
||||||
void setCallParty(
|
void setCallParty(const QString &userid,
|
||||||
const QString &userid,
|
const QString &displayName,
|
||||||
const QString &displayName,
|
const QString &roomName,
|
||||||
const QString &roomName,
|
const QString &avatarUrl);
|
||||||
const QString &avatarUrl);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QHBoxLayout *layout_ = nullptr;
|
QHBoxLayout *layout_ = nullptr;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdint>
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
#include <QMediaPlaylist>
|
#include <QMediaPlaylist>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include "CallManager.h"
|
|
||||||
#include "Cache.h"
|
#include "Cache.h"
|
||||||
|
#include "CallManager.h"
|
||||||
#include "ChatPage.h"
|
#include "ChatPage.h"
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
@ -34,389 +34,420 @@ getTurnURIs(const mtx::responses::TurnServer &turnServer);
|
|||||||
}
|
}
|
||||||
|
|
||||||
CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
|
CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
|
||||||
: QObject(),
|
: QObject()
|
||||||
session_(WebRTCSession::instance()),
|
, session_(WebRTCSession::instance())
|
||||||
turnServerTimer_(this),
|
, turnServerTimer_(this)
|
||||||
settings_(userSettings)
|
, settings_(userSettings)
|
||||||
{
|
{
|
||||||
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
|
qRegisterMetaType<std::vector<mtx::events::msg::CallCandidates::Candidate>>();
|
||||||
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
|
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
|
||||||
qRegisterMetaType<mtx::responses::TurnServer>();
|
qRegisterMetaType<mtx::responses::TurnServer>();
|
||||||
|
|
||||||
connect(&session_, &WebRTCSession::offerCreated, this,
|
connect(
|
||||||
[this](const std::string &sdp,
|
&session_,
|
||||||
const std::vector<CallCandidates::Candidate> &candidates)
|
&WebRTCSession::offerCreated,
|
||||||
{
|
this,
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
|
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
|
||||||
emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_});
|
nhlog::ui()->debug("WebRTC: call id: {} - sending offer", callid_);
|
||||||
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
|
emit newMessage(roomid_, CallInvite{callid_, sdp, 0, timeoutms_});
|
||||||
|
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
|
||||||
|
QTimer::singleShot(timeoutms_, this, [this]() {
|
||||||
|
if (session_.state() == WebRTCSession::State::OFFERSENT) {
|
||||||
|
hangUp(CallHangUp::Reason::InviteTimeOut);
|
||||||
|
emit ChatPage::instance()->showNotification(
|
||||||
|
"The remote side failed to pick up.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
QTimer::singleShot(timeoutms_, this, [this](){
|
connect(
|
||||||
if (session_.state() == WebRTCSession::State::OFFERSENT) {
|
&session_,
|
||||||
hangUp(CallHangUp::Reason::InviteTimeOut);
|
&WebRTCSession::answerCreated,
|
||||||
emit ChatPage::instance()->showNotification("The remote side failed to pick up.");
|
this,
|
||||||
}
|
[this](const std::string &sdp, const std::vector<CallCandidates::Candidate> &candidates) {
|
||||||
});
|
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
|
||||||
});
|
emit newMessage(roomid_, CallAnswer{callid_, sdp, 0});
|
||||||
|
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
|
||||||
|
});
|
||||||
|
|
||||||
connect(&session_, &WebRTCSession::answerCreated, this,
|
connect(&session_,
|
||||||
[this](const std::string &sdp,
|
&WebRTCSession::newICECandidate,
|
||||||
const std::vector<CallCandidates::Candidate> &candidates)
|
this,
|
||||||
{
|
[this](const CallCandidates::Candidate &candidate) {
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - sending answer", callid_);
|
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
|
||||||
emit newMessage(roomid_, CallAnswer{callid_, sdp, 0});
|
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0});
|
||||||
emit newMessage(roomid_, CallCandidates{callid_, candidates, 0});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
connect(&session_, &WebRTCSession::newICECandidate, this,
|
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
|
||||||
[this](const CallCandidates::Candidate &candidate)
|
|
||||||
{
|
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - sending ice candidate", callid_);
|
|
||||||
emit newMessage(roomid_, CallCandidates{callid_, {candidate}, 0});
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&turnServerTimer_, &QTimer::timeout, this, &CallManager::retrieveTurnServer);
|
connect(this,
|
||||||
|
&CallManager::turnServerRetrieved,
|
||||||
|
this,
|
||||||
|
[this](const mtx::responses::TurnServer &res) {
|
||||||
|
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
|
||||||
|
nhlog::net()->info("username: {}", res.username);
|
||||||
|
nhlog::net()->info("ttl: {} seconds", res.ttl);
|
||||||
|
for (const auto &u : res.uris)
|
||||||
|
nhlog::net()->info("uri: {}", u);
|
||||||
|
|
||||||
connect(this, &CallManager::turnServerRetrieved, this,
|
// Request new credentials close to expiry
|
||||||
[this](const mtx::responses::TurnServer &res)
|
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||||
{
|
turnURIs_ = getTurnURIs(res);
|
||||||
nhlog::net()->info("TURN server(s) retrieved from homeserver:");
|
uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
|
||||||
nhlog::net()->info("username: {}", res.username);
|
if (res.ttl < 3600)
|
||||||
nhlog::net()->info("ttl: {} seconds", res.ttl);
|
nhlog::net()->warn("Setting ttl to 1 hour");
|
||||||
for (const auto &u : res.uris)
|
turnServerTimer_.setInterval(ttl * 1000 * 0.9);
|
||||||
nhlog::net()->info("uri: {}", u);
|
});
|
||||||
|
|
||||||
// Request new credentials close to expiry
|
connect(&session_, &WebRTCSession::stateChanged, this, [this](WebRTCSession::State state) {
|
||||||
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
switch (state) {
|
||||||
turnURIs_ = getTurnURIs(res);
|
case WebRTCSession::State::DISCONNECTED:
|
||||||
uint32_t ttl = std::max(res.ttl, UINT32_C(3600));
|
playRingtone("qrc:/media/media/callend.ogg", false);
|
||||||
if (res.ttl < 3600)
|
clear();
|
||||||
nhlog::net()->warn("Setting ttl to 1 hour");
|
break;
|
||||||
turnServerTimer_.setInterval(ttl * 1000 * 0.9);
|
case WebRTCSession::State::ICEFAILED: {
|
||||||
});
|
QString error("Call connection failed.");
|
||||||
|
if (turnURIs_.empty())
|
||||||
|
error += " Your homeserver has no configured TURN server.";
|
||||||
|
emit ChatPage::instance()->showNotification(error);
|
||||||
|
hangUp(CallHangUp::Reason::ICEFailed);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
connect(&session_, &WebRTCSession::stateChanged, this,
|
connect(&player_,
|
||||||
[this](WebRTCSession::State state) {
|
&QMediaPlayer::mediaStatusChanged,
|
||||||
if (state == WebRTCSession::State::DISCONNECTED) {
|
this,
|
||||||
playRingtone("qrc:/media/media/callend.ogg", false);
|
[this](QMediaPlayer::MediaStatus status) {
|
||||||
}
|
if (status == QMediaPlayer::LoadedMedia)
|
||||||
else if (state == WebRTCSession::State::ICEFAILED) {
|
player_.play();
|
||||||
QString error("Call connection failed.");
|
});
|
||||||
if (turnURIs_.empty())
|
|
||||||
error += " Your homeserver has no configured TURN server.";
|
|
||||||
emit ChatPage::instance()->showNotification(error);
|
|
||||||
hangUp(CallHangUp::Reason::ICEFailed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(&player_, &QMediaPlayer::mediaStatusChanged, this,
|
|
||||||
[this](QMediaPlayer::MediaStatus status) {
|
|
||||||
if (status == QMediaPlayer::LoadedMedia)
|
|
||||||
player_.play();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::sendInvite(const QString &roomid)
|
CallManager::sendInvite(const QString &roomid)
|
||||||
{
|
{
|
||||||
if (onActiveCall())
|
if (onActiveCall())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
|
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
|
||||||
if (roomInfo.member_count != 2) {
|
if (roomInfo.member_count != 2) {
|
||||||
emit ChatPage::instance()->showNotification("Voice calls are limited to 1:1 rooms.");
|
emit ChatPage::instance()->showNotification(
|
||||||
return;
|
"Voice calls are limited to 1:1 rooms.");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string errorMessage;
|
std::string errorMessage;
|
||||||
if (!session_.init(&errorMessage)) {
|
if (!session_.init(&errorMessage)) {
|
||||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
roomid_ = roomid;
|
roomid_ = roomid;
|
||||||
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
|
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
|
||||||
session_.setTurnServers(turnURIs_);
|
session_.setTurnServers(turnURIs_);
|
||||||
|
|
||||||
generateCallID();
|
generateCallID();
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_);
|
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_);
|
||||||
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
|
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
|
||||||
const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front();
|
const RoomMember &callee =
|
||||||
emit newCallParty(callee.user_id, callee.display_name,
|
members.front().user_id == utils::localUser() ? members.back() : members.front();
|
||||||
QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url));
|
emit newCallParty(callee.user_id,
|
||||||
playRingtone("qrc:/media/media/ringback.ogg", true);
|
callee.display_name,
|
||||||
if (!session_.createOffer()) {
|
QString::fromStdString(roomInfo.name),
|
||||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
QString::fromStdString(roomInfo.avatar_url));
|
||||||
endCall();
|
playRingtone("qrc:/media/media/ringback.ogg", true);
|
||||||
}
|
if (!session_.createOffer()) {
|
||||||
|
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||||
|
endCall();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
std::string callHangUpReasonString(CallHangUp::Reason reason)
|
std::string
|
||||||
|
callHangUpReasonString(CallHangUp::Reason reason)
|
||||||
{
|
{
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case CallHangUp::Reason::ICEFailed:
|
case CallHangUp::Reason::ICEFailed:
|
||||||
return "ICE failed";
|
return "ICE failed";
|
||||||
case CallHangUp::Reason::InviteTimeOut:
|
case CallHangUp::Reason::InviteTimeOut:
|
||||||
return "Invite time out";
|
return "Invite time out";
|
||||||
default:
|
default:
|
||||||
return "User";
|
return "User";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::hangUp(CallHangUp::Reason reason)
|
CallManager::hangUp(CallHangUp::Reason reason)
|
||||||
{
|
{
|
||||||
if (!callid_.empty()) {
|
if (!callid_.empty()) {
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - hanging up ({})", callid_,
|
nhlog::ui()->debug(
|
||||||
callHangUpReasonString(reason));
|
"WebRTC: call id: {} - hanging up ({})", callid_, callHangUpReasonString(reason));
|
||||||
emit newMessage(roomid_, CallHangUp{callid_, 0, reason});
|
emit newMessage(roomid_, CallHangUp{callid_, 0, reason});
|
||||||
endCall();
|
endCall();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
CallManager::onActiveCall()
|
CallManager::onActiveCall()
|
||||||
{
|
{
|
||||||
return session_.state() != WebRTCSession::State::DISCONNECTED;
|
return session_.state() != WebRTCSession::State::DISCONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
|
void
|
||||||
|
CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
|
||||||
{
|
{
|
||||||
if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event)
|
if (handleEvent_<CallInvite>(event) || handleEvent_<CallCandidates>(event) ||
|
||||||
|| handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event))
|
handleEvent_<CallAnswer>(event) || handleEvent_<CallHangUp>(event))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
bool
|
bool
|
||||||
CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event)
|
CallManager::handleEvent_(const mtx::events::collections::TimelineEvents &event)
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<RoomEvent<T>>(event)) {
|
if (std::holds_alternative<RoomEvent<T>>(event)) {
|
||||||
handleEvent(std::get<RoomEvent<T>>(event));
|
handleEvent(std::get<RoomEvent<T>>(event));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
|
CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
|
||||||
{
|
{
|
||||||
const char video[] = "m=video";
|
const char video[] = "m=video";
|
||||||
const std::string &sdp = callInviteEvent.content.sdp;
|
const std::string &sdp = callInviteEvent.content.sdp;
|
||||||
bool isVideo = std::search(sdp.cbegin(), sdp.cend(), std::cbegin(video), std::cend(video) - 1,
|
bool isVideo = std::search(sdp.cbegin(),
|
||||||
[](unsigned char c1, unsigned char c2) {return std::tolower(c1) == std::tolower(c2);})
|
sdp.cend(),
|
||||||
!= sdp.cend();
|
std::cbegin(video),
|
||||||
|
std::cend(video) - 1,
|
||||||
|
[](unsigned char c1, unsigned char c2) {
|
||||||
|
return std::tolower(c1) == std::tolower(c2);
|
||||||
|
}) != sdp.cend();
|
||||||
|
|
||||||
nhlog::ui()->debug(std::string("WebRTC: call id: {} - incoming ") + (isVideo ? "video" : "voice") +
|
nhlog::ui()->debug(std::string("WebRTC: call id: {} - incoming ") +
|
||||||
" CallInvite from {}", callInviteEvent.content.call_id, callInviteEvent.sender);
|
(isVideo ? "video" : "voice") + " CallInvite from {}",
|
||||||
|
callInviteEvent.content.call_id,
|
||||||
|
callInviteEvent.sender);
|
||||||
|
|
||||||
if (callInviteEvent.content.call_id.empty())
|
if (callInviteEvent.content.call_id.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (isVideo) {
|
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
|
||||||
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
|
if (onActiveCall() || roomInfo.member_count != 2 || isVideo) {
|
||||||
CallHangUp{callInviteEvent.content.call_id, 0, CallHangUp::Reason::InviteTimeOut});
|
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
|
||||||
return;
|
CallHangUp{callInviteEvent.content.call_id,
|
||||||
}
|
0,
|
||||||
|
CallHangUp::Reason::InviteTimeOut});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
|
playRingtone("qrc:/media/media/ring.ogg", true);
|
||||||
if (onActiveCall() || roomInfo.member_count != 2) {
|
roomid_ = QString::fromStdString(callInviteEvent.room_id);
|
||||||
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
|
callid_ = callInviteEvent.content.call_id;
|
||||||
CallHangUp{callInviteEvent.content.call_id, 0, CallHangUp::Reason::InviteTimeOut});
|
remoteICECandidates_.clear();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
playRingtone("qrc:/media/media/ring.ogg", true);
|
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
|
||||||
roomid_ = QString::fromStdString(callInviteEvent.room_id);
|
const RoomMember &caller =
|
||||||
callid_ = callInviteEvent.content.call_id;
|
members.front().user_id == utils::localUser() ? members.back() : members.front();
|
||||||
remoteICECandidates_.clear();
|
emit newCallParty(caller.user_id,
|
||||||
|
caller.display_name,
|
||||||
|
QString::fromStdString(roomInfo.name),
|
||||||
|
QString::fromStdString(roomInfo.avatar_url));
|
||||||
|
|
||||||
std::vector<RoomMember> members(cache::getMembers(callInviteEvent.room_id));
|
auto dialog = new dialogs::AcceptCall(caller.user_id,
|
||||||
const RoomMember &caller =
|
caller.display_name,
|
||||||
members.front().user_id == utils::localUser() ? members.back() : members.front();
|
QString::fromStdString(roomInfo.name),
|
||||||
emit newCallParty(caller.user_id, caller.display_name,
|
QString::fromStdString(roomInfo.avatar_url),
|
||||||
QString::fromStdString(roomInfo.name), QString::fromStdString(roomInfo.avatar_url));
|
MainWindow::instance());
|
||||||
|
connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent]() {
|
||||||
auto dialog = new dialogs::AcceptCall(
|
MainWindow::instance()->hideOverlay();
|
||||||
caller.user_id,
|
answerInvite(callInviteEvent.content);
|
||||||
caller.display_name,
|
});
|
||||||
QString::fromStdString(roomInfo.name),
|
connect(dialog, &dialogs::AcceptCall::reject, this, [this]() {
|
||||||
QString::fromStdString(roomInfo.avatar_url),
|
MainWindow::instance()->hideOverlay();
|
||||||
MainWindow::instance());
|
hangUp();
|
||||||
connect(dialog, &dialogs::AcceptCall::accept, this,
|
});
|
||||||
[this, callInviteEvent](){
|
MainWindow::instance()->showSolidOverlayModal(dialog);
|
||||||
MainWindow::instance()->hideOverlay();
|
|
||||||
answerInvite(callInviteEvent.content);});
|
|
||||||
connect(dialog, &dialogs::AcceptCall::reject, this,
|
|
||||||
[this](){
|
|
||||||
MainWindow::instance()->hideOverlay();
|
|
||||||
hangUp();});
|
|
||||||
MainWindow::instance()->showSolidOverlayModal(dialog);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::answerInvite(const CallInvite &invite)
|
CallManager::answerInvite(const CallInvite &invite)
|
||||||
{
|
{
|
||||||
stopRingtone();
|
stopRingtone();
|
||||||
std::string errorMessage;
|
std::string errorMessage;
|
||||||
if (!session_.init(&errorMessage)) {
|
if (!session_.init(&errorMessage)) {
|
||||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||||
hangUp();
|
hangUp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
|
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
|
||||||
session_.setTurnServers(turnURIs_);
|
session_.setTurnServers(turnURIs_);
|
||||||
|
|
||||||
if (!session_.acceptOffer(invite.sdp)) {
|
if (!session_.acceptOffer(invite.sdp)) {
|
||||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||||
hangUp();
|
hangUp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
session_.acceptICECandidates(remoteICECandidates_);
|
session_.acceptICECandidates(remoteICECandidates_);
|
||||||
remoteICECandidates_.clear();
|
remoteICECandidates_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
|
CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
|
||||||
{
|
{
|
||||||
if (callCandidatesEvent.sender == utils::localUser().toStdString())
|
if (callCandidatesEvent.sender == utils::localUser().toStdString())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
|
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallCandidates from {}",
|
||||||
callCandidatesEvent.content.call_id, callCandidatesEvent.sender);
|
callCandidatesEvent.content.call_id,
|
||||||
|
callCandidatesEvent.sender);
|
||||||
|
|
||||||
if (callid_ == callCandidatesEvent.content.call_id) {
|
if (callid_ == callCandidatesEvent.content.call_id) {
|
||||||
if (onActiveCall())
|
if (onActiveCall())
|
||||||
session_.acceptICECandidates(callCandidatesEvent.content.candidates);
|
session_.acceptICECandidates(callCandidatesEvent.content.candidates);
|
||||||
else {
|
else {
|
||||||
// CallInvite has been received and we're awaiting localUser to accept or reject the call
|
// CallInvite has been received and we're awaiting localUser to accept or
|
||||||
for (const auto &c : callCandidatesEvent.content.candidates)
|
// reject the call
|
||||||
remoteICECandidates_.push_back(c);
|
for (const auto &c : callCandidatesEvent.content.candidates)
|
||||||
}
|
remoteICECandidates_.push_back(c);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
|
CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
|
||||||
{
|
{
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
|
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallAnswer from {}",
|
||||||
callAnswerEvent.content.call_id, callAnswerEvent.sender);
|
callAnswerEvent.content.call_id,
|
||||||
|
callAnswerEvent.sender);
|
||||||
|
|
||||||
if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
|
if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
|
||||||
callid_ == callAnswerEvent.content.call_id) {
|
callid_ == callAnswerEvent.content.call_id) {
|
||||||
emit ChatPage::instance()->showNotification("Call answered on another device.");
|
emit ChatPage::instance()->showNotification("Call answered on another device.");
|
||||||
stopRingtone();
|
stopRingtone();
|
||||||
MainWindow::instance()->hideOverlay();
|
MainWindow::instance()->hideOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
|
if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
|
||||||
stopRingtone();
|
stopRingtone();
|
||||||
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
|
if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
|
||||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||||
hangUp();
|
hangUp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
|
CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
|
||||||
{
|
{
|
||||||
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
|
nhlog::ui()->debug("WebRTC: call id: {} - incoming CallHangUp ({}) from {}",
|
||||||
callHangUpEvent.content.call_id, callHangUpReasonString(callHangUpEvent.content.reason),
|
callHangUpEvent.content.call_id,
|
||||||
callHangUpEvent.sender);
|
callHangUpReasonString(callHangUpEvent.content.reason),
|
||||||
|
callHangUpEvent.sender);
|
||||||
|
|
||||||
if (callid_ == callHangUpEvent.content.call_id) {
|
if (callid_ == callHangUpEvent.content.call_id) {
|
||||||
MainWindow::instance()->hideOverlay();
|
MainWindow::instance()->hideOverlay();
|
||||||
endCall();
|
endCall();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::generateCallID()
|
CallManager::generateCallID()
|
||||||
{
|
{
|
||||||
using namespace std::chrono;
|
using namespace std::chrono;
|
||||||
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
|
uint64_t ms = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
|
||||||
callid_ = "c" + std::to_string(ms);
|
callid_ = "c" + std::to_string(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
CallManager::clear()
|
||||||
|
{
|
||||||
|
roomid_.clear();
|
||||||
|
callid_.clear();
|
||||||
|
remoteICECandidates_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::endCall()
|
CallManager::endCall()
|
||||||
{
|
{
|
||||||
stopRingtone();
|
stopRingtone();
|
||||||
session_.end();
|
clear();
|
||||||
roomid_.clear();
|
session_.end();
|
||||||
callid_.clear();
|
|
||||||
remoteICECandidates_.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::refreshTurnServer()
|
CallManager::refreshTurnServer()
|
||||||
{
|
{
|
||||||
turnURIs_.clear();
|
turnURIs_.clear();
|
||||||
turnServerTimer_.start(2000);
|
turnServerTimer_.start(2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::retrieveTurnServer()
|
CallManager::retrieveTurnServer()
|
||||||
{
|
{
|
||||||
http::client()->get_turn_server(
|
http::client()->get_turn_server(
|
||||||
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
|
[this](const mtx::responses::TurnServer &res, mtx::http::RequestErr err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
turnServerTimer_.setInterval(5000);
|
turnServerTimer_.setInterval(5000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
emit turnServerRetrieved(res);
|
emit turnServerRetrieved(res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::playRingtone(const QString &ringtone, bool repeat)
|
CallManager::playRingtone(const QString &ringtone, bool repeat)
|
||||||
{
|
{
|
||||||
static QMediaPlaylist playlist;
|
static QMediaPlaylist playlist;
|
||||||
playlist.clear();
|
playlist.clear();
|
||||||
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop : QMediaPlaylist::CurrentItemOnce);
|
playlist.setPlaybackMode(repeat ? QMediaPlaylist::CurrentItemInLoop
|
||||||
playlist.addMedia(QUrl(ringtone));
|
: QMediaPlaylist::CurrentItemOnce);
|
||||||
player_.setVolume(100);
|
playlist.addMedia(QUrl(ringtone));
|
||||||
player_.setPlaylist(&playlist);
|
player_.setVolume(100);
|
||||||
|
player_.setPlaylist(&playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CallManager::stopRingtone()
|
CallManager::stopRingtone()
|
||||||
{
|
{
|
||||||
player_.setPlaylist(nullptr);
|
player_.setPlaylist(nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
std::vector<std::string>
|
std::vector<std::string>
|
||||||
getTurnURIs(const mtx::responses::TurnServer &turnServer)
|
getTurnURIs(const mtx::responses::TurnServer &turnServer)
|
||||||
{
|
{
|
||||||
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
|
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
|
||||||
// where username and password are percent-encoded
|
// where username and password are percent-encoded
|
||||||
std::vector<std::string> ret;
|
std::vector<std::string> ret;
|
||||||
for (const auto &uri : turnServer.uris) {
|
for (const auto &uri : turnServer.uris) {
|
||||||
if (auto c = uri.find(':'); c == std::string::npos) {
|
if (auto c = uri.find(':'); c == std::string::npos) {
|
||||||
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
|
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
|
||||||
continue;
|
continue;
|
||||||
}
|
} else {
|
||||||
else {
|
std::string scheme = std::string(uri, 0, c);
|
||||||
std::string scheme = std::string(uri, 0, c);
|
if (scheme != "turn" && scheme != "turns") {
|
||||||
if (scheme != "turn" && scheme != "turns") {
|
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
|
||||||
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
QString encodedUri = QString::fromStdString(scheme) + "://" +
|
QString encodedUri =
|
||||||
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) + ":" +
|
QString::fromStdString(scheme) + "://" +
|
||||||
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) + "@" +
|
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) +
|
||||||
QString::fromStdString(std::string(uri, ++c));
|
":" +
|
||||||
ret.push_back(encodedUri.toStdString());
|
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) +
|
||||||
}
|
"@" + QString::fromStdString(std::string(uri, ++c));
|
||||||
}
|
ret.push_back(encodedUri.toStdString());
|
||||||
return ret;
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QObject>
|
|
||||||
#include <QMediaPlayer>
|
#include <QMediaPlayer>
|
||||||
|
#include <QObject>
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@ -27,7 +27,8 @@ public:
|
|||||||
CallManager(QSharedPointer<UserSettings>);
|
CallManager(QSharedPointer<UserSettings>);
|
||||||
|
|
||||||
void sendInvite(const QString &roomid);
|
void sendInvite(const QString &roomid);
|
||||||
void hangUp(mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
|
void hangUp(
|
||||||
|
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
|
||||||
bool onActiveCall();
|
bool onActiveCall();
|
||||||
void refreshTurnServer();
|
void refreshTurnServer();
|
||||||
|
|
||||||
@ -35,22 +36,21 @@ public slots:
|
|||||||
void syncEvent(const mtx::events::collections::TimelineEvents &event);
|
void syncEvent(const mtx::events::collections::TimelineEvents &event);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite&);
|
void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
|
||||||
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates&);
|
void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
|
||||||
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer&);
|
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
|
||||||
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp&);
|
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
|
||||||
void turnServerRetrieved(const mtx::responses::TurnServer&);
|
void turnServerRetrieved(const mtx::responses::TurnServer &);
|
||||||
void newCallParty(
|
void newCallParty(const QString &userid,
|
||||||
const QString &userid,
|
const QString &displayName,
|
||||||
const QString &displayName,
|
const QString &roomName,
|
||||||
const QString &roomName,
|
const QString &avatarUrl);
|
||||||
const QString &avatarUrl);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void retrieveTurnServer();
|
void retrieveTurnServer();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
WebRTCSession& session_;
|
WebRTCSession &session_;
|
||||||
QString roomid_;
|
QString roomid_;
|
||||||
std::string callid_;
|
std::string callid_;
|
||||||
const uint32_t timeoutms_ = 120000;
|
const uint32_t timeoutms_ = 120000;
|
||||||
@ -62,12 +62,13 @@ private:
|
|||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
bool handleEvent_(const mtx::events::collections::TimelineEvents &event);
|
bool handleEvent_(const mtx::events::collections::TimelineEvents &event);
|
||||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite>&);
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallInvite> &);
|
||||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates>&);
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
|
||||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer>&);
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
|
||||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp>&);
|
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
|
||||||
void answerInvite(const mtx::events::msg::CallInvite&);
|
void answerInvite(const mtx::events::msg::CallInvite &);
|
||||||
void generateCallID();
|
void generateCallID();
|
||||||
|
void clear();
|
||||||
void endCall();
|
void endCall();
|
||||||
void playRingtone(const QString &ringtone, bool repeat);
|
void playRingtone(const QString &ringtone, bool repeat);
|
||||||
void stopRingtone();
|
void stopRingtone();
|
||||||
|
@ -460,9 +460,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||||||
if (callManager_.onActiveCall()) {
|
if (callManager_.onActiveCall()) {
|
||||||
callManager_.hangUp();
|
callManager_.hangUp();
|
||||||
} else {
|
} else {
|
||||||
if (auto roomInfo =
|
if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString());
|
||||||
cache::singleRoomInfo(current_room_.toStdString());
|
roomInfo.member_count != 2) {
|
||||||
roomInfo.member_count != 2) {
|
|
||||||
showNotification("Voice calls are limited to 1:1 rooms.");
|
showNotification("Voice calls are limited to 1:1 rooms.");
|
||||||
} else {
|
} else {
|
||||||
std::vector<RoomMember> members(
|
std::vector<RoomMember> members(
|
||||||
@ -471,11 +470,11 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||||||
members.front().user_id == utils::localUser() ? members.back()
|
members.front().user_id == utils::localUser() ? members.back()
|
||||||
: members.front();
|
: members.front();
|
||||||
auto dialog = new dialogs::PlaceCall(
|
auto dialog = new dialogs::PlaceCall(
|
||||||
callee.user_id,
|
callee.user_id,
|
||||||
callee.display_name,
|
callee.display_name,
|
||||||
QString::fromStdString(roomInfo.name),
|
QString::fromStdString(roomInfo.name),
|
||||||
QString::fromStdString(roomInfo.avatar_url),
|
QString::fromStdString(roomInfo.avatar_url),
|
||||||
MainWindow::instance());
|
MainWindow::instance());
|
||||||
connect(dialog, &dialogs::PlaceCall::voice, this, [this]() {
|
connect(dialog, &dialogs::PlaceCall::voice, this, [this]() {
|
||||||
callManager_.sendInvite(current_room_);
|
callManager_.sendInvite(current_room_);
|
||||||
});
|
});
|
||||||
|
@ -72,12 +72,19 @@ struct CallType
|
|||||||
template<class T>
|
template<class T>
|
||||||
std::string operator()(const T &e)
|
std::string operator()(const T &e)
|
||||||
{
|
{
|
||||||
if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>, T>) {
|
if constexpr (std::is_same_v<mtx::events::RoomEvent<mtx::events::msg::CallInvite>,
|
||||||
const char video[] = "m=video";
|
T>) {
|
||||||
const std::string &sdp = e.content.sdp;
|
const char video[] = "m=video";
|
||||||
return std::search(sdp.cbegin(), sdp.cend(), std::cbegin(video), std::cend(video) - 1,
|
const std::string &sdp = e.content.sdp;
|
||||||
[](unsigned char c1, unsigned char c2) {return std::tolower(c1) == std::tolower(c2);})
|
return std::search(sdp.cbegin(),
|
||||||
!= sdp.cend() ? "video" : "voice";
|
sdp.cend(),
|
||||||
|
std::cbegin(video),
|
||||||
|
std::cend(video) - 1,
|
||||||
|
[](unsigned char c1, unsigned char c2) {
|
||||||
|
return std::tolower(c1) == std::tolower(c2);
|
||||||
|
}) != sdp.cend()
|
||||||
|
? "video"
|
||||||
|
: "voice";
|
||||||
}
|
}
|
||||||
return std::string();
|
return std::string();
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
|
||||||
#include "WebRTCSession.h"
|
|
||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
|
#include "WebRTCSession.h"
|
||||||
|
|
||||||
extern "C" {
|
extern "C"
|
||||||
|
{
|
||||||
#include "gst/gst.h"
|
#include "gst/gst.h"
|
||||||
#include "gst/sdp/sdp.h"
|
#include "gst/sdp/sdp.h"
|
||||||
|
|
||||||
@ -13,180 +14,445 @@ extern "C" {
|
|||||||
|
|
||||||
Q_DECLARE_METATYPE(WebRTCSession::State)
|
Q_DECLARE_METATYPE(WebRTCSession::State)
|
||||||
|
|
||||||
namespace {
|
WebRTCSession::WebRTCSession()
|
||||||
bool isoffering_;
|
: QObject()
|
||||||
std::string localsdp_;
|
|
||||||
std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
|
|
||||||
|
|
||||||
gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data);
|
|
||||||
GstWebRTCSessionDescription* parseSDP(const std::string &sdp, GstWebRTCSDPType type);
|
|
||||||
void generateOffer(GstElement *webrtc);
|
|
||||||
void setLocalDescription(GstPromise *promise, gpointer webrtc);
|
|
||||||
void addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, guint mlineIndex, gchar *candidate, gpointer G_GNUC_UNUSED);
|
|
||||||
gboolean onICEGatheringCompletion(gpointer timerid);
|
|
||||||
void iceConnectionStateChanged(GstElement *webrtcbin, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED);
|
|
||||||
void createAnswer(GstPromise *promise, gpointer webrtc);
|
|
||||||
void addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe);
|
|
||||||
void linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe);
|
|
||||||
std::string::const_iterator findName(const std::string &sdp, const std::string &name);
|
|
||||||
int getPayloadType(const std::string &sdp, const std::string &name);
|
|
||||||
}
|
|
||||||
|
|
||||||
WebRTCSession::WebRTCSession() : QObject()
|
|
||||||
{
|
{
|
||||||
qRegisterMetaType<WebRTCSession::State>();
|
qRegisterMetaType<WebRTCSession::State>();
|
||||||
connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
|
connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
WebRTCSession::init(std::string *errorMessage)
|
WebRTCSession::init(std::string *errorMessage)
|
||||||
{
|
{
|
||||||
if (initialised_)
|
if (initialised_)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
GError *error = nullptr;
|
GError *error = nullptr;
|
||||||
if (!gst_init_check(nullptr, nullptr, &error)) {
|
if (!gst_init_check(nullptr, nullptr, &error)) {
|
||||||
std::string strError = std::string("WebRTC: failed to initialise GStreamer: ");
|
std::string strError = std::string("WebRTC: failed to initialise GStreamer: ");
|
||||||
if (error) {
|
if (error) {
|
||||||
strError += error->message;
|
strError += error->message;
|
||||||
g_error_free(error);
|
g_error_free(error);
|
||||||
}
|
}
|
||||||
nhlog::ui()->error(strError);
|
nhlog::ui()->error(strError);
|
||||||
if (errorMessage)
|
if (errorMessage)
|
||||||
*errorMessage = strError;
|
*errorMessage = strError;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
gchar *version = gst_version_string();
|
gchar *version = gst_version_string();
|
||||||
std::string gstVersion(version);
|
std::string gstVersion(version);
|
||||||
g_free(version);
|
g_free(version);
|
||||||
nhlog::ui()->info("WebRTC: initialised " + gstVersion);
|
nhlog::ui()->info("WebRTC: initialised " + gstVersion);
|
||||||
|
|
||||||
// GStreamer Plugins:
|
// GStreamer Plugins:
|
||||||
// Base: audioconvert, audioresample, opus, playback, volume
|
// Base: audioconvert, audioresample, opus, playback, volume
|
||||||
// Good: autodetect, rtpmanager
|
// Good: autodetect, rtpmanager
|
||||||
// Bad: dtls, srtp, webrtc
|
// Bad: dtls, srtp, webrtc
|
||||||
// libnice [GLib]: nice
|
// libnice [GLib]: nice
|
||||||
initialised_ = true;
|
initialised_ = true;
|
||||||
std::string strError = gstVersion + ": Missing plugins: ";
|
std::string strError = gstVersion + ": Missing plugins: ";
|
||||||
const gchar *needed[] = {"audioconvert", "audioresample", "autodetect", "dtls", "nice",
|
const gchar *needed[] = {"audioconvert",
|
||||||
"opus", "playback", "rtpmanager", "srtp", "volume", "webrtc", nullptr};
|
"audioresample",
|
||||||
GstRegistry *registry = gst_registry_get();
|
"autodetect",
|
||||||
for (guint i = 0; i < g_strv_length((gchar**)needed); i++) {
|
"dtls",
|
||||||
GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]);
|
"nice",
|
||||||
if (!plugin) {
|
"opus",
|
||||||
strError += std::string(needed[i]) + " ";
|
"playback",
|
||||||
initialised_ = false;
|
"rtpmanager",
|
||||||
continue;
|
"srtp",
|
||||||
}
|
"volume",
|
||||||
gst_object_unref(plugin);
|
"webrtc",
|
||||||
}
|
nullptr};
|
||||||
|
GstRegistry *registry = gst_registry_get();
|
||||||
|
for (guint i = 0; i < g_strv_length((gchar **)needed); i++) {
|
||||||
|
GstPlugin *plugin = gst_registry_find_plugin(registry, needed[i]);
|
||||||
|
if (!plugin) {
|
||||||
|
strError += std::string(needed[i]) + " ";
|
||||||
|
initialised_ = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gst_object_unref(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initialised_) {
|
||||||
|
nhlog::ui()->error(strError);
|
||||||
|
if (errorMessage)
|
||||||
|
*errorMessage = strError;
|
||||||
|
}
|
||||||
|
return initialised_;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool isoffering_;
|
||||||
|
std::string localsdp_;
|
||||||
|
std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
|
||||||
|
{
|
||||||
|
WebRTCSession *session = static_cast<WebRTCSession *>(user_data);
|
||||||
|
switch (GST_MESSAGE_TYPE(msg)) {
|
||||||
|
case GST_MESSAGE_EOS:
|
||||||
|
nhlog::ui()->error("WebRTC: end of stream");
|
||||||
|
session->end();
|
||||||
|
break;
|
||||||
|
case GST_MESSAGE_ERROR:
|
||||||
|
GError *error;
|
||||||
|
gchar *debug;
|
||||||
|
gst_message_parse_error(msg, &error, &debug);
|
||||||
|
nhlog::ui()->error(
|
||||||
|
"WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message);
|
||||||
|
g_clear_error(&error);
|
||||||
|
g_free(debug);
|
||||||
|
session->end();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
GstWebRTCSessionDescription *
|
||||||
|
parseSDP(const std::string &sdp, GstWebRTCSDPType type)
|
||||||
|
{
|
||||||
|
GstSDPMessage *msg;
|
||||||
|
gst_sdp_message_new(&msg);
|
||||||
|
if (gst_sdp_message_parse_buffer((guint8 *)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) {
|
||||||
|
return gst_webrtc_session_description_new(type, msg);
|
||||||
|
} else {
|
||||||
|
nhlog::ui()->error("WebRTC: failed to parse remote session description");
|
||||||
|
gst_object_unref(msg);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
setLocalDescription(GstPromise *promise, gpointer webrtc)
|
||||||
|
{
|
||||||
|
const GstStructure *reply = gst_promise_get_reply(promise);
|
||||||
|
gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer"));
|
||||||
|
GstWebRTCSessionDescription *gstsdp = nullptr;
|
||||||
|
gst_structure_get(reply,
|
||||||
|
isAnswer ? "answer" : "offer",
|
||||||
|
GST_TYPE_WEBRTC_SESSION_DESCRIPTION,
|
||||||
|
&gstsdp,
|
||||||
|
nullptr);
|
||||||
|
gst_promise_unref(promise);
|
||||||
|
g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr);
|
||||||
|
|
||||||
|
gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
|
||||||
|
localsdp_ = std::string(sdp);
|
||||||
|
g_free(sdp);
|
||||||
|
gst_webrtc_session_description_free(gstsdp);
|
||||||
|
|
||||||
|
nhlog::ui()->debug(
|
||||||
|
"WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
createOffer(GstElement *webrtc)
|
||||||
|
{
|
||||||
|
// create-offer first, then set-local-description
|
||||||
|
GstPromise *promise =
|
||||||
|
gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
|
||||||
|
g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
createAnswer(GstPromise *promise, gpointer webrtc)
|
||||||
|
{
|
||||||
|
// create-answer first, then set-local-description
|
||||||
|
gst_promise_unref(promise);
|
||||||
|
promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
|
||||||
|
g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
onICEGatheringCompletion(gpointer timerid)
|
||||||
|
{
|
||||||
|
*(guint *)(timerid) = 0;
|
||||||
|
if (isoffering_) {
|
||||||
|
emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_);
|
||||||
|
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT);
|
||||||
|
} else {
|
||||||
|
emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_);
|
||||||
|
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ANSWERSENT);
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED,
|
||||||
|
guint mlineIndex,
|
||||||
|
gchar *candidate,
|
||||||
|
gpointer G_GNUC_UNUSED)
|
||||||
|
{
|
||||||
|
nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
|
||||||
|
|
||||||
|
if (WebRTCSession::instance().state() >= WebRTCSession::State::OFFERSENT) {
|
||||||
|
emit WebRTCSession::instance().newICECandidate(
|
||||||
|
{"audio", (uint16_t)mlineIndex, candidate});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate});
|
||||||
|
|
||||||
|
// GStreamer v1.16: webrtcbin's notify::ice-gathering-state triggers
|
||||||
|
// GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early. Fixed in v1.18. Use a 100ms timeout in
|
||||||
|
// the meantime
|
||||||
|
static guint timerid = 0;
|
||||||
|
if (timerid)
|
||||||
|
g_source_remove(timerid);
|
||||||
|
|
||||||
|
timerid = g_timeout_add(100, onICEGatheringCompletion, &timerid);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
iceConnectionStateChanged(GstElement *webrtc,
|
||||||
|
GParamSpec *pspec G_GNUC_UNUSED,
|
||||||
|
gpointer user_data G_GNUC_UNUSED)
|
||||||
|
{
|
||||||
|
GstWebRTCICEConnectionState newState;
|
||||||
|
g_object_get(webrtc, "ice-connection-state", &newState, nullptr);
|
||||||
|
switch (newState) {
|
||||||
|
case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING:
|
||||||
|
nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking");
|
||||||
|
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTING);
|
||||||
|
break;
|
||||||
|
case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED:
|
||||||
|
nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed");
|
||||||
|
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ICEFAILED);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
|
||||||
|
{
|
||||||
|
GstCaps *caps = gst_pad_get_current_caps(newpad);
|
||||||
|
if (!caps)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const gchar *name = gst_structure_get_name(gst_caps_get_structure(caps, 0));
|
||||||
|
gst_caps_unref(caps);
|
||||||
|
|
||||||
|
GstPad *queuepad = nullptr;
|
||||||
|
if (g_str_has_prefix(name, "audio")) {
|
||||||
|
nhlog::ui()->debug("WebRTC: received incoming audio stream");
|
||||||
|
GstElement *queue = gst_element_factory_make("queue", nullptr);
|
||||||
|
GstElement *convert = gst_element_factory_make("audioconvert", nullptr);
|
||||||
|
GstElement *resample = gst_element_factory_make("audioresample", nullptr);
|
||||||
|
GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr);
|
||||||
|
gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr);
|
||||||
|
gst_element_sync_state_with_parent(queue);
|
||||||
|
gst_element_sync_state_with_parent(convert);
|
||||||
|
gst_element_sync_state_with_parent(resample);
|
||||||
|
gst_element_sync_state_with_parent(sink);
|
||||||
|
gst_element_link_many(queue, convert, resample, sink, nullptr);
|
||||||
|
queuepad = gst_element_get_static_pad(queue, "sink");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queuepad) {
|
||||||
|
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad)))
|
||||||
|
nhlog::ui()->error("WebRTC: unable to link new pad");
|
||||||
|
else {
|
||||||
|
emit WebRTCSession::instance().stateChanged(
|
||||||
|
WebRTCSession::State::CONNECTED);
|
||||||
|
}
|
||||||
|
gst_object_unref(queuepad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
|
||||||
|
{
|
||||||
|
if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC)
|
||||||
|
return;
|
||||||
|
|
||||||
|
nhlog::ui()->debug("WebRTC: received incoming stream");
|
||||||
|
GstElement *decodebin = gst_element_factory_make("decodebin", nullptr);
|
||||||
|
g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe);
|
||||||
|
gst_bin_add(GST_BIN(pipe), decodebin);
|
||||||
|
gst_element_sync_state_with_parent(decodebin);
|
||||||
|
GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink");
|
||||||
|
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad)))
|
||||||
|
nhlog::ui()->error("WebRTC: unable to link new pad");
|
||||||
|
gst_object_unref(sinkpad);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string::const_iterator
|
||||||
|
findName(const std::string &sdp, const std::string &name)
|
||||||
|
{
|
||||||
|
return std::search(
|
||||||
|
sdp.cbegin(),
|
||||||
|
sdp.cend(),
|
||||||
|
name.cbegin(),
|
||||||
|
name.cend(),
|
||||||
|
[](unsigned char c1, unsigned char c2) { return std::tolower(c1) == std::tolower(c2); });
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
getPayloadType(const std::string &sdp, const std::string &name)
|
||||||
|
{
|
||||||
|
// eg a=rtpmap:111 opus/48000/2
|
||||||
|
auto e = findName(sdp, name);
|
||||||
|
if (e == sdp.cend()) {
|
||||||
|
nhlog::ui()->error("WebRTC: remote offer - " + name + " attribute missing");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto s = sdp.rfind(':', e - sdp.cbegin()); s == std::string::npos) {
|
||||||
|
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name +
|
||||||
|
" payload type");
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
++s;
|
||||||
|
try {
|
||||||
|
return std::stoi(std::string(sdp, s, e - sdp.cbegin() - s));
|
||||||
|
} catch (...) {
|
||||||
|
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name +
|
||||||
|
" payload type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
if (!initialised_) {
|
|
||||||
nhlog::ui()->error(strError);
|
|
||||||
if (errorMessage)
|
|
||||||
*errorMessage = strError;
|
|
||||||
}
|
|
||||||
return initialised_;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
WebRTCSession::createOffer()
|
WebRTCSession::createOffer()
|
||||||
{
|
{
|
||||||
isoffering_ = true;
|
isoffering_ = true;
|
||||||
localsdp_.clear();
|
localsdp_.clear();
|
||||||
localcandidates_.clear();
|
localcandidates_.clear();
|
||||||
return startPipeline(111); // a dynamic opus payload type
|
return startPipeline(111); // a dynamic opus payload type
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
WebRTCSession::acceptOffer(const std::string &sdp)
|
WebRTCSession::acceptOffer(const std::string &sdp)
|
||||||
{
|
{
|
||||||
nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp);
|
nhlog::ui()->debug("WebRTC: received offer:\n{}", sdp);
|
||||||
if (state_ != State::DISCONNECTED)
|
if (state_ != State::DISCONNECTED)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
isoffering_ = false;
|
isoffering_ = false;
|
||||||
localsdp_.clear();
|
localsdp_.clear();
|
||||||
localcandidates_.clear();
|
localcandidates_.clear();
|
||||||
|
|
||||||
int opusPayloadType = getPayloadType(sdp, "opus");
|
int opusPayloadType = getPayloadType(sdp, "opus");
|
||||||
if (opusPayloadType == -1)
|
if (opusPayloadType == -1)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
|
GstWebRTCSessionDescription *offer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_OFFER);
|
||||||
if (!offer)
|
if (!offer)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!startPipeline(opusPayloadType)) {
|
if (!startPipeline(opusPayloadType)) {
|
||||||
gst_webrtc_session_description_free(offer);
|
gst_webrtc_session_description_free(offer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// set-remote-description first, then create-answer
|
// set-remote-description first, then create-answer
|
||||||
GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr);
|
GstPromise *promise = gst_promise_new_with_change_func(createAnswer, webrtc_, nullptr);
|
||||||
g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise);
|
g_signal_emit_by_name(webrtc_, "set-remote-description", offer, promise);
|
||||||
gst_webrtc_session_description_free(offer);
|
gst_webrtc_session_description_free(offer);
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
WebRTCSession::acceptAnswer(const std::string &sdp)
|
||||||
|
{
|
||||||
|
nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp);
|
||||||
|
if (state_ != State::OFFERSENT)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
|
||||||
|
if (!answer) {
|
||||||
|
end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr);
|
||||||
|
gst_webrtc_session_description_free(answer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
WebRTCSession::acceptICECandidates(
|
||||||
|
const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates)
|
||||||
|
{
|
||||||
|
if (state_ >= State::INITIATED) {
|
||||||
|
for (const auto &c : candidates) {
|
||||||
|
nhlog::ui()->debug(
|
||||||
|
"WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate);
|
||||||
|
g_signal_emit_by_name(
|
||||||
|
webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
WebRTCSession::startPipeline(int opusPayloadType)
|
WebRTCSession::startPipeline(int opusPayloadType)
|
||||||
{
|
{
|
||||||
if (state_ != State::DISCONNECTED)
|
if (state_ != State::DISCONNECTED)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
emit stateChanged(State::INITIATING);
|
emit stateChanged(State::INITIATING);
|
||||||
|
|
||||||
if (!createPipeline(opusPayloadType))
|
if (!createPipeline(opusPayloadType))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin");
|
webrtc_ = gst_bin_get_by_name(GST_BIN(pipe_), "webrtcbin");
|
||||||
|
|
||||||
if (!stunServer_.empty()) {
|
if (!stunServer_.empty()) {
|
||||||
nhlog::ui()->info("WebRTC: setting STUN server: {}", stunServer_);
|
nhlog::ui()->info("WebRTC: setting STUN server: {}", stunServer_);
|
||||||
g_object_set(webrtc_, "stun-server", stunServer_.c_str(), nullptr);
|
g_object_set(webrtc_, "stun-server", stunServer_.c_str(), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &uri : turnServers_) {
|
for (const auto &uri : turnServers_) {
|
||||||
nhlog::ui()->info("WebRTC: setting TURN server: {}", uri);
|
nhlog::ui()->info("WebRTC: setting TURN server: {}", uri);
|
||||||
gboolean udata;
|
gboolean udata;
|
||||||
g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata));
|
g_signal_emit_by_name(webrtc_, "add-turn-server", uri.c_str(), (gpointer)(&udata));
|
||||||
}
|
}
|
||||||
if (turnServers_.empty())
|
if (turnServers_.empty())
|
||||||
nhlog::ui()->warn("WebRTC: no TURN server provided");
|
nhlog::ui()->warn("WebRTC: no TURN server provided");
|
||||||
|
|
||||||
// generate the offer when the pipeline goes to PLAYING
|
// generate the offer when the pipeline goes to PLAYING
|
||||||
if (isoffering_)
|
if (isoffering_)
|
||||||
g_signal_connect(webrtc_, "on-negotiation-needed", G_CALLBACK(generateOffer), nullptr);
|
g_signal_connect(
|
||||||
|
webrtc_, "on-negotiation-needed", G_CALLBACK(::createOffer), nullptr);
|
||||||
|
|
||||||
// on-ice-candidate is emitted when a local ICE candidate has been gathered
|
// on-ice-candidate is emitted when a local ICE candidate has been gathered
|
||||||
g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr);
|
g_signal_connect(webrtc_, "on-ice-candidate", G_CALLBACK(addLocalICECandidate), nullptr);
|
||||||
|
|
||||||
// capture ICE failure
|
// capture ICE failure
|
||||||
g_signal_connect(webrtc_, "notify::ice-connection-state",
|
g_signal_connect(
|
||||||
G_CALLBACK(iceConnectionStateChanged), nullptr);
|
webrtc_, "notify::ice-connection-state", G_CALLBACK(iceConnectionStateChanged), nullptr);
|
||||||
|
|
||||||
// incoming streams trigger pad-added
|
// incoming streams trigger pad-added
|
||||||
gst_element_set_state(pipe_, GST_STATE_READY);
|
gst_element_set_state(pipe_, GST_STATE_READY);
|
||||||
g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_);
|
g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_);
|
||||||
|
|
||||||
// webrtcbin lifetime is the same as that of the pipeline
|
// webrtcbin lifetime is the same as that of the pipeline
|
||||||
gst_object_unref(webrtc_);
|
gst_object_unref(webrtc_);
|
||||||
|
|
||||||
// start the pipeline
|
// start the pipeline
|
||||||
GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING);
|
GstStateChangeReturn ret = gst_element_set_state(pipe_, GST_STATE_PLAYING);
|
||||||
if (ret == GST_STATE_CHANGE_FAILURE) {
|
if (ret == GST_STATE_CHANGE_FAILURE) {
|
||||||
nhlog::ui()->error("WebRTC: unable to start pipeline");
|
nhlog::ui()->error("WebRTC: unable to start pipeline");
|
||||||
end();
|
end();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
|
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
|
||||||
gst_bus_add_watch(bus, newBusMessage, this);
|
gst_bus_add_watch(bus, newBusMessage, this);
|
||||||
gst_object_unref(bus);
|
gst_object_unref(bus);
|
||||||
emit stateChanged(State::INITIATED);
|
emit stateChanged(State::INITIATED);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
|
#define RTP_CAPS_OPUS "application/x-rtp,media=audio,encoding-name=OPUS,payload="
|
||||||
@ -194,297 +460,52 @@ WebRTCSession::startPipeline(int opusPayloadType)
|
|||||||
bool
|
bool
|
||||||
WebRTCSession::createPipeline(int opusPayloadType)
|
WebRTCSession::createPipeline(int opusPayloadType)
|
||||||
{
|
{
|
||||||
std::string pipeline("webrtcbin bundle-policy=max-bundle name=webrtcbin "
|
std::string pipeline("webrtcbin bundle-policy=max-bundle name=webrtcbin "
|
||||||
"autoaudiosrc ! volume name=srclevel ! audioconvert ! audioresample ! queue ! opusenc ! rtpopuspay ! "
|
"autoaudiosrc ! volume name=srclevel ! audioconvert ! "
|
||||||
"queue ! " RTP_CAPS_OPUS + std::to_string(opusPayloadType) + " ! webrtcbin.");
|
"audioresample ! queue ! opusenc ! rtpopuspay ! "
|
||||||
|
"queue ! " RTP_CAPS_OPUS +
|
||||||
|
std::to_string(opusPayloadType) + " ! webrtcbin.");
|
||||||
|
|
||||||
webrtc_ = nullptr;
|
webrtc_ = nullptr;
|
||||||
GError *error = nullptr;
|
GError *error = nullptr;
|
||||||
pipe_ = gst_parse_launch(pipeline.c_str(), &error);
|
pipe_ = gst_parse_launch(pipeline.c_str(), &error);
|
||||||
if (error) {
|
if (error) {
|
||||||
nhlog::ui()->error("WebRTC: failed to parse pipeline: {}", error->message);
|
nhlog::ui()->error("WebRTC: failed to parse pipeline: {}", error->message);
|
||||||
g_error_free(error);
|
g_error_free(error);
|
||||||
end();
|
end();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
WebRTCSession::acceptAnswer(const std::string &sdp)
|
|
||||||
{
|
|
||||||
nhlog::ui()->debug("WebRTC: received answer:\n{}", sdp);
|
|
||||||
if (state_ != State::OFFERSENT)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
GstWebRTCSessionDescription *answer = parseSDP(sdp, GST_WEBRTC_SDP_TYPE_ANSWER);
|
|
||||||
if (!answer) {
|
|
||||||
end();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_signal_emit_by_name(webrtc_, "set-remote-description", answer, nullptr);
|
|
||||||
gst_webrtc_session_description_free(answer);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &candidates)
|
|
||||||
{
|
|
||||||
if (state_ >= State::INITIATED) {
|
|
||||||
for (const auto &c : candidates) {
|
|
||||||
nhlog::ui()->debug("WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate);
|
|
||||||
g_signal_emit_by_name(webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
WebRTCSession::toggleMuteAudioSrc(bool &isMuted)
|
WebRTCSession::toggleMuteAudioSrc(bool &isMuted)
|
||||||
{
|
{
|
||||||
if (state_ < State::INITIATED)
|
if (state_ < State::INITIATED)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
|
GstElement *srclevel = gst_bin_get_by_name(GST_BIN(pipe_), "srclevel");
|
||||||
if (!srclevel)
|
if (!srclevel)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
gboolean muted;
|
gboolean muted;
|
||||||
g_object_get(srclevel, "mute", &muted, nullptr);
|
g_object_get(srclevel, "mute", &muted, nullptr);
|
||||||
g_object_set(srclevel, "mute", !muted, nullptr);
|
g_object_set(srclevel, "mute", !muted, nullptr);
|
||||||
gst_object_unref(srclevel);
|
gst_object_unref(srclevel);
|
||||||
isMuted = !muted;
|
isMuted = !muted;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
WebRTCSession::end()
|
WebRTCSession::end()
|
||||||
{
|
{
|
||||||
nhlog::ui()->debug("WebRTC: ending session");
|
nhlog::ui()->debug("WebRTC: ending session");
|
||||||
if (pipe_) {
|
if (pipe_) {
|
||||||
gst_element_set_state(pipe_, GST_STATE_NULL);
|
gst_element_set_state(pipe_, GST_STATE_NULL);
|
||||||
gst_object_unref(pipe_);
|
gst_object_unref(pipe_);
|
||||||
pipe_ = nullptr;
|
pipe_ = nullptr;
|
||||||
}
|
}
|
||||||
webrtc_ = nullptr;
|
webrtc_ = nullptr;
|
||||||
if (state_ != State::DISCONNECTED)
|
if (state_ != State::DISCONNECTED)
|
||||||
emit stateChanged(State::DISCONNECTED);
|
emit stateChanged(State::DISCONNECTED);
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
std::string::const_iterator findName(const std::string &sdp, const std::string &name)
|
|
||||||
{
|
|
||||||
return std::search(sdp.cbegin(), sdp.cend(), name.cbegin(), name.cend(),
|
|
||||||
[](unsigned char c1, unsigned char c2) {return std::tolower(c1) == std::tolower(c2);});
|
|
||||||
}
|
|
||||||
|
|
||||||
int getPayloadType(const std::string &sdp, const std::string &name)
|
|
||||||
{
|
|
||||||
// eg a=rtpmap:111 opus/48000/2
|
|
||||||
auto e = findName(sdp, name);
|
|
||||||
if (e == sdp.cend()) {
|
|
||||||
nhlog::ui()->error("WebRTC: remote offer - " + name + " attribute missing");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto s = sdp.rfind(':', e - sdp.cbegin()); s == std::string::npos) {
|
|
||||||
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name + " payload type");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
++s;
|
|
||||||
try {
|
|
||||||
return std::stoi(std::string(sdp, s, e - sdp.cbegin() - s));
|
|
||||||
}
|
|
||||||
catch(...) {
|
|
||||||
nhlog::ui()->error("WebRTC: remote offer - unable to determine " + name + " payload type");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean
|
|
||||||
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
|
|
||||||
{
|
|
||||||
WebRTCSession *session = (WebRTCSession*)user_data;
|
|
||||||
switch (GST_MESSAGE_TYPE(msg)) {
|
|
||||||
case GST_MESSAGE_EOS:
|
|
||||||
nhlog::ui()->error("WebRTC: end of stream");
|
|
||||||
session->end();
|
|
||||||
break;
|
|
||||||
case GST_MESSAGE_ERROR:
|
|
||||||
GError *error;
|
|
||||||
gchar *debug;
|
|
||||||
gst_message_parse_error(msg, &error, &debug);
|
|
||||||
nhlog::ui()->error("WebRTC: error from element {}: {}", GST_OBJECT_NAME(msg->src), error->message);
|
|
||||||
g_clear_error(&error);
|
|
||||||
g_free(debug);
|
|
||||||
session->end();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
GstWebRTCSessionDescription*
|
|
||||||
parseSDP(const std::string &sdp, GstWebRTCSDPType type)
|
|
||||||
{
|
|
||||||
GstSDPMessage *msg;
|
|
||||||
gst_sdp_message_new(&msg);
|
|
||||||
if (gst_sdp_message_parse_buffer((guint8*)sdp.c_str(), sdp.size(), msg) == GST_SDP_OK) {
|
|
||||||
return gst_webrtc_session_description_new(type, msg);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
nhlog::ui()->error("WebRTC: failed to parse remote session description");
|
|
||||||
gst_object_unref(msg);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
generateOffer(GstElement *webrtc)
|
|
||||||
{
|
|
||||||
// create-offer first, then set-local-description
|
|
||||||
GstPromise *promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
|
|
||||||
g_signal_emit_by_name(webrtc, "create-offer", nullptr, promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
setLocalDescription(GstPromise *promise, gpointer webrtc)
|
|
||||||
{
|
|
||||||
const GstStructure *reply = gst_promise_get_reply(promise);
|
|
||||||
gboolean isAnswer = gst_structure_id_has_field(reply, g_quark_from_string("answer"));
|
|
||||||
GstWebRTCSessionDescription *gstsdp = nullptr;
|
|
||||||
gst_structure_get(reply, isAnswer ? "answer" : "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION, &gstsdp, nullptr);
|
|
||||||
gst_promise_unref(promise);
|
|
||||||
g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr);
|
|
||||||
|
|
||||||
gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
|
|
||||||
localsdp_ = std::string(sdp);
|
|
||||||
g_free(sdp);
|
|
||||||
gst_webrtc_session_description_free(gstsdp);
|
|
||||||
|
|
||||||
nhlog::ui()->debug("WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, guint mlineIndex, gchar *candidate, gpointer G_GNUC_UNUSED)
|
|
||||||
{
|
|
||||||
nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
|
|
||||||
|
|
||||||
if (WebRTCSession::instance().state() >= WebRTCSession::State::OFFERSENT) {
|
|
||||||
emit WebRTCSession::instance().newICECandidate({"audio", (uint16_t)mlineIndex, candidate});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate});
|
|
||||||
|
|
||||||
// GStreamer v1.16: webrtcbin's notify::ice-gathering-state triggers GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early
|
|
||||||
// fixed in v1.18
|
|
||||||
// use a 100ms timeout in the meantime
|
|
||||||
static guint timerid = 0;
|
|
||||||
if (timerid)
|
|
||||||
g_source_remove(timerid);
|
|
||||||
|
|
||||||
timerid = g_timeout_add(100, onICEGatheringCompletion, &timerid);
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean
|
|
||||||
onICEGatheringCompletion(gpointer timerid)
|
|
||||||
{
|
|
||||||
*(guint*)(timerid) = 0;
|
|
||||||
if (isoffering_) {
|
|
||||||
emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_);
|
|
||||||
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_);
|
|
||||||
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ANSWERSENT);
|
|
||||||
}
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
iceConnectionStateChanged(GstElement *webrtc, GParamSpec *pspec G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED)
|
|
||||||
{
|
|
||||||
GstWebRTCICEConnectionState newState;
|
|
||||||
g_object_get(webrtc, "ice-connection-state", &newState, nullptr);
|
|
||||||
switch (newState) {
|
|
||||||
case GST_WEBRTC_ICE_CONNECTION_STATE_CHECKING:
|
|
||||||
nhlog::ui()->debug("WebRTC: GstWebRTCICEConnectionState -> Checking");
|
|
||||||
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTING);
|
|
||||||
break;
|
|
||||||
case GST_WEBRTC_ICE_CONNECTION_STATE_FAILED:
|
|
||||||
nhlog::ui()->error("WebRTC: GstWebRTCICEConnectionState -> Failed");
|
|
||||||
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ICEFAILED);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
createAnswer(GstPromise *promise, gpointer webrtc)
|
|
||||||
{
|
|
||||||
// create-answer first, then set-local-description
|
|
||||||
gst_promise_unref(promise);
|
|
||||||
promise = gst_promise_new_with_change_func(setLocalDescription, webrtc, nullptr);
|
|
||||||
g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
|
|
||||||
{
|
|
||||||
if (GST_PAD_DIRECTION(newpad) != GST_PAD_SRC)
|
|
||||||
return;
|
|
||||||
|
|
||||||
nhlog::ui()->debug("WebRTC: received incoming stream");
|
|
||||||
GstElement *decodebin = gst_element_factory_make("decodebin", nullptr);
|
|
||||||
g_signal_connect(decodebin, "pad-added", G_CALLBACK(linkNewPad), pipe);
|
|
||||||
gst_bin_add(GST_BIN(pipe), decodebin);
|
|
||||||
gst_element_sync_state_with_parent(decodebin);
|
|
||||||
GstPad *sinkpad = gst_element_get_static_pad(decodebin, "sink");
|
|
||||||
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, sinkpad)))
|
|
||||||
nhlog::ui()->error("WebRTC: unable to link new pad");
|
|
||||||
gst_object_unref(sinkpad);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe)
|
|
||||||
{
|
|
||||||
GstCaps *caps = gst_pad_get_current_caps(newpad);
|
|
||||||
if (!caps)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const gchar *name = gst_structure_get_name(gst_caps_get_structure(caps, 0));
|
|
||||||
gst_caps_unref(caps);
|
|
||||||
|
|
||||||
GstPad *queuepad = nullptr;
|
|
||||||
if (g_str_has_prefix(name, "audio")) {
|
|
||||||
nhlog::ui()->debug("WebRTC: received incoming audio stream");
|
|
||||||
GstElement *queue = gst_element_factory_make("queue", nullptr);
|
|
||||||
GstElement *convert = gst_element_factory_make("audioconvert", nullptr);
|
|
||||||
GstElement *resample = gst_element_factory_make("audioresample", nullptr);
|
|
||||||
GstElement *sink = gst_element_factory_make("autoaudiosink", nullptr);
|
|
||||||
gst_bin_add_many(GST_BIN(pipe), queue, convert, resample, sink, nullptr);
|
|
||||||
gst_element_sync_state_with_parent(queue);
|
|
||||||
gst_element_sync_state_with_parent(convert);
|
|
||||||
gst_element_sync_state_with_parent(resample);
|
|
||||||
gst_element_sync_state_with_parent(sink);
|
|
||||||
gst_element_link_many(queue, convert, resample, sink, nullptr);
|
|
||||||
queuepad = gst_element_get_static_pad(queue, "sink");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queuepad) {
|
|
||||||
if (GST_PAD_LINK_FAILED(gst_pad_link(newpad, queuepad)))
|
|
||||||
nhlog::ui()->error("WebRTC: unable to link new pad");
|
|
||||||
else {
|
|
||||||
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTED);
|
|
||||||
}
|
|
||||||
gst_object_unref(queuepad);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,52 +14,55 @@ class WebRTCSession : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum class State {
|
enum class State
|
||||||
ICEFAILED,
|
{
|
||||||
DISCONNECTED,
|
DISCONNECTED,
|
||||||
INITIATING,
|
ICEFAILED,
|
||||||
INITIATED,
|
INITIATING,
|
||||||
OFFERSENT,
|
INITIATED,
|
||||||
ANSWERSENT,
|
OFFERSENT,
|
||||||
CONNECTING,
|
ANSWERSENT,
|
||||||
CONNECTED
|
CONNECTING,
|
||||||
|
CONNECTED
|
||||||
};
|
};
|
||||||
|
|
||||||
static WebRTCSession& instance()
|
static WebRTCSession &instance()
|
||||||
{
|
{
|
||||||
static WebRTCSession instance;
|
static WebRTCSession instance;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool init(std::string *errorMessage = nullptr);
|
bool init(std::string *errorMessage = nullptr);
|
||||||
State state() const {return state_;}
|
State state() const { return state_; }
|
||||||
|
|
||||||
bool createOffer();
|
bool createOffer();
|
||||||
bool acceptOffer(const std::string &sdp);
|
bool acceptOffer(const std::string &sdp);
|
||||||
bool acceptAnswer(const std::string &sdp);
|
bool acceptAnswer(const std::string &sdp);
|
||||||
void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate>&);
|
void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
|
||||||
|
|
||||||
bool toggleMuteAudioSrc(bool &isMuted);
|
bool toggleMuteAudioSrc(bool &isMuted);
|
||||||
void end();
|
void end();
|
||||||
|
|
||||||
void setStunServer(const std::string &stunServer) {stunServer_ = stunServer;}
|
void setStunServer(const std::string &stunServer) { stunServer_ = stunServer; }
|
||||||
void setTurnServers(const std::vector<std::string> &uris) {turnServers_ = uris;}
|
void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void offerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&);
|
void offerCreated(const std::string &sdp,
|
||||||
void answerCreated(const std::string &sdp, const std::vector<mtx::events::msg::CallCandidates::Candidate>&);
|
const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
|
||||||
void newICECandidate(const mtx::events::msg::CallCandidates::Candidate&);
|
void answerCreated(const std::string &sdp,
|
||||||
|
const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
|
||||||
|
void newICECandidate(const mtx::events::msg::CallCandidates::Candidate &);
|
||||||
void stateChanged(WebRTCSession::State); // explicit qualifier necessary for Qt
|
void stateChanged(WebRTCSession::State); // explicit qualifier necessary for Qt
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void setState(State state) {state_ = state;}
|
void setState(State state) { state_ = state; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
WebRTCSession();
|
WebRTCSession();
|
||||||
|
|
||||||
bool initialised_ = false;
|
bool initialised_ = false;
|
||||||
State state_ = State::DISCONNECTED;
|
State state_ = State::DISCONNECTED;
|
||||||
GstElement *pipe_ = nullptr;
|
GstElement *pipe_ = nullptr;
|
||||||
GstElement *webrtc_ = nullptr;
|
GstElement *webrtc_ = nullptr;
|
||||||
std::string stunServer_;
|
std::string stunServer_;
|
||||||
std::vector<std::string> turnServers_;
|
std::vector<std::string> turnServers_;
|
||||||
@ -68,6 +71,6 @@ private:
|
|||||||
bool createPipeline(int opusPayloadType);
|
bool createPipeline(int opusPayloadType);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
WebRTCSession(WebRTCSession const&) = delete;
|
WebRTCSession(WebRTCSession const &) = delete;
|
||||||
void operator=(WebRTCSession const&) = delete;
|
void operator=(WebRTCSession const &) = delete;
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@ -10,12 +11,12 @@
|
|||||||
|
|
||||||
namespace dialogs {
|
namespace dialogs {
|
||||||
|
|
||||||
AcceptCall::AcceptCall(
|
AcceptCall::AcceptCall(const QString &caller,
|
||||||
const QString &caller,
|
const QString &displayName,
|
||||||
const QString &displayName,
|
const QString &roomName,
|
||||||
const QString &roomName,
|
const QString &avatarUrl,
|
||||||
const QString &avatarUrl,
|
QWidget *parent)
|
||||||
QWidget *parent) : QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||||
@ -39,8 +40,8 @@ AcceptCall::AcceptCall(
|
|||||||
if (!displayName.isEmpty() && displayName != caller) {
|
if (!displayName.isEmpty() && displayName != caller) {
|
||||||
displayNameLabel = new QLabel(displayName, this);
|
displayNameLabel = new QLabel(displayName, this);
|
||||||
labelFont.setPointSizeF(f.pointSizeF() * 2);
|
labelFont.setPointSizeF(f.pointSizeF() * 2);
|
||||||
displayNameLabel ->setFont(labelFont);
|
displayNameLabel->setFont(labelFont);
|
||||||
displayNameLabel ->setAlignment(Qt::AlignCenter);
|
displayNameLabel->setAlignment(Qt::AlignCenter);
|
||||||
}
|
}
|
||||||
|
|
||||||
QLabel *callerLabel = new QLabel(caller, this);
|
QLabel *callerLabel = new QLabel(caller, this);
|
||||||
@ -48,19 +49,23 @@ AcceptCall::AcceptCall(
|
|||||||
callerLabel->setFont(labelFont);
|
callerLabel->setFont(labelFont);
|
||||||
callerLabel->setAlignment(Qt::AlignCenter);
|
callerLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
QLabel *voiceCallLabel = new QLabel("Voice Call", this);
|
|
||||||
labelFont.setPointSizeF(f.pointSizeF() * 1.1);
|
|
||||||
voiceCallLabel->setFont(labelFont);
|
|
||||||
voiceCallLabel->setAlignment(Qt::AlignCenter);
|
|
||||||
|
|
||||||
auto avatar = new Avatar(this, QFontMetrics(f).height() * 6);
|
auto avatar = new Avatar(this, QFontMetrics(f).height() * 6);
|
||||||
if (!avatarUrl.isEmpty())
|
if (!avatarUrl.isEmpty())
|
||||||
avatar->setImage(avatarUrl);
|
avatar->setImage(avatarUrl);
|
||||||
else
|
else
|
||||||
avatar->setLetter(utils::firstChar(roomName));
|
avatar->setLetter(utils::firstChar(roomName));
|
||||||
|
|
||||||
const int iconSize = 24;
|
const int iconSize = 24;
|
||||||
auto buttonLayout = new QHBoxLayout();
|
QLabel *callTypeIndicator = new QLabel(this);
|
||||||
|
QPixmap callIndicator(":/icons/icons/ui/place-call.png");
|
||||||
|
callTypeIndicator->setPixmap(callIndicator.scaled(iconSize * 2, iconSize * 2));
|
||||||
|
|
||||||
|
QLabel *callTypeLabel = new QLabel("Voice Call", this);
|
||||||
|
labelFont.setPointSizeF(f.pointSizeF() * 1.1);
|
||||||
|
callTypeLabel->setFont(labelFont);
|
||||||
|
callTypeLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
|
||||||
|
auto buttonLayout = new QHBoxLayout;
|
||||||
buttonLayout->setSpacing(20);
|
buttonLayout->setSpacing(20);
|
||||||
acceptBtn_ = new QPushButton(tr("Accept"), this);
|
acceptBtn_ = new QPushButton(tr("Accept"), this);
|
||||||
acceptBtn_->setDefault(true);
|
acceptBtn_->setDefault(true);
|
||||||
@ -74,10 +79,11 @@ AcceptCall::AcceptCall(
|
|||||||
buttonLayout->addWidget(rejectBtn_);
|
buttonLayout->addWidget(rejectBtn_);
|
||||||
|
|
||||||
if (displayNameLabel)
|
if (displayNameLabel)
|
||||||
layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
|
layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
|
||||||
layout->addWidget(callerLabel, 0, Qt::AlignCenter);
|
layout->addWidget(callerLabel, 0, Qt::AlignCenter);
|
||||||
layout->addWidget(voiceCallLabel, 0, Qt::AlignCenter);
|
|
||||||
layout->addWidget(avatar, 0, Qt::AlignCenter);
|
layout->addWidget(avatar, 0, Qt::AlignCenter);
|
||||||
|
layout->addWidget(callTypeIndicator, 0, Qt::AlignCenter);
|
||||||
|
layout->addWidget(callTypeLabel, 0, Qt::AlignCenter);
|
||||||
layout->addLayout(buttonLayout);
|
layout->addLayout(buttonLayout);
|
||||||
|
|
||||||
connect(acceptBtn_, &QPushButton::clicked, this, [this]() {
|
connect(acceptBtn_, &QPushButton::clicked, this, [this]() {
|
||||||
|
@ -12,12 +12,11 @@ class AcceptCall : public QWidget
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AcceptCall(
|
AcceptCall(const QString &caller,
|
||||||
const QString &caller,
|
const QString &displayName,
|
||||||
const QString &displayName,
|
const QString &roomName,
|
||||||
const QString &roomName,
|
const QString &avatarUrl,
|
||||||
const QString &avatarUrl,
|
QWidget *parent = nullptr);
|
||||||
QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void accept();
|
void accept();
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
|
|
||||||
namespace dialogs {
|
namespace dialogs {
|
||||||
|
|
||||||
PlaceCall::PlaceCall(
|
PlaceCall::PlaceCall(const QString &callee,
|
||||||
const QString &callee,
|
const QString &displayName,
|
||||||
const QString &displayName,
|
const QString &roomName,
|
||||||
const QString &roomName,
|
const QString &avatarUrl,
|
||||||
const QString &avatarUrl,
|
QWidget *parent)
|
||||||
QWidget *parent) : QWidget(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
setAutoFillBackground(true);
|
setAutoFillBackground(true);
|
||||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||||
@ -34,11 +34,13 @@ PlaceCall::PlaceCall(
|
|||||||
f.setPointSizeF(f.pointSizeF());
|
f.setPointSizeF(f.pointSizeF());
|
||||||
auto avatar = new Avatar(this, QFontMetrics(f).height() * 3);
|
auto avatar = new Avatar(this, QFontMetrics(f).height() * 3);
|
||||||
if (!avatarUrl.isEmpty())
|
if (!avatarUrl.isEmpty())
|
||||||
avatar->setImage(avatarUrl);
|
avatar->setImage(avatarUrl);
|
||||||
else
|
else
|
||||||
avatar->setLetter(utils::firstChar(roomName));
|
avatar->setLetter(utils::firstChar(roomName));
|
||||||
|
const int iconSize = 24;
|
||||||
voiceBtn_ = new QPushButton(tr("Voice Call"), this);
|
voiceBtn_ = new QPushButton(tr("Voice"), this);
|
||||||
|
voiceBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
|
||||||
|
voiceBtn_->setIconSize(QSize(iconSize, iconSize));
|
||||||
voiceBtn_->setDefault(true);
|
voiceBtn_->setDefault(true);
|
||||||
cancelBtn_ = new QPushButton(tr("Cancel"), this);
|
cancelBtn_ = new QPushButton(tr("Cancel"), this);
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ PlaceCall::PlaceCall(
|
|||||||
buttonLayout->addWidget(voiceBtn_);
|
buttonLayout->addWidget(voiceBtn_);
|
||||||
buttonLayout->addWidget(cancelBtn_);
|
buttonLayout->addWidget(cancelBtn_);
|
||||||
|
|
||||||
QString name = displayName.isEmpty() ? callee : displayName;
|
QString name = displayName.isEmpty() ? callee : displayName;
|
||||||
QLabel *label = new QLabel("Place a call to " + name + "?", this);
|
QLabel *label = new QLabel("Place a call to " + name + "?", this);
|
||||||
|
|
||||||
layout->addWidget(label);
|
layout->addWidget(label);
|
||||||
|
@ -12,12 +12,11 @@ class PlaceCall : public QWidget
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PlaceCall(
|
PlaceCall(const QString &callee,
|
||||||
const QString &callee,
|
const QString &displayName,
|
||||||
const QString &displayName,
|
const QString &roomName,
|
||||||
const QString &roomName,
|
const QString &avatarUrl,
|
||||||
const QString &avatarUrl,
|
QWidget *parent = nullptr);
|
||||||
QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void voice();
|
void voice();
|
||||||
|
@ -796,9 +796,11 @@ TimelineModel::internalAddEvents(
|
|||||||
} else if (std::holds_alternative<mtx::events::RoomEvent<
|
} else if (std::holds_alternative<mtx::events::RoomEvent<
|
||||||
mtx::events::msg::CallCandidates>>(e_) ||
|
mtx::events::msg::CallCandidates>>(e_) ||
|
||||||
std::holds_alternative<
|
std::holds_alternative<
|
||||||
mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>( e_) ||
|
mtx::events::RoomEvent<mtx::events::msg::CallAnswer>>(
|
||||||
|
e_) ||
|
||||||
std::holds_alternative<
|
std::holds_alternative<
|
||||||
mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>( e_)) {
|
mtx::events::RoomEvent<mtx::events::msg::CallHangUp>>(
|
||||||
|
e_)) {
|
||||||
emit newCallEvent(e_);
|
emit newCallEvent(e_);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user