Browse Source

Merge pull request #157 from paullouisageneau/finish-track

Finish track API
Paul-Louis Ageneau 4 years ago
parent
commit
b2b7bfe32a
9 changed files with 316 additions and 95 deletions
  1. 5 3
      CMakeLists.txt
  2. 57 54
      examples/media/main.cpp
  3. 1 1
      examples/media/main.html
  4. 3 2
      include/rtc/description.hpp
  5. 32 11
      src/description.cpp
  6. 69 24
      src/peerconnection.cpp
  7. 4 0
      test/connectivity.cpp
  8. 11 0
      test/main.cpp
  9. 134 0
      test/track.cpp

+ 5 - 3
CMakeLists.txt

@@ -7,7 +7,6 @@ project(libdatachannel
 # Options
 option(USE_GNUTLS "Use GnuTLS instead of OpenSSL" OFF)
 option(USE_NICE "Use libnice instead of libjuice" OFF)
-option(USE_SRTP "Enable SRTP for media support" OFF)
 option(NO_WEBSOCKET "Disable WebSocket support" OFF)
 option(NO_EXAMPLES "Disable examples" OFF)
 option(NO_TESTS "Disable tests build" OFF)
@@ -93,6 +92,7 @@ set(TESTS_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/test/main.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/connectivity.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/capi.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/test/track.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/websocket.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/test/benchmark.cpp
 )
@@ -159,8 +159,8 @@ if(WIN32)
 	target_link_libraries(datachannel-static PRIVATE wsock32 ws2_32) # winsock2
 endif()
 
-if(USE_SRTP)
-	find_package(SRTP REQUIRED)
+find_package(SRTP)
+if(SRTP_FOUND)
 	if(NOT TARGET SRTP::SRTP)
 		add_library(SRTP::SRTP UNKNOWN IMPORTED)
 		set_target_properties(SRTP::SRTP PROPERTIES
@@ -168,11 +168,13 @@ if(USE_SRTP)
 			IMPORTED_LINK_INTERFACE_LANGUAGES C
 			IMPORTED_LOCATION ${SRTP_LIBRARIES})
 	endif()
+	message(STATUS "LibSRTP found, compiling with media transport")
 	target_compile_definitions(datachannel PUBLIC RTC_ENABLE_MEDIA=1)
 	target_compile_definitions(datachannel-static PUBLIC RTC_ENABLE_MEDIA=1)
 	target_link_libraries(datachannel PRIVATE SRTP::SRTP)
 	target_link_libraries(datachannel-static PRIVATE SRTP::SRTP)
 else()
+	message(STATUS "LibSRTP NOT found, compiling WITHOUT media transport")
 	target_compile_definitions(datachannel PUBLIC RTC_ENABLE_MEDIA=0)
 	target_compile_definitions(datachannel-static PUBLIC RTC_ENABLE_MEDIA=0)
 endif()

+ 57 - 54
examples/media/main.cpp

@@ -37,58 +37,61 @@ typedef int SOCKET;
 using nlohmann::json;
 
 int main() {
-	rtc::InitLogger(rtc::LogLevel::Debug);
-	auto pc = std::make_shared<rtc::PeerConnection>();
-
-	pc->onStateChange(
-	    [](rtc::PeerConnection::State state) { std::cout << "State: " << state << std::endl; });
-
-	pc->onGatheringStateChange([pc](rtc::PeerConnection::GatheringState state) {
-		std::cout << "Gathering State: " << state << std::endl;
-		if (state == rtc::PeerConnection::GatheringState::Complete) {
-			auto description = pc->localDescription();
-			json message = {{"type", description->typeString()},
-			                {"sdp", std::string(description.value())}};
-			std::cout << message << std::endl;
-		}
-	});
-
-	SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
-	sockaddr_in addr;
-	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
-	addr.sin_port = htons(5000);
-	addr.sin_family = AF_INET;
-
-	rtc::Description::Video media("video", rtc::Description::Direction::RecvOnly);
-	media.addH264Codec(96);
-	media.setBitrate(
-	    3000); // Request 3Mbps (Browsers do not encode more than 2.5MBps from a webcam)
-
-	auto track = pc->createTrack(media);
-	auto dc = pc->createDataChannel("test");
-
-	auto session = std::make_shared<rtc::RtcpSession>();
-	track->setRtcpHandler(session);
-
-	track->onMessage(
-	    [session, sock, addr](rtc::binary message) {
-		    // This is an RTP packet
-		    sendto(sock, reinterpret_cast<const char *>(message.data()), message.size(), 0,
-		           reinterpret_cast<const struct sockaddr *>(&addr), sizeof(addr));
-	    },
-	    nullptr);
-
-	// 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;
-	std::string sdp;
-	std::getline(std::cin, sdp);
-	std::cout << "Got answer" << sdp << std::endl;
-	json j = json::parse(sdp);
-	rtc::Description answer(j["sdp"].get<std::string>(), j["type"].get<std::string>());
-	pc->setRemoteDescription(answer);
-	std::cout << "Press any key to exit." << std::endl;
-	std::cin >> sdp;
+	try {
+		rtc::InitLogger(rtc::LogLevel::Debug);
+		auto pc = std::make_shared<rtc::PeerConnection>();
+
+		pc->onStateChange(
+		    [](rtc::PeerConnection::State state) { std::cout << "State: " << state << std::endl; });
+
+		pc->onGatheringStateChange([pc](rtc::PeerConnection::GatheringState state) {
+			std::cout << "Gathering State: " << state << std::endl;
+			if (state == rtc::PeerConnection::GatheringState::Complete) {
+				auto description = pc->localDescription();
+				json message = {{"type", description->typeString()},
+				                {"sdp", std::string(description.value())}};
+				std::cout << message << std::endl;
+			}
+		});
+
+		SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
+		sockaddr_in addr;
+		addr.sin_addr.s_addr = inet_addr("127.0.0.1");
+		addr.sin_port = htons(5000);
+		addr.sin_family = AF_INET;
+
+		rtc::Description::Video media("video", rtc::Description::Direction::RecvOnly);
+		media.addH264Codec(96);
+		media.setBitrate(
+		    3000); // Request 3Mbps (Browsers do not encode more than 2.5MBps from a webcam)
+
+		auto track = pc->createTrack(media);
+
+		auto session = std::make_shared<rtc::RtcpSession>();
+		track->setRtcpHandler(session);
+
+		track->onMessage(
+		    [session, sock, addr](rtc::binary message) {
+			    // This is an RTP packet
+			    sendto(sock, reinterpret_cast<const char *>(message.data()), message.size(), 0,
+			           reinterpret_cast<const struct sockaddr *>(&addr), sizeof(addr));
+		    },
+		    nullptr);
+
+		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;
+		std::string sdp;
+		std::getline(std::cin, sdp);
+		std::cout << "Got answer" << sdp << std::endl;
+		json j = json::parse(sdp);
+		rtc::Description answer(j["sdp"].get<std::string>(), j["type"].get<std::string>());
+		pc->setRemoteDescription(answer);
+		std::cout << "Press any key to exit." << std::endl;
+		std::cin >> sdp;
+
+	} catch (const std::exception &e) {
+		std::cerr << "Error: " << e.what() << std::endl;
+	}
 }

+ 1 - 1
examples/media/main.html

@@ -2,7 +2,7 @@
 <html lang="en">
 <head>
     <meta charset="UTF-8">
-    <title>Title</title>
+    <title>libdatachannel media example</title>
 </head>
 <body>
 

+ 3 - 2
include/rtc/description.hpp

@@ -75,7 +75,7 @@ public:
 		virtual string generateSdp(string_view eol) const;
 
 	protected:
-		Entry(string mline, string mid = "", Direction dir = Direction::Unknown);
+		Entry(const string &mline, string mid, Direction dir = Direction::Unknown);
 
 		std::vector<string> mAttributes;
 
@@ -113,7 +113,8 @@ public:
 	// Media (non-data)
 	class Media : public Entry {
 	public:
-		Media(string mline, string mid = "media", Direction dir = Direction::SendOnly);
+		Media(const string &sdp);
+		Media(const string &mline, string mid, Direction dir = Direction::SendOnly);
 		Media(const Media &other) = default;
 		Media(Media &&other) = default;
 		virtual ~Media() = default;

+ 32 - 11
src/description.cpp

@@ -20,6 +20,7 @@
 #include "description.hpp"
 
 #include <algorithm>
+#include <array>
 #include <cctype>
 #include <chrono>
 #include <iostream>
@@ -365,13 +366,14 @@ Description::media(int index) const {
 
 int Description::mediaCount() const { return int(mEntries.size()); }
 
-Description::Entry::Entry(string mline, string mid, Direction dir)
+Description::Entry::Entry(const string &mline, string mid, Direction dir)
     : mMid(std::move(mid)), mDirection(dir) {
-	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 + 1));
+
+	unsigned int port;
+	std::istringstream ss(mline);
+	ss >> mType;
+	ss >> port; // ignored
+	ss >> mDescription;
 }
 
 void Description::Entry::setDirection(Direction dir) { mDirection = dir; }
@@ -473,10 +475,29 @@ void Description::Application::parseSdpLine(string_view line) {
 	}
 }
 
