diff --git a/deps/libjuice b/deps/libjuice index 9be39ad..d8179c8 160000 --- a/deps/libjuice +++ b/deps/libjuice @@ -1 +1 @@ -Subproject commit 9be39ad50bcad5900c26fe1f4a4f7f1de621d040 +Subproject commit d8179c83003043f1b7f5aac8fa10ba61aa634da2 diff --git a/examples/media/main.cpp b/examples/media/main.cpp index 3c4ba1e..9d0d309 100644 --- a/examples/media/main.cpp +++ b/examples/media/main.cpp @@ -59,7 +59,7 @@ int main() { addr.sin_port = htons(5000); addr.sin_family = AF_INET; - rtc::Description::VideoMedia media(rtc::Description::RecvOnly); + rtc::Description::Video media("video", rtc::Description::RecvOnly); media.addH264Codec(96); media.setBitrate( 3000); // Request 3Mbps (Browsers do not encode more than 2.5MBps from a webcam) @@ -78,7 +78,8 @@ int main() { }, nullptr); - pc->setLocalDescription(); + // TODO + // pc->setLocalDescription(); std::cout << "Expect RTP video traffic on localhost:5000" << std::endl; std::cout << "Please copy/paste the answer provided by the browser: " << std::endl; diff --git a/examples/media/main.html b/examples/media/main.html index 213be94..5fb536a 100644 --- a/examples/media/main.html +++ b/examples/media/main.html @@ -14,7 +14,7 @@ document.querySelector('button').addEventListener('click', async () => { let offer = JSON.parse(document.querySelector('textarea').value); rtc = new RTCPeerConnection({ - // Requirement of libdatachannel + // Recommended for libdatachannel bundlePolicy: "max-bundle", }); @@ -36,21 +36,10 @@ } }); media.getTracks().forEach(track => rtc.addTrack(track, media)); - let answer= await rtc.createAnswer(); + let answer = await rtc.createAnswer(); await rtc.setLocalDescription(answer); - - // For some reason, (at least) chrome requires for you to manually add the candidates. - offer.sdp.split("\n").forEach(line => { - if (line.startsWith("a=candidate")) { - let cand = line.substring(2); - rtc.addIceCandidate({ - sdpMid: "video", - candidate: cand.trim() - }); - } - }); }) - \ No newline at end of file + diff --git a/include/rtc/description.hpp b/include/rtc/description.hpp index 0dbc2bb..7e826ca 100644 --- a/include/rtc/description.hpp +++ b/include/rtc/description.hpp @@ -1,5 +1,6 @@ /** - * Copyright (c) 2019 Paul-Louis Ageneau + * Copyright (c) 2019-2020 Paul-Louis Ageneau + * Copyright (c) 2020 Staz M * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -24,12 +25,13 @@ #include #include +#include #include +#include #include namespace rtc { - class Description { public: enum class Type { Unspec = 0, Offer = 1, Answer = 2 }; @@ -44,18 +46,12 @@ public: string typeString() const; Role role() const; string roleString() const; - string dataMid() const; string bundleMid() const; std::optional fingerprint() const; - std::optional sctpPort() const; - std::optional maxMessageSize() const; bool ended() const; void hintType(Type type); - void setDataMid(string mid); void setFingerprint(string fingerprint); - void setSctpPort(uint16_t port); - void setMaxMessageSize(size_t size); void addCandidate(Candidate candidate); void endCandidates(); @@ -63,22 +59,68 @@ public: operator string() const; string generateSdp(string_view eol) const; - string generateDataSdp(string_view eol) const; + string generateApplicationSdp(string_view eol) const; + + class Entry { + public: + Entry(string mline, string mid = "", Direction dir = Direction::Unknown); + virtual ~Entry() = default; + + virtual string type() const { return mType; } + virtual string description() const { return mDescription; } + virtual string mid() const { return mMid; } + Direction direction() const; + + virtual void parseSdpLine(string_view line); + virtual string generateSdp(string_view eol) const; + + protected: + std::vector mAttributes; + Direction mDirection; + + private: + string mType; + string mDescription; + string mMid; + }; + + struct Application : public Entry { + public: + Application(string mid = "data"); + Application(const Application &other) = default; + Application(Application &&other) = default; + + string description() const override; + Application reciprocate() const; + + void setSctpPort(uint16_t port) { mSctpPort = port; } + void hintSctpPort(uint16_t port) { mSctpPort = mSctpPort.value_or(port); } + void setMaxMessageSize(size_t size) { mMaxMessageSize = size; } + + std::optional sctpPort() const { return mSctpPort; } + std::optional maxMessageSize() const { return mMaxMessageSize; } + + virtual void parseSdpLine(string_view line) override; + virtual string generateSdp(string_view eol) const override; + + private: + std::optional mSctpPort; + std::optional mMaxMessageSize; + }; // Media (non-data) - class Media { + class Media : public Entry { public: - Media(string mline, Direction dir = Direction::Unknown, string mid = "media"); + Media(string mline, string mid = "media", Direction dir = Direction::SendOnly); + Media(const Media &other) = default; + Media(Media &&other) = default; + virtual ~Media() = default; - string type() const { return mType; } - string description() const { return mDescription; } - string mid() const { return mMid; } + string description() const override; + Media reciprocate() const; void removeFormat(const string &fmt); - Direction getDirection(); - void setDirection(Direction dir); - void addVideoCodec(int payloadType, const string &codec); void addH264Codec(int payloadType); void addVP8Codec(int payloadType); @@ -87,19 +129,16 @@ public: void setBitrate(int bitrate); int getBitrate() const; - void parseSdpLine(string line); - string generateSdp(string_view eol) const; + bool hasPayloadType(int payloadType) const; + + virtual void parseSdpLine(string_view line) override; + virtual string generateSdp(string_view eol) const override; private: - string mType; - string mDescription; - string mMid; - std::vector mAttributes; - std::vector mAttributesl; int mBas = -1; struct RTPMap { - RTPMap(const string &mLine); + RTPMap(string_view mline); void removeFB(const string &string); void addFB(const string &string); @@ -119,35 +158,45 @@ public: std::map mRtpMap; }; - class AudioMedia : public Media { + class Audio : public Media { public: - AudioMedia(Direction dir, string mid = "audio"); + Audio(string mid = "audio", Direction dir = Direction::SendOnly); }; - class VideoMedia : public Media { + class Video : public Media { public: - VideoMedia(Direction dir, string mid = "video"); + Video(string mid = "video", Direction dir = Direction::SendOnly); }; - bool hasMedia() const; - void addMedia(Media media); + bool hasAudioOrVideo() const; + + int addMedia(Media media); + int addMedia(Application application); + int addApplication(string mid = "data"); + int addVideo(string mid = "video", Direction dir = Direction::SendOnly); + int addAudio(string mid = "audio", Direction dir = Direction::SendOnly); + + std::variant media(int index); + std::variant media(int index) const; + int mediaCount() const; + + Application *application(); private: + std::shared_ptr createEntry(string mline, string mid, Direction dir); + void removeApplication(); + Type mType; + + // Session-level attributes Role mRole; string mSessionId; string mIceUfrag, mIcePwd; std::optional mFingerprint; - struct Data { - string mid; - std::optional sctpPort; - std::optional maxMessageSize; - }; - - Data mData; - - std::map mMedia; // by m-line index + // Entries + std::vector> mEntries; + std::shared_ptr mApplication; // Candidates std::vector mCandidates; diff --git a/include/rtc/peerconnection.hpp b/include/rtc/peerconnection.hpp index a28d552..86b3731 100644 --- a/include/rtc/peerconnection.hpp +++ b/include/rtc/peerconnection.hpp @@ -158,6 +158,8 @@ private: std::unordered_map> mTracks; // by mid std::shared_mutex mDataChannelsMutex, mTracksMutex; + std::unordered_map mMidFromPayloadType; // cache + std::atomic mState; std::atomic mGatheringState; diff --git a/src/datachannel.cpp b/src/datachannel.cpp index 15d501f..4b84a48 100644 --- a/src/datachannel.cpp +++ b/src/datachannel.cpp @@ -144,8 +144,9 @@ size_t DataChannel::maxMessageSize() const { size_t remoteMax = DEFAULT_MAX_MESSAGE_SIZE; if (auto pc = mPeerConnection.lock()) if (auto description = pc->remoteDescription()) - if (auto maxMessageSize = description->maxMessageSize()) - remoteMax = *maxMessageSize > 0 ? *maxMessageSize : LOCAL_MAX_MESSAGE_SIZE; + if (auto *application = description->application()) + if (auto maxMessageSize = application->maxMessageSize()) + remoteMax = *maxMessageSize > 0 ? *maxMessageSize : LOCAL_MAX_MESSAGE_SIZE; return std::min(remoteMax, LOCAL_MAX_MESSAGE_SIZE); } diff --git a/src/description.cpp b/src/description.cpp index 2f35d05..b6b2965 100644 --- a/src/description.cpp +++ b/src/description.cpp @@ -1,5 +1,6 @@ /** - * Copyright (c) 2019 Paul-Louis Ageneau + * Copyright (c) 2019-2020 Paul-Louis Ageneau + * Copyright (c) 2020 Staz M * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -21,16 +22,19 @@ #include #include #include +#include #include #include +using std::shared_ptr; using std::size_t; using std::string; +using std::string_view; using std::chrono::system_clock; namespace { -inline bool match_prefix(const string &str, const string &prefix) { +inline bool match_prefix(string_view str, string_view prefix) { return str.size() >= prefix.size() && std::mismatch(prefix.begin(), prefix.end(), str.begin()).first == prefix.end(); } @@ -41,6 +45,21 @@ inline void trim_end(string &str) { str.end()); } +inline std::pair parse_pair(string_view attr) { + string_view key, value; + if (size_t separator = attr.find(':'); separator != string::npos) { + key = attr.substr(0, separator); + value = attr.substr(separator + 1); + } else { + key = attr; + } + return std::make_pair(std::move(key), std::move(value)); +} + +template T to_integer(string_view s) { + return std::is_signed::value ? T(std::stol(string(s))) : T(std::stoul(string(s))); +} + } // namespace namespace rtc { @@ -52,7 +71,6 @@ Description::Description(const string &sdp, Type type) : Description(sdp, type, Description::Description(const string &sdp, Type type, Role role) : mType(Type::Unspec), mRole(role) { - mData.mid = "data"; hintType(type); auto seed = static_cast(system_clock::now().time_since_epoch().count()); @@ -61,50 +79,25 @@ Description::Description(const string &sdp, Type type, Role role) mSessionId = std::to_string(uniform(generator)); std::istringstream ss(sdp); - std::optional currentMedia; + std::shared_ptr current; - int mlineIndex = 0; - bool finished; - do { - string line; - finished = !std::getline(ss, line) && line.empty(); + int index = -1; + string line; + while (std::getline(ss, line) || !line.empty()) { trim_end(line); // Media description line (aka m-line) - if (finished || match_prefix(line, "m=")) { - if (currentMedia) { - if (!currentMedia->mid().empty()) { - if (currentMedia->type() == "application") - mData.mid = currentMedia->mid(); - else - mMedia.emplace(mlineIndex, std::move(*currentMedia)); - - ++mlineIndex; - - } else if (line.find(" ICE/SDP") != string::npos) { - PLOG_WARNING << "SDP \"m=\" line has no corresponding mid, ignoring"; - } - } - if (!finished) - currentMedia.emplace(Media(line.substr(2))); + if (match_prefix(line, "m=")) { + ++index; + string mline = line.substr(2); + current = createEntry(std::move(mline), std::to_string(index), Direction::Unknown); // Attribute line } else if (match_prefix(line, "a=")) { string attr = line.substr(2); + auto [key, value] = parse_pair(attr); - string key, value; - if (size_t separator = attr.find(':'); separator != string::npos) { - key = attr.substr(0, separator); - value = attr.substr(separator + 1); - } else { - key = attr; - } - - if (key == "mid") { - if (currentMedia) - currentMedia->mid() = value; - - } else if (key == "setup") { + if (key == "setup") { if (value == "active") mRole = Role::Active; else if (value == "passive") @@ -125,21 +118,18 @@ Description::Description(const string &sdp, Type type, Role role) mIceUfrag = value; } else if (key == "ice-pwd") { mIcePwd = value; - } else if (key == "sctp-port") { - mData.sctpPort = uint16_t(std::stoul(value)); - } else if (key == "max-message-size") { - mData.maxMessageSize = size_t(std::stoul(value)); } else if (key == "candidate") { - addCandidate(Candidate(attr, currentMedia ? currentMedia->mid() : mData.mid)); + addCandidate(Candidate(attr, bundleMid())); } else if (key == "end-of-candidates") { mEnded = true; - } else if (currentMedia) { - currentMedia->parseSdpLine(std::move(line)); + } else if (current) { + current->parseSdpLine(std::move(line)); } - } else if (currentMedia) { - currentMedia->parseSdpLine(std::move(line)); + + } else if (current) { + current->parseSdpLine(std::move(line)); } - } while (!finished); + }; } Description::Type Description::type() const { return mType; } @@ -150,22 +140,13 @@ Description::Role Description::role() const { return mRole; } string Description::roleString() const { return roleToString(mRole); } -string Description::dataMid() const { return mData.mid; } - string Description::bundleMid() const { // Get the mid of the first media - if (auto it = mMedia.find(0); it != mMedia.end()) - return it->second.mid(); - else - return mData.mid; + return !mEntries.empty() ? mEntries[0]->mid() : "0"; } std::optional Description::fingerprint() const { return mFingerprint; } -std::optional Description::sctpPort() const { return mData.sctpPort; } - -std::optional Description::maxMessageSize() const { return mData.maxMessageSize; } - bool Description::ended() const { return mEnded; } void Description::hintType(Type type) { @@ -176,16 +157,10 @@ void Description::hintType(Type type) { } } -void Description::setDataMid(string mid) { mData.mid = mid; } - void Description::setFingerprint(string fingerprint) { mFingerprint.emplace(std::move(fingerprint)); } -void Description::setSctpPort(uint16_t port) { mData.sctpPort.emplace(port); } - -void Description::setMaxMessageSize(size_t size) { mData.maxMessageSize.emplace(size); } - void Description::addCandidate(Candidate candidate) { mCandidates.emplace_back(std::move(candidate)); } @@ -199,10 +174,6 @@ std::vector Description::extractCandidates() { return result; } -bool Description::hasMedia() const { return !mMedia.empty(); } - -void Description::addMedia(Media media) { mMedia.emplace(int(mMedia.size()), std::move(media)); } - Description::operator string() const { return generateSdp("\r\n"); } string Description::generateSdp(string_view eol) const { @@ -218,23 +189,62 @@ string Description::generateSdp(string_view eol) const { // see Negotiating Media Multiplexing Using the Session Description Protocol // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-54 sdp << "a=group:BUNDLE"; - for (int i = 0; i < int(mMedia.size() + 1); ++i) { - if (auto it = mMedia.find(i); it != mMedia.end()) - sdp << ' ' << it->second.mid(); - else - sdp << ' ' << mData.mid; - } + for (const auto &entry : mEntries) + sdp << ' ' << entry->mid(); sdp << eol; - // Non-data media - if (!mMedia.empty()) { - // Lip-sync - sdp << "a=group:LS"; - for (const auto &p : mMedia) - sdp << " " << p.second.mid(); - sdp << eol; + // Lip-sync + std::ostringstream lsGroup; + for (const auto &entry : mEntries) + if (entry != mApplication) + lsGroup << ' ' << entry->mid(); + + if (!lsGroup.str().empty()) + sdp << "a=group:LS" << lsGroup.str() << eol; + + // Session-level attributes + sdp << "a=msid-semantic:WMS *" << eol; + sdp << "a=setup:" << roleToString(mRole) << eol; + sdp << "a=ice-ufrag:" << mIceUfrag << eol; + sdp << "a=ice-pwd:" << mIcePwd << eol; + + if (mFingerprint) + sdp << "a=fingerprint:sha-256 " << *mFingerprint << eol; + + if (!mEnded) + sdp << "a=ice-options:trickle" << eol; + + // Entries + bool first = true; + for (const auto &entry : mEntries) { + sdp << entry->generateSdp(eol); + + if (std::exchange(first, false)) { + // Candidates + for (const auto &candidate : mCandidates) + sdp << string(candidate) << eol; + + if (mEnded) + sdp << "a=end-of-candidates" << eol; + } } + return sdp.str(); +} + +string Description::generateApplicationSdp(string_view eol) const { + std::ostringstream sdp; + + // Header + sdp << "v=0" << eol; + sdp << "o=- " << mSessionId << " 0 IN IP4 127.0.0.1" << eol; + sdp << "s=-" << eol; + sdp << "t=0 0" << eol; + + // Application + auto app = mApplication ? mApplication : std::make_shared(); + sdp << app->generateSdp(eol); + // Session-level attributes sdp << "a=msid-semantic:WMS *" << eol; sdp << "a=setup:" << roleToString(mRole) << eol; @@ -247,86 +257,243 @@ string Description::generateSdp(string_view eol) const { if (mFingerprint) sdp << "a=fingerprint:sha-256 " << *mFingerprint << eol; - // Media descriptions and attributes - for (int i = 0; i < int(mMedia.size() + 1); ++i) { - if (auto it = mMedia.find(i); it != mMedia.end()) { - sdp << it->second.generateSdp(eol); - } else { - // Data - const string description = "UDP/DTLS/SCTP webrtc-datachannel"; - sdp << "m=application" << ' ' << (!mMedia.empty() ? 0 : 9) << ' ' << description << eol; - sdp << "c=IN IP4 0.0.0.0" << eol; - if (!mMedia.empty()) - sdp << "a=bundle-only" << eol; - sdp << "a=mid:" << mData.mid << eol; - sdp << "a=sendrecv" << eol; - if (mData.sctpPort) - sdp << "a=sctp-port:" << *mData.sctpPort << eol; - if (mData.maxMessageSize) - sdp << "a=max-message-size:" << *mData.maxMessageSize << eol; - } + // Candidates + for (const auto &candidate : mCandidates) + sdp << string(candidate) << eol; + + if (mEnded) + sdp << "a=end-of-candidates" << eol; + + return sdp.str(); +} + +shared_ptr Description::createEntry(string mline, string mid, Direction dir) { + string type = mline.substr(0, mline.find(' ')); + if (type == "application") { + removeApplication(); + mApplication = std::make_shared(std::move(mid)); + mEntries.emplace_back(mApplication); + return mApplication; + } else { + auto media = std::make_shared(std::move(mline), std::move(mid), dir); + mEntries.emplace_back(media); + return media; } - - // Candidates - for (const auto &candidate : mCandidates) - sdp << string(candidate) << eol; - - if (mEnded) - sdp << "a=end-of-candidates" << eol; - - return sdp.str(); } -string Description::generateDataSdp(string_view eol) const { - std::ostringstream sdp; +void Description::removeApplication() { + if (!mApplication) + return; - // Header - sdp << "v=0" << eol; - sdp << "o=- " << mSessionId << " 0 IN IP4 127.0.0.1" << eol; - sdp << "s=-" << eol; - sdp << "t=0 0" << eol; + auto it = std::find(mEntries.begin(), mEntries.end(), mApplication); + if (it != mEntries.end()) + mEntries.erase(it); - // Data - sdp << "m=application 9 UDP/DTLS/SCTP webrtc-datachannel"; - sdp << "c=IN IP4 0.0.0.0" << eol; - sdp << "a=mid:" << mData.mid << eol; - sdp << "a=sendrecv" << eol; - if (mData.sctpPort) - sdp << "a=sctp-port:" << *mData.sctpPort << eol; - if (mData.maxMessageSize) - sdp << "a=max-message-size:" << *mData.maxMessageSize << eol; - - sdp << "a=setup:" << roleToString(mRole) << eol; - sdp << "a=ice-ufrag:" << mIceUfrag << eol; - sdp << "a=ice-pwd:" << mIcePwd << eol; - - if (!mEnded) - sdp << "a=ice-options:trickle" << eol; - - if (mFingerprint) - sdp << "a=fingerprint:sha-256 " << *mFingerprint << eol; - - // Candidates - for (const auto &candidate : mCandidates) - sdp << string(candidate) << eol; - - if (mEnded) - sdp << "a=end-of-candidates" << eol; - - return sdp.str(); + mApplication.reset(); } -Description::Media::Media(string mline, Direction dir, string mid) { +bool Description::hasAudioOrVideo() const { + for (auto entry : mEntries) + if (entry != mApplication) + return true; + + return false; +} + +int Description::addMedia(Media media) { + mEntries.emplace_back(std::make_shared(std::move(media))); + return int(mEntries.size()) - 1; +} + +int Description::addMedia(Application application) { + removeApplication(); + mApplication = std::make_shared(std::move(application)); + mEntries.emplace_back(mApplication); + return int(mEntries.size()) - 1; +} + +int Description::addApplication(string mid) { return addMedia(Application(std::move(mid))); } + +Description::Application *Description::application() { return mApplication.get(); } + +int Description::addVideo(string mid, Direction dir) { + return addMedia(Video(std::move(mid), dir)); +} + +int Description::addAudio(string mid, Direction dir) { + return addMedia(Audio(std::move(mid), dir)); +} + +std::variant Description::media(int index) { + if (index < 0 || index >= int(mEntries.size())) + throw std::out_of_range("Media index out of range"); + + const auto &entry = mEntries[index]; + if (entry == mApplication) { + auto result = dynamic_cast(entry.get()); + if (!result) + throw std::logic_error("Bad type of application in description"); + return result; + } else { + auto result = dynamic_cast(entry.get()); + if (!result) + throw std::logic_error("Bad type of media in description"); + return result; + } +} + +std::variant +Description::media(int index) const { + if (index < 0 || index >= int(mEntries.size())) + throw std::out_of_range("Media index out of range"); + + const auto &entry = mEntries[index]; + if (entry == mApplication) { + auto result = dynamic_cast(entry.get()); + if (!result) + throw std::logic_error("Bad type of application in description"); + return result; + } else { + auto result = dynamic_cast(entry.get()); + if (!result) + throw std::logic_error("Bad type of media in description"); + return result; + } +} + +int Description::mediaCount() const { return int(mEntries.size()); } + +Description::Entry::Entry(string mline, string mid, Direction dir) + : mDirection(dir), mMid(std::move(mid)) { size_t p = mline.find(' '); mType = mline.substr(0, p); if (p != string::npos) if (size_t q = mline.find(' ', p + 1); q != string::npos) - mDescription = mline.substr(q + 1, mline.find(' ', q + 1) - q - 2); + mDescription = mline.substr(q + 1, mline.find(' ', q + 1) - (q + 1)); +} - mMid = mid; +string Description::Entry::generateSdp(string_view eol) const { + std::ostringstream sdp; + sdp << "m=" << type() << ' ' << 0 << ' ' << description() << eol; + sdp << "c=IN IP4 0.0.0.0" << eol; + sdp << "a=bundle-only" << eol; + sdp << "a=mid:" << mMid << eol; + + switch (mDirection) { + case Direction::RecvOnly: + sdp << "a=recvonly" << eol; + break; + case Direction::SendOnly: + sdp << "a=sendonly" << eol; + break; + case Direction::SendRecv: + sdp << "a=sendrecv" << eol; + break; + default: + // Ignore + break; + } + + for (const auto &attr : mAttributes) + sdp << "a=" << attr << eol; + + return sdp.str(); +} + +void Description::Entry::parseSdpLine(string_view line) { + if (match_prefix(line, "a=")) { + string_view attr = line.substr(2); + auto [key, value] = parse_pair(attr); + + if (key == "mid") + mMid = value; + else if (key == "sendrecv") + mDirection = Direction::SendRecv; + else if (attr == "recvonly") + mDirection = Direction::RecvOnly; + else if (attr == "sendonly") + mDirection = Direction::SendOnly; + else + mAttributes.emplace_back(line.substr(2)); + } +} + +Description::Application::Application(string mid) + : Entry("application 9 UDP/DTLS/SCTP", std::move(mid), Direction::SendRecv) {} + +string Description::Application::description() const { + return Entry::description() + " webrtc-datachannel"; +} + +Description::Application Description::Application::reciprocate() const { + Application reciprocated(*this); + + reciprocated.mMaxMessageSize.reset(); + + return reciprocated; +} + +string Description::Application::generateSdp(string_view eol) const { + std::ostringstream sdp; + sdp << Entry::generateSdp(eol); + + if (mSctpPort) + sdp << "a=sctp-port:" << *mSctpPort << eol; + + if (mMaxMessageSize) + sdp << "a=max-message-size:" << *mMaxMessageSize << eol; + + return sdp.str(); +} + +void Description::Application::parseSdpLine(string_view line) { + if (match_prefix(line, "a=")) { + string_view attr = line.substr(2); + auto [key, value] = parse_pair(attr); + + if (key == "sctp-port") { + mSctpPort = to_integer(value); + } else if (key == "max-message-size") { + mMaxMessageSize = to_integer(value); + } else { + Entry::parseSdpLine(line); + } + } else { + Entry::parseSdpLine(line); + } +} + +Description::Media::Media(string mline, string mid, Direction dir) + : Entry(std::move(mline), std::move(mid), dir) { mAttributes.emplace_back("rtcp-mux"); +} - setDirection(dir); +string Description::Media::description() const { + std::ostringstream desc; + desc << Entry::description(); + for (auto it = mRtpMap.begin(); it != mRtpMap.end(); ++it) + desc << ' ' << it->first; + + return desc.str(); +} + +Description::Media Description::Media::reciprocate() const { + Media reciprocated(*this); + + // Invert direction + switch (reciprocated.mDirection) { + case Direction::RecvOnly: + reciprocated.mDirection = Direction::SendOnly; + break; + case Direction::SendOnly: + reciprocated.mDirection = Direction::RecvOnly; + break; + default: + // We are good + break; + } + + return reciprocated; } Description::Media::RTPMap &Description::Media::getFormat(int fmt) { @@ -368,7 +535,7 @@ void Description::Media::removeFormat(const string &fmt) { if (it2->find("apt=") == 0) { for (auto remid : remed) { if (it2->find(std::to_string(remid)) != string::npos) { - std::cout << *it2 << " " << remid << std::endl; + std::cout << *it2 << ' ' << remid << std::endl; it = mRtpMap.erase(it); rem = true; break; @@ -384,7 +551,7 @@ void Description::Media::removeFormat(const string &fmt) { } void Description::Media::addVideoCodec(int payloadType, const string &codec) { - RTPMap map(std::to_string(payloadType) + " " + codec + "/90000"); + RTPMap map(std::to_string(payloadType) + ' ' + codec + "/90000"); map.addFB("nack"); map.addFB("goog-remb"); mRtpMap.emplace(map.pt, map); @@ -396,128 +563,82 @@ void Description::Media::addVP8Codec(int payloadType) { addVideoCodec(payloadTyp void Description::Media::addVP9Codec(int payloadType) { addVideoCodec(payloadType, "VP9"); } -Description::Direction Description::Media::getDirection() { - for (auto attr : mAttributes) { - if (attr == "sendrecv") - return Direction::SendRecv; - if (attr == "recvonly") - return Direction::RecvOnly; - if (attr == "sendonly") - return Direction::SendOnly; - } - return Direction::Unknown; -} - void Description::Media::setBitrate(int bitrate) { mBas = bitrate; } int Description::Media::getBitrate() const { return mBas; } -void Description::Media::setDirection(Description::Direction dir) { - auto it = mAttributes.begin(); - while (it != mAttributes.end()) { - if (*it == "sendrecv" || *it == "sendonly" || *it == "recvonly") - it = mAttributes.erase(it); - else - it++; - } - if (dir == Direction::SendRecv) - mAttributes.emplace(mAttributes.begin(), "sendrecv"); - else if (dir == Direction::RecvOnly) - mAttributes.emplace(mAttributes.begin(), "recvonly"); - if (dir == Direction::SendOnly) - mAttributes.emplace(mAttributes.begin(), "sendonly"); +bool Description::Media::hasPayloadType(int payloadType) const { + return mRtpMap.find(payloadType) != mRtpMap.end(); } string Description::Media::generateSdp(string_view eol) const { std::ostringstream sdp; + sdp << Entry::generateSdp(eol); - sdp << "m=" << mType << ' ' << 0 << ' ' << mDescription; - - for (auto it = mRtpMap.begin(); it != mRtpMap.end(); ++it) - sdp << ' ' << it->first; - - sdp << eol; - sdp << "c=IN IP4 0.0.0.0" << eol; - if (mBas > -1) + if (mBas >= 0) sdp << "b=AS:" << mBas << eol; - sdp << "a=bundle-only" << eol; - sdp << "a=mid:" << mMid << eol; - - for (const auto &attr : mAttributes) - sdp << "a=" << attr << eol; - for (auto it = mRtpMap.begin(); it != mRtpMap.end(); ++it) { auto &map = it->second; // Create the a=rtpmap - sdp << "a=rtpmap:" << map.pt << " " << map.format << "/" << map.clockRate; + sdp << "a=rtpmap:" << map.pt << ' ' << map.format << '/' << map.clockRate; if (!map.encParams.empty()) - sdp << "/" << map.encParams; + sdp << '/' << map.encParams; sdp << eol; for (const auto &val : map.rtcpFbs) - sdp << "a=rtcp-fb:" << map.pt << " " << val << eol; + sdp << "a=rtcp-fb:" << map.pt << ' ' << val << eol; for (const auto &val : map.fmtps) - sdp << "a=fmtp:" << map.pt << " " << val << eol; + sdp << "a=fmtp:" << map.pt << ' ' << val << eol; } - for (const auto &attr : mAttributesl) - sdp << "a=" << attr << eol; - return sdp.str(); } -void Description::Media::parseSdpLine(string line) { +void Description::Media::parseSdpLine(string_view line) { if (match_prefix(line, "a=")) { - string attr = line.substr(2); + string_view attr = line.substr(2); + auto [key, value] = parse_pair(attr); - string key, value; - if (size_t separator = attr.find(':'); separator != string::npos) { - key = attr.substr(0, separator); - value = attr.substr(separator + 1); - } else { - key = attr; - } - - if (key == "mid") { - mMid = value; - } else if (key == "rtpmap") { + if (key == "rtpmap") { Description::Media::RTPMap map(value); - mRtpMap.emplace(map.pt, map); + int pt = map.pt; + mRtpMap.emplace(pt, std::move(map)); } else if (key == "rtcp-fb") { size_t p = value.find(' '); - int pt = std::stoi(value.substr(0, p)); + int pt = to_integer(value.substr(0, p)); auto it = mRtpMap.find(pt); if (it == mRtpMap.end()) { - PLOG_WARNING << "rtcp-fb applied before it's rtpmap. Ignoring"; - } else + PLOG_WARNING << "rtcp-fb applied before its rtpmap. Ignoring"; + } else { it->second.rtcpFbs.emplace_back(value.substr(p + 1)); + } } else if (key == "fmtp") { size_t p = value.find(' '); - int pt = std::stoi(value.substr(0, p)); + int pt = to_integer(value.substr(0, p)); auto it = mRtpMap.find(pt); if (it == mRtpMap.end()) { - PLOG_WARNING << "fmtp applied before it's rtpmap. Ignoring"; + PLOG_WARNING << "fmtp applied before its rtpmap. Ignoring"; } else { it->second.fmtps.emplace_back(value.substr(p + 1)); } - } else if (key == "b") { - // TODO } else { - mAttributes.emplace_back(line.substr(2)); + Entry::parseSdpLine(line); } } else if (match_prefix(line, "b=AS")) { - mBas = std::stoi(line.substr(line.find(':') + 1)); + mBas = to_integer(line.substr(line.find(':') + 1)); + } else { + Entry::parseSdpLine(line); } } -Description::Media::RTPMap::RTPMap(const string &mline) { +Description::Media::RTPMap::RTPMap(string_view mline) { size_t p = mline.find(' '); - this->pt = std::stoi(mline.substr(0, p)); + this->pt = to_integer(mline.substr(0, p)); - auto line = mline.substr(p + 1); + string_view line = mline.substr(p + 1); size_t spl = line.find('/'); this->format = line.substr(0, spl); @@ -527,14 +648,14 @@ Description::Media::RTPMap::RTPMap(const string &mline) { spl = line.find(' '); } if (spl == string::npos) - this->clockRate = std::stoi(line); + this->clockRate = to_integer(line); else { - this->clockRate = std::stoi(line.substr(0, spl)); + this->clockRate = to_integer(line.substr(0, spl)); this->encParams = line.substr(spl); } } -void Description::Media::RTPMap::removeFB(const string& string) { +void Description::Media::RTPMap::removeFB(const string &string) { auto it = rtcpFbs.begin(); while (it != rtcpFbs.end()) { if (it->find(string) != std::string::npos) { @@ -546,12 +667,11 @@ void Description::Media::RTPMap::removeFB(const string& string) { void Description::Media::RTPMap::addFB(const string &string) { rtcpFbs.emplace_back(string); } -Description::AudioMedia::AudioMedia(Direction dir, string mid) - : Media("audio 9 UDP/TLS/RTP/SAVPF", dir, std::move(mid)) { -} +Description::Audio::Audio(string mid, Direction dir) + : Media("audio 9 UDP/TLS/RTP/SAVPF", std::move(mid), dir) {} -Description::VideoMedia::VideoMedia(Direction dir, string mid) - : Media("video 9 UDP/TLS/RTP/SAVPF", dir, std::move(mid)) {} +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") @@ -589,4 +709,3 @@ string Description::roleToString(Role role) { std::ostream &operator<<(std::ostream &out, const rtc::Description &description) { return out << std::string(description); } - diff --git a/src/icetransport.cpp b/src/icetransport.cpp index 218d5c4..b3833df 100644 --- a/src/icetransport.cpp +++ b/src/icetransport.cpp @@ -124,7 +124,8 @@ void IceTransport::setRemoteDescription(const Description &description) { mRole = description.role() == Description::Role::Active ? Description::Role::Passive : Description::Role::Active; mMid = description.bundleMid(); - if (juice_set_remote_description(mAgent.get(), description.generateDataSdp("\r\n").c_str()) < 0) + if (juice_set_remote_description(mAgent.get(), + description.generateApplicationSdp("\r\n").c_str()) < 0) throw std::runtime_error("Failed to parse ICE settings from remote SDP"); } @@ -487,8 +488,8 @@ void IceTransport::setRemoteDescription(const Description &description) { mTrickleTimeout = !description.ended() ? 30s : 0s; // Warning: libnice expects "\n" as end of line - if (nice_agent_parse_remote_sdp(mNiceAgent.get(), description.generateDataSdp("\n").c_str()) < - 0) + if (nice_agent_parse_remote_sdp(mNiceAgent.get(), + description.generateApplicationSdp("\n").c_str()) < 0) throw std::runtime_error("Failed to parse ICE settings from remote SDP"); } diff --git a/src/peerconnection.cpp b/src/peerconnection.cpp index 29c1d13..b39a21d 100644 --- a/src/peerconnection.cpp +++ b/src/peerconnection.cpp @@ -99,6 +99,9 @@ void PeerConnection::setLocalDescription() { void PeerConnection::setRemoteDescription(Description description) { PLOG_VERBOSE << "Setting remote description: " << string(description); + if (description.mediaCount() == 0) + throw std::runtime_error("Remote description has no media line"); + if (!description.fingerprint()) throw std::runtime_error("Remote description is incomplete"); @@ -231,7 +234,7 @@ void PeerConnection::onGatheringStateChange(std::functionhasMedia()) || (remote && remote->hasMedia()); + return (local && local->hasAudioOrVideo()) || (remote && remote->hasAudioOrVideo()); } std::shared_ptr PeerConnection::createTrack(Description::Media description) { @@ -380,7 +383,11 @@ shared_ptr PeerConnection::initSctpTransport() { if (auto transport = std::atomic_load(&mSctpTransport)) return transport; - uint16_t sctpPort = remoteDescription()->sctpPort().value_or(DEFAULT_SCTP_PORT); + auto remote = remoteDescription(); + if (!remote || !remote->application()) + throw std::logic_error("Initializing SCTP transport without application description"); + + uint16_t sctpPort = remote->application()->sctpPort().value_or(DEFAULT_SCTP_PORT); auto lower = std::atomic_load(&mDtlsTransport); auto transport = std::make_shared( lower, sctpPort, weak_bind(&PeerConnection::forwardMessage, this, _1), @@ -512,11 +519,41 @@ void PeerConnection::forwardMedia(message_ptr message) { if (!message) return; - string mid; - // TODO: stream (PT) to mid + unsigned int payloadType = message->stream; + std::optional mid; + if (auto it = mMidFromPayloadType.find(payloadType); it != mMidFromPayloadType.end()) { + mid = it->second; + } else { + std::lock_guard lock(mLocalDescriptionMutex); + if (!mLocalDescription) + return; + + for (int i = 0; i < mLocalDescription->mediaCount(); ++i) { + if (auto found = std::visit( // reciprocate each media + rtc::overloaded{[&](Description::Application *) -> std::optional { + return std::nullopt; + }, + [&](Description::Media *media) -> std::optional { + return media->hasPayloadType(payloadType) + ? std::make_optional(media->mid()) + : nullopt; + }}, + mLocalDescription->media(i))) { + + mMidFromPayloadType.emplace(payloadType, *found); + mid = *found; + break; + } + } + } + + if (!mid) { + PLOG_WARNING << "Track not found for payload type " << payloadType; + return; + } std::shared_lock lock(mTracksMutex); // read-only - if (auto it = mTracks.find(mid); it != mTracks.end()) + if (auto it = mTracks.find(*mid); it != mTracks.end()) if (auto track = it->second.lock()) track->incoming(message); } @@ -613,25 +650,56 @@ void PeerConnection::remoteCloseDataChannels() { } void PeerConnection::processLocalDescription(Description description) { - std::optional remoteSctpPort; - std::optional remoteDataMid; if (auto remote = remoteDescription()) { - remoteDataMid = remote->dataMid(); - remoteSctpPort = remote->sctpPort(); + // Reciprocate remote description + for (int i = 0; i < remote->mediaCount(); ++i) + std::visit( // reciprocate each media + rtc::overloaded{ + [&](Description::Application *app) { + PLOG_DEBUG << "Reciprocating application in local description, mid=\"" + << app->mid() << "\""; + auto reciprocated = app->reciprocate(); + reciprocated.hintSctpPort(DEFAULT_SCTP_PORT); + reciprocated.setMaxMessageSize(LOCAL_MAX_MESSAGE_SIZE); + description.addMedia(std::move(reciprocated)); + }, + [&](Description::Media *media) { + PLOG_DEBUG << "Reciprocating media in local description, mid=\"" + << media->mid() << "\""; + + description.addMedia(media->reciprocate()); + }, + }, + remote->media(i)); + } else { + // Add application for data channels + { + std::shared_lock lock(mDataChannelsMutex); + if (!mDataChannels.empty()) { + const string mid = "data"; + PLOG_DEBUG << "Adding application to local description, mid=\"" << mid << "\""; + Description::Application app; + app.setSctpPort(DEFAULT_SCTP_PORT); + app.setMaxMessageSize(LOCAL_MAX_MESSAGE_SIZE); + description.addMedia(std::move(app)); + } + } + + // Add media for local tracks + { + std::shared_lock lock(mTracksMutex); + for (auto it = mTracks.begin(); it != mTracks.end(); ++it) { + if (auto track = it->second.lock()) { + PLOG_DEBUG << "Adding media to local description, mid=\"" << track->mid() + << "\""; + description.addMedia(track->description()); + } + } + } } - // Set the same data mid as remote - if (remoteDataMid) - description.setDataMid(*remoteDataMid); - - // Set the media - for (auto it = mTracks.begin(); it != mTracks.end(); ++it) - if (auto track = it->second.lock()) - description.addMedia(track->description()); - - description.setSctpPort(remoteSctpPort.value_or(DEFAULT_SCTP_PORT)); - description.setMaxMessageSize(LOCAL_MAX_MESSAGE_SIZE); - description.setFingerprint(mCertificate.get()->fingerprint()); // wait for certificate + // Set local fingerprint (wait for certificate if necessary) + description.setFingerprint(mCertificate.get()->fingerprint()); std::lock_guard lock(mLocalDescriptionMutex); mLocalDescription.emplace(std::move(description)); diff --git a/test/connectivity.cpp b/test/connectivity.cpp index c6a475f..e7ed1a9 100644 --- a/test/connectivity.cpp +++ b/test/connectivity.cpp @@ -52,7 +52,7 @@ void test_connectivity() { if (!pc2) return; cout << "Description 1: " << sdp << endl; - pc2->setRemoteDescription(std::move(sdp)); + pc2->setRemoteDescription(string(sdp)); }); pc1->onLocalCandidate([wpc2 = make_weak_ptr(pc2)](Candidate candidate) { @@ -60,7 +60,7 @@ void test_connectivity() { if (!pc2) return; cout << "Candidate 1: " << candidate << endl; - pc2->addRemoteCandidate(std::move(candidate)); + pc2->addRemoteCandidate(string(candidate)); }); pc1->onStateChange([](PeerConnection::State state) { cout << "State 1: " << state << endl; }); @@ -74,7 +74,7 @@ void test_connectivity() { if (!pc1) return; cout << "Description 2: " << sdp << endl; - pc1->setRemoteDescription(std::move(sdp)); + pc1->setRemoteDescription(string(sdp)); }); pc2->onLocalCandidate([wpc1 = make_weak_ptr(pc1)](Candidate candidate) { @@ -82,7 +82,7 @@ void test_connectivity() { if (!pc1) return; cout << "Candidate 2: " << candidate << endl; - pc1->addRemoteCandidate(std::move(candidate)); + pc1->addRemoteCandidate(string(candidate)); }); pc2->onStateChange([](PeerConnection::State state) { cout << "State 2: " << state << endl; });