Handle ICE failure

This commit is contained in:
trilene 2020-07-26 10:59:50 -04:00
parent 57d5a3d31f
commit 43ec0c0624
7 changed files with 131 additions and 73 deletions

View file

@ -123,25 +123,32 @@ ActiveCallBar::update(WebRTCSession::State state)
{ {
switch (state) { switch (state) {
case WebRTCSession::State::INITIATING: case WebRTCSession::State::INITIATING:
show();
stateLabel_->setText("Initiating call..."); stateLabel_->setText("Initiating call...");
break; break;
case WebRTCSession::State::INITIATED: case WebRTCSession::State::INITIATED:
show();
stateLabel_->setText("Call initiated..."); stateLabel_->setText("Call initiated...");
break; break;
case WebRTCSession::State::OFFERSENT: case WebRTCSession::State::OFFERSENT:
show();
stateLabel_->setText("Calling..."); stateLabel_->setText("Calling...");
break; break;
case WebRTCSession::State::CONNECTING: case WebRTCSession::State::CONNECTING:
show();
stateLabel_->setText("Connecting..."); stateLabel_->setText("Connecting...");
break; break;
case WebRTCSession::State::CONNECTED: case WebRTCSession::State::CONNECTED:
show();
callStartTime_ = QDateTime::currentSecsSinceEpoch(); callStartTime_ = QDateTime::currentSecsSinceEpoch();
timer_->start(1000); timer_->start(1000);
stateLabel_->setText("Voice call:"); stateLabel_->setText("Voice call:");
durationLabel_->setText("00:00"); durationLabel_->setText("00:00");
durationLabel_->show(); durationLabel_->show();
break; break;
case WebRTCSession::State::ICEFAILED:
case WebRTCSession::State::DISCONNECTED: case WebRTCSession::State::DISCONNECTED:
hide();
timer_->stop(); timer_->stop();
callPartyLabel_->setText(QString()); callPartyLabel_->setText(QString());
stateLabel_->setText(QString()); stateLabel_->setText(QString());

View file

@ -11,9 +11,10 @@
#include "MatrixClient.h" #include "MatrixClient.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "WebRTCSession.h" #include "WebRTCSession.h"
#include "dialogs/AcceptCall.h" #include "dialogs/AcceptCall.h"
#include "mtx/responses/turn_server.hpp"
Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>) Q_DECLARE_METATYPE(std::vector<mtx::events::msg::CallCandidates::Candidate>)
Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate) Q_DECLARE_METATYPE(mtx::events::msg::CallCandidates::Candidate)
Q_DECLARE_METATYPE(mtx::responses::TurnServer) Q_DECLARE_METATYPE(mtx::responses::TurnServer)
@ -24,6 +25,11 @@ using namespace mtx::events::msg;
// https://github.com/vector-im/riot-web/issues/10173 // https://github.com/vector-im/riot-web/issues/10173
#define STUN_SERVER "stun://turn.matrix.org:3478" #define STUN_SERVER "stun://turn.matrix.org:3478"
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer);
}
CallManager::CallManager(QSharedPointer<UserSettings> userSettings) CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
: QObject(), : QObject(),
session_(WebRTCSession::instance()), session_(WebRTCSession::instance()),
@ -80,14 +86,22 @@ CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
// Request new credentials close to expiry // Request new credentials close to expiry
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 // See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
turnServer_ = res; turnURIs_ = getTurnURIs(res);
turnServerTimer_.setInterval(res.ttl * 1000 * 0.9); turnServerTimer_.setInterval(res.ttl * 1000 * 0.9);
}); });
connect(&session_, &WebRTCSession::stateChanged, this, connect(&session_, &WebRTCSession::stateChanged, this,
[this](WebRTCSession::State state) { [this](WebRTCSession::State state) {
if (state == WebRTCSession::State::DISCONNECTED) if (state == WebRTCSession::State::DISCONNECTED) {
playRingtone("qrc:/media/media/callend.ogg", false); playRingtone("qrc:/media/media/callend.ogg", false);
}
else if (state == 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);
}
}); });
connect(&player_, &QMediaPlayer::mediaStatusChanged, this, connect(&player_, &QMediaPlayer::mediaStatusChanged, this,
@ -116,8 +130,8 @@ CallManager::sendInvite(const QString &roomid)
} }
roomid_ = roomid; roomid_ = roomid;
setTurnServers();
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
session_.setTurnServers(turnURIs_);
generateCallID(); generateCallID();
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_); nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_);
@ -132,11 +146,26 @@ CallManager::sendInvite(const QString &roomid)
} }
} }
namespace {
std::string callHangUpReasonString(CallHangUp::Reason reason)
{
switch (reason) {
case CallHangUp::Reason::ICEFailed:
return "ICE failed";
case CallHangUp::Reason::InviteTimeOut:
return "Invite time out";
default:
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("WebRTC: call id: {} - hanging up ({})", callid_,
callHangUpReasonString(reason));
emit newMessage(roomid_, CallHangUp{callid_, 0, reason}); emit newMessage(roomid_, CallHangUp{callid_, 0, reason});
endCall(); endCall();
} }
@ -221,8 +250,8 @@ CallManager::answerInvite(const CallInvite &invite)
return; return;
} }
setTurnServers();
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : ""); session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
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.");
@ -279,8 +308,9 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
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, callHangUpEvent.sender); callHangUpEvent.content.call_id, callHangUpReasonString(callHangUpEvent.content.reason),
callHangUpEvent.sender);
if (callid_ == callHangUpEvent.content.call_id) { if (callid_ == callHangUpEvent.content.call_id) {
MainWindow::instance()->hideOverlay(); MainWindow::instance()->hideOverlay();
@ -319,35 +349,6 @@ CallManager::retrieveTurnServer()
}); });
} }
void
CallManager::setTurnServers()
{
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
// where username and password are percent-encoded
std::vector<std::string> uris;
for (const auto &uri : turnServer_.uris) {
if (auto c = uri.find(':'); c == std::string::npos) {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
}
else {
std::string scheme = std::string(uri, 0, c);
if (scheme != "turn" && scheme != "turns") {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
}
QString encodedUri = QString::fromStdString(scheme) + "://" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer_.username)) + ":" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer_.password)) + "@" +
QString::fromStdString(std::string(uri, ++c));
uris.push_back(encodedUri.toStdString());
}
}
if (!uris.empty())
session_.setTurnServers(uris);
}
void void
CallManager::playRingtone(const QString &ringtone, bool repeat) CallManager::playRingtone(const QString &ringtone, bool repeat)
{ {
@ -364,3 +365,34 @@ CallManager::stopRingtone()
{ {
player_.setPlaylist(nullptr); player_.setPlaylist(nullptr);
} }
namespace {
std::vector<std::string>
getTurnURIs(const mtx::responses::TurnServer &turnServer)
{
// gstreamer expects: turn(s)://username:password@host:port?transport=udp(tcp)
// where username and password are percent-encoded
std::vector<std::string> ret;
for (const auto &uri : turnServer.uris) {
if (auto c = uri.find(':'); c == std::string::npos) {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
}
else {
std::string scheme = std::string(uri, 0, c);
if (scheme != "turn" && scheme != "turns") {
nhlog::ui()->error("Invalid TURN server uri: {}", uri);
continue;
}
QString encodedUri = QString::fromStdString(scheme) + "://" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.username)) + ":" +
QUrl::toPercentEncoding(QString::fromStdString(turnServer.password)) + "@" +
QString::fromStdString(std::string(uri, ++c));
ret.push_back(encodedUri.toStdString());
}
}
return ret;
}
}

