Browse Source

Merge branch 'paullouisageneau:master' into find-ntp

petersomers 1 month ago
parent
commit
2512614e83

+ 2 - 1
.gitignore

@@ -12,4 +12,5 @@ compile_commands.json
 /tests
 .DS_Store
 .idea
-
+# clangd cache
+.cache

+ 12 - 3
CMakeLists.txt

@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.13)
 project(libdatachannel
-	VERSION 0.23.1
+	VERSION 0.23.2
 	LANGUAGES CXX)
 set(PROJECT_DESCRIPTION "C/C++ WebRTC network library featuring Data Channels, Media Transport, and WebSockets")
 
@@ -226,6 +226,10 @@ set(TESTS_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/test/benchmark.cpp
 )
 
+set(TESTS_HEADERS 
+	${CMAKE_CURRENT_SOURCE_DIR}/test/test.hpp
+)
+
 set(TESTS_UWP_RESOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/test/uwp/tests/Logo.png
 	${CMAKE_CURRENT_SOURCE_DIR}/test/uwp/tests/package.appxManifest
@@ -511,6 +515,11 @@ install(TARGETS datachannel EXPORT LibDataChannelTargets
 	ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
 )
 
+if(MSVC AND BUILD_SHARED_LIBS)
+    install(FILES $<TARGET_PDB_FILE:datachannel>
+        DESTINATION ${CMAKE_INSTALL_BINDIR} OPTIONAL)
+endif()
+
 install(FILES ${LIBDATACHANNEL_HEADERS}
 	DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/rtc
 )
@@ -546,9 +555,9 @@ install(FILES
 if(NOT NO_TESTS)
 	if(CMAKE_SYSTEM_NAME STREQUAL "WindowsStore")
 		# Add resource files needed for UWP apps.
-		add_executable(datachannel-tests ${TESTS_SOURCES} ${TESTS_UWP_RESOURCES})
+		add_executable(datachannel-tests ${TESTS_SOURCES} ${TESTS_HEADERS} ${TESTS_UWP_RESOURCES})
 	else()
-		add_executable(datachannel-tests ${TESTS_SOURCES})
+		add_executable(datachannel-tests ${TESTS_SOURCES} ${TESTS_HEADERS})
 	endif()
 
 	set_target_properties(datachannel-tests PROPERTIES

+ 2 - 2
Jamfile

@@ -118,7 +118,7 @@ actions make_libusrsctp_msvc
     cd $(CWD)/deps/usrsctp
     mkdir $(BUILD_DIR)
     cd $(BUILD_DIR)
-    cmake -G "Visual Studio 16 2019" -Dsctp_werror=0 -Dsctp_build_shared_lib=0 -Dsctp_build_programs=0 -Dsctp_inet=0 -Dsctp_inet6=0 ..
+    cmake -G "Visual Studio 17 2022" -Dsctp_werror=0 -Dsctp_build_shared_lib=0 -Dsctp_build_programs=0 -Dsctp_inet=0 -Dsctp_inet6=0 ..
     msbuild usrsctplib.sln /property:Configuration=$(VARIANT)
     cd %OLDD%
     cp $(CWD)/deps/usrsctp/$(BUILD_DIR)/usrsctplib/Release/usrsctp.lib $(<)
@@ -182,7 +182,7 @@ actions make_libjuice_msvc
     cd $(CWD)/deps/libjuice
     mkdir $(BUILD_DIR)
     cd $(BUILD_DIR)
-    cmake -G "Visual Studio 16 2019" $(CMAKEOPTS) ..
+    cmake -G "Visual Studio 17 2022" $(CMAKEOPTS) ..
     msbuild libjuice.sln /property:Configuration=$(VARIANT)
     cd %OLDD%
     cp $(CWD)/deps/libjuice/$(BUILD_DIR)/Release/juice-static.lib $(<)

+ 1 - 1
deps/libjuice

@@ -1 +1 @@
-Subproject commit b509b6c1f90e4a1d4753e6bde62ffbeb4d10c6bb
+Subproject commit 85efaa9b5e1cb3d4d534fc85d69cc9f7b76a66d7

+ 3 - 3
examples/signaling-server-python/signaling-server.py

@@ -22,10 +22,10 @@ logger.addHandler(logging.StreamHandler(sys.stdout))
 clients = {}
 
 
-async def handle_websocket(websocket, path):
+async def handle_websocket(websocket):
     client_id = None
     try:
-        splitted = path.split('/')
+        splitted = websocket.request.path.split('/')
         splitted.pop(0)
         client_id = splitted.pop(0)
         print('Client {} connected'.format(client_id))
@@ -75,4 +75,4 @@ async def main():
 
 
 if __name__ == '__main__':
-    asyncio.run(main())
+    asyncio.run(main())

+ 8 - 0
include/rtc/rtc.h

@@ -352,6 +352,14 @@ typedef struct {
 	uint8_t playoutDelayId;
 	uint16_t playoutDelayMin;
 	uint16_t playoutDelayMax;
+
+	uint8_t colorSpaceId;
+	uint8_t colorChromaSitingHorz;
+	uint8_t colorChromaSitingVert;
+	uint8_t colorRange;
+	uint8_t colorPrimaries;
+	uint8_t colorTransfer;
+	uint8_t colorMatrix;
 } rtcPacketizerInit;
 
 // Deprecated, do not use

+ 9 - 0
include/rtc/rtppacketizationconfig.hpp

@@ -74,6 +74,15 @@ public:
 	uint16_t playoutDelayMin = 0;
 	uint16_t playoutDelayMax = 0;
 
+	// https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/color-space/
+	uint8_t colorSpaceId = 0;               // the negotiated ID of color space header extension
+	uint8_t colorChromaSitingHorz = 0;      // unspecified
+	uint8_t colorChromaSitingVert = 0;      // unspecified
+	uint8_t colorRange = 2;                 // full range
+	uint8_t colorPrimaries = 1;             // BT.709-6
+	uint8_t colorTransfer = 1;              // BT.709-6
+	uint8_t colorMatrix = 1;                // BT.709-6
+
 	/// Construct RTP configuration used in packetization process
 	/// @param ssrc SSRC of source
 	/// @param cname CNAME of source

+ 2 - 2
include/rtc/version.h

@@ -3,7 +3,7 @@
 
 #define RTC_VERSION_MAJOR 0
 #define RTC_VERSION_MINOR 23
-#define RTC_VERSION_PATCH 1
-#define RTC_VERSION "0.23.1"
+#define RTC_VERSION_PATCH 2
+#define RTC_VERSION "0.23.2"
 
 #endif

+ 2 - 0
pages/content/pages/reference.md

@@ -872,6 +872,7 @@ int rtcCreateWebSocketEx(const char *url, const rtcWsConfiguration *config)
 
 typedef struct {
 	bool disableTlsVerification;
+	const char *proxyServer;
 	const char **protocols;
 	int protocolsCount;
 	int connectionTimeoutMs;
@@ -887,6 +888,7 @@ Arguments:
 - `url`: a null-terminated string representing the fully-qualified URL to open.
 - `config`: a structure with the following parameters:
   - `disableTlsVerification`: if true, don't verify the TLS certificate, else try to verify it if possible
+  - `proxyServer` (optional): address of proxy server as string, only non-authenticated HTTP for now (NULL if unused)
   - `protocols` (optional): an array of pointers on null-terminated protocol names (NULL if unused)
   - `protocolsCount` (optional): number of URLs in the array pointed by `protocols` (0 if unused)
   - `connectionTimeoutMs` (optional): connection timeout in milliseconds (0 if default, < 0 if disabled)

+ 7 - 0
src/capi.cpp

@@ -278,6 +278,13 @@ createRtpPacketizationConfig(const rtcPacketizationHandlerInit *init) {
 	config->playoutDelayId = init->playoutDelayId;
 	config->playoutDelayMin = init->playoutDelayMin;
 	config->playoutDelayMax = init->playoutDelayMax;
+	config->colorSpaceId = init->colorSpaceId;
+	config->colorChromaSitingHorz = init->colorChromaSitingHorz;
+	config->colorChromaSitingVert = init->colorChromaSitingVert;
+	config->colorRange = init->colorRange;
+	config->colorPrimaries = init->colorPrimaries;
+	config->colorTransfer = init->colorTransfer;
+	config->colorMatrix = init->colorMatrix;
 	return config;
 }
 

+ 6 - 4
src/impl/dtlstransport.cpp

@@ -33,10 +33,12 @@ void DtlsTransport::enqueueRecv() {
 	if (mPendingRecvCount > 0)
 		return;
 
-	if (auto shared_this = weak_from_this().lock()) {
-		++mPendingRecvCount;
-		ThreadPool::Instance().enqueue(&DtlsTransport::doRecv, std::move(shared_this));
-	}
+	++mPendingRecvCount;
+
+	ThreadPool::Instance().enqueue([weak_this = weak_from_this()]() {
+		if (auto locked = weak_this.lock())
+			locked->doRecv();
+	});
 }
 
 #if USE_GNUTLS

+ 3 - 0
src/impl/peerconnection.cpp

@@ -1042,6 +1042,9 @@ void PeerConnection::processLocalDescription(Description description) {
 	if (description.mediaCount() == 0)
 		throw std::logic_error("Local description has no media line");
 
+	// Update the SSRC cache
+	updateTrackSsrcCache(description);
+
 	{
 		// Set as local description
 		std::lock_guard lock(mLocalDescriptionMutex);

+ 12 - 10
src/impl/sctptransport.cpp

@@ -82,7 +82,7 @@ private:
 	std::shared_mutex mMutex;
 };
 
-std::unique_ptr<SctpTransport::InstancesSet> SctpTransport::Instances = std::make_unique<InstancesSet>();
+SctpTransport::InstancesSet* SctpTransport::Instances = nullptr;
 
 void SctpTransport::Init() {
 	usrsctp_init(0, SctpTransport::WriteCallback, SctpTransport::DebugCallback);
@@ -94,6 +94,8 @@ void SctpTransport::Init() {
 #ifdef SCTP_DEBUG
 	usrsctp_sysctl_set_sctp_debug_on(SCTP_DEBUG_ALL);
 #endif
+
+	Instances = new InstancesSet;
 }
 
 void SctpTransport::SetSettings(const SctpSettings &s) {
@@ -148,6 +150,9 @@ void SctpTransport::SetSettings(const SctpSettings &s) {
 void SctpTransport::Cleanup() {
 	while (usrsctp_finish())
 		std::this_thread::sleep_for(100ms);
+
+	delete Instances;
+	Instances = nullptr;
 }
 
 SctpTransport::SctpTransport(shared_ptr<Transport> lower, const Configuration &config, Ports ports,
@@ -729,15 +734,12 @@ void SctpTransport::sendReset(uint16_t streamId) {
 	srs.srs_number_streams = 1;
 	srs.srs_stream_list[0] = streamId;
 
-	mWritten = false;
-	if (usrsctp_setsockopt(mSock, IPPROTO_SCTP, SCTP_RESET_STREAMS, &srs, len) == 0) {
-		std::unique_lock lock(mWriteMutex); // locking before setsockopt might deadlock usrsctp...
-		mWrittenCondition.wait_for(lock, 1000ms,
-		                           [&]() { return mWritten || state() != State::Connected; });
-	} else if (errno == EINVAL) {
-		PLOG_DEBUG << "SCTP stream " << streamId << " already reset";
-	} else {
-		PLOG_WARNING << "SCTP reset stream " << streamId << " failed, errno=" << errno;
+	if (usrsctp_setsockopt(mSock, IPPROTO_SCTP, SCTP_RESET_STREAMS, &srs, len)) {
+		if (errno == EINVAL) {
+			PLOG_DEBUG << "SCTP stream " << streamId << " already reset";
+		} else {
+			PLOG_WARNING << "SCTP reset stream " << streamId << " failed, errno=" << errno;
+		}
 	}
 }
 

+ 1 - 1
src/impl/sctptransport.hpp

@@ -127,7 +127,7 @@ private:
 	static void DebugCallback(const char *format, ...);
 
 	class InstancesSet;
-	static std::unique_ptr<InstancesSet> Instances;
+	static InstancesSet* Instances;
 };
 
 } // namespace rtc::impl

+ 1 - 1
src/rtp.cpp

@@ -588,7 +588,7 @@ unsigned int RtcpRemb::getNumSSRC() { return ntohl(_bitrate) >> 24u; }
 unsigned int RtcpRemb::getBitrate() {
 	uint32_t br = ntohl(_bitrate);
 	uint8_t exp = (br << 8u) >> 26u;
-	return (br & 0x3FFFF) * static_cast<unsigned int>(pow(exp, 2));
+	return (br & 0x3FFFF) * static_cast<unsigned int>(pow(2, exp));
 }
 
 unsigned int RtcpPli::Size() { return sizeof(RtcpFbHeader); }

+ 16 - 0
src/rtppacketizer.cpp

@@ -57,10 +57,14 @@ message_ptr RtpPacketizer::packetize(const binary &payload, bool mark) {
 		rtpExtHeaderSize += headerSize + 1;
 
 	const bool setPlayoutDelay = rtpConfig->playoutDelayId > 0;
+	const bool setColorSpace = rtpConfig->colorSpaceId > 0;
 
 	if (setPlayoutDelay)
 		rtpExtHeaderSize += headerSize + 3;
 
+	if (setColorSpace)
+		rtpExtHeaderSize += headerSize + 4;
+	
 	if (rtpConfig->mid.has_value())
 		rtpExtHeaderSize += headerSize + rtpConfig->mid->length();
 
@@ -74,6 +78,8 @@ message_ptr RtpPacketizer::packetize(const binary &payload, bool mark) {
 	if (rtpExtHeaderSize != 0)
 		rtpExtHeaderSize += 4;
 
+	// Align the size to the multiple of 4 bytes
+	// according to RFC 3550, sec. 5.3.1.
 	rtpExtHeaderSize = (rtpExtHeaderSize + 3) & ~3;
 
 	auto message = make_message(RtpHeaderSize + rtpExtHeaderSize + payload.size());
@@ -137,6 +143,16 @@ message_ptr RtpPacketizer::packetize(const binary &payload, bool mark) {
 			offset += extHeader->writeHeader(
 			    twoByteHeader, offset, rtpConfig->playoutDelayId, data, 3);
 		}
+
+		if (setColorSpace) {
+			uint8_t range_chr = (rtpConfig->colorRange << 4) + (rtpConfig->colorChromaSitingHorz << 2) + rtpConfig->colorChromaSitingVert;
+
+			byte data[] = {byte(rtpConfig->colorPrimaries), byte(rtpConfig->colorTransfer),
+			               byte(rtpConfig->colorMatrix), byte(range_chr)};
+
+			offset += extHeader->writeHeader(
+			    twoByteHeader, offset, rtpConfig->playoutDelayId, data, 4);
+		}
 	}
 
 	rtp->preparePacket();

+ 4 - 2
test/capi_connectivity.cpp

@@ -6,6 +6,7 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
+#include "test.hpp"
 #include <rtc/rtc.h>
 
 #include <cstdio>
@@ -392,7 +393,8 @@ error:
 
 #include <stdexcept>
 
-void test_capi_connectivity() {
+TestResult test_capi_connectivity() {
 	if (test_capi_connectivity_main())
-		throw std::runtime_error("Connection failed");
+		return TestResult(false, "Connection failed");
+	return TestResult(true);
 }

+ 4 - 2
test/capi_track.cpp

@@ -6,6 +6,7 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
+#include "test.hpp"
 #include <rtc/rtc.h>
 
 #include <cstdio>
@@ -238,7 +239,8 @@ error:
 
 #include <stdexcept>
 
-void test_capi_track() {
+TestResult test_capi_track() {
 	if (test_capi_track_main())
-		throw std::runtime_error("Connection failed");
+		return TestResult(false, "Connection failed");
+	return TestResult(true);
 }

+ 4 - 2
test/capi_websocketserver.cpp

@@ -6,6 +6,7 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
+#include "test.hpp"
 #include <rtc/rtc.h>
 
 #if RTC_ENABLE_WEBSOCKET
@@ -164,9 +165,10 @@ error:
 
 #include <stdexcept>
 
-void test_capi_websocketserver() {
+TestResult test_capi_websocketserver() {
 	if (test_capi_websocketserver_main())
-		throw std::runtime_error("WebSocketServer test failed");
+		return TestResult(false, "WebSocketServer test failed");
+	return TestResult(true);
 }
 
 #endif

+ 32 - 19
test/connectivity.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "rtc/rtc.hpp"
+#include "test.hpp"
 
 #include <atomic>
 #include <chrono>
@@ -21,7 +22,13 @@ using namespace std;
 
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
-void test_connectivity(bool signal_wrong_fingerprint) {
+TestResult test_connectivity(bool);
+
+TestResult test_connectivity() { return test_connectivity(false); }
+
+TestResult test_connectivity_fail_on_wrong_fingerprint() { return test_connectivity(true); }
+
+TestResult test_connectivity(bool signal_wrong_fingerprint) {
 	InitLogger(LogLevel::Debug);
 
 	Configuration config1;
@@ -128,7 +135,7 @@ void test_connectivity(bool signal_wrong_fingerprint) {
 	auto dc1 = pc1.createDataChannel("test");
 
 	if (dc1->id().has_value())
-		throw std::runtime_error("DataChannel stream id assigned before connection");
+		return TestResult(false, "DataChannel stream id assigned before connection");
 
 	dc1->onOpen([wdc1 = make_weak_ptr(dc1)]() {
 		if (auto dc1 = wdc1.lock()) {
@@ -152,30 +159,35 @@ void test_connectivity(bool signal_wrong_fingerprint) {
 		this_thread::sleep_for(1s);
 
 	if (pc1.state() != PeerConnection::State::Connected ||
-	    pc2.state() != PeerConnection::State::Connected)
-		throw runtime_error("PeerConnection is not connected");
+	    pc2.state() != PeerConnection::State::Connected) {
+		if (signal_wrong_fingerprint) {
+			return TestResult(true);
+		} else {
+			return TestResult(false, "PeerConnection is not connected");
+		}
+	}
 
 	if ((pc1.iceState() != PeerConnection::IceState::Connected &&
 	     pc1.iceState() != PeerConnection::IceState::Completed) ||
 	    (pc2.iceState() != PeerConnection::IceState::Connected &&
 	     pc2.iceState() != PeerConnection::IceState::Completed))
-		throw runtime_error("ICE is not connected");
+		return TestResult(false, "ICE is not connected");
 
 	if (!adc2 || !adc2->isOpen() || !dc1->isOpen())
-		throw runtime_error("DataChannel is not open");
+		return TestResult(false, "DataChannel is not open");
 
 	if (adc2->label() != "test")
-		throw runtime_error("Wrong DataChannel label");
+		return TestResult(false, "Wrong DataChannel label");
 
 	if (dc1->maxMessageSize() != CUSTOM_MAX_MESSAGE_SIZE ||
 	    dc2->maxMessageSize() != CUSTOM_MAX_MESSAGE_SIZE)
-		throw runtime_error("DataChannel max message size is incorrect");
+		return TestResult(false, "DataChannel max message size is incorrect");
 
 	if (!dc1->id().has_value())
-		throw runtime_error("DataChannel stream id is not assigned");
+		return TestResult(false, "DataChannel stream id is not assigned");
 
 	if (dc1->id().value() != adc2->id().value())
-		throw runtime_error("DataChannel stream ids do not match");
+		return TestResult(false, "DataChannel stream ids do not match");
 
 	if (auto addr = pc1.localAddress())
 		cout << "Local address 1:  " << *addr << endl;
@@ -244,16 +256,16 @@ void test_connectivity(bool signal_wrong_fingerprint) {
 		this_thread::sleep_for(1s);
 
 	if (!asecond2 || !asecond2->isOpen() || !second1->isOpen())
-		throw runtime_error("Second DataChannel is not open");
+		return TestResult(false, "Second DataChannel is not open");
 
 	if (asecond2->label() != "second")
-		throw runtime_error("Wrong second DataChannel label");
+		return TestResult(false, "Wrong second DataChannel label");
 
 	if (!second2->id().has_value() || !asecond2->id().has_value())
-		throw runtime_error("Second DataChannel stream id is not assigned");
+		return TestResult(false, "Second DataChannel stream id is not assigned");
 
 	if (second2->id().value() != asecond2->id().value())
-		throw runtime_error("Second DataChannel stream ids do not match");
+		return TestResult(false, "Second DataChannel stream ids do not match");
 
 	// Delay close of peer 2 to check closing works properly
 	pc1.close();
@@ -261,7 +273,7 @@ void test_connectivity(bool signal_wrong_fingerprint) {
 	pc2.close();
 	this_thread::sleep_for(1s);
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }
 
 const char* key_pem =
@@ -284,7 +296,7 @@ const char* cert_pem =
 "Ma9ayzQy\n"
 "-----END CERTIFICATE-----\n";
 
-void test_pem() {
+TestResult test_pem() {
 	InitLogger(LogLevel::Debug);
 
 	Configuration config1;
@@ -310,8 +322,9 @@ void test_pem() {
 
 	cout << "Fingerprint: " << f << endl;
 
-	if (f != "07:E5:6F:2A:1A:0C:2C:32:0E:C1:C3:9C:34:5A:78:4E:A5:8B:32:05:D1:57:D6:F4:E7:02:41:12:E6:01:C6:8F")
-		throw runtime_error("The fingerprint of the specified certificate do not match");
+	if (f != "07:E5:6F:2A:1A:0C:2C:32:0E:C1:C3:9C:34:5A:78:4E:A5:8B:32:05:D1:57:D6:F4:E7:02:41:12:"
+	         "E6:01:C6:8F")
+		return TestResult(false, "The fingerprint of the specified certificate do not match");
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }

+ 85 - 138
test/main.cpp

@@ -10,22 +10,29 @@
 #include <iostream>
 #include <thread>
 
+#include "test.hpp"
 #include <rtc/rtc.hpp>
 
 using namespace std;
 using namespace chrono_literals;
 
-void test_connectivity(bool signal_wrong_fingerprint);
-void test_pem();
-void test_negotiated();
-void test_reliability();
-void test_turn_connectivity();
-void test_track();
-void test_capi_connectivity();
-void test_capi_track();
-void test_websocket();
-void test_websocketserver();
-void test_capi_websocketserver();
+using chrono::duration_cast;
+using chrono::milliseconds;
+using chrono::seconds;
+using chrono::steady_clock;
+
+TestResult test_connectivity();
+TestResult test_connectivity_fail_on_wrong_fingerprint();
+TestResult test_pem();
+TestResult test_negotiated();
+TestResult test_reliability();
+TestResult test_turn_connectivity();
+TestResult test_track();
+TestResult test_capi_connectivity();
+TestResult test_capi_track();
+TestResult test_websocket();
+TestResult test_websocketserver();
+TestResult test_capi_websocketserver();
 size_t benchmark(chrono::milliseconds duration);
 
 void test_benchmark() {
@@ -39,149 +46,89 @@ void test_benchmark() {
 		throw runtime_error("Goodput is too low");
 }
 
-int main(int argc, char **argv) {
-	// C++ API tests
+TestResult test_cleanup() {
 	try {
-		cout << endl << "*** Running WebRTC connectivity test..." << endl;
-		test_connectivity(false);
-		cout << "*** Finished WebRTC connectivity test" << endl;
+		// Every created object must have been destroyed, otherwise the wait will block
+		if (rtc::Cleanup().wait_for(10s) == future_status::timeout)
+			return TestResult(false, "timeout");
+		return TestResult(true);
 	} catch (const exception &e) {
-		cerr << "WebRTC connectivity test failed: " << e.what() << endl;
-		return -1;
-	}
-	try {
-		cout << endl << "*** Running WebRTC broken fingerprint test..." << endl;
-		test_connectivity(true);
-		cerr << "WebRTC connectivity test failed to detect broken fingerprint" << endl;
-		return -1;
-	} catch (const exception &) {
+		return TestResult(false, e.what());
 	}
+}
 
+TestResult test_capi_cleanup() {
 	try {
-		cout << endl << "*** Running pem test..." << endl;
-		test_pem();
+		rtcCleanup();
+		return TestResult(true);
 	} catch (const exception &e) {
-		cerr << "pem test failed: " << e.what() << endl;
-		return -1;
+		return TestResult(false, e.what());
 	}
+}
 
-// TODO: Temporarily disabled as the Open Relay TURN server is unreliable
-/*
-	try {
-		cout << endl << "*** Running WebRTC TURN connectivity test..." << endl;
-		test_turn_connectivity();
-		cout << "*** Finished WebRTC TURN connectivity test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebRTC TURN connectivity test failed: " << e.what() << endl;
-		return -1;
-	}
-*/
-	try {
-		cout << endl << "*** Running WebRTC negotiated DataChannel test..." << endl;
-		test_negotiated();
-		cout << "*** Finished WebRTC negotiated DataChannel test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebRTC negotiated DataChannel test failed: " << e.what() << endl;
-		return -1;
-	}
-	try {
-		cout << endl << "*** Running WebRTC reliability mode test..." << endl;
-		test_reliability();
-		cout << "*** Finished WebRTC reliaility mode test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebRTC reliability test failed: " << e.what() << endl;
-		return -1;
-	}
+static const vector<Test> tests = {
+    // C++ API tests
+    Test("WebRTC connectivity", test_connectivity),
+    Test("WebRTC broken fingerprint", test_connectivity_fail_on_wrong_fingerprint),
+    Test("pem", test_pem),
+    // TODO: Temporarily disabled as the Open Relay TURN server is unreliable
+    // new Test("WebRTC TURN connectivity", test_turn_connectivity),
+    Test("WebRTC negotiated DataChannel", test_negotiated),
+    Test("WebRTC reliability mode", test_reliability),
 #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;
-	}
+    Test("WebRTC track", test_track),
 #endif
 #if RTC_ENABLE_WEBSOCKET
-// TODO: Temporarily disabled as the echo service is unreliable
-/*
-	try {
-		cout << endl << "*** Running WebSocket test..." << endl;
-		test_websocket();
-		cout << "*** Finished WebSocket test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebSocket test failed: " << e.what() << endl;
-		return -1;
-	}
-*/
-	try {
-		cout << endl << "*** Running WebSocketServer test..." << endl;
-		test_websocketserver();
-		cout << "*** Finished WebSocketServer test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebSocketServer test failed: " << e.what() << endl;
-		return -1;
-	}
+    // TODO: Temporarily disabled as the echo service is unreliable
+    // new Test("WebSocket", test_websocket),
+    Test("WebSocketServer", test_websocketserver),
 #endif
-	try {
-		// Every created object must have been destroyed, otherwise the wait will block
-		cout << endl << "*** Running cleanup..." << endl;
-		if(rtc::Cleanup().wait_for(10s) == future_status::timeout)
-			throw std::runtime_error("Timeout");
-		cout << "*** Finished cleanup..." << endl;
-	} catch (const exception &e) {
-		cerr << "Cleanup failed: " << e.what() << endl;
-		return -1;
-	}
-
-	// C API tests
-	try {
-		cout << endl << "*** Running WebRTC C API connectivity test..." << endl;
-		test_capi_connectivity();
-		cout << "*** Finished WebRTC C API connectivity test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebRTC C API connectivity test failed: " << e.what() << endl;
-		return -1;
-	}
+    Test("Cleanup", test_cleanup),
+    // C API tests
+    Test("WebRTC C API connectivity", test_capi_connectivity),
 #if RTC_ENABLE_MEDIA
-	try {
-		cout << endl << "*** Running WebRTC C API track test..." << endl;
-		test_capi_track();
-		cout << "*** Finished WebRTC C API track test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebRTC C API track test failed: " << e.what() << endl;
-		return -1;
-	}
+    Test("WebRTC C API track", test_capi_track),
 #endif
 #if RTC_ENABLE_WEBSOCKET
-	try {
-		cout << endl << "*** Running WebSocketServer C API test..." << endl;
-		test_capi_websocketserver();
-		cout << "*** Finished WebSocketServer C API test" << endl;
-	} catch (const exception &e) {
-		cerr << "WebSocketServer C API test failed: " << e.what() << endl;
-		return -1;
-	}
+    Test("WebSocketServer C API", test_capi_websocketserver),
 #endif
-	try {
-		cout << endl << "*** Running C API cleanup..." << endl;
-		rtcCleanup();
-		cout << "*** Finished C API cleanup..." << endl;
-	} catch (const exception &e) {
-		cerr << "C API cleanup failed: " << e.what() << endl;
-		return -1;
-	}
-/*
-	// Benchmark
-	try {
-		cout << endl << "*** Running WebRTC benchmark..." << endl;
-		test_benchmark();
-		cout << "*** Finished WebRTC benchmark" << endl;
-	} catch (const exception &e) {
-		cerr << "WebRTC benchmark failed: " << e.what() << endl;
-		std::this_thread::sleep_for(2s);
-		return -1;
+    Test("C API cleanup", test_capi_cleanup),
+};
+
+int main(int argc, char **argv) {
+	int success_tests = 0;
+	int failed_tests = 0;
+	steady_clock::time_point startTime, endTime;
+
+	startTime = steady_clock::now();
+
+	for (auto test : tests) {
+		auto res = test.run();
+		if (res.success) {
+			success_tests++;
+		} else {
+			failed_tests++;
+		}
 	}
-*/
+
+	endTime = steady_clock::now();
+
+	auto durationMs = duration_cast<milliseconds>(endTime - startTime);
+	auto durationS = duration_cast<seconds>(endTime - startTime);
+	cout << "Finished " << success_tests + failed_tests << " tests in " << durationS.count()
+	     << "s (" << durationMs.count() << " ms). Succeeded: " << success_tests
+	     << ". Failed: " << failed_tests << "." << endl;
+	/*
+	    // Benchmark
+	    try {
+	        cout << endl << "*** Running WebRTC benchmark..." << endl;
+	        test_benchmark();
+	        cout << "*** Finished WebRTC benchmark" << endl;
+	    } catch (const exception &e) {
+	        cerr << "WebRTC benchmark failed: " << e.what() << endl;
+	        std::this_thread::sleep_for(2s);
+	        return -1;
+	    }
+	*/
 	return 0;
 }

+ 6 - 5
test/negotiated.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "rtc/rtc.hpp"
+#include "test.hpp"
 
 #include <atomic>
 #include <chrono>
@@ -17,7 +18,7 @@
 using namespace rtc;
 using namespace std;
 
-void test_negotiated() {
+TestResult test_negotiated() {
 	InitLogger(LogLevel::Debug);
 
 	Configuration config1;
@@ -66,10 +67,10 @@ void test_negotiated() {
 
 	if (pc1.state() != PeerConnection::State::Connected ||
 	    pc2.state() != PeerConnection::State::Connected)
-		throw runtime_error("PeerConnection is not connected");
+		return TestResult(false, "PeerConnection is not connected");
 
 	if (!negotiated1->isOpen() || !negotiated2->isOpen())
-		throw runtime_error("Negotiated DataChannel is not open");
+		return TestResult(false, "Negotiated DataChannel is not open");
 
 	std::atomic<bool> received = false;
 	negotiated2->onMessage([&received](const variant<binary, string> &message) {
@@ -87,7 +88,7 @@ void test_negotiated() {
 		this_thread::sleep_for(1s);
 
 	if (!received)
-		throw runtime_error("Negotiated DataChannel failed");
+		return TestResult(false, "Negotiated DataChannel failed");
 
 	// Delay close of peer 2 to check closing works properly
 	pc1.close();
@@ -95,5 +96,5 @@ void test_negotiated() {
 	pc2.close();
 	this_thread::sleep_for(1s);
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }

+ 6 - 5
test/reliability.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "rtc/rtc.hpp"
+#include "test.hpp"
 
 #include <atomic>
 #include <chrono>
@@ -17,7 +18,7 @@
 using namespace rtc;
 using namespace std;
 
-void test_reliability() {
+TestResult test_reliability() {
 	InitLogger(LogLevel::Debug);
 
 	Configuration config1;
@@ -114,15 +115,15 @@ void test_reliability() {
 
 	if (pc1.state() != PeerConnection::State::Connected ||
 	    pc2.state() != PeerConnection::State::Connected)
-		throw runtime_error("PeerConnection is not connected");
+		return TestResult(false, "PeerConnection is not connected");
 
 	if (failed)
-		throw runtime_error("Incorrect reliability settings");
+		return TestResult(false, "Incorrect reliability settings");
 
 	if (count != 4)
-		throw runtime_error("Some DataChannels are not open");
+		return TestResult(false, "Some DataChannels are not open");
 
 	pc1.close();
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }

+ 40 - 0
test/test.hpp

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2025 Michal Sledz
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+#include <functional>
+#include <iostream>
+
+using namespace std;
+
+class TestResult {
+public:
+	bool success;
+	string err_reason;
+
+	TestResult(bool success, string err_reason = "") : success(success), err_reason(err_reason) {}
+};
+
+class Test {
+public:
+	string name;
+	function<TestResult(void)> f;
+
+	Test(string name, std::function<TestResult(void)> testFunc) : name(name), f(testFunc) {}
+
+	TestResult run() {
+		cout << endl << "*** Running " << name << " test" << endl;
+		TestResult res = this->f();
+		if (res.success) {
+			cout << "*** Finished " << name << " test" << endl;
+		} else {
+			cerr << name << " test failed. Reason: " << res.err_reason << endl;
+		}
+
+		return res;
+	}
+};

+ 49 - 10
test/track.cpp

@@ -7,9 +7,12 @@
  */
 
 #include "rtc/rtc.hpp"
+#include "rtc/rtp.hpp"
+#include "test.hpp"
 
 #include <atomic>
-#include <chrono>
+#include <cstring>
+#include <future>
 #include <iostream>
 #include <memory>
 #include <thread>
@@ -19,7 +22,7 @@ using namespace std;
 
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
-void test_track() {
+TestResult test_track() {
 	InitLogger(LogLevel::Debug);
 
 	Configuration config1;
@@ -71,7 +74,8 @@ void test_track() {
 
 	shared_ptr<Track> t2;
 	string newTrackMid;
-	pc2.onTrack([&t2, &newTrackMid](shared_ptr<Track> t) {
+	std::promise<rtc::binary> recvRtpPromise;
+	pc2.onTrack([&t2, &newTrackMid, &recvRtpPromise](shared_ptr<Track> t) {
 		string mid = t->mid();
 		cout << "Track 2: Received track with mid \"" << mid << "\"" << endl;
 		if (mid != newTrackMid) {
@@ -84,6 +88,13 @@ void test_track() {
 		t->onClosed(
 		    [mid]() { cout << "Track 2: Track with mid \"" << mid << "\" is closed" << endl; });
 
+		t->onMessage(
+		    [&recvRtpPromise](rtc::binary message) {
+			    // This is an RTP packet
+			    recvRtpPromise.set_value(message);
+		    },
+		    nullptr);
+
 		std::atomic_store(&t2, t);
 	});
 
@@ -99,7 +110,7 @@ void test_track() {
 	const auto mediaSdp2 = string(Description::Media(mediaSdp1));
 	if (mediaSdp2 != mediaSdp1) {
 		cout << mediaSdp2 << endl;
-		throw runtime_error("Media description parsing test failed");
+		return TestResult(false, "Media description parsing test failed");
 	}
 
 	auto t1 = pc1.addTrack(media);
@@ -113,10 +124,10 @@ void test_track() {
 
 	if (pc1.state() != PeerConnection::State::Connected ||
 	    pc2.state() != PeerConnection::State::Connected)
-		throw runtime_error("PeerConnection is not connected");
+		return TestResult(false, "PeerConnection is not connected");
 
 	if (!at2 || !at2->isOpen() || !t1->isOpen())
-		throw runtime_error("Track is not open");
+		return TestResult(false, "Track is not open");
 
 	// Test renegotiation
 	newTrackMid = "added";
@@ -138,9 +149,37 @@ void test_track() {
 		this_thread::sleep_for(1s);
 
 	if (!at2 || !at2->isOpen() || !t1->isOpen())
-		throw runtime_error("Renegotiated track is not open");
+		return TestResult(false, "Renegotiated track is not open");
+
+	std::vector<std::byte> payload = {std::byte{0}, std::byte{1}, std::byte{2}, std::byte{3}};
+	std::vector<std::byte> rtpRaw(sizeof(RtpHeader) + payload.size());
+	auto *rtp = reinterpret_cast<RtpHeader *>(rtpRaw.data());
+	rtp->setPayloadType(96);
+	rtp->setSeqNumber(1);
+	rtp->setTimestamp(3000);
+	rtp->setSsrc(2468);
+	rtp->preparePacket();
+	std::memcpy(rtpRaw.data() + sizeof(RtpHeader), payload.data(), payload.size());
+
+	if (!t1->send(rtpRaw.data(), rtpRaw.size())) {
+		throw runtime_error("Couldn't send RTP packet");
+	}
+
+	// wait for an RTP packet to be received
+	auto future = recvRtpPromise.get_future();
+	if (future.wait_for(5s) == std::future_status::timeout) {
+		throw runtime_error("Didn't receive RTP packet on pc2");
+	}
 
-	// TODO: Test sending RTP packets in track
+	auto receivedRtpRaw = future.get();
+	if (receivedRtpRaw.empty()) {
+		throw runtime_error("Didn't receive RTP packet on pc2");
+	}
+
+	if (receivedRtpRaw.size() != rtpRaw.size() ||
+	    memcmp(receivedRtpRaw.data(), rtpRaw.data(), rtpRaw.size()) != 0) {
+		throw runtime_error("Received RTP packet is different than the packet that was sent");
+	}
 
 	// Delay close of peer 2 to check closing works properly
 	pc1.close();
@@ -149,7 +188,7 @@ void test_track() {
 	this_thread::sleep_for(1s);
 
 	if (!t1->isClosed() || !t2->isClosed())
-		throw runtime_error("Track is not closed");
+		return TestResult(false, "Track is not closed");
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }

+ 11 - 10
test/turn_connectivity.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "rtc/rtc.hpp"
+#include "test.hpp"
 
 #include <atomic>
 #include <chrono>
@@ -19,7 +20,7 @@ using namespace std;
 
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
-void test_turn_connectivity() {
+TestResult test_turn_connectivity() {
 	InitLogger(LogLevel::Debug);
 
 	Configuration config1;
@@ -133,16 +134,16 @@ void test_turn_connectivity() {
 
 	if (pc1.state() != PeerConnection::State::Connected ||
 	    pc2.state() != PeerConnection::State::Connected)
-		throw runtime_error("PeerConnection is not connected");
+		return TestResult(false, "PeerConnection is not connected");
 
 	if ((pc1.iceState() != PeerConnection::IceState::Connected &&
 	     pc1.iceState() != PeerConnection::IceState::Completed) ||
 	    (pc2.iceState() != PeerConnection::IceState::Connected &&
 	     pc2.iceState() != PeerConnection::IceState::Completed))
-		throw runtime_error("ICE is not connected");
+		return TestResult(false, "ICE is not connected");
 
 	if (!adc2 || !adc2->isOpen() || !dc1->isOpen())
-		throw runtime_error("DataChannel is not open");
+		return TestResult(false, "DataChannel is not open");
 
 	if (auto addr = pc1.localAddress())
 		cout << "Local address 1:  " << *addr << endl;
@@ -155,13 +156,13 @@ void test_turn_connectivity() {
 
 	Candidate local, remote;
 	if (!pc1.getSelectedCandidatePair(&local, &remote))
-		throw runtime_error("getSelectedCandidatePair failed");
+		return TestResult(false, "getSelectedCandidatePair failed");
 
 	cout << "Local candidate 1:  " << local << endl;
 	cout << "Remote candidate 1: " << remote << endl;
 
 	if (local.type() != Candidate::Type::Relayed)
-		throw runtime_error("Connection is not relayed as expected");
+		return TestResult(false, "Connection is not relayed as expected");
 
 	// Try to open a second data channel with another label
 	shared_ptr<DataChannel> second2;
@@ -214,7 +215,7 @@ void test_turn_connectivity() {
 		this_thread::sleep_for(1s);
 
 	if (!asecond2 || !asecond2->isOpen() || !second1->isOpen())
-		throw runtime_error("Second DataChannel is not open");
+		return TestResult(false, "Second DataChannel is not open");
 
 	// Try to open a negotiated channel
 	DataChannelInit init;
@@ -224,7 +225,7 @@ void test_turn_connectivity() {
 	auto negotiated2 = pc2.createDataChannel("negoctated", init);
 
 	if (!negotiated1->isOpen() || !negotiated2->isOpen())
-		throw runtime_error("Negotiated DataChannel is not open");
+		return TestResult(false, "Negotiated DataChannel is not open");
 
 	std::atomic<bool> received = false;
 	negotiated2->onMessage([&received](const variant<binary, string> &message) {
@@ -242,7 +243,7 @@ void test_turn_connectivity() {
 		this_thread::sleep_for(1s);
 
 	if (!received)
-		throw runtime_error("Negotiated DataChannel failed");
+		return TestResult(false, "Negotiated DataChannel failed");
 
 	// Delay close of peer 2 to check closing works properly
 	pc1.close();
@@ -250,5 +251,5 @@ void test_turn_connectivity() {
 	pc2.close();
 	this_thread::sleep_for(1s);
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }

+ 5 - 4
test/websocket.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "rtc/rtc.hpp"
+#include "test.hpp"
 
 #if RTC_ENABLE_WEBSOCKET
 
@@ -21,7 +22,7 @@ using namespace std;
 
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
-void test_websocket() {
+TestResult test_websocket() {
 	InitLogger(LogLevel::Debug);
 
 	const string myMessage = "Hello world from libdatachannel";
@@ -57,15 +58,15 @@ void test_websocket() {
 		this_thread::sleep_for(1s);
 
 	if (!ws.isOpen())
-		throw runtime_error("WebSocket is not open");
+		return TestResult(false, "WebSocket is not open");
 
 	if (!received)
-		throw runtime_error("Expected message not received");
+		return TestResult(false, "Expected message not received");
 
 	ws.close();
 	this_thread::sleep_for(1s);
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }
 
 #endif

+ 11 - 13
test/websocketserver.cpp

@@ -7,6 +7,7 @@
  */
 
 #include "rtc/rtc.hpp"
+#include "test.hpp"
 
 #if RTC_ENABLE_WEBSOCKET
 
@@ -21,7 +22,7 @@ using namespace std;
 
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
-void test_websocketserver() {
+TestResult test_websocketserver() {
 	InitLogger(LogLevel::Debug);
 
 	WebSocketServer::Configuration serverConfig;
@@ -38,22 +39,20 @@ void test_websocketserver() {
 		cout << "WebSocketServer: Client connection received" << endl;
 		client = incoming;
 
-		if(auto addr = client->remoteAddress())
+		if (auto addr = client->remoteAddress())
 			cout << "WebSocketServer: Client remote address is " << *addr << endl;
 
 		client->onOpen([wclient = make_weak_ptr(client)]() {
 			cout << "WebSocketServer: Client connection open" << endl;
-			if(auto client = wclient.lock())
-				if(auto path = client->path())
+			if (auto client = wclient.lock())
+				if (auto path = client->path())
 					cout << "WebSocketServer: Requested path is " << *path << endl;
 		});
 
-		client->onClosed([]() {
-			cout << "WebSocketServer: Client connection closed" << endl;
-		});
+		client->onClosed([]() { cout << "WebSocketServer: Client connection closed" << endl; });
 
 		client->onMessage([wclient = make_weak_ptr(client)](variant<binary, string> message) {
-			if(auto client = wclient.lock())
+			if (auto client = wclient.lock())
 				client->send(std::move(message));
 		});
 	});
@@ -81,8 +80,7 @@ void test_websocketserver() {
 				cout << "WebSocket: Received expected message" << endl;
 			else
 				cout << "WebSocket: Received UNEXPECTED message" << endl;
-		}
-		else {
+		} else {
 			binary bin = std::move(get<binary>(message));
 			if ((maxSizeReceived = (bin.size() == 1000)))
 				cout << "WebSocket: Received large message truncated at max size" << endl;
@@ -98,10 +96,10 @@ void test_websocketserver() {
 		this_thread::sleep_for(1s);
 
 	if (!ws.isOpen())
-		throw runtime_error("WebSocket is not open");
+		return TestResult(false, "WebSocket is not open");
 
 	if (!received || !maxSizeReceived)
-		throw runtime_error("Expected messages not received");
+		return TestResult(false, "Expected messages not received");
 
 	ws.close();
 	this_thread::sleep_for(1s);
@@ -109,7 +107,7 @@ void test_websocketserver() {
 	server.stop();
 	this_thread::sleep_for(1s);
 
-	cout << "Success" << endl;
+	return TestResult(true);
 }
 
 #endif