From 00ffc07077b0d47256b61481b2bc0ce3e8fb8904 Mon Sep 17 00:00:00 2001 From: Paul-Louis Ageneau Date: Sat, 11 Jan 2025 11:21:21 +0100 Subject: [PATCH 1/4] Introduce createOffer() and createAnswer() methods --- include/rtc/description.hpp | 3 +- include/rtc/peerconnection.hpp | 6 +- src/description.cpp | 25 +++--- src/impl/peerconnection.cpp | 141 ++++++++++++++++----------------- src/impl/peerconnection.hpp | 3 +- src/peerconnection.cpp | 36 +++++++-- 6 files changed, 122 insertions(+), 92 deletions(-) diff --git a/include/rtc/description.hpp b/include/rtc/description.hpp index c1ce87b1d..bd6d87099 100644 --- a/include/rtc/description.hpp +++ b/include/rtc/description.hpp @@ -66,9 +66,10 @@ class RTC_CPP_EXPORT Description { bool ended() const; void hintType(Type type); - void setFingerprint(CertificateFingerprint f); void addIceOption(string option); void removeIceOption(const string &option); + void setIceAttribute(string ufrag, string pwd); + void setFingerprint(CertificateFingerprint f); std::vector attributes() const; void addAttribute(string attr); diff --git a/include/rtc/peerconnection.hpp b/include/rtc/peerconnection.hpp index d2d73537d..f7a76a16d 100644 --- a/include/rtc/peerconnection.hpp +++ b/include/rtc/peerconnection.hpp @@ -97,9 +97,13 @@ class RTC_CPP_EXPORT PeerConnection final : CheshireCat { bool getSelectedCandidatePair(Candidate *local, Candidate *remote); void setLocalDescription(Description::Type type = Description::Type::Unspec, LocalDescriptionInit init = {}); + void gatherLocalCandidates(std::vector additionalIceServers = {}); void setRemoteDescription(Description description); void addRemoteCandidate(Candidate candidate); - void gatherLocalCandidates(std::vector additionalIceServers = {}); + + // For specific use cases only + Description createOffer(); + Description createAnswer(); void setMediaHandler(shared_ptr handler); shared_ptr getMediaHandler(); diff --git a/src/description.cpp b/src/description.cpp index cf89510e2..6a37056b3 100644 --- a/src/description.cpp +++ b/src/description.cpp @@ -216,6 +216,21 @@ void Description::hintType(Type type) { mType = type; } +void Description::addIceOption(string option) { + if (std::find(mIceOptions.begin(), mIceOptions.end(), option) == mIceOptions.end()) + mIceOptions.emplace_back(std::move(option)); +} + +void Description::removeIceOption(const string &option) { + mIceOptions.erase(std::remove(mIceOptions.begin(), mIceOptions.end(), option), + mIceOptions.end()); +} + +void Description::setIceAttribute(string ufrag, string pwd) { + mIceUfrag = std::move(ufrag); + mIcePwd = std::move(pwd); +} + void Description::setFingerprint(CertificateFingerprint f) { if (!f.isValid()) throw std::invalid_argument("Invalid " + @@ -227,16 +242,6 @@ void Description::setFingerprint(CertificateFingerprint f) { mFingerprint = std::move(f); } -void Description::addIceOption(string option) { - if (std::find(mIceOptions.begin(), mIceOptions.end(), option) == mIceOptions.end()) - mIceOptions.emplace_back(std::move(option)); -} - -void Description::removeIceOption(const string &option) { - mIceOptions.erase(std::remove(mIceOptions.begin(), mIceOptions.end(), option), - mIceOptions.end()); -} - std::vector Description::Entry::attributes() const { return mAttributes; } void Description::Entry::addAttribute(string attr) { diff --git a/src/impl/peerconnection.cpp b/src/impl/peerconnection.cpp index ec10ca725..c04f3e50c 100644 --- a/src/impl/peerconnection.cpp +++ b/src/impl/peerconnection.cpp @@ -925,7 +925,7 @@ void PeerConnection::validateRemoteDescription(const Description &description) { PLOG_VERBOSE << "Remote description looks valid"; } -void PeerConnection::processLocalDescription(Description description) { +void PeerConnection::populateLocalDescription(Description &description) const { const uint16_t localSctpPort = DEFAULT_SCTP_PORT; const size_t localMaxMessageSize = config.maxMessageSize.value_or(DEFAULT_LOCAL_MAX_MESSAGE_SIZE); @@ -935,7 +935,7 @@ void PeerConnection::processLocalDescription(Description description) { if (auto remote = remoteDescription()) { // Reciprocate remote description - for (int i = 0; i < remote->mediaCount(); ++i) + for (int i = 0; i < remote->mediaCount(); ++i) { std::visit( // reciprocate each media rtc::overloaded{ [&](Description::Application *remoteApp) { @@ -950,78 +950,46 @@ void PeerConnection::processLocalDescription(Description description) { << app.mid() << "\""; description.addMedia(std::move(app)); - return; - } - auto reciprocated = remoteApp->reciprocate(); - reciprocated.hintSctpPort(localSctpPort); - reciprocated.setMaxMessageSize(localMaxMessageSize); + } else { + auto reciprocated = remoteApp->reciprocate(); + reciprocated.hintSctpPort(localSctpPort); + reciprocated.setMaxMessageSize(localMaxMessageSize); - PLOG_DEBUG << "Reciprocating application in local description, mid=\"" - << reciprocated.mid() << "\""; + PLOG_DEBUG << "Reciprocating application in local description, mid=\"" + << reciprocated.mid() << "\""; - description.addMedia(std::move(reciprocated)); + description.addMedia(std::move(reciprocated)); + } }, [&](Description::Media *remoteMedia) { - std::unique_lock lock(mTracksMutex); // we may emplace a track - if (auto it = mTracks.find(remoteMedia->mid()); it != mTracks.end()) { - // Prefer local description - if (auto track = it->second.lock()) { - auto media = track->description(); - - PLOG_DEBUG << "Adding media to local description, mid=\"" - << media.mid() << "\", removed=" << std::boolalpha - << media.isRemoved(); - - description.addMedia(std::move(media)); - - } else { - auto reciprocated = remoteMedia->reciprocate(); - reciprocated.markRemoved(); + std::shared_lock lock(mTracksMutex); + auto it = mTracks.find(remoteMedia->mid()); + auto track = it != mTracks.end() ? it->second.lock() : nullptr; + if(track) { + // Prefer local description + auto media = track->description(); - PLOG_DEBUG << "Adding media to local description, mid=\"" - << reciprocated.mid() - << "\", removed=true (track is destroyed)"; - - description.addMedia(std::move(reciprocated)); - } - return; - } - - auto reciprocated = remoteMedia->reciprocate(); -#if !RTC_ENABLE_MEDIA - if (!reciprocated.isRemoved()) { - // No media support, mark as removed - PLOG_WARNING << "Rejecting track (not compiled with media support)"; - reciprocated.markRemoved(); - } -#endif + PLOG_DEBUG << "Adding media to local description, mid=\"" + << media.mid() << "\", removed=" << std::boolalpha + << media.isRemoved(); - PLOG_DEBUG << "Reciprocating media in local description, mid=\"" - << reciprocated.mid() << "\", removed=" << std::boolalpha - << reciprocated.isRemoved(); + description.addMedia(std::move(media)); - // Create incoming track - auto track = - std::make_shared(weak_from_this(), std::move(reciprocated)); - mTracks.emplace(std::make_pair(track->mid(), track)); - mTrackLines.emplace_back(track); - triggerTrack(track); // The user may modify the track description + } else { + auto reciprocated = remoteMedia->reciprocate(); + reciprocated.markRemoved(); - auto handler = getMediaHandler(); - if (handler) - handler->media(track->description()); + PLOG_DEBUG << "Adding media to local description, mid=\"" + << reciprocated.mid() + << "\", removed=true (track is destroyed)"; - if (track->description().isRemoved()) - track->close(); - - description.addMedia(track->description()); + description.addMedia(std::move(reciprocated)); + } }, }, remote->media(i)); - - // We need to update the SSRC cache for newly-created incoming tracks - updateTrackSsrcCache(*remote); + } } if (description.type() == Description::Type::Offer) { @@ -1061,22 +1029,18 @@ void PeerConnection::processLocalDescription(Description description) { description.addMedia(std::move(app)); } } - - // There might be no media at this point, for instance if the user deleted tracks - if (description.mediaCount() == 0) - throw std::runtime_error("No DataChannel or Track to negotiate"); } // Set local fingerprint (wait for certificate if necessary) description.setFingerprint(mCertificate.get()->fingerprint()); +} +void PeerConnection::processLocalDescription(Description description) { PLOG_VERBOSE << "Issuing local description: " << description; if (description.mediaCount() == 0) throw std::logic_error("Local description has no media line"); - updateTrackSsrcCache(description); - { // Set as local description std::lock_guard lock(mLocalDescriptionMutex); @@ -1093,11 +1057,6 @@ void PeerConnection::processLocalDescription(Description description) { mProcessor.enqueue(&PeerConnection::trigger, shared_from_this(), &localDescriptionCallback, std::move(description)); - - // Reciprocated tracks might need to be open - if (auto dtlsTransport = std::atomic_load(&mDtlsTransport); - dtlsTransport && dtlsTransport->state() == Transport::State::Connected) - mProcessor.enqueue(&PeerConnection::openTracks, shared_from_this()); } void PeerConnection::processLocalCandidate(Candidate candidate) { @@ -1121,6 +1080,40 @@ void PeerConnection::processLocalCandidate(Candidate candidate) { } void PeerConnection::processRemoteDescription(Description description) { + // Create tracks from remote description + for (int i = 0; i < description.mediaCount(); ++i) { + if (std::holds_alternative(description.media(i))) { + auto remoteMedia = std::get(description.media(i)); + std::unique_lock lock(mTracksMutex); // we may emplace a track + if (auto it = mTracks.find(remoteMedia->mid()); it != mTracks.end()) + continue; + + PLOG_DEBUG << "New remote track, mid=\"" << remoteMedia->mid() << "\""; + + auto reciprocated = remoteMedia->reciprocate(); +#if !RTC_ENABLE_MEDIA + if (!reciprocated.isRemoved()) { + // No media support, mark as removed + PLOG_WARNING << "Rejecting track (not compiled with media support)"; + reciprocated.markRemoved(); + } +#endif + + // Create incoming track + auto track = std::make_shared(weak_from_this(), std::move(reciprocated)); + mTracks.emplace(std::make_pair(track->mid(), track)); + mTrackLines.emplace_back(track); + triggerTrack(track); // The user may modify the track description + + auto handler = getMediaHandler(); + if (handler) + handler->media(track->description()); + + if (track->description().isRemoved()) + track->close(); + } + } + // Update the SSRC cache for existing tracks updateTrackSsrcCache(description); @@ -1146,9 +1139,9 @@ void PeerConnection::processRemoteDescription(Description description) { mProcessor.enqueue(&PeerConnection::remoteCloseDataChannels, shared_from_this()); } + // Reciprocated tracks might need to be open if (dtlsTransport && dtlsTransport->state() == Transport::State::Connected) mProcessor.enqueue(&PeerConnection::openTracks, shared_from_this()); - } void PeerConnection::processRemoteCandidate(Candidate candidate) { @@ -1238,7 +1231,7 @@ void PeerConnection::setMediaHandler(shared_ptr handler) { mMediaHandler = handler; } -shared_ptr PeerConnection::getMediaHandler() { +shared_ptr PeerConnection::getMediaHandler() const { std::shared_lock lock(mMediaHandlerMutex); return mMediaHandler; } diff --git a/src/impl/peerconnection.hpp b/src/impl/peerconnection.hpp index 8f98ff6ed..22826cc51 100644 --- a/src/impl/peerconnection.hpp +++ b/src/impl/peerconnection.hpp @@ -75,6 +75,7 @@ struct PeerConnection : std::enable_shared_from_this { void closeTracks(); void validateRemoteDescription(const Description &description); + void populateLocalDescription(Description &description) const; void processLocalDescription(Description description); void processLocalCandidate(Candidate candidate); void processRemoteDescription(Description description); @@ -84,7 +85,7 @@ struct PeerConnection : std::enable_shared_from_this { bool negotiationNeeded() const; void setMediaHandler(shared_ptr handler); - shared_ptr getMediaHandler(); + shared_ptr getMediaHandler() const; void triggerDataChannel(weak_ptr weakDataChannel); void triggerTrack(weak_ptr weakTrack); diff --git a/src/peerconnection.cpp b/src/peerconnection.cpp index 5e6cd2739..135f4585b 100644 --- a/src/peerconnection.cpp +++ b/src/peerconnection.cpp @@ -85,6 +85,7 @@ void PeerConnection::setLocalDescription(Description::Type type, LocalDescriptio PLOG_VERBOSE << "Setting local description, type=" << Description::typeToString(type); SignalingState signalingState = impl()->signalingState.load(); + if (type == Description::Type::Rollback) { if (signalingState == SignalingState::HaveLocalOffer || signalingState == SignalingState::HaveLocalPranswer) { @@ -139,11 +140,17 @@ void PeerConnection::setLocalDescription(Description::Type type, LocalDescriptio return; // closed if (init.iceUfrag && init.icePwd) { - PLOG_DEBUG << "Using custom ICE attributes, ufrag=\"" << init.iceUfrag.value() << "\", pwd=\"" << init.icePwd.value() << "\""; - iceTransport->setIceAttributes(init.iceUfrag.value(), init.icePwd.value()); + PLOG_DEBUG << "Setting custom ICE attributes, ufrag=\"" << *init.iceUfrag << "\", pwd=\"" << *init.icePwd << "\""; + iceTransport->setIceAttributes(*init.iceUfrag, *init.icePwd); } Description local = iceTransport->getLocalDescription(type); + impl()->populateLocalDescription(local); + + // There might be no media at this point, for instance if the user deleted tracks + if (local.mediaCount() == 0) + throw std::runtime_error("No DataChannel or Track to negotiate"); + impl()->processLocalDescription(std::move(local)); impl()->changeSignalingState(newSignalingState); @@ -162,9 +169,8 @@ void PeerConnection::setLocalDescription(Description::Type type, LocalDescriptio void PeerConnection::gatherLocalCandidates(std::vector additionalIceServers) { auto iceTransport = impl()->getIceTransport(); - if (!iceTransport) { - throw std::logic_error("No IceTransport. Local Description has not been set"); - } + if (!iceTransport || !localDescription()) + throw std::logic_error("Local description has not been set before gathering"); if (impl()->gatheringState == GatheringState::New) { iceTransport->gatherLocalCandidates(impl()->localBundleMid(), additionalIceServers); @@ -282,6 +288,26 @@ void PeerConnection::addRemoteCandidate(Candidate candidate) { impl()->processRemoteCandidate(std::move(candidate)); } +Description PeerConnection::createOffer() { + auto iceTransport = impl()->initIceTransport(); + if (!iceTransport) + throw std::runtime_error("Peer connection is closed"); + + Description desc = iceTransport->getLocalDescription(rtc::Description::Type::Offer); + impl()->populateLocalDescription(desc); + return desc; +} + +Description PeerConnection::createAnswer() { + auto iceTransport = impl()->initIceTransport(); + if (!iceTransport) + throw std::runtime_error("Peer connection is closed"); + + Description desc = iceTransport->getLocalDescription(rtc::Description::Type::Answer); + impl()->populateLocalDescription(desc); + return desc; +} + void PeerConnection::setMediaHandler(shared_ptr handler) { impl()->setMediaHandler(std::move(handler)); }; From bab9544388df8c28787adfd3fdfdf3fdcf8ce16f Mon Sep 17 00:00:00 2001 From: Paul-Louis Ageneau Date: Sat, 18 Jan 2025 23:42:02 +0100 Subject: [PATCH 2/4] Add rtcCreateOffer() and rtcCreateAnswer() --- include/rtc/rtc.h | 6 +++++- src/capi.cpp | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/include/rtc/rtc.h b/include/rtc/rtc.h index 2fc70c90c..887a4141f 100644 --- a/include/rtc/rtc.h +++ b/include/rtc/rtc.h @@ -211,7 +211,7 @@ RTC_C_EXPORT int rtcSetIceStateChangeCallback(int pc, rtcIceStateChangeCallbackF RTC_C_EXPORT int rtcSetGatheringStateChangeCallback(int pc, rtcGatheringStateCallbackFunc cb); RTC_C_EXPORT int rtcSetSignalingStateChangeCallback(int pc, rtcSignalingStateCallbackFunc cb); -RTC_C_EXPORT int rtcSetLocalDescription(int pc, const char *type); +RTC_C_EXPORT int rtcSetLocalDescription(int pc, const char *type); // type may be NULL RTC_C_EXPORT int rtcSetRemoteDescription(int pc, const char *sdp, const char *type); RTC_C_EXPORT int rtcAddRemoteCandidate(int pc, const char *cand, const char *mid); @@ -221,6 +221,10 @@ RTC_C_EXPORT int rtcGetRemoteDescription(int pc, char *buffer, int size); RTC_C_EXPORT int rtcGetLocalDescriptionType(int pc, char *buffer, int size); RTC_C_EXPORT int rtcGetRemoteDescriptionType(int pc, char *buffer, int size); +// For specific use cases only +RTC_C_EXPORT int rtcCreateOffer(int pc, char *buffer, int size); +RTC_C_EXPORT int rtcCreateAnswer(int pc, char *buffer, int size); + RTC_C_EXPORT int rtcGetLocalAddress(int pc, char *buffer, int size); RTC_C_EXPORT int rtcGetRemoteAddress(int pc, char *buffer, int size); diff --git a/src/capi.cpp b/src/capi.cpp index ead5cedf3..710d9ddcc 100644 --- a/src/capi.cpp +++ b/src/capi.cpp @@ -631,6 +631,24 @@ int rtcGetRemoteDescriptionType(int pc, char *buffer, int size) { }); } +int rtcCreateOffer(int pc, char *buffer, int size) { + return wrap([&] { + auto peerConnection = getPeerConnection(pc); + + auto desc = peerConnection->createOffer(); + return copyAndReturn(string(desc), buffer, size); + }); +} + +int rtcCreateAnswer(int pc, char *buffer, int size) { + return wrap([&] { + auto peerConnection = getPeerConnection(pc); + + auto desc = peerConnection->createAnswer(); + return copyAndReturn(string(desc), buffer, size); + }); +} + int rtcGetLocalAddress(int pc, char *buffer, int size) { return wrap([&] { auto peerConnection = getPeerConnection(pc); From d2be4870e67f610c918b3182ead2a4fbb241e363 Mon Sep 17 00:00:00 2001 From: Paul-Louis Ageneau Date: Sat, 18 Jan 2025 23:42:08 +0100 Subject: [PATCH 3/4] Update doc --- DOC.md | 25 ++++++++++++++++++++++--- pages/content/pages/reference.md | 25 ++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/DOC.md b/DOC.md index ca6d27cca..92a45b3ff 100644 --- a/DOC.md +++ b/DOC.md @@ -207,7 +207,9 @@ Initiates the handshake process. Following this call, the local description call Arguments: - `pc`: the Peer Connection identifier -- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection. +- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (recommended). + +Warning: This function expects the optional type for the local description and not an SDP description. It is not possible to set an existing SDP description. #### rtcSetRemoteDescription @@ -220,7 +222,8 @@ Sets the remote description received from the remote peer by the user's method o Arguments: - `pc`: the Peer Connection identifier -- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection. +- `sdp`: the remote description in SDP format +- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (not recommended). If the remote description is an offer and `disableAutoNegotiation` was not set in `rtcConfiguration`, the library will automatically answer by calling `rtcSetLocalDescription` internally. Otherwise, the user must call it to answer the remote description. @@ -296,7 +299,7 @@ Return value: the length of the string copied in buffer (including the terminati If `buffer` is `NULL`, the description is not copied but the size is still returned. -#### rtcGetRemoteDescription +#### rtcGetRemoteDescriptionType ``` int rtcGetRemoteDescriptionType(int pc, char *buffer, int size) @@ -314,6 +317,22 @@ Return value: the length of the string copied in buffer (including the terminati If `buffer` is `NULL`, the description is not copied but the size is still returned. +#### rtcCreateOffer/rtcCreateAnswer + +``` +int rtcCreateOffer(int pc, char *buffer, int size) +int rtcCreateAnswer(int pc, char *buffer, int size) +``` + +Create a local offer or answer description in SDP format. These functions are intended only for specific use cases where the application needs to generate a description without setting it. It is useless to call them before `rtcSetLocalDescription` as it doesn't expect the user to supply a description. + +- `pc`: the Peer Connection identifier +- `buffer`: a user-supplied buffer to store the description +- `size`: the size of `buffer` + +Return value: the length of the string copied in buffer (including the terminating null character) or a negative error code + +If `buffer` is `NULL`, the description is not copied but the size is still returned. #### rtcGetLocalAddress diff --git a/pages/content/pages/reference.md b/pages/content/pages/reference.md index fd4049db2..714e024ca 100644 --- a/pages/content/pages/reference.md +++ b/pages/content/pages/reference.md @@ -210,7 +210,9 @@ Initiates the handshake process. Following this call, the local description call Arguments: - `pc`: the Peer Connection identifier -- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection. +- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (recommended). + +Warning: This function expects the optional type for the local description and not an SDP description. It is not possible to set an existing SDP description. #### rtcSetRemoteDescription @@ -223,7 +225,8 @@ Sets the remote description received from the remote peer by the user's method o Arguments: - `pc`: the Peer Connection identifier -- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection. +- `sdp`: the remote description in SDP format +- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (not recommended). If the remote description is an offer and `disableAutoNegotiation` was not set in `rtcConfiguration`, the library will automatically answer by calling `rtcSetLocalDescription` internally. Otherwise, the user must call it to answer the remote description. @@ -299,7 +302,7 @@ Return value: the length of the string copied in buffer (including the terminati If `buffer` is `NULL`, the description is not copied but the size is still returned. -#### rtcGetRemoteDescription +#### rtcGetRemoteDescriptionType ``` int rtcGetRemoteDescriptionType(int pc, char *buffer, int size) @@ -317,6 +320,22 @@ Return value: the length of the string copied in buffer (including the terminati If `buffer` is `NULL`, the description is not copied but the size is still returned. +#### rtcCreateOffer/rtcCreateAnswer + +``` +int rtcCreateOffer(int pc, char *buffer, int size) +int rtcCreateAnswer(int pc, char *buffer, int size) +``` + +Create a local offer or answer description in SDP format. These functions are intended only for specific use cases where the application needs to generate a description without setting it. It is useless to call them before `rtcSetLocalDescription` as it doesn't expect the user to supply a description. + +- `pc`: the Peer Connection identifier +- `buffer`: a user-supplied buffer to store the description +- `size`: the size of `buffer` + +Return value: the length of the string copied in buffer (including the terminating null character) or a negative error code + +If `buffer` is `NULL`, the description is not copied but the size is still returned. #### rtcGetLocalAddress From c81a4ab7b46650b57f1a4fb80349a0d64eda478a Mon Sep 17 00:00:00 2001 From: Paul-Louis Ageneau Date: Mon, 20 Jan 2025 11:36:51 +0100 Subject: [PATCH 4/4] Add test --- test/capi_track.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/capi_track.cpp b/test/capi_track.cpp index e728a339f..f85da9e0d 100644 --- a/test/capi_track.cpp +++ b/test/capi_track.cpp @@ -19,6 +19,8 @@ static void sleep(unsigned int secs) { Sleep(secs * 1000); } #include // for sleep #endif +#define BUFFER_SIZE 4096 + typedef struct { rtcState state; rtcGatheringState gatheringState; @@ -187,9 +189,25 @@ int test_capi_track_main() { goto error; } + // Test createOffer + char buffer[BUFFER_SIZE]; + if (rtcCreateOffer(peer1->pc, buffer, BUFFER_SIZE) < 0) { + fprintf(stderr, "rtcCreateOffer failed\n"); + goto error; + } + if (rtcGetLocalDescription(peer1->pc, buffer, BUFFER_SIZE) >= 0) { + fprintf(stderr, "rtcCreateOffer has set the local description\n"); + goto error; + } + // Initiate the handshake rtcSetLocalDescription(peer1->pc, NULL); + if (rtcGetLocalDescription(peer1->pc, buffer, BUFFER_SIZE) < 0) { + fprintf(stderr, "rtcGetLocalDescription failed\n"); + goto error; + } + attempts = 10; while ((!peer2->connected || !peer1->connected) && attempts--) sleep(1);