View file

@ -11,7 +11,10 @@
#include "mtx/events/collections.hpp" #include "mtx/events/collections.hpp"
#include "mtx/events/voip.hpp" #include "mtx/events/voip.hpp"
#include "mtx/responses/turn_server.hpp"
namespace mtx::responses {
struct TurnServer;
}
class UserSettings; class UserSettings;
class WebRTCSession; class WebRTCSession;
@ -51,7 +54,7 @@ private:
std::string callid_; std::string callid_;
const uint32_t timeoutms_ = 120000; const uint32_t timeoutms_ = 120000;
std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_; std::vector<mtx::events::msg::CallCandidates::Candidate> remoteICECandidates_;
mtx::responses::TurnServer turnServer_; std::vector<std::string> turnURIs_;
QTimer turnServerTimer_; QTimer turnServerTimer_;
QSharedPointer<UserSettings> settings_; QSharedPointer<UserSettings> settings_;
QMediaPlayer player_; QMediaPlayer player_;
@ -65,7 +68,6 @@ private:
void answerInvite(const mtx::events::msg::CallInvite&); void answerInvite(const mtx::events::msg::CallInvite&);
void generateCallID(); void generateCallID();
void endCall(); void endCall();
void setTurnServers();
void playRingtone(const QString &ringtone, bool repeat); void playRingtone(const QString &ringtone, bool repeat);
void stopRingtone(); void stopRingtone();
}; };

