├── .gitignore ├── doc ├── index.md └── Grammar.md ├── test ├── tests.cpp ├── data │ ├── invalid.sdp │ ├── st2022-6.sdp │ ├── aes67.sdp │ ├── alac.sdp │ ├── multicastttl.sdp │ ├── onvif.sdp │ ├── extmap-encrypt.sdp │ ├── icelite.sdp │ ├── st2110-20.sdp │ ├── simulcast.sdp │ ├── normal.sdp │ ├── jssip.sdp │ ├── jsep.sdp │ ├── hacky.sdp │ └── ssrc.sdp ├── CMakeLists.txt ├── include │ └── helpers.hpp └── parse.test.cpp ├── readme-helper ├── CMakeLists.txt └── readme.cpp ├── scripts ├── generate-readme-code.sh ├── test.sh └── get-dep.sh ├── CMakeLists.txt ├── include └── sdptransform.hpp ├── LICENSE ├── src ├── writer.cpp ├── parser.cpp └── grammar.cpp └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /deps/ 2 | /build/ 3 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | * [Grammar](Grammar.md) 4 | -------------------------------------------------------------------------------- /test/tests.cpp: -------------------------------------------------------------------------------- 1 | #include "sdptransform.hpp" 2 | #include "catch_amalgamated.hpp" 3 | 4 | int main(int argc, char* argv[]) 5 | { 6 | int ret = Catch::Session().run(argc, argv); 7 | 8 | return ret; 9 | } 10 | -------------------------------------------------------------------------------- /test/data/invalid.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 3710604898417546434 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | m=audio 1 RTP/AVP 0 6 | c=IN IP4 0.0.0.0 7 | a=rtpmap:0 PCMU/8000 8 | a=rtcp:1 IN IP7 X 9 | a=goo:hithere 10 | f=invalid:yes 11 | -------------------------------------------------------------------------------- /test/data/st2022-6.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 198403 11 IN IP4 192.168.20.20 3 | s=st2022-6 source 4 | t=0 0 5 | m=video 2004 RTP/AVP 98 6 | c=IN IP4 239.0.0.1/32 7 | a=rtpmap:98 SMPTE2022-6/27000000 8 | a=source-filter: incl IN IP4 239.0.0.1 192.168.20.20 9 | -------------------------------------------------------------------------------- /readme-helper/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${sdptransform_SOURCE_DIR}/include) 2 | 3 | set( 4 | SOURCE_FILES 5 | readme.cpp 6 | ) 7 | 8 | add_executable(sdptransform_readme_helper ${SOURCE_FILES}) 9 | 10 | target_link_libraries(sdptransform_readme_helper sdptransform) 11 | -------------------------------------------------------------------------------- /test/data/aes67.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1 1 IN IP4 192.168.2.3 3 | s=from-xnode 4 | c=IN IP4 239.192.1.33/128 5 | t=0 0 6 | m=audio 5004/2 RTP/AVP 96 7 | a=rtpmap:96 L24/48000/2 8 | a=ptime:1 9 | a=maxptime:5 10 | a=recvonly 11 | a=ts-refclk:ptp=IEEE1588-2008:00-1D-C1-FF-FE-12-00-A4:0 12 | a=mediaclk:direct=0 13 | a=sync-time:0 14 | -------------------------------------------------------------------------------- /scripts/generate-readme-code.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | PROJECT_PWD=${PWD} 6 | 7 | current_dir_name=${PROJECT_PWD##*/} 8 | if [ "${current_dir_name}" != "libsdptransform" ] ; then 9 | echo ">>> [ERROR] $(basename $0) must be called from libsdptransform/ root directory" >&2 10 | exit 1 11 | fi 12 | 13 | # Run readme helper. 14 | ./build/readme-helper/sdptransform_readme_helper 15 | -------------------------------------------------------------------------------- /test/data/alac.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=iTunes 3413821438 0 IN IP4 fe80::217:f2ff:fe0f:e0f6 3 | s=iTunes 4 | c=IN IP4 fe80::5a55:caff:fe1a:e187 5 | t=0 0 6 | m=audio 0 RTP/AVP 96 7 | a=rtpmap:96 AppleLossless 8 | a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100 9 | a=fpaeskey:RlBMWQECAQAAAAA8AAAAAPFOnNe+zWb5/n4L5KZkE2AAAAAQlDx69reTdwHF9LaNmhiRURTAbcL4brYAceAkZ49YirXm62N4 10 | a=aesiv:5b+YZi9Ikb845BmNhaVo+Q 11 | -------------------------------------------------------------------------------- /test/data/multicastttl.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1558439701980808 1 IN IP4 192.168.1.189 3 | s=- 4 | c=IN IP4 224.2.36.42/15 5 | t=0 0 6 | a=control:* 7 | a=type:broadcast 8 | a=range:npt=0- 9 | m=video 6970 RTP/AVP 96 10 | b=AS:1000 11 | a=rtpmap:96 H264/90000 12 | a=fmtp:96 packetization-mode=1;profile-level-id=42001F;sprop-parameter-sets=J0IAH5Y1AKALZNwUGBUAAAMD6AAB1MAE,KM4G4g== 13 | a=control:track1 14 | -------------------------------------------------------------------------------- /test/data/onvif.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 2890844256 2890842807 IN IP4 172.16.2.93 3 | s=RTSP Session 4 | m=audio 0 RTP/AVP 0 5 | a=control:rtsp://example.com/onvif_camera/audio 6 | m=video 0 RTP/AVP 26 7 | a=control:rtsp://example.com/onvif_camera/video 8 | m=application 0 RTP/AVP 107 9 | a=rtpmap:107 vnd.onvif.metadata/90000 10 | a=control:rtsp://example.com/onvif_camera/metadata 11 | a=recvonly 12 | a=rtpmap 13 | -------------------------------------------------------------------------------- /test/data/extmap-encrypt.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 20518 0 IN IP4 203.0.113.1 3 | s= 4 | c=IN IP4 203.0.113.1 5 | t=0 0 6 | m=audio 54400 RTP/SAVPF 96 7 | a=rtpmap:96 opus/48000 8 | a=extmap:1/sendonly URI-toffset 9 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 10 | a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24 11 | a=extmap:4/recvonly urn:ietf:params:rtp-hdrext:encrypt URI-gps-string 12 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${sdptransform_SOURCE_DIR}/include) 2 | include_directories(include) 3 | 4 | set( 5 | SOURCE_FILES 6 | tests.cpp 7 | parse.test.cpp 8 | catch_amalgamated.cpp 9 | ) 10 | 11 | set( 12 | HEADER_FILES 13 | ) 14 | 15 | add_definitions(-DCATCH_AMALGAMATED_CUSTOM_MAIN) 16 | add_executable(test_sdptransform ${SOURCE_FILES} ${HEADER_FILES}) 17 | 18 | if(${CMAKE_SYSTEM_NAME} STREQUAL "Android") 19 | target_link_libraries(test_sdptransform sdptransform android log) 20 | else() 21 | target_link_libraries(test_sdptransform sdptransform) 22 | endif() 23 | -------------------------------------------------------------------------------- /test/data/icelite.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 3622532974 3622532974 IN IP4 192.168.100.100 3 | s=- 4 | c=IN IP4 192.168.100.100 5 | t=0 0 6 | a=ice-lite 7 | m=audio 10018 RTP/SAVPF 8 0 101 8 | a=rtpmap:8 PCMA/8000 9 | a=rtpmap:0 PCMU/8000 10 | a=rtpmap:101 telephone-event/8000 11 | a=fmtp:101 0-15 12 | a=setup:actpass 13 | a=sendrecv 14 | a=ice-ufrag:nXET 15 | a=ice-pwd:d0iwx/Qam8JnuvL+wkcXee 16 | a=fingerprint:sha-256 CE:17:02:86:E2:E8:B0:EF:F9:F3:3F:82:8A:A6:F0:EF:30:73:1D:5D:B3:5A:60:D7:AC:FE:F0:E3:DF:D5:D9:7B 17 | a=candidate:X 1 UDP 659136 192.168.100.100 10018 typ host 18 | a=candidate:X 2 UDP 659134 192.168.100.100 10019 typ host 19 | a=rtcp-mux 20 | a=direction:both 21 | -------------------------------------------------------------------------------- /test/include/helpers.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SDPTRANSFORM_HELPERS_HPP 2 | #define SDPTRANSFORM_HELPERS_HPP 3 | 4 | #include 5 | #include // std::ifstream 6 | #include // std::istreambuf_iterator 7 | #include 8 | 9 | namespace helpers 10 | { 11 | inline std::string readFile(const char* file) 12 | { 13 | std::ifstream in(file); 14 | 15 | if (!in) 16 | throw std::invalid_argument("could not open file"); 17 | 18 | std::string content; 19 | 20 | in.seekg(0, std::ios::end); 21 | content.reserve(in.tellg()); 22 | in.seekg(0, std::ios::beg); 23 | 24 | content.assign( 25 | (std::istreambuf_iterator(in)), 26 | std::istreambuf_iterator()); 27 | 28 | return content; 29 | } 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | 3 | project(sdptransform VERSION 1.2.10) 4 | 5 | # For CMake >= 3.1. 6 | set(CMAKE_CXX_STANDARD 14) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | set(CMAKE_CXX_EXTENSIONS OFF) 9 | # For CMake < 3.1. 10 | add_compile_options(-std=c++14) 11 | 12 | subdirs(test readme-helper) 13 | 14 | include_directories(${sdptransform_SOURCE_DIR}/include) 15 | 16 | set( 17 | SOURCE_FILES 18 | src/grammar.cpp 19 | src/parser.cpp 20 | src/writer.cpp 21 | ) 22 | 23 | set( 24 | HEADER_FILES 25 | include/sdptransform.hpp 26 | include/json.hpp 27 | ) 28 | 29 | add_library(sdptransform STATIC ${SOURCE_FILES} ${HEADER_FILES}) 30 | 31 | install(TARGETS sdptransform DESTINATION lib) 32 | install(FILES ${HEADER_FILES} DESTINATION include/sdptransform) 33 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | PROJECT_PWD=${PWD} 6 | 7 | current_dir_name=${PROJECT_PWD##*/} 8 | 9 | if [ "${current_dir_name}" != "libsdptransform" ] ; then 10 | echo "[ERROR] $(basename $0) must be called from libsdptransform/ root directory" >&2 11 | 12 | exit 1 13 | fi 14 | 15 | # Rebuild everything. 16 | if [ "$1" == "rebuild" ]; then 17 | echo "[INFO] rebuilding CMake project: cmake . -Bbuild [...]" 18 | 19 | rm -rf build/ bin/ 20 | cmake . -Bbuild 21 | 22 | # Remove the 'rebuild' argument. 23 | shift 24 | fi 25 | 26 | # Compile. 27 | echo "[INFO] compiling sdptransform and test_sdptransform: cmake --build build" 28 | 29 | cmake --build build 30 | 31 | # Run test. 32 | TEST_BINARY=./build/test/test_sdptransform 33 | 34 | echo "[INFO] running tests: ${TEST_BINARY} $@" 35 | 36 | ${TEST_BINARY} $@ 37 | -------------------------------------------------------------------------------- /include/sdptransform.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SDPTRANSFORM_HPP 2 | #define SDPTRANSFORM_HPP 3 | 4 | #include "json.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using json = nlohmann::json; 12 | 13 | namespace sdptransform 14 | { 15 | namespace grammar 16 | { 17 | struct Rule 18 | { 19 | std::string name; 20 | std::string push; 21 | std::regex reg; 22 | std::vector names; 23 | std::vector types; 24 | std::string format; 25 | std::function formatFunc; 26 | }; 27 | 28 | extern const std::map> rulesMap; 29 | } 30 | 31 | json parse(const std::string& sdp); 32 | 33 | json parseParams(const std::string& str); 34 | 35 | std::vector parsePayloads(const std::string& str); 36 | 37 | json parseImageAttributes(const std::string& str); 38 | 39 | json parseSimulcastStreamList(const std::string& str); 40 | 41 | std::string write(json& session); 42 | } 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /test/data/st2110-20.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 123456 11 IN IP4 192.168.100.2 3 | s=Example of a SMPTE ST2110-20 signal 4 | i=this example is for 720p interlaced video 5 | t=0 0 6 | a=recvonly 7 | a=group:DUP primary secondary 8 | m=video 50000 RTP/AVP 112 9 | c=IN IP4 239.100.9.10/32 10 | a=source-filter: incl IN IP4 239.100.9.10 192.168.100.2 11 | a=rtpmap:112 raw/90000 12 | a=fmtp:112 sampling=YCbCr-4:2:2; width=1280; height=720; interlace; exactframerate=60000/1001; depth=10; TCS=SDR; colorimetry=BT709; PM=2110GPM; SSN=ST2110-20:2017; 13 | a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37 14 | a=mediaclk:direct=0 15 | a=mid:primary 16 | m=video 50020 RTP/AVP 112 17 | c=IN IP4 239.101.9.10/32 18 | a=source-filter: incl IN IP4 239.101.9.10 192.168.101.2 19 | a=rtpmap:112 raw/90000 20 | a=fmtp:112 sampling=YCbCr-4:2:2; width=1280; height=720; interlace; exactframerate=60000/1001; depth=10; TCS=SDR; colorimetry=BT709; PM=2110GPM; SSN=ST2110-20:2017; 21 | a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37 22 | a=mediaclk:direct=0 23 | a=mid:secondary 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Iñaki Baz Castillo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/data/simulcast.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=alice 2362969037 2362969040 IN IP4 192.0.2.156 3 | s=Simulcast Enabled Client 4 | c=IN IP4 192.0.2.156 5 | t=0 0 6 | m=audio 49200 RTP/AVP 0 7 | a=rtpmap:0 PCMU/8000 8 | m=video 49300 RTP/AVP 97 98 99 100 9 | a=rtpmap:97 H264/90000 10 | a=rtpmap:98 H264/90000 11 | a=rtpmap:99 H264/90000 12 | a=rtpmap:100 VP8/90000 13 | a=fmtp:97 profile-level-id=42c01f; max-fs=3600; max-mbps=108000 14 | a=fmtp:98 profile-level-id=42c00b; max-fs=240; max-mbps=3600 15 | a=fmtp:99 profile-level-id=42c00b; max-fs=120; max-mbps=1800 16 | a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:RtpStreamId 17 | a=rid:1 send pt=97;max-width=1280;max-height=720;max-fps=30 18 | a=rid:2 send pt=98 19 | a=rid:3 send pt=99 20 | a=rid:4 send pt=100 21 | a=rid:c recv pt=97 22 | a=imageattr:97 send [x=1280,y=720] recv [x=1280,y=720] [x=320,y=180] [x=160,y=90] 23 | a=imageattr:98 send [x=320,y=180,sar=1.1,q=0.6] 24 | a=imageattr:99 send [x=160,y=90] 25 | a=imageattr:100 recv [x=1280,y=720] [x=320,y=180] send [x=1280,y=720] 26 | a=imageattr:* recv * 27 | a=simulcast:send 1,~4;2;3 recv c 28 | a=simulcast: send rid=1,4;2;3 paused=4 recv rid=c 29 | -------------------------------------------------------------------------------- /test/data/normal.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 20518 0 IN IP4 203.0.113.1 3 | s= 4 | c=IN IP4 203.0.113.1 5 | t=0 0 6 | a=ice-ufrag:F7gI 7 | a=ice-pwd:x9cml/YzichV2+XlhiMu8g 8 | a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7 9 | m=audio 54400 RTP/SAVPF 0 96 10 | a=rtpmap:0 PCMU/8000 11 | a=rtpmap:96 opus/48000 12 | a=extmap:1 URI-toffset 13 | a=extmap:2/recvonly URI-gps-string 14 | a=extmap-allow-mixed 15 | a=ptime:20 16 | a=sendrecv 17 | a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host 18 | a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host 19 | a=candidate:2 1 UDP 1686052607 203.0.113.1 54402 typ srflx raddr 192.168.1.145 rport 54402 generation 0 network-id 3 network-cost 10 20 | a=candidate:3 2 UDP 1686052606 203.0.113.1 54403 typ srflx raddr 192.168.1.145 rport 54403 generation 0 network-id 3 network-cost 10 21 | m=video 55400 RTP/SAVPF 97 98 22 | a=rtpmap:97 H264/90000 23 | a=rtpmap:98 VP8/90000 24 | a=fmtp:97 profile-level-id=42e034;packetization-mode=1;sprop-parameter-sets=Z0IAH5WoFAFuQA==,aM48gA== 25 | a=fmtp:98 minptime=10; useinbandfec=1 26 | a=rtcp-fb:98 trr-int 100 27 | a=rtcp-fb:* nack 28 | a=rtcp-fb:98 nack rpsi 29 | a=crypto:1 AES_CM_128_HMAC_SHA1_32 inline:keNcG3HezSNID7LmfDa9J4lfdUL8W1F7TNJKcbuy|2^20|1:32 30 | a=sendrecv 31 | a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host 32 | a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host 33 | a=candidate:2 1 UDP 1686052607 203.0.113.1 55402 typ srflx raddr 192.168.1.145 rport 55402 generation 0 network-id 3 34 | a=candidate:3 2 UDP 1686052606 203.0.113.1 55403 typ srflx raddr 192.168.1.145 rport 55403 generation 0 network-id 3 35 | a=ssrc:1399694169 foo:bar 36 | a=ssrc:1399694169 baz 37 | a=ssrc:1399694169 foo-bar:baz 38 | -------------------------------------------------------------------------------- /test/data/jssip.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1334496563563564720 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE audio 6 | a=msid-semantic: WMS KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg 7 | m=audio 60017 RTP/SAVPF 111 103 104 0 8 106 105 13 126 8 | c=IN IP4 193.84.77.194 9 | a=rtcp:60017 IN IP4 193.84.77.194 10 | a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 11 | a=candidate:1162875081 2 udp 2113937151 192.168.34.75 60017 typ host generation 0 12 | a=candidate:3289912957 1 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 13 | a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 14 | a=candidate:198437945 1 tcp 1509957375 192.168.34.75 0 typ host generation 0 15 | a=candidate:198437945 2 tcp 1509957375 192.168.34.75 0 typ host generation 0 16 | a=ice-ufrag:5I2uVefP13X1wzOY 17 | a=ice-pwd:e46UjXntt0K/xTncQcDBQePn 18 | a=ice-options:google-ice 19 | a=fingerprint:sha-256 79:14:AB:AB:93:7F:07:E8:91:1A:11:16:36:D0:11:66:C4:4F:31:A0:74:46:65:58:70:E5:09:95:48:F4:4B:D9 20 | a=setup:actpass 21 | a=mid:audio 22 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 23 | a=sendrecv 24 | a=rtcp-mux 25 | a=crypto:0 AES_CM_128_HMAC_SHA1_32 inline:6JYKxLF+o2nhouDHr5J0oNb3CEGK3I/HHv9idGTY 26 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:ayId2M5kCitGTEEI9OjgEqatTA0IXGpQhFjmKOGk 27 | a=rtpmap:111 opus/48000/2 28 | a=fmtp:111 minptime=10 29 | a=rtpmap:103 ISAC/16000 30 | a=rtpmap:104 ISAC/32000 31 | a=rtpmap:0 PCMU/8000 32 | a=rtpmap:8 PCMA/8000 33 | a=rtpmap:106 CN/32000 34 | a=rtpmap:105 CN/16000 35 | a=rtpmap:13 CN/8000 36 | a=rtpmap:126 telephone-event/8000 37 | a=maxptime:60 38 | a=ssrc:1399694169 cname:w7AkLB30C7pk/PFE 39 | a=ssrc:1399694169 msid:KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg 775dca64-4698-455b-8a02-89833bd24773 40 | a=ssrc:1399694169 mslabel:KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg 41 | a=ssrc:1399694169 label:775dca64-4698-455b-8a02-89833bd24773 42 | -------------------------------------------------------------------------------- /test/data/jsep.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4962303333179871722 1 IN IP4 0.0.0.0 3 | s=- 4 | t=0 0 5 | a=msid-semantic:WMS 6 | a=group:BUNDLE a1 v1 7 | m=audio 56500 UDP/TLS/RTP/SAVPF 96 0 8 97 98 8 | c=IN IP4 192.0.2.1 9 | a=mid:a1 10 | a=rtcp:56501 IN IP4 192.0.2.1 11 | a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9 12 | a=sendrecv 13 | a=rtpmap:96 opus/48000/2 14 | a=rtpmap:0 PCMU/8000 15 | a=rtpmap:8 PCMA/8000 16 | a=rtpmap:97 telephone-event/8000 17 | a=rtpmap:98 telephone-event/48000 18 | a=maxptime:120 19 | a=ice-ufrag:ETEn1v9DoTMB9J4r 20 | a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl 21 | a=ice-options:trickle 22 | a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2 23 | a=setup:actpass 24 | a=rtcp-mux 25 | a=rtcp-rsize 26 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 27 | a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid 28 | a=ssrc:1732846380 cname:EocUG1f0fcg/yvY7 29 | a=candidate:3348148302 1 udp 2113937151 192.0.2.1 56500 typ host 30 | a=candidate:3348148302 2 udp 2113937151 192.0.2.1 56501 typ host 31 | a=end-of-candidates 32 | m=video 56502 UDP/TLS/RTP/SAVPF 100 101 33 | c=IN IP4 192.0.2.1 34 | a=rtcp:56503 IN IP4 192.0.2.1 35 | a=mid:v1 36 | a=msid:61317484-2ed4-49d7-9eb7-1414322a7aae f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0 37 | a=sendrecv 38 | a=rtpmap:100 VP8/90000 39 | a=rtpmap:101 rtx/90000 40 | a=fmtp:101 apt=100 41 | a=ice-ufrag:BGKkWnG5GmiUpdIV 42 | a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf 43 | a=ice-options:trickle 44 | a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2 45 | a=setup:actpass 46 | a=rtcp-mux 47 | a=rtcp-rsize 48 | a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid 49 | a=rtcp-fb:100 ccm fir 50 | a=rtcp-fb:100 nack 51 | a=rtcp-fb:100 nack pli 52 | a=ssrc:1366781083 cname:EocUG1f0fcg/yvY7 53 | a=ssrc:1366781084 cname:EocUG1f0fcg/yvY7 54 | a=ssrc-group:FID 1366781083 1366781084 55 | a=candidate:3348148302 1 udp 2113937151 192.0.2.1 56502 typ host 56 | a=candidate:3348148302 2 udp 2113937151 192.0.2.1 56503 typ host 57 | a=end-of-candidates 58 | -------------------------------------------------------------------------------- /scripts/get-dep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | PROJECT_PWD=${PWD} 6 | DEP=$1 7 | 8 | current_dir_name=${PROJECT_PWD##*/} 9 | if [ "${current_dir_name}" != "libsdptransform" ] ; then 10 | echo ">>> [ERROR] $(basename $0) must be called from libsdptransform/ root directory" >&2 11 | exit 1 12 | fi 13 | 14 | function get_dep() 15 | { 16 | GIT_REPO="$1" 17 | GIT_TAG="$2" 18 | DEST="$3" 19 | 20 | echo ">>> [INFO] getting dep '${DEP}' ..." 21 | 22 | if [ -d "${DEST}" ] ; then 23 | echo ">>> [INFO] deleting ${DEST} ..." 24 | git rm -rf --ignore-unmatch ${DEST} > /dev/null 25 | rm -rf ${DEST} 26 | fi 27 | 28 | echo ">>> [INFO] cloning ${GIT_REPO} ..." 29 | git clone ${GIT_REPO} ${DEST} 30 | 31 | cd ${DEST} 32 | 33 | echo ">>> [INFO] setting '${GIT_TAG}' git tag ..." 34 | git checkout --quiet ${GIT_TAG} 35 | set -e 36 | 37 | echo ">>> [INFO] got dep '${DEP}'" 38 | 39 | cd ${PROJECT_PWD} 40 | } 41 | 42 | function get_json() 43 | { 44 | GIT_REPO="https://github.com/nlohmann/json.git" 45 | GIT_TAG="v3.11.2" 46 | DEST="deps/json" 47 | 48 | get_dep "${GIT_REPO}" "${GIT_TAG}" "${DEST}" 49 | 50 | echo ">>> [INFO] copying json.hpp to include/ directory ..." 51 | cp ${DEST}/single_include/nlohmann/json.hpp include/ 52 | } 53 | 54 | function get_catch() 55 | { 56 | GIT_REPO="https://github.com/catchorg/Catch2.git" 57 | GIT_TAG="v3.2.1" 58 | DEST="deps/catch" 59 | 60 | get_dep "${GIT_REPO}" "${GIT_TAG}" "${DEST}" 61 | 62 | # Catch2 v3 is no longer a single header library and we should build it as a 63 | # proper (static) library. May do it in the future. 64 | # Doc: https://github.com/catchorg/Catch2/blob/devel/docs/migrate-v2-to-v3.md#migrating-from-v2-to-v3 65 | 66 | echo ">>> [INFO] copying include file to test/include/ directory ..." 67 | cp ${DEST}/extras/catch_amalgamated.hpp test/include/ 68 | 69 | echo ">>> [INFO] copying source file to test/ directory ..." 70 | cp ${DEST}/extras/catch_amalgamated.cpp test/ 71 | } 72 | 73 | case "${DEP}" in 74 | '-h') 75 | echo "Usage:" 76 | echo " ./scripts/$(basename $0) [json|catch]" 77 | echo 78 | ;; 79 | json) 80 | get_json 81 | ;; 82 | catch) 83 | get_catch 84 | ;; 85 | *) 86 | echo ">>> [ERROR] unknown dep '${DEP}'" >&2 87 | exit 1 88 | esac 89 | 90 | if [ $? -eq 0 ] ; then 91 | echo ">>> [INFO] done" 92 | else 93 | echo ">>> [ERROR] failed" >&2 94 | exit 1 95 | fi 96 | -------------------------------------------------------------------------------- /test/data/hacky.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 3710604898417546434 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 6 | a=group:BUNDLE audio video 7 | m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:111 opus/48000/2 10 | a=rtpmap:103 ISAC/16000 11 | a=rtpmap:104 ISAC/32000 12 | a=rtpmap:0 PCMU/8000 13 | a=rtpmap:8 PCMA/8000 14 | a=rtpmap:107 CN/48000 15 | a=rtpmap:106 CN/32000 16 | a=rtpmap:105 CN/16000 17 | a=rtpmap:13 CN/8000 18 | a=rtpmap:126 telephone-event/8000 19 | a=fmtp:111 minptime=10 20 | a=rtcp:1 IN IP4 0.0.0.0 21 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 22 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:8QVQSHJ2AM8gIumHpYRRdWHyZ5NkLhaTD1AENOWx 23 | a=mid:audio 24 | a=maxptime:60 25 | a=sendrecv 26 | a=ice-ufrag:lat6xwB1/flm+VwG 27 | a=ice-pwd:L5+HonleGeFHa8jPZLc/kr0E 28 | a=candidate:1127303604 1 udp 2122260223 0.0.0.0 60672 typ host generation 0 29 | a=candidate:229815620 1 tcp 1518280447 0.0.0.0 0 typ host tcptype active generation 0 30 | a=candidate:1 1 TCP 2128609279 10.0.1.1 9 typ host tcptype active 31 | a=candidate:2 1 TCP 2124414975 10.0.1.1 8998 typ host tcptype passive 32 | a=candidate:3 1 TCP 2120220671 10.0.1.1 8999 typ host tcptype so 33 | a=candidate:4 1 TCP 1688207359 192.0.2.3 9 typ srflx raddr 10.0.1.1 rport 9 tcptype active 34 | a=candidate:5 1 TCP 1684013055 192.0.2.3 45664 typ srflx raddr 10.0.1.1 rport 8998 tcptype passive generation 5 35 | a=candidate:6 1 TCP 1692401663 192.0.2.3 45687 typ srflx raddr 10.0.1.1 rport 8999 tcptype so 36 | a=ice-options:google-ice 37 | a=ssrc:2754920552 cname:t9YU8M1UxTF8Y1A1 38 | a=ssrc:2754920552 msid:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0 39 | a=ssrc:2754920552 mslabel:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 40 | a=ssrc:2754920552 label:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0 41 | a=rtcp-mux 42 | m=video 1 RTP/SAVPF 100 116 117 43 | c=IN IP4 0.0.0.0 44 | a=rtpmap:100 VP8/90000 45 | a=rtpmap:116 red/90000 46 | a=rtpmap:117 ulpfec/90000 47 | a=rtcp:12312 48 | a=rtcp-fb:100 ccm fir 49 | a=rtcp-fb:100 nack 50 | a=rtcp-fb:100 goog-remb 51 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 52 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:8QVQSHJ2AM8gIumHpYRRdWHyZ5NkLhaTD1AENOWx 53 | a=mid:video 54 | a=sendrecv 55 | a=ice-ufrag:lat6xwB1/flm+VwG 56 | a=ice-pwd:L5+HonleGeFHa8jPZLc/kr0E 57 | a=ice-options:google-ice 58 | a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 59 | a=ssrc:2566107569 msid:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVv0 60 | a=ssrc:2566107569 mslabel:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 61 | a=ssrc:2566107569 label:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVv0 62 | a=rtcp-mux 63 | a=framerate:1234.0 64 | m=application 9 DTLS/SCTP 5000 65 | c=IN IP4 0.0.0.0 66 | b=AS:30 67 | a=setup:active 68 | a=mid:33db2c4da91d73fd 69 | a=ice-ufrag:pDUB98Lc+2dc5+JF 70 | a=ice-pwd:G/CIMBOa9RQINDL4Y8NjpotH 71 | a=fingerprint:sha-256 F0:37:78:FE:3D:13:E9:10:B5:0C:4C:9E:48:37:E7:A0:F8:16:DC:1A:2C:69:67:B0:DF:E6:CB:73:F8:EF:BA:02 72 | a=sctpmap:5000 webrtc-datachannel 1024 73 | a=framerate:29.97 74 | -------------------------------------------------------------------------------- /readme-helper/readme.cpp: -------------------------------------------------------------------------------- 1 | #include "sdptransform.hpp" 2 | #include 3 | #include 4 | 5 | static std::string sdpStr = R"(v=0 6 | o=- 20518 0 IN IP4 203.0.113.1 7 | s= 8 | t=0 0 9 | c=IN IP4 203.0.113.1 10 | a=ice-ufrag:F7gI 11 | a=ice-pwd:x9cml/YzichV2+XlhiMu8g 12 | a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7 13 | m=audio 54400 RTP/SAVPF 0 96 14 | a=rtpmap:0 PCMU/8000 15 | a=rtpmap:96 opus/48000 16 | a=ptime:20 17 | a=sendrecv 18 | a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host 19 | a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host 20 | m=video 55400 RTP/SAVPF 97 98 21 | a=rtcp-fb:* nack 22 | a=rtpmap:97 H264/90000 23 | a=fmtp:97 profile-level-id=4d0028;packetization-mode=1 24 | a=rtcp-fb:97 trr-int 100 25 | a=rtcp-fb:97 nack rpsi 26 | a=rtpmap:98 VP8/90000 27 | a=rtcp-fb:98 trr-int 100 28 | a=rtcp-fb:98 nack rpsi 29 | a=sendrecv 30 | a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host 31 | a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host 32 | a=ssrc:1399694169 foo:bar 33 | a=ssrc:1399694169 baz 34 | )"; 35 | 36 | static json session; 37 | 38 | void printParserSection(); 39 | void printWriterSection(); 40 | 41 | int main(int argc, char* argv[]) 42 | { 43 | printParserSection(); 44 | printWriterSection(); 45 | 46 | return 0; 47 | } 48 | 49 | void printParserSection() 50 | { 51 | std::cout << "### Parser" << std::endl << std::endl; 52 | 53 | std::cout << ">>> sdptransform::parse():" << std::endl << std::endl; 54 | 55 | session = sdptransform::parse(sdpStr); 56 | 57 | std::cout << session.dump(2) << std::endl << std::endl; 58 | 59 | std::cout << "sdptransform::parseParams():" << std::endl << std::endl; 60 | 61 | json params = 62 | sdptransform::parseParams(session.at("media")[1].at("fmtp")[0].at("config")); 63 | 64 | std::cout << params.dump(2) << std::endl << std::endl; 65 | 66 | std::cout << "sdptransform::parsePayloads():" << std::endl << std::endl; 67 | 68 | json payloads = 69 | sdptransform::parsePayloads(session.at("media")[1].at("payloads")); 70 | 71 | std::cout << payloads.dump(2) << std::endl << std::endl; 72 | 73 | std::cout << "sdptransform::parseImageAttributes():" << std::endl << std::endl; 74 | 75 | std::string imageAttributesStr = "[x=1280,y=720] [x=320,y=180]"; 76 | 77 | json imageAttributes = sdptransform::parseImageAttributes(imageAttributesStr); 78 | 79 | std::cout << imageAttributes.dump(2) << std::endl << std::endl; 80 | 81 | std::cout << "sdptransform::parseSimulcastStreamList():" << std::endl << std::endl; 82 | 83 | std::string simulcastAttributesStr = "1,~4;2;3"; 84 | 85 | json simulcastAttributes = 86 | sdptransform::parseSimulcastStreamList(simulcastAttributesStr); 87 | 88 | std::cout << simulcastAttributes.dump(2) << std::endl << std::endl; 89 | } 90 | 91 | void printWriterSection() 92 | { 93 | std::cout << "### Writer" << std::endl << std::endl; 94 | 95 | std::cout << ">>> sdptransform::write():" << std::endl << std::endl; 96 | 97 | std::string newSdpStr = sdptransform::write(session); 98 | 99 | std::cout << newSdpStr << std::endl << std::endl; 100 | } 101 | -------------------------------------------------------------------------------- /test/data/ssrc.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4327261771880257373 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE audio video 6 | a=msid-semantic: WMS xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 7 | m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 8 | c=IN IP4 0.0.0.0 9 | a=rtcp:9 IN IP4 0.0.0.0 10 | a=ice-ufrag:ez5G 11 | a=ice-pwd:1F1qS++jzWLSQi0qQDZkX/QV 12 | a=fingerprint:sha-256 D2:FA:0E:C3:22:59:5E:14:95:69:92:3D:13:B4:84:24:2C:C2:A2:C0:3E:FD:34:8E:5E:EA:6F:AF:52:CE:E6:0F 13 | a=setup:actpass 14 | a=mid:audio 15 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 16 | a=sendrecv 17 | a=rtcp-mux 18 | a=rtpmap:111 opus/48000/2 19 | a=rtcp-fb:111 transport-cc 20 | a=fmtp:111 minptime=10;useinbandfec=1 21 | a=rtpmap:103 ISAC/16000 22 | a=rtpmap:104 ISAC/32000 23 | a=rtpmap:9 G722/8000 24 | a=rtpmap:0 PCMU/8000 25 | a=rtpmap:8 PCMA/8000 26 | a=rtpmap:106 CN/32000 27 | a=rtpmap:105 CN/16000 28 | a=rtpmap:13 CN/8000 29 | a=rtpmap:110 telephone-event/48000 30 | a=rtpmap:112 telephone-event/32000 31 | a=rtpmap:113 telephone-event/16000 32 | a=rtpmap:126 telephone-event/8000 33 | a=ssrc:3510681183 cname:loqPWNg7JMmrFUnr 34 | a=ssrc:3510681183 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 7ea47500-22eb-4815-a899-c74ef321b6ee 35 | a=ssrc:3510681183 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 36 | a=ssrc:3510681183 label:7ea47500-22eb-4815-a899-c74ef321b6ee 37 | m=video 9 UDP/TLS/RTP/SAVPF 96 98 100 102 127 125 97 99 101 124 38 | c=IN IP4 0.0.0.0 39 | a=rtcp:9 IN IP4 0.0.0.0 40 | a=ice-ufrag:ez5G 41 | a=ice-pwd:1F1qS++jzWLSQi0qQDZkX/QV 42 | a=fingerprint:sha-256 D2:FA:0E:C3:22:59:5E:14:95:69:92:3D:13:B4:84:24:2C:C2:A2:C0:3E:FD:34:8E:5E:EA:6F:AF:52:CE:E6:0F 43 | a=setup:actpass 44 | a=mid:video 45 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 46 | a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 47 | a=extmap:4 urn:3gpp:video-orientation 48 | a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 49 | a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay 50 | a=sendrecv 51 | a=rtcp-mux 52 | a=rtcp-rsize 53 | a=rtpmap:96 VP8/90000 54 | a=rtcp-fb:96 ccm fir 55 | a=rtcp-fb:96 nack 56 | a=rtcp-fb:96 nack pli 57 | a=rtcp-fb:96 goog-remb 58 | a=rtcp-fb:96 transport-cc 59 | a=rtpmap:98 VP9/90000 60 | a=rtcp-fb:98 ccm fir 61 | a=rtcp-fb:98 nack 62 | a=rtcp-fb:98 nack pli 63 | a=rtcp-fb:98 goog-remb 64 | a=rtcp-fb:98 transport-cc 65 | a=rtpmap:100 H264/90000 66 | a=rtcp-fb:100 ccm fir 67 | a=rtcp-fb:100 nack 68 | a=rtcp-fb:100 nack pli 69 | a=rtcp-fb:100 goog-remb 70 | a=rtcp-fb:100 transport-cc 71 | a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 72 | a=rtpmap:102 red/90000 73 | a=rtpmap:127 ulpfec/90000 74 | a=rtpmap:125 flexfec-03/90000 75 | a=rtcp-fb:125 ccm fir 76 | a=rtcp-fb:125 nack 77 | a=rtcp-fb:125 nack pli 78 | a=rtcp-fb:125 goog-remb 79 | a=rtcp-fb:125 transport-cc 80 | a=fmtp:125 repair-window=10000000 81 | a=rtpmap:97 rtx/90000 82 | a=fmtp:97 apt=96 83 | a=rtpmap:99 rtx/90000 84 | a=fmtp:99 apt=98 85 | a=rtpmap:101 rtx/90000 86 | a=fmtp:101 apt=100 87 | a=rtpmap:124 rtx/90000 88 | a=fmtp:124 apt=102 89 | a=ssrc-group:FID 3004364195 1126032854 90 | a=ssrc-group:FEC-FR 3004364195 1080772241 91 | a=ssrc:3004364195 cname:loqPWNg7JMmrFUnr 92 | a=ssrc:3004364195 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj cf093ab0-0b28-4930-8fe1-7ca8d529be25 93 | a=ssrc:3004364195 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 94 | a=ssrc:3004364195 label:cf093ab0-0b28-4930-8fe1-7ca8d529be25 95 | a=ssrc:1126032854 cname:loqPWNg7JMmrFUnr 96 | a=ssrc:1126032854 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj cf093ab0-0b28-4930-8fe1-7ca8d529be25 97 | a=ssrc:1126032854 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 98 | a=ssrc:1126032854 label:cf093ab0-0b28-4930-8fe1-7ca8d529be25 99 | a=ssrc:1080772241 cname:loqPWNg7JMmrFUnr 100 | a=ssrc:1080772241 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj cf093ab0-0b28-4930-8fe1-7ca8d529be25 101 | a=ssrc:1080772241 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 102 | a=ssrc:1080772241 label:cf093ab0-0b28-4930-8fe1-7ca8d529be25 103 | -------------------------------------------------------------------------------- /src/writer.cpp: -------------------------------------------------------------------------------- 1 | #include "sdptransform.hpp" 2 | #include // size_t 3 | #include // std::stringstream 4 | #include 5 | 6 | namespace sdptransform 7 | { 8 | void makeLine( 9 | std::stringstream& sdpstream, 10 | char type, 11 | const grammar::Rule& rule, 12 | const json& location 13 | ); 14 | 15 | std::string write(json& session) 16 | { 17 | // RFC specified order. 18 | static const std::vector OuterOrder = 19 | { 'v', 'o', 's', 'i', 'u', 'e', 'p', 'c', 'b', 't', 'r', 'z', 'a' }; 20 | static const std::vector InnerOrder = 21 | { 'i', 'c', 'b', 'a' }; 22 | 23 | if (!session.is_object()) 24 | throw std::invalid_argument("given session is not a JSON object"); 25 | 26 | // Ensure certain properties exist. 27 | 28 | if (session.find("version") == session.end()) 29 | session["version"] = 0; 30 | 31 | if (session.find("name") == session.end()) 32 | session["name"] = "-"; 33 | 34 | if (session.find("media") == session.end()) 35 | session["media"] = json::array(); 36 | 37 | for (auto& mLine : session.at("media")) 38 | { 39 | if (mLine.find("payloads") == mLine.end()) 40 | mLine["payloads"] = ""; 41 | } 42 | 43 | std::stringstream sdpstream; 44 | 45 | // Loop through OuterOrder for matching properties on session. 46 | for (auto type : OuterOrder) 47 | { 48 | for (auto& rule : grammar::rulesMap.at(type)) 49 | { 50 | json::const_iterator it; 51 | 52 | if ( 53 | !rule.name.empty() && 54 | (it = session.find(rule.name)) != session.end() && 55 | !it->is_null() 56 | ) 57 | { 58 | makeLine(sdpstream, type, rule, session); 59 | } 60 | else if ( 61 | !rule.push.empty() && 62 | (it = session.find(rule.push)) != session.end() && 63 | it->is_array() 64 | ) 65 | { 66 | for (auto& el : session.at(rule.push)) 67 | { 68 | makeLine(sdpstream, type, rule, el); 69 | } 70 | } 71 | } 72 | } 73 | 74 | // Then for each media line, follow the InnerOrder. 75 | for (auto& mLine : session.at("media")) 76 | { 77 | makeLine(sdpstream, 'm', grammar::rulesMap.at('m')[0], mLine); 78 | 79 | for (auto type : InnerOrder) 80 | { 81 | for (auto& rule : grammar::rulesMap.at(type)) 82 | { 83 | json::const_iterator it; 84 | 85 | if ( 86 | !rule.name.empty() && 87 | (it = mLine.find(rule.name)) != mLine.end() && 88 | !it->is_null() 89 | ) 90 | { 91 | makeLine(sdpstream, type, rule, mLine); 92 | } 93 | else if ( 94 | !rule.push.empty() && 95 | (it = mLine.find(rule.push)) != mLine.end() && 96 | it->is_array() 97 | ) 98 | { 99 | for (auto& el : mLine.at(rule.push)) 100 | { 101 | makeLine(sdpstream, type, rule, el); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | return sdpstream.str(); 109 | } 110 | 111 | void makeLine( 112 | std::stringstream& sdpstream, 113 | char type, 114 | const grammar::Rule& rule, 115 | const json& location 116 | ) 117 | { 118 | static const std::regex FormatRegex("%[sdv%]"); 119 | 120 | const std::string format = rule.format.empty() 121 | ? rule.formatFunc( 122 | !rule.push.empty() 123 | ? location 124 | : !rule.name.empty() 125 | ? location.at(rule.name) 126 | : location) 127 | : rule.format; 128 | 129 | std::vector args; 130 | auto it = location.find(rule.name); 131 | 132 | if (!rule.names.empty()) 133 | { 134 | for (auto& name : rule.names) 135 | { 136 | json::const_iterator it; 137 | 138 | if ( 139 | !rule.name.empty() && 140 | (it = location.find(rule.name)) != location.end() && 141 | it->find(name) != it->end() 142 | ) 143 | { 144 | args.push_back(location.at(rule.name).at(name)); 145 | } 146 | // For mLine and push attributes. 147 | else if (location.find(name) != location.end()) 148 | { 149 | args.push_back(location.at(name)); 150 | } 151 | // NOTE: Otherwise ensure an empty value is inserted into args array. 152 | else 153 | { 154 | args.push_back(""); 155 | } 156 | } 157 | } 158 | else if (it != location.end()) 159 | { 160 | args.push_back(*it); 161 | } 162 | 163 | std::stringstream linestream; 164 | size_t i = 0; 165 | size_t len = args.size(); 166 | 167 | linestream << type << "="; 168 | 169 | for( 170 | auto it = std::sregex_iterator(format.begin(), format.end(), FormatRegex); 171 | it != std::sregex_iterator(); 172 | ++it 173 | ) 174 | { 175 | const std::smatch& match = *it; 176 | const std::string& str = match.str(); 177 | 178 | if (i >= len) 179 | { 180 | linestream << str; 181 | } 182 | else 183 | { 184 | auto& arg = args[i]; 185 | i++; 186 | 187 | linestream << match.prefix(); 188 | 189 | if (str == "%%") 190 | { 191 | linestream << "%"; 192 | } 193 | else if (str == "%s" || str == "%d") 194 | { 195 | if (arg.is_string()) 196 | linestream << arg.get(); 197 | else 198 | linestream << arg; 199 | } 200 | else if (str == "%v") 201 | { 202 | // Do nothing. 203 | } 204 | } 205 | } 206 | 207 | linestream << "\r\n"; 208 | 209 | sdpstream << linestream.str(); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/parser.cpp: -------------------------------------------------------------------------------- 1 | #include "sdptransform.hpp" 2 | #include 3 | #include // size_t 4 | #include // std::addressof() 5 | #include // std::stringstream, std::istringstream 6 | #include // std::noskipws 7 | #include // std::find_if() 8 | #include // std::isspace() 9 | #include // std::uint64_t 10 | 11 | namespace sdptransform 12 | { 13 | void parseReg(const grammar::Rule& rule, json& location, const std::string& content); 14 | 15 | void attachProperties( 16 | const std::smatch& match, 17 | json& location, 18 | const std::vector& names, 19 | const std::string& rawName, 20 | const std::vector& types 21 | ); 22 | 23 | json toType(const std::string& str, char type); 24 | 25 | bool isNumber(const std::string& str); 26 | 27 | bool isInt(const std::string& str); 28 | 29 | bool isFloat(const std::string& str); 30 | 31 | void trim(std::string& str); 32 | 33 | void insertParam(json& o, const std::string& str); 34 | 35 | json parse(const std::string& sdp) 36 | { 37 | static const std::regex ValidLineRegex("^([a-z])=(.*)"); 38 | 39 | json session = json::object(); 40 | std::stringstream sdpstream(sdp); 41 | std::string line; 42 | json media = json::array(); 43 | json* location = std::addressof(session); 44 | 45 | while (std::getline(sdpstream, line, '\n')) 46 | { 47 | // Remove \r if lines are separated with \r\n (as mandated in SDP). 48 | if (line.size() && line[line.length() - 1] == '\r') 49 | line.pop_back(); 50 | 51 | // Ensure it's a valid SDP line. 52 | if (!std::regex_search(line, ValidLineRegex)) 53 | continue; 54 | 55 | char type = line[0]; 56 | std::string content = line.substr(2); 57 | 58 | if (type == 'm') 59 | { 60 | json m = json::object(); 61 | 62 | m["rtp"] = json::array(); 63 | m["fmtp"] = json::array(); 64 | 65 | media.push_back(m); 66 | 67 | // Point at latest media line. 68 | location = std::addressof(media[media.size() - 1]); 69 | } 70 | 71 | auto it = grammar::rulesMap.find(type); 72 | 73 | if (it == grammar::rulesMap.end()) 74 | continue; 75 | 76 | auto& rules = it->second; 77 | 78 | for (size_t j = 0; j < rules.size(); ++j) 79 | { 80 | auto& rule = rules[j]; 81 | 82 | if (std::regex_search(content, rule.reg)) 83 | { 84 | parseReg(rule, *location, content); 85 | 86 | break; 87 | } 88 | } 89 | } 90 | 91 | // Link it up. 92 | session["media"] = media; 93 | 94 | return session; 95 | } 96 | 97 | json parseParams(const std::string& str) 98 | { 99 | json obj = json::object(); 100 | std::stringstream ss(str); 101 | std::string param; 102 | 103 | while (std::getline(ss, param, ';')) 104 | { 105 | trim(param); 106 | 107 | if (param.length() == 0) 108 | continue; 109 | 110 | insertParam(obj, param); 111 | } 112 | 113 | return obj; 114 | } 115 | 116 | std::vector parsePayloads(const std::string& str) 117 | { 118 | std::vector arr; 119 | 120 | std::stringstream ss(str); 121 | std::string payload; 122 | 123 | while (std::getline(ss, payload, ' ')) 124 | { 125 | arr.push_back(std::stoi(payload)); 126 | } 127 | 128 | return arr; 129 | } 130 | 131 | json parseImageAttributes(const std::string& str) 132 | { 133 | json arr = json::array(); 134 | std::stringstream ss(str); 135 | std::string item; 136 | 137 | while (std::getline(ss, item, ' ')) 138 | { 139 | trim(item); 140 | 141 | // Special case for * value. 142 | if (item == "*") 143 | return item; 144 | 145 | if (item.length() < 5) // [x=0] 146 | continue; 147 | 148 | json obj = json::object(); 149 | std::stringstream ss2(item.substr(1, item.length() - 2)); 150 | std::string param; 151 | 152 | while (std::getline(ss2, param, ',')) 153 | { 154 | trim(param); 155 | 156 | if (param.length() == 0) 157 | continue; 158 | 159 | insertParam(obj, param); 160 | } 161 | 162 | arr.push_back(obj); 163 | } 164 | 165 | return arr; 166 | } 167 | 168 | json parseSimulcastStreamList(const std::string& str) 169 | { 170 | json arr = json::array(); 171 | std::stringstream ss(str); 172 | std::string item; 173 | 174 | while (std::getline(ss, item, ';')) 175 | { 176 | if (item.length() == 0) 177 | continue; 178 | 179 | json arr2 = json::array(); 180 | std::stringstream ss2(item); 181 | std::string format; 182 | 183 | while (std::getline(ss2, format, ',')) 184 | { 185 | if (format.length() == 0) 186 | continue; 187 | 188 | json obj = json::object(); 189 | 190 | if (format[0] != '~') 191 | { 192 | obj["scid"] = format; 193 | obj["paused"] = false; 194 | } 195 | else 196 | { 197 | obj["scid"] = format.substr(1); 198 | obj["paused"] = true; 199 | } 200 | 201 | arr2.push_back(obj); 202 | } 203 | 204 | arr.push_back(arr2); 205 | } 206 | 207 | return arr; 208 | } 209 | 210 | void parseReg(const grammar::Rule& rule, json& location, const std::string& content) 211 | { 212 | bool needsBlank = !rule.name.empty() && !rule.names.empty(); 213 | 214 | if (!rule.push.empty() && location.find(rule.push) == location.end()) 215 | { 216 | location[rule.push] = json::array(); 217 | } 218 | else if (needsBlank && location.find(rule.name) == location.end()) 219 | { 220 | location[rule.name] = json::object(); 221 | } 222 | 223 | std::smatch match; 224 | 225 | std::regex_search(content, match, rule.reg); 226 | 227 | json object = json::object(); 228 | json& keyLocation = !rule.push.empty() 229 | // Blank object that will be pushed. 230 | ? object 231 | // Otherwise named location or root. 232 | : needsBlank 233 | ? location[rule.name] 234 | : location; 235 | 236 | attachProperties(match, keyLocation, rule.names, rule.name, rule.types); 237 | 238 | if (!rule.push.empty()) 239 | location[rule.push].push_back(keyLocation); 240 | } 241 | 242 | void attachProperties( 243 | const std::smatch& match, 244 | json& location, 245 | const std::vector& names, 246 | const std::string& rawName, 247 | const std::vector& types 248 | ) 249 | { 250 | if (!rawName.empty() && names.empty()) 251 | { 252 | location[rawName] = toType(match[1].str(), types[0]); 253 | } 254 | else 255 | { 256 | for (size_t i = 0; i < names.size(); ++i) 257 | { 258 | if (i + 1 < match.size() && !match[i + 1].str().empty()) 259 | { 260 | location[names[i]] = toType(match[i + 1].str(), types[i]); 261 | } 262 | } 263 | } 264 | } 265 | 266 | bool isInt(const std::string& str) 267 | { 268 | std::istringstream iss(str); 269 | long l; 270 | 271 | iss >> std::noskipws >> l; 272 | 273 | return iss.eof() && !iss.fail(); 274 | } 275 | 276 | bool isFloat(const std::string& str) 277 | { 278 | std::istringstream iss(str); 279 | float f; 280 | 281 | iss >> std::noskipws >> f; 282 | 283 | return iss.eof() && !iss.fail(); 284 | } 285 | 286 | json toType(const std::string& str, char type) 287 | { 288 | // https://stackoverflow.com/a/447307/4827838. 289 | 290 | switch (type) 291 | { 292 | case 's': 293 | { 294 | return str; 295 | } 296 | 297 | case 'u': 298 | { 299 | std::istringstream iss(str); 300 | std::uint64_t ll; 301 | 302 | iss >> std::noskipws >> ll; 303 | 304 | if (iss.eof() && !iss.fail()) 305 | return ll; 306 | else 307 | return 0u; 308 | } 309 | 310 | case 'd': 311 | { 312 | std::istringstream iss(str); 313 | std::int64_t ll; 314 | 315 | iss >> std::noskipws >> ll; 316 | 317 | if (iss.eof() && !iss.fail()) 318 | return ll; 319 | else 320 | return 0; 321 | } 322 | 323 | case 'f': 324 | { 325 | std::istringstream iss(str); 326 | double d; 327 | 328 | iss >> std::noskipws >> d; 329 | 330 | if (iss.eof() && !iss.fail()) 331 | return std::stod(str); 332 | else 333 | return 0.0f; 334 | } 335 | } 336 | 337 | return nullptr; 338 | } 339 | 340 | void trim(std::string& str) 341 | { 342 | str.erase( 343 | str.begin(), 344 | std::find_if( 345 | str.begin(), str.end(), [](unsigned char ch) { return !std::isspace(ch); } 346 | ) 347 | ); 348 | 349 | str.erase( 350 | std::find_if( 351 | str.rbegin(), str.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), 352 | str.end() 353 | ); 354 | } 355 | 356 | // @str parameters is a string like "profile-level-id=42e034". 357 | void insertParam(json& o, const std::string& str) 358 | { 359 | static const std::regex KeyValueRegex("^\\s*([^= ]+)(?:\\s*=\\s*([^ ]+))?$"); 360 | static const std::unordered_map WellKnownParameters = 361 | { 362 | // H264 codec parameters. 363 | { "profile-level-id", 's' }, 364 | { "packetization-mode", 'd' }, 365 | // VP9 codec parameters. 366 | { "profile-id", 's' } 367 | }; 368 | 369 | std::smatch match; 370 | 371 | std::regex_match(str, match, KeyValueRegex); 372 | 373 | if (match.size() == 0) 374 | return; 375 | 376 | std::string param = match[1].str(); 377 | std::string value = match[2].str(); 378 | 379 | auto it = WellKnownParameters.find(param); 380 | char type; 381 | 382 | if (it != WellKnownParameters.end()) 383 | type = it->second; 384 | else if (isInt(match[2].str())) 385 | type = 'd'; 386 | else if (isFloat(match[2].str())) 387 | type = 'f'; 388 | else 389 | type = 's'; 390 | 391 | // Insert into the given JSON object. 392 | o[match[1].str()] = toType(match[2].str(), type); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /doc/Grammar.md: -------------------------------------------------------------------------------- 1 | # Grammar 2 | 3 | Here the syntax of the JSON object generated by `sdptransform::parse(sdp)`. Each section below defines how the corresponding SDP line is parsed. 4 | 5 | Some lines are parsed as a JSON object with different keys, while others have a JSON string, JSON integer or JSON float as value. 6 | 7 | * The name of each subsection below corresponds to the `key` string in the SDP JSON object (or the corresponding `media` section). 8 | * The `multiple` column means that, if at least one of those lines exists in the SDP, its value is a JSON array with multiple values (for eample, the `a=rtpmap` line). 9 | * `type` indicates the C++ valid conversion for the value. 10 | 11 | **IMPORTANT:** 12 | 13 | Some fields in a SDP line may be optional, and others may be mandatory but could have an empty string as valid value. In both cases (not present fields and fields with an empty string as value) the corresponding `key` is not inserted in the parsed JSON. 14 | 15 | This means that, before assuming that a JSON key exists, the presence of the key must be verified: 16 | 17 | ```c++ 18 | int sessionId; 19 | 20 | if ( 21 | session.find("origin") != session.end() && 22 | session.at("origin").find("sessionId") != session.at("origin").end() 23 | ) 24 | { 25 | sessionId = session.at("origin").at("sessionId"); 26 | } 27 | ``` 28 | 29 | 30 | ### version 31 | 32 | `v=0` 33 | 34 | * type: string 35 | * example: "0" 36 | 37 | 38 | ### origin 39 | 40 | `o=- 20518 0 IN IP4 203.0.113.1` 41 | 42 | * type: object 43 | 44 | | field | type | example 45 | | --------------- | ------- | ------------------------- 46 | | username | string | "-" 47 | | sessionId | integer | 20518 48 | | sessionVersion | integer | 0 49 | | netType | string | "IN" 50 | | ipVer | integer | 4 51 | | adddress | string | "203.0.113.1" 52 | `s=-` 53 | 54 | 55 | ### description 56 | 57 | `i=foo` 58 | 59 | * type: string 60 | * example: "foo" 61 | 62 | 63 | ### uri 64 | 65 | `u=https://foo.com` 66 | 67 | * type: string 68 | * example: "https://foo.com" 69 | 70 | 71 | ### email 72 | 73 | `e=alice@foo.com` 74 | 75 | * type: string 76 | * example: "alice@foo.com" 77 | 78 | 79 | ### phone 80 | 81 | `p=+12345678` 82 | 83 | * type: string 84 | * example: "+12345678" 85 | 86 | 87 | ### timing 88 | 89 | `t=0 0` 90 | 91 | * type: object 92 | 93 | | field | type | example 94 | | --------------- | ------- | ------------------------- 95 | | start | integer | 0 96 | | stop | integer | 0 97 | 98 | 99 | ### connection 100 | 101 | `c=IN IP4 10.47.197.26` 102 | 103 | `c=IN IP4 224.2.36.42/15` 104 | 105 | * type: object 106 | 107 | | field | type | example 108 | | --------------- | ------- | ------------------------- 109 | | version | integer | 4 110 | | ip | string | "10.47.197.26" 111 | | ttl | integer | 15 112 | 113 | *NOTE:* `ttl` is just present in the object if `ip` is followed by `/` plus a number. More info in the [RFC 4566 section 5.7](https://tools.ietf.org/html/rfc4566#section-5.7). 114 | 115 | ### bandwidth 116 | 117 | `b=AS:4000` 118 | 119 | * multiple 120 | * type: object 121 | 122 | | field | type | example 123 | | --------------- | ------- | ------------------------- 124 | | type | string | "AS" 125 | | limit | integer | 4000 126 | 127 | 128 | ### media 129 | 130 | `m=video 51744 RTP/AVP 126 97 98 34 31` 131 | 132 | `m=audio 5004/2 RTP/AVP 96` 133 | 134 | * multiple 135 | * type: object 136 | 137 | | field | type | example 138 | | --------------- | ------- | ------------------------- 139 | | type | string | "video" 140 | | port | integer | 51744 141 | | numPorts | integer | 2 (optional) 142 | | protocol | string | "RTP/AVP" 143 | | payloads | string | "126 97 98 34 31" 144 | 145 | *NOTE:* `numPorts` is just present in the object if `port` is followed by `/` plus a number. More info in the [RFC 4566 section 5.14](https://tools.ietf.org/html/rfc4566#section-5.14). 146 | 147 | 148 | ### rtp 149 | 150 | `a=rtpmap:110 opus/48000/2` 151 | 152 | * multiple 153 | * type: object 154 | 155 | | field | type | example 156 | | --------------- | ------- | ------------------------- 157 | | payload | integer | 110 158 | | codec | string | "opus" 159 | | rate | integer | 48000 160 | | encoding | string "2" 161 | 162 | 163 | ### fmtp 164 | 165 | `a=fmtp:108 profile-level-id=24;bitrate=64000` 166 | 167 | `a=fmtp:111 minptime=10; useinbandfec=1` 168 | 169 | * multiple 170 | * type: object 171 | 172 | | field | type | example 173 | | --------------- | ------- | ------------------------- 174 | | payload | integer | 108 175 | | config | string | "profile-level-id=24;bitrate=64000" 176 | 177 | 178 | ### control 179 | 180 | `a=control:streamid=0` 181 | 182 | * type: string 183 | * example: "streamid=0" 184 | 185 | 186 | ### rtcp 187 | 188 | `a=rtcp:65179 IN IP4 193.84.77.194` 189 | 190 | * type: object 191 | 192 | | field | type | example 193 | | --------------- | ------- | ------------------------- 194 | | port | integer | 65179 195 | | netType | string | "IN" 196 | | ipVer | integer | 4 197 | | adddress | string | "193.84.77.194" 198 | 199 | 200 | ### rtcpFbTrrInt 201 | 202 | `a=rtcp-fb:98 trr-int 100` 203 | 204 | * multiple 205 | * type: object 206 | 207 | | field | type | example 208 | | --------------- | ------- | ------------------------- 209 | | payload | string | "98" (could be "*") 210 | | value | integer | 100 211 | 212 | 213 | ### rtcpFb 214 | 215 | `a=rtcp-fb:98 nack rpsi` 216 | 217 | * multiple 218 | * type: object 219 | 220 | | field | type | example 221 | | --------------- | ------- | ------------------------- 222 | | payload | string | "98" (could be "*") 223 | | type | string | "nack" 224 | | subtype | string | "rpsi" 225 | 226 | 227 | ### ext 228 | 229 | `a=extmap:1/recvonly URI-gps-string` 230 | 231 | `a=extmap:2 urn:ietf:params:rtp-hdrext:toffset` 232 | 233 | `a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24` 234 | 235 | * multiple 236 | * type: object 237 | 238 | | field | type | example 239 | | --------------- | ------- | ------------------------- 240 | | value | integer | 1 241 | | direction | string | "recvonly" 242 | | uri | string | "URI-gps-string" 243 | | encrypt-uri | string | "urn:ietf:params:rtp-hdrext:encrypt" 244 | | config | string | 245 | 246 | 247 | ### crypto 248 | 249 | `a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32` 250 | 251 | * multiple 252 | * type: object 253 | 254 | | field | type | example 255 | | --------------- | ------- | ------------------------- 256 | | id | integer | 1 257 | | suite | string | "AES_CM_128_HMAC_SHA1_80" 258 | | config | string | "inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR\|2^20\|1:32" 259 | | sessionConfig | string | 260 | 261 | 262 | ### setup 263 | 264 | `a=setup:actpass` 265 | 266 | * type: string 267 | * example: "actpass" 268 | 269 | 270 | ### mid 271 | 272 | `a=mid:audio` 273 | 274 | * type: string 275 | * example: "audio" 276 | 277 | 278 | ### msid 279 | 280 | `a=msid:0c8b064d-d807-43b4 46e0-8e16-7ef0db0db64a` 281 | 282 | * type: string 283 | * example: "0c8b064d-d807-43b4 46e0-8e16-7ef0db0db64a" 284 | 285 | 286 | ### ptime 287 | 288 | `a=ptime:20` 289 | 290 | * type: integer 291 | * example: 20 292 | 293 | 294 | ### maxptime 295 | 296 | `a=maxptime:60` 297 | 298 | * type: integer 299 | * example: 60 300 | 301 | 302 | ### direction 303 | 304 | `a=sendrecv` 305 | 306 | * type: string 307 | * example: "sendrecv" 308 | 309 | 310 | ### icelite 311 | 312 | `a=ice-lite` 313 | 314 | * type: string 315 | * example: "ice-lite" 316 | 317 | 318 | ### iceUfrag 319 | 320 | `a=ice-ufrag:F7gI` 321 | 322 | * type: string 323 | * example: "F7gI" 324 | 325 | 326 | ### icePwd 327 | 328 | `a=ice-pwd:x9cml/YzichV2+XlhiMu8g` 329 | 330 | * type: string 331 | * example: "x9cml/YzichV2+XlhiMu8g" 332 | 333 | 334 | ### fingerprint 335 | 336 | `a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33` 337 | 338 | * type: object 339 | 340 | | field | type | example 341 | | --------------- | ------- | ------------------------- 342 | | type | string | "SHA-1" 343 | | hash | string | "00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33" 344 | 345 | 346 | ### candidates 347 | 348 | `a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host` 349 | 350 | `a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 network-id 3 network-cost 10` 351 | 352 | `a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10` 353 | 354 | `a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10` 355 | 356 | `a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10` 357 | 358 | * multiple 359 | * type: object 360 | 361 | | field | type | example 362 | | --------------- | ------- | ------------------------- 363 | | foundation | string | "3289912957" 364 | | component | integer | 2 365 | | transport | string | "tcp" 366 | | priority | integer | 1845501695 367 | | ip | string | "193.84.77.194" 368 | | port | integer | 60017 369 | | type | string | "srflx" 370 | | raddr | string | "192.168.34.75" 371 | | rport | integer | 60017 372 | | generation | integer | 0 373 | | network-id | integer | 3 374 | | network-cost | integer | 10 375 | 376 | 377 | ### endOfCandidates 378 | 379 | `a=end-of-candidates` 380 | 381 | * type: string 382 | * example: "end-of-candidates" 383 | 384 | 385 | ### remoteCandidates 386 | 387 | `a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401` 388 | 389 | * type: string 390 | * example: "1 203.0.113.1 54400 2 203.0.113.1 54401" 391 | 392 | 393 | ### iceOptions 394 | 395 | `a=ice-options:google-ice` 396 | 397 | * type: string 398 | * example: "google-ice" 399 | 400 | 401 | ### ssrcs 402 | 403 | `a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1` 404 | 405 | * multiple 406 | * type: object 407 | 408 | | field | type | example 409 | | --------------- | ------- | ------------------------- 410 | | id | integer | 2566107569 411 | | attribute | string | "cname" 412 | | value | string | "t9YU8M1UxTF8Y1A1" 413 | 414 | 415 | ### ssrcGroups 416 | 417 | `a=ssrc-group:FEC-FR 3004364195 1080772241` 418 | 419 | * multiple 420 | * type: object 421 | 422 | | field | type | example 423 | | --------------- | ------- | ------------------------- 424 | | semantics | string | "FEC-FR" 425 | | ssrcs | string | "3004364195 1080772241" 426 | 427 | 428 | ### msidSemantic 429 | 430 | `a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV` 431 | 432 | * type: object 433 | 434 | | field | type | example 435 | | --------------- | ------- | ------------------------- 436 | | semantic | string | "WMS" 437 | | token | string | "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV" 438 | 439 | 440 | ### groups 441 | 442 | `a=group:BUNDLE audio video` 443 | 444 | * multiple 445 | * type: object 446 | 447 | | field | type | example 448 | | --------------- | ------- | ------------------------- 449 | | type | string | "BUNDLE" 450 | | mids | string | "audio video" 451 | 452 | 453 | ### rtcpMux 454 | 455 | `a=rtcp-mux` 456 | 457 | * type: string 458 | * example: "rtcp-mux" 459 | 460 | 461 | ### rtcpRsize 462 | 463 | `a=rtcp-rsize` 464 | 465 | * type: string 466 | * example: "rtcp-rsize" 467 | 468 | 469 | ### sctpmap 470 | 471 | `a=sctpmap:5000 webrtc-datachannel 1024` 472 | 473 | * type: object 474 | 475 | | field | type | example 476 | | --------------- | ------- | ------------------------- 477 | | sctpmapNumber | integer | 5000 478 | | app | string | "webrtc-datachannel" 479 | | maxMessageSize | integer | 1024 480 | 481 | 482 | ### xGoogleFlag 483 | 484 | `a=x-google-flag:conference` 485 | 486 | * type: string 487 | * example: "conference" 488 | 489 | 490 | ### rids 491 | 492 | `a=rid:1 send max-width=1280;max-height=720` 493 | 494 | * multiple 495 | * type: object 496 | 497 | | field | type | example 498 | | --------------- | ------- | ------------------------- 499 | | id | string | "1" 500 | | direction | string | "send" 501 | | params | string | "max-width=1280;max-height=720" 502 | 503 | 504 | ### imageattrs 505 | 506 | `a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250]` 507 | 508 | `a=imageattr:* send [x=800,y=640] recv *` 509 | 510 | `a=imageattr:100 recv [x=320,y=240]` 511 | 512 | * multiple 513 | * type: object 514 | 515 | | field | type | example 516 | | --------------- | ------- | ------------------------- 517 | | pt | string | "97" (could be "*") 518 | | dir1 | string | "send" 519 | | attrs1 | string | "[x=800,y=640,sar=1.1,q=0.6] [x=480,y=320]" 520 | | dir2 | string | "recv" 521 | | attrs2 | string | "[x=330,y=250]" 522 | 523 | 524 | ### simulcast 525 | 526 | `a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8` 527 | 528 | `a=simulcast:recv 1;4,5 send 6;7` 529 | 530 | * type: object 531 | 532 | | field | type | example 533 | | --------------- | ------- | ------------------------- 534 | | dir1 | string | "send" 535 | | list1 | string | "1,2,3;~4,~5" 536 | | dir2 | string | "recv" 537 | | list2 | string | "6;~7,~8" 538 | 539 | 540 | ### simulcast_03 541 | 542 | Old simulcast draft [revision 03](https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03) (implemented by some browsers). 543 | 544 | `a=simulcast: recv pt=97;98 send pt=97` 545 | 546 | `a=simulcast: send rid=5;6;7 paused=6,7` 547 | 548 | * type: string 549 | * example: "recv pt=97;98 send pt=97" 550 | 551 | 552 | ### framerate 553 | 554 | `a=framerate:25` 555 | 556 | `a=framerate:29.97` 557 | 558 | * type: float 559 | * example: 25.0 560 | 561 | 562 | ### tsRefclk 563 | 564 | `a=ts-refclk:ptp=IEEE1588-2008:00-50-C2-FF-FE-90-04-37:0` 565 | 566 | * type: string 567 | * example: "ptp=IEEE1588-2008:00-1D-C1-FF-FE-12-00-A4:0" 568 | 569 | 570 | ### mediaclk 571 | 572 | `a=mediaclk:direct=0` 573 | 574 | `a=mediaclk:sender` 575 | 576 | * type: string 577 | * example: "direct=0" 578 | 579 | 580 | ### invalid 581 | 582 | Unknown SDP lines are stored within the `invalid` key. 583 | 584 | * multiple 585 | * type: object 586 | 587 | | field | type | example 588 | | --------------- | ------- | ------------------------- 589 | | value | string | 590 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libsdptransform 2 | 3 | C++ version of the [sdp-transform](https://github.com/clux/sdp-transform/) JavaScript library exposing the same API. 4 | 5 | **libsdptransform** is a simple parser and writer of SDP. Defines internal grammar based on [RFC4566 - SDP](http://tools.ietf.org/html/rfc4566), [RFC5245 - ICE](http://tools.ietf.org/html/rfc5245), and many more. 6 | 7 | 8 | ## Usage 9 | 10 | Once installed (see *Installation* below): 11 | 12 | ```c++ 13 | #include "sdptransform/sdptransform.hpp" 14 | ``` 15 | 16 | The **libsdptransform** API is exposed in the `sdptransform` C++ namespace. 17 | 18 | **libsdptransform** integrates the [JSON for Modern C++](https://github.com/nlohmann/json/) library and exposes it under the `json` C++ namespace. 19 | 20 | 21 | ### This is not JavaScript! 22 | 23 | It's important to recall that this is not JavaScript but C++. Operations that are safe on a JavaScript `Object` may not be safe in a C++ JSON object. 24 | 25 | So, before reading a JSON value, make sure that its corresponding `key` **does exist** and also check its type (`int`, `std::string`, `nullptr`, etc.) before assigning it to a C++ variable. 26 | 27 | * For example, assuming that the parsed SDP `session` does NOT have a `s=` line (name), the following code would crash: 28 | 29 | ```c++ 30 | std::string sdpName = session.at("name"); 31 | // => 32 | // terminating with uncaught exception of type nlohmann::detail::out_of_range: 33 | // [json.exception.out_of_range.403] key 'name' not found 34 | ``` 35 | 36 | * The safe way is: 37 | 38 | ```c++ 39 | std::string sdpName; 40 | 41 | if (session.find("name") != session.end()) 42 | { 43 | // NOTE: The API guarantees that the SDP name is a string (otherwise this 44 | // would crash). 45 | sdpName = session.at("name"); 46 | } 47 | ``` 48 | 49 | * And a more efficient way is: 50 | 51 | ```c++ 52 | std::string sdpName; 53 | auto it = session.find("name"); 54 | 55 | if (it != session.end()) 56 | { 57 | // NOTE: The API guarantees that the SDP name is a string (otherwise this 58 | // would crash). 59 | sdpName = it->get(); 60 | // or just: 61 | sdpName = *it; 62 | } 63 | ``` 64 | 65 | * Also, as in C++ maps, using the `[]` operator on a JSON object for reading the value of a given `key` will insert such a `key` in the `json` object with value `nullptr` if it did not exist before. 66 | 67 | * So, when using `parseParams()` or `parseImageAttributes()` exposed API, the application should do some checks before reading a value of a supposed type. So, for instance, let's assume that the first `a=fmtp` line in a `video` media section is `a=fmtp:97 profile-level-id=4d0028;packetization-mode=1`. The safe way to read its values is: 68 | 69 | ```c++ 70 | auto h264Fmtp = sdptransform::parseParams(video.at("fmtp")[0].at("config")); 71 | std::string profileLevelId; 72 | int packetizationMode; 73 | 74 | if ( 75 | h264Fmtp.find("profile-level-id") != h264Fmtp.end() && 76 | h264Fmtp["profile-level-id"].is_string() 77 | ) 78 | { 79 | profileLevelId = h264Fmtp.at("profile-level-id"); 80 | } 81 | 82 | if ( 83 | h264Fmtp.find("packetization-mode") != h264Fmtp.end() && 84 | h264Fmtp["packetization-mode"].is_number_unsigned() 85 | ) 86 | { 87 | packetizationMode = h264Fmtp.at("packetization-mode"); 88 | } 89 | ``` 90 | 91 | * And much more efficient: 92 | 93 | ```c++ 94 | auto h264Fmtp = sdptransform::parseParams(video.at("fmtp")[0].at("config")); 95 | std::string profileLevelId; 96 | int packetizationMode; 97 | 98 | auto profileLevelIdIterator = h264Fmtp.find("profile-level-id"); 99 | 100 | if ( 101 | profileLevelIdIterator != h264Fmtp.end() && 102 | profileLevelIdIterator->is_string() 103 | ) 104 | { 105 | profileLevelId = *profileLevelIdIterator; 106 | } 107 | 108 | auto packetizationModeIterator = h264Fmtp.find("packetization-mode"); 109 | 110 | if ( 111 | packetizationModeIterator != h264Fmtp.end() && 112 | packetizationModeIterator->is_number_unsigned() 113 | ) 114 | { 115 | packetizationMode = *packetizationModeIterator; 116 | } 117 | ``` 118 | 119 | It's **strongly** recommended to read the [JSON documentation](https://github.com/nlohmann/json/) and, before reading a parsed SDP, check whether the desired field exists and it has the desired type (string, integer, float, etc). 120 | 121 | 122 | ## Usage - Parser 123 | 124 | 125 | ### parse() 126 | 127 | ```c++ 128 | json parse(const std::string& sdp) 129 | ``` 130 | 131 | Parses an unprocessed SDP string and returns a JSON object. SDP lines can be terminated on `\r\n` (as per specification) or just `\n`. 132 | 133 | The syntax of the generated SDP object and each SDP line is documented [here](doc/Grammar.md). 134 | 135 | ```c++ 136 | std::string sdpStr = R"(v=0 137 | o=- 20518 0 IN IP4 203.0.113.1 138 | s= 139 | t=0 0 140 | c=IN IP4 203.0.113.1 141 | a=ice-ufrag:F7gI 142 | a=ice-pwd:x9cml/YzichV2+XlhiMu8g 143 | a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7 144 | m=audio 54400 RTP/SAVPF 0 96 145 | a=rtpmap:0 PCMU/8000 146 | a=rtpmap:96 opus/48000 147 | a=ptime:20 148 | a=sendrecv 149 | a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host 150 | a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host 151 | m=video 55400 RTP/SAVPF 97 98 152 | a=rtcp-fb:* nack 153 | a=rtpmap:97 H264/90000 154 | a=fmtp:97 profile-level-id=4d0028;packetization-mode=1 155 | a=rtcp-fb:97 trr-int 100 156 | a=rtcp-fb:97 nack rpsi 157 | a=rtpmap:98 VP8/90000 158 | a=rtcp-fb:98 trr-int 100 159 | a=rtcp-fb:98 nack rpsi 160 | a=sendrecv 161 | a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host 162 | a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host 163 | a=ssrc:1399694169 foo:bar 164 | a=ssrc:1399694169 baz 165 | )"; 166 | 167 | json session = sdptransform::parse(sdpStr); 168 | ``` 169 | 170 | Resulting `session` is a JSON object as follows: 171 | 172 | ```json 173 | { 174 | "connection": { 175 | "ip": "203.0.113.1", 176 | "version": 4 177 | }, 178 | "fingerprint": { 179 | "hash": "42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7", 180 | "type": "sha-1" 181 | }, 182 | "icePwd": "x9cml/YzichV2+XlhiMu8g", 183 | "iceUfrag": "F7gI", 184 | "media": [ 185 | { 186 | "candidates": [ 187 | { 188 | "component": 1, 189 | "foundation": "0", 190 | "ip": "203.0.113.1", 191 | "port": 54400, 192 | "priority": 2113667327, 193 | "transport": "UDP", 194 | "type": "host" 195 | }, 196 | { 197 | "component": 2, 198 | "foundation": "1", 199 | "ip": "203.0.113.1", 200 | "port": 54401, 201 | "priority": 2113667326, 202 | "transport": "UDP", 203 | "type": "host" 204 | } 205 | ], 206 | "direction": "sendrecv", 207 | "fmtp": [], 208 | "payloads": "0 96", 209 | "port": 54400, 210 | "protocol": "RTP/SAVPF", 211 | "ptime": 20, 212 | "rtp": [ 213 | { 214 | "codec": "PCMU", 215 | "payload": 0, 216 | "rate": 8000 217 | }, 218 | { 219 | "codec": "opus", 220 | "payload": 96, 221 | "rate": 48000 222 | } 223 | ], 224 | "type": "audio" 225 | }, 226 | { 227 | "candidates": [ 228 | { 229 | "component": 1, 230 | "foundation": "0", 231 | "ip": "203.0.113.1", 232 | "port": 55400, 233 | "priority": 2113667327, 234 | "transport": "UDP", 235 | "type": "host" 236 | }, 237 | { 238 | "component": 2, 239 | "foundation": "1", 240 | "ip": "203.0.113.1", 241 | "port": 55401, 242 | "priority": 2113667326, 243 | "transport": "UDP", 244 | "type": "host" 245 | } 246 | ], 247 | "direction": "sendrecv", 248 | "fmtp": [ 249 | { 250 | "config": "profile-level-id=4d0028;packetization-mode=1", 251 | "payload": 97 252 | } 253 | ], 254 | "payloads": "97 98", 255 | "port": 55400, 256 | "protocol": "RTP/SAVPF", 257 | "rtcpFb": [ 258 | { 259 | "payload": "*", 260 | "type": "nack" 261 | }, 262 | { 263 | "payload": "97", 264 | "subtype": "rpsi", 265 | "type": "nack" 266 | }, 267 | { 268 | "payload": "98", 269 | "subtype": "rpsi", 270 | "type": "nack" 271 | } 272 | ], 273 | "rtcpFbTrrInt": [ 274 | { 275 | "payload": "97", 276 | "value": 100 277 | }, 278 | { 279 | "payload": "98", 280 | "value": 100 281 | } 282 | ], 283 | "rtp": [ 284 | { 285 | "codec": "H264", 286 | "payload": 97, 287 | "rate": 90000 288 | }, 289 | { 290 | "codec": "VP8", 291 | "payload": 98, 292 | "rate": 90000 293 | } 294 | ], 295 | "ssrcs": [ 296 | { 297 | "attribute": "foo", 298 | "id": 1399694169, 299 | "value": "bar" 300 | }, 301 | { 302 | "attribute": "baz", 303 | "id": 1399694169 304 | } 305 | ], 306 | "type": "video" 307 | } 308 | ], 309 | "name": "", 310 | "origin": { 311 | "address": "203.0.113.1", 312 | "ipVer": 4, 313 | "netType": "IN", 314 | "sessionId": 20518, 315 | "sessionVersion": 0, 316 | "username": "-" 317 | }, 318 | "timing": { 319 | "start": 0, 320 | "stop": 0 321 | }, 322 | "version": 0 323 | } 324 | ``` 325 | 326 | 327 | ### Parser postprocessing 328 | 329 | No excess parsing is done to the raw strings because the writer is built to be the inverse of the parser. That said, a few helpers have been built in: 330 | 331 | 332 | #### parseParams() 333 | 334 | ```c++ 335 | json parseParams(const std::string& str) 336 | ``` 337 | 338 | Parses `fmtp.at("config")` and others such as `rid.at("params")` and returns an object with all the params in a key/value fashion. 339 | 340 | NOTE: The type of each value is auto-detected, so it can be a string, integer or float. Do **NOT** assume the type of a value! (read the **This is not JavaScript!** section above). 341 | 342 | ```c++ 343 | // a=fmtp:97 profile-level-id=4d0028;packetization-mode=1 344 | 345 | json params = 346 | sdptransform::parseParams(session.at("media")[1].at("fmtp")[0].at("config")); 347 | ``` 348 | 349 | Resulting `params` is a JSON object as follows: 350 | 351 | ```json 352 | { 353 | "packetization-mode": 1, 354 | "profile-level-id": "4d0028" 355 | } 356 | ``` 357 | 358 | 359 | #### parsePayloads() 360 | 361 | ```c++ 362 | std::vector parsePayloads(const std::string& str) 363 | ``` 364 | 365 | Returns a vector with all the payload advertised in the corresponding m-line. 366 | 367 | ```c++ 368 | // m=video 55400 RTP/SAVPF 97 98 369 | 370 | json payloads = 371 | sdptransform::parsePayloads(session.at("media")[1].at("payloads")); 372 | ``` 373 | 374 | Resulting `payloads` is a C++ vector of `int` elements as follows: 375 | 376 | ```json 377 | [ 97, 98 ] 378 | ``` 379 | 380 | 381 | #### parseImageAttributes() 382 | 383 | ```c++ 384 | json parseImageAttributes(const std::string& str) 385 | ``` 386 | 387 | Parses [Generic Image Attributes](https://tools.ietf.org/html/rfc6236). Must be provided with the `attrs1` or `attrs2` string of a `a=imageattr` line. Returns an array of key/value objects. 388 | 389 | NOTE: The type of each value is auto-detected, so it can be a string, integer or float. Do **NOT** assume the type of a value! (read the **This is not JavaScript!** section above). 390 | 391 | ```c++ 392 | // a=imageattr:97 send [x=1280,y=720] recv [x=1280,y=720] [x=320,y=180] 393 | 394 | std::string imageAttributesStr = "[x=1280,y=720] [x=320,y=180]"; 395 | 396 | json imageAttributes = sdptransform::parseImageAttributes(imageAttributesStr); 397 | ``` 398 | 399 | Resulting `imageAttributes` is a JSON array as follows: 400 | 401 | ```json 402 | [ 403 | { "x": 1280, "y": 720 }, 404 | { "x": 320, "y": 180 } 405 | ] 406 | ``` 407 | 408 | 409 | #### parseSimulcastStreamList() 410 | 411 | ```c++ 412 | json parseSimulcastStreamList(const std::string& str) 413 | ``` 414 | 415 | Parses [simulcast](https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast) streams/formats. Must be provided with the `attrs1` or `attrs2` string of the `a=simulcast` line. 416 | 417 | Returns an array of simulcast streams. Each entry is an array of alternative simulcast formats, which are objects with two keys: 418 | 419 | * `scid`: Simulcast identifier (string) 420 | * `paused`: Whether the simulcast format is paused (boolean) 421 | 422 | ```c++ 423 | // // a=simulcast:send 1,~4;2;3 recv c 424 | std::string simulcastAttributesStr = "1,~4;2;3"; 425 | 426 | json simulcastAttributes = 427 | sdptransform::parseSimulcastStreamList(simulcastAttributesStr); 428 | ``` 429 | 430 | Resulting `simulcastAttributes` is a JSON array as follows: 431 | 432 | ```json 433 | [ 434 | [ { "scid": "1", "paused": false }, { "scid": "4", "paused": true } ], 435 | [ { "scid": "2", "paused": false } ], 436 | [ { "scid": "3", "paused": false } ] 437 | ] 438 | ``` 439 | 440 | 441 | ## Usage - Writer 442 | 443 | 444 | ### write() 445 | 446 | ```c++ 447 | std::string write(json& session) 448 | ``` 449 | 450 | The writer is the inverse of the parser, and will need a struct equivalent to the one returned by it. 451 | 452 | ```c++ 453 | std::string newSdpStr = sdptransform::write(session); // session parsed above 454 | ``` 455 | 456 | Resulting `newSdpStr` is a string as follows: 457 | 458 | ``` 459 | v=0 460 | o=- 20518 0 IN IP4 203.0.113.1 461 | s= 462 | c=IN IP4 203.0.113.1 463 | t=0 0 464 | a=ice-ufrag:F7gI 465 | a=ice-pwd:x9cml/YzichV2+XlhiMu8g 466 | a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7 467 | m=audio 54400 RTP/SAVPF 0 96 468 | a=rtpmap:0 PCMU/8000 469 | a=rtpmap:96 opus/48000 470 | a=ptime:20 471 | a=sendrecv 472 | a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host 473 | a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host 474 | m=video 55400 RTP/SAVPF 97 98 475 | a=rtpmap:97 H264/90000 476 | a=rtpmap:98 VP8/90000 477 | a=fmtp:97 profile-level-id=4d0028;packetization-mode=1 478 | a=rtcp-fb:97 trr-int 100 479 | a=rtcp-fb:98 trr-int 100 480 | a=rtcp-fb:* nack 481 | a=rtcp-fb:97 nack rpsi 482 | a=rtcp-fb:98 nack rpsi 483 | a=sendrecv 484 | a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host 485 | a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host 486 | a=ssrc:1399694169 foo:bar 487 | a=ssrc:1399694169 baz 488 | ``` 489 | 490 | The only thing different from the original input is we follow the order specified by the SDP RFC, and we will always do so. 491 | 492 | 493 | ## Installation 494 | 495 | ```bash 496 | git clone https://github.com/ibc/libsdptransform.git 497 | cd libsdptransform/ 498 | cmake . -Bbuild 499 | make install -C build/ # or: cd build/ && make install 500 | ``` 501 | 502 | Depending on the host, it will generate the following static lib and header files: 503 | 504 | ``` 505 | -- Installing: /usr/local/lib/libsdptransform.a 506 | -- Up-to-date: /usr/local/include/sdptransform/sdptransform.hpp 507 | -- Up-to-date: /usr/local/include/sdptransform/json.hpp 508 | ``` 509 | 510 | 511 | ## Development 512 | 513 | * Build the lib: 514 | 515 | ```bash 516 | $ cmake . -Bbuild 517 | ``` 518 | 519 | * Run test units: 520 | 521 | ```bash 522 | $ ./scripts/test.sh 523 | ``` 524 | 525 | 526 | ## Author 527 | 528 | Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)] 529 | 530 | Special thanks to [Eirik Albrigtsen](https://github.com/clux), the author of the [sdp-transform](https://github.com/clux/sdp-transform/) JavaScript library. 531 | 532 | 533 | ## License 534 | 535 | [MIT](LICENSE) 536 | -------------------------------------------------------------------------------- /src/grammar.cpp: -------------------------------------------------------------------------------- 1 | #include "sdptransform.hpp" 2 | 3 | namespace sdptransform 4 | { 5 | namespace grammar 6 | { 7 | bool hasValue(const json& o, const std::string& key); 8 | 9 | const std::map> rulesMap = 10 | { 11 | { 12 | 'v', 13 | { 14 | // v=0 15 | { 16 | // name: 17 | "version", 18 | // push: 19 | "", 20 | // reg: 21 | std::regex("^(\\d*)$"), 22 | // names: 23 | { }, 24 | // types: 25 | { 'd' }, 26 | // format: 27 | "%d" 28 | } 29 | } 30 | }, 31 | 32 | { 33 | 'o', 34 | { 35 | // o=- 20518 0 IN IP4 203.0.113.1 36 | { 37 | // name: 38 | "origin", 39 | // push: 40 | "", 41 | // reg: 42 | std::regex("^(\\S*) (\\d*) (\\d*) (\\S*) IP(\\d) (\\S*)"), 43 | // names: 44 | { "username", "sessionId", "sessionVersion", "netType", "ipVer", "address" }, 45 | // types: 46 | { 's', 'u', 'u', 's', 'd', 's' }, 47 | // format: 48 | "%s %d %d %s IP%d %s" 49 | } 50 | } 51 | }, 52 | 53 | { 54 | 's', 55 | { 56 | // s=- 57 | { 58 | // name: 59 | "name", 60 | // push: 61 | "", 62 | // reg: 63 | std::regex("(.*)"), 64 | // names: 65 | { }, 66 | // types: 67 | { 's' }, 68 | // format: 69 | "%s" 70 | } 71 | } 72 | }, 73 | 74 | { 75 | 'i', 76 | { 77 | // i=foo 78 | { 79 | // name: 80 | "description", 81 | // push: 82 | "", 83 | // reg: 84 | std::regex("(.*)"), 85 | // names: 86 | { }, 87 | // types: 88 | { 's' }, 89 | // format: 90 | "%s" 91 | } 92 | } 93 | }, 94 | 95 | { 96 | 'u', 97 | { 98 | // u=https://foo.com 99 | { 100 | // name: 101 | "uri", 102 | // push: 103 | "", 104 | // reg: 105 | std::regex("(.*)"), 106 | // names: 107 | { }, 108 | // types: 109 | { 's' }, 110 | // format: 111 | "%s" 112 | } 113 | } 114 | }, 115 | 116 | { 117 | 'e', 118 | { 119 | // e=alice@foo.com 120 | { 121 | // name: 122 | "email", 123 | // push: 124 | "", 125 | // reg: 126 | std::regex("(.*)"), 127 | // names: 128 | { }, 129 | // types: 130 | { 's' }, 131 | // format: 132 | "%s" 133 | } 134 | } 135 | }, 136 | 137 | { 138 | 'p', 139 | { 140 | // p=+12345678 141 | { 142 | // name: 143 | "phone", 144 | // push: 145 | "", 146 | // reg: 147 | std::regex("(.*)"), 148 | // names: 149 | { }, 150 | // types: 151 | { 's' }, 152 | // format: 153 | "%s" 154 | } 155 | } 156 | }, 157 | 158 | { 159 | 'z', 160 | { 161 | { 162 | // name: 163 | "timezones", 164 | // push: 165 | "", 166 | // reg: 167 | std::regex("(.*)"), 168 | // names: 169 | { }, 170 | // types: 171 | { 's' }, 172 | // format: 173 | "%s" 174 | } 175 | } 176 | }, 177 | 178 | { 179 | 'r', 180 | { 181 | { 182 | // name: 183 | "repeats", 184 | // push: 185 | "", 186 | // reg: 187 | std::regex("(.*)"), 188 | // names: 189 | { }, 190 | // types: 191 | { 's' }, 192 | // format: 193 | "%s" 194 | } 195 | } 196 | }, 197 | 198 | { 199 | 't', 200 | { 201 | // t=0 0 202 | { 203 | // name: 204 | "timing", 205 | // push: 206 | "", 207 | // reg: 208 | std::regex("^(\\d*) (\\d*)"), 209 | // names: 210 | { "start", "stop" }, 211 | // types: 212 | { 'd', 'd' }, 213 | // format: 214 | "%d %d" 215 | } 216 | } 217 | }, 218 | 219 | { 220 | 'c', 221 | { 222 | // c=IN IP4 10.47.197.26 223 | { 224 | // name: 225 | "connection", 226 | // push: 227 | "", 228 | // reg: 229 | std::regex("^IN IP(\\d) ([^\\\\S/]*)(?:/(\\d*))?"), 230 | // names: 231 | { "version", "ip" , "ttl"}, 232 | // types: 233 | { 'd', 's', 'd'}, 234 | // format: 235 | "", 236 | // formatFunc: 237 | [](const json& o) 238 | { 239 | return hasValue(o, "ttl") 240 | ? "IN IP%d %s/%d" 241 | : "IN IP%d %s"; 242 | } 243 | } 244 | } 245 | }, 246 | 247 | { 248 | 'b', 249 | { 250 | // b=AS:4000 251 | { 252 | // name: 253 | "", 254 | // push: 255 | "bandwidth", 256 | // reg: 257 | std::regex("^(TIAS|AS|CT|RR|RS):(\\d*)"), 258 | // names: 259 | { "type", "limit" }, 260 | // types: 261 | { 's', 'd' }, 262 | // format: 263 | "%s:%d" 264 | } 265 | } 266 | }, 267 | 268 | { 269 | 'm', 270 | { 271 | // m=video 51744 RTP/AVP 126 97 98 34 31 272 | { 273 | // name: 274 | "", 275 | // push: 276 | "", 277 | // reg: 278 | std::regex("^(\\w*) (\\d*)(?:/(\\d*))? ([\\w\\/]*)(?: (.*))?"), 279 | // names: 280 | { "type", "port", "numPorts", "protocol", "payloads" }, 281 | // types: 282 | { 's', 'd', 'd', 's', 's' }, 283 | // format: 284 | "", 285 | // formatFunc: 286 | [](const json& o) 287 | { 288 | return hasValue(o, "numPorts") 289 | ? "%s %d/%d %s %s" 290 | : "%s %d%v %s %s"; 291 | } 292 | } 293 | } 294 | }, 295 | 296 | { 297 | 'a', 298 | { 299 | // a=rtpmap:110 opus/48000/2 300 | { 301 | // name: 302 | "", 303 | // push: 304 | "rtp", 305 | // reg: 306 | std::regex("^rtpmap:(\\d*) ([\\w\\-\\.]*)(?:\\s*\\/(\\d*)(?:\\s*\\/(\\S*))?)?"), 307 | // names: 308 | { "payload", "codec", "rate", "encoding" }, 309 | // types: 310 | { 'd', 's', 'd', 's' }, 311 | // format: 312 | "", 313 | // formatFunc: 314 | [](const json& o) 315 | { 316 | return hasValue(o, "encoding") 317 | ? "rtpmap:%d %s/%s/%s" 318 | : hasValue(o, "rate") 319 | ? "rtpmap:%d %s/%s" 320 | : "rtpmap:%d %s"; 321 | } 322 | }, 323 | 324 | // a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 325 | // a=fmtp:111 minptime=10; useinbandfec=1 326 | { 327 | // name: 328 | "", 329 | // push: 330 | "fmtp", 331 | // reg: 332 | std::regex("^fmtp:(\\d*) (.*)"), 333 | // names: 334 | { "payload", "config" }, 335 | // types: 336 | { 'd', 's' }, 337 | // format: 338 | "fmtp:%d %s" 339 | }, 340 | 341 | // a=control:streamid=0 342 | { 343 | // name: 344 | "control", 345 | // push: 346 | "", 347 | // reg: 348 | std::regex("^control:(.*)"), 349 | // names: 350 | { }, 351 | // types: 352 | { 's' }, 353 | // format: 354 | "control:%s" 355 | }, 356 | 357 | // a=rtcp:65179 IN IP4 193.84.77.194 358 | { 359 | // name: 360 | "rtcp", 361 | // push: 362 | "", 363 | // reg: 364 | std::regex("^rtcp:(\\d*)(?: (\\S*) IP(\\d) (\\S*))?"), 365 | // names: 366 | { "port", "netType", "ipVer", "address" }, 367 | // types: 368 | { 'd', 's', 'd', 's' }, 369 | // format: 370 | "", 371 | // formatFunc: 372 | [](const json& o) 373 | { 374 | return hasValue(o, "address") 375 | ? "rtcp:%d %s IP%d %s" 376 | : "rtcp:%d"; 377 | } 378 | }, 379 | 380 | // a=rtcp-fb:98 trr-int 100 381 | { 382 | // name: 383 | "", 384 | // push: 385 | "rtcpFbTrrInt", 386 | // reg: 387 | std::regex("^rtcp-fb:(\\*|\\d*) trr-int (\\d*)"), 388 | // names: 389 | { "payload", "value" }, 390 | // types: 391 | { 's', 'd' }, 392 | // format: 393 | "rtcp-fb:%s trr-int %d" 394 | }, 395 | 396 | // a=rtcp-fb:98 nack rpsi 397 | { 398 | // name: 399 | "", 400 | // push: 401 | "rtcpFb", 402 | // reg: 403 | std::regex("^rtcp-fb:(\\*|\\d*) ([\\w\\-_]*)(?: ([\\w\\-_]*))?"), 404 | // names: 405 | { "payload", "type", "subtype" }, 406 | // types: 407 | { 's', 's', 's' }, 408 | // format: 409 | "", 410 | // formatFunc: 411 | [](const json& o) 412 | { 413 | return hasValue(o, "subtype") 414 | ? "rtcp-fb:%s %s %s" 415 | : "rtcp-fb:%s %s"; 416 | } 417 | }, 418 | 419 | // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 420 | // a=extmap:1/recvonly URI-gps-string 421 | // a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24 422 | { 423 | // name: 424 | "", 425 | // push: 426 | "ext", 427 | // reg: 428 | std::regex("^extmap:(\\d+)(?:\\/(\\w+))?(?: (urn:ietf:params:rtp-hdrext:encrypt))? (\\S*)(?: (\\S*))?"), 429 | // names: 430 | { "value", "direction", "encrypt-uri", "uri", "config" }, 431 | // types: 432 | { 'd', 's', 's', 's', 's' }, 433 | // format: 434 | "", 435 | // formatFunc: 436 | [](const json& o) 437 | { 438 | return std::string("extmap:%d") + 439 | (hasValue(o, "direction") ? "/%s" : "%v") + 440 | (hasValue(o, "encrypt-uri") ? " %s" : "%v") + 441 | " %s" + 442 | (hasValue(o, "config") ? " %s" : ""); 443 | } 444 | }, 445 | 446 | // a=extmap-allow-mixed 447 | { 448 | // name: 449 | "extmapAllowMixed", 450 | // push: 451 | "", 452 | // reg: 453 | std::regex("^(extmap-allow-mixed)"), 454 | // names: 455 | { }, 456 | // types: 457 | { 's' }, 458 | // format: 459 | "%s" 460 | }, 461 | 462 | // a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 463 | { 464 | // name: 465 | "", 466 | // push: 467 | "crypto", 468 | // reg: 469 | std::regex("^crypto:(\\d*) ([\\w_]*) (\\S*)(?: (\\S*))?"), 470 | // names: 471 | { "id", "suite", "config", "sessionConfig" }, 472 | // types: 473 | { 'd', 's', 's', 's' }, 474 | // format: 475 | "", 476 | // formatFunc: 477 | [](const json& o) 478 | { 479 | return hasValue(o, "sessionConfig") 480 | ? "crypto:%d %s %s %s" 481 | : "crypto:%d %s %s"; 482 | } 483 | }, 484 | 485 | // a=setup:actpass 486 | { 487 | // name: 488 | "setup", 489 | // push: 490 | "", 491 | // reg: 492 | std::regex("^setup:(\\w*)"), 493 | // names: 494 | { }, 495 | // types: 496 | { 's' }, 497 | // format: 498 | "setup:%s" 499 | }, 500 | 501 | // a=mid:1 502 | { 503 | // name: 504 | "mid", 505 | // push: 506 | "", 507 | // reg: 508 | std::regex("^mid:([^\\s]*)"), 509 | // names: 510 | { }, 511 | // types: 512 | { 's' }, 513 | // format: 514 | "mid:%s" 515 | }, 516 | 517 | // a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a 518 | { 519 | // name: 520 | "msid", 521 | // push: 522 | "", 523 | // reg: 524 | std::regex("^msid:(.*)"), 525 | // names: 526 | { }, 527 | // types: 528 | { 's' }, 529 | // format: 530 | "msid:%s" 531 | }, 532 | 533 | // a=ptime:20 534 | { 535 | // name: 536 | "ptime", 537 | // push: 538 | "", 539 | // reg: 540 | std::regex("^ptime:(\\d*)"), 541 | // names: 542 | { }, 543 | // types: 544 | { 'd' }, 545 | // format: 546 | "ptime:%d" 547 | }, 548 | 549 | // a=maxptime:60 550 | { 551 | // name: 552 | "maxptime", 553 | // push: 554 | "", 555 | // reg: 556 | std::regex("^maxptime:(\\d*)"), 557 | // names: 558 | { }, 559 | // types: 560 | { 'd' }, 561 | // format: 562 | "maxptime:%d" 563 | }, 564 | 565 | // a=sendrecv 566 | { 567 | // name: 568 | "direction", 569 | // push: 570 | "", 571 | // reg: 572 | std::regex("^(sendrecv|recvonly|sendonly|inactive)"), 573 | // names: 574 | { }, 575 | // types: 576 | { 's' }, 577 | // format: 578 | "%s" 579 | }, 580 | 581 | // a=ice-lite 582 | { 583 | // name: 584 | "icelite", 585 | // push: 586 | "", 587 | // reg: 588 | std::regex("^(ice-lite)"), 589 | // names: 590 | { }, 591 | // types: 592 | { 's' }, 593 | // format: 594 | "%s" 595 | }, 596 | 597 | // a=ice-ufrag:F7gI 598 | { 599 | // name: 600 | "iceUfrag", 601 | // push: 602 | "", 603 | // reg: 604 | std::regex("^ice-ufrag:(\\S*)"), 605 | // names: 606 | { }, 607 | // types: 608 | { 's' }, 609 | // format: 610 | "ice-ufrag:%s" 611 | }, 612 | 613 | // a=ice-pwd:x9cml/YzichV2+XlhiMu8g 614 | { 615 | // name: 616 | "icePwd", 617 | // push: 618 | "", 619 | // reg: 620 | std::regex("^ice-pwd:(\\S*)"), 621 | // names: 622 | { }, 623 | // types: 624 | { 's' }, 625 | // format: 626 | "ice-pwd:%s" 627 | }, 628 | 629 | // a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 630 | { 631 | // name: 632 | "fingerprint", 633 | // push: 634 | "", 635 | // reg: 636 | std::regex("^fingerprint:(\\S*) (\\S*)"), 637 | // names: 638 | { "type", "hash" }, 639 | // types: 640 | { 's', 's' }, 641 | // format: 642 | "fingerprint:%s %s" 643 | }, 644 | 645 | // a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host 646 | // a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 network-id 3 network-cost 10 647 | // a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10 648 | // a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10 649 | // a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10 650 | { 651 | // name: 652 | "", 653 | // push: 654 | "candidates", 655 | // reg: 656 | std::regex("^candidate:(\\S*) (\\d*) (\\S*) (\\d*) (\\S*) (\\d*) typ (\\S*)(?: raddr (\\S*) rport (\\d*))?(?: tcptype (\\S*))?(?: generation (\\d*))?(?: network-id (\\d*))?(?: network-cost (\\d*))?"), 657 | // names: 658 | { "foundation", "component", "transport", "priority", "ip", "port", "type", "raddr", "rport", "tcptype", "generation", "network-id", "network-cost" }, 659 | // types: 660 | { 's', 'd', 's', 'd', 's', 'd', 's', 's', 'd', 's', 'd', 'd', 'd', 'd' }, 661 | // format: 662 | "", 663 | // formatFunc: 664 | [](const json& o) 665 | { 666 | std::string str = "candidate:%s %d %s %d %s %d typ %s"; 667 | 668 | str += hasValue(o, "raddr") ? " raddr %s rport %d" : "%v%v"; 669 | 670 | // NOTE: candidate has three optional chunks, so %void middles one if it's 671 | // missing. 672 | str += hasValue(o, "tcptype") ? " tcptype %s" : "%v"; 673 | 674 | if (hasValue(o, "generation")) 675 | str += " generation %d"; 676 | 677 | str += hasValue(o, "network-id") ? " network-id %d" : "%v"; 678 | str += hasValue(o, "network-cost") ? " network-cost %d" : "%v"; 679 | 680 | return str; 681 | } 682 | }, 683 | 684 | // a=end-of-candidates 685 | { 686 | // name: 687 | "endOfCandidates", 688 | // push: 689 | "", 690 | // reg: 691 | std::regex("^(end-of-candidates)"), 692 | // names: 693 | { }, 694 | // types: 695 | { 's' }, 696 | // format: 697 | "%s" 698 | }, 699 | 700 | // a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 701 | { 702 | // name: 703 | "remoteCandidates", 704 | // push: 705 | "", 706 | // reg: 707 | std::regex("^remote-candidates:(.*)"), 708 | // names: 709 | { }, 710 | // types: 711 | { 's' }, 712 | // format: 713 | "remote-candidates:%s" 714 | }, 715 | 716 | // a=ice-options:google-ice 717 | { 718 | // name: 719 | "iceOptions", 720 | // push: 721 | "", 722 | // reg: 723 | std::regex("^ice-options:(\\S*)"), 724 | // names: 725 | { }, 726 | // types: 727 | { 's' }, 728 | // format: 729 | "ice-options:%s" 730 | }, 731 | 732 | // a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 733 | { 734 | // name: 735 | "", 736 | // push: 737 | "ssrcs", 738 | // reg: 739 | std::regex("^ssrc:(\\d*) ([\\w_-]*)(?::(.*))?"), 740 | // names: 741 | { "id", "attribute", "value" }, 742 | // types: 743 | { 'd', 's', 's' }, 744 | // format: 745 | "", 746 | // formatFunc: 747 | [](const json& o) 748 | { 749 | std::string str = "ssrc:%d"; 750 | 751 | if (hasValue(o, "attribute")) 752 | { 753 | str += " %s"; 754 | 755 | if (hasValue(o, "value")) 756 | str += ":%s"; 757 | } 758 | 759 | return str; 760 | } 761 | }, 762 | 763 | // a=ssrc-group:FEC 1 2 764 | // a=ssrc-group:FEC-FR 3004364195 1080772241 765 | { 766 | // name: 767 | "", 768 | // push: 769 | "ssrcGroups", 770 | // reg: 771 | std::regex("^ssrc-group:([\x21\x23\x24\x25\x26\x27\x2A\x2B\x2D\x2E\\w]*) (.*)"), 772 | // names: 773 | { "semantics", "ssrcs" }, 774 | // types: 775 | { 's', 's' }, 776 | // format: 777 | "ssrc-group:%s %s" 778 | }, 779 | 780 | // a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 781 | { 782 | // name: 783 | "msidSemantic", 784 | // push: 785 | "", 786 | // reg: 787 | std::regex("^msid-semantic:\\s?(\\w*) (\\S*)"), 788 | // names: 789 | { "semantic", "token" }, 790 | // types: 791 | { 's', 's' }, 792 | // format: 793 | "msid-semantic: %s %s" // Space after ':' is not accidental. 794 | }, 795 | 796 | // a=group:BUNDLE audio video 797 | { 798 | // name: 799 | "", 800 | // push: 801 | "groups", 802 | // reg: 803 | std::regex("^group:(\\w*) (.*)"), 804 | // names: 805 | { "type", "mids" }, 806 | // types: 807 | { 's', 's' }, 808 | // format: 809 | "group:%s %s" 810 | }, 811 | 812 | // a=rtcp-mux 813 | { 814 | // name: 815 | "rtcpMux", 816 | // push: 817 | "", 818 | // reg: 819 | std::regex("^(rtcp-mux)"), 820 | // names: 821 | { }, 822 | // types: 823 | { 's' }, 824 | // format: 825 | "%s" 826 | }, 827 | 828 | // a=rtcp-rsize 829 | { 830 | // name: 831 | "rtcpRsize", 832 | // push: 833 | "", 834 | // reg: 835 | std::regex("^(rtcp-rsize)"), 836 | // names: 837 | { }, 838 | // types: 839 | { 's' }, 840 | // format: 841 | "%s" 842 | }, 843 | 844 | // a=sctpmap:5000 webrtc-datachannel 1024 845 | { 846 | // name: 847 | "sctpmap", 848 | // push: 849 | "", 850 | // reg: 851 | std::regex("^sctpmap:(\\d+) (\\S*)(?: (\\d*))?"), 852 | // names: 853 | { "sctpmapNumber", "app", "maxMessageSize" }, 854 | // types: 855 | { 'd', 's', 'd' }, 856 | // format: 857 | "", 858 | // formatFunc: 859 | [](const json& o) 860 | { 861 | return hasValue(o, "maxMessageSize") 862 | ? "sctpmap:%s %s %s" 863 | : "sctpmap:%s %s"; 864 | } 865 | }, 866 | 867 | // a=x-google-flag:conference 868 | { 869 | // name: 870 | "xGoogleFlag", 871 | // push: 872 | "", 873 | // reg: 874 | std::regex("x-google-flag:([^\\s]*)"), 875 | // names: 876 | { }, 877 | // types: 878 | { 's' }, 879 | // format: 880 | "x-google-flag:%s" 881 | }, 882 | 883 | // a=rid:1 send max-width=1280;max-height=720;max-fps=30;depend=0 884 | { 885 | // name: 886 | "", 887 | // push: 888 | "rids", 889 | // reg: 890 | std::regex("^rid:([\\d\\w]+) (\\w+)(?: (.*))?"), 891 | // names: 892 | { "id", "direction", "params" }, 893 | // types: 894 | { 's', 's', 's' }, 895 | // format: 896 | "", 897 | // formatFunc: 898 | [](const json& o) 899 | { 900 | return hasValue(o, "params") 901 | ? "rid:%s %s %s" 902 | : "rid:%s %s"; 903 | } 904 | }, 905 | 906 | // a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250] 907 | // a=imageattr:* send [x=800,y=640] recv * 908 | // a=imageattr:100 recv [x=320,y=240] 909 | { 910 | // name: 911 | "", 912 | // push: 913 | "imageattrs", 914 | // reg: 915 | std::regex( 916 | std::string() + 917 | // a=imageattr:97 918 | "^imageattr:(\\d+|\\*)" + 919 | // send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] 920 | // send * 921 | "[\\s\\t]+(send|recv)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*)" + 922 | // recv [x=330,y=250] 923 | // recv * 924 | "(?:[\\s\\t]+(recv|send)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*))?" 925 | ), 926 | // names: 927 | { "pt", "dir1", "attrs1", "dir2", "attrs2" }, 928 | // types: 929 | { 's', 's', 's', 's', 's' }, 930 | // format: 931 | "", 932 | // formatFunc: 933 | [](const json& o) 934 | { 935 | return std::string("imageattr:%s %s %s") + 936 | (hasValue(o, "dir2") ? " %s %s" : ""); 937 | } 938 | }, 939 | 940 | // a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8 941 | // a=simulcast:recv 1;4,5 send 6;7 942 | { 943 | // name: 944 | "simulcast", 945 | // push: 946 | "", 947 | // reg: 948 | std::regex( 949 | std::string() + 950 | // a=simulcast: 951 | "^simulcast:" + 952 | // send 1,2,3;~4,~5 953 | "(send|recv) ([a-zA-Z0-9\\-_~;,]+)" + 954 | // space + recv 6;~7,~8 955 | "(?:\\s?(send|recv) ([a-zA-Z0-9\\-_~;,]+))?" + 956 | // end 957 | "$" 958 | ), 959 | // names: 960 | { "dir1", "list1", "dir2", "list2" }, 961 | // types: 962 | { 's', 's', 's', 's' }, 963 | // format: 964 | "", 965 | // formatFunc: 966 | [](const json& o) 967 | { 968 | return std::string("simulcast:%s %s") + 969 | (hasValue(o, "dir2") ? " %s %s" : ""); 970 | } 971 | }, 972 | 973 | // Old simulcast draft 03 (implemented by Firefox). 974 | // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03 975 | // a=simulcast: recv pt=97;98 send pt=97 976 | // a=simulcast: send rid=5;6;7 paused=6,7 977 | { 978 | // name: 979 | "simulcast_03", 980 | // push: 981 | "", 982 | // reg: 983 | std::regex("^simulcast: (.+)$"), 984 | // names: 985 | { "value" }, 986 | // types: 987 | { 's' }, 988 | // format: 989 | "simulcast: %s" 990 | }, 991 | 992 | // a=framerate:25 993 | // a=framerate:29.97 994 | { 995 | // name: 996 | "framerate", 997 | // push: 998 | "", 999 | // reg: 1000 | std::regex("^framerate:(\\d+(?:$|\\.\\d+))"), 1001 | // names: 1002 | { }, 1003 | // types: 1004 | { 'f' }, 1005 | // format: 1006 | "framerate:%s" 1007 | }, 1008 | 1009 | // a=source-filter: incl IN IP4 239.5.2.31 10.1.15.5 1010 | { 1011 | // name: 1012 | "sourceFilter", 1013 | // push: 1014 | "", 1015 | // reg: 1016 | std::regex("^source-filter:[\\s\\t]*(excl|incl) (\\S*) (IP4|IP6|\\*) (\\S*) (.*)"), 1017 | // names: 1018 | { "filterMode", "netType", "addressTypes", "destAddress", "srcList" }, 1019 | // types: 1020 | { 's', 's', 's', 's', 's' }, 1021 | // format: 1022 | "source-filter: %s %s %s %s %s" 1023 | }, 1024 | 1025 | // a=ts-refclk:ptp=IEEE1588-2008:00-50-C2-FF-FE-90-04-37:0 1026 | { 1027 | // name: 1028 | "tsRefclk", 1029 | // push: 1030 | "", 1031 | // reg: 1032 | std::regex("^ts-refclk:(.*)"), 1033 | // names: 1034 | { }, 1035 | // types: 1036 | { 's' }, 1037 | // format: 1038 | "ts-refclk:%s" 1039 | }, 1040 | 1041 | // a=mediaclk:direct=0 1042 | { 1043 | // name: 1044 | "mediaclk", 1045 | // push: 1046 | "", 1047 | // reg: 1048 | std::regex("^mediaclk:(.*)"), 1049 | // names: 1050 | { }, 1051 | // types: 1052 | { 's' }, 1053 | // format: 1054 | "mediaclk:%s" 1055 | }, 1056 | 1057 | // Any a= that we don't understand is kepts verbatim on media.invalid. 1058 | { 1059 | // name: 1060 | "", 1061 | // push: 1062 | "invalid", 1063 | // reg: 1064 | std::regex("(.*)"), 1065 | // names: 1066 | { "value" }, 1067 | // types: 1068 | { 's' }, 1069 | // format: 1070 | "%s" 1071 | }, 1072 | } 1073 | } 1074 | }; 1075 | 1076 | bool hasValue(const json& o, const std::string& key) 1077 | { 1078 | auto it = o.find(key); 1079 | 1080 | if (it == o.end()) 1081 | return false; 1082 | 1083 | if (it->is_string()) 1084 | { 1085 | return !it->get().empty(); 1086 | } 1087 | else if (it->is_number()) 1088 | { 1089 | return true; 1090 | } 1091 | else 1092 | { 1093 | return false; 1094 | } 1095 | } 1096 | } 1097 | } 1098 | -------------------------------------------------------------------------------- /test/parse.test.cpp: -------------------------------------------------------------------------------- 1 | #include "sdptransform.hpp" 2 | #include "helpers.hpp" 3 | #include "catch_amalgamated.hpp" 4 | 5 | SCENARIO("normalSdp", "[parse]") 6 | { 7 | auto sdp = helpers::readFile("test/data/normal.sdp"); 8 | auto session = sdptransform::parse(sdp); 9 | 10 | REQUIRE(session.size() > 0); 11 | REQUIRE(session.find("media") != session.end()); 12 | 13 | auto& media = session.at("media"); 14 | 15 | REQUIRE(session.at("origin").at("username") == "-"); 16 | REQUIRE(session.at("origin").at("sessionId") == 20518); 17 | REQUIRE(session.at("origin").at("sessionVersion") == 0); 18 | REQUIRE(session.at("origin").at("netType") == "IN"); 19 | REQUIRE(session.at("origin").at("ipVer") == 4); 20 | REQUIRE(session.at("origin").at("address") == "203.0.113.1"); 21 | 22 | // Testing json efficient access. 23 | auto originIterator = session.find("origin"); 24 | auto addressIterator = originIterator->find("address"); 25 | auto sessionIdIterator = originIterator->find("sessionId"); 26 | 27 | REQUIRE(addressIterator != originIterator->end()); 28 | REQUIRE(*addressIterator == "203.0.113.1"); 29 | REQUIRE(addressIterator->get() == "203.0.113.1"); 30 | REQUIRE(sessionIdIterator != originIterator->end()); 31 | REQUIRE(*sessionIdIterator == 20518); 32 | REQUIRE(sessionIdIterator->get() == 20518); 33 | REQUIRE(sessionIdIterator->get() == 20518); 34 | REQUIRE(sessionIdIterator->get() == 20518); 35 | 36 | REQUIRE(session.at("connection").at("ip") == "203.0.113.1"); 37 | REQUIRE(session.at("connection").at("version") == 4); 38 | 39 | // Global ICE and fingerprint. 40 | REQUIRE(session.at("iceUfrag") == "F7gI"); 41 | REQUIRE(session.at("icePwd") == "x9cml/YzichV2+XlhiMu8g"); 42 | 43 | auto& audio = media[0]; 44 | auto audioPayloads = sdptransform::parsePayloads(audio.at("payloads")); 45 | 46 | REQUIRE(audioPayloads == R"([ 0, 96 ])"_json); 47 | 48 | REQUIRE(audio.at("type") == "audio"); 49 | REQUIRE(audio.at("port") == 54400); 50 | REQUIRE(audio.at("protocol") == "RTP/SAVPF"); 51 | REQUIRE(audio.at("direction") == "sendrecv"); 52 | REQUIRE(audio.at("rtp")[0].at("payload") == 0); 53 | REQUIRE(audio.at("rtp")[0].at("codec") == "PCMU"); 54 | REQUIRE(audio.at("rtp")[0].at("rate") == 8000); 55 | REQUIRE(audio.at("rtp")[1].at("payload") == 96); 56 | REQUIRE(audio.at("rtp")[1].at("codec") == "opus"); 57 | REQUIRE(audio.at("rtp")[1].at("rate") == 48000); 58 | REQUIRE( 59 | audio.at("ext")[0] == 60 | R"({ 61 | "value" : 1, 62 | "uri" : "URI-toffset" 63 | })"_json 64 | ); 65 | REQUIRE( 66 | audio.at("ext")[1] == 67 | R"({ 68 | "value" : 2, 69 | "direction" : "recvonly", 70 | "uri" : "URI-gps-string" 71 | })"_json 72 | ); 73 | REQUIRE(audio.at("extmapAllowMixed") == "extmap-allow-mixed"); 74 | 75 | auto& video = media[1]; 76 | auto videoPayloads = sdptransform::parsePayloads(video.at("payloads")); 77 | 78 | REQUIRE(videoPayloads == R"([ 97, 98 ])"_json); 79 | 80 | REQUIRE(video.at("type") == "video"); 81 | REQUIRE(video.at("port") == 55400); 82 | REQUIRE(video.at("protocol") == "RTP/SAVPF"); 83 | REQUIRE(video.at("direction") == "sendrecv"); 84 | REQUIRE(video.at("rtp")[0].at("payload") == 97); 85 | REQUIRE(video.at("rtp")[0].at("codec") == "H264"); 86 | REQUIRE(video.at("rtp")[0].at("rate") == 90000); 87 | REQUIRE(video.at("fmtp")[0].at("payload") == 97); 88 | 89 | auto vidFmtp = sdptransform::parseParams(video.at("fmtp")[0].at("config")); 90 | 91 | REQUIRE(vidFmtp.at("profile-level-id") == "42e034"); 92 | REQUIRE(vidFmtp.at("packetization-mode") == 1); 93 | REQUIRE(vidFmtp.at("sprop-parameter-sets") == "Z0IAH5WoFAFuQA==,aM48gA=="); 94 | 95 | // Testing json efficient access. 96 | auto profileLevelIdIterator = vidFmtp.find("profile-level-id"); 97 | 98 | REQUIRE(profileLevelIdIterator != vidFmtp.end()); 99 | REQUIRE(*profileLevelIdIterator == "42e034"); 100 | REQUIRE(profileLevelIdIterator->get() == "42e034"); 101 | 102 | REQUIRE(video.at("fmtp")[1].at("payload") == 98); 103 | 104 | auto vidFmtp2 = sdptransform::parseParams(video.at("fmtp")[1].at("config")); 105 | 106 | REQUIRE(vidFmtp2.at("minptime") == 10); 107 | REQUIRE(vidFmtp2.at("useinbandfec") == 1); 108 | 109 | REQUIRE(video.at("rtp")[1].at("payload") == 98); 110 | REQUIRE(video.at("rtp")[1].at("codec") == "VP8"); 111 | REQUIRE(video.at("rtp")[1].at("rate") == 90000); 112 | REQUIRE(video.at("rtcpFb")[0].at("payload") == "*"); 113 | REQUIRE(video.at("rtcpFb")[0].at("type") == "nack"); 114 | REQUIRE(video.at("rtcpFb")[1].at("payload") == "98"); 115 | REQUIRE(video.at("rtcpFb")[1].at("type") == "nack"); 116 | REQUIRE(video.at("rtcpFb")[1].at("subtype") == "rpsi"); 117 | REQUIRE(video.at("rtcpFbTrrInt")[0].at("payload") == "98"); 118 | REQUIRE(video.at("rtcpFbTrrInt")[0].at("value") == 100); 119 | REQUIRE(video.at("crypto")[0].at("id") == 1); 120 | REQUIRE(video.at("crypto")[0].at("suite") == "AES_CM_128_HMAC_SHA1_32"); 121 | REQUIRE(video.at("crypto")[0].at("config") == "inline:keNcG3HezSNID7LmfDa9J4lfdUL8W1F7TNJKcbuy|2^20|1:32"); 122 | REQUIRE(video.at("ssrcs").size() == 3); 123 | REQUIRE( 124 | video.at("ssrcs")[0] == 125 | R"({ 126 | "id" : 1399694169, 127 | "attribute" : "foo", 128 | "value" : "bar" 129 | })"_json 130 | ); 131 | REQUIRE( 132 | video.at("ssrcs")[1] == 133 | R"({ 134 | "id" : 1399694169, 135 | "attribute" : "baz" 136 | })"_json 137 | ); 138 | REQUIRE( 139 | video.at("ssrcs")[2] == 140 | R"({ 141 | "id" : 1399694169, 142 | "attribute" : "foo-bar", 143 | "value" : "baz" 144 | })"_json 145 | ); 146 | 147 | auto& cs = audio.at("candidates"); 148 | 149 | REQUIRE(cs.size() == 4); 150 | REQUIRE(cs[0].at("foundation") == "0"); 151 | REQUIRE(cs[0].at("component") == 1); 152 | REQUIRE(cs[0].at("transport") == "UDP"); 153 | REQUIRE(cs[0].at("priority") == 2113667327); 154 | REQUIRE(cs[0].at("ip") == "203.0.113.1"); 155 | REQUIRE(cs[0].at("port") == 54400); 156 | REQUIRE(cs[0].at("type") == "host"); 157 | REQUIRE(cs[1].at("foundation") == "1"); 158 | REQUIRE(cs[1].at("component") == 2); 159 | REQUIRE(cs[1].at("transport") == "UDP"); 160 | REQUIRE(cs[1].at("priority") == 2113667326); 161 | REQUIRE(cs[1].at("ip") == "203.0.113.1"); 162 | REQUIRE(cs[1].at("port") == 54401); 163 | REQUIRE(cs[1].at("type") == "host"); 164 | REQUIRE(cs[2].at("foundation") == "2"); 165 | REQUIRE(cs[2].at("component") == 1); 166 | REQUIRE(cs[2].at("transport") == "UDP"); 167 | REQUIRE(cs[2].at("priority") == 1686052607); 168 | REQUIRE(cs[2].at("ip") == "203.0.113.1"); 169 | REQUIRE(cs[2].at("port") == 54402); 170 | REQUIRE(cs[2].at("type") == "srflx"); 171 | REQUIRE(cs[2].at("raddr") == "192.168.1.145"); 172 | REQUIRE(cs[2].at("rport") == 54402); 173 | REQUIRE(cs[2].at("generation") == 0); 174 | REQUIRE(cs[2].at("network-id") == 3); 175 | REQUIRE(cs[2].at("network-cost") == 10); 176 | REQUIRE(cs[3].at("foundation") == "3"); 177 | REQUIRE(cs[3].at("component") == 2); 178 | REQUIRE(cs[3].at("transport") == "UDP"); 179 | REQUIRE(cs[3].at("priority") == 1686052606); 180 | REQUIRE(cs[3].at("ip") == "203.0.113.1"); 181 | REQUIRE(cs[3].at("port") == 54403); 182 | REQUIRE(cs[3].at("type") == "srflx"); 183 | REQUIRE(cs[3].at("raddr") == "192.168.1.145"); 184 | REQUIRE(cs[3].at("rport") == 54403); 185 | REQUIRE(cs[3].at("generation") == 0); 186 | REQUIRE(cs[3].at("network-id") == 3); 187 | REQUIRE(cs[3].at("network-cost") == 10); 188 | 189 | auto& cs2 = video.at("candidates"); 190 | 191 | REQUIRE(cs2[2].find("network-cost") == cs2[2].end()); 192 | REQUIRE(cs2[3].find("network-cost") == cs2[3].end()); 193 | 194 | REQUIRE(media.size() == 2); 195 | 196 | auto newSdp = sdptransform::write(session); 197 | 198 | REQUIRE(newSdp == sdp); 199 | } 200 | 201 | SCENARIO("hackySdp", "[parse]") 202 | { 203 | auto sdp = helpers::readFile("test/data/hacky.sdp"); 204 | auto session = sdptransform::parse(sdp); 205 | 206 | REQUIRE(session.size() > 0); 207 | REQUIRE(session.find("media") != session.end()); 208 | 209 | auto& media = session.at("media"); 210 | 211 | REQUIRE(session.at("origin").at("sessionId") == 3710604898417546434); 212 | REQUIRE(session.at("groups").size() == 1); 213 | REQUIRE(session.at("groups")[0].at("type") == "BUNDLE"); 214 | REQUIRE(session.at("groups")[0].at("mids") == "audio video"); 215 | REQUIRE(session.at("msidSemantic").at("semantic") == "WMS"); 216 | REQUIRE(session.at("msidSemantic").at("token") == "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV"); 217 | 218 | // Verify a=rtcp:65179 IN IP4 193.84.77.194. 219 | REQUIRE(media[0].at("rtcp").at("port") == 1); 220 | REQUIRE(media[0].at("rtcp").at("netType") == "IN"); 221 | REQUIRE(media[0].at("rtcp").at("ipVer") == 4); 222 | REQUIRE(media[0].at("rtcp").at("address") == "0.0.0.0"); 223 | 224 | // Verify ICE TCP types. 225 | REQUIRE( 226 | media[0].at("candidates")[0].find("tcptype") == media[0].at("candidates")[0].end() 227 | ); 228 | REQUIRE(media[0].at("candidates")[1].at("tcptype") == "active"); 229 | REQUIRE(media[0].at("candidates")[1].at("transport") == "tcp"); 230 | REQUIRE(media[0].at("candidates")[1].at("generation") == 0); 231 | REQUIRE(media[0].at("candidates")[1].at("type") == "host"); 232 | REQUIRE( 233 | media[0].at("candidates")[2].find("generation") == media[0].at("candidates")[2].end() 234 | ); 235 | REQUIRE(media[0].at("candidates")[2].at("type") == "host"); 236 | REQUIRE(media[0].at("candidates")[2].at("tcptype") == "active"); 237 | REQUIRE(media[0].at("candidates")[3].at("tcptype") == "passive"); 238 | REQUIRE(media[0].at("candidates")[4].at("tcptype") == "so"); 239 | // raddr + rport + tcptype + generation. 240 | REQUIRE(media[0].at("candidates")[5].at("type") == "srflx"); 241 | REQUIRE(media[0].at("candidates")[5].at("rport") == 9); 242 | REQUIRE(media[0].at("candidates")[5].at("raddr") == "10.0.1.1"); 243 | REQUIRE(media[0].at("candidates")[5].at("tcptype") == "active"); 244 | REQUIRE(media[0].at("candidates")[6].at("tcptype") == "passive"); 245 | REQUIRE(media[0].at("candidates")[6].at("rport") == 8998); 246 | REQUIRE(media[0].at("candidates")[6].at("raddr") == "10.0.1.1"); 247 | REQUIRE(media[0].at("candidates")[6].at("generation") == 5); 248 | 249 | // And verify it works without specifying the IP. 250 | REQUIRE(media[1].at("rtcp").at("port") == 12312); 251 | REQUIRE(media[1].at("rtcp").find("netType") == media[1].at("rtcp").end()); 252 | REQUIRE(media[1].at("rtcp").find("ipVer") == media[1].at("rtcp").end()); 253 | REQUIRE(media[1].at("rtcp").find("address") == media[1].at("rtcp").end()); 254 | 255 | // Verify a=rtpmap:126 telephone-event/8000. 256 | auto lastRtp = media[0].at("rtp").size() - 1; 257 | 258 | REQUIRE(media[0].at("rtp")[lastRtp].at("codec") == "telephone-event"); 259 | REQUIRE(media[0].at("rtp")[lastRtp].at("rate") == 8000); 260 | 261 | REQUIRE(media[0].at("iceOptions") == "google-ice"); 262 | REQUIRE(media[0].at("maxptime") == 60); 263 | REQUIRE(media[0].at("rtcpMux") == "rtcp-mux"); 264 | 265 | REQUIRE(media[0].at("rtp")[0].at("codec") == "opus"); 266 | REQUIRE(media[0].at("rtp")[0].at("encoding") == "2"); 267 | 268 | REQUIRE(media[0].at("ssrcs").size() == 4); 269 | 270 | auto& ssrcs = media[0].at("ssrcs"); 271 | 272 | REQUIRE( 273 | ssrcs[0] == 274 | R"({ 275 | "id" : 2754920552, 276 | "attribute" : "cname", 277 | "value" : "t9YU8M1UxTF8Y1A1" 278 | })"_json 279 | ); 280 | REQUIRE( 281 | ssrcs[1] == 282 | R"({ 283 | "id" : 2754920552, 284 | "attribute" : "msid", 285 | "value" : "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0" 286 | })"_json 287 | ); 288 | REQUIRE( 289 | ssrcs[2] == 290 | R"({ 291 | "id" : 2754920552, 292 | "attribute" : "mslabel", 293 | "value" : "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV" 294 | })"_json 295 | ); 296 | REQUIRE( 297 | ssrcs[3] == 298 | R"({ 299 | "id" : 2754920552, 300 | "attribute" : "label", 301 | "value" : "Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0" 302 | })"_json 303 | ); 304 | 305 | // Verify a=sctpmap:5000 webrtc-datachannel 1024. 306 | REQUIRE(media[2].find("sctpmap") != media[2].end()); 307 | REQUIRE(media[2].at("sctpmap").at("sctpmapNumber") == 5000); 308 | REQUIRE(media[2].at("sctpmap").at("app") == "webrtc-datachannel"); 309 | REQUIRE(media[2].at("sctpmap").at("maxMessageSize") == 1024); 310 | 311 | // Verify a=framerate:29.97. 312 | REQUIRE(media[1].at("framerate") == 1234); 313 | REQUIRE(media[2].at("framerate") == double{ 29.97 }); 314 | 315 | auto newSdp = sdptransform::write(session); 316 | 317 | REQUIRE(newSdp == sdp); 318 | } 319 | 320 | SCENARIO("iceliteSdp", "[parse]") 321 | { 322 | auto sdp = helpers::readFile("test/data/icelite.sdp"); 323 | auto session = sdptransform::parse(sdp); 324 | 325 | REQUIRE(session.size() > 0); 326 | REQUIRE(session.at("icelite") == "ice-lite"); 327 | 328 | auto newSdp = sdptransform::write(session); 329 | 330 | REQUIRE(newSdp == sdp); 331 | } 332 | 333 | SCENARIO("invalidSdp", "[parse]") 334 | { 335 | auto sdp = helpers::readFile("test/data/invalid.sdp"); 336 | auto session = sdptransform::parse(sdp); 337 | 338 | REQUIRE(session.size() > 0); 339 | REQUIRE(session.find("media") != session.end()); 340 | 341 | auto& media = session.at("media"); 342 | 343 | // Verify a=rtcp:65179 IN IP4 193.84.77.194- 344 | REQUIRE(media[0].at("rtcp").at("port") == 1); 345 | REQUIRE(media[0].at("rtcp").at("netType") == "IN"); 346 | REQUIRE(media[0].at("rtcp").at("ipVer") == 7); 347 | REQUIRE(media[0].at("rtcp").at("address") == "X"); 348 | REQUIRE(media[0].at("invalid").size() == 1); // f= was lost. 349 | REQUIRE(media[0].at("invalid")[0].at("value") == "goo:hithere"); 350 | 351 | auto newSdp = sdptransform::write(session); 352 | 353 | // Append the wrong (so lost) f= line. 354 | newSdp += "f=invalid:yes\r\n"; 355 | 356 | REQUIRE(newSdp == sdp); 357 | } 358 | 359 | SCENARIO("jssipSdp", "[parse]") 360 | { 361 | auto sdp = helpers::readFile("test/data/jssip.sdp"); 362 | auto session = sdptransform::parse(sdp); 363 | 364 | REQUIRE(session.size() > 0); 365 | REQUIRE(session.find("media") != session.end()); 366 | 367 | auto& media = session.at("media"); 368 | auto& audio = media[0]; 369 | auto& audCands = audio.at("candidates"); 370 | 371 | REQUIRE(audCands.size() == 6); 372 | 373 | // Testing ice optionals. 374 | REQUIRE( 375 | audCands[0] == 376 | R"({ 377 | "foundation" : "1162875081", 378 | "component" : 1, 379 | "transport" : "udp", 380 | "priority" : 2113937151, 381 | "ip" : "192.168.34.75", 382 | "port" : 60017, 383 | "type" : "host", 384 | "generation" : 0 385 | })"_json 386 | ); 387 | REQUIRE( 388 | audCands[2] == 389 | R"({ 390 | "foundation" : "3289912957", 391 | "component" : 1, 392 | "transport" : "udp", 393 | "priority" : 1845501695, 394 | "ip" : "193.84.77.194", 395 | "port" : 60017, 396 | "type" : "srflx", 397 | "raddr" : "192.168.34.75", 398 | "rport" : 60017, 399 | "generation" : 0 400 | })"_json 401 | ); 402 | REQUIRE( 403 | audCands[4] == 404 | R"({ 405 | "foundation" : "198437945", 406 | "component" : 1, 407 | "transport" : "tcp", 408 | "priority" : 1509957375, 409 | "ip" : "192.168.34.75", 410 | "port" : 0, 411 | "type" : "host", 412 | "generation" : 0 413 | })"_json 414 | ); 415 | } 416 | 417 | SCENARIO("jsepSdp", "[parse]") 418 | { 419 | auto sdp = helpers::readFile("test/data/jsep.sdp"); 420 | auto session = sdptransform::parse(sdp); 421 | 422 | REQUIRE(session.size() > 0); 423 | REQUIRE(session.find("media") != session.end()); 424 | 425 | auto& media = session.at("media"); 426 | 427 | REQUIRE(media.size() == 2); 428 | 429 | auto& video = media[1]; 430 | 431 | REQUIRE(video.at("ssrcGroups").size() == 1); 432 | REQUIRE( 433 | video.at("ssrcGroups")[0] == 434 | R"({ 435 | "semantics" : "FID", 436 | "ssrcs" : "1366781083 1366781084" 437 | })"_json 438 | ); 439 | 440 | REQUIRE( 441 | video.at("msid") == 442 | "61317484-2ed4-49d7-9eb7-1414322a7aae f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0" 443 | ); 444 | 445 | REQUIRE(video.find("rtcpRsize") != video.end()); 446 | REQUIRE(video.find("endOfCandidates") != video.end()); 447 | } 448 | 449 | SCENARIO("alacSdp", "[parse]") 450 | { 451 | auto sdp = helpers::readFile("test/data/alac.sdp"); 452 | auto session = sdptransform::parse(sdp); 453 | 454 | REQUIRE(session.size() > 0); 455 | REQUIRE(session.find("media") != session.end()); 456 | 457 | auto& media = session.at("media"); 458 | auto& audio = media[0]; 459 | auto audioPayloads = sdptransform::parsePayloads(audio.at("payloads")); 460 | 461 | REQUIRE(audioPayloads == R"([ 96 ])"_json); 462 | 463 | REQUIRE(audio.at("type") == "audio"); 464 | REQUIRE(audio.at("protocol") == "RTP/AVP"); 465 | REQUIRE(audio.at("fmtp")[0].at("payload") == 96); 466 | REQUIRE(audio.at("fmtp")[0].at("config") == "352 0 16 40 10 14 2 255 0 0 44100"); 467 | REQUIRE(audio.at("rtp")[0].at("payload") == 96); 468 | REQUIRE(audio.at("rtp")[0].at("codec") == "AppleLossless"); 469 | REQUIRE(audio.at("rtp")[0].find("rate") == audio.at("rtp")[0].end()); 470 | REQUIRE(audio.at("rtp")[0].find("encoding") == audio.at("rtp")[0].end()); 471 | 472 | auto newSdp = sdptransform::write(session); 473 | 474 | REQUIRE(newSdp == sdp); 475 | } 476 | 477 | SCENARIO("onvifSdp", "[parse]") 478 | { 479 | auto sdp = helpers::readFile("test/data/onvif.sdp"); 480 | auto session = sdptransform::parse(sdp); 481 | 482 | REQUIRE(session.size() > 0); 483 | REQUIRE(session.find("media") != session.end()); 484 | 485 | auto& media = session.at("media"); 486 | auto& audio = media[0]; 487 | 488 | REQUIRE(audio.at("type") == "audio"); 489 | REQUIRE(audio.at("port") == 0); 490 | REQUIRE(audio.at("protocol") == "RTP/AVP"); 491 | REQUIRE(audio.at("control") == "rtsp://example.com/onvif_camera/audio"); 492 | REQUIRE(audio.at("payloads") == "0"); 493 | 494 | auto& video = media[1]; 495 | 496 | REQUIRE(video.at("type") == "video"); 497 | REQUIRE(video.at("port") == 0); 498 | REQUIRE(video.at("protocol") == "RTP/AVP"); 499 | REQUIRE(video.at("control") == "rtsp://example.com/onvif_camera/video"); 500 | REQUIRE(video.at("payloads") == "26"); 501 | 502 | auto& application = media[2]; 503 | 504 | REQUIRE(application.at("type") == "application"); 505 | REQUIRE(application.at("port") == 0); 506 | REQUIRE(application.at("protocol") == "RTP/AVP"); 507 | REQUIRE(application.at("control") == "rtsp://example.com/onvif_camera/metadata"); 508 | REQUIRE(application.at("payloads") == "107"); 509 | REQUIRE(application.at("direction") == "recvonly"); 510 | REQUIRE(application.at("rtp")[0].at("payload") == 107); 511 | REQUIRE(application.at("rtp")[0].at("codec") == "vnd.onvif.metadata"); 512 | REQUIRE(application.at("rtp")[0].at("rate") == 90000); 513 | REQUIRE(application.at("rtp")[0].find("encoding") == application.at("rtp")[0].end()); 514 | 515 | auto newSdp = sdptransform::write(session); 516 | 517 | REQUIRE(newSdp == sdp); 518 | } 519 | 520 | SCENARIO("ssrcSdp", "[parse]") 521 | { 522 | auto sdp = helpers::readFile("test/data/ssrc.sdp"); 523 | auto session = sdptransform::parse(sdp); 524 | 525 | REQUIRE(session.size() > 0); 526 | REQUIRE(session.find("media") != session.end()); 527 | 528 | auto& media = session.at("media"); 529 | auto& video = media[1]; 530 | 531 | REQUIRE(video.at("ssrcGroups").size() == 2); 532 | REQUIRE( 533 | video.at("ssrcGroups") == 534 | R"([ 535 | { 536 | "semantics" : "FID", 537 | "ssrcs" : "3004364195 1126032854" 538 | }, 539 | { 540 | "semantics" : "FEC-FR", 541 | "ssrcs" : "3004364195 1080772241" 542 | } 543 | ])"_json 544 | ); 545 | } 546 | 547 | SCENARIO("simulcastSdp", "[parse]") 548 | { 549 | auto sdp = helpers::readFile("test/data/simulcast.sdp"); 550 | auto session = sdptransform::parse(sdp); 551 | 552 | REQUIRE(session.size() > 0); 553 | REQUIRE(session.find("media") != session.end()); 554 | 555 | auto& media = session.at("media"); 556 | auto& video = media[1]; 557 | 558 | REQUIRE(video.at("type") == "video"); 559 | 560 | // Test rid 1. 561 | REQUIRE( 562 | video.at("rids")[0] == 563 | R"({ 564 | "id" : "1", 565 | "direction" : "send", 566 | "params" : "pt=97;max-width=1280;max-height=720;max-fps=30" 567 | })"_json 568 | ); 569 | 570 | // Test rid 2. 571 | REQUIRE( 572 | video.at("rids")[1] == 573 | R"({ 574 | "id" : "2", 575 | "direction" : "send", 576 | "params" : "pt=98" 577 | })"_json 578 | ); 579 | 580 | // Test rid 3. 581 | REQUIRE( 582 | video.at("rids")[2] == 583 | R"({ 584 | "id" : "3", 585 | "direction" : "send", 586 | "params" : "pt=99" 587 | })"_json 588 | ); 589 | 590 | // Test rid 4. 591 | REQUIRE( 592 | video.at("rids")[3] == 593 | R"({ 594 | "id" : "4", 595 | "direction" : "send", 596 | "params" : "pt=100" 597 | })"_json 598 | ); 599 | 600 | // Test rid 5. 601 | REQUIRE( 602 | video.at("rids")[4] == 603 | R"({ 604 | "id" : "c", 605 | "direction" : "recv", 606 | "params" : "pt=97" 607 | })"_json 608 | ); 609 | 610 | // // Test rid 1 params. 611 | auto rid1Params = sdptransform::parseParams(video.at("rids")[0].at("params")); 612 | 613 | REQUIRE( 614 | rid1Params == 615 | R"({ 616 | "pt" : 97, 617 | "max-width" : 1280, 618 | "max-height" : 720, 619 | "max-fps" : 30 620 | })"_json 621 | ); 622 | 623 | // Test rid 2 params. 624 | auto rid2Params = sdptransform::parseParams(video.at("rids")[1].at("params")); 625 | 626 | REQUIRE( 627 | rid2Params == 628 | R"({ 629 | "pt" : 98 630 | })"_json 631 | ); 632 | 633 | // Test rid 3 params. 634 | auto rid3Params = sdptransform::parseParams(video.at("rids")[2].at("params")); 635 | 636 | REQUIRE( 637 | rid3Params == 638 | R"({ 639 | "pt" : 99 640 | })"_json 641 | ); 642 | 643 | // Test rid 4 params. 644 | auto rid4Params = sdptransform::parseParams(video.at("rids")[3].at("params")); 645 | 646 | REQUIRE( 647 | rid4Params == 648 | R"({ 649 | "pt" : 100 650 | })"_json 651 | ); 652 | 653 | // Test rid 5 params. 654 | auto rid5Params = sdptransform::parseParams(video.at("rids")[4].at("params")); 655 | 656 | REQUIRE( 657 | rid5Params == 658 | R"({ 659 | "pt" : 97 660 | })"_json 661 | ); 662 | 663 | // Test imageattr lines. 664 | REQUIRE(video.at("imageattrs").size() == 5); 665 | 666 | // Test imageattr 1. 667 | REQUIRE( 668 | video.at("imageattrs")[0] == 669 | R"({ 670 | "pt" : "97", 671 | "dir1" : "send", 672 | "attrs1" : "[x=1280,y=720]", 673 | "dir2" : "recv", 674 | "attrs2" : "[x=1280,y=720] [x=320,y=180] [x=160,y=90]" 675 | })"_json 676 | ); 677 | 678 | // Test imageattr 2. 679 | REQUIRE( 680 | video.at("imageattrs")[1] == 681 | R"({ 682 | "pt" : "98", 683 | "dir1" : "send", 684 | "attrs1" : "[x=320,y=180,sar=1.1,q=0.6]" 685 | })"_json 686 | ); 687 | 688 | // Test imageattr 3. 689 | REQUIRE( 690 | video.at("imageattrs")[2] == 691 | R"({ 692 | "pt" : "99", 693 | "dir1" : "send", 694 | "attrs1" : "[x=160,y=90]" 695 | })"_json 696 | ); 697 | 698 | // Test imageattr 4. 699 | REQUIRE( 700 | video.at("imageattrs")[3] == 701 | R"({ 702 | "pt" : "100", 703 | "dir1" : "recv", 704 | "attrs1" : "[x=1280,y=720] [x=320,y=180]", 705 | "dir2" : "send", 706 | "attrs2" : "[x=1280,y=720]" 707 | })"_json 708 | ); 709 | 710 | // Test imageattr 5. 711 | REQUIRE( 712 | video.at("imageattrs")[4] == 713 | R"({ 714 | "pt" : "*", 715 | "dir1" : "recv", 716 | "attrs1" : "*" 717 | })"_json 718 | ); 719 | 720 | // Test imageattr 2 send params. 721 | auto imageattr2SendParams = 722 | sdptransform::parseImageAttributes(video.at("imageattrs")[1].at("attrs1")); 723 | 724 | REQUIRE( 725 | imageattr2SendParams == 726 | R"([ 727 | { 728 | "x" : 320, 729 | "y" : 180, 730 | "sar" : 1.1, 731 | "q" : 0.6 732 | } 733 | ])"_json 734 | ); 735 | 736 | // Test imageattr 3 send params. 737 | auto imageattr3SendParams = 738 | sdptransform::parseImageAttributes(video.at("imageattrs")[2].at("attrs1")); 739 | 740 | REQUIRE( 741 | imageattr3SendParams == 742 | R"([ 743 | { 744 | "x" : 160, 745 | "y" : 90 746 | } 747 | ])"_json 748 | ); 749 | 750 | // Test imageattr 4 recv params. 751 | auto imageattr4RecvParams = 752 | sdptransform::parseImageAttributes(video.at("imageattrs")[3].at("attrs1")); 753 | 754 | REQUIRE( 755 | imageattr4RecvParams == 756 | R"([ 757 | { 758 | "x" : 1280, 759 | "y" : 720 760 | }, 761 | { 762 | "x" : 320, 763 | "y" : 180 764 | } 765 | ])"_json 766 | ); 767 | 768 | // Test imageattr 4 send params. 769 | auto imageattr4SendParams = 770 | sdptransform::parseImageAttributes(video.at("imageattrs")[3].at("attrs2")); 771 | 772 | REQUIRE( 773 | imageattr4SendParams == 774 | R"([ 775 | { 776 | "x" : 1280, 777 | "y" : 720 778 | } 779 | ])"_json 780 | ); 781 | 782 | // Test imageattr 5 recv params. 783 | auto imageattr5RecvParams = 784 | sdptransform::parseImageAttributes(video.at("imageattrs")[4].at("attrs1")); 785 | 786 | REQUIRE(imageattr5RecvParams == "*"); 787 | 788 | // Test simulcast line. 789 | REQUIRE( 790 | video.at("simulcast") == 791 | R"({ 792 | "dir1" : "send", 793 | "list1" : "1,~4;2;3", 794 | "dir2" : "recv", 795 | "list2" : "c" 796 | })"_json 797 | ); 798 | 799 | // Test simulcast send streams. 800 | auto simulcastSendStreams = 801 | sdptransform::parseSimulcastStreamList(video.at("simulcast").at("list1")); 802 | 803 | REQUIRE( 804 | simulcastSendStreams == 805 | R"([ 806 | [ { "scid": "1", "paused": false }, { "scid": "4", "paused": true } ], 807 | [ { "scid": "2", "paused": false } ], 808 | [ { "scid": "3", "paused": false } ] 809 | ])"_json 810 | ); 811 | 812 | // Test simulcast recv streams. 813 | auto simulcastRecvStreams = 814 | sdptransform::parseSimulcastStreamList(video.at("simulcast").at("list2")); 815 | 816 | REQUIRE( 817 | simulcastRecvStreams == 818 | R"([ 819 | [ { "scid": "c", "paused": false } ] 820 | ])"_json 821 | ); 822 | 823 | // Test simulcast version 03 line. 824 | REQUIRE( 825 | video.at("simulcast_03") == 826 | R"({ 827 | "value" : "send rid=1,4;2;3 paused=4 recv rid=c" 828 | })"_json 829 | ); 830 | 831 | auto newSdp = sdptransform::write(session); 832 | 833 | REQUIRE(newSdp == sdp); 834 | } 835 | 836 | SCENARIO("st2022-6Sdp", "[parse]") 837 | { 838 | auto sdp = helpers::readFile("test/data/st2022-6.sdp"); 839 | auto session = sdptransform::parse(sdp); 840 | 841 | // Session sanity check. 842 | REQUIRE(session.size() > 0); 843 | REQUIRE(session.find("media") != session.end()); 844 | auto& media = session.at("media"); 845 | 846 | // No invalid node 847 | REQUIRE(media.find("invalid") == media.end()); 848 | 849 | // Check sourceFilter node exists. 850 | auto& video = media[0]; 851 | REQUIRE(video.find("sourceFilter") != video.end()); 852 | auto& sourceFilter = video.at("sourceFilter"); 853 | 854 | // Check expected values are present. 855 | REQUIRE(sourceFilter.at("filterMode") == "incl"); 856 | REQUIRE(sourceFilter.at("netType") == "IN"); 857 | REQUIRE(sourceFilter.at("addressTypes") == "IP4"); 858 | REQUIRE(sourceFilter.at("destAddress") == "239.0.0.1"); 859 | REQUIRE(sourceFilter.at("srcList") == "192.168.20.20"); 860 | } 861 | 862 | SCENARIO("st2110-20Sdp", "[parse]") 863 | { 864 | auto sdp = helpers::readFile("test/data/st2110-20.sdp"); 865 | auto session = sdptransform::parse(sdp); 866 | 867 | // Session sanity check. 868 | REQUIRE(session.size() > 0); 869 | REQUIRE(session.find("media") != session.end()); 870 | auto& media = session.at("media"); 871 | 872 | // No invalid node 873 | REQUIRE(media.find("invalid") == media.end()); 874 | 875 | // Check sourceFilter node exists. 876 | auto& video = media[0]; 877 | REQUIRE(video.find("sourceFilter") != video.end()); 878 | auto& sourceFilter = video.at("sourceFilter"); 879 | 880 | // Check expected values are present. 881 | REQUIRE(sourceFilter.at("filterMode") == "incl"); 882 | REQUIRE(sourceFilter.at("netType") == "IN"); 883 | REQUIRE(sourceFilter.at("addressTypes") == "IP4"); 884 | REQUIRE(sourceFilter.at("destAddress") == "239.100.9.10"); 885 | REQUIRE(sourceFilter.at("srcList") == "192.168.100.2"); 886 | 887 | auto fmtp0Params = sdptransform::parseParams(video.at("fmtp")[0].at("config")); 888 | 889 | REQUIRE( 890 | fmtp0Params == 891 | R"({ 892 | "sampling" : "YCbCr-4:2:2", 893 | "width" : 1280, 894 | "height" : 720, 895 | "interlace" : "", 896 | "exactframerate" : "60000/1001", 897 | "depth" : 10, 898 | "TCS" : "SDR", 899 | "colorimetry" : "BT709", 900 | "PM" : "2110GPM", 901 | "SSN" : "ST2110-20:2017" 902 | })"_json 903 | ); 904 | } 905 | 906 | SCENARIO("aes67", "[parse]") 907 | { 908 | auto sdp = helpers::readFile("test/data/aes67.sdp"); 909 | auto session = sdptransform::parse(sdp); 910 | 911 | REQUIRE(session.size() > 0); 912 | REQUIRE(session.find("media") != session.end()); 913 | 914 | auto& media = session.at("media"); 915 | auto& audio = media[0]; 916 | auto audioPayloads = sdptransform::parsePayloads(audio.at("payloads")); 917 | 918 | REQUIRE(audioPayloads == R"([ 96 ])"_json); 919 | 920 | REQUIRE(audio.at("type") == "audio"); 921 | REQUIRE(audio.at("port") == 5004); 922 | REQUIRE(audio.at("numPorts") == 2); 923 | REQUIRE(audio.at("protocol") == "RTP/AVP"); 924 | REQUIRE(audio.at("rtp")[0].at("payload") == 96); 925 | REQUIRE(audio.at("rtp")[0].at("codec") == "L24"); 926 | REQUIRE(audio.at("rtp")[0].at("rate") == 48000); 927 | REQUIRE(audio.at("rtp")[0].at("encoding") == "2"); 928 | REQUIRE(audio.at("tsRefclk") == "ptp=IEEE1588-2008:00-1D-C1-FF-FE-12-00-A4:0"); 929 | REQUIRE(audio.at("mediaclk") == "direct=0"); 930 | 931 | auto newSdp = sdptransform::write(session); 932 | 933 | REQUIRE(newSdp == sdp); 934 | } 935 | 936 | SCENARIO("multicastttlSdp", "[parse]") 937 | { 938 | auto sdp = helpers::readFile("test/data/multicastttl.sdp"); 939 | auto session = sdptransform::parse(sdp); 940 | 941 | REQUIRE(session.size() > 0); 942 | REQUIRE(session.find("media") != session.end()); 943 | 944 | auto& media = session.at("media"); 945 | 946 | REQUIRE(session.at("origin").at("sessionId") == 1558439701980808); 947 | REQUIRE(session.at("origin").at("sessionVersion") == 1); 948 | REQUIRE(session.at("origin").at("netType") == "IN"); 949 | REQUIRE(session.at("origin").at("ipVer") == 4); 950 | REQUIRE(session.at("origin").at("address") == "192.168.1.189"); 951 | 952 | REQUIRE(session.at("connection").at("ip") == "224.2.36.42"); 953 | REQUIRE(session.at("connection").at("version") == 4); 954 | REQUIRE(session.at("connection").at("ttl") == 15); 955 | 956 | auto& video = media[0]; 957 | auto videoPayloads = sdptransform::parsePayloads(video.at("payloads")); 958 | 959 | REQUIRE(video.at("type") == "video"); 960 | REQUIRE(video.at("port") == 6970); 961 | REQUIRE(video.at("protocol") == "RTP/AVP"); 962 | REQUIRE(video.at("rtp")[0].at("payload") == 96); 963 | REQUIRE(video.at("rtp")[0].at("codec") == "H264"); 964 | REQUIRE(video.at("rtp")[0].at("rate") == 90000); 965 | REQUIRE(video.at("fmtp")[0].at("payload") == 96); 966 | 967 | auto newSdp = sdptransform::write(session); 968 | 969 | REQUIRE(newSdp == sdp); 970 | } 971 | 972 | SCENARIO("extmapEncryptSdp", "[parse]") 973 | { 974 | auto sdp = helpers::readFile("test/data/extmap-encrypt.sdp"); 975 | auto session = sdptransform::parse(sdp); 976 | 977 | REQUIRE(session.size() > 0); 978 | REQUIRE(session.find("media") != session.end()); 979 | 980 | auto& media = session.at("media"); 981 | auto& audio = media[0]; 982 | auto audioPayloads = sdptransform::parsePayloads(audio.at("payloads")); 983 | 984 | REQUIRE(audioPayloads == R"([ 96 ])"_json); 985 | 986 | REQUIRE(audio.at("type") == "audio"); 987 | REQUIRE(audio.at("port") == 54400); 988 | REQUIRE(audio.at("protocol") == "RTP/SAVPF"); 989 | REQUIRE(audio.at("rtp")[0].at("payload") == 96); 990 | REQUIRE(audio.at("rtp")[0].at("codec") == "opus"); 991 | REQUIRE(audio.at("rtp")[0].at("rate") == 48000); 992 | REQUIRE( 993 | audio.at("ext")[0] == 994 | R"({ 995 | "value" : 1, 996 | "direction" : "sendonly", 997 | "uri" : "URI-toffset" 998 | })"_json 999 | ); 1000 | REQUIRE( 1001 | audio.at("ext")[1] == 1002 | R"({ 1003 | "value" : 2, 1004 | "uri" : "urn:ietf:params:rtp-hdrext:toffset" 1005 | })"_json 1006 | ); 1007 | REQUIRE( 1008 | audio.at("ext")[2] == 1009 | R"({ 1010 | "value" : 3, 1011 | "encrypt-uri" : "urn:ietf:params:rtp-hdrext:encrypt", 1012 | "uri" : "urn:ietf:params:rtp-hdrext:smpte-tc", 1013 | "config" : "25@600/24" 1014 | })"_json 1015 | ); 1016 | REQUIRE( 1017 | audio.at("ext")[3] == 1018 | R"({ 1019 | "value" : 4, 1020 | "direction" : "recvonly", 1021 | "encrypt-uri" : "urn:ietf:params:rtp-hdrext:encrypt", 1022 | "uri" : "URI-gps-string" 1023 | })"_json 1024 | ); 1025 | 1026 | REQUIRE(media.size() == 1); 1027 | 1028 | auto newSdp = sdptransform::write(session); 1029 | 1030 | REQUIRE(newSdp == sdp); 1031 | } 1032 | --------------------------------------------------------------------------------