summaryrefslogtreecommitdiffstats
path: root/third_party/rust/webrtc-sdp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/rust/webrtc-sdp
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/webrtc-sdp')
-rw-r--r--third_party/rust/webrtc-sdp/.cargo-checksum.json1
-rw-r--r--third_party/rust/webrtc-sdp/CHANGELOG.md86
-rw-r--r--third_party/rust/webrtc-sdp/CODE_OF_CONDUCT.md8
-rw-r--r--third_party/rust/webrtc-sdp/Cargo.lock161
-rw-r--r--third_party/rust/webrtc-sdp/Cargo.toml65
-rw-r--r--third_party/rust/webrtc-sdp/LICENSE373
-rw-r--r--third_party/rust/webrtc-sdp/README.md73
-rw-r--r--third_party/rust/webrtc-sdp/examples/file_parser.rs72
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/02.sdp7
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/03.sdp38
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/04.sdp7
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/05.sdp6
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/06.sdp12
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/07.sdp8
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/08.sdp87
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/09.sdp34
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/10.sdp13
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/11.sdp66
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/12.sdp58
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/13.sdp12
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/14.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/15.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/16.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/17.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/18.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/19.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/20.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/21.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/22.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/23.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/24.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/25.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/26.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/27.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/28.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/29.sdp8
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/30.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/31.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/32.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/33.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/34.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/35.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/36.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/37.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/38.sdp9
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/39.sdp8
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/40.sdp8
-rw-r--r--third_party/rust/webrtc-sdp/examples/sdps/41.sdp91
-rwxr-xr-xthird_party/rust/webrtc-sdp/examples/sdps/extract.sh3
-rw-r--r--third_party/rust/webrtc-sdp/src/address.rs199
-rw-r--r--third_party/rust/webrtc-sdp/src/address_tests.rs73
-rw-r--r--third_party/rust/webrtc-sdp/src/anonymizer.rs198
-rw-r--r--third_party/rust/webrtc-sdp/src/anonymizer_tests.rs121
-rw-r--r--third_party/rust/webrtc-sdp/src/attribute_type.rs3319
-rw-r--r--third_party/rust/webrtc-sdp/src/attribute_type_tests.rs1102
-rw-r--r--third_party/rust/webrtc-sdp/src/error.rs194
-rw-r--r--third_party/rust/webrtc-sdp/src/error_tests.rs129
-rw-r--r--third_party/rust/webrtc-sdp/src/lib.rs907
-rw-r--r--third_party/rust/webrtc-sdp/src/lib_tests.rs774
-rw-r--r--third_party/rust/webrtc-sdp/src/media_type.rs480
-rw-r--r--third_party/rust/webrtc-sdp/src/media_type_tests.rs411
-rw-r--r--third_party/rust/webrtc-sdp/src/network.rs37
-rw-r--r--third_party/rust/webrtc-sdp/src/network_tests.rs33
-rw-r--r--third_party/rust/webrtc-sdp/tests/parse_sdp_tests.rs783
64 files changed, 10281 insertions, 0 deletions
diff --git a/third_party/rust/webrtc-sdp/.cargo-checksum.json b/third_party/rust/webrtc-sdp/.cargo-checksum.json
new file mode 100644
index 0000000000..778f77c392
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"CHANGELOG.md":"43327b4e426dcfc45fc4b5a6c3090064661033fbed4de24ba3fd15de414173fd","CODE_OF_CONDUCT.md":"ab482a6e8d9ed00cc77e0f90d823210e3368201ed98d9fda36033fd19aff24c4","Cargo.lock":"d7c87b5013450b940f537afc5d7cd8397aa93932228f26a4dc07df9d2553e9b3","Cargo.toml":"77e912d6089d0e49dbe83634246d0708040c49b57955aed943b712c728257001","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"7d0c31ab88fb4b9eff849652f89b14940a83b836f10f11750957d73842fe25f6","examples/file_parser.rs":"f95b331d331feb3859a5092d4ed70389711c9f461359d23303aea6a4f8ca5ff6","examples/sdps/02.sdp":"bda3c63d42091eb8dc6cda46e1ed252020b76bbc78563a36244d49a95ff11d00","examples/sdps/03.sdp":"a18aa7be19dd911779dafce2c92b3ed21084598ecb9eb45c0d665f1925030547","examples/sdps/04.sdp":"a8139befd817711e301a16db7f12e24410add1fa39c8a8de77ae25cdd4baab5e","examples/sdps/05.sdp":"445febdc16afeed4bf36c4b30e1952b8915078248c6bb6f9d4eb6e16c75f7065","examples/sdps/06.sdp":"49fb363dfab70340b3906015e1f4fb8d5058267995dd4e344b0e021429da58de","examples/sdps/07.sdp":"03d2a6847d9a405eeaaec2d7011e81aa73b63b33d22a4f7371dff7e9c82ba096","examples/sdps/08.sdp":"bcab5d8b258e3bcdcd21a3299e51b26e6563d549b74f612780d2e3be8a0495c5","examples/sdps/09.sdp":"c771c5aa3efc4acb4b47f488fb9ceaedd6f94f706fd92c704e98fa26d3f37e29","examples/sdps/10.sdp":"deb23d5578a20df6e16ffbda193a4ba97df7131ce95eac3677b4d2fc74e50003","examples/sdps/11.sdp":"7e14a6d290a659b715f32a5257cb0035126c9bb9c7e1dac26f757bf36c93dc09","examples/sdps/12.sdp":"8ad9b018ba1c62865520d65869b568f6750f4ff93deca238f6c04dd468562dc9","examples/sdps/13.sdp":"d396c398247c253fcf5ed087fd5a3334ce2f8c66efd93638d2e8884dc318eac1","examples/sdps/14.sdp":"b714a4216e8d44bc71b9430d49229b61dd9e6a5b3fbda15ed8c0c76843154c9e","examples/sdps/15.sdp":"c96e468a51db1e205898867dcc95af0bd544676eceb4d930c24cdd4c8e8ce152","examples/sdps/16.sdp":"034c556ef8d0c9972c7b8f37e8b419f343e3ac993794e2f20c946a8ebd1cb781","examples/sdps/17.sdp":"b35628e255798817539420528950b05dd2369920d5c00a0279de25679056363b","examples/sdps/18.sdp":"d60f4530b98f6ab9fa68a4e1c7d899220f43e3596f5ed82b008d867d27d464c6","examples/sdps/19.sdp":"2929f883bc54cfd52d8a41e6dbe4ab46a307611ea6f974d22f6f81e2a6e861cc","examples/sdps/20.sdp":"f087414d44f4edc0238ce429a7fd9ee764180a11758c80dcb3f18ff596411144","examples/sdps/21.sdp":"50ee7dad5aa101cc6460d6136a173b894f8ec3b2304eb6b89739d794c2593858","examples/sdps/22.sdp":"60ba3631584333b07c9ba482b2681dfd5f47961485bd9869c0ce399ac78a28c0","examples/sdps/23.sdp":"e19f374de91927c54019b41681ab9116a781b3434151bbb9b1e333ba5ba15305","examples/sdps/24.sdp":"cf410817c7ac2c61c94b02ae24696da54c5439d182b919f0804aefa985b7dec6","examples/sdps/25.sdp":"0aba5390d78b948477f4116a3665f3b0214f1c7c8cb7d7308563ff7b66b3320c","examples/sdps/26.sdp":"02edf268aec6562db717d2e7e090eccccc336f37232cbd0c9848dc396cd1f68d","examples/sdps/27.sdp":"1950a83ff7311d75eaee8fc6a88f54034540add4a413b8c97daad258bc11510a","examples/sdps/28.sdp":"0bd3d1dad72087f90d67996d71a190c56701e8866f248406db8e183c6deee4cd","examples/sdps/29.sdp":"60a71fab3f0facef4e045f78eabb1ff4f1150393427f6720e1228e010edc2f26","examples/sdps/30.sdp":"52fb5119e4a18026b9fe0e10732e621dd13ace847e68ab51687cb633761eeabc","examples/sdps/31.sdp":"6080176767fc4188903b08f98a6fdbca2478fb923b79f31bb741d91e8cc1c4a5","examples/sdps/32.sdp":"b764977a4be5d3808260eaf93c2ec7c6aba7e4589996dd514e9d3bd8d3d02975","examples/sdps/33.sdp":"bd6367ad1abe795576c58c34be6bf5d2073db3343c0edb02a88cf314176fdc51","examples/sdps/34.sdp":"4dc1bbfdf292f2303ff3e6f26ee9db87164e57464d077e8e94c3c11490d4f839","examples/sdps/35.sdp":"4dc1bbfdf292f2303ff3e6f26ee9db87164e57464d077e8e94c3c11490d4f839","examples/sdps/36.sdp":"4dc1bbfdf292f2303ff3e6f26ee9db87164e57464d077e8e94c3c11490d4f839","examples/sdps/37.sdp":"64623a06b325f6219b645c2cf3072703443104ac68c8f02aeb1ac68107f13ab8","examples/sdps/38.sdp":"f59fd5295eebd62acb79708f5258ac4e92082f5725f50348ba31675c2bd9c3a9","examples/sdps/39.sdp":"e3ea9870d76f6257d27d1222f7b74a247a6556d5361479c64d2db11b5c6e2b8d","examples/sdps/40.sdp":"e3ea9870d76f6257d27d1222f7b74a247a6556d5361479c64d2db11b5c6e2b8d","examples/sdps/41.sdp":"186e94e19a6bd802dd787a1a251c39d903aa86dda4f22bb6bb7aeb7678587141","examples/sdps/extract.sh":"fa9daf98313f57ff7717841da9a5f2bf3e30394baded7c42023173b53a09e6d8","src/address.rs":"03e0fa68654f6b3c741d2dd6fee0c0607c888a86db4fcc4407527ade75cc5cd6","src/address_tests.rs":"341ab6d0ca557f44afee38b3ac0686376380c514791f515a3bb47a626b570146","src/anonymizer.rs":"8a1a7a4760739fd8a485c6fa239904f1e220a2b4d82560a236366f14f7cd09fa","src/anonymizer_tests.rs":"83010f8cc3eca078cb0ff56330e7209e4ea252cb1af1a08b6586114f313af048","src/attribute_type.rs":"911b861ce4d472ff8d141b179d1e11b33ef8a4bd25d3d68324f6705002c8a268","src/attribute_type_tests.rs":"2de570936176084ec2dd459372093b824332287edce7ad244ee84d0d94acb0de","src/error.rs":"e5126288a412c1c4af8682ab1524e9136e5d1dfe5fc769a13b0b5bd6b1f3c3b0","src/error_tests.rs":"3b2e62656d7e4ef0a6510308141c5eb5aaf0d2153c867e632bc4eeb4df81b9e0","src/lib.rs":"4ff763fcf29f822ae0ac6ace347b46958630055fd9c438dc43be05cc0d520fac","src/lib_tests.rs":"84529660be0e7fa0a27ed0d0fb296e8a781c1213bfb9dfa6308c207c66a5be6c","src/media_type.rs":"05130ef272827282033894b1e3d2c6784be8dcde42c74c397d0fb76fd8e2c40c","src/media_type_tests.rs":"b5924469c5637521042e86941871f5d297b35dda5909652c61b2954e8217bb06","src/network.rs":"2a4e31da138f3b167af00690b625be25355ed705197b57b0dc3832db118f0926","src/network_tests.rs":"305d6ac7a7e95e81f96541a6e83fdfa2ba9ca00398803946ecbc2cae24719a2c","tests/parse_sdp_tests.rs":"3996ee888e238bd4dff1271ed120644bb22ac9b7d14510257c35417223e562d3"},"package":"c7351fba122c7f6566779efdef49d2213e842f69fa1c654eef1fd9301f425064"} \ No newline at end of file
diff --git a/third_party/rust/webrtc-sdp/CHANGELOG.md b/third_party/rust/webrtc-sdp/CHANGELOG.md
new file mode 100644
index 0000000000..42a0cc5dc1
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/CHANGELOG.md
@@ -0,0 +1,86 @@
+# Changelog
+## [0.3.11] - 2024-01-17
+- Permit a wider set of payload type numbers
+## [0.3.10] - 2023-01-05
+- Permit inconsistent simulcast directions
+## [0.3.9] - 2022-01-12
+- Add support for RFC8858 rtcp-mux-only
+- Correct seperation of tokens in FMTP parameters
+- Do not emit an empty line after a media description
+## [0.3.8] - 2021-01-16
+- fmt numbers 35 to 63 are now usable for dynamic allocation
+- parse extmap-allow-mixed as per RFC 8285
+## [0.3.7] - 2020-11-23
+- Minimum Rust version >= 1.45
+- Added feature for parse object tree wide debug formatting, defaulted to on for now
+- Moved check for multiple c lines within an m section out of the setter and into the parsing logic, credit Mnwa
+## [0.3.6] - 2020-05-07
+- Added support for Opus FMTP parameters ptime, maxptime, minptime, and maxaveragebitrate
+## [0.3.5] - 2020-04-07
+### Fixed
+- RTX apt can now be zero
+## [0.3.4] - 2020-03-31
+### Fixed
+- Fixed new clippy warnings in stable
+- Accept a lack of c= lines if there are no m= lines (for JSEP compat.)
+### Changed
+- Added support for ssrc-group
+- Added support for RTX FMTP parameters
+- Example runner can no be told to expect failure
+## [0.3.3] - 2019-12-10
+### Changed
+- Changed handling of default channel counts
+
+## [0.3.2] - 2019-12-02
+### Changed
+- Fixed handling of spaces in fmtp attributes
+- Minimum Rust version >= 1.36
+
+## [0.3.1] - 2019-09-12
+### Changed
+- Updated `urls` dependency to `0.2.1`
+
+### Removed
+- Removed `TcpTlsRtpSavpf` protocl token
+- Removed dependency on `enum-display-derive`
+
+## [0.3.0] - 2019-08-08
+### Changed
+- Unsafe code is forbidden now
+
+### Fixed
+- Fixed panic from slicing unicode character in image attr braces
+
+### Added
+- Added support for FQDN addresses
+- Added support for parsing ice-pacing
+- Added fuzzing target
+
+## [0.2.2] - 2019-06-21
+### Changed
+ - Minimum Rust version >= 1.35
+
+## [0.2.0] - 2019-06-15
+### Changed
+- Minimum Rust version >= 1.30.0
+- Changed code coverage from kcov to tarpaulin
+- Moved file parser example to examples sub directory
+- Replaced cause() with source() in unit test
+- Moved all unit tests into tests modules
+
+### Fixed
+- Unknown extensions in candidate attributes (#103)
+- Reduced amount of internal clone() calls significantly
+- Added dyn to error:Error impl required by more recent rust versions
+
+### Added
+- Support for anonymization to enable logging of SDP without personal
+ information
+- Quite a bit more unit testing got added
+
+### Removed
+- Replaced unsupported types with errors directly in lib.rs
+
+## [0.1.0] - 2019-01-26
+- Initial release
+- Minimum Rust version >= 1.17.0
diff --git a/third_party/rust/webrtc-sdp/CODE_OF_CONDUCT.md b/third_party/rust/webrtc-sdp/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..50e26b1c03
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/CODE_OF_CONDUCT.md
@@ -0,0 +1,8 @@
+# Community Participation Guidelines
+
+This repository is governed by Mozilla's code of conduct and etiquette guidelines.
+For more details, please read the
+[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
+
+## How to Report
+For more information on how to report violations of the Community Participation Guidelines, please read our [How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/) page.
diff --git a/third_party/rust/webrtc-sdp/Cargo.lock b/third_party/rust/webrtc-sdp/Cargo.lock
new file mode 100644
index 0000000000..f133c1f3fe
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/Cargo.lock
@@ -0,0 +1,161 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "idna"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "log"
+version = "0.4.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "serde"
+version = "1.0.173"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91f70896d6720bc714a4a57d22fc91f1db634680e65c8efe13323f1fa38d53f"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.173"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6250dde8342e0232232be9ca3db7aa40aceb5a3e5dd9bddbc00d99a007cde49"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "url"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "webrtc-sdp"
+version = "0.3.11"
+dependencies = [
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+]
diff --git a/third_party/rust/webrtc-sdp/Cargo.toml b/third_party/rust/webrtc-sdp/Cargo.toml
new file mode 100644
index 0000000000..a84ed53bb5
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/Cargo.toml
@@ -0,0 +1,65 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+name = "webrtc-sdp"
+version = "0.3.11"
+authors = [
+ "Nicolas Grunbaum <na-g+github@nostrum.com>",
+ "Nils Ohlmeier <github@ohlmeier.org>",
+]
+description = "webrtc-sdp parses strings in the format of the Session Description Protocol according to RFC4566. It specifically supports the subset of features required to support WebRTC according to the JSEP draft."
+readme = "README.md"
+keywords = [
+ "webrtc",
+ "sdp",
+ "jsep",
+]
+categories = [
+ "parsing",
+ "network-programming",
+]
+license = "MPL-2.0"
+repository = "https://github.com/mozilla/webrtc-sdp"
+
+[dependencies.log]
+version = "0.4"
+
+[dependencies.serde]
+version = "1.0"
+optional = true
+
+[dependencies.serde_derive]
+version = "1.0"
+optional = true
+
+[dependencies.url]
+version = "2.1.0"
+
+[dev-dependencies.serde_json]
+version = "1.0"
+
+[features]
+default = ["enhanced_debug"]
+enhanced_debug = []
+serialize = [
+ "serde",
+ "serde_derive",
+]
+
+[badges.codecov]
+branch = "master"
+repository = "mozilla/webrtc-sdp"
+service = "github"
+
+[badges.travis-ci]
+branch = "master"
+repository = "mozilla/webrtc-sdp"
diff --git a/third_party/rust/webrtc-sdp/LICENSE b/third_party/rust/webrtc-sdp/LICENSE
new file mode 100644
index 0000000000..a612ad9813
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/third_party/rust/webrtc-sdp/README.md b/third_party/rust/webrtc-sdp/README.md
new file mode 100644
index 0000000000..6c679ed3e2
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/README.md
@@ -0,0 +1,73 @@
+# webrtc-sdp
+
+[![Crates.io](https://img.shields.io/crates/v/webrtc-sdp.svg)](https://crates.io/crates/webrtc-sdp)
+[![Build Status](https://travis-ci.org/mozilla/webrtc-sdp.svg?branch=master)](https://travis-ci.org/mozilla/webrtc-sdp)
+[![Codecov coverage status](https://codecov.io/gh/mozilla/webrtc-sdp/branch/master/graph/badge.svg)](https://codecov.io/gh/webrtc-sdp/webrtc-sdp)
+[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](#License)
+[![dependency status](https://deps.rs/repo/github/mozilla/webrtc-sdp/status.svg)](https://deps.rs/repo/github/mozilla/webrtc-sdp)
+
+A SDP parser written in Rust specifically aimed to handle WebRTC SDP offers and answers.
+
+## Dependecies
+
+* Rust >= 1.60.0
+* log module
+* serde module
+* serde-derive module
+
+Cargo installs the missing modules automatically when building webrtc-sdp for the first time.
+
+## The webrtc-sdp API
+
+The main function is:
+```rust
+fn parse_sdp(sdp: &str, fail_on_warning: bool) -> Result<SdpSession, SdpParserError>
+```
+The `sdp` parameter is the string which will get parsed. The `fail_on_warning` parameter determines how to treat warnings encountered during parsing. Any problems encountered during are stored until the whole string has been parsed. Any problem during parsing falls into two catgeories:
+
+* Fatal error preventing further parsing or processing of the SDP
+* Warning which don't block further processing of the SDP
+
+Warnings will be for example unknown parameters in attributes. Setting `fail_on_warning` to `true` makes most sense during development, when you want to be aware of all potential problems. In production `fail_on_warning` is expected to be `false`.
+
+`parse_sdp()` returns either an `SdpSession` struct ([code](https://github.com/mozilla/webrtc-sdp/blob/master/src/lib.rs#L137)) which contains all the parsed information. Or in case a fatal error was encountered (or if `fail_on_warning` was set to `true` and any warnings were encountered) an `SdpParserError` ([code](https://github.com/mozilla/webrtc-sdp/blob/master/src/error.rs#L117)) will be returned as a `Result`.
+
+## Examples
+
+The [file parser](https://github.com/mozilla/webrtc-sdp/blob/master/examples/file_parser.rs) in the webrtc-sdp package gives you an easy example of how to invoke the webrtc-sdp parser.
+
+## Contributing
+
+As the Travis CI runs are checking for code formating and clippy warnings please run the following commands locally, before submitting a Pull Request.
+
+If you haven't clippy and Rust format installed already you add them like this:
+```
+rustup component add rustfmt-preview
+rustup component add clippy
+```
+
+Check with clippy for warnings in the code:
+```
+cargo clippy
+```
+
+And format all of the code according to Rust code style convention:
+```
+cargo fmt --all
+```
+
+## Fuzzing
+
+Install cargo-fuzz like this:
+```
+cargo install cargo-fuzz
+```
+
+With rust nightly you can start fuzzing like this:
+```
+cargo fuzz run fuzz_target_parse_sdp
+```
+
+## License
+
+Licensed under [MPL-2.0](https://www.mozilla.org/MPL/2.0/)
diff --git a/third_party/rust/webrtc-sdp/examples/file_parser.rs b/third_party/rust/webrtc-sdp/examples/file_parser.rs
new file mode 100644
index 0000000000..5997e8a770
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/file_parser.rs
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::fs::File;
+use std::io::prelude::*;
+use std::panic;
+use std::path::Path;
+extern crate webrtc_sdp;
+
+// Takes the filename of a file that contains SDP, and optionally the trailing
+// flag --expect-failure.
+// If --expect-failure is passed, then the program will exit with a successful
+// exit code if the file fails to parse.
+fn main() {
+ let mut args = std::env::args();
+ let filename = match args.nth(1) {
+ None => {
+ eprintln!("Missing file name argument!");
+ std::process::exit(1);
+ }
+ Some(x) => x,
+ };
+
+ let path = Path::new(filename.as_str());
+ let display = path.display();
+
+ let mut file = match File::open(path) {
+ Err(why) => panic!("Failed to open {}: {}", display, why),
+ Ok(file) => file,
+ };
+
+ let mut s = String::new();
+ match file.read_to_string(&mut s) {
+ Err(why) => panic!("Couldn't read {}: {}", display, why),
+ Ok(s) => s,
+ };
+
+ // Hook up the panic handler if it is expected to fail to parse
+ let expect_failure = if let Some(x) = args.next() {
+ if x.to_lowercase() != "--expect-failure" {
+ eprintln!("Extra arguments passed!");
+ std::process::exit(1);
+ }
+ panic::set_hook(Box::new(|_| {
+ println!("Exited with failure, as expected.");
+ std::process::exit(0);
+ }));
+ true
+ } else {
+ false
+ };
+
+ // Remove comment lines
+ let s = s
+ .lines()
+ .filter(|&l| !l.trim_start().starts_with(';'))
+ .collect::<Vec<&str>>()
+ .join("\r\n");
+
+ let res = webrtc_sdp::parse_sdp(&s, true);
+ match res {
+ Err(why) => panic!("Failed to parse SDP with error: {}", why),
+ Ok(sdp) => println!("Parsed SDP structure:\n{sdp:#?}"),
+ }
+
+ if expect_failure {
+ eprintln!("Successfully parsed SDP that was expected to fail. You may need to update the example expectations.");
+ std::process::exit(1);
+ }
+ println!("Successfully parsed SDP");
+}
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/02.sdp b/third_party/rust/webrtc-sdp/examples/sdps/02.sdp
new file mode 100644
index 0000000000..21d383d2f2
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/02.sdp
@@ -0,0 +1,7 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/03.sdp b/third_party/rust/webrtc-sdp/examples/sdps/03.sdp
new file mode 100644
index 0000000000..ef0a5efd90
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/03.sdp
@@ -0,0 +1,38 @@
+; Many RTCP-FB feed back types take a parameter
+; Most of these will not parse.
+; One can experiment by commenting out individual lines.
+; Note: comment lines only work in the example parser.
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
+a=rtpmap:122 red/90000
+a=rtcp-fb:120 ack rpsi
+a=rtcp-fb:120 ack app
+a=rtcp-fb:120 ack app foo
+a=rtcp-fb:120 ack foo bar
+a=rtcp-fb:120 ack foo bar baz
+a=rtcp-fb:120 nack
+a=rtcp-fb:120 nack pli
+a=rtcp-fb:120 nack sli
+a=rtcp-fb:120 nack rpsi
+a=rtcp-fb:120 nack app
+a=rtcp-fb:120 nack app foo
+a=rtcp-fb:120 nack app foo bar
+a=rtcp-fb:120 nack foo bar baz
+a=rtcp-fb:120 trr-int 0
+a=rtcp-fb:120 trr-int 123
+a=rtcp-fb:120 goog-remb
+a=rtcp-fb:120 ccm fir
+a=rtcp-fb:120 ccm tmmbr
+a=rtcp-fb:120 ccm tstr
+a=rtcp-fb:120 ccm vbcm 123 456 789
+a=rtcp-fb:120 ccm foo
+a=rtcp-fb:120 ccm foo bar baz
+a=rtcp-fb:120 foo
+a=rtcp-fb:120 foo bar
+a=rtcp-fb:120 foo bar baz
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/04.sdp b/third_party/rust/webrtc-sdp/examples/sdps/04.sdp
new file mode 100644
index 0000000000..0682623c93
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/04.sdp
@@ -0,0 +1,7 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+t=0 0
+m=video 56436 RTP/SAVPF 120
+c=IN IP4 198.51.100.7
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/05.sdp b/third_party/rust/webrtc-sdp/examples/sdps/05.sdp
new file mode 100644
index 0000000000..043046edb5
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/05.sdp
@@ -0,0 +1,6 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=ice-lite
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/06.sdp b/third_party/rust/webrtc-sdp/examples/sdps/06.sdp
new file mode 100644
index 0000000000..d20cb3fc70
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/06.sdp
@@ -0,0 +1,12 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+b=CT:5000
+b=FOOBAR:10
+b=AS:4
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
+m=audio 12345/2 RTP/SAVPF 0
+a=rtpmap:0 PCMU/8000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/07.sdp b/third_party/rust/webrtc-sdp/examples/sdps/07.sdp
new file mode 100644
index 0000000000..882adacb1a
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/07.sdp
@@ -0,0 +1,8 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=
+c=IN IP4 198.51.100.7
+t=0 0
+m=video 56436 RTP/SAVPF 120
+b=CT:1000
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/08.sdp b/third_party/rust/webrtc-sdp/examples/sdps/08.sdp
new file mode 100644
index 0000000000..5175638594
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/08.sdp
@@ -0,0 +1,87 @@
+;
+; This example fails to parse because of a mismatched fingerprint length
+;
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=ice-ufrag:4a799b2e
+a=ice-pwd:e4cc12a910f106a0a744719425510e17
+a=ice-lite
+a=ice-options:trickle foo
+a=msid-semantic:WMS stream streama
+a=msid-semantic:foo stream
+a=fingerprint:sha-256 DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C
+a=identity:eyJpZHAiOnsiZG9tYWluIjoiZXhhbXBsZS5vcmciLCJwcm90b2NvbCI6ImJvZ3VzIn0sImFzc2VydGlvbiI6IntcImlkZW50aXR5XCI6XCJib2JAZXhhbXBsZS5vcmdcIixcImNvbnRlbnRzXCI6XCJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3l6XCIsXCJzaWduYXR1cmVcIjpcIjAxMDIwMzA0MDUwNlwifSJ9
+a=group:BUNDLE first second
+a=group:BUNDLE third
+a=group:LS first third
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=mid:first
+a=rtpmap:109 opus/48000/2
+a=ptime:20
+a=maxptime:20
+a=rtpmap:9 G722/8000
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:101 telephone-event/8000
+a=fmtp:101 0-15,66,32-34,67
+a=ice-ufrag:00000000
+a=ice-pwd:0000000000000000000000000000000
+a=sendonly
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=setup:actpass
+a=rtcp-mux
+a=msid:stream track
+a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host
+a=candidate:2 1 UDP 1694236671 24.6.134.204 62453 typ srflx raddr 10.0.0.36 rport 62453
+a=candidate:3 1 UDP 100401151 162.222.183.171 49761 typ relay raddr 162.222.183.171 rport 49761
+a=candidate:6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr 162.222.183.171 rport 51858
+a=candidate:3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr 162.222.183.171 rport 62454
+a=candidate:2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr 10.0.0.36 rport 55428
+a=candidate:6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr 162.222.183.171 rport 50340
+a=candidate:0 2 UDP 2130379006 10.0.0.36 55428 typ host
+a=rtcp:62454 IN IP4 162.222.183.171
+a=end-of-candidates
+a=ssrc:5150
+m=video 9 RTP/SAVPF 120 121 122 123
+c=IN IP6 ::1
+a=fingerprint:sha-1 DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7
+a=mid:second
+a=rtpmap:120 VP8/90000
+a=rtpmap:121 VP9/90000
+a=rtpmap:122 red/90000
+a=rtpmap:123 ulpfec/90000
+a=recvonly
+a=rtcp-fb:120 nack
+a=rtcp-fb:120 nack pli
+a=rtcp-fb:120 ccm fir
+a=rtcp-fb:121 nack
+a=rtcp-fb:121 nack pli
+a=rtcp-fb:121 ccm fir
+a=setup:active
+a=rtcp-mux
+a=msid:streama tracka
+a=msid:streamb trackb
+a=candidate:0 1 UDP 2130379007 10.0.0.36 59530 typ host
+a=candidate:0 2 UDP 2130379006 10.0.0.36 64378 typ host
+a=candidate:2 2 UDP 1694236670 24.6.134.204 64378 typ srflx raddr 10.0.0.36 rport 64378
+a=candidate:6 2 UDP 16515070 162.222.183.171 64941 typ relay raddr 162.222.183.171 rport 64941
+a=candidate:6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr 162.222.183.171 rport 64800
+a=candidate:2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr 10.0.0.36 rport 59530
+a=candidate:3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr 162.222.183.171 rport 62935
+a=candidate:3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr 162.222.183.171 rport 61026
+a=rtcp:61026
+a=end-of-candidates
+a=ssrc:1111 foo
+a=ssrc:1111 foo:bar
+a=imageattr:120 send * recv *
+m=audio 9 RTP/SAVPF 0
+a=mid:third
+a=rtpmap:0 PCMU/8000
+a=ice-lite
+a=ice-options:foo bar
+a=msid:noappdata
+a=bundle-only
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/09.sdp b/third_party/rust/webrtc-sdp/examples/sdps/09.sdp
new file mode 100644
index 0000000000..f5c4acdbd8
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/09.sdp
@@ -0,0 +1,34 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+t=0 0
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=mid:first
+a=rtpmap:109 opus/48000/2
+a=ptime:20
+a=maxptime:20
+a=rtpmap:9 G722/8000
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:101 telephone-event/8000
+a=fmtp:101 0-15
+a=fmtp:101 0-5.
+a=fmtp:101 0-15,66,67
+a=fmtp:101 0,1,2-4,5-15,66,67
+a=fmtp:101 5,6,7
+a=fmtp:101 0
+a=fmtp:101 1
+a=fmtp:101 123
+a=fmtp:101 0-123
+a=fmtp:101 -12
+a=fmtp:101 12-
+a=fmtp:101 1,12-,4
+a=fmtp:101 ,2,3
+a=fmtp:101 ,,,2,3
+a=fmtp:101 1,,,,,,,,3
+a=fmtp:101 1,2,3,
+a=fmtp:101 1-2-3
+a=fmtp:101 112233
+a=fmtp:101 33-2
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/10.sdp b/third_party/rust/webrtc-sdp/examples/sdps/10.sdp
new file mode 100644
index 0000000000..783c0d0048
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/10.sdp
@@ -0,0 +1,13 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+t=0 0
+m=video 9 RTP/SAVPF 97 120 121 122 123
+c=IN IP6 ::1
+a=fingerprint:sha-1 DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C
+a=rtpmap:97 H264/90000
+a=rtpmap:120 VP8/90000
+a=rtpmap:121 VP9/90000
+a=rtpmap:122 red/90000
+a=rtpmap:123 ulpfec/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/11.sdp b/third_party/rust/webrtc-sdp/examples/sdps/11.sdp
new file mode 100644
index 0000000000..a138e09ff0
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/11.sdp
@@ -0,0 +1,66 @@
+;
+; This example fails to parse because a=ice-lite is session only
+;
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=ice-ufrag:4a799b2e
+a=ice-pwd:e4cc12a910f106a0a744719425510e17
+a=ice-lite
+a=msid-semantic:WMS stream streama
+a=fingerprint:sha-256 DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C
+a=group:BUNDLE first second
+a=group:BUNDLE third
+a=group:LS first third
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=mid:first
+a=rtpmap:109 opus/48000/2
+a=ptime:20
+a=maxptime:20
+a=rtpmap:9 G722/8000
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:101 telephone-event/8000
+a=fmtp:101 0-15,66,32-34,67
+a=ice-ufrag:00000000
+a=ice-pwd:0000000000000000000000000000000
+a=sendonly
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=setup:actpass
+a=rtcp-mux
+a=msid:stream track
+a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host
+a=candidate:2 1 UDP 1694236671 24.6.134.204 62453 typ srflx raddr 10.0.0.36 rport 62453
+a=candidate:3 1 UDP 100401151 162.222.183.171 49761 typ relay raddr 162.222.183.171 rport 49761
+a=candidate:6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr 162.222.183.171 rport 51858
+a=candidate:3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr 162.222.183.171 rport 62454
+a=candidate:2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr 10.0.0.36 rport 55428
+a=candidate:6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr 162.222.183.171 rport 50340
+a=candidate:0 2 UDP 2130379006 10.0.0.36 55428 typ host
+m=video 9 RTP/SAVPF 97 98 120
+c=IN IP6 ::1
+a=mid:second
+a=rtpmap:97 H264/90000
+a=rtpmap:98 H264/90000
+a=rtpmap:120 VP8/90000
+a=recvonly
+a=setup:active
+a=rtcp-mux
+a=msid:streama tracka
+a=msid:streamb trackb
+a=candidate:0 1 UDP 2130379007 10.0.0.36 59530 typ host
+a=candidate:0 2 UDP 2130379006 10.0.0.36 64378 typ host
+a=candidate:2 2 UDP 1694236670 24.6.134.204 64378 typ srflx raddr 10.0.0.36 rport 64378
+a=candidate:6 2 UDP 16515070 162.222.183.171 64941 typ relay raddr 162.222.183.171 rport 64941
+a=candidate:6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr 162.222.183.171 rport 64800
+a=candidate:2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr 10.0.0.36 rport 59530
+a=candidate:3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr 162.222.183.171 rport 62935
+a=candidate:3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr 162.222.183.171 rport 61026
+m=audio 9 RTP/SAVPF 0
+a=mid:third
+a=rtpmap:0 PCMU/8000
+a=ice-lite
+a=msid:noappdata
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/12.sdp b/third_party/rust/webrtc-sdp/examples/sdps/12.sdp
new file mode 100644
index 0000000000..647de5dca8
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/12.sdp
@@ -0,0 +1,58 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 27987 0 IN IP4 0.0.0.0
+s=SIP Call
+t=0 0
+a=ice-ufrag:8a39d2ae
+a=ice-pwd:601d53aba51a318351b3ecf5ee00048f
+a=fingerprint:sha-256 30:FF:8E:2B:AC:9D:ED:70:18:10:67:C8:AE:9E:68:F3:86:53:51:B0:AC:31:B7:BE:6D:CF:A4:2E:D3:6E:B4:28
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=rtpmap:109 opus/48000/2
+a=ptime:20
+a=rtpmap:9 G722/8000
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:101 telephone-event/8000
+a=fmtp:101 0-15
+a=sendrecv
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=extmap:2/sendonly some_extension
+a=extmap:3 some_other_extension some_params some more params
+a=setup:actpass
+a=rtcp-mux
+m=video 9 RTP/SAVPF 120 126 97
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
+a=rtpmap:126 H264/90000
+a=rtpmap:97 H264/90000
+a=sendrecv
+a=rtcp-fb:120 ack rpsi
+a=rtcp-fb:120 ack app foo
+a=rtcp-fb:120 ack foo
+a=rtcp-fb:120 nack
+a=rtcp-fb:120 nack sli
+a=rtcp-fb:120 nack pli
+a=rtcp-fb:120 nack rpsi
+a=rtcp-fb:120 nack app foo
+a=rtcp-fb:120 nack foo
+a=rtcp-fb:120 ccm fir
+a=rtcp-fb:120 ccm tmmbr
+a=rtcp-fb:120 ccm tstr
+a=rtcp-fb:120 ccm vbcm
+a=rtcp-fb:120 ccm foo
+a=rtcp-fb:120 trr-int 10
+a=rtcp-fb:120 goog-remb
+a=rtcp-fb:120 foo
+a=rtcp-fb:126 nack
+a=rtcp-fb:126 nack pli
+a=rtcp-fb:126 ccm fir
+a=rtcp-fb:97 nack
+a=rtcp-fb:97 nack pli
+a=rtcp-fb:97 ccm fir
+a=rtcp-fb:* ccm tmmbr
+a=setup:actpass
+a=rtcp-mux
+m=application 9 DTLS/SCTP 5000
+c=IN IP4 0.0.0.0
+a=sctpmap:5000 webrtc-datachannel 16
+a=setup:actpass
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/13.sdp b/third_party/rust/webrtc-sdp/examples/sdps/13.sdp
new file mode 100644
index 0000000000..31c3d8d86b
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/13.sdp
@@ -0,0 +1,12 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 27987 0 IN IP4 0.0.0.0
+s=SIP Call
+t=0 0
+a=ice-ufrag:8a39d2ae
+a=ice-pwd:601d53aba51a318351b3ecf5ee00048f
+a=fingerprint:sha-256 30:FF:8E:2B:AC:9D:ED:70:18:10:67:C8:AE:9E:68:F3:86:53:51:B0:AC:31:B7:BE:6D:CF:A4:2E:D3:6E:B4:28
+m=application 9 UDP/DTLS/SCTP webrtc-datachannel
+c=IN IP4 0.0.0.0
+a=sctp-port:5000
+a=max-message-size:10000
+a=setup:actpass
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/14.sdp b/third_party/rust/webrtc-sdp/examples/sdps/14.sdp
new file mode 100644
index 0000000000..cad2a9a199
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/14.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=rtpmap:109 opus/48000/2
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/15.sdp b/third_party/rust/webrtc-sdp/examples/sdps/15.sdp
new file mode 100644
index 0000000000..d356ecaf1f
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/15.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=bundle-only
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=rtpmap:109 opus/48000/2
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/16.sdp b/third_party/rust/webrtc-sdp/examples/sdps/16.sdp
new file mode 100644
index 0000000000..67326a6e9a
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/16.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=fmtp:109 0-15
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=rtpmap:109 opus/48000/2
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/17.sdp b/third_party/rust/webrtc-sdp/examples/sdps/17.sdp
new file mode 100644
index 0000000000..0023222191
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/17.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=ice-mismatch
+m=audio 9 RTP/SAVPF 109 9 0 8 101
+c=IN IP4 0.0.0.0
+a=rtpmap:109 opus/48000/2
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/18.sdp b/third_party/rust/webrtc-sdp/examples/sdps/18.sdp
new file mode 100644
index 0000000000..3287af6413
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/18.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=imageattr:120 send * recv *
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/19.sdp b/third_party/rust/webrtc-sdp/examples/sdps/19.sdp
new file mode 100644
index 0000000000..20bb0a8058
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/19.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=label:foobar
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/20.sdp b/third_party/rust/webrtc-sdp/examples/sdps/20.sdp
new file mode 100644
index 0000000000..4d28e12a9f
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/20.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=maxptime:100
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/21.sdp b/third_party/rust/webrtc-sdp/examples/sdps/21.sdp
new file mode 100644
index 0000000000..7a2d6fa9dc
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/21.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=mid:foobar
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/22.sdp b/third_party/rust/webrtc-sdp/examples/sdps/22.sdp
new file mode 100644
index 0000000000..95e5518123
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/22.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=msid:foobar
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/23.sdp b/third_party/rust/webrtc-sdp/examples/sdps/23.sdp
new file mode 100644
index 0000000000..7722360f32
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/23.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=ptime:50
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/24.sdp b/third_party/rust/webrtc-sdp/examples/sdps/24.sdp
new file mode 100644
index 0000000000..dd13f1e143
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/24.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=remote-candidates:0 10.0.0.1 5555
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/25.sdp b/third_party/rust/webrtc-sdp/examples/sdps/25.sdp
new file mode 100644
index 0000000000..7fd8a006fb
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/25.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=rtcp:5555
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/26.sdp b/third_party/rust/webrtc-sdp/examples/sdps/26.sdp
new file mode 100644
index 0000000000..789e062766
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/26.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=rtcp-fb:120 nack
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/27.sdp b/third_party/rust/webrtc-sdp/examples/sdps/27.sdp
new file mode 100644
index 0000000000..f8201916e9
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/27.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=rtcp-mux
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/28.sdp b/third_party/rust/webrtc-sdp/examples/sdps/28.sdp
new file mode 100644
index 0000000000..11d9331b10
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/28.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=rtcp-rsize
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/29.sdp b/third_party/rust/webrtc-sdp/examples/sdps/29.sdp
new file mode 100644
index 0000000000..f34b97fe58
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/29.sdp
@@ -0,0 +1,8 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=rtpmap:120 VP8/90000
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/30.sdp b/third_party/rust/webrtc-sdp/examples/sdps/30.sdp
new file mode 100644
index 0000000000..202ea88c08
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/30.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=sctpmap:5000
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/31.sdp b/third_party/rust/webrtc-sdp/examples/sdps/31.sdp
new file mode 100644
index 0000000000..cd510e1d46
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/31.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=ssrc:5000
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/32.sdp b/third_party/rust/webrtc-sdp/examples/sdps/32.sdp
new file mode 100644
index 0000000000..4569694768
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/32.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+a=ssrc-group:FID 5000
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/33.sdp b/third_party/rust/webrtc-sdp/examples/sdps/33.sdp
new file mode 100644
index 0000000000..179e048da8
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/33.sdp
@@ -0,0 +1,9 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
+a=imageattr:flob
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/34.sdp b/third_party/rust/webrtc-sdp/examples/sdps/34.sdp
new file mode 100644
index 0000000000..39ef01206d
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/34.sdp
@@ -0,0 +1,9 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+b=CT:5000
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
+a=sendrecv
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/35.sdp b/third_party/rust/webrtc-sdp/examples/sdps/35.sdp
new file mode 100644
index 0000000000..39ef01206d
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/35.sdp
@@ -0,0 +1,9 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+b=CT:5000
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
+a=sendrecv
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/36.sdp b/third_party/rust/webrtc-sdp/examples/sdps/36.sdp
new file mode 100644
index 0000000000..39ef01206d
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/36.sdp
@@ -0,0 +1,9 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+b=CT:5000
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
+a=sendrecv
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/37.sdp b/third_party/rust/webrtc-sdp/examples/sdps/37.sdp
new file mode 100644
index 0000000000..f03bbaa8ef
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/37.sdp
@@ -0,0 +1,9 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+b=CT:5000
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
+a=recvonly
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/38.sdp b/third_party/rust/webrtc-sdp/examples/sdps/38.sdp
new file mode 100644
index 0000000000..bbe385d984
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/38.sdp
@@ -0,0 +1,9 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+c=IN IP4 198.51.100.7
+b=CT:5000
+t=0 0
+m=video 56436 RTP/SAVPF 120
+a=rtpmap:120 VP8/90000
+a=sendonly
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/39.sdp b/third_party/rust/webrtc-sdp/examples/sdps/39.sdp
new file mode 100644
index 0000000000..723d2abf7f
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/39.sdp
@@ -0,0 +1,8 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/40.sdp b/third_party/rust/webrtc-sdp/examples/sdps/40.sdp
new file mode 100644
index 0000000000..723d2abf7f
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/40.sdp
@@ -0,0 +1,8 @@
+v=0
+o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0
+s=SIP Call
+c=IN IP4 224.0.0.1/100/12
+t=0 0
+m=video 9 RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=rtpmap:120 VP8/90000
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/41.sdp b/third_party/rust/webrtc-sdp/examples/sdps/41.sdp
new file mode 100644
index 0000000000..753ca5ccb4
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/41.sdp
@@ -0,0 +1,91 @@
+v=0
+o=- 1109973417102828257 2 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE audio video
+a=msid-semantic: WMS 1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP
+m=audio 32952 UDP/TLS/RTP/SAVPF 111 103 104 0 8 107 106 105 13 126
+c=IN IP4 128.64.32.16
+a=rtcp:32952 IN IP4 128.64.32.16
+a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host generation 0
+a=candidate:77142221 2 udp 2113937151 192.168.137.1 54081 typ host generation 0
+a=candidate:983072742 1 udp 2113937151 172.22.0.56 54082 typ host generation 0
+a=candidate:983072742 2 udp 2113937151 172.22.0.56 54082 typ host generation 0
+a=candidate:2245074553 1 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0
+a=candidate:2245074553 2 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0
+a=candidate:2479353907 1 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0
+a=candidate:2479353907 2 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0
+a=candidate:1243276349 1 tcp 1509957375 192.168.137.1 0 typ host generation 0
+a=candidate:1243276349 2 tcp 1509957375 192.168.137.1 0 typ host generation 0
+a=candidate:1947960086 1 tcp 1509957375 172.22.0.56 0 typ host generation 0
+a=candidate:1947960086 2 tcp 1509957375 172.22.0.56 0 typ host generation 0
+a=candidate:1808221584 1 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0
+a=candidate:1808221584 2 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0
+a=candidate:507872740 1 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0
+a=candidate:507872740 2 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0
+a=ice-ufrag:xQuJwjX3V3eMA81k
+a=ice-pwd:ZUiRmjS2GDhG140p73dAsSVP
+a=ice-options:google-ice
+a=fingerprint:sha-256 59:4A:8B:73:A7:73:53:71:88:D7:4D:58:28:0C:79:72:31:29:9B:05:37:DD:58:43:C2:D4:85:A2:B3:66:38:7A
+a=setup:active
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
+a=sendrecv
+a=mid:audio
+a=rtcp-mux
+a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:/U44g3ULdtapeiSg+T3n6dDLBKIjpOhb/NXAL/2b
+a=rtpmap:111 opus/48000/2
+a=rtpmap:103 ISAC/16000
+a=rtpmap:104 ISAC/32000
+a=rtpmap:0 PCMU/8000
+a=rtpmap:8 PCMA/8000
+a=rtpmap:107 CN/48000
+a=rtpmap:106 CN/32000
+a=rtpmap:105 CN/16000
+a=rtpmap:13 CN/8000
+a=rtpmap:126 telephone-event/8000
+a=maxptime:60
+a=ssrc:2271517329 cname:mKDNt7SQf6pwDlIn
+a=ssrc:2271517329 msid:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP 1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPa0
+a=ssrc:2271517329 mslabel:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP
+a=ssrc:2271517329 label:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPa0
+m=video 32952 UDP/TLS/RTP/SAVPF 100 116 117
+c=IN IP4 128.64.32.16
+a=rtcp:32952 IN IP4 128.64.32.16
+a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host generation 0
+a=candidate:77142221 2 udp 2113937151 192.168.137.1 54081 typ host generation 0
+a=candidate:983072742 1 udp 2113937151 172.22.0.56 54082 typ host generation 0
+a=candidate:983072742 2 udp 2113937151 172.22.0.56 54082 typ host generation 0
+a=candidate:2245074553 1 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0
+a=candidate:2245074553 2 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0
+a=candidate:2479353907 1 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0
+a=candidate:2479353907 2 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0
+a=candidate:1243276349 1 tcp 1509957375 192.168.137.1 0 typ host generation 0
+a=candidate:1243276349 2 tcp 1509957375 192.168.137.1 0 typ host generation 0
+a=candidate:1947960086 1 tcp 1509957375 172.22.0.56 0 typ host generation 0
+a=candidate:1947960086 2 tcp 1509957375 172.22.0.56 0 typ host generation 0
+a=candidate:1808221584 1 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0
+a=candidate:1808221584 2 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0
+a=candidate:507872740 1 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0
+a=candidate:507872740 2 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0
+a=ice-ufrag:xQuJwjX3V3eMA81k
+a=ice-pwd:ZUiRmjS2GDhG140p73dAsSVP
+a=ice-options:google-ice
+a=fingerprint:sha-256 59:4A:8B:73:A7:73:53:71:88:D7:4D:58:28:0C:79:72:31:29:9B:05:37:DD:58:43:C2:D4:85:A2:B3:66:38:7A
+a=setup:active
+a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
+a=sendrecv
+a=mid:video
+a=rtcp-mux
+a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:/U44g3ULdtapeiSg+T3n6dDLBKIjpOhb/NXAL/2b
+a=rtpmap:100 VP8/90000
+a=rtcp-fb:100 ccm fir
+a=rtcp-fb:100 nack
+a=rtcp-fb:100 goog-remb
+a=rtpmap:116 red/90000
+a=rtpmap:117 ulpfec/90000
+a=ssrc:54724160 cname:mKDNt7SQf6pwDlIn
+a=ssrc:54724160 msid:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP 1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPv0
+a=ssrc:54724160 mslabel:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP
+a=ssrc:54724160 label:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPv0
+
diff --git a/third_party/rust/webrtc-sdp/examples/sdps/extract.sh b/third_party/rust/webrtc-sdp/examples/sdps/extract.sh
new file mode 100755
index 0000000000..60f1b1c763
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/examples/sdps/extract.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+grep '\"[ a-z]=[^=]*$' sdp_unittests.cpp | grep -v 'ParseSdp(kVideoSdp' | grep -v 'kVideoWithRedAndUlpfec' | grep -v 'ASSERT_NE' | grep -v 'BASE64_DTLS_HELLO' | grep -v '^\/\/' | sed 's/ParseSdp(//g' | sed 's/^[[:space:]]*//' | sed 's/+ //' | sed 's/ \/\/.*$//' | sed 's/\;$//' | sed 's/)$//' | sed 's/, false//' | sed 's/" CRLF//' | sed 's/^\"//' | sed 's/\"$//' | sed 's/\\r\\n//' | gawk -v RS='(^|\n)v=' '/./ { print "v="$0 > NR".sdp" }'
diff --git a/third_party/rust/webrtc-sdp/src/address.rs b/third_party/rust/webrtc-sdp/src/address.rs
new file mode 100644
index 0000000000..cc1087499a
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/address.rs
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate url;
+use self::url::Host;
+use error::SdpParserInternalError;
+use std::convert::TryFrom;
+use std::fmt;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use std::str::FromStr;
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum Address {
+ Fqdn(String),
+ Ip(IpAddr),
+}
+
+impl fmt::Display for Address {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Address::Fqdn(fqdn) => fqdn.fmt(f),
+ Address::Ip(ip) => ip.fmt(f),
+ }
+ }
+}
+
+impl FromStr for Address {
+ type Err = SdpParserInternalError;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut e: Option<SdpParserInternalError> = None;
+ if s.find(':').is_some() {
+ match IpAddr::from_str(s) {
+ Ok(ip) => return Ok(Address::Ip(ip)),
+ Err(err) => e = Some(err.into()),
+ }
+ }
+ Host::parse(s)
+ .map(|host| match host {
+ Host::Domain(s) => Address::Fqdn(s),
+ Host::Ipv4(ip) => Address::Ip(IpAddr::V4(ip)),
+ Host::Ipv6(ip) => Address::Ip(IpAddr::V6(ip)),
+ })
+ .map_err(|err| e.unwrap_or_else(|| err.into()))
+ }
+}
+
+impl From<ExplicitlyTypedAddress> for Address {
+ fn from(item: ExplicitlyTypedAddress) -> Self {
+ match item {
+ ExplicitlyTypedAddress::Fqdn { domain, .. } => Address::Fqdn(domain),
+ ExplicitlyTypedAddress::Ip(ip) => Address::Ip(ip),
+ }
+ }
+}
+
+impl PartialEq for Address {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (Address::Fqdn(a), Address::Fqdn(b)) => a.to_lowercase() == b.to_lowercase(),
+ (Address::Ip(a), Address::Ip(b)) => a == b,
+ (_, _) => false,
+ }
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum AddressType {
+ IpV4 = 4,
+ IpV6 = 6,
+}
+
+impl fmt::Display for AddressType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ AddressType::IpV4 => "IP4",
+ AddressType::IpV6 => "IP6",
+ }
+ .fmt(f)
+ }
+}
+
+impl FromStr for AddressType {
+ type Err = SdpParserInternalError;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.to_uppercase().as_str() {
+ "IP4" => Ok(AddressType::IpV4),
+ "IP6" => Ok(AddressType::IpV6),
+ _ => Err(SdpParserInternalError::UnknownAddressType(s.to_owned())),
+ }
+ }
+}
+
+pub trait AddressTyped {
+ fn address_type(&self) -> AddressType;
+}
+
+impl AddressTyped for IpAddr {
+ fn address_type(&self) -> AddressType {
+ match self {
+ IpAddr::V4(_) => AddressType::IpV4,
+ IpAddr::V6(_) => AddressType::IpV6,
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum ExplicitlyTypedAddress {
+ Fqdn {
+ address_type: AddressType,
+ domain: String,
+ },
+ Ip(IpAddr),
+}
+
+impl fmt::Display for ExplicitlyTypedAddress {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "IN {} ", self.address_type())?;
+ match self {
+ ExplicitlyTypedAddress::Fqdn { domain, .. } => domain.fmt(f),
+ ExplicitlyTypedAddress::Ip(ip) => ip.fmt(f),
+ }
+ }
+}
+
+impl AddressTyped for ExplicitlyTypedAddress {
+ fn address_type(&self) -> AddressType {
+ match self {
+ ExplicitlyTypedAddress::Fqdn { address_type, .. } => *address_type,
+ ExplicitlyTypedAddress::Ip(ip) => ip.address_type(),
+ }
+ }
+}
+
+impl From<IpAddr> for ExplicitlyTypedAddress {
+ fn from(item: IpAddr) -> Self {
+ ExplicitlyTypedAddress::Ip(item)
+ }
+}
+
+impl From<Ipv4Addr> for ExplicitlyTypedAddress {
+ fn from(item: Ipv4Addr) -> Self {
+ ExplicitlyTypedAddress::Ip(IpAddr::V4(item))
+ }
+}
+
+impl From<Ipv6Addr> for ExplicitlyTypedAddress {
+ fn from(item: Ipv6Addr) -> Self {
+ ExplicitlyTypedAddress::Ip(IpAddr::V6(item))
+ }
+}
+
+impl TryFrom<(AddressType, &str)> for ExplicitlyTypedAddress {
+ type Error = SdpParserInternalError;
+ fn try_from(item: (AddressType, &str)) -> Result<Self, Self::Error> {
+ match Address::from_str(item.1)? {
+ Address::Ip(ip) => {
+ if ip.address_type() != item.0 {
+ Err(SdpParserInternalError::AddressTypeMismatch {
+ found: ip.address_type(),
+ expected: item.0,
+ })
+ } else {
+ Ok(ExplicitlyTypedAddress::Ip(ip))
+ }
+ }
+ Address::Fqdn(domain) => Ok(ExplicitlyTypedAddress::Fqdn {
+ address_type: item.0,
+ domain,
+ }),
+ }
+ }
+}
+
+impl PartialEq for ExplicitlyTypedAddress {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (
+ ExplicitlyTypedAddress::Fqdn {
+ address_type: a1,
+ domain: d1,
+ },
+ ExplicitlyTypedAddress::Fqdn {
+ address_type: a2,
+ domain: d2,
+ },
+ ) => a1 == a2 && d1.to_lowercase() == d2.to_lowercase(),
+ (ExplicitlyTypedAddress::Ip(a), ExplicitlyTypedAddress::Ip(b)) => a == b,
+ (_, _) => false,
+ }
+ }
+}
+
+#[cfg(test)]
+#[path = "./address_tests.rs"]
+mod address_tests;
diff --git a/third_party/rust/webrtc-sdp/src/address_tests.rs b/third_party/rust/webrtc-sdp/src/address_tests.rs
new file mode 100644
index 0000000000..8c68fd278d
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/address_tests.rs
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use self::url::ParseError;
+use super::*;
+use std::error::Error;
+use std::net::{AddrParseError, Ipv4Addr, Ipv6Addr};
+
+#[derive(Debug)]
+enum ParseTestError {
+ Host(ParseError),
+ Ip(AddrParseError),
+}
+impl fmt::Display for ParseTestError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ParseTestError::Host(inner) => inner.fmt(f),
+ ParseTestError::Ip(inner) => inner.fmt(f),
+ }
+ }
+}
+impl From<ParseError> for ParseTestError {
+ fn from(err: ParseError) -> Self {
+ ParseTestError::Host(err)
+ }
+}
+impl From<AddrParseError> for ParseTestError {
+ fn from(err: AddrParseError) -> Self {
+ ParseTestError::Ip(err)
+ }
+}
+impl Error for ParseTestError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ // Generic error, underlying cause isn't tracked.
+ match self {
+ ParseTestError::Host(a) => Some(a),
+ ParseTestError::Ip(a) => Some(a),
+ }
+ }
+}
+#[test]
+fn test_domain_name_parsing() -> Result<(), ParseTestError> {
+ let address = Host::parse("this.is.a.fqdn")?;
+ if let Host::Domain(domain) = address {
+ assert_eq!(domain, "this.is.a.fqdn");
+ } else {
+ panic!();
+ }
+ Ok(())
+}
+
+#[test]
+fn test_ipv4_address_parsing() -> Result<(), ParseTestError> {
+ let address = Host::parse("1.0.0.1")?;
+ if let Host::Ipv4(ip) = address {
+ assert_eq!(ip, "1.0.0.1".parse::<Ipv4Addr>()?);
+ } else {
+ panic!();
+ }
+ Ok(())
+}
+
+#[test]
+fn test_ipv6_address_parsing() -> Result<(), ParseTestError> {
+ let address = Host::parse("[::1]")?;
+ if let Host::Ipv6(ip) = address {
+ assert_eq!(ip, "::1".parse::<Ipv6Addr>()?);
+ } else {
+ panic!();
+ }
+ Ok(())
+}
diff --git a/third_party/rust/webrtc-sdp/src/anonymizer.rs b/third_party/rust/webrtc-sdp/src/anonymizer.rs
new file mode 100644
index 0000000000..a3d4d50aa7
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/anonymizer.rs
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate url;
+use address::{Address, ExplicitlyTypedAddress};
+use std::collections::HashMap;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+use std::num::Wrapping;
+
+pub trait AnonymizingClone {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self;
+}
+
+pub trait ToBytesVec {
+ fn to_byte_vec(&self) -> Vec<u8>;
+}
+
+impl ToBytesVec for u64 {
+ fn to_byte_vec(&self) -> Vec<u8> {
+ let mut bytes = Vec::new();
+ let mut val = *self;
+ for _ in 0..8 {
+ bytes.push(val as u8);
+ val <<= 8;
+ }
+ bytes.reverse();
+ bytes
+ }
+}
+
+/*
+* Anonymizes SDP in a stateful fashion, such that a pre-anonymized value will
+* always be transformed into the same anonymized value within the context of
+* the anonymizer.
+* Stores the opaque state necessary for intelligent anonymization of SDP. This
+* state can be stored and reused during the offer-answer period, and it
+* will maintain a stable set of masked values.
+*/
+pub struct StatefulSdpAnonymizer {
+ ips: HashMap<IpAddr, IpAddr>,
+ ip_v4_inc: Wrapping<u32>,
+ ip_v6_inc: Wrapping<u128>,
+ host_names: AnonymizationStrMap,
+ ports: HashMap<u32, u32>,
+ port_inc: Wrapping<u32>,
+ origin_users: AnonymizationStrMap,
+ ice_passwords: AnonymizationStrMap,
+ ice_users: AnonymizationStrMap,
+ cert_finger_prints: HashMap<Vec<u8>, Vec<u8>>,
+ cert_finger_print_inc: Wrapping<u64>,
+ cnames: AnonymizationStrMap,
+}
+
+impl Default for StatefulSdpAnonymizer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl StatefulSdpAnonymizer {
+ pub fn new() -> Self {
+ StatefulSdpAnonymizer {
+ ips: HashMap::new(),
+ ip_v4_inc: Wrapping(0),
+ ip_v6_inc: Wrapping(0),
+ host_names: AnonymizationStrMap::new("fqdn-", 8),
+ ports: HashMap::new(),
+ port_inc: Wrapping(0),
+ origin_users: AnonymizationStrMap::new("origin-user-", 8),
+ ice_passwords: AnonymizationStrMap::new("ice-password-", 8),
+ ice_users: AnonymizationStrMap::new("ice-user-", 8),
+ cert_finger_prints: HashMap::new(),
+ cert_finger_print_inc: Wrapping(0),
+ cnames: AnonymizationStrMap::new("cname-", 8),
+ }
+ }
+
+ pub fn mask_host(&mut self, host: &str) -> String {
+ self.host_names.mask(host)
+ }
+
+ pub fn mask_ip(&mut self, addr: &IpAddr) -> IpAddr {
+ if let Some(address) = self.ips.get(addr) {
+ return *address;
+ }
+ let mapped = match addr {
+ IpAddr::V4(_) => {
+ self.ip_v4_inc += Wrapping(1);
+ IpAddr::V4(Ipv4Addr::from(self.ip_v4_inc.0))
+ }
+ IpAddr::V6(_) => {
+ self.ip_v6_inc += Wrapping(1);
+ IpAddr::V6(Ipv6Addr::from(self.ip_v6_inc.0))
+ }
+ };
+ self.ips.insert(*addr, mapped);
+ mapped
+ }
+
+ pub fn mask_address(&mut self, address: &Address) -> Address {
+ match address {
+ Address::Fqdn(host) => Address::Fqdn(self.mask_host(host)),
+ Address::Ip(ip) => Address::Ip(self.mask_ip(ip)),
+ }
+ }
+
+ pub fn mask_typed_address(
+ &mut self,
+ address: &ExplicitlyTypedAddress,
+ ) -> ExplicitlyTypedAddress {
+ match address {
+ ExplicitlyTypedAddress::Fqdn {
+ address_type,
+ domain,
+ } => ExplicitlyTypedAddress::Fqdn {
+ address_type: *address_type,
+ domain: self.mask_host(domain),
+ },
+ ExplicitlyTypedAddress::Ip(ip) => ExplicitlyTypedAddress::Ip(self.mask_ip(ip)),
+ }
+ }
+
+ pub fn mask_port(&mut self, port: u32) -> u32 {
+ if let Some(stored) = self.ports.get(&port) {
+ return *stored;
+ }
+ self.port_inc += Wrapping(1);
+ self.ports.insert(port, self.port_inc.0);
+ self.port_inc.0
+ }
+
+ pub fn mask_origin_user(&mut self, user: &str) -> String {
+ self.origin_users.mask(user)
+ }
+
+ pub fn mask_ice_password(&mut self, password: &str) -> String {
+ self.ice_passwords.mask(password)
+ }
+
+ pub fn mask_ice_user(&mut self, user: &str) -> String {
+ self.ice_users.mask(user)
+ }
+
+ pub fn mask_cert_finger_print(&mut self, finger_print: &[u8]) -> Vec<u8> {
+ if let Some(stored) = self.cert_finger_prints.get(finger_print) {
+ return stored.clone();
+ }
+ self.cert_finger_print_inc += Wrapping(1);
+ self.cert_finger_prints.insert(
+ finger_print.to_vec(),
+ self.cert_finger_print_inc.0.to_byte_vec(),
+ );
+ self.cert_finger_print_inc.0.to_byte_vec()
+ }
+
+ pub fn mask_cname(&mut self, cname: &str) -> String {
+ self.cnames.mask(cname)
+ }
+}
+
+struct AnonymizationStrMap {
+ map: HashMap<String, String>,
+ counter: Wrapping<u64>,
+ prefix: &'static str,
+ padding: usize,
+}
+
+impl AnonymizationStrMap {
+ pub fn new(prefix: &'static str, padding: usize) -> Self {
+ Self {
+ map: HashMap::new(),
+ counter: Wrapping(0),
+ prefix,
+ padding,
+ }
+ }
+
+ pub fn mask(&mut self, value: &str) -> String {
+ let key = value.to_owned();
+ if let Some(stored) = self.map.get(&key) {
+ return stored.clone();
+ }
+ self.counter += Wrapping(1);
+ let store = format!(
+ "{}{:0padding$}",
+ self.prefix,
+ self.counter.0,
+ padding = self.padding
+ );
+ self.map.insert(key, store.clone());
+ store
+ }
+}
+
+#[cfg(test)]
+#[path = "./anonymizer_tests.rs"]
+mod tests;
diff --git a/third_party/rust/webrtc-sdp/src/anonymizer_tests.rs b/third_party/rust/webrtc-sdp/src/anonymizer_tests.rs
new file mode 100644
index 0000000000..dede32a2c5
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/anonymizer_tests.rs
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::*;
+
+#[test]
+fn test_mask_ip() {
+ let mut anon = StatefulSdpAnonymizer::default();
+ let v4 = [
+ Ipv4Addr::new(127, 0, 0, 1),
+ Ipv4Addr::new(10, 0, 0, 1),
+ Ipv4Addr::new(1, 1, 1, 1),
+ ];
+ let v4_masked = [
+ Ipv4Addr::new(0, 0, 0, 1),
+ Ipv4Addr::new(0, 0, 0, 2),
+ Ipv4Addr::new(0, 0, 0, 3),
+ ];
+ let v6 = [
+ Ipv6Addr::from(0),
+ Ipv6Addr::from(528_189_235),
+ Ipv6Addr::from(1_623_734_988_148_990),
+ ];
+ let v6_masked = [Ipv6Addr::from(1), Ipv6Addr::from(2), Ipv6Addr::from(3)];
+ for _ in 0..2 {
+ assert_eq!(anon.mask_ip(&IpAddr::V4(v4[0])), v4_masked[0]);
+ assert_eq!(anon.mask_ip(&IpAddr::V6(v6[0])), v6_masked[0]);
+
+ assert_eq!(anon.mask_ip(&IpAddr::V4(v4[1])), v4_masked[1]);
+ assert_eq!(anon.mask_ip(&IpAddr::V6(v6[1])), v6_masked[1]);
+
+ assert_eq!(anon.mask_ip(&IpAddr::V4(v4[2])), v4_masked[2]);
+ assert_eq!(anon.mask_ip(&IpAddr::V6(v6[2])), v6_masked[2]);
+ }
+}
+
+#[test]
+fn test_mask_port() {
+ let mut anon = StatefulSdpAnonymizer::default();
+ let ports = [0, 125, 12346];
+ let masked_ports = [1, 2, 3];
+ for _ in 0..2 {
+ assert_eq!(anon.mask_port(ports[0]), masked_ports[0]);
+ assert_eq!(anon.mask_port(ports[1]), masked_ports[1]);
+ assert_eq!(anon.mask_port(ports[2]), masked_ports[2]);
+ }
+}
+
+#[test]
+fn test_mask_ice_password() {
+ let mut anon = StatefulSdpAnonymizer::default();
+ let passwords = ["vasdfioqwenl14082`14", "0", "ncp HY878hp(poh"];
+ let masked_passwords = [
+ "ice-password-00000001",
+ "ice-password-00000002",
+ "ice-password-00000003",
+ ];
+ for _ in 0..2 {
+ assert_eq!(anon.mask_ice_password(passwords[0]), masked_passwords[0]);
+ assert_eq!(anon.mask_ice_password(passwords[1]), masked_passwords[1]);
+ assert_eq!(anon.mask_ice_password(passwords[2]), masked_passwords[2]);
+ }
+}
+
+#[test]
+fn test_mask_ice_user() {
+ let mut anon = StatefulSdpAnonymizer::default();
+ let users = ["user1", "user2", "8109q2asdf"];
+ let masked_users = [
+ "ice-user-00000001",
+ "ice-user-00000002",
+ "ice-user-00000003",
+ ];
+ for _ in 0..2 {
+ assert_eq!(anon.mask_ice_user(users[0]), masked_users[0]);
+ assert_eq!(anon.mask_ice_user(users[1]), masked_users[1]);
+ assert_eq!(anon.mask_ice_user(users[2]), masked_users[2]);
+ }
+}
+
+#[test]
+fn test_mask_cert_fingerprint() {
+ let mut anon = StatefulSdpAnonymizer::default();
+ let prints: [Vec<u8>; 3] = [
+ vec![
+ 0x59u8, 0x4A, 0x8B, 0x73, 0xA7, 0x73, 0x53, 0x71, 0x88, 0xD7, 0x4D, 0x58, 0x28, 0x0C,
+ 0x79, 0x72, 0x31, 0x29, 0x9B, 0x05, 0x37, 0xDD, 0x58, 0x43, 0xC2, 0xD4, 0x85, 0xA2,
+ 0xB3, 0x66, 0x38, 0x7A,
+ ],
+ vec![
+ 0x30u8, 0xFF, 0x8E, 0x2B, 0xAC, 0x9D, 0xED, 0x70, 0x18, 0x10, 0x67, 0xC8, 0xAE, 0x9E,
+ 0x68, 0xF3, 0x86, 0x53, 0x51, 0xB0, 0xAC, 0x31, 0xB7, 0xBE, 0x6D, 0xCF, 0xA4, 0x2E,
+ 0xD3, 0x6E, 0xB4, 0x28,
+ ],
+ vec![
+ 0xDFu8, 0x2E, 0xAC, 0x8A, 0xFD, 0x0A, 0x8E, 0x99, 0xBF, 0x5D, 0xE8, 0x3C, 0xE7, 0xFA,
+ 0xFB, 0x08, 0x3B, 0x3C, 0x54, 0x1D, 0xD7, 0xD4, 0x05, 0x77, 0xA0, 0x72, 0x9B, 0x14,
+ 0x08, 0x6D, 0x0F, 0x4C,
+ ],
+ ];
+
+ let masked_prints = [1u64.to_byte_vec(), 2u64.to_byte_vec(), 3u64.to_byte_vec()];
+ for _ in 0..2 {
+ assert_eq!(anon.mask_cert_finger_print(&prints[0]), masked_prints[0]);
+ assert_eq!(anon.mask_cert_finger_print(&prints[1]), masked_prints[1]);
+ assert_eq!(anon.mask_cert_finger_print(&prints[2]), masked_prints[2]);
+ }
+}
+
+#[test]
+fn test_mask_cname() {
+ let mut anon = StatefulSdpAnonymizer::default();
+ let cnames = ["mailto:foo@bar", "JohnDoe", "Jane Doe"];
+ let masked_cnames = ["cname-00000001", "cname-00000002", "cname-00000003"];
+ for _ in 0..2 {
+ assert_eq!(anon.mask_cname(cnames[0]), masked_cnames[0]);
+ assert_eq!(anon.mask_cname(cnames[1]), masked_cnames[1]);
+ assert_eq!(anon.mask_cname(cnames[2]), masked_cnames[2]);
+ }
+}
diff --git a/third_party/rust/webrtc-sdp/src/attribute_type.rs b/third_party/rust/webrtc-sdp/src/attribute_type.rs
new file mode 100644
index 0000000000..7c667698b5
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/attribute_type.rs
@@ -0,0 +1,3319 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate url;
+use std::convert::TryFrom;
+use std::fmt;
+use std::iter;
+use std::str::FromStr;
+
+use error::SdpParserInternalError;
+use network::{parse_network_type, parse_unicast_address};
+use SdpType;
+
+use address::{Address, AddressType, ExplicitlyTypedAddress};
+use anonymizer::{AnonymizingClone, StatefulSdpAnonymizer};
+
+// Serialization helper marcos and functions
+#[macro_export]
+macro_rules! option_to_string {
+ ($fmt_str:expr, $opt:expr) => {
+ match $opt {
+ Some(ref x) => format!($fmt_str, x),
+ None => "".to_string(),
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! write_option_string {
+ ($f:expr, $fmt_str:expr, $opt:expr) => {
+ match $opt {
+ Some(ref x) => write!($f, $fmt_str, x),
+ None => Ok(()),
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! maybe_vector_to_string {
+ ($fmt_str:expr, $vec:expr, $sep:expr) => {
+ match $vec.len() {
+ 0 => "".to_string(),
+ _ => format!(
+ $fmt_str,
+ $vec.iter()
+ .map(ToString::to_string)
+ .collect::<Vec<String>>()
+ .join($sep)
+ ),
+ }
+ };
+}
+
+#[macro_export]
+macro_rules! non_empty_string_vec {
+ ( $( $x:expr ),* ) => {
+ {
+ let mut temp_vec = Vec::new();
+ $(
+ if !$x.is_empty() {
+ temp_vec.push($x);
+ }
+ )*
+ temp_vec
+ }
+ };
+}
+
+pub fn maybe_print_param<T>(name: &str, param: T, default_value: T) -> String
+where
+ T: PartialEq + ToString,
+{
+ if param != default_value {
+ name.to_owned() + &param.to_string()
+ } else {
+ "".to_string()
+ }
+}
+
+pub fn maybe_print_bool_param(name: &str, param: bool, default_value: bool) -> String {
+ if param != default_value {
+ name.to_owned() + "=" + &(param as i32).to_string()
+ } else {
+ "".to_string()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpSingleDirection {
+ // This is explicitly 1 and 2 to match the defines in the C++ glue code.
+ Send = 1,
+ Recv = 2,
+}
+
+impl fmt::Display for SdpSingleDirection {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpSingleDirection::Send => "send",
+ SdpSingleDirection::Recv => "recv",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpAttributePayloadType {
+ PayloadType(u8),
+ Wildcard, // Wildcard means "*",
+}
+
+impl fmt::Display for SdpAttributePayloadType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributePayloadType::PayloadType(pt) => pt.fmt(f),
+ SdpAttributePayloadType::Wildcard => "*".fmt(f),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpAttributeCandidateTransport {
+ Udp,
+ Tcp,
+}
+
+impl fmt::Display for SdpAttributeCandidateTransport {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeCandidateTransport::Udp => "UDP",
+ SdpAttributeCandidateTransport::Tcp => "TCP",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpAttributeCandidateType {
+ Host,
+ Srflx,
+ Prflx,
+ Relay,
+}
+
+impl fmt::Display for SdpAttributeCandidateType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeCandidateType::Host => "host",
+ SdpAttributeCandidateType::Srflx => "srflx",
+ SdpAttributeCandidateType::Prflx => "prflx",
+ SdpAttributeCandidateType::Relay => "relay",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpAttributeCandidateTcpType {
+ Active,
+ Passive,
+ Simultaneous,
+}
+
+impl fmt::Display for SdpAttributeCandidateTcpType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeCandidateTcpType::Active => "active",
+ SdpAttributeCandidateTcpType::Passive => "passive",
+ SdpAttributeCandidateTcpType::Simultaneous => "so",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeCandidate {
+ pub foundation: String,
+ pub component: u32,
+ pub transport: SdpAttributeCandidateTransport,
+ pub priority: u64,
+ pub address: Address,
+ pub port: u32,
+ pub c_type: SdpAttributeCandidateType,
+ pub raddr: Option<Address>,
+ pub rport: Option<u32>,
+ pub tcp_type: Option<SdpAttributeCandidateTcpType>,
+ pub generation: Option<u32>,
+ pub ufrag: Option<String>,
+ pub networkcost: Option<u32>,
+ pub unknown_extensions: Vec<(String, String)>,
+}
+
+impl fmt::Display for SdpAttributeCandidate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{foundation} {component} {transport} {priority} \
+ {address} {port} typ {ctype}\
+ {raddr}{rport}{tcp_type}{generation}{ufrag}{cost}\
+ {unknown}",
+ foundation = self.foundation,
+ component = self.component,
+ transport = self.transport,
+ priority = self.priority,
+ address = self.address,
+ port = self.port,
+ ctype = self.c_type,
+ raddr = option_to_string!(" raddr {}", self.raddr),
+ rport = option_to_string!(" rport {}", self.rport),
+ tcp_type = option_to_string!(" tcptype {}", self.tcp_type),
+ generation = option_to_string!(" generation {}", self.generation),
+ ufrag = option_to_string!(" ufrag {}", self.ufrag),
+ cost = option_to_string!(" network-cost {}", self.networkcost),
+ unknown = self
+ .unknown_extensions
+ .iter()
+ .map(|(name, value)| format!(" {name} {value}"))
+ .collect::<String>()
+ )
+ }
+}
+
+impl SdpAttributeCandidate {
+ pub fn new(
+ foundation: String,
+ component: u32,
+ transport: SdpAttributeCandidateTransport,
+ priority: u64,
+ address: Address,
+ port: u32,
+ c_type: SdpAttributeCandidateType,
+ ) -> SdpAttributeCandidate {
+ SdpAttributeCandidate {
+ foundation,
+ component,
+ transport,
+ priority,
+ address,
+ port,
+ c_type,
+ raddr: None,
+ rport: None,
+ tcp_type: None,
+ generation: None,
+ ufrag: None,
+ networkcost: None,
+ unknown_extensions: Vec::new(),
+ }
+ }
+
+ fn set_remote_address(&mut self, addr: Address) {
+ self.raddr = Some(addr)
+ }
+
+ fn set_remote_port(&mut self, p: u32) {
+ self.rport = Some(p)
+ }
+
+ fn set_tcp_type(&mut self, t: SdpAttributeCandidateTcpType) {
+ self.tcp_type = Some(t)
+ }
+
+ fn set_generation(&mut self, g: u32) {
+ self.generation = Some(g)
+ }
+
+ fn set_ufrag(&mut self, u: String) {
+ self.ufrag = Some(u)
+ }
+
+ fn set_network_cost(&mut self, n: u32) {
+ self.networkcost = Some(n)
+ }
+
+ fn add_unknown_extension(&mut self, name: String, value: String) {
+ self.unknown_extensions.push((name, value));
+ }
+}
+
+impl AnonymizingClone for SdpAttributeCandidate {
+ fn masked_clone(&self, anonymizer: &mut StatefulSdpAnonymizer) -> Self {
+ let mut masked = self.clone();
+ masked.address = anonymizer.mask_address(&self.address);
+ masked.port = anonymizer.mask_port(self.port);
+ masked.raddr = self
+ .raddr
+ .clone()
+ .map(|addr| anonymizer.mask_address(&addr));
+ masked.rport = self.rport.map(|port| anonymizer.mask_port(port));
+ masked
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpAttributeDtlsMessage {
+ Client(String),
+ Server(String),
+}
+
+impl fmt::Display for SdpAttributeDtlsMessage {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ SdpAttributeDtlsMessage::Client(msg) => format!("client {msg}"),
+ SdpAttributeDtlsMessage::Server(msg) => format!("server {msg}"),
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeRemoteCandidate {
+ pub component: u32,
+ pub address: Address,
+ pub port: u32,
+}
+
+impl fmt::Display for SdpAttributeRemoteCandidate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{component} {addr} {port}",
+ component = self.component,
+ addr = self.address,
+ port = self.port
+ )
+ }
+}
+
+impl AnonymizingClone for SdpAttributeRemoteCandidate {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ SdpAttributeRemoteCandidate {
+ address: anon.mask_address(&self.address),
+ port: anon.mask_port(self.port),
+ component: self.component,
+ }
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeSimulcastId {
+ pub id: String,
+ pub paused: bool,
+}
+
+impl SdpAttributeSimulcastId {
+ pub fn new(idstr: &str) -> SdpAttributeSimulcastId {
+ if let Some(idstr) = idstr.strip_prefix('~') {
+ SdpAttributeSimulcastId {
+ id: idstr.to_string(),
+ paused: true,
+ }
+ } else {
+ SdpAttributeSimulcastId {
+ id: idstr.to_string(),
+ paused: false,
+ }
+ }
+ }
+}
+
+impl fmt::Display for SdpAttributeSimulcastId {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if self.paused {
+ write!(f, "~")?;
+ }
+ self.id.fmt(f)
+ }
+}
+
+#[repr(C)]
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeSimulcastVersion {
+ pub ids: Vec<SdpAttributeSimulcastId>,
+}
+
+impl SdpAttributeSimulcastVersion {
+ pub fn new(idlist: &str) -> SdpAttributeSimulcastVersion {
+ SdpAttributeSimulcastVersion {
+ ids: idlist
+ .split(',')
+ .map(SdpAttributeSimulcastId::new)
+ .collect(),
+ }
+ }
+}
+
+impl fmt::Display for SdpAttributeSimulcastVersion {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.ids
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<String>>()
+ .join(",")
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeSimulcast {
+ pub send: Vec<SdpAttributeSimulcastVersion>,
+ pub receive: Vec<SdpAttributeSimulcastVersion>,
+}
+
+impl fmt::Display for SdpAttributeSimulcast {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ non_empty_string_vec![
+ maybe_vector_to_string!("send {}", self.send, ";"),
+ maybe_vector_to_string!("recv {}", self.receive, ";")
+ ]
+ .join(" ")
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeRtcp {
+ pub port: u16,
+ pub unicast_addr: Option<ExplicitlyTypedAddress>,
+}
+
+impl SdpAttributeRtcp {
+ pub fn new(port: u16) -> SdpAttributeRtcp {
+ SdpAttributeRtcp {
+ port,
+ unicast_addr: None,
+ }
+ }
+
+ fn set_addr(&mut self, addr: ExplicitlyTypedAddress) {
+ self.unicast_addr = Some(addr)
+ }
+}
+
+impl fmt::Display for SdpAttributeRtcp {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self.unicast_addr {
+ Some(ref addr) => write!(f, "{} {}", self.port, addr),
+ None => self.port.fmt(f),
+ }
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpAttributeRtcpFbType {
+ Ack = 0,
+ Ccm = 2, // This is explicitly 2 to make the conversion to the
+ // enum used in the glue-code possible. The glue code has "app"
+ // in the place of 1
+ Nack,
+ TrrInt,
+ Remb,
+ TransCc,
+}
+
+impl fmt::Display for SdpAttributeRtcpFbType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeRtcpFbType::Ack => "ack",
+ SdpAttributeRtcpFbType::Ccm => "ccm",
+ SdpAttributeRtcpFbType::Nack => "nack",
+ SdpAttributeRtcpFbType::TrrInt => "trr-int",
+ SdpAttributeRtcpFbType::Remb => "goog-remb",
+ SdpAttributeRtcpFbType::TransCc => "transport-cc",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeRtcpFb {
+ pub payload_type: SdpAttributePayloadType,
+ pub feedback_type: SdpAttributeRtcpFbType,
+ pub parameter: String,
+ pub extra: String,
+}
+
+impl fmt::Display for SdpAttributeRtcpFb {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{} {}", self.payload_type, self.feedback_type,)?;
+ if !self.parameter.is_empty() {
+ write!(
+ f,
+ " {}{}",
+ self.parameter,
+ maybe_print_param(" ", self.extra.clone(), "".to_string()),
+ )?;
+ }
+ Ok(())
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpAttributeDirection {
+ Recvonly,
+ Sendonly,
+ Sendrecv,
+}
+
+impl fmt::Display for SdpAttributeDirection {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeDirection::Recvonly => "recvonly",
+ SdpAttributeDirection::Sendonly => "sendonly",
+ SdpAttributeDirection::Sendrecv => "sendrecv",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeExtmap {
+ pub id: u16,
+ pub direction: Option<SdpAttributeDirection>,
+ pub url: String,
+ pub extension_attributes: Option<String>,
+}
+
+impl fmt::Display for SdpAttributeExtmap {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{id}{direction} {url}{ext}",
+ id = self.id,
+ direction = option_to_string!("/{}", self.direction),
+ url = self.url,
+ ext = option_to_string!(" {}", self.extension_attributes)
+ )
+ }
+}
+
+#[derive(Clone, Copy)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct RtxFmtpParameters {
+ pub apt: u8,
+ pub rtx_time: Option<u32>,
+}
+
+impl fmt::Display for RtxFmtpParameters {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if let Some(rtx_time) = self.rtx_time {
+ write!(f, "apt={};rtx-time={}", self.apt, rtx_time)
+ } else {
+ write!(f, "apt={}", self.apt)
+ }
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeFmtpParameters {
+ // H264
+ // TODO(bug 1466859): Support sprop-parameter-sets
+ // pub static const max_sprop_len: u32 = 128,
+ // pub sprop_parameter_sets: [u8, max_sprop_len],
+ pub packetization_mode: u32,
+ pub level_asymmetry_allowed: bool,
+ pub profile_level_id: u32,
+ pub max_fs: u32,
+ pub max_cpb: u32,
+ pub max_dpb: u32,
+ pub max_br: u32,
+ pub max_mbps: u32,
+
+ // VP8 and VP9
+ // max_fs, already defined in H264
+ pub max_fr: u32,
+
+ // Opus https://tools.ietf.org/html/rfc7587
+ pub maxplaybackrate: u32,
+ pub maxaveragebitrate: u32,
+ pub usedtx: bool,
+ pub stereo: bool,
+ pub useinbandfec: bool,
+ pub cbr: bool,
+ pub ptime: u32,
+ pub minptime: u32,
+ pub maxptime: u32,
+
+ // Red
+ pub encodings: Vec<u8>,
+
+ // telephone-event
+ pub dtmf_tones: String,
+
+ // RTX
+ pub rtx: Option<RtxFmtpParameters>,
+
+ // Unknown
+ pub unknown_tokens: Vec<String>,
+}
+
+impl fmt::Display for SdpAttributeFmtpParameters {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ if let Some(ref rtx) = self.rtx {
+ // rtx
+ return write!(f, "{rtx}");
+ }
+ if !self.dtmf_tones.is_empty() {
+ // telephone-event
+ return write!(f, "{}", self.dtmf_tones);
+ } else if !self.encodings.is_empty() {
+ // red encodings
+ return self
+ .encodings
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<String>>()
+ .join("/")
+ .fmt(f);
+ };
+ write!(
+ f,
+ "{}",
+ non_empty_string_vec![
+ maybe_print_param(
+ "profile-level-id=",
+ format!("{:06x}", self.profile_level_id),
+ "420010".to_string()
+ ),
+ maybe_print_bool_param(
+ "level-asymmetry-allowed",
+ self.level_asymmetry_allowed,
+ false
+ ),
+ maybe_print_param("packetization-mode=", self.packetization_mode, 0),
+ maybe_print_param("max-fs=", self.max_fs, 0),
+ maybe_print_param("max-cpb=", self.max_cpb, 0),
+ maybe_print_param("max-dpb=", self.max_dpb, 0),
+ maybe_print_param("max-br=", self.max_br, 0),
+ maybe_print_param("max-mbps=", self.max_mbps, 0),
+ maybe_print_param("max-fr=", self.max_fr, 0),
+ maybe_print_param("maxplaybackrate=", self.maxplaybackrate, 48000),
+ maybe_print_param("maxaveragebitrate=", self.maxaveragebitrate, 0),
+ maybe_print_param("ptime=", self.ptime, 0),
+ maybe_print_param("minptime=", self.minptime, 0),
+ maybe_print_param("maxptime=", self.maxptime, 0),
+ maybe_print_bool_param("usedtx", self.usedtx, false),
+ maybe_print_bool_param("stereo", self.stereo, false),
+ maybe_print_bool_param("useinbandfec", self.useinbandfec, false),
+ maybe_print_bool_param("cbr", self.cbr, false),
+ maybe_vector_to_string!("{}", self.unknown_tokens, ",")
+ ]
+ .join(";")
+ )
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeFmtp {
+ pub payload_type: u8,
+ pub parameters: SdpAttributeFmtpParameters,
+}
+
+impl fmt::Display for SdpAttributeFmtp {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{pt} {parameter}",
+ pt = self.payload_type,
+ parameter = self.parameters
+ )
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpAttributeFingerprintHashType {
+ Sha1,
+ Sha224,
+ Sha256,
+ Sha384,
+ Sha512,
+}
+
+impl SdpAttributeFingerprintHashType {
+ pub fn try_from_name(name: &str) -> Result<Self, SdpParserInternalError> {
+ match name {
+ "sha-1" => Ok(Self::Sha1),
+ "sha-224" => Ok(Self::Sha224),
+ "sha-256" => Ok(Self::Sha256),
+ "sha-384" => Ok(Self::Sha384),
+ "sha-512" => Ok(Self::Sha512),
+ unknown => Err(SdpParserInternalError::Unsupported(format!(
+ "fingerprint contains an unsupported hash algorithm '{unknown}'"
+ ))),
+ }
+ }
+ pub fn octet_count(&self) -> usize {
+ match self {
+ Self::Sha1 => 20,
+ Self::Sha224 => 28,
+ Self::Sha256 => 32,
+ Self::Sha384 => 48,
+ Self::Sha512 => 64,
+ }
+ }
+
+ pub fn parse_octets(&self, octets_string: &str) -> Result<Vec<u8>, SdpParserInternalError> {
+ let bytes = octets_string
+ .split(':')
+ .map(|byte_token| {
+ if byte_token.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "fingerpint's byte tokens must have 2 hexdigits".to_string(),
+ ));
+ }
+ Ok(u8::from_str_radix(byte_token, 16)?)
+ })
+ .collect::<Result<Vec<u8>, _>>()?;
+
+ if bytes.len() != self.octet_count() {
+ return Err(SdpParserInternalError::Generic(format!(
+ "fingerprint has {} bytes but should have {} bytes",
+ bytes.len(),
+ self.octet_count(),
+ )));
+ }
+
+ Ok(bytes)
+ }
+}
+
+impl fmt::Display for SdpAttributeFingerprintHashType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeFingerprintHashType::Sha1 => "sha-1",
+ SdpAttributeFingerprintHashType::Sha224 => "sha-224",
+ SdpAttributeFingerprintHashType::Sha256 => "sha-256",
+ SdpAttributeFingerprintHashType::Sha384 => "sha-384",
+ SdpAttributeFingerprintHashType::Sha512 => "sha-512",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeFingerprint {
+ pub hash_algorithm: SdpAttributeFingerprintHashType,
+ pub fingerprint: Vec<u8>,
+}
+
+impl TryFrom<(SdpAttributeFingerprintHashType, Vec<u8>)> for SdpAttributeFingerprint {
+ type Error = SdpParserInternalError;
+ fn try_from(
+ parts: (SdpAttributeFingerprintHashType, Vec<u8>),
+ ) -> Result<Self, SdpParserInternalError> {
+ let (hash_algorithm, fingerprint) = parts;
+ match (hash_algorithm.octet_count(), fingerprint.len()) {
+ (a, b) if a == b => Ok(Self {
+ hash_algorithm,
+ fingerprint,
+ }),
+ (a, b) => Err(SdpParserInternalError::Generic(format!(
+ "Hash algoritm expects {a} fingerprint bytes not {b}",
+ ))),
+ }
+ }
+}
+
+impl fmt::Display for SdpAttributeFingerprint {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{hash} {fp}",
+ hash = self.hash_algorithm,
+ fp = self
+ .fingerprint
+ .iter()
+ .map(|byte| format!("{byte:02X}"))
+ .collect::<Vec<String>>()
+ .join(":")
+ )
+ }
+}
+
+impl AnonymizingClone for SdpAttributeFingerprint {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ SdpAttributeFingerprint {
+ hash_algorithm: self.hash_algorithm,
+ fingerprint: anon.mask_cert_finger_print(&self.fingerprint),
+ }
+ }
+}
+
+fn imageattr_discrete_value_list_to_string<T>(values: &[T]) -> String
+where
+ T: ToString,
+{
+ match values.len() {
+ 1 => values[0].to_string(),
+ _ => format!(
+ "[{}]",
+ values
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<String>>()
+ .join(",")
+ ),
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpAttributeImageAttrXyRange {
+ Range(u32, u32, Option<u32>), // min, max, step
+ DiscreteValues(Vec<u32>),
+}
+
+impl fmt::Display for SdpAttributeImageAttrXyRange {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeImageAttrXyRange::Range(ref min, ref max, ref step_opt) => {
+ write!(f, "[{min}:")?;
+ if step_opt.is_some() {
+ write!(f, "{}:", step_opt.unwrap())?;
+ }
+ write!(f, "{max}]")
+ }
+ SdpAttributeImageAttrXyRange::DiscreteValues(ref values) => {
+ write!(f, "{}", imageattr_discrete_value_list_to_string(values))
+ }
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpAttributeImageAttrSRange {
+ Range(f32, f32), // min, max
+ DiscreteValues(Vec<f32>),
+}
+
+impl fmt::Display for SdpAttributeImageAttrSRange {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ SdpAttributeImageAttrSRange::Range(min, max) => write!(f, "[{min}-{max}]"),
+ SdpAttributeImageAttrSRange::DiscreteValues(values) => {
+ write!(f, "{}", imageattr_discrete_value_list_to_string(values))
+ }
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub struct SdpAttributeImageAttrPRange {
+ pub min: f32,
+ pub max: f32,
+}
+
+impl fmt::Display for SdpAttributeImageAttrPRange {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "[{}-{}]", self.min, self.max)
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub struct SdpAttributeImageAttrSet {
+ pub x: SdpAttributeImageAttrXyRange,
+ pub y: SdpAttributeImageAttrXyRange,
+ pub sar: Option<SdpAttributeImageAttrSRange>,
+ pub par: Option<SdpAttributeImageAttrPRange>,
+ pub q: Option<f32>,
+}
+
+impl fmt::Display for SdpAttributeImageAttrSet {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "[x={x},y={y}", x = self.x, y = self.y)?;
+ write_option_string!(f, ",sar={}", self.sar)?;
+ write_option_string!(f, ",par={}", self.par)?;
+ write_option_string!(f, ",q={}", self.q)?;
+ write!(f, "]")
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpAttributeImageAttrSetList {
+ Sets(Vec<SdpAttributeImageAttrSet>),
+ Wildcard,
+}
+
+impl fmt::Display for SdpAttributeImageAttrSetList {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeImageAttrSetList::Sets(ref sets) => sets
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<String>>()
+ .join(" ")
+ .fmt(f),
+ SdpAttributeImageAttrSetList::Wildcard => "*".fmt(f),
+ }
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeImageAttr {
+ pub pt: SdpAttributePayloadType,
+ pub send: SdpAttributeImageAttrSetList,
+ pub recv: SdpAttributeImageAttrSetList,
+}
+
+impl fmt::Display for SdpAttributeImageAttr {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let maybe_sets_to_string = |set_list| match set_list {
+ SdpAttributeImageAttrSetList::Sets(sets) => match sets.len() {
+ 0 => None,
+ _ => Some(SdpAttributeImageAttrSetList::Sets(sets)),
+ },
+ x => Some(x),
+ };
+ self.pt.fmt(f)?;
+ write_option_string!(f, " send {}", maybe_sets_to_string(self.send.clone()))?;
+ write_option_string!(f, " recv {}", maybe_sets_to_string(self.recv.clone()))
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeSctpmap {
+ pub port: u16,
+ pub channels: u32,
+}
+
+impl fmt::Display for SdpAttributeSctpmap {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{port} webrtc-datachannel {channels}",
+ port = self.port,
+ channels = self.channels
+ )
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpAttributeGroupSemantic {
+ LipSynchronization, // RFC5888
+ FlowIdentification, // RFC5888
+ SingleReservationFlow, // RFC3524
+ AlternateNetworkAddressType, // RFC4091
+ ForwardErrorCorrection, // RFC5956
+ DecodingDependency, // RFC5583
+ Bundle, // draft-ietc-mmusic-bundle
+}
+
+impl fmt::Display for SdpAttributeGroupSemantic {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeGroupSemantic::LipSynchronization => "LS",
+ SdpAttributeGroupSemantic::FlowIdentification => "FID",
+ SdpAttributeGroupSemantic::SingleReservationFlow => "SRF",
+ SdpAttributeGroupSemantic::AlternateNetworkAddressType => "ANAT",
+ SdpAttributeGroupSemantic::ForwardErrorCorrection => "FEC",
+ SdpAttributeGroupSemantic::DecodingDependency => "DDP",
+ SdpAttributeGroupSemantic::Bundle => "BUNDLE",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeGroup {
+ pub semantics: SdpAttributeGroupSemantic,
+ pub tags: Vec<String>,
+}
+
+impl fmt::Display for SdpAttributeGroup {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}{}",
+ self.semantics,
+ maybe_vector_to_string!(" {}", self.tags, " ")
+ )
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeMsid {
+ pub id: String,
+ pub appdata: Option<String>,
+}
+
+impl fmt::Display for SdpAttributeMsid {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.id.fmt(f)?;
+ write_option_string!(f, " {}", self.appdata)
+ }
+}
+
+#[derive(Clone, Debug)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub struct SdpAttributeMsidSemantic {
+ pub semantic: String,
+ pub msids: Vec<String>,
+}
+
+impl fmt::Display for SdpAttributeMsidSemantic {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{} ", self.semantic)?;
+ match self.msids.len() {
+ 0 => "*".fmt(f),
+ _ => self.msids.join(" ").fmt(f),
+ }
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeRidParameters {
+ pub max_width: u32,
+ pub max_height: u32,
+ pub max_fps: u32,
+ pub max_fs: u32,
+ pub max_br: u32,
+ pub max_pps: u32,
+
+ pub unknown: Vec<String>,
+}
+
+impl fmt::Display for SdpAttributeRidParameters {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ non_empty_string_vec![
+ maybe_print_param("max-width=", self.max_width, 0),
+ maybe_print_param("max-height=", self.max_height, 0),
+ maybe_print_param("max-fps=", self.max_fps, 0),
+ maybe_print_param("max-fs=", self.max_fs, 0),
+ maybe_print_param("max-br=", self.max_br, 0),
+ maybe_print_param("max-pps=", self.max_pps, 0),
+ maybe_vector_to_string!("{}", self.unknown, ";")
+ ]
+ .join(";")
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeRid {
+ pub id: String,
+ pub direction: SdpSingleDirection,
+ pub formats: Vec<u16>,
+ pub params: SdpAttributeRidParameters,
+ pub depends: Vec<String>,
+}
+
+impl fmt::Display for SdpAttributeRid {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{id} {direction}{format}",
+ id = self.id,
+ direction = self.direction,
+ format = match non_empty_string_vec![
+ maybe_vector_to_string!("pt={}", self.formats, ","),
+ self.params.to_string(),
+ maybe_vector_to_string!("depends={}", self.depends, ",")
+ ]
+ .join(";")
+ .as_str()
+ {
+ "" => "".to_string(),
+ x => format!(" {x}"),
+ }
+ )
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeRtpmap {
+ pub payload_type: u8,
+ pub codec_name: String,
+ pub frequency: u32,
+ pub channels: Option<u32>,
+}
+
+impl SdpAttributeRtpmap {
+ pub fn new(payload_type: u8, codec_name: String, frequency: u32) -> SdpAttributeRtpmap {
+ SdpAttributeRtpmap {
+ payload_type,
+ codec_name,
+ frequency,
+ channels: None,
+ }
+ }
+
+ fn set_channels(&mut self, c: u32) {
+ self.channels = Some(c)
+ }
+}
+
+impl fmt::Display for SdpAttributeRtpmap {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{pt} {name}/{freq}",
+ pt = self.payload_type,
+ name = self.codec_name,
+ freq = self.frequency
+ )?;
+ write_option_string!(f, "/{}", self.channels)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpAttributeSetup {
+ Active,
+ Actpass,
+ Holdconn,
+ Passive,
+}
+
+impl fmt::Display for SdpAttributeSetup {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeSetup::Active => "active",
+ SdpAttributeSetup::Actpass => "actpass",
+ SdpAttributeSetup::Holdconn => "holdconn",
+ SdpAttributeSetup::Passive => "passive",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpAttributeSsrc {
+ pub id: u32,
+ pub attribute: Option<String>,
+ pub value: Option<String>,
+}
+
+impl SdpAttributeSsrc {
+ pub fn new(id: u32) -> SdpAttributeSsrc {
+ SdpAttributeSsrc {
+ id,
+ attribute: None,
+ value: None,
+ }
+ }
+
+ fn set_attribute(&mut self, a: &str) {
+ if a.find(':').is_none() {
+ self.attribute = Some(a.to_string());
+ } else {
+ let v: Vec<&str> = a.splitn(2, ':').collect();
+ self.attribute = Some(v[0].to_string());
+ self.value = Some(v[1].to_string());
+ }
+ }
+}
+
+impl fmt::Display for SdpAttributeSsrc {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.id.fmt(f)?;
+ write_option_string!(f, " {}", self.attribute)?;
+ write_option_string!(f, ":{}", self.value)
+ }
+}
+
+impl AnonymizingClone for SdpAttributeSsrc {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ Self {
+ id: self.id,
+ attribute: self.attribute.clone(),
+ value: self.attribute.as_ref().and_then(|attribute| {
+ match (attribute.to_lowercase().as_str(), &self.value) {
+ ("cname", Some(ref cname)) => Some(anon.mask_cname(cname.as_str())),
+ (_, Some(_)) => self.value.clone(),
+ (_, None) => None,
+ }
+ }),
+ }
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpSsrcGroupSemantic {
+ Duplication, // RFC7104
+ FlowIdentification, // RFC5576
+ ForwardErrorCorrection, // RFC5576
+ ForwardErrorCorrectionFr, // RFC5956
+ Sim, // not registered with IANA, but used in hangouts
+}
+
+impl fmt::Display for SdpSsrcGroupSemantic {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpSsrcGroupSemantic::Duplication => "DUP",
+ SdpSsrcGroupSemantic::FlowIdentification => "FID",
+ SdpSsrcGroupSemantic::ForwardErrorCorrection => "FEC",
+ SdpSsrcGroupSemantic::ForwardErrorCorrectionFr => "FEC-FR",
+ SdpSsrcGroupSemantic::Sim => "SIM",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpAttribute {
+ BundleOnly,
+ Candidate(SdpAttributeCandidate),
+ DtlsMessage(SdpAttributeDtlsMessage),
+ EndOfCandidates,
+ Extmap(SdpAttributeExtmap),
+ ExtmapAllowMixed,
+ Fingerprint(SdpAttributeFingerprint),
+ Fmtp(SdpAttributeFmtp),
+ Group(SdpAttributeGroup),
+ IceLite,
+ IceMismatch,
+ IceOptions(Vec<String>),
+ IcePacing(u64),
+ IcePwd(String),
+ IceUfrag(String),
+ Identity(String),
+ ImageAttr(SdpAttributeImageAttr),
+ Inactive,
+ Label(String),
+ MaxMessageSize(u64),
+ MaxPtime(u64),
+ Mid(String),
+ Msid(SdpAttributeMsid),
+ MsidSemantic(SdpAttributeMsidSemantic),
+ Ptime(u64),
+ Rid(SdpAttributeRid),
+ Recvonly,
+ RemoteCandidate(SdpAttributeRemoteCandidate),
+ Rtpmap(SdpAttributeRtpmap),
+ Rtcp(SdpAttributeRtcp),
+ Rtcpfb(SdpAttributeRtcpFb),
+ RtcpMux,
+ RtcpMuxOnly, // RFC8858
+ RtcpRsize,
+ Sctpmap(SdpAttributeSctpmap),
+ SctpPort(u64),
+ Sendonly,
+ Sendrecv,
+ Setup(SdpAttributeSetup),
+ Simulcast(SdpAttributeSimulcast),
+ Ssrc(SdpAttributeSsrc),
+ SsrcGroup(SdpSsrcGroupSemantic, Vec<SdpAttributeSsrc>),
+}
+
+impl SdpAttribute {
+ pub fn allowed_at_session_level(&self) -> bool {
+ match *self {
+ SdpAttribute::BundleOnly
+ | SdpAttribute::Candidate(..)
+ | SdpAttribute::Fmtp(..)
+ | SdpAttribute::IceMismatch
+ | SdpAttribute::ImageAttr(..)
+ | SdpAttribute::Label(..)
+ | SdpAttribute::MaxMessageSize(..)
+ | SdpAttribute::MaxPtime(..)
+ | SdpAttribute::Mid(..)
+ | SdpAttribute::Msid(..)
+ | SdpAttribute::Ptime(..)
+ | SdpAttribute::Rid(..)
+ | SdpAttribute::RemoteCandidate(..)
+ | SdpAttribute::Rtpmap(..)
+ | SdpAttribute::Rtcp(..)
+ | SdpAttribute::Rtcpfb(..)
+ | SdpAttribute::RtcpMux
+ | SdpAttribute::RtcpMuxOnly
+ | SdpAttribute::RtcpRsize
+ | SdpAttribute::Sctpmap(..)
+ | SdpAttribute::SctpPort(..)
+ | SdpAttribute::Simulcast(..)
+ | SdpAttribute::Ssrc(..)
+ | SdpAttribute::SsrcGroup(..) => false,
+
+ SdpAttribute::DtlsMessage { .. }
+ | SdpAttribute::EndOfCandidates
+ | SdpAttribute::Extmap(..)
+ | SdpAttribute::ExtmapAllowMixed
+ | SdpAttribute::Fingerprint(..)
+ | SdpAttribute::Group(..)
+ | SdpAttribute::IceLite
+ | SdpAttribute::IceOptions(..)
+ | SdpAttribute::IcePacing(..)
+ | SdpAttribute::IcePwd(..)
+ | SdpAttribute::IceUfrag(..)
+ | SdpAttribute::Identity(..)
+ | SdpAttribute::Inactive
+ | SdpAttribute::MsidSemantic(..)
+ | SdpAttribute::Recvonly
+ | SdpAttribute::Sendonly
+ | SdpAttribute::Sendrecv
+ | SdpAttribute::Setup(..) => true,
+ }
+ }
+
+ pub fn allowed_at_media_level(&self) -> bool {
+ match *self {
+ SdpAttribute::DtlsMessage { .. }
+ | SdpAttribute::Group(..)
+ | SdpAttribute::IceLite
+ | SdpAttribute::IcePacing(..)
+ | SdpAttribute::Identity(..)
+ | SdpAttribute::MsidSemantic(..) => false,
+
+ SdpAttribute::BundleOnly
+ | SdpAttribute::Candidate(..)
+ | SdpAttribute::EndOfCandidates
+ | SdpAttribute::Extmap(..)
+ | SdpAttribute::ExtmapAllowMixed
+ | SdpAttribute::Fingerprint(..)
+ | SdpAttribute::Fmtp(..)
+ | SdpAttribute::IceMismatch
+ | SdpAttribute::IceOptions(..)
+ | SdpAttribute::IcePwd(..)
+ | SdpAttribute::IceUfrag(..)
+ | SdpAttribute::ImageAttr(..)
+ | SdpAttribute::Inactive
+ | SdpAttribute::Label(..)
+ | SdpAttribute::MaxMessageSize(..)
+ | SdpAttribute::MaxPtime(..)
+ | SdpAttribute::Mid(..)
+ | SdpAttribute::Msid(..)
+ | SdpAttribute::Ptime(..)
+ | SdpAttribute::Rid(..)
+ | SdpAttribute::Recvonly
+ | SdpAttribute::RemoteCandidate(..)
+ | SdpAttribute::Rtpmap(..)
+ | SdpAttribute::Rtcp(..)
+ | SdpAttribute::Rtcpfb(..)
+ | SdpAttribute::RtcpMux
+ | SdpAttribute::RtcpMuxOnly
+ | SdpAttribute::RtcpRsize
+ | SdpAttribute::Sctpmap(..)
+ | SdpAttribute::SctpPort(..)
+ | SdpAttribute::Sendonly
+ | SdpAttribute::Sendrecv
+ | SdpAttribute::Setup(..)
+ | SdpAttribute::Simulcast(..)
+ | SdpAttribute::Ssrc(..)
+ | SdpAttribute::SsrcGroup(..) => true,
+ }
+ }
+}
+
+impl FromStr for SdpAttribute {
+ type Err = SdpParserInternalError;
+
+ fn from_str(line: &str) -> Result<Self, Self::Err> {
+ let tokens: Vec<_> = line.splitn(2, ':').collect();
+ let name = tokens[0].to_lowercase();
+ let val = match tokens.get(1) {
+ Some(x) => x.trim(),
+ None => "",
+ };
+ if tokens.len() > 1 {
+ match name.as_str() {
+ "bundle-only" | "end-of-candidates" | "extmap-allow-mixed" | "ice-lite"
+ | "ice-mismatch" | "inactive" | "recvonly" | "rtcp-mux" | "rtcp-mux-only"
+ | "rtcp-rsize" | "sendonly" | "sendrecv" => {
+ return Err(SdpParserInternalError::Generic(format!(
+ "{name} attribute is not allowed to have a value",
+ )));
+ }
+ _ => (),
+ }
+ }
+ match name.as_str() {
+ "bundle-only" => Ok(SdpAttribute::BundleOnly),
+ "dtls-message" => parse_dtls_message(val),
+ "end-of-candidates" => Ok(SdpAttribute::EndOfCandidates),
+ "ice-lite" => Ok(SdpAttribute::IceLite),
+ "ice-mismatch" => Ok(SdpAttribute::IceMismatch),
+ "extmap-allow-mixed" => Ok(SdpAttribute::ExtmapAllowMixed),
+ "ice-pwd" => Ok(SdpAttribute::IcePwd(string_or_empty(val)?)),
+ "ice-ufrag" => Ok(SdpAttribute::IceUfrag(string_or_empty(val)?)),
+ "identity" => Ok(SdpAttribute::Identity(string_or_empty(val)?)),
+ "imageattr" => parse_image_attr(val),
+ "inactive" => Ok(SdpAttribute::Inactive),
+ "label" => Ok(SdpAttribute::Label(string_or_empty(val)?)),
+ "max-message-size" => Ok(SdpAttribute::MaxMessageSize(val.parse()?)),
+ "maxptime" => Ok(SdpAttribute::MaxPtime(val.parse()?)),
+ "mid" => Ok(SdpAttribute::Mid(string_or_empty(val)?)),
+ "msid-semantic" => parse_msid_semantic(val),
+ "ptime" => Ok(SdpAttribute::Ptime(val.parse()?)),
+ "ice-pacing" => parse_ice_pacing(val),
+ "rid" => parse_rid(val),
+ "recvonly" => Ok(SdpAttribute::Recvonly),
+ "rtcp-mux" => Ok(SdpAttribute::RtcpMux),
+ "rtcp-mux-only" => Ok(SdpAttribute::RtcpMuxOnly),
+ "rtcp-rsize" => Ok(SdpAttribute::RtcpRsize),
+ "sendonly" => Ok(SdpAttribute::Sendonly),
+ "sendrecv" => Ok(SdpAttribute::Sendrecv),
+ "ssrc-group" => parse_ssrc_group(val),
+ "sctp-port" => parse_sctp_port(val),
+ "candidate" => parse_candidate(val),
+ "extmap" => parse_extmap(val),
+ "fingerprint" => parse_fingerprint(val),
+ "fmtp" => parse_fmtp(val),
+ "group" => parse_group(val),
+ "ice-options" => parse_ice_options(val),
+ "msid" => parse_msid(val),
+ "remote-candidates" => parse_remote_candidates(val),
+ "rtpmap" => parse_rtpmap(val),
+ "rtcp" => parse_rtcp(val),
+ "rtcp-fb" => parse_rtcp_fb(val),
+ "sctpmap" => parse_sctpmap(val),
+ "setup" => parse_setup(val),
+ "simulcast" => parse_simulcast(val),
+ "ssrc" => parse_ssrc(val),
+ _ => Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown attribute type {name}",
+ ))),
+ }
+ }
+}
+
+impl fmt::Display for SdpAttribute {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let attr_type_name = SdpAttributeType::from(self).to_string();
+ let attr_to_string = |attr_str: String| attr_type_name + ":" + &attr_str;
+ match *self {
+ SdpAttribute::BundleOnly => SdpAttributeType::BundleOnly.to_string(),
+ SdpAttribute::Candidate(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::DtlsMessage(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::EndOfCandidates => SdpAttributeType::EndOfCandidates.to_string(),
+ SdpAttribute::Extmap(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::ExtmapAllowMixed => SdpAttributeType::ExtmapAllowMixed.to_string(),
+ SdpAttribute::Fingerprint(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Fmtp(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Group(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::IceLite => SdpAttributeType::IceLite.to_string(),
+ SdpAttribute::IceMismatch => SdpAttributeType::IceMismatch.to_string(),
+ SdpAttribute::IceOptions(ref a) => attr_to_string(a.join(" ")),
+ SdpAttribute::IcePacing(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::IcePwd(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::IceUfrag(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Identity(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::ImageAttr(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Inactive => SdpAttributeType::Inactive.to_string(),
+ SdpAttribute::Label(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::MaxMessageSize(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::MaxPtime(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Mid(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Msid(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::MsidSemantic(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Ptime(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Rid(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Recvonly => SdpAttributeType::Recvonly.to_string(),
+ SdpAttribute::RemoteCandidate(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Rtpmap(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Rtcp(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Rtcpfb(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::RtcpMux => SdpAttributeType::RtcpMux.to_string(),
+ SdpAttribute::RtcpMuxOnly => SdpAttributeType::RtcpMuxOnly.to_string(),
+ SdpAttribute::RtcpRsize => SdpAttributeType::RtcpRsize.to_string(),
+ SdpAttribute::Sctpmap(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::SctpPort(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Sendonly => SdpAttributeType::Sendonly.to_string(),
+ SdpAttribute::Sendrecv => SdpAttributeType::Sendrecv.to_string(),
+ SdpAttribute::Setup(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Simulcast(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::Ssrc(ref a) => attr_to_string(a.to_string()),
+ SdpAttribute::SsrcGroup(ref a, ref ssrcs) => {
+ let stringified_ssrcs: Vec<String> =
+ ssrcs.iter().map(|ssrc| ssrc.to_string()).collect();
+ attr_to_string(a.to_string()) + " " + &stringified_ssrcs.join(" ")
+ }
+ }
+ .fmt(f)
+ }
+}
+
+impl AnonymizingClone for SdpAttribute {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ match self {
+ SdpAttribute::Candidate(i) => SdpAttribute::Candidate(i.masked_clone(anon)),
+ SdpAttribute::Fingerprint(i) => SdpAttribute::Fingerprint(i.masked_clone(anon)),
+ SdpAttribute::IcePwd(i) => SdpAttribute::IcePwd(anon.mask_ice_password(i)),
+ SdpAttribute::IceUfrag(i) => SdpAttribute::IceUfrag(anon.mask_ice_user(i)),
+ SdpAttribute::RemoteCandidate(i) => SdpAttribute::RemoteCandidate(i.masked_clone(anon)),
+ SdpAttribute::Ssrc(i) => SdpAttribute::Ssrc(i.masked_clone(anon)),
+ _ => self.clone(),
+ }
+ }
+}
+
+#[derive(Clone, PartialEq)]
+pub enum SdpAttributeType {
+ BundleOnly,
+ Candidate,
+ DtlsMessage,
+ EndOfCandidates,
+ Extmap,
+ ExtmapAllowMixed,
+ Fingerprint,
+ Fmtp,
+ Group,
+ IceLite,
+ IceMismatch,
+ IceOptions,
+ IcePacing,
+ IcePwd,
+ IceUfrag,
+ Identity,
+ ImageAttr,
+ Inactive,
+ Label,
+ MaxMessageSize,
+ MaxPtime,
+ Mid,
+ Msid,
+ MsidSemantic,
+ Ptime,
+ Rid,
+ Recvonly,
+ RemoteCandidate,
+ Rtpmap,
+ Rtcp,
+ Rtcpfb,
+ RtcpMux,
+ RtcpMuxOnly,
+ RtcpRsize,
+ Sctpmap,
+ SctpPort,
+ Sendonly,
+ Sendrecv,
+ Setup,
+ Simulcast,
+ Ssrc,
+ SsrcGroup,
+}
+
+impl<'a> From<&'a SdpAttribute> for SdpAttributeType {
+ fn from(other: &SdpAttribute) -> Self {
+ match *other {
+ SdpAttribute::BundleOnly { .. } => SdpAttributeType::BundleOnly,
+ SdpAttribute::Candidate { .. } => SdpAttributeType::Candidate,
+ SdpAttribute::DtlsMessage { .. } => SdpAttributeType::DtlsMessage,
+ SdpAttribute::EndOfCandidates { .. } => SdpAttributeType::EndOfCandidates,
+ SdpAttribute::Extmap { .. } => SdpAttributeType::Extmap,
+ SdpAttribute::ExtmapAllowMixed { .. } => SdpAttributeType::ExtmapAllowMixed,
+ SdpAttribute::Fingerprint { .. } => SdpAttributeType::Fingerprint,
+ SdpAttribute::Fmtp { .. } => SdpAttributeType::Fmtp,
+ SdpAttribute::Group { .. } => SdpAttributeType::Group,
+ SdpAttribute::IceLite { .. } => SdpAttributeType::IceLite,
+ SdpAttribute::IceMismatch { .. } => SdpAttributeType::IceMismatch,
+ SdpAttribute::IceOptions { .. } => SdpAttributeType::IceOptions,
+ SdpAttribute::IcePacing { .. } => SdpAttributeType::IcePacing,
+ SdpAttribute::IcePwd { .. } => SdpAttributeType::IcePwd,
+ SdpAttribute::IceUfrag { .. } => SdpAttributeType::IceUfrag,
+ SdpAttribute::Identity { .. } => SdpAttributeType::Identity,
+ SdpAttribute::ImageAttr { .. } => SdpAttributeType::ImageAttr,
+ SdpAttribute::Inactive { .. } => SdpAttributeType::Inactive,
+ SdpAttribute::Label { .. } => SdpAttributeType::Label,
+ SdpAttribute::MaxMessageSize { .. } => SdpAttributeType::MaxMessageSize,
+ SdpAttribute::MaxPtime { .. } => SdpAttributeType::MaxPtime,
+ SdpAttribute::Mid { .. } => SdpAttributeType::Mid,
+ SdpAttribute::Msid { .. } => SdpAttributeType::Msid,
+ SdpAttribute::MsidSemantic { .. } => SdpAttributeType::MsidSemantic,
+ SdpAttribute::Ptime { .. } => SdpAttributeType::Ptime,
+ SdpAttribute::Rid { .. } => SdpAttributeType::Rid,
+ SdpAttribute::Recvonly { .. } => SdpAttributeType::Recvonly,
+ SdpAttribute::RemoteCandidate { .. } => SdpAttributeType::RemoteCandidate,
+ SdpAttribute::Rtcp { .. } => SdpAttributeType::Rtcp,
+ SdpAttribute::Rtcpfb { .. } => SdpAttributeType::Rtcpfb,
+ SdpAttribute::RtcpMux { .. } => SdpAttributeType::RtcpMux,
+ SdpAttribute::RtcpMuxOnly { .. } => SdpAttributeType::RtcpMuxOnly,
+ SdpAttribute::RtcpRsize { .. } => SdpAttributeType::RtcpRsize,
+ SdpAttribute::Rtpmap { .. } => SdpAttributeType::Rtpmap,
+ SdpAttribute::Sctpmap { .. } => SdpAttributeType::Sctpmap,
+ SdpAttribute::SctpPort { .. } => SdpAttributeType::SctpPort,
+ SdpAttribute::Sendonly { .. } => SdpAttributeType::Sendonly,
+ SdpAttribute::Sendrecv { .. } => SdpAttributeType::Sendrecv,
+ SdpAttribute::Setup { .. } => SdpAttributeType::Setup,
+ SdpAttribute::Simulcast { .. } => SdpAttributeType::Simulcast,
+ SdpAttribute::Ssrc { .. } => SdpAttributeType::Ssrc,
+ SdpAttribute::SsrcGroup { .. } => SdpAttributeType::SsrcGroup,
+ }
+ }
+}
+
+impl fmt::Display for SdpAttributeType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpAttributeType::BundleOnly => "bundle-only",
+ SdpAttributeType::Candidate => "candidate",
+ SdpAttributeType::DtlsMessage => "dtls-message",
+ SdpAttributeType::EndOfCandidates => "end-of-candidates",
+ SdpAttributeType::Extmap => "extmap",
+ SdpAttributeType::ExtmapAllowMixed => "extmap-allow-mixed",
+ SdpAttributeType::Fingerprint => "fingerprint",
+ SdpAttributeType::Fmtp => "fmtp",
+ SdpAttributeType::Group => "group",
+ SdpAttributeType::IceLite => "ice-lite",
+ SdpAttributeType::IceMismatch => "ice-mismatch",
+ SdpAttributeType::IceOptions => "ice-options",
+ SdpAttributeType::IcePacing => "ice-pacing",
+ SdpAttributeType::IcePwd => "ice-pwd",
+ SdpAttributeType::IceUfrag => "ice-ufrag",
+ SdpAttributeType::Identity => "identity",
+ SdpAttributeType::ImageAttr => "imageattr",
+ SdpAttributeType::Inactive => "inactive",
+ SdpAttributeType::Label => "label",
+ SdpAttributeType::MaxMessageSize => "max-message-size",
+ SdpAttributeType::MaxPtime => "maxptime",
+ SdpAttributeType::Mid => "mid",
+ SdpAttributeType::Msid => "msid",
+ SdpAttributeType::MsidSemantic => "msid-semantic",
+ SdpAttributeType::Ptime => "ptime",
+ SdpAttributeType::Rid => "rid",
+ SdpAttributeType::Recvonly => "recvonly",
+ SdpAttributeType::RemoteCandidate => "remote-candidates",
+ SdpAttributeType::Rtpmap => "rtpmap",
+ SdpAttributeType::Rtcp => "rtcp",
+ SdpAttributeType::Rtcpfb => "rtcp-fb",
+ SdpAttributeType::RtcpMux => "rtcp-mux",
+ SdpAttributeType::RtcpMuxOnly => "rtcp-mux-only",
+ SdpAttributeType::RtcpRsize => "rtcp-rsize",
+ SdpAttributeType::Sctpmap => "sctpmap",
+ SdpAttributeType::SctpPort => "sctp-port",
+ SdpAttributeType::Sendonly => "sendonly",
+ SdpAttributeType::Sendrecv => "sendrecv",
+ SdpAttributeType::Setup => "setup",
+ SdpAttributeType::Simulcast => "simulcast",
+ SdpAttributeType::Ssrc => "ssrc",
+ SdpAttributeType::SsrcGroup => "ssrc-group",
+ }
+ .fmt(f)
+ }
+}
+
+fn string_or_empty(to_parse: &str) -> Result<String, SdpParserInternalError> {
+ if to_parse.is_empty() {
+ Err(SdpParserInternalError::Generic(
+ "This attribute is required to have a value".to_string(),
+ ))
+ } else {
+ Ok(to_parse.to_string())
+ }
+}
+
+fn parse_payload_type(to_parse: &str) -> Result<SdpAttributePayloadType, SdpParserInternalError> {
+ Ok(match to_parse {
+ "*" => SdpAttributePayloadType::Wildcard,
+ _ => SdpAttributePayloadType::PayloadType(to_parse.parse::<u8>()?),
+ })
+}
+
+fn parse_single_direction(to_parse: &str) -> Result<SdpSingleDirection, SdpParserInternalError> {
+ match to_parse {
+ "send" => Ok(SdpSingleDirection::Send),
+ "recv" => Ok(SdpSingleDirection::Recv),
+ x => Err(SdpParserInternalError::Generic(format!(
+ "Unknown direction description found: '{x:}'"
+ ))),
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=ssrc-group, RFC5576
+//-------------------------------------------------------------------------
+// a=ssrc-group:<semantics> <ssrc-id> ...
+fn parse_ssrc_group(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let semantics = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Ssrc group attribute is missing semantics".to_string(),
+ ));
+ }
+ Some(x) => match x.to_uppercase().as_ref() {
+ "DUP" => SdpSsrcGroupSemantic::Duplication,
+ "FID" => SdpSsrcGroupSemantic::FlowIdentification,
+ "FEC" => SdpSsrcGroupSemantic::ForwardErrorCorrection,
+ "FEC-FR" => SdpSsrcGroupSemantic::ForwardErrorCorrectionFr,
+ "SIM" => SdpSsrcGroupSemantic::Sim,
+ unknown => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown ssrc semantic '{unknown:?}' found"
+ )));
+ }
+ },
+ };
+
+ let mut ssrcs = Vec::new();
+ for token in tokens {
+ match parse_ssrc(token) {
+ Ok(SdpAttribute::Ssrc(ssrc)) => {
+ ssrcs.push(ssrc);
+ }
+ Err(err) => {
+ return Err(err);
+ }
+ _ => unreachable!(),
+ }
+ }
+
+ if ssrcs.is_empty() {
+ return Err(SdpParserInternalError::Generic(
+ "Ssrc group must contain at least one ssrc".to_string(),
+ ));
+ }
+
+ Ok(SdpAttribute::SsrcGroup(semantics, ssrcs))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=sctp-port, draft-ietf-mmusic-sctp-sdp-26#section-15.2.1
+//-------------------------------------------------------------------------
+// no ABNF given
+fn parse_sctp_port(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let port = to_parse.parse()?;
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic(format!(
+ "Sctpport port {port} can only be a bit 16bit number"
+ )));
+ }
+ Ok(SdpAttribute::SctpPort(port))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=candidate, RFC5245
+//-------------------------------------------------------------------------
+//
+// candidate-attribute = "candidate" ":" foundation SP component-id SP
+// transport SP
+// priority SP
+// connection-address SP ;from RFC 4566
+// port ;port from RFC 4566
+// SP cand-type
+// [SP rel-addr]
+// [SP rel-port]
+// *(SP extension-att-name SP
+// extension-att-value)
+// foundation = 1*32ice-char
+// component-id = 1*5DIGIT
+// transport = "UDP" / transport-extension
+// transport-extension = token ; from RFC 3261
+// priority = 1*10DIGIT
+// cand-type = "typ" SP candidate-types
+// candidate-types = "host" / "srflx" / "prflx" / "relay" / token
+// rel-addr = "raddr" SP connection-address
+// rel-port = "rport" SP port
+// extension-att-name = byte-string ;from RFC 4566
+// extension-att-value = byte-string
+// ice-char = ALPHA / DIGIT / "+" / "/"
+fn parse_candidate(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.split_whitespace().collect();
+ if tokens.len() < 8 {
+ return Err(SdpParserInternalError::Generic(
+ "Candidate needs to have minimum eigth tokens".to_string(),
+ ));
+ }
+ let component = tokens[1].parse::<u32>()?;
+ let transport = match tokens[2].to_lowercase().as_ref() {
+ "udp" => SdpAttributeCandidateTransport::Udp,
+ "tcp" => SdpAttributeCandidateTransport::Tcp,
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Unknonw candidate transport value".to_string(),
+ ));
+ }
+ };
+ let priority = tokens[3].parse::<u64>()?;
+ let address = Address::from_str(tokens[4])?;
+ let port = tokens[5].parse::<u32>()?;
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic(
+ "ICE candidate port can only be a bit 16bit number".to_string(),
+ ));
+ }
+ match tokens[6].to_lowercase().as_ref() {
+ "typ" => (),
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Candidate attribute token must be 'typ'".to_string(),
+ ));
+ }
+ };
+ let cand_type = match tokens[7].to_lowercase().as_ref() {
+ "host" => SdpAttributeCandidateType::Host,
+ "srflx" => SdpAttributeCandidateType::Srflx,
+ "prflx" => SdpAttributeCandidateType::Prflx,
+ "relay" => SdpAttributeCandidateType::Relay,
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Unknow candidate type value".to_string(),
+ ));
+ }
+ };
+ let mut cand = SdpAttributeCandidate::new(
+ tokens[0].to_string(),
+ component,
+ transport,
+ priority,
+ address,
+ port,
+ cand_type,
+ );
+ if tokens.len() > 8 {
+ let mut index = 8;
+ while tokens.len() > index + 1 {
+ match tokens[index].to_lowercase().as_ref() {
+ "generation" => {
+ let generation = tokens[index + 1].parse::<u32>()?;
+ cand.set_generation(generation);
+ index += 2;
+ }
+ "network-cost" => {
+ let cost = tokens[index + 1].parse::<u32>()?;
+ cand.set_network_cost(cost);
+ index += 2;
+ }
+ "raddr" => {
+ let addr = parse_unicast_address(tokens[index + 1])?;
+ cand.set_remote_address(addr);
+ index += 2;
+ }
+ "rport" => {
+ let port = tokens[index + 1].parse::<u32>()?;
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic(
+ "ICE candidate rport can only be a bit 16bit number".to_string(),
+ ));
+ }
+ cand.set_remote_port(port);
+ index += 2;
+ }
+ "tcptype" => {
+ cand.set_tcp_type(match tokens[index + 1].to_lowercase().as_ref() {
+ "active" => SdpAttributeCandidateTcpType::Active,
+ "passive" => SdpAttributeCandidateTcpType::Passive,
+ "so" => SdpAttributeCandidateTcpType::Simultaneous,
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Unknown tcptype value in candidate line".to_string(),
+ ));
+ }
+ });
+ index += 2;
+ }
+ "ufrag" => {
+ let ufrag = tokens[index + 1];
+ cand.set_ufrag(ufrag.to_string());
+ index += 2;
+ }
+ _ => {
+ let name = tokens[index].to_string();
+ let value = tokens[index + 1].to_string();
+ cand.add_unknown_extension(name, value);
+ index += 2;
+ }
+ };
+ }
+ if tokens.len() > index {
+ return Err(SdpParserInternalError::Unsupported(
+ "Ice candidate extension name without value".to_string(),
+ ));
+ }
+ }
+ Ok(SdpAttribute::Candidate(cand))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=dtls-message, draft-rescorla-dtls-in-sdp
+//-------------------------------------------------------------------------
+// attribute =/ dtls-message-attribute
+//
+// dtls-message-attribute = "dtls-message" ":" role SP value
+//
+// role = "client" / "server"
+//
+// value = 1*(ALPHA / DIGIT / "+" / "/" / "=" )
+// ; base64 encoded message
+fn parse_dtls_message(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.split(' ').collect();
+
+ if tokens.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "dtls-message must have a role token and a value token.".to_string(),
+ ));
+ }
+
+ Ok(SdpAttribute::DtlsMessage(match tokens[0] {
+ "client" => SdpAttributeDtlsMessage::Client(tokens[1].to_string()),
+ "server" => SdpAttributeDtlsMessage::Server(tokens[1].to_string()),
+ e => {
+ return Err(SdpParserInternalError::Generic(format!(
+ "dtls-message has unknown role token '{e}'"
+ )));
+ }
+ }))
+}
+
+// Returns true if valid byte-string as defined by RFC 4566
+// https://tools.ietf.org/html/rfc4566
+fn valid_byte_string(input: &str) -> bool {
+ !(input.contains(0x00 as char) || input.contains(0x0A as char) || input.contains(0x0D as char))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=extmap, RFC5285
+//-------------------------------------------------------------------------
+// RFC5285
+// extmap = mapentry SP extensionname [SP extensionattributes]
+//
+// extensionname = URI
+//
+// direction = "sendonly" / "recvonly" / "sendrecv" / "inactive"
+//
+// mapentry = "extmap:" 1*5DIGIT ["/" direction]
+//
+// extensionattributes = byte-string
+//
+// URI = <Defined in RFC 3986>
+//
+// byte-string = <Defined in RFC 4566>
+//
+// SP = <Defined in RFC 5234>
+//
+// DIGIT = <Defined in RFC 5234>
+fn parse_extmap(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.split_whitespace().collect();
+ if tokens.len() < 2 {
+ return Err(SdpParserInternalError::Generic(
+ "Extmap needs to have at least two tokens".to_string(),
+ ));
+ }
+ let id: u16;
+ let mut direction: Option<SdpAttributeDirection> = None;
+ if tokens[0].find('/').is_none() {
+ id = tokens[0].parse::<u16>()?;
+ } else {
+ let id_dir: Vec<&str> = tokens[0].splitn(2, '/').collect();
+ id = id_dir[0].parse::<u16>()?;
+ direction = Some(match id_dir[1].to_lowercase().as_ref() {
+ "recvonly" => SdpAttributeDirection::Recvonly,
+ "sendonly" => SdpAttributeDirection::Sendonly,
+ "sendrecv" => SdpAttributeDirection::Sendrecv,
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Unsupported direction in extmap value".to_string(),
+ ));
+ }
+ })
+ }
+ // Consider replacing to_parse.split_whitespace() above with splitn on space. Would we want the pattern to split on any amout of any kind of whitespace?
+ let extension_attributes = if tokens.len() == 2 {
+ None
+ } else {
+ let ext_string: String = tokens[2..].join(" ");
+ if !valid_byte_string(&ext_string) {
+ return Err(SdpParserInternalError::Generic(
+ "Illegal character in extmap extension attributes".to_string(),
+ ));
+ }
+ Some(ext_string)
+ };
+ Ok(SdpAttribute::Extmap(SdpAttributeExtmap {
+ id,
+ direction,
+ url: tokens[1].to_string(),
+ extension_attributes,
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=fingerprint, RFC4572
+//-------------------------------------------------------------------------
+// fingerprint-attribute = "fingerprint" ":" hash-func SP fingerprint
+//
+// hash-func = "sha-1" / "sha-224" / "sha-256" /
+// "sha-384" / "sha-512" /
+// "md5" / "md2" / token
+// ; Additional hash functions can only come
+// ; from updates to RFC 3279
+//
+// fingerprint = 2UHEX *(":" 2UHEX)
+// ; Each byte in upper-case hex, separated
+// ; by colons.
+//
+// UHEX = DIGIT / %x41-46 ; A-F uppercase
+fn parse_fingerprint(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.split_whitespace().collect();
+ if tokens.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "Fingerprint needs to have two tokens".to_string(),
+ ));
+ }
+
+ let hash_algorithm = SdpAttributeFingerprintHashType::try_from_name(tokens[0])?;
+ let bytes = hash_algorithm.parse_octets(tokens[1])?;
+ let fingerprint = SdpAttributeFingerprint::try_from((hash_algorithm, bytes))?;
+ Ok(SdpAttribute::Fingerprint(fingerprint))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=fmtp, RFC4566, RFC5576
+//-------------------------------------------------------------------------
+// a=fmtp:<format> <format specific parameters>
+fn parse_fmtp(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.splitn(2, ' ').collect();
+
+ // Support space seperated parameter blocks
+ if tokens.len() < 2 {
+ return Err(SdpParserInternalError::Unsupported(
+ "Fmtp attributes require a payload type and a parameter block.".to_string(),
+ ));
+ }
+
+ let payload_token = tokens[0];
+
+ // Default initiliaze SdpAttributeFmtpParameters
+ let mut parameters = SdpAttributeFmtpParameters {
+ packetization_mode: 0,
+ level_asymmetry_allowed: false,
+ profile_level_id: 0x0042_0010,
+ max_fs: 0,
+ max_cpb: 0,
+ max_dpb: 0,
+ max_br: 0,
+ max_mbps: 0,
+ usedtx: false,
+ stereo: false,
+ useinbandfec: false,
+ cbr: false,
+ max_fr: 0,
+ maxplaybackrate: 48000,
+ maxaveragebitrate: 0,
+ ptime: 0,
+ minptime: 0,
+ maxptime: 0,
+ encodings: Vec::new(),
+ dtmf_tones: "".to_string(),
+ rtx: None,
+ unknown_tokens: Vec::new(),
+ };
+
+ for parameter_token in tokens[1..].iter() {
+ if parameter_token.contains('=') {
+ // Permit Leading/Trailing/Inner ';' by filtering out empty splits
+ let parameter_tokens: Vec<&str> = parameter_token
+ .split(';')
+ .filter(|token| !token.is_empty())
+ .collect();
+ for parameter_token in parameter_tokens.iter() {
+ let name_value_pair: Vec<&str> = parameter_token.splitn(2, '=').collect();
+ if name_value_pair.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "A fmtp parameter must be either a telephone event, a parameter list or a red codec list"
+ .to_string(),
+ ));
+ }
+
+ let parse_bool =
+ |val: &str, param_name: &str| -> Result<bool, SdpParserInternalError> {
+ match val.parse::<u8>()? {
+ 0 => Ok(false),
+ 1 => Ok(true),
+ _ => Err(SdpParserInternalError::Generic(format!(
+ "The fmtp parameter '{param_name:}' must be 0 or 1"
+ ))),
+ }
+ };
+
+ let parameter_name = name_value_pair[0];
+ let parameter_val = name_value_pair[1];
+
+ match parameter_name.to_uppercase().as_str() {
+ // H264
+ "PROFILE-LEVEL-ID" => parameters.profile_level_id = match u32::from_str_radix(
+ parameter_val,
+ 16,
+ )? {
+ x @ 0..=0x00ff_ffff => x,
+ _ => return Err(SdpParserInternalError::Generic(
+ "The fmtp parameter 'profile-level-id' must be in range [0,0xffffff]"
+ .to_string(),
+ )),
+ },
+ "PACKETIZATION-MODE" => {
+ parameters.packetization_mode = match parameter_val.parse::<u32>()? {
+ x @ 0..=2 => x,
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "The fmtp parameter 'packetization-mode' must be 0,1 or 2"
+ .to_string(),
+ ));
+ }
+ }
+ }
+ "LEVEL-ASYMMETRY-ALLOWED" => {
+ parameters.level_asymmetry_allowed =
+ parse_bool(parameter_val, "level-asymmetry-allowed")?
+ }
+ "MAX-MBPS" => parameters.max_mbps = parameter_val.parse::<u32>()?,
+ "MAX-FS" => parameters.max_fs = parameter_val.parse::<u32>()?,
+ "MAX-CPB" => parameters.max_cpb = parameter_val.parse::<u32>()?,
+ "MAX-DPB" => parameters.max_dpb = parameter_val.parse::<u32>()?,
+ "MAX-BR" => parameters.max_br = parameter_val.parse::<u32>()?,
+
+ // VP8 and VP9
+ "MAX-FR" => parameters.max_fr = parameter_val.parse::<u32>()?,
+
+ //Opus https://tools.ietf.org/html/rfc7587
+ "MAXPLAYBACKRATE" => {
+ parameters.maxplaybackrate = parameter_val.parse::<u32>()?
+ }
+ "MAXAVERAGEBITRATE" => {
+ parameters.maxaveragebitrate = parameter_val.parse::<u32>()?
+ }
+ "PTIME" => parameters.ptime = parameter_val.parse::<u32>()?,
+ "MAXPTIME" => parameters.maxptime = parameter_val.parse::<u32>()?,
+ "MINPTIME" => parameters.minptime = parameter_val.parse::<u32>()?,
+ "USEDTX" => parameters.usedtx = parse_bool(parameter_val, "usedtx")?,
+ "STEREO" => parameters.stereo = parse_bool(parameter_val, "stereo")?,
+ "USEINBANDFEC" => {
+ parameters.useinbandfec = parse_bool(parameter_val, "useinbandfec")?
+ }
+ "CBR" => parameters.cbr = parse_bool(parameter_val, "cbr")?,
+ "APT" => {
+ parameters.rtx = Some(RtxFmtpParameters {
+ apt: parameter_val.parse::<u8>()?,
+ rtx_time: None,
+ })
+ }
+ "RTX-TIME" => {
+ if let Some(ref mut rtx) = parameters.rtx {
+ rtx.rtx_time = Some(parameter_val.parse::<u32>()?)
+ } else {
+ return Err(SdpParserInternalError::Generic(
+ "RTX codec must have an APT field".to_string(),
+ ));
+ }
+ }
+ _ => parameters
+ .unknown_tokens
+ .push((*parameter_token).to_string()),
+ }
+ }
+ } else if parameter_token.contains('/') {
+ let encodings: Vec<&str> = parameter_token.split('/').collect();
+
+ for encoding in encodings {
+ match encoding.parse::<u8>()? {
+ x @ 0..=128 => parameters.encodings.push(x),
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Red codec must be in range [0,128]".to_string(),
+ ));
+ }
+ }
+ }
+ } else {
+ // This is the case for the 'telephone-event' codec
+ let dtmf_tones: Vec<&str> = parameter_token.split(',').collect();
+ let mut dtmf_tone_is_ok = true;
+
+ // This closure verifies the output of some_number_as_string.parse::<u8>().ok() like calls
+ let validate_digits = |digit_option: Option<u8>| -> Option<u8> {
+ match digit_option {
+ Some(x) => match x {
+ 0..=100 => Some(x),
+ _ => None,
+ },
+ None => None,
+ }
+ };
+
+ // This loop does some sanity checking on the passed dtmf tones
+ for dtmf_tone in dtmf_tones {
+ let dtmf_tone_range: Vec<&str> = dtmf_tone.splitn(2, '-').collect();
+
+ dtmf_tone_is_ok = match dtmf_tone_range.len() {
+ // In this case the dtmf tone is a range
+ 2 => {
+ match validate_digits(dtmf_tone_range[0].parse::<u8>().ok()) {
+ Some(l) => match validate_digits(dtmf_tone_range[1].parse::<u8>().ok())
+ {
+ Some(u) => {
+ // Check that the first part of the range is smaller than the second part
+ l < u
+ }
+ None => false,
+ },
+ None => false,
+ }
+ }
+ // In this case the dtmf tone is a single tone
+ 1 => validate_digits(dtmf_tone.parse::<u8>().ok()).is_some(),
+ _ => false,
+ };
+
+ if !dtmf_tone_is_ok {
+ break;
+ }
+ }
+
+ // Set the parsed dtmf tones or in case the parsing was insuccessfull, set it to the default "0-15"
+ parameters.dtmf_tones = if dtmf_tone_is_ok {
+ (*parameter_token).to_string()
+ } else {
+ "0-15".to_string()
+ };
+ }
+ }
+ Ok(SdpAttribute::Fmtp(SdpAttributeFmtp {
+ payload_type: payload_token.parse::<u8>()?,
+ parameters,
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=group, RFC5888
+//-------------------------------------------------------------------------
+// group-attribute = "a=group:" semantics
+// *(SP identification-tag)
+// semantics = "LS" / "FID" / semantics-extension
+// semantics-extension = token
+// identification-tag = token
+fn parse_group(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let semantics = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Group attribute is missing semantics token".to_string(),
+ ));
+ }
+ Some(x) => match x.to_uppercase().as_ref() {
+ "LS" => SdpAttributeGroupSemantic::LipSynchronization,
+ "FID" => SdpAttributeGroupSemantic::FlowIdentification,
+ "SRF" => SdpAttributeGroupSemantic::SingleReservationFlow,
+ "ANAT" => SdpAttributeGroupSemantic::AlternateNetworkAddressType,
+ "FEC" => SdpAttributeGroupSemantic::ForwardErrorCorrection,
+ "DDP" => SdpAttributeGroupSemantic::DecodingDependency,
+ "BUNDLE" => SdpAttributeGroupSemantic::Bundle,
+ unknown => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown group semantic '{unknown:?}' found",
+ )));
+ }
+ },
+ };
+ Ok(SdpAttribute::Group(SdpAttributeGroup {
+ semantics,
+ tags: tokens.map(ToString::to_string).collect(),
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=ice-options, draft-ietf-mmusic-ice-sip-sdp
+//-------------------------------------------------------------------------
+// ice-options = "ice-options:" ice-option-tag
+// 0*(SP ice-option-tag)
+// ice-option-tag = 1*ice-char
+fn parse_ice_options(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ if to_parse.is_empty() {
+ return Err(SdpParserInternalError::Generic(
+ "ice-options is required to have a value".to_string(),
+ ));
+ }
+ Ok(SdpAttribute::IceOptions(
+ to_parse
+ .split_whitespace()
+ .map(ToString::to_string)
+ .collect(),
+ ))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=ice-pacing, draft-ietf-mmusic-ice-sip-sdp
+//-------------------------------------------------------------------------
+// ice-pacing-att = "ice-pacing:" pacing-value
+// pacing-value = 1*10DIGIT
+fn parse_ice_pacing(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let parsed = to_parse.parse::<u64>()?;
+ if parsed >= 1_00_00_00_00_00 {
+ return Err(SdpParserInternalError::Generic(
+ "ice-pacing value is not a 10 digit integer".to_string(),
+ ));
+ }
+ Ok(SdpAttribute::IcePacing(parsed))
+}
+
+fn parse_imageattr_tokens(to_parse: &str, separator: char) -> Vec<String> {
+ let mut tokens = Vec::new();
+ let mut open_braces_counter = 0;
+ let mut current_tokens = Vec::new();
+
+ for token in to_parse.split(separator) {
+ if token.contains('[') {
+ open_braces_counter += 1;
+ }
+ if token.contains(']') {
+ open_braces_counter -= 1;
+ }
+
+ current_tokens.push(token.to_string());
+
+ if open_braces_counter == 0 {
+ tokens.push(current_tokens.join(&separator.to_string()));
+ current_tokens = Vec::new();
+ }
+ }
+
+ tokens
+}
+
+fn parse_imagettr_braced_token(to_parse: &str) -> Option<&str> {
+ if !to_parse.starts_with('[') {
+ return None;
+ }
+
+ if !to_parse.ends_with(']') {
+ return None;
+ }
+
+ Some(&to_parse[1..to_parse.len() - 1])
+}
+
+fn parse_image_attr_xyrange(
+ to_parse: &str,
+) -> Result<SdpAttributeImageAttrXyRange, SdpParserInternalError> {
+ if to_parse.starts_with('[') {
+ let value_tokens = parse_imagettr_braced_token(to_parse).ok_or_else(|| {
+ SdpParserInternalError::Generic(
+ "imageattr's xyrange has no closing tag ']'".to_string(),
+ )
+ })?;
+
+ if to_parse.contains(':') {
+ // Range values
+ let range_tokens: Vec<&str> = value_tokens.split(':').collect();
+
+ if range_tokens.len() == 3 {
+ Ok(SdpAttributeImageAttrXyRange::Range(
+ range_tokens[0].parse::<u32>()?,
+ range_tokens[2].parse::<u32>()?,
+ Some(range_tokens[1].parse::<u32>()?),
+ ))
+ } else if range_tokens.len() == 2 {
+ Ok(SdpAttributeImageAttrXyRange::Range(
+ range_tokens[0].parse::<u32>()?,
+ range_tokens[1].parse::<u32>()?,
+ None,
+ ))
+ } else {
+ Err(SdpParserInternalError::Generic(
+ "imageattr's xyrange must contain 2 or 3 fields".to_string(),
+ ))
+ }
+ } else {
+ // Discrete values
+ let values = value_tokens
+ .split(',')
+ .map(str::parse::<u32>)
+ .collect::<Result<Vec<u32>, _>>()?;
+
+ if values.len() < 2 {
+ return Err(SdpParserInternalError::Generic(
+ "imageattr's discrete value list must have at least two elements".to_string(),
+ ));
+ }
+
+ Ok(SdpAttributeImageAttrXyRange::DiscreteValues(values))
+ }
+ } else {
+ Ok(SdpAttributeImageAttrXyRange::DiscreteValues(vec![
+ to_parse.parse::<u32>()?
+ ]))
+ }
+}
+
+fn parse_image_attr_set(
+ to_parse: &str,
+) -> Result<SdpAttributeImageAttrSet, SdpParserInternalError> {
+ let mut tokens = parse_imageattr_tokens(to_parse, ',').into_iter();
+
+ let x_token = tokens.next().ok_or_else(|| {
+ SdpParserInternalError::Generic("imageattr set is missing the 'x=' token".to_string())
+ })?;
+ if !x_token.starts_with("x=") {
+ return Err(SdpParserInternalError::Generic(
+ "The first token in an imageattr set must begin with 'x='".to_string(),
+ ));
+ }
+ let x = parse_image_attr_xyrange(&x_token[2..])?;
+
+ let y_token = tokens.next().ok_or_else(|| {
+ SdpParserInternalError::Generic("imageattr set is missing the 'y=' token".to_string())
+ })?;
+ if !y_token.starts_with("y=") {
+ return Err(SdpParserInternalError::Generic(
+ "The second token in an imageattr set must begin with 'y='".to_string(),
+ ));
+ }
+ let y = parse_image_attr_xyrange(&y_token[2..])?;
+
+ let mut sar = None;
+ let mut par = None;
+ let mut q = None;
+
+ let parse_ps_range = |resolution_range: &str| -> Result<(f32, f32), SdpParserInternalError> {
+ let minmax_pair: Vec<&str> = resolution_range.split('-').collect();
+
+ if minmax_pair.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "imageattr's par and sar ranges must have two components".to_string(),
+ ));
+ }
+
+ let min = minmax_pair[0].parse::<f32>()?;
+ let max = minmax_pair[1].parse::<f32>()?;
+
+ if min >= max {
+ return Err(SdpParserInternalError::Generic(
+ "In imageattr's par and sar ranges, first must be < than the second".to_string(),
+ ));
+ }
+
+ Ok((min, max))
+ };
+
+ for current_token in tokens {
+ if let Some(value_token) = current_token.strip_prefix("sar=") {
+ if value_token.starts_with('[') {
+ let sar_values = parse_imagettr_braced_token(value_token).ok_or_else(|| {
+ SdpParserInternalError::Generic(
+ "imageattr's sar value is missing closing tag ']'".to_string(),
+ )
+ })?;
+
+ if value_token.contains('-') {
+ // Range
+ let range = parse_ps_range(sar_values)?;
+ sar = Some(SdpAttributeImageAttrSRange::Range(range.0, range.1))
+ } else if value_token.contains(',') {
+ // Discrete values
+ let values = sar_values
+ .split(',')
+ .map(str::parse::<f32>)
+ .collect::<Result<Vec<f32>, _>>()?;
+
+ if values.len() < 2 {
+ return Err(SdpParserInternalError::Generic(
+ "imageattr's sar discrete value list must have at least two values"
+ .to_string(),
+ ));
+ }
+
+ // Check that all the values are ascending
+ let mut last_value = 0.0;
+ for value in &values {
+ if last_value >= *value {
+ return Err(SdpParserInternalError::Generic(
+ "imageattr's sar discrete value list must contain ascending values"
+ .to_string(),
+ ));
+ }
+ last_value = *value;
+ }
+ sar = Some(SdpAttributeImageAttrSRange::DiscreteValues(values))
+ }
+ } else {
+ sar = Some(SdpAttributeImageAttrSRange::DiscreteValues(vec![
+ value_token.parse::<f32>()?,
+ ]))
+ }
+ } else if let Some(braced_value_token) = current_token.strip_prefix("par=") {
+ if !braced_value_token.starts_with('[') {
+ return Err(SdpParserInternalError::Generic(
+ "imageattr's par value must start with '['".to_string(),
+ ));
+ }
+
+ let par_values = parse_imagettr_braced_token(braced_value_token).ok_or_else(|| {
+ SdpParserInternalError::Generic(
+ "imageattr's par value must be enclosed with ']'".to_string(),
+ )
+ })?;
+ let range = parse_ps_range(par_values)?;
+ par = Some(SdpAttributeImageAttrPRange {
+ min: range.0,
+ max: range.1,
+ })
+ } else if let Some(qval) = current_token.strip_prefix("q=") {
+ q = Some(qval.parse::<f32>()?);
+ }
+ }
+
+ Ok(SdpAttributeImageAttrSet { x, y, sar, par, q })
+}
+
+fn parse_image_attr_set_list<I>(
+ tokens: &mut iter::Peekable<I>,
+) -> Result<SdpAttributeImageAttrSetList, SdpParserInternalError>
+where
+ I: Iterator<Item = String> + Clone,
+{
+ let parse_set = |set_token: &str| -> Result<SdpAttributeImageAttrSet, SdpParserInternalError> {
+ parse_image_attr_set(parse_imagettr_braced_token(set_token).ok_or_else(|| {
+ SdpParserInternalError::Generic("imageattr sets must be enclosed by ']'".to_string())
+ })?)
+ };
+
+ match tokens
+ .next()
+ .ok_or_else(|| {
+ SdpParserInternalError::Generic(
+ "imageattr must have a parameter set after a direction token".to_string(),
+ )
+ })?
+ .as_str()
+ {
+ "*" => Ok(SdpAttributeImageAttrSetList::Wildcard),
+ x => {
+ let mut sets = vec![parse_set(x)?];
+ while let Some(set_str) = tokens.clone().peek() {
+ if set_str.starts_with('[') {
+ sets.push(parse_set(&tokens.next().unwrap())?);
+ } else {
+ break;
+ }
+ }
+
+ Ok(SdpAttributeImageAttrSetList::Sets(sets))
+ }
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=imageattr, RFC6236
+//-------------------------------------------------------------------------
+// image-attr = "imageattr:" PT 1*2( 1*WSP ( "send" / "recv" )
+// 1*WSP attr-list )
+// PT = 1*DIGIT / "*"
+// attr-list = ( set *(1*WSP set) ) / "*"
+// ; WSP and DIGIT defined in [RFC5234]
+//
+// set= "[" "x=" xyrange "," "y=" xyrange *( "," key-value ) "]"
+// ; x is the horizontal image size range (pixel count)
+// ; y is the vertical image size range (pixel count)
+//
+// key-value = ( "sar=" srange )
+// / ( "par=" prange )
+// / ( "q=" qvalue )
+// ; Key-value MAY be extended with other keyword
+// ; parameters.
+// ; At most, one instance each of sar, par, or q
+// ; is allowed in a set.
+// ;
+// ; sar (sample aspect ratio) is the sample aspect ratio
+// ; associated with the set (optional, MAY be ignored)
+// ; par (picture aspect ratio) is the allowed
+// ; ratio between the display's x and y physical
+// ; size (optional)
+// ; q (optional, range [0.0..1.0], default value 0.5)
+// ; is the preference for the given set,
+// ; a higher value means a higher preference
+//
+// onetonine = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
+// ; Digit between 1 and 9
+// xyvalue = onetonine *5DIGIT
+// ; Digit between 1 and 9 that is
+// ; followed by 0 to 5 other digits
+// step = xyvalue
+// xyrange = ( "[" xyvalue ":" [ step ":" ] xyvalue "]" )
+// ; Range between a lower and an upper value
+// ; with an optional step, default step = 1
+// ; The rightmost occurrence of xyvalue MUST have a
+// ; higher value than the leftmost occurrence.
+// / ( "[" xyvalue 1*( "," xyvalue ) "]" )
+// ; Discrete values separated by ','
+// / ( xyvalue )
+// ; A single value
+// spvalue = ( "0" "." onetonine *3DIGIT )
+// ; Values between 0.1000 and 0.9999
+// / ( onetonine "." 1*4DIGIT )
+// ; Values between 1.0000 and 9.9999
+// srange = ( "[" spvalue 1*( "," spvalue ) "]" )
+// ; Discrete values separated by ','.
+// ; Each occurrence of spvalue MUST be
+// ; greater than the previous occurrence.
+// / ( "[" spvalue "-" spvalue "]" )
+// ; Range between a lower and an upper level (inclusive)
+// ; The second occurrence of spvalue MUST have a higher
+// ; value than the first
+// / ( spvalue )
+// ; A single value
+//
+// prange = ( "[" spvalue "-" spvalue "]" )
+// ; Range between a lower and an upper level (inclusive)
+// ; The second occurrence of spvalue MUST have a higher
+// ; value than the first
+//
+// qvalue = ( "0" "." 1*2DIGIT )
+// / ( "1" "." 1*2("0") )
+// ; Values between 0.00 and 1.00
+fn parse_image_attr(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = parse_imageattr_tokens(to_parse, ' ').into_iter().peekable();
+
+ let pt = parse_payload_type(
+ tokens
+ .next()
+ .ok_or_else(|| {
+ SdpParserInternalError::Generic("imageattr requires a payload token".to_string())
+ })?
+ .as_str(),
+ )?;
+ let first_direction = parse_single_direction(
+ tokens
+ .next()
+ .ok_or_else(|| {
+ SdpParserInternalError::Generic(
+ "imageattr's second token must be a direction token".to_string(),
+ )
+ })?
+ .as_str(),
+ )?;
+
+ let first_set_list = parse_image_attr_set_list(&mut tokens)?;
+
+ let mut second_set_list = SdpAttributeImageAttrSetList::Sets(Vec::new());
+
+ // Check if there is a second direction defined
+ if let Some(direction_token) = tokens.next() {
+ if parse_single_direction(direction_token.as_str())? == first_direction {
+ return Err(SdpParserInternalError::Generic(
+ "imageattr's second direction token must be different from the first one"
+ .to_string(),
+ ));
+ }
+
+ second_set_list = parse_image_attr_set_list(&mut tokens)?;
+ }
+
+ if tokens.next().is_some() {
+ return Err(SdpParserInternalError::Generic(
+ "imageattr must not contain any token after the second set list".to_string(),
+ ));
+ }
+
+ Ok(SdpAttribute::ImageAttr(match first_direction {
+ SdpSingleDirection::Send => SdpAttributeImageAttr {
+ pt,
+ send: first_set_list,
+ recv: second_set_list,
+ },
+ SdpSingleDirection::Recv => SdpAttributeImageAttr {
+ pt,
+ send: second_set_list,
+ recv: first_set_list,
+ },
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=msid, draft-ietf-mmusic-msid
+//-------------------------------------------------------------------------
+// msid-attr = "msid:" identifier [ SP appdata ]
+// identifier = 1*64token-char ; see RFC 4566
+// appdata = 1*64token-char ; see RFC 4566
+fn parse_msid(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let id = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Msid attribute is missing msid-id token".to_string(),
+ ));
+ }
+ Some(x) => x.to_string(),
+ };
+ let appdata = tokens.next().map(|x| x.to_string());
+ Ok(SdpAttribute::Msid(SdpAttributeMsid { id, appdata }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=msid-semantic, draft-ietf-mmusic-msid
+//-------------------------------------------------------------------------
+// msid-semantic-attr = "msid-semantic:" msid-semantic msid-list
+// msid-semantic = token ; see RFC 4566
+// msid-list = *(" " msid-id) / " *"
+fn parse_msid_semantic(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<_> = to_parse.split_whitespace().collect();
+ if tokens.is_empty() {
+ return Err(SdpParserInternalError::Generic(
+ "Msid-semantic attribute is missing msid-semantic token".to_string(),
+ ));
+ }
+ // TODO: Should msids be checked to ensure they are non empty?
+ let semantic = SdpAttributeMsidSemantic {
+ semantic: tokens[0].to_string(),
+ msids: tokens[1..].iter().map(ToString::to_string).collect(),
+ };
+ Ok(SdpAttribute::MsidSemantic(semantic))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=rid, draft-ietf-mmusic-rid
+//-------------------------------------------------------------------------
+// rid-syntax = %s"a=rid:" rid-id SP rid-dir
+// [ rid-pt-param-list / rid-param-list ]
+// rid-id = 1*(alpha-numeric / "-" / "_")
+// alpha-numeric = < as defined in {{RFC4566}} >
+// rid-dir = %s"send" / %s"recv"
+// rid-pt-param-list = SP rid-fmt-list *(";" rid-param)
+// rid-param-list = SP rid-param *(";" rid-param)
+// rid-fmt-list = %s"pt=" fmt *( "," fmt )
+// fmt = < as defined in {{RFC4566}} >
+// rid-param = rid-width-param
+// / rid-height-param
+// / rid-fps-param
+// / rid-fs-param
+// / rid-br-param
+// / rid-pps-param
+// / rid-bpp-param
+// / rid-depend-param
+// / rid-param-other
+// rid-width-param = %s"max-width" [ "=" int-param-val ]
+// rid-height-param = %s"max-height" [ "=" int-param-val ]
+// rid-fps-param = %s"max-fps" [ "=" int-param-val ]
+// rid-fs-param = %s"max-fs" [ "=" int-param-val ]
+// rid-br-param = %s"max-br" [ "=" int-param-val ]
+// rid-pps-param = %s"max-pps" [ "=" int-param-val ]
+// rid-bpp-param = %s"max-bpp" [ "=" float-param-val ]
+// rid-depend-param = %s"depend=" rid-list
+// rid-param-other = 1*(alpha-numeric / "-") [ "=" param-val ]
+// rid-list = rid-id *( "," rid-id )
+// int-param-val = 1*DIGIT
+// float-param-val = 1*DIGIT "." 1*DIGIT
+// param-val = *( %x20-58 / %x60-7E )
+// ; Any printable character except semicolon
+fn parse_rid(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.splitn(3, ' ').collect();
+
+ if tokens.len() < 2 {
+ return Err(SdpParserInternalError::Generic(
+ "A rid attribute must at least have an id and a direction token.".to_string(),
+ ));
+ }
+
+ // Default initilize
+ let mut params = SdpAttributeRidParameters {
+ max_width: 0,
+ max_height: 0,
+ max_fps: 0,
+ max_fs: 0,
+ max_br: 0,
+ max_pps: 0,
+ unknown: Vec::new(),
+ };
+ let mut formats: Vec<u16> = Vec::new();
+ let mut depends: Vec<String> = Vec::new();
+
+ if let Some(param_token) = tokens.get(2) {
+ let mut parameters = param_token.split(';').peekable();
+
+ // The 'pt' parameter must be the first parameter if present, so it
+ // cannot be checked along with the other parameters below
+ if let Some(maybe_fmt_parameter) = parameters.clone().peek() {
+ if let Some(fmt_list) = maybe_fmt_parameter.strip_prefix("pt=") {
+ for fmt in fmt_list.split(',') {
+ formats.push(fmt.trim().parse::<u16>()?);
+ }
+ parameters.next();
+ }
+ }
+
+ for param in parameters {
+ // TODO: Bug 1225877. Add support for params without '='
+ let param_value_pair: Vec<&str> = param.splitn(2, '=').collect();
+ if param_value_pair.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "A rid parameter needs to be of form 'param=value'".to_string(),
+ ));
+ }
+
+ match param_value_pair[0] {
+ "max-width" => params.max_width = param_value_pair[1].parse::<u32>()?,
+ "max-height" => params.max_height = param_value_pair[1].parse::<u32>()?,
+ "max-fps" => params.max_fps = param_value_pair[1].parse::<u32>()?,
+ "max-fs" => params.max_fs = param_value_pair[1].parse::<u32>()?,
+ "max-br" => params.max_br = param_value_pair[1].parse::<u32>()?,
+ "max-pps" => params.max_pps = param_value_pair[1].parse::<u32>()?,
+ "depends" => {
+ depends.extend(param_value_pair[1].split(',').map(ToString::to_string));
+ }
+ _ => params.unknown.push(param.to_string()),
+ }
+ }
+ }
+
+ Ok(SdpAttribute::Rid(SdpAttributeRid {
+ id: tokens[0].to_string(),
+ direction: parse_single_direction(tokens[1])?,
+ formats,
+ params,
+ depends,
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=remote-candiate, RFC5245
+//-------------------------------------------------------------------------
+// remote-candidate-att = "remote-candidates" ":" remote-candidate
+// 0*(SP remote-candidate)
+// remote-candidate = component-ID SP connection-address SP port
+fn parse_remote_candidates(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let component = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Remote-candidate attribute is missing component ID".to_string(),
+ ));
+ }
+ Some(x) => x.parse::<u32>()?,
+ };
+ let address = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Remote-candidate attribute is missing connection address".to_string(),
+ ));
+ }
+ Some(x) => parse_unicast_address(x)?,
+ };
+ let port = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Remote-candidate attribute is missing port number".to_string(),
+ ));
+ }
+ Some(x) => x.parse::<u32>()?,
+ };
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic(
+ "Remote-candidate port can only be a bit 16bit number".to_string(),
+ ));
+ };
+ Ok(SdpAttribute::RemoteCandidate(SdpAttributeRemoteCandidate {
+ component,
+ address,
+ port,
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=rtpmap, RFC4566
+//-------------------------------------------------------------------------
+// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
+fn parse_rtpmap(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let payload_type: u8 = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtpmap missing payload type".to_string(),
+ ));
+ }
+ Some(x) => {
+ let pt = x.parse::<u8>()?;
+ if pt > 127 {
+ return Err(SdpParserInternalError::Generic(
+ "Rtpmap payload type must be less then 127".to_string(),
+ ));
+ };
+ pt
+ }
+ };
+ let mut parameters = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtpmap missing payload type".to_string(),
+ ));
+ }
+ Some(x) => x.split('/'),
+ };
+ let name = match parameters.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtpmap missing codec name".to_string(),
+ ));
+ }
+ Some(x) => x.to_string(),
+ };
+ let frequency = match parameters.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtpmap missing codec name".to_string(),
+ ));
+ }
+ Some(x) => x.parse::<u32>()?,
+ };
+ let mut rtpmap = SdpAttributeRtpmap::new(payload_type, name, frequency);
+ if let Some(x) = parameters.next() {
+ rtpmap.set_channels(x.parse::<u32>()?)
+ };
+ Ok(SdpAttribute::Rtpmap(rtpmap))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=rtcp, RFC3605
+//-------------------------------------------------------------------------
+// rtcp-attribute = "a=rtcp:" port [nettype space addrtype space
+// connection-address] CRLF
+fn parse_rtcp(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let port = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtcp attribute is missing port number".to_string(),
+ ));
+ }
+ Some(x) => x.parse::<u16>()?,
+ };
+ let mut rtcp = SdpAttributeRtcp::new(port);
+ match tokens.next() {
+ None => (),
+ Some(x) => {
+ parse_network_type(x)?;
+ match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtcp attribute is missing address type token".to_string(),
+ ));
+ }
+ Some(x) => {
+ let addrtype = AddressType::from_str(x)?;
+ let addr = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtcp attribute is missing ip address token".to_string(),
+ ));
+ }
+ Some(x) => match ExplicitlyTypedAddress::try_from((addrtype, x)) {
+ Ok(address) => address,
+ Err(e) => return Err(e),
+ },
+ };
+ rtcp.set_addr(addr);
+ }
+ };
+ }
+ };
+ Ok(SdpAttribute::Rtcp(rtcp))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=rtcp-fb, RFC4585
+//-------------------------------------------------------------------------
+// rtcp-fb-syntax = "a=rtcp-fb:" rtcp-fb-pt SP rtcp-fb-val CRLF
+//
+// rtcp-fb-pt = "*" ; wildcard: applies to all formats
+// / fmt ; as defined in SDP spec
+//
+// rtcp-fb-val = "ack" rtcp-fb-ack-param
+// / "nack" rtcp-fb-nack-param
+// / "trr-int" SP 1*DIGIT
+// / rtcp-fb-id rtcp-fb-param
+//
+// rtcp-fb-id = 1*(alpha-numeric / "-" / "_")
+//
+// rtcp-fb-param = SP "app" [SP byte-string]
+// / SP token [SP byte-string]
+// / ; empty
+//
+// rtcp-fb-ack-param = SP "rpsi"
+// / SP "app" [SP byte-string]
+// / SP token [SP byte-string]
+// / ; empty
+//
+// rtcp-fb-nack-param = SP "pli"
+// / SP "sli"
+// / SP "rpsi"
+// / SP "app" [SP byte-string]
+// / SP token [SP byte-string]
+// / ; empty
+fn parse_rtcp_fb(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.splitn(4, ' ').collect();
+
+ // Parse this in advance to use it later in the parameter switch
+ let feedback_type = match tokens.get(1) {
+ Some(x) => match *x {
+ "ack" => SdpAttributeRtcpFbType::Ack,
+ "ccm" => SdpAttributeRtcpFbType::Ccm,
+ "nack" => SdpAttributeRtcpFbType::Nack,
+ "trr-int" => SdpAttributeRtcpFbType::TrrInt,
+ "goog-remb" => SdpAttributeRtcpFbType::Remb,
+ "transport-cc" => SdpAttributeRtcpFbType::TransCc,
+ _ => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown rtcpfb feedback type: {x:?}"
+ )));
+ }
+ },
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Error parsing rtcpfb: no feedback type".to_string(),
+ ));
+ }
+ };
+
+ // Parse this in advance to make the initilization block below better readable
+ let parameter = match feedback_type {
+ SdpAttributeRtcpFbType::Ack => match tokens.get(2) {
+ Some(x) => match *x {
+ "rpsi" | "app" => (*x).to_string(),
+ _ => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown rtcpfb ack parameter: {x:?}"
+ )));
+ }
+ },
+ None => {
+ return Err(SdpParserInternalError::Unsupported(
+ "The rtcpfb ack feeback type needs a parameter:".to_string(),
+ ));
+ }
+ },
+ SdpAttributeRtcpFbType::Ccm => match tokens.get(2) {
+ Some(x) => match *x {
+ "fir" | "tmmbr" | "tstr" | "vbcm" => (*x).to_string(),
+ _ => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown rtcpfb ccm parameter: {x:?}"
+ )));
+ }
+ },
+ None => "".to_string(),
+ },
+ SdpAttributeRtcpFbType::Nack => match tokens.get(2) {
+ Some(x) => match *x {
+ "sli" | "pli" | "rpsi" | "app" => (*x).to_string(),
+ _ => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown rtcpfb nack parameter: {x:?}"
+ )));
+ }
+ },
+ None => "".to_string(),
+ },
+ SdpAttributeRtcpFbType::TrrInt => match tokens.get(2) {
+ Some(x) => match x {
+ _ if x.parse::<u32>().is_ok() => (*x).to_string(),
+ _ => {
+ return Err(SdpParserInternalError::Generic(format!(
+ "Unknown rtcpfb trr-int parameter: {x:?}"
+ )));
+ }
+ },
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "The rtcpfb trr-int feedback type needs a parameter".to_string(),
+ ));
+ }
+ },
+ SdpAttributeRtcpFbType::Remb | SdpAttributeRtcpFbType::TransCc => match tokens.get(2) {
+ Some(x) => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "Unknown rtcpfb {feedback_type} parameter: {x:?}"
+ )));
+ }
+ None => "".to_string(),
+ },
+ };
+
+ Ok(SdpAttribute::Rtcpfb(SdpAttributeRtcpFb {
+ payload_type: parse_payload_type(tokens[0])?,
+ feedback_type,
+ parameter,
+ extra: match tokens.get(3) {
+ Some(x) => (*x).to_string(),
+ None => "".to_string(),
+ },
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=sctpmap, draft-ietf-mmusic-sctp-sdp-05
+//-------------------------------------------------------------------------
+// sctpmap-attr = "a=sctpmap:" sctpmap-number media-subtypes
+// [streams]
+// sctpmap-number = 1*DIGIT
+// protocol = labelstring
+// labelstring = text
+// text = byte-string
+// streams = 1*DIGIT
+//
+// Note: this was replace in later versions of the draft by sctp-port
+fn parse_sctpmap(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.split_whitespace().collect();
+ if tokens.len() != 3 {
+ return Err(SdpParserInternalError::Generic(
+ "Sctpmap needs to have three tokens".to_string(),
+ ));
+ }
+ let port = tokens[0].parse::<u16>()?;
+ if tokens[1].to_lowercase() != "webrtc-datachannel" {
+ return Err(SdpParserInternalError::Generic(
+ "Unsupported sctpmap type token".to_string(),
+ ));
+ }
+ Ok(SdpAttribute::Sctpmap(SdpAttributeSctpmap {
+ port,
+ channels: tokens[2].parse::<u32>()?,
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=setup, RFC4145
+//-------------------------------------------------------------------------
+// setup-attr = "a=setup:" role
+// role = "active" / "passive" / "actpass" / "holdconn"
+fn parse_setup(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ Ok(SdpAttribute::Setup(
+ match to_parse.to_lowercase().as_ref() {
+ "active" => SdpAttributeSetup::Active,
+ "actpass" => SdpAttributeSetup::Actpass,
+ "holdconn" => SdpAttributeSetup::Holdconn,
+ "passive" => SdpAttributeSetup::Passive,
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Unsupported setup value".to_string(),
+ ));
+ }
+ },
+ ))
+}
+
+fn parse_simulcast_version_list(
+ to_parse: &str,
+) -> Result<Vec<SdpAttributeSimulcastVersion>, SdpParserInternalError> {
+ let make_version_list = |to_parse: &str| {
+ to_parse
+ .split(';')
+ .map(SdpAttributeSimulcastVersion::new)
+ .collect()
+ };
+ if to_parse.contains('=') {
+ let mut descriptor_versionlist_pair = to_parse.splitn(2, '=');
+ match descriptor_versionlist_pair.next().unwrap() {
+ // TODO Bug 1470568
+ "rid" => Ok(make_version_list(
+ descriptor_versionlist_pair.next().unwrap(),
+ )),
+ descriptor => Err(SdpParserInternalError::Generic(format!(
+ "Simulcast attribute has unknown list descriptor '{descriptor:?}'"
+ ))),
+ }
+ } else {
+ Ok(make_version_list(to_parse))
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=simulcast, draft-ietf-mmusic-sdp-simulcast
+//-------------------------------------------------------------------------
+// Old draft-04
+// sc-attr = "a=simulcast:" 1*2( WSP sc-str-list ) [WSP sc-pause-list]
+// sc-str-list = sc-dir WSP sc-id-type "=" sc-alt-list *( ";" sc-alt-list )
+// sc-pause-list = "paused=" sc-alt-list
+// sc-dir = "send" / "recv"
+// sc-id-type = "pt" / "rid" / token
+// sc-alt-list = sc-id *( "," sc-id )
+// sc-id = fmt / rid-identifier / token
+// ; WSP defined in [RFC5234]
+// ; fmt, token defined in [RFC4566]
+// ; rid-identifier defined in [I-D.pthatcher-mmusic-rid]
+//
+// New draft 14, need to parse this for now, will eventually emit it
+// sc-value = ( sc-send [SP sc-recv] ) / ( sc-recv [SP sc-send] )
+// sc-send = %s"send" SP sc-str-list
+// sc-recv = %s"recv" SP sc-str-list
+// sc-str-list = sc-alt-list *( ";" sc-alt-list )
+// sc-alt-list = sc-id *( "," sc-id )
+// sc-id-paused = "~"
+// sc-id = [sc-id-paused] rid-id
+// ; SP defined in [RFC5234]
+// ; rid-id defined in [I-D.ietf-mmusic-rid]
+fn parse_simulcast(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ // TODO: Bug 1225877: Stop accepting all kinds of whitespace here, and only accept SP
+ let mut tokens = to_parse.split_whitespace();
+ let first_direction = match tokens.next() {
+ Some(x) => parse_single_direction(x)?,
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Simulcast attribute is missing send/recv value".to_string(),
+ ));
+ }
+ };
+
+ let first_version_list = match tokens.next() {
+ Some(x) => parse_simulcast_version_list(x)?,
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Simulcast attribute must have an alternatives list after the direction token"
+ .to_string(),
+ ));
+ }
+ };
+
+ let mut second_version_list = Vec::new();
+ if let Some(x) = tokens.next() {
+ if parse_single_direction(x)? == first_direction {
+ return Err(SdpParserInternalError::Generic(
+ "Simulcast attribute has defined two times the same direction".to_string(),
+ ));
+ }
+
+ second_version_list = match tokens.next() {
+ Some(x) => parse_simulcast_version_list(x)?,
+ None => {
+ return Err(SdpParserInternalError::Generic(format!(
+ "{:?}{:?}",
+ "Simulcast has defined a second direction but",
+ "no second list of simulcast stream versions"
+ )));
+ }
+ }
+ }
+
+ Ok(SdpAttribute::Simulcast(match first_direction {
+ SdpSingleDirection::Send => SdpAttributeSimulcast {
+ send: first_version_list,
+ receive: second_version_list,
+ },
+ SdpSingleDirection::Recv => SdpAttributeSimulcast {
+ send: second_version_list,
+ receive: first_version_list,
+ },
+ }))
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=ssrc, RFC5576
+//-------------------------------------------------------------------------
+// ssrc-attr = "ssrc:" ssrc-id SP attribute
+// ; The base definition of "attribute" is in RFC 4566.
+// ; (It is the content of "a=" lines.)
+//
+// ssrc-id = integer ; 0 .. 2**32 - 1
+fn parse_ssrc(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.splitn(2, ' ');
+ let ssrc_id = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Ssrc attribute is missing ssrc-id value".to_string(),
+ ));
+ }
+ Some(x) => x.parse::<u32>()?,
+ };
+ let mut ssrc = SdpAttributeSsrc::new(ssrc_id);
+ match tokens.next() {
+ None => (),
+ Some(x) => ssrc.set_attribute(x),
+ };
+ Ok(SdpAttribute::Ssrc(ssrc))
+}
+
+pub fn parse_attribute(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ Ok(SdpType::Attribute(value.trim().parse()?))
+}
+
+#[cfg(test)]
+#[path = "./attribute_type_tests.rs"]
+mod tests;
diff --git a/third_party/rust/webrtc-sdp/src/attribute_type_tests.rs b/third_party/rust/webrtc-sdp/src/attribute_type_tests.rs
new file mode 100644
index 0000000000..077327a513
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/attribute_type_tests.rs
@@ -0,0 +1,1102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate url;
+use super::*;
+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+
+macro_rules! make_check_parse {
+ ($attr_type:ty, $attr_kind:path) => {
+ |attr_str: &str| -> $attr_type {
+ match parse_attribute(attr_str) {
+ Ok(SdpType::Attribute($attr_kind(attr))) => attr,
+ Err(e) => panic!("{}", e),
+ _ => unreachable!(),
+ }
+ }
+ };
+
+ ($attr_kind:path) => {
+ |attr_str: &str| -> SdpAttribute {
+ match parse_attribute(attr_str) {
+ Ok(SdpType::Attribute($attr_kind)) => $attr_kind,
+ Err(e) => panic!("{}", e),
+ _ => unreachable!(),
+ }
+ }
+ };
+}
+
+macro_rules! make_check_parse_and_serialize {
+ ($check_parse_func:ident, $attr_kind:path) => {
+ |attr_str: &str| {
+ let parsed = $attr_kind($check_parse_func(attr_str));
+ assert_eq!(parsed.to_string(), attr_str.to_string());
+ }
+ };
+
+ ($check_parse_func:ident) => {
+ |attr_str: &str| {
+ let parsed = $check_parse_func(attr_str);
+ assert_eq!(parsed.to_string(), attr_str.to_string());
+ }
+ };
+}
+
+#[test]
+fn test_parse_attribute_candidate_and_serialize() {
+ let check_parse = make_check_parse!(SdpAttributeCandidate, SdpAttribute::Candidate);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Candidate);
+
+ check_parse_and_serialize("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ host");
+ check_parse_and_serialize("candidate:foo 1 UDP 2122252543 172.16.156.106 49760 typ host");
+ check_parse_and_serialize("candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host");
+ check_parse_and_serialize("candidate:0 1 TCP 2122252543 ::1 49760 typ host");
+ check_parse_and_serialize("candidate:0 1 TCP 2122252543 2001:db8:4860::4444 49760 typ host");
+ check_parse_and_serialize("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ srflx");
+ check_parse_and_serialize("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ prflx");
+ check_parse_and_serialize("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ relay");
+ check_parse_and_serialize(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host tcptype active",
+ );
+ check_parse_and_serialize(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host tcptype passive",
+ );
+ check_parse_and_serialize(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host tcptype so",
+ );
+ check_parse_and_serialize(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host ufrag foobar",
+ );
+ check_parse_and_serialize(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host network-cost 50",
+ );
+ check_parse_and_serialize("candidate:1 1 UDP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 generation 0");
+ check_parse_and_serialize(
+ "candidate:1 1 UDP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665",
+ );
+ check_parse_and_serialize("candidate:1 1 TCP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 tcptype passive");
+ check_parse_and_serialize("candidate:1 1 TCP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 tcptype passive generation 1");
+ check_parse_and_serialize("candidate:1 1 TCP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 tcptype passive generation 1 ufrag +DGd");
+ check_parse_and_serialize("candidate:1 1 TCP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 tcptype passive generation 1 ufrag +DGd network-cost 1");
+ check_parse_and_serialize(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host unsupported foo",
+ );
+ check_parse_and_serialize("candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host unsupported foo more_unsupported bar");
+
+ let candidate = check_parse("candidate:1 1 TCP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 tcptype passive generation 1 ufrag +DGd network-cost 1 unsupported foo");
+ assert_eq!(candidate.foundation, "1".to_string());
+ assert_eq!(candidate.component, 1);
+ assert_eq!(candidate.transport, SdpAttributeCandidateTransport::Tcp);
+ assert_eq!(candidate.priority, 1_685_987_071);
+ assert_eq!(
+ candidate.address,
+ Address::from_str("24.23.204.141").unwrap()
+ );
+ assert_eq!(candidate.port, 54609);
+ assert_eq!(candidate.c_type, SdpAttributeCandidateType::Srflx);
+ assert_eq!(
+ candidate.raddr,
+ Some(Address::from_str("192.168.1.4").unwrap())
+ );
+ assert_eq!(candidate.rport, Some(61665));
+ assert_eq!(
+ candidate.tcp_type,
+ Some(SdpAttributeCandidateTcpType::Passive)
+ );
+ assert_eq!(candidate.generation, Some(1));
+ assert_eq!(candidate.ufrag, Some("+DGd".to_string()));
+ assert_eq!(candidate.networkcost, Some(1));
+ assert_eq!(
+ candidate.unknown_extensions,
+ vec![("unsupported".to_string(), "foo".to_string())]
+ )
+}
+
+#[test]
+fn test_anonymize_attribute_candidate() -> Result<(), SdpParserInternalError> {
+ let mut anon = StatefulSdpAnonymizer::new();
+ let candidate_1 = parse_attribute("candidate:0 1 TCP 2122252543 ::8 49760 typ host")?;
+ let candidate_2 =
+ parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 19361 typ srflx")?;
+ let candidate_3 = parse_attribute("candidate:1 1 TCP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 tcptype passive generation 1 ufrag +DGd")?;
+ if let SdpType::Attribute(SdpAttribute::Candidate(candidate)) = candidate_1 {
+ let masked = candidate.masked_clone(&mut anon);
+ assert!(masked.address == Address::Ip(IpAddr::V6(Ipv6Addr::from(1))));
+ assert!(masked.port == 1);
+ } else {
+ unreachable!();
+ }
+
+ if let SdpType::Attribute(SdpAttribute::Candidate(candidate)) = candidate_2 {
+ let masked = candidate.masked_clone(&mut anon);
+ assert!(masked.address == Address::Ip(IpAddr::V4(Ipv4Addr::from(1))));
+ assert!(masked.port == 2);
+ } else {
+ unreachable!();
+ }
+
+ if let SdpType::Attribute(SdpAttribute::Candidate(candidate)) = candidate_3 {
+ let masked = candidate.masked_clone(&mut anon);
+ assert!(masked.address == Address::Ip(IpAddr::V4(Ipv4Addr::from(2))));
+ assert!(masked.port == 3);
+ assert!(masked.raddr.unwrap() == Address::Ip(IpAddr::V4(Ipv4Addr::from(3))));
+ assert!(masked.rport.unwrap() == 4);
+ } else {
+ unreachable!();
+ }
+ Ok(())
+}
+
+#[test]
+fn test_parse_attribute_candidate_errors() {
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ").is_err());
+ assert!(
+ parse_attribute("candidate:0 foo UDP 2122252543 172.16.156.106 49760 typ host").is_err()
+ );
+ assert!(parse_attribute("candidate:0 1 FOO 2122252543 172.16.156.106 49760 typ host").is_err());
+ assert!(parse_attribute("candidate:0 1 UDP foo 172.16.156.106 49760 typ host").is_err());
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 372.16.356 49760 typ host").is_err());
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 70000 typ host").is_err());
+ assert!(
+ parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 49760 type host").is_err()
+ );
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ fost").is_err());
+ assert!(parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host unsupported"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host network-cost"
+ )
+ .is_err());
+ assert!(parse_attribute("candidate:1 1 UDP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 generation B").is_err());
+ assert!(parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host network-cost C"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "candidate:1 1 UDP 1685987071 24.23.204.141 54609 typ srflx raddr 1%92.168.1 rport 61665"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host tcptype foobar"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "candidate:1 1 UDP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 70000"
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_dtls_message() {
+ let check_parse = make_check_parse!(SdpAttributeDtlsMessage, SdpAttribute::DtlsMessage);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::DtlsMessage);
+
+ check_parse_and_serialize("dtls-message:client SGVsbG8gV29ybGQ=");
+ check_parse_and_serialize("dtls-message:server SGVsbG8gV29ybGQ=");
+ check_parse_and_serialize("dtls-message:client IGlzdCBl/W4gUeiBtaXQg+JSB1bmQCAkJJkSNEQ=");
+ check_parse_and_serialize("dtls-message:server IGlzdCBl/W4gUeiBtaXQg+JSB1bmQCAkJJkSNEQ=");
+
+ match check_parse("dtls-message:client SGVsbG8gV29ybGQ=") {
+ SdpAttributeDtlsMessage::Client(x) => {
+ assert_eq!(x, "SGVsbG8gV29ybGQ=");
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+
+ match check_parse("dtls-message:server SGVsbG8gV29ybGQ=") {
+ SdpAttributeDtlsMessage::Server(x) => {
+ assert_eq!(x, "SGVsbG8gV29ybGQ=");
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+
+ assert!(parse_attribute("dtls-message:client").is_err());
+ assert!(parse_attribute("dtls-message:server").is_err());
+ assert!(parse_attribute("dtls-message:unsupported SGVsbG8gV29ybGQ=").is_err());
+}
+
+#[test]
+fn test_parse_attribute_end_of_candidates() {
+ let check_parse = make_check_parse!(SdpAttribute::EndOfCandidates);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("end-of-candidates");
+ assert!(parse_attribute("end-of-candidates foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_extmap() {
+ let check_parse = make_check_parse!(SdpAttributeExtmap, SdpAttribute::Extmap);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Extmap);
+
+ check_parse_and_serialize("extmap:1/sendonly urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ check_parse_and_serialize("extmap:2/sendrecv urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ check_parse_and_serialize(
+ "extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
+ );
+ check_parse_and_serialize(
+ "extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time ext_attributes",
+ );
+
+ assert!(parse_attribute("extmap:1/sendrecv").is_err());
+ assert!(
+ parse_attribute("extmap:a/sendrecv urn:ietf:params:rtp-hdrext:ssrc-audio-level").is_err()
+ );
+ assert!(
+ parse_attribute("extmap:4/unsupported urn:ietf:params:rtp-hdrext:ssrc-audio-level")
+ .is_err()
+ );
+
+ let mut bad_char =
+ String::from("extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time ");
+ bad_char.push(0x00 as char);
+ assert!(parse_attribute(&bad_char).is_err());
+}
+
+#[test]
+fn test_parse_attribute_fingerprint() {
+ let check_parse = make_check_parse!(SdpAttributeFingerprint, SdpAttribute::Fingerprint);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Fingerprint);
+
+ check_parse_and_serialize(
+ "fingerprint:sha-1 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC",
+ );
+ check_parse_and_serialize(
+ "fingerprint:sha-224 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:\
+ 27:97:EB:0B:23:73:AC:BC",
+ );
+ check_parse_and_serialize(
+ "fingerprint:sha-256 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:\
+ 27:97:EB:0B:23:73:AC:BC:CD:34:D1:62",
+ );
+ check_parse_and_serialize(
+ "fingerprint:sha-384 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:\
+ 27:97:EB:0B:23:73:AC:BC:CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:\
+ 27:97:EB:0B:23:73:AC:BC",
+ );
+ check_parse_and_serialize(
+ "fingerprint:sha-512 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:\
+ 97:EB:0B:23:73:AC:BC:CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:\
+ EB:0B:23:73:AC:BC:27:97:EB:0B:23:73:AC:BC:27:97:EB:0B:23:73:\
+ BC:EB:0B:23",
+ );
+
+ assert!(parse_attribute("fingerprint:sha-1").is_err());
+ assert!(parse_attribute(
+ "fingerprint:unsupported CD:34:D1:62:16:95:7B:B7:EB:74:E1:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CDA:34:D1:62:16:95:7B:B7:EB:74:E1:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CD:34:D1:62:16:95:7B:B7:EB:74:E1:39:27:97:EB:0B:23:73:AC:"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CD:34:D1:62:16:95:7B:B7:EB:74:E1:39:27:97:EB:0B:23:73:AC"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CX:34:D1:62:16:95:7B:B7:EB:74:E1:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+
+ assert!(parse_attribute(
+ "fingerprint:sha-1 0xCD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CD:0x34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CD::D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CD:0000A:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+ assert!(parse_attribute(
+ "fingerprint:sha-1 CD:B:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC"
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_attribute_fmtp() {
+ let check_parse = make_check_parse!(SdpAttributeFmtp, SdpAttribute::Fmtp);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Fmtp);
+
+ check_parse_and_serialize("fmtp:109 maxplaybackrate=46000;stereo=1;useinbandfec=1");
+ check_parse_and_serialize(
+ "fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1",
+ );
+ check_parse_and_serialize("fmtp:66 0-15");
+ check_parse_and_serialize("fmtp:109 0-15,66");
+ check_parse_and_serialize("fmtp:66 111/115");
+ assert!(parse_attribute("fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1").is_ok());
+ assert!(parse_attribute("fmtp:109 maxplaybackrate=48000; stereo=1; useinbandfec=1").is_ok());
+ assert!(parse_attribute("fmtp:109 maxplaybackrate=48000; stereo=1;useinbandfec=1").is_ok());
+ check_parse_and_serialize("fmtp:8 maxplaybackrate=46000");
+ check_parse_and_serialize("fmtp:8 maxaveragebitrate=46000");
+ check_parse_and_serialize("fmtp:8 maxaveragebitrate=46000;ptime=60;minptime=20;maxptime=120");
+ check_parse_and_serialize(
+ "fmtp:8 max-cpb=1234;max-dpb=32000;max-br=3;max-mbps=46000;usedtx=1;cbr=1",
+ );
+ assert!(parse_attribute("fmtp:77 ").is_err());
+ assert!(parse_attribute("fmtp:109 stereo=2;").is_err());
+ assert!(parse_attribute("fmtp:109 111/129;").is_err());
+ assert!(parse_attribute("fmtp:109 packetization-mode=3;").is_err());
+ assert!(parse_attribute("fmtp:109 maxplaybackrate=48000stereo=1;").is_err());
+ assert!(parse_attribute("fmtp:8 ;maxplaybackrate=48000").is_ok());
+ assert!(parse_attribute("fmtp:8 packetization-mode=2;;maxplaybackrate=48000").is_ok());
+ assert!(parse_attribute("fmtp:8 packetization-mode=2; maxplaybackrate=48000").is_ok());
+ assert!(parse_attribute("fmtp:8 maxplaybackrate=48000;").is_ok());
+ assert!(parse_attribute("fmtp:8 x-google-start-bitrate=800; maxplaybackrate=48000;").is_ok());
+ check_parse_and_serialize("fmtp:97 apt=96");
+ check_parse_and_serialize("fmtp:97 apt=96;rtx-time=3000");
+ check_parse_and_serialize(
+ "fmtp:102 packetization-mode=1;sprop-parameter-sets=Z0LAFYyNQKD5APCIRqA=,aM48gA==",
+ );
+}
+
+#[test]
+fn test_anonymize_attribute_fingerprint() -> Result<(), SdpParserInternalError> {
+ let mut anon = StatefulSdpAnonymizer::new();
+ if let SdpType::Attribute(SdpAttribute::Fingerprint(print)) = parse_attribute(
+ "fingerprint:sha-1 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC",
+ )? {
+ assert!(print.masked_clone(&mut anon).to_string() == "sha-1 00:00:00:00:00:00:00:01");
+ } else {
+ unreachable!();
+ }
+ Ok(())
+}
+
+#[test]
+fn test_parse_attribute_group() {
+ let check_parse = make_check_parse!(SdpAttributeGroup, SdpAttribute::Group);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Group);
+
+ check_parse_and_serialize("group:LS");
+ check_parse_and_serialize("group:LS 1 2");
+ check_parse_and_serialize("group:FID 1 2");
+ check_parse_and_serialize("group:SRF 1 2");
+ check_parse_and_serialize("group:FEC S1 R1");
+ check_parse_and_serialize("group:ANAT S1 R1");
+ check_parse_and_serialize("group:DDP L1 L2 L3");
+ check_parse_and_serialize("group:BUNDLE sdparta_0 sdparta_1 sdparta_2");
+
+ assert!(parse_attribute("group:").is_err());
+ assert!(matches!(
+ parse_attribute("group:NEVER_SUPPORTED_SEMANTICS"),
+ Err(SdpParserInternalError::Unsupported(_))
+ ));
+}
+
+#[test]
+fn test_parse_attribute_bundle_only() {
+ let check_parse = make_check_parse!(SdpAttribute::BundleOnly);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("bundle-only");
+
+ assert!(parse_attribute("bundle-only foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_lite() {
+ let check_parse = make_check_parse!(SdpAttribute::IceLite);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("ice-lite");
+
+ assert!(parse_attribute("ice-lite foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_extmap_allow_mixed() {
+ let check_parse = make_check_parse!(SdpAttribute::ExtmapAllowMixed);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("extmap-allow-mixed");
+
+ assert!(parse_attribute("extmap-allow-mixed 100").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_mismatch() {
+ let check_parse = make_check_parse!(SdpAttribute::IceMismatch);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("ice-mismatch");
+
+ assert!(parse_attribute("ice-mismatch foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_options() {
+ let check_parse = make_check_parse!(Vec<String>, SdpAttribute::IceOptions);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::IceOptions);
+
+ check_parse_and_serialize("ice-options:trickle");
+
+ assert!(parse_attribute("ice-options:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_pacing() {
+ let check_parse = make_check_parse!(u64, SdpAttribute::IcePacing);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::IcePacing);
+
+ check_parse_and_serialize("ice-pacing:50");
+
+ assert!(parse_attribute("ice-pacing:").is_err());
+ assert!(parse_attribute("ice-pacing:10000000000").is_err());
+ assert!(parse_attribute("ice-pacing:50 100").is_err());
+ assert!(parse_attribute("ice-pacing:foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_pwd() {
+ let check_parse = make_check_parse!(String, SdpAttribute::IcePwd);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::IcePwd);
+
+ check_parse_and_serialize("ice-pwd:e3baa26dd2fa5030d881d385f1e36cce");
+
+ assert!(parse_attribute("ice-pwd:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_ufrag() {
+ let check_parse = make_check_parse!(String, SdpAttribute::IceUfrag);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::IceUfrag);
+
+ check_parse_and_serialize("ice-ufrag:58b99ead");
+
+ assert!(parse_attribute("ice-ufrag:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_identity() {
+ let check_parse = make_check_parse!(String, SdpAttribute::Identity);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Identity);
+
+ check_parse_and_serialize("identity:eyJpZHAiOnsiZG9tYWluIjoiZXhhbXBsZS5vcmciLCJwcm90b2NvbCI6ImJvZ3VzIn0sImFzc2VydGlvbiI6IntcImlkZW50aXR5XCI6XCJib2JAZXhhbXBsZS5vcmdcIixcImNvbnRlbnRzXCI6XCJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3l6XCIsXCJzaWduYXR1cmVcIjpcIjAxMDIwMzA0MDUwNlwifSJ9");
+
+ assert!(parse_attribute("identity:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_imageattr() {
+ let check_parse = make_check_parse!(SdpAttributeImageAttr, SdpAttribute::ImageAttr);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::ImageAttr);
+
+ check_parse_and_serialize("imageattr:120 send * recv *");
+ check_parse_and_serialize("imageattr:99 send [x=320,y=240] recv [x=320,y=240]");
+ check_parse_and_serialize(
+ "imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250]",
+ );
+ check_parse_and_serialize("imageattr:97 send [x=[480:16:800],y=[320:16:640],par=[1.2-1.3],q=0.6] [x=[176:8:208],y=[144:8:176],par=[1.2-1.3]] recv *");
+ assert!(parse_attribute("imageattr:97 recv [x=800,y=640,sar=1.1] send [x=330,y=250]").is_ok());
+
+ check_parse_and_serialize("imageattr:99 send [x=320,y=240]");
+ assert!(parse_attribute("imageattr:100 recv [x=320,y=240]").is_ok());
+ assert!(parse_attribute("imageattr:97 recv [x=800,y=640,sar=1.1,foo=[123,456],q=0.5] send [x=330,y=250,bar=foo,sar=[20-40]]").is_ok());
+ assert!(parse_attribute("imageattr:97 recv [x=800,y=640,sar=1.1,foo=abc xyz,q=0.5] send [x=330,y=250,bar=foo,sar=[20-40]]").is_ok());
+
+ assert!(parse_attribute("imageattr:").is_err());
+ assert!(parse_attribute("imageattr:100").is_err());
+ assert!(parse_attribute("imageattr:120 send * recv * send *").is_err());
+ assert!(parse_attribute("imageattr:99 send [x=320]").is_err());
+ assert!(parse_attribute("imageattr:99 recv [y=240]").is_err());
+ assert!(parse_attribute("imageattr:99 send [x=320,y=240").is_err());
+ assert!(parse_attribute("imageattr:99 send x=320,y=240]").is_err());
+ assert!(parse_attribute("imageattr:97 send [x=800,y=640,sar=1.1] send [x=330,y=250]").is_err());
+}
+
+#[test]
+fn test_parse_attribute_imageattr_recv_and_verify() {
+ let check_parse = make_check_parse!(SdpAttributeImageAttr, SdpAttribute::ImageAttr);
+
+ let imageattr = check_parse(
+ "imageattr:* recv [x=800,y=[50,80,30],sar=1.1] send [x=330,y=250,sar=[1.1,1.3,1.9],q=0.1]",
+ );
+ assert_eq!(imageattr.pt, SdpAttributePayloadType::Wildcard);
+ match imageattr.recv {
+ SdpAttributeImageAttrSetList::Sets(sets) => {
+ assert_eq!(sets.len(), 1);
+
+ let set = &sets[0];
+ assert_eq!(
+ set.x,
+ SdpAttributeImageAttrXyRange::DiscreteValues(vec![800])
+ );
+ assert_eq!(
+ set.y,
+ SdpAttributeImageAttrXyRange::DiscreteValues(vec![50, 80, 30])
+ );
+ assert_eq!(set.par, None);
+ assert_eq!(
+ set.sar,
+ Some(SdpAttributeImageAttrSRange::DiscreteValues(vec![1.1]))
+ );
+ assert_eq!(set.q, None);
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+ match imageattr.send {
+ SdpAttributeImageAttrSetList::Sets(sets) => {
+ assert_eq!(sets.len(), 1);
+
+ let set = &sets[0];
+ assert_eq!(
+ set.x,
+ SdpAttributeImageAttrXyRange::DiscreteValues(vec![330])
+ );
+ assert_eq!(
+ set.y,
+ SdpAttributeImageAttrXyRange::DiscreteValues(vec![250])
+ );
+ assert_eq!(set.par, None);
+ assert_eq!(
+ set.sar,
+ Some(SdpAttributeImageAttrSRange::DiscreteValues(vec![
+ 1.1, 1.3, 1.9,
+ ]))
+ );
+ assert_eq!(set.q, Some(0.1));
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+}
+
+#[test]
+fn test_parse_attribute_imageattr_send_and_verify() {
+ let check_parse = make_check_parse!(SdpAttributeImageAttr, SdpAttribute::ImageAttr);
+
+ let imageattr = check_parse(
+ "imageattr:97 send [x=[480:16:800],y=[100,200,300],par=[1.2-1.3],q=0.6] [x=1080,y=[144:176],sar=[0.5-0.7]] recv *"
+ );
+ assert_eq!(imageattr.pt, SdpAttributePayloadType::PayloadType(97));
+ match imageattr.send {
+ SdpAttributeImageAttrSetList::Sets(sets) => {
+ assert_eq!(sets.len(), 2);
+
+ let first_set = &sets[0];
+ assert_eq!(
+ first_set.x,
+ SdpAttributeImageAttrXyRange::Range(480, 800, Some(16))
+ );
+ assert_eq!(
+ first_set.y,
+ SdpAttributeImageAttrXyRange::DiscreteValues(vec![100, 200, 300])
+ );
+ assert_eq!(
+ first_set.par,
+ Some(SdpAttributeImageAttrPRange { min: 1.2, max: 1.3 })
+ );
+ assert_eq!(first_set.sar, None);
+ assert_eq!(first_set.q, Some(0.6));
+
+ let second_set = &sets[1];
+ assert_eq!(
+ second_set.x,
+ SdpAttributeImageAttrXyRange::DiscreteValues(vec![1080])
+ );
+ assert_eq!(
+ second_set.y,
+ SdpAttributeImageAttrXyRange::Range(144, 176, None)
+ );
+ assert_eq!(second_set.par, None);
+ assert_eq!(
+ second_set.sar,
+ Some(SdpAttributeImageAttrSRange::Range(0.5, 0.7))
+ );
+ assert_eq!(second_set.q, None);
+ }
+ _ => {
+ unreachable!();
+ }
+ }
+ assert_eq!(imageattr.recv, SdpAttributeImageAttrSetList::Wildcard);
+}
+
+#[test]
+fn test_parse_attribute_inactive() {
+ let check_parse = make_check_parse!(SdpAttribute::Inactive);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("inactive");
+ assert!(parse_attribute("inactive foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_label() {
+ let check_parse = make_check_parse!(String, SdpAttribute::Label);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Label);
+
+ check_parse_and_serialize("label:1");
+ check_parse_and_serialize("label:foobar");
+ check_parse_and_serialize("label:foobar barfoo");
+
+ assert!(parse_attribute("label:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_maxptime() {
+ let check_parse = make_check_parse!(u64, SdpAttribute::MaxPtime);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::MaxPtime);
+
+ check_parse_and_serialize("maxptime:60");
+
+ assert!(parse_attribute("maxptime:").is_err());
+ assert!(parse_attribute("maxptime:60 100").is_err());
+ assert!(parse_attribute("maxptime:foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_mid() {
+ let check_parse = make_check_parse!(String, SdpAttribute::Mid);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse, SdpAttribute::Mid);
+
+ check_parse_and_serialize("mid:sdparta_0");
+ check_parse_and_serialize("mid:sdparta_0 sdparta_1 sdparta_2");
+
+ assert!(parse_attribute("mid:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_msid() {
+ let check_parse = make_check_parse!(SdpAttributeMsid, SdpAttribute::Msid);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Msid);
+
+ check_parse_and_serialize("msid:{5a990edd-0568-ac40-8d97-310fc33f3411}");
+ check_parse_and_serialize(
+ "msid:{5a990edd-0568-ac40-8d97-310fc33f3411} {218cfa1c-617d-2249-9997-60929ce4c405}",
+ );
+
+ assert!(parse_attribute("msid:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_msid_semantics() {
+ let check_parse = make_check_parse!(SdpAttributeMsidSemantic, SdpAttribute::MsidSemantic);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::MsidSemantic);
+
+ check_parse_and_serialize("msid-semantic:WMS *");
+ check_parse_and_serialize("msid-semantic:WMS foo");
+
+ assert!(parse_attribute("msid-semantic:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ptime() {
+ let check_parse = make_check_parse!(u64, SdpAttribute::Ptime);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Ptime);
+
+ check_parse_and_serialize("ptime:30");
+ assert!(parse_attribute("ptime:").is_err());
+}
+
+#[test]
+fn test_anonymize_remote_candidate() -> Result<(), SdpParserInternalError> {
+ let mut anon = StatefulSdpAnonymizer::new();
+ if let SdpType::Attribute(SdpAttribute::RemoteCandidate(remote)) =
+ parse_attribute("remote-candidates:0 10.0.0.1 5555")?
+ {
+ let masked = remote.masked_clone(&mut anon);
+ assert_eq!(masked.address, Address::Ip(IpAddr::V4(Ipv4Addr::from(1))));
+ assert_eq!(masked.port, 1);
+ } else {
+ unreachable!();
+ }
+ Ok(())
+}
+
+#[test]
+fn test_parse_attribute_rid() {
+ let check_parse = make_check_parse!(SdpAttributeRid, SdpAttribute::Rid);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse, SdpAttribute::Rid);
+
+ check_parse_and_serialize("rid:foo send pt=10");
+ check_parse_and_serialize("rid:110 send pt=9,10");
+ check_parse_and_serialize("rid:110 send pt=9,10;max-fs=10");
+ check_parse_and_serialize("rid:110 send pt=9,10;max-width=10;depends=1,2,3");
+
+ assert!(
+ parse_attribute("rid:110 send pt=9, 10;max-fs=10;UNKNOWN=100; depends=1, 2, 3").is_ok()
+ );
+ assert!(parse_attribute("rid:110 send max-fs=10").is_ok());
+ assert!(parse_attribute("rid:110 recv max-width=1920;max-height=1080").is_ok());
+
+ check_parse_and_serialize("rid:110 recv max-mbps=420;max-cpb=3;max-dpb=3");
+ check_parse_and_serialize("rid:110 recv scale-down-by=1.35;depends=1,2,3");
+ check_parse_and_serialize("rid:110 recv max-width=10;depends=1,2,3");
+ check_parse_and_serialize("rid:110 recv max-fs=10;UNKNOWN=100;depends=1,2,3");
+
+ assert!(parse_attribute("rid:").is_err());
+ assert!(parse_attribute("rid:120 send pt=").is_err());
+ assert!(parse_attribute("rid:120 send pt=;max-width=10").is_err());
+ assert!(parse_attribute("rid:120 send pt=9;max-width=").is_err());
+ assert!(parse_attribute("rid:120 send pt=9;max-width=;max-width=10").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rid_and_verify() {
+ let check_parse = make_check_parse!(SdpAttributeRid, SdpAttribute::Rid);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse, SdpAttribute::Rid);
+
+ check_parse_and_serialize("rid:foo send");
+ let mut rid = check_parse("rid:foo send");
+ assert_eq!(rid.id, "foo");
+ assert_eq!(rid.direction, SdpSingleDirection::Send);
+
+ check_parse_and_serialize("rid:110 send pt=9");
+ rid = check_parse("rid:110 send pt=9");
+ assert_eq!(rid.id, "110");
+ assert_eq!(rid.direction, SdpSingleDirection::Send);
+ assert_eq!(rid.formats, vec![9]);
+
+ check_parse_and_serialize("rid:110 send pt=9,10;max-fs=10;UNKNOWN=100;depends=1,2,3");
+ rid = check_parse("rid:110 send pt=9,10;max-fs=10;UNKNOWN=100;depends=1,2,3");
+ assert_eq!(rid.id, "110");
+ assert_eq!(rid.direction, SdpSingleDirection::Send);
+ assert_eq!(rid.formats, vec![9, 10]);
+ assert_eq!(rid.params.max_fs, 10);
+ assert_eq!(rid.params.unknown, vec!["UNKNOWN=100"]);
+ assert_eq!(rid.depends, vec!["1", "2", "3"]);
+
+ check_parse_and_serialize("rid:110 recv max-fps=42;max-fs=10;max-br=3;max-pps=1000");
+ rid = check_parse("rid:110 recv max-fps=42;max-fs=10;max-br=3;max-pps=1000");
+ assert_eq!(rid.id, "110");
+ assert_eq!(rid.direction, SdpSingleDirection::Recv);
+ assert_eq!(rid.params.max_fps, 42);
+ assert_eq!(rid.params.max_fs, 10);
+ assert_eq!(rid.params.max_br, 3);
+ assert_eq!(rid.params.max_pps, 1000);
+}
+
+#[test]
+fn test_parse_attribute_recvonly() {
+ let check_parse = make_check_parse!(SdpAttribute::Recvonly);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("recvonly");
+ assert!(parse_attribute("recvonly foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_remote_candidate() {
+ let check_parse = make_check_parse!(SdpAttributeRemoteCandidate, SdpAttribute::RemoteCandidate);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::RemoteCandidate);
+
+ check_parse_and_serialize("remote-candidates:0 10.0.0.1 5555");
+ check_parse_and_serialize("remote-candidates:12345 ::1 5555");
+
+ assert!(parse_attribute("remote-candidates:abc 10.0.0.1 5555").is_err());
+ assert!(parse_attribute("remote-candidates:0 10.0.0.1 70000").is_err());
+ assert!(parse_attribute("remote-candidates:0 10.0.0.1").is_err());
+ assert!(parse_attribute("remote-candidates:0").is_err());
+ assert!(parse_attribute("remote-candidates:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_sendonly() {
+ let check_parse = make_check_parse!(SdpAttribute::Sendonly);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("sendonly");
+ assert!(parse_attribute("sendonly foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_sendrecv() {
+ let check_parse = make_check_parse!(SdpAttribute::Sendrecv);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("sendrecv");
+ assert!(parse_attribute("sendrecv foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_setup() {
+ let check_parse = make_check_parse!(SdpAttributeSetup, SdpAttribute::Setup);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Setup);
+
+ check_parse_and_serialize("setup:active");
+ check_parse_and_serialize("setup:passive");
+ check_parse_and_serialize("setup:actpass");
+ check_parse_and_serialize("setup:holdconn");
+
+ assert!(parse_attribute("setup:").is_err());
+ assert!(parse_attribute("setup:foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtcp() {
+ let check_parse = make_check_parse!(SdpAttributeRtcp, SdpAttribute::Rtcp);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Rtcp);
+
+ check_parse_and_serialize("rtcp:5000");
+ check_parse_and_serialize("rtcp:9 IN IP4 0.0.0.0");
+ check_parse_and_serialize("rtcp:9 IN IP6 2001:db8::1");
+
+ assert!(parse_attribute("rtcp:").is_err());
+ assert!(parse_attribute("rtcp:70000").is_err());
+ assert!(parse_attribute("rtcp:9 IN").is_err());
+ assert!(parse_attribute("rtcp:9 IN IP4").is_err());
+ assert!(parse_attribute("rtcp:9 IN IP4 ::1").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtcp_fb() {
+ let check_parse = make_check_parse!(SdpAttributeRtcpFb, SdpAttribute::Rtcpfb);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Rtcpfb);
+
+ check_parse_and_serialize("rtcp-fb:101 ack rpsi");
+ check_parse_and_serialize("rtcp-fb:101 ack app");
+ check_parse_and_serialize("rtcp-fb:101 ccm");
+ check_parse_and_serialize("rtcp-fb:101 ccm fir");
+ check_parse_and_serialize("rtcp-fb:101 ccm tmmbr");
+ check_parse_and_serialize("rtcp-fb:101 ccm tstr");
+ check_parse_and_serialize("rtcp-fb:101 ccm vbcm");
+ check_parse_and_serialize("rtcp-fb:101 nack");
+ check_parse_and_serialize("rtcp-fb:101 nack sli");
+ check_parse_and_serialize("rtcp-fb:101 nack pli");
+ check_parse_and_serialize("rtcp-fb:101 nack rpsi");
+ check_parse_and_serialize("rtcp-fb:101 nack app");
+ check_parse_and_serialize("rtcp-fb:101 trr-int 1");
+ check_parse_and_serialize("rtcp-fb:101 goog-remb");
+ check_parse_and_serialize("rtcp-fb:101 transport-cc");
+
+ assert!(parse_attribute("rtcp-fb:101 unknown").is_err());
+ assert!(parse_attribute("rtcp-fb:101 ack").is_err());
+ assert!(parse_attribute("rtcp-fb:101 ccm unknwon").is_err());
+ assert!(parse_attribute("rtcp-fb:101 nack unknown").is_err());
+ assert!(parse_attribute("rtcp-fb:101 trr-int").is_err());
+ assert!(parse_attribute("rtcp-fb:101 trr-int a").is_err());
+ assert!(parse_attribute("rtcp-fb:101 goog-remb unknown").is_err());
+ assert!(parse_attribute("rtcp-fb:101 transport-cc unknown").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtcp_mux() {
+ let check_parse = make_check_parse!(SdpAttribute::RtcpMux);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("rtcp-mux");
+ assert!(parse_attribute("rtcp-mux foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtcp_mux_only() {
+ let check_parse = make_check_parse!(SdpAttribute::RtcpMuxOnly);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("rtcp-mux-only");
+ assert!(parse_attribute("rtcp-mux-only bar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtcp_rsize() {
+ let check_parse = make_check_parse!(SdpAttribute::RtcpRsize);
+ let check_parse_and_serialize = make_check_parse_and_serialize!(check_parse);
+
+ check_parse_and_serialize("rtcp-rsize");
+ assert!(parse_attribute("rtcp-rsize foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtpmap() {
+ let check_parse = make_check_parse!(SdpAttributeRtpmap, SdpAttribute::Rtpmap);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Rtpmap);
+
+ check_parse_and_serialize("rtpmap:109 opus/48000");
+ check_parse_and_serialize("rtpmap:109 opus/48000/2");
+
+ assert!(parse_attribute("rtpmap: ").is_err());
+ assert!(parse_attribute("rtpmap:109 ").is_err());
+ assert!(parse_attribute("rtpmap:109 opus").is_err());
+ assert!(parse_attribute("rtpmap:128 opus/48000").is_err());
+}
+
+#[test]
+fn test_parse_attribute_sctpmap() {
+ let check_parse = make_check_parse!(SdpAttributeSctpmap, SdpAttribute::Sctpmap);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Sctpmap);
+
+ check_parse_and_serialize("sctpmap:5000 webrtc-datachannel 256");
+
+ assert!(parse_attribute("sctpmap:70000 webrtc-datachannel").is_err());
+ assert!(parse_attribute("sctpmap:70000 webrtc-datachannel 256").is_err());
+ assert!(parse_attribute("sctpmap:5000 unsupported 256").is_err());
+ assert!(parse_attribute("sctpmap:5000 webrtc-datachannel 2a").is_err());
+}
+
+#[test]
+fn test_parse_attribute_sctp_port() {
+ let check_parse = make_check_parse!(u64, SdpAttribute::SctpPort);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::SctpPort);
+
+ check_parse_and_serialize("sctp-port:5000");
+
+ assert!(parse_attribute("sctp-port:").is_err());
+ assert!(parse_attribute("sctp-port:70000").is_err());
+}
+
+#[test]
+fn test_parse_attribute_max_message_size() {
+ let check_parse = make_check_parse!(u64, SdpAttribute::MaxMessageSize);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::MaxMessageSize);
+
+ check_parse_and_serialize("max-message-size:1");
+ check_parse_and_serialize("max-message-size:100000");
+ check_parse_and_serialize("max-message-size:4294967297");
+ check_parse_and_serialize("max-message-size:0");
+
+ assert!(parse_attribute("max-message-size:").is_err());
+ assert!(parse_attribute("max-message-size:abc").is_err());
+}
+
+#[test]
+fn test_parse_attribute_simulcast() {
+ let check_parse = make_check_parse!(SdpAttributeSimulcast, SdpAttribute::Simulcast);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Simulcast);
+
+ check_parse_and_serialize("simulcast:send 1");
+ check_parse_and_serialize("simulcast:recv test");
+ check_parse_and_serialize("simulcast:recv ~test");
+ check_parse_and_serialize("simulcast:recv test;foo");
+ check_parse_and_serialize("simulcast:recv foo,bar");
+ check_parse_and_serialize("simulcast:recv foo,bar;test");
+ check_parse_and_serialize("simulcast:send 1;4,5 recv 6;7");
+ check_parse_and_serialize("simulcast:send 1,2,3;~4,~5 recv 6;~7,~8");
+ // old draft 03 notation used by Firefox 55
+ assert!(parse_attribute("simulcast: send rid=foo;bar").is_ok());
+
+ assert!(parse_attribute("simulcast:").is_err());
+ assert!(parse_attribute("simulcast:send").is_err());
+ assert!(parse_attribute("simulcast:foobar 1").is_err());
+ assert!(parse_attribute("simulcast:send 1 foobar 2").is_err());
+ // old draft 03 notation used by Firefox 55
+ assert!(parse_attribute("simulcast: send foo=8;10").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ssrc() {
+ let check_parse = make_check_parse!(SdpAttributeSsrc, SdpAttribute::Ssrc);
+ let check_parse_and_serialize =
+ make_check_parse_and_serialize!(check_parse, SdpAttribute::Ssrc);
+
+ check_parse_and_serialize("ssrc:2655508255");
+ check_parse_and_serialize("ssrc:2655508255 foo");
+ check_parse_and_serialize("ssrc:2655508255 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}");
+ check_parse_and_serialize("ssrc:2082260239 msid:1d0cdb4e-5934-4f0f-9f88-40392cb60d31 315b086a-5cb6-4221-89de-caf0b038c79d");
+
+ assert!(parse_attribute("ssrc:").is_err());
+ assert!(parse_attribute("ssrc:foo").is_err());
+}
+
+#[test]
+fn test_anonymize_attribute_ssrc() -> Result<(), SdpParserInternalError> {
+ let mut anon = StatefulSdpAnonymizer::new();
+ let parsed = parse_attribute("ssrc:2655508255 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}")?;
+ let (ssrc1, masked) = if let SdpType::Attribute(a) = parsed {
+ let masked = a.masked_clone(&mut anon);
+ match (a, masked) {
+ (SdpAttribute::Ssrc(ssrc), SdpAttribute::Ssrc(masked)) => (ssrc, masked),
+ (_, _) => unreachable!(),
+ }
+ } else {
+ unreachable!()
+ };
+ assert_eq!(ssrc1.id, masked.id);
+ assert_eq!(ssrc1.attribute, masked.attribute);
+ assert_eq!("cname-00000001", masked.value.unwrap());
+
+ let ssrc2 = parse_attribute("ssrc:2082260239 msid:1d0cdb4e-5934-4f0f-9f88-40392cb60d31 315b086a-5cb6-4221-89de-caf0b038c79d")?;
+ if let SdpType::Attribute(SdpAttribute::Ssrc(ssrc2)) = ssrc2 {
+ let masked = ssrc2.masked_clone(&mut anon);
+ assert_eq!(ssrc2.id, masked.id);
+ assert_eq!(ssrc2.attribute, masked.attribute);
+ assert_eq!(ssrc2.value, masked.value);
+ } else {
+ unreachable!()
+ }
+ Ok(())
+}
+
+#[test]
+fn test_parse_attribute_ssrc_group() {
+ let parsed = parse_attribute("ssrc-group:FID 3156517279 2673335628");
+ match parsed {
+ Ok(SdpType::Attribute(attr)) => {
+ assert_eq!(attr.to_string(), "ssrc-group:FID 3156517279 2673335628");
+ let (semantic, ssrcs) = match attr {
+ SdpAttribute::SsrcGroup(semantic, ssrcs) => {
+ let stringified_ssrcs: Vec<String> =
+ ssrcs.iter().map(|ssrc| ssrc.to_string()).collect();
+ (semantic.to_string(), stringified_ssrcs)
+ }
+ _ => unreachable!(),
+ };
+ assert_eq!(semantic, "FID");
+ assert_eq!(ssrcs.len(), 2);
+ assert_eq!(ssrcs[0], "3156517279");
+ assert_eq!(ssrcs[1], "2673335628");
+ }
+ Err(e) => panic!("{}", e),
+ _ => unreachable!(),
+ }
+
+ assert!(parse_attribute("ssrc-group:").is_err());
+ assert!(parse_attribute("ssrc-group:BLAH").is_err());
+ assert!(parse_attribute("ssrc-group:FID").is_err());
+}
+
+#[test]
+fn test_parse_unknown_attribute() {
+ assert!(parse_attribute("unknown").is_err())
+}
diff --git a/third_party/rust/webrtc-sdp/src/error.rs b/third_party/rust/webrtc-sdp/src/error.rs
new file mode 100644
index 0000000000..c1cbc49a72
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/error.rs
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#[cfg(feature = "serialize")]
+use serde::ser::{Serialize, SerializeStruct, Serializer};
+use std::error;
+use std::error::Error;
+use std::fmt;
+extern crate url;
+use address::AddressType;
+use std::num::ParseFloatError;
+use std::num::ParseIntError;
+
+#[derive(Debug, Clone)]
+pub enum SdpParserInternalError {
+ UnknownAddressType(String),
+ AddressTypeMismatch {
+ found: AddressType,
+ expected: AddressType,
+ },
+ Generic(String),
+ Unsupported(String),
+ Integer(ParseIntError),
+ Float(ParseFloatError),
+ Domain(url::ParseError),
+ IpAddress(std::net::AddrParseError),
+}
+
+const INTERNAL_ERROR_MESSAGE_UNKNOWN_ADDRESS_TYPE: &str = "Unknown address type";
+const INTERNAL_ERROR_MESSAGE_ADDRESS_TYPE_MISMATCH: &str =
+ "Address is of a different type(1) than declared(2)";
+
+impl fmt::Display for SdpParserInternalError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpParserInternalError::UnknownAddressType(ref unknown) => write!(
+ f,
+ "{INTERNAL_ERROR_MESSAGE_UNKNOWN_ADDRESS_TYPE}: {unknown}"
+ ),
+ SdpParserInternalError::AddressTypeMismatch { found, expected } => write!(
+ f,
+ "{INTERNAL_ERROR_MESSAGE_ADDRESS_TYPE_MISMATCH}: {found}, {expected}"
+ ),
+ SdpParserInternalError::Generic(ref message) => write!(f, "Parsing error: {message}"),
+ SdpParserInternalError::Unsupported(ref message) => {
+ write!(f, "Unsupported parsing error: {message}")
+ }
+ SdpParserInternalError::Integer(ref error) => {
+ write!(f, "Integer parsing error: {error}")
+ }
+ SdpParserInternalError::Float(ref error) => write!(f, "Float parsing error: {error}"),
+ SdpParserInternalError::Domain(ref error) => {
+ write!(f, "Domain name parsing error: {error}")
+ }
+ SdpParserInternalError::IpAddress(ref error) => {
+ write!(f, "IP address parsing error: {error}")
+ }
+ }
+ }
+}
+
+impl Error for SdpParserInternalError {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ match *self {
+ SdpParserInternalError::Integer(ref error) => Some(error),
+ SdpParserInternalError::Float(ref error) => Some(error),
+ SdpParserInternalError::Domain(ref error) => Some(error),
+ SdpParserInternalError::IpAddress(ref error) => Some(error),
+ // Can't tell much more about our internal errors
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum SdpParserError {
+ Line {
+ error: SdpParserInternalError,
+ line: String,
+ line_number: usize,
+ },
+ Unsupported {
+ error: SdpParserInternalError,
+ line: String,
+ line_number: usize,
+ },
+ Sequence {
+ message: String,
+ line_number: usize,
+ },
+}
+
+#[cfg(feature = "serialize")]
+impl Serialize for SdpParserError {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let mut state = serializer.serialize_struct(
+ "error",
+ match self {
+ SdpParserError::Sequence { .. } => 3,
+ _ => 4,
+ },
+ )?;
+ match self {
+ SdpParserError::Line { error, line, .. } => {
+ state.serialize_field("type", "Line")?;
+ state.serialize_field("message", &format!("{error}"))?;
+ state.serialize_field("line", line)?
+ }
+ SdpParserError::Unsupported { error, line, .. } => {
+ state.serialize_field("type", "Unsupported")?;
+ state.serialize_field("message", &format!("{error}"))?;
+ state.serialize_field("line", line)?
+ }
+ SdpParserError::Sequence { message, .. } => {
+ state.serialize_field("type", "Sequence")?;
+ state.serialize_field("message", message)?;
+ }
+ };
+ state.serialize_field(
+ "line_number",
+ &match *self {
+ SdpParserError::Line { line_number, .. } => line_number,
+ SdpParserError::Unsupported { line_number, .. } => line_number,
+ SdpParserError::Sequence { line_number, .. } => line_number,
+ },
+ )?;
+ state.end()
+ }
+}
+
+impl fmt::Display for SdpParserError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ SdpParserError::Line {
+ error,
+ line,
+ line_number,
+ } => write!(f, "Line error: {error} in line({line_number}): {line}"),
+ SdpParserError::Unsupported {
+ error,
+ line,
+ line_number,
+ } => write!(f, "Unsupported: {error} in line({line_number}): {line}",),
+ SdpParserError::Sequence {
+ message,
+ line_number,
+ } => write!(f, "Sequence error in line({line_number}): {message}"),
+ }
+ }
+}
+
+impl Error for SdpParserError {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ match self {
+ SdpParserError::Line { error, .. } | SdpParserError::Unsupported { error, .. } => {
+ Some(error)
+ }
+ // Can't tell much more about our internal errors
+ _ => None,
+ }
+ }
+}
+
+impl From<ParseIntError> for SdpParserInternalError {
+ fn from(err: ParseIntError) -> SdpParserInternalError {
+ SdpParserInternalError::Integer(err)
+ }
+}
+
+impl From<url::ParseError> for SdpParserInternalError {
+ fn from(err: url::ParseError) -> SdpParserInternalError {
+ SdpParserInternalError::Domain(err)
+ }
+}
+
+impl From<std::net::AddrParseError> for SdpParserInternalError {
+ fn from(err: std::net::AddrParseError) -> SdpParserInternalError {
+ SdpParserInternalError::IpAddress(err)
+ }
+}
+
+impl From<ParseFloatError> for SdpParserInternalError {
+ fn from(err: ParseFloatError) -> SdpParserInternalError {
+ SdpParserInternalError::Float(err)
+ }
+}
+
+#[cfg(test)]
+#[path = "./error_tests.rs"]
+mod tests;
diff --git a/third_party/rust/webrtc-sdp/src/error_tests.rs b/third_party/rust/webrtc-sdp/src/error_tests.rs
new file mode 100644
index 0000000000..49f88f044d
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/error_tests.rs
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::*;
+use address::Address;
+use std::str::FromStr;
+#[test]
+fn test_sdp_parser_internal_error_unknown_address_type() {
+ let error = SdpParserInternalError::UnknownAddressType("foo".to_string());
+ assert_eq!(
+ format!("{error}"),
+ format!("{}: {}", INTERNAL_ERROR_MESSAGE_UNKNOWN_ADDRESS_TYPE, "foo")
+ );
+ assert!(error.source().is_none());
+}
+#[test]
+fn test_sdp_parser_internal_error_address_type_mismatch() {
+ let error = SdpParserInternalError::AddressTypeMismatch {
+ found: AddressType::IpV4,
+ expected: AddressType::IpV6,
+ };
+ assert_eq!(
+ format!("{error}"),
+ format!(
+ "{}: {}, {}",
+ INTERNAL_ERROR_MESSAGE_ADDRESS_TYPE_MISMATCH,
+ AddressType::IpV4,
+ AddressType::IpV6
+ )
+ );
+ assert!(error.source().is_none());
+}
+
+#[test]
+fn test_sdp_parser_internal_error_generic() {
+ let generic = SdpParserInternalError::Generic("generic message".to_string());
+ assert_eq!(format!("{generic}"), "Parsing error: generic message");
+ assert!(generic.source().is_none());
+}
+
+#[test]
+fn test_sdp_parser_internal_error_unsupported() {
+ let unsupported =
+ SdpParserInternalError::Unsupported("unsupported internal message".to_string());
+ assert_eq!(
+ format!("{unsupported}"),
+ "Unsupported parsing error: unsupported internal message"
+ );
+ assert!(unsupported.source().is_none());
+}
+
+#[test]
+fn test_sdp_parser_internal_error_integer() {
+ let v = "12a";
+ let integer = v.parse::<u64>();
+ assert!(integer.is_err());
+ let int_err = SdpParserInternalError::Integer(integer.err().unwrap());
+ assert_eq!(
+ format!("{int_err}"),
+ "Integer parsing error: invalid digit found in string"
+ );
+ assert!(int_err.source().is_some());
+}
+
+#[test]
+fn test_sdp_parser_internal_error_float() {
+ let v = "12.2a";
+ let float = v.parse::<f32>();
+ assert!(float.is_err());
+ let int_err = SdpParserInternalError::Float(float.err().unwrap());
+ assert_eq!(
+ format!("{int_err}"),
+ "Float parsing error: invalid float literal"
+ );
+ assert!(int_err.source().is_some());
+}
+
+#[test]
+fn test_sdp_parser_internal_error_address() {
+ let v = "127.0.0.500";
+ let addr_err = Address::from_str(v).err().unwrap();
+ assert_eq!(
+ format!("{addr_err}"),
+ "Domain name parsing error: invalid IPv4 address"
+ );
+ assert!(addr_err.source().is_some());
+}
+
+#[test]
+fn test_sdp_parser_error_line() {
+ let line1 = SdpParserError::Line {
+ error: SdpParserInternalError::Generic("test message".to_string()),
+ line: "test line".to_string(),
+ line_number: 13,
+ };
+ assert_eq!(
+ format!("{line1}"),
+ "Line error: Parsing error: test message in line(13): test line"
+ );
+ assert!(line1.source().is_some());
+}
+
+#[test]
+fn test_sdp_parser_error_unsupported() {
+ let unsupported1 = SdpParserError::Unsupported {
+ error: SdpParserInternalError::Generic("unsupported value".to_string()),
+ line: "unsupported line".to_string(),
+ line_number: 21,
+ };
+ assert_eq!(
+ format!("{unsupported1}"),
+ "Unsupported: Parsing error: unsupported value in line(21): unsupported line"
+ );
+ assert!(unsupported1.source().is_some());
+}
+
+#[test]
+fn test_sdp_parser_error_sequence() {
+ let sequence1 = SdpParserError::Sequence {
+ message: "sequence message".to_string(),
+ line_number: 42,
+ };
+ assert_eq!(
+ format!("{sequence1}"),
+ "Sequence error in line(42): sequence message"
+ );
+ assert!(sequence1.source().is_none());
+}
diff --git a/third_party/rust/webrtc-sdp/src/lib.rs b/third_party/rust/webrtc-sdp/src/lib.rs
new file mode 100644
index 0000000000..0a26adc3ea
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/lib.rs
@@ -0,0 +1,907 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#![warn(clippy::all)]
+#![forbid(unsafe_code)]
+
+#[macro_use]
+extern crate log;
+#[cfg(feature = "serialize")]
+#[macro_use]
+extern crate serde_derive;
+#[cfg(feature = "serialize")]
+extern crate serde;
+use std::convert::TryFrom;
+use std::fmt;
+
+#[macro_use]
+pub mod attribute_type;
+pub mod address;
+pub mod anonymizer;
+pub mod error;
+pub mod media_type;
+pub mod network;
+
+use address::{AddressTyped, ExplicitlyTypedAddress};
+use anonymizer::{AnonymizingClone, StatefulSdpAnonymizer};
+use attribute_type::{
+ parse_attribute, SdpAttribute, SdpAttributeRid, SdpAttributeSimulcastVersion, SdpAttributeType,
+ SdpSingleDirection,
+};
+use error::{SdpParserError, SdpParserInternalError};
+use media_type::{
+ parse_media, parse_media_vector, SdpFormatList, SdpMedia, SdpMediaLine, SdpMediaValue,
+ SdpProtocolValue,
+};
+use network::{parse_address_type, parse_network_type};
+
+/*
+ * RFC4566
+ * bandwidth-fields = *(%x62 "=" bwtype ":" bandwidth CRLF)
+ */
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpBandwidth {
+ As(u32),
+ Ct(u32),
+ Tias(u32),
+ Unknown(String, u32),
+}
+
+impl fmt::Display for SdpBandwidth {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let (tp_string, value) = match *self {
+ SdpBandwidth::As(ref x) => ("AS", x),
+ SdpBandwidth::Ct(ref x) => ("CT", x),
+ SdpBandwidth::Tias(ref x) => ("TIAS", x),
+ SdpBandwidth::Unknown(ref tp, ref x) => (&tp[..], x),
+ };
+ write!(f, "{tp_string}:{value}")
+ }
+}
+
+/*
+ * RFC4566
+ * connection-field = [%x63 "=" nettype SP addrtype SP
+ * connection-address CRLF]
+ */
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpConnection {
+ pub address: ExplicitlyTypedAddress,
+ pub ttl: Option<u8>,
+ pub amount: Option<u32>,
+}
+
+impl fmt::Display for SdpConnection {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.address.fmt(f)?;
+ write_option_string!(f, "/{}", self.ttl)?;
+ write_option_string!(f, "/{}", self.amount)
+ }
+}
+
+impl AnonymizingClone for SdpConnection {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ let mut masked = self.clone();
+ masked.address = anon.mask_typed_address(&self.address);
+ masked
+ }
+}
+
+/*
+ * RFC4566
+ * origin-field = %x6f "=" username SP sess-id SP sess-version SP
+ * nettype SP addrtype SP unicast-address CRLF
+ */
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpOrigin {
+ pub username: String,
+ pub session_id: u64,
+ pub session_version: u64,
+ pub unicast_addr: ExplicitlyTypedAddress,
+}
+
+impl fmt::Display for SdpOrigin {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{username} {sess_id} {sess_vers} {unicast_addr}",
+ username = self.username,
+ sess_id = self.session_id,
+ sess_vers = self.session_version,
+ unicast_addr = self.unicast_addr
+ )
+ }
+}
+
+impl AnonymizingClone for SdpOrigin {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ let mut masked = self.clone();
+ masked.username = anon.mask_origin_user(&self.username);
+ masked.unicast_addr = anon.mask_typed_address(&masked.unicast_addr);
+ masked
+ }
+}
+
+/*
+ * RFC4566
+ * time-fields = 1*( %x74 "=" start-time SP stop-time
+ * *(CRLF repeat-fields) CRLF)
+ * [zone-adjustments CRLF]
+ */
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpTiming {
+ pub start: u64,
+ pub stop: u64,
+}
+
+impl fmt::Display for SdpTiming {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{start} {stop}", start = self.start, stop = self.stop)
+ }
+}
+
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpType {
+ // Note: Email, Information, Key, Phone, Repeat, Uri and Zone are left out
+ // on purposes as we don't want to support them.
+ Attribute(SdpAttribute),
+ Bandwidth(SdpBandwidth),
+ Connection(SdpConnection),
+ Media(SdpMediaLine),
+ Origin(SdpOrigin),
+ Session(String),
+ Timing(SdpTiming),
+ Version(u64),
+}
+
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpLine {
+ pub line_number: usize,
+ pub sdp_type: SdpType,
+ pub text: String,
+}
+
+/*
+ * RFC4566
+ * ; SDP Syntax
+ * session-description = proto-version
+ * origin-field
+ * session-name-field
+ * information-field
+ * uri-field
+ * email-fields
+ * phone-fields
+ * connection-field
+ * bandwidth-fields
+ * time-fields
+ * key-field
+ * attribute-fields
+ * media-descriptions
+ */
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpSession {
+ pub version: u64,
+ pub origin: SdpOrigin,
+ pub session: Option<String>,
+ pub connection: Option<SdpConnection>,
+ pub bandwidth: Vec<SdpBandwidth>,
+ pub timing: Option<SdpTiming>,
+ pub attribute: Vec<SdpAttribute>,
+ pub media: Vec<SdpMedia>,
+ pub warnings: Vec<SdpParserError>, // unsupported values:
+ // information: Option<String>,
+ // uri: Option<String>,
+ // email: Option<String>,
+ // phone: Option<String>,
+ // repeat: Option<String>,
+ // zone: Option<String>,
+ // key: Option<String>
+}
+
+impl fmt::Display for SdpSession {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "v={version}\r\n\
+ o={origin}\r\n\
+ s={session}\r\n\
+ {timing}\
+ {bandwidth}\
+ {connection}\
+ {session_attributes}\
+ {media_sections}",
+ version = self.version,
+ origin = self.origin,
+ session = self.get_session_text(),
+ timing = option_to_string!("t={}\r\n", self.timing),
+ bandwidth = maybe_vector_to_string!("b={}\r\n", self.bandwidth, "\r\nb="),
+ connection = option_to_string!("c={}\r\n", self.connection),
+ session_attributes = maybe_vector_to_string!("a={}\r\n", self.attribute, "\r\na="),
+ media_sections = self.media.iter().map(|s| s.to_string()).collect::<String>(),
+ )
+ }
+}
+
+impl SdpSession {
+ pub fn new(version: u64, origin: SdpOrigin, session: String) -> SdpSession {
+ let session = match session.trim() {
+ s if !s.is_empty() => Some(s.to_owned()),
+ _ => None,
+ };
+ SdpSession {
+ version,
+ origin,
+ session,
+ connection: None,
+ bandwidth: Vec::new(),
+ timing: None,
+ attribute: Vec::new(),
+ media: Vec::new(),
+ warnings: Vec::new(),
+ }
+ }
+
+ pub fn get_version(&self) -> u64 {
+ self.version
+ }
+
+ pub fn get_origin(&self) -> &SdpOrigin {
+ &self.origin
+ }
+
+ pub fn get_session(&self) -> &Option<String> {
+ &self.session
+ }
+
+ pub fn get_session_text(&self) -> &str {
+ if let Some(text) = &self.session {
+ text.as_str()
+ } else {
+ " "
+ }
+ }
+ pub fn get_connection(&self) -> &Option<SdpConnection> {
+ &self.connection
+ }
+
+ pub fn set_connection(&mut self, c: SdpConnection) {
+ self.connection = Some(c)
+ }
+
+ pub fn add_bandwidth(&mut self, b: SdpBandwidth) {
+ self.bandwidth.push(b)
+ }
+
+ pub fn set_timing(&mut self, t: SdpTiming) {
+ self.timing = Some(t)
+ }
+
+ pub fn add_attribute(&mut self, a: SdpAttribute) -> Result<(), SdpParserInternalError> {
+ if !a.allowed_at_session_level() {
+ return Err(SdpParserInternalError::Generic(format!(
+ "{a} not allowed at session level"
+ )));
+ };
+ self.attribute.push(a);
+ Ok(())
+ }
+
+ pub fn extend_media(&mut self, v: Vec<SdpMedia>) {
+ self.media.extend(v)
+ }
+
+ pub fn parse_session_vector(&mut self, lines: &mut Vec<SdpLine>) -> Result<(), SdpParserError> {
+ while !lines.is_empty() {
+ let line = lines.remove(0);
+ match line.sdp_type {
+ SdpType::Attribute(a) => {
+ let _line_number = line.line_number;
+ self.add_attribute(a).map_err(|e: SdpParserInternalError| {
+ SdpParserError::Sequence {
+ message: format!("{e}"),
+ line_number: _line_number,
+ }
+ })?
+ }
+ SdpType::Bandwidth(b) => self.add_bandwidth(b),
+ SdpType::Timing(t) => self.set_timing(t),
+ SdpType::Connection(c) => self.set_connection(c),
+
+ SdpType::Origin(_) | SdpType::Session(_) | SdpType::Version(_) => {
+ return Err(SdpParserError::Sequence {
+ message: "version, origin or session at wrong level".to_string(),
+ line_number: line.line_number,
+ });
+ }
+ SdpType::Media(_) => {
+ return Err(SdpParserError::Sequence {
+ message: "media line not allowed in session parser".to_string(),
+ line_number: line.line_number,
+ });
+ }
+ }
+ }
+ Ok(())
+ }
+
+ pub fn get_attribute(&self, t: SdpAttributeType) -> Option<&SdpAttribute> {
+ self.attribute
+ .iter()
+ .find(|a| SdpAttributeType::from(*a) == t)
+ }
+
+ pub fn add_media(
+ &mut self,
+ media_type: SdpMediaValue,
+ direction: SdpAttribute,
+ port: u32,
+ protocol: SdpProtocolValue,
+ addr: ExplicitlyTypedAddress,
+ ) -> Result<(), SdpParserInternalError> {
+ let mut media = SdpMedia::new(SdpMediaLine {
+ media: media_type,
+ port,
+ port_count: 1,
+ proto: protocol,
+ formats: SdpFormatList::Integers(Vec::new()),
+ });
+
+ media.add_attribute(direction)?;
+
+ media.set_connection(SdpConnection {
+ address: addr,
+ ttl: None,
+ amount: None,
+ });
+
+ self.media.push(media);
+
+ Ok(())
+ }
+}
+
+impl AnonymizingClone for SdpSession {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ let mut masked: SdpSession = SdpSession {
+ version: self.version,
+ session: self.session.clone(),
+ origin: self.origin.masked_clone(anon),
+ connection: self.connection.clone(),
+ timing: self.timing.clone(),
+ bandwidth: self.bandwidth.clone(),
+ attribute: Vec::new(),
+ media: Vec::new(),
+ warnings: Vec::new(),
+ };
+ masked.origin = self.origin.masked_clone(anon);
+ masked.connection = masked.connection.map(|con| con.masked_clone(anon));
+ for i in &self.attribute {
+ masked.attribute.push(i.masked_clone(anon));
+ }
+ masked
+ }
+}
+
+/* removing this wrap would not allow us to call this from the match statement inside
+ * parse_sdp_line() */
+#[allow(clippy::unnecessary_wraps)]
+fn parse_session(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ trace!("session: {}", value);
+ Ok(SdpType::Session(String::from(value)))
+}
+
+fn parse_version(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ let ver = value.parse::<u64>()?;
+ if ver != 0 {
+ return Err(SdpParserInternalError::Generic(format!(
+ "version type contains unsupported value {ver}"
+ )));
+ };
+ trace!("version: {}", ver);
+ Ok(SdpType::Version(ver))
+}
+
+fn parse_origin(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ let mut tokens = value.split_whitespace();
+ let username = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Origin type is missing username token".to_string(),
+ ));
+ }
+ Some(x) => x,
+ };
+ let session_id = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Origin type is missing session ID token".to_string(),
+ ));
+ }
+ Some(x) => x.parse::<u64>()?,
+ };
+ let session_version = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Origin type is missing session version token".to_string(),
+ ));
+ }
+ Some(x) => x.parse::<u64>()?,
+ };
+ match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Origin type is missing network type token".to_string(),
+ ));
+ }
+ Some(x) => parse_network_type(x)?,
+ };
+ let addrtype = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Origin type is missing address type token".to_string(),
+ ));
+ }
+ Some(x) => parse_address_type(x)?,
+ };
+ let unicast_addr = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Origin type is missing IP address token".to_string(),
+ ));
+ }
+ Some(x) => ExplicitlyTypedAddress::try_from((addrtype, x))?,
+ };
+ if addrtype != unicast_addr.address_type() {
+ return Err(SdpParserInternalError::Generic(
+ "Origin addrtype does not match address.".to_string(),
+ ));
+ }
+ let o = SdpOrigin {
+ username: String::from(username),
+ session_id,
+ session_version,
+ unicast_addr,
+ };
+ trace!("origin: {}", o);
+ Ok(SdpType::Origin(o))
+}
+
+fn parse_connection(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ let cv: Vec<&str> = value.split_whitespace().collect();
+ if cv.len() != 3 {
+ return Err(SdpParserInternalError::Generic(
+ "connection attribute must have three tokens".to_string(),
+ ));
+ }
+ parse_network_type(cv[0])?;
+ let addrtype = parse_address_type(cv[1])?;
+ let mut ttl = None;
+ let mut amount = None;
+ let mut addr_token = cv[2];
+ if addr_token.find('/').is_some() {
+ let addr_tokens: Vec<&str> = addr_token.split('/').collect();
+ if addr_tokens.len() >= 3 {
+ amount = Some(addr_tokens[2].parse::<u32>()?);
+ }
+ ttl = Some(addr_tokens[1].parse::<u8>()?);
+ addr_token = addr_tokens[0];
+ }
+ let address = ExplicitlyTypedAddress::try_from((addrtype, addr_token))?;
+ let c = SdpConnection {
+ address,
+ ttl,
+ amount,
+ };
+ trace!("connection: {}", c);
+ Ok(SdpType::Connection(c))
+}
+
+fn parse_bandwidth(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ let bv: Vec<&str> = value.split(':').collect();
+ if bv.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "bandwidth attribute must have two tokens".to_string(),
+ ));
+ }
+ let bandwidth = bv[1].parse::<u32>()?;
+ let bw = match bv[0].to_uppercase().as_ref() {
+ "AS" => SdpBandwidth::As(bandwidth),
+ "CT" => SdpBandwidth::Ct(bandwidth),
+ "TIAS" => SdpBandwidth::Tias(bandwidth),
+ _ => SdpBandwidth::Unknown(String::from(bv[0]), bandwidth),
+ };
+ trace!("bandwidth: {}", bw);
+ Ok(SdpType::Bandwidth(bw))
+}
+
+fn parse_timing(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ let tv: Vec<&str> = value.split_whitespace().collect();
+ if tv.len() != 2 {
+ return Err(SdpParserInternalError::Generic(
+ "timing attribute must have two tokens".to_string(),
+ ));
+ }
+ let start = tv[0].parse::<u64>()?;
+ let stop = tv[1].parse::<u64>()?;
+ let t = SdpTiming { start, stop };
+ trace!("timing: {}", t);
+ Ok(SdpType::Timing(t))
+}
+
+pub fn parse_sdp_line(line: &str, line_number: usize) -> Result<SdpLine, SdpParserError> {
+ if line.find('=').is_none() {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("missing = character in line".to_string()),
+ line: line.to_string(),
+ line_number,
+ });
+ }
+ let mut splitted_line = line.splitn(2, '=');
+ let line_type = match splitted_line.next() {
+ None => {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("missing type".to_string()),
+ line: line.to_string(),
+ line_number,
+ });
+ }
+ Some(t) => {
+ let trimmed = t.trim();
+ if trimmed.len() > 1 {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("type too long".to_string()),
+ line: line.to_string(),
+ line_number,
+ });
+ }
+ if trimmed.is_empty() {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("type is empty".to_string()),
+ line: line.to_string(),
+ line_number,
+ });
+ }
+ trimmed.to_lowercase()
+ }
+ };
+ let (line_value, untrimmed_line_value) = match splitted_line.next() {
+ None => {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("missing value".to_string()),
+ line: line.to_string(),
+ line_number,
+ });
+ }
+ Some(v) => {
+ let trimmed = v.trim();
+ // For compatibility with sites that don't adhere to "s=-" for no session ID
+ if trimmed.is_empty() && line_type.as_str() != "s" {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("value is empty".to_string()),
+ line: line.to_string(),
+ line_number,
+ });
+ }
+ (trimmed, v)
+ }
+ };
+ match line_type.as_ref() {
+ "a" => parse_attribute(line_value),
+ "b" => parse_bandwidth(line_value),
+ "c" => parse_connection(line_value),
+ "e" => Err(SdpParserInternalError::Generic(format!(
+ "unsupported type email: {line_value}"
+ ))),
+ "i" => Err(SdpParserInternalError::Generic(format!(
+ "unsupported type information: {line_value}"
+ ))),
+ "k" => Err(SdpParserInternalError::Generic(format!(
+ "unsupported insecure key exchange: {line_value}"
+ ))),
+ "m" => parse_media(line_value),
+ "o" => parse_origin(line_value),
+ "p" => Err(SdpParserInternalError::Generic(format!(
+ "unsupported type phone: {line_value}"
+ ))),
+ "r" => Err(SdpParserInternalError::Generic(format!(
+ "unsupported type repeat: {line_value}"
+ ))),
+ "s" => parse_session(untrimmed_line_value),
+ "t" => parse_timing(line_value),
+ "u" => Err(SdpParserInternalError::Generic(format!(
+ "unsupported type uri: {line_value}"
+ ))),
+ "v" => parse_version(line_value),
+ "z" => Err(SdpParserInternalError::Generic(format!(
+ "unsupported type zone: {line_value}"
+ ))),
+ _ => Err(SdpParserInternalError::Generic(
+ "unknown sdp type".to_string(),
+ )),
+ }
+ .map(|sdp_type| SdpLine {
+ line_number,
+ sdp_type,
+ text: line.to_owned(),
+ })
+ .map_err(|e| match e {
+ SdpParserInternalError::UnknownAddressType(..)
+ | SdpParserInternalError::AddressTypeMismatch { .. }
+ | SdpParserInternalError::Generic(..)
+ | SdpParserInternalError::Integer(..)
+ | SdpParserInternalError::Float(..)
+ | SdpParserInternalError::Domain(..)
+ | SdpParserInternalError::IpAddress(..) => SdpParserError::Line {
+ error: e,
+ line: line.to_string(),
+ line_number,
+ },
+ SdpParserInternalError::Unsupported(..) => SdpParserError::Unsupported {
+ error: e,
+ line: line.to_string(),
+ line_number,
+ },
+ })
+}
+
+fn sanity_check_sdp_session(session: &SdpSession) -> Result<(), SdpParserError> {
+ let make_seq_error = |x: &str| SdpParserError::Sequence {
+ message: x.to_string(),
+ line_number: 0,
+ };
+
+ if session.timing.is_none() {
+ return Err(make_seq_error("Missing timing type at session level"));
+ }
+ // Checks that all media have connections if there is no top level
+ // This explicitly allows for zero connection lines if there are no media
+ // sections for interoperability reasons.
+ let media_cons = &session.media.iter().all(|m| m.get_connection().is_some());
+ if !media_cons && session.get_connection().is_none() {
+ return Err(make_seq_error(
+ "Without connection type at session level all media sections must have connection types",
+ ));
+ }
+
+ // Check that extmaps are not defined on session and media level
+ if session.get_attribute(SdpAttributeType::Extmap).is_some() {
+ for msection in &session.media {
+ if msection.get_attribute(SdpAttributeType::Extmap).is_some() {
+ return Err(make_seq_error(
+ "Extmap can't be define at session and media level",
+ ));
+ }
+ }
+ }
+
+ for msection in &session.media {
+ if msection
+ .get_attribute(SdpAttributeType::RtcpMuxOnly)
+ .is_some()
+ && msection.get_attribute(SdpAttributeType::RtcpMux).is_none()
+ {
+ return Err(make_seq_error(
+ "rtcp-mux-only media sections must also contain the rtcp-mux attribute",
+ ));
+ }
+
+ let rids: Vec<&SdpAttributeRid> = msection
+ .get_attributes()
+ .iter()
+ .filter_map(|attr| match *attr {
+ SdpAttribute::Rid(ref rid) => Some(rid),
+ _ => None,
+ })
+ .collect();
+ let recv_rids: Vec<&str> = rids
+ .iter()
+ .filter_map(|rid| match rid.direction {
+ SdpSingleDirection::Recv => Some(rid.id.as_str()),
+ _ => None,
+ })
+ .collect();
+ let send_rids: Vec<&str> = rids
+ .iter()
+ .filter_map(|rid| match rid.direction {
+ SdpSingleDirection::Send => Some(rid.id.as_str()),
+ _ => None,
+ })
+ .collect();
+
+ for rid_format in rids.iter().flat_map(|rid| &rid.formats) {
+ match *msection.get_formats() {
+ SdpFormatList::Integers(ref int_fmt) => {
+ if !int_fmt.contains(&(u32::from(*rid_format))) {
+ return Err(make_seq_error(
+ "Rid pts must be declared in the media section",
+ ));
+ }
+ }
+ SdpFormatList::Strings(ref str_fmt) => {
+ if !str_fmt.contains(&rid_format.to_string()) {
+ return Err(make_seq_error(
+ "Rid pts must be declared in the media section",
+ ));
+ }
+ }
+ }
+ }
+
+ if let Some(SdpAttribute::Simulcast(simulcast)) =
+ msection.get_attribute(SdpAttributeType::Simulcast)
+ {
+ let check_defined_rids =
+ |simulcast_version_list: &Vec<SdpAttributeSimulcastVersion>,
+ rid_ids: &[&str]|
+ -> Result<(), SdpParserError> {
+ for simulcast_rid in simulcast_version_list.iter().flat_map(|x| &x.ids) {
+ if !rid_ids.contains(&simulcast_rid.id.as_str()) {
+ return Err(make_seq_error(
+ "Simulcast RIDs must be defined in any rid attribute",
+ ));
+ }
+ }
+ Ok(())
+ };
+
+ check_defined_rids(&simulcast.receive, &recv_rids)?;
+ check_defined_rids(&simulcast.send, &send_rids)?;
+ }
+ }
+
+ Ok(())
+}
+
+fn parse_sdp_vector(lines: &mut Vec<SdpLine>) -> Result<SdpSession, SdpParserError> {
+ if lines.len() < 4 {
+ return Err(SdpParserError::Sequence {
+ message: "SDP neeeds at least 4 lines".to_string(),
+ line_number: 0,
+ });
+ }
+
+ let version = match lines.remove(0).sdp_type {
+ SdpType::Version(v) => v,
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "first line needs to be version number".to_string(),
+ line_number: 0,
+ });
+ }
+ };
+ let origin = match lines.remove(0).sdp_type {
+ SdpType::Origin(v) => v,
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "second line needs to be origin".to_string(),
+ line_number: 1,
+ });
+ }
+ };
+ let session = match lines.remove(0).sdp_type {
+ SdpType::Session(v) => v,
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "third line needs to be session".to_string(),
+ line_number: 2,
+ });
+ }
+ };
+ let mut sdp_session = SdpSession::new(version, origin, session);
+
+ let _media_pos = lines
+ .iter()
+ .position(|l| matches!(l.sdp_type, SdpType::Media(_)));
+
+ match _media_pos {
+ Some(p) => {
+ let mut media: Vec<_> = lines.drain(p..).collect();
+ sdp_session.parse_session_vector(lines)?;
+ sdp_session.extend_media(parse_media_vector(&mut media)?);
+ }
+ None => sdp_session.parse_session_vector(lines)?,
+ };
+
+ sanity_check_sdp_session(&sdp_session)?;
+ Ok(sdp_session)
+}
+
+pub fn parse_sdp(sdp: &str, fail_on_warning: bool) -> Result<SdpSession, SdpParserError> {
+ if sdp.is_empty() {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("empty SDP".to_string()),
+ line: sdp.to_string(),
+ line_number: 0,
+ });
+ }
+ // see test_parse_sdp_minimal_sdp_successfully
+ if sdp.len() < 51 {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("string too short to be valid SDP".to_string()),
+ line: sdp.to_string(),
+ line_number: 0,
+ });
+ }
+ let lines = sdp.lines();
+ let mut errors: Vec<SdpParserError> = Vec::new();
+ let mut warnings: Vec<SdpParserError> = Vec::new();
+ let mut sdp_lines: Vec<SdpLine> = Vec::new();
+ for (line_number, line) in lines.enumerate() {
+ let stripped_line = line.trim();
+ if stripped_line.is_empty() {
+ continue;
+ }
+ match parse_sdp_line(line, line_number) {
+ Ok(n) => {
+ sdp_lines.push(n);
+ }
+ Err(e) => {
+ match e {
+ // TODO is this really a good way to accomplish this?
+ SdpParserError::Line {
+ error,
+ line,
+ line_number,
+ } => errors.push(SdpParserError::Line {
+ error,
+ line,
+ line_number,
+ }),
+ SdpParserError::Unsupported {
+ error,
+ line,
+ line_number,
+ } => {
+ warnings.push(SdpParserError::Unsupported {
+ error,
+ line,
+ line_number,
+ });
+ }
+ SdpParserError::Sequence {
+ message,
+ line_number,
+ } => errors.push(SdpParserError::Sequence {
+ message,
+ line_number,
+ }),
+ }
+ }
+ };
+ }
+
+ if fail_on_warning && (!warnings.is_empty()) {
+ return Err(warnings.remove(0));
+ }
+
+ // We just return the last of the errors here
+ if let Some(e) = errors.pop() {
+ return Err(e);
+ };
+
+ let mut session = parse_sdp_vector(&mut sdp_lines)?;
+ session.warnings = warnings;
+
+ for warning in &session.warnings {
+ warn!("Warning: {}", &warning);
+ }
+
+ Ok(session)
+}
+
+#[cfg(test)]
+#[path = "./lib_tests.rs"]
+mod tests;
diff --git a/third_party/rust/webrtc-sdp/src/lib_tests.rs b/third_party/rust/webrtc-sdp/src/lib_tests.rs
new file mode 100644
index 0000000000..3b20fc9fb8
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/lib_tests.rs
@@ -0,0 +1,774 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate url;
+use super::*;
+use address::{Address, AddressType};
+use anonymizer::ToBytesVec;
+use std::net::IpAddr;
+use std::net::Ipv4Addr;
+
+fn create_dummy_sdp_session() -> SdpSession {
+ let origin = parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0");
+ assert!(origin.is_ok());
+ let connection = parse_connection("IN IP4 198.51.100.7");
+ assert!(connection.is_ok());
+ let mut sdp_session;
+ if let SdpType::Origin(o) = origin.unwrap() {
+ sdp_session = SdpSession::new(0, o, "-".to_string());
+
+ if let Ok(SdpType::Connection(c)) = connection {
+ sdp_session.connection = Some(c);
+ } else {
+ unreachable!();
+ }
+ } else {
+ unreachable!();
+ }
+ sdp_session
+}
+
+pub fn create_dummy_media_section() -> SdpMedia {
+ let media_line = SdpMediaLine {
+ media: SdpMediaValue::Audio,
+ port: 9,
+ port_count: 0,
+ proto: SdpProtocolValue::RtpSavpf,
+ formats: SdpFormatList::Integers(Vec::new()),
+ };
+ SdpMedia::new(media_line)
+}
+
+#[test]
+fn test_session_works() -> Result<(), SdpParserInternalError> {
+ parse_session("topic")?;
+ Ok(())
+}
+
+#[test]
+fn test_version_works() -> Result<(), SdpParserInternalError> {
+ parse_version("0")?;
+ Ok(())
+}
+
+#[test]
+fn test_version_unsupported_input() {
+ assert!(parse_version("1").is_err());
+ assert!(parse_version("11").is_err());
+ assert!(parse_version("a").is_err());
+}
+
+#[test]
+fn test_origin_works() -> Result<(), SdpParserInternalError> {
+ parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0")?;
+ parse_origin("mozilla 506705521068071134 0 IN IP6 2001:db8::1")?;
+ Ok(())
+}
+
+#[test]
+fn test_origin_missing_username() {
+ assert!(parse_origin("").is_err());
+}
+
+#[test]
+fn test_origin_missing_session_id() {
+ assert!(parse_origin("mozilla ").is_err());
+}
+
+#[test]
+fn test_origin_missing_session_version() {
+ assert!(parse_origin("mozilla 506705521068071134 ").is_err());
+}
+
+#[test]
+fn test_origin_missing_nettype() {
+ assert!(parse_origin("mozilla 506705521068071134 0 ").is_err());
+}
+
+#[test]
+fn test_origin_unsupported_nettype() {
+ assert!(parse_origin("mozilla 506705521068071134 0 UNSUPPORTED IP4 0.0.0.0").is_err());
+}
+
+#[test]
+fn test_origin_missing_addtype() {
+ assert!(parse_origin("mozilla 506705521068071134 0 IN ").is_err());
+}
+
+#[test]
+fn test_origin_missing_ip_addr() {
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP4 ").is_err());
+}
+
+#[test]
+fn test_origin_unsupported_addrtpe() {
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP1 0.0.0.0").is_err());
+}
+
+#[test]
+fn test_origin_invalid_ip_addr() {
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP4 1.1.1.256").is_err());
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP6 ::g").is_err());
+}
+
+#[test]
+fn test_origin_addr_type_mismatch() {
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP4 ::1").is_err());
+}
+
+#[test]
+fn connection_works() -> Result<(), SdpParserInternalError> {
+ parse_connection("IN IP4 127.0.0.1")?;
+ parse_connection("IN IP4 127.0.0.1/10/10")?;
+ parse_connection("IN IP6 ::1")?;
+ parse_connection("IN IP6 ::1/1/1")?;
+ Ok(())
+}
+
+#[test]
+fn connection_lots_of_whitespace() -> Result<(), SdpParserInternalError> {
+ parse_connection("IN IP4 127.0.0.1")?;
+ Ok(())
+}
+
+#[test]
+fn connection_wrong_amount_of_tokens() {
+ assert!(parse_connection("IN IP4").is_err());
+ assert!(parse_connection("IN IP4 0.0.0.0 foobar").is_err());
+}
+
+#[test]
+fn connection_unsupported_nettype() {
+ assert!(parse_connection("UNSUPPORTED IP4 0.0.0.0").is_err());
+}
+
+#[test]
+fn connection_unsupported_addrtpe() {
+ assert!(parse_connection("IN IP1 0.0.0.0").is_err());
+}
+
+#[test]
+fn connection_broken_ip_addr() {
+ assert!(parse_connection("IN IP4 1.1.1.256").is_err());
+ assert!(parse_connection("IN IP6 ::g").is_err());
+}
+
+#[test]
+fn connection_addr_type_mismatch() {
+ assert!(parse_connection("IN IP4 ::1").is_err());
+}
+
+#[test]
+fn bandwidth_works() -> Result<(), SdpParserInternalError> {
+ parse_bandwidth("AS:1")?;
+ parse_bandwidth("CT:123")?;
+ parse_bandwidth("TIAS:12345")?;
+ Ok(())
+}
+
+#[test]
+fn bandwidth_wrong_amount_of_tokens() {
+ assert!(parse_bandwidth("TIAS").is_err());
+ assert!(parse_bandwidth("TIAS:12345:xyz").is_err());
+}
+
+#[test]
+fn bandwidth_unsupported_type() -> Result<(), SdpParserInternalError> {
+ parse_bandwidth("UNSUPPORTED:12345")?;
+ Ok(())
+}
+
+#[test]
+fn test_timing_works() -> Result<(), SdpParserInternalError> {
+ parse_timing("0 0")?;
+ Ok(())
+}
+
+#[test]
+fn test_timing_non_numeric_tokens() {
+ assert!(parse_timing("a 0").is_err());
+ assert!(parse_timing("0 a").is_err());
+}
+
+#[test]
+fn test_timing_wrong_amount_of_tokens() {
+ assert!(parse_timing("0").is_err());
+ assert!(parse_timing("0 0 0").is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_works() -> Result<(), SdpParserError> {
+ parse_sdp_line("v=0", 0)?;
+ parse_sdp_line("s=somesession", 0)?;
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_line_empty_line() {
+ assert!(parse_sdp_line("", 0).is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_unsupported_types() {
+ assert!(parse_sdp_line("e=foobar", 0).is_err());
+ assert!(parse_sdp_line("i=foobar", 0).is_err());
+ assert!(parse_sdp_line("k=foobar", 0).is_err());
+ assert!(parse_sdp_line("p=foobar", 0).is_err());
+ assert!(parse_sdp_line("r=foobar", 0).is_err());
+ assert!(parse_sdp_line("u=foobar", 0).is_err());
+ assert!(parse_sdp_line("z=foobar", 0).is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_unknown_key() {
+ assert!(parse_sdp_line("y=foobar", 0).is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_too_long_type() {
+ assert!(parse_sdp_line("ab=foobar", 0).is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_without_equal() {
+ assert!(parse_sdp_line("abcd", 0).is_err());
+ assert!(parse_sdp_line("ab cd", 0).is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_empty_value() {
+ assert!(parse_sdp_line("v=", 0).is_err());
+ assert!(parse_sdp_line("o=", 0).is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_empty_name() {
+ assert!(parse_sdp_line("=abc", 0).is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_valid_a_line() -> Result<(), SdpParserError> {
+ parse_sdp_line("a=rtpmap:8 PCMA/8000", 0)?;
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_line_invalid_a_line() {
+ assert!(parse_sdp_line("a=rtpmap:200 PCMA/8000", 0).is_err());
+}
+
+#[test]
+fn test_add_attribute() -> Result<(), SdpParserInternalError> {
+ let mut sdp_session = create_dummy_sdp_session();
+
+ sdp_session.add_attribute(SdpAttribute::Sendrecv)?;
+ assert!(sdp_session.add_attribute(SdpAttribute::BundleOnly).is_err());
+ assert_eq!(sdp_session.attribute.len(), 1);
+ Ok(())
+}
+
+#[test]
+fn test_sanity_check_sdp_session_timing() -> Result<(), SdpParserError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ sdp_session.extend_media(vec![create_dummy_media_section()]);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_err());
+
+ let t = SdpTiming { start: 0, stop: 0 };
+ sdp_session.set_timing(t);
+
+ sanity_check_sdp_session(&sdp_session)?;
+ Ok(())
+}
+
+#[test]
+fn test_sanity_check_sdp_session_media() -> Result<(), SdpParserError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ let t = SdpTiming { start: 0, stop: 0 };
+ sdp_session.set_timing(t);
+
+ sanity_check_sdp_session(&sdp_session)?;
+
+ sdp_session.extend_media(vec![create_dummy_media_section()]);
+
+ sanity_check_sdp_session(&sdp_session)?;
+ Ok(())
+}
+
+#[test]
+fn test_sanity_check_sdp_connection() -> Result<(), SdpParserInternalError> {
+ let origin = parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0")?;
+ let mut sdp_session;
+ if let SdpType::Origin(o) = origin {
+ sdp_session = SdpSession::new(0, o, "-".to_string());
+ } else {
+ unreachable!();
+ }
+ let t = SdpTiming { start: 0, stop: 0 };
+ sdp_session.set_timing(t);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+
+ // the dummy media section doesn't contain a connection
+ sdp_session.extend_media(vec![create_dummy_media_section()]);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_err());
+
+ let connection = parse_connection("IN IP6 ::1")?;
+ if let SdpType::Connection(c) = connection {
+ sdp_session.connection = Some(c);
+ } else {
+ unreachable!();
+ }
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+
+ let mut second_media = create_dummy_media_section();
+ let mconnection = parse_connection("IN IP4 0.0.0.0")?;
+ if let SdpType::Connection(c) = mconnection {
+ second_media.set_connection(c);
+ } else {
+ unreachable!();
+ }
+ sdp_session.extend_media(vec![second_media]);
+ assert!(sdp_session.media.len() == 2);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+ Ok(())
+}
+
+#[test]
+fn test_sanity_check_sdp_session_extmap() -> Result<(), SdpParserInternalError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ let t = SdpTiming { start: 0, stop: 0 };
+ sdp_session.set_timing(t);
+ sdp_session.extend_media(vec![create_dummy_media_section()]);
+
+ let attribute =
+ parse_attribute("extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time")?;
+ if let SdpType::Attribute(a) = attribute {
+ sdp_session.add_attribute(a)?;
+ } else {
+ unreachable!();
+ }
+ assert!(sdp_session
+ .get_attribute(SdpAttributeType::Extmap)
+ .is_some());
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+
+ let mut second_media = create_dummy_media_section();
+ let mattribute =
+ parse_attribute("extmap:1/sendonly urn:ietf:params:rtp-hdrext:ssrc-audio-level")?;
+ if let SdpType::Attribute(ma) = mattribute {
+ second_media.add_attribute(ma)?;
+ } else {
+ unreachable!();
+ }
+ assert!(second_media
+ .get_attribute(SdpAttributeType::Extmap)
+ .is_some());
+
+ sdp_session.extend_media(vec![second_media]);
+ assert!(sdp_session.media.len() == 2);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_err());
+
+ sdp_session.attribute = Vec::new();
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+ Ok(())
+}
+
+#[test]
+fn test_sanity_check_sdp_session_simulcast() -> Result<(), SdpParserError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ let t = SdpTiming { start: 0, stop: 0 };
+ sdp_session.set_timing(t);
+ sdp_session.extend_media(vec![create_dummy_media_section()]);
+
+ sanity_check_sdp_session(&sdp_session)?;
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_zero_length_string_fails() {
+ assert!(parse_sdp("", true).is_err());
+}
+
+#[test]
+fn test_parse_sdp_to_short_string() {
+ assert!(parse_sdp("fooooobarrrr", true).is_err());
+}
+
+#[test]
+fn test_parse_sdp_minimal_sdp_successfully() -> Result<(), SdpParserError> {
+ parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP6 ::1\r\n
+s=-\r\n
+c=IN IP6 ::1\r\n
+t=0 0\r\n",
+ true,
+ )?;
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_too_short() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+s=-\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_sdp_line_error() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+s=-\r\n
+t=0 foobar\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_sdp_unsupported_error() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+s=-\r\n
+t=0 0\r\n
+m=foobar 0 UDP/TLS/RTP/SAVPF 0\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_sdp_unsupported_warning() -> Result<(), SdpParserError> {
+ parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+s=-\r\n
+c=IN IP4 198.51.100.7\r\n
+t=0 0\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n
+a=unsupported\r\n",
+ false,
+ )?;
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_sequence_error() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+s=-\r\n
+t=0 0\r\n
+a=bundle-only\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_sdp_integer_error() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+s=-\r\n
+t=0 0\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n
+a=rtcp:34er21\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_sdp_ipaddr_error() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.a.b.0\r\n
+s=-\r\n
+t=0 0\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_sdp_invalid_session_attribute() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.a.b.0\r\n
+s=-\r\n
+t=0 0\r\n
+a=bundle-only\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_parse_sdp_invalid_media_attribute() {
+ assert!(parse_sdp(
+ "v=0\r\n
+o=- 0 0 IN IP4 0.a.b.0\r\n
+s=-\r\n
+t=0 0\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n
+a=ice-lite\r\n",
+ true
+ )
+ .is_err());
+}
+
+#[test]
+fn test_mask_origin() {
+ let mut anon = StatefulSdpAnonymizer::new();
+ if let SdpType::Origin(origin_1) =
+ parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0").unwrap()
+ {
+ for _ in 0..2 {
+ let masked = origin_1.masked_clone(&mut anon);
+ assert_eq!(masked.username, "origin-user-00000001");
+ assert_eq!(
+ masked.unicast_addr,
+ ExplicitlyTypedAddress::Ip(IpAddr::V4(Ipv4Addr::from(1)))
+ );
+ }
+ } else {
+ unreachable!();
+ }
+}
+
+#[test]
+fn test_mask_sdp() {
+ let mut anon = StatefulSdpAnonymizer::new();
+ let sdp = parse_sdp(
+ "v=0\r\n
+ o=ausername 4294967296 2 IN IP4 127.0.0.1\r\n
+ s=SIP Call\r\n
+ c=IN IP4 198.51.100.7/51\r\n
+ a=ice-pwd:12340\r\n
+ a=ice-ufrag:4a799b2e\r\n
+ a=fingerprint:sha-1 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC\r\n
+ t=0 0\r\n
+ m=video 56436 RTP/SAVPF 120\r\n
+ a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host\r\n
+ a=remote-candidates:0 10.0.0.1 5555\r\n
+ a=rtpmap:120 VP8/90000\r\n",
+ true,
+ )
+ .unwrap();
+ let mut masked = sdp.masked_clone(&mut anon);
+ assert_eq!(masked.origin.username, "origin-user-00000001");
+ assert_eq!(
+ masked.origin.unicast_addr,
+ ExplicitlyTypedAddress::Ip(IpAddr::V4(Ipv4Addr::from(1)))
+ );
+ assert_eq!(
+ masked.connection.unwrap().address,
+ ExplicitlyTypedAddress::Ip(IpAddr::V4(Ipv4Addr::from(2)))
+ );
+ let mut attributes = masked.attribute;
+ for m in &mut masked.media {
+ for attribute in m.get_attributes() {
+ attributes.push(attribute.clone());
+ }
+ }
+ for attribute in attributes {
+ match attribute {
+ SdpAttribute::Candidate(c) => {
+ assert_eq!(c.address, Address::Ip(IpAddr::V4(Ipv4Addr::from(3))));
+ assert_eq!(c.port, 1);
+ }
+ SdpAttribute::Fingerprint(f) => {
+ assert_eq!(f.fingerprint, 1u64.to_byte_vec());
+ }
+ SdpAttribute::IcePwd(p) => {
+ assert_eq!(p, "ice-password-00000001");
+ }
+ SdpAttribute::IceUfrag(u) => {
+ assert_eq!(u, "ice-user-00000001");
+ }
+ SdpAttribute::RemoteCandidate(r) => {
+ assert_eq!(r.address, Address::Ip(IpAddr::V4(Ipv4Addr::from(4))));
+ assert_eq!(r.port, 2);
+ }
+ _ => {}
+ }
+ }
+}
+
+#[test]
+fn test_parse_session_vector() -> Result<(), SdpParserError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("a=sendrecv", 1)?];
+ sdp_session.parse_session_vector(&mut lines)?;
+ assert_eq!(sdp_session.attribute.len(), 1);
+ Ok(())
+}
+
+#[test]
+fn test_parse_session_vector_non_session_attribute() -> Result<(), SdpParserError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("a=bundle-only", 2)?];
+ assert!(sdp_session.parse_session_vector(&mut lines).is_err());
+ assert_eq!(sdp_session.attribute.len(), 0);
+ Ok(())
+}
+
+#[test]
+fn test_parse_session_vector_version_repeated() -> Result<(), SdpParserError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("v=0", 3)?];
+ assert!(sdp_session.parse_session_vector(&mut lines).is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_session_vector_contains_media_type() -> Result<(), SdpParserError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("m=audio 0 UDP/TLS/RTP/SAVPF 0", 4)?];
+ assert!(sdp_session.parse_session_vector(&mut lines).is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_vector_no_media_section() -> Result<(), SdpParserError> {
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("v=0", 1)?];
+ lines.push(parse_sdp_line(
+ "o=ausername 4294967296 2 IN IP4 127.0.0.1",
+ 1,
+ )?);
+ lines.push(parse_sdp_line("s=SIP Call", 1)?);
+ lines.push(parse_sdp_line("t=0 0", 1)?);
+ lines.push(parse_sdp_line("c=IN IP6 ::1", 1)?);
+ assert!(parse_sdp_vector(&mut lines).is_ok());
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_vector_with_media_section() -> Result<(), SdpParserError> {
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("v=0", 1)?];
+ lines.push(parse_sdp_line(
+ "o=ausername 4294967296 2 IN IP4 127.0.0.1",
+ 1,
+ )?);
+ lines.push(parse_sdp_line("s=SIP Call", 1)?);
+ lines.push(parse_sdp_line("t=0 0", 1)?);
+ lines.push(parse_sdp_line("m=video 56436 RTP/SAVPF 120", 1)?);
+ lines.push(parse_sdp_line("c=IN IP6 ::1", 1)?);
+ assert!(parse_sdp_vector(&mut lines).is_ok());
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_vector_with_missing_rtcp_mux() -> Result<(), SdpParserError> {
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("v=0", 1)?];
+ lines.push(parse_sdp_line(
+ "o=ausername 4294967296 2 IN IP4 127.0.0.1",
+ 1,
+ )?);
+ lines.push(parse_sdp_line("s=SIP Call", 1)?);
+ lines.push(parse_sdp_line("t=0 0", 1)?);
+ lines.push(parse_sdp_line("m=video 56436 RTP/SAVPF 120", 1)?);
+ lines.push(parse_sdp_line("c=IN IP6 ::1", 1)?);
+ lines.push(parse_sdp_line("a=rtcp-mux-only", 1)?);
+ assert!(parse_sdp_vector(&mut lines).is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_vector_too_short() -> Result<(), SdpParserError> {
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("v=0", 1)?];
+ assert!(parse_sdp_vector(&mut lines).is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_vector_missing_version() -> Result<(), SdpParserError> {
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line(
+ "o=ausername 4294967296 2 IN IP4 127.0.0.1",
+ 1,
+ )?];
+ for _ in 0..3 {
+ lines.push(parse_sdp_line("a=sendrecv", 1)?);
+ }
+ assert!(parse_sdp_vector(&mut lines).is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_vector_missing_origin() -> Result<(), SdpParserError> {
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("v=0", 1)?];
+ for _ in 0..3 {
+ lines.push(parse_sdp_line("a=sendrecv", 1)?);
+ }
+ assert!(parse_sdp_vector(&mut lines).is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_sdp_vector_missing_session() -> Result<(), SdpParserError> {
+ let mut lines: Vec<SdpLine> = vec![parse_sdp_line("v=0", 1)?];
+ lines.push(parse_sdp_line(
+ "o=ausername 4294967296 2 IN IP4 127.0.0.1",
+ 1,
+ )?);
+ for _ in 0..2 {
+ lines.push(parse_sdp_line("a=sendrecv", 1)?);
+ }
+ assert!(parse_sdp_vector(&mut lines).is_err());
+ Ok(())
+}
+
+#[test]
+fn test_session_add_media_works() {
+ let mut sdp_session = create_dummy_sdp_session();
+ assert!(sdp_session
+ .add_media(
+ SdpMediaValue::Audio,
+ SdpAttribute::Sendrecv,
+ 99,
+ SdpProtocolValue::RtpSavpf,
+ ExplicitlyTypedAddress::from(Ipv4Addr::new(127, 0, 0, 1))
+ )
+ .is_ok());
+ assert!(sdp_session.get_connection().is_some());
+ assert_eq!(sdp_session.attribute.len(), 0);
+ assert_eq!(sdp_session.media.len(), 1);
+ assert_eq!(sdp_session.media[0].get_attributes().len(), 1);
+ assert!(sdp_session.media[0]
+ .get_attribute(SdpAttributeType::Sendrecv)
+ .is_some());
+}
+
+#[test]
+fn test_session_add_media_invalid_attribute_fails() -> Result<(), SdpParserInternalError> {
+ let mut sdp_session = create_dummy_sdp_session();
+ assert!(sdp_session
+ .add_media(
+ SdpMediaValue::Audio,
+ SdpAttribute::IceLite,
+ 99,
+ SdpProtocolValue::RtpSavpf,
+ ExplicitlyTypedAddress::try_from((AddressType::IpV4, "127.0.0.1"))?
+ )
+ .is_err());
+ Ok(())
+}
diff --git a/third_party/rust/webrtc-sdp/src/media_type.rs b/third_party/rust/webrtc-sdp/src/media_type.rs
new file mode 100644
index 0000000000..91a11c6039
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/media_type.rs
@@ -0,0 +1,480 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use anonymizer::{AnonymizingClone, StatefulSdpAnonymizer};
+use attribute_type::{
+ maybe_print_param, SdpAttribute, SdpAttributeRtpmap, SdpAttributeSctpmap, SdpAttributeType,
+};
+use error::{SdpParserError, SdpParserInternalError};
+use std::fmt;
+use {SdpBandwidth, SdpConnection, SdpLine, SdpType};
+
+/*
+ * RFC4566
+ * media-field = %x6d "=" media SP port ["/" integer]
+ * SP proto 1*(SP fmt) CRLF
+ */
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpMediaLine {
+ pub media: SdpMediaValue,
+ pub port: u32,
+ pub port_count: u32,
+ pub proto: SdpProtocolValue,
+ pub formats: SdpFormatList,
+}
+
+impl fmt::Display for SdpMediaLine {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{media} {port}{pcount} {proto} {formats}",
+ media = self.media,
+ port = self.port,
+ pcount = maybe_print_param("/", self.port_count, 0),
+ proto = self.proto,
+ formats = self.formats
+ )
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpMediaValue {
+ Audio,
+ Video,
+ Application,
+}
+
+impl fmt::Display for SdpMediaValue {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpMediaValue::Audio => "audio",
+ SdpMediaValue::Video => "video",
+ SdpMediaValue::Application => "application",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+pub enum SdpProtocolValue {
+ RtpAvp, /* RTP/AVP [RFC4566] */
+ RtpAvpf, /* RTP/AVPF [RFC4585] */
+ RtpSavp, /* RTP/SAVP [RFC3711] */
+ RtpSavpf, /* RTP/SAVPF [RFC5124] */
+ TcpDtlsRtpSavp, /* TCP/DTLS/RTP/SAVP [RFC7850] */
+ TcpDtlsRtpSavpf, /* TCP/DTLS/RTP/SAVPF [RFC7850] */
+ UdpTlsRtpSavp, /* UDP/TLS/RTP/SAVP [RFC5764] */
+ UdpTlsRtpSavpf, /* UDP/TLS/RTP/SAVPF [RFC5764] */
+ DtlsSctp, /* DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-07] */
+ UdpDtlsSctp, /* UDP/DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-26] */
+ TcpDtlsSctp, /* TCP/DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-26] */
+}
+
+impl fmt::Display for SdpProtocolValue {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpProtocolValue::RtpAvp => "RTP/AVP",
+ SdpProtocolValue::RtpAvpf => "RTP/AVPF",
+ SdpProtocolValue::RtpSavp => "RTP/SAVP",
+ SdpProtocolValue::RtpSavpf => "RTP/SAVPF",
+ SdpProtocolValue::TcpDtlsRtpSavp => "TCP/DTLS/RTP/SAVP",
+ SdpProtocolValue::TcpDtlsRtpSavpf => "TCP/DTLS/RTP/SAVPF",
+ SdpProtocolValue::UdpTlsRtpSavp => "UDP/TLS/RTP/SAVP",
+ SdpProtocolValue::UdpTlsRtpSavpf => "UDP/TLS/RTP/SAVPF",
+ SdpProtocolValue::DtlsSctp => "DTLS/SCTP",
+ SdpProtocolValue::UdpDtlsSctp => "UDP/DTLS/SCTP",
+ SdpProtocolValue::TcpDtlsSctp => "TCP/DTLS/SCTP",
+ }
+ .fmt(f)
+ }
+}
+
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub enum SdpFormatList {
+ Integers(Vec<u32>),
+ Strings(Vec<String>),
+}
+
+impl fmt::Display for SdpFormatList {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpFormatList::Integers(ref x) => maybe_vector_to_string!("{}", x, " "),
+ SdpFormatList::Strings(ref x) => x.join(" "),
+ }
+ .fmt(f)
+ }
+}
+
+/*
+ * RFC4566
+ * media-descriptions = *( media-field
+ * information-field
+ * *connection-field
+ * bandwidth-fields
+ * key-field
+ * attribute-fields )
+ */
+#[derive(Clone)]
+#[cfg_attr(feature = "serialize", derive(Serialize))]
+#[cfg_attr(feature = "enhanced_debug", derive(Debug))]
+pub struct SdpMedia {
+ media: SdpMediaLine,
+ connection: Option<SdpConnection>,
+ bandwidth: Vec<SdpBandwidth>,
+ attribute: Vec<SdpAttribute>,
+ // unsupported values:
+ // information: Option<String>,
+ // key: Option<String>,
+}
+
+impl fmt::Display for SdpMedia {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "m={mline}\r\n{bw}{connection}{attributes}",
+ mline = self.media,
+ bw = maybe_vector_to_string!("b={}\r\n", self.bandwidth, "\r\nb="),
+ connection = option_to_string!("c={}\r\n", self.connection),
+ attributes = maybe_vector_to_string!("a={}\r\n", self.attribute, "\r\na=")
+ )
+ }
+}
+
+impl SdpMedia {
+ pub fn new(media: SdpMediaLine) -> SdpMedia {
+ SdpMedia {
+ media,
+ connection: None,
+ bandwidth: Vec::new(),
+ attribute: Vec::new(),
+ }
+ }
+
+ pub fn get_type(&self) -> &SdpMediaValue {
+ &self.media.media
+ }
+
+ pub fn set_port(&mut self, port: u32) {
+ self.media.port = port;
+ }
+
+ pub fn get_port(&self) -> u32 {
+ self.media.port
+ }
+
+ pub fn get_port_count(&self) -> u32 {
+ self.media.port_count
+ }
+
+ pub fn get_proto(&self) -> &SdpProtocolValue {
+ &self.media.proto
+ }
+
+ pub fn get_formats(&self) -> &SdpFormatList {
+ &self.media.formats
+ }
+
+ pub fn get_bandwidth(&self) -> &Vec<SdpBandwidth> {
+ &self.bandwidth
+ }
+
+ pub fn add_bandwidth(&mut self, bw: SdpBandwidth) {
+ self.bandwidth.push(bw)
+ }
+
+ pub fn get_attributes(&self) -> &Vec<SdpAttribute> {
+ &self.attribute
+ }
+
+ pub fn add_attribute(&mut self, attr: SdpAttribute) -> Result<(), SdpParserInternalError> {
+ if !attr.allowed_at_media_level() {
+ return Err(SdpParserInternalError::Generic(format!(
+ "{attr} not allowed at media level"
+ )));
+ }
+ self.attribute.push(attr);
+ Ok(())
+ }
+
+ pub fn get_attribute(&self, t: SdpAttributeType) -> Option<&SdpAttribute> {
+ self.attribute
+ .iter()
+ .find(|a| SdpAttributeType::from(*a) == t)
+ }
+
+ pub fn remove_attribute(&mut self, t: SdpAttributeType) {
+ self.attribute.retain(|a| SdpAttributeType::from(a) != t);
+ }
+
+ pub fn set_attribute(&mut self, attr: SdpAttribute) -> Result<(), SdpParserInternalError> {
+ self.remove_attribute(SdpAttributeType::from(&attr));
+ self.add_attribute(attr)
+ }
+
+ pub fn remove_codecs(&mut self) {
+ match self.media.formats {
+ SdpFormatList::Integers(_) => self.media.formats = SdpFormatList::Integers(Vec::new()),
+ SdpFormatList::Strings(_) => self.media.formats = SdpFormatList::Strings(Vec::new()),
+ }
+
+ self.attribute.retain({
+ |x| {
+ !matches!(
+ *x,
+ SdpAttribute::Rtpmap(_)
+ | SdpAttribute::Fmtp(_)
+ | SdpAttribute::Rtcpfb(_)
+ | SdpAttribute::Sctpmap(_)
+ | SdpAttribute::SctpPort(_)
+ )
+ }
+ });
+ }
+
+ pub fn add_codec(&mut self, rtpmap: SdpAttributeRtpmap) -> Result<(), SdpParserInternalError> {
+ match self.media.formats {
+ SdpFormatList::Integers(ref mut x) => x.push(u32::from(rtpmap.payload_type)),
+ SdpFormatList::Strings(ref mut x) => x.push(rtpmap.payload_type.to_string()),
+ }
+
+ self.add_attribute(SdpAttribute::Rtpmap(rtpmap))?;
+ Ok(())
+ }
+
+ pub fn get_attributes_of_type(&self, t: SdpAttributeType) -> Vec<&SdpAttribute> {
+ self.attribute
+ .iter()
+ .filter(|a| SdpAttributeType::from(*a) == t)
+ .collect()
+ }
+
+ pub fn get_connection(&self) -> &Option<SdpConnection> {
+ &self.connection
+ }
+
+ pub fn set_connection(&mut self, c: SdpConnection) {
+ self.connection = Some(c)
+ }
+
+ pub fn add_datachannel(
+ &mut self,
+ name: String,
+ port: u16,
+ streams: u16,
+ msg_size: u32,
+ ) -> Result<(), SdpParserInternalError> {
+ // Only one allowed, for now. This may change as the specs (and deployments) evolve.
+ match self.media.proto {
+ SdpProtocolValue::UdpDtlsSctp | SdpProtocolValue::TcpDtlsSctp => {
+ // new data channel format according to draft 21
+ self.media.formats = SdpFormatList::Strings(vec![name]);
+ self.set_attribute(SdpAttribute::SctpPort(u64::from(port)))?;
+ }
+ _ => {
+ // old data channels format according to draft 05
+ self.media.formats = SdpFormatList::Integers(vec![u32::from(port)]);
+ self.set_attribute(SdpAttribute::Sctpmap(SdpAttributeSctpmap {
+ port,
+ channels: u32::from(streams),
+ }))?;
+ }
+ }
+ if msg_size > 0 {
+ self.set_attribute(SdpAttribute::MaxMessageSize(u64::from(msg_size)))?;
+ }
+ self.media.media = SdpMediaValue::Application;
+
+ Ok(())
+ }
+}
+
+impl AnonymizingClone for SdpMedia {
+ fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self {
+ let mut masked = SdpMedia {
+ media: self.media.clone(),
+ bandwidth: self.bandwidth.clone(),
+ connection: self.connection.clone(),
+ attribute: Vec::new(),
+ };
+ for i in &self.attribute {
+ masked.attribute.push(i.masked_clone(anon));
+ }
+ masked
+ }
+}
+
+fn parse_media_token(value: &str) -> Result<SdpMediaValue, SdpParserInternalError> {
+ Ok(match value.to_lowercase().as_ref() {
+ "audio" => SdpMediaValue::Audio,
+ "video" => SdpMediaValue::Video,
+ "application" => SdpMediaValue::Application,
+ _ => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "unsupported media value: {value}"
+ )));
+ }
+ })
+}
+
+fn parse_protocol_token(value: &str) -> Result<SdpProtocolValue, SdpParserInternalError> {
+ Ok(match value.to_uppercase().as_ref() {
+ "RTP/AVP" => SdpProtocolValue::RtpAvp,
+ "RTP/AVPF" => SdpProtocolValue::RtpAvpf,
+ "RTP/SAVP" => SdpProtocolValue::RtpSavp,
+ "RTP/SAVPF" => SdpProtocolValue::RtpSavpf,
+ "TCP/DTLS/RTP/SAVP" => SdpProtocolValue::TcpDtlsRtpSavp,
+ "TCP/DTLS/RTP/SAVPF" => SdpProtocolValue::TcpDtlsRtpSavpf,
+ "UDP/TLS/RTP/SAVP" => SdpProtocolValue::UdpTlsRtpSavp,
+ "UDP/TLS/RTP/SAVPF" => SdpProtocolValue::UdpTlsRtpSavpf,
+ "DTLS/SCTP" => SdpProtocolValue::DtlsSctp,
+ "UDP/DTLS/SCTP" => SdpProtocolValue::UdpDtlsSctp,
+ "TCP/DTLS/SCTP" => SdpProtocolValue::TcpDtlsSctp,
+ _ => {
+ return Err(SdpParserInternalError::Unsupported(format!(
+ "unsupported protocol value: {value}"
+ )));
+ }
+ })
+}
+
+pub fn parse_media(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ let mv: Vec<&str> = value.split_whitespace().collect();
+ if mv.len() < 4 {
+ return Err(SdpParserInternalError::Generic(
+ "media attribute must have at least four tokens".to_string(),
+ ));
+ }
+ let media = parse_media_token(mv[0])?;
+ let mut ptokens = mv[1].split('/');
+ let port = match ptokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "missing port token".to_string(),
+ ));
+ }
+ Some(p) => p.parse::<u32>()?,
+ };
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic(
+ "media port token is too big".to_string(),
+ ));
+ }
+ let port_count = match ptokens.next() {
+ None => 0,
+ Some(c) => c.parse::<u32>()?,
+ };
+ let proto = parse_protocol_token(mv[2])?;
+ let fmt_slice: &[&str] = &mv[3..];
+ let formats = match media {
+ SdpMediaValue::Audio | SdpMediaValue::Video => {
+ let mut fmt_vec: Vec<u32> = vec![];
+ for num in fmt_slice {
+ let fmt_num = num.parse::<u32>()?;
+ if matches!(fmt_num, 1 | 2 | 19 | 64..=95 | 128 .. ) {
+ return Err(SdpParserInternalError::Generic(
+ "format number in media line is out of range".to_string(),
+ ));
+ }
+ fmt_vec.push(fmt_num);
+ }
+ SdpFormatList::Integers(fmt_vec)
+ }
+ SdpMediaValue::Application => {
+ let mut fmt_vec: Vec<String> = vec![];
+ // TODO enforce length == 1 and content 'webrtc-datachannel' only?
+ for token in fmt_slice {
+ fmt_vec.push(String::from(*token));
+ }
+ SdpFormatList::Strings(fmt_vec)
+ }
+ };
+ let m = SdpMediaLine {
+ media,
+ port,
+ port_count,
+ proto,
+ formats,
+ };
+ trace!("media: {}, {}, {}, {}", m.media, m.port, m.proto, m.formats);
+ Ok(SdpType::Media(m))
+}
+
+pub fn parse_media_vector(lines: &mut Vec<SdpLine>) -> Result<Vec<SdpMedia>, SdpParserError> {
+ let mut media_sections: Vec<SdpMedia> = Vec::new();
+
+ let media_line = lines.remove(0);
+ let mut sdp_media = match media_line.sdp_type {
+ SdpType::Media(v) => SdpMedia::new(v),
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "first line in media section needs to be a media line".to_string(),
+ line_number: media_line.line_number,
+ });
+ }
+ };
+
+ while !lines.is_empty() {
+ let line = lines.remove(0);
+ let _line_number = line.line_number;
+ match line.sdp_type {
+ SdpType::Connection(c) => {
+ if sdp_media.connection.is_some() {
+ return Err(SdpParserError::Sequence {
+ message: "connection type already exists at this media level".to_string(),
+ line_number: _line_number,
+ });
+ }
+
+ sdp_media.set_connection(c);
+ }
+ SdpType::Bandwidth(b) => sdp_media.add_bandwidth(b),
+ SdpType::Attribute(a) => {
+ match a {
+ SdpAttribute::DtlsMessage(_) => {
+ // Ignore this attribute on media level
+ Ok(())
+ }
+ SdpAttribute::Rtpmap(rtpmap) => {
+ sdp_media.add_attribute(SdpAttribute::Rtpmap(SdpAttributeRtpmap {
+ payload_type: rtpmap.payload_type,
+ codec_name: rtpmap.codec_name.clone(),
+ frequency: rtpmap.frequency,
+ channels: rtpmap.channels,
+ }))
+ }
+ _ => sdp_media.add_attribute(a),
+ }
+ .map_err(|e: SdpParserInternalError| SdpParserError::Sequence {
+ message: format!("{e}"),
+ line_number: _line_number,
+ })?
+ }
+ SdpType::Media(v) => {
+ media_sections.push(sdp_media);
+ sdp_media = SdpMedia::new(v);
+ }
+
+ SdpType::Origin(_) | SdpType::Session(_) | SdpType::Timing(_) | SdpType::Version(_) => {
+ return Err(SdpParserError::Sequence {
+ message: "invalid type in media section".to_string(),
+ line_number: line.line_number,
+ });
+ }
+ };
+ }
+
+ media_sections.push(sdp_media);
+
+ Ok(media_sections)
+}
+
+#[cfg(test)]
+#[path = "./media_type_tests.rs"]
+mod media_type_tests;
diff --git a/third_party/rust/webrtc-sdp/src/media_type_tests.rs b/third_party/rust/webrtc-sdp/src/media_type_tests.rs
new file mode 100644
index 0000000000..5e25cddb88
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/media_type_tests.rs
@@ -0,0 +1,411 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::*;
+use address::{AddressType, ExplicitlyTypedAddress};
+use attribute_type::{
+ SdpAttributeFmtp, SdpAttributeFmtpParameters, SdpAttributePayloadType, SdpAttributeRtcpFb,
+ SdpAttributeRtcpFbType,
+};
+use std::convert::TryFrom;
+
+pub fn create_dummy_media_section() -> SdpMedia {
+ let media_line = SdpMediaLine {
+ media: SdpMediaValue::Audio,
+ port: 9,
+ port_count: 0,
+ proto: SdpProtocolValue::RtpSavpf,
+ formats: SdpFormatList::Integers(Vec::new()),
+ };
+ SdpMedia::new(media_line)
+}
+
+// TODO is this useful outside of tests?
+impl SdpFormatList {
+ fn len(&self) -> usize {
+ match *self {
+ SdpFormatList::Integers(ref x) => x.len(),
+ SdpFormatList::Strings(ref x) => x.len(),
+ }
+ }
+}
+
+pub fn add_dummy_attributes(media: &mut SdpMedia) {
+ assert!(media
+ .add_attribute(SdpAttribute::Rtcpfb(SdpAttributeRtcpFb {
+ payload_type: SdpAttributePayloadType::Wildcard,
+ feedback_type: SdpAttributeRtcpFbType::Ack,
+ parameter: "".to_string(),
+ extra: "".to_string(),
+ },))
+ .is_ok());
+ assert!(media
+ .add_attribute(SdpAttribute::Fmtp(SdpAttributeFmtp {
+ payload_type: 1,
+ parameters: SdpAttributeFmtpParameters {
+ packetization_mode: 0,
+ level_asymmetry_allowed: false,
+ profile_level_id: 0x0042_0010,
+ max_fs: 0,
+ max_cpb: 0,
+ max_dpb: 0,
+ max_br: 0,
+ max_mbps: 0,
+ usedtx: false,
+ stereo: false,
+ useinbandfec: false,
+ cbr: false,
+ max_fr: 0,
+ maxplaybackrate: 48000,
+ maxaveragebitrate: 0,
+ ptime: 0,
+ minptime: 0,
+ maxptime: 0,
+ encodings: Vec::new(),
+ dtmf_tones: "".to_string(),
+ rtx: None,
+ unknown_tokens: Vec::new()
+ }
+ },))
+ .is_ok());
+ assert!(media
+ .add_attribute(SdpAttribute::Sctpmap(SdpAttributeSctpmap {
+ port: 5000,
+ channels: 2,
+ }))
+ .is_ok());
+ assert!(media.add_attribute(SdpAttribute::BundleOnly).is_ok());
+ assert!(media.add_attribute(SdpAttribute::SctpPort(5000)).is_ok());
+
+ assert!(media.get_attribute(SdpAttributeType::Rtpmap).is_some());
+ assert!(media.get_attribute(SdpAttributeType::Rtcpfb).is_some());
+ assert!(media.get_attribute(SdpAttributeType::Fmtp).is_some());
+ assert!(media.get_attribute(SdpAttributeType::Sctpmap).is_some());
+ assert!(media.get_attribute(SdpAttributeType::SctpPort).is_some());
+ assert!(media.get_attribute(SdpAttributeType::BundleOnly).is_some());
+}
+
+fn check_parse(media_line_str: &str) -> SdpMediaLine {
+ if let Ok(SdpType::Media(media_line)) = parse_media(media_line_str) {
+ media_line
+ } else {
+ unreachable!()
+ }
+}
+
+fn check_parse_and_serialize(media_line_str: &str) {
+ let parsed = check_parse(media_line_str);
+ assert_eq!(parsed.to_string(), media_line_str.to_string());
+}
+
+#[test]
+fn test_get_set_port() {
+ let mut msection = create_dummy_media_section();
+ assert_eq!(msection.get_port(), 9);
+ msection.set_port(2048);
+ assert_eq!(msection.get_port(), 2048);
+}
+
+#[test]
+fn test_add_codec() -> Result<(), SdpParserInternalError> {
+ let mut msection = create_dummy_media_section();
+ msection.add_codec(SdpAttributeRtpmap::new(96, "foobar".to_string(), 1000))?;
+ assert_eq!(msection.get_formats().len(), 1);
+ assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_some());
+
+ let mut msection = create_dummy_media_section();
+ msection.media.formats = SdpFormatList::Strings(Vec::new());
+ msection.add_codec(SdpAttributeRtpmap::new(97, "boofar".to_string(), 1001))?;
+ assert_eq!(msection.get_formats().len(), 1);
+ assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_some());
+ Ok(())
+}
+
+#[test]
+fn test_remove_codecs() -> Result<(), SdpParserInternalError> {
+ let mut msection = create_dummy_media_section();
+ msection.add_codec(SdpAttributeRtpmap::new(96, "foobar".to_string(), 1000))?;
+ assert_eq!(msection.get_formats().len(), 1);
+ assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_some());
+ msection.remove_codecs();
+ assert_eq!(msection.get_formats().len(), 0);
+ assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_none());
+
+ let mut msection = create_dummy_media_section();
+ msection.media.formats = SdpFormatList::Strings(Vec::new());
+ msection.add_codec(SdpAttributeRtpmap::new(97, "boofar".to_string(), 1001))?;
+ assert_eq!(msection.get_formats().len(), 1);
+
+ add_dummy_attributes(&mut msection);
+
+ msection.remove_codecs();
+ assert_eq!(msection.get_formats().len(), 0);
+ assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_none());
+ assert!(msection.get_attribute(SdpAttributeType::Rtcpfb).is_none());
+ assert!(msection.get_attribute(SdpAttributeType::Fmtp).is_none());
+ assert!(msection.get_attribute(SdpAttributeType::Sctpmap).is_none());
+ assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_none());
+ Ok(())
+}
+
+#[test]
+fn test_add_datachannel() -> Result<(), SdpParserInternalError> {
+ let mut msection = create_dummy_media_section();
+ msection.add_datachannel("foo".to_string(), 5000, 256, 0)?;
+ assert_eq!(*msection.get_type(), SdpMediaValue::Application);
+ assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_none());
+ assert!(msection
+ .get_attribute(SdpAttributeType::MaxMessageSize)
+ .is_none());
+ assert!(msection.get_attribute(SdpAttributeType::Sctpmap).is_some());
+ match *msection.get_attribute(SdpAttributeType::Sctpmap).unwrap() {
+ SdpAttribute::Sctpmap(ref s) => {
+ assert_eq!(s.port, 5000);
+ assert_eq!(s.channels, 256);
+ }
+ _ => unreachable!(),
+ }
+
+ let mut msection = create_dummy_media_section();
+ msection.add_datachannel("foo".to_string(), 5000, 256, 1234)?;
+ assert_eq!(*msection.get_type(), SdpMediaValue::Application);
+ assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_none());
+ assert!(msection
+ .get_attribute(SdpAttributeType::MaxMessageSize)
+ .is_some());
+ match *msection
+ .get_attribute(SdpAttributeType::MaxMessageSize)
+ .unwrap()
+ {
+ SdpAttribute::MaxMessageSize(m) => {
+ assert_eq!(m, 1234);
+ }
+ _ => unreachable!(),
+ }
+
+ let mut msection = create_dummy_media_section();
+ msection.media.proto = SdpProtocolValue::UdpDtlsSctp;
+ msection.add_datachannel("foo".to_string(), 5000, 256, 5678)?;
+ assert_eq!(*msection.get_type(), SdpMediaValue::Application);
+ assert!(msection.get_attribute(SdpAttributeType::Sctpmap).is_none());
+ assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_some());
+ match *msection.get_attribute(SdpAttributeType::SctpPort).unwrap() {
+ SdpAttribute::SctpPort(s) => {
+ assert_eq!(s, 5000);
+ }
+ _ => unreachable!(),
+ }
+ assert!(msection
+ .get_attribute(SdpAttributeType::MaxMessageSize)
+ .is_some());
+ match *msection
+ .get_attribute(SdpAttributeType::MaxMessageSize)
+ .unwrap()
+ {
+ SdpAttribute::MaxMessageSize(m) => {
+ assert_eq!(m, 5678);
+ }
+ _ => unreachable!(),
+ }
+ Ok(())
+}
+
+#[test]
+fn test_parse_media_token() -> Result<(), SdpParserInternalError> {
+ let audio = parse_media_token("audio")?;
+ assert_eq!(audio, SdpMediaValue::Audio);
+ let video = parse_media_token("VIDEO")?;
+ assert_eq!(video, SdpMediaValue::Video);
+ let app = parse_media_token("aPplIcatIOn")?;
+ assert_eq!(app, SdpMediaValue::Application);
+
+ assert!(parse_media_token("").is_err());
+ assert!(parse_media_token("foobar").is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_protocol_rtp_token() -> Result<(), SdpParserInternalError> {
+ fn parse_and_serialize_protocol_token(
+ token: &str,
+ result: SdpProtocolValue,
+ ) -> Result<(), SdpParserInternalError> {
+ let rtps = parse_protocol_token(token)?;
+ assert_eq!(rtps, result);
+ assert_eq!(rtps.to_string(), token.to_uppercase());
+ Ok(())
+ }
+ parse_and_serialize_protocol_token("rtp/avp", SdpProtocolValue::RtpAvp)?;
+ parse_and_serialize_protocol_token("rtp/avpf", SdpProtocolValue::RtpAvpf)?;
+ parse_and_serialize_protocol_token("rtp/savp", SdpProtocolValue::RtpSavp)?;
+ parse_and_serialize_protocol_token("rtp/savpf", SdpProtocolValue::RtpSavpf)?;
+ parse_and_serialize_protocol_token("udp/tls/rtp/savp", SdpProtocolValue::UdpTlsRtpSavp)?;
+ parse_and_serialize_protocol_token("udp/tls/rtp/savpf", SdpProtocolValue::UdpTlsRtpSavpf)?;
+ parse_and_serialize_protocol_token("TCP/dtls/rtp/savp", SdpProtocolValue::TcpDtlsRtpSavp)?;
+ parse_and_serialize_protocol_token("tcp/DTLS/rtp/savpf", SdpProtocolValue::TcpDtlsRtpSavpf)?;
+
+ assert!(parse_protocol_token("").is_err());
+ assert!(parse_protocol_token("foobar").is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_protocol_sctp_token() -> Result<(), SdpParserInternalError> {
+ fn parse_and_serialize_protocol_token(
+ token: &str,
+ result: SdpProtocolValue,
+ ) -> Result<(), SdpParserInternalError> {
+ let rtps = parse_protocol_token(token)?;
+ assert_eq!(rtps, result);
+ assert_eq!(rtps.to_string(), token.to_uppercase());
+ Ok(())
+ }
+ parse_and_serialize_protocol_token("dtLs/ScTP", SdpProtocolValue::DtlsSctp)?;
+ parse_and_serialize_protocol_token("udp/DTLS/sctp", SdpProtocolValue::UdpDtlsSctp)?;
+ parse_and_serialize_protocol_token("tcp/dtls/SCTP", SdpProtocolValue::TcpDtlsSctp)?;
+ Ok(())
+}
+
+#[test]
+fn test_media_works() {
+ check_parse_and_serialize("audio 9 UDP/TLS/RTP/SAVPF 109");
+ check_parse_and_serialize("video 9 UDP/TLS/RTP/SAVPF 126");
+ check_parse_and_serialize("application 9 DTLS/SCTP 5000");
+ check_parse_and_serialize("application 9 UDP/DTLS/SCTP webrtc-datachannel");
+
+ check_parse_and_serialize("audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8");
+ check_parse_and_serialize("audio 0 UDP/TLS/RTP/SAVPF 8");
+ check_parse_and_serialize("audio 9/2 UDP/TLS/RTP/SAVPF 8");
+}
+
+#[test]
+fn test_media_missing_token() {
+ assert!(parse_media("video 9 UDP/TLS/RTP/SAVPF").is_err());
+}
+
+#[test]
+fn test_media_invalid_port_number() {
+ assert!(parse_media("video 75123 UDP/TLS/RTP/SAVPF 8").is_err());
+}
+
+#[test]
+fn test_media_invalid_type() {
+ assert!(parse_media("invalid 9 UDP/TLS/RTP/SAVPF 8").is_err());
+}
+
+#[test]
+fn test_media_invalid_port() {
+ assert!(parse_media("audio / UDP/TLS/RTP/SAVPF 8").is_err());
+}
+
+#[test]
+fn test_media_invalid_transport() {
+ assert!(parse_media("audio 9 invalid/invalid 8").is_err());
+}
+
+#[test]
+fn test_media_invalid_payload() {
+ assert!(parse_media("audio 9 UDP/TLS/RTP/SAVPF 300").is_err());
+}
+
+#[test]
+fn test_media_vector_first_line_failure() {
+ let mut sdp_lines: Vec<SdpLine> = Vec::new();
+ let line = SdpLine {
+ line_number: 0,
+ sdp_type: SdpType::Session("hello".to_string()),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(line);
+ assert!(parse_media_vector(&mut sdp_lines).is_err());
+}
+
+#[test]
+fn test_media_vector_multiple_connections() {
+ let mut sdp_lines: Vec<SdpLine> = Vec::new();
+ let media_line = SdpMediaLine {
+ media: SdpMediaValue::Audio,
+ port: 9,
+ port_count: 0,
+ proto: SdpProtocolValue::RtpSavpf,
+ formats: SdpFormatList::Integers(Vec::new()),
+ };
+ let media = SdpLine {
+ line_number: 0,
+ sdp_type: SdpType::Media(media_line),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(media);
+ let c = SdpConnection {
+ address: ExplicitlyTypedAddress::try_from((AddressType::IpV4, "127.0.0.1")).unwrap(),
+ ttl: None,
+ amount: None,
+ };
+ let c1 = SdpLine {
+ line_number: 1,
+ sdp_type: SdpType::Connection(c.clone()),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(c1);
+ let c2 = SdpLine {
+ line_number: 2,
+ sdp_type: SdpType::Connection(c),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(c2);
+ assert!(parse_media_vector(&mut sdp_lines).is_err());
+}
+
+#[test]
+fn test_media_vector_invalid_types() {
+ let mut sdp_lines: Vec<SdpLine> = Vec::new();
+ let media_line = SdpMediaLine {
+ media: SdpMediaValue::Audio,
+ port: 9,
+ port_count: 0,
+ proto: SdpProtocolValue::RtpSavpf,
+ formats: SdpFormatList::Integers(Vec::new()),
+ };
+ let media = SdpLine {
+ line_number: 0,
+ sdp_type: SdpType::Media(media_line),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(media);
+ use SdpTiming;
+ let t = SdpTiming { start: 0, stop: 0 };
+ let tline = SdpLine {
+ line_number: 1,
+ sdp_type: SdpType::Timing(t),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(tline);
+ assert!(parse_media_vector(&mut sdp_lines).is_err());
+}
+
+#[test]
+fn test_media_vector_invalid_media_level_attribute() {
+ let mut sdp_lines: Vec<SdpLine> = Vec::new();
+ let media_line = SdpMediaLine {
+ media: SdpMediaValue::Audio,
+ port: 9,
+ port_count: 0,
+ proto: SdpProtocolValue::RtpSavpf,
+ formats: SdpFormatList::Integers(Vec::new()),
+ };
+ let media = SdpLine {
+ line_number: 0,
+ sdp_type: SdpType::Media(media_line),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(media);
+ let a = SdpAttribute::IceLite;
+ let aline = SdpLine {
+ line_number: 1,
+ sdp_type: SdpType::Attribute(a),
+ text: "".to_owned(),
+ };
+ sdp_lines.push(aline);
+ assert!(parse_media_vector(&mut sdp_lines).is_err());
+}
diff --git a/third_party/rust/webrtc-sdp/src/network.rs b/third_party/rust/webrtc-sdp/src/network.rs
new file mode 100644
index 0000000000..87e44052fa
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/network.rs
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use address::{Address, AddressType};
+use error::SdpParserInternalError;
+use std::net::IpAddr;
+use std::str::FromStr;
+
+pub fn ip_address_to_string(addr: IpAddr) -> String {
+ match addr {
+ IpAddr::V4(ipv4) => format!("IN IP4 {ipv4}"),
+ IpAddr::V6(ipv6) => format!("IN IP6 {ipv6}"),
+ }
+}
+
+pub fn parse_network_type(value: &str) -> Result<(), SdpParserInternalError> {
+ if value.to_uppercase() != "IN" {
+ return Err(SdpParserInternalError::Generic(
+ "nettype must be IN".to_string(),
+ ));
+ };
+ Ok(())
+}
+
+pub fn parse_address_type(value: &str) -> Result<AddressType, SdpParserInternalError> {
+ AddressType::from_str(value.to_uppercase().as_str())
+ .map_err(|_| SdpParserInternalError::Generic("address type must be IP4 or IP6".to_string()))
+}
+
+pub fn parse_unicast_address(value: &str) -> Result<Address, SdpParserInternalError> {
+ Address::from_str(value)
+}
+
+#[cfg(test)]
+#[path = "./network_tests.rs"]
+mod tests;
diff --git a/third_party/rust/webrtc-sdp/src/network_tests.rs b/third_party/rust/webrtc-sdp/src/network_tests.rs
new file mode 100644
index 0000000000..4380894899
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/src/network_tests.rs
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use super::*;
+
+#[test]
+fn test_parse_network_type() -> Result<(), SdpParserInternalError> {
+ parse_network_type("iN")?;
+
+ assert!(parse_network_type("").is_err());
+ assert!(parse_network_type("FOO").is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_address_type() -> Result<(), SdpParserInternalError> {
+ let ip4 = parse_address_type("iP4")?;
+ assert_eq!(ip4, AddressType::IpV4);
+ let ip6 = parse_address_type("Ip6")?;
+ assert_eq!(ip6, AddressType::IpV6);
+
+ assert!(parse_address_type("").is_err());
+ assert!(parse_address_type("IP5").is_err());
+ Ok(())
+}
+
+#[test]
+fn test_parse_unicast_address() -> Result<(), SdpParserInternalError> {
+ parse_unicast_address("127.0.0.1")?;
+ parse_unicast_address("::1")?;
+ Ok(())
+}
diff --git a/third_party/rust/webrtc-sdp/tests/parse_sdp_tests.rs b/third_party/rust/webrtc-sdp/tests/parse_sdp_tests.rs
new file mode 100644
index 0000000000..1ee33e2beb
--- /dev/null
+++ b/third_party/rust/webrtc-sdp/tests/parse_sdp_tests.rs
@@ -0,0 +1,783 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate webrtc_sdp;
+
+#[cfg(test)]
+fn check_parse_and_serialize(sdp_str: &str) {
+ let parsed_sdp = webrtc_sdp::parse_sdp(sdp_str, true);
+ assert!(parsed_sdp.is_ok());
+ let serialized_sdp = parsed_sdp.unwrap().to_string();
+ assert_eq!(serialized_sdp, sdp_str)
+}
+
+#[test]
+fn parse_minimal_sdp() {
+ let sdp_str = "v=0\r\n\
+ o=- 1 1 IN IP4 0.0.0.0\r\n\
+ s=-\r\n\
+ t=0 0\r\n\
+ c=IN IP4 0.0.0.0\r\n\
+ m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.get_version(), 0);
+ let o = sdp.get_origin();
+ assert_eq!(o.username, "-");
+ assert_eq!(o.session_id, 1);
+ assert_eq!(o.session_version, 1);
+ assert_eq!(sdp.get_session(), &Some("-".to_owned()));
+ assert!(sdp.timing.is_some());
+ assert!(sdp.get_connection().is_some());
+ assert_eq!(sdp.attribute.len(), 0);
+ assert_eq!(sdp.media.len(), 1);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(
+ *msection.get_type(),
+ webrtc_sdp::media_type::SdpMediaValue::Audio
+ );
+ assert_eq!(msection.get_port(), 0);
+ assert_eq!(msection.get_port_count(), 0);
+ assert_eq!(
+ *msection.get_proto(),
+ webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf
+ );
+ assert!(msection.get_attributes().is_empty());
+ assert!(msection.get_bandwidth().is_empty());
+ assert!(msection.get_connection().is_none());
+
+ check_parse_and_serialize(sdp_str);
+}
+
+#[test]
+fn parse_minimal_sdp_with_emtpy_lines() {
+ let sdp = "v=0\r\n
+\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+ \r\n
+s=-\r\n
+c=IN IP4 0.0.0.0\r\n
+t=0 0\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp, false);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.get_version(), 0);
+ assert_eq!(sdp.get_session(), &Some("-".to_owned()));
+}
+
+#[test]
+fn parse_minimal_sdp_with_single_space_session() {
+ let sdp = "v=0\r\n
+\r\n
+o=- 0 0 IN IP4 0.0.0.0\r\n
+ \r\n
+s= \r\n
+c=IN IP4 0.0.0.0\r\n
+t=0 0\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp, false);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.get_version(), 0);
+ assert_eq!(sdp.get_session(), &None);
+}
+
+#[test]
+fn parse_minimal_sdp_with_most_session_types() {
+ let sdp_str = "v=0\r\n\
+ o=- 0 0 IN IP4 0.0.0.0\r\n\
+ s=-\r\n\
+ t=0 0\r\n\
+ b=AS:1\r\n\
+ b=CT:123\r\n\
+ b=TIAS:12345\r\n\
+ b=UNKNOWN:9\r\n\
+ c=IN IP6 ::1/1/1\r\n\
+ a=ice-options:trickle\r\n\
+ m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp_str, false);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.session, Some("-".to_owned()));
+ assert!(sdp.get_connection().is_some());
+
+ check_parse_and_serialize(sdp_str);
+}
+
+#[test]
+fn parse_minimal_sdp_with_most_media_types() {
+ let sdp_str = "v=0\r\n\
+ o=- 0 0 IN IP4 0.0.0.0\r\n\
+ s=-\r\n\
+ t=0 0\r\n\
+ m=video 0 UDP/TLS/RTP/SAVPF 0\r\n\
+ b=AS:1\r\n\
+ b=CT:123\r\n\
+ b=TIAS:12345\r\n\
+ c=IN IP4 0.0.0.0\r\n\
+ a=sendrecv\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp_str, false);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.session, Some("-".to_owned()));
+ assert_eq!(sdp.attribute.len(), 0);
+ assert_eq!(sdp.media.len(), 1);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(
+ *msection.get_type(),
+ webrtc_sdp::media_type::SdpMediaValue::Video
+ );
+ assert_eq!(msection.get_port(), 0);
+ assert_eq!(
+ *msection.get_proto(),
+ webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf
+ );
+ assert!(!msection.get_bandwidth().is_empty());
+ assert!(!msection.get_connection().is_none());
+ assert!(!msection.get_attributes().is_empty());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sendrecv)
+ .is_some());
+
+ check_parse_and_serialize(sdp_str);
+}
+
+#[test]
+fn parse_firefox_audio_offer() {
+ let sdp_str = "v=0\r\n\
+o=mozilla...THIS_IS_SDPARTA-52.0a1 506705521068071134 0 IN IP4 0.0.0.0\r\n\
+s=-\r\n\
+t=0 0\r\n\
+a=fingerprint:sha-256 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:BF:2F:E3:91:CB:57:A9:9D:4A:A2:0B:40\r\n\
+a=group:BUNDLE sdparta_0\r\n\
+a=ice-options:trickle\r\n\
+a=msid-semantic:WMS *\r\n\
+m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8\r\n\
+c=IN IP4 0.0.0.0\r\n\
+a=sendrecv\r\n\
+a=extmap:1/sendonly urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\
+a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n\
+a=ice-pwd:e3baa26dd2fa5030d881d385f1e36cce\r\n\
+a=ice-ufrag:58b99ead\r\n\
+a=mid:sdparta_0\r\n\
+a=msid:{5a990edd-0568-ac40-8d97-310fc33f3411} {218cfa1c-617d-2249-9997-60929ce4c405}\r\n\
+a=rtcp-mux\r\n\
+a=rtpmap:109 opus/48000/2\r\n\
+a=rtpmap:9 G722/8000/1\r\n\
+a=rtpmap:0 PCMU/8000\r\n\
+a=rtpmap:8 PCMA/8000\r\n\
+a=setup:actpass\r\n\
+a=ssrc:2655508255 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.media.len(), 1);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(
+ *msection.get_type(),
+ webrtc_sdp::media_type::SdpMediaValue::Audio
+ );
+ assert_eq!(msection.get_port(), 9);
+ assert_eq!(msection.get_port_count(), 0);
+ assert_eq!(
+ *msection.get_proto(),
+ webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf
+ );
+ assert!(msection.get_connection().is_some());
+ assert!(msection.get_bandwidth().is_empty());
+ assert!(!msection.get_attributes().is_empty());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sendrecv)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Extmap)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Fmtp)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IcePwd)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IceUfrag)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Msid)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::RtcpMux)
+ .is_some());
+ assert_eq!(
+ msection
+ .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Rtpmap)
+ .len(),
+ 4
+ );
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Setup)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Ssrc)
+ .is_some());
+}
+
+#[test]
+fn parse_firefox_video_offer() {
+ let sdp_str = "v=0\r\n\
+o=mozilla...THIS_IS_SDPARTA-52.0a1 506705521068071134 0 IN IP4 0.0.0.0\r\n\
+s=-\r\n\
+t=0 0\r\n\
+a=extmap-allow-mixed\r\n
+a=fingerprint:sha-256 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:BF:2F:E3:91:CB:57:A9:9D:4A:A2:0B:40\r\n\
+a=group:BUNDLE sdparta_2\r\n\
+a=ice-options:trickle\r\n\
+a=msid-semantic:WMS *\r\n\
+m=video 9 UDP/TLS/RTP/SAVPF 126 120 97\r\n\
+c=IN IP4 0.0.0.0\r\n\
+a=recvonly\r\n\
+a=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1\r\n\
+a=fmtp:120 max-fs=12288;max-fr=60\r\n\
+a=fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1\r\n\
+a=ice-pwd:e3baa26dd2fa5030d881d385f1e36cce\r\n\
+a=ice-ufrag:58b99ead\r\n\
+a=mid:sdparta_2\r\n\
+a=rtcp-fb:126 nack\r\n\
+a=rtcp-fb:126 nack pli\r\n\
+a=rtcp-fb:126 ccm fir\r\n\
+a=rtcp-fb:126 goog-remb\r\n\
+a=rtcp-fb:120 nack\r\n\
+a=rtcp-fb:120 nack pli\r\n\
+a=rtcp-fb:120 ccm fir\r\n\
+a=rtcp-fb:120 goog-remb\r\n\
+a=rtcp-fb:97 nack\r\n\
+a=rtcp-fb:97 nack pli\r\n\
+a=rtcp-fb:97 ccm fir\r\n\
+a=rtcp-fb:97 goog-remb\r\n\
+a=rtcp-mux\r\n\
+a=rtpmap:126 H264/90000\r\n\
+a=rtpmap:120 VP8/90000\r\n\
+a=rtpmap:97 H264/90000\r\n\
+a=setup:actpass\r\n\
+a=extmap-allow-mixed\r\n
+a=ssrc:2709871439 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.media.len(), 1);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(
+ *msection.get_type(),
+ webrtc_sdp::media_type::SdpMediaValue::Video
+ );
+ assert_eq!(msection.get_port(), 9);
+ assert_eq!(
+ *msection.get_proto(),
+ webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf
+ );
+ assert!(msection.get_connection().is_some());
+ assert!(msection.get_bandwidth().is_empty());
+ assert!(!msection.get_attributes().is_empty());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Recvonly)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Extmap)
+ .is_none());
+ assert_eq!(
+ msection
+ .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Fmtp)
+ .len(),
+ 3
+ );
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IcePwd)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IceUfrag)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Msid)
+ .is_none());
+ assert_eq!(
+ msection
+ .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Rtcpfb)
+ .len(),
+ 12
+ );
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::RtcpMux)
+ .is_some());
+ assert_eq!(
+ msection
+ .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Rtpmap)
+ .len(),
+ 3
+ );
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Setup)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Ssrc)
+ .is_some());
+}
+#[test]
+fn parse_firefox_datachannel_offer() {
+ let sdp_str = "v=0\r\n\
+ o=mozilla...THIS_IS_SDPARTA-52.0a2 3327975756663609975 0 IN IP4 0.0.0.0\r\n\
+ s=-\r\n\
+ t=0 0\r\n\
+ a=sendrecv\r\n\
+ a=fingerprint:sha-256 AC:72:CB:D6:1E:A3:A3:B0:E7:97:77:25:03:4B:5B:FF:19:6C:02:C6:93:7D:EB:5C:81:6F:36:D9:02:32:F8:23\r\n\
+ a=ice-options:trickle\r\n\
+ a=msid-semantic:WMS *\r\n\
+ m=application 49760 DTLS/SCTP 5000\r\n\
+ c=IN IP4 172.16.156.106\r\n\
+ a=candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ host\r\n\
+ a=sendrecv\r\n\
+ a=end-of-candidates\r\n\
+ a=ice-pwd:24f485c580129b36447b65df77429a82\r\n\
+ a=ice-ufrag:4cba30fe\r\n\
+ a=mid:sdparta_0\r\n\
+ a=sctpmap:5000 webrtc-datachannel 256\r\n\
+ a=setup:active\r\n\
+ a=ssrc:3376683177 cname:{62f78ee0-620f-a043-86ca-b69f189f1aea}\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.media.len(), 1);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(
+ *msection.get_type(),
+ webrtc_sdp::media_type::SdpMediaValue::Application
+ );
+ assert_eq!(msection.get_port(), 49760);
+ assert_eq!(
+ *msection.get_proto(),
+ webrtc_sdp::media_type::SdpProtocolValue::DtlsSctp
+ );
+ assert!(msection.get_connection().is_some());
+ assert!(msection.get_bandwidth().is_empty());
+ assert!(!msection.get_attributes().is_empty());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sendrecv)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Extmap)
+ .is_none());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IcePwd)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IceUfrag)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::EndOfCandidates)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Msid)
+ .is_none());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Rtcpfb)
+ .is_none());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::RtcpMux)
+ .is_none());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Rtpmap)
+ .is_none());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sctpmap)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Setup)
+ .is_some());
+ assert!(msection
+ .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Ssrc)
+ .is_some());
+
+ check_parse_and_serialize(sdp_str);
+}
+
+#[test]
+fn parse_chrome_audio_video_offer() {
+ let sdp = "v=0\r\n
+o=- 3836772544440436510 2 IN IP4 127.0.0.1\r\n
+s=-\r\n
+t=0 0\r\n
+a=group:BUNDLE audio video\r\n
+a=msid-semantic: WMS HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126\r\n
+c=IN IP4 0.0.0.0\r\n
+a=rtcp:9 IN IP4 0.0.0.0\r\n
+a=ice-ufrag:A4by\r\n
+a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n
+a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n
+a=setup:actpass\r\n
+a=mid:audio\r\n
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n
+a=sendrecv\r\n
+a=rtcp-mux\r\n
+a=rtpmap:111 opus/48000/2\r\n
+a=rtcp-fb:111 transport-cc\r\n
+a=fmtp:111 minptime=10;useinbandfec=1\r\n
+a=rtpmap:103 ISAC/16000\r\n
+a=rtpmap:104 ISAC/32000\r\n
+a=rtpmap:9 G722/8000\r\n
+a=rtpmap:0 PCMU/8000\r\n
+a=rtpmap:8 PCMA/8000\r\n
+a=rtpmap:106 CN/32000\r\n
+a=rtpmap:105 CN/16000\r\n
+a=rtpmap:13 CN/8000\r\n
+a=rtpmap:126 telephone-event/8000\r\n
+a=ssrc:162559313 cname:qPTZ+BI+42mgbOi+\r\n
+a=ssrc:162559313 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n
+a=ssrc:162559313 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+a=ssrc:162559313 label:f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n
+m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98\r\n
+c=IN IP4 0.0.0.0\r\n
+a=rtcp:9 IN IP4 0.0.0.0\r\n
+a=ice-ufrag:A4by\r\n
+a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n
+a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n
+a=setup:actpass\r\n
+a=mid:video\r\n
+a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n
+a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n
+a=extmap:4 urn:3gpp:video-orientation\r\n
+a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n
+a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n
+a=sendrecv\r\n
+a=rtcp-mux\r\n
+a=rtcp-rsize\r\n
+a=rtpmap:100 VP8/90000\r\n
+a=rtcp-fb:100 ccm fir\r\n
+a=rtcp-fb:100 nack\r\n
+a=rtcp-fb:100 nack pli\r\n
+a=rtcp-fb:100 goog-remb\r\n
+a=rtcp-fb:100 transport-cc\r\n
+a=rtpmap:101 VP9/90000\r\n
+a=rtcp-fb:101 ccm fir\r\n
+a=rtcp-fb:101 nack\r\n
+a=rtcp-fb:101 nack pli\r\n
+a=rtcp-fb:101 goog-remb\r\n
+a=rtcp-fb:101 transport-cc\r\n
+a=rtpmap:107 H264/90000\r\n
+a=rtcp-fb:107 ccm fir\r\n
+a=rtcp-fb:107 nack\r\n
+a=rtcp-fb:107 nack pli\r\n
+a=rtcp-fb:107 goog-remb\r\n
+a=rtcp-fb:107 transport-cc\r\n
+a=fmtp:107 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n
+a=rtpmap:116 red/90000\r\n
+a=rtpmap:117 ulpfec/90000\r\n
+a=rtpmap:96 rtx/90000\r\n
+a=fmtp:96 apt=100\r\n
+a=rtpmap:97 rtx/90000\r\n
+a=fmtp:97 apt=101\r\n
+a=rtpmap:99 rtx/90000\r\n
+a=fmtp:99 apt=107\r\n
+a=rtpmap:98 rtx/90000\r\n
+a=fmtp:98 apt=116\r\n
+a=ssrc-group:FID 3156517279 2673335628\r\n
+a=ssrc:3156517279 cname:qPTZ+BI+42mgbOi+\r\n
+a=ssrc:3156517279 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n
+a=ssrc:3156517279 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+a=ssrc:3156517279 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n
+a=ssrc:2673335628 cname:qPTZ+BI+42mgbOi+\r\n
+a=ssrc:2673335628 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n
+a=ssrc:2673335628 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+a=ssrc:2673335628 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp, true);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.media.len(), 2);
+
+ let msection1 = &(sdp.media[0]);
+ assert_eq!(
+ *msection1.get_type(),
+ webrtc_sdp::media_type::SdpMediaValue::Audio
+ );
+ assert_eq!(msection1.get_port(), 9);
+ assert_eq!(
+ *msection1.get_proto(),
+ webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf
+ );
+ assert!(!msection1.get_attributes().is_empty());
+ assert!(msection1.get_connection().is_some());
+ assert!(msection1.get_bandwidth().is_empty());
+
+ let msection2 = &(sdp.media[1]);
+ assert_eq!(
+ *msection2.get_type(),
+ webrtc_sdp::media_type::SdpMediaValue::Video
+ );
+ assert_eq!(msection2.get_port(), 9);
+ assert_eq!(
+ *msection2.get_proto(),
+ webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf
+ );
+ assert!(!msection2.get_attributes().is_empty());
+ assert!(msection2.get_connection().is_some());
+ assert!(msection2.get_bandwidth().is_empty());
+}
+
+#[test]
+fn parse_firefox_simulcast_offer() {
+ let sdp = "v=0\r\n
+o=mozilla...THIS_IS_SDPARTA-55.0a1 983028567300715536 0 IN IP4 0.0.0.0\r\n
+s=-\r\n
+t=0 0\r\n
+a=fingerprint:sha-256 68:42:13:88:B6:C1:7D:18:79:07:8A:C6:DC:28:D6:DC:DD:E3:C9:41:E7:80:A7:FE:02:65:FB:76:A0:CD:58:ED\r\n
+a=ice-options:trickle\r\n
+a=msid-semantic:WMS *\r\n
+m=video 9 UDP/TLS/RTP/SAVPF 120 121 126 97\r\n
+c=IN IP4 0.0.0.0\r\n
+a=sendrecv\r\n
+a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n
+a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n
+a=extmap:3/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n
+a=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1\r\n
+a=fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1\r\n
+a=fmtp:120 max-fs=12288;max-fr=60\r\n
+a=fmtp:121 max-fs=12288;max-fr=60\r\n
+a=ice-pwd:4af388405d558b91f5ba6c2c48f161bf\r\n
+a=ice-ufrag:ce1ac488\r\n
+a=mid:sdparta_0\r\n
+a=msid:{fb6d1fa3-d993-f244-a0fe-d9fb99214c23} {8be9a0f7-9272-6c42-90f3-985d55bd8de5}\r\n
+a=rid:foo send\r\n
+a=rid:bar send\r\n
+a=rtcp-fb:120 nack\r\n
+a=rtcp-fb:120 nack pli\r\n
+a=rtcp-fb:120 ccm fir\r\n
+a=rtcp-fb:120 goog-remb\r\n
+a=rtcp-fb:121 nack\r\n
+a=rtcp-fb:121 nack pli\r\n
+a=rtcp-fb:121 ccm fir\r\n
+a=rtcp-fb:121 goog-remb\r\n
+a=rtcp-fb:126 nack\r\n
+a=rtcp-fb:126 nack pli\r\n
+a=rtcp-fb:126 ccm fir\r\n
+a=rtcp-fb:126 goog-remb\r\n
+a=rtcp-fb:97 nack\r\n
+a=rtcp-fb:97 nack pli\r\n
+a=rtcp-fb:97 ccm fir\r\n
+a=rtcp-fb:97 goog-remb\r\n
+a=rtcp-mux\r\n
+a=rtpmap:120 VP8/90000\r\n
+a=rtpmap:121 VP9/90000\r\n
+a=rtpmap:126 H264/90000\r\n
+a=rtpmap:97 H264/90000\r\n
+a=setup:actpass\r\n
+a=simulcast: send rid=foo;bar\r\n
+a=ssrc:2988475468 cname:{77067f00-2e8d-8b4c-8992-cfe338f56851}\r\n
+a=ssrc:1649784806 cname:{77067f00-2e8d-8b4c-8992-cfe338f56851}\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp, true);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.media.len(), 1);
+}
+
+#[test]
+fn parse_firefox_simulcast_answer() {
+ let sdp_str = "v=0\r\n\
+ o=mozilla...THIS_IS_SDPARTA-55.0a1 7548296603161351381 0 IN IP4 0.0.0.0\r\n\
+ s=-\r\n\
+ t=0 0\r\n\
+ a=fingerprint:sha-256 B1:47:49:4F:7D:83:03:BE:E9:FC:73:A3:FB:33:38:40:0B:3B:6A:56:78:EB:EE:D5:6D:2D:D5:3A:B6:13:97:E7\r\n\
+ a=ice-options:trickle\r\n\
+ a=msid-semantic:WMS *\r\n\
+ m=video 9 UDP/TLS/RTP/SAVPF 120\r\n\
+ c=IN IP4 0.0.0.0\r\n
+ a=recvonly\r\n\
+ a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\
+ a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n\
+ a=fmtp:120 max-fs=12288;max-fr=60\r\n\
+ a=ice-pwd:c886e2caf2ae397446312930cd1afe51\r\n\
+ a=ice-ufrag:f57396c0\r\n\
+ a=mid:sdparta_0\r\n\
+ a=rtcp-fb:120 nack\r\n\
+ a=rtcp-fb:120 nack pli\r\n\
+ a=rtcp-fb:120 ccm fir\r\n\
+ a=rtcp-fb:120 goog-remb\r\n\
+ a=rtcp-mux\r\n\
+ a=rtpmap:120 VP8/90000\r\n\
+ a=setup:active\r\n\
+ a=ssrc:2564157021 cname:{cae1cd32-7433-5b48-8dc8-8e3f8b2f96cd}\r\n\
+ a=simulcast: recv rid=foo;bar\r\n\
+ a=rid:foo recv\r\n\
+ a=rid:bar recv\r\n\
+ a=extmap:3/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n";
+ let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true);
+ assert!(sdp_res.is_ok());
+ let sdp_opt = sdp_res.ok();
+ assert!(sdp_opt.is_some());
+ let sdp = sdp_opt.unwrap();
+ assert_eq!(sdp.version, 0);
+ assert_eq!(sdp.media.len(), 1);
+}
+
+#[test]
+fn parse_and_serialize_sdp_with_unusual_attributes() {
+ let sdp_str = "v=0\r\n\
+ o=- 0 0 IN IP6 2001:db8::4444\r\n\
+ s=-\r\n\
+ t=0 0\r\n\
+ a=ice-pacing:500\r\n\
+ m=video 0 UDP/TLS/RTP/SAVPF 0\r\n\
+ b=UNSUPPORTED:12345\r\n\
+ c=IN IP6 ::1\r\n\
+ a=rtcp:9 IN IP6 2001:db8::8888\r\n\
+ a=rtcp-fb:* nack\r\n\
+ a=extmap:1/recvonly urn:ietf:params:rtp-hdrext:toffset\r\n\
+ a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset\r\n\
+ a=extmap:3/sendrecv urn:ietf:params:rtp-hdrext:toffset\r\n\
+ a=imageattr:* send [x=330,y=250,sar=[1.1,1.3,1.9],q=0.1] recv [x=800,y=[50,80,30],sar=1.1]\r\n\
+ a=imageattr:97 send [x=[480:16:800],y=[100,200,300],par=[1.2-1.3],q=0.6] [x=1080,y=[144:176],sar=[0.5-0.7]] recv *\r\n\
+ a=sendrecv\r\n";
+
+ check_parse_and_serialize(sdp_str);
+}
+
+#[test]
+fn serialize_complex_sdp_and_validate_no_empty_lines() {
+ let sdp = "v=0\r\n
+o=- 3836772544440436510 2 IN IP4 127.0.0.1\r\n
+s=-\r\n
+t=0 0\r\n
+a=group:BUNDLE audio video\r\n
+a=msid-semantic: WMS HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126\r\n
+c=IN IP4 0.0.0.0\r\n
+a=rtcp:9 IN IP4 0.0.0.0\r\n
+a=ice-ufrag:A4by\r\n
+a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n
+a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n
+a=setup:actpass\r\n
+a=mid:audio\r\n
+a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n
+a=sendrecv\r\n
+a=rtcp-mux\r\n
+a=rtcp-mux-only\r\n
+a=rtpmap:111 opus/48000/2\r\n
+a=rtcp-fb:111 transport-cc\r\n
+a=fmtp:111 minptime=10;useinbandfec=1\r\n
+a=rtpmap:103 ISAC/16000\r\n
+a=rtpmap:104 ISAC/32000\r\n
+a=rtpmap:9 G722/8000\r\n
+a=rtpmap:0 PCMU/8000\r\n
+a=rtpmap:8 PCMA/8000\r\n
+a=rtpmap:106 CN/32000\r\n
+a=rtpmap:105 CN/16000\r\n
+a=rtpmap:13 CN/8000\r\n
+a=rtpmap:126 telephone-event/8000\r\n
+a=ssrc:162559313 cname:qPTZ+BI+42mgbOi+\r\n
+a=ssrc:162559313 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n
+a=ssrc:162559313 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+a=ssrc:162559313 label:f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n
+m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98\r\n
+c=IN IP4 0.0.0.0\r\n
+a=rtcp:9 IN IP4 0.0.0.0\r\n
+a=ice-ufrag:A4by\r\n
+a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n
+a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n
+a=setup:actpass\r\n
+a=mid:video\r\n
+a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n
+a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n
+a=extmap:4 urn:3gpp:video-orientation\r\n
+a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n
+a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n
+a=sendrecv\r\n
+a=rtcp-mux\r\n
+a=rtcp-rsize\r\n
+a=rtpmap:100 VP8/90000\r\n
+a=rtcp-fb:100 ccm fir\r\n
+a=rtcp-fb:100 nack\r\n
+a=rtcp-fb:100 nack pli\r\n
+a=rtcp-fb:100 goog-remb\r\n
+a=rtcp-fb:100 transport-cc\r\n
+a=rtpmap:101 VP9/90000\r\n
+a=rtcp-fb:101 ccm fir\r\n
+a=rtcp-fb:101 nack\r\n
+a=rtcp-fb:101 nack pli\r\n
+a=rtcp-fb:101 goog-remb\r\n
+a=rtcp-fb:101 transport-cc\r\n
+a=rtpmap:107 H264/90000\r\n
+a=rtcp-fb:107 ccm fir\r\n
+a=rtcp-fb:107 nack\r\n
+a=rtcp-fb:107 nack pli\r\n
+a=rtcp-fb:107 goog-remb\r\n
+a=rtcp-fb:107 transport-cc\r\n
+a=fmtp:107 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n
+a=rtpmap:116 red/90000\r\n
+a=rtpmap:117 ulpfec/90000\r\n
+a=rtpmap:96 rtx/90000\r\n
+a=fmtp:96 apt=100\r\n
+a=rtpmap:97 rtx/90000\r\n
+a=fmtp:97 apt=101\r\n
+a=rtpmap:99 rtx/90000\r\n
+a=fmtp:99 apt=107\r\n
+a=rtpmap:98 rtx/90000\r\n
+a=fmtp:98 apt=116\r\n
+a=ssrc-group:FID 3156517279 2673335628\r\n
+a=ssrc:3156517279 cname:qPTZ+BI+42mgbOi+\r\n
+a=ssrc:3156517279 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n
+a=ssrc:3156517279 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+a=ssrc:3156517279 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n
+a=ssrc:2673335628 cname:qPTZ+BI+42mgbOi+\r\n
+a=ssrc:2673335628 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n
+a=ssrc:2673335628 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n
+a=ssrc:2673335628 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n";
+ let parsed_sdp = webrtc_sdp::parse_sdp(sdp, true).unwrap();
+ assert!(!parsed_sdp.to_string().contains("\r\n\r\n"));
+}