Browse Source

Refactored description for media handling

Paul-Louis Ageneau 4 years ago
parent
commit
6c3aaf8a77

+ 1 - 1
deps/libjuice

@@ -1 +1 @@
-Subproject commit 9be39ad50bcad5900c26fe1f4a4f7f1de621d040
+Subproject commit d8179c83003043f1b7f5aac8fa10ba61aa634da2

+ 3 - 2
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;

+ 3 - 14
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()
-                });
-            }
-        });
     })
 </script>
 
 </body>
-</html>
+</html>

+ 89 - 40
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 <iostream>
 #include <map>
+#include <memory>
 #include <optional>
+#include <variant>
 #include <vector>
 
 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<string> fingerprint() const;
-	std::optional<uint16_t> sctpPort() const;
-	std::optional<size_t> 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<string> 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<uint16_t> sctpPort() const { return mSctpPort; }
+		std::optional<size_t> maxMessageSize() const { return mMaxMessageSize; }
+
+		virtual void parseSdpLine(string_view line) override;
+		virtual string generateSdp(string_view eol) const override;
+
+	private:
+		std::optional<uint16_t> mSctpPort;
+		std::optional<size_t> 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<string> mAttributes;
-		std::vector<string> 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<int, RTPMap> 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 *, Application *> media(int index);
+	std::variant<const Media *, const Application *> media(int index) const;
+	int mediaCount() const;
+
+	Application *application();
 
 private:
+	std::shared_ptr<Entry> createEntry(string mline, string mid, Direction dir);
+	void removeApplication();
+
 	Type mType;
+
+	// Session-level attributes
 	Role mRole;
 	string mSessionId;
 	string mIceUfrag, mIcePwd;
 	std::optional<string> mFingerprint;
 
-	struct Data {
-		string mid;
-		std::optional<uint16_t> sctpPort;
-		std::optional<size_t> maxMessageSize;
-	};
-
-	Data mData;
-
-	std::map<int, Media> mMedia; // by m-line index
+	// Entries
+	std::vector<std::shared_ptr<Entry>> mEntries;
+	std::shared_ptr<Application> mApplication;
 
 	// Candidates
 	std::vector<Candidate> mCandidates;

+ 2 - 0
include/rtc/peerconnection.hpp

@@ -158,6 +158,8 @@ private:
 	std::unordered_map<string, std::weak_ptr<Track>> mTracks;                   // by mid
 	std::shared_mutex mDataChannelsMutex, mTracksMutex;
 
+	std::unordered_map<unsigned int, string> mMidFromPayloadType; // cache
+
 	std::atomic<State> mState;
 	std::atomic<GatheringState> mGatheringState;
 

+ 3 - 2
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);
 }

+ 325 - 206
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 <algorithm>
 #include <cctype>
 #include <chrono>
+#include <iostream>
 #include <random>
 #include <sstream>
 
