Procházet zdrojové kódy

Merge remote-tracking branch 'libdatachannel/master' into dependency-descriptor

melpon před 4 měsíci
rodič
revize
a81c918d39
80 změnil soubory, kde provedl 1864 přidání a 795 odebrání
  1. 3 1
      .github/workflows/build-nomedia.yml
  2. 5 3
      .github/workflows/build-openssl.yml
  3. 1 1
      .github/workflows/check-version.yml
  4. 1 1
      BUILDING.md
  5. 89 47
      CMakeLists.txt
  6. 34 3
      DOC.md
  7. 2 2
      Jamfile
  8. 2 2
      README.md
  9. 2 2
      cmake/Modules/FindLibJuice.cmake
  10. 1 1
      deps/libjuice
  11. 4 17
      examples/streamer/main.cpp
  12. 11 10
      include/rtc/av1rtppacketizer.hpp
  13. 0 1
      include/rtc/common.hpp
  14. 5 4
      include/rtc/description.hpp
  15. 11 3
      include/rtc/frameinfo.hpp
  16. 2 0
      include/rtc/h264rtpdepacketizer.hpp
  17. 8 9
      include/rtc/h264rtppacketizer.hpp
  18. 12 4
      include/rtc/h265nalunit.hpp
  19. 51 0
      include/rtc/h265rtpdepacketizer.hpp
  20. 9 10
      include/rtc/h265rtppacketizer.hpp
  21. 47 0
      include/rtc/iceudpmuxlistener.hpp
  22. 20 4
      include/rtc/message.hpp
  23. 27 57
      include/rtc/nalunit.hpp
  24. 13 2
      include/rtc/peerconnection.hpp
  25. 35 0
      include/rtc/rembhandler.hpp
  26. 14 4
      include/rtc/rtc.h
  27. 3 0
      include/rtc/rtc.hpp
  28. 4 4
      include/rtc/rtcpnackresponder.hpp
  29. 7 4
      include/rtc/rtcpsrreporter.hpp
  30. 2 0
      include/rtc/rtp.hpp
  31. 6 2
      include/rtc/rtpdepacketizer.hpp
  32. 3 3
      include/rtc/rtppacketizationconfig.hpp
  33. 20 4
      include/rtc/rtppacketizer.hpp
  34. 3 1
      include/rtc/track.hpp
  35. 3 3
      include/rtc/version.h
  36. 34 3
      pages/content/pages/reference.md
  37. 70 91
      src/av1rtppacketizer.cpp
  38. 45 13
      src/capi.cpp
  39. 30 26
      src/description.cpp
  40. 1 1
      src/global.cpp
  41. 7 5
      src/h264rtpdepacketizer.cpp
  42. 35 49
      src/h264rtppacketizer.cpp
  43. 60 34
      src/h265nalunit.cpp
  44. 194 0
      src/h265rtpdepacketizer.cpp
  45. 36 50
      src/h265rtppacketizer.cpp
  46. 29 0
      src/iceudpmuxlistener.cpp
  47. 32 8
      src/impl/certificate.cpp
  48. 3 1
      src/impl/certificate.hpp
  49. 4 1
      src/impl/datachannel.cpp
  50. 8 4
      src/impl/dtlstransport.cpp
  51. 1 1
      src/impl/dtlstransport.hpp
  52. 42 13
      src/impl/icetransport.cpp
  53. 12 2
      src/impl/icetransport.hpp
  54. 62 0
      src/impl/iceudpmuxlistener.cpp
  55. 45 0
      src/impl/iceudpmuxlistener.hpp
  56. 206 120
      src/impl/peerconnection.cpp
  57. 17 8
      src/impl/peerconnection.hpp
  58. 4 3
      src/impl/pollservice.cpp
  59. 1 0
      src/impl/queue.hpp
  60. 1 1
      src/impl/sctptransport.cpp
  61. 1 1
      src/impl/sctptransport.hpp
  62. 17 14
      src/impl/tcptransport.cpp
  63. 5 1
      src/impl/tls.cpp
  64. 3 0
      src/impl/tlstransport.cpp
  65. 1 1
      src/impl/tlstransport.hpp
  66. 16 14
      src/impl/track.cpp
  67. 3 2
      src/impl/track.hpp
  68. 6 3
      src/impl/websocket.cpp
  69. 13 9
      src/impl/wshandshake.cpp
  70. 5 2
      src/message.cpp
  71. 103 33
      src/nalunit.cpp
  72. 72 27
      src/peerconnection.cpp
  73. 47 0
      src/rembhandler.cpp
  74. 8 9
      src/rtcpnackresponder.cpp
  75. 27 15
      src/rtcpsrreporter.cpp
  76. 9 1
      src/rtp.cpp
  77. 13 3
      src/rtpdepacketizer.cpp
  78. 45 13
      src/rtppacketizer.cpp
  79. 13 4
      src/track.cpp
  80. 18 0
      test/capi_track.cpp

+ 3 - 1
.github/workflows/build-nomedia.yml

@@ -36,5 +36,7 @@ jobs:
         set CL=/MP
         nmake
     - name: test
-      run: build/tests.exe
+      run: |
+        cd build
+        ./tests
 

+ 5 - 3
.github/workflows/build-openssl.yml

@@ -24,13 +24,13 @@ jobs:
     steps:
     - uses: actions/checkout@v2
     - name: install packages
-      run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew reinstall openssl@1.1
+      run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew reinstall openssl@3
     - name: submodules
       run: git submodule update --init --recursive --depth 1
     - name: cmake
       run: cmake -B build -DUSE_GNUTLS=0 -WARNINGS_AS_ERRORS=1 -DENABLE_LOCAL_ADDRESS_TRANSLATION=1
       env:
-        OPENSSL_ROOT_DIR: /usr/local/opt/openssl@1.1
+        OPENSSL_ROOT_DIR: /usr/local/opt/openssl@3
     - name: make
       run: (cd build; make -j2)
     - name: test
@@ -52,5 +52,7 @@ jobs:
         set CL=/MP
         nmake
     - name: test
-      run: build/tests.exe
+      run: |
+        cd build
+        ./tests
 

+ 1 - 1
.github/workflows/check-version.yml

@@ -14,7 +14,7 @@ jobs:
     - name: submodules
       run: git submodule update --init --recursive --depth 1
     - name: cmake
-      run: cmake -B build -DUSE_GNUTLS=0 -DUSE_SYSTEM_SRTP=1 -DWARNINGS_AS_ERRORS=1
+      run: cmake -B build -DUSE_GNUTLS=0 -DUSE_SYSTEM_SRTP=1 -DWARNINGS_AS_ERRORS=1 -DRTC_UPDATE_VERSION_HEADER=1
     - name: check diff
       run: |
         if ! git diff --exit-code

+ 1 - 1
BUILDING.md

@@ -70,7 +70,7 @@ $ make -j2
 ### Microsoft Windows with Microsoft Visual C++
 
 ```bash
-$ cmake -B build -G "NMake Makefiles"
+$ cmake -B build -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
 $ cd build
 $ nmake
 ```

+ 89 - 47
CMakeLists.txt

@@ -1,16 +1,18 @@
-cmake_minimum_required(VERSION 3.7)
+cmake_minimum_required(VERSION 3.13)
 project(libdatachannel
-	VERSION 0.21.1
+	VERSION 0.22.6
 	LANGUAGES CXX)
 set(PROJECT_DESCRIPTION "C/C++ WebRTC network library featuring Data Channels, Media Transport, and WebSockets")
 
 include(GNUInstallDirs)
 
 # Options
+option(BUILD_SHARED_LIBS "Build shared library" ON)
+option(BUILD_SHARED_DEPS_LIBS "Build submodules as shared libraries" OFF)
 option(USE_GNUTLS "Use GnuTLS instead of OpenSSL" OFF)
 option(USE_MBEDTLS "Use Mbed TLS instead of OpenSSL" OFF)
 option(USE_NICE "Use libnice instead of libjuice" OFF)
-option(PREFER_SYSTEM_LIB "Prefer system libraries over deps folder" OFF)
+option(PREFER_SYSTEM_LIB "Prefer system libraries over submodules" OFF)
 option(USE_SYSTEM_SRTP "Use system libSRTP" ${PREFER_SYSTEM_LIB})
 option(USE_SYSTEM_JUICE "Use system libjuice" ${PREFER_SYSTEM_LIB})
 option(USE_SYSTEM_USRSCTP "Use system libusrsctp" ${PREFER_SYSTEM_LIB})
@@ -23,6 +25,7 @@ option(NO_TESTS "Disable tests build" OFF)
 option(WARNINGS_AS_ERRORS "Treat warnings as errors" OFF)
 option(CAPI_STDCALL "Set calling convention of C API callbacks stdcall" OFF)
 option(SCTP_DEBUG "Enable SCTP debugging output to verbose log" OFF)
+option(RTC_UPDATE_VERSION_HEADER "Enable updating the version header" OFF)
 
 if (USE_GNUTLS AND USE_MBEDTLS)
 	message(FATAL_ERROR "Both USE_MBEDTLS and USE_GNUTLS cannot be enabled at the same time")
@@ -48,7 +51,6 @@ endif()
 
 list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules)
 set(CMAKE_POSITION_INDEPENDENT_CODE ON)
-set(BUILD_SHARED_LIBS OFF) # to force usrsctp to be built static
 
 if(WIN32)
 	add_definitions(-DWIN32_LEAN_AND_MEAN)