View file

@ -137,15 +137,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
activeCallBar_->hide(); activeCallBar_->hide();
connect( connect(
&callManager_, &CallManager::newCallParty, activeCallBar_, &ActiveCallBar::setCallParty); &callManager_, &CallManager::newCallParty, activeCallBar_, &ActiveCallBar::setCallParty);
connect(&WebRTCSession::instance(),
&WebRTCSession::stateChanged,
this,
[this](WebRTCSession::State state) {
if (state == WebRTCSession::State::DISCONNECTED)
activeCallBar_->hide();
else
activeCallBar_->show();
});
// Splitter // Splitter
splitter->addWidget(sideBar_); splitter->addWidget(sideBar_);

View file

@ -666,7 +666,8 @@ void
TextInputWidget::changeCallButtonState(WebRTCSession::State state) TextInputWidget::changeCallButtonState(WebRTCSession::State state)
{ {
QIcon icon; QIcon icon;
if (state == WebRTCSession::State::DISCONNECTED) { if (state == WebRTCSession::State::ICEFAILED ||
state == WebRTCSession::State::DISCONNECTED) {
callBtn_->setToolTip(tr("Place a call")); callBtn_->setToolTip(tr("Place a call"));
icon.addFile(":/icons/icons/ui/place-call.png"); icon.addFile(":/icons/icons/ui/place-call.png");
} else { } else {

View file

@ -14,9 +14,9 @@ extern "C" {
Q_DECLARE_METATYPE(WebRTCSession::State) Q_DECLARE_METATYPE(WebRTCSession::State)
namespace { namespace {
bool gisoffer; bool isoffering_;
std::string glocalsdp; std::string localsdp_;
std::vector<mtx::events::msg::CallCandidates::Candidate> gcandidates; std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data); gboolean newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data);
GstWebRTCSessionDescription* parseSDP(const std::string &sdp, GstWebRTCSDPType type); GstWebRTCSessionDescription* parseSDP(const std::string &sdp, GstWebRTCSDPType type);
@ -24,6 +24,7 @@ void generateOffer(GstElement *webrtc);
void setLocalDescription(GstPromise *promise, gpointer webrtc); void setLocalDescription(GstPromise *promise, gpointer webrtc);
void addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, guint mlineIndex, gchar *candidate, gpointer G_GNUC_UNUSED); void addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, guint mlineIndex, gchar *candidate, gpointer G_GNUC_UNUSED);
gboolean onICEGatheringCompletion(gpointer timerid); 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 createAnswer(GstPromise *promise, gpointer webrtc);
void addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe); void addDecodeBin(GstElement *webrtc G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe);
void linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe); void linkNewPad(GstElement *decodebin G_GNUC_UNUSED, GstPad *newpad, GstElement *pipe);
@ -92,9 +93,9 @@ WebRTCSession::init(std::string *errorMessage)
bool bool
WebRTCSession::createOffer() WebRTCSession::createOffer()
{ {
gisoffer = true; isoffering_ = true;
glocalsdp.clear(); localsdp_.clear();
gcandidates.clear(); localcandidates_.clear();
return startPipeline(111); // a dynamic opus payload type return startPipeline(111); // a dynamic opus payload type
} }
@ -105,9 +106,9 @@ WebRTCSession::acceptOffer(const std::string &sdp)
if (state_ != State::DISCONNECTED) if (state_ != State::DISCONNECTED)
return false; return false;
gisoffer = false; isoffering_ = false;
glocalsdp.clear(); localsdp_.clear();
gcandidates.clear(); localcandidates_.clear();
int opusPayloadType = getPayloadType(sdp, "opus"); int opusPayloadType = getPayloadType(sdp, "opus");
if (opusPayloadType == -1) if (opusPayloadType == -1)
@ -152,14 +153,20 @@ WebRTCSession::startPipeline(int opusPayloadType)
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())
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 (gisoffer) if (isoffering_)
g_signal_connect(webrtc_, "on-negotiation-needed", G_CALLBACK(generateOffer), nullptr); g_signal_connect(webrtc_, "on-negotiation-needed", G_CALLBACK(generateOffer), 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
g_signal_connect(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_);
@ -229,8 +236,6 @@ WebRTCSession::acceptICECandidates(const std::vector<mtx::events::msg::CallCandi
nhlog::ui()->debug("WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate); 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()); g_signal_emit_by_name(webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
} }
if (state_ == State::OFFERSENT)
emit stateChanged(State::CONNECTING);
} }
} }
@ -357,11 +362,11 @@ setLocalDescription(GstPromise *promise, gpointer webrtc)
g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr); g_signal_emit_by_name(webrtc, "set-local-description", gstsdp, nullptr);
gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp); gchar *sdp = gst_sdp_message_as_text(gstsdp->sdp);
glocalsdp = std::string(sdp); localsdp_ = std::string(sdp);
g_free(sdp); g_free(sdp);
gst_webrtc_session_description_free(gstsdp); gst_webrtc_session_description_free(gstsdp);
nhlog::ui()->debug("WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", glocalsdp); nhlog::ui()->debug("WebRTC: local description set ({}):\n{}", isAnswer ? "answer" : "offer", localsdp_);
} }
void void
@ -369,12 +374,12 @@ addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED, guint mlineIndex, gchar *
{ {
nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate); nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
if (WebRTCSession::instance().state() == WebRTCSession::State::CONNECTED) { if (WebRTCSession::instance().state() >= WebRTCSession::State::OFFERSENT) {
emit WebRTCSession::instance().newICECandidate({"audio", (uint16_t)mlineIndex, candidate}); emit WebRTCSession::instance().newICECandidate({"audio", (uint16_t)mlineIndex, candidate});
return; return;
} }
gcandidates.push_back({"audio", (uint16_t)mlineIndex, candidate}); 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 // GStreamer v1.16: webrtcbin's notify::ice-gathering-state triggers GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early
// fixed in v1.18 // fixed in v1.18
@ -390,16 +395,34 @@ gboolean
onICEGatheringCompletion(gpointer timerid) onICEGatheringCompletion(gpointer timerid)
{ {
*(guint*)(timerid) = 0; *(guint*)(timerid) = 0;
if (gisoffer) { if (isoffering_) {
emit WebRTCSession::instance().offerCreated(glocalsdp, gcandidates); emit WebRTCSession::instance().offerCreated(localsdp_, localcandidates_);
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT); emit WebRTCSession::instance().stateChanged(WebRTCSession::State::OFFERSENT);
} }
else { else {
emit WebRTCSession::instance().answerCreated(glocalsdp, gcandidates); emit WebRTCSession::instance().answerCreated(localsdp_, localcandidates_);
emit WebRTCSession::instance().stateChanged(WebRTCSession::State::CONNECTING); emit WebRTCSession::instance().stateChanged(WebRTCSession::State::ANSWERSENT);
}
return FALSE;
} }
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 void

View file

@ -15,10 +15,12 @@ class WebRTCSession : public QObject
public: public:
enum class State { enum class State {
ICEFAILED,
DISCONNECTED, DISCONNECTED,
INITIATING, INITIATING,
INITIATED, INITIATED,
OFFERSENT, OFFERSENT,
ANSWERSENT,
CONNECTING, CONNECTING,
CONNECTED CONNECTED
}; };
@ -30,13 +32,13 @@ public:
} }
bool init(std::string *errorMessage = nullptr); bool init(std::string *errorMessage = nullptr);
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>&);
State state() const {return state_;}
bool toggleMuteAudioSrc(bool &isMuted); bool toggleMuteAudioSrc(bool &isMuted);
void end(); void end();