new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/.gitignore
@@ -0,0 +1,2 @@
+target
+Cargo.lock
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/.travis.yml
@@ -0,0 +1,68 @@
+language: rust
+cache: cargo
+sudo: true
+os:
+ - linux
+# Taken out temporarily because it's to slow
+# - osx
+
+rust:
+ - nightly
+ - beta
+ - stable
+ # mimimum stable version because we use init shorthand
+ - 1.17.0
+
+matrix:
+ allow_failures:
+ - rust: nightly
+
+before_install:
+ - sudo apt-get update
+
+addons:
+ apt:
+ packages:
+ - libcurl4-openssl-dev
+ - libelf-dev
+ - libdw-dev
+ - cmake
+ - gcc
+ - binutils-dev
+
+# Add clippy
+before_script:
+ - export PATH=$PATH:~/.cargo/bin
+ - |
+ if [[ "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
+ cargo install --force clippy;
+ fi
+
+script:
+ - cargo build --verbose --all
+ - |
+ if [[ "$TRAVIS_RUST_VERSION" == "nightly" &&
+ -f ~/.cargo/bin/cargo-clippy ]]; then
+ cargo clippy;
+ fi
+ - cargo test --verbose --all
+
+after_success:
+ - |
+ if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
+ wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz &&
+ tar xzf master.tar.gz &&
+ cd kcov-master &&
+ mkdir build &&
+ cd build &&
+ cmake .. &&
+ make &&
+ sudo make install &&
+ cd ../.. &&
+ rm -rf kcov-master &&
+ kcov --version &&
+ for file in target/debug/rsdparsa-*[^\.d]; do echo "$file"; mkdir -p "target/cov/$(basename $file)"; kcov --verify --exclude-pattern=/.cargo,/usr/lib "target/cov/$(basename $file)" "$file"; done &&
+ for file in target/debug/unit_tests-*[^\.d]; do echo "$file"; mkdir -p "target/cov/$(basename $file)"; kcov --verify --exclude-pattern=/.cargo,/usr/lib "target/cov/$(basename $file)" "$file"; done &&
+ bash <(curl -s https://codecov.io/bash) &&
+ echo "Uploaded code coverage"
+ fi
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "rsdparsa"
+version = "0.1.0"
+authors = ["Nils Ohlmeier <github@ohlmeier.org>"]
+
+[dependencies]
+clippy = {version = "*", optional = true}
+
+[features]
+default = []
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/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.
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/README.md
@@ -0,0 +1,13 @@
+# rsdparsa
+
+[![Build Status](https://travis-ci.org/nils-ohlmeier/rsdparsa.svg?branch=master)](https://travis-ci.org/nils-ohlmeier/rsdparsa)
+[![Codecov coverage status](https://codecov.io/gh/nils-ohlmeier/rsdparsa/branch/master/graph/badge.svg)](https://codecov.io/gh/nils-ohlmeier/rsdparsa)
+[![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](#License)
+
+A SDP parser written in Rust specifically aimed for WebRTC
+
+Requires minimum Rust 1.17
+
+## License
+
+Licensed under [MPL](https://www.mozilla.org/MPL/2.0/).
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/attribute_type.rs
@@ -0,0 +1,1465 @@
+use std::net::IpAddr;
+use std::str::FromStr;
+use std::fmt;
+
+use SdpType;
+use error::SdpParserInternalError;
+use network::{parse_nettype, parse_addrtype, parse_unicast_addr};
+
+#[derive(Clone)]
+pub enum SdpAttributeCandidateTransport {
+ Udp,
+ Tcp,
+}
+
+#[derive(Clone)]
+pub enum SdpAttributeCandidateType {
+ Host,
+ Srflx,
+ Prflx,
+ Relay,
+}
+
+#[derive(Clone)]
+pub enum SdpAttributeCandidateTcpType {
+ Active,
+ Passive,
+ Simultaneous,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeCandidate {
+ pub foundation: String,
+ pub component: u32,
+ pub transport: SdpAttributeCandidateTransport,
+ pub priority: u64,
+ pub address: IpAddr,
+ pub port: u32,
+ pub c_type: SdpAttributeCandidateType,
+ pub raddr: Option<IpAddr>,
+ pub rport: Option<u32>,
+ pub tcp_type: Option<SdpAttributeCandidateTcpType>,
+ pub generation: Option<u32>,
+ pub ufrag: Option<String>,
+ pub networkcost: Option<u32>,
+}
+
+impl SdpAttributeCandidate {
+ pub fn new(foundation: String,
+ component: u32,
+ transport: SdpAttributeCandidateTransport,
+ priority: u64,
+ address: IpAddr,
+ 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,
+ }
+ }
+
+ fn set_remote_address(&mut self, ip: IpAddr) {
+ self.raddr = Some(ip)
+ }
+
+ 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)
+ }
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeRemoteCandidate {
+ pub component: u32,
+ pub address: IpAddr,
+ pub port: u32,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeSimulcastId {
+ pub id: String,
+ pub paused: bool,
+}
+
+impl SdpAttributeSimulcastId {
+ pub fn new(idstr: String) -> SdpAttributeSimulcastId {
+ if idstr.starts_with('~') {
+ SdpAttributeSimulcastId {
+ id: idstr[1..].to_string(),
+ paused: true,
+ }
+ } else {
+ SdpAttributeSimulcastId {
+ id: idstr,
+ paused: false,
+ }
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeSimulcastAlternatives {
+ pub ids: Vec<SdpAttributeSimulcastId>,
+}
+
+impl SdpAttributeSimulcastAlternatives {
+ pub fn new(idlist: String) -> SdpAttributeSimulcastAlternatives {
+ SdpAttributeSimulcastAlternatives {
+ ids: idlist
+ .split(',')
+ .map(|x| x.to_string())
+ .map(SdpAttributeSimulcastId::new)
+ .collect(),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeSimulcast {
+ pub send: Vec<SdpAttributeSimulcastAlternatives>,
+ pub receive: Vec<SdpAttributeSimulcastAlternatives>,
+}
+
+impl SdpAttributeSimulcast {
+ fn parse_ids(&mut self, direction: SdpAttributeDirection, idlist: String) {
+ let list = idlist
+ .split(';')
+ .map(|x| x.to_string())
+ .map(SdpAttributeSimulcastAlternatives::new)
+ .collect();
+ // TODO prevent over-writing existing values
+ match direction {
+ SdpAttributeDirection::Recvonly => self.receive = list,
+ SdpAttributeDirection::Sendonly => self.send = list,
+ _ => (),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeRtcp {
+ pub port: u32,
+ pub unicast_addr: Option<IpAddr>,
+}
+
+impl SdpAttributeRtcp {
+ pub fn new(port: u32) -> SdpAttributeRtcp {
+ SdpAttributeRtcp {
+ port,
+ unicast_addr: None,
+ }
+ }
+
+ fn set_addr(&mut self, addr: IpAddr) {
+ self.unicast_addr = Some(addr)
+ }
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeRtcpFb {
+ pub payload_type: u32,
+ // TODO parse this and use an enum instead?
+ pub feedback_type: String,
+}
+
+#[derive(Clone)]
+pub enum SdpAttributeDirection {
+ Recvonly,
+ Sendonly,
+ Sendrecv,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeExtmap {
+ pub id: u32,
+ pub direction: Option<SdpAttributeDirection>,
+ pub url: String,
+ pub extension_attributes: Option<String>,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeFmtp {
+ pub payload_type: u32,
+ pub tokens: Vec<String>,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeFingerprint {
+ // TODO turn the supported hash algorithms into an enum?
+ pub hash_algorithm: String,
+ pub fingerprint: String,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeSctpmap {
+ pub port: u32,
+ pub channels: u32,
+}
+
+#[derive(Clone)]
+pub enum SdpAttributeGroupSemantic {
+ LipSynchronization,
+ FlowIdentification,
+ SingleReservationFlow,
+ AlternateNetworkAddressType,
+ ForwardErrorCorrection,
+ DecodingDependency,
+ Bundle,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeGroup {
+ pub semantics: SdpAttributeGroupSemantic,
+ pub tags: Vec<String>,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeMsid {
+ pub id: String,
+ pub appdata: Option<String>,
+}
+
+#[derive(Clone, Debug)]
+pub struct SdpAttributeMsidSemantic {
+ pub semantic: String,
+ pub msids: Vec<String>,
+}
+
+#[derive(Clone)]
+pub struct SdpAttributeRtpmap {
+ pub payload_type: u32,
+ pub codec_name: String,
+ pub frequency: u32,
+ pub channels: Option<u32>,
+}
+
+impl SdpAttributeRtpmap {
+ pub fn new(payload_type: u32, 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)
+ }
+}
+
+#[derive(Clone)]
+pub enum SdpAttributeSetup {
+ Active,
+ Actpass,
+ Holdconn,
+ Passive,
+}
+
+#[derive(Clone)]
+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(':') == 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());
+ }
+ }
+}
+
+#[derive(Clone)]
+pub enum SdpAttribute {
+ BundleOnly,
+ Candidate(SdpAttributeCandidate),
+ EndOfCandidates,
+ Extmap(SdpAttributeExtmap),
+ Fingerprint(SdpAttributeFingerprint),
+ Fmtp(SdpAttributeFmtp),
+ Group(SdpAttributeGroup),
+ IceLite,
+ IceMismatch,
+ IceOptions(Vec<String>),
+ IcePwd(String),
+ IceUfrag(String),
+ Identity(String),
+ ImageAttr(String),
+ Inactive,
+ Label(String),
+ MaxMessageSize(u64),
+ MaxPtime(u64),
+ Mid(String),
+ Msid(SdpAttributeMsid),
+ MsidSemantic(SdpAttributeMsidSemantic),
+ Ptime(u64),
+ Rid(String),
+ Recvonly,
+ RemoteCandidate(SdpAttributeRemoteCandidate),
+ Rtpmap(SdpAttributeRtpmap),
+ Rtcp(SdpAttributeRtcp),
+ Rtcpfb(SdpAttributeRtcpFb),
+ RtcpMux,
+ RtcpRsize,
+ Sctpmap(SdpAttributeSctpmap),
+ SctpPort(u64),
+ Sendonly,
+ Sendrecv,
+ Setup(SdpAttributeSetup),
+ Simulcast(SdpAttributeSimulcast),
+ Ssrc(SdpAttributeSsrc),
+ SsrcGroup(String),
+}
+
+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::RtcpRsize |
+ SdpAttribute::Sctpmap(..) |
+ SdpAttribute::SctpPort(..) |
+ SdpAttribute::Simulcast(..) |
+ SdpAttribute::Ssrc(..) |
+ SdpAttribute::SsrcGroup(..) => false,
+
+ SdpAttribute::EndOfCandidates |
+ SdpAttribute::Extmap(..) |
+ SdpAttribute::Fingerprint(..) |
+ SdpAttribute::Group(..) |
+ SdpAttribute::IceLite |
+ SdpAttribute::IceOptions(..) |
+ 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::Group(..) |
+ SdpAttribute::IceLite |
+ SdpAttribute::Identity(..) |
+ SdpAttribute::MsidSemantic(..) => false,
+
+ SdpAttribute::BundleOnly |
+ SdpAttribute::Candidate(..) |
+ SdpAttribute::EndOfCandidates |
+ SdpAttribute::Extmap(..) |
+ 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::RtcpRsize |
+ SdpAttribute::Sctpmap(..) |
+ SdpAttribute::SctpPort(..) |
+ SdpAttribute::Sendonly |
+ SdpAttribute::Sendrecv |
+ SdpAttribute::Setup(..) |
+ SdpAttribute::Simulcast(..) |
+ SdpAttribute::Ssrc(..) |
+ SdpAttribute::SsrcGroup(..) => true,
+ }
+ }
+}
+
+impl fmt::Display for SdpAttribute {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let printable = match *self {
+ SdpAttribute::BundleOnly => "bundle-only",
+ SdpAttribute::Candidate(..) => "candidate",
+ SdpAttribute::EndOfCandidates => "end-of-candidates",
+ SdpAttribute::Extmap(..) => "extmap",
+ SdpAttribute::Fingerprint(..) => "fingerprint",
+ SdpAttribute::Fmtp(..) => "fmtp",
+ SdpAttribute::Group(..) => "group",
+ SdpAttribute::IceLite => "ice-lite",
+ SdpAttribute::IceMismatch => "ice-mismatch",
+ SdpAttribute::IceOptions(..) => "ice-options",
+ SdpAttribute::IcePwd(..) => "ice-pwd",
+ SdpAttribute::IceUfrag(..) => "ice-ufrag",
+ SdpAttribute::Identity(..) => "identity",
+ SdpAttribute::ImageAttr(..) => "imageattr",
+ SdpAttribute::Inactive => "inactive",
+ SdpAttribute::Label(..) => "label",
+ SdpAttribute::MaxMessageSize(..) => "max-message-size",
+ SdpAttribute::MaxPtime(..) => "max-ptime",
+ SdpAttribute::Mid(..) => "mid",
+ SdpAttribute::Msid(..) => "msid",
+ SdpAttribute::MsidSemantic(..) => "msid-semantic",
+ SdpAttribute::Ptime(..) => "ptime",
+ SdpAttribute::Rid(..) => "rid",
+ SdpAttribute::Recvonly => "recvonly",
+ SdpAttribute::RemoteCandidate(..) => "remote-candidate",
+ SdpAttribute::Rtpmap(..) => "rtpmap",
+ SdpAttribute::Rtcp(..) => "rtcp",
+ SdpAttribute::Rtcpfb(..) => "rtcp-fb",
+ SdpAttribute::RtcpMux => "rtcp-mux",
+ SdpAttribute::RtcpRsize => "rtcp-rsize",
+ SdpAttribute::Sctpmap(..) => "sctpmap",
+ SdpAttribute::SctpPort(..) => "sctp-port",
+ SdpAttribute::Sendonly => "sendonly",
+ SdpAttribute::Sendrecv => "sendrecv",
+ SdpAttribute::Setup(..) => "setup",
+ SdpAttribute::Simulcast(..) => "simulcast",
+ SdpAttribute::Ssrc(..) => "ssrc",
+ SdpAttribute::SsrcGroup(..) => "ssrc-group",
+ };
+ write!(f, "attribute: {}", printable)
+ }
+}
+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" |
+ "ice-lite" |
+ "ice-mismatch" |
+ "inactive" |
+ "recvonly" |
+ "rtcp-mux" |
+ "rtcp-rsize" |
+ "sendonly" |
+ "sendrecv" => {
+ return Err(SdpParserInternalError::Generic(format!("{} attribute is not allowed to have a value",
+ name)));
+ }
+ _ => (),
+ }
+ }
+ match name.as_str() {
+ "bundle-only" => Ok(SdpAttribute::BundleOnly),
+ "end-of-candidates" => Ok(SdpAttribute::EndOfCandidates),
+ "ice-lite" => Ok(SdpAttribute::IceLite),
+ "ice-mismatch" => Ok(SdpAttribute::IceMismatch),
+ "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" => Ok(SdpAttribute::ImageAttr(string_or_empty(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()?)),
+ "rid" => Ok(SdpAttribute::Rid(string_or_empty(val)?)),
+ "recvonly" => Ok(SdpAttribute::Recvonly),
+ "rtcp-mux" => Ok(SdpAttribute::RtcpMux),
+ "rtcp-rsize" => Ok(SdpAttribute::RtcpRsize),
+ "sendonly" => Ok(SdpAttribute::Sendonly),
+ "sendrecv" => Ok(SdpAttribute::Sendrecv),
+ "ssrc-group" => Ok(SdpAttribute::SsrcGroup(string_or_empty(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)))
+ }
+ }
+ }
+}
+
+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_sctp_port(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let port = to_parse.parse()?;
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic(format!("Sctpport port {} can only be a bit 16bit number",
+ port)));
+ }
+ Ok(SdpAttribute::SctpPort(port))
+}
+
+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 = parse_unicast_addr(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_addr(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;
+ }
+ _ => {
+ return Err(SdpParserInternalError::Unsupported("Uknown candidate extension name"
+ .to_string()))
+ }
+ };
+ }
+ }
+ Ok(SdpAttribute::Candidate(cand))
+}
+
+// ABNF for extmap is defined in RFC 5285
+// https://tools.ietf.org/html/rfc5285#section-7
+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: u32;
+ let mut direction: Option<SdpAttributeDirection> = None;
+ if tokens[0].find('/') == None {
+ id = tokens[0].parse::<u32>()?;
+ } else {
+ let id_dir: Vec<&str> = tokens[0].splitn(2, '/').collect();
+ id = id_dir[0].parse::<u32>()?;
+ 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: extension_attributes,
+ }))
+}
+
+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()));
+ }
+ Ok(SdpAttribute::Fingerprint(SdpAttributeFingerprint {
+ hash_algorithm: tokens[0].to_string(),
+ fingerprint: tokens[1].to_string(),
+ }))
+}
+
+fn parse_fmtp(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.split_whitespace().collect();
+ if tokens.len() != 2 {
+ return Err(SdpParserInternalError::Generic("Fmtp needs to have two tokens".to_string()));
+ }
+ Ok(SdpAttribute::Fmtp(SdpAttributeFmtp {
+ // TODO check for dynamic PT range
+ payload_type: tokens[0].parse::<u32>()?,
+ // TODO this should probably be slit into known tokens
+ // plus a list of unknown tokens
+ tokens: to_parse.split(';').map(|x| x.to_string()).collect(),
+ }))
+}
+
+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,
+ _ => {
+ return Err(SdpParserInternalError::Generic("Unsupported group semantics"
+ .to_string()))
+ }
+ }
+ }
+ };
+ Ok(SdpAttribute::Group(SdpAttributeGroup {
+ semantics,
+ tags: tokens.map(|x| x.to_string()).collect(),
+ }))
+}
+
+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(|x| x.to_string()).collect()))
+}
+
+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 = match tokens.next() {
+ None => None,
+ Some(x) => Some(x.to_string()),
+ };
+ Ok(SdpAttribute::Msid(SdpAttributeMsid { id, appdata }))
+
+}
+
+fn parse_msid_semantic(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<_> = to_parse.split_whitespace().collect();
+ if tokens.len() < 1 {
+ 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(|x| x.to_string()).collect(),
+ };
+ Ok(SdpAttribute::MsidSemantic(semantic))
+}
+
+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_addr(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,
+ }))
+}
+
+fn parse_rtpmap(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let payload_type: u32 = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic("Rtpmap missing payload type".to_string()))
+ }
+ Some(x) => {
+ let pt = x.parse::<u32>()?;
+ 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);
+ match parameters.next() {
+ Some(x) => rtpmap.set_channels(x.parse::<u32>()?),
+ None => (),
+ };
+ Ok(SdpAttribute::Rtpmap(rtpmap))
+}
+
+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::<u32>()?,
+ };
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic("Rtcp port can only be a bit 16bit number"
+ .to_string()));
+ };
+ let mut rtcp = SdpAttributeRtcp::new(port);
+ match tokens.next() {
+ None => (),
+ Some(x) => {
+ parse_nettype(x)?;
+ match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtcp attribute is missing address type token"
+ .to_string(),
+ ))
+ }
+ Some(x) => {
+ let addrtype = parse_addrtype(x)?;
+ let addr = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Rtcp attribute is missing ip address token"
+ .to_string(),
+ ))
+ }
+ Some(x) => {
+ let addr = parse_unicast_addr(x)?;
+ if !addrtype.same_protocol(&addr) {
+ return Err(SdpParserInternalError::Generic(
+ "Failed to parse unicast address attribute.\
+ addrtype does not match address."
+ .to_string(),
+ ));
+ }
+ addr
+ }
+ };
+ rtcp.set_addr(addr);
+ }
+ };
+ }
+ };
+ Ok(SdpAttribute::Rtcp(rtcp))
+}
+
+fn parse_rtcp_fb(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let tokens: Vec<&str> = to_parse.splitn(2, ' ').collect();
+ Ok(SdpAttribute::Rtcpfb(SdpAttributeRtcpFb {
+ // TODO limit this to dymaic PTs
+ payload_type: tokens[0].parse::<u32>()?,
+ feedback_type: match tokens.get(1) {
+ Some(x) => x.to_string(),
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Error parsing rtcpfb".to_string(),
+ ))
+ }
+ },
+ }))
+}
+
+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::<u32>()?;
+ if port > 65535 {
+ return Err(SdpParserInternalError::Generic("Sctpmap port can only be a bit 16bit number"
+ .to_string()));
+ }
+ 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>()?,
+ }))
+}
+
+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(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ let mut token = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic(
+ "Simulcast attribute is missing send/recv value".to_string(),
+ ))
+ }
+ Some(x) => x,
+ };
+ let mut sc = SdpAttributeSimulcast {
+ send: Vec::new(),
+ receive: Vec::new(),
+ };
+ loop {
+ let sendrecv = match token.to_lowercase().as_ref() {
+ "send" => SdpAttributeDirection::Sendonly,
+ "recv" => SdpAttributeDirection::Recvonly,
+ _ => {
+ return Err(SdpParserInternalError::Generic(
+ "Unsupported send/recv value in simulcast attribute"
+ .to_string(),
+ ))
+ }
+ };
+ match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic("Simulcast attribute is missing id list"
+ .to_string()))
+ }
+ Some(x) => sc.parse_ids(sendrecv, x.to_string()),
+ };
+ token = match tokens.next() {
+ None => {
+ break;
+ }
+ Some(x) => x,
+ };
+ }
+ Ok(SdpAttribute::Simulcast(sc))
+}
+
+fn parse_ssrc(to_parse: &str) -> Result<SdpAttribute, SdpParserInternalError> {
+ let mut tokens = to_parse.split_whitespace();
+ 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()?))
+}
+
+#[test]
+fn test_parse_attribute_candidate() {
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ host").is_ok());
+ assert!(parse_attribute("candidate:foo 1 UDP 2122252543 172.16.156.106 49760 typ host")
+ .is_ok());
+ assert!(parse_attribute("candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host").is_ok());
+ assert!(parse_attribute("candidate:0 1 TCP 2122252543 ::1 49760 typ host").is_ok());
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ srflx").is_ok());
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ prflx").is_ok());
+ assert!(parse_attribute("candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ relay").is_ok());
+ assert!(
+ parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host tcptype active"
+ ).is_ok()
+ );
+ assert!(
+ parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host tcptype passive"
+ ).is_ok()
+ );
+ assert!(
+ parse_attribute("candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host tcptype so")
+ .is_ok()
+ );
+ assert!(
+ parse_attribute("candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host ufrag foobar")
+ .is_ok()
+ );
+ assert!(
+ parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host network-cost 50"
+ ).is_ok()
+ );
+ assert!(parse_attribute("candidate:1 1 UDP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 generation 0").is_ok());
+ assert!(parse_attribute("candidate:1 1 UDP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665").is_ok());
+ assert!(parse_attribute("candidate:1 1 TCP 1685987071 24.23.204.141 54609 typ srflx raddr 192.168.1.4 rport 61665 tcptype passive").is_ok());
+ assert!(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").is_ok());
+ assert!(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").is_ok());
+ assert!(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 network-cost 1").is_ok());
+
+ 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 172.16.156 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());
+ // FIXME this should fail without the extra 'foobar' at the end
+ assert!(
+ parse_attribute(
+ "candidate:0 1 TCP 2122252543 172.16.156.106 49760 typ host unsupported foobar"
+ ).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 192.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 rport 61665"
+ ).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_attribute_end_of_candidates() {
+ assert!(parse_attribute("end-of-candidates").is_ok());
+ assert!(parse_attribute("end-of-candidates foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_extmap() {
+ assert!(parse_attribute("extmap:1/sendonly urn:ietf:params:rtp-hdrext:ssrc-audio-level")
+ .is_ok());
+ assert!(parse_attribute("extmap:2/sendrecv urn:ietf:params:rtp-hdrext:ssrc-audio-level")
+ .is_ok());
+ assert!(parse_attribute("extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time")
+ .is_ok());
+
+ assert!(parse_attribute("extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time ext_attributes")
+ .is_ok());
+
+ 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() {
+ assert!(parse_attribute("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").is_ok())
+}
+
+#[test]
+fn test_parse_attribute_fmtp() {
+ assert!(parse_attribute("fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1").is_ok())
+}
+
+#[test]
+fn test_parse_attribute_group() {
+ assert!(parse_attribute("group:LS").is_ok());
+ assert!(parse_attribute("group:LS 1 2").is_ok());
+ assert!(parse_attribute("group:BUNDLE sdparta_0 sdparta_1 sdparta_2").is_ok());
+
+ assert!(parse_attribute("group:").is_err());
+ assert!(parse_attribute("group:NEVER_SUPPORTED_SEMANTICS").is_err());
+}
+
+#[test]
+fn test_parse_attribute_bundle_only() {
+ assert!(parse_attribute("bundle-only").is_ok());
+ assert!(parse_attribute("bundle-only foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_lite() {
+ assert!(parse_attribute("ice-lite").is_ok());
+
+ assert!(parse_attribute("ice-lite foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_mismatch() {
+ assert!(parse_attribute("ice-mismatch").is_ok());
+
+ assert!(parse_attribute("ice-mismatch foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_options() {
+ assert!(parse_attribute("ice-options:trickle").is_ok());
+
+ assert!(parse_attribute("ice-options:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_pwd() {
+ assert!(parse_attribute("ice-pwd:e3baa26dd2fa5030d881d385f1e36cce").is_ok());
+
+ assert!(parse_attribute("ice-pwd:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ice_ufrag() {
+ assert!(parse_attribute("ice-ufrag:58b99ead").is_ok());
+
+ assert!(parse_attribute("ice-ufrag:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_identity() {
+ assert!(parse_attribute("identity:eyJpZHAiOnsiZG9tYWluIjoiZXhhbXBsZS5vcmciLCJwcm90b2NvbCI6ImJvZ3VzIn0sImFzc2VydGlvbiI6IntcImlkZW50aXR5XCI6XCJib2JAZXhhbXBsZS5vcmdcIixcImNvbnRlbnRzXCI6XCJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3l6XCIsXCJzaWduYXR1cmVcIjpcIjAxMDIwMzA0MDUwNlwifSJ9").is_ok());
+
+ assert!(parse_attribute("identity:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_imageattr() {
+ assert!(parse_attribute("imageattr:120 send * recv *").is_ok());
+ assert!(
+ parse_attribute(
+ "imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250]"
+ ).is_ok()
+ );
+ assert!(parse_attribute("imageattr:97 recv [x=800,y=640,sar=1.1] send [x=330,y=250]").is_ok());
+ assert!(parse_attribute("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 *").is_ok());
+
+ assert!(parse_attribute("imageattr:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_inactive() {
+ assert!(parse_attribute("inactive").is_ok());
+ assert!(parse_attribute("inactive foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_label() {
+ assert!(parse_attribute("label:1").is_ok());
+ assert!(parse_attribute("label:foobar").is_ok());
+ assert!(parse_attribute("label:foobar barfoo").is_ok());
+
+ assert!(parse_attribute("label:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_maxptime() {
+ assert!(parse_attribute("maxptime:60").is_ok());
+
+ assert!(parse_attribute("maxptime:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_mid() {
+ assert!(parse_attribute("mid:sdparta_0").is_ok());
+ assert!(parse_attribute("mid:sdparta_0 sdparta_1 sdparta_2").is_ok());
+
+ assert!(parse_attribute("mid:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_msid() {
+ assert!(parse_attribute("msid:{5a990edd-0568-ac40-8d97-310fc33f3411}").is_ok());
+ assert!(
+ parse_attribute(
+ "msid:{5a990edd-0568-ac40-8d97-310fc33f3411} {218cfa1c-617d-2249-9997-60929ce4c405}"
+ ).is_ok()
+ );
+
+ assert!(parse_attribute("msid:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_msid_semantics() {
+ assert!(parse_attribute("msid-semantic:WMS *").is_ok())
+}
+
+#[test]
+fn test_parse_attribute_ptime() {
+ assert!(parse_attribute("ptime:30").is_ok());
+
+ assert!(parse_attribute("ptime:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rid() {
+ assert!(parse_attribute("rid:foo send").is_ok());
+ assert!(parse_attribute("rid:foo").is_ok());
+
+ assert!(parse_attribute("rid:").is_err());
+}
+
+#[test]
+fn test_parse_attribute_recvonly() {
+ assert!(parse_attribute("recvonly").is_ok());
+ assert!(parse_attribute("recvonly foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_remote_candidate() {
+ assert!(parse_attribute("remote-candidates:0 10.0.0.1 5555").is_ok());
+ assert!(parse_attribute("remote-candidates:12345 ::1 5555").is_ok());
+
+ assert!(parse_attribute("remote-candidates:abc 10.0.0.1 5555").is_err());
+ assert!(parse_attribute("remote-candidates:0 10.a.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() {
+ assert!(parse_attribute("sendonly").is_ok());
+ assert!(parse_attribute("sendonly foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_sendrecv() {
+ assert!(parse_attribute("sendrecv").is_ok());
+ assert!(parse_attribute("sendrecv foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_setup() {
+ assert!(parse_attribute("setup:active").is_ok());
+ assert!(parse_attribute("setup:passive").is_ok());
+ assert!(parse_attribute("setup:actpass").is_ok());
+ assert!(parse_attribute("setup:holdconn").is_ok());
+
+ assert!(parse_attribute("setup:").is_err());
+ assert!(parse_attribute("setup:foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtcp() {
+ assert!(parse_attribute("rtcp:5000").is_ok());
+ assert!(parse_attribute("rtcp:9 IN IP4 0.0.0.0").is_ok());
+
+ 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() {
+ assert!(parse_attribute("rtcp-fb:101 ccm fir").is_ok())
+}
+
+#[test]
+fn test_parse_attribute_rtcp_mux() {
+ assert!(parse_attribute("rtcp-mux").is_ok());
+ assert!(parse_attribute("rtcp-mux foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtcp_rsize() {
+ assert!(parse_attribute("rtcp-rsize").is_ok());
+ assert!(parse_attribute("rtcp-rsize foobar").is_err());
+}
+
+#[test]
+fn test_parse_attribute_rtpmap() {
+ assert!(parse_attribute("rtpmap:109 opus/48000").is_ok());
+ assert!(parse_attribute("rtpmap:109 opus/48000/2").is_ok());
+
+ 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() {
+ assert!(parse_attribute("sctpmap:5000 webrtc-datachannel 256").is_ok());
+
+ 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() {
+ assert!(parse_attribute("sctp-port:5000").is_ok());
+
+ assert!(parse_attribute("sctp-port:").is_err());
+ assert!(parse_attribute("sctp-port:70000").is_err());
+}
+
+#[test]
+fn test_parse_attribute_max_message_size() {
+ assert!(parse_attribute("max-message-size:1").is_ok());
+ assert!(parse_attribute("max-message-size:100000").is_ok());
+ assert!(parse_attribute("max-message-size:4294967297").is_ok());
+ assert!(parse_attribute("max-message-size:0").is_ok());
+
+ assert!(parse_attribute("max-message-size:").is_err());
+ assert!(parse_attribute("max-message-size:abc").is_err());
+}
+
+#[test]
+fn test_parse_attribute_simulcast() {
+ assert!(parse_attribute("simulcast:send 1").is_ok());
+ assert!(parse_attribute("simulcast:recv test").is_ok());
+ assert!(parse_attribute("simulcast:recv ~test").is_ok());
+ assert!(parse_attribute("simulcast:recv test;foo").is_ok());
+ assert!(parse_attribute("simulcast:recv foo,bar").is_ok());
+ assert!(parse_attribute("simulcast:recv foo,bar;test").is_ok());
+ assert!(parse_attribute("simulcast:recv 1;4,5 send 6;7").is_ok());
+ assert!(parse_attribute("simulcast:send 1,2,3;~4,~5 recv 6;~7,~8").is_ok());
+ // 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());
+}
+
+#[test]
+fn test_parse_attribute_ssrc() {
+ assert!(parse_attribute("ssrc:2655508255").is_ok());
+ assert!(parse_attribute("ssrc:2655508255 foo").is_ok());
+ assert!(parse_attribute("ssrc:2655508255 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}")
+ .is_ok());
+
+ assert!(parse_attribute("ssrc:").is_err());
+ assert!(parse_attribute("ssrc:foo").is_err());
+}
+
+#[test]
+fn test_parse_attribute_ssrc_group() {
+ assert!(parse_attribute("ssrc-group:FID 3156517279 2673335628").is_ok())
+}
+
+#[test]
+fn test_parse_unknown_attribute() {
+ assert!(parse_attribute("unknown").is_err())
+}
+
+// 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))
+}
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/file_parser.rs
@@ -0,0 +1,35 @@
+use std::error::Error;
+use std::io::prelude::*;
+use std::fs::File;
+use std::path::Path;
+use std::env;
+extern crate rsdparsa;
+
+fn main() {
+ let filename = match env::args().nth(1) {
+ None => {
+ println!("Missing file name argument!");
+ return;
+ },
+ 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.description()),
+ Ok(file) => file
+ };
+
+ let mut s = String::new();
+ match file.read_to_string(&mut s) {
+ Err(why) => panic!("couldn't read {}: {}",
+ display,
+ why.description()),
+ Ok(s) => s
+ };
+
+ rsdparsa::parse_sdp(&s, true).is_ok();
+}
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7
+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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/11.sdp
@@ -0,0 +1,63 @@
+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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/2.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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/3.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=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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/4.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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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
+
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/5.sdp
@@ -0,0 +1,5 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+s=SIP Call
+t=0 0
+a=ice-lite
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/6.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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/7.sdp
@@ -0,0 +1,7 @@
+v=0
+o=- 4294967296 2 IN IP4 127.0.0.1
+c=IN IP4 198.51.100.7
+t=0 0
+m=video 56436 RTP/SAVPF 120
+b=CT:1000
+a=rtpmap:120 VP8/90000
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/8.sdp
@@ -0,0 +1,84 @@
+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
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/sdps/9.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
new file mode 100755
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/bin/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" }'
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/error.rs
@@ -0,0 +1,215 @@
+use std::num::ParseIntError;
+use std::net::AddrParseError;
+use std::fmt;
+use std::error;
+use std::error::Error;
+
+#[derive(Debug)]
+pub enum SdpParserInternalError {
+ Generic(String),
+ Unsupported(String),
+ Integer(ParseIntError),
+ Address(AddrParseError),
+}
+
+impl fmt::Display for SdpParserInternalError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpParserInternalError::Generic(ref message) => {
+ write!(f, "Generic parsing error: {}", message)
+ }
+ SdpParserInternalError::Unsupported(ref message) => {
+ write!(f, "Unsupported parsing error: {}", message)
+ }
+ SdpParserInternalError::Integer(ref error) => {
+ write!(f, "Integer parsing error: {}", error.description())
+ }
+ SdpParserInternalError::Address(ref error) => {
+ write!(f, "IP address parsing error: {}", error.description())
+ }
+ }
+ }
+}
+
+impl error::Error for SdpParserInternalError {
+ fn description(&self) -> &str {
+ match *self {
+ SdpParserInternalError::Generic(ref message) |
+ SdpParserInternalError::Unsupported(ref message) => message,
+ SdpParserInternalError::Integer(ref error) => error.description(),
+ SdpParserInternalError::Address(ref error) => error.description(),
+ }
+ }
+
+ fn cause(&self) -> Option<&error::Error> {
+ match *self {
+ SdpParserInternalError::Integer(ref error) => Some(error),
+ SdpParserInternalError::Address(ref error) => Some(error),
+ // Can't tell much more about our internal errors
+ _ => None,
+ }
+ }
+}
+
+#[test]
+fn test_sdp_parser_internal_error_generic() {
+ let generic = SdpParserInternalError::Generic("generic message".to_string());
+ assert_eq!(format!("{}", generic),
+ "Generic parsing error: generic message");
+ assert_eq!(generic.description(), "generic message");
+ assert!(generic.cause().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_eq!(unsupported.description(), "unsupported internal message");
+ assert!(unsupported.cause().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_eq!(int_err.description(), "invalid digit found in string");
+ assert!(!int_err.cause().is_none());
+}
+
+#[test]
+fn test_sdp_parser_internal_error_address() {
+ let v = "127.0.0.a";
+ use std::str::FromStr;
+ use std::net::IpAddr;
+ let addr = IpAddr::from_str(v);
+ assert!(addr.is_err());
+ let addr_err = SdpParserInternalError::Address(addr.err().unwrap());
+ assert_eq!(format!("{}", addr_err),
+ "IP address parsing error: invalid IP address syntax");
+ assert_eq!(addr_err.description(), "invalid IP address syntax");
+ assert!(!addr_err.cause().is_none());
+}
+
+#[derive(Debug)]
+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 },
+}
+
+impl fmt::Display for SdpParserError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ SdpParserError::Line {
+ ref error,
+ ref line,
+ ref line_number,
+ } => {
+ write!(f,
+ "Line error: {} in line({}): {}",
+ error.description(),
+ line_number,
+ line)
+ }
+ SdpParserError::Unsupported {
+ ref error,
+ ref line,
+ ref line_number,
+ } => {
+ write!(f,
+ "Unsupported: {} in line({}): {}",
+ error.description(),
+ line_number,
+ line)
+ }
+ SdpParserError::Sequence {
+ ref message,
+ ref line_number,
+ } => write!(f, "Sequence error in line({}): {}", line_number, message),
+ }
+ }
+}
+
+
+impl error::Error for SdpParserError {
+ fn description(&self) -> &str {
+ match *self {
+ SdpParserError::Line { ref error, .. } |
+ SdpParserError::Unsupported { ref error, .. } => error.description(),
+ SdpParserError::Sequence { ref message, .. } => message,
+ }
+ }
+
+ fn cause(&self) -> Option<&error::Error> {
+ match *self {
+ SdpParserError::Line { ref error, .. } |
+ SdpParserError::Unsupported { ref 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<AddrParseError> for SdpParserInternalError {
+ fn from(err: AddrParseError) -> SdpParserInternalError {
+ SdpParserInternalError::Address(err)
+ }
+}
+
+#[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: test message in line(13): test line");
+ assert_eq!(line1.description(), "test message");
+ assert!(line1.cause().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: unsupported value in line(21): unsupported line");
+ assert_eq!(unsupported1.description(), "unsupported value");
+ assert!(unsupported1.cause().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_eq!(sequence1.description(), "sequence message");
+ assert!(sequence1.cause().is_none());
+}
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/lib.rs
@@ -0,0 +1,974 @@
+#![cfg_attr(feature="clippy", feature(plugin))]
+
+use std::net::IpAddr;
+use std::fmt;
+
+pub mod attribute_type;
+pub mod error;
+pub mod media_type;
+pub mod network;
+pub mod unsupported_types;
+
+use attribute_type::{SdpAttribute, parse_attribute};
+use error::{SdpParserInternalError, SdpParserError};
+use media_type::{SdpMedia, SdpMediaLine, parse_media, parse_media_vector};
+use network::{parse_addrtype, parse_nettype, parse_unicast_addr};
+use unsupported_types::{parse_email, parse_information, parse_key, parse_phone, parse_repeat,
+ parse_uri, parse_zone};
+
+#[derive(Clone)]
+pub enum SdpBandwidth {
+ As(u32),
+ Ct(u32),
+ Tias(u32),
+ Unknown(String, u32),
+}
+
+#[derive(Clone)]
+pub struct SdpConnection {
+ pub addr: IpAddr,
+ pub ttl: Option<u32>,
+ pub amount: Option<u32>,
+}
+
+#[derive(Clone)]
+pub struct SdpOrigin {
+ pub username: String,
+ pub session_id: u64,
+ pub session_version: u64,
+ pub unicast_addr: IpAddr,
+}
+
+impl fmt::Display for SdpOrigin {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f,
+ "origin: {}, {}, {}, {}",
+ self.username,
+ self.session_id,
+ self.session_version,
+ self.unicast_addr)
+ }
+}
+
+#[derive(Clone)]
+pub struct SdpTiming {
+ pub start: u64,
+ pub stop: u64,
+}
+
+pub enum SdpType {
+ Attribute(SdpAttribute),
+ Bandwidth(SdpBandwidth),
+ Connection(SdpConnection),
+ Email(String),
+ Information(String),
+ Key(String),
+ Media(SdpMediaLine),
+ Phone(String),
+ Origin(SdpOrigin),
+ Repeat(String),
+ Session(String),
+ Timing(SdpTiming),
+ Uri(String),
+ Version(u64),
+ Zone(String),
+}
+
+pub struct SdpLine {
+ pub line_number: usize,
+ pub sdp_type: SdpType,
+}
+
+pub struct SdpSession {
+ pub version: u64,
+ pub origin: SdpOrigin,
+ pub session: String,
+ pub connection: Option<SdpConnection>,
+ pub bandwidth: Vec<SdpBandwidth>,
+ pub timing: Option<SdpTiming>,
+ pub attribute: Vec<SdpAttribute>,
+ pub media: Vec<SdpMedia>,
+ // unsupported values:
+ // information: Option<String>,
+ // uri: Option<String>,
+ // email: Option<String>,
+ // phone: Option<String>,
+ // repeat: Option<String>,
+ // zone: Option<String>,
+ // key: Option<String>,
+}
+
+impl SdpSession {
+ pub fn new(version: u64, origin: SdpOrigin, session: String) -> SdpSession {
+ SdpSession {
+ version,
+ origin,
+ session,
+ connection: None,
+ bandwidth: Vec::new(),
+ timing: None,
+ attribute: Vec::new(),
+ media: Vec::new(),
+ }
+ }
+
+ pub fn get_version(&self) -> u64 {
+ self.version
+ }
+
+ pub fn get_origin(&self) -> &SdpOrigin {
+ &self.origin
+ }
+
+ pub fn get_session(&self) -> &String {
+ &self.session
+ }
+
+ pub fn get_connection(&self) -> &Option<SdpConnection> {
+ &self.connection
+ }
+
+ pub fn set_connection(&mut self, c: &SdpConnection) {
+ self.connection = Some(c.clone())
+ }
+
+ pub fn add_bandwidth(&mut self, b: &SdpBandwidth) {
+ self.bandwidth.push(b.clone())
+ }
+
+ pub fn set_timing(&mut self, t: &SdpTiming) {
+ self.timing = Some(t.clone())
+ }
+
+ pub fn add_attribute(&mut self, a: &SdpAttribute) -> Result<(), SdpParserInternalError> {
+ if !a.allowed_at_session_level() {
+ return Err(SdpParserInternalError::Generic(format!("{} not allowed at session level",
+ a)));
+ };
+ Ok(self.attribute.push(a.clone()))
+ }
+
+ pub fn extend_media(&mut self, v: Vec<SdpMedia>) {
+ self.media.extend(v)
+ }
+
+ pub fn has_timing(&self) -> bool {
+ self.timing.is_some()
+ }
+
+ pub fn has_attributes(&self) -> bool {
+ !self.attribute.is_empty()
+ }
+
+ // FIXME this is a temporary hack until we re-oranize the SdpAttribute enum
+ // so that we can build a generic has_attribute(X) function
+ fn has_extmap_attribute(&self) -> bool {
+ for attribute in &self.attribute {
+ if let &SdpAttribute::Extmap(_) = attribute {
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn has_media(&self) -> bool {
+ !self.media.is_empty()
+ }
+}
+
+fn parse_session(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ println!("session: {}", value);
+ Ok(SdpType::Session(String::from(value)))
+}
+
+#[test]
+fn test_session_works() {
+ assert!(parse_session("topic").is_ok());
+}
+
+
+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)));
+ };
+ println!("version: {}", ver);
+ Ok(SdpType::Version(ver))
+}
+
+#[test]
+fn test_version_works() {
+ assert!(parse_version("0").is_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());
+}
+
+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 session version token".to_string(),
+ ))
+ }
+ Some(x) => parse_nettype(x)?,
+ };
+ let addrtype = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic("Origin type is missing address type token"
+ .to_string()))
+ }
+ Some(x) => parse_addrtype(x)?,
+ };
+ let unicast_addr = match tokens.next() {
+ None => {
+ return Err(SdpParserInternalError::Generic("Origin type is missing IP address token"
+ .to_string()))
+ }
+ Some(x) => parse_unicast_addr(x)?,
+ };
+ if !addrtype.same_protocol(&unicast_addr) {
+ 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,
+ };
+ println!("{}", o);
+ Ok(SdpType::Origin(o))
+}
+
+#[test]
+fn test_origin_works() {
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0").is_ok());
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP6 ::1").is_ok());
+}
+
+#[test]
+fn test_origin_wrong_amount_of_tokens() {
+ assert!(parse_origin("a b c d e").is_err());
+ assert!(parse_origin("a b c d e f g").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_unsupported_addrtpe() {
+ assert!(parse_origin("mozilla 506705521068071134 0 IN IP1 0.0.0.0").is_err());
+}
+
+#[test]
+fn test_origin_broken_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());
+}
+
+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_nettype(cv[0])?;
+ let addrtype = parse_addrtype(cv[1])?;
+ let mut ttl = None;
+ let mut amount = None;
+ let mut addr_token = cv[2];
+ if addr_token.find('/') != None {
+ 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::<u32>()?);
+ addr_token = addr_tokens[0];
+ }
+ let addr = parse_unicast_addr(addr_token)?;
+ if !addrtype.same_protocol(&addr) {
+ return Err(SdpParserInternalError::Generic("connection addrtype does not match address."
+ .to_string()));
+ }
+ let c = SdpConnection { addr, ttl, amount };
+ println!("connection: {}", c.addr);
+ Ok(SdpType::Connection(c))
+}
+
+#[test]
+fn connection_works() {
+ assert!(parse_connection("IN IP4 127.0.0.1").is_ok());
+ assert!(parse_connection("IN IP4 127.0.0.1/10/10").is_ok());
+}
+
+#[test]
+fn connection_lots_of_whitespace() {
+ assert!(parse_connection("IN IP4 127.0.0.1").is_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());
+}
+
+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),
+ };
+ println!("bandwidth: {}, {}", bv[0], bandwidth);
+ Ok(SdpType::Bandwidth(bw))
+}
+
+#[test]
+fn bandwidth_works() {
+ assert!(parse_bandwidth("AS:1").is_ok());
+ assert!(parse_bandwidth("CT:123").is_ok());
+ assert!(parse_bandwidth("TIAS:12345").is_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() {
+ assert!(parse_bandwidth("UNSUPPORTED:12345").is_ok());
+}
+
+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 };
+ println!("timing: {}, {}", t.start, t.stop);
+ Ok(SdpType::Timing(t))
+}
+
+#[test]
+fn test_timing_works() {
+ assert!(parse_timing("0 0").is_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());
+}
+
+fn parse_sdp_line(line: &str, line_number: usize) -> Result<SdpLine, SdpParserError> {
+ if line.find('=') == None {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("missing = character in line"
+ .to_string()),
+ line: line.to_string(),
+ line_number: 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: 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: line_number,
+ });
+ }
+ if trimmed.is_empty() {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("type is empty".to_string()),
+ line: line.to_string(),
+ line_number: line_number,
+ });
+ }
+ trimmed
+ }
+ };
+ let line_value = match splitted_line.next() {
+ None => {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("missing value".to_string()),
+ line: line.to_string(),
+ line_number: line_number,
+ })
+ }
+ Some(v) => {
+ let trimmed = v.trim();
+ if trimmed.is_empty() {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("value is empty".to_string()),
+ line: line.to_string(),
+ line_number: line_number,
+ });
+ }
+ trimmed
+ }
+ };
+ match line_type.to_lowercase().as_ref() {
+ "a" => parse_attribute(line_value),
+ "b" => parse_bandwidth(line_value),
+ "c" => parse_connection(line_value),
+ "e" => parse_email(line_value),
+ "i" => parse_information(line_value),
+ "k" => parse_key(line_value),
+ "m" => parse_media(line_value),
+ "o" => parse_origin(line_value),
+ "p" => parse_phone(line_value),
+ "r" => parse_repeat(line_value),
+ "s" => parse_session(line_value),
+ "t" => parse_timing(line_value),
+ "u" => parse_uri(line_value),
+ "v" => parse_version(line_value),
+ "z" => parse_zone(line_value),
+ _ => Err(SdpParserInternalError::Generic("unknown sdp type".to_string())),
+ }
+ .map(|sdp_type| {
+ SdpLine {
+ line_number,
+ sdp_type,
+ }
+ })
+ .map_err(|e| match e {
+ SdpParserInternalError::Generic(..) |
+ SdpParserInternalError::Integer(..) |
+ SdpParserInternalError::Address(..) => {
+ SdpParserError::Line {
+ error: e,
+ line: line.to_string(),
+ line_number: line_number,
+ }
+ }
+ SdpParserInternalError::Unsupported(..) => {
+ SdpParserError::Unsupported {
+ error: e,
+ line: line.to_string(),
+ line_number: line_number,
+ }
+ }
+ })
+}
+
+#[test]
+fn test_parse_sdp_line_works() {
+ assert!(parse_sdp_line("v=0", 0).is_ok());
+ assert!(parse_sdp_line("s=somesession", 0).is_ok());
+}
+
+#[test]
+fn test_parse_sdp_line_empty_line() {
+ assert!(parse_sdp_line("", 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());
+ assert!(parse_sdp_line("s=", 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() {
+ assert!(parse_sdp_line("a=rtpmap:8 PCMA/8000", 0).is_ok());
+}
+
+#[test]
+fn test_parse_sdp_line_invalid_a_line() {
+ assert!(parse_sdp_line("a=rtpmap:200 PCMA/8000", 0).is_err());
+}
+
+fn sanity_check_sdp_session(session: &SdpSession) -> Result<(), SdpParserError> {
+ if !session.has_timing() {
+ return Err(SdpParserError::Sequence {
+ message: "Missing timing type".to_string(),
+ line_number: 0,
+ });
+ }
+
+ if !session.has_media() {
+ return Err(SdpParserError::Sequence {
+ message: "Missing media setion".to_string(),
+ line_number: 0,
+ });
+ }
+
+ // Check that extmaps are not defined on session and media level
+ if session.has_extmap_attribute() {
+ for msection in &session.media {
+ if msection.has_extmap_attribute() {
+ return Err(SdpParserError::Sequence {
+ message: "Extmap can't be define at session and media level"
+ .to_string(),
+ line_number: 0,
+ });
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+fn create_dummy_sdp_session() -> SdpSession {
+ let origin = parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0");
+ assert!(origin.is_ok());
+ let sdp_session;
+ if let SdpType::Origin(o) = origin.unwrap() {
+ sdp_session = SdpSession::new(0, o, "-".to_string());
+ } else {
+ panic!("SdpType is not Origin");
+ }
+ sdp_session
+}
+
+#[cfg(test)]
+use media_type::create_dummy_media_section;
+
+#[test]
+fn test_sanity_check_sdp_session_timing() {
+ 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);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+}
+
+#[test]
+fn test_sanity_check_sdp_session_media() {
+ let mut sdp_session = create_dummy_sdp_session();
+ let t = SdpTiming { start: 0, stop: 0 };
+ sdp_session.set_timing(&t);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_err());
+
+ sdp_session.extend_media(vec![create_dummy_media_section()]);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+}
+
+#[test]
+fn test_sanity_check_sdp_session_extmap() {
+ 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",);
+ assert!(attribute.is_ok());
+ let extmap;
+ if let SdpType::Attribute(a) = attribute.unwrap() {
+ extmap = a;
+ } else {
+ panic!("SdpType is not Attribute");
+ }
+ let ret = sdp_session.add_attribute(&extmap);
+ assert!(ret.is_ok());
+ assert!(sdp_session.has_extmap_attribute());
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+
+ let mattribute = parse_attribute("extmap:1/sendonly urn:ietf:params:rtp-hdrext:ssrc-audio-level",);
+ assert!(mattribute.is_ok());
+ let mextmap;
+ if let SdpType::Attribute(ma) = mattribute.unwrap() {
+ mextmap = ma;
+ } else {
+ panic!("SdpType is not Attribute");
+ }
+ let mut second_media = create_dummy_media_section();
+ assert!(second_media.add_attribute(&mextmap).is_ok());
+ assert!(second_media.has_extmap_attribute());
+
+ 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());
+}
+
+#[test]
+fn test_sanity_check_sdp_session_simulcast() {
+ 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()]);
+
+ assert!(sanity_check_sdp_session(&sdp_session).is_ok());
+}
+
+// TODO add unit tests
+fn parse_sdp_vector(lines: &[SdpLine]) -> Result<SdpSession, SdpParserError> {
+ if lines.len() < 5 {
+ return Err(SdpParserError::Sequence {
+ message: "SDP neeeds at least 5 lines".to_string(),
+ line_number: 0,
+ });
+ }
+
+ // TODO are these mataches really the only way to verify the types?
+ let version: u64 = match lines[0].sdp_type {
+ SdpType::Version(v) => v,
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "first line needs to be version number".to_string(),
+ line_number: lines[0].line_number,
+ })
+ }
+ };
+ let origin: SdpOrigin = match lines[1].sdp_type {
+ SdpType::Origin(ref v) => v.clone(),
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "second line needs to be origin".to_string(),
+ line_number: lines[1].line_number,
+ })
+ }
+ };
+ let session: String = match lines[2].sdp_type {
+ SdpType::Session(ref v) => v.clone(),
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "third line needs to be session".to_string(),
+ line_number: lines[2].line_number,
+ })
+ }
+ };
+ let mut sdp_session = SdpSession::new(version, origin, session);
+ for (index, line) in lines.iter().enumerate().skip(3) {
+ match line.sdp_type {
+ SdpType::Attribute(ref a) => {
+ sdp_session
+ .add_attribute(a)
+ .map_err(|e: SdpParserInternalError| {
+ SdpParserError::Sequence {
+ message: format!("{}", e),
+ line_number: line.line_number,
+ }
+ })?
+ }
+ SdpType::Bandwidth(ref b) => sdp_session.add_bandwidth(b),
+ SdpType::Timing(ref t) => sdp_session.set_timing(t),
+ SdpType::Connection(ref c) => sdp_session.set_connection(c),
+ SdpType::Media(_) => sdp_session.extend_media(parse_media_vector(&lines[index..])?),
+ 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,
+ })
+ }
+ // the line parsers throw unsupported errors for these already
+ SdpType::Email(_) |
+ SdpType::Information(_) |
+ SdpType::Key(_) |
+ SdpType::Phone(_) |
+ SdpType::Repeat(_) |
+ SdpType::Uri(_) |
+ SdpType::Zone(_) => (),
+ };
+ if sdp_session.has_media() {
+ break;
+ };
+ }
+ 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,
+ });
+ }
+ if sdp.len() < 62 {
+ return Err(SdpParserError::Line {
+ error: SdpParserInternalError::Generic("string to 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(stripped_line, line_number) {
+ Ok(n) => {
+ sdp_lines.push(n);
+ }
+ Err(e) => {
+ match e {
+ // FIXME 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,
+ })
+ }
+ }
+ }
+ };
+ }
+ for warning in warnings {
+ if fail_on_warning {
+ return Err(warning);
+ } else {
+ println!("Warning: {}", warning);
+ };
+ }
+ // We just return the last of the errors here
+ if let Some(e) = errors.pop() {
+ return Err(e);
+ };
+ let session = parse_sdp_vector(&sdp_lines)?;
+ Ok(session)
+}
+
+#[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_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() {
+ 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=unsupported\r\n",
+ false)
+ .is_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
+t=0 0\r\n
+s=-\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());
+}
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/media_type.rs
@@ -0,0 +1,414 @@
+use std::fmt;
+use {SdpType, SdpLine, SdpBandwidth, SdpConnection};
+use attribute_type::SdpAttribute;
+use error::{SdpParserError, SdpParserInternalError};
+
+#[derive(Clone)]
+pub struct SdpMediaLine {
+ pub media: SdpMediaValue,
+ pub port: u32,
+ pub port_count: u32,
+ pub proto: SdpProtocolValue,
+ pub formats: SdpFormatList,
+}
+
+#[derive(Clone,Debug,PartialEq)]
+pub enum SdpMediaValue {
+ Audio,
+ Video,
+ Application,
+}
+
+impl fmt::Display for SdpMediaValue {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let printable = match *self {
+ SdpMediaValue::Audio => "Audio",
+ SdpMediaValue::Video => "Video",
+ SdpMediaValue::Application => "Application",
+ };
+ write!(f, "{}", printable)
+ }
+}
+
+#[derive(Clone,Debug,PartialEq)]
+pub enum SdpProtocolValue {
+ RtpSavpf,
+ UdpTlsRtpSavpf,
+ TcpTlsRtpSavpf,
+ DtlsSctp,
+ UdpDtlsSctp,
+ TcpDtlsSctp,
+}
+
+impl fmt::Display for SdpProtocolValue {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let printable = match *self {
+ SdpProtocolValue::RtpSavpf => "Rtp/Savpf",
+ SdpProtocolValue::UdpTlsRtpSavpf => "Udp/Tls/Rtp/Savpf",
+ SdpProtocolValue::TcpTlsRtpSavpf => "Tcp/Tls/Rtp/Savpf",
+ SdpProtocolValue::DtlsSctp => "Dtls/Sctp",
+ SdpProtocolValue::UdpDtlsSctp => "Udp/Dtls/Sctp",
+ SdpProtocolValue::TcpDtlsSctp => "Tcp/Dtls/Sctp",
+ };
+ write!(f, "{}", printable)
+ }
+}
+
+#[derive(Clone)]
+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) => write!(f, "{:?}", x),
+ SdpFormatList::Strings(ref x) => write!(f, "{:?}", x),
+ }
+ }
+}
+
+pub struct SdpMedia {
+ media: SdpMediaLine,
+ connection: Option<SdpConnection>,
+ bandwidth: Vec<SdpBandwidth>,
+ attribute: Vec<SdpAttribute>,
+ // unsupported values:
+ // information: Option<String>,
+ // key: Option<String>,
+}
+
+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 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 has_bandwidth(&self) -> bool {
+ !self.bandwidth.is_empty()
+ }
+
+ pub fn get_bandwidth(&self) -> &Vec<SdpBandwidth> {
+ &self.bandwidth
+ }
+
+ pub fn add_bandwidth(&mut self, bw: &SdpBandwidth) {
+ self.bandwidth.push(bw.clone())
+ }
+
+ pub fn has_attributes(&self) -> bool {
+ !self.attribute.is_empty()
+ }
+
+ 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!("{} not allowed at media level",
+ attr)));
+ }
+ Ok(self.attribute.push(attr.clone()))
+ }
+
+ // FIXME this is a temporary hack until we re-oranize the SdpAttribute enum
+ // so that we can build a generic has_attribute(X) function
+ pub fn has_extmap_attribute(&self) -> bool {
+ for attribute in &self.attribute {
+ if let &SdpAttribute::Extmap(_) = attribute {
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn has_connection(&self) -> bool {
+ self.connection.is_some()
+ }
+
+ pub fn get_connection(&self) -> &Option<SdpConnection> {
+ &self.connection
+ }
+
+ pub fn set_connection(&mut self, c: &SdpConnection) -> Result<(), SdpParserInternalError> {
+ if self.connection.is_some() {
+ return Err(SdpParserInternalError::Generic("connection type already exists at this media level"
+ .to_string(),
+ ));
+ }
+ Ok(self.connection = Some(c.clone()))
+ }
+}
+
+#[cfg(test)]
+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)
+}
+
+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)))
+ }
+ })
+}
+
+#[test]
+fn test_parse_media_token() {
+ let audio = parse_media_token("audio");
+ assert!(audio.is_ok());
+ assert_eq!(audio.unwrap(), SdpMediaValue::Audio);
+ let video = parse_media_token("VIDEO");
+ assert!(video.is_ok());
+ assert_eq!(video.unwrap(), SdpMediaValue::Video);
+ let app = parse_media_token("aPplIcatIOn");
+ assert!(app.is_ok());
+ assert_eq!(app.unwrap(), SdpMediaValue::Application);
+
+ assert!(parse_media_token("").is_err());
+ assert!(parse_media_token("foobar").is_err());
+}
+
+
+fn parse_protocol_token(value: &str) -> Result<SdpProtocolValue, SdpParserInternalError> {
+ Ok(match value.to_uppercase().as_ref() {
+ "RTP/SAVPF" => SdpProtocolValue::RtpSavpf,
+ "UDP/TLS/RTP/SAVPF" => SdpProtocolValue::UdpTlsRtpSavpf,
+ "TCP/TLS/RTP/SAVPF" => SdpProtocolValue::TcpTlsRtpSavpf,
+ "DTLS/SCTP" => SdpProtocolValue::DtlsSctp,
+ "UDP/DTLS/SCTP" => SdpProtocolValue::UdpDtlsSctp,
+ "TCP/DTLS/SCTP" => SdpProtocolValue::TcpDtlsSctp,
+ _ => {
+ return Err(SdpParserInternalError::Unsupported(format!("unsupported protocol value: {}",
+ value)))
+ }
+ })
+}
+
+#[test]
+fn test_parse_protocol_token() {
+ let rtps = parse_protocol_token("rtp/savpf");
+ assert!(rtps.is_ok());
+ assert_eq!(rtps.unwrap(), SdpProtocolValue::RtpSavpf);
+ let udps = parse_protocol_token("udp/tls/rtp/savpf");
+ assert!(udps.is_ok());
+ assert_eq!(udps.unwrap(), SdpProtocolValue::UdpTlsRtpSavpf);
+ let tcps = parse_protocol_token("TCP/tls/rtp/savpf");
+ assert!(tcps.is_ok());
+ assert_eq!(tcps.unwrap(), SdpProtocolValue::TcpTlsRtpSavpf);
+ let dtls = parse_protocol_token("dtLs/ScTP");
+ assert!(dtls.is_ok());
+ assert_eq!(dtls.unwrap(), SdpProtocolValue::DtlsSctp);
+ let usctp = parse_protocol_token("udp/DTLS/sctp");
+ assert!(usctp.is_ok());
+ assert_eq!(usctp.unwrap(), SdpProtocolValue::UdpDtlsSctp);
+ let tsctp = parse_protocol_token("tcp/dtls/SCTP");
+ assert!(tsctp.is_ok());
+ assert_eq!(tsctp.unwrap(), SdpProtocolValue::TcpDtlsSctp);
+
+ assert!(parse_protocol_token("").is_err());
+ assert!(parse_protocol_token("foobar").is_err());
+}
+
+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>()?;
+ match fmt_num {
+ 0 | // PCMU
+ 8 | // PCMA
+ 9 | // G722
+ 13 | // Comfort Noise
+ 96 ... 127 => (), // dynamic range
+ _ => 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,
+ };
+ println!("media: {}, {}, {}, {}", m.media, m.port, m.proto, m.formats);
+ Ok(SdpType::Media(m))
+}
+
+#[test]
+fn test_media_works() {
+ assert!(parse_media("audio 9 UDP/TLS/RTP/SAVPF 109").is_ok());
+ assert!(parse_media("video 9 UDP/TLS/RTP/SAVPF 126").is_ok());
+ assert!(parse_media("application 9 DTLS/SCTP 5000").is_ok());
+ assert!(parse_media("application 9 UDP/DTLS/SCTP webrtc-datachannel").is_ok());
+
+ assert!(parse_media("audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8").is_ok());
+ assert!(parse_media("audio 0 UDP/TLS/RTP/SAVPF 8").is_ok());
+ assert!(parse_media("audio 9/2 UDP/TLS/RTP/SAVPF 8").is_ok());
+}
+
+#[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());
+}
+
+pub fn parse_media_vector(lines: &[SdpLine]) -> Result<Vec<SdpMedia>, SdpParserError> {
+ let mut media_sections: Vec<SdpMedia> = Vec::new();
+ let mut sdp_media = match lines[0].sdp_type {
+ SdpType::Media(ref v) => SdpMedia::new(v.clone()),
+ _ => {
+ return Err(SdpParserError::Sequence {
+ message: "first line in media section needs to be a media line"
+ .to_string(),
+ line_number: lines[0].line_number,
+ })
+ }
+ };
+ for line in lines.iter().skip(1) {
+ match line.sdp_type {
+ SdpType::Connection(ref c) => {
+ sdp_media
+ .set_connection(c)
+ .map_err(|e: SdpParserInternalError| {
+ SdpParserError::Sequence {
+ message: format!("{}", e),
+ line_number: line.line_number,
+ }
+ })?
+ }
+ SdpType::Bandwidth(ref b) => sdp_media.add_bandwidth(b),
+ SdpType::Attribute(ref a) => {
+ sdp_media
+ .add_attribute(a)
+ .map_err(|e: SdpParserInternalError| {
+ SdpParserError::Sequence {
+ message: format!("{}", e),
+ line_number: line.line_number,
+ }
+ })?
+ }
+ SdpType::Media(ref v) => {
+ media_sections.push(sdp_media);
+ sdp_media = SdpMedia::new(v.clone());
+ }
+
+ SdpType::Email(_) |
+ SdpType::Phone(_) |
+ SdpType::Origin(_) |
+ SdpType::Repeat(_) |
+ SdpType::Session(_) |
+ SdpType::Timing(_) |
+ SdpType::Uri(_) |
+ SdpType::Version(_) |
+ SdpType::Zone(_) => {
+ return Err(SdpParserError::Sequence {
+ message: "invalid type in media section".to_string(),
+ line_number: line.line_number,
+ })
+ }
+
+ // the line parsers throw unsupported errors for these already
+ SdpType::Information(_) |
+ SdpType::Key(_) => (),
+ };
+ }
+ media_sections.push(sdp_media);
+ Ok(media_sections)
+}
+// TODO add unit tests for parse_media_vector
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/network.rs
@@ -0,0 +1,80 @@
+use std::str::FromStr;
+use std::fmt;
+use std::net::IpAddr;
+
+use error::SdpParserInternalError;
+
+#[derive(Clone,Copy,Debug,PartialEq)]
+pub enum SdpAddrType {
+ IP4 = 4,
+ IP6 = 6,
+}
+
+impl SdpAddrType {
+ pub fn same_protocol(&self, addr: &IpAddr) -> bool {
+ (addr.is_ipv6() && *self == SdpAddrType::IP6) ||
+ (addr.is_ipv4() && *self == SdpAddrType::IP4)
+ }
+}
+
+impl fmt::Display for SdpAddrType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let printable = match *self {
+ SdpAddrType::IP4 => "Ip4",
+ SdpAddrType::IP6 => "Ip6",
+ };
+ write!(f, "{}", printable)
+ }
+}
+
+pub fn parse_nettype(value: &str) -> Result<(), SdpParserInternalError> {
+ if value.to_uppercase() != "IN" {
+ return Err(SdpParserInternalError::Generic("nettype needs to be IN".to_string()));
+ };
+ Ok(())
+}
+
+#[test]
+fn test_parse_nettype() {
+ let internet = parse_nettype("iN");
+ assert!(internet.is_ok());
+
+ assert!(parse_nettype("").is_err());
+ assert!(parse_nettype("FOO").is_err());
+}
+
+pub fn parse_addrtype(value: &str) -> Result<SdpAddrType, SdpParserInternalError> {
+ Ok(match value.to_uppercase().as_ref() {
+ "IP4" => SdpAddrType::IP4,
+ "IP6" => SdpAddrType::IP6,
+ _ => {
+ return Err(SdpParserInternalError::Generic("address type needs to be IP4 or IP6"
+ .to_string()))
+ }
+ })
+}
+
+#[test]
+fn test_parse_addrtype() {
+ let ip4 = parse_addrtype("iP4");
+ assert!(ip4.is_ok());
+ assert_eq!(ip4.unwrap(), SdpAddrType::IP4);
+ let ip6 = parse_addrtype("Ip6");
+ assert!(ip6.is_ok());
+ assert_eq!(ip6.unwrap(), SdpAddrType::IP6);
+
+ assert!(parse_addrtype("").is_err());
+ assert!(parse_addrtype("IP5").is_err());
+}
+
+pub fn parse_unicast_addr(value: &str) -> Result<IpAddr, SdpParserInternalError> {
+ Ok(IpAddr::from_str(value)?)
+}
+
+#[test]
+fn test_parse_unicast_addr() {
+ let ip4 = parse_unicast_addr("127.0.0.1");
+ assert!(ip4.is_ok());
+ let ip6 = parse_unicast_addr("::1");
+ assert!(ip6.is_ok());
+}
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/src/unsupported_types.rs
@@ -0,0 +1,74 @@
+use error::SdpParserInternalError;
+use SdpType;
+
+pub fn parse_repeat(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ // TODO implement this if it's ever needed
+ Err(SdpParserInternalError::Unsupported(format!("unsupported type repeat: {} ", value)))
+}
+
+#[test]
+fn test_repeat_works() {
+ // FIXME use a proper r value here
+ assert!(parse_repeat("0 0").is_err());
+}
+
+pub fn parse_zone(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ // TODO implement this if it's ever needed
+ Err(SdpParserInternalError::Unsupported(format!("unsupported type zone: {}", value)))
+}
+
+#[test]
+fn test_zone_works() {
+ // FIXME use a proper z value here
+ assert!(parse_zone("0 0").is_err());
+}
+
+pub fn parse_key(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ // TODO implement this if it's ever needed
+ Err(SdpParserInternalError::Unsupported(format!("unsupported type key: {}", value)))
+}
+
+#[test]
+fn test_keys_works() {
+ // FIXME use a proper k value here
+ assert!(parse_key("12345").is_err());
+}
+
+pub fn parse_information(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ Err(SdpParserInternalError::Unsupported(format!("unsupported type information: {}", value)))
+}
+
+#[test]
+fn test_information_works() {
+ assert!(parse_information("foobar").is_err());
+}
+
+pub fn parse_uri(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ // TODO check if this is really a URI
+ Err(SdpParserInternalError::Unsupported(format!("unsupported type uri: {}", value)))
+}
+
+#[test]
+fn test_uri_works() {
+ assert!(parse_uri("http://www.mozilla.org").is_err());
+}
+
+pub fn parse_email(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ // TODO check if this is really an email address
+ Err(SdpParserInternalError::Unsupported(format!("unsupported type email: {}", value)))
+}
+
+#[test]
+fn test_email_works() {
+ assert!(parse_email("nils@mozilla.com").is_err());
+}
+
+pub fn parse_phone(value: &str) -> Result<SdpType, SdpParserInternalError> {
+ // TODO check if this is really a phone number
+ Err(SdpParserInternalError::Unsupported(format!("unsupported type phone: {}", value)))
+}
+
+#[test]
+fn test_phone_works() {
+ assert!(parse_phone("+123456789").is_err());
+}
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/sdp/rsdparsa/tests/unit_tests.rs
@@ -0,0 +1,423 @@
+extern crate rsdparsa;
+
+#[test]
+fn parse_minimal_sdp() {
+ let 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";
+ let sdp_res = rsdparsa::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.session, "-");
+ assert!(sdp.connection.is_none());
+ assert_eq!(sdp.attribute.len(), 0);
+ assert_eq!(sdp.media.len(), 1);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(*msection.get_type(),
+ rsdparsa::media_type::SdpMediaValue::Audio);
+ assert_eq!(msection.get_port(), 0);
+ assert_eq!(*msection.get_proto(),
+ rsdparsa::media_type::SdpProtocolValue::UdpTlsRtpSavpf);
+ assert!(!msection.has_attributes());
+ assert!(!msection.has_bandwidth());
+ assert!(!msection.has_connection());
+ assert!(msection.get_connection().is_none());
+}
+
+#[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
+t=0 0\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n";
+ let sdp_res = rsdparsa::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.version, 0);
+ assert_eq!(sdp.session, "-");
+}
+
+#[test]
+fn parse_minimal_sdp_with_most_session_types() {
+ let sdp = "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
+c=IN IP4 0.0.0.0\r\n
+a=ice-options:trickle\r\n
+m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n";
+ let sdp_res = rsdparsa::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.version, 0);
+ assert_eq!(sdp.session, "-");
+ assert!(sdp.get_connection().is_some());
+}
+
+#[test]
+fn parse_firefox_audio_offer() {
+ let sdp = "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 = rsdparsa::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);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(*msection.get_type(),
+ rsdparsa::media_type::SdpMediaValue::Audio);
+ assert_eq!(msection.get_port(), 9);
+ assert_eq!(*msection.get_proto(),
+ rsdparsa::media_type::SdpProtocolValue::UdpTlsRtpSavpf);
+ assert!(msection.has_attributes());
+ assert!(msection.has_connection());
+ assert!(msection.get_connection().is_some());
+ assert!(!msection.has_bandwidth());
+}
+
+#[test]
+fn parse_firefox_video_offer() {
+ let sdp = "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_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=ssrc:2709871439 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}";
+ let sdp_res = rsdparsa::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);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(*msection.get_type(),
+ rsdparsa::media_type::SdpMediaValue::Video);
+ assert_eq!(msection.get_port(), 9);
+ assert_eq!(*msection.get_proto(),
+ rsdparsa::media_type::SdpProtocolValue::UdpTlsRtpSavpf);
+}
+
+#[test]
+fn parse_firefox_datachannel_offer() {
+ let sdp = "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 = rsdparsa::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);
+
+ let msection = &(sdp.media[0]);
+ assert_eq!(*msection.get_type(),
+ rsdparsa::media_type::SdpMediaValue::Application);
+ assert_eq!(msection.get_port(), 49760);
+ assert_eq!(*msection.get_proto(),
+ rsdparsa::media_type::SdpProtocolValue::DtlsSctp);
+}
+
+#[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 = rsdparsa::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(),
+ rsdparsa::media_type::SdpMediaValue::Audio);
+ assert_eq!(msection1.get_port(), 9);
+ assert_eq!(*msection1.get_proto(),
+ rsdparsa::media_type::SdpProtocolValue::UdpTlsRtpSavpf);
+ assert!(msection1.has_attributes());
+ assert!(msection1.has_connection());
+ assert!(!msection1.has_bandwidth());
+
+ let msection2 = &(sdp.media[1]);
+ assert_eq!(*msection2.get_type(),
+ rsdparsa::media_type::SdpMediaValue::Video);
+ assert_eq!(msection2.get_port(), 9);
+ assert_eq!(*msection2.get_proto(),
+ rsdparsa::media_type::SdpProtocolValue::UdpTlsRtpSavpf);
+ assert!(msection2.has_attributes());
+ assert!(msection2.has_connection());
+ assert!(!msection2.has_bandwidth());
+}
+
+#[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 = rsdparsa::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 = "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 = rsdparsa::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);
+}