diff --git a/include/rtc/description.hpp b/include/rtc/description.hpp index 3fec1bc..8f3b42f 100644 --- a/include/rtc/description.hpp +++ b/include/rtc/description.hpp @@ -34,8 +34,8 @@ namespace rtc { class Description { public: - enum class Type { Unspec = 0, Offer = 1, Answer = 2 }; - enum class Role { ActPass = 0, Passive = 1, Active = 2 }; + enum class Type { Unspec, Offer, Answer, Pranswer, Rollback }; + enum class Role { ActPass, Passive, Active }; enum class Direction { SendOnly, RecvOnly, SendRecv, Inactive, Unknown }; Description(const string &sdp, const string &typeString = ""); @@ -45,7 +45,6 @@ public: Type type() const; string typeString() const; Role role() const; - string roleString() const; string bundleMid() const; string iceUfrag() const; string icePwd() const; @@ -193,6 +192,9 @@ public: Application *application(); + static Type stringToType(const string &typeString); + static string typeToString(Type type); + private: std::optional defaultCandidate() const; std::shared_ptr createEntry(string mline, string mid, Direction dir); @@ -214,14 +216,12 @@ private: // Candidates std::vector mCandidates; bool mEnded = false; - - static Type stringToType(const string &typeString); - static string typeToString(Type type); - static string roleToString(Role role); }; } // namespace rtc std::ostream &operator<<(std::ostream &out, const rtc::Description &description); +std::ostream &operator<<(std::ostream &out, rtc::Description::Type type); +std::ostream &operator<<(std::ostream &out, rtc::Description::Role role); #endif diff --git a/include/rtc/peerconnection.hpp b/include/rtc/peerconnection.hpp index 5f0c521..1b5fbfc 100644 --- a/include/rtc/peerconnection.hpp +++ b/include/rtc/peerconnection.hpp @@ -67,6 +67,14 @@ public: Complete = RTC_GATHERING_COMPLETE }; + enum class SignalingState : int { + Stable = RTC_SIGNALING_STABLE, + HaveLocalOffer = RTC_SIGNALING_HAVE_LOCAL_OFFER, + HaveRemoteOffer = RTC_SIGNALING_HAVE_REMOTE_OFFER, + HaveLocalPranswer = RTC_SIGNALING_HAVE_LOCAL_PRANSWER, + HaveRemotePranswer = RTC_SIGNALING_HAVE_REMOTE_PRANSWER, + } rtcSignalingState; + PeerConnection(void); PeerConnection(const Configuration &config); ~PeerConnection(); @@ -76,6 +84,7 @@ public: const Configuration *config() const; State state() const; GatheringState gatheringState() const; + SignalingState signalingState() const; bool hasLocalDescription() const; bool hasRemoteDescription() const; bool hasMedia() const; @@ -83,8 +92,9 @@ public: std::optional remoteDescription() const; std::optional localAddress() const; std::optional remoteAddress() const; + bool getSelectedCandidatePair(Candidate *local, Candidate *remote); - void setLocalDescription(); + void setLocalDescription(Description::Type type = Description::Type::Unspec); void setRemoteDescription(Description description); void addRemoteCandidate(Candidate candidate); @@ -100,6 +110,7 @@ public: void onLocalCandidate(std::function callback); void onStateChange(std::function callback); void onGatheringStateChange(std::function callback); + void onSignalingStateChange(std::function callback); // Stats void clearStats(); @@ -111,9 +122,6 @@ public: std::shared_ptr addTrack(Description::Media description); void onTrack(std::function track)> callback); - // libnice only - bool getSelectedCandidatePair(Candidate *local, Candidate *remote); - private: std::shared_ptr initIceTransport(Description::Role role); std::shared_ptr initDtlsTransport(); @@ -143,6 +151,7 @@ private: void triggerTrack(std::shared_ptr track); bool changeState(State state); bool changeGatheringState(GatheringState state); + bool changeSignalingState(SignalingState state); void resetCallbacks(); @@ -168,6 +177,7 @@ private: std::atomic mState; std::atomic mGatheringState; + std::atomic mSignalingState; std::atomic mNegociationNeeded; synchronized_callback> mDataChannelCallback; @@ -175,12 +185,14 @@ private: synchronized_callback mLocalCandidateCallback; synchronized_callback mStateChangeCallback; synchronized_callback mGatheringStateChangeCallback; + synchronized_callback mSignalingStateChangeCallback; synchronized_callback> mTrackCallback; }; } // namespace rtc -std::ostream &operator<<(std::ostream &out, const rtc::PeerConnection::State &state); -std::ostream &operator<<(std::ostream &out, const rtc::PeerConnection::GatheringState &state); +std::ostream &operator<<(std::ostream &out, rtc::PeerConnection::State state); +std::ostream &operator<<(std::ostream &out, rtc::PeerConnection::GatheringState state); +std::ostream &operator<<(std::ostream &out, rtc::PeerConnection::SignalingState state); #endif diff --git a/include/rtc/rtc.h b/include/rtc/rtc.h index f1e43f6..5c39745 100644 --- a/include/rtc/rtc.h +++ b/include/rtc/rtc.h @@ -59,6 +59,14 @@ typedef enum { RTC_GATHERING_COMPLETE = 2 } rtcGatheringState; +typedef enum { + RTC_SIGNALING_STABLE = 0, + RTC_SIGNALING_HAVE_LOCAL_OFFER = 1, + RTC_SIGNALING_HAVE_REMOTE_OFFER = 2, + RTC_SIGNALING_HAVE_LOCAL_PRANSWER = 3, + RTC_SIGNALING_HAVE_REMOTE_PRANSWER = 4, +} rtcSignalingState; + typedef enum { // Don't change, it must match plog severity RTC_LOG_NONE = 0, RTC_LOG_FATAL = 1, diff --git a/src/description.cpp b/src/description.cpp index 2d50654..e40112a 100644 --- a/src/description.cpp +++ b/src/description.cpp @@ -26,6 +26,7 @@ #include #include #include +#include using std::shared_ptr; using std::size_t; @@ -767,12 +768,14 @@ Description::Video::Video(string mid, Direction dir) : Media("video 9 UDP/TLS/RTP/SAVPF", std::move(mid), dir) {} Description::Type Description::stringToType(const string &typeString) { - if (typeString == "offer") - return Type::Offer; - else if (typeString == "answer") - return Type::Answer; - else - return Type::Unspec; + using TypeMap_t = std::unordered_map; + static const TypeMap_t TypeMap = {{"unspec", Type::Unspec}, + {"offer", Type::Offer}, + {"answer", Type::Pranswer}, + {"pranswer", Type::Pranswer}, + {"rollback", Type::Rollback}}; + auto it = TypeMap.find(typeString); + return it != TypeMap.end() ? it->second : Type::Unspec; } string Description::typeToString(Type type) { @@ -781,19 +784,12 @@ string Description::typeToString(Type type) { return "offer"; case Type::Answer: return "answer"; + case Type::Pranswer: + return "pranswer"; + case Type::Rollback: + return "rollback"; default: - return ""; - } -} - -string Description::roleToString(Role role) { - switch (role) { - case Role::Active: - return "active"; - case Role::Passive: - return "passive"; - default: - return "actpass"; + return "unknown"; } } @@ -802,3 +798,25 @@ string Description::roleToString(Role role) { std::ostream &operator<<(std::ostream &out, const rtc::Description &description) { return out << std::string(description); } + +std::ostream &operator<<(std::ostream &out, rtc::Description::Type type) { + return out << rtc::Description::typeToString(type); +} + +std::ostream &operator<<(std::ostream &out, rtc::Description::Role role) { + using Role = rtc::Description::Role; + const char *str; + switch (role) { + case Role::Active: + str = "active"; + break; + case Role::Passive: + str = "passive"; + break; + default: + str = "actpass"; + break; + } + return out << str; +} + diff --git a/src/peerconnection.cpp b/src/peerconnection.cpp index a59251a..4646565 100644 --- a/src/peerconnection.cpp +++ b/src/peerconnection.cpp @@ -44,7 +44,8 @@ PeerConnection::PeerConnection() : PeerConnection(Configuration()) {} PeerConnection::PeerConnection(const Configuration &config) : mConfig(config), mCertificate(make_certificate()), mProcessor(std::make_unique()), - mState(State::New), mGatheringState(GatheringState::New), mNegociationNeeded(false) { + mState(State::New), mGatheringState(GatheringState::New), + mSignalingState(SignalingState::Stable), mNegociationNeeded(false) { PLOG_VERBOSE << "Creating PeerConnection"; if (config.portRangeEnd && config.portRangeBegin > config.portRangeEnd) @@ -74,6 +75,8 @@ PeerConnection::State PeerConnection::state() const { return mState; } PeerConnection::GatheringState PeerConnection::gatheringState() const { return mGatheringState; } +PeerConnection::SignalingState PeerConnection::signalingState() const { return mSignalingState; } + std::optional PeerConnection::localDescription() const { std::lock_guard lock(mLocalDescriptionMutex); return mLocalDescription; @@ -99,7 +102,7 @@ bool PeerConnection::hasMedia() const { return local && local->hasAudioOrVideo(); } -void PeerConnection::setLocalDescription() { +void PeerConnection::setLocalDescription(Description::Type type) { PLOG_VERBOSE << "Setting local description"; if (!mNegociationNeeded.exchange(false)) { @@ -107,13 +110,30 @@ void PeerConnection::setLocalDescription() { return; } - // RFC 5763: The endpoint that is the offerer MUST use the setup attribute value of - // setup:actpass. - // See https://tools.ietf.org/html/rfc5763#section-5 - auto iceTransport = initIceTransport(Description::Role::ActPass); - Description localDescription = iceTransport->getLocalDescription(Description::Type::Offer); + // Guess the description type if unspecified + if (type == Description::Type::Unspec) { + if (mSignalingState == SignalingState::HaveRemoteOffer) + type = Description::Type::Answer; + else + type = Description::Type::Offer; + } + + auto iceTransport = std::atomic_load(&mIceTransport); + if (!iceTransport) { + if (type != Description::Type::Offer) { + // RFC 5763: The endpoint that is the offerer MUST use the setup attribute value of + // setup:actpass. + // See https://tools.ietf.org/html/rfc5763#section-5 + if (!iceTransport) + iceTransport = initIceTransport(Description::Role::ActPass); + } + } + + Description localDescription = iceTransport->getLocalDescription(type); processLocalDescription(localDescription); - iceTransport->gatherLocalCandidates(); + + if (mGatheringState == GatheringState::New) + iceTransport->gatherLocalCandidates(); } void PeerConnection::setRemoteDescription(Description description) { @@ -143,21 +163,40 @@ void PeerConnection::setRemoteDescription(Description description) { throw std::logic_error("Got the local description as remote description"); } - description.hintType(hasLocalDescription() ? Description::Type::Answer - : Description::Type::Offer); - - // If there is no remote description, this is the first negociation - // Check it is what we expect - if (!hasRemoteDescription()) { - if (description.type() == Description::Type::Offer) { - if (hasLocalDescription()) { - PLOG_ERROR << "Got a remote offer description while an answer was expected"; - throw std::logic_error("Got an unexpected remote offer description"); - } - } else { // Answer - PLOG_ERROR << "Got a remote answer description while an offer was expected"; - throw std::logic_error("Got an unexpected remote answer description"); + // Get the new signaling state + SignalingState signalingState = mSignalingState.load(); + SignalingState newSignalingState; + switch (signalingState) { + case SignalingState::Stable: + description.hintType(Description::Type::Offer); + if (description.type() != Description::Type::Offer) { + LOG_ERROR << "Unexpected remote " << description.type() + << " description in signaling state " << signalingState << ", expected offer"; + std::ostringstream oss; + oss << "Unexpected remote " << description.type() << " description"; + throw std::logic_error(oss.str()); } + newSignalingState = SignalingState::HaveRemoteOffer; + break; + + case SignalingState::HaveLocalOffer: + case SignalingState::HaveRemotePranswer: + description.hintType(Description::Type::Answer); + if (description.type() != Description::Type::Answer || + description.type() != Description::Type::Pranswer) { + LOG_ERROR << "Unexpected remote " << description.type() + << " description in signaling state " << signalingState + << ", expected answer"; + std::ostringstream oss; + oss << "Unexpected remote " << description.type() << " description"; + throw std::logic_error(oss.str()); + } + newSignalingState = SignalingState::Stable; + break; + + default: + LOG_ERROR << "Unexpected remote description in signaling state " << signalingState; + throw std::logic_error("Unexpected remote description"); } // Candidates will be added at the end, extract them for now @@ -168,12 +207,19 @@ void PeerConnection::setRemoteDescription(Description description) { iceTransport = initIceTransport(Description::Role::ActPass); iceTransport->setRemoteDescription(description); + if (description.hasApplication()) { + if (auto current = remoteDescription(); current && !current->hasApplication()) + if (auto dtlsTransport = std::atomic_load(&mDtlsTransport); + dtlsTransport && dtlsTransport->state() == Transport::State::Connected) + initSctpTransport(); + } + { // Set as remote description std::lock_guard lock(mRemoteDescriptionMutex); - + std::vector existingCandidates; - if(mRemoteDescription) + if (mRemoteDescription) existingCandidates = mRemoteDescription->extractCandidates(); mRemoteDescription.emplace(std::move(description)); @@ -181,11 +227,12 @@ void PeerConnection::setRemoteDescription(Description description) { mRemoteDescription->addCandidate(candidate); } + changeSignalingState(newSignalingState); + if (description.type() == Description::Type::Offer) { // This is an offer, we need to answer - Description localDescription = iceTransport->getLocalDescription(Description::Type::Answer); - processLocalDescription(localDescription); - iceTransport->gatherLocalCandidates(); + mNegociationNeeded = true; + setLocalDescription(Description::Type::Answer); } else { // This is an answer auto sctpTransport = std::atomic_load(&mSctpTransport); @@ -204,6 +251,8 @@ void PeerConnection::setRemoteDescription(Description description) { } std::swap(mDataChannels, newDataChannels); } + + changeSignalingState(SignalingState::Stable); } for (const auto &candidate : remoteCandidates) @@ -261,7 +310,7 @@ shared_ptr PeerConnection::addDataChannel(string label, string prot if (transport->state() == SctpTransport::State::Connected) channel->open(transport); - // Renegociation is needed if the current local description does not have application + // Renegociation is needed iff the current local description does not have application std::lock_guard lock(mLocalDescriptionMutex); if (!mLocalDescription || !mLocalDescription->hasApplication()) mNegociationNeeded = true; @@ -297,6 +346,10 @@ void PeerConnection::onGatheringStateChange(std::function callback) { + mSignalingStateChangeCallback = callback; +} + std::shared_ptr PeerConnection::addTrack(Description::Media description) { if (hasLocalDescription()) throw std::logic_error("Tracks must be created before local description"); @@ -401,7 +454,7 @@ shared_ptr PeerConnection::initDtlsTransport() { switch (state) { case DtlsTransport::State::Connected: - if (auto local = localDescription(); local && local->hasApplication()) + if (auto remote = remoteDescription(); remote && remote->hasApplication()) initSctpTransport(); else changeState(State::Connected); @@ -833,9 +886,9 @@ void PeerConnection::processLocalDescription(Description description) { { // Set as local description std::lock_guard lock(mLocalDescriptionMutex); - + std::vector existingCandidates; - if(mLocalDescription) + if (mLocalDescription) existingCandidates = mLocalDescription->extractCandidates(); mLocalDescription.emplace(std::move(description)); @@ -910,6 +963,16 @@ bool PeerConnection::changeGatheringState(GatheringState state) { return true; } +bool PeerConnection::changeSignalingState(SignalingState state) { + if (mSignalingState.exchange(state) != state) { + std::ostringstream s; + s << state; + PLOG_INFO << "Changed signaling state to " << s.str(); + mProcessor->enqueue([this, state] { mSignalingStateChangeCallback(state); }); + } + return true; +} + void PeerConnection::resetCallbacks() { // Unregister all callbacks mDataChannelCallback = nullptr; @@ -957,7 +1020,7 @@ std::optional PeerConnection::rtt() { std::ostream &operator<<(std::ostream &out, const rtc::PeerConnection::State &state) { using State = rtc::PeerConnection::State; - std::string str; + const char *str; switch (state) { case State::New: str = "new"; @@ -986,13 +1049,13 @@ std::ostream &operator<<(std::ostream &out, const rtc::PeerConnection::State &st std::ostream &operator<<(std::ostream &out, const rtc::PeerConnection::GatheringState &state) { using GatheringState = rtc::PeerConnection::GatheringState; - std::string str; + const char *str; switch (state) { case GatheringState::New: str = "new"; break; case GatheringState::InProgress: - str = "in_progress"; + str = "in-progress"; break; case GatheringState::Complete: str = "complete"; @@ -1003,3 +1066,29 @@ std::ostream &operator<<(std::ostream &out, const rtc::PeerConnection::Gathering } return out << str; } + +std::ostream &operator<<(std::ostream &out, const rtc::PeerConnection::SignalingState &state) { + using SignalingState = rtc::PeerConnection::SignalingState; + const char *str; + switch (state) { + case SignalingState::Stable: + str = "stable"; + break; + case SignalingState::HaveLocalOffer: + str = "have-local-offer"; + break; + case SignalingState::HaveRemoteOffer: + str = "have-remote-offer"; + break; + case SignalingState::HaveLocalPranswer: + str = "have-local-pranswer"; + break; + case SignalingState::HaveRemotePranswer: + str = "have-remote-pranswer"; + break; + default: + str = "unknown"; + break; + } + return out << str; +}