-Description::Media::Media(string mline, string mid, Direction dir)
-    : Entry(std::move(mline), std::move(mid), dir) {
-	mAttributes.emplace_back("rtcp-mux");
-	mAttributes.emplace_back("rtcp-mux-only");
+Description::Media::Media(const string &sdp) : Entry(sdp, "", Direction::Unknown) {
+	std::istringstream ss(sdp);
+	string line;
+	while (std::getline(ss, line) || !line.empty()) {
+		trim_end(line);
+		parseSdpLine(line);
+	}
+
+	if (mid().empty())
+		throw std::invalid_argument("Missing mid in media SDP");
+
+	const std::array<string, 2> attributes = {"rtcp-mux", "rtcp-mux-only"};
+	for (auto attr : attributes)
+		if (std::find(mAttributes.begin(), mAttributes.end(), attr) != mAttributes.end())
+			mAttributes.emplace_back(attr);
+}
+
+Description::Media::Media(const string &mline, string mid, Direction dir)
+    : Entry(mline, std::move(mid), dir) {
+
+	const std::array<string, 2> attributes = {"rtcp-mux", "rtcp-mux-only"};
+	for (auto attr : attributes)
+		mAttributes.emplace_back(attr);
 }
 
 string Description::Media::description() const {
@@ -512,7 +533,7 @@ Description::Media::RTPMap &Description::Media::getFormat(int fmt) {
 	if (it != mRtpMap.end())
 		return it->second;
 
-	throw std::invalid_argument("mLineIndex is out of bounds");
+	throw std::invalid_argument("m-line index is out of bounds");
 }
 
 Description::Media::RTPMap &Description::Media::getFormat(const string &fmt) {

+ 69 - 24
src/peerconnection.cpp

@@ -30,6 +30,7 @@
 #include "dtlssrtptransport.hpp"
 #endif
 
+#include <iomanip>
 #include <thread>
 
 namespace rtc {
@@ -100,10 +101,23 @@ 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");
+		throw std::invalid_argument("Remote description has no media line");
+
+	int activeMediaCount = 0;
+	for (int i = 0; i < description.mediaCount(); ++i)
+		std::visit( // reciprocate each media
+		    rtc::overloaded{[&](Description::Application *) { ++activeMediaCount; },
+		                    [&](Description::Media *media) {
+			                    if (media->direction() != Description::Direction::Inactive)
+				                    ++activeMediaCount;
+		                    }},
+		    description.media(i));
+
+	if (activeMediaCount == 0)
+		throw std::invalid_argument("Remote description has no active media");
 
 	if (!description.fingerprint())
-		throw std::runtime_error("Remote description is incomplete");
+		throw std::invalid_argument("Remote description has no fingerprint");
 
 	description.hintType(localDescription() ? Description::Type::Answer : Description::Type::Offer);
 	auto type = description.type();
@@ -146,7 +160,7 @@ void PeerConnection::setRemoteDescription(Description description) {
 
 	for (const auto &candidate : remoteCandidates)
 		addRemoteCandidate(candidate);
-}
+	}
 
 void PeerConnection::addRemoteCandidate(Candidate candidate) {
 	PLOG_VERBOSE << "Adding remote candidate: " << string(candidate);
@@ -185,6 +199,11 @@ std::optional<string> PeerConnection::remoteAddress() const {
 
 shared_ptr<DataChannel> PeerConnection::createDataChannel(string label, string protocol,
                                                           Reliability reliability) {
+	if (auto local = localDescription(); local && !local->hasApplication()) {
+		PLOG_ERROR << "The PeerConnection was negociated without DataChannel support.";
+		throw std::runtime_error("No DataChannel support on the PeerConnection");
+	}
+
 	// RFC 5763: The answerer MUST use either a setup attribute value of setup:active or
 	// setup:passive. [...] Thus, setup:active is RECOMMENDED.
 	// See https://tools.ietf.org/html/rfc5763#section-5
@@ -244,6 +263,11 @@ std::shared_ptr<Track> PeerConnection::createTrack(Description::Media descriptio
 		if (auto track = it->second.lock())
 			return track;
 
+#if !RTC_ENABLE_MEDIA
+	if (mTracks.empty()) {
+		PLOG_WARNING << "Tracks will be inative (not compiled with SRTP support)";
+	}
+#endif
 	auto track = std::make_shared<Track>(std::move(description));
 	mTracks.emplace(std::make_pair(track->mid(), track));
 	return track;
@@ -331,9 +355,10 @@ shared_ptr<DtlsTransport> PeerConnection::initDtlsTransport() {
 
 			switch (state) {
 			case DtlsTransport::State::Connected:
-				if (auto local = localDescription())
-					if (local->hasApplication())
-						initSctpTransport();
+				if (auto local = localDescription(); local && local->hasApplication())
+					initSctpTransport();
+				else
+					changeState(State::Connected);
 
 				openTracks();
 				break;
@@ -410,13 +435,7 @@ shared_ptr<SctpTransport> PeerConnection::initSctpTransport() {
 			    case SctpTransport::State::Failed:
 				    LOG_WARNING << "SCTP transport failed";
 				    remoteCloseDataChannels();
-#if RTC_ENABLE_MEDIA
-				    // Ignore SCTP failure if media is present
-				    if (!hasMedia())
-					    changeState(State::Failed);
-#else
 				    changeState(State::Failed);
-#endif
 				    break;
 			    case SctpTransport::State::Disconnected:
 				    remoteCloseDataChannels();
@@ -652,6 +671,11 @@ void PeerConnection::remoteCloseDataChannels() {
 
 void PeerConnection::incomingTrack(Description::Media description) {
 	std::unique_lock lock(mTracksMutex); // we are going to emplace
+#if !RTC_ENABLE_MEDIA
+	if (mTracks.empty()) {
+		PLOG_WARNING << "Tracks will be inative (not compiled with SRTP support)";
+	}
+#endif
 	if (mTracks.find(description.mid()) == mTracks.end()) {
 		auto track = std::make_shared<Track>(std::move(description));
 		mTracks.emplace(std::make_pair(track->mid(), track));
@@ -676,31 +700,40 @@ void PeerConnection::openTracks() {
 
 
 void PeerConnection::processLocalDescription(Description description) {
+	int activeMediaCount = 0;
+
 	if (auto remote = remoteDescription()) {
 		// 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);
+				        ++activeMediaCount;
+
+				        PLOG_DEBUG << "Reciprocating application in local description, mid=\""
+				                   << reciprocated.mid() << "\"";
+
 				        description.addMedia(std::move(reciprocated));
 			        },
 			        [&](Description::Media *media) {
-				        PLOG_DEBUG << "Reciprocating media in local description, mid=\""
-				                   << media->mid() << "\"";
-
 				        auto reciprocated = media->reciprocate();
 #if RTC_ENABLE_MEDIA
 				        if (reciprocated.direction() != Description::Direction::Inactive)
-					        incomingTrack(reciprocated);
+					        ++activeMediaCount;
 #else
 				        // No media support, mark as inactive
 				        reciprocated.setDirection(Description::Direction::Inactive);
 #endif
+				        incomingTrack(reciprocated);
+
+				        PLOG_DEBUG
+				            << "Reciprocating media in local description, mid=\""
+				            << reciprocated.mid() << "\", active=" << std::boolalpha
+				            << (reciprocated.direction() != Description::Direction::Inactive);
+
 				        description.addMedia(std::move(reciprocated));
 			        },
 			    },
@@ -710,11 +743,14 @@ void PeerConnection::processLocalDescription(Description description) {
 		{
 			std::shared_lock lock(mDataChannelsMutex);
 			if (!mDataChannels.empty()) {
-				const string mid = "data";
-				PLOG_DEBUG << "Adding application to local description, mid=\"" << mid << "\"";
-				Description::Application app;
+				Description::Application app("data");
 				app.setSctpPort(DEFAULT_SCTP_PORT);
 				app.setMaxMessageSize(LOCAL_MAX_MESSAGE_SIZE);
+				++activeMediaCount;
+
+				PLOG_DEBUG << "Adding application to local description, mid=\"" << app.mid()
+				           << "\"";
+
 				description.addMedia(std::move(app));
 			}
 		}
@@ -724,19 +760,28 @@ void PeerConnection::processLocalDescription(Description description) {
 			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()
-					           << "\"";
 					auto media = track->description();
-#if !RTC_ENABLE_MEDIA
+#if RTC_ENABLE_MEDIA
+					if (media.direction() != Description::Direction::Inactive)
+						++activeMediaCount;
+#else
 					// No media support, mark as inactive
 					media.setDirection(Description::Direction::Inactive);
 #endif
+					PLOG_DEBUG << "Adding media to local description, mid=\"" << media.mid()
+					           << "\", active=" << std::boolalpha
+					           << (media.direction() != Description::Direction::Inactive);
+
 					description.addMedia(std::move(media));
 				}
 			}
 		}
 	}
 
+	// There must be at least one active media to negociate
+	if (activeMediaCount == 0)
+		throw std::runtime_error("Nothing to negociate");
+
 	// Set local fingerprint (wait for certificate if necessary)
 	description.setFingerprint(mCertificate.get()->fingerprint());
 

+ 4 - 0
test/connectivity.cpp

@@ -94,6 +94,10 @@ void test_connectivity() {
 	shared_ptr<DataChannel> dc2;
 	pc2->onDataChannel([&dc2](shared_ptr<DataChannel> dc) {
 		cout << "DataChannel 2: Received with label \"" << dc->label() << "\"" << endl;
+		if (dc->label() != "test") {
+			cerr << "Wrong DataChannel label" << endl;
+			return;
+		}
 
 		dc->onMessage([](variant<binary, string> message) {
 			if (holds_alternative<string>(message)) {

+ 11 - 0
test/main.cpp

@@ -25,6 +25,7 @@ using namespace chrono_literals;
 
 void test_connectivity();
 void test_capi();
+void test_track();
 void test_websocket();
 size_t benchmark(chrono::milliseconds duration);
 
@@ -56,6 +57,16 @@ int main(int argc, char **argv) {
 		cerr << "WebRTC C API test failed: " << e.what() << endl;
 		return -1;
 	}
+#if RTC_ENABLE_MEDIA
+	try {
+		cout << endl << "*** Running WebRTC Track test..." << endl;
+		test_track();
+		cout << "*** Finished WebRTC Track test" << endl;
+	} catch (const exception &e) {
+		cerr << "WebRTC Track test failed: " << e.what() << endl;
+		return -1;
+	}
+#endif
 #if RTC_ENABLE_WEBSOCKET
 	try {
 		cout << endl << "*** Running WebSocket test..." << endl;

+ 134 - 0
test/track.cpp

@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2019 Paul-Louis Ageneau
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "rtc/rtc.hpp"
+
+#include <atomic>
+#include <chrono>
+#include <iostream>
+#include <memory>
+#include <thread>
+
+using namespace rtc;
+using namespace std;
+
+template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
+
+void test_track() {
+	InitLogger(LogLevel::Debug);
+
+	Configuration config1;
+	// STUN server example
+	// config1.iceServers.emplace_back("stun:stun.l.google.com:19302");
+
+	auto pc1 = std::make_shared<PeerConnection>(config1);
+
+	Configuration config2;
+	// STUN server example
+	// config2.iceServers.emplace_back("stun:stun.l.google.com:19302");
+	// Port range example
+	config2.portRangeBegin = 5000;
+	config2.portRangeEnd = 6000;
+
+	auto pc2 = std::make_shared<PeerConnection>(config2);
+
+	pc1->onLocalDescription([wpc2 = make_weak_ptr(pc2)](Description sdp) {
+		auto pc2 = wpc2.lock();
+		if (!pc2)
+			return;
+		cout << "Description 1: " << sdp << endl;
+		pc2->setRemoteDescription(string(sdp));
+	});
+
+	pc1->onLocalCandidate([wpc2 = make_weak_ptr(pc2)](Candidate candidate) {
+		auto pc2 = wpc2.lock();
+		if (!pc2)
+			return;
+		cout << "Candidate 1: " << candidate << endl;
+		pc2->addRemoteCandidate(string(candidate));
+	});
+
+	pc1->onStateChange([](PeerConnection::State state) { cout << "State 1: " << state << endl; });
+
+	pc1->onGatheringStateChange([](PeerConnection::GatheringState state) {
+		cout << "Gathering state 1: " << state << endl;
+	});
+
+	pc2->onLocalDescription([wpc1 = make_weak_ptr(pc1)](Description sdp) {
+		auto pc1 = wpc1.lock();
+		if (!pc1)
+			return;
+		cout << "Description 2: " << sdp << endl;
+		pc1->setRemoteDescription(string(sdp));
+	});
+
+	pc2->onLocalCandidate([wpc1 = make_weak_ptr(pc1)](Candidate candidate) {
+		auto pc1 = wpc1.lock();
+		if (!pc1)
+			return;
+		cout << "Candidate 2: " << candidate << endl;
+		pc1->addRemoteCandidate(string(candidate));
+	});
+
+	pc2->onStateChange([](PeerConnection::State state) { cout << "State 2: " << state << endl; });
+
+	pc2->onGatheringStateChange([](PeerConnection::GatheringState state) {
+		cout << "Gathering state 2: " << state << endl;
+	});
+
+	shared_ptr<Track> t2;
+	pc2->onTrack([&t2](shared_ptr<Track> t) {
+		cout << "Track 2: Received with mid \"" << t->mid() << "\"" << endl;
+		if (t->mid() != "test") {
+			cerr << "Wrong track mid" << endl;
+			return;
+		}
+
+		std::atomic_store(&t2, t);
+	});
+
+	auto t1 = pc1->createTrack(Description::Video("test"));
+
+	pc1->setLocalDescription();
+
+	int attempts = 10;
+	shared_ptr<Track> at2;
+	while ((!(at2 = std::atomic_load(&t2)) || !at2->isOpen() || !t1->isOpen()) && attempts--)
+		this_thread::sleep_for(1s);
+
+	if (pc1->state() != PeerConnection::State::Connected &&
+	    pc2->state() != PeerConnection::State::Connected)
+		throw runtime_error("PeerConnection is not connected");
+
+	if (!at2 || !at2->isOpen() || !t1->isOpen())
+		throw runtime_error("Track is not open");
+
+	// TODO: Test sending RTP packets in track
+
+	// Delay close of peer 2 to check closing works properly
+	pc1->close();
+	this_thread::sleep_for(1s);
+	pc2->close();
+	this_thread::sleep_for(1s);
+
+	// You may call rtc::Cleanup() when finished to free static resources
+	rtc::Cleanup();
+	this_thread::sleep_for(2s);
+
+	cout << "Success" << endl;
+}