@@ -65,6 +67,7 @@ set(LIBDATACHANNEL_SOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/src/datachannel.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/dependencydescriptor.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/description.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/src/iceudpmuxlistener.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/mediahandler.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/global.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/message.cpp
@@ -81,6 +84,7 @@ set(LIBDATACHANNEL_SOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/src/h264rtpdepacketizer.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/nalunit.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/h265rtppacketizer.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/src/h265rtpdepacketizer.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/h265nalunit.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/av1rtppacketizer.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/rtcpnackresponder.cpp
@@ -88,6 +92,7 @@ set(LIBDATACHANNEL_SOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/src/capi.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/plihandler.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/pacinghandler.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/src/rembhandler.cpp
 )
 
 set(LIBDATACHANNEL_HEADERS
@@ -97,6 +102,7 @@ set(LIBDATACHANNEL_HEADERS
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/datachannel.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/dependencydescriptor.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/description.hpp
+	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/iceudpmuxlistener.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/mediahandler.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/rtcpreceivingsession.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/common.hpp
@@ -119,12 +125,14 @@ set(LIBDATACHANNEL_HEADERS
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/h264rtpdepacketizer.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/nalunit.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/h265rtppacketizer.hpp
+	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/h265rtpdepacketizer.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/h265nalunit.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/av1rtppacketizer.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/rtcpnackresponder.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/utils.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/plihandler.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/pacinghandler.hpp
+	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/rembhandler.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/version.h
 )
 
@@ -135,6 +143,7 @@ set(LIBDATACHANNEL_IMPL_SOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/dtlssrtptransport.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/dtlstransport.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/icetransport.cpp
+	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/iceudpmuxlistener.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/init.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/peerconnection.cpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/logcounter.cpp
@@ -167,6 +176,7 @@ set(LIBDATACHANNEL_IMPL_HEADERS
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/dtlssrtptransport.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/dtlstransport.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/icetransport.hpp
+	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/iceudpmuxlistener.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/init.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/internals.hpp
 	${CMAKE_CURRENT_SOURCE_DIR}/src/impl/peerconnection.hpp
@@ -229,6 +239,52 @@ set(BENCHMARK_UWP_RESOURCES
 	${CMAKE_CURRENT_SOURCE_DIR}/test/uwp/benchmark/Windows_TemporaryKey.pfx
 )
 
+if(RTC_UPDATE_VERSION_HEADER)
+	configure_file (
+		${PROJECT_SOURCE_DIR}/cmake/version.h.in
+		${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/version.h
+	)
+endif()
+
+add_library(datachannel
+	${LIBDATACHANNEL_SOURCES}
+	${LIBDATACHANNEL_HEADERS}
+	${LIBDATACHANNEL_IMPL_SOURCES}
+	${LIBDATACHANNEL_IMPL_HEADERS})
+set_target_properties(datachannel PROPERTIES
+	VERSION ${PROJECT_VERSION}
+	SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
+	CXX_STANDARD 17
+	CXX_VISIBILITY_PRESET default)
+if(APPLE)
+	set_target_properties(datachannel PROPERTIES
+		VERSION ${PROJECT_VERSION_MAJOR}
+		SOVERSION ${PROJECT_VERSION_MAJOR})
+endif()
+
+add_library(datachannel-static STATIC EXCLUDE_FROM_ALL
+	${LIBDATACHANNEL_SOURCES}
+	${LIBDATACHANNEL_HEADERS}
+	${LIBDATACHANNEL_IMPL_SOURCES}
+	${LIBDATACHANNEL_IMPL_HEADERS})
+set_target_properties(datachannel-static PROPERTIES
+	VERSION ${PROJECT_VERSION}
+	CXX_STANDARD 17)
+
+target_compile_definitions(datachannel PRIVATE RTC_EXPORTS)
+if (NOT BUILD_SHARED_LIBS)
+	target_compile_definitions(datachannel PUBLIC RTC_STATIC)
+endif()
+target_compile_definitions(datachannel-static PRIVATE RTC_EXPORTS)
+target_compile_definitions(datachannel-static PUBLIC RTC_STATIC)
+
+if(BUILD_SHARED_LIBS AND NOT BUILD_SHARED_DEPS_LIBS)
+	set(BUILD_SHARED_LIBS OFF)
+	set(INSTALL_DEPS_LIBS OFF)
+else()
+	set(INSTALL_DEPS_LIBS ON)
+endif()
+
 set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
 set(THREADS_PREFER_PTHREAD_FLAG TRUE)
 find_package(Threads REQUIRED)
@@ -258,58 +314,38 @@ else()
 		target_compile_definitions(usrsctp PUBLIC -DSCTP_STDINT_INCLUDE=<stdint.h>)
 	endif()
 	add_library(Usrsctp::Usrsctp ALIAS usrsctp)
-endif()
 
-configure_file (
-    ${PROJECT_SOURCE_DIR}/cmake/version.h.in
-	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc/version.h
-)
-
-add_library(datachannel SHARED
-	${LIBDATACHANNEL_SOURCES}
-	${LIBDATACHANNEL_HEADERS}
-	${LIBDATACHANNEL_IMPL_SOURCES}
-	${LIBDATACHANNEL_IMPL_HEADERS})
-set_target_properties(datachannel PROPERTIES
-	VERSION ${PROJECT_VERSION}
-	SOVERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
-	CXX_STANDARD 17
-	CXX_VISIBILITY_PRESET default)
-
-if(APPLE)
-	set_target_properties(datachannel PROPERTIES
-		VERSION ${PROJECT_VERSION_MAJOR}
-		SOVERSION ${PROJECT_VERSION_MAJOR})
+	if(INSTALL_DEPS_LIBS)
+		install(TARGETS usrsctp EXPORT LibDataChannelTargets)
+		# Fix directories
+		set_target_properties(usrsctp PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "")
+		target_include_directories(usrsctp INTERFACE
+			$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/deps/usrsctp/usrsctplib>
+			$<INSTALL_INTERFACE:>)
+	endif()
 endif()
 
-target_compile_definitions(datachannel PRIVATE RTC_EXPORTS)
-
-add_library(datachannel-static STATIC EXCLUDE_FROM_ALL
-	${LIBDATACHANNEL_SOURCES}
-	${LIBDATACHANNEL_HEADERS}
-	${LIBDATACHANNEL_IMPL_SOURCES}
-	${LIBDATACHANNEL_IMPL_HEADERS})
-set_target_properties(datachannel-static PROPERTIES
-	VERSION ${PROJECT_VERSION}
-	CXX_STANDARD 17)
-target_compile_definitions(datachannel-static PRIVATE RTC_EXPORTS)
-target_compile_definitions(datachannel-static PUBLIC RTC_STATIC)
-
 target_include_directories(datachannel PUBLIC
     $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
     $<INSTALL_INTERFACE:include>)
-target_include_directories(datachannel PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/rtc)
-target_include_directories(datachannel PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
-target_link_libraries(datachannel PRIVATE Threads::Threads)
-target_link_libraries(datachannel PRIVATE Usrsctp::Usrsctp plog::plog)
+target_include_directories(datachannel PRIVATE
+	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc
+	${CMAKE_CURRENT_SOURCE_DIR}/src)
+target_link_libraries(datachannel PRIVATE
+	Threads::Threads
+	Usrsctp::Usrsctp
+	$<BUILD_INTERFACE:plog::plog>)
 
 target_include_directories(datachannel-static PUBLIC
     $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
     $<INSTALL_INTERFACE:include>)
-target_include_directories(datachannel-static PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/rtc)
-target_include_directories(datachannel-static PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
-target_link_libraries(datachannel-static PRIVATE Threads::Threads)
-target_link_libraries(datachannel-static PRIVATE Usrsctp::Usrsctp plog::plog)
+target_include_directories(datachannel-static PRIVATE
+	${CMAKE_CURRENT_SOURCE_DIR}/include/rtc
+	${CMAKE_CURRENT_SOURCE_DIR}/src)
+target_link_libraries(datachannel-static PRIVATE
+	Threads::Threads
+	Usrsctp::Usrsctp
+	$<BUILD_INTERFACE:plog::plog>)
 
 if(WIN32)
 	target_link_libraries(datachannel PUBLIC ws2_32) # winsock2
@@ -346,6 +382,9 @@ else()
 	else()
 		if(NOT TARGET srtp2)
 			add_subdirectory(deps/libsrtp EXCLUDE_FROM_ALL)
+			if(INSTALL_DEPS_LIBS)
+				install(TARGETS srtp2 EXPORT LibDataChannelTargets)
+			endif()
 		endif()
 		target_compile_definitions(datachannel PRIVATE RTC_SYSTEM_SRTP=0)
 		target_compile_definitions(datachannel-static PRIVATE RTC_SYSTEM_SRTP=0)
@@ -423,9 +462,12 @@ else()
 		target_link_libraries(datachannel-static PRIVATE LibJuice::LibJuice)
 	else()
 		add_subdirectory(deps/libjuice EXCLUDE_FROM_ALL)
+		if(INSTALL_DEPS_LIBS)
+			install(TARGETS juice EXPORT LibDataChannelTargets)
+		endif()
 		target_compile_definitions(datachannel PRIVATE RTC_SYSTEM_JUICE=0)
 		target_compile_definitions(datachannel-static PRIVATE RTC_SYSTEM_JUICE=0)
-		target_link_libraries(datachannel PRIVATE LibJuice::LibJuiceStatic)
+		target_link_libraries(datachannel PRIVATE LibJuice::LibJuice)
 		target_link_libraries(datachannel-static PRIVATE LibJuice::LibJuiceStatic)
 	endif()
 endif()

+ 34 - 3
DOC.md

@@ -207,7 +207,9 @@ Initiates the handshake process. Following this call, the local description call
 Arguments:
 
 - `pc`: the Peer Connection identifier
-- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection.
+- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (recommended).
+
+Warning: This function expects the optional type for the local description and not an SDP description. It is not possible to set an existing SDP description.
 
 #### rtcSetRemoteDescription
 
@@ -220,7 +222,8 @@ Sets the remote description received from the remote peer by the user's method o
 Arguments:
 
 - `pc`: the Peer Connection identifier
-- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection.
+- `sdp`: the remote description in SDP format
+- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (not recommended).
 
 If the remote description is an offer and `disableAutoNegotiation` was not set in `rtcConfiguration`, the library will automatically answer by calling `rtcSetLocalDescription` internally. Otherwise, the user must call it to answer the remote description.
 
@@ -296,7 +299,7 @@ Return value: the length of the string copied in buffer (including the terminati
 
 If `buffer` is `NULL`, the description is not copied but the size is still returned.
 
-#### rtcGetRemoteDescription
+#### rtcGetRemoteDescriptionType
 
 ```
 int rtcGetRemoteDescriptionType(int pc, char *buffer, int size)
@@ -314,6 +317,22 @@ Return value: the length of the string copied in buffer (including the terminati
 
 If `buffer` is `NULL`, the description is not copied but the size is still returned.
 
+#### rtcCreateOffer/rtcCreateAnswer
+
+```
+int rtcCreateOffer(int pc, char *buffer, int size)
+int rtcCreateAnswer(int pc, char *buffer, int size)
+```
+
+Create a local offer or answer description in SDP format. These functions are intended only for specific use cases where the application needs to generate a description without setting it. It is useless to call them before `rtcSetLocalDescription` as it doesn't expect the user to supply a description.
+
+- `pc`: the Peer Connection identifier
+- `buffer`: a user-supplied buffer to store the description
+- `size`: the size of `buffer`
+
+Return value: the length of the string copied in buffer (including the terminating null character) or a negative error code
+
+If `buffer` is `NULL`, the description is not copied but the size is still returned.
 
 #### rtcGetLocalAddress
 
@@ -371,6 +390,18 @@ Return value: the maximun length of strings copied in buffers (including the ter
 
 If `local`, `remote`, or both, are `NULL`, the corresponding candidate is not copied, but the maximum length is still returned.
 
+#### rtcIsNegotiationNeeded
+```
+bool rtcIsNegotiationNeeded(int pc);
+```
+
+Return true if negotiation needs to be started or restarted, for instance to signal new tracks. If so, the user may call `rtcSetLocalDescription()` to start it.
+
+Arguments:
+- `pc`: the Peer Connection identifier
+
+Return value: true if negotiation is needed
+
 #### rtcGetMaxDataChannelStream
 ```
 int rtcGetMaxDataChannelStream(int pc);

+ 2 - 2
Jamfile

@@ -103,7 +103,7 @@ rule make_libusrsctp ( targets * : sources * : properties * )
 }
 actions make_libusrsctp
 {
-    (cd $(CWD)/deps/usrsctp && mkdir -p $(BUILD_DIR) && cd $(BUILD_DIR) && cmake -DCMAKE_BUILD_TYPE=$(VARIANT) -DCMAKE_C_FLAGS="-fPIC -Wno-unknown-warning-option -Wno-format-truncation" -Dsctp_build_shared_lib=0 -Dsctp_build_programs=0 -Dsctp_inet=0 -Dsctp_inet6=0 .. && make -j2 usrsctp)
+    (cd $(CWD)/deps/usrsctp && mkdir -p $(BUILD_DIR) && cd $(BUILD_DIR) && cmake -DCMAKE_BUILD_TYPE=$(VARIANT) -DCMAKE_C_FLAGS="-fPIC" -Dsctp_werror=0 -Dsctp_build_shared_lib=0 -Dsctp_build_programs=0 -Dsctp_inet=0 -Dsctp_inet6=0 .. && make -j2 usrsctp)
     cp $(CWD)/deps/usrsctp/$(BUILD_DIR)/usrsctplib/libusrsctp.a $(<)
 }
 rule make_libusrsctp_msvc ( targets * : sources * : properties * )
@@ -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_build_shared_lib=0 -Dsctp_build_programs=0 ..
+    cmake -G "Visual Studio 16 2019" -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 $(<)

+ 2 - 2
README.md

@@ -10,7 +10,7 @@
 [![Gitter](https://badges.gitter.im/libdatachannel/community.svg)](https://gitter.im/libdatachannel/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 [![Discord](https://img.shields.io/discord/903257095539925006?logo=discord)](https://discord.gg/jXAP8jp3Nn)
 
-libdatachannel is a standalone implementation of WebRTC Data Channels, WebRTC Media Transport, and WebSockets in C++17 with C bindings for POSIX platforms (including GNU/Linux, Android, FreeBSD, Apple macOS and iOS) and Microsoft Windows. WebRTC is a W3C and IETF standard enabling real-time peer-to-peer data and media exchange between two devices.
+libdatachannel is a standalone implementation of WebRTC Data Channels, WebRTC Media Transport, and WebSockets in C++ with C bindings for multiple platforms, including GNU/Linux, Android, FreeBSD, Apple macOS, iOS, and Microsoft Windows. WebRTC is a W3C and IETF standard enabling real-time peer-to-peer data and media exchange between two devices.
 
 The library aims at being both straightforward and lightweight with minimal external dependencies, to enable direct connectivity between native applications and web browsers without the pain of importing Google's bloated [reference library](https://webrtc.googlesource.com/src/). The interface consists of somewhat simplified versions of the JavaScript WebRTC and WebSocket APIs present in browsers, in order to ease the design of cross-environment applications.
 
@@ -22,7 +22,7 @@ The WebRTC stack is fully compatible with browsers like Firefox and Chromium, se
 
 libdatachannel is licensed under MPL 2.0 since version 0.18, see [LICENSE](https://github.com/paullouisageneau/libdatachannel/blob/master/LICENSE) (previous versions were licensed under LGPLv2.1 or later).
 
-libdatachannel is available on [AUR](https://aur.archlinux.org/packages/libdatachannel/), [vcpkg](https://vcpkg.io/en/getting-started), and [FreeBSD ports](https://www.freshports.org/www/libdatachannel). Bindings are available for [Rust](https://crates.io/crates/datachannel) and [Node.js](https://www.npmjs.com/package/node-datachannel).
+libdatachannel is available on [AUR](https://aur.archlinux.org/packages/libdatachannel/), [vcpkg](https://vcpkg.io/en/getting-started), [conan](https://conan.io/center/recipes/libdatachannel), and [FreeBSD ports](https://www.freshports.org/www/libdatachannel). Bindings are available for [Rust](https://crates.io/crates/datachannel) and [Node.js](https://www.npmjs.com/package/node-datachannel).
 
 ## Dependencies
 

+ 2 - 2
cmake/Modules/FindLibJuice.cmake

@@ -9,8 +9,8 @@ if (NOT TARGET LibJuice::LibJuice)
         add_library(LibJuice::LibJuice UNKNOWN IMPORTED)
         set_target_properties(LibJuice::LibJuice PROPERTIES
             IMPORTED_LOCATION "${JUICE_LIBRARY}"
-            INTERFACE_INCLUDE_DIRECTORIES "${JUICE_INCLUDE_DIRS}"
-            INTERFACE_LINK_LIBRARIES "${JUICE_LIBRARIES}"
+            INTERFACE_INCLUDE_DIRECTORIES "${JUICE_INCLUDE_DIR}"
+            INTERFACE_LINK_LIBRARIES "${JUICE_LIBRARY}"
                 IMPORTED_LINK_INTERFACE_LANGUAGES "C")
     endif ()
 endif ()

+ 1 - 1
deps/libjuice

@@ -1 +1 @@
-Subproject commit 2de35247f0b15fa385406f3e2020d0e3d4d5cfcc
+Subproject commit 77daa8befd828046169adbb012d8de928bfdcdd4

+ 4 - 17
examples/streamer/main.cpp

@@ -20,6 +20,8 @@
 #include "helpers.hpp"
 #include "ArgParser.hpp"
 
+#include <chrono>
+
 using namespace rtc;
 using namespace std;
 using namespace std::chrono_literals;
@@ -208,7 +210,7 @@ shared_ptr<ClientTrackData> addVideo(const shared_ptr<PeerConnection> pc, const
     video.addSSRC(ssrc, cname, msid, cname);
     auto track = pc->addTrack(video);
     // create RTP configuration
-    auto rtpConfig = make_shared<RtpPacketizationConfig>(ssrc, cname, payloadType, H264RtpPacketizer::defaultClockRate);
+    auto rtpConfig = make_shared<RtpPacketizationConfig>(ssrc, cname, payloadType, H264RtpPacketizer::ClockRate);
     // create packetizer
     auto packetizer = make_shared<H264RtpPacketizer>(NalUnit::Separator::Length, rtpConfig);
     // add RTCP SR handler
@@ -351,26 +353,11 @@ shared_ptr<Stream> createStream(const string h264Samples, const unsigned fps, co
             for (auto clientTrack: tracks) {
                 auto client = clientTrack.id;
                 auto trackData = clientTrack.trackData;
-                auto rtpConfig = trackData->sender->rtpConfig;
-
-                // sample time is in us, we need to convert it to seconds
-                auto elapsedSeconds = double(sampleTime) / (1000 * 1000);
-                // get elapsed time in clock rate
-                uint32_t elapsedTimestamp = rtpConfig->secondsToTimestamp(elapsedSeconds);
-                // set new timestamp
-                rtpConfig->timestamp = rtpConfig->startTimestamp + elapsedTimestamp;
-
-                // get elapsed time in clock rate from last RTCP sender report
-                auto reportElapsedTimestamp = rtpConfig->timestamp - trackData->sender->lastReportedTimestamp();
-                // check if last report was at least 1 second ago
-                if (rtpConfig->timestampToSeconds(reportElapsedTimestamp) > 1) {
-                    trackData->sender->setNeedsToReport();
-                }
 
                 cout << "Sending " << streamType << " sample with size: " << to_string(sample.size()) << " to " << client << endl;
                 try {
                     // send sample
-                    trackData->track->send(sample);
+                    trackData->track->sendFrame(sample, std::chrono::duration<double, std::micro>(sampleTime));
                 } catch (const std::exception &e) {
                     cerr << "Unable to send "<< streamType << " packet: " << e.what() << endl;
                 }

+ 11 - 10
include/rtc/av1rtppacketizer.hpp

@@ -20,8 +20,8 @@ namespace rtc {
 // RTP packetization of AV1 payload
 class RTC_CPP_EXPORT AV1RtpPacketizer final : public RtpPacketizer {
 public:
-	// Default clock rate for AV1 in RTP
-	inline static const uint32_t defaultClockRate = 90 * 1000;
+	inline static const uint32_t ClockRate = VideoClockRate;
+	[[deprecated("Use ClockRate")]] inline static const uint32_t defaultClockRate = ClockRate;
 
 	// Define how OBUs are seperated in a AV1 Sample
 	enum class Packetization {
@@ -33,17 +33,18 @@ public:
 	// @note RTP configuration is used in packetization process which may change some configuration
 	// properties such as sequence number.
 	AV1RtpPacketizer(Packetization packetization, shared_ptr<RtpPacketizationConfig> rtpConfig,
-	                 uint16_t maxFragmentSize = NalUnits::defaultMaximumFragmentSize);
-
-	void outgoing(message_vector &messages, const message_callback &send) override;
+	                 size_t maxFragmentSize = DefaultMaxFragmentSize);
 
 private:
-	shared_ptr<NalUnits> splitMessage(binary_ptr message);
-	std::vector<shared_ptr<binary>> packetizeObu(binary_ptr message, uint16_t maxFragmentSize);
+	static std::vector<binary> extractTemporalUnitObus(const binary &data);
+
+	std::vector<binary> fragment(binary data) override;
+	std::vector<binary> fragmentObu(const binary &data);
+
+	const Packetization mPacketization;
+	const size_t mMaxFragmentSize;
 
-	const uint16_t maxFragmentSize;
-	const Packetization packetization;
-	std::shared_ptr<binary> sequenceHeader;
+	std::unique_ptr<binary> mSequenceHeader;
 };
 
 // For backward compatibility, do not use

+ 0 - 1
include/rtc/common.hpp

@@ -67,7 +67,6 @@ using std::variant;
 using std::weak_ptr;
 
 using binary = std::vector<byte>;
-using binary_ptr = shared_ptr<binary>;
 using message_variant = variant<binary, string>;
 
 using std::int16_t;

+ 5 - 4
include/rtc/description.hpp

@@ -66,9 +66,10 @@ public:
 	bool ended() const;
 
 	void hintType(Type type);
-	void setFingerprint(CertificateFingerprint f);
 	void addIceOption(string option);
 	void removeIceOption(const string &option);
+	void setIceAttribute(string ufrag, string pwd);
+	void setFingerprint(CertificateFingerprint f);
 
 	std::vector<string> attributes() const;
 	void addAttribute(string attr);
@@ -281,9 +282,9 @@ public:
 	int addAudio(string mid = "audio", Direction dir = Direction::SendOnly);
 	void clearMedia();
 
-	variant<Media *, Application *> media(unsigned int index);
-	variant<const Media *, const Application *> media(unsigned int index) const;
-	unsigned int mediaCount() const;
+	variant<Media *, Application *> media(int index);
+	variant<const Media *, const Application *> media(int index) const;
+	int mediaCount() const;
 
 	const Application *application() const;
 	Application *application();

+ 11 - 3
include/rtc/frameinfo.hpp

@@ -11,12 +11,20 @@
 
 #include "common.hpp"
 
+#include <chrono>
+
 namespace rtc {
 
 struct RTC_CPP_EXPORT FrameInfo {
-	FrameInfo(uint8_t payloadType, uint32_t timestamp) : payloadType(payloadType), timestamp(timestamp){};
-	uint8_t payloadType; // Indicates codec of the frame
-	uint32_t timestamp = 0; // RTP Timestamp
+	FrameInfo(uint32_t timestamp) : timestamp(timestamp) {};
+	template<typename Period = std::ratio<1>> FrameInfo(std::chrono::duration<double, Period> timestamp) : timestampSeconds(timestamp) {};
+
+	[[deprecated]] FrameInfo(uint8_t payloadType, uint32_t timestamp) : timestamp(timestamp), payloadType(payloadType) {};
+
+	uint32_t timestamp = 0;
+	uint8_t payloadType = 0;
+
+	optional<std::chrono::duration<double>> timestampSeconds;
 };
 
 } // namespace rtc

+ 2 - 0
include/rtc/h264rtpdepacketizer.hpp

@@ -27,6 +27,8 @@ class RTC_CPP_EXPORT H264RtpDepacketizer : public MediaHandler {
 public:
 	using Separator = NalUnit::Separator;
 
+	inline static const uint32_t ClockRate = 90 * 1000;
+
 	H264RtpDepacketizer(Separator separator = Separator::LongStartSequence);
 	virtual ~H264RtpDepacketizer() = default;
 

+ 8 - 9
include/rtc/h264rtppacketizer.hpp

@@ -22,8 +22,8 @@ class RTC_CPP_EXPORT H264RtpPacketizer final : public RtpPacketizer {
 public:
 	using Separator = NalUnit::Separator;
 
-	/// Default clock rate for H264 in RTP
-	inline static const uint32_t defaultClockRate = 90 * 1000;
+	inline static const uint32_t ClockRate = VideoClockRate;
+	[[deprecated("Use ClockRate")]] inline static const uint32_t defaultClockRate = ClockRate;
 
 	/// Constructs h264 payload packetizer with given RTP configuration.
 	/// @note RTP configuration is used in packetization process which may change some configuration
@@ -32,20 +32,19 @@ public:
 	/// @param rtpConfig RTP configuration
 	/// @param maxFragmentSize maximum size of one NALU fragment
 	H264RtpPacketizer(Separator separator, shared_ptr<RtpPacketizationConfig> rtpConfig,
-	                  uint16_t maxFragmentSize = NalUnits::defaultMaximumFragmentSize);
+	                  size_t maxFragmentSize = DefaultMaxFragmentSize);
 
 	// For backward compatibility, do not use
 	[[deprecated]] H264RtpPacketizer(
 	    shared_ptr<RtpPacketizationConfig> rtpConfig,
-	    uint16_t maxFragmentSize = NalUnits::defaultMaximumFragmentSize);
-
-	void outgoing(message_vector &messages, const message_callback &send) override;
+	    size_t maxFragmentSize = DefaultMaxFragmentSize);
 
 private:
-	shared_ptr<NalUnits> splitMessage(binary_ptr message);
+	std::vector<binary> fragment(binary data) override;
+	std::vector<NalUnit> splitFrame(const binary &frame);
 
-	const uint16_t maxFragmentSize;
-	const Separator separator;
+	const Separator mSeparator;
+	const size_t mMaxFragmentSize;
 };
 
 // For backward compatibility, do not use

+ 12 - 4
include/rtc/h265nalunit.hpp

@@ -15,6 +15,7 @@
 #include "nalunit.hpp"
 
 #include <cassert>
+#include <vector>
 
 namespace rtc {
 
@@ -72,8 +73,13 @@ struct RTC_CPP_EXPORT H265NalUnitFragmentHeader {
 
 #pragma pack(pop)
 
-/// Nal unit
+struct H265NalUnitFragment;
+
+/// NAL unit
 struct RTC_CPP_EXPORT H265NalUnit : NalUnit {
+	static std::vector<binary> GenerateFragments(const std::vector<H265NalUnit> &nalus,
+	                                             size_t maxFragmentSize);
+
 	H265NalUnit(const H265NalUnit &unit) = default;
 	H265NalUnit(size_t size, bool includingHeader = true)
 	    : NalUnit(size, includingHeader, NalUnit::Type::H265) {}
@@ -104,6 +110,8 @@ struct RTC_CPP_EXPORT H265NalUnit : NalUnit {
 		insert(end(), payload.begin(), payload.end());
 	}
 
+	std::vector<H265NalUnitFragment> generateFragments(size_t maxFragmentSize) const;
+
 protected:
 	const H265NalUnitHeader *header() const {
 		assert(size() >= H265_NAL_HEADER_SIZE);
@@ -116,9 +124,9 @@ protected:
 	}
 };
 
-/// Nal unit fragment A
+/// NAL unit fragment
 struct RTC_CPP_EXPORT H265NalUnitFragment : H265NalUnit {
-	static std::vector<shared_ptr<H265NalUnitFragment>> fragmentsFrom(shared_ptr<H265NalUnit> nalu,
+	[[deprecated]] static std::vector<shared_ptr<H265NalUnitFragment>> fragmentsFrom(shared_ptr<H265NalUnit> nalu,
 	                                                                  uint16_t maxFragmentSize);
 
 	enum class FragmentType { Start, Middle, End };
@@ -171,7 +179,7 @@ protected:
 	}
 };
 
-class RTC_CPP_EXPORT H265NalUnits : public std::vector<shared_ptr<H265NalUnit>> {
+class [[deprecated]] RTC_CPP_EXPORT H265NalUnits : public std::vector<shared_ptr<H265NalUnit>> {
 public:
 	static const uint16_t defaultMaximumFragmentSize =
 	    uint16_t(RTC_DEFAULT_MTU - 12 - 8 - 40); // SRTP/UDP/IPv6

+ 51 - 0
include/rtc/h265rtpdepacketizer.hpp

@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2020 Staz Modrzynski
+ * Copyright (c) 2020-2024 Paul-Louis Ageneau
+ * Copyright (c) 2024 Robert Edmonds
+ *
+ * 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/.
+ */
+
+#ifndef RTC_H265_RTP_DEPACKETIZER_H
+#define RTC_H265_RTP_DEPACKETIZER_H
+
+#if RTC_ENABLE_MEDIA
+
+#include "common.hpp"
+#include "h265nalunit.hpp"
+#include "mediahandler.hpp"
+#include "message.hpp"
+#include "rtp.hpp"
+
+#include <iterator>
+
+namespace rtc {
+
+/// RTP depacketization for H265
+class RTC_CPP_EXPORT H265RtpDepacketizer : public MediaHandler {
+public:
+	using Separator = NalUnit::Separator;
+
+	inline static const uint32_t ClockRate = 90 * 1000;
+
+	H265RtpDepacketizer(Separator separator = Separator::LongStartSequence);
+	virtual ~H265RtpDepacketizer() = default;
+
+	void incoming(message_vector &messages, const message_callback &send) override;
+
+private:
+	std::vector<message_ptr> mRtpBuffer;
+	const NalUnit::Separator separator;
+
+	void addSeparator(binary& accessUnit);
+	message_vector buildFrames(message_vector::iterator firstPkt, message_vector::iterator lastPkt,
+	                           uint8_t payloadType, uint32_t timestamp);
+};
+
+} // namespace rtc
+
+#endif // RTC_ENABLE_MEDIA
+
+#endif // RTC_H265_RTP_DEPACKETIZER_H

+ 9 - 10
include/rtc/h265rtppacketizer.hpp

@@ -21,8 +21,8 @@ class RTC_CPP_EXPORT H265RtpPacketizer final : public RtpPacketizer {
 public:
 	using Separator = NalUnit::Separator;
 
-	// Default clock rate for H265 in RTP
-	inline static const uint32_t defaultClockRate = 90 * 1000;
+	inline static const uint32_t ClockRate = VideoClockRate;
+	[[deprecated("Use ClockRate")]] inline static const uint32_t defaultClockRate = ClockRate;
 
 	// Constructs h265 payload packetizer with given RTP configuration.
 	// @note RTP configuration is used in packetization process which may change some configuration
@@ -31,19 +31,18 @@ public:
 	// @param rtpConfig  RTP configuration
 	// @param maxFragmentSize maximum size of one NALU fragment
 	H265RtpPacketizer(Separator separator, shared_ptr<RtpPacketizationConfig> rtpConfig,
-	                  uint16_t maxFragmentSize = H265NalUnits::defaultMaximumFragmentSize);
+	                  size_t maxFragmentSize = DefaultMaxFragmentSize);
 
-	// for backward compatibility
+	// For backward compatibility, do not use
 	[[deprecated]] H265RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig,
-	                  uint16_t maxFragmentSize = H265NalUnits::defaultMaximumFragmentSize);
-
-	void outgoing(message_vector &messages, const message_callback &send) override;
+	                  size_t maxFragmentSize = DefaultMaxFragmentSize);
 
 private:
-	shared_ptr<H265NalUnits> splitMessage(binary_ptr message);
+	std::vector<binary> fragment(binary data) override;
+	std::vector<H265NalUnit> splitFrame(const binary &frame);
 
-	const uint16_t maxFragmentSize;
-	const NalUnit::Separator separator;
+	const NalUnit::Separator mSeparator;
+	const size_t mMaxFragmentSize;
 };
 
 // For backward compatibility, do not use

+ 47 - 0
include/rtc/iceudpmuxlistener.hpp

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2025 Alex Potsides
+ * Copyright (c) 2025 Paul-Louis Ageneau
+ *
+ * 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/.
+ */
+
+#ifndef RTC_ICE_UDP_MUX_LISTENER_H
+#define RTC_ICE_UDP_MUX_LISTENER_H
+
+#include "common.hpp"
+
+namespace rtc {
+
+namespace impl {
+
+struct IceUdpMuxListener;
+
+} // namespace impl
+
+struct IceUdpMuxRequest { // TODO change name
+	string localUfrag;
+	string remoteUfrag;
+	string remoteAddress;
+	uint16_t remotePort;
+};
+
+class RTC_CPP_EXPORT IceUdpMuxListener final : private CheshireCat<impl::IceUdpMuxListener> {
+public:
+	IceUdpMuxListener(uint16_t port, optional<string> bindAddress = nullopt);
+	~IceUdpMuxListener();
+
+	void stop();
+
+	uint16_t port() const;
+
+	void OnUnhandledStunRequest(std::function<void(IceUdpMuxRequest)> callback);
+
+private:
+	using CheshireCat<impl::IceUdpMuxListener>::impl;
+};
+
+} // namespace rtc
+
+#endif

+ 20 - 4
include/rtc/message.hpp

@@ -46,11 +46,26 @@ inline size_t message_size_func(const message_ptr &m) {
 
 template <typename Iterator>
 message_ptr make_message(Iterator begin, Iterator end, Message::Type type = Message::Binary,
-                         unsigned int stream = 0, shared_ptr<Reliability> reliability = nullptr,
-                         shared_ptr<FrameInfo> frameInfo = nullptr) {
+                         unsigned int stream = 0, shared_ptr<Reliability> reliability = nullptr) {
 	auto message = std::make_shared<Message>(begin, end, type);
 	message->stream = stream;
 	message->reliability = reliability;
+	return message;
+}
+
+template <typename Iterator>
+message_ptr make_message(Iterator begin, Iterator end, shared_ptr<FrameInfo> frameInfo) {
+	auto message = std::make_shared<Message>(begin, end);
+	message->frameInfo = frameInfo;
+	return message;
+}
+
+// For backward compatibiity, do not use
+template <typename Iterator>
+[[deprecated]] message_ptr make_message(Iterator begin, Iterator end, Message::Type type,
+                         unsigned int stream, shared_ptr<FrameInfo> frameInfo) {
+	auto message = std::make_shared<Message>(begin, end, type);
+	message->stream = stream;
 	message->frameInfo = frameInfo;
 	return message;
 }
@@ -61,8 +76,9 @@ RTC_CPP_EXPORT message_ptr make_message(size_t size, Message::Type type = Messag
 
 RTC_CPP_EXPORT message_ptr make_message(binary &&data, Message::Type type = Message::Binary,
                                         unsigned int stream = 0,
-                                        shared_ptr<Reliability> reliability = nullptr,
-                                        shared_ptr<FrameInfo> frameInfo = nullptr);
+                                        shared_ptr<Reliability> reliability = nullptr);
+
+RTC_CPP_EXPORT message_ptr make_message(binary &&data, shared_ptr<FrameInfo> frameInfo);
 
 RTC_CPP_EXPORT message_ptr make_message(size_t size, message_ptr orig);
 

+ 27 - 57
include/rtc/nalunit.hpp

@@ -13,6 +13,7 @@
 
 #include "common.hpp"
 
+#include <vector>
 #include <cassert>
 
 namespace rtc {
@@ -61,15 +62,31 @@ enum NalUnitStartSequenceMatch {
 
 static const size_t H264_NAL_HEADER_SIZE = 1;
 static const size_t H265_NAL_HEADER_SIZE = 2;
-/// Nal unit
+
+struct NalUnitFragmentA;
+
+/// NAL unit
 struct RTC_CPP_EXPORT NalUnit : binary {
+	static std::vector<binary> GenerateFragments(const std::vector<NalUnit> &nalus,
+	                                             size_t maxFragmentSize);
+
+	enum class Separator {
+		Length = RTC_NAL_SEPARATOR_LENGTH, // first 4 bytes are NAL unit length
+		LongStartSequence = RTC_NAL_SEPARATOR_LONG_START_SEQUENCE,   // 0x00, 0x00, 0x00, 0x01
+		ShortStartSequence = RTC_NAL_SEPARATOR_SHORT_START_SEQUENCE, // 0x00, 0x00, 0x01
+		StartSequence = RTC_NAL_SEPARATOR_START_SEQUENCE, // LongStartSequence or ShortStartSequence
+	};
+
+	static NalUnitStartSequenceMatch StartSequenceMatchSucc(NalUnitStartSequenceMatch match,
+	                                                 std::byte _byte, Separator separator);
+
 	enum class Type { H264, H265 };
 
 	NalUnit(const NalUnit &unit) = default;
 	NalUnit(size_t size, bool includingHeader = true, Type type = Type::H264)
-	    : binary(size + (includingHeader
-	                         ? 0
-	                         : (type == Type::H264 ? H264_NAL_HEADER_SIZE : H265_NAL_HEADER_SIZE))) {}
+	    : binary(size + (includingHeader ? 0
+	                                     : (type == Type::H264 ? H264_NAL_HEADER_SIZE
+	                                                           : H265_NAL_HEADER_SIZE))) {}
 	NalUnit(binary &&data) : binary(std::move(data)) {}
 	NalUnit(Type type = Type::H264)
 	    : binary(type == Type::H264 ? H264_NAL_HEADER_SIZE : H265_NAL_HEADER_SIZE) {}
@@ -94,56 +111,7 @@ struct RTC_CPP_EXPORT NalUnit : binary {
 		insert(end(), payload.begin(), payload.end());
 	}
 
-	/// NAL unit separator
-	enum class Separator {
-		Length = RTC_NAL_SEPARATOR_LENGTH, // first 4 bytes are NAL unit length
-		LongStartSequence = RTC_NAL_SEPARATOR_LONG_START_SEQUENCE,   // 0x00, 0x00, 0x00, 0x01
-		ShortStartSequence = RTC_NAL_SEPARATOR_SHORT_START_SEQUENCE, // 0x00, 0x00, 0x01
-		StartSequence = RTC_NAL_SEPARATOR_START_SEQUENCE, // LongStartSequence or ShortStartSequence
-	};
-
-	static NalUnitStartSequenceMatch StartSequenceMatchSucc(NalUnitStartSequenceMatch match,
-	                                                        std::byte _byte, Separator separator) {
-		assert(separator != Separator::Length);
-		auto byte = (uint8_t)_byte;
-		auto detectShort =
-		    separator == Separator::ShortStartSequence || separator == Separator::StartSequence;
-		auto detectLong =
-		    separator == Separator::LongStartSequence || separator == Separator::StartSequence;
-		switch (match) {
-		case NUSM_noMatch:
-			if (byte == 0x00) {
-				return NUSM_firstZero;
-			}
-			break;
-		case NUSM_firstZero:
-			if (byte == 0x00) {
-				return NUSM_secondZero;
-			}
-			break;
-		case NUSM_secondZero:
-			if (byte == 0x00 && detectLong) {
-				return NUSM_thirdZero;
-			} else if (byte == 0x00 && detectShort) {
-				return NUSM_secondZero;
-			} else if (byte == 0x01 && detectShort) {
-				return NUSM_shortMatch;
-			}
-			break;
-		case NUSM_thirdZero:
-			if (byte == 0x00 && detectLong) {
-				return NUSM_thirdZero;
-			} else if (byte == 0x01 && detectLong) {
-				return NUSM_longMatch;
-			}
-			break;
-		case NUSM_shortMatch:
-			return NUSM_shortMatch;
-		case NUSM_longMatch:
-			return NUSM_longMatch;
-		}
-		return NUSM_noMatch;
-	}
+	std::vector<NalUnitFragmentA> generateFragments(size_t maxFragmentSize) const;
 
 protected:
 	const NalUnitHeader *header() const {
@@ -159,8 +127,9 @@ protected:
 
 /// Nal unit fragment A
 struct RTC_CPP_EXPORT NalUnitFragmentA : NalUnit {
-	static std::vector<shared_ptr<NalUnitFragmentA>> fragmentsFrom(shared_ptr<NalUnit> nalu,
-	                                                               uint16_t maxFragmentSize);
+	// For backward compatibility, do not use
+	[[deprecated]] static std::vector<shared_ptr<NalUnitFragmentA>>
+	fragmentsFrom(shared_ptr<NalUnit> nalu, uint16_t maxFragmentSize);
 
 	enum class FragmentType { Start, Middle, End };
 
@@ -212,7 +181,8 @@ protected:
 	}
 };
 
-class RTC_CPP_EXPORT NalUnits : public std::vector<shared_ptr<NalUnit>> {
+// For backward compatibility, do not use
+class [[deprecated]] RTC_CPP_EXPORT NalUnits : public std::vector<shared_ptr<NalUnit>> {
 public:
 	static const uint16_t defaultMaximumFragmentSize =
 	    uint16_t(RTC_DEFAULT_MTU - 12 - 8 - 40); // SRTP/UDP/IPv6

+ 13 - 2
include/rtc/peerconnection.hpp

@@ -35,6 +35,11 @@ struct RTC_CPP_EXPORT DataChannelInit {
 	string protocol = "";
 };
 
+struct RTC_CPP_EXPORT LocalDescriptionInit {
+    optional<string> iceUfrag;
+    optional<string> icePwd;
+};
+
 class RTC_CPP_EXPORT PeerConnection final : CheshireCat<impl::PeerConnection> {
 public:
 	enum class State : int {
@@ -81,6 +86,7 @@ public:
 	IceState iceState() const;
 	GatheringState gatheringState() const;
 	SignalingState signalingState() const;
+	bool negotiationNeeded() const;
 	bool hasMedia() const;
 	optional<Description> localDescription() const;
 	optional<Description> remoteDescription() const;
@@ -90,10 +96,14 @@ public:
 	uint16_t maxDataChannelId() const;
 	bool getSelectedCandidatePair(Candidate *local, Candidate *remote);
 
-	void setLocalDescription(Description::Type type = Description::Type::Unspec);
+	void setLocalDescription(Description::Type type = Description::Type::Unspec, LocalDescriptionInit init = {});
+	void gatherLocalCandidates(std::vector<IceServer> additionalIceServers = {});
 	void setRemoteDescription(Description description);
 	void addRemoteCandidate(Candidate candidate);
-	void gatherLocalCandidates(std::vector<IceServer> additionalIceServers = {});
+
+	// For specific use cases only
+	Description createOffer();
+	Description createAnswer();
 
 	void setMediaHandler(shared_ptr<MediaHandler> handler);
 	shared_ptr<MediaHandler> getMediaHandler();
@@ -113,6 +123,7 @@ public:
 	void onSignalingStateChange(std::function<void(SignalingState state)> callback);
 
 	void resetCallbacks();
+	CertificateFingerprint remoteFingerprint();
 
 	// Stats
 	void clearStats();

+ 35 - 0
include/rtc/rembhandler.hpp

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2024 Vladimir Voronin
+ *
+ * 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/.
+ */
+
+#ifndef RTC_REMB_RESPONDER_H
+#define RTC_REMB_RESPONDER_H
+
+#if RTC_ENABLE_MEDIA
+
+#include "mediahandler.hpp"
+#include "utils.hpp"
+
+namespace rtc {
+
+/// Responds to REMB messages sent by the receiver.
+class RTC_CPP_EXPORT RembHandler final : public MediaHandler {
+    rtc::synchronized_callback<unsigned int> mOnRemb;
+
+public:
+	/// Constructs the RembResponder object to notify whenever a bitrate
+	/// @param onRemb The callback that gets called whenever a bitrate by the receiver
+    RembHandler(std::function<void(unsigned int)> onRemb);
+
+	void incoming(message_vector &messages, const message_callback &send) override;
+};
+
+}
+
+#endif // RTC_ENABLE_MEDIA
+
+#endif // RTC_REMB_RESPONDER_H

+ 14 - 4
include/rtc/rtc.h

@@ -170,6 +170,7 @@ typedef void *(RTC_API *rtcInterceptorCallbackFunc)(int pc, const char *message,
 typedef void(RTC_API *rtcBufferedAmountLowCallbackFunc)(int id, void *ptr);
 typedef void(RTC_API *rtcAvailableCallbackFunc)(int id, void *ptr);
 typedef void(RTC_API *rtcPliHandlerCallbackFunc)(int tr, void *ptr);
+typedef void(RTC_API *rtcRembHandlerCallbackFunc)(int tr, unsigned int bitrate, void *ptr);
 
 // Log
 
@@ -210,7 +211,7 @@ RTC_C_EXPORT int rtcSetIceStateChangeCallback(int pc, rtcIceStateChangeCallbackF
 RTC_C_EXPORT int rtcSetGatheringStateChangeCallback(int pc, rtcGatheringStateCallbackFunc cb);
 RTC_C_EXPORT int rtcSetSignalingStateChangeCallback(int pc, rtcSignalingStateCallbackFunc cb);
 
-RTC_C_EXPORT int rtcSetLocalDescription(int pc, const char *type);
+RTC_C_EXPORT int rtcSetLocalDescription(int pc, const char *type); // type may be NULL
 RTC_C_EXPORT int rtcSetRemoteDescription(int pc, const char *sdp, const char *type);
 RTC_C_EXPORT int rtcAddRemoteCandidate(int pc, const char *cand, const char *mid);
 
@@ -220,12 +221,18 @@ RTC_C_EXPORT int rtcGetRemoteDescription(int pc, char *buffer, int size);
 RTC_C_EXPORT int rtcGetLocalDescriptionType(int pc, char *buffer, int size);
 RTC_C_EXPORT int rtcGetRemoteDescriptionType(int pc, char *buffer, int size);
 
+// For specific use cases only
+RTC_C_EXPORT int rtcCreateOffer(int pc, char *buffer, int size);
+RTC_C_EXPORT int rtcCreateAnswer(int pc, char *buffer, int size);
+
 RTC_C_EXPORT int rtcGetLocalAddress(int pc, char *buffer, int size);
 RTC_C_EXPORT int rtcGetRemoteAddress(int pc, char *buffer, int size);
 
 RTC_C_EXPORT int rtcGetSelectedCandidatePair(int pc, char *local, int localSize, char *remote,
                                              int remoteSize);
 
+RTC_C_EXPORT bool rtcIsNegotiationNeeded(int pc);
+
 RTC_C_EXPORT int rtcGetMaxDataChannelStream(int pc);
 RTC_C_EXPORT int rtcGetRemoteMaxMessageSize(int pc);
 
@@ -409,6 +416,9 @@ RTC_C_EXPORT int rtcChainRtcpNackResponder(int tr, unsigned int maxStoredPackets
 // Chain PliHandler on track
 RTC_C_EXPORT int rtcChainPliHandler(int tr, rtcPliHandlerCallbackFunc cb);
 
+// Chain RembHandler on track
+RTC_C_EXPORT int rtcChainRembHandler(int tr, rtcRembHandlerCallbackFunc cb);
+
 // Transform seconds to timestamp using track's clock rate, result is written to timestamp
 RTC_C_EXPORT int rtcTransformSecondsToTimestamp(int id, double seconds, uint32_t *timestamp);
 
@@ -424,9 +434,6 @@ RTC_C_EXPORT int rtcSetTrackRtpTimestamp(int id, uint32_t timestamp);
 // Get timestamp of last RTCP SR, result is written to timestamp
 RTC_C_EXPORT int rtcGetLastTrackSenderReportTimestamp(int id, uint32_t *timestamp);
 
-// Set NeedsToReport flag in RtcpSrReporter handler identified by given track id
-RTC_C_EXPORT int rtcSetNeedsToSendRtcpSr(int id);
-
 // Get all available payload types for given codec and stores them in buffer, does nothing if
 // buffer is NULL
 int rtcGetTrackPayloadTypesForCodec(int tr, const char *ccodec, int *buffer, int size);
@@ -444,6 +451,9 @@ int rtcGetSsrcsForType(const char *mediaType, const char *sdp, uint32_t *buffer,
 int rtcSetSsrcForType(const char *mediaType, const char *sdp, char *buffer, const int bufferSize,
                       rtcSsrcForTypeInit *init);
 
+// For backward compatibility, do not use
+RTC_C_EXPORT RTC_DEPRECATED int rtcSetNeedsToSendRtcpSr(int id);
+
 #endif // RTC_ENABLE_MEDIA
 
 #if RTC_ENABLE_WEBSOCKET

+ 3 - 0
include/rtc/rtc.hpp

@@ -16,6 +16,7 @@
 #include "datachannel.hpp"
 #include "peerconnection.hpp"
 #include "track.hpp"
+#include "iceudpmuxlistener.hpp"
 
 #if RTC_ENABLE_WEBSOCKET
 
@@ -33,8 +34,10 @@
 #include "h264rtppacketizer.hpp"
 #include "h264rtpdepacketizer.hpp"
 #include "h265rtppacketizer.hpp"
+#include "h265rtpdepacketizer.hpp"
 #include "mediahandler.hpp"
 #include "plihandler.hpp"
+#include "rembhandler.hpp"
 #include "pacinghandler.hpp"
 #include "rtcpnackresponder.hpp"
 #include "rtcpreceivingsession.hpp"

+ 4 - 4
include/rtc/rtcpnackresponder.hpp

@@ -33,8 +33,8 @@ private:
 
 		/// Packet storage element
 		struct RTC_CPP_EXPORT Element {
-			Element(binary_ptr packet, uint16_t sequenceNumber, shared_ptr<Element> next = nullptr);
-			const binary_ptr packet;
+			Element(message_ptr packet, uint16_t sequenceNumber, shared_ptr<Element> next = nullptr);
+			const message_ptr packet;
 			const uint16_t sequenceNumber;
 			/// Pointer to newer element
 			shared_ptr<Element> next = nullptr;
@@ -59,11 +59,11 @@ private:
 		Storage(size_t _maxSize);
 
 		/// Returns packet with given sequence number
-		optional<binary_ptr> get(uint16_t sequenceNumber);
+		message_ptr get(uint16_t sequenceNumber);
 
 		/// Stores packet
 		/// @param packet Packet
-		void store(binary_ptr packet);
+		void store(message_ptr packet);
 	};
 
 	const shared_ptr<Storage> mStorage;

+ 7 - 4
include/rtc/rtcpsrreporter.hpp

@@ -12,17 +12,20 @@
 #if RTC_ENABLE_MEDIA
 
 #include "mediahandler.hpp"
-#include "rtppacketizationconfig.hpp"
 #include "rtp.hpp"
+#include "rtppacketizationconfig.hpp"
+
+#include <chrono>
 
 namespace rtc {
 
 class RTC_CPP_EXPORT RtcpSrReporter final : public MediaHandler {
 public:
 	RtcpSrReporter(shared_ptr<RtpPacketizationConfig> rtpConfig);
+	~RtcpSrReporter();
 
 	uint32_t lastReportedTimestamp() const;
-	void setNeedsToReport();
+	[[deprecated]] void setNeedsToReport();
 
 	void outgoing(message_vector &messages, const message_callback &send) override;
 
@@ -30,13 +33,13 @@ public:
 	const shared_ptr<RtpPacketizationConfig> rtpConfig;
 
 private:
-	void addToReport(RtpHeader *rtp, uint32_t rtpSize);
+	void addToReport(RtpHeader *header, size_t size);
 	message_ptr getSenderReport(uint32_t timestamp);
 
 	uint32_t mPacketCount = 0;
 	uint32_t mPayloadOctets = 0;
 	uint32_t mLastReportedTimestamp = 0;
-	bool mNeedsToReport = false;
+	std::chrono::steady_clock::time_point mLastReportTime;
 };
 
 } // namespace rtc

+ 2 - 0
include/rtc/rtp.hpp

@@ -278,6 +278,8 @@ struct RTC_CPP_EXPORT RtcpRemb {
 	void preparePacket(SSRC senderSSRC, unsigned int numSSRC, unsigned int in_bitrate);
 	void setBitrate(unsigned int numSSRC, unsigned int in_bitrate);
 	void setSsrc(int iterator, SSRC newSssrc);
+	unsigned int getNumSSRC();
+	unsigned int getBitrate();
 };
 
 struct RTC_CPP_EXPORT RtcpPli {

+ 6 - 2
include/rtc/rtpdepacketizer.hpp

@@ -18,10 +18,14 @@ namespace rtc {
 
 class RTC_CPP_EXPORT RtpDepacketizer : public MediaHandler {
 public:
-	RtpDepacketizer() = default;
-	virtual ~RtpDepacketizer() = default;
+	RtpDepacketizer();
+	RtpDepacketizer(uint32_t clockRate);
+	virtual ~RtpDepacketizer();
 
 	virtual void incoming(message_vector &messages, const message_callback &send) override;
+
+private:
+	uint32_t mClockRate;
 };
 
 using OpusRtpDepacketizer = RtpDepacketizer;

+ 3 - 3
include/rtc/rtppacketizationconfig.hpp

@@ -73,11 +73,11 @@ public:
 	optional<DependencyDescriptorContext> dependencyDescriptorContext;
 	// the negotiated ID of the playout delay header extension
 	// https://webrtc.googlesource.com/src/+/main/docs/native-code/rtp-hdrext/playout-delay/README.md
-	uint8_t playoutDelayId;
+	uint8_t playoutDelayId = 0;
 
 	// Minimum/maxiumum playout delay, in 10ms intervals. A value of 10 would equal a 100ms delay
-	uint16_t playoutDelayMin;
-	uint16_t playoutDelayMax;
+	uint16_t playoutDelayMin = 0;
+	uint16_t playoutDelayMax = 0;
 
 	/// Construct RTP configuration used in packetization process
 	/// @param ssrc SSRC of source

+ 20 - 4
include/rtc/rtppacketizer.hpp

@@ -20,6 +20,12 @@ namespace rtc {
 /// RTP packetizer
 class RTC_CPP_EXPORT RtpPacketizer : public MediaHandler {
 public:
+	/// Default maximum fragment size (for video packetizers)
+	inline static const size_t DefaultMaxFragmentSize = RTC_DEFAULT_MAX_FRAGMENT_SIZE;
+
+	/// Clock rate for video in RTP
+	inline static const uint32_t VideoClockRate = 90 * 1000;
+
 	/// Constructs packetizer with given RTP configuration
 	/// @note RTP configuration is used in packetization process which may change some configuration
 	/// properties such as sequence number.
@@ -34,11 +40,19 @@ public:
 	const shared_ptr<RtpPacketizationConfig> rtpConfig;
 
 protected:
-	/// Creates RTP packet for given payload
-	/// @note This function increase sequence number after packetization.
+	/// Fragment data into payloads
+	/// Default implementation returns data as a single payload
+	/// @param message Input data
+	virtual std::vector<binary> fragment(binary data);
+
+	/// Creates an RTP packet for a payload
+	/// @note This function increases the sequence number.
 	/// @param payload RTP payload
-	/// @param setMark Set marker flag in RTP packet if true
-	virtual message_ptr packetize(shared_ptr<binary> payload, bool mark);
+	/// @param mark Set marker flag in RTP packet if true
+	virtual message_ptr packetize(const binary &payload, bool mark);
+
+	// For backward compatibility, do not use
+	[[deprecated]] virtual message_ptr packetize(shared_ptr<binary> payload, bool mark);
 
 private:
 	static const auto RtpHeaderSize = 12;
@@ -60,6 +74,8 @@ public:
 // Audio RTP packetizers
 using OpusRtpPacketizer = AudioRtpPacketizer<48000>;
 using AACRtpPacketizer = AudioRtpPacketizer<48000>;
+using PCMARtpPacketizer = AudioRtpPacketizer<8000>;
+using PCMURtpPacketizer = AudioRtpPacketizer<8000>;
 
 // Dummy wrapper for backward compatibility, do not use
 class RTC_CPP_EXPORT PacketizationHandler final : public MediaHandler {

+ 3 - 1
include/rtc/track.hpp

@@ -41,7 +41,9 @@ public:
 	bool isClosed(void) const override;
 	size_t maxMessageSize() const override;
 
-	void onFrame(std::function<void(binary data, FrameInfo frame)> callback);
+	void sendFrame(binary data, FrameInfo info);
+	void sendFrame(const byte *data, size_t size, FrameInfo info);
+	void onFrame(std::function<void(binary data, FrameInfo info)> callback);
 
 	bool requestKeyframe();
 	bool requestBitrate(unsigned int bitrate);

+ 3 - 3
include/rtc/version.h

@@ -2,8 +2,8 @@
 #define RTC_VERSION_H
 
 #define RTC_VERSION_MAJOR 0
-#define RTC_VERSION_MINOR 21
-#define RTC_VERSION_PATCH 1
-#define RTC_VERSION "0.21.1"
+#define RTC_VERSION_MINOR 22
+#define RTC_VERSION_PATCH 6
+#define RTC_VERSION "0.22.6"
 
 #endif

+ 34 - 3
pages/content/pages/reference.md

@@ -210,7 +210,9 @@ Initiates the handshake process. Following this call, the local description call
 Arguments:
 
 - `pc`: the Peer Connection identifier
-- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection.
+- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (recommended).
+
+Warning: This function expects the optional type for the local description and not an SDP description. It is not possible to set an existing SDP description.
 
 #### rtcSetRemoteDescription
 
@@ -223,7 +225,8 @@ Sets the remote description received from the remote peer by the user's method o
 Arguments:
 
 - `pc`: the Peer Connection identifier
-- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for autodetection.
+- `sdp`: the remote description in SDP format
+- `type` (optional): type of the description ("offer", "answer", "pranswer", or "rollback") or NULL for automatic (not recommended).
 
 If the remote description is an offer and `disableAutoNegotiation` was not set in `rtcConfiguration`, the library will automatically answer by calling `rtcSetLocalDescription` internally. Otherwise, the user must call it to answer the remote description.
 
@@ -299,7 +302,7 @@ Return value: the length of the string copied in buffer (including the terminati
 
 If `buffer` is `NULL`, the description is not copied but the size is still returned.
 
-#### rtcGetRemoteDescription
+#### rtcGetRemoteDescriptionType
 
 ```
 int rtcGetRemoteDescriptionType(int pc, char *buffer, int size)
@@ -317,6 +320,22 @@ Return value: the length of the string copied in buffer (including the terminati
 
 If `buffer` is `NULL`, the description is not copied but the size is still returned.
 
+#### rtcCreateOffer/rtcCreateAnswer
+
+```
+int rtcCreateOffer(int pc, char *buffer, int size)
+int rtcCreateAnswer(int pc, char *buffer, int size)
+```
+
+Create a local offer or answer description in SDP format. These functions are intended only for specific use cases where the application needs to generate a description without setting it. It is useless to call them before `rtcSetLocalDescription` as it doesn't expect the user to supply a description.
+
+- `pc`: the Peer Connection identifier
+- `buffer`: a user-supplied buffer to store the description
+- `size`: the size of `buffer`
+
+Return value: the length of the string copied in buffer (including the terminating null character) or a negative error code
+
+If `buffer` is `NULL`, the description is not copied but the size is still returned.
 
 #### rtcGetLocalAddress
 
@@ -374,6 +393,18 @@ Return value: the maximun length of strings copied in buffers (including the ter
 
 If `local`, `remote`, or both, are `NULL`, the corresponding candidate is not copied, but the maximum length is still returned.
 
+#### rtcIsNegotiationNeeded
+```
+bool rtcIsNegotiationNeeded(int pc);
+```
+
+Return true if negotiation needs to be started or restarted, for instance to signal new tracks. If so, the user may call `rtcSetLocalDescription()` to start it.
+
+Arguments:
+- `pc`: the Peer Connection identifier
+
+Return value: true if negotiation is needed
+
 #### rtcGetMaxDataChannelStream
 ```
 int rtcGetMaxDataChannelStream(int pc);

+ 70 - 91
src/av1rtppacketizer.cpp

@@ -12,6 +12,8 @@
 
 #include "impl/internals.hpp"
 
+#include <algorithm>
+
 namespace rtc {
 
 const auto payloadHeaderSize = 1;
@@ -38,34 +40,39 @@ const auto oneByteLeb128Size = 1;
 const uint8_t sevenLsbBitmask = 0b01111111;
 const uint8_t msbBitmask = 0b10000000;
 
-std::vector<binary_ptr> extractTemporalUnitObus(binary_ptr message) {
-	std::vector<shared_ptr<binary>> obus{};
+AV1RtpPacketizer::AV1RtpPacketizer(Packetization packetization,
+                                   shared_ptr<RtpPacketizationConfig> rtpConfig,
+                                   size_t maxFragmentSize)
+    : RtpPacketizer(rtpConfig), mPacketization(packetization), mMaxFragmentSize(maxFragmentSize) {}
+
+std::vector<binary> AV1RtpPacketizer::extractTemporalUnitObus(const binary &data) {
+	std::vector<binary> obus;
 
-	if (message->size() <= 2 || (message->at(0) != obuTemporalUnitDelimiter.at(0)) ||
-	    (message->at(1) != obuTemporalUnitDelimiter.at(1))) {
-		return obus;
+	if (data.size() <= 2 || (data.at(0) != obuTemporalUnitDelimiter.at(0)) ||
+	    (data.at(1) != obuTemporalUnitDelimiter.at(1))) {
+		return {};
 	}
 
-	size_t messageIndex = 2;
-	while (messageIndex < message->size()) {
-		if ((message->at(messageIndex) & obuHasSizeMask) == byte(0)) {
+	size_t index = 2;
+	while (index < data.size()) {
+		if ((data.at(index) & obuHasSizeMask) == byte(0)) {
 			return obus;
 		}
 
-		if ((message->at(messageIndex) & obuHasExtensionMask) != byte(0)) {
-			messageIndex++;
+		if ((data.at(index) & obuHasExtensionMask) != byte(0)) {
+			index++;
 		}
 
 		// https://aomediacodec.github.io/av1-spec/#leb128
 		uint32_t obuLength = 0;
 		uint8_t leb128Size = 0;
 		while (leb128Size < 8) {
-			auto leb128Index = messageIndex + leb128Size + obuHeaderSize;
-			if (message->size() < leb128Index) {
+			auto leb128Index = index + leb128Size + obuHeaderSize;
+			if (data.size() < leb128Index) {
 				break;
 			}
 
-			auto leb128_byte = uint8_t(message->at(leb128Index));
+			auto leb128_byte = uint8_t(data.at(leb128Index));
 
 			obuLength |= ((leb128_byte & sevenLsbBitmask) << (leb128Size * 7));
 			leb128Size++;
@@ -75,16 +82,31 @@ std::vector<binary_ptr> extractTemporalUnitObus(binary_ptr message) {
 			}
 		}
 
-		obus.push_back(std::make_shared<binary>(message->begin() + messageIndex,
-		                                        message->begin() + messageIndex + obuHeaderSize +
-		                                            leb128Size + obuLength));
+		obus.emplace_back(data.begin() + index,
+		                  data.begin() + index + obuHeaderSize + leb128Size + obuLength);
 
-		messageIndex += obuHeaderSize + leb128Size + obuLength;
+		index += obuHeaderSize + leb128Size + obuLength;
 	}
 
 	return obus;
 }
 
+std::vector<binary> AV1RtpPacketizer::fragment(binary data) {
+	if (mPacketization == AV1RtpPacketizer::Packetization::TemporalUnit) {
+		std::vector<binary> result;
+		auto obus = extractTemporalUnitObus(data);
+		for (auto obu : obus) {
+			auto fragments = fragmentObu(obu);
+			result.reserve(result.size() + fragments.size());
+			for(auto &fragment : fragments)
+				fragments.push_back(std::move(fragment));
+		}
+		return result;
+	} else {
+		return fragmentObu(data);
+	}
+}
+
 /*
  *  0 1 2 3 4 5 6 7
  * +-+-+-+-+-+-+-+-+
@@ -116,114 +138,71 @@ std::vector<binary_ptr> extractTemporalUnitObus(binary_ptr message) {
  *
  **/
 
-std::vector<binary_ptr> AV1RtpPacketizer::packetizeObu(binary_ptr message,
-                                                       uint16_t maxFragmentSize) {
-
-	std::vector<shared_ptr<binary>> payloads{};
-	size_t messageIndex = 0;
+std::vector<binary> AV1RtpPacketizer::fragmentObu(const binary &data) {
+	std::vector<binary> payloads;
 
-	if (message->size() < 1) {
-		return payloads;
-	}
+	if (data.size() < 1)
+		return {};
 
 	// Cache sequence header and packetize with next OBU
-	auto frameType = (message->at(0) & obuFrameTypeMask) >> obuFrameTypeBitshift;
+	auto frameType = (data.at(0) & obuFrameTypeMask) >> obuFrameTypeBitshift;
 	if (frameType == obuFrameTypeSequenceHeader) {
-		sequenceHeader = std::make_shared<binary>(message->begin(), message->end());
-		return payloads;
+		mSequenceHeader = std::make_unique<binary>(data.begin(), data.end());
+		return {};
 	}
 
-	size_t messageRemaining = message->size();
-	while (messageRemaining > 0) {
-		auto obuCount = 1;
-		auto metadataSize = payloadHeaderSize;
+	size_t index = 0;
+	size_t remaining = data.size();
+	while (remaining > 0) {
+		size_t obuCount = 1;
+		size_t metadataSize = payloadHeaderSize;
 
-		if (sequenceHeader != nullptr) {
+		if (mSequenceHeader) {
 			obuCount++;
-			metadataSize += /* 1 byte leb128 */ 1 + int(sequenceHeader->size());
+			metadataSize += 1 + int(mSequenceHeader->size()); // 1 byte leb128
 		}
 
-		auto payload = std::make_shared<binary>(
-		    std::min(size_t(maxFragmentSize), messageRemaining + metadataSize));
-		auto payloadOffset = payloadHeaderSize;
+		binary payload(std::min(size_t(mMaxFragmentSize), remaining + metadataSize));
+		size_t payloadOffset = payloadHeaderSize;
 
-		payload->at(0) = byte(obuCount) << wBitshift;
+		payload.at(0) = byte(obuCount) << wBitshift;
 
 		// Packetize cached SequenceHeader
 		if (obuCount == 2) {
-			payload->at(0) ^= nMask;
-			payload->at(1) = byte(sequenceHeader->size() & sevenLsbBitmask);
+			payload.at(0) ^= nMask;
+			payload.at(1) = byte(mSequenceHeader->size() & sevenLsbBitmask);
 			payloadOffset += oneByteLeb128Size;
 
-			std::memcpy(payload->data() + payloadOffset, sequenceHeader->data(),
-			            sequenceHeader->size());
-			payloadOffset += int(sequenceHeader->size());
+			std::memcpy(payload.data() + payloadOffset, mSequenceHeader->data(),
+			            mSequenceHeader->size());
+			payloadOffset += int(mSequenceHeader->size());
 
-			sequenceHeader = nullptr;
+			mSequenceHeader = nullptr;
 		}
 
 		// Copy as much of OBU as possible into Payload
-		auto payloadRemaining = payload->size() - payloadOffset;
-		std::memcpy(payload->data() + payloadOffset, message->data() + messageIndex,
+		size_t payloadRemaining = payload.size() - payloadOffset;
+		std::memcpy(payload.data() + payloadOffset, data.data() + index,
 		            payloadRemaining);
-		messageRemaining -= payloadRemaining;
-		messageIndex += payloadRemaining;
+		remaining -= payloadRemaining;
+		index += payloadRemaining;
 
 		// Does this Fragment contain an OBU that started in a previous payload
 		if (payloads.size() > 0) {
-			payload->at(0) ^= zMask;
+			payload.at(0) ^= zMask;
 		}
 
 		// This OBU will be continued in next Payload
-		if (messageIndex < message->size()) {
-			payload->at(0) ^= yMask;
+		if (index < data.size()) {
+			payload.at(0) ^= yMask;
 		}
 
-		payloads.push_back(payload);
+		payloads.push_back(std::move(payload));
 	}
 
 	return payloads;
 }
 
-AV1RtpPacketizer::AV1RtpPacketizer(AV1RtpPacketizer::Packetization packetization,
-                                   shared_ptr<RtpPacketizationConfig> rtpConfig,
-                                   uint16_t maxFragmentSize)
-    : RtpPacketizer(rtpConfig), maxFragmentSize(maxFragmentSize), packetization(packetization) {}
-
-void AV1RtpPacketizer::outgoing(message_vector &messages,
-                                [[maybe_unused]] const message_callback &send) {
-	message_vector result;
-	for (const auto &message : messages) {
-		std::vector<binary_ptr> obus;
-		if (packetization == AV1RtpPacketizer::Packetization::TemporalUnit) {
-			obus = extractTemporalUnitObus(message);
-		} else {
-			obus.push_back(message);
-		}
-
-		std::vector<binary_ptr> fragments;
-		for (auto obu : obus) {
-			auto p = packetizeObu(obu, maxFragmentSize);
-			fragments.insert(fragments.end(), p.begin(), p.end());
-		}
-
-		if (fragments.size() == 0)
-			continue;
-
-		for (size_t i = 0; i < fragments.size(); i++) {
-			if (rtpConfig->dependencyDescriptorContext.has_value()) {
-				auto &ctx = *rtpConfig->dependencyDescriptorContext;
-				ctx.descriptor.startOfFrame = i == 0;
-				ctx.descriptor.endOfFrame = i == fragments.size() - 1;
-			}
-			bool mark = i == fragments.size() - 1;
-			result.push_back(packetize(fragments[i], mark));
-		}
-	}
-
-	messages.swap(result);
-}
-
 } // namespace rtc
 
 #endif /* RTC_ENABLE_MEDIA */

+ 45 - 13
src/capi.cpp

@@ -631,6 +631,24 @@ int rtcGetRemoteDescriptionType(int pc, char *buffer, int size) {
 	});
 }
 
+int rtcCreateOffer(int pc, char *buffer, int size) {
+	return wrap([&] {
+		auto peerConnection = getPeerConnection(pc);
+
+		auto desc = peerConnection->createOffer();
+		return copyAndReturn(string(desc), buffer, size);
+	});
+}
+
+int rtcCreateAnswer(int pc, char *buffer, int size) {
+	return wrap([&] {
+		auto peerConnection = getPeerConnection(pc);
+
+		auto desc = peerConnection->createAnswer();
+		return copyAndReturn(string(desc), buffer, size);
+	});
+}
+
 int rtcGetLocalAddress(int pc, char *buffer, int size) {
 	return wrap([&] {
 		auto peerConnection = getPeerConnection(pc);
@@ -674,6 +692,11 @@ int rtcGetSelectedCandidatePair(int pc, char *local, int localSize, char *remote
 	});
 }
 
+bool rtcIsNegotiationNeeded(int pc) {
+	return wrap([&] { return getPeerConnection(pc)->negotiationNeeded() ? 0 : 1; }) == 0 ? true
+	                                                                                     : false;
+}
+
 int rtcGetMaxDataChannelStream(int pc) {
 	return wrap([&] {
 		auto peerConnection = getPeerConnection(pc);
@@ -1331,6 +1354,18 @@ int rtcChainPliHandler(int tr, rtcPliHandlerCallbackFunc cb) {
 	});
 }
 
+int rtcChainRembHandler(int tr, rtcRembHandlerCallbackFunc cb) {
+	return wrap([&] {
+		auto track = getTrack(tr);
+		auto handler = std::make_shared<RembHandler>([tr, cb](unsigned int bitrate) {
+			if (auto ptr = getUserPointer(tr))
+				cb(tr, bitrate, *ptr);
+		});
+		track->chainMediaHandler(handler);
+		return RTC_ERR_SUCCESS;
+	});
+}
+
 int rtcTransformSecondsToTimestamp(int id, double seconds, uint32_t *timestamp) {
 	return wrap([&] {
 		auto config = getRtpConfig(id);
@@ -1379,14 +1414,6 @@ int rtcGetLastTrackSenderReportTimestamp(int id, uint32_t *timestamp) {
 	});
 }
 
-int rtcSetNeedsToSendRtcpSr(int id) {
-	return wrap([id] {
-		auto sender = getRtcpSrReporter(id);
-		sender->setNeedsToReport();
-		return RTC_ERR_SUCCESS;
-	});
-}
-
 int rtcGetTrackPayloadTypesForCodec(int tr, const char *ccodec, int *buffer, int size) {
 	return wrap([&] {
 		auto track = getTrack(tr);
@@ -1428,7 +1455,7 @@ int rtcGetSsrcsForType(const char *mediaType, const char *sdp, uint32_t *buffer,
 		auto oldSDP = string(sdp);
 		auto description = Description(oldSDP, "unspec");
 		auto mediaCount = description.mediaCount();
-		for (unsigned int i = 0; i < mediaCount; i++) {
+		for (int i = 0; i < mediaCount; i++) {
 			if (std::holds_alternative<Description::Media *>(description.media(i))) {
 				auto media = std::get<Description::Media *>(description.media(i));
 				auto currentMediaType = lowercased(media->type());
@@ -1449,7 +1476,7 @@ int rtcSetSsrcForType(const char *mediaType, const char *sdp, char *buffer, cons
 		auto prevSDP = string(sdp);
 		auto description = Description(prevSDP, "unspec");
 		auto mediaCount = description.mediaCount();
-		for (unsigned int i = 0; i < mediaCount; i++) {
+		for (int i = 0; i < mediaCount; i++) {
 			if (std::holds_alternative<Description::Media *>(description.media(i))) {
 				auto media = std::get<Description::Media *>(description.media(i));
 				auto currentMediaType = lowercased(media->type());
@@ -1463,6 +1490,11 @@ int rtcSetSsrcForType(const char *mediaType, const char *sdp, char *buffer, cons
 	});
 }
 
+int rtcSetNeedsToSendRtcpSr(int) {
+	// Dummy
+	return RTC_ERR_SUCCESS;
+}
+
 #endif // RTC_ENABLE_MEDIA
 
 #if RTC_ENABLE_WEBSOCKET
@@ -1546,7 +1578,7 @@ int rtcGetWebSocketPath(int ws, char *buffer, int size) {
 	});
 }
 
-RTC_C_EXPORT int rtcCreateWebSocketServer(const rtcWsServerConfiguration *config,
+int rtcCreateWebSocketServer(const rtcWsServerConfiguration *config,
                                           rtcWebSocketClientCallbackFunc cb) {
 	return wrap([&] {
 		if (!config)
@@ -1583,7 +1615,7 @@ RTC_C_EXPORT int rtcCreateWebSocketServer(const rtcWsServerConfiguration *config
 	});
 }
 
-RTC_C_EXPORT int rtcDeleteWebSocketServer(int wsserver) {
+int rtcDeleteWebSocketServer(int wsserver) {
 	return wrap([&] {
 		auto webSocketServer = getWebSocketServer(wsserver);
 		webSocketServer->onClient(nullptr);
@@ -1593,7 +1625,7 @@ RTC_C_EXPORT int rtcDeleteWebSocketServer(int wsserver) {
 	});
 }
 
-RTC_C_EXPORT int rtcGetWebSocketServerPort(int wsserver) {
+int rtcGetWebSocketServerPort(int wsserver) {
 	return wrap([&] {
 		auto webSocketServer = getWebSocketServer(wsserver);
 		return int(webSocketServer->port());

+ 30 - 26
src/description.cpp

@@ -216,6 +216,21 @@ void Description::hintType(Type type) {
 		mType = type;
 }
 
+void Description::addIceOption(string option) {
+	if (std::find(mIceOptions.begin(), mIceOptions.end(), option) == mIceOptions.end())
+		mIceOptions.emplace_back(std::move(option));
+}
+
+void Description::removeIceOption(const string &option) {
+	mIceOptions.erase(std::remove(mIceOptions.begin(), mIceOptions.end(), option),
+	                  mIceOptions.end());
+}
+
+void Description::setIceAttribute(string ufrag, string pwd) {
+	mIceUfrag = std::move(ufrag);
+	mIcePwd = std::move(pwd);
+}
+
 void Description::setFingerprint(CertificateFingerprint f) {
 	if (!f.isValid())
 		throw std::invalid_argument("Invalid " +
@@ -227,16 +242,6 @@ void Description::setFingerprint(CertificateFingerprint f) {
 	mFingerprint = std::move(f);
 }
 
-void Description::addIceOption(string option) {
-	if (std::find(mIceOptions.begin(), mIceOptions.end(), option) == mIceOptions.end())
-		mIceOptions.emplace_back(std::move(option));
-}
-
-void Description::removeIceOption(const string &option) {
-	mIceOptions.erase(std::remove(mIceOptions.begin(), mIceOptions.end(), option),
-	                  mIceOptions.end());
-}
-
 std::vector<string> Description::Entry::attributes() const { return mAttributes; }
 
 void Description::Entry::addAttribute(string attr) {
@@ -310,7 +315,6 @@ string Description::generateSdp(string_view eol) const {
 
 	// Session-level attributes
 	sdp << "a=msid-semantic:WMS *" << eol;
-
 	if (!mIceOptions.empty())
 		sdp << "a=ice-options:" << utils::implode(mIceOptions, ',') << eol;
 	if (mFingerprint)
@@ -372,28 +376,29 @@ string Description::generateApplicationSdp(string_view eol) const {
 	const uint16_t port =
 	    cand && cand->isResolved() ? *cand->port() : 9; // Port 9 is the discard protocol
 
+	// Session-level attributes
+	sdp << "a=msid-semantic:WMS *" << eol;
+	if (!mIceOptions.empty())
+		sdp << "a=ice-options:" << utils::implode(mIceOptions, ',') << eol;
+
+	for (const auto &attr : mAttributes)
+		sdp << "a=" << attr << eol;
+
 	// Application
 	auto app = mApplication ? mApplication : std::make_shared<Application>();
 	sdp << app->generateSdp(eol, addr, port);
 
-	// Session-level attributes
-	sdp << "a=msid-semantic:WMS *" << eol;
+	// Media-level attributes
 	sdp << "a=setup:" << mRole << eol;
-
 	if (mIceUfrag)
 		sdp << "a=ice-ufrag:" << *mIceUfrag << eol;
 	if (mIcePwd)
 		sdp << "a=ice-pwd:" << *mIcePwd << eol;
-	if (!mIceOptions.empty())
-		sdp << "a=ice-options:" << utils::implode(mIceOptions, ',') << eol;
 	if (mFingerprint)
 		sdp << "a=fingerprint:"
 		    << CertificateFingerprint::AlgorithmIdentifier(mFingerprint->algorithm) << " "
 		    << mFingerprint->value << eol;
 
-	for (const auto &attr : mAttributes)
-		sdp << "a=" << attr << eol;
-
 	// Candidates
 	for (const auto &candidate : mCandidates)
 		sdp << string(candidate) << eol;
@@ -493,8 +498,8 @@ void Description::clearMedia() {
 	mApplication.reset();
 }
 
-variant<Description::Media *, Description::Application *> Description::media(unsigned int index) {
-	if (index >= mEntries.size())
+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];
@@ -514,9 +519,8 @@ variant<Description::Media *, Description::Application *> Description::media(uns
 	}
 }
 
-variant<const Description::Media *, const Description::Application *>
-Description::media(unsigned int index) const {
-	if (index >= mEntries.size())
+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];
@@ -536,7 +540,7 @@ Description::media(unsigned int index) const {
 	}
 }
 
-unsigned int Description::mediaCount() const { return unsigned(mEntries.size()); }
+int Description::mediaCount() const { return int(mEntries.size()); }
 
 Description::Entry::Entry(const string &mline, string mid, Direction dir)
     : mMid(std::move(mid)), mDirection(dir) {
@@ -1340,7 +1344,7 @@ std::string CertificateFingerprint::AlgorithmIdentifier(
 	case CertificateFingerprint::Algorithm::Sha256:
 		return "sha-256";
 	case CertificateFingerprint::Algorithm::Sha384:
-		return "sha-256";
+		return "sha-384";
 	case CertificateFingerprint::Algorithm::Sha512:
 		return "sha-512";
 	default:

+ 1 - 1
src/global.cpp

@@ -88,7 +88,7 @@ std::shared_future<void> Cleanup() { return impl::Init::Instance().cleanup(); }
 
 void SetSctpSettings(SctpSettings s) { impl::Init::Instance().setSctpSettings(std::move(s)); }
 
-RTC_CPP_EXPORT std::ostream &operator<<(std::ostream &out, LogLevel level) {
+std::ostream &operator<<(std::ostream &out, LogLevel level) {
 	switch (level) {
 	case LogLevel::Fatal:
 		out << "fatal";

+ 7 - 5
src/h264rtpdepacketizer.cpp

@@ -14,6 +14,7 @@
 #include "impl/internals.hpp"
 
 #include <algorithm>
+#include <chrono>
 
 namespace rtc {
 
@@ -43,9 +44,8 @@ void H264RtpDepacketizer::addSeparator(binary &accessUnit) {
 message_vector H264RtpDepacketizer::buildFrames(message_vector::iterator begin,
                                                 message_vector::iterator end, uint8_t payloadType,
                                                 uint32_t timestamp) {
-	message_vector out = {};
-	auto accessUnit = binary{};
-	auto frameInfo = std::make_shared<FrameInfo>(payloadType, timestamp);
+	message_vector out;
+	binary accessUnit;
 
 	for (auto it = begin; it != end; ++it) {
 		auto pkt = it->get();
@@ -109,8 +109,10 @@ message_vector H264RtpDepacketizer::buildFrames(message_vector::iterator begin,
 	}
 
 	if (!accessUnit.empty()) {
-		out.emplace_back(
-		    make_message(std::move(accessUnit), Message::Binary, 0, nullptr, frameInfo));
+		auto frameInfo = std::make_shared<FrameInfo>(timestamp);
+		frameInfo->timestampSeconds = std::chrono::duration<double>(double(timestamp) / double(ClockRate));
+		frameInfo->payloadType = payloadType;
+		out.emplace_back(make_message(std::move(accessUnit), frameInfo));
 	}
 
 	return out;

+ 35 - 49
src/h264rtppacketizer.cpp

@@ -23,37 +23,50 @@
 
 namespace rtc {
 
-shared_ptr<NalUnits> H264RtpPacketizer::splitMessage(binary_ptr message) {
-	auto nalus = std::make_shared<NalUnits>();
-	if (separator == Separator::Length) {
+H264RtpPacketizer::H264RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig,
+                                     size_t maxFragmentSize)
+    : RtpPacketizer(std::move(rtpConfig)), mSeparator(Separator::Length), mMaxFragmentSize(maxFragmentSize) {}
+
+H264RtpPacketizer::H264RtpPacketizer(Separator separator,
+                                     shared_ptr<RtpPacketizationConfig> rtpConfig,
+                                     size_t maxFragmentSize)
+    : RtpPacketizer(rtpConfig), mSeparator(separator), mMaxFragmentSize(maxFragmentSize) {}
+
+std::vector<binary> H264RtpPacketizer::fragment(binary data) {
+	return NalUnit::GenerateFragments(splitFrame(data), mMaxFragmentSize);
+}
+
+std::vector<NalUnit> H264RtpPacketizer::splitFrame(const binary &frame) {
+	std::vector<NalUnit> nalus;
+	if (mSeparator == Separator::Length) {
 		size_t index = 0;
-		while (index < message->size()) {
-			assert(index + 4 < message->size());
-			if (index + 4 >= message->size()) {
+		while (index < frame.size()) {
+			assert(index + 4 < frame.size());
+			if (index + 4 >= frame.size()) {
 				LOG_WARNING << "Invalid NAL Unit data (incomplete length), ignoring!";
 				break;
 			}
 			uint32_t length;
-			std::memcpy(&length, message->data() + index, sizeof(uint32_t));
+			std::memcpy(&length, frame.data() + index, sizeof(uint32_t));
 			length = ntohl(length);
 			auto naluStartIndex = index + 4;
 			auto naluEndIndex = naluStartIndex + length;
 
-			assert(naluEndIndex <= message->size());
-			if (naluEndIndex > message->size()) {
+			assert(naluEndIndex <= frame.size());
+			if (naluEndIndex > frame.size()) {
 				LOG_WARNING << "Invalid NAL Unit data (incomplete unit), ignoring!";
 				break;
 			}
-			auto begin = message->begin() + naluStartIndex;
-			auto end = message->begin() + naluEndIndex;
-			nalus->push_back(std::make_shared<NalUnit>(begin, end));
+			auto begin = frame.begin() + naluStartIndex;
+			auto end = frame.begin() + naluEndIndex;
+			nalus.emplace_back(begin, end);
 			index = naluEndIndex;
 		}
 	} else {
 		NalUnitStartSequenceMatch match = NUSM_noMatch;
 		size_t index = 0;
-		while (index < message->size()) {
-			match = NalUnit::StartSequenceMatchSucc(match, (*message)[index++], separator);
+		while (index < frame.size()) {
+			match = NalUnit::StartSequenceMatchSucc(match, frame[index++], mSeparator);
 			if (match == NUSM_longMatch || match == NUSM_shortMatch) {
 				match = NUSM_noMatch;
 				break;
@@ -62,53 +75,26 @@ shared_ptr<NalUnits> H264RtpPacketizer::splitMessage(binary_ptr message) {
 
 		size_t naluStartIndex = index;
 
-		while (index < message->size()) {
-			match = NalUnit::StartSequenceMatchSucc(match, (*message)[index], separator);
+		while (index < frame.size()) {
+			match = NalUnit::StartSequenceMatchSucc(match, frame[index], mSeparator);
 			if (match == NUSM_longMatch || match == NUSM_shortMatch) {
 				auto sequenceLength = match == NUSM_longMatch ? 4 : 3;
 				size_t naluEndIndex = index - sequenceLength;
 				match = NUSM_noMatch;
-				auto begin = message->begin() + naluStartIndex;
-				auto end = message->begin() + naluEndIndex + 1;
-				nalus->push_back(std::make_shared<NalUnit>(begin, end));
+				auto begin = frame.begin() + naluStartIndex;
+				auto end = frame.begin() + naluEndIndex + 1;
+				nalus.emplace_back(begin, end);
 				naluStartIndex = index + 1;
 			}
 			index++;
 		}
-		auto begin = message->begin() + naluStartIndex;
-		auto end = message->end();
-		nalus->push_back(std::make_shared<NalUnit>(begin, end));
+		auto begin = frame.begin() + naluStartIndex;
+		auto end = frame.end();
+		nalus.emplace_back(begin, end);
 	}
 	return nalus;
 }
 
-H264RtpPacketizer::H264RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig,
-                                     uint16_t maxFragmentSize)
-    : RtpPacketizer(std::move(rtpConfig)), maxFragmentSize(maxFragmentSize),
-      separator(Separator::Length) {}
-
-H264RtpPacketizer::H264RtpPacketizer(Separator separator,
-                                     shared_ptr<RtpPacketizationConfig> rtpConfig,
-                                     uint16_t maxFragmentSize)
-    : RtpPacketizer(rtpConfig), maxFragmentSize(maxFragmentSize), separator(separator) {}
-
-void H264RtpPacketizer::outgoing(message_vector &messages, [[maybe_unused]] const message_callback &send) {
-	message_vector result;
-	for(const auto &message : messages) {
-		auto nalus = splitMessage(message);
-		auto fragments = nalus->generateFragments(maxFragmentSize);
-		if (fragments.size() == 0)
-			continue;
-
-		for (size_t i = 0; i < fragments.size() - 1; i++)
-			result.push_back(packetize(fragments[i], false));
-
-		result.push_back(packetize(fragments[fragments.size() - 1], true));
-	}
-
-	messages.swap(result);
-}
-
 } // namespace rtc
 
 #endif /* RTC_ENABLE_MEDIA */

+ 60 - 34
src/h265nalunit.cpp

@@ -16,35 +16,39 @@
 
 namespace rtc {
 
-H265NalUnitFragment::H265NalUnitFragment(FragmentType type, bool forbiddenBit, uint8_t nuhLayerId,
-                                         uint8_t nuhTempIdPlus1, uint8_t unitType, binary data)
-    : H265NalUnit(data.size() + H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE) {
-	setForbiddenBit(forbiddenBit);
-	setNuhLayerId(nuhLayerId);
-	setNuhTempIdPlus1(nuhTempIdPlus1);
-	fragmentIndicator()->setUnitType(H265NalUnitFragment::nal_type_fu);
-	setFragmentType(type);
-	setUnitType(unitType);
-	copy(data.begin(), data.end(), begin() + H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE);
+std::vector<binary> H265NalUnit::GenerateFragments(const std::vector<H265NalUnit> &nalus,
+                                                   size_t maxFragmentSize) {
+	std::vector<binary> result;
+	for (auto nalu : nalus) {
+		if (nalu.size() > maxFragmentSize) {
+			auto fragments = nalu.generateFragments(maxFragmentSize);
+			result.insert(result.end(), fragments.begin(), fragments.end());
+		} else {
+			// TODO: check
+			result.push_back(nalu);
+		}
+	}
+	return result;
 }
 
-std::vector<shared_ptr<H265NalUnitFragment>>
-H265NalUnitFragment::fragmentsFrom(shared_ptr<H265NalUnit> nalu, uint16_t maxFragmentSize) {
-	assert(nalu->size() > maxFragmentSize);
-	auto fragments_count = ceil(double(nalu->size()) / maxFragmentSize);
-	maxFragmentSize = uint16_t(int(ceil(nalu->size() / fragments_count)));
+std::vector<H265NalUnitFragment> H265NalUnit::generateFragments(size_t maxFragmentSize) const {
+	// TODO: check
+	assert(size() > maxFragmentSize);
+	auto fragments_count = ceil(double(size()) / maxFragmentSize);
+	maxFragmentSize = uint16_t(int(ceil(size() / fragments_count)));
 
 	// 3 bytes for FU indicator and FU header
 	maxFragmentSize -= (H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE);
-	auto f = nalu->forbiddenBit();
-	uint8_t nuhLayerId = nalu->nuhLayerId() & 0x3F;        // 6 bits
-	uint8_t nuhTempIdPlus1 = nalu->nuhTempIdPlus1() & 0x7; // 3 bits
-	uint8_t naluType = nalu->unitType() & 0x3F;            // 6 bits
-	auto payload = nalu->payload();
-	vector<shared_ptr<H265NalUnitFragment>> result{};
+	bool forbiddenBit = this->forbiddenBit();
+	uint8_t nuhLayerId = this->nuhLayerId() & 0x3F;        // 6 bits
+	uint8_t nuhTempIdPlus1 = this->nuhTempIdPlus1() & 0x7; // 3 bits
+	uint8_t naluType = this->unitType() & 0x3F;            // 6 bits
+	auto payload = this->payload();
+	vector<H265NalUnitFragment> result;
 	uint64_t offset = 0;
 	while (offset < payload.size()) {
 		vector<byte> fragmentData;
+		using FragmentType = H265NalUnitFragment::FragmentType;
 		FragmentType fragmentType;
 		if (offset == 0) {
 			fragmentType = FragmentType::Start;
@@ -57,14 +61,36 @@ H265NalUnitFragment::fragmentsFrom(shared_ptr<H265NalUnit> nalu, uint16_t maxFra
 			fragmentType = FragmentType::End;
 		}
 		fragmentData = {payload.begin() + offset, payload.begin() + offset + maxFragmentSize};
-		auto fragment = std::make_shared<H265NalUnitFragment>(
-		    fragmentType, f, nuhLayerId, nuhTempIdPlus1, naluType, fragmentData);
-		result.push_back(fragment);
+		result.emplace_back(fragmentType, forbiddenBit, nuhLayerId, nuhTempIdPlus1, naluType,
+		                    fragmentData);
 		offset += maxFragmentSize;
 	}
 	return result;
 }
 
+H265NalUnitFragment::H265NalUnitFragment(FragmentType type, bool forbiddenBit, uint8_t nuhLayerId,
+                                         uint8_t nuhTempIdPlus1, uint8_t unitType, binary data)
+    : H265NalUnit(data.size() + H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE) {
+	setForbiddenBit(forbiddenBit);
+	setNuhLayerId(nuhLayerId);
+	setNuhTempIdPlus1(nuhTempIdPlus1);
+	fragmentIndicator()->setUnitType(H265NalUnitFragment::nal_type_fu);
+	setFragmentType(type);
+	setUnitType(unitType);
+	copy(data.begin(), data.end(), begin() + H265_NAL_HEADER_SIZE + H265_FU_HEADER_SIZE);
+}
+
+std::vector<shared_ptr<H265NalUnitFragment>>
+H265NalUnitFragment::fragmentsFrom(shared_ptr<H265NalUnit> nalu, uint16_t maxFragmentSize) {
+	auto fragments = nalu->generateFragments(maxFragmentSize);
+	std::vector<shared_ptr<H265NalUnitFragment>> result;
+	result.reserve(fragments.size());
+	for (auto fragment : fragments)
+		result.push_back(std::make_shared<H265NalUnitFragment>(std::move(fragment)));
+
+	return result;
+}
+
 void H265NalUnitFragment::setFragmentType(FragmentType type) {
 	switch (type) {
 	case FragmentType::Start:
@@ -82,16 +108,16 @@ void H265NalUnitFragment::setFragmentType(FragmentType type) {
 }
 
 std::vector<shared_ptr<binary>> H265NalUnits::generateFragments(uint16_t maxFragmentSize) {
-	vector<shared_ptr<binary>> result{};
-	for (auto nalu : *this) {
-		if (nalu->size() > maxFragmentSize) {
-			std::vector<shared_ptr<H265NalUnitFragment>> fragments =
-			    H265NalUnitFragment::fragmentsFrom(nalu, maxFragmentSize);
-			result.insert(result.end(), fragments.begin(), fragments.end());
-		} else {
-			result.push_back(nalu);
-		}
-	}
+	std::vector<H265NalUnit> nalus;
+	for (auto nalu : *this)
+		nalus.push_back(*nalu);
+
+	auto fragments = H265NalUnit::GenerateFragments(nalus, maxFragmentSize);
+	std::vector<shared_ptr<binary>> result;
+	result.reserve(fragments.size());
+	for (auto fragment : fragments)
+		result.push_back(std::make_shared<binary>(std::move(fragment)));
+
 	return result;
 }
 

+ 194 - 0
src/h265rtpdepacketizer.cpp

@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2023-2024 Paul-Louis Ageneau
+ * Copyright (c) 2024 Robert Edmonds
+ *
+ * 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/.
+ */
+
+#if RTC_ENABLE_MEDIA
+
+#include "h265rtpdepacketizer.hpp"
+#include "h265nalunit.hpp"
+
+#include "impl/internals.hpp"
+
+#include <algorithm>
+
+namespace rtc {
+
+const binary naluLongStartCode = {byte{0}, byte{0}, byte{0}, byte{1}};
+const binary naluShortStartCode = {byte{0}, byte{0}, byte{1}};
+
+const uint8_t naluTypeAP = 48;
+const uint8_t naluTypeFU = 49;
+
+H265RtpDepacketizer::H265RtpDepacketizer(Separator separator) : separator(separator) {
+	switch (separator) {
+	case Separator::StartSequence: [[fallthrough]];
+	case Separator::LongStartSequence: [[fallthrough]];
+	case Separator::ShortStartSequence:
+		break;
+	case Separator::Length: [[fallthrough]];
+	default:
+		throw std::invalid_argument("Invalid separator");
+	}
+}
+
+void H265RtpDepacketizer::addSeparator(binary& accessUnit)
+{
+	switch (separator) {
+	case Separator::StartSequence: [[fallthrough]];
+	case Separator::LongStartSequence:
+		accessUnit.insert(accessUnit.end(),
+		                  naluLongStartCode.begin(),
+		                  naluLongStartCode.end());
+		break;
+	case Separator::ShortStartSequence:
+		accessUnit.insert(accessUnit.end(),
+		                  naluShortStartCode.begin(),
+		                  naluShortStartCode.end());
+		break;
+	case Separator::Length: [[fallthrough]];
+	default:
+		throw std::invalid_argument("Invalid separator");
+	}
+}
+
+message_vector H265RtpDepacketizer::buildFrames(message_vector::iterator begin,
+                                                message_vector::iterator end,
+                                                uint8_t payloadType, uint32_t timestamp) {
+	message_vector out;
+	binary accessUnit;
+
+	for (auto it = begin; it != end; ++it) {
+		auto pkt = it->get();
+		auto pktParsed = reinterpret_cast<const rtc::RtpHeader *>(pkt->data());
+		auto rtpHeaderSize = pktParsed->getSize() + pktParsed->getExtensionHeaderSize();
+		auto rtpPaddingSize = 0;
+
+		if (pktParsed->padding()) {
+			rtpPaddingSize = std::to_integer<uint8_t>(pkt->at(pkt->size() - 1));
+		}
+
+		if (pkt->size() == rtpHeaderSize + rtpPaddingSize) {
+			PLOG_VERBOSE << "H.265 RTP packet has empty payload";
+			continue;
+		}
+
+		auto nalUnitHeader =
+		    H265NalUnitHeader{std::to_integer<uint8_t>(pkt->at(rtpHeaderSize)),
+		                      std::to_integer<uint8_t>(pkt->at(rtpHeaderSize + 1))};
+
+		if (nalUnitHeader.unitType() == naluTypeFU) {
+			auto nalUnitFragmentHeader = H265NalUnitFragmentHeader{
+			    std::to_integer<uint8_t>(pkt->at(rtpHeaderSize + sizeof(H265NalUnitHeader)))};
+
+			// RFC 7798: "When set to 1, the S bit indicates the start of a fragmented
+			// NAL unit, i.e., the first byte of the FU payload is also the first byte of
+			// the payload of the fragmented NAL unit. When the FU payload is not the start
+			// of the fragmented NAL unit payload, the S bit MUST be set to 0."
+			if (nalUnitFragmentHeader.isStart() || accessUnit.empty()) {
+				addSeparator(accessUnit);
+				nalUnitHeader.setUnitType(nalUnitFragmentHeader.unitType());
+				accessUnit.emplace_back(byte(nalUnitHeader._first));
+				accessUnit.emplace_back(byte(nalUnitHeader._second));
+			}
+
+			accessUnit.insert(accessUnit.end(),
+			                  pkt->begin() + rtpHeaderSize + sizeof(H265NalUnitHeader) +
+			                      sizeof(H265NalUnitFragmentHeader),
+			                  pkt->end());
+		} else if (nalUnitHeader.unitType() == naluTypeAP) {
+			auto currOffset = rtpHeaderSize + sizeof(H265NalUnitHeader);
+
+			while (currOffset + sizeof(uint16_t) < pkt->size()) {
+				auto naluSize = std::to_integer<uint16_t>(pkt->at(currOffset)) << 8 |
+				                std::to_integer<uint16_t>(pkt->at(currOffset + 1));
+
+				currOffset += sizeof(uint16_t);
+
+				if (pkt->size() < currOffset + naluSize) {
+					throw std::runtime_error("H265 AP declared size is larger than buffer");
+				}
+
+				addSeparator(accessUnit);
+				accessUnit.insert(accessUnit.end(), pkt->begin() + currOffset,
+				                  pkt->begin() + currOffset + naluSize);
+
+				currOffset += naluSize;
+			}
+		} else if (nalUnitHeader.unitType() < naluTypeAP) {
+			// "NAL units with NAL unit type values in the range of 0 to 47, inclusive, may be
+			// passed to the decoder."
+			addSeparator(accessUnit);
+			accessUnit.insert(accessUnit.end(), pkt->begin() + rtpHeaderSize, pkt->end());
+		} else {
+			// "NAL-unit-like structures with NAL unit type values in the range of 48 to 63,
+			// inclusive, MUST NOT be passed to the decoder."
+		}
+	}
+
+	if (!accessUnit.empty()) {
+		auto frameInfo = std::make_shared<FrameInfo>(timestamp);
+		frameInfo->timestampSeconds = std::chrono::duration<double>(double(timestamp) / double(ClockRate));
+		frameInfo->payloadType = payloadType;
+		out.emplace_back(make_message(std::move(accessUnit), frameInfo));
+	}
+
+	return out;
+}
+
+void H265RtpDepacketizer::incoming(message_vector &messages, const message_callback &) {
+	messages.erase(std::remove_if(messages.begin(), messages.end(),
+	                              [&](message_ptr message) {
+		                              if (message->type == Message::Control) {
+			                              return false;
+		                              }
+
+		                              if (message->size() < sizeof(RtpHeader)) {
+			                              PLOG_VERBOSE << "RTP packet is too small, size="
+			                                           << message->size();
+			                              return true;
+		                              }
+
+		                              mRtpBuffer.push_back(std::move(message));
+		                              return true;
+	                              }),
+	               messages.end());
+
+	while (mRtpBuffer.size() != 0) {
+		uint8_t payload_type = 0;
+		uint32_t current_timestamp = 0;
+		size_t packets_in_timestamp = 0;
+
+		for (const auto &pkt : mRtpBuffer) {
+			auto p = reinterpret_cast<const rtc::RtpHeader *>(pkt->data());
+
+			if (current_timestamp == 0) {
+				current_timestamp = p->timestamp();
+				payload_type = p->payloadType(); // should all be the same for data of the same codec
+			} else if (current_timestamp != p->timestamp()) {
+				break;
+			}
+
+			packets_in_timestamp++;
+		}
+
+		if (packets_in_timestamp == mRtpBuffer.size()) {
+			break;
+		}
+
+		auto begin = mRtpBuffer.begin();
+		auto end = mRtpBuffer.begin() + (packets_in_timestamp - 1);
+
+		auto frames = buildFrames(begin, end + 1, payload_type, current_timestamp);
+		messages.insert(messages.end(), frames.begin(), frames.end());
+		mRtpBuffer.erase(mRtpBuffer.begin(), mRtpBuffer.begin() + packets_in_timestamp);
+	}
+}
+
+} // namespace rtc
+
+#endif // RTC_ENABLE_MEDIA

+ 36 - 50
src/h265rtppacketizer.cpp

@@ -22,37 +22,51 @@
 
 namespace rtc {
 
-shared_ptr<H265NalUnits> H265RtpPacketizer::splitMessage(binary_ptr message) {
-	auto nalus = std::make_shared<H265NalUnits>();
-	if (separator == NalUnit::Separator::Length) {
+H265RtpPacketizer::H265RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig,
+                                     size_t maxFragmentSize)
+    : RtpPacketizer(std::move(rtpConfig)), mSeparator(NalUnit::Separator::Length),
+      mMaxFragmentSize(maxFragmentSize) {}
+
+H265RtpPacketizer::H265RtpPacketizer(NalUnit::Separator separator,
+                                     shared_ptr<RtpPacketizationConfig> rtpConfig,
+                                     size_t maxFragmentSize)
+    : RtpPacketizer(std::move(rtpConfig)), mSeparator(separator), mMaxFragmentSize(maxFragmentSize) {}
+
+std::vector<binary> H265RtpPacketizer::fragment(binary data) {
+	return H265NalUnit::GenerateFragments(splitFrame(data), mMaxFragmentSize);
+}
+
+std::vector<H265NalUnit> H265RtpPacketizer::splitFrame(const binary &frame) {
+	std::vector<H265NalUnit> nalus;
+	if (mSeparator == NalUnit::Separator::Length) {
 		size_t index = 0;
-		while (index < message->size()) {
-			assert(index + 4 < message->size());
-			if (index + 4 >= message->size()) {
+		while (index < frame.size()) {
+			assert(index + 4 < frame.size());
+			if (index + 4 >= frame.size()) {
 				LOG_WARNING << "Invalid NAL Unit data (incomplete length), ignoring!";
 				break;
 			}
 			uint32_t length;
-			std::memcpy(&length, message->data() + index, sizeof(uint32_t));
+			std::memcpy(&length, frame.data() + index, sizeof(uint32_t));
 			length = ntohl(length);
 			auto naluStartIndex = index + 4;
 			auto naluEndIndex = naluStartIndex + length;
 
-			assert(naluEndIndex <= message->size());
-			if (naluEndIndex > message->size()) {
+			assert(naluEndIndex <= frame.size());
+			if (naluEndIndex > frame.size()) {
 				LOG_WARNING << "Invalid NAL Unit data (incomplete unit), ignoring!";
 				break;
 			}
-			auto begin = message->begin() + naluStartIndex;
-			auto end = message->begin() + naluEndIndex;
-			nalus->push_back(std::make_shared<H265NalUnit>(begin, end));
+			auto begin = frame.begin() + naluStartIndex;
+			auto end = frame.begin() + naluEndIndex;
+			nalus.emplace_back(begin, end);
 			index = naluEndIndex;
 		}
 	} else {
 		NalUnitStartSequenceMatch match = NUSM_noMatch;
 		size_t index = 0;
-		while (index < message->size()) {
-			match = NalUnit::StartSequenceMatchSucc(match, (*message)[index++], separator);
+		while (index < frame.size()) {
+			match = NalUnit::StartSequenceMatchSucc(match, frame[index++], mSeparator);
 			if (match == NUSM_longMatch || match == NUSM_shortMatch) {
 				match = NUSM_noMatch;
 				break;
@@ -61,54 +75,26 @@ shared_ptr<H265NalUnits> H265RtpPacketizer::splitMessage(binary_ptr message) {
 
 		size_t naluStartIndex = index;
 
-		while (index < message->size()) {
-			match = NalUnit::StartSequenceMatchSucc(match, (*message)[index], separator);
+		while (index < frame.size()) {
+			match = NalUnit::StartSequenceMatchSucc(match, frame[index], mSeparator);
 			if (match == NUSM_longMatch || match == NUSM_shortMatch) {
 				auto sequenceLength = match == NUSM_longMatch ? 4 : 3;
 				size_t naluEndIndex = index - sequenceLength;
 				match = NUSM_noMatch;
-				auto begin = message->begin() + naluStartIndex;
-				auto end = message->begin() + naluEndIndex + 1;
-				nalus->push_back(std::make_shared<H265NalUnit>(begin, end));
+				auto begin = frame.begin() + naluStartIndex;
+				auto end = frame.begin() + naluEndIndex + 1;
+				nalus.emplace_back(begin, end);
 				naluStartIndex = index + 1;
 			}
 			index++;
 		}
-		auto begin = message->begin() + naluStartIndex;
-		auto end = message->end();
-		nalus->push_back(std::make_shared<H265NalUnit>(begin, end));
+		auto begin = frame.begin() + naluStartIndex;
+		auto end = frame.end();
+		nalus.emplace_back(begin, end);
 	}
 	return nalus;
 }
 
-H265RtpPacketizer::H265RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig,
-                                     uint16_t maxFragmentSize)
-    : RtpPacketizer(std::move(rtpConfig)), maxFragmentSize(maxFragmentSize),
-      separator(NalUnit::Separator::Length) {}
-
-H265RtpPacketizer::H265RtpPacketizer(NalUnit::Separator separator,
-                                     shared_ptr<RtpPacketizationConfig> rtpConfig,
-                                     uint16_t maxFragmentSize)
-    : RtpPacketizer(std::move(rtpConfig)), maxFragmentSize(maxFragmentSize),
-      separator(separator) {}
-
-void H265RtpPacketizer::outgoing(message_vector &messages, [[maybe_unused]] const message_callback &send) {
-	message_vector result;
-	for (const auto &message : messages) {
-		auto nalus = splitMessage(message);
-		auto fragments = nalus->generateFragments(maxFragmentSize);
-		if (fragments.size() == 0)
-			continue;
-
-		for (size_t i = 0; i < fragments.size() - 1; i++)
-			result.push_back(packetize(fragments[i], false));
-
-		result.push_back(packetize(fragments[fragments.size() - 1], true));
-	}
-
-	messages.swap(result);
-}
-
 } // namespace rtc
 
 #endif /* RTC_ENABLE_MEDIA */

+ 29 - 0
src/iceudpmuxlistener.cpp

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2025 Alex Potsides
+ * Copyright (c) 2025 Paul-Louis Ageneau
+ *
+ * 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 "iceudpmuxlistener.hpp"
+
+#include "impl/iceudpmuxlistener.hpp"
+
+namespace rtc {
+
+IceUdpMuxListener::IceUdpMuxListener(uint16_t port, optional<string> bindAddress)
+    : CheshireCat<impl::IceUdpMuxListener>(port, std::move(bindAddress)) {}
+
+IceUdpMuxListener::~IceUdpMuxListener() {}
+
+void IceUdpMuxListener::stop() { impl()->stop(); }
+
+uint16_t IceUdpMuxListener::port() const { return impl()->port; }
+
+void IceUdpMuxListener::OnUnhandledStunRequest(std::function<void(IceUdpMuxRequest)> callback) {
+	impl()->unhandledStunRequestCallback = callback;
+}
+
+} // namespace rtc

+ 32 - 8
src/impl/certificate.cpp

@@ -9,6 +9,7 @@
 #include "certificate.hpp"
 #include "threadpool.hpp"
 
+#include <algorithm>
 #include <cassert>
 #include <chrono>
 #include <iomanip>
@@ -384,9 +385,16 @@ Certificate Certificate::FromString(string crt_pem, string key_pem) {
 	BIO *bio = BIO_new(BIO_s_mem());
 	BIO_write(bio, crt_pem.data(), int(crt_pem.size()));
 	auto x509 = shared_ptr<X509>(PEM_read_bio_X509(bio, nullptr, nullptr, nullptr), X509_free);
-	BIO_free(bio);
-	if (!x509)
+	if (!x509) {
+		BIO_free(bio);
 		throw std::invalid_argument("Unable to import PEM certificate");
+	}
+	std::vector<shared_ptr<X509>> chain;
+	while (auto extra =
+	           shared_ptr<X509>(PEM_read_bio_X509(bio, nullptr, nullptr, nullptr), X509_free)) {
+		chain.push_back(std::move(extra));
+	}
+	BIO_free(bio);
 
 	bio = BIO_new(BIO_s_mem());
 	BIO_write(bio, key_pem.data(), int(key_pem.size()));
@@ -396,7 +404,7 @@ Certificate Certificate::FromString(string crt_pem, string key_pem) {
 	if (!pkey)
 		throw std::invalid_argument("Unable to import PEM key");
 
-	return Certificate(x509, pkey);
+	return Certificate(x509, pkey, std::move(chain));
 }
 
 Certificate Certificate::FromFile(const string &crt_pem_file, const string &key_pem_file,
@@ -408,9 +416,16 @@ Certificate Certificate::FromFile(const string &crt_pem_file, const string &key_
 		throw std::invalid_argument("Unable to open PEM certificate file");
 
 	auto x509 = shared_ptr<X509>(PEM_read_bio_X509(bio, nullptr, nullptr, nullptr), X509_free);
-	BIO_free(bio);
-	if (!x509)
+	if (!x509) {
+		BIO_free(bio);
 		throw std::invalid_argument("Unable to import PEM certificate from file");
+	}
+	std::vector<shared_ptr<X509>> chain;
+	while (auto extra =
+	           shared_ptr<X509>(PEM_read_bio_X509(bio, nullptr, nullptr, nullptr), X509_free)) {
+		chain.push_back(std::move(extra));
+	}
+	BIO_free(bio);
 
 	bio = openssl::BIO_new_from_file(key_pem_file);
 	if (!bio)
@@ -423,7 +438,7 @@ Certificate Certificate::FromFile(const string &crt_pem_file, const string &key_
 	if (!pkey)
 		throw std::invalid_argument("Unable to import PEM key from file");
 
-	return Certificate(x509, pkey);
+	return Certificate(x509, pkey, std::move(chain));
 }
 
 Certificate Certificate::Generate(CertificateType type, const string &commonName) {
@@ -514,14 +529,23 @@ Certificate Certificate::Generate(CertificateType type, const string &commonName
 	return Certificate(x509, pkey);
 }
 
-Certificate::Certificate(shared_ptr<X509> x509, shared_ptr<EVP_PKEY> pkey)
-    : mX509(std::move(x509)), mPKey(std::move(pkey)),
+Certificate::Certificate(shared_ptr<X509> x509, shared_ptr<EVP_PKEY> pkey,
+                         std::vector<shared_ptr<X509>> chain)
+    : mX509(std::move(x509)), mPKey(std::move(pkey)), mChain(std::move(chain)),
       mFingerprint(make_fingerprint(mX509.get(), CertificateFingerprint::Algorithm::Sha256)) {}
 
 std::tuple<X509 *, EVP_PKEY *> Certificate::credentials() const {
 	return {mX509.get(), mPKey.get()};
 }
 
+std::vector<X509 *> Certificate::chain() const {
+	std::vector<X509 *> v;
+	v.reserve(mChain.size());
+	std::transform(mChain.begin(), mChain.end(), std::back_inserter(v),
+	               [](const auto &c) { return c.get(); });
+	return v;
+}
+
 string make_fingerprint(X509 *x509, CertificateFingerprint::Algorithm fingerprintAlgorithm) {
 	size_t size = CertificateFingerprint::AlgorithmSize(fingerprintAlgorithm);
 	std::vector<unsigned char> buffer(size);

+ 3 - 1
src/impl/certificate.hpp

@@ -34,8 +34,9 @@ public:
 	Certificate(shared_ptr<mbedtls_x509_crt> crt, shared_ptr<mbedtls_pk_context> pk);
 	std::tuple<shared_ptr<mbedtls_x509_crt>, shared_ptr<mbedtls_pk_context>> credentials() const;
 #else // OPENSSL
-	Certificate(shared_ptr<X509> x509, shared_ptr<EVP_PKEY> pkey);
+	Certificate(shared_ptr<X509> x509, shared_ptr<EVP_PKEY> pkey, std::vector<shared_ptr<X509>> chain = {});
 	std::tuple<X509 *, EVP_PKEY *> credentials() const;
+	std::vector<X509 *> chain() const;
 #endif
 
 	CertificateFingerprint fingerprint() const;
@@ -52,6 +53,7 @@ private:
 #else
 	const shared_ptr<X509> mX509;
 	const shared_ptr<EVP_PKEY> mPKey;
+	const std::vector<shared_ptr<X509>> mChain;
 #endif
 
 	const string mFingerprint;

+ 4 - 1
src/impl/datachannel.cpp

@@ -185,9 +185,12 @@ bool DataChannel::outgoing(message_ptr message) {
 		std::shared_lock lock(mMutex);
 		transport = mSctpTransport.lock();
 
-		if (!transport || mIsClosed)
+		if (mIsClosed)
 			throw std::runtime_error("DataChannel is closed");
 
+		if (!transport)
+			throw std::runtime_error("DataChannel not open");
+
 		if (!mStream.has_value())
 			throw std::logic_error("DataChannel has no stream assigned");
 

+ 8 - 4
src/impl/dtlstransport.cpp

@@ -53,7 +53,8 @@ DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, certificate_ptr cer
                              verifier_callback verifierCallback, state_callback stateChangeCallback)
     : Transport(lower, std::move(stateChangeCallback)), mMtu(mtu), mCertificate(certificate),
       mFingerprintAlgorithm(fingerprintAlgorithm), mVerifierCallback(std::move(verifierCallback)),
-      mIsClient(lower->role() == Description::Role::Active) {
+      mIsClient(lower->role() == Description::Role::Active),
+      mIncomingQueue(RECV_QUEUE_LIMIT, message_size_func) {
 
 	PLOG_DEBUG << "Initializing DTLS transport (GnuTLS)";
 
@@ -380,7 +381,8 @@ DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, certificate_ptr cer
                              verifier_callback verifierCallback, state_callback stateChangeCallback)
     : Transport(lower, std::move(stateChangeCallback)), mMtu(mtu), mCertificate(certificate),
       mFingerprintAlgorithm(fingerprintAlgorithm), mVerifierCallback(std::move(verifierCallback)),
-      mIsClient(lower->role() == Description::Role::Active) {
+      mIsClient(lower->role() == Description::Role::Active),
+      mIncomingQueue(RECV_QUEUE_LIMIT, message_size_func) {
 
 	PLOG_DEBUG << "Initializing DTLS transport (MbedTLS)";
 
@@ -729,7 +731,9 @@ DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, certificate_ptr cer
                              verifier_callback verifierCallback, state_callback stateChangeCallback)
     : Transport(lower, std::move(stateChangeCallback)), mMtu(mtu), mCertificate(certificate),
       mFingerprintAlgorithm(fingerprintAlgorithm), mVerifierCallback(std::move(verifierCallback)),
-      mIsClient(lower->role() == Description::Role::Active) {
+      mIsClient(lower->role() == Description::Role::Active),
+      mIncomingQueue(RECV_QUEUE_LIMIT, message_size_func) {
+
 	PLOG_DEBUG << "Initializing DTLS transport (OpenSSL)";
 
 	if (!mCertificate)
@@ -757,7 +761,7 @@ DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, certificate_ptr cer
 		                   CertificateCallback);
 		SSL_CTX_set_verify_depth(mCtx, 1);
 
-		openssl::check(SSL_CTX_set_cipher_list(mCtx, "ALL:!LOW:!EXP:!RC4:!MD5:@STRENGTH"),
+		openssl::check(SSL_CTX_set_cipher_list(mCtx, "ALL:!SHA256:!SHA384:!aPSK:!ECDSA+SHA1:!ADH:!LOW:!EXP:!MD5:!3DES:!SSLv3:!TLSv1"),
 		               "Failed to set SSL priorities");
 
 #if OPENSSL_VERSION_NUMBER >= 0x30000000

+ 1 - 1
src/impl/dtlstransport.hpp

@@ -78,7 +78,7 @@ protected:
 	mbedtls_ssl_config mConf;
 	mbedtls_ssl_context mSsl;
 
-	std::mutex mSslMutex;
+	std::recursive_mutex mSslMutex;
 
 	uint32_t mFinMs = 0, mIntMs = 0;
 	std::chrono::time_point<std::chrono::steady_clock> mTimerSetAt;

+ 42 - 13
src/impl/icetransport.cpp

@@ -140,6 +140,12 @@ IceTransport::IceTransport(const Configuration &config, candidate_callback candi
 			addIceServer(server);
 }
 
+void IceTransport::setIceAttributes(string uFrag, string pwd) {
+	if (juice_set_local_ice_attributes(mAgent.get(), uFrag.c_str(), pwd.c_str()) < 0) {
+		throw std::invalid_argument("Invalid ICE attributes");
+	}
+}
+
 void IceTransport::addIceServer(IceServer server) {
 	if (server.hostname.empty())
 		return;
@@ -149,6 +155,11 @@ void IceTransport::addIceServer(IceServer server) {
 		return;
 	}
 
+	if (server.relayType != IceServer::RelayType::TurnUdp) {
+		PLOG_WARNING << "TURN transports TCP and TLS are not supported with libjuice";
+		return;
+	}
+
 	if (mTurnServersAdded >= MAX_TURN_SERVERS_COUNT)
 		return;
 
@@ -381,8 +392,22 @@ void IceTransport::LogCallback(juice_log_level_t level, const char *message) {
 
 #else // USE_NICE == 1
 
-unique_ptr<GMainLoop, void (*)(GMainLoop *)> IceTransport::MainLoop(nullptr, nullptr);
-std::thread IceTransport::MainLoopThread;
+IceTransport::MainLoopWrapper *IceTransport::MainLoop = nullptr;
+
+IceTransport::MainLoopWrapper::MainLoopWrapper()
+    : mMainLoop(g_main_loop_new(nullptr, FALSE), g_main_loop_unref) {
+	if (!mMainLoop)
+		throw std::runtime_error("Failed to create the glib main loop");
+
+	mThread = std::thread(g_main_loop_run, mMainLoop.get());
+}
+
+IceTransport::MainLoopWrapper::~MainLoopWrapper() {
+	g_main_loop_quit(mMainLoop.get());
+	mThread.join();
+}
+
+GMainLoop *IceTransport::MainLoopWrapper::get() const { return mMainLoop.get(); }
 
 void IceTransport::Init() {
 	g_log_set_handler("libnice", G_LOG_LEVEL_MASK, LogCallback, nullptr);
@@ -391,17 +416,12 @@ void IceTransport::Init() {
 		nice_debug_enable(false); // do not output STUN debug messages
 	}
 
-	MainLoop = decltype(MainLoop)(g_main_loop_new(nullptr, FALSE), g_main_loop_unref);
-	if (!MainLoop)
-		throw std::runtime_error("Failed to create the main loop");
-
-	MainLoopThread = std::thread(g_main_loop_run, MainLoop.get());
+	MainLoop = new MainLoopWrapper;
 }
 
 void IceTransport::Cleanup() {
-	g_main_loop_quit(MainLoop.get());
-	MainLoopThread.join();
-	MainLoop.reset();
+	delete MainLoop;
+	MainLoop = nullptr;
 }
 
 static void closeNiceAgentCallback(GObject *niceAgent, GAsyncResult *, gpointer) {
@@ -436,7 +456,7 @@ IceTransport::IceTransport(const Configuration &config, candidate_callback candi
 	// Create agent
 	mNiceAgent = decltype(mNiceAgent)(
 	    nice_agent_new_full(
-	        g_main_loop_get_context(MainLoop.get()),
+	        g_main_loop_get_context(MainLoop->get()),
 	        NICE_COMPATIBILITY_RFC5245, // RFC 5245 was obsoleted by RFC 8445 but this should be OK
 	        flags),
 	    closeNiceAgent);
@@ -461,6 +481,10 @@ IceTransport::IceTransport(const Configuration &config, candidate_callback candi
 	// the characteristics of the associated data.
 	g_object_set(G_OBJECT(mNiceAgent.get()), "stun-pacing-timer", 25, nullptr);
 
+	// Enable RFC 7675 ICE consent freshness support (requires libnice 0.1.19)
+	g_object_set(G_OBJECT(mNiceAgent.get()), "keepalive-conncheck", TRUE, nullptr);
+	g_object_set(G_OBJECT(mNiceAgent.get()), "consent-freshness", TRUE, nullptr);
+
 	g_object_set(G_OBJECT(mNiceAgent.get()), "upnp", FALSE, nullptr);
 	g_object_set(G_OBJECT(mNiceAgent.get()), "upnp-timeout", 200, nullptr);
 
@@ -565,10 +589,15 @@ IceTransport::IceTransport(const Configuration &config, candidate_callback candi
 	nice_agent_set_port_range(mNiceAgent.get(), mStreamId, 1, config.portRangeBegin,
 	                          config.portRangeEnd);
 
-	nice_agent_attach_recv(mNiceAgent.get(), mStreamId, 1, g_main_loop_get_context(MainLoop.get()),
+	nice_agent_attach_recv(mNiceAgent.get(), mStreamId, 1, g_main_loop_get_context(MainLoop->get()),
 	                       RecvCallback, this);
 }
 
+void IceTransport::setIceAttributes([[maybe_unused]] string uFrag, [[maybe_unused]] string pwd) {
+	PLOG_WARNING
+	    << "Setting custom ICE attributes is not supported with libnice, please use libjuice";
+}
+
 void IceTransport::addIceServer(IceServer server) {
 	if (server.hostname.empty())
 		return;
@@ -628,7 +657,7 @@ void IceTransport::addIceServer(IceServer server) {
 
 IceTransport::~IceTransport() {
 	PLOG_DEBUG << "Destroying ICE transport";
-	nice_agent_attach_recv(mNiceAgent.get(), mStreamId, 1, g_main_loop_get_context(MainLoop.get()),
+	nice_agent_attach_recv(mNiceAgent.get(), mStreamId, 1, g_main_loop_get_context(MainLoop->get()),
 	                       NULL, NULL);
 	nice_agent_remove_stream(mNiceAgent.get(), mStreamId);
 	mNiceAgent.reset();

+ 12 - 2
src/impl/icetransport.hpp

@@ -51,6 +51,7 @@ public:
 	void setRemoteDescription(const Description &description);
 	bool addRemoteCandidate(const Candidate &candidate);
 	void gatherLocalCandidates(string mid, std::vector<IceServer> additionalIceServers = {});
+	void setIceAttributes(string uFrag, string pwd);
 
 	optional<string> getLocalAddress() const;
 	optional<string> getRemoteAddress() const;
@@ -89,8 +90,17 @@ private:
 	static void RecvCallback(juice_agent_t *agent, const char *data, size_t size, void *user_ptr);
 	static void LogCallback(juice_log_level_t level, const char *message);
 #else
-	static unique_ptr<GMainLoop, void (*)(GMainLoop *)> MainLoop;
-	static std::thread MainLoopThread;
+	class MainLoopWrapper {
+	public:
+		MainLoopWrapper();
+		~MainLoopWrapper();
+		GMainLoop *get() const;
+
+	private:
+		unique_ptr<GMainLoop, void (*)(GMainLoop *)> mMainLoop;
+		std::thread mThread;
+	};
+	static MainLoopWrapper *MainLoop;
 
 	unique_ptr<NiceAgent, void (*)(NiceAgent *)> mNiceAgent;
 	uint32_t mStreamId = 0;

+ 62 - 0
src/impl/iceudpmuxlistener.cpp

@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2025 Alex Potsides
+ * Copyright (c) 2025 Paul-Louis Ageneau
+ *
+ * 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 "iceudpmuxlistener.hpp"
+#include "internals.hpp"
+
+namespace rtc::impl {
+
+#if !USE_NICE
+void IceUdpMuxListener::UnhandledStunRequestCallback(const juice_mux_binding_request *info,
+                                                     void *user_ptr) {
+	auto listener = static_cast<IceUdpMuxListener *>(user_ptr);
+	if (!listener)
+		return;
+
+	IceUdpMuxRequest request;
+	request.localUfrag = info->local_ufrag;
+	request.remoteUfrag = info->remote_ufrag;
+	request.remoteAddress = info->address;
+	request.remotePort = info->port;
+	listener->unhandledStunRequestCallback(std::move(request));
+}
+#endif
+
+IceUdpMuxListener::IceUdpMuxListener(uint16_t port, [[maybe_unused]] optional<string> bindAddress) : port(port) {
+	PLOG_VERBOSE << "Creating IceUdpMuxListener";
+
+#if !USE_NICE
+	PLOG_DEBUG << "Registering ICE UDP mux listener for port " << port;
+	if (juice_mux_listen(bindAddress ? bindAddress->c_str() : NULL, port,
+	                     IceUdpMuxListener::UnhandledStunRequestCallback, this) < 0) {
+		throw std::runtime_error("Failed to register ICE UDP mux listener");
+	}
+#else
+	PLOG_WARNING << "ICE UDP mux is not available with libnice";
+#endif
+}
+
+IceUdpMuxListener::~IceUdpMuxListener() {
+	PLOG_VERBOSE << "Destroying IceUdpMuxListener";
+	stop();
+}
+
+void IceUdpMuxListener::stop() {
+	if (mStopped.exchange(true))
+		return;
+
+#if !USE_NICE
+	PLOG_DEBUG << "Unregistering ICE UDP mux listener for port " << port;
+	if (juice_mux_listen(NULL, port, NULL, NULL) < 0) {
+		PLOG_ERROR << "Failed to unregister ICE UDP mux listener";
+	}
+#endif
+}
+
+} // namespace rtc::impl

+ 45 - 0
src/impl/iceudpmuxlistener.hpp

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2025 Alex Potsides
+ * Copyright (c) 2025 Paul-Louis Ageneau
+ *
+ * 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/.
+ */
+
+#ifndef RTC_IMPL_ICE_UDP_MUX_LISTENER_H
+#define RTC_IMPL_ICE_UDP_MUX_LISTENER_H
+
+#include "common.hpp"
+
+#include "rtc/iceudpmuxlistener.hpp"
+
+#if !USE_NICE
+#include <juice/juice.h>
+#endif
+
+#include <atomic>
+
+namespace rtc::impl {
+
+struct IceUdpMuxListener final {
+	IceUdpMuxListener(uint16_t port, optional<string> bindAddress = nullopt);
+	~IceUdpMuxListener();
+
+	void stop();
+
+	const uint16_t port;
+	synchronized_callback<IceUdpMuxRequest> unhandledStunRequestCallback;
+
+private:
+#if !USE_NICE
+	static void UnhandledStunRequestCallback(const juice_mux_binding_request *info, void *user_ptr);
+#endif
+
+	std::atomic<bool> mStopped;
+};
+
+}
+
+#endif
+

+ 206 - 120
src/impl/peerconnection.cpp

@@ -46,24 +46,22 @@ static LogCounter
 
 const string PemBeginCertificateTag = "-----BEGIN CERTIFICATE-----";
 
-PeerConnection::PeerConnection(Configuration config_)
-    : config(std::move(config_)) {
+PeerConnection::PeerConnection(Configuration config_) : config(std::move(config_)) {
 	PLOG_VERBOSE << "Creating PeerConnection";
 
-
 	if (config.certificatePemFile && config.keyPemFile) {
 		std::promise<certificate_ptr> cert;
 		cert.set_value(std::make_shared<Certificate>(
-			config.certificatePemFile->find(PemBeginCertificateTag) != string::npos
-				? Certificate::FromString(*config.certificatePemFile, *config.keyPemFile)
-				: Certificate::FromFile(*config.certificatePemFile, *config.keyPemFile,
-										config.keyPemPass.value_or(""))));
+		    config.certificatePemFile->find(PemBeginCertificateTag) != string::npos
+		        ? Certificate::FromString(*config.certificatePemFile, *config.keyPemFile)
+		        : Certificate::FromFile(*config.certificatePemFile, *config.keyPemFile,
+		                                config.keyPemPass.value_or(""))));
 		mCertificate = cert.get_future();
 	} else if (!config.certificatePemFile && !config.keyPemFile) {
 		mCertificate = make_certificate(config.certificateType);
 	} else {
 		throw std::invalid_argument(
-			"Either none or both certificate and key PEM files must be specified");
+		    "Either none or both certificate and key PEM files must be specified");
 	}
 
 	if (config.portRangeEnd && config.portRangeBegin > config.portRangeEnd)
@@ -87,7 +85,6 @@ PeerConnection::~PeerConnection() {
 }
 
 void PeerConnection::close() {
-	negotiationNeeded = false;
 	if (!closing.exchange(true)) {
 		PLOG_VERBOSE << "Closing PeerConnection";
 		if (auto transport = std::atomic_load(&mSctpTransport))
@@ -229,9 +226,13 @@ shared_ptr<DtlsTransport> PeerConnection::initDtlsTransport() {
 
 		PLOG_VERBOSE << "Starting DTLS transport";
 
-		auto fingerprintAlgorithm = CertificateFingerprint::Algorithm::Sha256;
-		if (auto remote = remoteDescription(); remote && remote->fingerprint()) {
-			fingerprintAlgorithm = remote->fingerprint()->algorithm;
+		CertificateFingerprint::Algorithm fingerprintAlgorithm;
+		{
+			std::lock_guard lock(mRemoteDescriptionMutex);
+			if (mRemoteDescription && mRemoteDescription->fingerprint()) {
+				mRemoteFingerprintAlgorithm = mRemoteDescription->fingerprint()->algorithm;
+			}
+			fingerprintAlgorithm = mRemoteFingerprintAlgorithm;
 		}
 
 		auto lower = std::atomic_load(&mIceTransport);
@@ -439,21 +440,27 @@ void PeerConnection::rollbackLocalDescription() {
 	}
 }
 
-bool PeerConnection::checkFingerprint(const std::string &fingerprint) const {
+bool PeerConnection::checkFingerprint(const std::string &fingerprint) {
 	std::lock_guard lock(mRemoteDescriptionMutex);
-	if (!mRemoteDescription || !mRemoteDescription->fingerprint())
+	mRemoteFingerprint = fingerprint;
+
+	if (!mRemoteDescription || !mRemoteDescription->fingerprint()
+			|| mRemoteFingerprintAlgorithm != mRemoteDescription->fingerprint()->algorithm)
 		return false;
 
-	if (config.disableFingerprintVerification)
+	if (config.disableFingerprintVerification) {
+		PLOG_VERBOSE << "Skipping fingerprint validation";
 		return true;
+	}
 
 	auto expectedFingerprint = mRemoteDescription->fingerprint()->value;
-	if (expectedFingerprint  == fingerprint) {
+	if (expectedFingerprint == fingerprint) {
 		PLOG_VERBOSE << "Valid fingerprint \"" << fingerprint << "\"";
 		return true;
 	}
 
-	PLOG_ERROR << "Invalid fingerprint \"" << fingerprint << "\", expected \"" << expectedFingerprint << "\"";
+	PLOG_ERROR << "Invalid fingerprint \"" << fingerprint << "\", expected \""
+	           << expectedFingerprint << "\"";
 	return false;
 }
 
@@ -531,11 +538,16 @@ void PeerConnection::forwardMedia([[maybe_unused]] message_ptr message) {
 	if (auto handler = getMediaHandler()) {
 		message_vector messages{std::move(message)};
 
-		handler->incoming(messages, [this](message_ptr message) {
-			auto transport = std::atomic_load(&mDtlsTransport);
-			if (auto srtpTransport = std::dynamic_pointer_cast<DtlsSrtpTransport>(transport))
-				srtpTransport->send(std::move(message));
-		});
+		try {
+			handler->incomingChain(messages, [this](message_ptr message) {
+				auto transport = std::atomic_load(&mDtlsTransport);
+				if (auto srtpTransport = std::dynamic_pointer_cast<DtlsSrtpTransport>(transport))
+					srtpTransport->send(std::move(message));
+			});
+		} catch(const std::exception &e) {
+			PLOG_WARNING << "Exception in global incoming media handler: " << e.what();
+			return;
+		}
 
 		for (auto &m : messages)
 			dispatchMedia(std::move(m));
@@ -549,7 +561,7 @@ void PeerConnection::forwardMedia([[maybe_unused]] message_ptr message) {
 void PeerConnection::dispatchMedia([[maybe_unused]] message_ptr message) {
 #if RTC_ENABLE_MEDIA
 	std::shared_lock lock(mTracksMutex); // read-only
-	if (mTrackLines.size()==1) {
+	if (mTrackLines.size() == 1) {
 		if (auto track = mTrackLines.front().lock())
 			track->incoming(message);
 		return;
@@ -736,7 +748,7 @@ void PeerConnection::iterateDataChannels(
 	{
 		std::shared_lock lock(mDataChannelsMutex); // read-only
 		locked.reserve(mDataChannels.size());
-		for(auto it = mDataChannels.begin(); it != mDataChannels.end(); ++it) {
+		for (auto it = mDataChannels.begin(); it != mDataChannels.end(); ++it) {
 			auto channel = it->second.lock();
 			if (channel && !channel->isClosed())
 				locked.push_back(std::move(channel));
@@ -805,7 +817,7 @@ void PeerConnection::iterateTracks(std::function<void(shared_ptr<Track> track)>
 	{
 		std::shared_lock lock(mTracksMutex); // read-only
 		locked.reserve(mTrackLines.size());
-		for(auto it = mTrackLines.begin(); it != mTrackLines.end(); ++it) {
+		for (auto it = mTrackLines.begin(); it != mTrackLines.end(); ++it) {
 			auto track = it->lock();
 			if (track && !track->isClosed())
 				locked.push_back(std::move(track));
@@ -821,27 +833,58 @@ void PeerConnection::iterateTracks(std::function<void(shared_ptr<Track> track)>
 	}
 }
 
+void PeerConnection::iterateRemoteTracks(std::function<void(shared_ptr<Track> track)> func) {
+	auto remote = remoteDescription();
+	if(!remote)
+		return;
+
+	std::vector<shared_ptr<Track>> locked;
+	{
+		std::shared_lock lock(mTracksMutex); // read-only
+		locked.reserve(remote->mediaCount());
+		for(int i = 0; i < remote->mediaCount(); ++i) {
+			if (std::holds_alternative<Description::Media *>(remote->media(i))) {
+				auto remoteMedia = std::get<Description::Media *>(remote->media(i));
+				if (!remoteMedia->isRemoved())
+					if (auto it = mTracks.find(remoteMedia->mid()); it != mTracks.end())
+						if (auto track = it->second.lock())
+							locked.push_back(std::move(track));
+			}
+		}
+	}
+
+	for (auto &track : locked) {
+		try {
+			func(std::move(track));
+		} catch (const std::exception &e) {
+			PLOG_WARNING << e.what();
+		}
+	}
+}
+
+
 void PeerConnection::openTracks() {
 #if RTC_ENABLE_MEDIA
-	if (auto transport = std::atomic_load(&mDtlsTransport)) {
-		auto srtpTransport = std::dynamic_pointer_cast<DtlsSrtpTransport>(transport);
-
-		iterateTracks([&](const shared_ptr<Track> &track) {
-			if (!track->isOpen()) {
-				if (srtpTransport) {
-					track->open(srtpTransport);
-				} else {
-					// A track was added during a latter renegotiation, whereas SRTP transport was
-					// not initialized. This is an optimization to use the library with data
-					// channels only. Set forceMediaTransport to true to initialize the transport
-					// before dynamically adding tracks.
-					auto errorMsg = "The connection has no media transport";
-					PLOG_ERROR << errorMsg;
-					track->triggerError(errorMsg);
-				}
+	auto transport = std::atomic_load(&mDtlsTransport);
+	if (!transport)
+		return;
+
+	auto srtpTransport = std::dynamic_pointer_cast<DtlsSrtpTransport>(transport);
+	iterateRemoteTracks([&](shared_ptr<Track> track) {
+		if(!track->isOpen()) {
+			if (srtpTransport) {
+				track->open(srtpTransport);
+			} else {
+				// A track was added during a latter renegotiation, whereas SRTP transport was
+				// not initialized. This is an optimization to use the library with data
+				// channels only. Set forceMediaTransport to true to initialize the transport
+				// before dynamically adding tracks.
+				auto errorMsg = "The connection has no media transport";
+				PLOG_ERROR << errorMsg;
+				track->triggerError(errorMsg);
 			}
-		});
-	}
+		}
+	});
 #endif
 }
 
@@ -864,7 +907,7 @@ void PeerConnection::validateRemoteDescription(const Description &description) {
 		throw std::invalid_argument("Remote description has no media line");
 
 	int activeMediaCount = 0;
-	for (unsigned int i = 0; i < description.mediaCount(); ++i)
+	for (int i = 0; i < description.mediaCount(); ++i)
 		std::visit(rtc::overloaded{[&](const Description::Application *application) {
 			                           if (!application->isRemoved())
 				                           ++activeMediaCount;
@@ -882,7 +925,7 @@ void PeerConnection::validateRemoteDescription(const Description &description) {
 	PLOG_VERBOSE << "Remote description looks valid";
 }
 
-void PeerConnection::processLocalDescription(Description description) {
+void PeerConnection::populateLocalDescription(Description &description) const {
 	const uint16_t localSctpPort = DEFAULT_SCTP_PORT;
 	const size_t localMaxMessageSize =
 	    config.maxMessageSize.value_or(DEFAULT_LOCAL_MAX_MESSAGE_SIZE);
@@ -892,7 +935,7 @@ void PeerConnection::processLocalDescription(Description description) {
 
 	if (auto remote = remoteDescription()) {
 		// Reciprocate remote description
-		for (unsigned int i = 0; i < remote->mediaCount(); ++i)
+		for (int i = 0; i < remote->mediaCount(); ++i) {
 			std::visit( // reciprocate each media
 			    rtc::overloaded{
 			        [&](Description::Application *remoteApp) {
@@ -907,78 +950,46 @@ void PeerConnection::processLocalDescription(Description description) {
 					                   << app.mid() << "\"";
 
 					        description.addMedia(std::move(app));
-					        return;
-				        }
 
-				        auto reciprocated = remoteApp->reciprocate();
-				        reciprocated.hintSctpPort(localSctpPort);
-				        reciprocated.setMaxMessageSize(localMaxMessageSize);
+				        } else {
+							auto reciprocated = remoteApp->reciprocate();
+							reciprocated.hintSctpPort(localSctpPort);
+							reciprocated.setMaxMessageSize(localMaxMessageSize);
 
-				        PLOG_DEBUG << "Reciprocating application in local description, mid=\""
-				                   << reciprocated.mid() << "\"";
+							PLOG_DEBUG << "Reciprocating application in local description, mid=\""
+								       << reciprocated.mid() << "\"";
 
-				        description.addMedia(std::move(reciprocated));
+							description.addMedia(std::move(reciprocated));
+						}
 			        },
 			        [&](Description::Media *remoteMedia) {
-				        std::unique_lock lock(mTracksMutex); // we may emplace a track
-				        if (auto it = mTracks.find(remoteMedia->mid()); it != mTracks.end()) {
-					        // Prefer local description
-					        if (auto track = it->second.lock()) {
-						        auto media = track->description();
-
-						        PLOG_DEBUG << "Adding media to local description, mid=\""
-						                   << media.mid() << "\", removed=" << std::boolalpha
-						                   << media.isRemoved();
-
-						        description.addMedia(std::move(media));
-
-					        } else {
-						        auto reciprocated = remoteMedia->reciprocate();
-						        reciprocated.markRemoved();
-
-						        PLOG_DEBUG << "Adding media to local description, mid=\""
-						                   << reciprocated.mid()
-						                   << "\", removed=true (track is destroyed)";
-
-						        description.addMedia(std::move(reciprocated));
-					        }
-					        return;
-				        }
-
-				        auto reciprocated = remoteMedia->reciprocate();
-#if !RTC_ENABLE_MEDIA
-				        if (!reciprocated.isRemoved()) {
-					        // No media support, mark as removed
-					        PLOG_WARNING << "Rejecting track (not compiled with media support)";
-					        reciprocated.markRemoved();
-				        }
-#endif
+				        std::shared_lock lock(mTracksMutex);
+				        auto it = mTracks.find(remoteMedia->mid());
+					    auto track = it != mTracks.end() ? it->second.lock() : nullptr;
+						if(track) {
+							// Prefer local description
+							auto media = track->description();
 
-				        PLOG_DEBUG << "Reciprocating media in local description, mid=\""
-				                   << reciprocated.mid() << "\", removed=" << std::boolalpha
-				                   << reciprocated.isRemoved();
+						    PLOG_DEBUG << "Adding media to local description, mid=\""
+						                << media.mid() << "\", removed=" << std::boolalpha
+						                << media.isRemoved();
 
-				        // Create incoming track
-				        auto track =
-				            std::make_shared<Track>(weak_from_this(), std::move(reciprocated));
-				        mTracks.emplace(std::make_pair(track->mid(), track));
-				        mTrackLines.emplace_back(track);
-				        triggerTrack(track); // The user may modify the track description
+						    description.addMedia(std::move(media));
 
-				        auto handler = getMediaHandler();
-				        if (handler)
-					        handler->media(track->description());
+					    } else {
+							auto reciprocated = remoteMedia->reciprocate();
+							reciprocated.markRemoved();
 
-				        if (track->description().isRemoved())
-					        track->close();
+						    PLOG_DEBUG << "Adding media to local description, mid=\""
+						                << reciprocated.mid()
+						                << "\", removed=true (track is destroyed)";
 
-				        description.addMedia(track->description());
+						    description.addMedia(std::move(reciprocated));
+					    }
 			        },
 			    },
 			    remote->media(i));
-
-		// We need to update the SSRC cache for newly-created incoming tracks
-		updateTrackSsrcCache(*remote);
+		}
 	}
 
 	if (description.type() == Description::Type::Offer) {
@@ -1018,23 +1029,18 @@ void PeerConnection::processLocalDescription(Description description) {
 				description.addMedia(std::move(app));
 			}
 		}
-
-		// There might be no media at this point if the user created a Track, deleted it,
-		// then called setLocalDescription().
-		if (description.mediaCount() == 0)
-			throw std::runtime_error("No DataChannel or Track to negotiate");
 	}
 
 	// Set local fingerprint (wait for certificate if necessary)
 	description.setFingerprint(mCertificate.get()->fingerprint());
+}
 
+void PeerConnection::processLocalDescription(Description description) {
 	PLOG_VERBOSE << "Issuing local description: " << description;
 
 	if (description.mediaCount() == 0)
 		throw std::logic_error("Local description has no media line");
 
-	updateTrackSsrcCache(description);
-
 	{
 		// Set as local description
 		std::lock_guard lock(mLocalDescriptionMutex);
@@ -1051,11 +1057,6 @@ void PeerConnection::processLocalDescription(Description description) {
 
 	mProcessor.enqueue(&PeerConnection::trigger<Description>, shared_from_this(),
 	                   &localDescriptionCallback, std::move(description));
-
-	// Reciprocated tracks might need to be open
-	if (auto dtlsTransport = std::atomic_load(&mDtlsTransport);
-	    dtlsTransport && dtlsTransport->state() == Transport::State::Connected)
-		mProcessor.enqueue(&PeerConnection::openTracks, shared_from_this());
 }
 
 void PeerConnection::processLocalCandidate(Candidate candidate) {
@@ -1079,6 +1080,40 @@ void PeerConnection::processLocalCandidate(Candidate candidate) {
 }
 
 void PeerConnection::processRemoteDescription(Description description) {
+	// Create tracks from remote description
+	for (int i = 0; i < description.mediaCount(); ++i) {
+		if (std::holds_alternative<Description::Media *>(description.media(i))) {
+			auto remoteMedia = std::get<Description::Media *>(description.media(i));
+			std::unique_lock lock(mTracksMutex); // we may emplace a track
+			if (auto it = mTracks.find(remoteMedia->mid()); it != mTracks.end())
+				continue;
+
+			PLOG_DEBUG << "New remote track, mid=\"" << remoteMedia->mid() << "\"";
+
+			auto reciprocated = remoteMedia->reciprocate();
+#if !RTC_ENABLE_MEDIA
+			if (!reciprocated.isRemoved()) {
+				// No media support, mark as removed
+				PLOG_WARNING << "Rejecting track (not compiled with media support)";
+				reciprocated.markRemoved();
+			}
+#endif
+
+			// Create incoming track
+			auto track = std::make_shared<Track>(weak_from_this(), std::move(reciprocated));
+			mTracks.emplace(std::make_pair(track->mid(), track));
+			mTrackLines.emplace_back(track);
+			triggerTrack(track); // The user may modify the track description
+
+			auto handler = getMediaHandler();
+			if (handler)
+				handler->media(track->description());
+
+			if (track->description().isRemoved())
+				track->close();
+		}
+	}
+
 	// Update the SSRC cache for existing tracks
 	updateTrackSsrcCache(description);
 
@@ -1094,8 +1129,8 @@ void PeerConnection::processRemoteDescription(Description description) {
 		mRemoteDescription->addCandidates(std::move(existingCandidates));
 	}
 
+	auto dtlsTransport = std::atomic_load(&mDtlsTransport);
 	if (description.hasApplication()) {
-		auto dtlsTransport = std::atomic_load(&mDtlsTransport);
 		auto sctpTransport = std::atomic_load(&mSctpTransport);
 		if (!sctpTransport && dtlsTransport &&
 		    dtlsTransport->state() == Transport::State::Connected)
@@ -1103,6 +1138,10 @@ void PeerConnection::processRemoteDescription(Description description) {
 	} else {
 		mProcessor.enqueue(&PeerConnection::remoteCloseDataChannels, shared_from_this());
 	}
+
+	// Reciprocated tracks might need to be open
+	if (dtlsTransport && dtlsTransport->state() == Transport::State::Connected)
+		mProcessor.enqueue(&PeerConnection::openTracks, shared_from_this());
 }
 
 void PeerConnection::processRemoteCandidate(Candidate candidate) {
@@ -1148,12 +1187,51 @@ string PeerConnection::localBundleMid() const {
 	return mLocalDescription ? mLocalDescription->bundleMid() : "0";
 }
 
+bool PeerConnection::negotiationNeeded() const {
+	auto description = localDescription();
+
+	{
+		std::shared_lock lock(mDataChannelsMutex);
+		if (!mDataChannels.empty() || !mUnassignedDataChannels.empty())
+			if(!description || !description->hasApplication()) {
+				PLOG_DEBUG << "Negotiation needed for data channels";
+				return true;
+			}
+	}
+
+	{
+		std::shared_lock lock(mTracksMutex);
+		for(const auto &[mid, weakTrack] : mTracks)
+			if (auto track = weakTrack.lock())
+				if (!description || !description->hasMid(track->mid())) {
+					PLOG_DEBUG << "Negotiation needed to add track, mid=" << track->mid();
+					return true;
+				}
+
+		if(description) {
+			for(int i = 0; i < description->mediaCount(); ++i) {
+				if (std::holds_alternative<Description::Media *>(description->media(i))) {
+					auto media = std::get<Description::Media *>(description->media(i));
+					if (!media->isRemoved())
+						if (auto it = mTracks.find(media->mid()); it != mTracks.end())
+							if (auto track = it->second.lock(); !track || track->isClosed()) {
+								PLOG_DEBUG << "Negotiation needed to remove track, mid=" << media->mid();
+								return true;
+							}
+				}
+			}
+		}
+	}
+
+	return false;
+}
+
 void PeerConnection::setMediaHandler(shared_ptr<MediaHandler> handler) {
 	std::unique_lock lock(mMediaHandlerMutex);
 	mMediaHandler = handler;
 }
 
-shared_ptr<MediaHandler> PeerConnection::getMediaHandler() {
+shared_ptr<MediaHandler> PeerConnection::getMediaHandler() const {
 	std::shared_lock lock(mMediaHandlerMutex);
 	return mMediaHandler;
 }
@@ -1301,11 +1379,19 @@ void PeerConnection::resetCallbacks() {
 	trackCallback = nullptr;
 }
 
+CertificateFingerprint PeerConnection::remoteFingerprint() {
+	std::lock_guard lock(mRemoteDescriptionMutex);
+	if (mRemoteFingerprint)
+		return {CertificateFingerprint{mRemoteFingerprintAlgorithm, *mRemoteFingerprint}};
+	else
+		return {};
+}
+
 void PeerConnection::updateTrackSsrcCache(const Description &description) {
 	std::unique_lock lock(mTracksMutex); // for safely writing to mTracksBySsrc
 
 	// Setup SSRC -> Track mapping
-	for (unsigned int i = 0; i < description.mediaCount(); ++i)
+	for (int i = 0; i < description.mediaCount(); ++i)
 		std::visit( // ssrc -> track mapping
 		    rtc::overloaded{
 		        [&](Description::Application const *) { return; },

+ 17 - 8
src/impl/peerconnection.hpp

@@ -53,7 +53,7 @@ struct PeerConnection : std::enable_shared_from_this<PeerConnection> {
 
 	void endLocalCandidates();
 	void rollbackLocalDescription();
-	bool checkFingerprint(const std::string &fingerprint) const;
+	bool checkFingerprint(const std::string &fingerprint);
 	void forwardMessage(message_ptr message);
 	void forwardMedia(message_ptr message);
 	void forwardBufferedAmount(uint16_t stream, size_t amount);
@@ -70,18 +70,22 @@ struct PeerConnection : std::enable_shared_from_this<PeerConnection> {
 
 	shared_ptr<Track> emplaceTrack(Description::Media description);
 	void iterateTracks(std::function<void(shared_ptr<Track> track)> func);
+	void iterateRemoteTracks(std::function<void(shared_ptr<Track> track)> func);
 	void openTracks();
 	void closeTracks();
 
 	void validateRemoteDescription(const Description &description);
+	void populateLocalDescription(Description &description) const;
 	void processLocalDescription(Description description);
 	void processLocalCandidate(Candidate candidate);
 	void processRemoteDescription(Description description);
 	void processRemoteCandidate(Candidate candidate);
 	string localBundleMid() const;
 
+	bool negotiationNeeded() const;
+
 	void setMediaHandler(shared_ptr<MediaHandler> handler);
-	shared_ptr<MediaHandler> getMediaHandler();
+	shared_ptr<MediaHandler> getMediaHandler() const;
 
 	void triggerDataChannel(weak_ptr<DataChannel> weakDataChannel);
 	void triggerTrack(weak_ptr<Track> weakTrack);
@@ -99,6 +103,8 @@ struct PeerConnection : std::enable_shared_from_this<PeerConnection> {
 
 	void resetCallbacks();
 
+	CertificateFingerprint remoteFingerprint();
+
 	// Helper method for asynchronous callback invocation
 	template <typename... Args> void trigger(synchronized_callback<Args...> *cb, Args... args) {
 		try {
@@ -113,7 +119,6 @@ struct PeerConnection : std::enable_shared_from_this<PeerConnection> {
 	std::atomic<IceState> iceState = IceState::New;
 	std::atomic<GatheringState> gatheringState = GatheringState::New;
 	std::atomic<SignalingState> signalingState = SignalingState::Stable;
-	std::atomic<bool> negotiationNeeded = false;
 	std::atomic<bool> closing = false;
 	std::mutex signalingMutex;
 
@@ -134,12 +139,16 @@ private:
 	future_certificate_ptr mCertificate;
 
 	Processor mProcessor;
-	optional<Description> mLocalDescription, mRemoteDescription;
+	optional<Description> mLocalDescription;
 	optional<Description> mCurrentLocalDescription;
-	mutable std::mutex mLocalDescriptionMutex, mRemoteDescriptionMutex;
+	mutable std::mutex mLocalDescriptionMutex;
 
-	shared_ptr<MediaHandler> mMediaHandler;
+	optional<Description> mRemoteDescription;
+	CertificateFingerprint::Algorithm mRemoteFingerprintAlgorithm = CertificateFingerprint::Algorithm::Sha256;
+	optional<string> mRemoteFingerprint;
+	mutable std::mutex mRemoteDescriptionMutex;
 
+	shared_ptr<MediaHandler> mMediaHandler;
 	mutable std::shared_mutex mMediaHandlerMutex;
 
 	shared_ptr<IceTransport> mIceTransport;
@@ -148,12 +157,12 @@ private:
 
 	std::unordered_map<uint16_t, weak_ptr<DataChannel>> mDataChannels; // by stream ID
 	std::vector<weak_ptr<DataChannel>> mUnassignedDataChannels;
-	std::shared_mutex mDataChannelsMutex;
+	mutable std::shared_mutex mDataChannelsMutex;
 
 	std::unordered_map<string, weak_ptr<Track>> mTracks;         // by mid
 	std::unordered_map<uint32_t, weak_ptr<Track>> mTracksBySsrc; // by SSRC
 	std::vector<weak_ptr<Track>> mTrackLines;                    // by SDP order
-	std::shared_mutex mTracksMutex;
+	mutable std::shared_mutex mTracksMutex;
 
 	Queue<shared_ptr<DataChannel>> mPendingDataChannels;
 	Queue<shared_ptr<Track>> mPendingTracks;

+ 4 - 3
src/impl/pollservice.cpp

@@ -189,12 +189,13 @@ void PollService::runLoop() {
 
 			} while (ret < 0 && (sockerrno == SEINTR || sockerrno == SEAGAIN));
 
+			if (ret < 0) {
 #ifdef _WIN32
-			if (ret == WSAENOTSOCK)
-				continue; // prepare again as the fd has been removed
+				if (sockerrno == WSAENOTSOCK)
+					continue; // prepare again as the fd has been removed
 #endif
-			if (ret < 0)
 				throw std::runtime_error("poll failed, errno=" + std::to_string(sockerrno));
+			}
 
 			process(pfds);
 		}

+ 1 - 0
src/impl/queue.hpp

@@ -105,6 +105,7 @@ template <typename T> optional<T> Queue<T>::pop() {
 	mAmount -= mAmountFunction(mQueue.front());
 	optional<T> element{std::move(mQueue.front())};
 	mQueue.pop();
+	mPushCondition.notify_one();
 	return element;
 }
 

+ 1 - 1
src/impl/sctptransport.cpp

@@ -82,7 +82,7 @@ private:
 	std::shared_mutex mMutex;
 };
 
-SctpTransport::InstancesSet *SctpTransport::Instances = new InstancesSet;
+std::unique_ptr<SctpTransport::InstancesSet> SctpTransport::Instances = std::make_unique<InstancesSet>();
 
 void SctpTransport::Init() {
 	usrsctp_init(0, SctpTransport::WriteCallback, SctpTransport::DebugCallback);

+ 1 - 1
src/impl/sctptransport.hpp

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

+ 17 - 14
src/impl/tcptransport.cpp

@@ -200,23 +200,23 @@ void TcpTransport::resolve() {
 }
 
 void TcpTransport::attempt() {
-	std::lock_guard lock(mSendMutex);
+	try {
+		std::lock_guard lock(mSendMutex);
 
-	if (state() != State::Connecting)
-		return; // Cancelled
+		if (state() != State::Connecting)
+			return; // Cancelled
 
-	if (mSock == INVALID_SOCKET) {
-		::closesocket(mSock);
-		mSock = INVALID_SOCKET;
-	}
+		if (mSock == INVALID_SOCKET) {
+			::closesocket(mSock);
+			mSock = INVALID_SOCKET;
+		}
 
-	if (mResolved.empty()) {
-		PLOG_WARNING << "Connection to " << mHostname << ":" << mService << " failed";
-		changeState(State::Failed);
-		return;
-	}
+		if (mResolved.empty()) {
+			PLOG_WARNING << "Connection to " << mHostname << ":" << mService << " failed";
+			changeState(State::Failed);
+			return;
+		}
 
-	try {
 		auto [addr, addrlen] = mResolved.front();
 		mResolved.pop_front();
 
@@ -372,7 +372,9 @@ bool TcpTransport::trySendMessage(message_ptr &message) {
 		int len = ::send(mSock, data, int(size), flags);
 		if (len < 0) {
 			if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) {
-				message = make_message(message->end() - size, message->end());
+				if (size < message->size())
+					message = make_message(message->end() - size, message->end());
+
 				return false;
 			} else {
 				PLOG_ERROR << "Connection closed, errno=" << sockerrno;
@@ -427,6 +429,7 @@ void TcpTransport::process(PollService::Event event) {
 		}
 
 		case PollService::Event::Out: {
+			std::lock_guard lock(mSendMutex);
 			if (trySendQueue())
 				setPoll(PollService::Direction::In);
 

+ 5 - 1
src/impl/tls.cpp

@@ -158,7 +158,11 @@ void init() {
 
 	std::lock_guard lock(mutex);
 	if (!std::exchange(done, true)) {
-		OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr);
+		uint64_t ssl_opts = OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS;
+#ifdef OPENSSL_INIT_NO_ATEXIT
+		ssl_opts |= OPENSSL_INIT_NO_ATEXIT;
+#endif
+		OPENSSL_init_ssl(ssl_opts, nullptr);
 		OPENSSL_init_crypto(OPENSSL_INIT_LOAD_CRYPTO_STRINGS, nullptr);
 	}
 }

+ 3 - 0
src/impl/tlstransport.cpp

@@ -603,6 +603,9 @@ TlsTransport::TlsTransport(variant<shared_ptr<TcpTransport>, shared_ptr<HttpProx
 			auto [x509, pkey] = certificate->credentials();
 			SSL_CTX_use_certificate(mCtx, x509);
 			SSL_CTX_use_PrivateKey(mCtx, pkey);
+
+			for (auto c : certificate->chain())
+				SSL_CTX_add1_chain_cert(mCtx, c); // add1 increments reference count
 		}
 
 		SSL_CTX_set_options(mCtx, SSL_OP_NO_SSLv3 | SSL_OP_NO_RENEGOTIATION);

+ 1 - 1
src/impl/tlstransport.hpp

@@ -72,7 +72,7 @@ protected:
 	mbedtls_ssl_config mConf;
 	mbedtls_ssl_context mSsl;
 
-	std::mutex mSslMutex;
+	std::recursive_mutex mSslMutex;
 	std::atomic<bool> mOutgoingResult = true;
 
 	message_ptr mIncomingMessage;

+ 16 - 14
src/impl/track.cpp

@@ -141,12 +141,18 @@ void Track::incoming(message_ptr message) {
 	}
 
 	message_vector messages{std::move(message)};
-	if (auto handler = getMediaHandler())
-		handler->incomingChain(messages, [this, weak_this = weak_from_this()](message_ptr m) {
-			if (auto locked = weak_this.lock()) {
-				transportSend(m);
-			}
-		});
+	if (auto handler = getMediaHandler()) {
+		try {
+			handler->incomingChain(messages, [this, weak_this = weak_from_this()](message_ptr m) {
+				if (auto locked = weak_this.lock()) {
+					transportSend(m);
+				}
+			});
+		} catch (const std::exception &e) {
+			PLOG_WARNING << "Exception in incoming media handler: " << e.what();
+			return;
+		}
+	}
 
 	for (auto &m : messages) {
 		// Tail drop if queue is full
@@ -184,6 +190,7 @@ bool Track::outgoing(message_ptr message) {
 				transportSend(m);
 			}
 		});
+
 		bool ret = false;
 		for (auto &m : messages)
 			ret = transportSend(std::move(m));
@@ -202,7 +209,7 @@ bool Track::transportSend([[maybe_unused]] message_ptr message) {
 		std::shared_lock lock(mMutex);
 		transport = mDtlsSrtpTransport.lock();
 		if (!transport)
-			throw std::runtime_error("Track is closed");
+			throw std::runtime_error("Track is not open");
 
 		// Set recommended medium-priority DSCP value
 		// See https://www.rfc-editor.org/rfc/rfc8837.html#section-5
@@ -233,11 +240,6 @@ shared_ptr<MediaHandler> Track::getMediaHandler() {
 	return mMediaHandler;
 }
 
-void Track::onFrame(std::function<void(binary data, FrameInfo frame)> callback) {
-	frameCallback = callback;
-	flushPendingMessages();
-}
-
 void Track::flushPendingMessages() {
 	if (!mOpenTriggered)
 		return;
@@ -249,9 +251,9 @@ void Track::flushPendingMessages() {
 
 		auto message = next.value();
 		try {
-			if (message->frameInfo != nullptr && frameCallback) {
+			if (message->frameInfo && frameCallback) {
 				frameCallback(std::move(*message), std::move(*message->frameInfo));
-			} else if (message->frameInfo == nullptr && messageCallback) {
+			} else if (!message->frameInfo && messageCallback) {
 				messageCallback(trackMessageToVariant(message));
 			}
 		} catch (const std::exception &e) {

+ 3 - 2
src/impl/track.hpp

@@ -41,7 +41,7 @@ public:
 	void flushPendingMessages() override;
 	message_variant trackMessageToVariant(message_ptr message);
 
-	void onFrame(std::function<void(binary data, FrameInfo frame)> callback);
+	void sendFrame(binary data, const FrameInfo &frame);
 
 	bool isOpen() const;
 	bool isClosed() const;
@@ -61,6 +61,8 @@ public:
 
 	bool transportSend(message_ptr message);
 
+	synchronized_callback<binary, FrameInfo> frameCallback;
+
 private:
 	const weak_ptr<PeerConnection> mPeerConnection;
 #if RTC_ENABLE_MEDIA
@@ -76,7 +78,6 @@ private:
 
 	Queue<message_ptr> mRecvQueue;
 
-	synchronized_callback<binary, FrameInfo> frameCallback;
 };
 
 } // namespace rtc::impl

+ 6 - 3
src/impl/websocket.cpp

@@ -252,7 +252,8 @@ shared_ptr<TcpTransport> WebSocket::setTcpTransport(shared_ptr<TcpTransport> tra
 				remoteClose();
 				break;
 			case State::Disconnected:
-				remoteClose();
+				if(state == WebSocket::State::Connecting)
+					remoteClose();
 				break;
 			default:
 				// Ignore
@@ -303,7 +304,8 @@ shared_ptr<HttpProxyTransport> WebSocket::initProxyTransport() {
 				remoteClose();
 				break;
 			case State::Disconnected:
-				remoteClose();
+				if(state == WebSocket::State::Connecting)
+					remoteClose();
 				break;
 			default:
 				// Ignore
@@ -358,7 +360,8 @@ shared_ptr<TlsTransport> WebSocket::initTlsTransport() {
 				remoteClose();
 				break;
 			case State::Disconnected:
-				remoteClose();
+				if(state == WebSocket::State::Connecting)
+					remoteClose();
 				break;
 			default:
 				// Ignore

+ 13 - 9
src/impl/wshandshake.cpp

@@ -64,7 +64,7 @@ string WsHandshake::generateHttpRequest() {
 	             "Host: " +
 	             mHost +
 	             "\r\n"
-	             "Connection: upgrade\r\n"
+	             "Connection: Upgrade\r\n"
 	             "Upgrade: websocket\r\n"
 	             "Sec-WebSocket-Version: 13\r\n"
 	             "Sec-WebSocket-Key: " +
@@ -80,12 +80,18 @@ string WsHandshake::generateHttpRequest() {
 
 string WsHandshake::generateHttpResponse() {
 	std::unique_lock lock(mMutex);
-	const string out = "HTTP/1.1 101 Switching Protocols\r\n"
-	                   "Server: libdatachannel\r\n"
-	                   "Connection: upgrade\r\n"
-	                   "Upgrade: websocket\r\n"
-	                   "Sec-WebSocket-Accept: " +
-	                   computeAcceptKey(mKey) + "\r\n\r\n";
+
+	string out = "HTTP/1.1 101 Switching Protocols\r\n"
+	             "Server: libdatachannel\r\n"
+	             "Connection: Upgrade\r\n"
+	             "Upgrade: websocket\r\n"
+	             "Sec-WebSocket-Accept: " +
+	             computeAcceptKey(mKey) + "\r\n";
+
+	if (!mProtocols.empty())
+		out += "Sec-WebSocket-Protocol: " + utils::implode(mProtocols, ',') + "\r\n";
+
+	out += "\r\n";
 
 	return out;
 }
@@ -119,8 +125,6 @@ string WsHandshake::generateHttpError(int responseCode) {
 	const string out = "HTTP/1.1 " + error +
 	                   "\r\n"
 	                   "Server: libdatachannel\r\n"
-	                   "Connection: upgrade\r\n"
-	                   "Upgrade: websocket\r\n"
 	                   "Content-Type: text/plain\r\n"
 	                   "Content-Length: " +
 	                   to_string(error.size()) +

+ 5 - 2
src/message.cpp

@@ -18,11 +18,14 @@ message_ptr make_message(size_t size, Message::Type type, unsigned int stream,
 	return message;
 }
 
-message_ptr make_message(binary &&data, Message::Type type, unsigned int stream,
-                         shared_ptr<Reliability> reliability, shared_ptr<FrameInfo> frameInfo) {
+message_ptr make_message(binary &&data, Message::Type type, unsigned int stream, shared_ptr<Reliability> reliability) {
 	auto message = std::make_shared<Message>(std::move(data), type);
 	message->stream = stream;
 	message->reliability = reliability;
+	return message;
+}
+message_ptr make_message(binary &&data, shared_ptr<FrameInfo> frameInfo) {
+	auto message = std::make_shared<Message>(std::move(data));
 	message->frameInfo = frameInfo;
 	return message;
 }

+ 103 - 33
src/nalunit.cpp

@@ -16,33 +16,38 @@
 
 namespace rtc {
 
-NalUnitFragmentA::NalUnitFragmentA(FragmentType type, bool forbiddenBit, uint8_t nri,
-                                   uint8_t unitType, binary data)
-    : NalUnit(data.size() + 2) {
-	setForbiddenBit(forbiddenBit);
-	setNRI(nri);
-	fragmentIndicator()->setUnitType(NalUnitFragmentA::nal_type_fu_A);
-	setFragmentType(type);
-	setUnitType(unitType);
-	copy(data.begin(), data.end(), begin() + 2);
+std::vector<binary> NalUnit::GenerateFragments(const std::vector<NalUnit> &nalus,
+                                               size_t maxFragmentSize) {
+	std::vector<binary> result;
+	for (const auto &nalu : nalus) {
+		if (nalu.size() > maxFragmentSize) {
+			auto fragments = nalu.generateFragments(maxFragmentSize);
+			result.insert(result.end(), fragments.begin(), fragments.end());
+		} else {
+			// TODO: check this
+			result.push_back(nalu);
+		}
+	}
+	return result;
 }
 
-std::vector<shared_ptr<NalUnitFragmentA>>
-NalUnitFragmentA::fragmentsFrom(shared_ptr<NalUnit> nalu, uint16_t maxFragmentSize) {
-	assert(nalu->size() > maxFragmentSize);
-	auto fragments_count = ceil(double(nalu->size()) / maxFragmentSize);
-	maxFragmentSize = uint16_t(int(ceil(nalu->size() / fragments_count)));
+std::vector<NalUnitFragmentA> NalUnit::generateFragments(size_t maxFragmentSize) const {
+	assert(size() > maxFragmentSize);
+	// TODO: check this
+	auto fragments_count = ceil(double(size()) / maxFragmentSize);
+	maxFragmentSize = uint16_t(int(ceil(size() / fragments_count)));
 
 	// 2 bytes for FU indicator and FU header
 	maxFragmentSize -= 2;
-	auto f = nalu->forbiddenBit();
-	uint8_t nri = nalu->nri() & 0x03;
-	uint8_t naluType = nalu->unitType() & 0x1F;
-	auto payload = nalu->payload();
-	vector<shared_ptr<NalUnitFragmentA>> result{};
-	uint64_t offset = 0;
+	auto f = forbiddenBit();
+	uint8_t nri = this->nri() & 0x03;
+	uint8_t unitType = this->unitType() & 0x1F;
+	auto payload = this->payload();
+	size_t offset = 0;
+	std::vector<NalUnitFragmentA> result;
 	while (offset < payload.size()) {
 		vector<byte> fragmentData;
+		using FragmentType = NalUnitFragmentA::FragmentType;
 		FragmentType fragmentType;
 		if (offset == 0) {
 			fragmentType = FragmentType::Start;
@@ -55,14 +60,78 @@ NalUnitFragmentA::fragmentsFrom(shared_ptr<NalUnit> nalu, uint16_t maxFragmentSi
 			fragmentType = FragmentType::End;
 		}
 		fragmentData = {payload.begin() + offset, payload.begin() + offset + maxFragmentSize};
-		auto fragment =
-		    std::make_shared<NalUnitFragmentA>(fragmentType, f, nri, naluType, fragmentData);
-		result.push_back(fragment);
+		result.emplace_back(fragmentType, f, nri, unitType, fragmentData);
 		offset += maxFragmentSize;
 	}
 	return result;
 }
 
+NalUnitStartSequenceMatch NalUnit::StartSequenceMatchSucc(NalUnitStartSequenceMatch match,
+                                                          std::byte _byte, Separator separator) {
+	assert(separator != Separator::Length);
+	auto byte = (uint8_t)_byte;
+	auto detectShort =
+	    separator == Separator::ShortStartSequence || separator == Separator::StartSequence;
+	auto detectLong =
+	    separator == Separator::LongStartSequence || separator == Separator::StartSequence;
+	switch (match) {
+	case NUSM_noMatch:
+		if (byte == 0x00) {
+			return NUSM_firstZero;
+		}
+		break;
+	case NUSM_firstZero:
+		if (byte == 0x00) {
+			return NUSM_secondZero;
+		}
+		break;
+	case NUSM_secondZero:
+		if (byte == 0x00 && detectLong) {
+			return NUSM_thirdZero;
+		} else if (byte == 0x00 && detectShort) {
+			return NUSM_secondZero;
+		} else if (byte == 0x01 && detectShort) {
+			return NUSM_shortMatch;
+		}
+		break;
+	case NUSM_thirdZero:
+		if (byte == 0x00 && detectLong) {
+			return NUSM_thirdZero;
+		} else if (byte == 0x01 && detectLong) {
+			return NUSM_longMatch;
+		}
+		break;
+	case NUSM_shortMatch:
+		return NUSM_shortMatch;
+	case NUSM_longMatch:
+		return NUSM_longMatch;
+	}
+	return NUSM_noMatch;
+}
+
+NalUnitFragmentA::NalUnitFragmentA(FragmentType type, bool forbiddenBit, uint8_t nri,
+                                   uint8_t unitType, binary data)
+    : NalUnit(data.size() + 2) {
+	setForbiddenBit(forbiddenBit);
+	setNRI(nri);
+	fragmentIndicator()->setUnitType(NalUnitFragmentA::nal_type_fu_A);
+	setFragmentType(type);
+	setUnitType(unitType);
+	copy(data.begin(), data.end(), begin() + 2);
+}
+
+// For backward compatibility, do not use
+std::vector<shared_ptr<NalUnitFragmentA>>
+NalUnitFragmentA::fragmentsFrom(shared_ptr<NalUnit> nalu, uint16_t maxFragmentSize) {
+	auto fragments = nalu->generateFragments(maxFragmentSize);
+	std::vector<shared_ptr<NalUnitFragmentA>> result;
+	result.reserve(fragments.size());
+	for (auto fragment : fragments)
+		result.push_back(std::make_shared<NalUnitFragmentA>(std::move(fragment)));
+
+	return result;
+}
+
 void NalUnitFragmentA::setFragmentType(FragmentType type) {
 	fragmentHeader()->setReservedBit6(false);
 	switch (type) {
@@ -80,17 +149,18 @@ void NalUnitFragmentA::setFragmentType(FragmentType type) {
 	}
 }
 
+// For backward compatibility, do not use
 std::vector<shared_ptr<binary>> NalUnits::generateFragments(uint16_t maxFragmentSize) {
-	vector<shared_ptr<binary>> result{};
-	for (auto nalu : *this) {
-		if (nalu->size() > maxFragmentSize) {
-			std::vector<shared_ptr<NalUnitFragmentA>> fragments =
-			    NalUnitFragmentA::fragmentsFrom(nalu, maxFragmentSize);
-			result.insert(result.end(), fragments.begin(), fragments.end());
-		} else {
-			result.push_back(nalu);
-		}
-	}
+	std::vector<NalUnit> nalus;
+	for (auto nalu : *this)
+		nalus.push_back(*nalu);
+
+	auto fragments = NalUnit::GenerateFragments(nalus, maxFragmentSize);
+	std::vector<shared_ptr<binary>> result;
+	result.reserve(fragments.size());
+	for (auto fragment : fragments)
+		result.push_back(std::make_shared<binary>(std::move(fragment)));
+
 	return result;
 }
 

+ 72 - 27
src/peerconnection.cpp

@@ -61,6 +61,10 @@ PeerConnection::SignalingState PeerConnection::signalingState() const {
 	return impl()->signalingState;
 }
 
+bool PeerConnection::negotiationNeeded() const {
+	return impl()->negotiationNeeded();
+}
+
 optional<Description> PeerConnection::localDescription() const {
 	return impl()->localDescription();
 }
@@ -76,11 +80,12 @@ bool PeerConnection::hasMedia() const {
 	return local && local->hasAudioOrVideo();
 }
 
-void PeerConnection::setLocalDescription(Description::Type type) {
+void PeerConnection::setLocalDescription(Description::Type type, LocalDescriptionInit init) {
 	std::unique_lock signalingLock(impl()->signalingMutex);
 	PLOG_VERBOSE << "Setting local description, type=" << Description::typeToString(type);
 
 	SignalingState signalingState = impl()->signalingState.load();
+
 	if (type == Description::Type::Rollback) {
 		if (signalingState == SignalingState::HaveLocalOffer ||
 		    signalingState == SignalingState::HaveLocalPranswer) {
@@ -98,12 +103,6 @@ void PeerConnection::setLocalDescription(Description::Type type) {
 			type = Description::Type::Offer;
 	}
 
-	// Only a local offer resets the negotiation needed flag
-	if (type == Description::Type::Offer && !impl()->negotiationNeeded.exchange(false)) {
-		PLOG_DEBUG << "No negotiation needed";
-		return;
-	}
-
 	// Get the new signaling state
 	SignalingState newSignalingState;
 	switch (signalingState) {
@@ -140,12 +139,29 @@ void PeerConnection::setLocalDescription(Description::Type type) {
 	if (!iceTransport)
 		return; // closed
 
+	if (init.iceUfrag && init.icePwd) {
+		PLOG_DEBUG << "Setting custom ICE attributes, ufrag=\"" << *init.iceUfrag << "\", pwd=\"" << *init.icePwd << "\"";
+		iceTransport->setIceAttributes(*init.iceUfrag, *init.icePwd);
+	}
+
 	Description local = iceTransport->getLocalDescription(type);
+	impl()->populateLocalDescription(local);
+
+	// There might be no media at this point, for instance if the user deleted tracks
+	if (local.mediaCount() == 0)
+		throw std::runtime_error("No DataChannel or Track to negotiate");
+
 	impl()->processLocalDescription(std::move(local));
 
 	impl()->changeSignalingState(newSignalingState);
 	signalingLock.unlock();
 
+	if (!impl()->config.disableAutoNegotiation && newSignalingState == SignalingState::Stable) {
+		// We might need to make a new offer
+		if (impl()->negotiationNeeded())
+			setLocalDescription(Description::Type::Offer);
+	}
+
 	if (impl()->gatheringState == GatheringState::New && !impl()->config.disableAutoGathering) {
 		iceTransport->gatherLocalCandidates(impl()->localBundleMid());
 	}
@@ -153,9 +169,8 @@ void PeerConnection::setLocalDescription(Description::Type type) {
 
 void PeerConnection::gatherLocalCandidates(std::vector<IceServer> additionalIceServers) {
 	auto iceTransport = impl()->getIceTransport();
-	if (!iceTransport) {
-		throw std::logic_error("No IceTransport. Local Description has not been set");
-	}
+	if (!iceTransport || !localDescription())
+		throw std::logic_error("Local description has not been set before gathering");
 
 	if (impl()->gatheringState == GatheringState::New) {
 		iceTransport->gatherLocalCandidates(impl()->localBundleMid(), additionalIceServers);
@@ -234,7 +249,6 @@ void PeerConnection::setRemoteDescription(Description description) {
 
 	// Candidates will be added at the end, extract them for now
 	auto remoteCandidates = description.extractCandidates();
-	auto type = description.type();
 
 	auto iceTransport = impl()->initIceTransport();
 	if (!iceTransport)
@@ -246,14 +260,26 @@ void PeerConnection::setRemoteDescription(Description description) {
 	impl()->changeSignalingState(newSignalingState);
 	signalingLock.unlock();
 
-	if (type == Description::Type::Offer) {
-		// This is an offer, we need to answer
-		if (!impl()->config.disableAutoNegotiation)
-			setLocalDescription(Description::Type::Answer);
-	}
-
 	for (const auto &candidate : remoteCandidates)
 		addRemoteCandidate(candidate);
+
+	if (!impl()->config.disableAutoNegotiation) {
+		switch (newSignalingState) {
+		case SignalingState::Stable:
+			// We might need to make a new offer
+			if (impl()->negotiationNeeded())
+				setLocalDescription(Description::Type::Offer);
+			break;
+
+		case SignalingState::HaveRemoteOffer:
+			// We need to answer
+			setLocalDescription(Description::Type::Answer);
+			break;
+
+		default:
+			break;
+		}
+	}
 }
 
 void PeerConnection::addRemoteCandidate(Candidate candidate) {
@@ -262,6 +288,26 @@ void PeerConnection::addRemoteCandidate(Candidate candidate) {
 	impl()->processRemoteCandidate(std::move(candidate));
 }
 
+Description PeerConnection::createOffer() {
+	auto iceTransport = impl()->initIceTransport();
+	if (!iceTransport)
+		throw std::runtime_error("Peer connection is closed");
+
+	Description desc = iceTransport->getLocalDescription(rtc::Description::Type::Offer);
+	impl()->populateLocalDescription(desc);
+	return desc;
+}
+
+Description PeerConnection::createAnswer() {
+	auto iceTransport = impl()->initIceTransport();
+	if (!iceTransport)
+		throw std::runtime_error("Peer connection is closed");
+
+	Description desc = iceTransport->getLocalDescription(rtc::Description::Type::Answer);
+	impl()->populateLocalDescription(desc);
+	return desc;
+}
+
 void PeerConnection::setMediaHandler(shared_ptr<MediaHandler> handler) {
 	impl()->setMediaHandler(std::move(handler));
 };
@@ -284,13 +330,11 @@ shared_ptr<DataChannel> PeerConnection::createDataChannel(string label, DataChan
 	auto channelImpl = impl()->emplaceDataChannel(std::move(label), std::move(init));
 	auto channel = std::make_shared<DataChannel>(channelImpl);
 
-	// Renegotiation is needed iff the current local description does not have application
-	auto local = impl()->localDescription();
-	if (!local || !local->hasApplication())
-		impl()->negotiationNeeded = true;
-
-	if (!impl()->config.disableAutoNegotiation)
-		setLocalDescription();
+	if (!impl()->config.disableAutoNegotiation && impl()->signalingState.load() == SignalingState::Stable) {
+		// We might need to make a new offer
+		if (impl()->negotiationNeeded())
+			setLocalDescription(Description::Type::Offer);
+	}
 
 	return channel;
 }
@@ -305,9 +349,6 @@ std::shared_ptr<Track> PeerConnection::addTrack(Description::Media description)
 	auto trackImpl = impl()->emplaceTrack(std::move(description));
 	auto track = std::make_shared<Track>(trackImpl);
 
-	// Renegotiation is needed for the new or updated track
-	impl()->negotiationNeeded = true;
-
 	return track;
 }
 
@@ -367,6 +408,10 @@ optional<std::chrono::milliseconds> PeerConnection::rtt() {
 	return sctpTransport ? sctpTransport->rtt() : nullopt;
 }
 
+CertificateFingerprint PeerConnection::remoteFingerprint() {
+	return impl()->remoteFingerprint();
+}
+
 std::ostream &operator<<(std::ostream &out, PeerConnection::State state) {
 	using State = PeerConnection::State;
 	const char *str;

+ 47 - 0
src/rembhandler.cpp

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2024 Vladimir Voronin
+ *
+ * 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 "rembhandler.hpp"
+#include "rtp.hpp"
+
+#ifdef _WIN32
+#include <winsock2.h>
+#else
+#include <arpa/inet.h>
+#endif
+
+#if RTC_ENABLE_MEDIA
+
+namespace rtc {
+
+RembHandler::RembHandler(std::function<void(unsigned int)> onRemb) : mOnRemb(onRemb) {}
+
+void RembHandler::incoming(message_vector &messages, [[maybe_unused]] const message_callback &send) {
+	for (const auto &message : messages) {
+		size_t offset = 0;
+		while ((sizeof(RtcpHeader) + offset) <= message->size()) {
+			auto header = reinterpret_cast<RtcpHeader *>(message->data() + offset);
+			uint8_t payload_type = header->payloadType();
+
+			if (payload_type == 206 && header->reportCount() == 15 && header->lengthInBytes() == sizeof(RtcpRemb)) {
+				auto remb = reinterpret_cast<RtcpRemb *>(message->data() + offset);
+
+				if (remb->_id[0] == 'R' && remb->_id[1] == 'E' && remb->_id[2] == 'M' && remb->_id[3] == 'B') {
+					mOnRemb(remb->getBitrate());
+					break;
+				}
+			}
+
+			offset += header->lengthInBytes();
+		}
+	}
+}
+
+} // namespace rtc
+
+#endif // RTC_ENABLE_MEDIA

+ 8 - 9
src/rtcpnackresponder.cpp

@@ -46,10 +46,9 @@ void RtcpNackResponder::incoming(message_vector &messages, const message_callbac
 				                              newMissingSeqenceNumbers.end());
 			}
 
-			for (auto sequenceNumber : missingSequenceNumbers) {
-				if (auto optPacket = mStorage->get(sequenceNumber))
-					send(make_message(*optPacket.value()));
-			}
+			for (auto sequenceNumber : missingSequenceNumbers)
+				if (auto packet = mStorage->get(sequenceNumber))
+					send(packet);
 		}
 	}
 }
@@ -61,7 +60,7 @@ void RtcpNackResponder::outgoing(message_vector &messages,
 			mStorage->store(message);
 }
 
-RtcpNackResponder::Storage::Element::Element(binary_ptr packet, uint16_t sequenceNumber,
+RtcpNackResponder::Storage::Element::Element(message_ptr packet, uint16_t sequenceNumber,
                                              shared_ptr<Element> next)
     : packet(packet), sequenceNumber(sequenceNumber), next(next) {}
 
@@ -72,14 +71,14 @@ RtcpNackResponder::Storage::Storage(size_t _maxSize) : maxSize(_maxSize) {
 	storage.reserve(maxSize);
 }
 
-optional<binary_ptr> RtcpNackResponder::Storage::get(uint16_t sequenceNumber) {
+message_ptr RtcpNackResponder::Storage::get(uint16_t sequenceNumber) {
 	std::lock_guard lock(mutex);
 	auto position = storage.find(sequenceNumber);
-	return position != storage.end() ? std::make_optional(storage.at(sequenceNumber)->packet)
-	                                 : nullopt;
+	return position != storage.end() ? storage.at(sequenceNumber)->packet
+	                                 : nullptr;
 }
 
-void RtcpNackResponder::Storage::store(binary_ptr packet) {
+void RtcpNackResponder::Storage::store(message_ptr packet) {
 	if (!packet || packet->size() < sizeof(RtpHeader))
 		return;
 

+ 27 - 15
src/rtcpsrreporter.cpp

@@ -14,6 +14,8 @@
 #include <chrono>
 #include <cmath>
 
+using namespace std::chrono_literals;
+
 namespace {
 
 // TODO: move to utils
@@ -28,16 +30,21 @@ uint64_t ntp_time() {
 
 namespace rtc {
 
-RtcpSrReporter::RtcpSrReporter(shared_ptr<RtpPacketizationConfig> rtpConfig)
-    : rtpConfig(rtpConfig) {
-	mLastReportedTimestamp = rtpConfig->timestamp;
-}
+RtcpSrReporter::RtcpSrReporter(shared_ptr<RtpPacketizationConfig> rtpConfig) : rtpConfig(rtpConfig) {}
 
-void RtcpSrReporter::setNeedsToReport() { mNeedsToReport = true; }
+RtcpSrReporter::~RtcpSrReporter() {}
+
+void RtcpSrReporter::setNeedsToReport() {
+	// Dummy
+}
 
 uint32_t RtcpSrReporter::lastReportedTimestamp() const { return mLastReportedTimestamp; }
 
 void RtcpSrReporter::outgoing(message_vector &messages, const message_callback &send) {
+	if (messages.empty())
+		return;
+
+	uint32_t timestamp = 0;
 	for (const auto &message : messages) {
 		if (message->type == Message::Control)
 			continue;
@@ -45,21 +52,27 @@ void RtcpSrReporter::outgoing(message_vector &messages, const message_callback &
 		if (message->size() < sizeof(RtpHeader))
 			continue;
 
-		auto rtp = reinterpret_cast<RtpHeader *>(message->data());
-		addToReport(rtp, uint32_t(message->size()));
+		auto header = reinterpret_cast<RtpHeader *>(message->data());
+		if(header->ssrc() != rtpConfig->ssrc)
+			continue;
+
+		timestamp = header->timestamp();
+
+		addToReport(header, message->size());
 	}
 
-	if (std::exchange(mNeedsToReport, false)) {
-		auto timestamp = rtpConfig->timestamp;
-		auto sr = getSenderReport(timestamp);
-		send(sr);
+	auto now = std::chrono::steady_clock::now();
+	if (now >= mLastReportTime + 1s) {
+		send(getSenderReport(timestamp));
+		mLastReportedTimestamp = timestamp;
+		mLastReportTime = now;
 	}
 }
 
-void RtcpSrReporter::addToReport(RtpHeader *rtp, uint32_t rtpSize) {
+void RtcpSrReporter::addToReport(RtpHeader *header, size_t size) {
 	mPacketCount += 1;
-	assert(!rtp->padding());
-	mPayloadOctets += rtpSize - uint32_t(rtp->getSize());
+	assert(!header->padding());
+	mPayloadOctets += uint32_t(size - header->getSize());
 }
 
 message_ptr RtcpSrReporter::getSenderReport(uint32_t timestamp) {
@@ -81,7 +94,6 @@ message_ptr RtcpSrReporter::getSenderReport(uint32_t timestamp) {
 	item->setText(rtpConfig->cname);
 	sdes->preparePacket(1);
 
-	mLastReportedTimestamp = timestamp;
 	return msg;
 }
 

+ 9 - 1
src/rtp.cpp

@@ -571,7 +571,7 @@ void RtcpRemb::preparePacket(SSRC senderSSRC, unsigned int numSSRC, unsigned int
 
 void RtcpRemb::setBitrate(unsigned int numSSRC, unsigned int in_bitrate) {
 	unsigned int exp = 0;
-	while (in_bitrate > pow(2, 18) - 1) {
+	while (in_bitrate > 0x3FFFF) {
 		exp++;
 		in_bitrate /= 2;
 	}
@@ -584,6 +584,14 @@ void RtcpRemb::setBitrate(unsigned int numSSRC, unsigned int in_bitrate) {
 
 void RtcpRemb::setSsrc(int iterator, SSRC newSssrc) { _ssrc[iterator] = htonl(newSssrc); }
 
+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));
+}
+
 unsigned int RtcpPli::Size() { return sizeof(RtcpFbHeader); }
 
 void RtcpPli::preparePacket(SSRC messageSSRC) {

+ 13 - 3
src/rtpdepacketizer.cpp

@@ -18,6 +18,12 @@
 
 namespace rtc {
 
+RtpDepacketizer::RtpDepacketizer() : mClockRate(0) {}
+
+RtpDepacketizer::RtpDepacketizer(uint32_t clockRate) : mClockRate(clockRate) {}
+
+RtpDepacketizer::~RtpDepacketizer() {}
+
 void RtpDepacketizer::incoming([[maybe_unused]] message_vector &messages,
                                [[maybe_unused]] const message_callback &send) {
 	message_vector result;
@@ -34,9 +40,13 @@ void RtpDepacketizer::incoming([[maybe_unused]] message_vector &messages,
 
 		auto pkt = reinterpret_cast<const rtc::RtpHeader *>(message->data());
 		auto headerSize = sizeof(rtc::RtpHeader) + pkt->csrcCount() + pkt->getExtensionHeaderSize();
-		result.push_back(make_message(message->begin() + headerSize, message->end(),
-		                              Message::Binary, 0, nullptr,
-		                              std::make_shared<FrameInfo>(pkt->payloadType(), pkt->timestamp())));
+
+		auto frameInfo = std::make_shared<FrameInfo>(pkt->timestamp());
+		if (mClockRate > 0)
+			frameInfo->timestampSeconds =
+			    std::chrono::duration<double>(double(pkt->timestamp()) / double(mClockRate));
+		frameInfo->payloadType = pkt->payloadType();
+		result.push_back(make_message(message->begin() + headerSize, message->end(), frameInfo));
 	}
 
 	messages.swap(result);

+ 45 - 13
src/rtppacketizer.cpp

@@ -19,7 +19,12 @@ RtpPacketizer::RtpPacketizer(shared_ptr<RtpPacketizationConfig> rtpConfig) : rtp
 
 RtpPacketizer::~RtpPacketizer() {}
 
-message_ptr RtpPacketizer::packetize(shared_ptr<binary> payload, bool mark) {
+std::vector<binary> RtpPacketizer::fragment(binary data) {
+	// Default implementation
+	return {std::move(data)};
+}
+
+message_ptr RtpPacketizer::packetize(const binary &payload, bool mark) {
 	size_t rtpExtHeaderSize = 0;
 	bool twoByteHeader = false;
 
@@ -69,7 +74,7 @@ message_ptr RtpPacketizer::packetize(shared_ptr<binary> payload, bool mark) {
 
 	rtpExtHeaderSize = (rtpExtHeaderSize + 3) & ~3;
 
-	auto message = make_message(RtpHeaderSize + rtpExtHeaderSize + payload->size());
+	auto message = make_message(RtpHeaderSize + rtpExtHeaderSize + payload.size());
 	auto *rtp = (RtpHeader *)message->data();
 	rtp->setPayloadType(rtpConfig->payloadType);
 	rtp->setSeqNumber(rtpConfig->sequenceNumber++); // increase sequence number
@@ -127,11 +132,8 @@ message_ptr RtpPacketizer::packetize(shared_ptr<binary> payload, bool mark) {
 			uint16_t max = rtpConfig->playoutDelayMax & 0xFFF;
 
 			// 12 bits for min + 12 bits for max
-			byte data[] = {
-				byte((min >> 4) & 0xFF), 
-				byte(((min & 0xF) << 4) | ((max >> 8) & 0xF)),
-				byte(max & 0xFF)
-			};
+			byte data[] = {byte((min >> 4) & 0xFF), byte(((min & 0xF) << 4) | ((max >> 8) & 0xF)),
+			               byte(max & 0xFF)};
 
 			extHeader->writeOneByteHeader(offset, rtpConfig->playoutDelayId, data, 3);
 			offset += 4;
@@ -140,19 +142,49 @@ message_ptr RtpPacketizer::packetize(shared_ptr<binary> payload, bool mark) {
 
 	rtp->preparePacket();
 
-	std::memcpy(message->data() + RtpHeaderSize + rtpExtHeaderSize, payload->data(),
-	            payload->size());
+	std::memcpy(message->data() + RtpHeaderSize + rtpExtHeaderSize, payload.data(), payload.size());
 
 	return message;
 }
 
+message_ptr RtpPacketizer::packetize(shared_ptr<binary> payload, bool mark) {
+	return packetize(*payload, mark);
+}
+
 void RtpPacketizer::media([[maybe_unused]] const Description::Media &desc) {}
 
-void RtpPacketizer::outgoing([[maybe_unused]] message_vector &messages,
+void RtpPacketizer::outgoing(message_vector &messages,
                              [[maybe_unused]] const message_callback &send) {
-	// Default implementation
-	for (auto &message : messages)
-		message = packetize(message, false);
+	message_vector result;
+	for (const auto &message : messages) {
+		if (const auto &frameInfo = message->frameInfo) {
+			if (frameInfo->payloadType && frameInfo->payloadType != rtpConfig->payloadType)
+				continue;
+
+			if (frameInfo->timestampSeconds)
+				rtpConfig->timestamp =
+				    rtpConfig->startTimestamp +
+				    rtpConfig->secondsToTimestamp(
+				        std::chrono::duration<double>(*frameInfo->timestampSeconds).count());
+			else
+				rtpConfig->timestamp = frameInfo->timestamp;
+		}
+
+		auto payloads = fragment(std::move(*message));
+		if (payloads.size() > 0) {
+			for (size_t i = 0; i < payloads.size(); i++) {
+				if (rtpConfig->dependencyDescriptorContext.has_value()) {
+					auto &ctx = *rtpConfig->dependencyDescriptorContext;
+					ctx.descriptor.startOfFrame = i == 0;
+					ctx.descriptor.endOfFrame = i == payloads.size() - 1;
+				}
+				bool mark = i == payloads.size() - 1;
+				result.push_back(packetize(payloads[i], mark));
+			}
+		}
+	}
+
+	messages.swap(result);
 }
 
 } // namespace rtc

+ 13 - 4
src/track.cpp

@@ -40,6 +40,19 @@ bool Track::isClosed(void) const { return impl()->isClosed(); }
 
 size_t Track::maxMessageSize() const { return impl()->maxMessageSize(); }
 
+void Track::sendFrame(binary data, FrameInfo info) {
+	impl()->outgoing(make_message(std::move(data), std::make_shared<FrameInfo>(std::move(info))));
+}
+
+void Track::sendFrame(const byte *data, size_t size, FrameInfo info) {
+	sendFrame(binary(data, data + size), std::move(info));
+}
+
+void Track::onFrame(std::function<void(binary data, FrameInfo frame)> callback) {
+	impl()->frameCallback = callback;
+	impl()->flushPendingMessages();
+}
+
 void Track::setMediaHandler(shared_ptr<MediaHandler> handler) {
 	impl()->setMediaHandler(std::move(handler));
 }
@@ -70,8 +83,4 @@ bool Track::requestBitrate(unsigned int bitrate) {
 
 shared_ptr<MediaHandler> Track::getMediaHandler() { return impl()->getMediaHandler(); }
 
-void Track::onFrame(std::function<void(binary data, FrameInfo frame)> callback) {
-	impl()->onFrame(callback);
-}
-
 } // namespace rtc

+ 18 - 0
test/capi_track.cpp

@@ -19,6 +19,8 @@ static void sleep(unsigned int secs) { Sleep(secs * 1000); }
 #include <unistd.h> // for sleep
 #endif
 
+#define BUFFER_SIZE 4096
+
 typedef struct {
 	rtcState state;
 	rtcGatheringState gatheringState;
@@ -187,9 +189,25 @@ int test_capi_track_main() {
 		goto error;
 	}
 
+	// Test createOffer
+	char buffer[BUFFER_SIZE];
+	if (rtcCreateOffer(peer1->pc, buffer, BUFFER_SIZE) < 0) {
+		fprintf(stderr, "rtcCreateOffer failed\n");
+		goto error;
+	}
+	if (rtcGetLocalDescription(peer1->pc, buffer, BUFFER_SIZE) >= 0) {
+		fprintf(stderr, "rtcCreateOffer has set the local description\n");
+		goto error;
+	}
+
 	// Initiate the handshake
 	rtcSetLocalDescription(peer1->pc, NULL);
 
+	if (rtcGetLocalDescription(peer1->pc, buffer, BUFFER_SIZE) < 0) {
+		fprintf(stderr, "rtcGetLocalDescription failed\n");
+		goto error;
+	}
+
 	attempts = 10;
 	while ((!peer2->connected || !peer1->connected) && attempts--)
 		sleep(1);