+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<string_view, string_view> 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 <typename T> T to_integer(string_view s) {
+	return std::is_signed<T>::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<unsigned int>(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<Media> currentMedia;
+	std::shared_ptr<Entry> 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<string> Description::fingerprint() const { return mFingerprint; }
 
-std::optional<uint16_t> Description::sctpPort() const { return mData.sctpPort; }
-
-std::optional<size_t> 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<Candidate> 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,22 +189,18 @@ 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;
@@ -241,43 +208,31 @@ string Description::generateSdp(string_view eol) const {
 	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;
 
-	if (mFingerprint)
-		sdp << "a=fingerprint:sha-256 " << *mFingerprint << eol;
+	// Entries
+	bool first = true;
+	for (const auto &entry : mEntries) {
+		sdp << entry->generateSdp(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;
+		if (std::exchange(first, false)) {
+			// Candidates
+			for (const auto &candidate : mCandidates)
+				sdp << string(candidate) << eol;
+
+			if (mEnded)
+				sdp << "a=end-of-candidates" << eol;
 		}
 	}
 
-	// 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 {
+string Description::generateApplicationSdp(string_view eol) const {
 	std::ostringstream sdp;
 
 	// Header
@@ -286,16 +241,12 @@ string Description::generateDataSdp(string_view eol) const {
 	sdp << "s=-" << eol;
 	sdp << "t=0 0" << eol;
 
-	// 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;
+	// Application
+	auto app = mApplication ? mApplication : std::make_shared<Application>();
+	sdp << app->generateSdp(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;
@@ -316,17 +267,233 @@ string Description::generateDataSdp(string_view eol) const {
 	return sdp.str();
 }
 
-Description::Media::Media(string mline, Direction dir, string mid) {
+shared_ptr<Description::Entry> Description::createEntry(string mline, string mid, Direction dir) {
+	string type = mline.substr(0, mline.find(' '));
+	if (type == "application") {
+		removeApplication();
+		mApplication = std::make_shared<Application>(std::move(mid));
+		mEntries.emplace_back(mApplication);
+		return mApplication;
+	} else {
+		auto media = std::make_shared<Media>(std::move(mline), std::move(mid), dir);
+		mEntries.emplace_back(media);
+		return media;
+	}
+}
+
+void Description::removeApplication() {
+	if (!mApplication)
+		return;
+
+	auto it = std::find(mEntries.begin(), mEntries.end(), mApplication);
+	if (it != mEntries.end())
+		mEntries.erase(it);
+
+	mApplication.reset();
+}
+
+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<Media>(std::move(media)));
+	return int(mEntries.size()) - 1;
+}
+
+int Description::addMedia(Application application) {
+	removeApplication();
+	mApplication = std::make_shared<Application>(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 *, Description::Application *> 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<Application *>(entry.get());
+		if (!result)
+			throw std::logic_error("Bad type of application in description");
+		return result;
+	} else {
+		auto result = dynamic_cast<Media *>(entry.get());
+		if (!result)
+			throw std::logic_error("Bad type of media in description");
+		return result;
+	}
+}
+
+std::variant<const Description::Media *, const Description::Application *>
+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<Application *>(entry.get());
+		if (!result)
+			throw std::logic_error("Bad type of application in description");
+		return result;
+	} else {
+		auto result = dynamic_cast<Media *>(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));
+}
+
+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) {}
 
-	mMid = mid;
+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<uint16_t>(value);
+		} else if (key == "max-message-size") {
+			mMaxMessageSize = to_integer<size_t>(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");
+}
+
+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();
+}
 
-	setDirection(dir);
+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<int>(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<int>(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<int>(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<int>(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<int>(line);
 	else {
-		this->clockRate = std::stoi(line.substr(0, spl));
+		this->clockRate = to_integer<int>(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);
 }
-

+ 4 - 3
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");
 }
 

+ 89 - 21
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::function<void(GatheringState st
 bool PeerConnection::hasMedia() const {
 	auto local = localDescription();
 	auto remote = remoteDescription();
-	return (local && local->hasMedia()) || (remote && remote->hasMedia());
+	return (local && local->hasAudioOrVideo()) || (remote && remote->hasAudioOrVideo());
 }
 
 std::shared_ptr<Track> PeerConnection::createTrack(Description::Media description) {
@@ -380,7 +383,11 @@ shared_ptr<SctpTransport> 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<SctpTransport>(
 		    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<string> 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<string> {
+				                        return std::nullopt;
+			                        },
+			                        [&](Description::Media *media) -> std::optional<string> {
+				                        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<uint16_t> remoteSctpPort;
-	std::optional<string> remoteDataMid;
 	if (auto remote = remoteDescription()) {
-		remoteDataMid = remote->dataMid();
-	    remoteSctpPort = remote->sctpPort();
-	}
-
-	// Set the same data mid as remote
-	if (remoteDataMid)
-		description.setDataMid(*remoteDataMid);
+		// 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));
+			}
+		}
 
-	// Set the media
-	for (auto it = mTracks.begin(); it != mTracks.end(); ++it)
-		if (auto track = it->second.lock())
-			description.addMedia(track->description());
+		// 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());
+				}
+			}
+		}
+	}
 
-	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));

+ 4 - 4
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; });